[
  {
    "path": ".coderabbit.yaml",
    "content": "# https://docs.coderabbit.ai/guides/configure-coderabbit\nlanguage: \"en-US\"\nearly_access: false\nchat: { auto_reply: true }\n\nreviews:\n  profile: chill\n  high_level_summary: true\n\n  poem: false\n\n  collapse_walkthrough: true\n  sequence_diagrams: true\n\n\n  path_instructions:\n    - path: 'docs/**/*.md'\n      instructions: >-\n        Review the documentation for clarity, grammar, and spelling.\n        Make sure that the documentation is easy to understand and follow.\n        There is currently a migration underway from the Jekyll based documentation in `docs` to the Starlight + Astro based documentation in `docs`. Whenever changes are made to the `docs` directory, ensure that an equivalent change is made in the `docs` directory to keep the `docs` documentation accurate.\n\n    - path: 'docs/**/*.md*'\n      instructions: >-\n        Review the documentation for clarity, grammar, and spelling.\n        Make sure that the documentation is easy to understand and follow.\n        There is currently a migration underway from the Jekyll based documentation in `docs` to the Starlight + Astro based documentation in `docs`. Make sure that the `docs` documentation is accurate and up-to-date with the `docs` documentation, and that any difference between them results in an improvement in the `docs` documentation.\n\n    - path: 'docs/**/*.astro'\n      instructions: >-\n        Review the Astro code in the `docs` directory for quality and correctness.\n        Make sure that the Astro code follows best practices and is easy to understand, maintain, and follows best practices.\n        When possible, suggest improvements to the Astro code to make it better.\n\n    - path: '**/*.go'\n      instructions: >-\n        Review the Go code for quality and correctness.\n        Make sure that the Go code follows best practices, is performant, and is easy to understand and maintain.\n\n  tools:\n    languagetool:\n      enabled: true\n      level: default\n"
  },
  {
    "path": ".codespellrc",
    "content": "[codespell]\nskip = go.mod,go.sum,*.svg,Gemfile.lock,bun.lock,package-lock.json,node_modules,dist\nignore-words-list = dRan,implementors\n"
  },
  {
    "path": ".gitattributes",
    "content": "# Ensure golden test files are not treated as text files to prevent line ending conversions\n*.golden -text\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: gruntwork-io\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/01-bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a bug report to help us improve Terragrunt.\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n## Describe the bug\n\nA clear and concise description of what the bug is.\n\n## Reproducing bugs\n\nIt is vital when reporting bugs that you provide the exact steps that _anyone_ would be able to follow to reproduce the bug you are seeing.\n\nWithout this information, it is much less likely that maintainers will invest time in investigating and fixing the bug, and maintainers may not know if they have actually fixed the bug once a fix is made.\n\nThe exceptions to requiring steps to reproduce are:\n\n1. You are reporting a bug that you don't know how to reproduce, but you are reporting it so that others in the community are aware of it.\n2. You are willing to fix the bug yourself, and you accept the responsibility of ensuring that the bug is valid, and that the fix is well tested.\n\nHow to provide steps for reproduction:\n\nThe most common way to provide steps for reproduction is to create a minimal example that reproduces the bug, and steps to run that example to reproduce the issue. Maintainers will refer to this example as a \"fixture\" when asking questions about reproduction.\n\nYou can either do so with code snippets in this issue, or by creating a public Git repository that contains the minimal example, with instructions for running the example.\n\nYou can delete this section right before submitting the issue, if you like.\n\n## Steps To Reproduce\n\nSteps to reproduce the behavior, code snippets and examples which can be used to reproduce the issue.\n\nBe sure that the maintainers can actually reproduce the issue. Bug reports that are too vague or hard to reproduce are hard to troubleshoot and fix.\n\n```hcl\n// paste code snippets here\n```\n\n## Expected behavior\n\nA clear and concise description of what you expected to happen.\n\n## Must haves\n\n- [ ] Steps for reproduction provided.\n\n## Nice to haves\n\n- [ ] Terminal output\n- [ ] Screenshots\n\n## Versions\n\n- Terragrunt version:\n- OpenTofu/Terraform version:\n- Environment details (Ubuntu 20.04, Windows 10, etc.):\n\n## Additional context\n\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/02-bad_error_message.md",
    "content": "---\nname: Bad Error Message Report\nabout: Report an error message that is unclear, unhelpful, or confusing to users.\ntitle: ''\nlabels:\n  - bug\n  - error-message\nassignees: ''\n\n---\n\n## Error Message Details\n\n### The Error Message\n\nPlease paste the exact error message you received:\n\n```bash\n// Paste the complete error message here\n```\n\n## Why This Error Message is Unhelpful\n\n### Current Problems\n\nPlease explain why this error message is not helpful.\n\n## Recommended Improvement\n\n### What the Error Message Should Say\n\nPlease suggest how the error message could be improved:\n\n```bash\n// Example of what a better error message might look like\n```\n\n## Error Reproduction Steps\n\nProvide the exact steps required for _anyone_ to successfully reproduce the error.\n\nThis includes the Terragrunt command and any relevant configuration files required to reproduce the error. If you need a large amount of Terragrunt configuration to reproduce the error, consider creating a minimal reproduction repository.\n\n### Steps to Reproduce\n\n1.\n2.\n3.\n\n## Environment Information\n\n- **Terragrunt version**:\n- **OpenTofu/Terraform version**:\n- **Operating system**:\n\n## Additional Context\n\nAny other information that might help improve the error message.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/03-rfc.yml",
    "content": "# Inspired by:\n# - The previous RFC template.\n# - the OpenTofu RFC template: https://raw.githubusercontent.com/opentofu/opentofu/main/.github/ISSUE_TEMPLATE/rfc.yml\n\nname: RFC\ndescription: Submit a Request For Comments (RFC) to significantly change Terragrunt.\nlabels: [\"rfc\", \"pending-decision\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        # Request For Comments\n\n        This form will guide you through the process for submitting a Request For Comments (RFC) for a change.\n\n        ## Before you start\n\n        - Make sure you search for an issue that would already cover your RFC in the [issues tab](https://github.com/gruntwork-io/terragrunt/issues?q=is%3Aissue).\n        - Search through [Terragrunt documentation](https://docs.terragrunt.com/) to see if your RFC is already supported.\n\n        ## After you submit\n\n        - Share your RFC with your community for feedback and reactions! The more activity and feedback you get, the more likely it is to be reviewed by the maintainers.\n        - If your RFC has enough traction, it will be reviewed by the maintainers and a decision will be made.\n        - Once a decision is made, one of two things will happen:\n          1. If the RFC is accepted, the `pending-decision` label will be replaced with the `accepted` label. At this stage, pull requests can be submitted by maintainers or the community to implement the RFC, with a description including `Closes #<RFC number>`.\n          2. If the RFC is rejected, the `pending-decision` label will be replaced with the `rejected` label. The maintainers will provide a reason for the rejection. If applicable, it might be made clear that a new RFC with changes is welcome.\n\n  - type: textarea\n    id: summary\n    attributes:\n      label: Summary\n      description: A brief summary of the RFC.\n    validations:\n      required: true\n\n  - type: textarea\n    id: motivation\n    attributes:\n      label: Motivation\n      description: What is the problem you're trying to solve? What are the goals you're trying to achieve?\n    validations:\n      required: true\n\n  - type: textarea\n    id: proposal\n    attributes:\n      label: Proposal\n      description: |\n        In a manner that is as specific as possible, describe the proposal you have in mind. This should include:\n        - As minimally technical a description of the proposal as possible.\n        - An explanation of how the proposal addresses the motivation above.\n        - Examples of how the proposal might present itself to a user if implemented.\n    validations:\n      required: true\n\n  - type: textarea\n    id: technical-details\n    attributes:\n      label: Technical Details\n      description: |\n        Provide technical details for the proposal. This should include:\n        - List of components that will be affected by the proposal.\n        - How those components will be affected.\n        - Any documentation that will help in understanding or developing a solution to the proposal.\n    validations:\n      required: true\n\n  - type: textarea\n    id: press-release\n    attributes:\n      label: Press Release\n      description: If this RFC were implemented, how would you describe it to the community in a mock press release?\n    validations:\n      required: true\n\n  - type: textarea\n    id: drawbacks\n    attributes:\n      label: Drawbacks\n      description: What are the drawbacks of the proposal, if any?\n    validations:\n      required: false\n\n  - type: textarea\n    id: alternatives\n    attributes:\n      label: Alternatives\n      description: What are the alternatives to the proposal, if any?\n    validations:\n      required: false\n\n  - type: textarea\n    id: migration-strategy\n    attributes:\n      label: Migration Strategy\n      description: If this proposal requires a migration strategy for existing code bases, what is it?\n    validations:\n      required: false\n\n  - type: textarea\n    id: unresolved-questions\n    attributes:\n      label: Unresolved Questions\n      description: |\n        What parts of the proposal are still unresolved?\n        - Is there anything that you're unsure about?\n        - Are there any questions that you have that you haven't answered yet?\n        - Would you like to encourage feedback on any particular part of the proposal you haven't fully fleshed out?\n    validations:\n      required: false\n\n  - type: textarea\n    id: references\n    attributes:\n      label: References\n      description: |\n        Are there any references that should be linked here?\n        - Links to other RFCs, issues, or documentation.\n    validations:\n      required: false\n\n  - type: textarea\n    id: poc-pull-request\n    attributes:\n      label: Proof of Concept Pull Request\n      description: If you have a proof of concept or an in-draft pull request that demonstrates the proposal, please link it here.\n    validations:\n      required: false\n\n  - type: checkboxes\n    id: support\n    attributes:\n      label: Support Level\n      description: Please let us know if you are a paying customer. This helps us prioritize RFCs that are important to our customers.\n      options:\n        - label: I have Terragrunt Enterprise Support\n          required: false\n        - label: I am a paying Gruntwork customer\n          required: false\n\n  - type: input\n    id: customer-name\n    attributes:\n      label: Customer Name\n      description: If you are a paying Gruntwork customer, please provide your name.\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/04-feature-request.md",
    "content": "---\nname: Enhancement\nabout: Request a simple feature or enhancement.\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n## Describe the enhancement\n\nA clear and concise description of what the enhancement is.\n\n## Additional context\n\nAdd any other context about the enhancement here.\n\nThings you might want to address include:\n\n- [ ] Changes required.\n- [ ] Implications of the feature.\n- [ ] Alternatives considered.\n- [ ] Level of effort.\n\n## PoC (Proof of Concept)\n\nLink to a Proof of Concept if you have one:\n\n- [ ] [PoC]()\n\nIncluding a PoC can help others understand the feature better and implement it faster.\n\n## RFC Not Needed\n\n- [ ] I have evaluated the complexity of this enhancement, and I believe it does not require an RFC.\n\n"
  },
  {
    "path": ".github/assets/release-assets-config.json",
    "content": "{\n  \"platforms\": [\n    {\n      \"os\": \"darwin\",\n      \"arch\": \"amd64\",\n      \"signed\": true,\n      \"binary\": \"terragrunt_darwin_amd64\"\n    },\n    {\n      \"os\": \"darwin\",\n      \"arch\": \"arm64\",\n      \"signed\": true,\n      \"binary\": \"terragrunt_darwin_arm64\"\n    },\n    {\n      \"os\": \"linux\",\n      \"arch\": \"386\",\n      \"signed\": false,\n      \"binary\": \"terragrunt_linux_386\"\n    },\n    {\n      \"os\": \"linux\",\n      \"arch\": \"amd64\",\n      \"signed\": false,\n      \"binary\": \"terragrunt_linux_amd64\"\n    },\n    {\n      \"os\": \"linux\",\n      \"arch\": \"arm64\",\n      \"signed\": false,\n      \"binary\": \"terragrunt_linux_arm64\"\n    },\n    {\n      \"os\": \"windows\",\n      \"arch\": \"386\",\n      \"signed\": false,\n      \"binary\": \"terragrunt_windows_386.exe\"\n    },\n    {\n      \"os\": \"windows\",\n      \"arch\": \"amd64\",\n      \"signed\": true,\n      \"binary\": \"terragrunt_windows_amd64.exe\"\n    }\n  ],\n  \"archive_formats\": [\n    {\n      \"extension\": \"zip\",\n      \"description\": \"ZIP archive\"\n    },\n    {\n      \"extension\": \"tar.gz\",\n      \"description\": \"TAR.GZ archive\"\n    }\n  ],\n  \"additional_files\": [\n    {\n      \"name\": \"SHA256SUMS\",\n      \"description\": \"Checksums for all files\"\n    },\n    {\n      \"name\": \"SHA256SUMS.gpgsig\",\n      \"description\": \"GPG detached signature\"\n    },\n    {\n      \"name\": \"SHA256SUMS.sigstore.json\",\n      \"description\": \"Cosign sigstore bundle\"\n    },\n    {\n      \"name\": \"SHA256SUMS.sig\",\n      \"description\": \"Cosign signature (legacy)\"\n    },\n    {\n      \"name\": \"SHA256SUMS.pem\",\n      \"description\": \"Cosign certificate (legacy)\"\n    },\n    {\n      \"name\": \"terragrunt-signing-key.asc\",\n      \"description\": \"GPG public key for signature verification\"\n    }\n  ]\n}\n"
  },
  {
    "path": ".github/cloud-nuke/config.yml",
    "content": "s3:\n  timeout: 1h\n  include:\n    names_regex:\n      - \"^terragrunt-test-bucket-[a-zA-Z0-9]{6}.*\"\n\nvpc:\n  include:\n    name_regex:\n      - \"^vpc-.*\"\n      - \"^step-.*\"\n\nec2:\n  include:\n    name_regex:\n      - \"^single-instance$\"\n\ndynamodb:\n  include:\n    table_names_regex:\n      - \"^terragrunt-test.*\"\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n\nversion: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    groups:\n      go-dependencies:\n        patterns:\n          - \"*\"\n    labels:\n      - \"go\"\n      - \"dependencies\"\n    ignore:\n      - dependency-name: \"github.com/charmbracelet/glamour\"\n        versions: [\"<= 0.10.1\"]\n      - dependency-name: \"github.com/charmbracelet/x/ansi\"\n        # https://github.com/charmbracelet/bubbletea/issues/1448#issuecomment-3105363044\n\n  - package-ecosystem: \"bun\"\n    directories:\n      - \"**/*\"\n    schedule:\n      interval: \"weekly\"\n    groups:\n      js-dependencies:\n        patterns:\n          - \"*\"\n    labels:\n      - \"javascript\"\n      - \"dependencies\"\n    ignore:\n      - dependency-name: \"@astrojs/starlight\"\n        # Custom patch applied - manual updates required\n        # When updating, you have to manually update the patch in the docs/package.json\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!-- Prepend '[WIP]' to the title if this PR is still a work-in-progress. Remove it when it is ready for review! -->\n\n## Description\n\nFixes #000.\n\n<!-- Description of the changes introduced by this PR. -->\n\n## TODOs\n\nRead the [Gruntwork contribution guidelines](https://gruntwork.notion.site/Gruntwork-Coding-Methodology-02fdcd6e4b004e818553684760bf691e).\n\n- [ ] I authored this code entirely myself\n- [ ] I am submitting code based on open source software (e.g. MIT, MPL-2.0, Apache)]\n- [ ] I am adding or upgrading a dependency or adapted code and confirm it has a compatible open source license\n- [ ] Update the docs.\n- [ ] Run the relevant tests successfully, including pre-commit checks.\n- [ ] Include release notes. If this PR is backward incompatible, include a migration guide.\n\n## Release Notes (draft)\n\n<!-- One-line description of the PR that can be included in the final release notes. -->\nAdded / Removed / Updated [X].\n\n### Migration Guide\n\n<!-- Important: If you made any backward incompatible changes, then you must write a migration guide! -->\n\n"
  },
  {
    "path": ".github/scripts/announce-release.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\nURL=\"${URL:?Required environment variable URL}\"\nREPO=\"${REPO:?Required environment variable REPO}\"\nTAG_NAME=\"${TAG_NAME:?Required environment variable TAG_NAME}\"\nROLE_ID=\"${ROLE_ID:?Required environment variable ROLE_ID}\"\nUSERNAME=\"${USERNAME:?Required environment variable USERNAME}\"\nAVATAR_URL=\"${AVATAR_URL:?Required environment variable AVATAR_URL}\"\n\nif RELEASE_JSON=$(gh -R \"$REPO\" release view \"$TAG_NAME\" --json body --json url --json name); then\n\tRELEASE_NOTES_LENGTH=$(jq '.body | length' <<<\"$RELEASE_JSON\")\n\n\tRELEASE_NOTES=$(jq '.body' <<<\"$RELEASE_JSON\")\n\n\t# This math is a little weird.\n\t# We have a budget of 200 characters for everything we add around the release notes.\n\t# We also lower the budget by 3 characters for the ellipsis we add at the end when truncating.\n\t# So, it's 2000 characters for the release notes,\n\t# minus 200 characters for everything else,\n\t# minus 3 characters for the ellipsis\n\t# = 1797 characters.\n\tif [[ \"$RELEASE_NOTES_LENGTH\" -gt 1800 ]]; then\n\t\techo \"Release notes are too long ($RELEASE_NOTES_LENGTH characters), truncating to 1797 characters, truncating the last line, then appending '…'\"\n\t\tRELEASE_NOTES=$(jq '.body |= .[:1797]' <<<\"$RELEASE_JSON\" | jq '.body | split(\"\\r\\n\") | del(.[-1]) | join(\"\\r\\n\")' | jq '. + \"\\r\\n…\"')\n\tfi\n\n\tPAYLOAD=$(\n\t\tjq \\\n\t\t\t--argjson release_notes \"$RELEASE_NOTES\" \\\n\t\t\t--arg username \"$USERNAME\" \\\n\t\t\t--arg avatar_url \"$AVATAR_URL\" \\\n\t\t\t-cn '{\"content\": $release_notes, username: $username, avatar_url: $avatar_url, \"flags\": 4}'\n\t)\n\n\ttmpfile=$(mktemp)\n\tjq '.content = \"'\"<@&$ROLE_ID> $(jq -r '.name' <<<\"$RELEASE_JSON\")\\n\"'>>> \" + .content + \"'\"\\n\\n**[View release on GitHub]($(jq -r '.url' <<<\"$RELEASE_JSON\"))**\"'\"' <<<\"$PAYLOAD\" >\"$tmpfile\"\n\n\tjq '.content' <\"$tmpfile\"\n\n\tcurl -X POST \\\n\t\t--data-binary \"@$tmpfile\" \\\n\t\t-H \"Content-Type: application/json\" \\\n\t\t\"$URL\"\nfi\n"
  },
  {
    "path": ".github/scripts/gopls/check-for-changes.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n: \"${HAS_FIXES:?}\"\n\nif [[ \"$HAS_FIXES\" != \"true\" ]]; then\n  echo \"has_changes=false\" >> \"$GITHUB_OUTPUT\"\n  exit 0\nfi\n\nif git diff --staged --quiet; then\n  echo \"has_changes=false\" >> \"$GITHUB_OUTPUT\"\n  exit 0\nfi\n\necho \"has_changes=true\" >> \"$GITHUB_OUTPUT\"\n"
  },
  {
    "path": ".github/scripts/gopls/create-issue.js",
    "content": "const fs = require('fs');\n\n/**\n * Creates a GitHub issue for gopls quickfix problems found\n * @param {Object} params - Parameters object\n * @param {Object} params.github - GitHub API client\n * @param {Object} params.context - GitHub Actions context\n * @param {Object} params.core - GitHub Actions core utilities\n * @param {string} params.fixedFilesPath - Path to the fixed files list\n * @param {string} params.outputFilePath - Path to the gopls output file\n * @returns {Promise<number>} The created issue number\n */\nmodule.exports = async ({ github, context, core, fixedFilesPath, outputFilePath }) => {\n  try {\n    // Read the files that were fixed from provided paths\n    const fixedFiles = fs.readFileSync(fixedFilesPath, 'utf8');\n    const goplsOutput = fs.readFileSync(outputFilePath, 'utf8');\n\n    const issueBody = `## Gopls Quickfix Issues Found\n\nThe monthly gopls quickfix check found issues in the following files:\n\n\\`\\`\\`\n${fixedFiles}\n\\`\\`\\`\n\n### Details\n- **Run Date**: ${new Date().toISOString()}\n- **Workflow**: [${context.workflow}](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})\n- **Commit**: ${context.sha}\n\n### Next Steps\nA pull request will be created to address these issues automatically.\n\n### Full Output\n<details>\n<summary>Click to expand gopls output</summary>\n\n\\`\\`\\`\n${goplsOutput}\n\\`\\`\\`\n\n</details>\n`;\n\n    const issue = await github.rest.issues.create({\n      owner: context.repo.owner,\n      repo: context.repo.repo,\n      title: `🔧 Gopls Quickfix Issues Found - ${new Date().toISOString().split('T')[0]}`,\n      body: issueBody,\n      labels: ['gopls', 'automated', 'maintenance']\n    });\n\n    const issueNumber = issue.data.number;\n    core.setOutput('issue_number', issueNumber);\n    console.log(`Created issue #${issueNumber}`);\n\n    return issueNumber;\n  } catch (error) {\n    core.setFailed(`Failed to create issue: ${error.message}`);\n    throw error;\n  }\n};\n"
  },
  {
    "path": ".github/scripts/gopls/create-pr.js",
    "content": "const fs = require('fs');\n\n/**\n * Creates a GitHub pull request for gopls quickfixes\n * @param {Object} params - Parameters object\n * @param {Object} params.github - GitHub API client\n * @param {Object} params.context - GitHub Actions context\n * @param {Object} params.core - GitHub Actions core utilities\n * @param {Object} params.exec - GitHub Actions exec utilities\n * @param {string} params.issueNumber - The issue number to link to (optional)\n * @param {string} params.fixedFilesPath - Path to the fixed files list\n * @returns {Promise<number>} The created PR number\n */\nmodule.exports = async ({ github, context, core, exec, issueNumber, fixedFilesPath }) => {\n  try {\n    const fixedFiles = fs.readFileSync(fixedFilesPath, 'utf8');\n\n    // Configure git\n    await exec.exec('git', ['config', '--local', 'user.email', 'action@github.com']);\n    await exec.exec('git', ['config', '--local', 'user.name', 'github-actions[bot]']);\n\n    // Create branch name\n    const branchName = `gopls-quickfix-${new Date().toISOString().split('T')[0].replace(/-/g, '')}`;\n\n    // Create and push branch\n    await exec.exec('git', ['checkout', '-b', branchName]);\n    await exec.exec('git', ['add', '.']);\n    await exec.exec('git', ['commit', '-m', '🔧 Apply gopls quickfixes']);\n    await exec.exec('git', ['push', 'origin', branchName]);\n\n    // Create PR\n    const prBody = `## 🔧 Gopls Quickfixes Applied\n\nThis PR applies automatic fixes suggested by gopls for code quality improvements.\n\n> [!TIP]\n> You might want to push an empty commit to this PR to trigger CI checks.\n\n### Files Modified\n\\`\\`\\`\n${fixedFiles}\n\\`\\`\\`\n\n### Changes\n- Applied gopls quickfixes to improve code quality\n- All changes are automated and safe\n- Generated by monthly gopls check workflow\n\n### Related Issue\n${issueNumber ? `Closes #${issueNumber}` : ''}\n\n---\n*This PR was automatically generated by the monthly gopls quickfix workflow.*\n`;\n\n    const pr = await github.rest.pulls.create({\n      owner: context.repo.owner,\n      repo: context.repo.repo,\n      title: `🔧 Apply gopls quickfixes - ${new Date().toISOString().split('T')[0]}`,\n      body: prBody,\n      head: branchName,\n      base: 'main',\n      labels: ['gopls', 'automated', 'maintenance']\n    });\n\n    const prNumber = pr.data.number;\n    console.log(`Created PR #${prNumber}`);\n\n    // Link PR to issue if issue was created\n    if (issueNumber) {\n      await github.rest.issues.createComment({\n        owner: context.repo.owner,\n        repo: context.repo.repo,\n        issue_number: parseInt(issueNumber),\n        body: `🔗 **Pull Request Created**\n\nA pull request has been created to address the gopls quickfix issues found in this issue.\n\n**PR**: #${prNumber}\n**Status**: Ready for review\n\nThe PR will automatically close this issue when merged.`\n      });\n    }\n\n    return prNumber;\n  } catch (error) {\n    core.setFailed(`Failed to create pull request: ${error.message}`);\n    throw error;\n  }\n};\n"
  },
  {
    "path": ".github/scripts/gopls/run.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\nmise use -g go:golang.org/x/tools/gopls@v0.18.1\n\ngopls version\n\nTEMP_DIR=$(mktemp -d)\nFIXED_FILES=\"$TEMP_DIR/fixed_files.txt\"\nFAILURES_FILE=\"$TEMP_DIR/gopls_failures.txt\"\nOUTPUT_FILE=\"$TEMP_DIR/gopls_output.txt\"\n\ntouch \"$FIXED_FILES\"\ntouch \"$FAILURES_FILE\"\ntouch \"$OUTPUT_FILE\"\n\nwhile IFS= read -r file; do\n    echo \"START: $file\" | tee -a \"$OUTPUT_FILE\"\n\n    if gopls codeaction -kind=quickfix -write \"$file\"; then\n        echo \"SUCCESS: $file\" | tee -a \"$OUTPUT_FILE\"\n    else\n        echo \"FAILED: $file\" | tee -a \"$FAILURES_FILE\" \"$OUTPUT_FILE\"\n        echo \"$file\" >> \"$FIXED_FILES\"\n    fi\n\n    echo \"END: $file\" | tee -a \"$OUTPUT_FILE\"\ndone < gofiles.txt\n\nprintf '\\n==== gopls failures (if any) ====\\n' | tee -a \"$OUTPUT_FILE\"\ntee -a \"$OUTPUT_FILE\" < \"$FAILURES_FILE\" || true\n\n# Check if any files were modified\nif [[ -s \"$FIXED_FILES\" ]]; then\n    echo \"has_fixes=true\" >> \"$GITHUB_OUTPUT\"\n    echo \"Files with fixes:\" | tee -a \"$OUTPUT_FILE\"\n    tee -a \"$OUTPUT_FILE\" < \"$FIXED_FILES\"\nelse\n    echo \"has_fixes=false\" >> \"$GITHUB_OUTPUT\"\n    echo \"No files were modified by gopls quickfixes\" | tee -a \"$OUTPUT_FILE\"\nfi\n\n# Output file paths for other steps to use\necho \"fixed_files_path=$FIXED_FILES\" >> \"$GITHUB_OUTPUT\"\necho \"output_file_path=$OUTPUT_FILE\" >> \"$GITHUB_OUTPUT\"\n"
  },
  {
    "path": ".github/scripts/release/README.md",
    "content": "# Release Scripts\n\nThis directory contains scripts used by the GitHub Actions release workflow to build, sign, and publish Terragrunt releases.\n\n## Overview\n\nAll inline bash and PowerShell code from GitHub Actions workflows has been extracted into these standalone scripts for better:\n- **Syntax highlighting** - Proper IDE support\n- **Linting** - Can run shellcheck/PSScriptAnalyzer\n- **Testing** - Scripts can be tested independently\n- **Maintainability** - Easier to update and debug\n- **Reusability** - Can be called from multiple workflows\n\n## Script Overview\n\n### Core Library\n- **`lib-release-config.sh`** - Helper library to read centralized release configuration from JSON\n\n### General Release Scripts\n- **`get-version.sh`** - Extracts and validates version from git tag or workflow input\n- **`check-release-exists.sh`** - Checks if GitHub release exists for a version\n- **`verify-binaries-downloaded.sh`** - Verifies expected binaries were downloaded from artifacts\n- **`set-permissions.sh`** - Sets executable permissions (+x) on all binaries\n- **`create-archives.sh`** - Creates ZIP and TAR.GZ archives for each binary\n- **`generate-checksums.sh`** - Generates SHA256SUMS file for all release files\n- **`verify-files.sh`** - Verifies all required files are present before upload\n- **`upload-assets.sh`** - Uploads all assets to GitHub release (with optional --clobber)\n- **`verify-assets-uploaded.sh`** - Verifies uploads and retries missing files\n- **`generate-upload-summary.sh`** - Generates GitHub Actions step summary with release details\n\n### macOS Signing Scripts\n- **`prepare-macos-artifacts.sh`** - Prepares macOS artifacts for signing (filters darwin_* binaries)\n- **`install-gon.sh`** - Downloads and installs gon tool for macOS code signing\n- **`sign-macos-binaries.sh`** - Signs macOS binaries using gon and Apple notarization\n\n### Windows Signing Scripts\n- **`prepare-windows-artifacts.ps1`** - Prepares Windows artifacts for signing\n- **`install-go-winres.ps1`** - Installs go-winres tool for patching Windows resources\n- **`verify-smctl.ps1`** - Verifies DigiCert smctl tool is installed and accessible\n- **`restore-p12-certificate.ps1`** - Restores P12 client certificate from base64 encoding\n- **`sign-windows.ps1`** - Config-driven: patches all binaries, signs only those marked `signed: true`\n\n## Centralized Configuration\n\nRelease asset configuration is maintained in a single source of truth:\n\n### `../assets/release-assets-config.json`\nJSON file defining all platforms, binaries, archive formats, and additional files.\n\n**Schema:**\n```json\n{\n  \"platforms\": [\n    {\n      \"os\": \"darwin|linux|windows\",\n      \"arch\": \"386|amd64|arm64\",\n      \"signed\": true|false,\n      \"binary\": \"terragrunt_<os>_<arch>[.exe]\"\n    }\n  ],\n  \"archive_formats\": [\n    {\n      \"extension\": \"zip|tar.gz\",\n      \"description\": \"Format description\"\n    }\n  ],\n  \"additional_files\": [\n    {\n      \"name\": \"SHA256SUMS\",\n      \"description\": \"File description\"\n    }\n  ]\n}\n```\n\n### `lib-release-config.sh`\nHelper library providing functions to read the centralized configuration.\n\n**Usage:**\n```bash\n#!/bin/bash\nsource \"$(dirname \"$0\")/lib-release-config.sh\"\n\n# Verify config file exists\nverify_config_file\n\n# Get all binary filenames\nget_all_binaries\n\n# Get binary count\nget_binary_count\n\n# Get total expected file count\nget_total_file_count\n\n# Get archive extensions\nget_archive_extensions\n\n# Get additional files\nget_additional_files\n\n# Get all expected files (binaries + archives + additional)\nget_all_expected_files\n\n# Get platform info for specific binary\nget_platform_info \"terragrunt_darwin_amd64\"\n\n# Generate markdown table rows for summary\ngenerate_platform_table_rows\n```\n\n**Scripts Using Configuration:**\n- `verify-binaries-downloaded.sh` - Uses `get_binary_count()`\n- `set-permissions.sh` - Uses `get_all_binaries()`\n- `verify-assets-uploaded.sh` - Uses `get_all_expected_files()`, `get_total_file_count()`, `get_binary_count()`\n- `generate-upload-summary.sh` - Uses `get_binary_count()`, `get_total_file_count()`, `generate_platform_table_rows()`\n\n## General Scripts\n\n### `get-version.sh`\nExtracts version from either workflow dispatch input or git tag.\n\n**Environment Variables:**\n- `INPUT_TAG`: Tag provided via workflow_dispatch\n- `EVENT_NAME`: GitHub event name (workflow_dispatch or push)\n- `GITHUB_REF`: Git reference (e.g., refs/tags/v0.93.4)\n- `GITHUB_OUTPUT`: Path to GitHub output file\n\n**Usage:**\n```bash\n.github/scripts/release/get-version.sh\n```\n\n### `check-release-exists.sh`\nChecks if a GitHub release exists for a given tag using the GitHub CLI.\n\n**Environment Variables:**\n- `VERSION`: The version/tag to check for\n- `GH_TOKEN`: GitHub token for authentication\n- `GITHUB_OUTPUT`: Path to GitHub output file\n\n**Usage:**\n```bash\nexport VERSION=v0.93.4\nexport GH_TOKEN=<token>\n.github/scripts/release/check-release-exists.sh\n```\n\n### `verify-binaries-downloaded.sh`\nVerifies all expected binaries were downloaded from build artifacts.\n\n**Parameters:**\n- `bin-directory`: Directory containing binaries (default: `bin`)\n- `expected-count`: Minimum number of binaries expected (default: `7`)\n\n**Usage:**\n```bash\n.github/scripts/release/verify-binaries-downloaded.sh bin 7\n```\n\n**Features:**\n- Lists all downloaded binaries with details\n- Counts total files using `find`\n- Validates minimum expected count\n- Exits with error if count is below threshold\n\n### `set-permissions.sh`\nSets executable permissions (+x) on all Terragrunt binaries.\n\n**Usage:**\n```bash\n.github/scripts/release/set-permissions.sh bin\n```\n\n### `create-archives.sh`\nCreates both ZIP and TAR.GZ archives for each binary, preserving execute permissions.\n\n**Usage:**\n```bash\n.github/scripts/release/create-archives.sh bin\n```\n\n**Output:**\n- Creates `.zip` and `.tar.gz` for each binary\n- ZIP files preserve Unix permissions\n- TAR.GZ files natively preserve all file attributes\n\n### `generate-checksums.sh`\nGenerates SHA256 checksums for all release files (binaries and archives).\n\n**Usage:**\n```bash\n.github/scripts/release/generate-checksums.sh bin\n```\n\n**Output:**\n- Creates `SHA256SUMS` file with checksums for all files\n\n### `verify-files.sh`\nVerifies all required files are present before upload.\n\n**Usage:**\n```bash\n.github/scripts/release/verify-files.sh bin\n```\n\n**Checks:**\n- All platform binaries (macOS, Linux, Windows)\n- SHA256SUMS file\n\n### `upload-assets.sh`\nUploads all release assets to an existing GitHub release.\n\n**Environment Variables:**\n- `VERSION`: The version/tag to upload to\n- `GH_TOKEN`: GitHub token for authentication\n\n**Usage:**\n```bash\nexport VERSION=v0.93.4\nexport GH_TOKEN=<token>\n.github/scripts/release/upload-assets.sh bin\n```\n\n### `verify-assets-uploaded.sh`\nVerifies all assets were successfully uploaded and retries any missing files.\n\n**Environment Variables:**\n- `VERSION`: The version/tag to verify\n- `GH_TOKEN`: GitHub token for authentication\n- `CLOBBER`: Set to 'true' to overwrite existing assets during retry (default: false)\n\n**Usage:**\n```bash\nexport VERSION=v0.93.4\nexport GH_TOKEN=<token>\nexport CLOBBER=false\n.github/scripts/release/verify-assets-uploaded.sh bin\n```\n\n**Features:**\n- Checks for 22 expected files (7 binaries + 7 ZIPs + 7 TAR.GZ + SHA256SUMS)\n- Automatically retries failed uploads (max 10 attempts)\n- Verifies asset downloadability\n\n### `generate-upload-summary.sh`\nGenerates a GitHub Actions step summary with release upload details.\n\n**Environment Variables:**\n- `VERSION`: Release version/tag\n- `RELEASE_ID`: GitHub release ID\n- `IS_DRAFT`: Whether release was a draft\n- `GITHUB_STEP_SUMMARY`: Path to GitHub step summary file\n\n**Usage:**\n```bash\nexport VERSION=v0.93.4\nexport RELEASE_ID=123456\nexport IS_DRAFT=false\nexport GITHUB_STEP_SUMMARY=$GITHUB_STEP_SUMMARY\n.github/scripts/release/generate-upload-summary.sh\n```\n\n**Features:**\n- Creates formatted markdown summary\n- Shows release details (version, ID, draft status)\n- Displays platform/architecture table\n- Lists archive files and totals\n- Always runs (even on failure) via `if: always()` in workflow\n\n## macOS Scripts\n\n### `prepare-macos-artifacts.sh`\nPrepares macOS artifacts for signing by copying them from the artifacts directory to the bin directory.\n\n**Usage:**\n```bash\n.github/scripts/release/prepare-macos-artifacts.sh artifacts bin\n```\n\n### `install-gon.sh`\nDownloads and installs the gon binary for macOS code signing and notarization.\n\n**Environment Variables:**\n- `GON_VERSION`: Version of gon to install (default: v0.0.37)\n\n**Usage:**\n```bash\nexport GON_VERSION=v0.0.37\n.github/scripts/release/install-gon.sh\n# or pass version as argument\n.github/scripts/release/install-gon.sh v0.0.37\n```\n\n**Features:**\n- Downloads gon from GitHub releases\n- Installs to `/usr/local/bin`\n- Verifies installation\n- Cleans up temporary files\n\n### `sign-macos-binaries.sh`\nSigns macOS binaries using gon and Apple notarization service.\n\n**Environment Variables:**\n- `AC_PASSWORD`: Apple Connect password\n- `AC_PROVIDER`: Apple Connect provider\n- `AC_USERNAME`: Apple Connect username\n- `MACOS_CERTIFICATE`: macOS certificate in P12 format (base64 encoded)\n- `MACOS_CERTIFICATE_PASSWORD`: Certificate password\n\n**Usage:**\n```bash\nexport AC_PASSWORD=<password>\nexport AC_PROVIDER=<provider>\nexport AC_USERNAME=<username>\nexport MACOS_CERTIFICATE=<base64-cert>\nexport MACOS_CERTIFICATE_PASSWORD=<password>\n.github/scripts/release/sign-macos-binaries.sh bin\n```\n\n**Process:**\n1. Signs amd64 binary using `.gon_amd64.hcl`\n2. Signs arm64 binary using `.gon_arm64.hcl`\n3. Removes unsigned binaries from bin directory\n4. Extracts signed binaries from ZIP archives\n5. Moves signed binaries to bin directory (replacing unsigned versions)\n6. Verifies signatures using `codesign -dv --verbose=4`\n\n## Windows Scripts\n\n### `prepare-windows-artifacts.ps1`\nPrepares Windows artifacts for signing by copying them from the artifacts directory to the bin directory.\n\n**Parameters:**\n- `-ArtifactsDirectory`: Source directory (default: `artifacts`)\n- `-BinDirectory`: Destination directory (default: `bin`)\n\n**Usage:**\n```powershell\n.github/scripts/release/prepare-windows-artifacts.ps1 -ArtifactsDirectory artifacts -BinDirectory bin\n```\n\n### `install-go-winres.ps1`\nInstalls go-winres tool and adds it to PATH.\n\n**Usage:**\n```powershell\n.github/scripts/release/install-go-winres.ps1\n```\n\n**Features:**\n- Installs go-winres from GitHub\n- Adds Go bin directory to PATH\n- Exports PATH to GitHub environment\n- Verifies installation with `go-winres help`\n\n### `verify-smctl.ps1`\nVerifies that DigiCert smctl tool is installed and accessible.\n\n**Usage:**\n```powershell\n.github/scripts/release/verify-smctl.ps1\n```\n\n**Checks:**\n- smctl.exe is in PATH\n- Displays smctl version\n- Confirms tool is ready for use\n\n### `restore-p12-certificate.ps1`\nRestores P12 client certificate from base64 encoding.\n\n**Environment Variables:**\n- `WINDOWS_SIGNING_P12_BASE64`: Base64 encoded P12 certificate (required)\n- `RUNNER_TEMP`: Temporary directory for certificate file\n- `GITHUB_ENV`: Path to GitHub environment file\n\n**Usage:**\n```powershell\n$env:WINDOWS_SIGNING_P12_BASE64 = \"<base64-string>\"\n.github/scripts/release/restore-p12-certificate.ps1\n```\n\n**Output:**\n- Creates certificate file in `$RUNNER_TEMP/sm_client_auth.p12`\n- Exports `SM_CLIENT_CERT_FILE` environment variable\n\n### `sign-windows.ps1`\nComprehensive Windows binary signing script using DigiCert KeyLocker. **Fully driven by configuration**.\n\n**Environment Variables:**\n- `GITHUB_REF_NAME`: Git ref name (e.g., v0.93.4 or beta-2025111001)\n- `SM_HOST`: DigiCert host URL\n- `SM_API_KEY`: DigiCert API key\n- `SM_CLIENT_CERT_FILE`: Path to P12 certificate file\n- `SM_CLIENT_CERT_PASSWORD`: Certificate password\n- `SM_KEYPAIR_ALIAS`: DigiCert keypair alias\n\n**Parameters:**\n- `-BinDirectory`: Directory containing binaries (default: `bin`)\n\n**Usage:**\n```powershell\n.github/scripts/release/sign-windows.ps1 -BinDirectory bin\n```\n\n**Process:**\n1. **Configuration Loading**: Reads `release-assets-config.json` to discover all Windows platforms\n2. **Version Detection**: Parses git ref to extract version\n   - Standard: `v0.93.4` → `0.93.4`\n   - Pre-release: `beta-2025111001` → `2025.1110.01.0`\n   - Generic: `<prefix>-YYYYMMDDNN` → `YYYY.MMDD.NN.0`\n3. **Resource Generation**: Dynamically generates `winres.json` with version info, icon, manifest, and metadata\n4. **Binary Patching**: Uses go-winres to patch **all** Windows binaries with icon and metadata (regardless of signing status)\n5. **Credential Setup**: Saves DigiCert credentials\n6. **Healthcheck**: Runs smctl healthcheck\n7. **Certificate Sync**: Syncs certificates from DigiCert KeyLocker\n8. **Selective Signing**: For each Windows platform in config:\n   - If `\"signed\": true` → Signs with DigiCert and verifies signature\n   - If `\"signed\": false` → Skips signing (patching only)\n9. **Summary**: Reports how many binaries were signed vs. patched-only\n\n**Configuration-Driven Behavior:**\nThe script reads `.github/assets/release-assets-config.json` to determine:\n- Which Windows binaries exist (`binary` field)\n- Which binaries should be signed (`signed: true/false`)\n- All patching decisions are automated based on JSON\n\n**Example Config:**\n```json\n{\n  \"platforms\": [\n    {\n      \"os\": \"windows\",\n      \"arch\": \"386\",\n      \"signed\": false,\n      \"binary\": \"terragrunt_windows_386.exe\"\n    },\n    {\n      \"os\": \"windows\",\n      \"arch\": \"amd64\",\n      \"signed\": true,\n      \"binary\": \"terragrunt_windows_amd64.exe\"\n    }\n  ]\n}\n```\n\nWith this config, the script will:\n- Patch both binaries with icon/manifest\n- Sign only the amd64 binary (conserving signature quota)\n- Leave 386 unsigned\n\n## Testing\n\n### Bash Scripts\n\n```bash\n# Install shellcheck\nsudo apt-get install shellcheck  # Ubuntu/Debian\n# or\nbrew install shellcheck  # macOS\n\n# Check all bash scripts\nshellcheck .github/scripts/release/*.sh\n\n# Test individual script\nexport VERSION=v0.93.4\nexport GH_TOKEN=<token>\n.github/scripts/release/get-version.sh\n```\n\n### PowerShell Scripts\n\n```powershell\n# Install PSScriptAnalyzer\nInstall-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser\n\n# Analyze all PowerShell scripts\nGet-ChildItem .github/scripts/release/*.ps1 | ForEach-Object {\n    Invoke-ScriptAnalyzer -Path $_.FullName\n}\n\n# Test individual script\n.github/scripts/release/verify-smctl.ps1\n```\n\n## Workflow Integration\n\nThese scripts are used by:\n\n- **`.github/workflows/release.yml`** - Main release workflow\n  - Uses: `get-version.sh`, `check-release-exists.sh`, `set-permissions.sh`, `create-archives.sh`, `generate-checksums.sh`, `verify-files.sh`, `upload-assets.sh`, `verify-assets-uploaded.sh`\n\n- **`.github/workflows/sign-macos.yml`** - macOS signing workflow\n  - Uses: `prepare-macos-artifacts.sh`, `install-gon.sh`, `sign-macos-binaries.sh`\n\n- **`.github/workflows/sign-windows.yml`** - Windows signing workflow\n  - Uses: `prepare-windows-artifacts.ps1`, `install-go-winres.ps1`, `verify-smctl.ps1`, `restore-p12-certificate.ps1`, `sign-windows.ps1`\n\n## Version Format Support\n\nThe scripts support multiple version tag formats:\n\n| Format   | Example            | Windows FileVersion | Description                     |\n|----------|--------------------|---------------------|---------------------------------|\n| Standard | `v0.93.4`          | `0.93.4.0`          | Semantic version with v prefix  |\n| Beta     | `beta-2025111001`  | `2025.1110.01.0`    | Pre-release with date timestamp |\n| Alpha    | `alpha-2025110301` | `2025.1103.01.0`    | Alpha with date timestamp       |\n\n**Windows Version Constraints:**\n- Each component must be ≤ 65535\n- Format: YYYY.MMDD.NN.0 keeps all components within limits\n\n## Security Notes\n\n- All scripts use proper quoting to prevent command injection\n- Environment variables are validated before use\n- Sensitive data (tokens, passwords) passed via environment variables only\n- Scripts fail fast on errors:\n  - Bash: `set -e`\n  - PowerShell: `$ErrorActionPreference = 'Stop'`\n- No secrets are logged or printed to stdout\n- Certificate files are stored in temporary directories\n\n## Script Conventions\n\n### Bash Scripts\n- Use `#!/bin/bash` shebang\n- Enable fail-fast: `set -e`\n- Use functions for organization\n- Validate environment variables with `assert_env_var_not_empty`\n- Accept directory paths as arguments (default: `bin`)\n- Use `printf` instead of `echo` for variable output\n- Proper quoting: `\"$variable\"` not `$variable`\n\n### PowerShell Scripts\n- Use strict error handling: `$ErrorActionPreference = 'Stop'`\n- Use parameters with defaults: `param([string]$BinDirectory = \"bin\")`\n- Check exit codes: `if ($LASTEXITCODE -ne 0) { exit 1 }`\n- Organize code into functions\n- Use `Write-Host` for informational output\n- Use `Write-Error` for errors\n"
  },
  {
    "path": ".github/scripts/release/check-release-exists.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Script to check if a GitHub release exists for a given tag\n# Usage: check-release-exists.sh\n# Environment variables:\n#   VERSION: The version/tag to check for\n#   GH_TOKEN: GitHub token for authentication\n#   GITHUB_OUTPUT: Path to GitHub output file\n\nfunction main {\n  # Validate required environment variables\n  : \"${VERSION:?ERROR: VERSION is a required environment variable}\"\n  : \"${GH_TOKEN:?ERROR: GH_TOKEN is a required environment variable}\"\n  : \"${GITHUB_OUTPUT:?ERROR: GITHUB_OUTPUT is a required environment variable}\"\n\n  printf 'Checking if release exists for tag: %s\\n' \"$VERSION\"\n\n  # Check if release exists using gh CLI (only care about exit code)\n  if ! gh release view \"$VERSION\" > /dev/null 2>&1; then\n    printf 'exists=false\\n' >> \"$GITHUB_OUTPUT\"\n    printf 'Release not found for tag %s\\n' \"$VERSION\"\n    exit 1\n  fi\n\n  # Get release details\n  local release_json\n  release_json=$(gh release view \"$VERSION\" --json 'id,uploadUrl,isDraft')\n\n  local release_id\n  local upload_url\n  local is_draft\n\n  release_id=$(jq -r '.id' <<< \"$release_json\")\n  upload_url=$(jq -r '.uploadUrl' <<< \"$release_json\")\n  is_draft=$(jq -r '.isDraft' <<< \"$release_json\")\n\n  # Write to GitHub output\n  printf 'exists=true\\n' >> \"$GITHUB_OUTPUT\"\n  printf 'release_id=%s\\n' \"$release_id\" >> \"$GITHUB_OUTPUT\"\n  printf 'upload_url=%s\\n' \"$upload_url\" >> \"$GITHUB_OUTPUT\"\n  printf 'is_draft=%s\\n' \"$is_draft\" >> \"$GITHUB_OUTPUT\"\n\n  echo \"Found existing release:\"\n  printf '  Release ID: %s\\n' \"$release_id\"\n  printf '  Draft: %s\\n' \"$is_draft\"\n  printf '  Upload URL: %s\\n' \"${upload_url%\\{*}\"\n\n  return 0\n}\n\nmain \"$@\"\n"
  },
  {
    "path": ".github/scripts/release/create-archives.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Script to create ZIP and TAR.GZ archives for each binary\n# Usage: create-archives.sh <bin-directory>\n\nfunction main {\n  local -r bin_dir=\"${1:-bin}\"\n\n  if [[ ! -d \"$bin_dir\" ]]; then\n    echo \"ERROR: Directory $bin_dir does not exist\" >&2\n    exit 1\n  fi\n\n  # Use pushd/popd to avoid side effects on caller's working directory\n  pushd \"$bin_dir\" || return 1\n\n  echo \"Creating individual archives for each binary...\"\n\n  # Create individual ZIP and TAR.GZ archives for each binary (preserving execute permissions)\n  for binary in terragrunt_*; do\n    # Skip if it's already an archive file\n    if [[ \"$binary\" == *.zip ]] || [[ \"$binary\" == *.tar.gz ]]; then\n      continue\n    fi\n\n    # Create ZIP archive\n    zip \"$binary.zip\" \"$binary\"\n    echo \"Created: $binary.zip\"\n\n    # Create TAR.GZ archive (preserves Unix permissions including +x)\n    tar -czf \"$binary.tar.gz\" \"$binary\"\n    echo \"Created: $binary.tar.gz\"\n  done\n\n  echo \"\"\n  echo \"All individual archives created:\"\n  echo \"ZIP archives:\"\n  ls -lh *.zip\n  echo \"\"\n  echo \"TAR.GZ archives:\"\n  ls -lh *.tar.gz\n\n  # Return to original directory\n  popd || return 1\n\n  return 0\n}\n\nmain \"$@\"\n"
  },
  {
    "path": ".github/scripts/release/generate-checksums.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Script to generate SHA256 checksums for all release files\n# Usage: generate-checksums.sh <bin-directory>\n\nfunction main {\n  local -r bin_dir=\"${1:-bin}\"\n\n  if [[ ! -d \"$bin_dir\" ]]; then\n    echo \"ERROR: Directory $bin_dir does not exist\" >&2\n    exit 1\n  fi\n\n  # Use pushd/popd to avoid side effects on caller's working directory\n  pushd \"$bin_dir\" || return 1\n\n  # Generate checksums for all files including individual ZIPs and TAR.GZ archives\n  sha256sum terragrunt_* > SHA256SUMS\n\n  echo \"SHA256SUMS generated:\"\n  cat SHA256SUMS\n\n  echo \"\"\n  echo \"Total files with checksums: $(wc -l < SHA256SUMS)\"\n\n  # Return to original directory\n  popd || return 1\n\n  return 0\n}\n\nmain \"$@\"\n"
  },
  {
    "path": ".github/scripts/release/generate-upload-summary.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Script to generate GitHub Actions step summary for release uploads\n# Usage: generate-upload-summary.sh\n# Environment variables:\n#   VERSION: Release version/tag\n#   RELEASE_ID: GitHub release ID\n#   IS_DRAFT: Whether release was a draft\n#   GITHUB_STEP_SUMMARY: Path to GitHub step summary file\n\n# Source configuration library\n# shellcheck source=lib-release-config.sh\nsource \"$(dirname \"$0\")/lib-release-config.sh\"\n\nmain() {\n  require_env_vars VERSION RELEASE_ID IS_DRAFT GITHUB_STEP_SUMMARY\n  verify_config_file\n\n  echo \"Generating upload summary...\"\n\n  local binary_count\n  binary_count=$(get_binary_count)\n  local total_count\n  total_count=$(get_total_file_count)\n\n  cat >>\"$GITHUB_STEP_SUMMARY\" <<EOF\n## Release Asset Upload Summary\n\n**Version**: $VERSION\n**Release ID**: $RELEASE_ID\n**Was Draft**: $IS_DRAFT\n\n### Assets Uploaded\n\n| Platform | Architecture | Signed | Status |\n|----------|--------------|--------|--------|\nEOF\n\n  # Generate platform table rows from configuration\n  generate_platform_table_rows >>\"$GITHUB_STEP_SUMMARY\"\n\n  cat >>\"$GITHUB_STEP_SUMMARY\" <<EOF\n\n**Archive Files**:\n- Individual ZIP archives: $binary_count files (one per binary, with +x permissions)\n- Individual TAR.GZ archives: $binary_count files (one per binary, with +x permissions)\n- **SHA256SUMS**: Checksums for all files\n\n**Total Files**: $total_count ($binary_count binaries + $binary_count ZIPs + $binary_count TAR.GZ + SHA256SUMS)\n\nAll assets uploaded successfully to existing release!\nEOF\n\n  echo \"Upload summary generated successfully\"\n\n  return 0\n}\n\nrequire_env_vars() {\n  local missing=0\n\n  for var_name in \"$@\"; do\n    if [[ -z \"${!var_name:-}\" ]]; then\n      echo \"ERROR: Required environment variable $var_name not set.\" >&2\n      missing=1\n    fi\n  done\n\n  if [[ \"$missing\" -eq 1 ]]; then\n    exit 1\n  fi\n\n  return 0\n}\n\nmain \"$@\"\n"
  },
  {
    "path": ".github/scripts/release/get-version.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Script to get the version from either workflow dispatch input or git ref\n# Usage: get-version.sh\n# Environment variables:\n#   INPUT_TAG: Tag provided via workflow_dispatch\n#   EVENT_NAME: GitHub event name (workflow_dispatch or push)\n#   GITHUB_REF: Git reference (e.g., refs/tags/v0.93.4)\n#   GITHUB_OUTPUT: Path to GitHub output file\n\nfunction resolve_version {\n  # Handle workflow_dispatch event (manual trigger with INPUT_TAG)\n  if [[ \"$EVENT_NAME\" = \"workflow_dispatch\" ]]; then\n    if [[ -z \"$INPUT_TAG\" ]]; then\n      echo \"ERROR: INPUT_TAG is empty for workflow_dispatch event\" >&2\n      exit 1\n    fi\n    echo \"$INPUT_TAG\"\n    return 0\n  fi\n\n  # Handle push event (tag push with GITHUB_REF)\n  if [[ -z \"$GITHUB_REF\" ]]; then\n    echo \"ERROR: GITHUB_REF is empty\" >&2\n    exit 1\n  fi\n\n  if [[ ! \"$GITHUB_REF\" =~ ^refs/tags/ ]]; then\n    echo \"ERROR: GITHUB_REF does not start with 'refs/tags/': $GITHUB_REF\" >&2\n    exit 1\n  fi\n\n  # Strip refs/tags/ prefix and return\n  echo \"${GITHUB_REF#refs/tags/}\"\n\n  return 0\n}\n\nfunction validate_version {\n  local -r version=\"$1\"\n\n  if [[ -z \"$version\" ]]; then\n    echo \"ERROR: Extracted version is empty\" >&2\n    exit 1\n  fi\n\n  # Validate version matches expected pattern (tag-like: starts with letter/digit)\n  if [[ ! \"$version\" =~ ^[a-zA-Z0-9] ]]; then\n    echo \"ERROR: Invalid version format: '$version' (must start with alphanumeric character)\" >&2\n    exit 1\n  fi\n\n  return 0\n}\n\nfunction main {\n  local version\n  version=$(resolve_version)\n\n  validate_version \"$version\"\n\n  # Write to GitHub output\n  printf 'version=%s\\n' \"$version\" >> \"$GITHUB_OUTPUT\"\n  printf 'Release version: %s\\n' \"$version\"\n\n  return 0\n}\n\nmain \"$@\"\n"
  },
  {
    "path": ".github/scripts/release/install-go-winres.ps1",
    "content": "# Script to install go-winres tool\n# Usage: install-go-winres.ps1\n\n$ErrorActionPreference = 'Stop'\n\nWrite-Host \"Installing go-winres...\"\n\n# Install go-winres\ngo install github.com/tc-hib/go-winres@latest\n\nif ($LASTEXITCODE -ne 0) {\n    Write-Error \"Failed to install go-winres\"\n    exit 1\n}\n\n# Add Go bin to PATH\n$goPath = & go env GOPATH\n$goBinPath = Join-Path $goPath \"bin\"\n$env:PATH = \"$goBinPath;$env:PATH\"\n\n# Export PATH to GitHub environment\necho \"$goBinPath\" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append\n\nWrite-Host \"go-winres installed to: $goBinPath\"\nWrite-Host \"\"\nWrite-Host \"Verifying go-winres installation...\"\n\n& go-winres help\n\nif ($LASTEXITCODE -ne 0) {\n    Write-Error \"go-winres verification failed\"\n    exit 1\n}\n\nWrite-Host \"\"\nWrite-Host \"go-winres installed successfully\"\n"
  },
  {
    "path": ".github/scripts/release/install-gon.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Script to download and install gon binary for macOS code signing\n# Usage: install-gon.sh [gon-version]\n# Environment variables:\n#   GON_VERSION: Version of gon to install (default: v0.0.37)\n\nfunction main {\n  local gon_version=\"${1:-${GON_VERSION:-v0.0.37}}\"\n\n  echo \"Installing gon version $gon_version...\"\n\n  # Download gon release\n  local download_url=\"https://github.com/Bearer/gon/releases/download/${gon_version}/gon_macos.zip\"\n\n  echo \"Downloading gon from: $download_url\"\n  if ! curl -L -o gon.zip \"$download_url\"; then\n    echo \"ERROR: Failed to download gon from $download_url\" >&2\n    rm -f gon.zip\n    exit 1\n  fi\n\n  # Extract to specific target\n  echo \"Extracting gon binary...\"\n  if ! unzip -o gon.zip -d . gon; then\n    echo \"ERROR: Failed to extract gon from gon.zip\" >&2\n    rm -f gon.zip\n    exit 1\n  fi\n\n  # Verify extracted binary exists and is a regular file\n  if [[ ! -f ./gon ]]; then\n    echo \"ERROR: Expected file './gon' not found after extraction\" >&2\n    rm -f gon.zip\n    exit 1\n  fi\n\n  # Make executable\n  echo \"Setting executable permissions...\"\n  if ! chmod +x ./gon; then\n    echo \"ERROR: Failed to set executable permissions on ./gon\" >&2\n    rm -f gon.zip ./gon\n    exit 1\n  fi\n\n  # Verify it's executable\n  if [[ ! -x ./gon ]]; then\n    echo \"ERROR: File ./gon is not executable after chmod\" >&2\n    rm -f gon.zip ./gon\n    exit 1\n  fi\n\n  # Move to system path\n  echo \"Moving gon to /usr/local/bin/\"\n  if ! sudo mv ./gon /usr/local/bin/gon; then\n    echo \"ERROR: Failed to move gon to /usr/local/bin/\" >&2\n    rm -f gon.zip ./gon\n    exit 1\n  fi\n\n  if ! sudo chmod +x /usr/local/bin/gon; then\n    echo \"ERROR: Failed to set executable permissions on /usr/local/bin/gon\" >&2\n    exit 1\n  fi\n\n  # Verify installation\n  echo \"Verifying gon installation...\"\n  if ! gon --version; then\n    echo \"ERROR: gon --version failed after installation\" >&2\n    exit 1\n  fi\n\n  # Cleanup\n  rm -f gon.zip\n\n  echo \"gon installed successfully\"\n\n  return 0\n}\n\nmain \"$@\"\n"
  },
  {
    "path": ".github/scripts/release/lib-release-config.sh",
    "content": "#!/usr/bin/env bash\n\n# Library script to read release assets configuration\n# Usage: source .github/scripts/release/lib-release-config.sh\n\nreadonly RELEASE_CONFIG_FILE=\".github/assets/release-assets-config.json\"\n\n# Get list of all binary filenames\nget_all_binaries() {\n  jq -r '.platforms[].binary' \"$RELEASE_CONFIG_FILE\"\n}\n\n# Get total binary count (computed from platforms array)\nget_binary_count() {\n  jq -r '.platforms | length' \"$RELEASE_CONFIG_FILE\"\n}\n\n# Get total expected file count (computed: binaries + archives + additional files)\nget_total_file_count() {\n  jq -r '\n    (.platforms | length) as $binaries |\n    (.archive_formats | length) as $formats |\n    (.additional_files | length) as $additional |\n    $binaries + ($binaries * $formats) + $additional\n  ' \"$RELEASE_CONFIG_FILE\"\n}\n\n# Get list of archive extensions\nget_archive_extensions() {\n  jq -r '.archive_formats[].extension' \"$RELEASE_CONFIG_FILE\"\n}\n\n# Get list of additional files\nget_additional_files() {\n  jq -r '.additional_files[].name' \"$RELEASE_CONFIG_FILE\"\n}\n\n# Generate expected files list (for verification)\nget_all_expected_files() {\n  local binaries\n\n  # Get binaries\n  binaries=$(get_all_binaries)\n\n  # Generate list: binaries + archives + additional files\n  echo \"$binaries\"\n\n  # Add archives for each binary\n  for binary in $binaries; do\n    while IFS= read -r ext; do\n      echo \"${binary}.${ext}\"\n    done < <(get_archive_extensions)\n  done\n\n  # Add additional files\n  get_additional_files\n\n  return 0\n}\n\n# Get platform info as JSON for a specific binary\nget_platform_info() {\n  local binary=\"$1\"\n  jq --arg binary \"$binary\" '.platforms[] | select(.binary == $binary)' \"$RELEASE_CONFIG_FILE\"\n}\n\n# Generate markdown table rows for summary\ngenerate_platform_table_rows() {\n  jq -r '.platforms[] | \"| \\(.os | ascii_downcase) | \\(.arch) | \\(if .signed then \"Yes\" else \"No\" end) | Uploaded |\"' \"$RELEASE_CONFIG_FILE\" |\n  awk '{\n    # Capitalize first letter of OS\n    if ($2 == \"darwin\") $2 = \"macOS\"\n    else if ($2 == \"linux\") $2 = \"Linux\"\n    else if ($2 == \"windows\") $2 = \"Windows\"\n    print\n  }'\n}\n\n# Check if config file exists\nverify_config_file() {\n  if [[ ! -f \"$RELEASE_CONFIG_FILE\" ]]; then\n    echo \"ERROR: Release config file not found: $RELEASE_CONFIG_FILE\" >&2\n    return 1\n  fi\n\n  return 0\n}\n"
  },
  {
    "path": ".github/scripts/release/prepare-macos-artifacts.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Script to prepare macOS artifacts for signing\n# Usage: prepare-macos-artifacts.sh <artifacts-dir> <bin-dir>\n\nfunction main {\n  local -r artifacts_dir=\"${1:-artifacts}\"\n  local -r bin_dir=\"${2:-bin}\"\n\n  if [[ ! -d \"$artifacts_dir\" ]]; then\n    echo \"ERROR: Artifacts directory $artifacts_dir does not exist\" >&2\n    exit 1\n  fi\n\n  echo \"Preparing macOS build artifacts...\"\n\n  # Create bin directory\n  mkdir -p \"$bin_dir\"\n\n  # Copy only macOS artifacts (terragrunt_darwin_*) to bin directory\n  find \"$artifacts_dir\" -type f -name 'terragrunt_darwin_*' -exec cp {} \"$bin_dir/\" \\;\n\n  # Verify we found macOS binaries\n  if ! ls \"$bin_dir\"/terragrunt_darwin_* > /dev/null 2>&1; then\n    echo \"ERROR: No macOS binaries (terragrunt_darwin_*) found in $artifacts_dir\" >&2\n    exit 1\n  fi\n\n  echo \"Binary files to sign:\"\n  ls -lahrt \"$bin_dir\"/*\n\n  return 0\n}\n\nmain \"$@\"\n"
  },
  {
    "path": ".github/scripts/release/prepare-windows-artifacts.ps1",
    "content": "# Script to prepare Windows artifacts for signing\n# Usage: prepare-windows-artifacts.ps1 -ArtifactsDirectory <path> -BinDirectory <path>\n\nparam(\n    [Parameter(Mandatory=$false)]\n    [string]$ArtifactsDirectory = \"artifacts\",\n\n    [Parameter(Mandatory=$false)]\n    [string]$BinDirectory = \"bin\"\n)\n\n$ErrorActionPreference = 'Stop'\n\nWrite-Host \"Preparing Windows build artifacts...\"\n\n# Create bin directory\nNew-Item -ItemType Directory -Force -Path $BinDirectory | Out-Null\n\n# Check if artifacts directory exists\nif (-not (Test-Path $ArtifactsDirectory)) {\n    Write-Error \"Artifacts directory not found: $ArtifactsDirectory\"\n    exit 1\n}\n\n# Copy Windows binaries to bin directory\nGet-ChildItem -Path $ArtifactsDirectory -Filter \"terragrunt_windows_*\" -Recurse -File |\nForEach-Object {\n    Copy-Item $_.FullName -Destination $BinDirectory/\n    Write-Host \"Copied: $($_.Name)\"\n}\n\nWrite-Host \"\"\nWrite-Host \"Binary files to sign:\"\nGet-ChildItem -Path $BinDirectory | ForEach-Object { Write-Host $_.FullName }\n\nWrite-Host \"\"\nWrite-Host \"Artifacts prepared successfully\"\n"
  },
  {
    "path": ".github/scripts/release/restore-p12-certificate.ps1",
    "content": "# Script to restore P12 client certificate from base64\n# Usage: restore-p12-certificate.ps1\n# Environment variables:\n#   WINDOWS_SIGNING_P12_BASE64: Base64 encoded P12 certificate (required)\n#   RUNNER_TEMP: Temporary directory for certificate file\n#   GITHUB_ENV: Path to GitHub environment file\n\n$ErrorActionPreference = 'Stop'\n\nWrite-Host \"Restoring P12 client certificate from base64...\"\n\n# Get certificate from environment variable\n$base64Certificate = $env:WINDOWS_SIGNING_P12_BASE64\nif ([string]::IsNullOrEmpty($base64Certificate)) {\n    Write-Error \"ERROR: Required environment variable WINDOWS_SIGNING_P12_BASE64 not set.\"\n    exit 1\n}\n\n# Decode base64 certificate\n$bytes = [Convert]::FromBase64String($base64Certificate)\n\n# Generate output path\n$path = Join-Path $env:RUNNER_TEMP \"sm_client_auth.p12\"\n\n# Write certificate to file\n[IO.File]::WriteAllBytes($path, $bytes)\n\n# Verify file was created\nif (Test-Path $path) {\n    Write-Host \"Certificate file created: $path\"\n    $fileInfo = Get-Item $path\n    Write-Host \"Size: $($fileInfo.Length) bytes\"\n} else {\n    Write-Error \"Failed to create certificate file\"\n    exit 1\n}\n\n# Export to GitHub environment\necho \"SM_CLIENT_CERT_FILE=$path\" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append\n\nWrite-Host \"\"\nWrite-Host \"SM_CLIENT_CERT_FILE set to: $path\"\nWrite-Host \"Certificate restored successfully\"\n"
  },
  {
    "path": ".github/scripts/release/set-permissions.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Script to set execution permissions on binaries\n# Usage: set-permissions.sh <bin-directory>\n\n# Source configuration library\n# shellcheck source=lib-release-config.sh\nsource \"$(dirname \"$0\")/lib-release-config.sh\"\n\nfunction main {\n  local -r bin_dir=\"${1:-bin}\"\n\n  if [[ ! -d \"$bin_dir\" ]]; then\n    echo \"ERROR: Directory $bin_dir does not exist\" >&2\n    exit 1\n  fi\n\n  verify_config_file\n\n  # Use pushd/popd to avoid side effects on caller's working directory\n  pushd \"$bin_dir\" || return 1\n\n  # Get list of all binaries from configuration\n  local binaries\n  mapfile -t binaries < <(get_all_binaries)\n\n  # Set execution permissions on all binaries\n  for binary in \"${binaries[@]}\"; do\n    if [[ -f \"$binary\" ]]; then\n      chmod +x \"$binary\"\n      echo \"Set +x on $binary\"\n    else\n      echo \"Warning: Binary $binary not found, skipping\" >&2\n    fi\n  done\n\n  echo \"Execution permissions set on all binaries\"\n\n  # Return to original directory\n  popd || return 1\n\n  return 0\n}\n\nmain \"$@\"\n"
  },
  {
    "path": ".github/scripts/release/sign-checksums.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Script to sign SHA256SUMS with GPG and Cosign\n# Usage: sign-checksums.sh <bin-directory>\n#\n# Environment variables:\n#   GPG_FINGERPRINT        - GPG key fingerprint for signing (required)\n#   SIGNING_GPG_PASSPHRASE - GPG key passphrase (required)\n#\n# Outputs:\n#   SHA256SUMS.gpgsig - GPG detached signature\n#   SHA256SUMS.sigstore.json - Cosign sigstore bundle\n#   SHA256SUMS.sig           - Cosign signature (legacy, extracted from bundle)\n#   SHA256SUMS.pem           - Cosign certificate (legacy, extracted from bundle)\n\nfunction main {\n  local -r bin_dir=\"${1:-bin}\"\n\n  if [[ ! -d \"$bin_dir\" ]]; then\n    echo \"ERROR: Directory $bin_dir does not exist\" >&2\n    exit 1\n  fi\n\n  if [[ -z \"${GPG_FINGERPRINT}\" ]]; then\n    echo \"ERROR: GPG_FINGERPRINT environment variable is not set\" >&2\n    exit 1\n  fi\n\n  if [[ -z \"${SIGNING_GPG_PASSPHRASE}\" ]]; then\n    echo \"ERROR: SIGNING_GPG_PASSPHRASE environment variable is not set\" >&2\n    exit 1\n  fi\n\n  # Use pushd/popd to avoid side effects on caller's working directory\n  pushd \"$bin_dir\" || exit 1\n\n  if [[ ! -f \"SHA256SUMS\" ]]; then\n    echo \"ERROR: SHA256SUMS file not found in $bin_dir\" >&2\n    popd || exit 1\n    exit 1\n  fi\n\n  # GPG signing\n  echo \"Signing SHA256SUMS with GPG...\"\n  gpg --batch --yes -u \"${GPG_FINGERPRINT}\" \\\n      --pinentry-mode loopback \\\n      --passphrase \"${SIGNING_GPG_PASSPHRASE}\" \\\n      --output SHA256SUMS.gpgsig \\\n      --detach-sign SHA256SUMS\n\n  echo \"GPG signature created: SHA256SUMS.gpgsig\"\n\n  # Cosign signing (keyless OIDC) - produces sigstore bundle\n  echo \"Signing SHA256SUMS with Cosign...\"\n  cosign sign-blob SHA256SUMS \\\n      --bundle=SHA256SUMS.sigstore.json \\\n      --yes\n\n  echo \"Cosign bundle created: SHA256SUMS.sigstore.json\"\n\n  # Extract legacy .sig and .pem from bundle for backward compatibility\n  echo \"Extracting legacy signature files from bundle...\"\n  jq -r '.messageSignature.signature' SHA256SUMS.sigstore.json > SHA256SUMS.sig\n  jq -r '.verificationMaterial.certificate.rawBytes' SHA256SUMS.sigstore.json | \\\n      base64 --decode | \\\n      openssl x509 -inform DER -outform PEM -out SHA256SUMS.pem\n\n  echo \"Cosign signature created: SHA256SUMS.sig\"\n  echo \"Cosign certificate created: SHA256SUMS.pem\"\n\n  echo \"\"\n  echo \"All signatures generated successfully:\"\n  ls -la SHA256SUMS*\n\n  popd || exit 1\n\n  return 0\n}\n\nmain \"$@\"\n"
  },
  {
    "path": ".github/scripts/release/sign-macos-binaries.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Script to sign macOS binaries using gon and Apple notarization\n# Usage: sign-macos-binaries.sh <bin-dir>\n# Environment variables:\n#   AC_PASSWORD: Apple Connect password\n#   AC_PROVIDER: Apple Connect provider\n#   AC_USERNAME: Apple Connect username\n#   MACOS_CERTIFICATE: macOS certificate in P12 format (base64 encoded)\n#   MACOS_CERTIFICATE_PASSWORD: Certificate password\n\nfunction main {\n  local -r bin_dir=\"${1:-bin}\"\n\n  # Validate required environment variables\n  : \"${AC_PASSWORD:?ERROR: AC_PASSWORD is a required environment variable}\"\n  : \"${AC_PROVIDER:?ERROR: AC_PROVIDER is a required environment variable}\"\n  : \"${AC_USERNAME:?ERROR: AC_USERNAME is a required environment variable}\"\n  : \"${MACOS_CERTIFICATE:?ERROR: MACOS_CERTIFICATE is a required environment variable}\"\n  : \"${MACOS_CERTIFICATE_PASSWORD:?ERROR: MACOS_CERTIFICATE_PASSWORD is a required environment variable}\"\n\n  if [[ ! -d \"$bin_dir\" ]]; then\n    echo \"ERROR: Directory $bin_dir does not exist\" >&2\n    exit 1\n  fi\n\n  echo \"Signing macOS binaries...\"\n\n  # Sign amd64 binary\n  echo \"Signing amd64 binary...\"\n  .github/scripts/setup/mac-sign.sh .gon_amd64.hcl\n\n  # Sign arm64 binary\n  echo \"Signing arm64 binary...\"\n  .github/scripts/setup/mac-sign.sh .gon_arm64.hcl\n\n  echo \"Done signing the binaries\"\n\n  # Source configuration library\n  # shellcheck source=lib-release-config.sh\n  source \"$(dirname \"$0\")/lib-release-config.sh\"\n\n  verify_config_file\n\n  # Get list of macOS binaries from config (compatible with bash 3.2+)\n  local macos_binaries\n  macos_binaries=$(jq -r '.platforms[] | select(.os == \"darwin\") | .binary' \"$RELEASE_CONFIG_FILE\")\n\n  echo \"Expected macOS binaries from config: $macos_binaries\"\n\n  # Remove old unsigned binaries from bin directory\n  echo \"Removing unsigned binaries from $bin_dir...\"\n  for binary in $macos_binaries; do\n    rm -f \"$bin_dir/$binary\"\n    echo \"  Removed: $bin_dir/$binary\"\n  done\n\n  # Extract and verify signed binaries\n  echo \"\"\n  echo \"Extracting and verifying signed binaries...\"\n\n  for binary in $macos_binaries; do\n    local zip_file=\"${binary}.zip\"\n\n    echo \"Processing $binary...\"\n\n    # Check ZIP file exists\n    [[ -f \"$zip_file\" ]] || {\n      echo \"ERROR: ZIP file $zip_file not found for binary $binary\" >&2\n      exit 1\n    }\n\n    echo \"  Found $zip_file, extracting...\"\n    unzip -o \"$zip_file\"\n\n    # Check extraction succeeded\n    [[ -f \"$binary\" ]] || {\n      echo \"  ERROR: Failed to extract binary $binary from $zip_file\" >&2\n      exit 1\n    }\n\n    echo \"  Extracted binary exists, checking signature...\"\n    codesign -dv --verbose=4 \"$binary\" 2>&1 || {\n      echo \"  ERROR: Signature verification failed for binary $binary\" >&2\n      exit 1\n    }\n\n    echo \"  Signature verified\"\n    mv \"$binary\" \"$bin_dir/\"\n    echo \"  Moved signed binary to $bin_dir/\"\n\n    # Also move the ZIP file to bin directory\n    mv \"$zip_file\" \"$bin_dir/\"\n    echo \"  Moved $zip_file to $bin_dir/\"\n    echo \"\"\n  done\n\n  # Final verification of all binaries in bin directory\n  echo \"Final verification of all binaries in $bin_dir...\"\n  for binary in $macos_binaries; do\n    echo \"Verifying $binary...\"\n\n    [[ -f \"$bin_dir/$binary\" ]] || {\n      echo \"ERROR: Binary $bin_dir/$binary not found after processing\" >&2\n      exit 1\n    }\n\n    codesign -dv --verbose=4 \"$bin_dir/$binary\" || {\n      echo \"ERROR: Signature verification failed for binary $bin_dir/$binary\" >&2\n      exit 1\n    }\n  done\n\n  echo \"\"\n  echo \"All macOS binaries signed and verified successfully\"\n\n  # Show final contents of bin directory for debugging\n  echo \"\"\n  echo \"Final contents of $bin_dir directory:\"\n  ls -lah \"$bin_dir/\"\n\n  return 0\n}\n\nmain \"$@\"\n"
  },
  {
    "path": ".github/scripts/release/sign-windows.ps1",
    "content": "# Script to sign Windows binaries using DigiCert and patch with go-winres\n# Usage: sign-windows.ps1 -BinDirectory <path>\n# Environment variables:\n#   GITHUB_REF_NAME: Git ref name (e.g., v0.93.4 or beta-2025111001)\n#   SM_HOST: DigiCert host\n#   SM_API_KEY: DigiCert API key\n#   SM_CLIENT_CERT_FILE: Path to P12 certificate file\n#   SM_CLIENT_CERT_PASSWORD: Certificate password\n#   SM_KEYPAIR_ALIAS: DigiCert keypair alias\n\nparam(\n    [Parameter(Mandatory=$false)]\n    [string]$BinDirectory = \"bin\"\n)\n\n$ErrorActionPreference = 'Stop'\n\n# Path to centralized configuration\n$ConfigFile = \".github/assets/release-assets-config.json\"\n\nfunction Assert-EnvVar {\n    param([string]$Name)\n\n    $value = [Environment]::GetEnvironmentVariable($Name)\n    if ([string]::IsNullOrEmpty($value)) {\n        Write-Error \"ERROR: Required environment variable $Name not set.\"\n        exit 1\n    }\n}\n\nfunction Get-WindowsPlatforms {\n    # Read configuration file\n    if (-not (Test-Path $ConfigFile)) {\n        Write-Error \"Configuration file not found: $ConfigFile\"\n        exit 1\n    }\n\n    Write-Host \"Reading configuration from: $ConfigFile\"\n    $config = Get-Content $ConfigFile -Raw | ConvertFrom-Json\n\n    # Filter Windows platforms\n    $windowsPlatforms = $config.platforms | Where-Object { $_.os -eq \"windows\" }\n\n    if ($windowsPlatforms.Count -eq 0) {\n        Write-Error \"No Windows platforms found in configuration\"\n        exit 1\n    }\n\n    Write-Host \"Found $($windowsPlatforms.Count) Windows platform(s) in configuration\"\n    return $windowsPlatforms\n}\n\nfunction Update-WinresVersion {\n    # Get version from git ref\n    $rawVersion = $env:GITHUB_REF_NAME\n    if ([string]::IsNullOrEmpty($rawVersion) -or $rawVersion -eq \"refs/heads/main\") {\n        $rawVersion = \"0.0.0-dev\"\n    }\n\n    Write-Host \"Raw version from git ref: $rawVersion\"\n\n    # Parse version based on tag format\n    $version = \"\"\n    $major = \"0\"\n    $minor = \"0\"\n    $patch = \"0\"\n    $build = \"0\"\n\n    if ($rawVersion -match '^v(\\d+)\\.(\\d+)\\.(\\d+)') {\n        # Standard version tag: v0.93.4\n        $major = $matches[1]\n        $minor = $matches[2]\n        $patch = $matches[3]\n        $version = \"$major.$minor.$patch\"\n        Write-Host \"Detected standard version tag: v$version\"\n    }\n    elseif ($rawVersion -match '^([a-zA-Z0-9_-]+)-(\\d{4})(\\d{2})(\\d{2})(\\d{2})') {\n        # Generic pre-release tag: <prefix>-YYYYMMDDNN\n        # Examples: beta-2025111001, alpha-2025110301, rc-2025120101, dev-2025110501\n        # Extract prefix and date components: prefix-YYYY-MM-DD-build\n        $prefix = $matches[1]\n        $year = $matches[2]\n        $month = $matches[3]\n        $day = $matches[4]\n        $buildNum = $matches[5]\n\n        # Windows FileVersion components are limited to 65535\n        # Use format: YYYY.MMDD.NN.0 (all components within limits)\n        $major = $year\n        $minor = \"$month$day\"\n        $patch = $buildNum\n        $version = \"$prefix-$year$month$day$buildNum\"\n        Write-Host \"Detected pre-release tag: $version (Prefix: $prefix, FileVersion will be $year.$month$day.$buildNum.0)\"\n    }\n    elseif ($rawVersion -match '^\\d+\\.\\d+') {\n        # Version without 'v' prefix\n        $version = $rawVersion\n        $versionParts = $version.Split('.')\n        $major = if ($versionParts.Length -gt 0) { $versionParts[0] } else { \"0\" }\n        $minor = if ($versionParts.Length -gt 1) { $versionParts[1] } else { \"0\" }\n        $patch = if ($versionParts.Length -gt 2) { $versionParts[2].Split('-')[0] } else { \"0\" }\n        Write-Host \"Detected version without prefix: $version\"\n    }\n    else {\n        # Branch name or dev version\n        $major = \"0\"\n        $minor = \"0\"\n        $patch = \"0\"\n        $version = \"0.0.0-dev\"\n        Write-Host \"Using dev version: $version\"\n    }\n\n    $fileVersion = \"$major.$minor.$patch.0\"\n    $copyrightYear = (Get-Date).Year\n\n    Write-Host \"Final version: $version\"\n    Write-Host \"File version (for Windows): $fileVersion\"\n    Write-Host \"\"\n\n    # Generate winres.json dynamically\n    Write-Host \"Generating winres.json...\"\n    $winresConfig = @{\n        RT_GROUP_ICON = @{\n            APP = @{\n                \"0409\" = \".github/assets/terragrunt.png\"\n            }\n        }\n        RT_MANIFEST = @{\n            \"#1\" = @{\n                \"0409\" = @{\n                    assembly = @{\n                        identity = @{\n                            name = \"Terragrunt\"\n                            version = $fileVersion\n                        }\n                        description = \"Terragrunt - Orchestrate OpenTofu and Terraform at Scale\"\n                    }\n                    compatibility = @{\n                        application = @(\n                            @{\n                                supportedOS = @{\n                                    Id = \"{e2011457-1546-43c5-a5fe-008deee3d3f0}\"\n                                    comment = \"Windows Vista / Windows Server 2008\"\n                                }\n                            },\n                            @{\n                                supportedOS = @{\n                                    Id = \"{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\"\n                                    comment = \"Windows 7 / Windows Server 2008 R2\"\n                                }\n                            },\n                            @{\n                                supportedOS = @{\n                                    Id = \"{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\"\n                                    comment = \"Windows 8 / Windows Server 2012\"\n                                }\n                            },\n                            @{\n                                supportedOS = @{\n                                    Id = \"{1f676c76-80e1-4239-95bb-83d0f6d0da78}\"\n                                    comment = \"Windows 8.1 / Windows Server 2012 R2\"\n                                }\n                            },\n                            @{\n                                supportedOS = @{\n                                    Id = \"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\"\n                                    comment = \"Windows 10, Windows 11 / Windows Server 2016, 2019, 2022\"\n                                }\n                            }\n                        )\n                    }\n                    dpiAwareness = \"PerMonitorV2, PerMonitor\"\n                }\n            }\n        }\n        RT_VERSION = @{\n            \"#1\" = @{\n                \"0409\" = @{\n                    fixed = @{\n                        file_version = $fileVersion\n                        product_version = $fileVersion\n                    }\n                    info = @{\n                        \"0409\" = @{\n                            Comments = \"Standardize IaC and manage growing infra complexity: define units, stacks, cut repetition with includes/hooks, execute modules in dependency order across environments\"\n                            CompanyName = \"Gruntwork, Inc.\"\n                            FileDescription = \"Terragrunt - Orchestrate OpenTofu and Terraform at Scale\"\n                            FileVersion = $version\n                            InternalName = \"terragrunt\"\n                            LegalCopyright = \"Copyright (C) $copyrightYear Gruntwork, Inc.\"\n                            OriginalFilename = \"terragrunt.exe\"\n                            ProductName = \"Terragrunt\"\n                            ProductVersion = $version\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    # Write winres.json to current directory\n    $jsonOutput = $winresConfig | ConvertTo-Json -Depth 10 -Compress:$false\n    [System.IO.File]::WriteAllText(\"winres.json\", $jsonOutput)\n\n    Write-Host \"Generated winres.json:\"\n    Get-Content winres.json\n\n    # Verify icon file exists\n    Write-Host \"\"\n    Write-Host \"Verifying icon file...\"\n    if (Test-Path \".github/assets/terragrunt.png\") {\n        Write-Host \"Icon file exists: .github/assets/terragrunt.png\"\n        $iconInfo = Get-Item \".github/assets/terragrunt.png\"\n        Write-Host \"Icon size: $($iconInfo.Length) bytes\"\n    } else {\n        Write-Error \"Icon file not found: .github/assets/terragrunt.png\"\n        exit 1\n    }\n}\n\nfunction Patch-Binaries {\n    param([array]$Platforms)\n\n    # Add Go bin to PATH\n    $goPath = & go env GOPATH\n    $goBinPath = Join-Path $goPath \"bin\"\n    $env:PATH = \"$goBinPath;$env:PATH\"\n\n    Write-Host \"Patching Windows binaries with icon and version info...\"\n\n    foreach ($platform in $Platforms) {\n        $binaryPath = Join-Path $BinDirectory $platform.binary\n\n        if (Test-Path $binaryPath) {\n            Write-Host \"Patching $($platform.binary) ($($platform.arch))...\"\n            & go-winres patch --in winres.json $binaryPath\n\n            if ($LASTEXITCODE -ne 0) {\n                Write-Error \"Failed to patch $($platform.binary)\"\n                exit 1\n            }\n\n            Write-Host \"Successfully patched $($platform.binary)\"\n        } else {\n            Write-Error \"Binary not found: $binaryPath\"\n            exit 1\n        }\n    }\n\n    Write-Host \"All Windows binaries patched with resources\"\n}\n\nfunction Save-Credentials {\n    Write-Host \"Saving credentials to Windows Credential Manager...\"\n\n    & smctl.exe credentials save $env:SM_API_KEY $env:SM_CLIENT_CERT_PASSWORD\n\n    if ($LASTEXITCODE -ne 0) {\n        Write-Error \"Failed to save credentials\"\n        exit 1\n    }\n\n    Write-Host \"Credentials saved to Windows Credential Manager\"\n}\n\nfunction Invoke-Healthcheck {\n    Write-Host \"Running smctl healthcheck...\"\n\n    & smctl.exe healthcheck\n\n    if ($LASTEXITCODE -ne 0) {\n        Write-Warning \"Healthcheck failed (exit code: $LASTEXITCODE)\"\n        Write-Warning \"Continuing anyway - signing step will be the real test\"\n    } else {\n        Write-Host \"Healthcheck passed\"\n    }\n}\n\nfunction Sync-Certificates {\n    Write-Host \"Syncing certificates from DigiCert KeyLocker...\"\n\n    & smctl.exe windows certsync --keypair-alias \"$env:SM_KEYPAIR_ALIAS\"\n\n    if ($LASTEXITCODE -ne 0) {\n        Write-Error \"Certificate sync failed\"\n        exit 1\n    }\n\n    Write-Host \"Certificates synced to Windows store\"\n}\n\nfunction Sign-Binary {\n    param([string]$BinaryPath)\n\n    Write-Host \"Signing: $BinaryPath\"\n\n    & smctl.exe sign `\n        --keypair-alias \"$env:SM_KEYPAIR_ALIAS\" `\n        --input \"$BinaryPath\" `\n        --simple `\n        --verbose\n\n    if ($LASTEXITCODE -ne 0) {\n        Write-Error \"Signing failed for $BinaryPath\"\n        exit 1\n    }\n\n    Write-Host \"Successfully signed $BinaryPath\"\n}\n\nfunction Verify-Signature {\n    param([string]$BinaryPath)\n\n    Write-Host \"Verifying signature on: $BinaryPath\"\n\n    & smctl.exe sign verify --input \"$BinaryPath\"\n\n    if ($LASTEXITCODE -ne 0) {\n        Write-Warning \"Signature verification returned non-zero exit code (may be expected)\"\n    } else {\n        Write-Host \"Signature verified successfully\"\n    }\n}\n\nfunction Main {\n    # Verify environment variables\n    Assert-EnvVar \"SM_HOST\"\n    Assert-EnvVar \"SM_API_KEY\"\n    Assert-EnvVar \"SM_CLIENT_CERT_FILE\"\n    Assert-EnvVar \"SM_CLIENT_CERT_PASSWORD\"\n    Assert-EnvVar \"SM_KEYPAIR_ALIAS\"\n    Assert-EnvVar \"GITHUB_REF_NAME\"\n\n    if (-not (Test-Path $BinDirectory)) {\n        Write-Error \"Directory $BinDirectory does not exist\"\n        exit 1\n    }\n\n    # Get Windows platforms from configuration\n    $windowsPlatforms = Get-WindowsPlatforms\n\n    # Update winres.json with version info\n    Update-WinresVersion\n\n    # Patch all Windows binaries with resources (icon, manifest, version info)\n    Patch-Binaries -Platforms $windowsPlatforms\n\n    # Save credentials\n    Save-Credentials\n\n    # Run healthcheck\n    Invoke-Healthcheck\n\n    # Sync certificates\n    Sync-Certificates\n\n    # Sign binaries based on configuration\n    Write-Host \"\"\n    Write-Host \"Processing Windows binaries for signing...\"\n    Write-Host \"\"\n\n    $signedCount = 0\n    $unsignedCount = 0\n\n    foreach ($platform in $windowsPlatforms) {\n        $binaryPath = Join-Path $BinDirectory $platform.binary\n\n        if (-not (Test-Path $binaryPath)) {\n            Write-Error \"Binary not found: $binaryPath\"\n            exit 1\n        }\n\n        if ($platform.signed -eq $true) {\n            Write-Host \"Signing $($platform.binary) ($($platform.arch))...\"\n            Sign-Binary -BinaryPath $binaryPath\n\n            Write-Host \"Verifying signature on $($platform.binary)...\"\n            Verify-Signature -BinaryPath $binaryPath\n\n            Write-Host \"✓ $($platform.binary): signed and verified\"\n            $signedCount++\n        } else {\n            Write-Host \"○ $($platform.binary) ($($platform.arch)): patched with resources only (not signed per config)\"\n            $unsignedCount++\n        }\n        Write-Host \"\"\n    }\n\n    Write-Host \"Windows processing completed successfully:\"\n    Write-Host \"  - Signed: $signedCount binary(ies)\"\n    Write-Host \"  - Patched only: $unsignedCount binary(ies)\"\n    Write-Host \"\"\n    Write-Host \"All decisions based on configuration: $ConfigFile\"\n}\n\nMain\n"
  },
  {
    "path": ".github/scripts/release/upload-assets.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Script to upload release assets to GitHub\n# Usage: upload-assets.sh <bin-directory>\n# Environment variables:\n#   VERSION: The version/tag to upload to\n#   GH_TOKEN: GitHub token for authentication\n#   CLOBBER: Set to 'true' to overwrite existing assets (default: false)\n\nfunction main {\n  local -r bin_dir=\"${1:-bin}\"\n  local -r clobber=\"${CLOBBER:-false}\"\n\n  # Validate required environment variables\n  : \"${VERSION:?ERROR: VERSION is a required environment variable}\"\n  : \"${GH_TOKEN:?ERROR: GH_TOKEN is a required environment variable}\"\n\n  if [[ ! -d \"$bin_dir\" ]]; then\n    echo \"ERROR: Directory $bin_dir does not exist\" >&2\n    exit 1\n  fi\n\n  # Build upload command with optional --clobber flag\n  local clobber_flag=\"\"\n  if [[ \"$clobber\" == \"true\" ]]; then\n    clobber_flag=\"--clobber\"\n    echo \"Note: --clobber enabled - will overwrite existing assets\"\n  else\n    echo \"Note: --clobber disabled - will fail if assets already exist\"\n  fi\n\n  printf 'Uploading assets to existing release %s...\\n' \"$VERSION\"\n\n  # Use pushd/popd to avoid side effects on caller's working directory\n  pushd \"$bin_dir\" || return 1\n\n  # Upload all files using gh CLI\n  for file in *; do\n    echo \"Uploading $file...\"\n    if gh release upload \"$VERSION\" \"$file\" $clobber_flag; then\n      echo \"Uploaded $file\"\n    else\n      echo \"Upload failed for $file (will retry in verification)\" >&2\n    fi\n  done\n\n  # Return to original directory\n  popd || return 1\n\n  echo \"Upload phase completed\"\n\n  return 0\n}\n\nmain \"$@\"\n"
  },
  {
    "path": ".github/scripts/release/verify-assets-uploaded.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Script to verify all assets were uploaded to the GitHub release\n# Usage: verify-assets-uploaded.sh <bin-directory>\n# Environment variables:\n#   VERSION: The version/tag to verify\n#   GH_TOKEN: GitHub token for authentication\n#   CLOBBER: Set to 'true' to overwrite existing assets during retry (default: false)\n\n# Source configuration library\n# shellcheck source=lib-release-config.sh\nsource \"$(dirname \"$0\")/lib-release-config.sh\"\n\nreadonly MAX_RETRIES=10\n\nfunction main {\n  local -r bin_dir=\"${1:-bin}\"\n  local -r clobber=\"${CLOBBER:-false}\"\n\n  # Validate required environment variables\n  : \"${VERSION:?ERROR: VERSION is a required environment variable}\"\n  : \"${GH_TOKEN:?ERROR: GH_TOKEN is a required environment variable}\"\n  verify_config_file\n\n  # Build upload command with optional --clobber flag\n  local clobber_flag=\"\"\n  if [[ \"$clobber\" == \"true\" ]]; then\n    clobber_flag=\"--clobber\"\n  fi\n\n  echo \"Verifying all assets are accessible...\"\n\n  # Get list of assets in the release\n  local assets\n  assets=$(gh release view \"$VERSION\" --json 'assets' --jq '.assets[].name')\n\n  local asset_count\n  asset_count=$(wc -l <<< \"$assets\")\n\n  echo \"Found $asset_count assets in release\"\n\n  # Get expected files from centralized configuration\n  local expected_files\n  mapfile -t expected_files < <(get_all_expected_files)\n\n  # Check each expected file\n  for expected_file in \"${expected_files[@]}\"; do\n    echo \"Checking $expected_file...\"\n\n    # Check if file exists in release\n    if ! grep -q \"^${expected_file}$\" <<< \"$assets\"; then\n      echo \"$expected_file not found in release, uploading...\"\n\n      # Upload the missing file\n      if [[ -f \"$bin_dir/$expected_file\" ]]; then\n        local i\n        for ((i=0; i<MAX_RETRIES; i++)); do\n          if gh release upload \"$VERSION\" \"$bin_dir/$expected_file\" $clobber_flag; then\n            echo \"Uploaded $expected_file\"\n            break\n          fi\n\n          echo \"Upload attempt $((i+1))/$MAX_RETRIES failed\" >&2\n          sleep 5\n        done\n\n        if (( i == MAX_RETRIES )); then\n          echo \"Failed to upload $expected_file after $MAX_RETRIES retries\" >&2\n          exit 1\n        fi\n      else\n        echo \"File $bin_dir/$expected_file not found locally\" >&2\n        exit 1\n      fi\n    else\n      echo \"$expected_file present\"\n    fi\n  done\n\n  # Verify we can download assets (spot check)\n  echo \"\"\n  echo \"Verifying asset downloads (spot check)...\"\n  local download_url\n  download_url=$(gh release view \"$VERSION\" --json 'assets' --jq '.assets[0].url')\n\n  if curl -sILf \"$download_url\" > /dev/null; then\n    echo \"Assets are downloadable\"\n  else\n    echo \"Warning: Could not verify asset download URL\" >&2\n  fi\n\n  local expected_count\n  expected_count=$(get_total_file_count)\n  local binary_count\n  binary_count=$(get_binary_count)\n\n  echo \"\"\n  echo \"All required assets verified!\"\n  echo \"Expected files: $expected_count ($binary_count binaries + archives + checksums)\"\n  echo \"Actual files: $asset_count\"\n\n  if [[ \"$asset_count\" -lt \"$expected_count\" ]]; then\n    echo \"Warning: Expected $expected_count files, found $asset_count\" >&2\n  fi\n\n  return 0\n}\n\nmain \"$@\"\n"
  },
  {
    "path": ".github/scripts/release/verify-binaries-downloaded.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Script to verify all expected binaries were downloaded\n# Usage: verify-binaries-downloaded.sh <bin-directory> [expected-count]\n\n# Source configuration library\n# shellcheck source=lib-release-config.sh\nsource \"$(dirname \"$0\")/lib-release-config.sh\"\n\nfunction resolve_expected_count {\n  local -r count_override=\"$1\"\n\n  # If override provided, use it; otherwise get from config\n  if [[ -n \"$count_override\" ]]; then\n    echo \"$count_override\"\n    return 0\n  fi\n\n  verify_config_file\n  get_binary_count\n\n  return 0\n}\n\nfunction main {\n  local -r bin_dir=\"${1:-bin}\"\n  local -r count_override=\"${2:-}\"\n\n  local expected_count\n  expected_count=$(resolve_expected_count \"$count_override\")\n\n  if [[ ! -d \"$bin_dir\" ]]; then\n    echo \"ERROR: Directory $bin_dir does not exist\" >&2\n    exit 1\n  fi\n\n  # Count binaries first\n  local binary_count\n  binary_count=$(find \"$bin_dir/\" -type f | wc -l)\n\n  # List binaries if any exist (resilient to empty directory)\n  if [[ \"$binary_count\" -gt 0 ]]; then\n    echo \"Downloaded binaries:\"\n    ls -lahrt \"$bin_dir\"/*\n  else\n    echo \"No binaries found in $bin_dir\"\n  fi\n\n  echo \"Total binaries: $binary_count\"\n\n  # Verify expected count\n  echo \"Expected: at least $expected_count binaries\"\n\n  if [[ \"$binary_count\" -lt \"$expected_count\" ]]; then\n    echo \"ERROR: Expected at least $expected_count binaries, found $binary_count\" >&2\n    exit 1\n  fi\n\n  echo \"All binaries present ($binary_count files)\"\n\n  return 0\n}\n\nmain \"$@\"\n"
  },
  {
    "path": ".github/scripts/release/verify-files.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Script to verify all required files are present before upload\n# Usage: verify-files.sh <bin-directory>\n\n# Source configuration library\n# shellcheck source=lib-release-config.sh\nsource \"$(dirname \"$0\")/lib-release-config.sh\"\n\nfunction main {\n  local -r bin_dir=\"${1:-bin}\"\n\n  if [[ ! -d \"$bin_dir\" ]]; then\n    echo \"ERROR: Directory $bin_dir does not exist\" >&2\n    exit 1\n  fi\n\n  verify_config_file\n\n  echo \"Verifying required files...\"\n\n  # Get all binaries from configuration\n  local binaries\n  mapfile -t binaries < <(get_all_binaries)\n\n  # Check each binary\n  for file in \"${binaries[@]}\"; do\n    if [[ -f \"$bin_dir/$file\" ]]; then\n      echo \"$file present\"\n    else\n      echo \"$file missing\" >&2\n      exit 1\n    fi\n  done\n\n  # Check additional files from configuration\n  local additional_files\n  mapfile -t additional_files < <(get_additional_files)\n\n  for file in \"${additional_files[@]}\"; do\n    if [[ -f \"$bin_dir/$file\" ]]; then\n      echo \"$file present\"\n    else\n      echo \"$file missing\" >&2\n      exit 1\n    fi\n  done\n\n  echo \"All required files verified\"\n\n  return 0\n}\n\nmain \"$@\"\n"
  },
  {
    "path": ".github/scripts/release/verify-smctl.ps1",
    "content": "# Script to verify smctl installation\n# Usage: verify-smctl.ps1\n\n$ErrorActionPreference = 'Stop'\n\nWrite-Host \"Checking smctl installation...\"\n\n# Check if smctl is in PATH\n$smctlPath = Get-Command smctl.exe -ErrorAction SilentlyContinue\n\nif (-not $smctlPath) {\n    Write-Error \"smctl.exe not found in PATH\"\n    exit 1\n}\n\nWrite-Host \"smctl found at: $($smctlPath.Source)\"\n\nWrite-Host \"Checking smctl version...\"\n\n# Capture stderr and stdout\n$output = & smctl.exe --version 2>&1\n\n# Check exit code\nif ($LASTEXITCODE -ne 0) {\n    Write-Error \"smctl --version failed with exit code $LASTEXITCODE. Output: $output\"\n    exit $LASTEXITCODE\n}\n\n# Display output if successful\nWrite-Host $output\n\nWrite-Host \"\"\nWrite-Host \"smctl is ready\"\n"
  },
  {
    "path": ".github/scripts/release/verify-static-binary.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Script to verify that binaries are statically linked\n# Usage: verify-static-binary.sh <binary-path> <os> <arch>\n\ndie() {\n  echo \"ERROR: $*\" >&2\n  exit 1\n}\n\nrequire_arg() {\n  local -r value=\"$1\"\n  local -r name=\"$2\"\n\n  [[ -n \"$value\" ]] || die \"$name is required\"\n\n  return 0\n}\n\nexpect_empty() {\n  local -r value=\"$1\"\n  local -r message=\"$2\"\n\n  [[ -z \"$value\" ]] || {\n    echo \"$message\" >&2\n    echo \"$value\" >&2\n    exit 1\n  }\n\n  return 0\n}\n\nverify_linux_binary() {\n  local -r binary=\"$1\"\n  local -r file_info=\"$2\"\n\n  echo \"Verifying Linux binary is statically linked...\"\n  echo \"$file_info\"\n\n  grep -q \"statically linked\" <<<\"$file_info\"\n  echo \"Linux binary is statically linked\"\n\n  # Verify with ldd - it should either say \"not a dynamic executable\" or \"statically linked\"\n  local ldd_output\n  ldd_output=$(ldd \"$binary\" 2>&1 || true)\n\n  grep -qE \"not a dynamic executable|statically linked\" <<<\"$ldd_output\"\n  echo \"Linux binary has no dynamic dependencies\"\n\n  return 0\n}\n\nverify_darwin_binary() {\n  local -r binary=\"$1\"\n  local -r file_info=\"$2\"\n\n  echo \"Verifying macOS binary...\"\n  echo \"$file_info\"\n\n  grep -q \"Mach-O.*executable\" <<<\"$file_info\"\n  echo \"macOS binary is Mach-O executable\"\n\n  return 0\n}\n\nverify_windows_binary() {\n  local -r binary=\"$1\"\n  local -r file_info=\"$2\"\n\n  echo \"Verifying Windows binary...\"\n  echo \"$file_info\"\n\n  grep -q \"PE32.*executable.*Windows\" <<<\"$file_info\"\n  echo \"Windows binary is PE32 executable\"\n\n  local unexpected_dlls\n  unexpected_dlls=$(\n    objdump -p \"$binary\" 2>/dev/null |\n      grep -i \"DLL Name\" |\n      grep -vi \"KERNEL32.dll\\|msvcrt.dll\\|WS2_32.dll\\|ADVAPI32.dll\\|SHELL32.dll\\|ole32.dll\" || true\n  )\n\n  expect_empty \"$unexpected_dlls\" \"Windows binary links to unexpected DLLs:\"\n  echo \"Windows binary has standard system DLL dependencies only\"\n\n  return 0\n}\n\nmain() {\n  local -r binary=\"${1:-}\"\n  local -r os=\"${2:-}\"\n  local -r arch=\"${3:-}\"\n\n  require_arg \"$binary\" \"binary path\"\n  require_arg \"$os\" \"os\"\n  require_arg \"$arch\" \"arch\"\n\n  [[ -f \"$binary\" ]] || die \"Binary $binary does not exist\"\n\n  echo \"Verifying static linking for $binary ($os/$arch)...\"\n  local file_info\n  file_info=$(file \"$binary\")\n\n  case \"$os\" in\n    linux)\n      verify_linux_binary \"$binary\" \"$file_info\"\n      ;;\n    darwin)\n      verify_darwin_binary \"$binary\" \"$file_info\"\n      ;;\n    windows)\n      verify_windows_binary \"$binary\" \"$file_info\"\n      ;;\n    *)\n      die \"Unsupported OS: $os\"\n      ;;\n  esac\n\n  echo \"Static linking verification passed for $os/$arch!\"\n\n  return 0\n}\n\nmain \"$@\"\n"
  },
  {
    "path": ".github/scripts/setup/cas.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n: \"${ENV_FILE:?ENV_FILE is not set}\"\n\ntouch \"$ENV_FILE\"\n\nprintf \"export TG_EXPERIMENT='%s'\\n\" \"cas\" >> \"$ENV_FILE\"\n"
  },
  {
    "path": ".github/scripts/setup/engine.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\nexport TOFU_ENGINE_VERSION=\"v0.0.20\"\nexport REPO=\"gruntwork-io/terragrunt-engine-opentofu\"\nexport ASSET_NAME=\"terragrunt-iac-engine-opentofu_rpc_${TOFU_ENGINE_VERSION}_linux_amd64.zip\"\npushd .\n# Download the engine binary\nmkdir -p /tmp/engine\ncd /tmp/engine\nwget -O \"engine.zip\" \"https://github.com/${REPO}/releases/download/${TOFU_ENGINE_VERSION}/${ASSET_NAME}\"\nunzip -o \"engine.zip\"\npopd\n"
  },
  {
    "path": ".github/scripts/setup/experiment-mode.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n: \"${ENV_FILE:?ENV_FILE is not set}\"\n\ntouch \"$ENV_FILE\"\n\nprintf \"export TG_EXPERIMENT_MODE=%s\\n\" \"true\" >> \"$ENV_FILE\"\n"
  },
  {
    "path": ".github/scripts/setup/gcp.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n: \"${ENV_FILE:?ENV_FILE is not set}\"\n\ntouch \"$ENV_FILE\"\n\necho \"$GCLOUD_SERVICE_KEY\" > \"${HOME}/gcloud-service-key.json\"\nexport GOOGLE_APPLICATION_CREDENTIALS=\"${HOME}/gcloud-service-key.json\"\nprintf \"export GOOGLE_APPLICATION_CREDENTIALS='%s'\\n\" \"${HOME}/gcloud-service-key.json\" >> \"$ENV_FILE\"\n\n# Save gcloud commands to ENV_FILE\nprintf \"gcloud auth activate-service-account --key-file=\\\"%s\\\" --quiet\\n\" \"${HOME}/gcloud-service-key.json\" >> \"$ENV_FILE\"\nprintf \"gcloud config set project '%s'\\n\" \"${GOOGLE_PROJECT_ID}\" >> \"$ENV_FILE\"\n\nprintf \"export GOOGLE_CLOUD_PROJECT='%s'\\n\" \"${GOOGLE_PROJECT_ID}\" >> \"$ENV_FILE\"\n"
  },
  {
    "path": ".github/scripts/setup/generate-mocks.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\nmake generate-mocks\n"
  },
  {
    "path": ".github/scripts/setup/generate-secrets.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Required environment variables\n: \"${NAME:?NAME is not set}\"\n: \"${ENV_FILE:?ENV_FILE is not set}\"\n: \"${GITHUB_WORKSPACE:?GITHUB_WORKSPACE is not set}\"\n: \"${GHA_DEPLOY_KEY:?GHA_DEPLOY_KEY is not set}\"\n\n: \"${AWS_ACCESS_KEY_ID:?AWS_ACCESS_KEY_ID is not set}\"\n: \"${AWS_SECRET_ACCESS_KEY:?AWS_SECRET_ACCESS_KEY is not set}\"\n: \"${AWS_TEST_S3_ASSUME_ROLE:?AWS_TEST_S3_ASSUME_ROLE is not set}\"\n: \"${AWS_TEST_OIDC_ROLE_ARN:?AWS_TEST_OIDC_ROLE_ARN is not set}\"\n\n: \"${GCLOUD_SERVICE_KEY:?GCLOUD_SERVICE_KEY is not set}\"\n: \"${GOOGLE_CLOUD_PROJECT:?GOOGLE_CLOUD_PROJECT is not set}\"\n: \"${GOOGLE_COMPUTE_ZONE:?GOOGLE_COMPUTE_ZONE is not set}\"\n: \"${GOOGLE_IDENTITY_EMAIL:?GOOGLE_IDENTITY_EMAIL is not set}\"\n: \"${GOOGLE_PROJECT_ID:?GOOGLE_PROJECT_ID is not set}\"\n: \"${GCLOUD_SERVICE_KEY_IMPERSONATOR:?GCLOUD_SERVICE_KEY_IMPERSONATOR is not set}\"\n\n# Optional environment variables\nSECRETS=\"${SECRETS:-}\"\n\ntouch \"$ENV_FILE\"\n\n# Manually export each secret listed in matrix.integration.secrets\nfor SECRET in $SECRETS; do\n    if [[ \"$SECRET\" == \"GHA_DEPLOY_KEY\" && -n \"${GHA_DEPLOY_KEY}\" ]]; then\n        printf \"export GHA_DEPLOY_KEY='%s'\\n\" \"${GHA_DEPLOY_KEY}\" >> \"$ENV_FILE\"\n    elif [[ \"$SECRET\" == \"AWS_ACCESS_KEY_ID\" && -n \"${AWS_ACCESS_KEY_ID}\" ]]; then\n        printf \"export AWS_ACCESS_KEY_ID='%s'\\n\" \"${AWS_ACCESS_KEY_ID}\" >> \"$ENV_FILE\"\n    elif [[ \"$SECRET\" == \"AWS_SECRET_ACCESS_KEY\" && -n \"${AWS_SECRET_ACCESS_KEY}\" ]]; then\n        printf \"export AWS_SECRET_ACCESS_KEY='%s'\\n\" \"${AWS_SECRET_ACCESS_KEY}\" >> \"$ENV_FILE\"\n    elif [[ \"$SECRET\" == \"GCLOUD_SERVICE_KEY\" && -n \"${GCLOUD_SERVICE_KEY}\" ]]; then\n        printf \"export GCLOUD_SERVICE_KEY='%s'\\n\" \"${GCLOUD_SERVICE_KEY}\" >> \"$ENV_FILE\"\n        printf \"export GOOGLE_SERVICE_ACCOUNT_JSON='%s'\\n\" \"${GCLOUD_SERVICE_KEY}\" >> \"$ENV_FILE\"\n    elif [[ \"$SECRET\" == \"GOOGLE_CLOUD_PROJECT\" && -n \"${GOOGLE_CLOUD_PROJECT}\" ]]; then\n        printf \"export GOOGLE_CLOUD_PROJECT='%s'\\n\" \"${GOOGLE_CLOUD_PROJECT}\" >> \"$ENV_FILE\"\n    elif [[ \"$SECRET\" == \"GOOGLE_COMPUTE_ZONE\" && -n \"${GOOGLE_COMPUTE_ZONE}\" ]]; then\n        printf \"export GOOGLE_COMPUTE_ZONE='%s'\\n\" \"${GOOGLE_COMPUTE_ZONE}\" >> \"$ENV_FILE\"\n    elif [[ \"$SECRET\" == \"GOOGLE_IDENTITY_EMAIL\" && -n \"${GOOGLE_IDENTITY_EMAIL}\" ]]; then\n        printf \"export GOOGLE_IDENTITY_EMAIL='%s'\\n\" \"${GOOGLE_IDENTITY_EMAIL}\" >> \"$ENV_FILE\"\n    elif [[ \"$SECRET\" == \"GOOGLE_PROJECT_ID\" && -n \"${GOOGLE_PROJECT_ID}\" ]]; then\n        printf \"export GOOGLE_PROJECT_ID='%s'\\n\" \"${GOOGLE_PROJECT_ID}\" >> \"$ENV_FILE\"\n    elif [[ \"$SECRET\" == \"GCLOUD_SERVICE_KEY_IMPERSONATOR\" && -n \"${GCLOUD_SERVICE_KEY_IMPERSONATOR}\" ]]; then\n        printf \"export GCLOUD_SERVICE_KEY_IMPERSONATOR='%s'\\n\" \"${GCLOUD_SERVICE_KEY_IMPERSONATOR}\" >> \"$ENV_FILE\"\n    elif [[ \"$SECRET\" == \"AWS_ACCESS_KEY_ID\" && -n \"${AWS_ACCESS_KEY_ID}\" ]]; then\n        printf \"export AWS_ACCESS_KEY_ID='%s'\\n\" \"${AWS_ACCESS_KEY_ID}\" >> \"$ENV_FILE\"\n    elif [[ \"$SECRET\" == \"AWS_SECRET_ACCESS_KEY\" && -n \"${AWS_SECRET_ACCESS_KEY}\" ]]; then\n        printf \"export AWS_SECRET_ACCESS_KEY='%s'\\n\" \"${AWS_SECRET_ACCESS_KEY}\" >> \"$ENV_FILE\"\n    elif [[ \"$SECRET\" == \"AWS_TEST_S3_ASSUME_ROLE\" && -n \"${AWS_TEST_S3_ASSUME_ROLE}\" ]]; then\n        printf \"export AWS_TEST_S3_ASSUME_ROLE='%s'\\n\" \"${AWS_TEST_S3_ASSUME_ROLE}\" >> \"$ENV_FILE\"\n    elif [[ \"$SECRET\" == \"AWS_TEST_OIDC_ROLE_ARN\" && -n \"${AWS_TEST_OIDC_ROLE_ARN}\" ]]; then\n        printf \"export AWS_TEST_OIDC_ROLE_ARN='%s'\\n\" \"${AWS_TEST_OIDC_ROLE_ARN}\" >> \"$ENV_FILE\"\n    fi\ndone\n\necho \"Created environment file with secrets for $NAME\"\n"
  },
  {
    "path": ".github/scripts/setup/mac-sign.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Apple certificate used to validate developer certificates https://www.apple.com/certificateauthority/\nreadonly APPLE_ROOT_CERTIFICATE=\"http://certs.apple.com/devidg2.der\"\n\nfunction print_usage {\n  echo\n  echo \"Usage: $0 [OPTIONS] <Path to files used to sign...>\"\n  echo\n  printf '  MACOS_CERTIFICATE\\t\\tMac developer certificate in P12 format, encoded in base64.\\n'\n  printf '  MACOS_CERTIFICATE_PASSWORD\\tMac certificate password\\n'\n  echo\n  echo \"Optional Arguments:\"\n  printf '  --macos-skip-root-certificate\\t\\tSkip importing Apple Root certificate. Useful when running in already configured environment.\\n'\n  printf '  --help\\t\\t\\t\\tShow this help text and exit.\\n'\n  echo\n  echo \"Examples:\"\n  echo \"  $0 sign.hcl\"\n\n  return 0\n}\n\nfunction main {\n  local mac_skip_root_certificate=\"\"\n  local assets=()\n\n  while [[ $# -gt 0 ]]; do\n    local key=\"$1\"\n    case \"$key\" in\n      --macos-skip-root-certificate)\n        mac_skip_root_certificate=true\n        shift\n        ;;\n      --help)\n        print_usage\n        exit\n        ;;\n      -* )\n        echo \"ERROR: Unrecognized argument: $key\" >&2\n        print_usage\n        exit 1\n        ;;\n      * )\n        assets=(\"$@\")\n        break\n    esac\n  done\n  ensure_macos\n  import_certificate_mac \"${mac_skip_root_certificate}\"\n  sign_mac \"${assets[@]}\"\n\n  return 0\n}\n\nfunction ensure_macos {\n  if [[ $OSTYPE != 'darwin'* ]]; then\n    echo \"Signing of Mac binaries is supported only on MacOS\" >&2\n    exit 1\n  fi\n\n  return 0\n}\n\nfunction sign_mac {\n  local -r assets=(\"$@\")\n  local gon_cmd=\"gon\"\n  for filepath in \"${assets[@]}\"; do\n    echo \"Signing ${filepath}\"\n    \"${gon_cmd}\" -log-level=info \"${filepath}\"\n  done\n\n  return 0\n}\n\nfunction import_certificate_mac {\n  local -r mac_skip_root_certificate=\"$1\"\n  assert_env_var_not_empty \"MACOS_CERTIFICATE\"\n  assert_env_var_not_empty \"MACOS_CERTIFICATE_PASSWORD\"\n\n  trap \"rm -rf /tmp/*-keychain\" EXIT\n\n  local mac_certificate_pwd=\"${MACOS_CERTIFICATE_PASSWORD}\"\n  local keystore_pw=\"${RANDOM}\"\n\n  # create separated keychain file to store certificate and do quick cleanup of sensitive data\n  local db_file\n  db_file=$(mktemp \"/tmp/XXXXXX-keychain\")\n  rm -rf \"${db_file}\"\n  echo \"Creating separated keychain for certificate\"\n  security create-keychain -p \"${keystore_pw}\" \"${db_file}\"\n  security default-keychain -s \"${db_file}\"\n  security unlock-keychain -p \"${keystore_pw}\" \"${db_file}\"\n  echo \"${MACOS_CERTIFICATE}\" | base64 -d | security import /dev/stdin -f pkcs12 -k \"${db_file}\" -P \"${mac_certificate_pwd}\" -T /usr/bin/codesign\n  if [[ \"${mac_skip_root_certificate}\" == \"\" ]]; then\n    # download apple root certificate used as root for developer certificate\n    curl -v \"${APPLE_ROOT_CERTIFICATE}\" --output certificate.der\n    sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain certificate.der\n  fi\n  security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k \"${keystore_pw}\" \"${db_file}\"\n\n  return 0\n}\n\nfunction assert_env_var_not_empty {\n  local -r var_name=\"$1\"\n  local -r var_value=\"${!var_name}\"\n\n  if [[ -z \"$var_value\" ]]; then\n    echo \"ERROR: Required environment $var_name not set.\" >&2\n    exit 1\n  fi\n\n  return 0\n}\n\nmain \"$@\"\n\n"
  },
  {
    "path": ".github/scripts/setup/provider-cache-server.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n: \"${ENV_FILE:?ENV_FILE is not set}\"\n\ntouch \"$ENV_FILE\"\n\nprintf \"export TG_PROVIDER_CACHE='%s'\\n\" \"1\" >> \"$ENV_FILE\"\n"
  },
  {
    "path": ".github/scripts/setup/run-setup-scripts.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Required environment variables\n: \"${ENV_FILE:?ENV_FILE is not set}\"\n\n# Optional environment variables\nSETUP_SCRIPTS=\"${SETUP_SCRIPTS:-}\"\n\n# Source the environment file\n# shellcheck source=/dev/null\nsource \"${ENV_FILE}\"\n\n# Loop through setup scripts and execute them\nfor SCRIPT in $SETUP_SCRIPTS; do\n    echo \"Running setup script: $SCRIPT\"\n    \"$SCRIPT\"\n    echo \"Setup script $SCRIPT completed\"\ndone\n"
  },
  {
    "path": ".github/scripts/setup/sops.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\ngpg --import --no-tty --batch --yes ./test/fixtures/sops/test_pgp_key.asc\n"
  },
  {
    "path": ".github/scripts/setup/ssh.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\nSSH_KEY=\"${GHA_DEPLOY_KEY:?Required environment variable GHA_DEPLOY_KEY}\"\n\nmkdir -p ~/.ssh\necho \"$SSH_KEY\" > ~/.ssh/id_rsa\nchmod 600 ~/.ssh/id_rsa\n"
  },
  {
    "path": ".github/scripts/setup/terraform-switch-latest.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Install Terraform 1.14.4\n.github/scripts/setup/terraform-switch.sh 1.14.4\n"
  },
  {
    "path": ".github/scripts/setup/terraform-switch.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n: \"${ENV_FILE:?ENV_FILE is not set}\"\n\nif [[ $# -lt 1 ]]; then\n  echo \"Usage: $0 <terraform-version>\" >&2\n  exit 1\nfi\n\nTF_VERSION=\"$1\"\n\ntouch \"$ENV_FILE\"\n\nmise uninstall opentofu\nmise use \"terraform@${TF_VERSION}\"\n\nterraform --version\n\nprintf \"export TG_TF_PATH='%s'\\n\" \"terraform\" >> \"$ENV_FILE\"\n\n"
  },
  {
    "path": ".github/scripts/setup/tofu-switch.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n: \"${ENV_FILE:?ENV_FILE is not set}\"\n\ntouch \"$ENV_FILE\"\n\nmise uninstall --all terraform\n\ntofu --version\n\nprintf \"export TG_TF_PATH='%s'\\n\" \"tofu\" >> \"$ENV_FILE\"\n\n"
  },
  {
    "path": ".github/scripts/setup/windows-setup.ps1",
    "content": "git config --global core.compression 9\ngit config --system core.longpaths true\ngit config --global core.longpaths true\ngit config --local core.longpaths true\nSet-ItemProperty -Path 'HKLM:\\SYSTEM\\CurrentControlSet\\Control\\FileSystem' -Name 'LongPathsEnabled' -Value 1\nreg add \"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock\" /t REG_DWORD /f /v \"AllowDevelopmentWithoutDevLicense\" /d \"1\"\n\nmkdir C:\\bin\ncmd /c mklink C:\\bin\\sh.exe \"C:\\Program Files\\Git\\usr\\bin\\bash.exe\"\ncmd /c mklink C:\\bin\\bash.exe \"C:\\Program Files\\Git\\usr\\bin\\bash.exe\"\necho \"C:\\bin\" | Out-File -Append -FilePath $env:GITHUB_PATH\n"
  },
  {
    "path": ".github/workflows/announce-release.yml",
    "content": "name: Announce Release\non:\n  release:\n    # This is intentionally not `published` to avoid announcing pre-releases\n    types: [released]\n  workflow_dispatch:\n    inputs:\n      tag_name:\n        description: 'The tag name of the release'\n        required: true\njobs:\n  release:\n    runs-on: ubuntu-slim\n    steps:\n      - name: Checkout\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n      - name: Announce Release\n        run: ./.github/scripts/announce-release.sh\n        env:\n          GH_TOKEN: ${{ github.token }}\n          REPO: ${{ github.repository }}\n          TAG_NAME: ${{ github.event.release.tag_name || inputs.tag_name }}\n          URL: ${{ secrets.RELEASE_ANNOUNCEMENT_URL }}\n          ROLE_ID: ${{ secrets.RELEASE_ANNOUNCEMENT_ROLE_ID }}\n          USERNAME: ${{ secrets.RELEASE_ANNOUNCEMENT_USERNAME }}\n          AVATAR_URL: ${{ secrets.RELEASE_ANNOUNCEMENT_AVATAR_URL }}\n"
  },
  {
    "path": ".github/workflows/base-test.yml",
    "content": "name: Base Tests\n\non:\n  workflow_call:\n\njobs:\n  test:\n    name: Test (${{ matrix.os }})\n    runs-on: ${{ matrix.os }}-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu, macos]\n    env:\n      MISE_PROFILE: cicd\n      # Reduce GC frequency (default 100) to speed up builds/tests at cost of higher memory\n      GOGC: \"400\"\n\n    steps:\n      - name: \"Mount tmpfs\"\n        shell: bash\n        if: runner.os == 'Linux'\n        run: |\n          sudo mount -t tmpfs -o size=12G tmpfs /tmp\n          mkdir -p /home/runner/go\n          sudo mount -t tmpfs -o size=12G tmpfs /home/runner/go\n          mkdir -p /home/runner/.cache/go-build\n          sudo mount -t tmpfs -o size=4G tmpfs /home/runner/.cache/go-build\n          mkdir -p /home/runner/.cache/terragrunt\n          sudo mount -t tmpfs -o size=4G tmpfs /home/runner/.cache/terragrunt\n\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n        with:\n          fetch-depth: 1\n\n      # RAM disk for go-build cache — persists through post-steps so cache save works.\n      # If ramdisk is unmounted before job cleanup, cache save will fail silently.\n      - name: \"Mount RAM disk\"\n        shell: bash\n        if: runner.os == 'macOS'\n        run: |\n          RAMDISK=$(hdiutil attach -nomount ram://8388608 | tr -d '[:space:]') || { echo \"Failed to create RAM disk\"; exit 1; }\n          # Wait for disk device to be registered in kernel (hdiutil may return before kernel is ready)\n          for i in $(seq 1 10); do\n            diskutil info \"$RAMDISK\" > /dev/null 2>&1 && break\n            sleep 1\n          done\n          diskutil erasevolume HFS+ RAMDisk \"$RAMDISK\"\n          mkdir -p /Volumes/RAMDisk/go-build\n          rm -rf ~/Library/Caches/go-build\n          ln -sf /Volumes/RAMDisk/go-build ~/Library/Caches/go-build\n\n      - name: Use mise to install dependencies\n        uses: jdx/mise-action@c1ecc8f748cd28cdeabf76dab3cccde4ce692fe4 # v4.0.0\n        with:\n          version: 2026.1.9\n          experimental: true\n        env:\n          # Adding token here to reduce the likelihood of hitting rate limit issues.\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - id: go-cache-paths\n        run: |\n          echo \"go-build=$(go env GOCACHE)\" >> \"$GITHUB_OUTPUT\"\n          echo \"go-mod=$(go env GOMODCACHE)\" >> \"$GITHUB_OUTPUT\"\n        shell: bash\n\n      - name: Go Build Cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: ${{ steps.go-cache-paths.outputs.go-build }}\n          key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}-${{ matrix.os }}-amd64\n          restore-keys: |\n            ${{ runner.os }}-go-build-\n\n      - name: Go Mod Cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: ${{ steps.go-cache-paths.outputs.go-mod }}\n          key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}-${{ matrix.os }}-amd64\n          restore-keys: |\n            ${{ runner.os }}-go-mod-\n\n      - name: Terragrunt Provider Cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: ${{ runner.os == 'Linux' && '~/.cache/terragrunt' || '~/Library/Caches/terragrunt' }}\n          key: ${{ runner.os }}-terragrunt-provider-cache-${{ hashFiles('**/.terraform.lock.hcl') }}\n          restore-keys: |\n            ${{ runner.os }}-terragrunt-provider-cache-\n\n      - name: Run Tests\n        id: run-tests\n        run: |\n          set -o pipefail\n          go test -v ./... -timeout 45m | tee >(go-junit-report -set-exit-code > result.xml)\n        shell: bash\n        env:\n          # Adding token here to reduce the likelihood of hitting rate limit issues.\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          # Use shorter temp path on macOS to avoid path length issues with Git worktrees.\n          # Default /var/folders/.../T/ paths are too long for Git's path resolution.\n          TMPDIR: ${{ matrix.os == 'macos' && '/tmp' || '' }}\n\n      - name: Upload Report (${{ matrix.os }})\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: test-report-${{ matrix.os }}\n          path: result.xml\n\n      - name: Display Test Results (${{ matrix.os }})\n        uses: mikepenz/action-junit-report@49b2ca06f62aa7ef83ae6769a2179271e160d8e4 # v6\n        if: always()\n        with:\n          report_paths: result.xml\n          detailed_summary: 'true'\n          include_time_in_summary: 'true'\n          group_suite: 'true'\n"
  },
  {
    "path": ".github/workflows/build-no-proxy.yml",
    "content": "name: Build Without Go Proxy\n\non:\n  workflow_call:\n  workflow_dispatch:\n\njobs:\n  build-no-proxy:\n    name: Build (${{ matrix.os }}/${{ matrix.arch }})\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        include:\n          - os: darwin\n            arch: amd64\n          - os: darwin\n            arch: arm64\n          - os: linux\n            arch: \"386\"\n          - os: linux\n            arch: amd64\n          - os: linux\n            arch: arm64\n          - os: windows\n            arch: \"386\"\n          - os: windows\n            arch: amd64\n\n    steps:\n      - name: \"Mount tmpfs\"\n        shell: bash\n        if: runner.os == 'Linux'\n        run: |\n          sudo mount -t tmpfs -o size=12G tmpfs /tmp\n          mkdir -p /home/runner/go\n          sudo mount -t tmpfs -o size=12G tmpfs /home/runner/go\n          mkdir -p /home/runner/.cache/go-build\n          sudo mount -t tmpfs -o size=4G tmpfs /home/runner/.cache/go-build\n\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n      - name: Use mise to install dependencies\n        uses: jdx/mise-action@c1ecc8f748cd28cdeabf76dab3cccde4ce692fe4 # v4.0.0\n        with:\n          version: 2026.1.9\n          experimental: true\n        env:\n          # Adding token here to reduce the likelihood of hitting rate limit issues.\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - id: go-cache-paths\n        run: |\n          echo \"go-build=$(go env GOCACHE)\" >> \"$GITHUB_OUTPUT\"\n          echo \"go-mod=$(go env GOMODCACHE)\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Go Build Cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: ${{ steps.go-cache-paths.outputs.go-build }}\n          key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}-${{ matrix.os }}-${{ matrix.arch }}\n          restore-keys: |\n            ${{ runner.os }}-go-build-\n\n      - name: Go Mod Cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: ${{ steps.go-cache-paths.outputs.go-mod }}\n          key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ${{ runner.os }}-go-mod-\n\n      - name: Build Terragrunt without Go proxy\n        env:\n          GOPROXY: direct\n          GOOS: ${{ matrix.os }}\n          GOARCH: ${{ matrix.arch }}\n        run: |\n          OUTPUT=\"bin/terragrunt-${GOOS}-${GOARCH}\"\n          if [ \"${GOOS}\" = \"windows\" ]; then\n            OUTPUT=\"${OUTPUT}.exe\"\n          fi\n          go build -o \"${OUTPUT}\" \\\n            -ldflags \"-X github.com/gruntwork-io/go-commons/version.Version=${GITHUB_REF_NAME} -extldflags '-static'\" \\\n            .\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non:\n  workflow_call:\n\njobs:\n  detect_release:\n    name: Detect if this is a release build\n    runs-on: ubuntu-latest\n    outputs:\n      is_release: ${{ steps.check.outputs.is_release }}\n    steps:\n      - name: Check if release tag\n        id: check\n        run: |\n          REF=\"${GITHUB_REF}\"\n          echo \"Git ref: $REF\"\n\n          # Check if this is a release tag (v*, alpha*, beta*)\n          if [[ \"$REF\" =~ ^refs/tags/v.* ]] || \\\n             [[ \"$REF\" =~ ^refs/tags/alpha.* ]] || \\\n             [[ \"$REF\" =~ ^refs/tags/beta.* ]]; then\n            echo \"is_release=true\" >> \"$GITHUB_OUTPUT\"\n            echo \"This is a RELEASE build - signing will be enabled\"\n          else\n            echo \"is_release=false\" >> \"$GITHUB_OUTPUT\"\n            echo \"This is NOT a release build - signing will be skipped\"\n          fi\n\n  build:\n    name: Build (${{ matrix.os }}/${{ matrix.arch }})\n    needs: detect_release\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        include:\n          - os: darwin\n            arch: amd64\n          - os: darwin\n            arch: arm64\n          - os: linux\n            arch: \"386\"\n          - os: linux\n            arch: amd64\n          - os: linux\n            arch: arm64\n          - os: windows\n            arch: \"386\"\n          - os: windows\n            arch: amd64\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n      - name: Use mise to install dependencies\n        uses: jdx/mise-action@c1ecc8f748cd28cdeabf76dab3cccde4ce692fe4 # v4.0.0\n        with:\n          version: 2026.1.9\n          experimental: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - id: go-cache-paths\n        run: |\n          echo \"go-build=$(go env GOCACHE)\" >> \"$GITHUB_OUTPUT\"\n          echo \"go-mod=$(go env GOMODCACHE)\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Go Build Cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: ${{ steps.go-cache-paths.outputs.go-build }}\n          key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}-${{ matrix.os }}-${{ matrix.arch }}\n          restore-keys: |\n            ${{ runner.os }}-go-build-\n\n      - name: Go Mod Cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: ${{ steps.go-cache-paths.outputs.go-mod }}\n          key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ${{ runner.os }}-go-mod-\n\n      - name: Build Terragrunt\n        env:\n          GOOS: ${{ matrix.os }}\n          GOARCH: ${{ matrix.arch }}\n          CGO_ENABLED: 0\n        run: |\n          OUTPUT=\"bin/terragrunt_${GOOS}_${GOARCH}\"\n          if [[ \"${GOOS}\" == \"windows\" ]]; then\n            OUTPUT=\"${OUTPUT}.exe\"\n          fi\n          go build -o \"${OUTPUT}\" \\\n            -ldflags \"-s -w -X github.com/gruntwork-io/go-commons/version.Version=${GITHUB_REF_NAME}\" \\\n            .\n\n      - name: Verify Static Linking\n        env:\n          GOOS: ${{ matrix.os }}\n          GOARCH: ${{ matrix.arch }}\n        run: |\n          OUTPUT=\"bin/terragrunt_${GOOS}_${GOARCH}\"\n          if [[ \"${GOOS}\" == \"windows\" ]]; then\n            OUTPUT=\"${OUTPUT}.exe\"\n          fi\n          .github/scripts/release/verify-static-binary.sh \"${OUTPUT}\" \"${GOOS}\" \"${GOARCH}\"\n\n      - name: Upload Build Artifact\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: terragrunt_${{ matrix.os }}_${{ matrix.arch }}\n          path: bin/terragrunt_${{ matrix.os }}_${{ matrix.arch }}*\n\n  sign_macos:\n    name: Sign MacOS Binaries\n    if: ${{ needs.detect_release.outputs.is_release == 'true' }}\n    needs: [detect_release, build]\n    uses: ./.github/workflows/sign-macos.yml\n    secrets: inherit\n\n  sign_windows:\n    name: Sign Windows Binaries\n    if: ${{ needs.detect_release.outputs.is_release == 'true' }}\n    needs: [detect_release, build]\n    uses: ./.github/workflows/sign-windows.yml\n    secrets: inherit\n\n  merge_signed_binaries:\n    name: Merge All Signed Binaries\n    if: ${{ needs.detect_release.outputs.is_release == 'true' }}\n    needs: [detect_release, sign_macos, sign_windows]\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n      - name: Download macOS signed binaries\n        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n        with:\n          name: macos-signed-files\n          path: bin/\n\n      - name: Verify macOS binaries after download\n        run: |\n          # Get list of expected macOS binaries from config\n          macos_binaries=$(jq -r '.platforms[] | select(.os == \"darwin\") | .binary' .github/assets/release-assets-config.json)\n\n          echo \"Expected macOS binaries from config:\"\n          echo \"$macos_binaries\"\n          echo \"\"\n\n          echo \"Listing expected files in bin/:\"\n          for binary in $macos_binaries; do\n            if [[ -f \"bin/$binary\" ]]; then\n              ls -lh \"bin/$binary\"\n            fi\n            if [[ -f \"bin/$binary.zip\" ]]; then\n              ls -lh \"bin/$binary.zip\"\n            fi\n          done\n          echo \"\"\n\n          echo \"Verifying macOS binaries:\"\n          for binary in $macos_binaries; do\n            echo \"Checking bin/$binary:\"\n\n            [[ -f \"bin/$binary\" ]] || {\n              echo \"  ERROR: Binary bin/$binary is missing from downloaded artifact\"\n              exit 1\n            }\n\n            echo \"  OK: bin/$binary\"\n            file \"bin/$binary\"\n\n            # Check for ZIP file\n            if [[ -f \"bin/$binary.zip\" ]]; then\n              echo \"  OK: bin/$binary.zip\"\n              ls -lh \"bin/$binary.zip\"\n            else\n              echo \"  NOTICE: bin/$binary.zip (not present, will be created in merge)\"\n            fi\n            echo \"\"\n          done\n\n      - name: Download Windows signed binaries\n        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n        with:\n          name: windows-signed-files\n          path: bin/\n\n      - name: Download Linux binaries (unsigned)\n        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n        with:\n          pattern: terragrunt_linux_*\n          path: bin/\n          merge-multiple: true\n\n      - name: List all binaries\n        run: |\n          echo \"All binaries ready for release:\"\n          ls -lahrt bin/*\n\n      - name: Upload All Signed Binaries\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: all-signed-binaries\n          path: bin/*\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n\nconcurrency:\n  group: ci-${{ github.ref }}\n  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}\n\njobs:\n  lint:\n    uses: ./.github/workflows/lint.yml\n    permissions:\n      contents: read\n    secrets: inherit\n\n  precommit:\n    uses: ./.github/workflows/precommit.yml\n    permissions:\n      contents: read\n    secrets: inherit\n\n  codespell:\n    uses: ./.github/workflows/codespell.yml\n    permissions:\n      contents: read\n    secrets: inherit\n\n  go_mod_tidy_check:\n    uses: ./.github/workflows/go-mod-tidy-check.yml\n    permissions:\n      contents: read\n    secrets: inherit\n\n  markdownlint:\n    uses: ./.github/workflows/markdownlint.yml\n    permissions:\n      contents: read\n    secrets: inherit\n\n  install_script_test:\n    uses: ./.github/workflows/install-script-test.yml\n    permissions:\n      contents: read\n    secrets: inherit\n\n  license_check:\n    uses: ./.github/workflows/license-check.yml\n    permissions:\n      contents: read\n    secrets: inherit\n\n  fuzz:\n    uses: ./.github/workflows/fuzz.yml\n    permissions:\n      contents: read\n    secrets: inherit\n\n  # Fast feedback: only gate on lint/precommit/go_mod_tidy. Other checks (codespell,\n  # markdownlint, license_check, install_script_test) run in parallel and are enforced\n  # by branch protection rules, not by job dependencies.\n  base_tests:\n    needs: [lint, precommit, go_mod_tidy_check]\n    uses: ./.github/workflows/base-test.yml\n    permissions:\n      contents: read\n      checks: write\n    secrets: inherit\n\n  build:\n    needs: [lint, precommit, go_mod_tidy_check]\n    uses: ./.github/workflows/build.yml\n    permissions:\n      contents: read\n    secrets: inherit\n\n  build_no_proxy:\n    # Only run no_proxy builds on main branch to save CI time\n    if: github.ref == 'refs/heads/main'\n    needs: [lint, precommit, go_mod_tidy_check]\n    uses: ./.github/workflows/build-no-proxy.yml\n    permissions:\n      contents: read\n    secrets: inherit\n\n  integration_tests:\n    needs: [base_tests, build]\n    uses: ./.github/workflows/integration-test.yml\n    permissions:\n      contents: read\n      checks: write\n    secrets: inherit\n\n  oidc_integration_tests:\n    needs: [base_tests, build]\n    uses: ./.github/workflows/oidc-integration-test.yml\n    permissions:\n      id-token: write\n      contents: read\n      checks: write\n    secrets: inherit\n\n\n"
  },
  {
    "path": ".github/workflows/cloud-nuke.yml",
    "content": "name: Hourly Cloud Nuke\non:\n  schedule:\n    - cron: \"0 * * * *\" # Runs every hour\n  workflow_dispatch:\n\n\njobs:\n  run_cloud_nuke:\n    permissions:\n      id-token: write\n      contents: read\n    name: Nuke\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n      - name: Install cloud-nuke\n        run: |\n            wget -O /usr/local/bin/cloud-nuke \\\n                --header=\"Authorization: Bearer ${GITHUB_TOKEN}\" \\\n                \"https://github.com/gruntwork-io/cloud-nuke/releases/download/v${VERSION}/cloud-nuke_linux_amd64\"\n\n            chmod +x /usr/local/bin/cloud-nuke\n        env:\n            # Authenticate to reduce the likelihood of hitting rate limit issues.\n            GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n            VERSION: 0.40.0\n\n      - name: Authenticate to AWS\n        uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # v6\n        with:\n          role-to-assume: ${{ secrets.CLOUD_NUKE_ROLE }}\n          aws-region: us-east-1\n\n      - name: Run cloud-nuke\n        run: |\n            cloud-nuke aws \\\n                --force \\\n                --log-level debug \\\n                --resource-type s3 \\\n                --resource-type vpc \\\n                --resource-type ec2 \\\n                --resource-type dynamodb \\\n                --region us-east-1 \\\n                --region us-west-2 \\\n                --older-than 1h \\\n                --config .github/cloud-nuke/config.yml\n"
  },
  {
    "path": ".github/workflows/codespell.yml",
    "content": "name: Codespell\n\non:\n  workflow_call:\n\njobs:\n  codespell:\n    name: Check Spelling\n    runs-on: ubuntu-slim\n\n    env:\n      MISE_PROFILE: cicd\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n      - name: Use mise to install dependencies\n        uses: jdx/mise-action@c1ecc8f748cd28cdeabf76dab3cccde4ce692fe4 # v4.0.0\n        with:\n          version: 2026.1.9\n        env:\n          # Adding token here to reduce the likelihood of hitting rate limit issues.\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Run codespell\n        run: codespell\n"
  },
  {
    "path": ".github/workflows/flake.yml",
    "content": "name: Flake\non:\n  release:\n    types: [prereleased]\n  workflow_dispatch:\n\njobs:\n  test:\n    name: Flake Test (${{ matrix.flake.name }})\n    runs-on: ${{ matrix.flake.os }}-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        flake:\n          - name: Ubuntu Base tests\n            os: ubuntu\n            count: 3\n            timeout: 45m\n          - name: macOS Base tests\n            os: macos\n            count: 3\n            timeout: 45m\n\n    env:\n      MISE_PROFILE: cicd\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n      - name: Use mise to install dependencies\n        uses: jdx/mise-action@c1ecc8f748cd28cdeabf76dab3cccde4ce692fe4 # v4.0.0\n        with:\n          version: 2026.1.9\n          experimental: true\n        env:\n          # Adding token here to reduce the likelihood of hitting rate limit issues.\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - id: go-cache-paths\n        run: |\n          echo \"go-build=$(go env GOCACHE)\" >> \"$GITHUB_OUTPUT\"\n          echo \"go-mod=$(go env GOMODCACHE)\" >> \"$GITHUB_OUTPUT\"\n        shell: bash\n\n      - name: Go Build Cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: |\n            ${{ steps.go-cache-paths.outputs.go-build }}\n            ${{ steps.go-cache-paths.outputs.go-mod }}\n          key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}-${{ matrix.flake.os }}-amd64\n\n      - name: Run Tests\n        id: run-tests\n        run: |\n          set -o pipefail\n          go test -v ./... -count=${COUNT} -timeout ${TIMEOUT} | tee >(go-junit-report -set-exit-code > result.xml)\n        shell: bash\n        env:\n          # Adding token here to reduce the likelihood of hitting rate limit issues.\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          COUNT: ${{ matrix.flake.count }}\n          TIMEOUT: ${{ matrix.flake.timeout }}\n\n      - name: Upload Report (${{ matrix.flake.name }})\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: test-report-${{ matrix.flake.name }}\n          path: result.xml\n\n      - name: Display Test Results (${{ matrix.flake.name }})\n        uses: mikepenz/action-junit-report@49b2ca06f62aa7ef83ae6769a2179271e160d8e4 # v6\n        if: always()\n        with:\n          report_paths: result.xml\n          detailed_summary: 'true'\n          include_time_in_summary: 'true'\n          group_suite: 'true'\n"
  },
  {
    "path": ".github/workflows/fuzz.yml",
    "content": "name: Fuzz\n\non:\n  workflow_call:\n\njobs:\n  fuzz:\n    name: Fuzz Tests\n    runs-on: ubuntu-latest\n    env:\n      MISE_PROFILE: cicd\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n        with:\n          fetch-depth: 1\n\n      - name: Use mise to install dependencies\n        uses: jdx/mise-action@c1ecc8f748cd28cdeabf76dab3cccde4ce692fe4 # v4.0.0\n        with:\n          version: 2026.1.9\n          experimental: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - id: go-cache-paths\n        run: |\n          echo \"go-build=$(go env GOCACHE)\" >> \"$GITHUB_OUTPUT\"\n          echo \"go-mod=$(go env GOMODCACHE)\" >> \"$GITHUB_OUTPUT\"\n        shell: bash\n\n      - name: Go Build Cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: ${{ steps.go-cache-paths.outputs.go-build }}\n          key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}-linux-amd64\n          restore-keys: |\n            ${{ runner.os }}-go-build-\n\n      - name: Go Mod Cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: ${{ steps.go-cache-paths.outputs.go-mod }}\n          key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}-linux-amd64\n          restore-keys: |\n            ${{ runner.os }}-go-mod-\n\n      - name: Fuzz Corpus Cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: ~/.cache/go-build/fuzz\n          key: ${{ runner.os }}-fuzz-corpus-${{ github.sha }}\n          restore-keys: |\n            ${{ runner.os }}-fuzz-corpus-\n\n      - name: Run Fuzz Tests\n        run: make fuzz\n\n      - name: Archive Fuzz Failures\n        if: failure()\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: fuzz-failures\n          path: |\n            **/testdata/fuzz/**\n"
  },
  {
    "path": ".github/workflows/go-mod-tidy-check.yml",
    "content": "name: Go Mod Tidy Check\n\non:\n  workflow_call:\n\njobs:\n  go-mod-tidy-check:\n    name: Check go.mod and go.sum are tidy\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n      - name: Use mise to install dependencies\n        uses: jdx/mise-action@c1ecc8f748cd28cdeabf76dab3cccde4ce692fe4 # v4.0.0\n        with:\n          version: 2026.1.9\n          experimental: true\n        env:\n          # Adding token here to reduce the likelihood of hitting rate limit issues.\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Run go mod tidy\n        run: go mod tidy\n\n      - name: Check for changes\n        run: |\n          if ! git diff --exit-code go.mod go.sum; then\n            echo \"::error::go.mod or go.sum are not tidy. Please run 'go mod tidy' locally and commit the changes.\"\n            exit 1\n          fi\n          echo \"go.mod and go.sum are tidy\"\n"
  },
  {
    "path": ".github/workflows/gopls.yml",
    "content": "name: Gopls Quickfix Check\n\non:\n  schedule:\n    - cron: '0 2 1 * *'\n\n  workflow_dispatch:\n\njobs:\n  gopls-quickfix:\n    name: Gopls Quickfix\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      issues: write\n      pull-requests: write\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          ref: main\n\n      - name: Use mise to install dependencies\n        uses: jdx/mise-action@c1ecc8f748cd28cdeabf76dab3cccde4ce692fe4 # v4.0.0\n        with:\n          version: 2026.1.9\n          experimental: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          MISE_PROFILE: cicd\n\n      - name: Find Go files\n        id: gofiles\n        run: |\n          find . -type f -name '*.go' -not -path './vendor/*' > gofiles.txt\n\n      - name: Install parallel\n        run: sudo apt-get update && sudo apt-get install -y parallel\n\n      - name: Run gopls quickfixes\n        id: gopls_run\n        run: ./.github/scripts/gopls/run.sh\n\n      - name: Check for changes\n        id: check-changes\n        run: ./.github/scripts/gopls/check-for-changes.sh\n        env:\n          HAS_FIXES: ${{ steps.gopls_run.outputs.has_fixes }}\n\n      - name: Create issue for problems found\n        id: create_issue_for_problems\n        if: steps.gopls_run.outputs.has_fixes == 'true'\n        uses: actions/github-script@v8\n        env:\n          FIXED_FILES_PATH: ${{ steps.gopls_run.outputs.fixed_files_path }}\n          OUTPUT_FILE_PATH: ${{ steps.gopls_run.outputs.output_file_path }}\n        with:\n          script: |\n            const createIssue = require('./.github/scripts/gopls/create-issue.js');\n            const fixedFilesPath = process.env.FIXED_FILES_PATH;\n            const outputFilePath = process.env.OUTPUT_FILE_PATH;\n            await createIssue({ github, context, core, fixedFilesPath, outputFilePath });\n\n      - name: Create pull request for fixes\n        if: steps.check-changes.outputs.has_changes == 'true'\n        uses: actions/github-script@v8\n        env:\n          ISSUE_NUMBER: ${{ steps.create_issue_for_problems.outputs.issue_number }}\n          FIXED_FILES_PATH: ${{ steps.gopls_run.outputs.fixed_files_path }}\n        with:\n          script: |\n            const createPR = require('./.github/scripts/gopls/create-pr.js');\n            const issueNumber = process.env.ISSUE_NUMBER;\n            const fixedFilesPath = process.env.FIXED_FILES_PATH;\n            await createPR({ github, context, core, exec, issueNumber, fixedFilesPath });\n\n      - name: Success message\n        if: steps.gopls_run.outputs.has_fixes == 'false'\n        run: |\n          echo \"✅ No gopls quickfix issues found!\"\n          echo \"All Go files are up to date with gopls recommendations.\"\n"
  },
  {
    "path": ".github/workflows/install-script-test.yml",
    "content": "name: Install Script Test\n\non:\n  workflow_call:\n\njobs:\n  install-script-test:\n    name: Install Script Tests (${{ matrix.os }})\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest, macos-latest]\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n      - name: Install GPG (Ubuntu)\n        if: matrix.os == 'ubuntu-latest'\n        run: sudo apt-get update && sudo apt-get install -y gnupg\n\n      - name: Install Cosign\n        uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4\n\n      - name: Run install script tests\n        run: ./docs/tests/install_test.sh\n"
  },
  {
    "path": ".github/workflows/integration-test.yml",
    "content": "name: Integration Tests\n\non:\n  workflow_call:\n\njobs:\n  test:\n    name: Test (${{ matrix.integration.name }})\n    runs-on: ${{ matrix.integration.os }}-latest\n    env:\n      MISE_PROFILE: cicd\n    strategy:\n      fail-fast: false\n      matrix:\n        integration:\n          - name: Fixtures with OpenTofu\n            os: ubuntu\n            target: ./test\n            tags: tofu\n            setup_scripts:\n              - .github/scripts/setup/tofu-switch.sh\n          - name: Fixtures with Latest Terraform\n            os: ubuntu\n            target: ./test\n            setup_scripts:\n              - .github/scripts/setup/terraform-switch-latest.sh\n          - name: SSH\n            os: ubuntu\n            target: ./...\n            setup_scripts:\n              - .github/scripts/setup/ssh.sh\n            tags: ssh\n            run: '^TestSSH'\n            secrets: [GHA_DEPLOY_KEY]\n          - name: SOPS\n            os: ubuntu\n            target: ./...\n            setup_scripts:\n              - .github/scripts/setup/sops.sh\n            tags: sops\n            run: '^TestSOPS'\n          - name: Tflint\n            os: ubuntu\n            target: ./...\n            tags: tflint\n            run: '^TestTflint'\n            secrets: [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY]\n          - name: GCP\n            os: ubuntu\n            target: ./...\n            setup_scripts:\n              - .github/scripts/setup/gcp.sh\n            tags: gcp\n            run: '^TestGcp'\n            secrets: [GCLOUD_SERVICE_KEY, GOOGLE_CLOUD_PROJECT, GOOGLE_COMPUTE_ZONE, GOOGLE_IDENTITY_EMAIL, GOOGLE_PROJECT_ID, GCLOUD_SERVICE_KEY_IMPERSONATOR]\n          - name: AWS Tofu\n            os: ubuntu\n            target: ./...\n            tags: 'aws,tofu'\n            run: '^TestAws'\n            secrets: [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_TEST_S3_ASSUME_ROLE]\n            setup_scripts:\n              - .github/scripts/setup/tofu-switch.sh\n          - name: AWS with Latest Terraform\n            os: ubuntu\n            target: ./...\n            tags: aws\n            run: '^TestAws'\n            secrets: [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_TEST_S3_ASSUME_ROLE]\n            setup_scripts:\n              - .github/scripts/setup/terraform-switch-latest.sh\n          - name: AWSGCP\n            os: ubuntu\n            target: ./...\n            setup_scripts:\n              - .github/scripts/setup/gcp.sh\n            tags: awsgcp\n            run: '^TestAwsGcp'\n            secrets: [AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, GCLOUD_SERVICE_KEY, GOOGLE_CLOUD_PROJECT, GOOGLE_COMPUTE_ZONE, GOOGLE_IDENTITY_EMAIL, GOOGLE_PROJECT_ID]\n          - name: Engine\n            os: ubuntu\n            target: ./...\n            setup_scripts:\n              - .github/scripts/setup/engine.sh\n            tags: engine\n            run: '^TestEngine'\n          - name: Windows\n            os: windows\n            target: ./...\n            setup_scripts:\n              - .github/scripts/setup/windows-setup.ps1\n            tags: windows\n            run: '^TestWindows'\n          - name: Provider Cache Server with Latest Terraform\n            os: ubuntu\n            target: ./test\n            setup_scripts:\n              - .github/scripts/setup/provider-cache-server.sh\n              - .github/scripts/setup/terraform-switch-latest.sh\n          - name: Provider Cache Server with Tofu\n            os: ubuntu\n            target: ./test\n            tags: tofu\n            setup_scripts:\n              - .github/scripts/setup/provider-cache-server.sh\n              - .github/scripts/setup/tofu-switch.sh\n          - name: Deprecated\n            os: ubuntu\n            target: ./...\n            tags: deprecated\n            run: '^TestDeprecated'\n          - name: Mock\n            os: ubuntu\n            target: ./...\n            tags: mocks\n            run: '^TestMock'\n            setup_scripts:\n              - .github/scripts/setup/generate-mocks.sh\n          - name: Race\n            os: ubuntu\n            target: ./...\n            run: '.*WithRacing'\n            test_args: \"-race\"\n          - name: Parse\n            os: ubuntu\n            target: ./...\n            tags: parse\n            run: '^TestParse'\n          - name: CAS\n            os: ubuntu\n            target: ./...\n            setup_scripts:\n              - .github/scripts/setup/cas.sh\n          - name: Experiment mode\n            os: ubuntu\n            target: ./...\n            setup_scripts:\n              - .github/scripts/setup/experiment-mode.sh\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n        with:\n          fetch-depth: 1\n\n      - name: \"Setup Docker\"\n        if: runner.os == 'Linux'\n        id: set-up-docker\n        uses: docker/setup-docker-action@1a6edb0ba9ac496f6850236981f15d8f9a82254d # v5\n\n      - name: \"Save space on node\"\n        if: runner.os != 'Windows'\n        run: |\n          sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL\n          sudo docker image prune --all --force\n          sudo docker builder prune -a\n          df -h\n\n      - name: \"Mount tmpfs\"\n        shell: bash\n        if: runner.os == 'Linux'\n        run: |\n          mkdir -p /home/runner/.cache/go-build\n          sudo mount -t tmpfs -o size=4G tmpfs /home/runner/.cache/go-build\n\n      # install dependencies for the integration tests\n      - name: Use mise to install dependencies\n        uses: jdx/mise-action@c1ecc8f748cd28cdeabf76dab3cccde4ce692fe4 # v4.0.0\n        with:\n          version: 2026.1.9\n          experimental: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Generate Secrets Environment\n        run: ./.github/scripts/setup/generate-secrets.sh\n        env:\n          NAME: ${{ matrix.integration.name }}\n          ENV_FILE: ${{ github.workspace }}/.env.secrets\n          SECRETS: ${{ join(matrix.integration.secrets, ' ') }}\n          GHA_DEPLOY_KEY: ${{ secrets.GHA_DEPLOY_KEY }}\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          AWS_TEST_OIDC_ROLE_ARN: ${{ secrets.AWS_TEST_OIDC_ROLE_ARN }}\n          GCLOUD_SERVICE_KEY: ${{ secrets.GCLOUD_SERVICE_KEY }}\n          GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}\n          GOOGLE_COMPUTE_ZONE: ${{ secrets.GOOGLE_COMPUTE_ZONE }}\n          GOOGLE_IDENTITY_EMAIL: ${{ secrets.GOOGLE_IDENTITY_EMAIL }}\n          GOOGLE_PROJECT_ID: ${{ secrets.GOOGLE_PROJECT_ID }}\n          GCLOUD_SERVICE_KEY_IMPERSONATOR: ${{ secrets.GCLOUD_SERVICE_KEY_IMPERSONATOR }}\n          AWS_TEST_S3_ASSUME_ROLE: ${{ secrets.AWS_TEST_S3_ASSUME_ROLE }}\n        shell: bash\n\n      - name: Setup\n        if: runner.os != 'Windows'\n        run: ./.github/scripts/setup/run-setup-scripts.sh\n        shell: bash\n        env:\n          ENV_FILE: ${{ github.workspace }}/.env.secrets\n          SETUP_SCRIPTS: ${{ join(matrix.integration.setup_scripts, ' ') }}\n\n      - name: Windows Setup\n        if: runner.os == 'Windows'\n        run: pwsh -File ./.github/scripts/setup/windows-setup.ps1\n        shell: pwsh\n        env:\n          ENV_FILE: ${{ github.workspace }}/.env.secrets\n          SETUP_SCRIPTS: ${{ join(matrix.integration.setup_scripts, ' ') }}\n\n      - id: go-cache-paths\n        run: |\n          echo \"go-build=$(go env GOCACHE)\" >> \"$GITHUB_OUTPUT\"\n          echo \"go-mod=$(go env GOMODCACHE)\" >> \"$GITHUB_OUTPUT\"\n        shell: bash\n\n      - name: Go Build Cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: |\n            ${{ steps.go-cache-paths.outputs.go-build }}\n            ${{ steps.go-cache-paths.outputs.go-mod }}\n          key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}-${{ matrix.integration.os }}-amd64\n          restore-keys: |\n            ${{ runner.os }}-go-build-\n\n      - name: Terragrunt Provider Cache\n        if: runner.os == 'Linux'\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: ~/.cache/terragrunt\n          key: ${{ runner.os }}-terragrunt-provider-cache-${{ hashFiles('**/.terraform.lock.hcl') }}\n          restore-keys: |\n            ${{ runner.os }}-terragrunt-provider-cache-\n\n      - name: Run Tests\n        run: |\n          if [ \"$SKIP\" != \"true\" ]; then\n            source \"${GITHUB_WORKSPACE}/.env.secrets\"\n            # print command arguments\n            set -x\n            if [[ \"$HAS_DOCKER\" == \"true\" ]]; then\n              TAGS=\"${TAGS:+$TAGS,docker}\"\n              TAGS=\"${TAGS:=docker}\"\n            fi\n            go test -v -timeout 45m ${TAGS:+-tags \"$TAGS\"} ${RUN:+-run \"$RUN\"} ${TEST_ARGS} \"${TARGET}\" | tee test_output.log\n            # Generate XML report from test output\n            go-junit-report < test_output.log > result.xml\n          else\n            echo \"Skipping tests for $NAME as the skip flag is true.\"\n          fi\n        shell: bash\n        env:\n          GITHUB_OAUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TARGET: ${{ matrix.integration.target }}\n          TAGS: ${{ matrix.integration.tags }}\n          RUN: ${{ matrix.integration.run }}\n          SKIP: ${{ matrix.integration.skip }}\n          NAME: ${{ matrix.integration.name }}\n          TEST_ARGS: ${{ matrix.integration.test_args }}\n          HAS_DOCKER: ${{ runner.os == 'Linux' }}\n\n      - name: Upload Test Results (${{ matrix.integration.name }})\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: test-results-${{ matrix.integration.name }}\n          path: |\n            test_output.log\n            result.xml\n\n      - name: Display Test Results (${{ matrix.integration.name }})\n        uses: mikepenz/action-junit-report@49b2ca06f62aa7ef83ae6769a2179271e160d8e4 # v6\n        if: always()\n        with:\n          report_paths: result.xml\n          detailed_summary: 'true'\n          include_time_in_summary: 'true'\n          group_suite: 'true'\n\n      - name: Print Failed Tests (${{ matrix.integration.name }})\n        run: |\n          echo \"Failed Tests in ${{ matrix.integration.name }}\"\n          if [[ -f test_output.log ]]; then\n            # Count only test failure lines in a way that is safe with -e -o pipefail\n            failed_count=$(grep -E -c '^--- FAIL:' test_output.log || echo 0)\n            echo \"Failed tests count: $failed_count\"\n            if [[ \"${failed_count}\" -gt 0 ]]; then\n              echo \"Failed test names:\"\n              grep -E '^--- FAIL:' test_output.log | sed 's/.*FAIL:[[:space:]]*//'\n            else\n              echo \"No failed tests found\"\n            fi\n          else\n            echo \"No test output found\"\n          fi\n          echo \"\"\n        shell: bash\n"
  },
  {
    "path": ".github/workflows/license-check.yml",
    "content": "name: License Check\n\non:\n  workflow_call:\n\njobs:\n  license-check:\n    name: License Check\n    runs-on: ubuntu-slim\n    env:\n      MISE_PROFILE: cicd\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n      - name: Use mise to install dependencies\n        uses: jdx/mise-action@c1ecc8f748cd28cdeabf76dab3cccde4ce692fe4 # v4.0.0\n        with:\n          version: 2026.1.9\n          experimental: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - id: go-cache-paths\n        run: |\n          echo \"go-build=$(go env GOCACHE)\" >> \"$GITHUB_OUTPUT\"\n          echo \"go-mod=$(go env GOMODCACHE)\" >> \"$GITHUB_OUTPUT\"\n        shell: bash\n\n      - name: Go Build Cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: ${{ steps.go-cache-paths.outputs.go-build }}\n          key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}-linux-amd64\n          restore-keys: |\n            ${{ runner.os }}-go-build-\n\n      - name: Go Mod Cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: ${{ steps.go-cache-paths.outputs.go-mod }}\n          key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}-linux-amd64\n          restore-keys: |\n            ${{ runner.os }}-go-mod-\n\n      - name: Run License Check\n        id: run-license-check\n        run: |\n          set -o pipefail\n          make license-check | tee license-check.log\n        shell: bash\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Upload License Check Report\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: license-check-report-ubuntu\n          path: license-check.log\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Lint\n\non:\n  workflow_call:\n\njobs:\n  lint:\n    name: Lint (${{ matrix.os }})\n    runs-on: ${{ matrix.os }}-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu, macos]\n    env:\n      # Reduce GC frequency (default 100) to speed up builds/tests at cost of higher memory\n      GOGC: \"400\"\n\n    steps:\n    - name: \"Mount tmpfs\"\n      shell: bash\n      if: runner.os == 'Linux'\n      run: |\n        sudo mount -t tmpfs -o size=12G tmpfs /tmp\n        mkdir -p /home/runner/go\n        sudo mount -t tmpfs -o size=12G tmpfs /home/runner/go\n        mkdir -p /home/runner/.cache/go-build\n        sudo mount -t tmpfs -o size=4G tmpfs /home/runner/.cache/go-build\n        mkdir -p /home/runner/.cache/golangci-lint\n        sudo mount -t tmpfs -o size=2G tmpfs /home/runner/.cache/golangci-lint\n\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n      with:\n        fetch-depth: 0\n\n    - name: Set up mise\n      uses: jdx/mise-action@c1ecc8f748cd28cdeabf76dab3cccde4ce692fe4 # v4.0.0\n      with:\n        version: 2026.1.9\n        experimental: true\n      env:\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n    - id: go-cache-paths\n      shell: bash\n      run: |\n        echo \"go-build=$(go env GOCACHE)\" >> \"$GITHUB_OUTPUT\"\n        echo \"go-mod=$(go env GOMODCACHE)\" >> \"$GITHUB_OUTPUT\"\n\n        if [ \"$RUNNER_OS\" == \"macOS\" ]; then\n          echo \"golangci-lint-cache=$HOME/Library/Caches/golangci-lint\" >> \"$GITHUB_OUTPUT\"\n          exit 0\n        fi\n\n        echo \"golangci-lint-cache=$HOME/.cache/golangci-lint\" >> \"$GITHUB_OUTPUT\"\n\n    - name: Go Build Cache\n      uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n      with:\n        path: ${{ steps.go-cache-paths.outputs.go-build }}\n        key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}-${{ matrix.os }}-amd64\n        restore-keys: |\n          ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}-${{ matrix.os }}-\n\n    - name: Go Mod Cache\n      uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n      with:\n        path: ${{ steps.go-cache-paths.outputs.go-mod }}\n        key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}-${{ matrix.os }}-amd64\n        restore-keys: |\n          ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}-${{ matrix.os }}-\n          ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }}-\n          ${{ runner.os }}-go-mod-\n\n    - name: golangci-lint Cache\n      uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n      with:\n        path: ${{ steps.go-cache-paths.outputs.golangci-lint-cache }}\n        key: ${{ runner.os }}-golangci-lint-${{ hashFiles('**/go.sum') }}-${{ matrix.os }}-amd64\n        restore-keys: |\n          ${{ runner.os }}-golangci-lint-${{ hashFiles('**/go.sum') }}-${{ matrix.os }}-\n          ${{ runner.os }}-golangci-lint-\n\n    # fetch-depth: 0 fetches current branch history; origin/main ref must be fetched separately.\n    # Use refspec main:main to create a local branch (required for git merge-base main HEAD).\n    - name: Fetch main branch\n      if: startsWith(github.ref, 'refs/heads/') && github.ref != 'refs/heads/main'\n      run: git fetch origin main:main\n\n    # Ensure Go build cache directory exists on macOS.\n    # Prevents \"mkdir go-build: file exists\" or \"No such file or directory\"\n    # errors in golangci-lint.\n    - name: Ensure Go build cache dir (macOS)\n      if: runner.os == 'macOS'\n      run: |\n        cache_dir=\"$(go env GOCACHE)\"\n        if [ -f \"$cache_dir\" ]; then rm -f \"$cache_dir\"; fi\n        mkdir -p \"$cache_dir\"\n\n    # macOS runners have ~14GB RAM. GOGC=400 lets the Go heap grow to ~31GB (measured), causing\n    # heavy swap and lint timeout. Cap heap to keep golangci-lint within available physical memory.\n    - name: Set memory limits (macOS)\n      if: runner.os == 'macOS'\n      run: |\n        echo \"GOGC=100\" >> \"$GITHUB_ENV\"\n        echo \"GOMEMLIMIT=10GiB\" >> \"$GITHUB_ENV\"\n\n    # Linux runners have ~28GB RAM but GOGC=400 still causes the heap to grow beyond available\n    # physical memory when linting with many build tags, triggering runner shutdown signals.\n    # Cap the heap to keep golangci-lint stable on Linux.\n    - name: Set memory limits (Linux)\n      if: runner.os == 'Linux'\n      run: |\n        echo \"GOGC=100\" >> \"$GITHUB_ENV\"\n        echo \"GOMEMLIMIT=20GiB\" >> \"$GITHUB_ENV\"\n\n    - name: Check for lint config changes\n      id: lint-config\n      if: startsWith(github.ref, 'refs/heads/') && github.ref != 'refs/heads/main'\n      run: |\n        if git diff --name-only origin/main...HEAD | grep -q '^\\.golangci\\.yml$'; then\n          echo \"changed=true\" >> \"$GITHUB_OUTPUT\"\n        else\n          echo \"changed=false\" >> \"$GITHUB_OUTPUT\"\n        fi\n\n    - name: Lint\n      run: |\n        # Full lint on main, tags, or when lint config changed\n        if [[ \"${{ github.ref }}\" != refs/heads/* ]] || \\\n           [[ \"${{ github.ref }}\" == \"refs/heads/main\" ]] || \\\n           [[ \"${{ steps.lint-config.outputs.changed }}\" == \"true\" ]]; then\n          make run-lint\n        else\n          make run-lint-incremental\n        fi\n"
  },
  {
    "path": ".github/workflows/markdownlint.yml",
    "content": "name: Markdown Lint\n\non:\n  workflow_call:\n\njobs:\n  markdownlint:\n    name: Run Lint\n    runs-on: ubuntu-slim\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n      - name: Run markdownlint\n        uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v22\n        with:\n          globs: |\n            docs/**/*.md\n"
  },
  {
    "path": ".github/workflows/oidc-integration-test.yml",
    "content": "# These tests require different top-level permissions\n# than the other integration tests, so we're keeping them\n# in a separate workflow.\n\nname: OIDC Integration Tests\n\non:\n  workflow_call:\n\njobs:\n  test:\n    permissions:\n      id-token: write\n      contents: read\n      checks: write\n    name: Test OIDC (${{ matrix.integration.name }})\n    runs-on: ${{ matrix.integration.os }}-latest\n    env:\n      MISE_PROFILE: cicd\n    strategy:\n      fail-fast: false\n      matrix:\n        integration:\n          - name: GHA AWS\n            os: ubuntu\n            target: ./...\n            tags: awsoidc\n            run: '^TestAws'\n            # We leave the key and secret on so that cleanup steps can use them.\n            secrets: [AWS_TEST_OIDC_ROLE_ARN, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY]\n            setup_scripts:\n              - .github/scripts/setup/tofu-switch.sh\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n      - name: \"Save space on node\"\n        if: runner.os != 'Windows'\n        run: |\n          sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL\n          sudo docker image prune --all --force\n          sudo docker builder prune -a\n          df -h\n\n      # install dependencies for the integration tests\n      - name: Use mise to install dependencies\n        uses: jdx/mise-action@c1ecc8f748cd28cdeabf76dab3cccde4ce692fe4 # v4.0.0\n        with:\n          version: 2026.1.9\n          experimental: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Generate Secrets Environment\n        run: ./.github/scripts/setup/generate-secrets.sh\n        env:\n          NAME: ${{ matrix.integration.name }}\n          ENV_FILE: ${{ github.workspace }}/.env.secrets\n          SECRETS: ${{ join(matrix.integration.secrets, ' ') }}\n          GHA_DEPLOY_KEY: ${{ secrets.GHA_DEPLOY_KEY }}\n          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          AWS_TEST_OIDC_ROLE_ARN: ${{ secrets.AWS_TEST_OIDC_ROLE_ARN }}\n          GCLOUD_SERVICE_KEY: ${{ secrets.GCLOUD_SERVICE_KEY }}\n          GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}\n          GOOGLE_COMPUTE_ZONE: ${{ secrets.GOOGLE_COMPUTE_ZONE }}\n          GOOGLE_IDENTITY_EMAIL: ${{ secrets.GOOGLE_IDENTITY_EMAIL }}\n          GOOGLE_PROJECT_ID: ${{ secrets.GOOGLE_PROJECT_ID }}\n          GCLOUD_SERVICE_KEY_IMPERSONATOR: ${{ secrets.GCLOUD_SERVICE_KEY_IMPERSONATOR }}\n          AWS_TEST_S3_ASSUME_ROLE: ${{ secrets.AWS_TEST_S3_ASSUME_ROLE }}\n        shell: bash\n\n      - name: Setup\n        if: runner.os != 'Windows'\n        run: ./.github/scripts/setup/run-setup-scripts.sh\n        shell: bash\n        env:\n          ENV_FILE: ${{ github.workspace }}/.env.secrets\n          SETUP_SCRIPTS: ${{ join(matrix.integration.setup_scripts, ' ') }}\n\n      - name: Windows Setup\n        if: runner.os == 'Windows'\n        run: pwsh -File ./.github/scripts/setup/windows-setup.ps1\n        shell: pwsh\n        env:\n          ENV_FILE: ${{ github.workspace }}/.env.secrets\n          SETUP_SCRIPTS: ${{ join(matrix.integration.setup_scripts, ' ') }}\n\n      - id: go-cache-paths\n        run: |\n          echo \"go-build=$(go env GOCACHE)\" >> \"$GITHUB_OUTPUT\"\n          echo \"go-mod=$(go env GOMODCACHE)\" >> \"$GITHUB_OUTPUT\"\n        shell: bash\n\n      - name: Go Build Cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: |\n            ${{ steps.go-cache-paths.outputs.go-build }}\n            ${{ steps.go-cache-paths.outputs.go-mod }}\n          key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}-${{ matrix.integration.os }}-amd64\n\n      - name: Terragrunt Provider Cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: ~/.cache/terragrunt\n          key: ${{ runner.os }}-terragrunt-provider-cache-${{ hashFiles('**/.terraform.lock.hcl') }}\n          restore-keys: |\n            ${{ runner.os }}-terragrunt-provider-cache-\n\n      - name: Run Tests\n        run: |\n          if [ \"$SKIP\" != \"true\" ]; then\n            source \"${GITHUB_WORKSPACE}/.env.secrets\"\n            # print command arguments\n            set -x\n            go test -v -timeout 45m ${TAGS:+-tags \"$TAGS\"} ${RUN:+-run \"$RUN\"} ${TEST_ARGS} \"${TARGET}\" | tee >(go-junit-report -set-exit-code > result.xml)\n          else\n            echo \"Skipping tests for $NAME as the skip flag is true.\"\n          fi\n        shell: bash\n        env:\n          GITHUB_OAUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          TARGET: ${{ matrix.integration.target }}\n          TAGS: ${{ matrix.integration.tags }}\n          RUN: ${{ matrix.integration.run }}\n          SKIP: ${{ matrix.integration.skip }}\n          NAME: ${{ matrix.integration.name }}\n          TEST_ARGS: ${{ matrix.integration.test_args }}\n\n      - name: Upload Report (${{ matrix.integration.name }})\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: test-report-${{ matrix.integration.name }}\n          path: result.xml\n\n      - name: Display Test Results (${{ matrix.integration.name }})\n        uses: mikepenz/action-junit-report@49b2ca06f62aa7ef83ae6769a2179271e160d8e4 # v6\n        if: always()\n        with:\n          report_paths: result.xml\n          detailed_summary: 'true'\n          include_time_in_summary: 'true'\n          group_suite: 'true'\n"
  },
  {
    "path": ".github/workflows/precommit.yml",
    "content": "name: Pre-commit\n\non:\n  workflow_call:\n\njobs:\n  precommit:\n    name: Run pre-commit hooks\n    runs-on: ubuntu-latest\n    env:\n      MISE_PROFILE: cicd\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n      - name: Use mise to install dependencies\n        uses: jdx/mise-action@c1ecc8f748cd28cdeabf76dab3cccde4ce692fe4 # v4.0.0\n        with:\n          version: 2026.1.9\n          experimental: true\n        env:\n          # Adding token here to reduce the likelihood of hitting rate limit issues.\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - id: go-cache-paths\n        run: |\n          echo \"go-build=$(go env GOCACHE)\" >> \"$GITHUB_OUTPUT\"\n          echo \"go-mod=$(go env GOMODCACHE)\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Go Build Cache\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: |\n            ${{ steps.go-cache-paths.outputs.go-build }}\n            ${{ steps.go-cache-paths.outputs.go-mod }}\n          key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }}-linux-amd64\n          restore-keys: |\n            ${{ runner.os }}-go-build-\n\n      - name: Run pre-commit hooks\n        env:\n          GOPROXY: direct\n          GOOS: linux\n          GOARCH: amd64\n        run: |\n          pre-commit install\n          pre-commit run --all-files\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - 'v*'\n      - 'alpha*'\n      - 'beta*'\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: 'Tag to release (e.g., v0.58.8)'\n        required: true\n        type: string\n      clobber:\n        description: 'Overwrite existing release assets (--clobber)'\n        required: false\n        type: boolean\n        default: false\n\njobs:\n  # Build and sign all binaries (reuses build.yml workflow)\n  build-and-sign:\n    name: Build and Sign All Binaries\n    uses: ./.github/workflows/build.yml\n    permissions:\n      contents: write\n      id-token: write\n      actions: read\n    secrets: inherit\n\n  # Upload binaries to existing GitHub release\n  upload-assets:\n    name: Upload Release Assets\n    needs: build-and-sign\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      id-token: write\n      actions: read\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n      - name: Get version\n        id: version\n        env:\n          INPUT_TAG: ${{ inputs.tag }}\n          EVENT_NAME: ${{ github.event_name }}\n        run: .github/scripts/release/get-version.sh\n\n      - name: Check if release exists\n        id: check_release\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          VERSION: ${{ steps.version.outputs.version }}\n        run: .github/scripts/release/check-release-exists.sh\n\n      - name: Download pre-built signed binaries\n        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n        with:\n          name: all-signed-binaries\n          path: bin/\n\n      - name: Verify binaries downloaded\n        run: .github/scripts/release/verify-binaries-downloaded.sh bin 7\n\n      - name: Set execution permissions on binaries\n        run: .github/scripts/release/set-permissions.sh bin\n\n      - name: Create ZIP and TAR.GZ archives\n        run: .github/scripts/release/create-archives.sh bin\n\n      - name: Generate SHA256SUMS\n        run: .github/scripts/release/generate-checksums.sh bin\n\n      - name: Import GPG key and export public key\n        env:\n          SIGNING_GPG_PRIVATE_KEY: ${{ secrets.SIGNING_GPG_PRIVATE_KEY }}\n        run: |\n          echo \"${SIGNING_GPG_PRIVATE_KEY}\" | base64 --decode | gpg --batch --import\n          GPG_FINGERPRINT=$(gpg --list-secret-keys --keyid-format LONG | awk '/^sec/{sub(/.*\\//, \"\", $2); print $2; exit}')\n          echo \"GPG_FINGERPRINT=${GPG_FINGERPRINT}\" >> \"${GITHUB_ENV}\"\n          gpg --armor --export \"${GPG_FINGERPRINT}\" > bin/terragrunt-signing-key.asc\n\n      - name: Install Cosign\n        uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4\n\n      - name: Sign SHA256SUMS\n        env:\n          SIGNING_GPG_PASSPHRASE: ${{ secrets.SIGNING_GPG_PASSPHRASE }}\n        run: .github/scripts/release/sign-checksums.sh bin\n\n      - name: Verify signatures before upload\n        run: .github/scripts/release/verify-files.sh bin\n\n      - name: Upload assets to release\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          VERSION: ${{ steps.version.outputs.version }}\n          CLOBBER: ${{ github.event_name == 'workflow_dispatch' && inputs.clobber || 'false' }}\n        run: .github/scripts/release/upload-assets.sh bin\n\n      - name: Verify all assets uploaded\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          VERSION: ${{ steps.version.outputs.version }}\n          CLOBBER: ${{ github.event_name == 'workflow_dispatch' && inputs.clobber || 'false' }}\n        run: .github/scripts/release/verify-assets-uploaded.sh bin\n\n      - name: Upload summary\n        if: always()\n        env:\n          VERSION: ${{ steps.version.outputs.version }}\n          RELEASE_ID: ${{ steps.check_release.outputs.release_id }}\n          IS_DRAFT: ${{ steps.check_release.outputs.is_draft }}\n        run: .github/scripts/release/generate-upload-summary.sh\n"
  },
  {
    "path": ".github/workflows/sign-macos.yml",
    "content": "name: Sign MacOS Binaries\n\non:\n  workflow_dispatch:\n    inputs:\n      artifact-pattern:\n        description: 'Pattern for artifacts to download (default: terragrunt_darwin_*)'\n        required: false\n        type: string\n        default: 'terragrunt_darwin_*'\n      upload-artifact-name:\n        description: 'Name for the uploaded signed artifacts'\n        required: false\n        type: string\n        default: 'macos-signed-files'\n  workflow_call:\n    inputs:\n      artifact-pattern:\n        description: 'Pattern for artifacts to download (default: terragrunt_darwin_*)'\n        required: false\n        type: string\n        default: 'terragrunt_darwin_*'\n      upload-artifact-name:\n        description: 'Name for the uploaded signed artifacts'\n        required: false\n        type: string\n        default: 'macos-signed-files'\n\njobs:\n  sign-macos:\n    name: Sign MacOS Binaries\n    runs-on: macos-latest\n    env:\n      MISE_PROFILE: cicd\n      GON_VERSION: v0.0.37\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n      - name: Download macOS build artifacts\n        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n        with:\n          pattern: ${{ inputs.artifact-pattern }}\n          path: artifacts/\n\n      - name: Prepare build artifacts\n        run: .github/scripts/release/prepare-macos-artifacts.sh artifacts bin\n\n      - name: Use mise to install dependencies\n        uses: jdx/mise-action@c1ecc8f748cd28cdeabf76dab3cccde4ce692fe4 # v4.0.0\n        with:\n          version: 2026.1.9\n          experimental: true\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Cache gon binary\n        id: cache-gon\n        uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5\n        with:\n          path: gon\n          key: gon-${{ env.GON_VERSION }}\n\n      - name: Download and install gon\n        if: steps.cache-gon.outputs.cache-hit != 'true'\n        run: .github/scripts/release/install-gon.sh ${{ env.GON_VERSION }}\n\n      - name: Verify gon installation\n        run: gon --version\n\n      - name: Sign MacOS Binaries\n        env:\n          AC_PASSWORD: ${{ secrets.MACOS_AC_PASSWORD }}\n          AC_PROVIDER: ${{ secrets.MACOS_AC_PROVIDER }}\n          AC_USERNAME: ${{ secrets.MACOS_AC_LOGIN }}\n          MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}\n          MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}\n        run: .github/scripts/release/sign-macos-binaries.sh bin\n\n      - name: Verify codesign signatures\n        run: |\n          # Get list of expected macOS binaries from config\n          macos_binaries=$(jq -r '.platforms[] | select(.os == \"darwin\") | .binary' .github/assets/release-assets-config.json)\n\n          for binary in $macos_binaries; do\n            codesign -dv --verbose=4 \"bin/$binary\" 2>&1 || {\n              echo \"ERROR: No valid signature found for bin/$binary\"\n              exit 1\n            }\n          done\n\n      - name: Upload Signed MacOS Binaries\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: ${{ inputs.upload-artifact-name }}\n          path: bin/terragrunt_darwin_*\n          if-no-files-found: error\n"
  },
  {
    "path": ".github/workflows/sign-windows.yml",
    "content": "name: Sign Windows Binaries\n\non:\n  workflow_call:\n    inputs:\n      artifact_pattern:\n        description: 'Pattern for artifacts to download (default: terragrunt_windows_*)'\n        required: false\n        type: string\n        default: 'terragrunt_windows_*'\n      upload_artifact_name:\n        description: 'Name for the uploaded signed artifacts'\n        required: false\n        type: string\n        default: 'windows-signed-files'\n\njobs:\n  sign-windows:\n    name: Sign Windows Binaries\n    runs-on: windows-latest\n\n    env:\n      SM_HOST: https://clientauth.one.digicert.com\n      SM_API_KEY: ${{ secrets.WINDOWS_SIGNING_API_KEY }}\n      SM_CLIENT_CERT_PASSWORD: ${{ secrets.WINDOWS_SIGNING_P12_PASSWORD }}\n      SM_KEYPAIR_ALIAS: ${{ secrets.WINDOWS_SIGNING_KEYPAIR_ALIAS }}\n      WINDOWS_SIGNING_P12_BASE64: ${{ secrets.WINDOWS_SIGNING_P12_BASE64 }}\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n      - name: Download Windows build artifacts\n        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n        with:\n          pattern: ${{ inputs.artifact_pattern }}\n          path: artifacts/\n          merge-multiple: true\n\n      - name: Prepare build artifacts\n        shell: pwsh\n        run: .github/scripts/release/prepare-windows-artifacts.ps1 -ArtifactsDirectory artifacts -BinDirectory bin\n\n      - name: Setup Go\n        uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6\n        with:\n          go-version-file: go.mod\n\n      - name: Install go-winres\n        shell: pwsh\n        run: .github/scripts/release/install-go-winres.ps1\n\n      # Install DigiCert smtools (smctl)\n      - name: Install DigiCert smtools\n        uses: digicert/ssm-code-signing@1d820463733701cf1484c7eb5d7d24a15ca2c454 # v1.2.1\n        with:\n          force-download-tools: 'true'\n\n      # Verify smctl is available\n      - name: Verify smctl installation\n        shell: pwsh\n        run: .github/scripts/release/verify-smctl.ps1\n\n      # Restore P12 client certificate and set SM_CLIENT_CERT_FILE\n      - name: Restore P12 client certificate\n        shell: pwsh\n        run: .github/scripts/release/restore-p12-certificate.ps1\n\n      # Sign Windows binaries using external script\n      - name: Sign and patch Windows binaries\n        shell: pwsh\n        run: .github/scripts/release/sign-windows.ps1 -BinDirectory bin\n\n      # Upload Windows binaries (signed amd64 + unsigned 386)\n      - name: Upload Windows Binaries\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        with:\n          name: ${{ inputs.upload_artifact_name }}\n          path: |\n            bin/terragrunt_windows_amd64.exe\n            bin/terragrunt_windows_386.exe\n          if-no-files-found: error\n"
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "name: 'Close stale issues and PRs'\non:\n  schedule:\n    - cron: '30 1 * * *'\n\njobs:\n  stale:\n    permissions:\n      contents: read\n      issues: write\n      pull-requests: write\n    runs-on: ubuntu-slim\n    steps:\n      - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10\n        with:\n          stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for raising this issue.'\n          stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for submitting this pull request.'\n          days-before-stale: 90\n          days-before-close: 7\n          stale-issue-label: 'stale'\n          stale-pr-label: 'stale'\n          exempt-issue-labels: 'rfc,preserved'\n          exempt-draft-pr: true\n\n"
  },
  {
    "path": ".github/workflows/update-codified-remote-deps.yml",
    "content": "name: Update Codified Remote Dependencies\n\non:\n  schedule:\n    - cron: '0 9 * * 1' # Every Monday at 9:00 UTC\n  workflow_dispatch:\n\njobs:\n  live-stacks-example:\n    permissions:\n      contents: write\n      pull-requests: write\n    name: Live Stacks Example Commit\n    runs-on: ubuntu-slim\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6\n\n      - name: Check for update\n        id: check\n        run: |\n          LATEST_COMMIT=$(git ls-remote https://github.com/gruntwork-io/terragrunt-infrastructure-live-stacks-example.git HEAD | awk '{print $1}')\n          CURRENT_COMMIT=$(grep -oP 'git\", \"checkout\", \"\\K[0-9a-f]{40}' test/integration_example_live_stacks_test.go)\n\n          echo \"latest=$LATEST_COMMIT\" >> \"$GITHUB_OUTPUT\"\n          echo \"current=$CURRENT_COMMIT\" >> \"$GITHUB_OUTPUT\"\n\n          if [[ \"$LATEST_COMMIT\" == \"$CURRENT_COMMIT\" ]]; then\n            echo \"needs_update=false\" >> \"$GITHUB_OUTPUT\"\n          else\n            echo \"needs_update=true\" >> \"$GITHUB_OUTPUT\"\n          fi\n\n      - name: Update commit hash\n        if: steps.check.outputs.needs_update == 'true'\n        env:\n          CURRENT_COMMIT: ${{ steps.check.outputs.current }}\n          LATEST_COMMIT: ${{ steps.check.outputs.latest }}\n        run: |\n          sed -i \"s/$CURRENT_COMMIT/$LATEST_COMMIT/\" test/integration_example_live_stacks_test.go\n\n      - name: Create pull request\n        if: steps.check.outputs.needs_update == 'true'\n        uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7\n        with:\n          branch: chore/update-live-stacks-example-commit\n          commit-message: 'chore: Update live stacks example commit to ${{ steps.check.outputs.latest }}'\n          title: 'chore: Update live stacks example commit'\n          body: |\n            Updates the pinned commit in `TestExampleLiveStacks` from:\n            - `${{ steps.check.outputs.current }}`\n            to:\n            - `${{ steps.check.outputs.latest }}`\n\n            [Compare changes](https://github.com/gruntwork-io/terragrunt-infrastructure-live-stacks-example/compare/${{ steps.check.outputs.current }}...${{ steps.check.outputs.latest }})\n          labels: automated\n"
  },
  {
    "path": ".gitignore",
    "content": ".*.sw?\n.idea\nterragrunt.iml\nvendor\n.terraform\n.vscode\n*.tfstate\n*.tfstate.backup\n*.out\n.terragrunt-cache\n.bundle\n.ruby-version\nterragrunt\n.DS_Store\n.go-version\n.terragrunt-stack\n.devcontainer.json\n.cursor/\n.env\n.licensei.cache\nbin\n\n# Windows code signing resources\nrsrc.syso\nresource.syso\n*.syso\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\nrun:\n  go: \"1.26\"\n  issues-exit-code: 1\n  tests: true\noutput:\n  formats:\n    text:\n      path: stdout\n      print-linter-name: true\n      print-issued-lines: true\nlinters:\n  enable:\n    - asasalint\n    - asciicheck\n    - bidichk\n    - bodyclose\n    - contextcheck\n    - dupl\n    - durationcheck\n    - errchkjson\n    - errorlint\n    - exhaustive\n    - fatcontext\n    - gocheckcompilerdirectives\n    - gochecksumtype\n    - goconst\n    - gocritic\n    - gosmopolitan\n    - lll\n    - loggercheck\n    - makezero\n    - misspell\n    - mnd\n    - musttag\n    - nilerr\n    - nilnesserr\n    - noctx\n    - paralleltest\n    - perfsprint\n    - prealloc\n    - protogetter\n    - reassign\n    - rowserrcheck\n    - spancheck\n    - sqlclosecheck\n    - staticcheck\n    - testableexamples\n    - testifylint\n    - testpackage\n    - thelper\n    - tparallel\n    - unconvert\n    - unparam\n    - usetesting\n    - wastedassign\n    - wsl_v5\n    - zerologlint\n  settings:\n    dupl:\n      threshold: 120\n    errcheck:\n      check-type-assertions: false\n      check-blank: false\n      exclude-functions:\n        - (*os.File).Close\n    errorlint:\n      errorf: true\n      asserts: true\n      comparison: true\n    goconst:\n      min-len: 3\n      min-occurrences: 5\n    gocritic:\n      enabled-tags:\n        - performance\n      disabled-tags:\n        - experimental\n    govet:\n      enable:\n        - fieldalignment\n        - printf\n        - unusedwrite\n    nakedret:\n      max-func-lines: 20\n    staticcheck:\n      checks:\n        - all\n        - -SA9005\n        - -QF1008\n        - -ST1001\n    unparam:\n      check-exported: false\n    wsl_v5:\n      allow-whole-block: false\n      branch-max-lines: 2\n  exclusions:\n    generated: lax\n    rules:\n      - linters:\n          - dupl\n          - errcheck\n          - gocyclo\n          - mnd\n          - unparam\n          - wsl\n        path: _test\\.go\n      # We end up with duplicated content in this package to save us from duplicating code in other packages.\n      - linters:\n          - dupl\n        path: cli/flags/shared\n      # Incrementally linting lines that are too long to ensure that\n      # we don't have conflicts on every file in the codebase while\n      # trying to get this merged in.\n      - linters:\n          - lll\n        path-except: '^(internal/awshelper/|internal/cas/)'\n    paths:\n      - docs\n      - _ci\n      - .github\n      - .circleci\n      - third_party$\n      - builtin$\n      - examples$\nissues:\n  max-issues-per-linter: 0\n  max-same-issues: 0\nformatters:\n  enable:\n    - goimports\n  settings:\n    gofmt:\n      simplify: true\n  exclusions:\n    generated: lax\n    paths:\n      - docs\n      - _ci\n      - .github\n      - .circleci\n      - third_party$\n      - builtin$\n      - examples$\n"
  },
  {
    "path": ".gon_amd64.hcl",
    "content": "# See https://github.com/gruntwork-io/terraform-aws-ci/blob/main/modules/sign-binary-helpers/\n# for further instructions on how to sign the binary + submitting for notarization.\n\nsource = [\"./bin/terragrunt_darwin_amd64\"]\n\nbundle_id = \"io.gruntwork.app.terragrunt\"\n\napple_id {\n  username = \"machine.apple@gruntwork.io\"\n}\n\nsign {\n  application_identity = \"Developer ID Application: Gruntwork, Inc.\"\n}\n\nzip {\n  output_path = \"terragrunt_darwin_amd64.zip\"\n}\n"
  },
  {
    "path": ".gon_arm64.hcl",
    "content": "# See https://github.com/gruntwork-io/terraform-aws-ci/blob/main/modules/sign-binary-helpers/\n# for further instructions on how to sign the binary + submitting for notarization.\n\nsource = [\"./bin/terragrunt_darwin_arm64\"]\n\nbundle_id = \"io.gruntwork.app.terragrunt\"\n\napple_id {\n  username = \"machine.apple@gruntwork.io\"\n}\n\nsign {\n  application_identity = \"Developer ID Application: Gruntwork, Inc.\"\n}\n\nzip {\n  output_path = \"terragrunt_darwin_arm64.zip\"\n}\n"
  },
  {
    "path": ".licensei.toml",
    "content": "approved = [\n  \"apache-2.0\",\n  \"bsd-2-clause\",\n  \"bsd-3-clause\",\n  \"isc\",\n  \"mpl-2.0\",\n  \"mit\",\n]\n\nignored = [\n  \"github.com/terraform-linters/tflint-plugin-sdk\",\n  \"github.com/owenrumney/go-sarif\",\n  \"github.com/davecgh/go-spew\"\n]\n\n\n[header]\nignorePaths = [\"vendor\", \".gen\"]\nignoreFiles = [\"mock_*.go\", \"*_gen.go\"]\n"
  },
  {
    "path": ".markdownlint-cli2.yaml",
    "content": "config:\n  # Disable line length limit\n  MD013: false\n\n  # Disable multiple headers with the same content\n  MD024: false\n\n  # Disable requirement for descriptive links (e.g. allow click [here]())\n  MD059: false\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n  - repo: https://github.com/gruntwork-io/pre-commit\n    rev: v0.1.29\n    hooks:\n      - id: tofu-fmt\n        exclude: test/fixtures/hclvalidate/valid/.*\n      - id: goimports\n"
  },
  {
    "path": ".sonarcloud.properties",
    "content": "# Source File Exclusions: Patterns used to exclude some source files from analysis.\nsonar.exclusions=**/*_test.go\n# Test File Inclusions: Patterns used to include some test files and only these ones in analysis.\nsonar.test.inclusions=**/*_test.go\n"
  },
  {
    "path": "CODEOWNERS",
    "content": "* @denis256 @thisguycodes @yhakbar\n"
  },
  {
    "path": "KEYS",
    "content": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmDMEaWUgtBYJKwYBBAHaRw8BAQdA9b1hTzoHHbAYEqd4F+8hBnuw89vQ35F5gaWE\n9Tpns760NEdydW50d29yayAoQ29kZSBTaWduaW5nIEtleSkgPHNlY3VyaXR5QGdy\ndW50d29yay5pbz6IkwQTFgoAOxYhBGjID4bfmOcQwPIuLld3dKyoR8xJBQJpZSC0\nAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEFd3dKyoR8xJN9ABAKHD\n87thKPV4afl81OA+R+Fqr9x2eFI7EygeWec3b2pUAPwNV6sfkzzPARTKzsZeqcxW\nvDJAtK5LYaokTLdsXb8bBA==\n=6QIi\n-----END PGP PUBLIC KEY BLOCK-----\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "The MIT License (MIT)\nCopyright (c) 2016 Gruntwork, LLC\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
  },
  {
    "path": "Makefile",
    "content": "GOFMT_FILES?=$$(find . -name '*.go' | grep -v vendor)\n\nhelp:\n\t@echo \"Various utilities for managing the terragrunt repository\"\n\nfmt:\n\t@echo \"Running source files through gofmt...\"\n\tgofmt -w $(GOFMT_FILES)\n\nfmtcheck:\n\tpre-commit run goimports --all-files\n\ninstall-pre-commit-hook:\n\tpre-commit install\n\n# This build target just for convenience for those building directly from\n# source. See also: .github/workflows/build.yml\nbuild: terragrunt\nterragrunt: $(shell find . \\( -type d -name 'vendor' -prune \\) \\\n                        -o \\( -type f -name '*.go'   -print \\) )\n\tset -xe ;\\\n\tvtag_maybe_extra=$$(git describe --tags --abbrev=12 --dirty --broken) ;\\\n\tCGO_ENABLED=0 go build -o $@ -ldflags \"-s -w -X github.com/gruntwork-io/go-commons/version.Version=$${vtag_maybe_extra}\" .\n\nclean:\n\trm -f terragrunt\n\nIGNORE_TAGS := windows|linux|darwin|freebsd|openbsd|netbsd|dragonfly|solaris|plan9|js|wasip1|aix|android|illumos|ios|386|amd64|arm|arm64|mips|mips64|mips64le|mipsle|ppc64|ppc64le|riscv64|s390x|wasm\n\nLINT_TAGS := $(shell grep -rh --include='*.go' 'go:build' . | \\\n\tsed 's/.*go:build\\s*//' | \\\n\ttr -cs '[:alnum:]_' '\\n' | \\\n\tgrep -vE '^($(IGNORE_TAGS))$$' | \\\n\tsed '/^$$/d' | \\\n\tsort -u | \\\n\tpaste -sd, -)\n\nrun-lint:\n\t@echo \"Linting with feature flags: [$(LINT_TAGS)]\"\n\tGOFLAGS=\"-tags=$(LINT_TAGS)\" golangci-lint run -v --timeout=30m ./...\n\nrun-lint-incremental:\n\t@echo \"Incremental lint (new issues only) with feature flags: [$(LINT_TAGS)]\"\n\tGOFLAGS=\"-tags=$(LINT_TAGS)\" golangci-lint run -v --timeout=30m --new-from-merge-base=main ./...\n\nrun-lint-fix:\n\t@echo \"Linting with feature flags: [$(LINT_TAGS)]\"\n\tGOFLAGS=\"-tags=$(LINT_TAGS)\" golangci-lint run -v --timeout=30m --fix ./...\n\ngenerate-mocks:\n\tgo generate ./...\n\nlicense-check:\n\tgo mod vendor\n\tlicensei cache --debug\n\tlicensei check --debug\n\tlicensei header --debug\n\nfuzz:\n\t@for package in $$(go list ./...); do \\\n\t\tfor fuzz_test in $$(go test -list 'Fuzz' \"$$package\" 2>/dev/null | grep '^Fuzz' || true); do \\\n\t\t\techo \"Fuzzing $$fuzz_test in $$package\"; \\\n\t\t\tgo test -run '^$$' -fuzztime=\"30s\" -v -fuzz \"^$$fuzz_test$$\" \"$$package\"; \\\n\t\tdone; \\\n\tdone\n\n.PHONY: help fmt fmtcheck install-pre-commit-hook clean run-lint run-lint-fix fuzz\n"
  },
  {
    "path": "README.md",
    "content": "# Terragrunt\n\n[![Maintained by Gruntwork.io](https://img.shields.io/badge/maintained%20by-gruntwork.io-%235849a6.svg)](https://gruntwork.io/?ref=repo_terragrunt)\n[![Go Report Card](https://goreportcard.com/badge/github.com/gruntwork-io/terragrunt)](https://goreportcard.com/report/github.com/gruntwork-io/terragrunt)\n[![GoDoc](https://godoc.org/github.com/gruntwork-io/terragrunt?status.svg)](https://godoc.org/github.com/gruntwork-io/terragrunt)\n![OpenTofu Version](https://img.shields.io/badge/tofu-%3E%3D1.6.0-blue.svg)\n![Terraform Version](https://img.shields.io/badge/tf-%3E%3D0.12.0-blue.svg)\n\nTerragrunt is a flexible orchestration tool that allows Infrastructure as Code written in [OpenTofu](https://opentofu.org)/[Terraform](https://www.terraform.io) to scale.\n\nPlease see the following for more info, including install instructions and complete documentation:\n\n* [Terragrunt Website](https://terragrunt.com)\n* [Getting started with Terragrunt](https://docs.terragrunt.com/getting-started/quick-start/)\n* [Terragrunt Documentation](https://docs.terragrunt.com/)\n* [Contributing to Terragrunt](https://docs.terragrunt.com/community/contributing)\n* [Commercial Support](https://gruntwork.io/support/)\n\n## Join the Discord!\n\nJoin [our community](https://discord.com/invite/YENaT9h8jh) for discussions, support, and contributions:\n\n[![](https://dcbadge.limes.pink/api/server/https://discord.com/invite/YENaT9h8jh)](https://discord.com/invite/YENaT9h8jh)\n\n## License\n\nThis code is released under the MIT License. See [LICENSE.txt](LICENSE.txt).\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Reporting Security Issues\n\nGruntwork takes security seriously, and we value the input of independent security researchers. If you're reading this because you're looking to engage in responsible disclosure of a security vulnerability, we want to start with thanking you for your efforts. We appreciate your work and will make every effort to acknowledge your contributions.\n\nTo report a security issue, please use the GitHub Security Advisory [\"Report a vulnerability\"](https://github.com/gruntwork-io/terragrunt/security/advisories/new) button in the [\"Security\"](https://github.com/gruntwork-io/terragrunt/security) tab.\n\nAfter receiving the report, we will investigate the issue and inform you of next steps. After the initial reply, we may ask for additional information, and will endeavor to keep you informed of our progress.\n\nIf you are reporting a bug related to an associated tool that Terragrunt integrates with, we ask that you report the issue directly to the maintainers of that tool.\n\nPlease do not disclose the issue publicly until we have had a chance to address it.\n\n## Expectations on timelines\n\nYou can expect that Gruntwork will take any report of a security vulnerability seriously, but we ask that you also respect that it can take time to investigate and address issues given the size of the team maintaining Terragrunt. We will do our best to keep you informed of our progress, and provide insight into the timeline for addressing the issue.\n\n## Thank you\n\nWe appreciate your help in making Terragrunt more secure. Thank you for your efforts in responsibly disclosing security issues, and for your patience as we work to address them.\n\n## Verifying Release Signatures\n\nAll Terragrunt releases are signed with both GPG and Cosign. You can verify the authenticity of downloaded binaries using either method.\n\n### Download Verification Files\n\n```bash\nVERSION=\"v0.XX.X\"  # Replace with actual version\ncurl -LO \"https://github.com/gruntwork-io/terragrunt/releases/download/${VERSION}/SHA256SUMS\"\ncurl -LO \"https://github.com/gruntwork-io/terragrunt/releases/download/${VERSION}/SHA256SUMS.gpgsig\"\ncurl -LO \"https://github.com/gruntwork-io/terragrunt/releases/download/${VERSION}/SHA256SUMS.sig\"\ncurl -LO \"https://github.com/gruntwork-io/terragrunt/releases/download/${VERSION}/SHA256SUMS.pem\"\n```\n\n### GPG Verification\n\n```bash\n# Import the public key (first time only)\ncurl -s https://gruntwork.io/.well-known/pgp-key.txt | gpg --import\n\n# Verify the signature\ngpg --verify SHA256SUMS.gpgsig SHA256SUMS\n\n# Verify binary checksum\nsha256sum -c SHA256SUMS --ignore-missing\n```\n\n### Cosign Verification\n\n```bash\n# Install cosign: https://docs.sigstore.dev/cosign/system_config/installation/\ncosign verify-blob SHA256SUMS \\\n  --signature SHA256SUMS.sig \\\n  --certificate SHA256SUMS.pem \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n  --certificate-identity-regexp \"github.com/gruntwork-io/terragrunt\"\n\n# Verify binary checksum\nsha256sum -c SHA256SUMS --ignore-missing\n```\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "# build output\ndist/\n\n# generated types\n.astro/\n\n# dependencies\nnode_modules/\n\n# logs\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\n\n\n# environment variables\n.env\n.env.production\n\n# macOS-specific files\n.DS_Store\n\n# Vercel files\n.vercel\n"
  },
  {
    "path": "docs/.vercelignore",
    "content": "node_modules\n.env\n.env.local\n.env.production\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Terragrunt Documentation\n\nThis is the documentation for Terragrunt (hosted at <https://docs.terragrunt.com>), built using [Starlight](https://github.com/withastro/starlight), a documentation framework for Astro.\n\n## Development\n\nTo get started, install the requisite dependencies to run the project locally using [mise](https://mise.jdx.dev/):\n\n```bash\nmise install\n```\n\nAfterwards, you'll want to install the NPM dependencies for the project:\n\n```bash\nbun i\n```\n\nYou'll also need to install [d2](https://github.com/terrastruct/d2/blob/master/docs/INSTALL.md) to build any diagrams referenced in the documentation:\n\nYou can now start the development server:\n\n```bash\nbun dev\n```\n\nThis will start a development server on <http://127.0.0.1:4321> that will be automatically reloaded when you make changes to documentation.\n\n## Building\n\nWhen the project is ready to deployed, it will be built using the following command:\n\n```bash\nbun run build\n```\n\nThis will generate a `dist` directory with the built documentation.\n\nRunning this locally can be useful if you see that the build fails in CI, as additional checks are performed in the build process, like ensuring that all links are valid.\n\n## Hosting\n\nThe website is hosted on [Vercel](https://vercel.com/), and is automatically deployed when a new commit is pushed to the `main` branch.\n\nEvery pull request will result in a preview deployment of the documentation site. This preview site is only accessible by maintainers of the project to prevent running untrusted code in Vercel builds.\n"
  },
  {
    "path": "docs/astro.config.mjs",
    "content": "// @ts-check\nimport { defineConfig } from \"astro/config\";\n\nimport starlight from \"@astrojs/starlight\";\nimport sitemap from \"@astrojs/sitemap\";\nimport vercel from \"@astrojs/vercel\";\nimport partytown from \"@astrojs/partytown\";\nimport tailwindcss from \"@tailwindcss/vite\";\nimport react from \"@astrojs/react\";\n\nimport starlightLinksValidator from \"starlight-links-validator\";\nimport starlightLlmsTxt from \"starlight-llms-txt\";\nimport d2 from \"astro-d2\";\n\n// Check if we're in Vercel environment\nconst isVercel = globalThis.process?.env?.VERCEL;\n\nexport const sidebar = [\n  {\n    label: \"Getting Started\",\n    autogenerate: { directory: \"01-getting-started\" },\n  },\n  {\n    label: \"Guides\",\n    items: [\n      {\n        label: \"Terralith to Terragrunt\",\n        autogenerate: { directory: \"02-guides/01-terralith-to-terragrunt\", collapsed: true },\n      },\n    ],\n    collapsed: true,\n  },\n  {\n    label: \"Features\",\n    collapsed: true,\n    items: [\n      {\n        label: \"Units\",\n        collapsed: true,\n        autogenerate: { directory: \"03-features/01-units\", collapsed: true },\n      },\n      {\n        label: \"Stacks\",\n        collapsed: true,\n        autogenerate: { directory: \"03-features/02-stacks\", collapsed: true },\n      },\n      {\n        label: \"Catalog\",\n        collapsed: true,\n        autogenerate: { directory: \"03-features/06-catalog\", collapsed: true },\n      },\n      {\n        label: \"Caching\",\n        collapsed: true,\n        autogenerate: { directory: \"03-features/07-caching\", collapsed: true },\n      },\n      {\n        label: \"Filters\",\n        collapsed: true,\n        autogenerate: { directory: \"03-features/08-filter\", collapsed: true },\n      },\n    ],\n  },\n  {\n    label: \"Reference\",\n    collapsed: true,\n    items: [\n      {\n        label: \"HCL\",\n        autogenerate: { directory: \"04-reference/01-hcl\", collapsed: true },\n      },\n      {\n        label: \"CLI\",\n        collapsed: true,\n        items: [\n          { label: \"Overview\", slug: \"reference/cli\" },\n          {\n            label: \"Commands\",\n            autogenerate: {\n              directory: \"04-reference/02-cli/02-commands\",\n              collapsed: true,\n            },\n          },\n          { label: \"Global Flags\", slug: \"reference/cli/global-flags\" },\n        ],\n      },\n      { label: \"Strict Controls\", slug: \"reference/strict-controls\" },\n      { label: \"Experiments\", slug: \"reference/experiments\" },\n      {\n        label: \"Supported Versions\",\n        slug: \"reference/supported-versions\",\n      },\n      { label: \"Lock Files\", slug: \"reference/lock-files\" },\n      {\n        label: \"Logging\",\n        autogenerate: { directory: \"04-reference/07-logging\", collapsed: true },\n      },\n      { label: \"Terragrunt Cache\", slug: \"reference/terragrunt-cache\" },\n    ],\n  },\n  {\n    label: \"Community\",\n    autogenerate: { directory: \"05-community\", collapsed: true },\n    collapsed: true,\n  },\n  {\n    label: \"Troubleshooting\",\n    autogenerate: { directory: \"06-troubleshooting\", collapsed: true },\n    collapsed: true,\n  },\n  {\n    label: \"Process\",\n    autogenerate: { directory: \"07-process\", collapsed: true },\n    collapsed: true,\n  },\n  {\n    label: \"Migrate\",\n    autogenerate: { directory: \"08-migrate\", collapsed: true },\n    collapsed: true,\n  },\n];\n\n// https://astro.build/config\nexport default defineConfig({\n  site: \"https://docs.terragrunt.com\",\n  base: \"/\",\n  output: isVercel ? \"server\" : \"static\",\n  adapter: isVercel\n    ? vercel({\n      imageService: true,\n      isr: {\n        expiration: 60 * 60 * 24, // 24 hours\n      },\n    })\n    : undefined,\n  integrations: [\n    // We use React for the shadcn/ui components.\n    react(),\n    starlight({\n      title: \"Terragrunt\",\n      description: \"Terragrunt is a flexible orchestration tool that allows Infrastructure as Code written in OpenTofu/Terraform to scale.\",\n      editLink: {\n        // TODO: update this once the docs live in `docs`.\n        baseUrl:\n          \"https://github.com/gruntwork-io/terragrunt/edit/main/docs\",\n      },\n      customCss: [\"./src/styles/global.css\"],\n      head: [\n        {\n          tag: 'meta',\n          attrs: {\n            name: 'description',\n            content: 'Terragrunt is a flexible orchestration tool that allows Infrastructure as Code written in OpenTofu/Terraform to scale.',\n          },\n        },\n        {\n          tag: 'meta',\n          attrs: {\n            property: 'og:title',\n            content: 'Terragrunt',\n          },\n        },\n        {\n          tag: 'meta',\n          attrs: {\n            property: 'og:description',\n            content: 'Terragrunt is a flexible orchestration tool that allows Infrastructure as Code written in OpenTofu/Terraform to scale.',\n          },\n        },\n        {\n          tag: 'meta',\n          attrs: {\n            property: 'og:type',\n            content: 'website',\n          },\n        },\n        {\n          tag: 'meta',\n          attrs: {\n            property: 'og:url',\n            content: 'https://docs.terragrunt.com',\n          },\n        },\n        {\n          tag: 'meta',\n          attrs: {\n            name: 'twitter:card',\n            content: 'summary_large_image',\n          },\n        },\n        {\n          tag: 'meta',\n          attrs: {\n            name: 'twitter:title',\n            content: 'Terragrunt',\n          },\n        },\n        {\n          tag: 'meta',\n          attrs: {\n            name: 'twitter:description',\n            content: 'Terragrunt is a flexible orchestration tool that allows Infrastructure as Code written in OpenTofu/Terraform to scale.',\n          },\n        },\n      ],\n      components: {\n        Header: \"./src/components/Header.astro\",\n        PageSidebar: \"./src/components/PageSidebar.astro\",\n        SiteTitle: \"./src/components/SiteTitle.astro\",\n        SkipLink: \"./src/components/SkipLink.astro\",\n      },\n      logo: {\n        dark: \"/src/assets/horizontal-logo-light.svg\",\n        light: \"/src/assets/horizontal-logo-dark.svg\",\n      },\n      social: [\n        {\n          href: \"/community/invite\",\n          icon: \"discord\",\n          label: \"Discord\",\n        },\n      ],\n      sidebar: sidebar,\n      plugins: [\n        starlightLinksValidator({\n          exclude: [\n            // Used in the docs for OpenTelemetry\n            \"http://localhost:16686/\",\n            \"http://localhost:9090/\",\n\n            // Unfortunately, these have to be ignored, as they're referencing content\n            // that is generated outside the contents of the markdown file.\n            \"/reference/cli/commands/run#*\",\n            \"/reference/cli/commands/run/#*\",\n            \"/reference/cli/commands/list#*\",\n            \"/reference/cli/commands/list/#*\",\n            \"/reference/cli/commands/find#*\",\n            \"/reference/cli/commands/find/#*\",\n\n            // Used as a redirect to the Terragrunt Discord server\n            \"/community/invite\",\n          ],\n        }),\n        starlightLlmsTxt()\n      ],\n    }),\n    d2({\n      // It's recommended that we just skip generation in Vercel,\n      // and generate diagrams locally:\n      // https://astro-d2.vercel.app/guides/how-astro-d2-works/#deployment\n      skipGeneration: !!isVercel,\n    }),\n    partytown({\n      config: {\n        debug: false,\n        logCalls: false,\n        logGetters: false,\n        logSetters: false,\n        logImageRequests: false,\n        logScriptExecution: false,\n        logStackTraces: false,\n        forward: ['dataLayer.push'],\n      },\n    }),\n    sitemap(),\n  ],\n  // Note that some redirects are handled in vercel.json instead.\n  //\n  // This is because Astro won't do dynamic redirects for external destinations.\n  // It's faster to have Vercel handle it anyways.\n  redirects: {\n    // Catch-all redirect from /docs/* to /*\n    \"/docs/[...slug]\": \"/[...slug]\",\n\n    // Root redirects\n    \"/\": \"/getting-started/quick-start/\",\n    \"/docs/\": \"/getting-started/quick-start/\",\n\n    // Pages that have been rehomed.\n    \"/features/scaffold/\": \"/features/catalog/scaffold/\",\n    \"/features/run-queue/\": \"/features/stacks/run-queue/\",\n    \"/features/debugging/\": \"/troubleshooting/debugging/\",\n    \"/upgrade/upgrading_to_terragrunt_0.19.x/\": \"/migrate/upgrading_to_terragrunt_0.19.x/\",\n\n    // Merged pages\n    \"/features/stacks/dependencies/\": \"/features/stacks/stack-operations/\",\n    \"/features/stacks/orchestration/\": \"/features/stacks/stack-operations/\",\n\n    // Redirects to external sites.\n    \"/terragrunt-ambassador\": \"https://terragrunt.com/terragrunt-ambassador\",\n    \"/terragrunt-scale\": \"https://terragrunt.com/terragrunt-scale\",\n    \"/contact/\": \"https://gruntwork.io/contact\",\n    \"/commercial-support/\": \"https://gruntwork.io/support\",\n    \"/cookie-policy/\": \"https://gruntwork.io/legal/cookie-policy/\",\n\n    // Restructured docs\n    \"/reference/configuration/\": \"/reference/hcl/\",\n    \"/reference/cli-options/\": \"/reference/cli/\",\n    \"/reference/built-in-functions/\": \"/reference/hcl/functions/\",\n    \"/reference/config-blocks-and-attributes/\": \"/reference/hcl/blocks/\",\n    \"/reference/strict-mode/\": \"/reference/strict-controls/\",\n    \"/reference/log-formatting/\": \"/reference/logging/formatting/\",\n    \"/features/aws-authentication/\": \"/features/units/authentication/\",\n    \"/reference/experiment-mode/\": \"/reference/experiments/\",\n\n    // Support old doc structure paths\n    \"/getting-started/\": \"/getting-started/quick-start/\",\n    \"/features/\": \"/features/units/\",\n    \"/reference/\": \"/reference/hcl/\",\n    \"/troubleshooting/\": \"/troubleshooting/debugging/\",\n    \"/migrate/\": \"/migrate/migrating-from-root-terragrunt-hcl/\",\n\n    // Support old community paths\n    \"/community/\": \"/community/contributing/\",\n    \"/support/\": \"/community/support/\",\n\n    // Support old feature paths\n    \"/features/inputs/\": \"/features/units/\",\n    \"/features/locals/\": \"/features/units/\",\n    \"/features/keep-your-terraform-code-dry/\": \"/features/units/\",\n    \"/features/execute-terraform-commands-on-multiple-units-at-once/\": \"/features/stacks/\",\n    \"/features/keep-your-terragrunt-architecture-dry/\": \"/features/units/includes/\",\n    \"/features/keep-your-remote-state-configuration-dry/\": \"/features/units/state-backend/\",\n    \"/features/keep-your-cli-flags-dry/\": \"/features/units/extra-arguments/\",\n    \"/features/aws-auth/\": \"/features/units/authentication/\",\n    \"/features/work-with-multiple-aws-accounts/\": \"/features/units/authentication/\",\n    \"/features/auto-retry/\": \"/features/units/runtime-control/\",\n    \"/features/provider-cache/\": \"/features/caching/provider-cache-server/\",\n    \"/features/provider-caching/\": \"/features/caching/provider-cache-server/\",\n    \"/features/engine/\": \"/features/units/engine/\",\n    \"/features/run-report/\": \"/features/stacks/run-report/\",\n    \"/features/provider-cache-server/\": \"/features/caching/provider-cache-server/\",\n    \"/features/auto-provider-cache-dir/\": \"/features/caching/auto-provider-cache-dir/\",\n    \"/features/cas/\": \"/features/caching/cas/\",\n\n    // Additional redirects for 404ing URLs\n    \"/features/execute-terraform-commands-on-multiple-modules-at-once/\": \"/features/stacks/\",\n    \"/getting-started/configuration/\": \"/reference/hcl/\",\n    \"/features/before-and-after-hooks/\": \"/features/units/hooks/\",\n    \"/etting-started/configuration/\": \"/reference/hcl/\", // typo in original URL\n    \"/features/log-formatting\": \"/reference/logging/formatting/\",\n    \"/reference/lock-file-handling/\": \"/reference/lock-files/\",\n\n    // Restructured docs\n    \"/reference/cli/rules\": \"/process/cli-rules/\",\n\n    // Unit features rehomed under /features/units/\n    \"/features/includes/\": \"/features/units/includes/\",\n    \"/features/state-backend/\": \"/features/units/state-backend/\",\n    \"/features/extra-arguments/\": \"/features/units/extra-arguments/\",\n    \"/features/authentication/\": \"/features/units/authentication/\",\n    \"/features/hooks/\": \"/features/units/hooks/\",\n    \"/features/auto-init/\": \"/features/units/auto-init/\",\n    \"/features/runtime-control/\": \"/features/units/runtime-control/\",\n\n    // Redirects for external resources\n    \"/community/invite\": \"https://discord.com/invite/YENaT9h8jh\",\n  },\n  vite: {\n    plugins: [\n      tailwindcss(),\n      {\n        name: 'compatibility-query-redirect',\n        configureServer(server) {\n          server.middlewares.use((req, _res, next) => {\n            const url = req.url ?? '';\n            if (url === '/api/v1/compatibility' || url.startsWith('/api/v1/compatibility?')) {\n              const qs = url.includes('?') ? url.split('?')[1] : '';\n              const tool = new URLSearchParams(qs).get('tool');\n              if (tool === 'opentofu' || tool === 'terraform') {\n                req.url = `/api/v1/compatibility/${tool}`;\n              } else {\n                req.url = '/api/v1/compatibility/index';\n              }\n            }\n            next();\n          });\n        },\n      },\n    ],\n  },\n});\n"
  },
  {
    "path": "docs/components.json",
    "content": "{\n    \"_comment\": \"This file configures the shadcn/ui CLI for adding new components. Use 'npx shadcn@latest add [component-name]' to add new shadcn/ui components. Components will be installed to src/components/ui/ and will use our existing Starlight color system.\",\n    \"$schema\": \"https://ui.shadcn.com/schema.json\",\n    \"style\": \"default\",\n    \"rsc\": false,\n    \"tsx\": false,\n    \"tailwind\": {\n        \"config\": \"tailwind.config.mjs\",\n        \"css\": \"src/styles/global.css\",\n        \"baseColor\": \"slate\",\n        \"cssVariables\": true,\n        \"prefix\": \"\"\n    },\n    \"aliases\": {\n        \"components\": \"src/components\",\n        \"utils\": \"src/lib/utils\"\n    }\n  }\n"
  },
  {
    "path": "docs/mise.toml",
    "content": "[tools]\nbun = \"1.2.2\"\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"docs\",\n  \"type\": \"module\",\n  \"version\": \"0.0.1\",\n  \"scripts\": {\n    \"dev\": \"astro dev\",\n    \"start\": \"astro dev\",\n    \"build\": \"astro build\",\n    \"preview\": \"astro preview\",\n    \"astro\": \"astro\"\n  },\n  \"dependencies\": {\n    \"@astrojs/compiler-rs\": \"^0.1.4\",\n    \"@astrojs/markdown-remark\": \"7.0.0\",\n    \"@astrojs/partytown\": \"^2.1.5\",\n    \"@astrojs/react\": \"5.0.0\",\n    \"@astrojs/sitemap\": \"3.7.1\",\n    \"@astrojs/starlight\": \"0.38.1\",\n    \"@astrojs/starlight-tailwind\": \"5.0.0\",\n    \"@astrojs/vercel\": \"10.0.0\",\n    \"@tailwindcss/vite\": \"^4.2.1\",\n    \"astro\": \"6.0.4\",\n    \"astro-d2\": \"^0.10.0\",\n    \"clsx\": \"^2.1.1\",\n    \"gray-matter\": \"^4.0.3\",\n    \"lucide-react\": \"^0.577.0\",\n    \"sharp\": \"^0.34.5\",\n    \"starlight-links-validator\": \"^0.20.1\",\n    \"starlight-llms-txt\": \"^0.8.0\",\n    \"tailwind-merge\": \"^3.5.0\"\n  },\n  \"devDependencies\": {\n    \"@types/bun\": \"^1.3.10\"\n  },\n  \"overrides\": {\n    \"estree-walker\": \"2.0.2\"\n  }\n}\n"
  },
  {
    "path": "docs/public/install",
    "content": "#!/usr/bin/env bash\n# Terragrunt Installer\n#\n# Supported platforms: Linux, macOS (Darwin)\n# Supported architectures: x86_64 (amd64), aarch64/arm64, i386/i686 (386)\n# Requirements: bash 3.2+, curl, sha256sum or shasum\n#\n# Note: This script requires bash (not sh) for:\n#   - pipefail option (set -o pipefail)\n#   - local variables in functions\n#   - [[ ]] test syntax\n#   - arrays and readonly declarations\n#\n# Usage:\n#   curl -sL https://docs.terragrunt.com/install | bash\n#   curl -sL https://docs.terragrunt.com/install | bash -s -- -v v0.72.5\n#   curl -sL https://docs.terragrunt.com/install | bash -s -- -d ~/bin\n#\n# Options:\n#   -v, --version VERSION    Install specific version (default: latest)\n#   -d, --dir PATH           Installation directory (default: ~/.terragrunt/bin)\n#   -f, --force              Overwrite existing installation\n#   --verify-cosign          Use Cosign instead of GPG for signature verification\n#   --no-verify-sig          Skip GPG/Cosign signature verification\n#   --no-verify              Skip SHA256 checksum verification\n#   -h, --help               Show this help message\n#\n# Signature verification (GPG) is enabled by default for versions >= v0.98.0.\n# Use --no-verify-sig to skip, or --verify-cosign to use Cosign instead of GPG.\n#\n# Environment:\n#   TERRAGRUNT_VERSION       Override version (same as -v)\n#   TERRAGRUNT_INSTALL_DIR   Override install directory (same as -d)\n\nset -euo pipefail\n\n# --- Constants ---\nreadonly GITHUB_REPO=\"gruntwork-io/terragrunt\"\nreadonly GPG_KEY_URL=\"https://gruntwork.io/.well-known/pgp-key.txt\"\nreadonly DEFAULT_INSTALL_DIR=\"${HOME}/.terragrunt/bin\"\nreadonly BINARY_NAME=\"terragrunt\"\n# Minimum version that has signed release assets (GPG and Cosign)\nreadonly MIN_SIGNED_VERSION=\"0.98.0\"\n\n# --- Colors (if terminal) ---\n# Use $'...' syntax for reliable escape sequence interpretation on macOS/Linux\nif [[ -t 1 ]]; then\n    readonly RED=$'\\033[0;31m'\n    readonly GREEN=$'\\033[0;32m'\n    readonly YELLOW=$'\\033[0;33m'\n    readonly BLUE=$'\\033[0;34m'\n    readonly NC=$'\\033[0m' # No Color\nelse\n    readonly RED=''\n    readonly GREEN=''\n    readonly YELLOW=''\n    readonly BLUE=''\n    readonly NC=''\nfi\n\n# --- Helper Functions ---\nabort() {\n    printf \"${RED}Error: %s${NC}\\n\" \"$1\" >&2\n    exit 1\n}\n\ninfo() {\n    printf \"${BLUE}==> ${NC}%s\\n\" \"$1\"\n}\n\nwarn() {\n    printf \"${YELLOW}Warning: %s${NC}\\n\" \"$1\" >&2\n}\n\nsuccess() {\n    printf \"${GREEN}==> %s${NC}\\n\" \"$1\"\n}\n\nusage() {\n    cat <<EOF\nTerragrunt Installer\n\nUsage:\n  curl -sL https://docs.terragrunt.com/install | bash\n  curl -sL https://docs.terragrunt.com/install | bash -s -- [OPTIONS]\n\nOptions:\n  -v, --version VERSION    Install specific version (default: latest)\n  -d, --dir PATH           Installation directory (default: ~/.terragrunt/bin)\n  -f, --force              Overwrite existing installation\n  --verify-cosign          Use Cosign instead of GPG for signature verification\n  --no-verify-sig          Skip GPG/Cosign signature verification\n  --no-verify              Skip SHA256 checksum verification\n  -h, --help               Show this help message\n\nSignature verification (GPG) is enabled by default for versions >= v0.98.0.\nUse --no-verify-sig to skip, or --verify-cosign to use Cosign instead of GPG.\n\nExamples:\n  # Install latest version\n  curl -sL https://docs.terragrunt.com/install | bash\n\n  # Install specific version\n  curl -sL https://docs.terragrunt.com/install | bash -s -- -v v0.98.0\n\n  # Install to custom directory\n  curl -sL https://docs.terragrunt.com/install | bash -s -- -d ~/bin\n\n  # Install without signature verification\n  curl -sL https://docs.terragrunt.com/install | bash -s -- --no-verify-sig\n\n  # Install using Cosign instead of GPG\n  curl -sL https://docs.terragrunt.com/install | bash -s -- --verify-cosign\nEOF\n}\n\n# --- Version Comparison ---\n# Compare two semantic versions. Returns 0 if $1 >= $2, 1 otherwise.\nversion_gte() {\n    local version=$1\n    local min_version=$2\n\n    # Strip 'v' prefix if present\n    version=\"${version#v}\"\n    min_version=\"${min_version#v}\"\n\n    # Use sort -V if available, otherwise fall back to manual comparison\n    if echo | sort -V >/dev/null 2>&1; then\n        local sorted_first\n        sorted_first=$(printf '%s\\n%s' \"$min_version\" \"$version\" | sort -V | head -n1)\n        [[ \"$sorted_first\" == \"$min_version\" ]]\n    else\n        # Manual version comparison for systems without sort -V (e.g., older macOS)\n        local i\n        local IFS='.'\n        read -ra v1 <<<\"$version\"\n        read -ra v2 <<<\"$min_version\"\n\n        for ((i = 0; i < ${#v2[@]}; i++)); do\n            local n1=${v1[i]:-0}\n            local n2=${v2[i]:-0}\n            if ((n1 > n2)); then\n                return 0\n            elif ((n1 < n2)); then\n                return 1\n            fi\n        done\n        return 0\n    fi\n}\n\n# Check if version supports signature verification\nsupports_signature_verification() {\n    local version=$1\n    version_gte \"$version\" \"$MIN_SIGNED_VERSION\"\n}\n\n# --- OS/Arch Detection ---\ndetect_os() {\n    local os\n    os=\"$(uname -s)\"\n    case \"$os\" in\n        Darwin) echo \"darwin\" ;;\n        Linux)  echo \"linux\" ;;\n        MINGW*|MSYS*|CYGWIN*)\n            abort \"Windows detected. Please use PowerShell or install via Chocolatey:\n  choco install terragrunt\n\nOr download manually from: https://github.com/gruntwork-io/terragrunt/releases\"\n            ;;\n        *)\n            abort \"Unsupported operating system: $os\nSupported: Linux, macOS (Darwin)\"\n            ;;\n    esac\n}\n\ndetect_arch() {\n    local arch\n    arch=\"$(uname -m)\"\n    case \"$arch\" in\n        x86_64|amd64)  echo \"amd64\" ;;\n        aarch64|arm64) echo \"arm64\" ;;\n        i386|i686)     echo \"386\" ;;\n        *)\n            abort \"Unsupported architecture: $arch\nSupported: x86_64 (amd64), aarch64 (arm64), i386/i686 (386)\"\n            ;;\n    esac\n}\n\n# --- Version Resolution ---\nget_latest_version() {\n    local version\n\n    # Method 1: Use redirect URL (higher rate limits than API)\n    # GitHub redirects /releases/latest to /releases/tag/vX.Y.Z\n    local redirect_url\n    if redirect_url=$(curl -fsI \"https://github.com/${GITHUB_REPO}/releases/latest\" 2>/dev/null | grep -i '^location:' | tr -d '\\r'); then\n        version=$(echo \"$redirect_url\" | grep -oE 'v[0-9]+\\.[0-9]+\\.[0-9]+' | head -1)\n        if [[ -n \"$version\" ]]; then\n            echo \"$version\"\n            return 0\n        fi\n    fi\n\n    # Method 2: Fallback to GitHub API (may hit rate limits: 60 req/hour unauthenticated)\n    if version=$(curl -fsL \"https://api.github.com/repos/${GITHUB_REPO}/releases/latest\" 2>/dev/null | grep -o '\"tag_name\": \"[^\"]*' | cut -d'\"' -f4); then\n        if [[ -n \"$version\" ]]; then\n            echo \"$version\"\n            return 0\n        fi\n    fi\n\n    abort \"Could not determine latest version.\nThis may be due to GitHub API rate limits (60 requests/hour).\nSpecify a version manually with -v, e.g.: -v v0.72.5\"\n}\n\nvalidate_version() {\n    local version=\"$1\"\n    # Allow any version/tag (semver, release candidates, custom builds)\n    [[ -z \"$version\" ]] && abort \"Version cannot be empty\"\n    echo \"$version\"\n}\n\n# --- Download Functions ---\ndownload_file() {\n    local url=\"$1\"\n    local output=\"$2\"\n    local description=\"$3\"\n\n    info \"Downloading $description...\"\n    if ! curl -sL --fail \"$url\" -o \"$output\" 2>/dev/null; then\n        abort \"Failed to download $description from: $url\"\n    fi\n}\n\ndownload_binary() {\n    local version=\"$1\"\n    local binary_name=\"$2\"\n    local output_dir=\"$3\"\n\n    local url=\"https://github.com/${GITHUB_REPO}/releases/download/${version}/${binary_name}\"\n    download_file \"$url\" \"${output_dir}/${binary_name}\" \"Terragrunt ${version}\"\n}\n\ndownload_checksums() {\n    local version=\"$1\"\n    local output_dir=\"$2\"\n\n    local url=\"https://github.com/${GITHUB_REPO}/releases/download/${version}/SHA256SUMS\"\n    download_file \"$url\" \"${output_dir}/SHA256SUMS\" \"checksums\"\n}\n\n# --- Verification Functions ---\nverify_sha256() {\n    local binary_path=\"$1\"\n    local checksums_path=\"$2\"\n    local binary_name=\"$3\"\n\n    info \"Verifying SHA256 checksum...\"\n\n    local actual_checksum\n    if command -v sha256sum &>/dev/null; then\n        actual_checksum=$(sha256sum \"$binary_path\" | awk '{print $1}')\n    elif command -v shasum &>/dev/null; then\n        actual_checksum=$(shasum -a 256 \"$binary_path\" | awk '{print $1}')\n    else\n        abort \"Neither sha256sum nor shasum found. Cannot verify checksum.\"\n    fi\n\n    local expected_checksum\n    # Strip CRLF and find checksum for binary\n    expected_checksum=$(tr -d '\\r' < \"$checksums_path\" | awk -v bin=\"$binary_name\" '$2 == bin {print $1; exit}')\n\n    if [[ -z \"$expected_checksum\" ]]; then\n        abort \"Could not find checksum for $binary_name in SHA256SUMS file\"\n    fi\n\n    if [[ \"$actual_checksum\" != \"$expected_checksum\" ]]; then\n        abort \"Checksum verification failed!\nExpected: $expected_checksum\nGot:      $actual_checksum\n\nThe downloaded file may be corrupted or tampered with.\"\n    fi\n}\n\nverify_gpg() {\n    local version=\"$1\"\n    local checksums_path=\"$2\"\n    local tmpdir=\"$3\"\n\n    local sig_url=\"https://github.com/${GITHUB_REPO}/releases/download/${version}/SHA256SUMS.gpgsig\"\n    local sig_path=\"${tmpdir}/SHA256SUMS.gpgsig\"\n    local gnupg_home=\"${tmpdir}/gnupg\"\n\n    info \"Downloading GPG signature...\"\n    if ! curl -sL --fail \"$sig_url\" -o \"$sig_path\" 2>/dev/null; then\n        warn \"Failed to download GPG signature file\"\n        return 1\n    fi\n\n    # Create temporary GNUPGHOME to avoid polluting user's keyring\n    mkdir -p \"$gnupg_home\"\n    chmod 700 \"$gnupg_home\"\n\n    info \"Importing Gruntwork GPG key...\"\n    if ! curl -sL \"$GPG_KEY_URL\" | GNUPGHOME=\"$gnupg_home\" gpg --import 2>/dev/null; then\n        warn \"Failed to import GPG key\"\n        return 1\n    fi\n\n    info \"Verifying GPG signature...\"\n    if GNUPGHOME=\"$gnupg_home\" gpg --verify \"$sig_path\" \"$checksums_path\" 2>/dev/null; then\n        return 0\n    else\n        return 1\n    fi\n}\n\nverify_cosign() {\n    local version=\"$1\"\n    local checksums_path=\"$2\"\n    local tmpdir=\"$3\"\n\n    # Try bundle verification first (cosign v3+ / sigstore bundle)\n    local bundle_url=\"https://github.com/${GITHUB_REPO}/releases/download/${version}/SHA256SUMS.sigstore.json\"\n    local bundle_path=\"${tmpdir}/SHA256SUMS.sigstore.json\"\n\n    if curl -sL --fail \"$bundle_url\" -o \"$bundle_path\" 2>/dev/null; then\n        info \"Verifying Cosign signature (bundle)...\"\n        cosign verify-blob \"$checksums_path\" \\\n            --bundle \"$bundle_path\" \\\n            --certificate-oidc-issuer \"https://token.actions.githubusercontent.com\" \\\n            --certificate-identity-regexp \"github.com/gruntwork-io/terragrunt\" 2>/dev/null\n        return $?\n    fi\n\n    # Legacy .sig/.pem verification (older releases without bundle)\n    local sig_url=\"https://github.com/${GITHUB_REPO}/releases/download/${version}/SHA256SUMS.sig\"\n    local cert_url=\"https://github.com/${GITHUB_REPO}/releases/download/${version}/SHA256SUMS.pem\"\n    local sig_path=\"${tmpdir}/SHA256SUMS.sig\"\n    local cert_path=\"${tmpdir}/SHA256SUMS.pem\"\n\n    info \"Downloading Cosign signature files...\"\n    curl -sL --fail \"$sig_url\" -o \"$sig_path\" 2>/dev/null || { warn \"Failed to download Cosign signature file\"; return 1; }\n    curl -sL --fail \"$cert_url\" -o \"$cert_path\" 2>/dev/null || { warn \"Failed to download Cosign certificate file\"; return 1; }\n\n    info \"Verifying Cosign signature...\"\n    cosign verify-blob \"$checksums_path\" \\\n        --signature \"$sig_path\" \\\n        --certificate \"$cert_path\" \\\n        --certificate-oidc-issuer \"https://token.actions.githubusercontent.com\" \\\n        --certificate-identity-regexp \"github.com/gruntwork-io/terragrunt\" 2>/dev/null\n    return $?\n}\n\n# Verify signature using specified method\nverify_signature() {\n    local version=\"$1\"\n    local checksums_path=\"$2\"\n    local tmpdir=\"$3\"\n    local method=\"$4\"  # gpg or cosign\n\n    case \"$method\" in\n        gpg)\n            command -v gpg &>/dev/null || abort \"GPG verification requested but gpg is not installed\"\n            verify_gpg \"$version\" \"$checksums_path\" \"$tmpdir\" && return 0\n            abort \"GPG signature verification failed!\"\n            ;;\n        cosign)\n            command -v cosign &>/dev/null || abort \"Cosign verification requested but cosign is not installed\"\n            verify_cosign \"$version\" \"$checksums_path\" \"$tmpdir\" && return 0\n            abort \"Cosign signature verification failed!\"\n            ;;\n    esac\n}\n\n# --- Shell RC Detection ---\ndetect_shell_rc() {\n    local shell_name\n    shell_name=$(basename \"${SHELL:-}\")\n    case \"$shell_name\" in\n        bash)\n            if [[ -f \"${HOME}/.bashrc\" ]]; then\n                echo \"${HOME}/.bashrc\"\n            elif [[ -f \"${HOME}/.bash_profile\" ]]; then\n                echo \"${HOME}/.bash_profile\"\n            fi\n            ;;\n        zsh)\n            echo \"${HOME}/.zshrc\"\n            ;;\n        fish)\n            echo \"${HOME}/.config/fish/config.fish\"\n            ;;\n    esac\n}\n\n# Check if PATH already contains install dir\npath_already_configured() {\n    local install_dir=\"$1\"\n    local rc_file\n    rc_file=$(detect_shell_rc)\n\n    [[ -n \"$rc_file\" ]] && grep -Fq \"${install_dir}\" \"$rc_file\" 2>/dev/null\n}\n\n# --- Installation ---\ninstall_binary() {\n    local binary_path=\"$1\"\n    local install_dir=\"$2\"\n    local force=\"$3\"\n    local requested_version=\"$4\"\n    local target_path=\"${install_dir}/${BINARY_NAME}\"\n\n    # Check if already exists (skip if force)\n    if [[ -f \"$target_path\" && \"$force\" != \"true\" ]]; then\n        local existing_version\n        existing_version=$(\"$target_path\" --version 2>/dev/null | grep -oE 'v[0-9]+\\.[0-9]+\\.[0-9]+' | head -n 1 || echo \"unknown\")\n\n        [[ \"$existing_version\" == \"$requested_version\" ]] && \\\n            abort \"Terragrunt ${existing_version} is already installed at $target_path\"\n\n        abort \"A different version (${existing_version}) is installed at $target_path\nUse --force to upgrade/downgrade to ${requested_version}\"\n    fi\n\n    # Create install directory if needed\n    [[ ! -d \"$install_dir\" ]] && {\n        info \"Creating installation directory: $install_dir\"\n        mkdir -p \"$install_dir\" 2>/dev/null || abort \"Failed to create installation directory: $install_dir\"\n    }\n\n    # Check write permissions\n    [[ ! -w \"$install_dir\" ]] && abort \"Cannot write to $install_dir\nRun with sudo:\n  curl -sL https://docs.terragrunt.com/install | sudo bash\nOr specify a different directory:\n  curl -sL https://docs.terragrunt.com/install | bash -s -- -d ~/bin\"\n\n    info \"Installing to ${target_path}...\"\n    install -m 0755 \"$binary_path\" \"$target_path\"\n}\n\n# --- Argument Parsing ---\nparse_args() {\n    # Set defaults from environment or hardcoded values\n    VERSION=\"${TERRAGRUNT_VERSION:-}\"\n    INSTALL_DIR=\"${TERRAGRUNT_INSTALL_DIR:-$DEFAULT_INSTALL_DIR}\"\n    VERIFY_SHA=true\n    VERIFY_SIG=\"gpg\"  # gpg (default), cosign, or empty (via --no-verify-sig)\n    SKIP_SIG_VERIFY=false  # set by --no-verify-sig to disable signature verification\n    FORCE=false\n\n    while [[ $# -gt 0 ]]; do\n        case \"$1\" in\n            -v|--version)\n                [[ -z \"${2:-}\" ]] && abort \"Option $1 requires a version argument\"\n                VERSION=\"$2\"\n                shift 2\n                ;;\n            -d|--dir)\n                [[ -z \"${2:-}\" ]] && abort \"Option $1 requires a directory argument\"\n                INSTALL_DIR=\"$2\"\n                shift 2\n                ;;\n            -f|--force)\n                FORCE=true\n                shift\n                ;;\n            --verify-cosign)\n                VERIFY_SIG=\"cosign\"\n                shift\n                ;;\n            --no-verify-sig)\n                SKIP_SIG_VERIFY=true\n                shift\n                ;;\n            --no-verify)\n                VERIFY_SHA=false\n                shift\n                ;;\n            -h|--help)\n                usage\n                exit 0\n                ;;\n            -*)\n                abort \"Unknown option: $1\nUse -h or --help for usage information\"\n                ;;\n            *)\n                abort \"Unexpected argument: $1\nUse -h or --help for usage information\"\n                ;;\n        esac\n    done\n}\n\n# --- Dependency Check ---\ncheck_dependencies() {\n    command -v curl &>/dev/null || abort \"curl is required but not installed.\nPlease install curl and try again.\"\n\n    [[ \"$VERIFY_SHA\" == true ]] && ! command -v sha256sum &>/dev/null && ! command -v shasum &>/dev/null && \\\n        abort \"Neither sha256sum nor shasum found.\nInstall one of these tools or skip checksum verification with:\n  curl -sL https://docs.terragrunt.com/install | bash -s -- --no-verify\"\n\n    # Handle signature verification setup\n    [[ \"$SKIP_SIG_VERIFY\" == true ]] && { VERIFY_SIG=\"\"; return; }\n\n    # Verify required tool is available\n    case \"$VERIFY_SIG\" in\n        gpg)\n            command -v gpg &>/dev/null || abort \"GPG signature verification requires gpg but it is not installed.\nInstall gpg or skip signature verification with:\n  curl -sL https://docs.terragrunt.com/install | bash -s -- --no-verify-sig\nOr use Cosign instead:\n  curl -sL https://docs.terragrunt.com/install | bash -s -- --verify-cosign\"\n            ;;\n        cosign)\n            command -v cosign &>/dev/null || abort \"Cosign verification requested but cosign is not installed.\"\n            ;;\n    esac\n}\n\n# --- Main ---\nmain() {\n    parse_args \"$@\"\n\n    # Expand tilde in INSTALL_DIR (bash doesn't expand ~ in quoted variables)\n    INSTALL_DIR=\"${INSTALL_DIR/#\\~/$HOME}\"\n\n    # Check dependencies\n    check_dependencies\n\n    # Detect platform\n    local os arch version binary_name\n    os=$(detect_os)\n    arch=$(detect_arch)\n\n    # Resolve version\n    if [[ -z \"$VERSION\" ]]; then\n        info \"Fetching latest version...\"\n        version=$(get_latest_version)\n    else\n        version=$(validate_version \"$VERSION\")\n    fi\n\n    binary_name=\"terragrunt_${os}_${arch}\"\n\n    info \"Installing Terragrunt ${version} for ${os}/${arch}\"\n\n    # Create temp directory with safe cleanup\n    local tmpdir\n    tmpdir=$(mktemp -d 2>/dev/null || mktemp -d -t 'terragrunt-install')\n    trap '[[ -n \"${tmpdir:-}\" && -d \"${tmpdir:-}\" ]] && rm -rf \"$tmpdir\"' EXIT\n\n    # Download files\n    download_binary \"$version\" \"$binary_name\" \"$tmpdir\"\n    download_checksums \"$version\" \"$tmpdir\"\n\n    # Verify signature first (authenticates SHA256SUMS file)\n    if [[ -n \"$VERIFY_SIG\" ]]; then\n        if supports_signature_verification \"$version\"; then\n            verify_signature \"$version\" \"$tmpdir/SHA256SUMS\" \"$tmpdir\" \"$VERIFY_SIG\"\n            success \"Signature verified\"\n        else\n            warn \"Skipping signature verification: not available for versions older than v${MIN_SIGNED_VERSION}\"\n        fi\n    fi\n\n    # Verify checksum (validates binary against authenticated checksums)\n    if [[ \"$VERIFY_SHA\" == true ]]; then\n        verify_sha256 \"$tmpdir/$binary_name\" \"$tmpdir/SHA256SUMS\" \"$binary_name\"\n        success \"SHA256 checksum verified\"\n    else\n        warn \"Skipping checksum verification (--no-verify specified)\"\n    fi\n\n    # Install\n    install_binary \"$tmpdir/$binary_name\" \"$INSTALL_DIR\" \"$FORCE\" \"$version\"\n\n    local target_path=\"${INSTALL_DIR}/${BINARY_NAME}\"\n    success \"Terragrunt ${version} installed successfully to ${target_path}\"\n    echo \"\"\n\n    # Show PATH instructions if using default dir and not already configured\n    if [[ \"$INSTALL_DIR\" == \"$DEFAULT_INSTALL_DIR\" ]] && ! path_already_configured \"$INSTALL_DIR\"; then\n        local rc_file\n        rc_file=$(detect_shell_rc)\n\n        if [[ -n \"$rc_file\" ]]; then\n            echo \"To add terragrunt to your PATH, run:\"\n            echo \"\"\n            echo \"  echo 'export PATH=\\\"${INSTALL_DIR}:\\$PATH\\\"' >> ${rc_file}\"\n            echo \"  source ${rc_file}\"\n        else\n            echo \"Add to your shell configuration:\"\n            echo \"\"\n            echo \"  export PATH=\\\"${INSTALL_DIR}:\\$PATH\\\"\"\n        fi\n        echo \"\"\n    fi\n\n    echo \"Run 'terragrunt --help' to get started.\"\n    echo \"For documentation, visit: https://docs.terragrunt.com/\"\n}\n\nmain \"$@\"\n"
  },
  {
    "path": "docs/public/robots.txt",
    "content": "User-agent: *\nAllow: /\n\nSitemap: https://docs.terragrunt.com/sitemap-index.xml\n"
  },
  {
    "path": "docs/public/schemas/auth-provider-cmd/v1/schema.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"https://terragrunt.gruntwork.io/schemas/auth-provider-cmd/v1/schema.json\",\n  \"title\": \"Terragrunt Auth Provider Command Response Schema\",\n  \"description\": \"Schema for the JSON response expected from an auth provider command\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"awsCredentials\": {\n      \"type\": \"object\",\n      \"description\": \"AWS credentials to set as environment variables\",\n      \"properties\": {\n        \"ACCESS_KEY_ID\": {\n          \"type\": \"string\",\n          \"description\": \"AWS access key ID\"\n        },\n        \"SECRET_ACCESS_KEY\": {\n          \"type\": \"string\",\n          \"description\": \"AWS secret access key\"\n        },\n        \"SESSION_TOKEN\": {\n          \"type\": \"string\",\n          \"description\": \"AWS session token (optional)\"\n        }\n      },\n      \"required\": [\n        \"ACCESS_KEY_ID\",\n        \"SECRET_ACCESS_KEY\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"awsRole\": {\n      \"type\": \"object\",\n      \"description\": \"AWS role to assume\",\n      \"properties\": {\n        \"roleARN\": {\n          \"type\": \"string\",\n          \"description\": \"The ARN of the IAM role to assume\"\n        },\n        \"roleSessionName\": {\n          \"type\": \"string\",\n          \"description\": \"The session name for the assumed role\"\n        },\n        \"duration\": {\n          \"type\": \"integer\",\n          \"description\": \"Duration in seconds for the assumed role session\",\n          \"minimum\": 0\n        },\n        \"webIdentityToken\": {\n          \"type\": \"string\",\n          \"description\": \"Web identity token for OIDC-based role assumption\"\n        }\n      },\n      \"required\": [\n        \"roleARN\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"envs\": {\n      \"type\": \"object\",\n      \"description\": \"Additional environment variables to set\",\n      \"additionalProperties\": {\n        \"type\": \"string\"\n      }\n    }\n  },\n  \"additionalProperties\": false\n}\n"
  },
  {
    "path": "docs/public/schemas/auth-provider-cmd/v2/schema.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"https://docs.terragrunt.com/schemas/auth-provider-cmd/v2/schema.json\",\n  \"title\": \"Terragrunt Auth Provider Command Response Schema\",\n  \"description\": \"Schema for the JSON response expected from an auth provider command\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"awsCredentials\": {\n      \"type\": \"object\",\n      \"description\": \"AWS credentials to set as environment variables\",\n      \"properties\": {\n        \"ACCESS_KEY_ID\": {\n          \"type\": \"string\",\n          \"description\": \"AWS access key ID\"\n        },\n        \"SECRET_ACCESS_KEY\": {\n          \"type\": \"string\",\n          \"description\": \"AWS secret access key\"\n        },\n        \"SESSION_TOKEN\": {\n          \"type\": \"string\",\n          \"description\": \"AWS session token (optional)\"\n        }\n      },\n      \"required\": [\n        \"ACCESS_KEY_ID\",\n        \"SECRET_ACCESS_KEY\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"awsRole\": {\n      \"type\": \"object\",\n      \"description\": \"AWS role to assume\",\n      \"properties\": {\n        \"roleARN\": {\n          \"type\": \"string\",\n          \"description\": \"The ARN of the IAM role to assume\"\n        },\n        \"roleSessionName\": {\n          \"type\": \"string\",\n          \"description\": \"The session name for the assumed role\"\n        },\n        \"duration\": {\n          \"type\": \"integer\",\n          \"description\": \"Duration in seconds for the assumed role session\",\n          \"minimum\": 0\n        },\n        \"webIdentityToken\": {\n          \"type\": \"string\",\n          \"description\": \"Web identity token for OIDC-based role assumption\"\n        }\n      },\n      \"required\": [\n        \"roleARN\"\n      ],\n      \"additionalProperties\": false\n    },\n    \"envs\": {\n      \"type\": \"object\",\n      \"description\": \"Additional environment variables to set\",\n      \"additionalProperties\": {\n        \"type\": \"string\"\n      }\n    }\n  },\n  \"additionalProperties\": false\n}\n"
  },
  {
    "path": "docs/public/schemas/run/report/v1/schema.json",
    "content": "{\n  \"items\": {\n    \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n    \"$id\": \"https://terragrunt.gruntwork.io/schemas/run/report/v1/schema.json\",\n    \"properties\": {\n      \"Started\": {\n        \"type\": \"string\",\n        \"format\": \"date-time\"\n      },\n      \"Ended\": {\n        \"type\": \"string\",\n        \"format\": \"date-time\"\n      },\n      \"Reason\": {\n        \"type\": \"string\",\n        \"enum\": [\n          \"retry succeeded\",\n          \"error ignored\",\n          \"run error\",\n          \"--queue-exclude-dir\",\n          \"exclude block\",\n          \"ancestor error\"\n        ]\n      },\n      \"Cause\": {\n        \"type\": \"string\"\n      },\n      \"Name\": {\n        \"type\": \"string\"\n      },\n      \"Result\": {\n        \"type\": \"string\",\n        \"enum\": [\n          \"succeeded\",\n          \"failed\",\n          \"early exit\",\n          \"excluded\"\n        ]\n      }\n    },\n    \"additionalProperties\": false,\n    \"type\": \"object\",\n    \"required\": [\n      \"Started\",\n      \"Ended\",\n      \"Name\",\n      \"Result\"\n    ],\n    \"title\": \"Terragrunt Run Report Schema\",\n    \"description\": \"Schema for Terragrunt run report\"\n  },\n  \"type\": \"array\",\n  \"title\": \"Terragrunt Run Report Schema\",\n  \"description\": \"Array of Terragrunt runs\"\n}\n"
  },
  {
    "path": "docs/public/schemas/run/report/v2/schema.json",
    "content": "{\n  \"items\": {\n    \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n    \"$id\": \"https://terragrunt.gruntwork.io/schemas/run/report/v2/schema.json\",\n    \"properties\": {\n      \"Started\": {\n        \"type\": \"string\",\n        \"format\": \"date-time\"\n      },\n      \"Ended\": {\n        \"type\": \"string\",\n        \"format\": \"date-time\"\n      },\n      \"Reason\": {\n        \"type\": \"string\",\n        \"enum\": [\n          \"retry succeeded\",\n          \"error ignored\",\n          \"run error\",\n          \"exclude block\",\n          \"ancestor error\"\n        ]\n      },\n      \"Cause\": {\n        \"type\": \"string\"\n      },\n      \"Name\": {\n        \"type\": \"string\"\n      },\n      \"Result\": {\n        \"type\": \"string\",\n        \"enum\": [\n          \"succeeded\",\n          \"failed\",\n          \"early exit\",\n          \"excluded\"\n        ]\n      }\n    },\n    \"additionalProperties\": false,\n    \"type\": \"object\",\n    \"required\": [\n      \"Started\",\n      \"Ended\",\n      \"Name\",\n      \"Result\"\n    ],\n    \"title\": \"Terragrunt Run Report Schema\",\n    \"description\": \"Schema for Terragrunt run report\"\n  },\n  \"type\": \"array\",\n  \"title\": \"Terragrunt Run Report Schema\",\n  \"description\": \"Array of Terragrunt runs\"\n}\n"
  },
  {
    "path": "docs/public/schemas/run/report/v3/schema.json",
    "content": "{\n  \"items\": {\n    \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n    \"$id\": \"https://terragrunt.gruntwork.io/schemas/run/report/v3/schema.json\",\n    \"properties\": {\n      \"Started\": {\n        \"type\": \"string\",\n        \"format\": \"date-time\"\n      },\n      \"Ended\": {\n        \"type\": \"string\",\n        \"format\": \"date-time\"\n      },\n      \"Reason\": {\n        \"type\": \"string\",\n        \"enum\": [\n          \"retry succeeded\",\n          \"error ignored\",\n          \"run error\",\n          \"exclude block\",\n          \"ancestor error\"\n        ]\n      },\n      \"Cause\": {\n        \"type\": \"string\"\n      },\n      \"Name\": {\n        \"type\": \"string\"\n      },\n      \"Result\": {\n        \"type\": \"string\",\n        \"enum\": [\n          \"succeeded\",\n          \"failed\",\n          \"early exit\",\n          \"excluded\"\n        ]\n      },\n      \"Ref\": {\n        \"type\": \"string\"\n      },\n      \"Cmd\": {\n        \"type\": \"string\"\n      },\n      \"Args\": {\n        \"items\": {\n          \"type\": \"string\"\n        },\n        \"type\": \"array\"\n      }\n    },\n    \"additionalProperties\": false,\n    \"type\": \"object\",\n    \"required\": [\n      \"Started\",\n      \"Ended\",\n      \"Name\",\n      \"Result\"\n    ],\n    \"title\": \"Terragrunt Run Report Schema\",\n    \"description\": \"Schema for Terragrunt run report\"\n  },\n  \"type\": \"array\",\n  \"title\": \"Terragrunt Run Report Schema\",\n  \"description\": \"Array of Terragrunt runs\"\n}\n"
  },
  {
    "path": "docs/public/schemas/run/report/v4/schema.json",
    "content": "{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"https://docs.terragrunt.com/schemas/run/report/v4/schema.json\",\n  \"items\": {\n    \"properties\": {\n      \"Started\": {\n        \"type\": \"string\",\n        \"format\": \"date-time\"\n      },\n      \"Ended\": {\n        \"type\": \"string\",\n        \"format\": \"date-time\"\n      },\n      \"Reason\": {\n        \"type\": \"string\",\n        \"enum\": [\n          \"retry succeeded\",\n          \"error ignored\",\n          \"run error\",\n          \"exclude block\",\n          \"ancestor error\"\n        ]\n      },\n      \"Cause\": {\n        \"type\": \"string\"\n      },\n      \"Name\": {\n        \"type\": \"string\"\n      },\n      \"Result\": {\n        \"type\": \"string\",\n        \"enum\": [\n          \"succeeded\",\n          \"failed\",\n          \"early exit\",\n          \"excluded\"\n        ]\n      },\n      \"Ref\": {\n        \"type\": \"string\"\n      },\n      \"Cmd\": {\n        \"type\": \"string\"\n      },\n      \"Args\": {\n        \"items\": {\n          \"type\": \"string\"\n        },\n        \"type\": \"array\"\n      }\n    },\n    \"additionalProperties\": false,\n    \"type\": \"object\",\n    \"required\": [\n      \"Started\",\n      \"Ended\",\n      \"Name\",\n      \"Result\"\n    ],\n    \"title\": \"Terragrunt Run Report Schema\",\n    \"description\": \"Schema for Terragrunt run report\"\n  },\n  \"type\": \"array\",\n  \"title\": \"Terragrunt Run Report Schema\",\n  \"description\": \"Array of Terragrunt runs\"\n}\n"
  },
  {
    "path": "docs/src/assets/icons/terragrunt-icon-accent.astro",
    "content": "---\n\n---\n\n<svg width=\"137\" height=\"138\" viewBox=\"0 0 137 138\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n  <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M68.7321 0.869873L128.22 35.1199V103.62L68.7321 137.87L9.24414 103.62V35.1199L68.7321 0.869873ZM121.798 38.8172L68.7321 8.26463L15.666 38.8172V99.9225L68.7321 130.475L121.798 99.9225V38.8172Z\" fill=\"#160C56\"/>\n  <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M68.7321 130.475V99.9228V69.3701L42.199 54.0938L15.666 38.8175V69.3701V99.9228L68.7321 130.475Z\" fill=\"white\"/>\n  <path d=\"M95.2651 115.199L121.798 99.9228L95.2651 84.6465L68.7321 99.9228V130.475L95.2651 115.199Z\" fill=\"#87E0E1\"/>\n  <path d=\"M68.7321 69.3701V99.9228L95.2651 84.6465L68.7321 69.3701Z\" fill=\"#4F2FD0\"/>\n  <path d=\"M95.2651 54.0938L68.7321 69.3701L95.2651 84.6465L121.798 69.3701L95.2651 54.0938Z\" fill=\"#7B5AFF\"/>\n  <path d=\"M95.2651 23.5412L68.7321 8.26489L42.199 23.5412L15.666 38.8175L42.199 54.0938L68.7321 38.8175L95.2651 54.0938L121.798 38.8175L95.2651 23.5412Z\" fill=\"#F9DB4E\"/>\n  <path d=\"M68.7321 69.3701L95.2651 54.0938L68.7321 38.8175L42.199 54.0938L68.7321 69.3701Z\" fill=\"#E94A5D\"/>\n</svg>\n"
  },
  {
    "path": "docs/src/components/Command.astro",
    "content": "---\nimport { Aside, Code } from '@astrojs/starlight/components';\nimport { getEntry, render } from 'astro:content';\nimport type { CollectionEntry } from 'astro:content';\nimport Flag from './Flag.astro';\n\nconst { path } = Astro.props;\n\nconst command = await getEntry('commands', path) as CollectionEntry<'commands'>;\n\nconst data = command?.data;\n\nconst usage = data?.usage;\nconst { Content } = await render(command);\n---\n\n{\n  data?.experiment ? (\n    <Aside type=\"tip\" title={data?.experiment?.name}>\n      The <code dir=\"auto\">{data?.name}</code> command is experimental, usage requires the <a href={\"/reference/experiments#\"+data?.experiment?.control}><code dir=\"auto\">--experiment {data?.experiment?.control}</code></a> flag.\n    </Aside>\n  ) : null\n}\n\n<h2 id=\"usage\">Usage</h2>\n\n{\n  usage?.split('\\n').map((line) => (\n    <p>{line}</p>\n  ))\n}\n\n{\n  data?.examples ? (\n  <h2 id=\"examples\">Examples</h2>\n  <div>\n    {data?.examples.map((example) => (\n      example && (\n        <p>{example?.description}</p>\n        <Code code={example?.code} lang=\"bash\" />\n      )\n    ))}\n  </div>\n  ) : null\n}\n\n<Content />\n\n{\n  data?.flags ? (\n  <h2 id=\"flags\">Flags</h2>\n  <div>\n    {data?.flags.map((flagSlug) => (\n      flagSlug && <Flag slug={flagSlug} />\n    ))}\n  </div>\n  ) : null\n}\n"
  },
  {
    "path": "docs/src/components/CompactFooter.astro",
    "content": "---\nimport GruntworkLogo from '@assets/gruntwork-logo.svg';\nimport { Image } from 'astro:assets';\nimport PatternDots from '@assets/pattern-dots.png';\n\nimport '@styles/global.css';\n---\n<footer class=\"relative flex flex-col bg-[#FAFAFA] border-b border-solid border-gray-3\">\n\n    <!-- Divider -->\n    <div class=\"relative w-full h-[13px] bg-gradient-to-l from-[#4F2FD0] to-bg-dark overflow-hidden\">\n      <Image\n        src={PatternDots}\n        alt=\"Pattern Dots\"\n        class=\"absolute inset-0 w-full h-full object-cover z-0 pointer-events-none select-none\"\n      />\n    </div>\n\n    <!-- Copyright -->\n    <div class=\"flex flex-col md:flex-row items-start md:items-center md:justify-between py-6 px-6 bg-[#FAFAFA] mt-auto gap-3\">\n      <div class=\"flex items-center gap-2\">\n        <Image src={GruntworkLogo} alt=\"Gruntwork Logo\" class=\"w-[30px] h-[30px]\" />\n        <span class=\"text-sm text-[#777888]\">From the DevOps experts at\n          <a href=\"https://gruntwork.io\" class=\"text-accent-1 no-underline border-b border-gray-1 hover:cursor-pointer hover:text-accent transition duration-100 ease-in-out\">\n            Gruntwork\n          </a>\n        </span>\n      </div>\n      <div>\n        <p class=\"text-center text-sm text-gray-1 select-none\">\n          &copy; {new Date().getFullYear()} Gruntwork, Inc. All rights reserved.\n        </p>\n      </div>\n    </div>\n\n</footer>\n"
  },
  {
    "path": "docs/src/components/CompatibilityTable.astro",
    "content": "---\nimport { getCollection } from 'astro:content';\n\ninterface Props {\n  tool: 'opentofu' | 'terraform';\n}\n\nconst { tool } = Astro.props;\nconst entries = (await getCollection('compatibility'))\n  .filter(e => e.data.tool === tool)\n  .sort((a, b) => b.data.order - a.data.order);\n\nconst label = tool === 'opentofu' ? 'OpenTofu' : 'Terraform';\nconst baseUrl = 'https://github.com/gruntwork-io/terragrunt/releases/tag/v';\n---\n\n<table>\n  <thead>\n    <tr>\n      <th>{label} Version</th>\n      <th>Terragrunt Version</th>\n    </tr>\n  </thead>\n  <tbody>\n    {entries.map(e => (\n      <tr>\n        <td>{e.data.version}</td>\n        <td>\n          {e.data.terragrunt_max ? (\n            <><a href={`${baseUrl}${e.data.terragrunt_min}`}>{e.data.terragrunt_min}</a> - <a href={`${baseUrl}${e.data.terragrunt_max}`}>{e.data.terragrunt_max}</a></>\n          ) : (\n            <>&gt;= <a href={`${baseUrl}${e.data.terragrunt_min}`}>{e.data.terragrunt_min}</a></>\n          )}\n        </td>\n      </tr>\n    ))}\n  </tbody>\n</table>\n"
  },
  {
    "path": "docs/src/components/Flag.astro",
    "content": "---\nimport { Badge } from '@astrojs/starlight/components';\nimport Card from '@components/vendored/starlight/Card.astro';\nimport type { CollectionEntry } from 'astro:content';\nimport { render, getEntry } from 'astro:content';\n\ninterface Props {\n  slug: string;\n}\n\nconst { slug } = Astro.props;\n\nconst flagEntry = await getEntry('flags', slug) as CollectionEntry<'flags'>;\nconst { Content } = await render(flagEntry);\nconst { aliases, description, type, env, name, defaultVal } = flagEntry.data;\n---\n\n<section id={name}>\n  <Card title={\"--\" + name} icon=\"custom:terragrunt\">\n    <p id={name}>{description}</p>\n    <Content />\n    <div>\n      <span>Type: </span><Badge text={type} variant=\"note\" style={{ fontWeight: 'bold' }}/>\n    </div>\n    {defaultVal && (\n      <div>\n        <span>Default: </span><Badge text={defaultVal} variant=\"tip\" style={{ fontWeight: 'bold' }}/>\n      </div>\n    )}\n    {aliases && aliases.length > 0 && (\n      <>\n        <p>Aliases:</p>\n        <ul>\n          {aliases.map((alias: string) => (\n            <li><code dir=\"auto\">{alias}</code></li>\n          ))}\n        </ul>\n      </>\n    )}\n    {env && (\n      <>\n        <p>Environment Variables:</p>\n        <ul>\n          {env.map((envVar: string) => (\n            <li><code dir=\"auto\">{envVar}</code></li>\n          ))}\n        </ul>\n      </>\n    )}\n  </Card>\n</section>\n"
  },
  {
    "path": "docs/src/components/Header.astro",
    "content": "---\nimport config from 'virtual:starlight/user-config';\nimport { Image } from 'astro:assets';\n// @ts-ignore\nimport Search from 'virtual:starlight/components/Search';\n\nimport HeadphonesIcon from '@assets/headset-icon.svg';\nimport PipelineIcon from '@assets/pipelines.svg';\nimport TerragruntLogo from '@assets/horizontal-logo-light.svg';\nimport ThemeToggle from '@components/ThemeToggle.astro';\nimport ButtonLink from '@components/ui/ButtonLink';\nimport { getGitHubRepo, formatStarCount } from '@lib/github';\n\nimport '@styles/global.css';\n\n/**\n * Render the `Search` component if Pagefind is enabled or the default search component has been overridden.\n */\nconst shouldRenderSearch =\n\tconfig.pagefind || config.components.Search !== '@astrojs/starlight/components/Search.astro';\n\n// This will switch between dark and light mode.\n// Some pages do not have a dark mode, so this allows us to show or hide the switch.\ninterface Props {\n\tshowThemeToggle?: boolean;\n}\n\nconst { showThemeToggle = true } = Astro.props;\n\n// Github Stars\nlet starCountDisplay = process.env.GITHUB_STARS || '9.2k';\n\nif (!process.env.GITHUB_STARS) {\n    const repoData = await getGitHubRepo('gruntwork-io', 'terragrunt');\n    if (repoData && typeof repoData.stargazers_count === 'number') {\n        starCountDisplay = formatStarCount(repoData.stargazers_count);\n    }\n}\n---\n\n<!--\n  This component is inheriting `data-theme=\"dark\"` or `data-theme=\"light\"` from the root element.\n  But the problem is that based on the design, we always want this header to behave as if it's in dark mode.\n  But that's tricky because dark mode colors are set in the CSS \"theme\" layer, which is then inherited by this component,\n  and this component can't change those variables directly.\n\n  To solve this, we override the variables causing us trouble.\n-->\n<style>\n  .header-dark-theme {\n    --sl-color-gray-1: #e8eefc;\n    --sl-color-gray-2: #bbc2d4;\n    --sl-color-gray-3: #7e8bac;\n    --sl-color-gray-4: #4c5776;\n    --sl-color-gray-5: #2d3754;\n    --sl-color-gray-6: #1c2541;\n    --sl-color-gray-7: #0f1731;\n    --sl-color-gray-8: #0f0934;\n    --sl-color-white: #ffffff;\n  }\n\n  :global(body.has-banner) .navbar {\n    top: 2.5rem;\n  }\n</style>\n\n<div class=\"navbar header-dark-theme fixed top-0 w-full h-[var(--sl-nav-height)] p-0 z-50 border-b border-stroke-dark bg-bg-dark\">\n  <div class=\"flex flex-row justify-between px-4 py-2.5 md:py-6 xl:h-full xl:mt-1\">\n\n    <!-- Logo -->\n    <a href=\"https://terragrunt.com\" class=\"flex flex-row items-center hover:cursor-pointer mr-2\">\n      <Image\n        alt=\"Terragrunt Logo\"\n        class=\"shrink h-auto max-w-[145px] md:max-w-[180px] xl:max-w-[200px]\"\n        loading=\"eager\"\n        src={TerragruntLogo}\n      />\n    </a>\n\n    <!-- Right Menu -->\n    <div class=\"flex items-center xl:gap-4\">\n      <div class=\"flex items-center gap-2 xl:gap-4\">\n\n        <!-- Docs / Ambassadors -->\n        <div class=\"flex items-center gap-4 xl:gap-4 mr-2\">\n\n          <!-- Docs -->\n          <!--\n          <a href=\"/\" class=\"flex items-center gap-2 text-gray-1 text-sm font-sans no-underline hover:underline hover:decoration-[var(--sl-color-gray-1)] hover:text-[var(--sl-color-gray-1)]\" aria-label=\"Terragrunt Documentation\">\n            <svg class=\"\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\" xmlns=\"http://www.w3.org/2000/svg\">\n              <path opacity=\"0.1\" d=\"M17.8284 6.82843C18.4065 7.40649 18.6955 7.69552 18.8478 8.06306C19 8.4306 19 8.83935 19 9.65685L19 17C19 18.8856 19 19.8284 18.4142 20.4142C17.8284 21 16.8856 21 15 21H9C7.11438 21 6.17157 21 5.58579 20.4142C5 19.8284 5 18.8856 5 17L5 7C5 5.11438 5 4.17157 5.58579 3.58579C6.17157 3 7.11438 3 9 3H12.3431C13.1606 3 13.5694 3 13.9369 3.15224C14.3045 3.30448 14.5935 3.59351 15.1716 4.17157L17.8284 6.82843Z\" fill=\"currentColor\"/>\n              <path d=\"M17.8284 6.82843C18.4065 7.40649 18.6955 7.69552 18.8478 8.06306C19 8.4306 19 8.83935 19 9.65685L19 17C19 18.8856 19 19.8284 18.4142 20.4142C17.8284 21 16.8856 21 15 21H9C7.11438 21 6.17157 21 5.58579 20.4142C5 19.8284 5 18.8856 5 17L5 7C5 5.11438 5 4.17157 5.58579 3.58579C6.17157 3 7.11438 3 9 3H12.3431C13.1606 3 13.5694 3 13.9369 3.15224C14.3045 3.30448 14.5935 3.59351 15.1716 4.17157L17.8284 6.82843Z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\"/>\n              <path d=\"M9 12H15\" stroke=\"currentColor\" stroke-width=\"1\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n              <path d=\"M9 15H15\" stroke=\"currentColor\" stroke-width=\"1\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n            </svg>\n            <span class=\"hidden sm:block\">Docs</span>\n          </a>\n          -->\n\n          <!-- Ambassadors -->\n          <a href=\"/terragrunt-ambassador\" class=\"flex items-center gap-2 text-gray-1 text-sm font-sans no-underline hover:underline hover:decoration-[var(--sl-color-gray-1)] hover:text-[var(--sl-color-gray-1)]\" aria-label=\"Learn more about the Terragrunt Ambassador Program\">\n            <svg class=\"\" width=\"24\" height=\"24\" viewBox=\"0 0 615 615\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n              <path d=\"M379.128 40.1797L461.25 41.1973L503.191 111.809L573.803 153.75L574.82 235.872L615 307.5L574.82 379.128L573.803 461.25L503.191 503.191L461.25 573.803L379.128 574.82L307.5 615L235.872 574.82L153.75 573.803L111.809 503.191L41.1973 461.25L40.1797 379.128L0 307.5L40.1797 235.872L41.1973 153.75L111.809 111.809L153.75 41.1973L235.872 40.1797L307.5 0L379.128 40.1797ZM139.792 211V404L307.417 500.5L475.042 404V211L307.417 114.5L139.792 211Z\" fill=\"currentColor\"/>\n              <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M306.931 457.048V382.116V307.184L372.005 269.718L437.079 232.252V307.184V382.116L306.931 457.048Z\" fill=\"currentColor\"/>\n              <path d=\"M241.858 194.786L306.931 157.32L372.005 194.786L437.079 232.252L372.005 269.718L306.931 232.252L241.858 269.718L176.784 232.252L241.858 194.786Z\" fill=\"currentColor\"/>\n              <path d=\"M306.931 307.184L241.858 269.718L306.931 232.252L372.005 269.718L306.931 307.184Z\" fill=\"currentColor\"/>\n            </svg>\n            <span class=\"hidden sm:block\">Ambassadors</span>\n          </a>\n        </div>\n\n        <!-- Socials -->\n        <div class=\"flex xl:border-r border-stroke-dark items-center gap-4 xl:gap-4 pr-4\">\n\n          <!-- Github -->\n          <a\n            aria-label=\"Visit the Terragrunt GitHub Repository\"\n            class=\"social-icon flex items-center gap-3 transition duration-[250ms] ease-in-out no-underline\"\n            href=\"https://github.com/gruntwork-io/terragrunt\"\n            rel=\"noopener noreferrer\"\n            target=\"_blank\"\n          >\n            <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"currentColor\" class=\"bi bi-github\" viewBox=\"0 0 16 16\" aria-hidden=\"true\">\n              <path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8\"/>\n            </svg>\n            <span class=\"hidden xl:block text-sm font-sans\">{starCountDisplay}</span>\n          </a>\n\n          <!-- Discord -->\n          <a\n            aria-label=\"Join the Terragrunt Discord\"\n            class=\"social-icon flex items-center gap-3 transition duration-[250ms] ease-in-out\"\n            href=\"/community/invite\"\n            rel=\"noopener noreferrer\"\n            target=\"_blank\"\n          >\n            <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" fill=\"currentColor\" class=\"bi bi-discord\" viewBox=\"0 0 16 16\" aria-hidden=\"true\">\n              <path d=\"M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612\"/>\n            </svg>\n          </a>\n\n          {showThemeToggle && <ThemeToggle className=\"p-0\" />}\n        </div>\n      </div>\n\n      <div id=\"search-and-buttons\" class=\"flex items-center w-full absolute bottom-4 xl:bottom-0 left-0 xl:relative gap-2 xl:gap-4 px-4 xl:px-0 h-[46px]\">\n\n        <!-- SearchBar -->\n        <div class=\"search-container bg-bg-dark w-full h-full xl:w-[200px]\">\n          {shouldRenderSearch && <Search {...Astro.props} />}\n        </div>\n\n        <!-- Buttons -->\n        <ButtonLink\n          href=\"/terragrunt-scale\"\n          variant=\"secondary\"\n          className=\"hidden md:flex whitespace-nowrap\"\n          size={'lg'}\n        >\n          Automate Deployments\n        </ButtonLink>\n        <ButtonLink\n          href=\"/terragrunt-scale\"\n          variant=\"secondary\"\n          size=\"icon\"\n          className=\"md:hidden\"\n        >\n          <Image\n            alt=\"Pipelines Icon\"\n            height={20}\n            src={PipelineIcon}\n            width={20}\n          />\n        </ButtonLink>\n\n        <ButtonLink\n          href=\"https://www.gruntwork.io/services/terragrunt\"\n          variant=\"primary\"\n          className=\"hidden md:flex whitespace-nowrap\"\n          isExternalLink={true}\n          size={'lg'}\n        >\n          Enterprise Support\n        </ButtonLink>\n\n        <ButtonLink\n          href=\"https://www.gruntwork.io/services/terragrunt\"\n          variant=\"primary\"\n          size=\"icon\"\n          className=\"md:hidden\"\n          isExternalLink={true}\n        >\n          <Image\n            alt=\"Headphones Icon\"\n            height={20}\n            src={HeadphonesIcon}\n            width={20}\n          />\n        </ButtonLink>\n      </div>\n\n    </div>\n  </div>\n</div>\n\n<script src=\"https://www.googletagmanager.com/gtm.js?id=GTM-5TTJJGTL\" type=\"text/javascript\"></script>\n<script type=\"text/javascript\">\n  (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-5TTJJGTL')\n</script>\n"
  },
  {
    "path": "docs/src/components/InstallTab.astro",
    "content": "---\nimport { TabItem } from '@astrojs/starlight/components';\nimport { Code } from '@astrojs/starlight/components';\n\nconst { os, arch, label, version } = Astro.props;\n---\n\n{os === 'windows' ? (\n<TabItem label={label}>\n<Code\n  lang=\"powershell\"\n  code={`$os = \"${os}\"\n$arch = \"${arch}\"\n$version = \"${version}\"\n$binaryName = \"terragrunt_\\${os}_\\${arch}.exe\"\ntry {\n    $ProgressPreference = 'SilentlyContinue'\n    $baseUrl = \"https://github.com/gruntwork-io/terragrunt/releases/download/$version\"\n    Write-Host \"Downloading Terragrunt $version...\"\n    Invoke-WebRequest -Uri \"$baseUrl/$binaryName\" -OutFile $binaryName -UseBasicParsing\n    Invoke-WebRequest -Uri \"$baseUrl/SHA256SUMS\" -OutFile \"SHA256SUMS\" -UseBasicParsing\n    Invoke-WebRequest -Uri \"$baseUrl/SHA256SUMS.gpgsig\" -OutFile \"SHA256SUMS.gpgsig\" -UseBasicParsing\n\n    # First: Verify GPG signature of checksum file (requires gpg installed)\n    Write-Host \"Importing Gruntwork signing key...\"\n    Invoke-WebRequest -Uri \"https://gruntwork.io/.well-known/pgp-key.txt\" -OutFile \"pgp-key.txt\" -UseBasicParsing\n    gpg --import pgp-key.txt 2>$null\n    Write-Host \"Verifying GPG signature of SHA256SUMS...\"\n    gpg --verify SHA256SUMS.gpgsig SHA256SUMS\n    if ($LASTEXITCODE -ne 0) {\n        Write-Error \"GPG signature verification failed\"\n        exit 1\n    }\n    Write-Host \"GPG signature verified!\"\n\n    # Second: Verify checksum of binary against trusted SHA256SUMS\n    $actualChecksum = (Get-FileHash -Algorithm SHA256 $binaryName).Hash.ToLower()\n    $expectedChecksum = (Get-Content \"SHA256SUMS\" | ForEach-Object { $parts = $_ -split '\\s+'; if ($parts[1] -eq $binaryName) { return $parts[0].ToLower() } } | Select-Object -First 1)\n    if ($actualChecksum -ne $expectedChecksum) {\n        Write-Error \"Checksum verification failed\"\n        exit 1\n    }\n    Write-Host \"Checksum verified!\"\n    Write-Host \"Terragrunt $version downloaded and verified successfully\"\n}\ncatch {\n    Write-Error \"Failed: $_\"\n    exit 1\n}\nfinally {\n    $ProgressPreference = 'Continue'\n}\n`}\n  frame='terminal'\n\t>\n</Code>\n</TabItem>\n) : (\n<TabItem label={label}>\n<Code\n  lang=\"bash\"\n  code={`\nset -euo pipefail\n\nOS=\"${os}\"\nARCH=\"${arch}\"\nVERSION=\"${version}\"\nBINARY_NAME=\"terragrunt_\\${OS}_\\${ARCH}\"\nBASE_URL=\"https://github.com/gruntwork-io/terragrunt/releases/download/\\$VERSION\"\n\n# Download binary and verification files\ncurl -sL \"\\$BASE_URL/\\$BINARY_NAME\" -o \"\\$BINARY_NAME\"\ncurl -sL \"\\$BASE_URL/SHA256SUMS\" -o SHA256SUMS\ncurl -sL \"\\$BASE_URL/SHA256SUMS.gpgsig\" -o SHA256SUMS.gpgsig\n\n# First: Import Gruntwork signing key and verify GPG signature of checksum file\ncurl -s https://gruntwork.io/.well-known/pgp-key.txt | gpg --import 2>/dev/null\nif gpg --verify SHA256SUMS.gpgsig SHA256SUMS 2>/dev/null; then\n  echo \"GPG signature verified!\"\nelse\n  echo \"GPG signature verification failed!\"\n  exit 1\nfi\n\n# Second: Verify checksum of binary against trusted SHA256SUMS\nCHECKSUM=\"\\$(${os == 'linux' ? 'sha256sum' : 'shasum -a 256'} \"\\$BINARY_NAME\" | awk '{print \\$1}')\"\nEXPECTED_CHECKSUM=\"\\$(awk -v binary=\"\\$BINARY_NAME\" '\\$2 == binary {print \\$1; exit}' SHA256SUMS)\"\n\nif [ \"\\$CHECKSUM\" != \"\\$EXPECTED_CHECKSUM\" ]; then\n  echo \"Checksum verification failed!\"\n  exit 1\nfi\necho \"Checksum verified!\"\n\necho \"Terragrunt \\$VERSION downloaded and verified successfully\"`}\n  frame='terminal'\n\t>\n</Code>\n</TabItem>\n)}\n\n"
  },
  {
    "path": "docs/src/components/InstallTabs.astro",
    "content": "---\nimport { Tabs } from '@astrojs/starlight/components';\nimport InstallTab from './InstallTab.astro';\n\nconst tabs = [\n\t{\n\t\tos: 'linux',\n\t\tarch: 'amd64',\n\t\tlabel: 'Linux (x86)',\n\t},\n\t{\n\t\tos: 'darwin',\n\t\tarch: 'arm64',\n\t\tlabel: 'macOS (ARM)',\n\t},\n\t{\n\t\tos: 'windows',\n\t\tarch: 'amd64',\n\t\tlabel: 'Windows',\n\t},\n\t{\n\t\tos: 'linux',\n\t\tarch: 'arm64',\n\t\tlabel: 'Linux (ARM)',\n\t},\n\t{\n\t\tos: 'darwin',\n\t\tarch: 'x86',\n\t\tlabel: 'macOS (x86)',\n\t},\n];\n\nconst { version } = Astro.props;\n---\n\n<Tabs syncKey=\"operating-systems\">\n{tabs.map(({ os, arch, label }) => (\n\t<InstallTab os={os} arch={arch} label={label} version={version} />\n))}\n</Tabs>\n"
  },
  {
    "path": "docs/src/components/PageSidebar.astro",
    "content": "---\nimport DefaultPageSidebar from '@astrojs/starlight/components/PageSidebar.astro';\nimport { Icon } from '@astrojs/starlight/components';\n---\n\n<div class=\"h-full min-[1152px]:max-w-[400px] min-[1152px]:fixed border-r border-dashed border-gray-3 dark:border-stroke-dark\">\n\n  <DefaultPageSidebar>\n    <slot />\n  </DefaultPageSidebar>\n\n  <!-- Terragrunt Scale CTA -->\n  <div class=\"hidden min-[1152px]:block\">\n    <div class=\"w-full h-[32px] bg-[url(/images/pattern-div.svg)]\"></div>\n    <div class=\"m-0 p-6 border-y border-dashed border-gray-3 dark:border-stroke-dark\">\n      <div class=\"flex flex-col gap-3\">\n        <p class=\"text-primary font-medium text-xs leading-[150%] dark:text-white\">\n          Scale up your Terragrunt\n        </p>\n        <p class=\"text-xs text-gray-1 dark:text-gray-1\">\n          GitOps infrastructure pipelines, drift detection, and automated updates. No black boxes.\n        </p>\n        <a class=\"text-xs flex flex-row items-center\" href=\"https://docs.terragrunt.com/terragrunt-scale\">\n          Learn about Terragrunt Scale\n          <Icon name=\"right-arrow\" class=\"shrink-0\" />\n        </a>\n      </div>\n    </div>\n  </div>\n\n</div>\n"
  },
  {
    "path": "docs/src/components/SectionSpacer.astro",
    "content": "---\n---\n\n<div class=\"w-full h-[150px]\"></div>\n"
  },
  {
    "path": "docs/src/components/SiteTitle.astro",
    "content": "---\nimport { logos } from 'virtual:starlight/user-images';\nimport config from 'virtual:starlight/user-config';\nimport type { Props } from '@astrojs/starlight/props';\nconst { siteTitleHref } = Astro.props;\n---\n\n<a href=\"https://terragrunt.com\" class=\"site-title sl-flex\">\n\t{\n\t\tconfig.logo && logos.dark && (\n\t\t\t<>\n\t\t\t\t<img\n\t\t\t\t\tclass:list={{ 'light:sl-hidden print:hidden light-logo': !('src' in config.logo) }}\n\t\t\t\t\talt={config.logo.alt}\n\t\t\t\t\tsrc={logos.dark.src}\n\t\t\t\t\twidth={logos.dark.width}\n\t\t\t\t\theight={logos.dark.height}\n\t\t\t\t/>\n\t\t\t\t{/* Show light alternate if a user configure both light and dark logos. */}\n\t\t\t\t{!('src' in config.logo) && (\n\t\t\t\t\t<img\n\t\t\t\t\t\tclass=\"dark:sl-hidden print:block dark-logo\"\n\t\t\t\t\t\talt={config.logo.alt}\n\t\t\t\t\t\tsrc={logos.light?.src}\n\t\t\t\t\t\twidth={logos.light?.width}\n\t\t\t\t\t\theight={logos.light?.height}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t</>\n\t\t)\n\t}\n</a>\n\n<style>\n\t.site-title {\n\t\talign-items: center;\n\t\tgap: var(--sl-nav-gap);\n\t\tfont-size: var(--sl-text-h4);\n\t\tfont-weight: 600;\n\t\tcolor: var(--sl-color-text-accent);\n\t\ttext-decoration: none;\n\t\twhite-space: nowrap;\n\t}\n\timg {\n\t\theight: calc(var(--sl-nav-height) - 2 * var(--sl-nav-pad-y));\n\t\twidth: auto;\n\t\tmax-width: 100%;\n\t\tmin-width: var(--sl-nav-height);\n\t\tobject-fit: contain;\n\t\tobject-position: 0 50%;\n\t}\n\n\t@media (max-width: 768px) {\n\t\timg.dark-logo {\n\t\t\tcontent: url('/src/assets/logo-dark.svg');\n\t\t}\n\n\t\timg.light-logo {\n\t\t\tcontent: url('/src/assets/logo-light.svg');\n\t\t}\n\t}\n</style>\n"
  },
  {
    "path": "docs/src/components/SkipLink.astro",
    "content": "---\nimport DefaultSkipLink from '@astrojs/starlight/components/SkipLink.astro';\n---\n\n<DefaultSkipLink>\n  <slot />\n</DefaultSkipLink>\n"
  },
  {
    "path": "docs/src/components/ThemeToggle.astro",
    "content": "---\n\nconst {\n  className: customClass = \"\",\n  id: uniqueId = `themeToggle-${Math.random().toString(36).substr(2, 9)}`\n} = Astro.props;\n\n---\n\n<button id={uniqueId} data-theme-toggle aria-label=\"Toggle theme\" class={`flex items-center bg-transparent cursor-pointer border-0 ${customClass}`}>\n  <svg width=\"24px\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\">\n    <path class=\"moon\" fill-rule=\"evenodd\" d=\"M12 17.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zm0 1.5a7 7 0 1 0 0-14 7 7 0 0 0 0 14zm12-7a.8.8 0 0 1-.8.8h-2.4a.8.8 0 0 1 0-1.6h2.4a.8.8 0 0 1 .8.8zM4 12a.8.8 0 0 1-.8.8H.8a.8.8 0 0 1 0-1.6h2.5a.8.8 0 0 1 .8.8zm16.5-8.5a.8.8 0 0 1 0 1l-1.8 1.8a.8.8 0 0 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM6.3 17.7a.8.8 0 0 1 0 1l-1.7 1.8a.8.8 0 1 1-1-1l1.7-1.8a.8.8 0 0 1 1 0zM12 0a.8.8 0 0 1 .8.8v2.5a.8.8 0 0 1-1.6 0V.8A.8.8 0 0 1 12 0zm0 20a.8.8 0 0 1 .8.8v2.4a.8.8 0 0 1-1.6 0v-2.4a.8.8 0 0 1 .8-.8zM3.5 3.5a.8.8 0 0 1 1 0l1.8 1.8a.8.8 0 1 1-1 1L3.5 4.6a.8.8 0 0 1 0-1zm14.2 14.2a.8.8 0 0 1 1 0l1.8 1.7a.8.8 0 0 1-1 1l-1.8-1.7a.8.8 0 0 1 0-1z\"/>\n    <path class=\"sun\" fill-rule=\"evenodd\" d=\"M16.5 6A10.5 10.5 0 0 1 4.7 16.4 8.5 8.5 0 1 0 16.4 4.7l.1 1.3zm-1.7-2a9 9 0 0 1 .2 2 9 9 0 0 1-11 8.8 9.4 9.4 0 0 1-.8-.3c-.4 0-.8.3-.7.7a10 10 0 0 0 .3.8 10 10 0 0 0 9.2 6 10 10 0 0 0 4-19.2 9.7 9.7 0 0 0-.9-.3c-.3-.1-.7.3-.6.7a9 9 0 0 1 .3.8z\"/>\n  </svg>\n</button>\n\n<style>\n  .sun { fill: white; }\n  .moon { fill: transparent; }\n  :global([data-theme=\"dark\"]) .sun { fill: transparent; }\n  :global([data-theme=\"dark\"]) .moon { fill: white; }\n</style>\n\n<script>\n  // Theme switching functionality\n  (function() {\n    const themeToggles = document.querySelectorAll('[data-theme-toggle]');\n    const html = document.documentElement;\n    const THEME_STORAGE_KEY = 'starlight-theme';\n\n    // Function to get user's system preference\n    function getSystemTheme() {\n      return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n    }\n\n    // Function to get stored theme or system preference\n    function getCurrentTheme() {\n      const stored = localStorage.getItem(THEME_STORAGE_KEY);\n      if (stored === 'light' || stored === 'dark') {\n        return stored;\n      }\n      return getSystemTheme();\n    }\n\n    // Function to apply theme\n    function applyTheme(theme: any) {\n      html.setAttribute('data-theme', theme);\n    }\n\n    // Function to set theme and store preference\n    function setTheme(theme: any) {\n      applyTheme(theme);\n      localStorage.setItem(THEME_STORAGE_KEY, theme);\n    }\n\n    // Function to toggle theme\n    function toggleTheme() {\n      const currentTheme = getCurrentTheme();\n      const newTheme = currentTheme === 'light' ? 'dark' : 'light';\n      setTheme(newTheme);\n    }\n\n    // Initialize theme on page load\n    function initializeTheme() {\n      const theme = getCurrentTheme();\n      applyTheme(theme);\n    }\n\n    // Listen for system theme changes\n    function handleSystemThemeChange(e: any) {\n      // Only update if user hasn't set a preference\n      if (!localStorage.getItem(THEME_STORAGE_KEY)) {\n        const newTheme = e.matches ? 'dark' : 'light';\n        applyTheme(newTheme);\n      }\n    }\n\n    // Add event listeners to all theme toggle buttons\n    themeToggles.forEach(toggle => {\n      toggle.addEventListener('click', toggleTheme);\n    });\n\n    // Listen for system theme changes\n    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n    mediaQuery.addEventListener('change', handleSystemThemeChange);\n\n    // Initialize theme immediately for first paint, and again after Astro navigations.\n    initializeTheme();\n    document.addEventListener('astro:page-load', initializeTheme);\n  })();\n</script>\n"
  },
  {
    "path": "docs/src/components/dv-IconButton.astro",
    "content": "---\nimport '@styles/global.css';\nimport { Image } from \"astro:assets\";\nimport type { ImageMetadata } from 'astro';\n\ninterface Props {\n    alt: string;\n    bgcolor?: string;\n    eager?: boolean;\n    src: ImageMetadata;\n    text: string;\n    textColor?: string;\n}\n\nconst {\n  alt,\n  bgcolor = '',\n  eager = false,\n  src,\n  text,\n  textColor = '',\n} = Astro.props as Props;\n\nconst loadingMode = eager ? 'eager' : 'lazy';\n---\n\n<div class={`flex border-stroke-dark border rounded-md p-1 pr-2.5 gap-1.5 items-center ${bgcolor}`}>\n  <Image\n    src={src}\n    alt={alt}\n    class=\"w-5 h-5 rounded-sm\"\n    loading={loadingMode}\n  />\n  <span class={`text-xs font-mono ${textColor}`}>{text}</span>\n</div>\n"
  },
  {
    "path": "docs/src/components/ui/Button.tsx",
    "content": "// Generated with 'npx shadcn@latest add button'\n// Customize this as needed!\n\nimport { cn } from \"../../lib/utils\";\nimport { ExternalLink } from \"lucide-react\";\n\nexport interface ButtonProps {\n  // TODO: Style secondary, ghost, outline, and link bvariants\n  variant?: \"primary\" | \"secondary\" | \"ghost\" | \"outline\" | \"destructive\" | \"link\";\n  size?: \"default\" | \"sm\" | \"lg\" | \"full\" | \"icon\";\n  className?: string;\n  children: React.ReactNode;\n  id?: string;\n  onClick?: () => void;\n  type?: \"button\" | \"submit\" | \"reset\";\n  isExternalLink?: boolean;\n}\n\nexport function isSizeIcon(size?: string): boolean {\n  return size === \"icon\";\n}\n\nexport default function Button({\n  variant = \"primary\",\n  size = \"default\",\n  className,\n  children,\n  onClick,\n  type = \"button\",\n  isExternalLink = false,\n  ...props\n}: ButtonProps) {\n  return (\n    <button\n      type={type}\n      id={props.id}\n      className={cn(\n        // Base styles\n        \"cursor-pointer py-2.5 px-5 m-0 font-medium\",\n        // Font smoothing for consistent rendering across pages\n        \"antialiased\",\n        // Border\n        \"border border-solid\",\n        // Focus\n        \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n        // Text\n        \"font-sans rounded-lg text-sm text-center leading-normal\",\n        // Box shadow\n        \"shadow-md ring-1 ring-white/5\",\n        // Outline properties\n        \"outline-none outline-offset-0\",\n        // Text decoration properties\n        \"no-underline\",\n        // Transitions with gradient variables and correct timing function\n        \"transition-[color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to] duration-150 ease-[cubic-bezier(0.4,0,0.2,1)]\",\n        // Variant styles\n        {\n          \"bg-[var(--color-accent-1)] text-[var(--sl-color-white)] hover:bg-[var(--sl-color-accent)] border-[var(--color-button-primary-border)]\": variant === \"primary\",\n          \"bg-[var(--color-bg-dark)] text-[var(--sl-color-white)] hover:bg-[var(--sl-color-gray-5)] border-gray-500\": variant === \"secondary\",\n          \"bg-red-500 text-white hover:bg-red-600 border-red-500\": variant === \"destructive\",\n          \"border border-[var(--sl-color-docs-stroke)] bg-[var(--sl-color-bg)] hover:bg-[var(--sl-color-gray-6)]\": variant === \"outline\",\n          \"hover:bg-[var(--sl-color-gray-6)] border-transparent\": variant === \"ghost\",\n          \"text-[var(--sl-color-accent)] underline-offset-4 hover:underline border-transparent\": variant === \"link\",\n        },\n        {\n          \"block w-fit\": size === \"default\",\n          \"block h-9 pt-2 pb-2 pl-3 pr-3 w-auto\": size === \"sm\",\n          \"block h-11 pt-2 pb-2 pl-8 pr-8 w-auto\": size === \"lg\",\n          \"block h-11 pt-2 pb-2 pl-8 pr-8 w-full\": size === \"full\",\n          \"flex w-auto p-3\": size === \"icon\",\n        },\n        className\n      )}\n      onClick={onClick}\n      {...props}\n    >\n      {children}\n      {isExternalLink && !isSizeIcon(size) && (\n        <ExternalLink\n          className=\"inline-block ml-1 transform translate-y-0.25 -translate-x-0.75 w-3.5 h-3.5\"\n          aria-label=\"External link icon\"\n        />\n      )}\n    </button>\n  );\n}\n"
  },
  {
    "path": "docs/src/components/ui/ButtonLink.tsx",
    "content": "// A plain Button is often used as a link, so rather than wrapping plain button in an <a> tag, we can use this component.\n\nimport { cn } from \"../../lib/utils\";\nimport Button from \"./Button\";\n\n// Extract the ButtonProps interface from Button component\ntype ButtonProps = React.ComponentProps<typeof Button>;\n\ninterface ButtonLinkProps extends ButtonProps {\n  buttonClassName?: string;\n  href?: string;\n  rel?: string;\n  target?: \"_blank\" | \"_self\" | \"_parent\" | \"_top\";\n}\n\nexport default function ButtonLink({\n  buttonClassName,\n  href,\n  rel,\n  target,\n  className,\n  ...buttonProps\n}: ButtonLinkProps) {\n\n  return (\n    <a\n      href={href}\n      target={target}\n      rel={rel}\n      className={cn(\n        // Override default link styling\n        \"no-underline\",\n        \"text-inherit\",\n        \"hover:no-underline\",\n        \"hover:text-inherit\",\n        // Ensure proper display\n        \"inline-block\",\n        className\n      )}\n    >\n      <Button {...buttonProps} className={buttonClassName} />\n    </a>\n  );\n}\n"
  },
  {
    "path": "docs/src/components/vendored/starlight/Card.astro",
    "content": "---\nimport Icon from './Icon.astro';\nimport type { StarlightIcon } from './Icons';\n\ninterface Props {\n\ticon?: StarlightIcon;\n\ttitle: string;\n}\n\nconst { icon, title } = Astro.props;\n---\n\n<article class=\"card sl-flex\">\n\t<p class=\"title sl-flex\">\n\t\t{icon && <Icon name={icon} class=\"icon\" size=\"1.333em\" />}\n\t\t<span set:html={title} />\n\t</p>\n\t<div class=\"body\"><slot /></div>\n</article>\n\n<style>\n\t@layer starlight.components {\n\t\t.card {\n\t\t\t--sl-card-border: var(--sl-color-purple);\n\t\t\t--sl-card-bg: var(--sl-color-purple-low);\n\t\t\tborder: 1px solid var(--sl-color-gray-5);\n\t\t\tbackground-color: var(--sl-color-black);\n\t\t\tpadding: clamp(1rem, calc(0.125rem + 3vw), 2.5rem);\n\t\t\tflex-direction: column;\n\t\t\tgap: clamp(0.5rem, calc(0.125rem + 1vw), 1rem);\n\t\t}\n\t\t.card:nth-child(4n + 1) {\n\t\t\t--sl-card-border: var(--sl-color-orange);\n\t\t\t--sl-card-bg: var(--sl-color-orange-low);\n\t\t}\n\t\t.card:nth-child(4n + 3) {\n\t\t\t--sl-card-border: var(--sl-color-green);\n\t\t\t--sl-card-bg: var(--sl-color-green-low);\n\t\t}\n\t\t.card:nth-child(4n + 4) {\n\t\t\t--sl-card-border: var(--sl-color-red);\n\t\t\t--sl-card-bg: var(--sl-color-red-low);\n\t\t}\n\t\t.card:nth-child(4n + 5) {\n\t\t\t--sl-card-border: var(--sl-color-blue);\n\t\t\t--sl-card-bg: var(--sl-color-blue-low);\n\t\t}\n\t\t.title {\n\t\t\tfont-weight: 600;\n\t\t\tfont-size: var(--sl-text-h4);\n\t\t\tcolor: var(--sl-color-white);\n\t\t\tline-height: var(--sl-line-height-headings);\n\t\t\tgap: 1rem;\n\t\t\talign-items: center;\n\t\t}\n\t\t.card .icon {\n\t\t\tborder: 1px solid var(--sl-card-border);\n\t\t\tbackground-color: var(--sl-card-bg);\n\t\t\tpadding: 0.2em;\n\t\t\tborder-radius: 0.25rem;\n\t\t\tflex-shrink: 0;\n\t\t}\n\t\t.card .body {\n\t\t\tmargin: 0;\n\t\t\tfont-size: clamp(var(--sl-text-sm), calc(0.5rem + 1vw), var(--sl-text-body));\n\t\t}\n\t}\n</style>\n"
  },
  {
    "path": "docs/src/components/vendored/starlight/FileTree.astro",
    "content": "---\nimport { processFileTree } from './rehype-file-tree';\n\nconst fileTreeHtml = await Astro.slots.render('default');\nconst html = processFileTree(fileTreeHtml, Astro.locals.t('fileTree.directory'));\n---\n\n<starlight-file-tree set:html={html} class=\"not-content\" data-pagefind-ignore />\n\n<style>\n\t@layer starlight.components {\n\t\tstarlight-file-tree {\n\t\t\t--x-space: 1.5rem;\n\t\t\t--y-space: 0.125rem;\n\t\t\t--y-pad: 0;\n\n\t\t\tdisplay: block;\n\t\t\tborder: 1px solid var(--sl-color-gray-5);\n\t\t\tpadding: 1rem;\n\t\t\tbackground-color: var(--sl-color-gray-6);\n\t\t\tfont-size: var(--sl-text-xs);\n\t\t\tfont-family: var(--__sl-font-mono);\n\t\t\toverflow-x: auto;\n\t\t}\n\n\t\tstarlight-file-tree :global(.directory > details) {\n\t\t\tborder: 0;\n\t\t\tpadding: 0;\n\t\t\tpadding-inline-start: var(--x-space);\n\t\t\tbackground: transparent;\n\t\t}\n\n\t\tstarlight-file-tree :global(.directory > details > summary) {\n\t\t\tmargin-inline-start: calc(-1 * var(--x-space));\n\t\t\tborder: 0;\n\t\t\tpadding: var(--y-pad) 0.625rem;\n\t\t\tfont-weight: normal;\n\t\t\tcolor: var(--sl-color-white);\n\t\t\tmax-width: 100%;\n\t\t}\n\n\t\tstarlight-file-tree :global(.directory > details > summary::marker),\n\t\tstarlight-file-tree :global(.directory > details > summary::-webkit-details-marker) {\n\t\t\tcolor: var(--sl-color-gray-3);\n\t\t}\n\n\t\tstarlight-file-tree :global(.directory > details > summary:hover),\n\t\tstarlight-file-tree :global(.directory > details > summary:hover .tree-icon) {\n\t\t\tcursor: pointer;\n\t\t\tcolor: var(--sl-color-text-accent);\n\t\t\tfill: currentColor;\n\t\t}\n\n\t\tstarlight-file-tree :global(.directory > details > summary:hover ~ ul) {\n\t\t\tborder-color: var(--sl-color-gray-4);\n\t\t}\n\n\t\tstarlight-file-tree :global(.directory > details > summary:hover .highlight .tree-icon) {\n\t\t\tcolor: var(--sl-color-text-invert);\n\t\t\tfill: currentColor;\n\t\t}\n\n\t\tstarlight-file-tree :global(ul) {\n\t\t\tmargin-inline-start: 0.5rem;\n\t\t\tborder-inline-start: 1px solid var(--sl-color-gray-5);\n\t\t\tpadding: 0;\n\t\t\tpadding-inline-start: 0.125rem;\n\t\t\tlist-style: none;\n\t\t}\n\n\t\tstarlight-file-tree > :global(ul) {\n\t\t\tmargin: 0;\n\t\t\tborder: 0;\n\t\t\tpadding: 0;\n\t\t}\n\n\t\tstarlight-file-tree :global(li) {\n\t\t\tmargin: var(--y-space) 0;\n\t\t\tpadding: var(--y-pad) 0;\n\t\t}\n\n\t\tstarlight-file-tree :global(.file) {\n\t\t\tmargin-inline-start: calc(var(--x-space) - 0.125rem);\n\t\t\tcolor: var(--sl-color-white);\n\t\t}\n\n\t\tstarlight-file-tree :global(.tree-entry) {\n\t\t\tdisplay: inline-flex;\n\t\t\talign-items: flex-start;\n\t\t\tflex-wrap: wrap;\n\t\t\tmax-width: calc(100% - 1rem);\n\t\t}\n\n\t\t@media (min-width: 30em) {\n\t\t\tstarlight-file-tree :global(.tree-entry) {\n\t\t\t\tflex-wrap: nowrap;\n\t\t\t}\n\t\t}\n\n\t\tstarlight-file-tree :global(.tree-entry > :first-child) {\n\t\t\tflex-shrink: 0;\n\t\t}\n\n\t\tstarlight-file-tree :global(.empty) {\n\t\t\tcolor: var(--sl-color-gray-3);\n\t\t\tpadding-inline-start: 0.375rem;\n\t\t}\n\n\t\tstarlight-file-tree :global(.comment) {\n\t\t\tcolor: var(--sl-color-gray-3);\n\t\t\tpadding-inline-start: 1.625rem;\n\t\t\tmax-width: 24rem;\n\t\t\tmin-width: 12rem;\n\t\t}\n\n\t\tstarlight-file-tree :global(.highlight) {\n\t\t\tdisplay: inline-block;\n\t\t\tborder-radius: 0.25rem;\n\t\t\tpadding-inline-end: 0.5rem;\n\t\t\tcolor: var(--sl-color-text-invert);\n\t\t\tbackground-color: var(--sl-color-text-accent);\n\t\t}\n\n\t\tstarlight-file-tree :global(svg) {\n\t\t\tdisplay: inline;\n\t\t\tfill: var(--sl-color-gray-3);\n\t\t\tvertical-align: middle;\n\t\t\tmargin-inline: 0.25rem 0.375rem;\n\t\t\twidth: 0.875rem;\n\t\t\theight: 0.875rem;\n\t\t}\n\n\t\tstarlight-file-tree :global(.highlight svg.tree-icon) {\n\t\t\tfill: currentColor;\n\t\t}\n\t}\n</style>\n"
  },
  {
    "path": "docs/src/components/vendored/starlight/Icon.astro",
    "content": "---\nimport { Icons, type StarlightIcon } from './Icons';\n\ninterface Props {\n\tname: StarlightIcon;\n\tlabel?: string;\n\tcolor?: string;\n\tsize?: string;\n\tclass?: string;\n}\n\nconst { name, label, size = '1em', color } = Astro.props;\nconst a11yAttrs = label ? ({ 'aria-label': label } as const) : ({ 'aria-hidden': 'true' } as const);\n\n/**\n * The fragment around the element is used as a workaround to avoid a trailing whitespace in the output.\n * @see https://github.com/withastro/compiler/issues/1003\n */\n---\n\n<>\n\t<svg\n\t\t{...a11yAttrs}\n\t\tclass={Astro.props.class}\n\t\twidth=\"16\"\n\t\theight=\"16\"\n\t\tviewBox=\"0 0 24 24\"\n\t\tfill=\"currentColor\"\n\t\tset:html={Icons[name]}\n\t/>\n</>\n\n<style define:vars={{ 'sl-icon-color': color, 'sl-icon-size': size }}>\n\t@layer starlight.components {\n\t\tsvg {\n\t\t\tcolor: var(--sl-icon-color);\n\t\t\tfont-size: var(--sl-icon-size, 1em);\n\t\t\twidth: 1em;\n\t\t\theight: 1em;\n\t\t}\n\t}\n</style>\n"
  },
  {
    "path": "docs/src/components/vendored/starlight/Icons.ts",
    "content": "import { FileIcons } from './file-tree-icons';\n\nexport const BuiltInIcons = {\n\t'up-caret':\n\t\t'<path d=\"m17 13.41-4.29-4.24a.999.999 0 0 0-1.42 0l-4.24 4.24a1 1 0 1 0 1.41 1.42L12 11.29l3.54 3.54a1 1 0 0 0 1.41 0 1 1 0 0 0 .05-1.42Z\"/>',\n\t'down-caret':\n\t\t'<path d=\"M17 9.17a1 1 0 0 0-1.41 0L12 12.71 8.46 9.17a1 1 0 1 0-1.41 1.42l4.24 4.24a1.002 1.002 0 0 0 1.42 0L17 10.59a1.002 1.002 0 0 0 0-1.42Z\"/>',\n\t'right-caret':\n\t\t'<path d=\"m14.83 11.29-4.24-4.24a1 1 0 1 0-1.42 1.41L12.71 12l-3.54 3.54a1 1 0 0 0 0 1.41 1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.24-4.24a1.002 1.002 0 0 0 0-1.42Z\"/>',\n\t'left-caret':\n\t\t'<path d=\"m11.29 12 3.54-3.54a1 1 0 0 0 0-1.41 1 1 0 0 0-1.42 0l-4.24 4.24a1 1 0 0 0 0 1.42L13.41 17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29 1 1 0 0 0 0-1.41Z\"/>',\n\t'up-arrow':\n\t\t'<path d=\"m17.71 11.29-5-5a1 1 0 0 0-.33-.21 1 1 0 0 0-.76 0 1 1 0 0 0-.33.21l-5 5a1 1 0 0 0 1.42 1.42L11 9.41V17a1 1 0 0 0 2 0V9.41l3.29 3.3a1 1 0 0 0 1.42 0 1 1 0 0 0 0-1.42Z\"/>',\n\t'down-arrow':\n\t\t'<path d=\"M17.71 11.29a1 1 0 0 0-1.42 0L13 14.59V7a1 1 0 0 0-2 0v7.59l-3.29-3.3a1 1 0 0 0-1.42 1.42l5 5a1 1 0 0 0 .33.21.94.94 0 0 0 .76 0 1 1 0 0 0 .33-.21l5-5a1 1 0 0 0 0-1.42Z\"/>',\n\t'right-arrow':\n\t\t'<path d=\"M17.92 11.62a1.001 1.001 0 0 0-.21-.33l-5-5a1.003 1.003 0 1 0-1.42 1.42l3.3 3.29H7a1 1 0 0 0 0 2h7.59l-3.3 3.29a1.002 1.002 0 0 0 .325 1.639 1 1 0 0 0 1.095-.219l5-5a1 1 0 0 0 .21-.33 1 1 0 0 0 0-.76Z\"/>',\n\t'left-arrow':\n\t\t'<path d=\"M17 11H9.41l3.3-3.29a1.004 1.004 0 1 0-1.42-1.42l-5 5a1 1 0 0 0-.21.33 1 1 0 0 0 0 .76 1 1 0 0 0 .21.33l5 5a1.002 1.002 0 0 0 1.639-.325 1 1 0 0 0-.219-1.095L9.41 13H17a1 1 0 0 0 0-2Z\"/>',\n\tbars: '<path d=\"M3 8h18a1 1 0 1 0 0-2H3a1 1 0 0 0 0 2Zm18 8H3a1 1 0 0 0 0 2h18a1 1 0 0 0 0-2Zm0-5H3a1 1 0 0 0 0 2h18a1 1 0 0 0 0-2Z\"/>',\n\ttranslate:\n\t\t'<path fill-rule=\"evenodd\" d=\"M8.516 3a.94.94 0 0 0-.941.94v1.15H2.94a.94.94 0 1 0 0 1.882h7.362a7.422 7.422 0 0 1-1.787 3.958 7.42 7.42 0 0 1-1.422-2.425.94.94 0 1 0-1.774.627 9.303 9.303 0 0 0 1.785 3.043 7.422 7.422 0 0 1-4.164 1.278.94.94 0 1 0 0 1.881 9.303 9.303 0 0 0 5.575-1.855 9.303 9.303 0 0 0 4.11 1.74l-.763 1.525a.968.968 0 0 0-.016.034l-1.385 2.77a.94.94 0 1 0 1.683.841l1.133-2.267h5.806l1.134 2.267a.94.94 0 0 0 1.683-.841l-1.385-2.769a.95.95 0 0 0-.018-.036l-3.476-6.951a.94.94 0 0 0-1.682 0l-1.82 3.639a7.423 7.423 0 0 1-3.593-1.256 9.303 9.303 0 0 0 2.27-5.203h1.894a.94.94 0 0 0 0-1.881H9.456V3.94A.94.94 0 0 0 8.516 3Zm6.426 11.794a1.068 1.068 0 0 1-.02.039l-.703 1.407h3.924l-1.962-3.924-1.24 2.478Z\" clip-rule=\"evenodd\"/>',\n\tpencil:\n\t\t'<path d=\"M22 7.24a1 1 0 0 0-.29-.71l-4.24-4.24a1 1 0 0 0-1.1-.22 1 1 0 0 0-.32.22l-2.83 2.83L2.29 16.05a1 1 0 0 0-.29.71V21a1 1 0 0 0 1 1h4.24a1 1 0 0 0 .76-.29l10.87-10.93L21.71 8c.1-.1.17-.2.22-.33a1 1 0 0 0 0-.24v-.14l.07-.05ZM6.83 20H4v-2.83l9.93-9.93 2.83 2.83L6.83 20ZM18.17 8.66l-2.83-2.83 1.42-1.41 2.82 2.82-1.41 1.42Z\"/>',\n\tpen: '<path d=\"M21 12a1 1 0 0 0-1 1v6a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h6a1 1 0 1 0 0-2H5a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-6a1 1 0 0 0-1-1Zm-15 .76V17a1 1 0 0 0 1 1h4.24a1 1 0 0 0 .71-.29l6.92-6.93L21.71 8a1 1 0 0 0 0-1.42l-4.24-4.29a1 1 0 0 0-1.42 0l-2.82 2.83-6.94 6.93a1 1 0 0 0-.29.71Zm10.76-8.35 2.83 2.83-1.42 1.42-2.83-2.83 1.42-1.42ZM8 13.17l5.93-5.93 2.83 2.83L10.83 16H8v-2.83Z\"/>',\n\tdocument:\n\t\t'<path d=\"M9 10h1a1 1 0 1 0 0-2H9a1 1 0 0 0 0 2Zm0 2a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2H9Zm11-3.06a1.3 1.3 0 0 0-.06-.27v-.09c-.05-.1-.11-.2-.19-.28l-6-6a1.07 1.07 0 0 0-.28-.19h-.09a.88.88 0 0 0-.33-.11H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V8.94Zm-6-3.53L16.59 8H15a1 1 0 0 1-1-1V5.41ZM18 19a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5v3a3 3 0 0 0 3 3h3v9Zm-3-3H9a1 1 0 0 0 0 2h6a1 1 0 0 0 0-2Z\"/>',\n\t'add-document':\n\t\t'<path d=\"M20 8.94a1.3 1.3 0 0 0-.06-.27v-.09c-.05-.1-.11-.2-.19-.28l-6-6a1.07 1.07 0 0 0-.28-.19h-.09a.88.88 0 0 0-.33-.11H7a3 3 0 0 0-3 3v14a3 3 0 0 0 3 3h10a3 3 0 0 0 3-3V8.94Zm-6-3.53L16.59 8H15a1 1 0 0 1-1-1V5.41ZM18 19a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h5v3a3 3 0 0 0 3 3h3v9Zm-4-5h-1v-1a1 1 0 0 0-2 0v1h-1a1 1 0 0 0 0 2h1v1a1 1 0 0 0 2 0v-1h1a1 1 0 0 0 0-2Z\"/>',\n\tsetting:\n\t\t'<path d=\"m21.32 9.55-1.89-.63.89-1.78A1 1 0 0 0 20.13 6L18 3.87a1 1 0 0 0-1.15-.19l-1.78.89-.63-1.89A1 1 0 0 0 13.5 2h-3a1 1 0 0 0-.95.68l-.63 1.89-1.78-.89A1 1 0 0 0 6 3.87L3.87 6a1 1 0 0 0-.19 1.15l.89 1.78-1.89.63a1 1 0 0 0-.68.94v3a1 1 0 0 0 .68.95l1.89.63-.89 1.78A1 1 0 0 0 3.87 18L6 20.13a1 1 0 0 0 1.15.19l1.78-.89.63 1.89a1 1 0 0 0 .95.68h3a1 1 0 0 0 .95-.68l.63-1.89 1.78.89a1 1 0 0 0 1.13-.19L20.13 18a1 1 0 0 0 .19-1.15l-.89-1.78 1.89-.63a1 1 0 0 0 .68-.94v-3a1 1 0 0 0-.68-.95ZM20 12.78l-1.2.4A2 2 0 0 0 17.64 16l.57 1.14-1.1 1.1-1.11-.6a2 2 0 0 0-2.79 1.16l-.4 1.2h-1.59l-.4-1.2A2 2 0 0 0 8 17.64l-1.14.57-1.1-1.1.6-1.11a2 2 0 0 0-1.16-2.82l-1.2-.4v-1.56l1.2-.4A2 2 0 0 0 6.36 8l-.57-1.11 1.1-1.1L8 6.36a2 2 0 0 0 2.82-1.16l.4-1.2h1.56l.4 1.2A2 2 0 0 0 16 6.36l1.14-.57 1.1 1.1-.6 1.11a2 2 0 0 0 1.16 2.79l1.2.4v1.59ZM12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4Z\"/>',\n\texternal:\n\t\t'<path d=\"M19.33 10.18a1 1 0 0 1-.77 0 1 1 0 0 1-.62-.93l.01-1.83-8.2 8.2a1 1 0 0 1-1.41-1.42l8.2-8.2H14.7a1 1 0 0 1 0-2h4.25a1 1 0 0 1 1 1v4.25a1 1 0 0 1-.62.93Z\"/><path d=\"M11 4a1 1 0 1 1 0 2H7a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-4a1 1 0 1 1 2 0v4a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3h4Z\"/>',\n\tdownload:\n\t\t'<path d=\"M8.29 13.29a1 1 0 0 0 0 1.42l3 3a1 1 0 0 0 1.42 0l3-3a1 1 0 0 0-1.42-1.42L13 14.59V3a1 1 0 0 0-2 0v11.59l-1.29-1.3a1 1 0 0 0-1.42 0ZM18 9h-2a1 1 0 0 0 0 2h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1h2a1 1 0 0 0 0-2H6a3 3 0 0 0-3 3v7a3 3 0 0 0 3 3h12a3 3 0 0 0 3-3v-7a3 3 0 0 0-3-3Z\"/>',\n\t'cloud-download':\n\t\t'<path d=\"M14.29 17.29 13 18.59V13a1 1 0 0 0-2 0v5.59l-1.29-1.3a1 1 0 0 0-1.42 1.42l3 3a1 1 0 0 0 .33.21.94.94 0 0 0 .76 0 1 1 0 0 0 .33-.21l3-3a1 1 0 0 0-1.42-1.42Zm4.13-11.07A7 7 0 0 0 5.06 8.11 4 4 0 0 0 6 16a1 1 0 0 0 0-2 2 2 0 0 1 0-4 1 1 0 0 0 1-1 5 5 0 0 1 9.73-1.61 1 1 0 0 0 .78.67 3 3 0 0 1 .24 5.84 1 1 0 1 0 .5 1.94 5 5 0 0 0 .17-9.62Z\"/>',\n\tmoon: '<path d=\"M21.64 13a1 1 0 0 0-1.05-.14 8.049 8.049 0 0 1-3.37.73 8.15 8.15 0 0 1-8.14-8.1 8.59 8.59 0 0 1 .25-2A1 1 0 0 0 8 2.36a10.14 10.14 0 1 0 14 11.69 1 1 0 0 0-.36-1.05Zm-9.5 6.69A8.14 8.14 0 0 1 7.08 5.22v.27a10.15 10.15 0 0 0 10.14 10.14 9.784 9.784 0 0 0 2.1-.22 8.11 8.11 0 0 1-7.18 4.32v-.04Z\"/>',\n\tsun: '<path d=\"M5 12a1 1 0 0 0-1-1H3a1 1 0 0 0 0 2h1a1 1 0 0 0 1-1Zm.64 5-.71.71a1 1 0 0 0 0 1.41 1 1 0 0 0 1.41 0l.71-.71A1 1 0 0 0 5.64 17ZM12 5a1 1 0 0 0 1-1V3a1 1 0 0 0-2 0v1a1 1 0 0 0 1 1Zm5.66 2.34a1 1 0 0 0 .7-.29l.71-.71a1 1 0 1 0-1.41-1.41l-.66.71a1 1 0 0 0 0 1.41 1 1 0 0 0 .66.29Zm-12-.29a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-.71-.71a1.004 1.004 0 1 0-1.43 1.41l.73.71ZM21 11h-1a1 1 0 0 0 0 2h1a1 1 0 0 0 0-2Zm-2.64 6A1 1 0 0 0 17 18.36l.71.71a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-.76-.66ZM12 6.5a5.5 5.5 0 1 0 5.5 5.5A5.51 5.51 0 0 0 12 6.5Zm0 9a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7Zm0 3.5a1 1 0 0 0-1 1v1a1 1 0 0 0 2 0v-1a1 1 0 0 0-1-1Z\"/>',\n\tlaptop:\n\t\t'<path d=\"M21 14h-1V7a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v7H3a1 1 0 0 0-1 1v2a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3v-2a1 1 0 0 0-1-1ZM6 7a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v7H6V7Zm14 10a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-1h16v1Z\"/>',\n\t'open-book':\n\t\t'<path d=\"M21.17 2.06A13.1 13.1 0 0 0 19 1.87a12.94 12.94 0 0 0-7 2.05 12.94 12.94 0 0 0-7-2 13.1 13.1 0 0 0-2.17.19 1 1 0 0 0-.83 1v12a1 1 0 0 0 1.17 1 10.9 10.9 0 0 1 8.25 1.91l.12.07h.11a.91.91 0 0 0 .7 0h.11l.12-.07A10.899 10.899 0 0 1 20.83 16 1 1 0 0 0 22 15V3a1 1 0 0 0-.83-.94ZM11 15.35a12.87 12.87 0 0 0-6-1.48H4v-10c.333-.02.667-.02 1 0a10.86 10.86 0 0 1 6 1.8v9.68Zm9-1.44h-1a12.87 12.87 0 0 0-6 1.48V5.67a10.86 10.86 0 0 1 6-1.8c.333-.02.667-.02 1 0v10.04Zm1.17 4.15a13.098 13.098 0 0 0-2.17-.19 12.94 12.94 0 0 0-7 2.05 12.94 12.94 0 0 0-7-2.05c-.727.003-1.453.066-2.17.19A1 1 0 0 0 2 19.21a1 1 0 0 0 1.17.79 10.9 10.9 0 0 1 8.25 1.91 1 1 0 0 0 1.16 0A10.9 10.9 0 0 1 20.83 20a1 1 0 0 0 1.17-.79 1 1 0 0 0-.83-1.15Z\"/>',\n\tinformation:\n\t\t'<path d=\"M12 11a1 1 0 0 0-1 1v4a1 1 0 0 0 2 0v-4a1 1 0 0 0-1-1Zm.38-3.92a1 1 0 0 0-.76 0 1 1 0 0 0-.33.21 1.15 1.15 0 0 0-.21.33 1 1 0 0 0 .21 1.09c.097.088.209.16.33.21A1 1 0 0 0 13 8a1.05 1.05 0 0 0-.29-.71 1 1 0 0 0-.33-.21ZM12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm0 18a8 8 0 1 1 0-16.001A8 8 0 0 1 12 20Z\"/>',\n\tmagnifier:\n\t\t'<path d=\"M21.71 20.29 18 16.61A9 9 0 1 0 16.61 18l3.68 3.68a.999.999 0 0 0 1.42 0 1 1 0 0 0 0-1.39ZM11 18a7 7 0 1 1 0-14 7 7 0 0 1 0 14Z\"/>',\n\t'forward-slash':\n\t\t'<path d=\"M17 2H7a5 5 0 0 0-5 5v10a5 5 0 0 0 5 5h10a5 5 0 0 0 5-5V7a5 5 0 0 0-5-5Zm3 15a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v10Z\"/><path d=\"M15.293 6.707a1 1 0 1 1 1.414 1.414l-8.485 8.486a1 1 0 0 1-1.414-1.415l8.485-8.485Z\"/>',\n\tclose:\n\t\t'<path d=\"m13.41 12 6.3-6.29a1.004 1.004 0 1 0-1.42-1.42L12 10.59l-6.29-6.3a1.004 1.004 0 0 0-1.42 1.42l6.3 6.29-6.3 6.29a1 1 0 0 0 0 1.42.998.998 0 0 0 1.42 0l6.29-6.3 6.29 6.3a.999.999 0 0 0 1.42 0 1 1 0 0 0 0-1.42L13.41 12Z\"/>',\n\terror:\n\t\t'<path d=\"M12 7a1 1 0 0 0-1 1v4a1 1 0 0 0 2 0V8a1 1 0 0 0-1-1Zm0 8a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm9.71-7.44-5.27-5.27a1.05 1.05 0 0 0-.71-.29H8.27a1.05 1.05 0 0 0-.71.29L2.29 7.56a1.05 1.05 0 0 0-.29.71v7.46c.004.265.107.518.29.71l5.27 5.27c.192.183.445.286.71.29h7.46a1.05 1.05 0 0 0 .71-.29l5.27-5.27a1.05 1.05 0 0 0 .29-.71V8.27a1.05 1.05 0 0 0-.29-.71ZM20 15.31 15.31 20H8.69L4 15.31V8.69L8.69 4h6.62L20 8.69v6.62Z\"/>',\n\twarning:\n\t\t'<path d=\"M12 16a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm10.67 1.47-8.05-14a3 3 0 0 0-5.24 0l-8 14A3 3 0 0 0 3.94 22h16.12a3 3 0 0 0 2.61-4.53Zm-1.73 2a1 1 0 0 1-.88.51H3.94a1 1 0 0 1-.88-.51 1 1 0 0 1 0-1l8-14a1 1 0 0 1 1.78 0l8.05 14a1 1 0 0 1 .05 1.02v-.02ZM12 8a1 1 0 0 0-1 1v4a1 1 0 0 0 2 0V9a1 1 0 0 0-1-1Z\"/>',\n\t'approve-check-circle':\n\t\t'<path d=\"m14.72 8.79-4.29 4.3-1.65-1.65a1 1 0 1 0-1.41 1.41l2.35 2.36a1 1 0 0 0 1.41 0l5-5a1.002 1.002 0 1 0-1.41-1.42ZM12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm0 18a8 8 0 1 1 0-16.001A8 8 0 0 1 12 20Z\"/>',\n\t'approve-check':\n\t\t'<path d=\"M18.71 7.21a1 1 0 0 0-1.42 0l-7.45 7.46-3.13-3.14A1.02 1.02 0 1 0 5.29 13l3.84 3.84a1.001 1.001 0 0 0 1.42 0l8.16-8.16a1 1 0 0 0 0-1.47Z\"/>',\n\trocket:\n\t\t'<path fill-rule=\"evenodd\" d=\"M1.44 8.855v-.001l3.527-3.516c.34-.344.802-.541 1.285-.548h6.649l.947-.947c3.07-3.07 6.207-3.072 7.62-2.868a1.821 1.821 0 0 1 1.557 1.557c.204 1.413.203 4.55-2.868 7.62l-.946.946v6.649a1.845 1.845 0 0 1-.549 1.286l-3.516 3.528a1.844 1.844 0 0 1-3.11-.944l-.858-4.275-4.52-4.52-2.31-.463-1.964-.394A1.847 1.847 0 0 1 .98 10.693a1.843 1.843 0 0 1 .46-1.838Zm5.379 2.017-3.873-.776L6.32 6.733h4.638l-4.14 4.14Zm8.403-5.655c2.459-2.46 4.856-2.463 5.89-2.33.134 1.035.13 3.432-2.329 5.891l-6.71 6.71-3.561-3.56 6.71-6.711Zm-1.318 15.837-.776-3.873 4.14-4.14v4.639l-3.364 3.374Z\" clip-rule=\"evenodd\"/><path d=\"M9.318 18.345a.972.972 0 0 0-1.86-.561c-.482 1.435-1.687 2.204-2.934 2.619a8.22 8.22 0 0 1-1.23.302c.062-.365.157-.79.303-1.229.415-1.247 1.184-2.452 2.62-2.935a.971.971 0 1 0-.62-1.842c-.12.04-.236.084-.35.13-2.02.828-3.012 2.588-3.493 4.033a10.383 10.383 0 0 0-.51 2.845l-.001.016v.063c0 .536.434.972.97.972H2.24a7.21 7.21 0 0 0 .878-.065c.527-.063 1.248-.19 2.02-.447 1.445-.48 3.205-1.472 4.033-3.494a5.828 5.828 0 0 0 .147-.407Z\"/>',\n\tstar: '<path d=\"M22 9.67a1 1 0 0 0-.86-.67l-5.69-.83L12.9 3a1 1 0 0 0-1.8 0L8.55 8.16 2.86 9a1 1 0 0 0-.81.68 1 1 0 0 0 .25 1l4.13 4-1 5.68a1 1 0 0 0 1.45 1.07L12 18.76l5.1 2.68c.14.08.3.12.46.12a1 1 0 0 0 .99-1.19l-1-5.68 4.13-4A1 1 0 0 0 22 9.67Zm-6.15 4a1 1 0 0 0-.29.89l.72 4.19-3.76-2a1 1 0 0 0-.94 0l-3.76 2 .72-4.19a1 1 0 0 0-.29-.89l-3-3 4.21-.61a1 1 0 0 0 .76-.55L12 5.7l1.88 3.82a1 1 0 0 0 .76.55l4.21.61-3 2.99Z\"/>',\n\tpuzzle:\n\t\t'<path d=\"M17 22H5a3 3 0 0 1-3-3V9a3 3 0 0 1 3-3h1a4 4 0 0 1 7.3-2.18c.448.64.692 1.4.7 2.18h3a1 1 0 0 1 1 1v3a4 4 0 0 1 2.18 7.3A3.86 3.86 0 0 1 18 18v3a1 1 0 0 1-1 1ZM5 8a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h11v-3.18a1 1 0 0 1 1.33-.95 1.77 1.77 0 0 0 1.74-.23 2 2 0 0 0 .93-1.37 2 2 0 0 0-.48-1.59 1.89 1.89 0 0 0-2.17-.55 1 1 0 0 1-1.33-.95V8h-3.2a1 1 0 0 1-1-1.33 1.77 1.77 0 0 0-.23-1.74 1.939 1.939 0 0 0-3-.43A2 2 0 0 0 8 6c.002.23.046.456.13.67A1 1 0 0 1 7.18 8H5Z\"/>',\n\t'list-format':\n\t\t'<path d=\"M3.71 16.29a1 1 0 0 0-.33-.21 1 1 0 0 0-.76 0 1 1 0 0 0-.33.21 1 1 0 0 0-.21.33 1 1 0 0 0 .21 1.09c.097.088.209.16.33.21a.94.94 0 0 0 .76 0 1.15 1.15 0 0 0 .33-.21 1 1 0 0 0 .21-1.09 1 1 0 0 0-.21-.33ZM7 8h14a1 1 0 1 0 0-2H7a1 1 0 0 0 0 2Zm-3.29 3.29a1 1 0 0 0-1.09-.21 1.15 1.15 0 0 0-.33.21 1 1 0 0 0-.21.33.94.94 0 0 0 0 .76c.05.121.122.233.21.33.097.088.209.16.33.21a.94.94 0 0 0 .76 0 1.15 1.15 0 0 0 .33-.21 1.15 1.15 0 0 0 .21-.33.94.94 0 0 0 0-.76 1 1 0 0 0-.21-.33ZM21 11H7a1 1 0 0 0 0 2h14a1 1 0 0 0 0-2ZM3.71 6.29a1 1 0 0 0-.33-.21 1 1 0 0 0-1.09.21 1.15 1.15 0 0 0-.21.33.94.94 0 0 0 0 .76c.05.121.122.233.21.33.097.088.209.16.33.21a1 1 0 0 0 1.09-.21 1.15 1.15 0 0 0 .21-.33.94.94 0 0 0 0-.76 1.15 1.15 0 0 0-.21-.33ZM21 16H7a1 1 0 0 0 0 2h14a1 1 0 0 0 0-2Z\"/>',\n\trandom:\n\t\t'<path d=\"M8.7 10a1 1 0 0 0 1.41 0 1 1 0 0 0 0-1.41l-6.27-6.3a1 1 0 0 0-1.42 1.42ZM21 14a1 1 0 0 0-1 1v3.59L15.44 14A1 1 0 0 0 14 15.44L18.59 20H15a1 1 0 0 0 0 2h6a1 1 0 0 0 .38-.08 1 1 0 0 0 .54-.54A1 1 0 0 0 22 21v-6a1 1 0 0 0-1-1Zm.92-11.38a1 1 0 0 0-.54-.54A1 1 0 0 0 21 2h-6a1 1 0 0 0 0 2h3.59L2.29 20.29a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0L20 5.41V9a1 1 0 0 0 2 0V3a1 1 0 0 0-.08-.38Z\"/>',\n\tcomment:\n\t\t'<path d=\"M12 2A10 10 0 0 0 2 12a9.9 9.9 0 0 0 2.3 6.3l-2 2a1 1 0 0 0-.3 1.1 1 1 0 0 0 1 .6h9a10 10 0 0 0 0-20m0 18H5.4l1-1a1 1 0 0 0 0-1.3A8 8 0 1 1 12 20\"/>',\n\t'comment-alt':\n\t\t'<path d=\"M19 2H5a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h11.6l3.7 3.7a1 1 0 0 0 .7.3.8.8 0 0 0 .4 0 1 1 0 0 0 .6-1V5a3 3 0 0 0-3-3m1 16.6-2.3-2.3a1 1 0 0 0-.7-.3H5a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1Z\"/>',\n\theart:\n\t\t'<path d=\"M20.16 5A6.29 6.29 0 0 0 12 4.36a6.27 6.27 0 0 0-8.16 9.48l6.21 6.22a2.78 2.78 0 0 0 3.9 0l6.21-6.22a6.27 6.27 0 0 0 0-8.84m-1.41 7.46-6.21 6.21a.76.76 0 0 1-1.08 0l-6.21-6.24a4.29 4.29 0 0 1 0-6 4.27 4.27 0 0 1 6 0 1 1 0 0 0 1.42 0 4.27 4.27 0 0 1 6 0 4.29 4.29 0 0 1 .08 6Z\"/>',\n\tgithub:\n\t\t'<path d=\"M12 .3a12 12 0 0 0-3.8 23.38c.6.12.83-.26.83-.57L9 21.07c-3.34.72-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.08-.74.09-.73.09-.73 1.2.09 1.83 1.24 1.83 1.24 1.08 1.83 2.81 1.3 3.5 1 .1-.78.42-1.31.76-1.61-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.14-.3-.54-1.52.1-3.18 0 0 1-.32 3.3 1.23a11.5 11.5 0 0 1 6 0c2.28-1.55 3.29-1.23 3.29-1.23.64 1.66.24 2.88.12 3.18a4.65 4.65 0 0 1 1.23 3.22c0 4.61-2.8 5.63-5.48 5.92.42.36.81 1.1.81 2.22l-.01 3.29c0 .31.2.69.82.57A12 12 0 0 0 12 .3Z\"/>',\n\tgitlab:\n\t\t'<path d=\"m22.63 9.8-.03-.09-3-7.81a.78.78 0 0 0-.76-.5.8.8 0 0 0-.46.18.8.8 0 0 0-.26.4L16.1 8.17H7.9l-2-6.19a.79.79 0 0 0-1.5-.08l-3 7.81-.02.08a5.56 5.56 0 0 0 1.84 6.43h.01l.03.02 4.56 3.42 2.26 1.7 1.37 1.05a.92.92 0 0 0 1.12 0l1.38-1.04 2.25-1.71 4.6-3.44a5.56 5.56 0 0 0 1.84-6.43Z\"/>',\n\tbitbucket:\n\t\t'<path d=\"M1 1.5a.8.8 0 0 0-.7.9l3.2 19.3c0 .5.5.8 1 .8h15.2c.4 0 .7-.2.8-.6l3.2-19.5a.7.7 0 0 0-.8-.9H1zm13.4 14H9.6l-1.3-7h7.3l-1.2 7z\"/>',\n\tcodePen:\n\t\t'<path d=\"M23.5 7.5 12.5.2a1 1 0 0 0-1 0L.4 7.5a1 1 0 0 0-.5.8v7.4c0 .3.2.6.5.8l11 7.3c.3.3.7.3 1 0l11-7.3c.3-.2.5-.5.5-.8V8.3a1 1 0 0 0-.5-.8zM13 3l8.1 5.3-3.6 2.5-4.5-3V3zm-2 0v4.8l-4.5 3-3.6-2.5 8-5.3zm-9 7.3L4.7 12l-2.5 1.7v-3.4zM11 21l-8.1-5.3 3.6-2.5 4.5 3V21zm1-6.6L8.4 12 12 9.6l3.6 2.4-3.6 2.4zm1 6.6v-4.8l4.5-3 3.6 2.5-8 5.3zm9-7.3L19.3 12l2.5-1.7v3.4z\"/>',\n\tfarcaster:\n\t\t'<path d=\"M6.187 3.733h11.627v16.533h-1.707v-7.573h-.017a4.107 4.107 0 0 0-8.18 0h-.017v7.573H6.186z\"/><path d=\"m3.093 6.08.693 2.347h.587v9.493a.533.533 0 0 0-.533.533v.64h-.107a.533.533 0 0 0-.533.533v.64h5.973v-.64a.533.533 0 0 0-.533-.533h-.107v-.64A.533.533 0 0 0 8 17.92h-.64V6.08zM16.213 17.92a.533.533 0 0 0-.533.533v.64h-.107a.533.533 0 0 0-.533.533v.64h5.973v-.64a.533.533 0 0 0-.533-.533h-.107v-.64a.533.533 0 0 0-.533-.533V8.427h.587l.693-2.347h-4.267v11.84z\"/>',\n\tdiscord:\n\t\t'<path d=\"M20.32 4.37a19.8 19.8 0 0 0-4.93-1.51 13.78 13.78 0 0 0-.64 1.28 18.27 18.27 0 0 0-5.5 0 12.64 12.64 0 0 0-.64-1.28h-.05A19.74 19.74 0 0 0 3.64 4.4 20.26 20.26 0 0 0 .11 18.09l.02.02a19.9 19.9 0 0 0 6.04 3.03l.04-.02a14.24 14.24 0 0 0 1.23-2.03.08.08 0 0 0-.05-.07 13.1 13.1 0 0 1-1.9-.92.08.08 0 0 1 .02-.1 10.2 10.2 0 0 0 .41-.31h.04a14.2 14.2 0 0 0 12.1 0l.04.01a9.63 9.63 0 0 0 .4.32.08.08 0 0 1-.03.1 12.29 12.29 0 0 1-1.9.91.08.08 0 0 0-.02.1 15.97 15.97 0 0 0 1.27 2.01h.04a19.84 19.84 0 0 0 6.03-3.05v-.03a20.12 20.12 0 0 0-3.57-13.69ZM8.02 15.33c-1.18 0-2.16-1.08-2.16-2.42 0-1.33.96-2.42 2.16-2.42 1.21 0 2.18 1.1 2.16 2.42 0 1.34-.96 2.42-2.16 2.42Zm7.97 0c-1.18 0-2.15-1.08-2.15-2.42 0-1.33.95-2.42 2.15-2.42 1.22 0 2.18 1.1 2.16 2.42 0 1.34-.94 2.42-2.16 2.42Z\"/>',\n\tgitter:\n\t\t'<path d=\"M6.11 15.12H3.75V0h2.36v15.12zm4.71-11.55H8.46V24h2.36V3.57zm4.72 0h-2.36V24h2.36V3.57zm4.71 0h-2.36v11.57h2.36V3.56z\"/>',\n\ttwitter:\n\t\t'<path d=\"M24 4.4a10 10 0 0 1-2.83.78 5.05 5.05 0 0 0 2.17-2.79 9.7 9.7 0 0 1-3.13 1.23 4.89 4.89 0 0 0-5.94-1.03 5 5 0 0 0-2.17 2.38 5.15 5.15 0 0 0-.3 3.25c-1.95-.1-3.86-.63-5.61-1.53a14.04 14.04 0 0 1-4.52-3.74 5.2 5.2 0 0 0-.09 4.91c.39.74.94 1.35 1.61 1.82a4.77 4.77 0 0 1-2.23-.63v.06c0 1.16.4 2.29 1.12 3.18a4.9 4.9 0 0 0 2.84 1.74c-.73.22-1.5.26-2.24.12a4.89 4.89 0 0 0 4.59 3.49A9.78 9.78 0 0 1 0 19.73 13.65 13.65 0 0 0 7.55 22a13.63 13.63 0 0 0 9.96-4.16A14.26 14.26 0 0 0 21.6 7.65V7c.94-.72 1.75-1.6 2.4-2.6Z\"/>',\n\t'x.com':\n\t\t'<path d=\"M 18.242188 2.25 L 21.554688 2.25 L 14.324219 10.507812 L 22.828125 21.75 L 16.171875 21.75 L 10.953125 14.933594 L 4.992188 21.75 L 1.679688 21.75 L 9.40625 12.914062 L 1.257812 2.25 L 8.082031 2.25 L 12.792969 8.480469 Z M 17.082031 19.773438 L 18.914062 19.773438 L 7.082031 4.125 L 5.113281 4.125 Z M 17.082031 19.773438 \"/>',\n\tmastodon:\n\t\t'<path d=\"M16.45 17.77c2.77-.33 5.18-2.03 5.49-3.58.47-2.45.44-5.97.44-5.97 0-4.77-3.15-6.17-3.15-6.17-1.58-.72-4.3-1.03-7.13-1.05h-.07c-2.83.02-5.55.33-7.13 1.05 0 0-3.14 1.4-3.14 6.17v.91c-.01.88-.02 1.86 0 2.88.12 4.67.87 9.27 5.2 10.4 2 .53 3.72.64 5.1.57 2.51-.14 3.92-.9 3.92-.9l-.08-1.8s-1.8.56-3.8.5c-2-.08-4.1-.22-4.43-2.66a4.97 4.97 0 0 1-.04-.68s1.96.48 4.44.59c1.51.07 2.94-.09 4.38-.26Zm2.22-3.4h-2.3v-5.6c0-1.19-.5-1.79-1.5-1.79-1.1 0-1.66.71-1.66 2.12v3.07h-2.3V9.1c0-1.4-.55-2.12-1.65-2.12-1 0-1.5.6-1.5 1.78v5.61h-2.3V8.6c0-1.18.3-2.12.9-2.81a3.17 3.17 0 0 1 2.47-1.05c1.18 0 2.07.45 2.66 1.35l.57.96.58-.96a2.97 2.97 0 0 1 2.66-1.35c1.01 0 1.83.36 2.46 1.05.6.7.9 1.63.9 2.81v5.78Z\"/>',\n\tcodeberg:\n\t\t'<path d=\"M12 .5a12 12 0 0 0-12 12 12 12 0 0 0 1.8 6.4l10-13a.2.1 0 0 1 .4 0l10 13a12 12 0 0 0 1.8-6.4 12 12 0 0 0-12-12zm.3 6.5 4.4 16.5a12 12 0 0 0 5.2-4.2z\"/>',\n\tyoutube:\n\t\t'<path d=\"M23.5 6.2A3 3 0 0 0 21.4 4c-1.9-.5-9.4-.5-9.4-.5s-7.5 0-9.4.5A3 3 0 0 0 .5 6.3C0 8 0 12 0 12s0 4 .5 5.8A3 3 0 0 0 2.6 20c1.9.6 9.4.6 9.4.6s7.5 0 9.4-.6a3 3 0 0 0 2.1-2c.5-2 .5-5.9.5-5.9s0-4-.5-5.8zm-14 9.4V8.4l6.3 3.6-6.3 3.6z\"/>',\n\tthreads:\n\t\t'<path d=\"m17.73 11.2-.29-.13c-.17-3.13-1.88-4.92-4.75-4.94h-.04c-1.72 0-3.14.73-4.02 2.06l1.58 1.09a2.8 2.8 0 0 1 2.47-1.21c.94 0 1.66.28 2.12.81.33.4.56.93.67 1.61-.84-.14-1.74-.18-2.71-.13-2.73.16-4.49 1.75-4.37 3.97a3.41 3.41 0 0 0 1.57 2.71c.81.54 1.85.8 2.93.74a4.32 4.32 0 0 0 3.33-1.62 6 6 0 0 0 1.14-2.97 3.5 3.5 0 0 1 1.46 1.6 4 4 0 0 1-.98 4.4c-1.3 1.3-2.86 1.85-5.21 1.87-2.62-.02-4.6-.86-5.88-2.5-1.2-1.52-1.82-3.73-1.85-6.56.03-2.83.65-5.04 1.85-6.57 1.29-1.63 3.26-2.47 5.88-2.49 2.63.02 4.64.86 5.97 2.5.66.8 1.15 1.82 1.48 3l1.85-.5c-.4-1.44-1.02-2.7-1.86-3.73-1.71-2.1-4.21-3.19-7.44-3.21h-.01c-3.22.02-5.7 1.1-7.35 3.22C3.79 6.1 3.03 8.72 3 11.99V12c.03 3.29.79 5.9 2.27 7.78 1.66 2.12 4.13 3.2 7.35 3.22h.01c2.86-.02 4.88-.77 6.54-2.43a5.95 5.95 0 0 0 1.4-6.56 5.62 5.62 0 0 0-2.84-2.81Zm-4.94 4.64c-1.2.07-2.44-.47-2.5-1.62-.05-.85.6-1.8 2.57-1.92l.67-.02c.71 0 1.38.07 1.99.2-.23 2.84-1.56 3.3-2.73 3.36Z\"/>',\n\tlinkedin:\n\t\t'<path d=\"M20.47 2H3.53a1.45 1.45 0 0 0-1.47 1.43v17.14A1.45 1.45 0 0 0 3.53 22h16.94a1.45 1.45 0 0 0 1.47-1.43V3.43A1.45 1.45 0 0 0 20.47 2ZM8.09 18.74h-3v-9h3v9ZM6.59 8.48a1.56 1.56 0 0 1 0-3.12 1.57 1.57 0 1 1 0 3.12Zm12.32 10.26h-3v-4.83c0-1.21-.43-2-1.52-2A1.65 1.65 0 0 0 12.85 13a2 2 0 0 0-.1.73v5h-3v-9h3V11a3 3 0 0 1 2.71-1.5c2 0 3.45 1.29 3.45 4.06v5.18Z\"/>',\n\ttwitch:\n\t\t'<path d=\"M2.5 1 1 4.8v15.4h5.5V23h3.1l3-2.8H17l6-5.7V1H2.6ZM21 13.5l-3.4 3.3H12l-3 2.8v-2.8H4.5V3H21v10.5Zm-3.4-6.8v5.8h-2V6.7h2Zm-5.5 0v5.8h-2V6.7h2Z\"/>',\n\tazureDevOps:\n\t\t'<path d=\"M17,4v9.74l-4,3.28-6.2-2.26V17L3.29,12.41l10.23.8V4.44Zm-3.41.49L7.85,1V3.29L2.58,4.84,1,6.87v4.61l2.26,1V6.57Z\"/>',\n\tmicrosoftTeams:\n\t\t'<path d=\"M13.78 7.2a3.63 3.63 0 1 0-4.3-3.68h1.78a2.52 2.52 0 0 1 2.52 2.53V7.2zM7.34 18.8h3.92a2.52 2.52 0 0 0 2.52-2.52V8.37h4.17c.58.01 1.04.5 1.03 1.07v6.45a6.3 6.3 0 0 1-6.14 6.43 6.3 6.3 0 0 1-5.5-3.52zm16.1-14.06a2.51 2.51 0 1 1-5.02 0 2.51 2.51 0 0 1 5.02 0zm-3.36 14.24h-.17c.4-1 .59-2.05.57-3.11V9.46c0-.38-.07-.75-.23-1.09h2.69c.58 0 1.06.48 1.06 1.06v5.65a3.9 3.9 0 0 1-3.9 3.9h-.02z\"/><path d=\"M1.02 5.02h10.24c.56 0 1.02.46 1.02 1.03v10.23a1.02 1.02 0 0 1-1.02 1.02H1.02A1.02 1.02 0 0 1 0 16.28V6.04c0-.56.46-1.02 1.02-1.02zm7.81 3.9V7.84H3.45v1.08h2.03v5.57h1.3V8.92h2.05z\"/>',\n\tinstagram:\n\t\t'<path d=\"M17.3 5.5a1.2 1.2 0 1 0 1.2 1.2 1.2 1.2 0 0 0-1.2-1.2ZM22 7.9a7.6 7.6 0 0 0-.4-2.5 5 5 0 0 0-1.2-1.7 4.7 4.7 0 0 0-1.8-1.2 7.3 7.3 0 0 0-2.4-.4L12 2H7.9a7.3 7.3 0 0 0-2.5.5 4.8 4.8 0 0 0-1.7 1.2 4.7 4.7 0 0 0-1.2 1.8 7.3 7.3 0 0 0-.4 2.4L2 12v4.1a7.3 7.3 0 0 0 .5 2.4 4.7 4.7 0 0 0 1.2 1.8 4.8 4.8 0 0 0 1.8 1.2 7.3 7.3 0 0 0 2.4.4l4.1.1h4.1a7.3 7.3 0 0 0 2.4-.5 4.7 4.7 0 0 0 1.8-1.2 4.8 4.8 0 0 0 1.2-1.7 7.6 7.6 0 0 0 .4-2.5L22 12V7.9ZM20.1 16a5.6 5.6 0 0 1-.3 1.9A3 3 0 0 1 19 19a3.2 3.2 0 0 1-1.1.8 5.6 5.6 0 0 1-1.9.3H8a5.7 5.7 0 0 1-1.9-.3A3.3 3.3 0 0 1 5 19a3 3 0 0 1-.7-1.1 5.5 5.5 0 0 1-.4-1.9l-.1-4V8a5.5 5.5 0 0 1 .4-1.9A3 3 0 0 1 5 5a3.1 3.1 0 0 1 1.1-.8A5.7 5.7 0 0 1 8 3.9l4-.1h4a5.6 5.6 0 0 1 1.9.4A3 3 0 0 1 19 5a3 3 0 0 1 .7 1.1A5.6 5.6 0 0 1 20 8l.1 4v4ZM12 6.9a5.1 5.1 0 1 0 5.1 5.1A5.1 5.1 0 0 0 12 6.9Zm0 8.4a3.3 3.3 0 1 1 3.3-3.3 3.3 3.3 0 0 1-3.3 3.3Z\"/>',\n\tstackOverflow:\n\t\t'<path d=\"M15.72 0 14 1.28l6.4 8.58 1.7-1.26L15.73 0zm-3.94 3.42-1.36 1.64 8.22 6.85 1.37-1.64-8.23-6.85zM8.64 7.88l-.91 1.94 9.7 4.52.9-1.94-9.7-4.52zm-1.86 4.86-.44 2.1 10.48 2.2.44-2.1-10.47-2.2zM1.9 15.47V24h19.19v-8.53h-2.13v6.4H4.02v-6.4H1.9zm4.26 2.13v2.13h10.66V17.6H6.15Z\"/>',\n\ttelegram:\n\t\t'<path d=\"M22.265 2.428a2.048 2.048 0 0 0-2.078-.324L2.266 9.339a2.043 2.043 0 0 0 .104 3.818l3.625 1.261 2.02 6.682a.998.998 0 0 0 .119.252c.008.012.019.02.027.033a.988.988 0 0 0 .211.215.972.972 0 0 0 .07.05.986.986 0 0 0 .31.136l.013.001.006.003a1.022 1.022 0 0 0 .203.02l.018-.003a.993.993 0 0 0 .301-.052c.023-.008.042-.02.064-.03a.993.993 0 0 0 .205-.114 250.76 250.76 0 0 1 .152-.129l2.702-2.983 4.03 3.122a2.023 2.023 0 0 0 1.241.427 2.054 2.054 0 0 0 2.008-1.633l3.263-16.017a2.03 2.03 0 0 0-.693-1.97ZM9.37 14.736a.994.994 0 0 0-.272.506l-.31 1.504-.784-2.593 4.065-2.117Zm8.302 5.304-4.763-3.69a1.001 1.001 0 0 0-1.353.12l-.866.955.306-1.487 7.083-7.083a1 1 0 0 0-1.169-1.593L6.745 12.554 3.02 11.191 20.999 4Z\"/>',\n\trss: '<path d=\"M2.88 16.88a3 3 0 0 0 0 4.24 3 3 0 0 0 4.24 0 3 3 0 0 0-4.24-4.24Zm2.83 2.83a1 1 0 0 1-1.42-1.42 1 1 0 0 1 1.42 0 1 1 0 0 1 0 1.42ZM5 12a1 1 0 0 0 0 2 5 5 0 0 1 5 5 1 1 0 0 0 2 0 7 7 0 0 0-7-7Zm0-4a1 1 0 0 0 0 2 9 9 0 0 1 9 9 1 1 0 0 0 2 0 11.08 11.08 0 0 0-3.22-7.78A11.08 11.08 0 0 0 5 8Zm10.61.39A15.11 15.11 0 0 0 5 4a1 1 0 0 0 0 2 13 13 0 0 1 13 13 1 1 0 0 0 2 0 15.11 15.11 0 0 0-4.39-10.61Z\"/>',\n\tfacebook:\n\t\t'<path d=\"M20.9 2H3.1A1.1 1.1 0 0 0 2 3.1v17.8A1.1 1.1 0 0 0 3.1 22h9.58v-7.75h-2.6v-3h2.6V9a3.64 3.64 0 0 1 3.88-4 20.26 20.26 0 0 1 2.33.12v2.7H17.3c-1.26 0-1.5.6-1.5 1.47v1.93h3l-.39 3H15.8V22h5.1a1.1 1.1 0 0 0 1.1-1.1V3.1A1.1 1.1 0 0 0 20.9 2Z\"/>',\n\temail:\n\t\t'<path d=\"M19 4H5a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h14a3 3 0 0 0 3-3V7a3 3 0 0 0-3-3Zm-.41 2-5.88 5.88a1 1 0 0 1-1.42 0L5.41 6ZM20 17a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7.41l5.88 5.88a3 3 0 0 0 4.24 0L20 7.41Z\"/>',\n\tphone:\n\t\t'<path d=\"M19.44 13c-.22 0-.45-.07-.67-.12a9.44 9.44 0 0 1-1.31-.39 2 2 0 0 0-2.48 1l-.22.45a12.18 12.18 0 0 1-2.66-2 12.18 12.18 0 0 1-2-2.66l.42-.28a2 2 0 0 0 1-2.48 10.33 10.33 0 0 1-.39-1.31c-.05-.22-.09-.45-.12-.68a3 3 0 0 0-3-2.49h-3a3 3 0 0 0-3 3.41 19 19 0 0 0 16.52 16.46h.38a3 3 0 0 0 2-.76 3 3 0 0 0 1-2.25v-3a3 3 0 0 0-2.47-2.9Zm.5 6a1 1 0 0 1-.34.75 1.05 1.05 0 0 1-.82.25A17 17 0 0 1 4.07 5.22a1.09 1.09 0 0 1 .25-.82 1 1 0 0 1 .75-.34h3a1 1 0 0 1 1 .79q.06.41.15.81a11.12 11.12 0 0 0 .46 1.55l-1.4.65a1 1 0 0 0-.49 1.33 14.49 14.49 0 0 0 7 7 1 1 0 0 0 .76 0 1 1 0 0 0 .57-.52l.62-1.4a13.69 13.69 0 0 0 1.58.46q.4.09.81.15a1 1 0 0 1 .79 1Z\"/>',\n\treddit:\n\t\t'<path d=\"M14.41 16.87a3.38 3.38 0 0 1-2.37.63 3.37 3.37 0 0 1-2.36-.63 1 1 0 0 0-1.42 1.41 5.11 5.11 0 0 0 3.78 1.22 5.12 5.12 0 0 0 3.78-1.22 1 1 0 1 0-1.41-1.41ZM9.2 15a1 1 0 1 0-1-1 1 1 0 0 0 1 1Zm6-2a1 1 0 1 0 1 1 1 1 0 0 0-1-1Zm7.8-1.22a3.77 3.77 0 0 0-6.8-2.26 16.5 16.5 0 0 0-3.04-.48l.85-5.7 2.09.7a3 3 0 0 0 6-.06v-.02a3.03 3.03 0 0 0-3-2.96 2.98 2.98 0 0 0-2.34 1.16l-3.24-1.1a1 1 0 0 0-1.3.8l-1.09 7.17a16.66 16.66 0 0 0-3.34.49 3.77 3.77 0 0 0-6.22 4.23A4.86 4.86 0 0 0 1 16c0 3.92 4.83 7 11 7s11-3.08 11-7a4.86 4.86 0 0 0-.57-2.25 3.78 3.78 0 0 0 .57-1.97ZM19.1 3a1 1 0 1 1-1 1 1.02 1.02 0 0 1 1-1ZM4.77 10a1.76 1.76 0 0 1 .88.25A9.98 9.98 0 0 0 3 11.92v-.14A1.78 1.78 0 0 1 4.78 10ZM12 21c-4.88 0-9-2.29-9-5s4.12-5 9-5 9 2.29 9 5-4.12 5-9 5Zm8.99-9.08a9.98 9.98 0 0 0-2.65-1.67 1.76 1.76 0 0 1 .88-.25A1.78 1.78 0 0 1 21 11.78l-.01.14Z\"/>',\n\tpatreon:\n\t\t'<path d=\"M22.04 7.6c0-2.8-2.19-5.1-4.75-5.93a15.19 15.19 0 0 0-10.44.55C3.16 3.96 2 7.78 1.95 11.58c-.02 3.12.3 11.36 4.94 11.42 3.45.04 3.97-4.4 5.56-6.55 1.14-1.52 2.6-1.95 4.4-2.4 3.1-.76 5.2-3.2 5.2-6.44Z\"/>',\n\tsignal:\n\t\t'<path d=\"m9.12.35.27 1.09a10.9 10.9 0 0 0-3.015 1.248l-.578-.964A12 12 0 0 1 9.12.35m5.76 0-.27 1.09a10.9 10.9 0 0 1 3.015 1.248l.581-.964A12 12 0 0 0 14.88.35M1.725 5.797A12 12 0 0 0 .351 9.119l1.09.27A10.9 10.9 0 0 1 2.69 6.374zm-.6 6.202a11 11 0 0 1 .122-1.63l-1.112-.168a12 12 0 0 0 0 3.596l1.112-.169A11 11 0 0 1 1.125 12zm17.078 10.275-.578-.964a10.9 10.9 0 0 1-3.011 1.247l.27 1.091a12 12 0 0 0 3.319-1.374M22.875 12a11 11 0 0 1-.122 1.63l1.112.168a12 12 0 0 0 0-3.596l-1.112.169a11 11 0 0 1 .122 1.63zm.774 2.88-1.09-.27a10.9 10.9 0 0 1-1.248 3.015l.964.581a12 12 0 0 0 1.374-3.326m-10.02 7.875a11 11 0 0 1-3.258 0l-.17 1.112a12 12 0 0 0 3.597 0zm7.125-4.303a11 11 0 0 1-2.304 2.302l.668.906a12 12 0 0 0 2.542-2.535zM18.45 3.245a11 11 0 0 1 2.304 2.304l.906-.675a12 12 0 0 0-2.535-2.535zM3.246 5.549A11 11 0 0 1 5.55 3.245l-.675-.906A12 12 0 0 0 2.34 4.874zm19.029.248-.964.577a10.9 10.9 0 0 1 1.247 3.011l1.091-.27a12 12 0 0 0-1.374-3.318M10.371 1.246a11 11 0 0 1 3.258 0L13.8.134a12 12 0 0 0-3.597 0zM3.823 21.957 1.5 22.5l.542-2.323-1.095-.257-.542 2.323a1.125 1.125 0 0 0 1.352 1.352l2.321-.532zm-2.642-3.041 1.095.255.375-1.61a10.8 10.8 0 0 1-1.21-2.952l-1.09.27a12 12 0 0 0 1.106 2.852zm5.25 2.437-1.61.375.255 1.095 1.185-.275a12 12 0 0 0 2.851 1.106l.27-1.091a10.8 10.8 0 0 1-2.943-1.217zM12 2.25a9.75 9.75 0 0 0-8.25 14.938l-.938 4 4-.938A9.75 9.75 0 1 0 12 2.25\"/>',\n\tslack:\n\t\t'<path d=\"M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52Zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313ZM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834Zm0 1.271a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312Zm10.122 2.521a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834Zm-1.268 0a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312Zm-2.523 10.122a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52Zm0-1.268a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313Z\"/>',\n\tmatrix:\n\t\t'<path d=\"M22.5 1.5v21h-2.25V24H24V0h-3.75v1.5h2.25ZM7.46 7.95V9.1h.04a3.02 3.02 0 0 1 2.61-1.39c.54 0 1.03.1 1.48.32.44.2.78.58 1.01 1.1.26-.37.6-.7 1.03-.99.44-.28.95-.43 1.55-.43.45 0 .87.06 1.26.17.38.11.71.29.99.53.27.24.49.56.64.95.15.4.23.86.23 1.42v5.72h-2.34v-4.85c0-.29-.01-.56-.04-.8a1.73 1.73 0 0 0-.18-.67 1.1 1.1 0 0 0-.44-.45 1.6 1.6 0 0 0-.78-.16c-.33 0-.6.06-.8.19-.2.12-.37.29-.48.5a2 2 0 0 0-.23.69c-.04.26-.06.52-.06.78v4.77H10.6v-4.8l-.01-.75a2.29 2.29 0 0 0-.14-.69c-.08-.2-.23-.38-.42-.5a1.5 1.5 0 0 0-.85-.2c-.15.01-.3.04-.44.08-.19.06-.37.15-.52.28-.18.14-.32.34-.44.6-.12.26-.18.6-.18 1.02v4.96H5.25V7.94h2.21ZM1.5 1.5v21h2.25V24H0V0h3.75v1.5H1.5Z\"/>',\n\thackerOne:\n\t\t'<path d=\"M7.2 0Q6.5 0 6 .3a1 1 0 0 0-.4.8v21.8q0 .4.4.7.5.4 1.2.4t1.2-.3q.5-.4.5-.8V1c0-.3-.2-.6-.5-.8Q7.9 0 7.2 0m9.5 8.7q-.7 0-1.1.3L11 11.7c-.2.2-.3.5-.2.9q0 .6.5 1c.3.4.7.7 1 .7q.7.2 1-.1l1.7-1v9.7q0 .4.5.7c.3.3.7.4 1.1.4q.7 0 1.2-.3c.4-.3.5-.5.5-.8V9.7q0-.4-.5-.7-.4-.3-1.2-.3\"/>',\n\topenCollective:\n\t\t'<path d=\"M21.86 5.17a11.94 11.94 0 0 1 0 13.66l-3.1-3.1a7.68 7.68 0 0 0 0-7.46l3.1-3.1Zm-3.03-3.03-3.1 3.1a7.71 7.71 0 1 0 0 13.51l3.1 3.11a12 12 0 1 1 0-19.73Z\"/><path d=\"M21.86 5.17a11.94 11.94 0 0 1 0 13.66l-3.1-3.1a7.68 7.68 0 0 0 0-7.46l3.1-3.1Z\"/>',\n\tblueSky:\n\t\t'<path d=\"M12 10.8c-1-2.1-4-6-6.8-8C2.6 1 1.6 1.3.9 1.6.1 1.9 0 3 0 3.8c0 .7.4 5.6.6 6.4C1.4 13 4.3 14 7 13.6h.4H7c-4 .6-7.4 2-2.8 7 5 5.3 6.8-1 7.8-4.2 1 3.2 2 9.3 7.7 4.3 4.3-4.3 1.2-6.5-2.7-7a9 9 0 0 1-.4-.1h.4c2.7.3 5.6-.6 6.4-3.4.2-.8.6-5.7.6-6.4 0-.7-.1-1.9-.9-2.2-.7-.3-1.7-.7-4.3 1.2-2.8 2-5.7 5.9-6.8 8\"/>',\n\tdiscourse:\n\t\t'<path d=\"M12.102 0h-.081C5.462 0 .13 5.252.001 11.779v.012L.007 24l12.097-.01c6.582-.055 11.897-5.404 11.897-11.995S18.686.056 12.109 0h-.005zM12 18.857h-.015a6.778 6.778 0 0 1-2.94-.666l.041.018-4.345 1.077 1.227-4.018a6.78 6.78 0 0 1-.83-3.27A6.86 6.86 0 1 1 12 18.857z\"/>',\n\tzulip:\n\t\t'<path d=\"M21 19c0 1.7-1.2 3-2.7 3H5.7C4.2 22 3 20.7 3 19a3 3 0 0 1 1.2-2.4l6.7-6c0-.1.3 0 .2.2l-2.5 5s0 .2.2.2h9.5c1.5 0 2.7 1.4 2.7 3Zm0-14a3 3 0 0 1-1.2 2.4l-6.7 6c0 .1-.2 0-.2-.2l2.5-5s0-.2-.2-.2H5.7C4.2 8 3 6.6 3 5c0-1.7 1.2-3 2.7-3h12.6C19.8 2 21 3.3 21 5Z\"/>',\n\tpinterest:\n\t\t'<path d=\"M12 0a12 12 0 0 0-4.4 23.1c0-.9-.2-2.4 0-3.4l1.5-6s-.4-.7-.4-1.7c0-1.7 1-3 2.2-3 1 0 1.5.8 1.5 1.7 0 1-.6 2.6-1 4-.3 1.2.6 2.2 1.8 2.2 2.1 0 3.8-2.2 3.8-5.5 0-2.8-2-4.9-5-4.9a5.2 5.2 0 0 0-5.4 5.2c0 1 .3 2.2.8 2.8.1.1.2.2.1.3l-.3 1.4c0 .2-.2.3-.4.2-1.5-.7-2.4-3-2.4-4.7C4.4 8 7 4.5 12.3 4.5c4.1 0 7.4 3 7.4 6.9 0 4.1-2.6 7.4-6.3 7.4-1.2 0-2.3-.6-2.7-1.3l-.8 2.8c-.2 1-1 2.4-1.5 3.2A12 12 0 1 0 12 0z\"/>',\n\ttiktok:\n\t\t'<path d=\"M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z\"/>',\n\tastro:\n\t\t'<path d=\"M7.233 15.856c-.456 1.5-.132 3.586.948 4.57v-.036l.036-.096c.132-.636.648-1.032 1.309-1.008.612.012.96.336 1.044 1.044.036.264.036.528.048.803v.084c0 .6.168 1.176.504 1.68.3.48.72.851 1.284 1.103l-.024-.048-.024-.096c-.42-1.26-.12-2.135.984-2.879l.336-.227.745-.492a3.647 3.647 0 0 0 1.536-2.603c.06-.456 0-.9-.132-1.331l-.18.12c-1.668.887-3.577 1.2-5.425.84-1.117-.169-2.197-.48-3-1.416l.011-.012ZM2 15.592s3.205-1.559 6.421-1.559l2.437-7.508c.084-.36.348-.6.648-.6.3 0 .552.24.648.612l2.425 7.496c3.816 0 6.421 1.56 6.421 1.56L15.539.72c-.144-.444-.42-.72-.768-.72H8.24c-.348 0-.6.276-.768.72L2 15.592Z\"/>',\n\talpine: '<path d=\"m18.7 6 5.3 5.3-5.3 5.3-5.4-5.3L18.7 6zM5.3 6l11 11H5.8L0 11.2 5.3 6z\"/>',\n\tpnpm: '<path d=\"M0 0v7.5h7.5V0H0Zm8.25 0v7.5h7.498V0H8.25Zm8.25 0v7.5H24V0h-7.5ZM8.25 8.25v7.5h7.498v-7.5H8.25Zm8.25 0v7.5H24v-7.5h-7.5ZM0 16.5V24h7.5v-7.5H0Zm8.25 0V24h7.498v-7.5H8.25Zm8.25 0V24H24v-7.5h-7.5Z\"/>',\n\tbiome:\n\t\t'<path d=\"m12 2-5.346 9.259a12.065 12.065 0 0 1 6.326-.22l1.807.427-1.7 7.208-1.81-.427c-2.223-.524-4.36.644-5.263 2.507l-1.672-.809c1.276-2.636 4.284-4.232 7.363-3.505l.848-3.593A10.213 10.213 0 0 0 0 22.785h24L12 2Z\"/>',\n\tbun: '<path d=\"M11.966 22.132c6.609 0 11.966-4.326 11.966-9.661 0-3.308-2.051-6.23-5.204-7.963-1.283-.713-2.291-1.353-3.13-1.885C14.018 1.619 13.043 1 11.966 1c-1.094 0-2.327.783-3.955 1.816a49.78 49.78 0 0 1-2.808 1.692C2.051 6.241 0 9.163 0 12.471c0 5.335 5.357 9.661 11.966 9.661Zm-1.397-17.83a5.885 5.885 0 0 0 .497-2.403c0-.144.201-.186.229-.028.656 2.775-.9 4.15-2.051 4.61-.124.048-.199-.12-.103-.208a5.747 5.747 0 0 0 1.428-1.971Zm2.052-.102a5.795 5.795 0 0 0-.78-2.3v-.015c-.068-.123.086-.263.185-.172 1.956 2.105 1.303 4.055.554 5.037-.082.102-.229-.003-.188-.126a5.837 5.837 0 0 0 .229-2.424Zm1.771-.559a5.709 5.709 0 0 0-1.607-1.801v-.014c-.112-.085-.024-.274.113-.218 2.588 1.084 2.766 3.171 2.452 4.395a.116.116 0 0 1-.13.09.11.11 0 0 1-.071-.045.118.118 0 0 1-.022-.083 5.863 5.863 0 0 0-.735-2.324ZM9.32 4.2c-.616.544-1.279.758-2.058.997-.116 0-.194-.078-.155-.18 1.747-.907 2.369-1.645 2.99-2.771 0 0 .155-.117.188.085 0 .303-.348 1.325-.965 1.869Zm4.931 11.205a2.95 2.95 0 0 1-.935 1.549 2.16 2.16 0 0 1-1.282.618 2.167 2.167 0 0 1-1.323-.618 2.95 2.95 0 0 1-.923-1.549.243.243 0 0 1 .064-.197.23.23 0 0 1 .192-.069h3.954a.227.227 0 0 1 .244.16c.01.035.014.07.009.106Zm-5.443-2.17a1.85 1.85 0 0 1-2.377-.244 1.969 1.969 0 0 1-.233-2.44c.207-.318.502-.565.846-.711a1.84 1.84 0 0 1 2.053.42c.264.27.443.616.515.99a1.98 1.98 0 0 1-.108 1.118c-.142.35-.384.653-.696.867Zm8.471.005a1.85 1.85 0 0 1-2.374-.252 1.956 1.956 0 0 1-.546-1.362c0-.383.11-.758.319-1.076.207-.318.502-.566.847-.711a1.84 1.84 0 0 1 1.09-.108c.366.076.702.261.965.533s.44.617.512.993a1.98 1.98 0 0 1-.113 1.118 1.922 1.922 0 0 1-.7.865Z\"/>',\n\tmdx: '<path d=\"m15.494 12.406-3.169 3.169-3.25-3.169.894-.894 1.706 1.707V8.588h1.219V13.3l1.706-1.706.894.812Zm-13.65-.65 2.193 2.194 2.276-2.194v3.575H7.53v-6.58l-3.494 3.493L.625 8.75v6.581h1.219v-3.575ZM22.4 15.25l-2.519-2.519-2.518 2.519-.813-.894 2.519-2.518-2.6-2.6.893-.813 2.52 2.6 2.6-2.6.893.813-2.6 2.6 2.519 2.518-.894.894Z\"/>',\n\tapple:\n\t\t'<path d=\"M14.94 5.19A4.38 4.38 0 0 0 16 2a4.44 4.44 0 0 0-3 1.52 4.17 4.17 0 0 0-1 3.09 3.69 3.69 0 0 0 2.94-1.42Zm2.52 7.44a4.51 4.51 0 0 1 2.16-3.81 4.66 4.66 0 0 0-3.66-2c-1.56-.16-3 .91-3.83.91s-2-.89-3.3-.87a4.92 4.92 0 0 0-4.14 2.53C2.93 12.45 4.24 17 6 19.47c.8 1.21 1.8 2.58 3.12 2.53s1.75-.82 3.28-.82 2 .82 3.3.79 2.22-1.24 3.06-2.45a11 11 0 0 0 1.38-2.85 4.41 4.41 0 0 1-2.68-4.04Z\"/>',\n\tlinux:\n\t\t'<path d=\"M19.7 17.6c-.1-.2-.2-.4-.2-.6 0-.4-.2-.7-.5-1-.1-.1-.3-.2-.4-.2.6-1.8-.3-3.6-1.3-4.9-.8-1.2-2-2.1-1.9-3.7 0-1.9.2-5.4-3.3-5.1-3.6.2-2.6 3.9-2.7 5.2 0 1.1-.5 2.2-1.3 3.1-.2.2-.4.5-.5.7-1 1.2-1.5 2.8-1.5 4.3-.2.2-.4.4-.5.6-.1.1-.2.2-.2.3-.1.1-.3.2-.5.3-.4.1-.7.3-.9.7-.1.3-.2.7-.1 1.1.1.2.1.4 0 .7-.2.4-.2.9 0 1.4.3.4.8.5 1.5.6.5 0 1.1.2 1.6.4.5.3 1.1.5 1.7.5.3 0 .7-.1 1-.2.3-.2.5-.4.6-.7.4 0 1-.2 1.7-.2.6 0 1.2.2 2 .1 0 .1 0 .2.1.3.2.5.7.9 1.3 1h.2c.8-.1 1.6-.5 2.1-1.1.4-.4.9-.7 1.4-.9.6-.3 1-.5 1.1-1 .1-.7-.1-1.1-.5-1.7zM12.8 4.8c.6.1 1.1.6 1 1.2 0 .3-.1.6-.3.9h-.1c-.2-.1-.3-.1-.4-.2.1-.1.1-.3.2-.5 0-.4-.2-.7-.4-.7-.3 0-.5.3-.5.7v.1c-.1-.1-.3-.1-.4-.2V6c-.1-.5.3-1.1.9-1.2zm-.3 2c.1.1.3.2.4.2.1 0 .3.1.4.2.2.1.4.2.4.5s-.3.6-.9.8c-.2.1-.3.1-.4.2-.3.2-.6.3-1 .3-.3 0-.6-.2-.8-.4-.1-.1-.2-.2-.4-.3-.1-.1-.3-.3-.4-.6 0-.1.1-.2.2-.3.3-.2.4-.3.5-.4l.1-.1c.2-.3.6-.5 1-.5.3.1.6.2.9.4zM10.4 5c.4 0 .7.4.8 1.1v.2c-.1 0-.3.1-.4.2v-.2c0-.3-.2-.6-.4-.5-.2 0-.3.3-.3.6 0 .2.1.3.2.4 0 0-.1.1-.2.1-.2-.2-.4-.5-.4-.8 0-.6.3-1.1.7-1.1zm-1 16.1c-.7.3-1.6.2-2.2-.2-.6-.3-1.1-.4-1.8-.4-.5-.1-1-.1-1.1-.3-.1-.2-.1-.5.1-1 .1-.3.1-.6 0-.9-.1-.3-.1-.5 0-.8.1-.3.3-.4.6-.5.3-.1.5-.2.7-.4.1-.1.2-.2.3-.4.3-.4.5-.6.8-.6.6.1 1.1 1 1.5 1.9.2.3.4.7.7 1 .4.5.9 1.2.9 1.6 0 .5-.2.8-.5 1zm4.9-2.2c0 .1 0 .1-.1.2-1.2.9-2.8 1-4.1.3l-.6-.9c.9-.1.7-1.3-1.2-2.5-2-1.3-.6-3.7.1-4.8.1-.1.1 0-.3.8-.3.6-.9 2.1-.1 3.2 0-.8.2-1.6.5-2.4.7-1.3 1.2-2.8 1.5-4.3.1.1.1.1.2.1.1.1.2.2.3.2.2.3.6.4.9.4h.1c.4 0 .8-.1 1.1-.4.1-.1.2-.2.4-.2.3-.1.6-.3.9-.6.4 1.3.8 2.5 1.4 3.6.4.8.7 1.6.9 2.5.3 0 .7.1 1 .3.8.4 1.1.7 1 1.2H18c0-.3-.2-.6-.9-.9-.7-.3-1.3-.3-1.5.4-.1 0-.2.1-.3.1-.8.4-.8 1.5-.9 2.6.1.4 0 .7-.1 1.1zm4.6.6c-.6.2-1.1.6-1.5 1.1-.4.6-1.1 1-1.9.9-.4 0-.8-.3-.9-.7-.1-.6-.1-1.2.2-1.8.1-.4.2-.7.3-1.1.1-1.2.1-1.9.6-2.2 0 .5.3.8.7 1 .5 0 1-.1 1.4-.5h.2c.3 0 .5 0 .7.2.2.2.3.5.3.7 0 .3.2.6.3.9.5.5.5.8.5.9-.1.2-.5.4-.9.6zm-9-12c-.1 0-.1 0-.1.1 0 0 0 .1.1.1s.1.1.1.1c.3.4.8.6 1.4.7.5-.1 1-.2 1.5-.6l.6-.3c.1 0 .1-.1.1-.1 0-.1 0-.1-.1-.1-.2.1-.5.2-.7.3-.4.3-.9.5-1.4.5-.5 0-.9-.3-1.2-.6-.1 0-.2-.1-.3-.1z\"/>',\n\thomebrew:\n\t\t'<path d=\"M7.94 0a.21.21 0 0 0-.2.16c-.32 1.1.17 2.15.83 2.93.15.18.31.35.48.5a2.04 2.04 0 0 0-.67.02c-1.18.24-2.2.99-2.74 2.53a3.9 3.9 0 0 0-.2 1.47 1.56 1.56 0 0 0-1.16 1.5 1.59 1.59 0 0 0 1.23 1.55l.03 12.04c0 .2.1.38.26.48a.21.21 0 0 0 .01 0c.54.32 2.05.82 5.21.82 3.24 0 4.7-.68 5.18-1.04a.57.57 0 0 0 .22-.45v-1.6a.14.14 0 0 1 .14-.14h1.32a1.83 1.83 0 0 0 1.83-1.82v-5.8a1.83 1.83 0 0 0-1.82-1.83h-1.33a.14.14 0 0 1-.14-.15v-.57a1.57 1.57 0 0 0 1.36-1.56c0-.81-.63-1.49-1.42-1.56a4.34 4.34 0 0 0-.74-2.58 3.1 3.1 0 0 0-2.28-1.32c-.5-.02-.84.12-1.13.25-.21.1-.42.18-.67.22 0-1.28.95-1.98.95-1.98a.21.21 0 0 0 .05-.3s-.09-.12-.21-.26c-.12-.13-.27-.3-.47-.38a.21.21 0 0 0-.08-.01.21.21 0 0 0-.14.05 4.3 4.3 0 0 0-.88 1.1 3.42 3.42 0 0 0-.13.28 3.5 3.5 0 0 0-.38-.85A4.44 4.44 0 0 0 8.02.02.21.21 0 0 0 7.94 0zm.15.52c.85.38 1.43.83 1.8 1.4.27.45.42.97.48 1.6a3.07 3.07 0 0 0-.01.45 6.9 6.9 0 0 1-.17-.05 5.49 5.49 0 0 1-1.3-1.1c-.54-.66-.93-1.46-.8-2.3m3.71 1.1c.07.05.14.1.21.18l.06.07a2.97 2.97 0 0 0-.95 2.45.21.21 0 0 0 .22.2c.47-.02.78-.17 1.06-.3.27-.13.5-.23.93-.21.87.02 1.64.71 1.94 1.13.3.45.65 1 .66 2.36a1.66 1.66 0 0 0-.41.14 1.94 1.94 0 0 0-1.77-1.16 1.94 1.94 0 0 0-1.87 1.45 1.78 1.78 0 0 0-1.36-.64c-.48 0-.9.2-1.23.52a1.87 1.87 0 0 0-1.85-1.63c-.65 0-1.22.34-1.55.84a3.1 3.1 0 0 1 .16-.73c.5-1.44 1.35-2.05 2.42-2.26.36-.07.66 0 .99.1.32.1.67.26 1.09.34a.21.21 0 0 0 .25-.25c-.11-.67.07-1.26.34-1.74a3.71 3.71 0 0 1 .66-.86m-4.36 5A1.44 1.44 0 0 1 8.8 8.53a.21.21 0 0 0 .17.28.21.21 0 0 0 .24-.15 1.37 1.37 0 0 1 2.62 0 .21.21 0 0 0 .41-.1 1.5 1.5 0 0 1 1.5-1.66c.69 0 1.26.44 1.45 1.05a.21.21 0 0 0 .26.15l.15-.04a.21.21 0 0 0 .05-.02 1.14 1.14 0 0 1 1.7 1 1.14 1.14 0 0 1-.98 1.12 2.21 2.21 0 0 0-.49.13 10.65 10.65 0 0 1-1.18.36.21.21 0 0 0-.16.2 1.28 1.28 0 0 1-.14.47 2.07 2.07 0 0 0-.24 1.11v.15a.44.44 0 0 1-.16.36.67.67 0 0 1-.43.14.59.59 0 0 1-.59-.59.8.8 0 0 0-.38-.68 1.28 1.28 0 0 1-.53-.64.21.21 0 0 0-.21-.14 19.47 19.47 0 0 1-5.37-.6 9 9 0 0 0-.84-.2 1.16 1.16 0 0 1-.94-1.13c0-.62.5-1.11 1.1-1.14a.21.21 0 0 0 .21-.17A1.44 1.44 0 0 1 7.44 6.6m8.55 4.1v.46c0 .32.26.57.57.57h1.33a1.4 1.4 0 0 1 1.4 1.4v5.8a1.4 1.4 0 0 1-1.4 1.4h-1.32a.57.57 0 0 0-.58.57v1.6c0 .05-.02.08-.05.11-.35.26-1.75.95-4.92.95-3.1 0-4.59-.52-4.99-.75a.14.14 0 0 1-.06-.12l-.03-11.95.43.1.39.1v10.37c0 .13.07.25.18.31.45.22 1.77.74 4.07.74 2.32 0 3.6-.63 4.02-.89a.36.36 0 0 0 .17-.3v-10.2l.79-.26m-8 .9a.5.5 0 0 1 .5.48v8.58a.5.5 0 0 1-.49.5.5.5 0 0 1-.5-.5V12.1a.5.5 0 0 1 .5-.49zm8.66 1.13a.66.66 0 0 0-.66.66v5.21a.66.66 0 0 0 .66.66h1.14a.66.66 0 0 0 .66-.66v-5.2a.66.66 0 0 0-.66-.67zm0 .43h1.14a.23.23 0 0 1 .23.23v5.21a.23.23 0 0 1-.23.23h-1.14a.23.23 0 0 1-.23-.23v-5.2a.23.23 0 0 1 .23-.24\"/>',\n\tnix: '<path d=\"M7.4 1.6H6l-.7 1.1L7 5.5H3.7L2.4 7.8h11.7l-1.3-2.3H9.6l-2.2-4zm6.1 0h-2.7l5.9 10.1L18 9.4l-1.6-2.8 2.3-3.8-.7-1.2h-1.3L15 4.3l-1.6-2.7zm7 4.2-6 10.1h2.8l1.6-2.8h4.4L24 12l-.7-1.2h-3.1l1.6-2.7-1.4-2.3zM9.4 8H6.6L5 10.8H.7L0 12l.7 1.2h3.1l-1.6 2.7 1.4 2.3zm-2.2 4.2L6 14.6l1.6 2.8-2.3 3.8.7 1.2h1.3L9 19.7l1.6 2.7h2.7zm2.6 3.9 1.3 2.3h3.2l2.2 3.9H18l.7-1.2-1.6-2.7h3.2l1.3-2.3z\"/>',\n\tstarlight:\n\t\t'<path fill-rule=\"evenodd\" d=\"M15.19 6.75 12 0 8.81 6.75l-.19.38-1.68-1.88a1.2 1.2 0 1 0-1.69 1.69L7.13 8.8h-.38L0 12l6.75 3.19h.38l-1.88 1.87a1.2 1.2 0 1 0 1.69 1.69l1.68-1.88.2.38L12 24l3.19-6.75v-.38l1.69 1.88a1.2 1.2 0 1 0 1.68-1.69l-1.68-1.68.37-.2L24 12l-6.75-3.19-.38-.19 1.7-1.68a1.2 1.2 0 1 0-1.7-1.69L15.2 7.13v-.38ZM12 7.13l-.38.93a8.18 8.18 0 0 1-3.56 3.56l-.94.38.94.38a8.18 8.18 0 0 1 3.56 3.56l.38.94.38-.94a8.18 8.18 0 0 1 3.56-3.56l.94-.38-.94-.38a8.18 8.18 0 0 1-3.56-3.56L12 7.12Z\"/><path d=\"M22.12 3.56a1.2 1.2 0 1 0-1.68-1.69l-.57.57a1.2 1.2 0 0 0 1.7 1.68l.55-.56Zm-18 .75c-.37.38-1.12.38-1.68 0l-.57-.56a1.2 1.2 0 0 1 1.7-1.69l.55.56c.57.38.57 1.13 0 1.7Zm0 15.38c-.37-.38-1.12-.38-1.68 0l-.57.56a1.2 1.2 0 1 0 1.7 1.69l.55-.57c.57-.37.57-1.12 0-1.68Zm18 .75a1.2 1.2 0 1 1-1.68 1.68l-.57-.56a1.2 1.2 0 0 1 1.7-1.69l.55.57Z\"/>',\n\tpkl: '<path fill-rule=\"evenodd\" d=\"M18.7 1.8 18 5a9 9 0 0 1 2 2.4l3.2.2c.4 1 .6 2 .7 3.1L21 12.2a9 9 0 0 1-.7 3l1.9 2.7c-.6 1-1.2 1.7-2 2.5l-3-1.3c-.9.6-1.9 1-2.9 1.4l-.9 3c-1 .2-2.1.2-3.2 0l-.8-3c-1-.4-2-.8-2.9-1.5l-3 1.3a12 12 0 0 1-2-2.5l2-2.6a9 9 0 0 1-.8-3L0 10.5c.1-1.1.3-2.1.7-3.1L4 7.3A9 9 0 0 1 6 5l-.6-3.2c1-.6 2-1 3-1.3l2 2.4c1.1-.2 2.2-.2 3.2 0L15.8.4c1 .3 2 .8 2.9 1.4Zm1 9.8c0 4.2-3.3 7.5-7.5 7.5a7.5 7.5 0 0 1-7.6-7.5c0-4.1 3.4-7.5 7.6-7.5 4.2 0 7.6 3.4 7.6 7.5Z\"></path><path fill-opacity=\".5\" d=\"M11.4 10.8c-6.6-2.7-3.6-5.5.4-5.5 4.3 0 7.8 2.5 1.2 5.5a2 2 0 0 1-1.6 0Zm.4 1.9c1 7-3 5.8-5 2.5-2.1-3.7-1.7-8 4.2-3.9a2 2 0 0 1 .8 1.4Zm6.2 1.7c2-3.3 1-7.3-4.7-3a2 2 0 0 0-.8 1.4c-.7 7.1 3.3 5.4 5.5 1.7Z\"/>',\n\tnode: '<path d=\"M12 23.96c-.34 0-.66-.1-.96-.25l-3.03-1.73c-.45-.25-.22-.33-.09-.38.62-.2.73-.24 1.37-.6.07-.04.16-.02.23.03l2.32 1.34c.1.05.2.05.27 0l9.1-5.08a.27.27 0 0 0 .13-.24V6.9c0-.11-.05-.2-.14-.24L12.11 1.6c-.09-.05-.2-.05-.27 0L2.75 6.66c-.09.04-.13.15-.13.24v10.14c0 .1.04.2.13.25l2.49 1.38c1.34.66 2.18-.1 2.18-.88V7.78c0-.13.12-.26.28-.26h1.16c.13 0 .27.1.27.26v10.01c0 1.74-.98 2.75-2.69 2.75-.52 0-.93 0-2.1-.55l-2.38-1.32a1.85 1.85 0 0 1-.96-1.6V6.92c0-.66.36-1.28.96-1.6l9.08-5.1a2.1 2.1 0 0 1 1.92 0l9.08 5.08c.6.33.96.95.96 1.61v10.15c0 .66-.36 1.27-.96 1.6l-9.08 5.09a2.4 2.4 0 0 1-.96.2m2.8-6.98c-3.98 0-4.8-1.76-4.8-3.26 0-.13.11-.26.27-.26h1.18c.14 0 .25.08.25.22.19 1.16.71 1.73 3.12 1.73 1.92 0 2.74-.41 2.74-1.4 0-.58-.23-1-3.21-1.28-2.49-.24-4.04-.77-4.04-2.68 0-1.79 1.55-2.84 4.15-2.84 2.91 0 4.35.96 4.53 3.08a.35.35 0 0 1-.07.2.27.27 0 0 1-.18.08h-1.18a.27.27 0 0 1-.25-.2c-.28-1.2-.98-1.6-2.85-1.6-2.1 0-2.35.7-2.35 1.23 0 .64.3.84 3.12 1.19 2.8.35 4.13.86 4.13 2.75-.03 1.94-1.67 3.04-4.56 3.04\"/>',\n\tcloudflare:\n\t\t'<path d=\"M16.5 16.84c.16-.5.1-.97-.15-1.3-.22-.33-.6-.5-1.06-.53l-8.66-.11a.15.15 0 0 1-.13-.08.21.21 0 0 1-.02-.15.25.25 0 0 1 .2-.15l8.74-.12a3.13 3.13 0 0 0 2.55-1.91l.5-1.3a.25.25 0 0 0 .01-.17 5.68 5.68 0 0 0-10.93-.59 2.58 2.58 0 0 0-3.35.24 2.55 2.55 0 0 0-.67 2.44 3.64 3.64 0 0 0-3.5 4.17.18.18 0 0 0 .17.15h15.98a.22.22 0 0 0 .21-.16l.12-.43Zm2.77-5.56-.24.01c-.06 0-.1.05-.13.1l-.34 1.18c-.15.5-.1.97.16 1.31.22.32.6.5 1.06.52l1.84.12c.06 0 .1.02.14.07.02.04.03.1.02.15a.23.23 0 0 1-.2.15l-1.93.12a3.11 3.11 0 0 0-2.55 1.91l-.14.36c-.03.07.02.14.1.14h6.6a.18.18 0 0 0 .16-.12 4.74 4.74 0 0 0-4.56-6v-.02Z\"/>',\n\tvercel: '<path d=\"m12 1l12 21H0z\"/>',\n\tnetlify:\n\t\t'<path d=\"M6.49 19.04h-.23L5.13 17.9v-.23l1.73-1.71h1.2l.15.15v1.2L6.5 19.04ZM5.13 6.31V6.1l1.13-1.13h.23L8.2 6.68v1.2l-.15.15h-1.2zm9.96 9.09h-1.65l-.14-.13v-3.83c0-.68-.27-1.2-1.1-1.23c-.42 0-.9 0-1.43.02l-.07.08v4.96l-.14.14H8.9l-.13-.14V8.73l.13-.14h3.7a2.6 2.6 0 0 1 2.61 2.6v4.08l-.13.14Zm-8.37-2.44H.14L0 12.82v-1.64l.14-.14h6.58l.14.14v1.64zm17.14 0h-6.58l-.14-.14v-1.64l.14-.14h6.58l.14.14v1.64zM11.05 6.55V1.64l.14-.14h1.65l.14.14v4.9l-.14.14h-1.65zm0 15.81v-4.9l.14-.14h1.65l.14.13v4.91l-.14.14h-1.65z\"/>',\n\tdeno: '<path d=\"M12 0a12 12 0 1 1 0 24 12 12 0 0 1 0-24m-.47 6.8c-3.49 0-6.2 2.19-6.2 4.92 0 2.58 2.5 4.23 6.37 4.15h.12l.42-.02-.1.28v.03l.09.22v.03l.02.04.02.07.02.04.01.05.02.05.02.07.02.08.02.06.02.08.02.09.02.09.03.1.01.06.03.1.02.1.03.15.02.07.02.11.03.12.02.12.04.17.02.15.04.2.02.1.03.15.03.15.04.22.04.23.04.23.04.24.04.24.04.26.04.26.04.2.05.34.02.14.06.36.04.3.04.22.04.31.03.16a10.76 10.76 0 0 0 6.53-3.41l.05-.06-.24-.89-.64-2.37-.39-1.47-.35-1.3-.21-.78-.14-.5-.08-.3-.07-.26-.03-.11-.02-.07-.01-.03v-.03a6.04 6.04 0 0 0-2.05-2.97 6.75 6.75 0 0 0-4.25-1.35M8.47 19.3a.59.59 0 0 0-.72.4v.01l-.53 1.96q.5.24 1.01.43l.08.03.57-2.11V20a.59.59 0 0 0-.41-.7m3.26-1.43a.59.59 0 0 0-.71.4v.01l-.8 2.96v.01a.59.59 0 0 0 1.12.3l.8-2.96v-.02l.02-.06v-.02l-.02-.1-.02-.14-.02-.08a.58.58 0 0 0-.37-.3Zm-5.55-3.04a1 1 0 0 0-.04.09v.02l-.8 2.95v.02a.59.59 0 0 0 1.13.3v-.01l.72-2.68a5.3 5.3 0 0 1-1.01-.7Zm-1.9-3.4a.59.59 0 0 0-.72.4v.02l-.8 2.95v.01a.59.59 0 0 0 1.13.3l.8-2.96v-.01a.59.59 0 0 0-.41-.7m17.87-.68a.59.59 0 0 0-.72.4v.02l-.8 2.95v.01a.59.59 0 0 0 1.13.3l.8-2.96v-.01a.59.59 0 0 0-.41-.7M2.55 6.81a10.7 10.7 0 0 0-1.26 3.93.59.59 0 0 0 1-.22v-.02l.8-2.95v-.01a.59.59 0 0 0-.55-.73m17.59.02a.59.59 0 0 0-.72.4v.01l-.8 2.96v.01a.59.59 0 0 0 1.13.3l.8-2.96v-.01a.59.59 0 0 0-.41-.7Zm-7.85 1.93a.75.75 0 1 1 0 1.5.75.75 0 0 1 0-1.5M6.01 4.03a.59.59 0 0 0-.71.4v.02L4.5 7.4v.01a.59.59 0 0 0 1.12.3l.8-2.96v-.01a.59.59 0 0 0-.41-.7Zm10.24.56a.59.59 0 0 0-.71.4V5L15 7q.52.26.99.6l.05.04.62-2.32V5.3a.59.59 0 0 0-.41-.7m-5.21-3.34a11 11 0 0 0-1.12.16l-.07.01L9.1 4.2v.01a.59.59 0 0 0 1.13.3l.8-2.96v-.01a.6.6 0 0 0 0-.27m7.34 2.04-.16.58v.02a.59.59 0 0 0 1.13.3V4.2l.02-.07a11 11 0 0 0-.92-.77zm-4.64-1.94-.28 1.05a.59.59 0 0 0 1.13.31v-.01l.3-1.1q-.52-.15-1.06-.24z\"/>',\n\tjsr: '<path d=\"M3.7 5.54v3.7H0v7.38h7.38v1.84h12.93v-3.7H24V7.39h-7.38V5.54Zm1.84 1.85h1.85v7.38H1.84v-3.7h1.84v1.85h1.85zm3.7 0h5.53v1.84h-3.7v1.85h3.7v5.53H9.23v-1.84h3.7v-1.85h-3.7Zm7.38 1.84h5.53v3.7h-1.84v-1.85h-1.85v5.54h-1.84z\"/>',\n\tnostr:\n\t\t'<path d=\"M21.6 10.6v9.7a.7.7 0 0 1-.6.6h-8a.7.7 0 0 1-.6-.6v-1.8c0-2.2.3-4.4.8-5.3a2 2 0 0 1 1.3-1c1-.4 2.9-.2 3.7-.2 0 0 2.3 0 2.3-1.3 0-1-1-1-1-1-1.2 0-2 0-2.6-.2-1-.4-1-1.1-1-1.3 0-2.7-4-3-7.5-2.4-3.9.7 0 6.2 0 13.5v1a.7.7 0 0 1-.7.6H3.8a.7.7 0 0 1-.6-.6V3.5a.7.7 0 0 1 .6-.6h3.7a.7.7 0 0 1 .7.6c0 .6.6.9 1 .6 1.3-1 3-1.5 5-1.5 4.2 0 7.4 2.5 7.4 8zm-7-2a1.4 1.4 0 1 0-2.9 0 1.4 1.4 0 0 0 2.9 0z\"/>',\n\tbackstage:\n\t\t'<path d=\"M18.4 9.3a4.4 4.4 0 0 0 .7-.5l.1-.1a4.5 4.5 0 0 0 .8-1.1l.3-1c0-1-.6-2-2-3L12.7.4 6 6.6l-4.3 4 6 3.7a6.1 6.1 0 0 0 3 .9c1.5 0 2.8-.5 3.7-1.4 1-1 1.4-2.3.8-3.4a2.8 2.8 0 0 0-.4-.5l1 .1a4.6 4.6 0 0 0 1.8-.3 4.5 4.5 0 0 0 .7-.3zm-5.5 3.3c-1 1-2.8 1.2-4.2.4l-4.1-2.5 3.7-3.6 4.2 2.6c1.5.9 1.4 2.1.4 3.1zm.5-4.5-4-2.3L13 2.4l3.8 2.3c1.4.9 1.6 2 .6 3a3.3 3.3 0 0 1-4 .4zM15 18.5c-1 1-2.5 1.6-4.1 1.6a6.8 6.8 0 0 1-3.5-1l-5.6-3.4v1.4l6 3.6a6.1 6.1 0 0 0 3 .9c1.5 0 2.8-.5 3.7-1.4.7-.7 1.1-1.5 1.1-2.3l-.6.6zm0-2.1c-1 1-2.5 1.6-4.1 1.6a6.8 6.8 0 0 1-3.5-1l-5.6-3.4V15l6 3.6a6.1 6.1 0 0 0 3 .9c1.5 0 2.8-.5 3.7-1.4.7-.7 1.1-1.5 1.1-2.3v-.1l-.6.7zm0-2.1c-1 1-2.5 1.6-4.1 1.6a6.8 6.8 0 0 1-3.5-1l-5.6-3.4v1.3l6 3.6a6.1 6.1 0 0 0 3 1c1.5 0 2.8-.6 3.7-1.5.7-.7 1.1-1.5 1.1-2.3l-.6.7zm4.6 1.4a5.2 5.2 0 0 1-3.3 1.4v1.5a4.5 4.5 0 0 0 2.8-1.3c.8-.7 1.2-1.5 1.2-2.3v-.1l-.7.8zm-4.6 5c-1 1-2.5 1.6-4.1 1.6a6.8 6.8 0 0 1-3.5-1l-5.6-3.4v1.3l6 3.6a6.1 6.1 0 0 0 3 1c1.5 0 2.8-.5 3.7-1.5.7-.6 1.1-1.5 1.1-2.2V20l-.6.7zM19.7 9l-.1.2-1.2.8a5.2 5.2 0 0 1-1.5.5 5.2 5.2 0 0 1-.8 0l.1.4V12a4.6 4.6 0 0 0 1.5-.3A4.4 4.4 0 0 0 19 11l.1-.1a4.5 4.5 0 0 0 .8-1.1 2.6 2.6 0 0 0 .3-1.1v-.1l-.2.1-.4.6zm0 4.3a6 6 0 0 1-.1.1 5.2 5.2 0 0 1-3.3 1.5v1.4a4.5 4.5 0 0 0 2.8-1.2l1-1.2.2-1v-.1l-.2.2a4.8 4.8 0 0 1-.4.5zm0-2.1-.1.1a4.8 4.8 0 0 1-.6.5 5.2 5.2 0 0 1-2.7 1v1.4A4.5 4.5 0 0 0 19 13h.1a4.2 4.2 0 0 0 .8-1.2l.3-.9v-.3a4 4 0 0 1-.2.2l-.4.5z\"/>',\n\tconfluence:\n\t\t'<path d=\"M.85 18.07.1 19.32a.76.76 0 0 0-.1.28.76.76 0 0 0 .02.28.75.75 0 0 0 .33.46l4.97 3.07a.76.76 0 0 0 .86-.03.75.75 0 0 0 .2-.23l.73-1.23c1.97-3.23 3.97-2.83 7.54-1.14l4.93 2.34a.76.76 0 0 0 .6.03.76.76 0 0 0 .43-.4l2.36-5.35a.75.75 0 0 0 .02-.57.77.77 0 0 0-.38-.43c-1.04-.49-3.1-1.45-4.96-2.36C10.9 10.8 5.2 11 .85 18.07Z\"/><path d=\"m23.15 5.94.75-1.25a.77.77 0 0 0 .08-.57.76.76 0 0 0-.13-.27.76.76 0 0 0-.22-.2L18.67.6a.76.76 0 0 0-.29-.1.77.77 0 0 0-.57.13.77.77 0 0 0-.2.23l-.73 1.22c-1.98 3.25-3.96 2.86-7.53 1.16L4.42.89a.78.78 0 0 0-.59-.03.76.76 0 0 0-.26.15.76.76 0 0 0-.18.24L1.02 6.61a.77.77 0 0 0-.02.57c.04.1.09.18.15.25a.76.76 0 0 0 .24.18c1.04.5 3.11 1.45 4.96 2.36 6.73 3.26 12.44 3.04 16.8-4.03z\"/>',\n\tjira: '<path d=\"M7.75 16.3H5.62C2.4 16.3.09 14.31.09 11.43h11.47c.6 0 .98.42.98 1.02V24c-2.87 0-4.79-2.32-4.79-5.56Zm5.67-5.74h-2.14c-3.21 0-5.52-1.94-5.52-4.82h11.47c.6 0 1.01.38 1.01.98v11.54c-2.87 0-4.82-2.32-4.82-5.56zm5.7-5.7h-2.14c-3.21 0-5.52-1.97-5.52-4.86h11.47c.6 0 .98.42.98.99v11.54c-2.87 0-4.8-2.32-4.8-5.56z\"/>',\n\tstorybook:\n\t\t'<path d=\"m20.35 0-1.32.08.1 2.78a.18.18 0 0 1-.3.14l-.9-.7-1.05.8a.18.18 0 0 1-.25-.03.18.18 0 0 1-.04-.12l.12-2.72-13.21.82A1.2 1.2 0 0 0 2.37 2.3l.74 19.82a1.2 1.2 0 0 0 1.15 1.16l16.11.72h.06c.66 0 1.2-.54 1.2-1.2V1.12A1.2 1.2 0 0 0 20.35 0zm-7.99 4.08c3.14 0 4.86 1.68 4.86 4.88-.42.33-3.59.56-3.59.09.07-1.8-.73-1.87-1.18-1.87-.42 0-1.13.12-1.13 1.08 0 2.37 6.1 2.24 6.1 7.02 0 2.69-2.18 4.17-4.97 4.17-2.87 0-5.38-1.16-5.1-5.2.11-.47 3.77-.35 3.77 0-.05 1.67.33 2.16 1.29 2.16.73 0 1.07-.4 1.07-1.09 0-2.43-6.02-2.51-6.02-6.97 0-2.56 1.76-4.27 4.9-4.27z\"/>',\n\tvscode:\n\t\t'<path d=\"M23.15 2.59 18.2.2a1.5 1.5 0 0 0-1.7.29L7.04 9.13 2.93 6a1 1 0 0 0-1.28.06L.33 7.26a1 1 0 0 0 0 1.48L3.9 12 .32 15.26a1 1 0 0 0 0 1.48l1.33 1.2a1 1 0 0 0 1.28.06l4.12-3.13 9.46 8.63c.44.45 1.13.57 1.7.29l4.94-2.38c.52-.25.85-.77.85-1.35V3.94c0-.58-.33-1.1-.85-1.36ZM18 17.45 10.82 12 18 6.55v10.9Z\"/>',\n\tjetbrains:\n\t\t'<path fill-rule=\"evenodd\" d=\"M2.05 24h9.33c1.17 0 2.28-.52 3.11-1.47l5.22-5.97A5.45 5.45 0 0 0 21 13.01V2.34C21 1.05 20.08 0 18.95 0H9.62C8.45 0 7.34.52 6.5 1.47L1.29 7.43A5.45 5.45 0 0 0 0 11v10.66C0 22.95.92 24 2.05 24ZM3.47 6.5l3.72-4.25A3.2 3.2 0 0 1 9.62 1.1h9.33c.6 0 1.08.56 1.08 1.23V13a4.3 4.3 0 0 1-1 2.77l-3.73 4.25V6.51H3.47Zm-.17.2v13.52h11.83l-1.32 1.51a3.22 3.22 0 0 1-2.43 1.15H2.05c-.6 0-1.08-.56-1.08-1.24V11a4.3 4.3 0 0 1 1-2.77L3.3 6.71Zm6.6 10.43H4.8v1.37h5.1v-1.37Z\"/>',\n\tzed: '<path d=\"M2.25 1.5a.75.75 0 0 0-.75.75v16.5H0V2.25C0 1.01 1 0 2.25 0h20.1c1 0 1.5 1.21.79 1.92L10.76 14.3h3.49v-1.55h1.5v1.92c0 .62-.5 1.13-1.13 1.13H9.27L6.7 18.37h11.69V9h1.5v9.38c0 .82-.68 1.5-1.5 1.5H5.18L2.57 22.5h19.19c.41 0 .75-.34.75-.75V5.25H24v16.5c0 1.24-1 2.25-2.25 2.25H1.65c-1 0-1.5-1.21-.79-1.92L13.19 9.75H9.75v1.5h-1.5V9.37c0-.62.5-1.12 1.12-1.12h5.32l2.62-2.62H5.63V15h-1.5V5.63c0-.83.67-1.5 1.5-1.5H18.8l2.63-2.63H2.25Z\"/>',\n\tvim: '<path d=\"m19.83 16.57.45-.49h1.25l.29.4-1.19 3.84h.46l-.07.2h-1.67l1.05-3.34h-1.89l-1 3.18h.4l-.08.16h-1.5l1.04-3.33H15.4l-1 3.13h.41l-.06.2h-1.56l1.42-4.18h-.55l.09-.26h1.54l.49.5h.85l.46-.51h1l.45.5h.9ZM6 20.27H4.4l-.25-.14V3.65H2.98l-.1-.1v-1.1l.14-.14h6.91l.2.2v1.04L10 3.7H8.99v8.14l8.25-8.14H15.3l-.17-.18V2.44l.12-.1h7.02l.12.13v1l-9.46 9.7H12.5a.24.24 0 0 0-.11.06l-.32.28a.25.25 0 0 0-.07.1l-.28.78-5.74 5.88Zm7.45-6.75.14.14-.26.87-.2.2h-.91l-.17-.16.29-.82.27-.23h.84Zm-3.47 7.04 1.48-4.22h-.47l.28-.29h1.56l-1.47 4.27h.59l-.08.24H9.97ZM23.25 12h-.03l-4.05-4.05 4.04-4.14V2.12l-.61-.6H14.9l-.61.56v.98L12 .78V.75L12 .77l-.01-.02v.03l-1.21 1.2-.5-.5H2.65l-.6.65V3.9l.58.58h.67v4.97L.78 12H.75l.02.01-.02.01h.03l2.53 2.54v6.06l.85.5h2.18l1.74-1.8 3.9 3.91v.03l.02-.02.01.02v-.03l2.35-2.35h.46c.1 0 .2-.07.23-.17l.14-.4.01-.07c0-.06-.01-.11-.04-.15l1.37-1.37-.58 1.84v.07c0 .11.06.2.17.24l.07.01h1.7c.11 0 .2-.06.24-.15l.15-.37a.25.25 0 0 0 0-.2.25.25 0 0 0-.23-.15h-.07l.79-2.47h1.15l-.95 3.02-.01.07c0 .11.07.2.17.24l.08.01h1.88c.1 0 .2-.07.23-.16l.15-.4a.25.25 0 0 0-.15-.32.24.24 0 0 0-.08-.02h-.14l1.06-3.44.02-.08c0-.05-.02-.1-.05-.14l-.36-.48a.25.25 0 0 0-.2-.1h-1.34a.25.25 0 0 0-.18.08l-.37.41h-.59l-.04-.04 4.17-4.17h.03-.02l.02-.02Z\"/>',\n\tfigma:\n\t\t'<path d=\"M5.77 8.28A4.44 4.44 0 0 1 8.19.1h7.6a4.44 4.44 0 0 1 2.43 8.17 4.44 4.44 0 0 1-2.42 8.16h-.08a4.42 4.42 0 0 1-3-1.17v4.14a4.5 4.5 0 0 1-4.51 4.48 4.46 4.46 0 0 1-2.44-8.17 4.44 4.44 0 0 1 0-7.44ZM12.7 12a3 3 0 0 0 3 3h.09a3 3 0 1 0 0-6h-.08a3 3 0 0 0-3 3Zm-1.43-3H8.19a3 3 0 0 0-.01 6h3.1V9Zm-3.09 7.44h-.01a3 3 0 1 0 .03 6.01 3.06 3.06 0 0 0 3.07-3.04v-2.97H8.19Zm3.09-8.88V1.55H8.19a3 3 0 1 0 0 6.01h3.09Zm4.52 0a3 3 0 1 0 0-6.01H12.7v6.01h3.09Z\"/>',\n\tsketch:\n\t\t'<path d=\"m.29 8.99 4.8-6.53a.6.6 0 0 1 .42-.24l6.42-.71a.6.6 0 0 1 .14 0l6.42.71a.6.6 0 0 1 .42.24L23.7 9a.6.6 0 0 1-.02.75L12.34 22.86a.45.45 0 0 1-.68 0L.31 9.74a.6.6 0 0 1-.02-.75Zm13.36-5.55a.15.15 0 0 0-.21.2l3.04 3.75a.3.3 0 0 1-.23.49h-8.5a.3.3 0 0 1-.23-.5l3.05-3.74a.15.15 0 0 0-.22-.2L5.8 7.72a.3.3 0 0 1-.5-.24l.21-3.04a.15.15 0 0 0-.3-.05l-.99 3.48a.75.75 0 0 1-.48.5l-2.19.71a.15.15 0 0 0 .05.3h2.1a.75.75 0 0 1 .64.34l5.56 8.74a.23.23 0 0 0 .39-.22L6.3 10.02a.45.45 0 0 1 .4-.64h10.57a.45.45 0 0 1 .4.64l-3.98 8.22a.22.22 0 0 0 .4.22l5.55-8.74a.75.75 0 0 1 .64-.34h2.04a.15.15 0 0 0 .04-.3l-2.12-.7a.75.75 0 0 1-.48-.51l-1-3.48a.15.15 0 0 0-.17-.1.15.15 0 0 0-.12.15l.22 3.04a.3.3 0 0 1-.51.24l-4.54-4.28Z\"/>',\n\tnpm: '<path d=\"M1.76 0h20.48a1.76 1.76 0 0 1 1.76 1.76v20.48a1.76 1.76 0 0 1-1.76 1.76H1.76A1.76 1.76 0 0 1 0 22.24V1.76A1.76 1.76 0 0 1 1.76 0zM5.11 19.16h6.93V8.8h3.47v10.36h3.47V5.34H5.13v13.82z\"></path>',\n\tsourcehut:\n\t\t'<path d=\"M12 20a8 8 0 0 1-8-8 8 8 0 0 1 8-8 8 8 0 0 1 8 8 8 8 0 0 1-8 8m0-18A10 10 0 0 0 2 12a10 10 0 0 0 10 10 10 10 0 0 0 10-10A10 10 0 0 0 12 2\"/>',\n\tsubstack:\n\t\t'<path d=\"M22.5 8.2h-21V5.4h21v2.8zm-21 2.6V24L12 18.1 22.5 24V10.8h-21zM22.5 0h-21v2.8h21V0z\"/>',\n};\n\nexport const Icons = {\n\t...BuiltInIcons,\n\t...FileIcons,\n};\n\nexport type StarlightIcon = keyof typeof Icons;\n"
  },
  {
    "path": "docs/src/components/vendored/starlight/README.md",
    "content": "# Vendored Starlight Components\n\nThis directory contains components vendored from `@astrojs/starlight` version 0.35.2.\n\n## Why Vendored?\n\nThese components were vendored to add custom file tree icons for Terragrunt and OpenTofu file types, eliminating the need to patch the `@astrojs/starlight` dependency.\n\n## Custom Icons\n\nThe following custom icons have been added to `file-tree-icons.ts` and `Icons.ts`:\n\n- **`.hcl`** → `custom:terragrunt` (Terragrunt logo)\n- **`.tf`, `.tf.json`, `.tfvars`, `.tfvars.json`** → `custom:opentofu` (OpenTofu logo)\n- **`.terraform.lock.hcl`, `.tfplan`** → `custom:opentofu` (OpenTofu logo)\n\n## Components\n\n### File Tree Components\n\n- **`FileTree.astro`** - Main FileTree component for rendering file/directory trees in documentation\n- **`rehype-file-tree.ts`** - Rehype plugin that processes FileTree markup\n- **`file-tree-icons.ts`** - Icon definitions and mappings for file types (includes custom icons)\n\n### Card Components\n\n- **`Card.astro`** - Card component for displaying content with optional icons (vendored to support custom icons)\n- **`Icon.astro`** - Icon component for rendering SVG icons (vendored to use custom icon definitions)\n\n### Icon Registry\n\n- **`Icons.ts`** - Icon registry and SVG path definitions (includes custom Terragrunt and OpenTofu icons)\n\n## Usage\n\n### FileTree Component\n\n```astro\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\n\n<FileTree>\n- src/\n  - main.tf\n  - variables.tf\n  - terragrunt.hcl\n- .terraform.lock.hcl\n</FileTree>\n```\n\n### Card Component\n\n```astro\nimport Card from '@components/vendored/starlight/Card.astro';\n\n<Card title=\"Terragrunt Configuration\" icon=\"custom:terragrunt\">\n  Content goes here\n</Card>\n```\n\n## Maintenance\n\nWhen upgrading `@astrojs/starlight`, check if there are changes to these components that should be merged. The original components can be found at:\n\n- `node_modules/@astrojs/starlight/user-components/FileTree.astro`\n- `node_modules/@astrojs/starlight/user-components/rehype-file-tree.ts`\n- `node_modules/@astrojs/starlight/user-components/file-tree-icons.ts`\n- `node_modules/@astrojs/starlight/user-components/Card.astro`\n- `node_modules/@astrojs/starlight/user-components/Icon.astro`\n- `node_modules/@astrojs/starlight/components/Icons.ts`\n"
  },
  {
    "path": "docs/src/components/vendored/starlight/file-tree-icons.ts",
    "content": "/**\n * This file was generated by the `file-icons-generator` package.\n * Do not edit this file directly as it will be overwritten.\n */\n\nimport type { Definitions } from './rehype-file-tree.ts';\n\n/**\n * Based on https://github.com/elviswolcott/seti-icons which\n * is derived from https://github.com/jesseweed/seti-ui/\n *\n * Copyright (c) 2014 Jesse Weed\n *\n * Permission is hereby granted, free of charge, to any person obtaining\n * a copy of this software and associated documentation files (the\n * \"Software\"), to deal in the Software without restriction, including\n * without limitation the rights to use, copy, modify, merge, publish,\n * distribute, sublicense, and/or sell copies of the Software, and to\n * permit persons to whom the Software is furnished to do so, subject to\n * the following conditions:\n *\n * The above copyright notice and this permission notice shall be\n * included in all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\n * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\n * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\n * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\n * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\n * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n */\nexport const definitions: Definitions = {\n\tfiles: {\n\t\t'astro.config.js': 'astro',\n\t\t'astro.config.mjs': 'astro',\n\t\t'astro.config.cjs': 'astro',\n\t\t'astro.config.ts': 'astro',\n\t\t'pnpm-debug.log': 'pnpm',\n\t\t'pnpm-lock.yaml': 'pnpm',\n\t\t'pnpm-workspace.yaml': 'pnpm',\n\t\t'biome.json': 'biome',\n\t\t'bun.lockb': 'bun',\n\t\tCOMMIT_EDITMSG: 'seti:git',\n\t\tMERGE_MSG: 'seti:git',\n\t\t'karma.conf.js': 'seti:karma',\n\t\t'karma.conf.cjs': 'seti:karma',\n\t\t'karma.conf.mjs': 'seti:karma',\n\t\t'karma.conf.coffee': 'seti:karma',\n\t\t'README.md': 'seti:info',\n\t\t'README.txt': 'seti:info',\n\t\tREADME: 'seti:info',\n\t\t'CHANGELOG.md': 'seti:clock',\n\t\t'CHANGELOG.txt': 'seti:clock',\n\t\tCHANGELOG: 'seti:clock',\n\t\t'CHANGES.md': 'seti:clock',\n\t\t'CHANGES.txt': 'seti:clock',\n\t\tCHANGES: 'seti:clock',\n\t\t'VERSION.md': 'seti:clock',\n\t\t'VERSION.txt': 'seti:clock',\n\t\tVERSION: 'seti:clock',\n\t\tmvnw: 'seti:maven',\n\t\t'pom.xml': 'seti:maven',\n\t\t'tsconfig.json': 'seti:tsconfig',\n\t\t'vite.config.js': 'seti:vite',\n\t\t'vite.config.ts': 'seti:vite',\n\t\t'vite.config.mjs': 'seti:vite',\n\t\t'vite.config.mts': 'seti:vite',\n\t\t'vite.config.cjs': 'seti:vite',\n\t\t'vite.config.cts': 'seti:vite',\n\t\t'swagger.json': 'seti:json',\n\t\t'swagger.yml': 'seti:json',\n\t\t'swagger.yaml': 'seti:json',\n\t\t'mime.types': 'seti:config',\n\t\tJenkinsfile: 'seti:jenkins',\n\t\t'babel.config.js': 'seti:babel',\n\t\t'babel.config.json': 'seti:babel',\n\t\t'babel.config.cjs': 'seti:babel',\n\t\tBUILD: 'seti:bazel',\n\t\t'BUILD.bazel': 'seti:bazel',\n\t\tWORKSPACE: 'seti:bazel',\n\t\t'WORKSPACE.bazel': 'seti:bazel',\n\t\t'bower.json': 'seti:bower',\n\t\t'Bower.json': 'seti:bower',\n\t\t'eslint.config.js': 'seti:eslint',\n\t\t'firebase.json': 'seti:firebase',\n\t\tgeckodriver: 'seti:firefox',\n\t\t'Gruntfile.js': 'seti:grunt',\n\t\t'gruntfile.babel.js': 'seti:grunt',\n\t\t'Gruntfile.babel.js': 'seti:grunt',\n\t\t'gruntfile.js': 'seti:grunt',\n\t\t'Gruntfile.coffee': 'seti:grunt',\n\t\t'gruntfile.coffee': 'seti:grunt',\n\t\t'ionic.config.json': 'seti:ionic',\n\t\t'Ionic.config.json': 'seti:ionic',\n\t\t'ionic.project': 'seti:ionic',\n\t\t'Ionic.project': 'seti:ionic',\n\t\t'platformio.ini': 'seti:platformio',\n\t\t'rollup.config.js': 'seti:rollup',\n\t\t'sass-lint.yml': 'seti:sass',\n\t\t'stylelint.config.js': 'seti:stylelint',\n\t\t'stylelint.config.cjs': 'seti:stylelint',\n\t\t'stylelint.config.mjs': 'seti:stylelint',\n\t\t'yarn.clean': 'seti:yarn',\n\t\t'yarn.lock': 'seti:yarn',\n\t\t'webpack.config.js': 'seti:webpack',\n\t\t'webpack.config.cjs': 'seti:webpack',\n\t\t'webpack.config.mjs': 'seti:webpack',\n\t\t'webpack.config.ts': 'seti:webpack',\n\t\t'webpack.config.build.js': 'seti:webpack',\n\t\t'webpack.config.build.cjs': 'seti:webpack',\n\t\t'webpack.config.build.mjs': 'seti:webpack',\n\t\t'webpack.config.build.ts': 'seti:webpack',\n\t\t'webpack.common.js': 'seti:webpack',\n\t\t'webpack.common.cjs': 'seti:webpack',\n\t\t'webpack.common.mjs': 'seti:webpack',\n\t\t'webpack.common.ts': 'seti:webpack',\n\t\t'webpack.dev.js': 'seti:webpack',\n\t\t'webpack.dev.cjs': 'seti:webpack',\n\t\t'webpack.dev.mjs': 'seti:webpack',\n\t\t'webpack.dev.ts': 'seti:webpack',\n\t\t'webpack.prod.js': 'seti:webpack',\n\t\t'webpack.prod.cjs': 'seti:webpack',\n\t\t'webpack.prod.mjs': 'seti:webpack',\n\t\t'webpack.prod.ts': 'seti:webpack',\n\t\t'npm-debug.log': 'seti:npm_ignored',\n\t\t'.terraform.lock.hcl': 'custom:opentofu',\n\t},\n\textensions: {\n\t\t'.astro': 'astro',\n\t\t'.mdx': 'mdx',\n\t\t'.pkl': 'pkl',\n\t\t'.bsl': 'seti:bsl',\n\t\t'.mdo': 'seti:mdo',\n\t\t'.cls': 'seti:salesforce',\n\t\t'.apex': 'seti:salesforce',\n\t\t'.asm': 'seti:asm',\n\t\t'.s': 'seti:asm',\n\t\t'.bicep': 'seti:bicep',\n\t\t'.bzl': 'seti:bazel',\n\t\t'.bazel': 'seti:bazel',\n\t\t'.BUILD': 'seti:bazel',\n\t\t'.WORKSPACE': 'seti:bazel',\n\t\t'.bazelignore': 'seti:bazel',\n\t\t'.bazelversion': 'seti:bazel',\n\t\t'.c': 'seti:c',\n\t\t'.h': 'seti:c',\n\t\t'.m': 'seti:c',\n\t\t'.cs': 'seti:c-sharp',\n\t\t'.cshtml': 'seti:html',\n\t\t'.aspx': 'seti:html',\n\t\t'.ascx': 'seti:html',\n\t\t'.asax': 'seti:html',\n\t\t'.master': 'seti:html',\n\t\t'.cc': 'seti:cpp',\n\t\t'.cpp': 'seti:cpp',\n\t\t'.cxx': 'seti:cpp',\n\t\t'.c++': 'seti:cpp',\n\t\t'.hh': 'seti:cpp',\n\t\t'.hpp': 'seti:cpp',\n\t\t'.hxx': 'seti:cpp',\n\t\t'.h++': 'seti:cpp',\n\t\t'.mm': 'seti:cpp',\n\t\t'.clj': 'seti:clojure',\n\t\t'.cljs': 'seti:clojure',\n\t\t'.cljc': 'seti:clojure',\n\t\t'.edn': 'seti:clojure',\n\t\t'.cfc': 'seti:coldfusion',\n\t\t'.cfm': 'seti:coldfusion',\n\t\t'.coffee': 'seti:cjsx',\n\t\t'.litcoffee': 'seti:cjsx',\n\t\t'.config': 'seti:config',\n\t\t'.cfg': 'seti:config',\n\t\t'.conf': 'seti:config',\n\t\t'.cr': 'seti:crystal',\n\t\t'.ecr': 'seti:crystal_embedded',\n\t\t'.slang': 'seti:crystal_embedded',\n\t\t'.cson': 'seti:json',\n\t\t'.css': 'seti:css',\n\t\t'.css.map': 'seti:css',\n\t\t'.sss': 'seti:css',\n\t\t'.csv': 'seti:csv',\n\t\t'.xls': 'seti:xls',\n\t\t'.xlsx': 'seti:xls',\n\t\t'.cu': 'seti:cu',\n\t\t'.cuh': 'seti:cu',\n\t\t'.hu': 'seti:cu',\n\t\t'.cake': 'seti:cake',\n\t\t'.ctp': 'seti:cake_php',\n\t\t'.d': 'seti:d',\n\t\t'.doc': 'seti:word',\n\t\t'.docx': 'seti:word',\n\t\t'.ejs': 'seti:ejs',\n\t\t'.ex': 'seti:elixir',\n\t\t'.exs': 'seti:elixir_script',\n\t\t'.elm': 'seti:elm',\n\t\t'.ico': 'seti:favicon',\n\t\t'.fs': 'seti:f-sharp',\n\t\t'.fsx': 'seti:f-sharp',\n\t\t'.gitignore': 'seti:git',\n\t\t'.gitconfig': 'seti:git',\n\t\t'.gitkeep': 'seti:git',\n\t\t'.gitattributes': 'seti:git',\n\t\t'.gitmodules': 'seti:git',\n\t\t'.go': 'seti:go',\n\t\t'.slide': 'seti:go',\n\t\t'.article': 'seti:go',\n\t\t'.gd': 'seti:godot',\n\t\t'.godot': 'seti:godot',\n\t\t'.tres': 'seti:godot',\n\t\t'.tscn': 'seti:godot',\n\t\t'.gradle': 'seti:gradle',\n\t\t'.groovy': 'seti:grails',\n\t\t'.gsp': 'seti:grails',\n\t\t'.gql': 'seti:graphql',\n\t\t'.graphql': 'seti:graphql',\n\t\t'.graphqls': 'seti:graphql',\n\t\t'.hack': 'seti:hacklang',\n\t\t'.haml': 'seti:haml',\n\t\t'.handlebars': 'seti:mustache',\n\t\t'.hbs': 'seti:mustache',\n\t\t'.hjs': 'seti:mustache',\n\t\t'.hs': 'seti:haskell',\n\t\t'.lhs': 'seti:haskell',\n\t\t'.hx': 'seti:haxe',\n\t\t'.hxs': 'seti:haxe',\n\t\t'.hxp': 'seti:haxe',\n\t\t'.hxml': 'seti:haxe',\n\t\t'.html': 'seti:html',\n\t\t'.jade': 'seti:jade',\n\t\t'.java': 'seti:java',\n\t\t'.class': 'seti:java',\n\t\t'.classpath': 'seti:java',\n\t\t'.properties': 'seti:java',\n\t\t'.js': 'seti:javascript',\n\t\t'.js.map': 'seti:javascript',\n\t\t'.cjs': 'seti:javascript',\n\t\t'.cjs.map': 'seti:javascript',\n\t\t'.mjs': 'seti:javascript',\n\t\t'.mjs.map': 'seti:javascript',\n\t\t'.spec.js': 'seti:javascript',\n\t\t'.spec.cjs': 'seti:javascript',\n\t\t'.spec.mjs': 'seti:javascript',\n\t\t'.test.js': 'seti:javascript',\n\t\t'.test.cjs': 'seti:javascript',\n\t\t'.test.mjs': 'seti:javascript',\n\t\t'.es': 'seti:javascript',\n\t\t'.es5': 'seti:javascript',\n\t\t'.es6': 'seti:javascript',\n\t\t'.es7': 'seti:javascript',\n\t\t'.jinja': 'seti:jinja',\n\t\t'.jinja2': 'seti:jinja',\n\t\t'.json': 'seti:json',\n\t\t'.jl': 'seti:julia',\n\t\t'.kt': 'seti:kotlin',\n\t\t'.kts': 'seti:kotlin',\n\t\t'.dart': 'seti:dart',\n\t\t'.less': 'seti:json',\n\t\t'.liquid': 'seti:liquid',\n\t\t'.ls': 'seti:livescript',\n\t\t'.lua': 'seti:lua',\n\t\t'.markdown': 'seti:markdown',\n\t\t'.md': 'seti:markdown',\n\t\t'.argdown': 'seti:argdown',\n\t\t'.ad': 'seti:argdown',\n\t\t'.mustache': 'seti:mustache',\n\t\t'.stache': 'seti:mustache',\n\t\t'.nim': 'seti:nim',\n\t\t'.nims': 'seti:nim',\n\t\t'.github-issues': 'seti:github',\n\t\t'.ipynb': 'seti:notebook',\n\t\t'.njk': 'seti:nunjucks',\n\t\t'.nunjucks': 'seti:nunjucks',\n\t\t'.nunjs': 'seti:nunjucks',\n\t\t'.nunj': 'seti:nunjucks',\n\t\t'.njs': 'seti:nunjucks',\n\t\t'.nj': 'seti:nunjucks',\n\t\t'.npm-debug.log': 'seti:npm',\n\t\t'.npmignore': 'seti:npm',\n\t\t'.npmrc': 'seti:npm',\n\t\t'.ml': 'seti:ocaml',\n\t\t'.mli': 'seti:ocaml',\n\t\t'.cmx': 'seti:ocaml',\n\t\t'.cmxa': 'seti:ocaml',\n\t\t'.odata': 'seti:odata',\n\t\t'.pl': 'seti:perl',\n\t\t'.php': 'seti:php',\n\t\t'.php.inc': 'seti:php',\n\t\t'.pipeline': 'seti:pipeline',\n\t\t'.pddl': 'seti:pddl',\n\t\t'.plan': 'seti:plan',\n\t\t'.happenings': 'seti:happenings',\n\t\t'.ps1': 'seti:powershell',\n\t\t'.psd1': 'seti:powershell',\n\t\t'.psm1': 'seti:powershell',\n\t\t'.prisma': 'seti:prisma',\n\t\t'.pug': 'seti:pug',\n\t\t'.pp': 'seti:puppet',\n\t\t'.epp': 'seti:puppet',\n\t\t'.purs': 'seti:purescript',\n\t\t'.py': 'seti:python',\n\t\t'.jsx': 'seti:react',\n\t\t'.spec.jsx': 'seti:react',\n\t\t'.test.jsx': 'seti:react',\n\t\t'.cjsx': 'seti:react',\n\t\t'.tsx': 'seti:react',\n\t\t'.spec.tsx': 'seti:react',\n\t\t'.test.tsx': 'seti:react',\n\t\t'.res': 'seti:rescript',\n\t\t'.resi': 'seti:rescript',\n\t\t'.R': 'seti:R',\n\t\t'.rmd': 'seti:R',\n\t\t'.rb': 'seti:ruby',\n\t\t'.erb': 'seti:html',\n\t\t'.erb.html': 'seti:html',\n\t\t'.html.erb': 'seti:html',\n\t\t'.rs': 'seti:rust',\n\t\t'.sass': 'seti:sass',\n\t\t'.scss': 'seti:sass',\n\t\t'.springBeans': 'seti:spring',\n\t\t'.slim': 'seti:slim',\n\t\t'.smarty.tpl': 'seti:smarty',\n\t\t'.tpl': 'seti:smarty',\n\t\t'.sbt': 'seti:sbt',\n\t\t'.scala': 'seti:scala',\n\t\t'.sol': 'seti:ethereum',\n\t\t'.styl': 'seti:stylus',\n\t\t'.svelte': 'seti:svelte',\n\t\t'.swift': 'seti:swift',\n\t\t'.sql': 'seti:db',\n\t\t'.soql': 'seti:db',\n\t\t'.tf': 'custom:opentofu',\n\t\t'.tf.json': 'custom:opentofu',\n\t\t'.tfvars': 'custom:opentofu',\n\t\t'.tfvars.json': 'custom:opentofu',\n\t\t'.tex': 'seti:tex',\n\t\t'.sty': 'seti:tex',\n\t\t'.dtx': 'seti:tex',\n\t\t'.ins': 'seti:tex',\n\t\t'.txt': 'seti:default',\n\t\t'.toml': 'seti:config',\n\t\t'.twig': 'seti:twig',\n\t\t'.ts': 'seti:typescript',\n\t\t'.spec.ts': 'seti:typescript',\n\t\t'.test.ts': 'seti:typescript',\n\t\t'.vala': 'seti:vala',\n\t\t'.vapi': 'seti:vala',\n\t\t'.component': 'seti:html',\n\t\t'.vue': 'seti:vue',\n\t\t'.wasm': 'seti:wasm',\n\t\t'.wat': 'seti:wat',\n\t\t'.xml': 'seti:xml',\n\t\t'.yml': 'seti:yml',\n\t\t'.yaml': 'seti:yml',\n\t\t'.pro': 'seti:prolog',\n\t\t'.zig': 'seti:zig',\n\t\t'.jar': 'seti:zip',\n\t\t'.zip': 'seti:zip',\n\t\t'.wgt': 'seti:wgt',\n\t\t'.ai': 'seti:illustrator',\n\t\t'.psd': 'seti:photoshop',\n\t\t'.pdf': 'seti:pdf',\n\t\t'.eot': 'seti:font',\n\t\t'.ttf': 'seti:font',\n\t\t'.woff': 'seti:font',\n\t\t'.woff2': 'seti:font',\n\t\t'.otf': 'seti:font',\n\t\t'.avif': 'seti:image',\n\t\t'.gif': 'seti:image',\n\t\t'.jpg': 'seti:image',\n\t\t'.jpeg': 'seti:image',\n\t\t'.png': 'seti:image',\n\t\t'.pxm': 'seti:image',\n\t\t'.svg': 'seti:svg',\n\t\t'.svgx': 'seti:image',\n\t\t'.tiff': 'seti:image',\n\t\t'.webp': 'seti:image',\n\t\t'.sublime-project': 'seti:sublime',\n\t\t'.sublime-workspace': 'seti:sublime',\n\t\t'.code-search': 'seti:code-search',\n\t\t'.sh': 'seti:shell',\n\t\t'.zsh': 'seti:shell',\n\t\t'.fish': 'seti:shell',\n\t\t'.zshrc': 'seti:shell',\n\t\t'.bashrc': 'seti:shell',\n\t\t'.mov': 'seti:video',\n\t\t'.ogv': 'seti:video',\n\t\t'.webm': 'seti:video',\n\t\t'.avi': 'seti:video',\n\t\t'.mpg': 'seti:video',\n\t\t'.mp4': 'seti:video',\n\t\t'.mp3': 'seti:audio',\n\t\t'.ogg': 'seti:audio',\n\t\t'.wav': 'seti:audio',\n\t\t'.flac': 'seti:audio',\n\t\t'.3ds': 'seti:svg',\n\t\t'.3dm': 'seti:svg',\n\t\t'.stl': 'seti:svg',\n\t\t'.obj': 'seti:svg',\n\t\t'.dae': 'seti:svg',\n\t\t'.bat': 'seti:windows',\n\t\t'.cmd': 'seti:windows',\n\t\t'.babelrc': 'seti:babel',\n\t\t'.babelrc.js': 'seti:babel',\n\t\t'.babelrc.cjs': 'seti:babel',\n\t\t'.bazelrc': 'seti:bazel',\n\t\t'.bowerrc': 'seti:bower',\n\t\t'.codeclimate.yml': 'seti:code-climate',\n\t\t'.eslintrc': 'seti:eslint',\n\t\t'.eslintrc.js': 'seti:eslint',\n\t\t'.eslintrc.cjs': 'seti:eslint',\n\t\t'.eslintrc.yaml': 'seti:eslint',\n\t\t'.eslintrc.yml': 'seti:eslint',\n\t\t'.eslintrc.json': 'seti:eslint',\n\t\t'.eslintignore': 'seti:eslint',\n\t\t'.firebaserc': 'seti:firebase',\n\t\t'.gitlab-ci.yml': 'seti:gitlab',\n\t\t'.jshintrc': 'seti:javascript',\n\t\t'.jscsrc': 'seti:javascript',\n\t\t'.stylelintrc': 'seti:stylelint',\n\t\t'.stylelintrc.json': 'seti:stylelint',\n\t\t'.stylelintrc.yaml': 'seti:stylelint',\n\t\t'.stylelintrc.yml': 'seti:stylelint',\n\t\t'.stylelintrc.js': 'seti:stylelint',\n\t\t'.stylelintignore': 'seti:stylelint',\n\t\t'.direnv': 'seti:config',\n\t\t'.env': 'seti:config',\n\t\t'.static': 'seti:config',\n\t\t'.editorconfig': 'seti:config',\n\t\t'.slugignore': 'seti:config',\n\t\t'.tmp': 'seti:clock',\n\t\t'.htaccess': 'seti:config',\n\t\t'.key': 'seti:lock',\n\t\t'.cert': 'seti:lock',\n\t\t'.cer': 'seti:lock',\n\t\t'.crt': 'seti:lock',\n\t\t'.pem': 'seti:lock',\n\t\t'.DS_Store': 'seti:ignored',\n\t\t'.hcl': 'custom:terragrunt',\n\t\t'.tfplan': 'custom:opentofu',\n\t},\n\tpartials: {\n\t\tmix: 'seti:hex',\n\t\tGemfile: 'seti:ruby',\n\t\tgemfile: 'seti:ruby',\n\t\tdockerfile: 'seti:docker',\n\t\tDockerfile: 'seti:docker',\n\t\tDOCKERFILE: 'seti:docker',\n\t\t'.dockerignore': 'seti:docker',\n\t\t'docker-healthcheck': 'seti:docker',\n\t\t'docker-compose.yml': 'seti:docker',\n\t\t'docker-compose.yaml': 'seti:docker',\n\t\t'docker-compose.override.yml': 'seti:docker',\n\t\t'docker-compose.override.yaml': 'seti:docker',\n\t\tGULPFILE: 'seti:gulp',\n\t\tGulpfile: 'seti:gulp',\n\t\tgulpfile: 'seti:gulp',\n\t\t'gulpfile.js': 'seti:gulp',\n\t\tLICENSE: 'seti:license',\n\t\tLICENCE: 'seti:license',\n\t\t'LICENSE.txt': 'seti:license',\n\t\t'LICENCE.txt': 'seti:license',\n\t\t'LICENSE.md': 'seti:license',\n\t\t'LICENCE.md': 'seti:license',\n\t\tCOPYING: 'seti:license',\n\t\t'COPYING.txt': 'seti:license',\n\t\t'COPYING.md': 'seti:license',\n\t\tCOMPILING: 'seti:license',\n\t\t'COMPILING.txt': 'seti:license',\n\t\t'COMPILING.md': 'seti:license',\n\t\tCONTRIBUTING: 'seti:license',\n\t\t'CONTRIBUTING.txt': 'seti:license',\n\t\t'CONTRIBUTING.md': 'seti:license',\n\t\tMAKEFILE: 'seti:makefile',\n\t\tMakefile: 'seti:makefile',\n\t\tmakefile: 'seti:makefile',\n\t\tQMAKEFILE: 'seti:makefile',\n\t\tQMakefile: 'seti:makefile',\n\t\tqmakefile: 'seti:makefile',\n\t\tOMAKEFILE: 'seti:makefile',\n\t\tOMakefile: 'seti:makefile',\n\t\tomakefile: 'seti:makefile',\n\t\t'CMAKELISTS.TXT': 'seti:makefile',\n\t\t'CMAKELISTS.txt': 'seti:makefile',\n\t\t'CMakeLists.txt': 'seti:makefile',\n\t\t'cmakelists.txt': 'seti:makefile',\n\t\tProcfile: 'seti:heroku',\n\t\tTODO: 'seti:todo',\n\t\t'TODO.txt': 'seti:todo',\n\t\t'TODO.md': 'seti:todo',\n\t},\n};\n\nexport const FileIcons = {\n\t'seti:folder':\n\t\t'<path d=\"M22.073 4.900L22.073 4.900L12.148 4.900L12.148 3.950Q12.148 3.125 11.585 2.563Q11.023 2 10.198 2L10.198 2L0.048 2L0.048 22L23.948 22L23.948 6.850Q23.998 6.025 23.448 5.462Q22.898 4.900 22.073 4.900Z\"/>',\n\t'seti:bsl':\n\t\t'<path d=\"M23.696 15.213L12.850 15.213L12.850 18.375L23.696 18.375L23.696 15.213ZM2.446 18.375L2.446 5.625L5.642 5.625L5.642 18.375L2.446 18.375ZM0.304 9.875L0.304 6.713L2.446 6.713L2.446 9.875L0.304 9.875ZM9.892 12.017L9.892 12.017Q9.892 10.657 10.793 9.739Q11.694 8.821 13.054 8.821L13.054 8.821Q14.108 8.821 14.907 9.416Q15.706 10.011 16.046 10.963L16.046 10.963L19.344 10.963Q19.072 9.467 18.188 8.260Q17.304 7.053 15.961 6.339Q14.618 5.625 13.054 5.625L13.054 5.625Q11.354 5.625 9.875 6.492Q8.396 7.359 7.546 8.821Q6.696 10.283 6.696 12.017Q6.696 13.751 7.546 15.213Q8.396 16.675 9.875 17.525Q11.354 18.375 13.071 18.375Q14.788 18.375 16.284 17.508Q17.780 16.641 18.596 15.213L18.596 15.213L13.054 15.213Q11.694 15.213 10.793 14.295Q9.892 13.377 9.892 12.017Z\"/>',\n\t'seti:mdo':\n\t\t'<path d=\"M14.375 14.128L14.375 14.014Q13.463 14.014 12.836 13.387Q12.209 12.760 12.209 11.867Q12.209 10.974 12.836 10.366Q13.463 9.758 14.375 9.758L14.375 9.758Q14.983 9.758 15.553 10.100Q16.123 10.442 16.389 11.050L16.389 11.050L18.175 11.050Q17.795 9.720 16.750 8.903Q15.705 8.086 14.375 8.086L14.375 8.086Q13.311 8.086 12.399 8.618Q11.487 9.150 10.955 10.043Q10.423 10.936 10.423 12Q10.423 13.064 10.955 13.957Q11.487 14.850 12.399 15.382Q13.311 15.914 14.375 15.914L14.375 15.914L19.923 15.914L19.923 14.128L14.375 14.128ZM7.839 15.800L7.839 8.086L9.625 8.086L9.625 15.800L7.839 15.800ZM6.167 11.164L6.167 9.378L7.953 9.378L7.953 11.164L6.167 11.164ZM20.189 3.792L20.189 3.792L4.153 3.792L0.125 12L2.139 16.066Q4.153 20.132 4.153 20.208L4.153 20.208L20.189 20.208Q21.671 20.208 22.773 19.125Q23.875 18.042 23.875 16.522L23.875 16.522L23.875 7.364Q23.875 5.920 22.811 4.856Q21.747 3.792 20.189 3.792ZM22.089 7.478L22.089 16.636Q22.089 17.396 21.557 17.909Q21.025 18.422 20.303 18.422L20.303 18.422L5.559 18.422L3.735 15.458Q1.873 12.456 1.873 12.228L1.873 12.228L5.559 5.692L20.303 5.692Q21.063 5.692 21.576 6.224Q22.089 6.756 22.089 7.478L22.089 7.478Z\"/>',\n\t'seti:salesforce':\n\t\t'<path d=\"M6.648 3.525L6.696 3.525Q8.232 3.765 9.240 4.581L9.240 4.581L9.528 4.869Q9.864 5.109 9.960 5.301L9.960 5.301L10.104 5.301Q10.248 5.157 10.560 4.941Q10.872 4.725 11.016 4.581L11.016 4.581L12.072 4.101Q12.360 4.101 12.696 3.957L12.696 3.957L13.560 3.957Q14.376 3.957 15.096 4.437L15.096 4.437Q16.104 5.061 16.728 6.069L16.728 6.069Q16.728 6.165 16.800 6.165Q16.872 6.165 16.872 6.069L16.872 6.069L17.160 5.973Q17.736 5.781 18.072 5.781L18.072 5.781L19.272 5.781Q20.040 5.781 21.240 6.357L21.240 6.357L21.576 6.597Q22.200 6.933 22.440 7.269L22.440 7.269Q23.016 7.893 23.496 8.757L23.496 8.757Q23.928 9.429 23.928 10.437L23.928 10.437L23.928 11.637Q23.928 12.117 23.736 12.645L23.736 12.645Q23.640 12.981 23.304 13.557L23.304 13.557Q22.872 14.469 22.104 15.237L22.104 15.237Q21.576 15.765 20.904 15.957L20.904 15.957L19.560 16.437L17.640 16.437Q17.400 16.869 16.776 17.445L16.776 17.445L16.440 17.781Q15.864 18.117 14.904 18.357L14.904 18.357L14.040 18.357Q13.656 18.357 13.080 18.165L13.080 18.165L12.840 18.069L12.696 18.069Q12.552 18.405 12.168 18.885L12.168 18.885L11.928 19.269Q11.496 19.941 10.728 20.181L10.728 20.181Q9.336 20.853 7.824 20.493Q6.312 20.133 5.304 18.837L5.304 18.837Q4.968 18.453 4.728 17.781L4.728 17.781Q4.728 17.685 4.632 17.733L4.632 17.733L4.584 17.781L3.528 17.781Q3.288 17.781 2.952 17.709Q2.616 17.637 2.472 17.637L2.472 17.637Q1.896 17.445 1.272 17.013L1.272 17.013Q0.360 16.149 0.072 15.525L0.072 15.525Q0.312 14.613 0.072 14.181L0.072 14.181L0.072 13.269Q0.072 12.693 0.360 12.093Q0.648 11.493 0.960 11.181Q1.272 10.869 1.272 10.725L1.272 10.725Q1.704 10.293 2.040 10.149L2.040 10.149L2.088 10.101Q2.136 10.053 2.040 9.957L2.040 9.957Q1.704 9.381 1.704 8.949L1.704 8.949L1.704 7.557Q1.944 5.877 3.168 4.677Q4.392 3.477 6.072 3.381L6.072 3.381L6.360 3.381Q6.504 3.525 6.648 3.525L6.648 3.525L6.648 3.525Z\"/>',\n\t'seti:asm':\n\t\t'<path d=\"M1.322 16.313L0.106 16.313L3.222 7.687L4.856 7.687L7.972 16.313L6.300 16.313L5.426 13.995L2.158 13.995L1.322 16.313ZM3.792 9.435L2.538 12.931L5.008 12.931L3.792 9.435ZM8.390 16.237L8.390 16.237L8.390 14.793Q10.214 15.401 11.240 15.401L11.240 15.401Q12.000 15.401 12.570 15.097L12.570 15.097Q12.836 14.907 12.950 14.717Q13.064 14.527 13.064 14.223L13.064 14.223Q13.064 13.767 12.798 13.539L12.798 13.539Q12.456 13.159 11.658 12.855L11.658 12.855L10.936 12.551Q9.758 11.943 8.960 11.221L8.960 11.221Q8.390 10.613 8.390 9.853L8.390 9.853Q8.390 8.713 9.340 8.143L9.340 8.143Q10.290 7.459 11.886 7.459L11.886 7.459Q13.330 7.459 14.622 7.763L14.622 7.763L14.622 9.131Q13.026 8.675 12.076 8.675L12.076 8.675Q11.392 8.675 10.936 8.903L10.936 8.903Q10.518 9.245 10.518 9.625Q10.518 10.005 10.822 10.233L10.822 10.233Q11.240 10.537 11.962 10.917L11.962 10.917L12.646 11.221Q14.128 11.943 14.698 12.551Q15.268 13.159 15.268 13.995L15.268 13.995Q15.268 14.603 15.002 15.097Q14.736 15.591 14.204 15.857L14.204 15.857Q13.254 16.541 11.316 16.541L11.316 16.541Q9.910 16.541 8.390 16.237ZM17.168 16.313L15.914 16.313L15.914 7.763L18.118 7.763L20.018 13.843L21.994 7.763L23.894 7.763L23.894 16.313L22.336 16.313L22.336 9.777L20.436 15.705L19.106 15.705L17.168 9.701L17.168 16.313Z\"/>',\n\t'seti:bicep':\n\t\t'<path d=\"M19.224 14.604L19.224 14.604L8.052 14.604L10.698 8.052L15.024 8.052L15.948 6.078L12 6.078L10.698 4.104L12 2.172L15.948 2.172L15.024 0.198L10.698 0.198L7.800 4.104L8.976 6.078L1.122 15.948Q0.702 16.452 0.450 17.166Q0.198 17.880 0.198 18.552L0.198 18.552Q0.198 20.022 1.143 21.156Q2.088 22.290 3.474 22.500L3.474 22.500L10.614 23.172Q18.006 23.802 19.224 23.802Q20.442 23.802 21.492 23.172Q22.542 22.542 23.172 21.492Q23.802 20.442 23.802 19.224Q23.802 18.006 23.172 16.935Q22.542 15.864 21.492 15.234Q20.442 14.604 19.224 14.604ZM4.146 20.526L4.146 20.526Q3.348 20.526 2.760 19.938Q2.172 19.350 2.172 18.552Q2.172 17.754 2.760 17.166Q3.348 16.578 4.125 16.578Q4.902 16.578 5.490 17.166Q6.078 17.754 6.078 18.552Q6.078 19.350 5.490 19.938Q4.902 20.526 4.146 20.526ZM19.224 21.828L19.224 21.828Q18.132 21.828 17.355 21.072Q16.578 20.316 16.578 19.224Q16.578 18.132 17.355 17.355Q18.132 16.578 19.224 16.578Q20.316 16.578 21.072 17.355Q21.828 18.132 21.828 19.224Q21.828 20.316 21.072 21.072Q20.316 21.828 19.224 21.828Z\"/>',\n\t'seti:bazel':\n\t\t'<path d=\"M0.198 6.078L6.078 0.198L12 6.078L17.922 0.198L23.802 6.078L23.802 12L12 23.802L0.198 12L0.198 6.078Z\"/>',\n\t'seti:c':\n\t\t'<path d=\"M21.330 17.405L21.330 22.152Q19.732 22.951 18.063 23.398Q16.395 23.844 14.562 23.844L14.562 23.844Q9.063 23.844 5.867 20.648Q2.671 17.452 2.671 12Q2.671 6.548 5.867 3.352Q9.063 0.156 14.562 0.156L14.562 0.156Q16.395 0.156 18.087 0.579Q19.779 1.002 21.330 1.848L21.330 1.848L21.330 6.595Q19.732 5.467 18.204 4.950Q16.677 4.433 15.031 4.433L15.031 4.433Q11.976 4.433 10.238 6.454Q8.499 8.475 8.499 12Q8.499 15.525 10.238 17.546Q11.976 19.567 15.031 19.567L15.031 19.567Q16.677 19.567 18.204 19.050Q19.732 18.533 21.330 17.405L21.330 17.405Z\"/>',\n\t'seti:c-sharp':\n\t\t'<path d=\"M0.261 11.937L0.261 11.937Q0.261 9.417 1.038 7.464Q1.815 5.511 3.117 4.209Q4.419 2.907 6.309 2.067L6.309 2.067Q7.779 1.437 10.089 1.437L10.089 1.437Q12.063 1.437 13.638 2.214Q15.213 2.991 16.263 4.041L16.263 4.041L13.365 7.317L13.029 7.065Q12.357 6.519 11.937 6.435L11.937 6.435L11.517 6.309Q10.677 6.015 10.089 6.015L10.089 6.015Q9.165 6.015 8.409 6.435L8.409 6.435Q7.485 6.855 6.939 7.590Q6.393 8.325 6.015 9.480Q5.637 10.635 5.637 11.937L5.637 11.937Q5.637 14.961 6.939 16.263L6.939 16.263Q7.653 17.061 8.472 17.460Q9.291 17.859 10.341 17.859Q11.391 17.859 12.189 17.439L12.189 17.439Q13.071 17.019 13.659 16.263L13.659 16.263L16.515 19.539Q15.297 20.925 13.659 21.765L13.659 21.765Q12.063 22.563 10.089 22.563L10.089 22.563Q8.283 22.563 6.309 21.891L6.309 21.891Q4.503 21.303 3.159 19.959L3.159 19.959Q2.529 19.329 2.025 18.321L2.025 18.321Q1.689 17.691 1.185 16.389L1.185 16.389Q0.261 14.751 0.261 11.937ZM19.203 15.129L19.539 13.491L18.237 13.491L17.733 16.641L16.137 16.641L16.683 13.491L15.087 13.491L15.087 12.063L17.061 12.063L17.313 9.963L15.633 9.963L15.633 8.535L17.565 8.535L18.111 5.385L19.665 5.385L19.161 8.535L20.463 8.535L21.009 5.385L22.563 5.385L22.059 8.535L23.739 8.535L23.739 9.963L21.639 9.963L21.387 12.063L23.109 12.063L23.109 13.491L21.009 13.491L20.463 16.641L18.909 16.641Q18.909 16.725 19.203 15.129L19.203 15.129ZM18.783 10.089L18.489 12.189L19.833 12.189L20.085 10.089L18.783 10.089Z\"/>',\n\t'seti:html':\n\t\t'<path d=\"M0 13.488L0 10.512L9.024 2.112L9.024 6L2.256 12L9.024 18L9.024 21.888L0 13.488ZM24 10.368L24 13.632L15.024 22.032L15.024 18L21.888 12L15.024 6L15.024 1.968L24 10.368Z\"/>',\n\t'seti:cpp':\n\t\t'<path d=\"M0.024 11.935L0.024 11.935Q0.024 9.398 0.841 7.377Q1.659 5.356 2.970 4.045Q4.282 2.733 6.216 1.873L6.216 1.873Q7.722 1.185 10.130 1.185L10.130 1.185Q12.151 1.185 13.763 2.002Q15.376 2.819 16.451 3.894L16.451 3.894L13.484 7.248L13.140 6.990Q12.409 6.431 12.021 6.302L12.021 6.302L11.549 6.173Q10.689 5.915 10.130 5.915L10.130 5.915Q9.184 5.915 8.367 6.302L8.367 6.302Q7.463 6.775 6.905 7.528Q6.346 8.280 5.959 9.441Q5.572 10.602 5.572 11.935L5.572 11.935Q5.572 15.031 6.905 16.364L6.905 16.364Q7.593 17.224 8.453 17.611Q9.313 17.998 10.387 17.998Q11.463 17.998 12.280 17.611L12.280 17.611Q13.183 17.138 13.742 16.364L13.742 16.364L16.709 19.761Q15.462 21.180 13.742 22.041L13.742 22.041Q12.151 22.814 10.130 22.814L10.130 22.814Q8.238 22.814 6.216 22.169L6.216 22.169Q4.411 21.567 2.992 20.148L2.992 20.148Q2.389 19.503 1.831 18.471L1.831 18.471Q1.530 17.869 0.970 16.537L0.970 16.537Q0.024 14.644 0.024 11.935ZM13.355 10.731L13.355 8.452L11.463 8.452L11.463 10.731L9.313 10.731L9.313 12.623L11.463 12.623L11.463 15.031L13.355 15.031L13.355 12.623L15.505 12.623L15.505 10.731L13.355 10.731ZM23.976 10.602L21.396 10.602L21.396 7.936L19.117 7.936L19.117 10.602L16.580 10.602L16.580 12.881L19.117 12.881L19.117 15.720L21.396 15.720L21.396 12.881L23.976 12.881L23.976 10.602Z\"/>',\n\t'seti:clojure':\n\t\t'<path d=\"M11.426 12.105L11.426 12.105L11.006 12.903Q9.998 14.919 9.830 16.179L9.830 16.179Q9.704 16.473 9.704 17.229L9.704 17.229L9.704 17.775Q10.670 18.153 11.804 18.153L11.804 18.153Q12.476 18.153 13.778 17.901L13.778 17.901L13.400 17.523Q12.896 16.683 12.434 15.339L12.434 15.339Q12.056 14.247 11.426 12.105ZM8.402 6.729L8.402 6.729Q7.184 7.653 6.470 8.997Q5.756 10.341 5.756 11.874Q5.756 13.407 6.470 14.730Q7.184 16.053 8.402 16.977L8.402 16.977Q8.570 16.263 8.990 15.339L8.990 15.339Q9.242 14.751 9.956 13.407L9.956 13.407Q10.712 11.895 11.132 10.929L11.132 10.929Q11.090 10.803 11.027 10.551Q10.964 10.299 10.880 10.173L10.880 10.173Q10.208 8.367 9.452 7.401L9.452 7.401Q8.864 7.191 8.402 6.729ZM17.432 19.077L17.432 19.077Q15.878 18.825 15.332 18.699L15.332 18.699Q13.778 19.455 12.056 19.455L12.056 19.455Q9.998 19.455 8.297 18.447Q6.596 17.439 5.588 15.717Q4.580 13.995 4.580 11.979L4.580 11.979Q4.580 10.383 5.294 8.871L5.294 8.871Q5.924 7.485 7.100 6.351L7.100 6.351Q6.680 6.225 5.756 6.225L5.756 6.225Q3.950 6.225 2.564 7.233L2.564 7.233Q1.010 8.409 0.254 10.803L0.254 10.803Q0.128 11.349 0.128 12.105L0.128 12.105Q0.128 15.339 1.703 18.048Q3.278 20.757 5.987 22.353Q8.696 23.949 11.930 23.949L11.930 23.949Q14.828 23.949 17.432 22.563L17.432 22.563Q19.952 21.261 21.632 18.951L21.632 18.951Q19.868 19.329 18.356 19.329L18.356 19.329Q17.978 19.077 17.432 19.077ZM11.930 0.051L11.930 0.051Q9.032 0.051 6.491 1.395Q3.950 2.739 2.354 5.049L2.354 5.049Q3.824 4.125 5.630 4.125L5.630 4.125Q6.512 4.125 7.436 4.335L7.436 4.335Q8.150 4.545 8.654 4.755L8.654 4.755Q8.738 4.839 8.864 4.902Q8.990 4.965 9.032 5.049L9.032 5.049Q10.586 4.377 12.056 4.377L12.056 4.377Q14.114 4.377 15.836 5.406Q17.558 6.435 18.545 8.136Q19.532 9.837 19.532 11.853L19.532 11.853Q19.532 14.793 17.306 17.229L17.306 17.229L18.356 17.229Q20.834 17.229 22.178 16.053L22.178 16.053Q23.396 15.087 23.732 13.323L23.732 13.323Q23.732 13.029 23.816 12.483L23.816 12.483Q23.900 12.063 23.858 11.853L23.858 11.853Q23.816 8.619 22.199 5.910Q20.582 3.201 17.936 1.647L17.936 1.647Q15.164 0.051 11.930 0.051Z\"/>',\n\t'seti:coldfusion':\n\t\t'<path d=\"M6.963 10.543L6.963 10.543Q6.808 10.543 6.560 10.512L6.560 10.512Q5.940 10.481 5.692 10.543L5.692 10.543L5.382 10.574Q3.925 10.760 3.305 10.977L3.305 10.977Q2.220 11.349 1.631 12.186L1.631 12.186Q0.577 13.519 0.407 14.945Q0.236 16.371 0.949 17.797L0.949 17.797Q2.313 20.556 5.692 20.711L5.692 20.711L9.567 20.711Q13.163 20.587 15.364 17.611L15.364 17.611Q16.542 16.154 17.131 14.418L17.131 14.418Q17.348 13.829 17.674 13.597Q17.999 13.364 18.588 13.364L18.588 13.364Q19.084 13.426 20.045 13.364L20.045 13.364L20.789 13.364Q21.130 13.364 21.254 13.302L21.254 13.302Q21.471 13.209 21.471 12.961L21.471 12.961L21.533 12.620Q21.626 11.969 21.626 11.659L21.626 11.659Q21.595 11.132 21.378 10.729L21.378 10.729Q21.254 10.481 20.975 10.419L20.975 10.419Q20.820 10.388 20.448 10.434Q20.076 10.481 19.921 10.450L19.921 10.450L19.456 10.450Q19.053 10.450 18.945 10.311Q18.836 10.171 18.960 9.768L18.960 9.768Q19.921 7.536 21.967 6.854L21.967 6.854Q22.122 6.792 22.510 6.761Q22.897 6.730 23.052 6.637L23.052 6.637Q23.331 6.544 23.424 6.296L23.424 6.296Q23.579 6.079 23.610 5.707L23.610 5.707Q23.641 5.490 23.610 4.994L23.610 4.994L23.610 3.754Q23.610 3.289 23.021 3.289L23.021 3.289Q21.471 3.444 20.386 3.878L20.386 3.878Q19.084 4.405 18.278 5.397L18.278 5.397Q17.410 6.575 16.635 8.001L16.635 8.001Q16.015 9.210 15.364 10.822L15.364 10.822L15.178 11.411Q14.558 13.054 14.217 13.829L14.217 13.829Q13.597 15.100 12.853 15.968L12.853 15.968Q11.954 17.022 10.621 17.332L10.621 17.332Q7.986 17.735 5.103 17.146L5.103 17.146Q4.390 16.991 4.018 16.526Q3.646 16.061 3.646 15.364Q3.646 14.666 4.080 14.170Q4.514 13.674 5.289 13.457L5.289 13.457Q5.878 13.333 7.025 13.302L7.025 13.302Q7.831 13.302 8.234 13.287Q8.637 13.271 8.808 13.101Q8.978 12.930 8.978 12.496L8.978 12.496L8.978 11.938Q8.978 11.070 8.947 10.853L8.947 10.853Q8.885 10.543 8.637 10.481L8.637 10.481Q8.482 10.419 7.800 10.450L7.800 10.450L7.056 10.450Q7.118 10.512 7.087 10.527Q7.056 10.543 6.963 10.543Z\"/>',\n\t'seti:config':\n\t\t'<path d=\"M23.165 13.645L23.165 13.645Q22.829 13.561 22.199 13.309L22.199 13.309L21.695 13.099Q21.695 12.637 21.632 11.797Q21.569 10.957 21.569 10.495L21.569 10.495Q21.569 10.411 21.632 10.348Q21.695 10.285 21.695 10.201L21.695 10.201L23.165 9.571Q23.543 9.361 23.669 9.004Q23.795 8.647 23.669 8.269L23.669 8.269L22.493 5.623Q22.283 5.203 21.926 5.056Q21.569 4.909 21.191 5.119L21.191 5.119L19.721 5.749Q19.469 5.749 19.469 5.623L19.469 5.623Q19.301 5.371 18.839 4.951L18.839 4.951L18.545 4.699Q18.419 4.573 18.104 4.300Q17.789 4.027 17.621 3.901L17.621 3.901Q17.789 3.649 17.978 3.124Q18.167 2.599 18.293 2.347L18.293 2.347Q18.503 1.843 18.314 1.465Q18.125 1.087 17.621 0.919L17.621 0.919Q17.075 0.793 15.815 0.373L15.815 0.373L15.143 0.121Q14.639-0.089 14.261 0.100Q13.883 0.289 13.715 0.751L13.715 0.751Q13.631 1.087 13.379 1.717L13.379 1.717L13.169 2.221Q12.707 2.221 11.867 2.284Q11.027 2.347 10.565 2.347L10.565 2.347Q10.481 2.347 10.418 2.284Q10.355 2.221 10.271 2.221L10.271 2.221L9.641 0.751Q9.431 0.373 9.074 0.247Q8.717 0.121 8.339 0.247L8.339 0.247L5.693 1.423Q5.273 1.633 5.126 1.990Q4.979 2.347 5.189 2.725L5.189 2.725L5.819 4.195Q5.819 4.447 5.693 4.447L5.693 4.447L5.399 4.699Q5.105 4.909 5.021 5.077L5.021 5.077Q4.853 5.287 4.517 5.686Q4.181 6.085 3.971 6.295L3.971 6.295Q3.677 6.211 3.047 5.959L3.047 5.959L2.543 5.749Q2.039 5.539 1.661 5.728Q1.283 5.917 1.115 6.421L1.115 6.421L0.905 6.925Q0.401 8.185 0.191 8.773Q-0.019 9.361 0.128 9.697Q0.275 10.033 0.821 10.201L0.821 10.201Q1.157 10.285 1.787 10.537L1.787 10.537L2.291 10.747Q2.291 11.209 2.354 12.049Q2.417 12.889 2.417 13.351L2.417 13.351Q2.417 13.435 2.354 13.498Q2.291 13.561 2.291 13.645L2.291 13.645L0.821 14.275Q0.443 14.485 0.317 14.842Q0.191 15.199 0.317 15.577L0.317 15.577L1.493 18.223Q1.703 18.643 2.060 18.790Q2.417 18.937 2.795 18.727L2.795 18.727L4.265 18.097Q4.517 18.097 4.517 18.223L4.517 18.223Q4.685 18.475 5.147 18.895L5.147 18.895L5.441 19.147Q5.567 19.273 5.882 19.525Q6.197 19.777 6.365 19.945L6.365 19.945Q6.239 20.197 6.029 20.722Q5.819 21.247 5.693 21.499L5.693 21.499Q5.483 22.003 5.672 22.381Q5.861 22.759 6.365 22.969L6.365 22.969Q6.659 23.053 7.331 23.305L7.331 23.305Q8.297 23.725 8.864 23.893Q9.431 24.061 9.767 23.935Q10.103 23.809 10.271 23.221L10.271 23.221Q10.355 22.885 10.607 22.255L10.607 22.255L10.817 21.751Q11.279 21.751 12.119 21.688Q12.959 21.625 13.421 21.625L13.421 21.625Q13.505 21.625 13.568 21.688Q13.631 21.751 13.715 21.751L13.715 21.751L14.345 23.221Q14.555 23.599 14.912 23.725Q15.269 23.851 15.647 23.725L15.647 23.725L18.293 22.549Q18.713 22.339 18.860 21.982Q19.007 21.625 18.797 21.247L18.797 21.247L18.167 19.819Q18.167 19.525 18.293 19.525L18.293 19.525Q18.545 19.357 18.965 18.895L18.965 18.895L19.217 18.601Q19.343 18.475 19.595 18.160Q19.847 17.845 20.015 17.719L20.015 17.719Q20.309 17.761 20.939 18.055L20.939 18.055L21.443 18.223Q21.947 18.433 22.325 18.244Q22.703 18.055 22.871 17.551L22.871 17.551Q22.997 17.257 23.249 16.585L23.249 16.585Q23.669 15.619 23.795 15.073L23.795 15.073Q24.257 13.981 23.165 13.645ZM13.967 16.375L13.967 16.375Q12.077 17.173 10.250 16.459Q8.423 15.745 7.541 13.897L7.541 13.897Q6.743 12.007 7.457 10.180Q8.171 8.353 10.019 7.471L10.019 7.471Q11.909 6.673 13.736 7.387Q15.563 8.101 16.445 9.949L16.445 9.949Q17.243 11.839 16.529 13.666Q15.815 15.493 13.967 16.375Z\"/>',\n\t'seti:crystal':\n\t\t'<path d=\"M17.084 3.165L12.000 0.220L6.916 3.165L1.801 6.110L1.801 17.890L12.000 23.780L22.199 17.890L22.199 6.110L17.084 3.165ZM12.000 11.938L9.334 16.557L6.668 11.938L4.002 7.319L14.666 7.319L12.000 11.938Z\"/>',\n\t'seti:crystal_embedded':\n\t\t'<path d=\"M17.084 3.165L12.000 0.220L6.916 3.165L1.801 6.110L1.801 17.890L12.000 23.780L22.199 17.890L22.199 6.110L17.084 3.165ZM11.411 15.596L10.140 16.867L4.653 12.248L10.140 7.660L11.411 8.931L7.381 12.248L11.411 15.596ZM12.620 8.931L13.860 7.660L19.347 12.248L13.860 16.867L12.620 15.596L16.619 12.279L12.620 8.931Z\"/>',\n\t'seti:json':\n\t\t'<path d=\"M0.734 13.269L0.562 10.732Q1.938 10.732 2.497 10.087L2.497 10.087Q2.884 9.614 2.884 8.711L2.884 8.711Q2.884 8.324 2.798 7.571Q2.712 6.819 2.712 6.410Q2.712 6.002 2.669 5.185L2.669 5.185Q2.583 4.454 2.583 4.153L2.583 4.153Q2.583 2.089 3.787 1.099Q4.991 0.111 7.184 0.111L7.184 0.111L8.259 0.111L8.259 2.648L7.700 2.648Q6.754 2.648 6.345 3.185Q5.937 3.723 5.937 4.798L5.937 4.798Q5.937 5.056 6.023 5.572L6.023 5.572Q6.109 6.217 6.109 6.561L6.109 6.561Q6.109 6.819 6.152 7.378L6.152 7.378Q6.238 8.152 6.238 8.582L6.238 8.582Q6.238 10.216 5.550 11.033L5.550 11.033Q4.948 11.764 3.658 12.065L3.658 12.065Q4.948 12.409 5.550 13.097L5.550 13.097Q6.238 13.957 6.238 15.548L6.238 15.548Q6.238 16.021 6.152 16.795L6.152 16.795Q6.066 17.354 6.088 17.612Q6.109 17.870 6.023 18.515L6.023 18.515Q5.937 18.988 5.937 19.203L5.937 19.203Q5.937 20.278 6.345 20.815Q6.754 21.353 7.700 21.353L7.700 21.353L8.259 21.353L8.259 23.890L7.184 23.890Q2.712 23.890 2.712 19.848L2.712 19.848Q2.712 18.386 2.862 17.590Q3.013 16.795 3.013 15.290L3.013 15.290Q3.013 13.269 0.734 13.269L0.734 13.269ZM23.438 10.732L23.438 13.011Q21.159 13.011 21.159 15.032L21.159 15.032Q21.159 15.419 21.224 16.171Q21.288 16.924 21.288 17.311L21.288 17.311Q21.417 18.128 21.417 19.590L21.417 19.590Q21.417 23.632 16.859 23.632L16.859 23.632L15.784 23.632L15.784 21.353L16.300 21.353Q17.246 21.353 17.654 20.815Q18.063 20.278 18.063 19.203Q18.063 18.128 17.934 17.569L17.934 17.569Q17.934 17.225 17.848 16.558Q17.762 15.892 17.762 15.548L17.762 15.548Q17.762 13.957 18.450 13.097L18.450 13.097Q19.052 12.409 20.342 12.065L20.342 12.065Q19.052 11.764 18.450 11.033L18.450 11.033Q17.762 10.216 17.762 8.582L17.762 8.582Q17.762 8.152 17.848 7.378L17.848 7.378Q17.934 6.819 17.934 6.561L17.934 6.561Q18.063 5.873 18.063 4.841Q18.063 3.809 17.633 3.293Q17.203 2.777 16.300 2.648L16.300 2.648L15.784 2.648L15.784 0.111L16.859 0.111Q19.009 0.111 20.213 1.099Q21.417 2.089 21.417 4.153L21.417 4.153Q21.417 4.540 21.352 5.292Q21.288 6.045 21.288 6.432L21.288 6.432Q21.159 7.249 21.116 8.711L21.116 8.711Q21.159 9.614 21.503 10.087L21.503 10.087Q22.062 10.732 23.438 10.732L23.438 10.732Z\"/>',\n\t'seti:css':\n\t\t'<path d=\"M7.486 23.628L2.845 23.628L4.120 17.253L0.142 17.253L0.142 13.887L4.936 13.887L5.701 9.909L1.570 9.909L1.570 6.594L6.517 6.594L7.792 0.372L12.229 0.372L10.954 6.594L15.442 6.594L16.717 0.372L21.154 0.372L19.880 6.594L23.858 6.594L23.858 9.909L19.267 9.909L18.299 13.887L22.429 13.887L22.429 17.253L17.686 17.253L16.412 23.628L11.924 23.628L13.198 17.253L8.761 17.253L7.486 23.628ZM9.373 13.938L13.861 13.938L14.627 9.909L10.189 9.909L9.373 13.938Z\"/>',\n\t'seti:csv':\n\t\t'<path d=\"M23.802 0.324L0.198 0.324L0.198 23.676L23.802 23.676L23.802 0.324ZM3.978 6.876L3.978 5.154L10.698 5.154L10.698 6.876L3.978 6.876ZM13.176 6.876L13.176 5.154L19.896 5.154L19.896 6.876L13.176 6.876ZM3.978 10.824L3.978 9.102L10.698 9.102L10.698 10.824L3.978 10.824ZM13.176 10.824L13.176 9.102L19.896 9.102L19.896 10.824L13.176 10.824ZM3.978 14.772L3.978 13.050L10.698 13.050L10.698 14.772L3.978 14.772ZM13.176 14.772L13.176 13.050L19.896 13.050L19.896 14.772L13.176 14.772ZM3.978 18.678L3.978 16.998L10.698 16.998L10.698 18.678L3.978 18.678ZM13.176 18.678L13.176 16.998L19.896 16.998L19.896 18.678L13.176 18.678Z\"/>',\n\t'seti:xls':\n\t\t'<path d=\"M23.780 3.944L23.818 3.944L23.818 20.094Q23.818 20.132 23.742 20.246Q23.666 20.360 23.666 20.436L23.666 20.436Q23.286 21.044 22.716 21.044L22.716 21.044L13.596 21.044L13.596 23.400Q13.406 23.362 12.988 23.286Q12.570 23.210 12.418 23.172L12.418 23.172Q12 23.096 11.107 22.925Q10.214 22.754 9.758 22.697Q9.302 22.640 8.352 22.450Q7.402 22.260 6.927 22.203Q6.452 22.146 5.502 21.975Q4.552 21.804 4.115 21.747Q3.678 21.690 2.785 21.500Q1.892 21.310 1.474 21.272L1.474 21.272Q1.246 21.196 0.828 21.139Q0.410 21.082 0.182 21.044L0.182 21.044L0.182 3.564Q0.220 3.564 0.410 3.507Q0.600 3.450 0.638 3.450L0.638 3.450Q1.664 2.728 3.374 2.500L3.374 2.500Q3.830 2.424 4.742 2.234L4.742 2.234Q5.426 2.082 5.730 2.044L5.730 2.044L6.034 1.968Q7.364 1.778 8.010 1.550L8.010 1.550Q8.428 1.512 9.321 1.322Q10.214 1.132 10.651 1.075Q11.088 1.018 11.981 0.847Q12.874 0.676 13.368 0.600L13.368 0.600L13.482 0.600L13.482 2.842L22.716 2.842Q23.096 2.842 23.400 3.089Q23.704 3.336 23.780 3.678L23.780 3.678L23.780 3.944ZM22.868 19.714L22.868 3.792L13.482 3.792L13.482 5.236L16.674 5.236L16.674 7.250L13.482 7.250L13.482 7.972L16.674 7.972L16.674 9.986L13.482 9.986L13.482 10.708L16.674 10.708L16.674 12.722L13.482 12.722L13.482 13.558L16.788 13.558L16.788 15.572L13.482 15.572L13.482 16.294L16.674 16.294L16.674 18.308L13.482 18.308L13.482 19.714L22.868 19.714ZM10.366 17.472L10.366 17.472Q10.366 17.434 10.347 17.396Q10.328 17.358 10.252 17.358L10.252 17.358Q9.834 16.522 8.941 14.850Q8.048 13.178 7.630 12.342L7.630 12.342L7.630 12.114Q8.846 9.758 10.252 7.250L10.252 7.250L10.252 7.136L10.138 7.136Q9.530 7.136 9.302 7.250L9.302 7.250Q8.618 7.250 8.124 7.364L8.124 7.364Q8.048 7.364 8.029 7.383Q8.010 7.402 8.010 7.478L8.010 7.478L7.060 9.492L6.338 11.164Q6.300 11.050 6.224 10.746Q6.148 10.442 6.110 10.328L6.110 10.328L5.046 7.744Q4.932 7.554 4.894 7.516Q4.856 7.478 4.647 7.478Q4.438 7.478 3.963 7.535Q3.488 7.592 3.260 7.592L3.260 7.592L2.766 7.592L2.766 7.744L3.146 8.542L4.932 12.114L4.932 12.228Q4.552 12.950 3.754 14.470L3.754 14.470Q3.032 15.800 2.652 16.522L2.652 16.522Q2.652 16.560 2.595 16.636Q2.538 16.712 2.538 16.750L2.538 16.750L3.260 16.750Q3.488 16.750 3.906 16.807Q4.324 16.864 4.552 16.864L4.552 16.864Q4.666 16.864 4.666 16.845Q4.666 16.826 4.666 16.750L4.666 16.750Q5.388 15.344 5.768 14.508L5.768 14.508Q5.806 14.318 5.977 13.957Q6.148 13.596 6.224 13.444L6.224 13.444Q6.224 13.178 6.338 13.064L6.338 13.064L6.338 13.178Q6.376 13.292 6.452 13.501Q6.528 13.710 6.566 13.786L6.566 13.786Q6.870 14.394 7.402 15.610L7.402 15.610L7.896 16.750L7.972 16.864Q8.086 16.978 8.238 16.978L8.238 16.978Q8.542 16.978 9.131 17.054Q9.720 17.130 10.024 17.130L10.024 17.130Q10.100 17.358 10.176 17.415Q10.252 17.472 10.366 17.472ZM17.624 5.236L21.310 5.236L21.310 7.250L17.624 7.250L17.624 5.236ZM21.310 18.536L17.624 18.536L17.624 16.522L21.310 16.522L21.310 18.536ZM21.310 12.950L17.624 12.950L17.624 10.936L21.310 10.936L21.310 12.950ZM17.624 8.086L21.310 8.086L21.310 10.100L17.624 10.100L17.624 8.086ZM17.624 13.786L21.310 13.786L21.310 15.800L17.624 15.800L17.624 13.786Z\"/>',\n\t'seti:cu':\n\t\t'<path d=\"M0.024 11.875L0.024 11.875Q0.024 8.736 1.745 6.070L1.745 6.070Q3.335 3.533 5.937 2.200Q8.539 0.867 11.248 1.211L11.248 1.211Q14.215 1.555 16.451 3.834L16.451 3.834L13.484 7.188Q11.549 5.898 9.957 5.855L9.957 5.855Q8.539 5.769 7.506 6.715L7.506 6.715Q6.561 7.575 6.088 9.080L6.088 9.080Q5.658 10.499 5.701 12.155Q5.744 13.810 6.303 15.229Q6.861 16.648 7.851 17.379L7.851 17.379Q8.969 18.239 10.345 18.024L10.345 18.024Q11.936 17.809 13.742 16.304L13.742 16.304L16.709 19.701Q14.473 22.238 11.506 22.754L11.506 22.754Q8.754 23.184 6.088 21.851Q3.421 20.518 1.787 17.895L1.787 17.895Q0.024 15.143 0.024 11.875ZM22.084 9.209L23.976 14.971L22.901 14.971L22.471 13.638L20.622 13.638L20.192 14.971L19.117 14.971L21.009 9.209L22.084 9.209ZM20.880 12.692L22.213 12.692L21.524 10.542L20.880 12.692ZM10.044 9.209L10.087 9.209L11.033 9.209L11.033 12.348Q11.033 13.079 11.076 13.294L11.076 13.294Q11.119 13.681 11.355 13.896Q11.591 14.111 11.979 14.111L11.979 14.111Q12.280 14.111 12.495 13.961Q12.710 13.810 12.796 13.552Q12.882 13.294 12.882 12.434L12.882 12.434L12.882 9.209L13.828 9.209L13.828 12.262Q13.828 13.423 13.720 13.939Q13.613 14.455 13.161 14.778Q12.710 15.100 12 15.100Q11.291 15.100 10.861 14.821Q10.431 14.541 10.237 14.047Q10.044 13.552 10.044 12.305L10.044 12.305L10.044 9.209ZM14.817 14.971L14.817 9.209L16.580 9.209Q17.225 9.209 17.569 9.360Q17.913 9.510 18.192 9.854Q18.472 10.198 18.644 10.779Q18.816 11.359 18.816 12.133Q18.816 12.907 18.622 13.466Q18.429 14.025 18.192 14.348Q17.956 14.670 17.569 14.821Q17.182 14.971 16.623 14.971L16.623 14.971L14.817 14.971ZM16.193 10.198L15.806 10.198L15.806 13.982L16.494 13.982Q16.966 13.982 17.182 13.918Q17.397 13.853 17.526 13.681Q17.655 13.509 17.741 13.122Q17.827 12.735 17.827 12.090Q17.827 11.445 17.741 11.123Q17.655 10.800 17.440 10.564Q17.225 10.327 17.010 10.263Q16.795 10.198 16.193 10.198L16.193 10.198Z\"/>',\n\t'seti:cake':\n\t\t'<path d=\"M23.692 12.305L16.958 13.119Q17.328 14.340 18.179 17.522L18.179 17.522L18.956 20.408L23.692 19.816L23.692 12.305ZM10.372 21.444L8.892 13.933L0.308 14.858L0.308 22.739L10.372 21.444ZM16.033 6.311L16.033 6.311Q15.589 7.236 15.219 7.569L15.219 7.569L15.922 9.900L23.581 8.975L22.989 8.087Q22.249 7.051 21.435 6.163L21.435 6.163Q20.288 4.942 19.178 4.239L19.178 4.239Q17.772 3.351 16.403 2.944L16.403 2.944Q16.810 4.461 16.033 6.311ZM7.708 10.011L7.708 10.011Q5.747 9.826 4.489 8.383L4.489 8.383L0.308 11.639L7.967 10.714Q7.893 10.566 7.838 10.344Q7.782 10.122 7.708 10.011ZM13.998 10.825L13.147 7.680Q13.073 7.421 13.092 7.236Q13.110 7.051 13.258 6.866L13.258 6.866Q13.739 6.200 13.961 5.719L13.961 5.719Q14.775 3.906 13.850 2.500L13.850 2.500Q12.851 0.835 10.853 1.205L10.853 1.205Q10.594 1.205 10.520 1.427Q10.446 1.649 10.631 1.908L10.631 1.908L11.667 3.980L11.667 4.091Q11.704 4.165 11.686 4.202Q11.667 4.239 11.556 4.239L11.556 4.239Q10.150 5.164 9.558 5.497L9.558 5.497Q9.410 5.682 9.225 5.497L9.225 5.497Q8.781 5.016 8.078 4.091L8.078 4.091Q7.930 3.980 7.708 3.795L7.708 3.795L7.486 3.647L7.301 3.610Q7.116 3.610 7.042 3.758L7.042 3.758Q6.561 4.535 6.450 5.497L6.450 5.497Q6.302 6.607 6.783 7.347L6.783 7.347Q7.671 8.716 9.225 8.716L9.225 8.716Q9.447 8.716 9.595 8.790Q9.743 8.864 9.817 9.086L9.817 9.086L10.335 11.417Q12.518 20.556 12.703 21.703L12.703 21.703Q12.777 22.221 13.110 22.572Q13.443 22.924 13.850 22.850L13.850 22.850L14.220 22.776Q14.960 22.702 15.293 22.591L15.293 22.591Q15.885 22.406 16.292 22.036L16.292 22.036Q16.921 21.407 16.736 20.519L16.736 20.519Q16.477 20.038 13.998 10.825L13.998 10.825Z\"/>',\n\t'seti:cake_php':\n\t\t'<path d=\"M11.924 10.157L11.924 10.157L11.924 13.843L10.518 13.843Q8.542 13.729 7.554 13.615L7.554 13.615Q5.958 13.425 4.666 13.007L4.666 13.007Q3.716 12.779 2.082 12.057L2.082 12.057Q1.588 11.943 0.866 11.487L0.866 11.487Q0.448 11.107 0.258 10.727Q0.068 10.347 0.068 9.815L0.068 9.815L0.068 6.699Q0.068 6.015 0.638 5.407L0.638 5.407Q1.398 4.647 2.918 4.229L2.918 4.229Q5.350 3.355 8.010 3.165L8.010 3.165L8.314 3.127Q11.316 2.899 12.874 2.899L12.874 2.899Q17.092 2.899 20.816 4.115L20.816 4.115Q21.310 4.267 22.222 4.761L22.222 4.761L22.982 5.179Q23.932 5.749 23.932 6.965L23.932 6.965L23.932 9.929Q23.932 11.107 23.096 11.601L23.096 11.601Q22.716 11.867 21.918 12.247L21.918 12.247L21.538 12.437Q21.462 12.513 21.196 12.437L21.196 12.437L21.082 12.437Q15.382 10.993 12.418 10.271L12.418 10.271Q12.152 10.157 11.924 10.157ZM11.924 17.985L11.924 21.101L9.568 21.101Q5.996 20.873 3.602 20.151L3.602 20.151Q3.108 19.999 2.348 19.657L2.348 19.657L1.132 19.049Q0.068 18.365 0.068 17.035L0.068 17.035L0.068 14.299Q0.752 15.249 1.968 15.857L1.968 15.857Q2.652 16.199 4.210 16.693L4.210 16.693Q7.516 17.529 11.468 17.529L11.468 17.529Q11.734 17.529 11.829 17.624Q11.924 17.719 11.924 17.985L11.924 17.985ZM23.818 14.185L23.818 14.185L23.818 17.529Q23.780 18.175 23.096 18.707L23.096 18.707Q21.918 19.429 21.310 19.657L21.310 19.657Q21.234 19.733 20.968 19.695L20.968 19.695L20.702 19.657L12.152 17.529Q12.076 17.529 12.000 17.415L12.000 17.415L11.924 17.301L11.924 13.957Q12.342 14.071 13.121 14.261Q13.900 14.451 14.318 14.565L14.318 14.565Q15.420 14.869 17.624 15.401Q19.828 15.933 20.968 16.199L20.968 16.199L21.424 16.199L21.880 15.933Q22.602 15.515 22.944 15.249L22.944 15.249Q23.476 14.793 23.818 14.185Z\"/>',\n\t'seti:d':\n\t\t'<path d=\"M23.232 11.712L23.232 11.712Q23.232 17.760 19.464 20.880Q15.696 24 8.400 24L8.400 24L0.768 24L0.768 0L9.312 0Q16.032 0 19.632 3.168L19.632 3.168Q21.456 4.608 22.344 6.792Q23.232 8.976 23.232 11.712ZM17.232 11.856L17.232 11.856Q17.232 4.032 9.456 4.032L9.456 4.032L6.432 4.032L6.432 19.776L9.024 19.776Q13.152 19.776 15.192 17.832Q17.232 15.888 17.232 11.856Z\"/>',\n\t'seti:word':\n\t\t'<path d=\"M23.875 6.585L23.875 6.585L23.875 3.507Q23.875 3.127 23.609 2.842Q23.343 2.557 22.925 2.557L22.925 2.557L14.223 2.557L14.223 0.429L12.589 0.429Q12.475 0.429 12.285 0.486Q12.095 0.543 11.981 0.543L11.981 0.543Q11.563 0.581 10.727 0.771Q9.891 0.961 9.473 0.999L9.473 0.999Q7.839 1.265 7.117 1.493L7.117 1.493Q5.445 1.721 4.495 1.949L4.495 1.949L1.873 2.329Q0.695 2.557 0.239 2.557L0.239 2.557L0.125 2.557L0.125 21.443Q0.581 21.481 1.531 21.671Q2.481 21.861 2.956 21.918Q3.431 21.975 4.438 22.146Q5.445 22.317 5.920 22.374Q6.395 22.431 7.345 22.621Q8.295 22.811 8.789 22.849L8.789 22.849Q9.967 23.001 11.753 23.457L11.753 23.457Q12.095 23.571 12.931 23.571L12.931 23.571L13.995 23.571L13.995 21.557Q13.995 21.481 14.014 21.462Q14.033 21.443 14.109 21.443L14.109 21.443L22.773 21.443Q23.153 21.443 23.267 21.329L23.267 21.329Q23.609 21.329 23.609 20.949L23.609 20.949Q23.609 20.835 23.666 20.664Q23.723 20.493 23.723 20.379L23.723 20.379L23.723 6.585L23.837 6.699Q23.875 6.699 23.875 6.585ZM10.803 8.371L10.803 8.371L9.739 13.007Q9.587 13.463 9.397 14.413L9.397 14.413Q9.245 15.211 9.131 15.629L9.131 15.629Q9.131 15.705 9.055 15.705L9.055 15.705L9.017 15.743Q8.865 15.819 8.523 15.781L8.523 15.781L8.295 15.743L7.573 15.743Q7.497 15.743 7.478 15.724Q7.459 15.705 7.459 15.629L7.459 15.629Q7.421 15.249 7.231 14.489Q7.041 13.729 7.003 13.349L7.003 13.349Q6.889 12.893 6.699 11.943Q6.509 10.993 6.395 10.499L6.395 10.499Q6.395 10.613 6.338 10.860Q6.281 11.107 6.281 11.221L6.281 11.221L5.559 14.793Q5.559 14.907 5.502 15.211Q5.445 15.515 5.445 15.610Q5.445 15.705 5.426 15.724Q5.407 15.743 5.331 15.743L5.331 15.743Q5.103 15.743 4.628 15.686Q4.153 15.629 3.925 15.629L3.925 15.629Q3.811 15.629 3.792 15.610Q3.773 15.591 3.773 15.515L3.773 15.515Q3.089 11.715 2.595 9.701L2.595 9.701Q2.557 9.511 2.481 9.093Q2.405 8.675 2.367 8.485L2.367 8.485L2.367 8.371L3.773 8.371Q3.887 9.131 4.153 10.689L4.153 10.689Q4.495 12.513 4.609 13.463L4.609 13.463Q4.609 13.349 4.666 13.121Q4.723 12.893 4.723 12.779L4.723 12.779Q4.913 12.057 5.217 10.575Q5.521 9.093 5.673 8.371L5.673 8.371Q5.673 8.295 5.692 8.276Q5.711 8.257 5.825 8.257L5.825 8.257L7.003 8.257Q7.155 8.257 7.212 8.295Q7.269 8.333 7.345 8.485L7.345 8.485Q8.067 11.829 8.409 13.615L8.409 13.615L8.409 13.729Q8.485 13.235 8.656 12.285Q8.827 11.335 8.884 10.898Q8.941 10.461 9.131 9.568Q9.321 8.675 9.359 8.257L9.359 8.257L9.397 8.181Q9.397 8.143 9.473 8.143L9.473 8.143Q10.423 8.143 11.031 8.029L11.031 8.029L11.145 8.029Q10.993 8.029 10.898 8.143Q10.803 8.257 10.803 8.371ZM23.039 3.507L23.039 3.507Q23.039 3.545 23.039 3.545L23.039 3.545L23.039 3.507L23.039 20.721L14.109 20.721L14.109 18.593L21.139 18.593L21.139 17.529L14.375 17.529Q14.261 17.529 14.242 17.510Q14.223 17.491 14.223 17.415L14.223 17.415L14.223 16.313Q14.223 16.237 14.242 16.218Q14.261 16.199 14.375 16.199L14.375 16.199L21.253 16.199L21.253 15.135L14.223 15.135L14.223 13.843L21.139 13.843Q21.215 13.843 21.234 13.824Q21.253 13.805 21.253 13.729L21.253 13.729L21.253 12.893Q21.253 12.817 21.234 12.798Q21.215 12.779 21.139 12.779L21.139 12.779L14.375 12.779Q14.261 12.779 14.242 12.760Q14.223 12.741 14.223 12.665L14.223 12.665L14.223 11.449L21.253 11.449L21.253 10.385L14.489 10.385Q14.375 10.385 14.356 10.366Q14.337 10.347 14.337 10.271L14.337 10.271L14.337 9.207Q14.337 9.131 14.356 9.112Q14.375 9.093 14.489 9.093L14.489 9.093L21.253 9.093L21.253 8.029L14.223 8.029L14.223 6.585L21.139 6.585Q21.215 6.585 21.234 6.566Q21.253 6.547 21.253 6.471L21.253 6.471L21.253 5.635Q21.253 5.559 21.234 5.540Q21.215 5.521 21.139 5.521L21.139 5.521L14.109 5.521L14.109 3.735Q14.109 3.659 14.128 3.640Q14.147 3.621 14.223 3.621L14.223 3.621L22.925 3.621L23.001 3.431Q23.039 3.431 23.039 3.507Z\"/>',\n\t'seti:elixir':\n\t\t'<path d=\"M13.540 5.776L13.540 5.776Q13.995 6.721 14.800 7.736L14.800 7.736Q15.290 8.331 16.375 9.451L16.375 9.451Q17.530 10.711 18.055 11.341L18.055 11.341Q18.895 12.461 19.280 13.511L19.280 13.511Q19.735 14.806 19.595 16.276L19.595 16.276Q19.385 19.006 18.055 20.826L18.055 20.826Q17.005 22.331 15.325 23.101L15.325 23.101Q13.995 23.731 12.542 23.783Q11.090 23.836 9.725 23.416L9.725 23.416Q7.905 22.891 6.680 21.631L6.680 21.631Q5.140 20.056 4.580 17.501L4.580 17.501Q4.090 15.261 4.755 12.321L4.755 12.321Q5.385 9.731 6.785 7.001L6.785 7.001Q8.115 4.481 9.725 2.608Q11.335 0.736 12.630 0.211L12.630 0.211Q12.525 1.436 12.770 2.976L12.770 2.976Q13.015 4.656 13.540 5.776ZM10.495 21.736L10.495 21.736Q11.930 22.051 12.490 22.051L12.490 22.051Q13.295 22.086 13.400 21.666L13.400 21.666Q13.645 20.581 7.485 19.916L7.485 19.916Q8.045 20.476 8.972 21.053Q9.900 21.631 10.495 21.736Z\"/>',\n\t'seti:elixir_script':\n\t\t'<path d=\"M15.570 8.681L15.570 8.646Q15.850 8.926 16.375 9.451L16.375 9.451Q17.530 10.711 18.055 11.341L18.055 11.341Q18.895 12.461 19.280 13.511L19.280 13.511Q19.735 14.806 19.595 16.276L19.595 16.276Q19.385 19.006 18.055 20.826L18.055 20.826Q17.005 22.331 15.325 23.101L15.325 23.101Q13.995 23.731 12.542 23.783Q11.090 23.836 9.725 23.416L9.725 23.416Q7.905 22.891 6.680 21.631L6.680 21.631Q5.140 20.056 4.580 17.501L4.580 17.501Q4.090 15.261 4.755 12.321L4.755 12.321Q5.385 9.731 6.785 7.001L6.785 7.001Q8.115 4.481 9.725 2.608Q11.335 0.736 12.630 0.211L12.630 0.211Q12.525 1.366 12.735 2.871L12.735 2.871Q12.980 4.446 13.470 5.601L13.470 5.601Q13.085 5.566 12.735 5.566L12.735 5.566Q10.530 5.566 9.147 6.756Q7.765 7.946 7.765 9.836L7.765 9.836Q7.765 10.921 8.307 11.551Q8.850 12.181 10.495 12.881L10.495 12.881L11.510 13.301Q12.070 13.546 12.385 13.931Q12.700 14.316 12.700 14.841L12.700 14.841Q12.700 15.716 12.035 16.206Q11.370 16.696 10.250 16.696L10.250 16.696Q9.340 16.696 8.412 16.328Q7.485 15.961 6.680 15.296L6.680 15.296L6.190 17.886Q7.170 18.341 8.220 18.586L8.220 18.586Q9.165 18.796 10.145 18.796L10.145 18.796Q12.490 18.796 13.872 17.623Q15.255 16.451 15.255 14.491L15.255 14.491Q15.255 13.336 14.677 12.566Q14.100 11.796 12.840 11.236L12.840 11.236L11.790 10.781Q10.215 10.046 10.215 9.311Q10.215 8.576 10.845 8.103Q11.475 7.631 12.455 7.631Q13.435 7.631 14.222 7.893Q15.010 8.156 15.570 8.681L15.570 8.681ZM10.495 21.736L10.495 21.736Q11.930 22.051 12.490 22.051L12.490 22.051Q13.295 22.086 13.400 21.666L13.400 21.666Q13.645 20.581 7.485 19.916L7.485 19.916Q8.045 20.476 8.972 21.053Q9.900 21.631 10.495 21.736Z\"/>',\n\t'seti:hex':\n\t\t'<path d=\"M23.856 12L17.928 22.260L6.072 22.260L0.144 12L6.072 1.740L17.928 1.740L23.856 12ZM8.998 17.206L15.002 17.206L18.004 12L15.002 6.794L8.998 6.794L5.996 12L8.998 17.206Z\"/>',\n\t'seti:elm':\n\t\t'<path d=\"M23.193 23.856L12.000 12.663L0.807 23.856L23.193 23.856ZM23.856 13.248L18.864 18.240L23.856 23.193L23.856 13.248ZM0.144 23.193L11.337 12L0.144 0.807L0.144 23.193ZM13.326 0.144L23.856 10.674L23.856 0.144L13.326 0.144ZM12.663 12L18.240 6.384L23.817 11.961L18.240 17.577L12.663 12ZM12.000 0.144L0.807 0.144L5.955 5.331L17.187 5.331L12.000 0.144ZM6.891 6.228L12.000 11.337L17.109 6.228L6.891 6.228Z\"/>',\n\t'seti:favicon':\n\t\t'<path d=\"M8.295 8.200L11.981 0.714L15.667 8.200L23.875 9.378L17.909 15.078L19.353 23.286L11.981 19.486L4.609 23.286L6.053 15.078L0.125 9.378L8.295 8.200Z\"/>',\n\t'seti:f-sharp':\n\t\t'<path d=\"M11.496 23.130L0.198 11.874L11.328 0.702L11.328 6.330L5.826 11.874L11.496 17.502L11.496 23.130ZM11.496 15.780L7.422 11.874L11.496 7.800L11.496 15.780ZM12.252 23.298L23.802 11.874L12.252 0.702L12.252 6.330L17.922 12L12.252 17.628L12.252 23.298Z\"/>',\n\t'seti:git':\n\t\t'<path d=\"M0.355 12.231L0.355 12.231L0.355 11.853Q0.397 11.517 0.565 11.265L0.565 11.265Q0.691 11.097 0.943 10.803L0.943 10.803L1.279 10.425L7.831 3.831L7.873 3.873Q7.957 3.873 7.957 3.957L7.957 3.957L10.309 6.351Q10.519 6.519 10.309 6.729L10.309 6.729Q10.225 7.317 10.435 7.863Q10.645 8.409 11.107 8.703L11.107 8.703Q11.275 8.787 11.317 8.892Q11.359 8.997 11.359 9.207L11.359 9.207L11.359 15.003Q11.359 15.255 11.107 15.507L11.107 15.507Q10.603 15.843 10.372 16.389Q10.141 16.935 10.330 17.502Q10.519 18.069 10.981 18.447Q11.443 18.825 12.031 18.825Q12.619 18.825 13.081 18.489Q13.543 18.153 13.732 17.586Q13.921 17.019 13.753 16.473Q13.585 15.927 13.081 15.507L13.081 15.507Q12.913 15.423 12.871 15.318Q12.829 15.213 12.829 15.003L12.829 15.003L12.829 9.081L12.955 9.081L15.055 11.181Q15.139 11.265 15.139 11.391L15.139 11.391L15.181 11.475L15.181 12.231Q15.265 13.029 15.874 13.470Q16.483 13.911 17.281 13.806Q18.079 13.701 18.562 13.029Q19.045 12.357 18.835 11.601L18.835 11.601Q18.751 10.929 18.100 10.488Q17.449 10.047 16.735 10.131L16.735 10.131Q16.483 10.131 16.231 10.005L16.231 10.005L14.005 7.779Q13.879 7.653 13.879 7.401L13.879 7.401Q14.005 6.813 13.711 6.267Q13.417 5.721 12.871 5.448Q12.325 5.175 11.779 5.301L11.779 5.301Q11.359 5.301 11.359 5.175L11.359 5.175Q9.805 3.579 9.007 2.907L9.007 2.907Q8.797 2.739 9.007 2.529L9.007 2.529Q9.469 2.193 10.183 1.353L10.183 1.353L10.729 0.807Q11.905-0.369 13.081 0.807L13.081 0.807L23.035 10.803Q24.253 11.979 23.035 13.155L23.035 13.155L13.291 22.941Q12.913 23.319 12.745 23.445L12.745 23.445Q12.409 23.739 12.031 23.781L12.031 23.781L11.779 23.781L11.569 23.697Q11.065 23.487 10.855 23.277L10.855 23.277L10.057 22.479Q8.881 21.387 8.335 20.757L8.335 20.757L1.153 13.575Q1.069 13.365 0.775 13.029L0.775 13.029Q0.397 12.525 0.355 12.231Z\"/>',\n\t'seti:go':\n\t\t'<path d=\"M6.119 10.273L1.933 10.247Q1.855 10.247 1.881 10.195L1.881 10.195L2.141 9.883Q2.167 9.831 2.271 9.831L2.271 9.831L6.379 9.831Q6.457 9.831 6.431 9.883L6.431 9.883L6.223 10.195Q6.171 10.273 6.119 10.273L6.119 10.273ZM5.651 11.313L0.191 11.313Q0.113 11.313 0.139 11.261L0.139 11.261L0.399 10.949Q0.425 10.897 0.529 10.897L0.529 10.897L5.781 10.897Q5.859 10.897 5.833 10.949L5.833 10.949L5.755 11.235Q5.729 11.313 5.651 11.313L5.651 11.313ZM5.495 12.379L2.973 12.379Q2.895 12.379 2.947 12.301L2.947 12.301L3.103 12.015Q3.155 11.963 3.233 11.963L3.233 11.963L5.521 11.963Q5.599 11.963 5.599 12.041L5.599 12.041L5.573 12.301Q5.573 12.379 5.495 12.379L5.495 12.379ZM14.985 10.039L14.985 10.039Q14.569 10.143 13.893 10.325L13.893 10.325L13.009 10.559Q12.905 10.585 12.853 10.572Q12.801 10.559 12.697 10.429L12.697 10.429Q12.411 10.117 12.151 9.987L12.151 9.987Q11.111 9.493 10.071 10.169L10.071 10.169Q8.875 10.949 8.875 12.379L8.875 12.379Q8.901 13.055 9.330 13.575Q9.759 14.095 10.435 14.173L10.435 14.173Q11.605 14.329 12.411 13.419L12.411 13.419L12.723 13.003L10.487 13.003Q10.305 13.003 10.253 12.899Q10.201 12.795 10.279 12.639L10.279 12.639Q10.591 11.911 10.851 11.391L10.851 11.391Q10.955 11.209 11.137 11.209L11.137 11.209L15.349 11.209L15.349 11.521Q15.323 11.937 15.297 12.145L15.297 12.145Q15.089 13.419 14.335 14.407L14.335 14.407Q13.061 16.097 11.033 16.357L11.033 16.357Q9.265 16.591 7.939 15.603L7.939 15.603Q6.665 14.641 6.457 13.029L6.457 13.029Q6.249 11.209 7.445 9.649L7.445 9.649Q8.693 8.011 10.695 7.647L10.695 7.647Q12.437 7.335 13.737 8.219L13.737 8.219Q14.647 8.817 15.089 9.857L15.089 9.857Q15.193 9.987 14.985 10.039ZM19.119 16.409L18.807 16.435Q17.117 16.409 15.973 15.421L15.973 15.421Q14.933 14.511 14.725 13.185L14.725 13.185Q14.439 11.313 15.661 9.701L15.661 9.701Q16.285 8.869 17.065 8.414Q17.845 7.959 18.911 7.777L18.911 7.777Q20.835 7.439 22.226 8.375Q23.617 9.311 23.825 10.923L23.825 10.923Q24.111 13.211 22.499 14.849L22.499 14.849Q21.381 15.993 19.717 16.331L19.717 16.331Q19.509 16.357 19.119 16.409L19.119 16.409ZM21.537 11.755L21.537 11.755Q21.537 11.729 21.537 11.651L21.537 11.651Q21.537 11.469 21.511 11.391L21.511 11.391Q21.355 10.533 20.666 10.091Q19.977 9.649 19.145 9.857L19.145 9.857Q17.507 10.221 17.143 11.859L17.143 11.859Q16.987 12.535 17.286 13.146Q17.585 13.757 18.209 14.043L18.209 14.043Q19.145 14.459 20.081 13.965L20.081 13.965Q21.459 13.263 21.537 11.755Z\"/>',\n\t'seti:godot':\n\t\t'<path d=\"M9.606 0.870L9.606 0.870Q7.800 1.248 6.456 1.920L6.456 1.920Q6.498 3.390 6.624 4.524L6.624 4.524L6.288 4.734Q5.742 5.070 5.490 5.280L5.490 5.280L5.280 5.406Q4.692 5.910 4.398 6.162L4.398 6.162Q3.390 5.490 2.298 4.944L2.298 4.944Q0.996 6.372 0.198 7.716L0.198 7.716Q1.038 9.018 1.626 9.774L1.626 9.774L1.626 16.032L5.490 16.410Q5.658 16.410 5.763 16.515Q5.868 16.620 5.868 16.788L5.868 16.788L5.994 18.468L9.354 18.720L9.564 17.166Q9.606 16.998 9.711 16.893Q9.816 16.788 9.984 16.788L9.984 16.788L14.016 16.788Q14.184 16.788 14.289 16.893Q14.394 16.998 14.436 17.166L14.436 17.166L14.646 18.720L18.006 18.468L18.132 16.788Q18.132 16.620 18.237 16.515Q18.342 16.410 18.510 16.410L18.510 16.410L22.374 16.032L22.374 9.774Q23.088 8.850 23.802 7.716L23.802 7.716Q23.004 6.372 21.702 4.944L21.702 4.944Q20.610 5.490 19.602 6.162L19.602 6.162Q19.308 5.868 18.678 5.406L18.678 5.406L18.510 5.280Q18.258 5.070 17.712 4.734L17.712 4.734L17.334 4.524Q17.502 3.222 17.544 1.920L17.544 1.920Q16.200 1.248 14.394 0.870L14.394 0.870Q13.764 1.962 13.218 3.138L13.218 3.138Q12.630 3.054 12 3.054L12 3.054L12 3.054Q11.370 3.054 10.782 3.138L10.782 3.138Q10.236 1.962 9.606 0.870ZM6.582 10.026L6.582 10.026Q7.506 10.026 8.178 10.698Q8.850 11.370 8.850 12.315Q8.850 13.260 8.178 13.953Q7.506 14.646 6.561 14.646Q5.616 14.646 4.923 13.953Q4.230 13.260 4.230 12.315Q4.230 11.370 4.923 10.698Q5.616 10.026 6.582 10.026ZM17.418 10.026L17.418 10.026Q18.384 10.026 19.056 10.698Q19.728 11.370 19.728 12.315Q19.728 13.260 19.056 13.953Q18.384 14.646 17.439 14.646Q16.494 14.646 15.801 13.953Q15.108 13.260 15.108 12.315Q15.108 11.370 15.801 10.698Q16.494 10.026 17.418 10.026ZM12 11.370L12 11.370Q12.294 11.370 12.525 11.559Q12.756 11.748 12.756 12.042L12.756 12.042L12.756 14.184Q12.756 14.436 12.525 14.646Q12.294 14.856 12 14.856Q11.706 14.856 11.475 14.646Q11.244 14.436 11.244 14.184L11.244 14.184L11.244 12.042Q11.244 11.748 11.475 11.559Q11.706 11.370 12 11.370ZM22.374 16.872L18.930 17.208L18.804 18.888Q18.804 19.056 18.699 19.161Q18.594 19.266 18.426 19.266L18.426 19.266L14.310 19.560Q14.142 19.560 14.037 19.476Q13.932 19.392 13.890 19.224L13.890 19.224L13.680 17.628L10.320 17.628L10.110 19.224Q10.068 19.392 9.942 19.497Q9.816 19.602 9.648 19.560L9.648 19.560L5.574 19.266Q5.406 19.266 5.301 19.161Q5.196 19.056 5.196 18.888L5.196 18.888L5.070 17.208L1.626 16.872L1.626 17.712Q1.626 20.358 4.650 21.786L4.650 21.786Q7.464 23.130 12 23.130L12 23.130L12 23.130Q16.536 23.130 19.350 21.786L19.350 21.786Q22.374 20.358 22.374 17.712L22.374 17.712L22.374 16.872ZM8.304 12.462L8.304 12.462Q8.346 12.882 8.136 13.239Q7.926 13.596 7.569 13.827Q7.212 14.058 6.792 14.058Q6.372 14.058 6.015 13.827Q5.658 13.596 5.448 13.239Q5.238 12.882 5.238 12.462L5.238 12.462Q5.280 11.832 5.721 11.391Q6.162 10.950 6.792 10.950Q7.422 10.950 7.863 11.391Q8.304 11.832 8.304 12.462ZM15.696 12.462L15.696 12.462Q15.696 13.092 16.137 13.533Q16.578 13.974 17.208 13.974Q17.838 13.974 18.300 13.533Q18.762 13.092 18.762 12.462Q18.762 11.832 18.300 11.370Q17.838 10.908 17.208 10.908Q16.578 10.908 16.137 11.370Q15.696 11.832 15.696 12.462Z\"/>',\n\t'seti:gradle':\n\t\t'<path d=\"M16.344 11.229L16.344 11.229L15.197 10.640L15.197 10.640Q15.197 10.361 15.414 10.144Q15.631 9.927 15.894 9.927Q16.158 9.927 16.344 10.067Q16.530 10.206 16.607 10.423Q16.685 10.640 16.607 10.857Q16.530 11.074 16.344 11.229ZM22.389 4.502L22.389 4.502Q21.707 3.789 20.777 3.541Q19.847 3.293 18.917 3.510Q17.987 3.727 17.243 4.409L17.243 4.409Q17.150 4.502 17.150 4.657Q17.150 4.812 17.243 4.905L17.243 4.905L17.708 5.370Q17.801 5.463 17.940 5.479Q18.080 5.494 18.173 5.401L18.173 5.401Q18.731 4.998 19.444 4.998L19.444 4.998Q20.312 4.998 20.947 5.618Q21.583 6.238 21.583 7.122Q21.583 8.005 20.978 8.594Q20.374 9.183 19.661 9.276L19.661 9.276Q19.041 9.307 18.204 8.997L18.204 8.997Q17.677 8.780 16.499 8.191L16.499 8.191Q14.639 7.261 13.554 6.889L13.554 6.889Q11.694 6.238 9.834 6.331L9.834 6.331Q7.602 6.424 5.184 7.571L5.184 7.571Q4.719 7.788 4.548 8.300Q4.378 8.811 4.657 9.245L4.657 9.245L6.176 11.911Q6.424 12.345 6.904 12.469Q7.385 12.593 7.819 12.345L7.819 12.345L7.850 12.345L7.819 12.345L8.501 11.973L9.214 11.508Q10.020 10.950 10.640 10.392L10.640 10.392Q10.733 10.299 10.872 10.284Q11.012 10.268 11.136 10.361L11.136 10.361L11.136 10.361Q11.260 10.454 11.260 10.625Q11.260 10.795 11.136 10.888L11.136 10.888Q10.082 11.849 8.873 12.593L8.873 12.593L8.191 12.996Q7.478 13.368 6.718 13.167Q5.959 12.965 5.556 12.283L5.556 12.283L4.130 9.772Q1.929 11.322 0.968 13.988L0.968 13.988Q-0.086 16.840 0.565 20.312L0.565 20.312Q0.596 20.436 0.689 20.514Q0.782 20.591 0.906 20.591L0.906 20.591L2.549 20.591Q2.704 20.591 2.797 20.498Q2.890 20.405 2.921 20.281L2.921 20.281Q3.014 19.382 3.696 18.778Q4.378 18.173 5.292 18.173Q6.207 18.173 6.889 18.778Q7.571 19.382 7.695 20.281L7.695 20.281Q7.695 20.405 7.803 20.498Q7.912 20.591 8.036 20.591L8.036 20.591L9.648 20.591Q9.772 20.591 9.865 20.498Q9.958 20.405 9.989 20.281L9.989 20.281Q10.113 19.382 10.795 18.778Q11.477 18.173 12.376 18.173Q13.275 18.173 13.957 18.778Q14.639 19.382 14.763 20.281L14.763 20.281Q14.794 20.405 14.887 20.498Q14.980 20.591 15.104 20.591L15.104 20.591L16.685 20.591Q16.840 20.591 16.948 20.483Q17.057 20.374 17.057 20.250L17.057 20.250Q17.088 18.359 17.584 16.840L17.584 16.840Q18.204 15.042 19.413 14.143L19.413 14.143Q22.110 12.097 23.133 9.958L23.133 9.958Q23.939 8.253 23.567 6.610L23.567 6.610Q23.257 5.370 22.389 4.502Z\"/>',\n\t'seti:grails':\n\t\t'<path d=\"M11.488 18.832L11.488 18.832L11.200 18.832Q11.104 18.736 10.784 18.736L10.784 18.736L9.792 18.448Q9.568 18.288 9.504 18.128Q9.440 17.968 9.600 17.744L9.600 17.744Q9.792 17.456 10.016 16.848L10.016 16.848Q10.432 15.856 10.624 15.184L10.624 15.184Q10.848 14.224 10.688 13.360L10.688 13.360Q10.528 11.728 9.088 11.168Q7.648 10.608 6.912 9.840L6.912 9.840Q5.728 8.752 5.184 7.760L5.184 7.760Q4.544 6.544 4.608 5.136L4.608 5.136L19.392 5.136Q19.392 6.736 18.688 7.856L18.688 7.856Q17.472 10.032 14.912 11.152L14.912 11.152Q13.344 11.920 13.216 13.552L13.216 13.552Q13.120 14.480 13.472 15.568L13.472 15.568Q13.728 16.304 14.304 17.456L14.304 17.456Q14.528 17.872 14.448 18.048Q14.368 18.224 13.888 18.448L13.888 18.448L13.408 18.640Q13.248 18.704 12.896 18.752Q12.544 18.800 12.384 18.864L12.384 18.864L12 18.864Q11.840 18.672 11.488 18.832ZM24 9.360L24 9.936Q23.872 10.064 23.776 10.416L23.776 10.416Q23.744 10.576 23.712 10.640L23.712 10.640Q22.912 12.400 21.312 12.848L21.312 12.848Q20.288 13.104 20.288 14.352L20.288 14.352Q20.224 14.896 20.416 15.472L20.416 15.472Q20.512 15.856 20.800 16.560L20.800 16.560Q21.024 17.008 20.928 17.184Q20.832 17.360 20.288 17.456L20.288 17.456Q19.200 17.616 18.304 17.360L18.304 17.360Q17.824 17.200 18.112 16.752L18.112 16.752Q18.432 16.144 18.592 15.664L18.592 15.664Q18.784 15.056 18.688 14.448L18.688 14.448Q18.624 13.680 18.416 13.408Q18.208 13.136 17.696 13.040L17.696 13.040Q17.344 13.040 17.216 12.752L17.216 12.752Q17.248 12.752 17.296 12.704Q17.344 12.656 17.408 12.656L17.408 12.656Q19.616 11.824 20.896 9.552L20.896 9.552Q20.960 9.424 21.072 9.392Q21.184 9.360 21.408 9.360L21.408 9.360L24 9.360ZM0 10.160L0 9.232L2.496 9.232Q2.688 9.232 2.752 9.264Q2.816 9.296 2.912 9.456L2.912 9.456Q4.160 11.536 6.688 12.656L6.688 12.656Q6.656 12.688 6.560 12.736Q6.464 12.784 6.400 12.848L6.400 12.848Q5.792 13.072 5.552 13.472Q5.312 13.872 5.312 14.544L5.312 14.544Q5.312 15.344 5.792 16.336L5.792 16.336Q6.016 16.752 6.096 16.880Q6.176 17.008 6.160 17.120Q6.144 17.232 6.016 17.232L6.016 17.232Q4.640 17.840 3.296 17.232L3.296 17.232Q3.104 17.232 3.056 17.120Q3.008 17.008 3.104 16.752L3.104 16.752Q3.488 15.952 3.648 15.536L3.648 15.536Q3.872 14.864 3.792 14.256Q3.712 13.648 3.520 13.424Q3.328 13.200 2.816 13.040L2.816 13.040Q1.056 12.528 0.192 10.448L0.192 10.448Q0.160 10.416 0.144 10.288Q0.128 10.160 0 10.160L0 10.160Z\"/>',\n\t'seti:graphql':\n\t\t'<path d=\"M21.337 14.958L21.337 14.958Q21.269 14.958 21.201 14.890L21.201 14.890Q20.963 14.754 20.793 14.754L20.793 14.754L20.793 9.246Q21.133 9.246 21.337 9.042L21.337 9.042Q22.085 8.600 22.323 7.767Q22.561 6.934 22.119 6.169Q21.677 5.404 20.827 5.183Q19.977 4.962 19.229 5.404L19.229 5.404Q18.923 5.540 18.787 5.846L18.787 5.846L13.993 3.092Q13.993 2.888 14.061 2.718Q14.129 2.548 14.095 2.446L14.095 2.446Q14.129 1.562 13.500 0.933Q12.871 0.304 11.987 0.304Q11.103 0.304 10.491 0.933Q9.879 1.562 9.879 2.446L9.879 2.446Q9.879 2.854 9.981 3.092L9.981 3.092L5.187 5.846Q5.119 5.676 4.881 5.506L4.881 5.506L4.779 5.404Q3.997 4.962 3.147 5.183Q2.297 5.404 1.889 6.135Q1.481 6.866 1.702 7.699Q1.923 8.532 2.637 9.042L2.637 9.042L2.773 9.110Q3.011 9.246 3.181 9.246L3.181 9.246L3.181 14.754Q2.841 14.754 2.637 14.992L2.637 14.992Q1.889 15.400 1.651 16.233Q1.413 17.066 1.855 17.831Q2.297 18.596 3.147 18.817Q3.997 19.038 4.779 18.596L4.779 18.596Q5.051 18.460 5.187 18.154L5.187 18.154L9.981 20.942Q9.981 21.112 9.930 21.282Q9.879 21.452 9.845 21.554L9.845 21.554Q9.879 22.438 10.491 23.067Q11.103 23.696 11.987 23.696Q12.871 23.696 13.500 23.067Q14.129 22.438 14.129 21.554L14.129 21.554Q14.129 21.146 13.993 20.942L13.993 20.942L18.787 18.154Q18.889 18.392 19.331 18.596L19.331 18.596Q20.045 19.004 20.878 18.783Q21.711 18.562 22.187 17.848L22.187 17.848Q22.595 17.134 22.323 16.301Q22.051 15.468 21.337 14.958ZM18.243 16.352L5.731 16.352Q5.731 16.046 5.493 15.808L5.493 15.808Q5.391 15.604 5.187 15.400L5.187 15.400L11.443 4.452Q11.579 4.452 11.817 4.520L11.817 4.520L11.987 4.554Q12.429 4.554 12.531 4.452L12.531 4.452L18.787 15.400Q18.481 15.706 18.481 15.808L18.481 15.808L18.379 15.978Q18.243 16.216 18.243 16.352L18.243 16.352ZM13.585 3.806L18.243 6.594Q18.073 7.512 18.481 8.158L18.481 8.158Q18.957 8.974 19.739 9.144L19.739 9.144L19.739 14.652L19.637 14.652L13.483 3.908L13.585 3.806ZM5.629 6.696L10.491 3.806Q10.491 3.840 10.491 3.874L10.491 3.874L10.491 3.806L4.235 14.754L4.133 14.754L4.133 9.246Q4.915 9.076 5.391 8.294L5.391 8.294Q5.799 7.614 5.629 6.696L5.629 6.696ZM18.243 17.406L13.483 20.194Q12.837 19.548 11.987 19.548L11.987 19.548Q10.967 19.548 10.491 20.194L10.491 20.194L5.731 17.406L5.731 17.304L18.243 17.304L18.243 17.406Z\"/>',\n\t'seti:hacklang':\n\t\t'<path d=\"M11.252 0.202L11.252 0.202L11.252 6.900Q11.252 7.104 11.150 7.206L11.150 7.206L4.350 14.006L4.316 14.074Q4.282 14.142 4.248 14.142L4.248 14.142L4.248 7.342Q4.248 7.240 4.248 7.223Q4.248 7.206 4.350 7.206L4.350 7.206Q5.506 6.050 7.784 3.721Q10.062 1.392 11.252 0.202ZM19.650 9.994L19.752 9.994L19.752 16.794Q19.752 16.862 19.735 16.879Q19.718 16.896 19.650 16.896L19.650 16.896L12.748 23.798L12.612 23.798L12.612 16.998Q12.612 16.930 12.629 16.913Q12.646 16.896 12.748 16.896L12.748 16.896Q13.904 15.774 16.216 13.462Q18.528 11.150 19.752 9.994L19.752 9.994L19.650 9.994ZM12.748 8.396L19.412 8.396L12.612 15.196L12.612 8.498Q12.612 8.430 12.629 8.413Q12.646 8.396 12.748 8.396L12.748 8.396ZM11.354 8.702L11.354 8.702L11.354 15.400Q11.354 15.468 11.337 15.485Q11.320 15.502 11.252 15.502L11.252 15.502L4.554 15.502Q7.886 12.034 11.354 8.702ZM4.248 23.594L4.248 23.594Q4.248 23.560 4.248 23.560L4.248 23.560L4.248 23.594L4.248 16.896Q4.248 16.828 4.248 16.811Q4.248 16.794 4.350 16.794L4.350 16.794L11.014 16.794L8.804 19.038Q5.778 22.098 4.248 23.594Z\"/>',\n\t'seti:haml':\n\t\t'<path d=\"M17.267 10.696L17.267 10.696Q16.679 10.444 16.595 9.772L16.595 9.772Q16.511 9.268 16.679 8.638L16.679 8.638L16.889 8.050L18.317 6.748L18.569 6.790Q18.863 6.790 19.094 6.643Q19.325 6.496 19.409 6.286Q19.493 6.076 19.493 5.866L19.493 5.866L19.493 5.698L20.585 4.480Q20.627 4.396 20.543 4.270Q20.459 4.144 20.186 3.871Q19.913 3.598 19.367 3.199Q18.821 2.800 18.359 2.632L18.359 2.632Q18.023 2.464 17.771 2.485Q17.519 2.506 17.393 2.590L17.393 2.590L16.091 3.598L15.839 3.598Q15.587 3.640 15.419 3.724Q15.251 3.808 15.335 4.102L15.335 4.102Q15.377 4.270 15.419 4.396L15.419 4.396L10.043 8.848L7.313 2.548L7.397 2.380Q7.523 2.128 7.481 1.897Q7.439 1.666 7.229 1.540L7.229 1.540L7.019 1.498Q6.599 0.742 6.305 0.448L6.305 0.448Q6.179 0.322 6.095 0.322L6.095 0.322L6.095 0.322Q4.709-0.014 3.701 0.574L3.701 0.574Q3.197 0.868 2.945 1.246L2.945 1.246L2.945 1.246Q2.861 1.330 2.987 1.750L2.987 1.750L3.113 2.170L3.029 2.212Q2.945 2.296 2.924 2.464Q2.903 2.632 2.945 2.926L2.945 2.926Q3.113 3.514 3.449 3.556L3.449 3.556Q3.617 3.556 3.743 3.472L3.743 3.472L7.313 11.326Q7.313 11.998 4.541 22.372L4.541 22.372L4.541 22.372Q5.843 23.758 7.943 23.800L7.943 23.800Q8.993 23.800 9.791 23.548L9.791 23.548L9.791 23.548Q9.791 22.246 11.345 14.644L11.345 14.644L11.345 14.644Q12.479 15.652 13.865 16.114L13.865 16.114Q14.789 16.450 15.545 16.450L15.545 16.450Q15.545 16.996 15.671 17.374L15.671 17.374L16.091 17.500Q16.595 17.584 17.099 17.584L17.099 17.584Q17.813 17.584 18.359 17.332L18.359 17.332Q19.031 16.996 19.493 16.324L19.493 16.324Q19.619 16.198 19.619 15.946L19.619 15.946L19.997 15.736Q20.417 15.484 20.711 15.148L20.711 15.148Q21.089 14.686 21.089 14.098L21.089 14.098Q21.089 12.880 19.661 11.914L19.661 11.914Q18.821 11.326 17.267 10.696ZM19.913 15.400L19.913 15.400Q19.997 15.022 19.955 14.644L19.955 14.644Q19.871 14.350 19.745 14.140L19.745 14.140L19.619 13.972L19.535 14.182Q19.409 14.434 19.115 14.686L19.115 14.686Q18.695 14.980 18.044 15.148Q17.393 15.316 16.931 15.274L16.931 15.274Q16.595 15.190 16.385 15.022L16.385 15.022L16.217 14.896L16.049 15.148Q15.839 15.484 15.713 15.820L15.713 15.820Q12.815 15.316 11.513 13.384L11.513 13.384L11.513 13.300Q11.345 12.922 11.639 11.998L11.639 11.998L15.545 9.100Q15.377 10.276 16.385 11.032L16.385 11.032Q16.889 11.452 17.393 11.620L17.393 11.620L18.191 11.914Q19.073 12.376 19.619 12.838L19.619 12.838Q20.417 13.510 20.417 14.224L20.417 14.224Q20.501 14.686 20.291 15.022L20.291 15.022Q20.165 15.274 19.913 15.400Z\"/>',\n\t'seti:mustache':\n\t\t'<path d=\"M2.990 11.701L2.990 11.701Q2.990 11.259 2.888 11.157L2.888 11.157Q2.718 10.987 2.463 11.004Q2.208 11.021 2.140 11.259L2.140 11.259Q1.902 11.667 2.106 12.024Q2.310 12.381 2.786 12.551L2.786 12.551Q3.126 12.619 3.381 12.585Q3.636 12.551 3.840 12.313L3.840 12.313Q4.928 11.463 5.438 10.953L5.438 10.953L5.744 10.681Q6.356 10.137 6.696 9.933L6.696 9.933Q7.240 9.525 7.784 9.355L7.784 9.355Q9.994 8.607 11.388 9.865L11.388 9.865L12.238 10.715Q12.340 10.511 12.544 10.307L12.544 10.307Q13.564 9.151 15.128 9.151L15.128 9.151Q16.862 9.151 18.188 10.307L18.188 10.307Q18.494 10.579 19.089 11.106Q19.684 11.633 19.990 11.905L19.990 11.905Q20.500 12.381 21.044 12.551L21.044 12.551Q21.452 12.619 21.809 12.432Q22.166 12.245 22.319 11.871Q22.472 11.497 22.132 11.157L22.132 11.157Q21.826 10.885 21.486 11.055L21.486 11.055Q21.316 11.123 21.248 11.276Q21.180 11.429 21.282 11.565L21.282 11.565L21.282 11.701Q20.840 11.395 20.840 10.851Q20.840 10.307 21.486 10.103L21.486 10.103Q22.200 9.865 22.829 10.222Q23.458 10.579 23.594 11.361L23.594 11.361Q23.798 12.551 23.390 13.401L23.390 13.401Q23.186 13.911 22.710 14.217L22.710 14.217Q22.336 14.489 21.690 14.659L21.690 14.659Q20.670 14.965 19.548 14.795L19.548 14.795Q18.630 14.693 17.440 14.251L17.440 14.251L14.584 12.959Q12.646 12.075 10.844 12.551L10.844 12.551Q9.790 12.857 8.532 13.401L8.532 13.401L7.852 13.707Q6.492 14.319 5.744 14.557L5.744 14.557Q3.874 15.237 2.242 14.659L2.242 14.659Q0.202 13.911 0.338 11.803L0.338 11.803Q0.440 11.157 0.644 10.783L0.644 10.783Q0.882 10.341 1.392 10.205L1.392 10.205Q1.732 10.035 2.106 10.035Q2.480 10.035 2.786 10.205L2.786 10.205Q3.330 10.443 3.364 10.902Q3.398 11.361 2.990 11.701Z\"/>',\n\t'seti:haskell':\n\t\t'<path d=\"M5.673 3.564L9.853 3.564Q10.537 4.590 11.905 6.661Q13.273 8.732 13.995 9.758L13.995 9.758L16.047 12.798Q19.391 17.776 21.025 20.322L21.025 20.322L16.845 20.322L16.731 20.208Q16.199 19.372 15.078 17.700Q13.957 16.028 13.425 15.192L13.425 15.192Q13.349 15.154 13.349 15.135Q13.349 15.116 13.349 15.116Q13.349 15.116 13.349 15.135Q13.349 15.154 13.273 15.192L13.273 15.192Q12.741 16.028 11.620 17.700Q10.499 19.372 9.967 20.208L9.967 20.208L9.853 20.322L5.673 20.322L5.673 20.208Q6.585 18.840 8.428 16.104Q10.271 13.368 11.145 12L11.145 12L11.145 11.886Q10.271 10.518 8.428 7.782Q6.585 5.046 5.673 3.678L5.673 3.678L5.673 3.564ZM4.343 20.360L4.267 20.436L0.125 20.436L0.125 20.322L0.505 19.676Q1.227 18.574 1.645 18.042L1.645 18.042L5.559 12.114L5.559 12Q2.747 7.478 0.125 3.678L0.125 3.678L4.267 3.678L4.381 3.792Q4.875 4.476 5.749 5.825Q6.623 7.174 7.117 7.858L7.117 7.858L7.915 9.036Q9.245 10.974 9.853 12L9.853 12L9.853 12.114Q8.789 13.748 6.547 17.016L6.547 17.016L4.267 20.436L4.343 20.360ZM23.875 8.428L23.875 11.278L16.389 11.278L14.603 8.542L14.603 8.428L23.875 8.428ZM23.875 12.722L23.875 15.572Q23.875 15.534 23.875 15.534L23.875 15.534L23.875 15.572L19.125 15.572L17.339 12.836L17.339 12.722L23.875 12.722Z\"/>',\n\t'seti:haxe':\n\t\t'<path d=\"M23.802 17.376L23.676 17.250L21.072 12L23.676 6.750L23.802 6.624L23.802 0.198L17.376 0.198L17.250 0.324L12 2.928L6.750 0.324L6.624 0.198L0.198 0.198L0.198 6.624L0.324 6.750L2.928 12L0.324 17.250L0.198 17.376L0.198 23.802L6.624 23.802L6.750 23.676L12 21.072L17.250 23.676L17.376 23.802L23.802 23.802L23.802 17.376ZM22.374 1.626L19.896 11.454L12.546 4.104L22.374 1.626ZM11.496 4.104L4.146 11.454L1.626 1.626L11.496 4.104ZM1.626 22.374L4.146 12.504L11.496 19.854L1.626 22.374ZM12.546 19.854L19.896 12.504L22.374 22.374L12.546 19.854Z\"/>',\n\t'seti:jade':\n\t\t'<path d=\"M23.247 16.411L23.247 16.411Q22.923 14.719 23.103 12.595L23.103 12.595Q23.247 11.911 23.247 11.551L23.247 11.551Q23.283 10.975 23.103 10.435L23.103 10.435Q22.887 9.751 22.671 8.419L22.671 8.419L22.635 8.203Q22.419 6.799 22.275 6.079L22.275 6.079Q22.023 4.855 21.663 3.919L21.663 3.919Q21.447 3.523 21.267 3.343L21.267 3.343Q21.015 3.127 20.655 3.127L20.655 3.127Q19.971 3.127 19.971 3.811L19.971 3.811Q20.007 4.315 20.187 5.377Q20.367 6.439 20.403 6.943L20.403 6.943Q20.475 7.339 20.637 8.077Q20.799 8.815 20.871 9.211L20.871 9.211L20.871 9.643Q20.547 9.643 20.403 9.535L20.403 9.535L20.043 9.283Q19.431 8.851 19.179 8.527L19.179 8.527Q18.351 7.339 17.847 6.151L17.847 6.151L17.307 4.927Q16.371 2.659 15.795 1.687L15.795 1.687Q15.327 0.895 14.607 0.571L14.607 0.571Q14.031 0.319 13.095 0.319L13.095 0.319Q12.663 0.319 12.663 0.751L12.663 0.751L12.735 1.111Q12.843 1.543 12.987 1.795L12.987 1.795Q13.491 2.695 14.445 4.549Q15.399 6.403 15.903 7.303L15.903 7.303Q16.191 7.735 16.839 8.599L16.839 8.599L17.055 8.851Q17.379 9.355 17.307 9.607Q17.235 9.859 16.695 10.111L16.695 10.111L16.587 10.111L16.479 10.219Q15.327 10.507 14.355 9.535L14.355 9.535Q13.419 8.815 12.303 8.635L12.303 8.635Q12.159 8.635 11.763 8.581Q11.367 8.527 11.187 8.527L11.187 8.527Q9.819 8.419 8.883 8.491L8.883 8.491Q7.659 8.599 6.579 8.995L6.579 8.995Q4.995 9.535 3.879 10.831L3.879 10.831Q2.943 11.947 2.295 13.711L2.295 13.711Q2.079 14.395 1.647 15.943L1.647 15.943Q1.503 15.727 1.395 15.619L1.395 15.619Q1.179 15.403 1.179 15.187L1.179 15.187L1.035 15.259Q0.819 15.475 0.747 15.619L0.747 15.619L0.747 16.195Q0.747 16.627 0.963 17.095L0.963 17.095Q2.259 20.227 3.195 22.927L3.195 22.927Q3.555 23.611 4.203 23.611L4.203 23.611Q6.111 23.791 7.479 23.503L7.479 23.503L9.495 23.143Q9.063 22.711 8.847 22.603L8.847 22.603Q8.847 22.531 8.901 22.477Q8.955 22.423 8.955 22.351L8.955 22.351L10.503 22.711Q13.347 23.755 16.047 23.611L16.047 23.611Q16.659 23.611 16.803 23.143L16.803 23.143L17.163 22.495Q17.415 21.415 18.279 20.119L18.279 20.119L18.315 20.047Q18.711 19.507 18.855 19.363Q18.999 19.219 19.197 19.219Q19.395 19.219 19.863 19.363L19.863 19.363L20.187 19.435Q21.195 19.795 22.023 19.345Q22.851 18.895 23.103 17.851L23.103 17.851Q23.247 17.419 23.247 16.411ZM21.987 15.043L21.987 15.043Q21.663 15.403 21.555 15.403L21.555 15.403Q19.683 15.583 18.603 15.043L18.603 15.043Q18.351 14.971 17.955 14.539L17.955 14.539L17.703 14.287Q18.387 14.035 18.603 13.927L18.603 13.927Q20.007 13.783 21.447 14.503L21.447 14.503Q21.483 14.503 21.591 14.539L21.591 14.539L21.663 14.611Q21.987 14.935 21.987 15.043Z\"/>',\n\t'seti:java':\n\t\t'<path d=\"M19.896 0.198L19.896 14.940Q19.854 15.108 19.854 15.444L19.854 15.444Q19.854 16.410 19.812 16.914L19.812 16.914Q19.728 17.796 19.518 18.384L19.518 18.384Q18.972 20.106 17.733 21.387Q16.494 22.668 14.856 23.340L14.856 23.340Q14.100 23.634 13.365 23.718Q12.630 23.802 11.454 23.802L11.454 23.802Q9.060 23.802 7.338 23.130L7.338 23.130Q5.448 22.248 4.104 20.610L4.104 20.610L6.750 17.208Q7.548 18.258 8.745 18.846Q9.942 19.434 11.328 19.434L11.328 19.434Q13.176 19.434 14.205 18.216Q15.234 16.998 15.234 14.562L15.234 14.562L15.234 0.198L19.896 0.198Z\"/>',\n\t'seti:javascript':\n\t\t'<path d=\"M5.376 14.300L5.376 3.352L9.286 3.352L9.286 14.300Q9.286 17.842 7.630 19.452L7.630 19.452Q6.112 20.924 3.076 20.924L3.076 20.924Q2.386 20.924 1.558 20.809Q0.730 20.694 0.224 20.464L0.224 20.464L0.638 17.336Q1.512 17.750 2.662 17.750L2.662 17.750Q3.950 17.750 4.594 17.060L4.594 17.060Q5.376 16.232 5.376 14.300L5.376 14.300ZM11.862 19.912L12.736 16.600Q13.564 17.060 14.668 17.382L14.668 17.382Q15.910 17.750 17.014 17.750L17.014 17.750Q18.394 17.750 19.084 17.267Q19.774 16.784 19.774 15.910Q19.774 15.036 19.130 14.507Q18.486 13.978 16.876 13.426L16.876 13.426Q12.138 11.770 12.138 8.274L12.138 8.274Q12.138 5.974 13.909 4.525Q15.680 3.076 18.762 3.076L18.762 3.076Q21.200 3.076 23.224 3.950L23.224 3.950L22.350 7.124L22.074 6.986Q21.246 6.664 20.740 6.526L20.740 6.526Q19.774 6.250 18.762 6.250L18.762 6.250Q17.520 6.250 16.853 6.733Q16.186 7.216 16.186 7.998Q16.186 8.780 16.922 9.286L16.922 9.286Q17.474 9.700 19.314 10.436L19.314 10.436Q21.614 11.310 22.695 12.552Q23.776 13.794 23.776 15.588L23.776 15.588Q23.776 17.888 22.028 19.314L22.028 19.314Q20.142 20.924 16.738 20.924L16.738 20.924Q15.404 20.924 13.932 20.556L13.932 20.556Q12.782 20.326 11.862 19.912L11.862 19.912Z\"/>',\n\t'seti:jinja':\n\t\t'<path d=\"M23.919 0.975L23.919 0.975Q23.919 0.926 23.772 0.828L23.772 0.828L23.674 0.730L23.527 0.730L21.469 1.661Q17.353 3.425 11.963 3.768L11.963 3.768L7.553 4.062Q5.299 4.209 4.123 4.111L4.123 4.111Q2.261 3.964 0.742 3.278L0.742 3.278Q1.330 4.552 0.056 5.091L0.056 5.091L0.056 5.238L0.301 5.385Q0.644 5.581 0.840 5.630L0.840 5.630L1.085 5.679Q1.722 5.875 2.016 6.120L2.016 6.120Q2.702 6.610 2.898 7.247L2.898 7.247Q2.947 7.394 3.290 7.394L3.290 7.394L3.633 7.443Q4.564 7.492 5.030 7.467Q5.495 7.443 5.814 7.590Q6.132 7.737 6.083 8.031Q6.034 8.325 5.691 8.595Q5.348 8.864 4.981 8.962Q4.613 9.060 3.878 9.011L3.878 9.011L3.437 8.962Q3.094 8.913 2.972 9.035Q2.849 9.158 2.996 9.501L2.996 9.501Q3.045 9.697 3.094 10.187L3.094 10.187L3.143 10.481L5.985 10.481L5.789 14.499L3.584 14.352L3.486 13.764L4.662 12.882Q4.025 12.441 3.070 12.416Q2.114 12.392 1.036 12.833L1.036 12.833L1.281 13.029Q1.575 13.323 1.771 13.421L1.771 13.421Q2.212 13.764 2.188 13.985Q2.163 14.205 1.575 14.499L1.575 14.499Q2.065 15.332 2.163 16.410L2.163 16.410Q2.261 17.145 2.163 18.419L2.163 18.419Q2.163 18.517 2.016 18.738Q1.869 18.958 1.869 19.056L1.869 19.056L1.820 19.252Q1.624 19.987 1.771 20.134L1.771 20.134Q1.967 20.330 2.016 20.624L2.016 20.624Q2.065 20.820 2.016 21.163L2.016 21.163L2.016 21.506Q2.065 21.849 2.016 22.143L2.016 22.143Q1.918 22.731 2.555 22.682L2.555 22.682L3.731 22.682L3.437 20.428L5.397 20.428L5.152 22.437L7.602 22.437L7.602 20.232L8.925 20.134L8.925 22.290L10.395 22.290L10.395 21.702L10.444 21.065Q10.542 20.232 10.542 19.791L10.542 19.791Q10.542 19.546 10.469 19.130Q10.395 18.713 10.346 18.517L10.346 18.517L10.297 17.880Q10.199 17.145 10.199 16.753L10.199 16.753Q10.199 16.116 10.444 15.626L10.444 15.626Q10.836 14.940 10.297 14.597L10.297 14.597Q10.199 14.548 10.224 14.278Q10.248 14.009 10.371 13.837Q10.493 13.666 10.934 13.323L10.934 13.323L11.179 13.078Q9.562 12.441 7.945 13.078L7.945 13.078L8.974 14.352L8.827 14.548L7.406 14.548L7.406 10.383L15.148 9.942L14.903 14.254L13.629 14.254L13.629 13.372L14.854 12.441Q12.992 11.608 10.983 12.392L10.983 12.392L11.816 13.274Q12.110 13.568 12.086 13.715Q12.061 13.862 11.865 14.107L11.865 14.107L11.620 14.352Q11.767 14.499 11.743 14.842Q11.718 15.185 11.767 15.381Q11.816 15.577 12.061 15.724L12.061 15.724Q12.159 15.724 12.110 16.018L12.110 16.018L12.110 16.753Q12.110 17.929 12.061 18.566L12.061 18.566Q12.061 18.909 11.669 19.448L11.669 19.448L11.816 20.134Q11.963 20.967 11.963 21.359L11.963 21.359Q12.012 22.094 11.914 22.682L11.914 22.682Q11.865 22.780 12.012 23.074L12.012 23.074L12.110 23.270L13.972 23.270L13.727 20.673L15.050 20.673L15.050 22.878L17.696 22.878L17.451 20.526L18.627 20.526L18.627 22.633L18.872 22.731Q19.656 22.780 19.901 22.755Q20.146 22.731 20.195 22.486Q20.244 22.241 20.244 21.506L20.244 21.506L20.244 21.457Q20.195 21.016 20.342 20.134L20.342 20.134L20.440 19.399Q20.440 19.252 20.440 19.154L20.440 19.154Q19.999 18.419 19.999 17.439L19.999 17.439Q20.048 16.851 20.244 15.724L20.244 15.724L20.244 15.577Q20.293 15.332 20.195 14.793L20.195 14.793L20.097 14.401L20.048 13.764L21.175 12.784Q19.313 12.098 17.598 12.784L17.598 12.784Q17.696 13.029 18.113 13.299Q18.529 13.568 18.627 13.715L18.627 13.715Q18.823 14.009 18.725 14.401L18.725 14.401L17.402 14.401Q17.304 14.401 17.182 14.328Q17.059 14.254 17.059 14.156L17.059 14.156L16.471 9.942L20.391 9.599L20.440 9.158Q20.587 8.521 20.587 8.178L20.587 8.178Q20.587 8.080 20.416 7.957Q20.244 7.835 20.146 7.835L20.146 7.835L20.146 7.835Q18.872 7.884 18.333 8.080L18.333 8.080Q17.892 8.227 17.500 8.080L17.500 8.080Q17.255 7.982 16.863 7.688L16.863 7.688L16.618 7.541L16.716 7.345Q16.814 7.051 16.912 6.953L16.912 6.953Q17.157 6.610 17.353 6.561L17.353 6.561L20.097 6.169Q20.538 6.071 20.587 5.924L20.587 5.924Q20.783 5.189 21.371 4.846L21.371 4.846Q21.714 4.601 22.547 4.356L22.547 4.356L23.919 3.915L23.919 3.670Q23.968 2.102 23.919 0.975ZM5.544 15.871L5.544 18.860Q4.662 18.909 3.535 19.007L3.535 19.007L3.535 15.724L5.544 15.871ZM7.406 15.871L8.925 15.871Q8.925 16.116 8.925 16.655L8.925 16.655Q8.925 17.880 8.925 18.517L8.925 18.517Q8.925 18.811 8.778 18.860Q8.631 18.909 8.386 18.860L8.386 18.860L8.043 18.860Q7.749 18.909 7.602 18.860Q7.455 18.811 7.455 18.468L7.455 18.468L7.406 15.871ZM8.141 8.668L8.141 8.668Q7.553 8.521 7.553 8.129L7.553 8.129Q7.553 7.835 7.896 7.345L7.896 7.345L9.660 7.198L9.905 8.668L9.317 8.717Q8.484 8.766 8.141 8.668ZM14.952 15.724L14.952 19.154L13.727 19.154L13.727 15.724L14.952 15.724ZM17.304 15.773L18.725 15.724L18.725 19.007L17.500 19.105L17.304 15.773ZM12.600 8.423L12.600 8.423Q12.551 7.688 12.649 7.394Q12.747 7.100 13.139 6.977Q13.531 6.855 14.658 6.806L14.658 6.806L15.197 7.492Q14.756 8.178 14.021 8.374L14.021 8.374Q13.482 8.521 12.600 8.423Z\"/>',\n\t'seti:julia':\n\t\t'<path d=\"M0.164 17.896L0.164 17.896Q0.164 19.348 0.890 20.580Q1.616 21.812 2.848 22.538Q4.080 23.264 5.532 23.264Q6.984 23.264 8.216 22.538Q9.448 21.812 10.174 20.580Q10.900 19.348 10.900 17.896Q10.900 16.444 10.174 15.212Q9.448 13.980 8.216 13.254Q6.984 12.528 5.532 12.528Q4.080 12.528 2.848 13.254Q1.616 13.980 0.890 15.212Q0.164 16.444 0.164 17.896ZM13.100 17.896L13.100 17.896Q13.100 19.348 13.826 20.580Q14.552 21.812 15.784 22.538Q17.016 23.264 18.468 23.264Q19.920 23.264 21.152 22.538Q22.384 21.812 23.110 20.580Q23.836 19.348 23.836 17.896Q23.836 16.444 23.110 15.212Q22.384 13.980 21.152 13.276Q19.920 12.572 18.468 12.572Q17.016 12.572 15.784 13.276Q14.552 13.980 13.826 15.212Q13.100 16.444 13.100 17.896ZM6.632 6.104L6.632 6.104Q6.632 7.556 7.358 8.788Q8.084 10.020 9.316 10.724Q10.548 11.428 12.000 11.428Q13.452 11.428 14.684 10.724Q15.916 10.020 16.642 8.788Q17.368 7.556 17.368 6.104Q17.368 4.652 16.642 3.420Q15.916 2.188 14.684 1.462Q13.452 0.736 12.000 0.736Q10.548 0.736 9.316 1.462Q8.084 2.188 7.358 3.420Q6.632 4.652 6.632 6.104Z\"/>',\n\t'seti:karma':\n\t\t'<path d=\"M8.964 0.164L2.364 0.164L2.364 6.764L6.896 23.968L8.964 23.968L8.964 17.104L10.064 17.104L14.772 23.968L21.636 23.968L13.496 11.736L13.496 11.032L21.064 0.032L13.804 0.032L10.064 6.104L8.964 6.104L8.964 0.164Z\"/>',\n\t'seti:kotlin':\n\t\t'<path d=\"M23.739 23.739L0.261 23.739L12.043 11.957L23.739 23.739ZM0.261 12.989L0.261 0.261L12.043 0.261L0.261 12.989ZM23.739 0.261L12.043 0.261L0.261 12.645L0.261 23.739L23.739 0.261Z\"/>',\n\t'seti:dart':\n\t\t'<path d=\"M23.840 19.720L23.840 9.880L18.880 4.960L14.920 1Q14.600 0.640 14.060 0.400Q13.520 0.160 13.080 0.160L13.080 0.160Q12.120 0.160 11.560 0.480L11.560 0.480L4.640 3.960L4.160 4.200L0.480 11.880Q0.160 12.560 0.160 13.040L0.160 13.040Q0.160 14.240 1 15.080L1 15.080L9.760 23.840L19.720 23.840L19.720 19.720L23.840 19.720ZM5.480 4.120L11.840 0.920Q12.280 0.640 13.080 0.640L13.080 0.640Q13.400 0.640 13.840 0.860Q14.280 1.080 14.560 1.360L14.560 1.360L17.360 4.160L17.280 4.160Q16.920 4.120 16.560 4.120L16.560 4.120L5.480 4.120ZM19.200 19.560L19.200 23.320L9.960 23.320L5.320 18.680Q4.880 18.240 4.760 17.900Q4.640 17.560 4.640 16.720L4.640 16.720L4.640 5L19.200 19.560Z\"/>',\n\t'seti:liquid':\n\t\t'<path d=\"M14.413 15.173L14.451 15.287Q14.451 15.591 14.394 16.256Q14.337 16.921 14.337 17.225L14.337 17.225Q14.071 20.987 14.071 22.925L14.071 22.925L14.071 23.723Q14.071 23.837 14.052 23.856Q14.033 23.875 13.957 23.875L13.957 23.875L10.385 23.153Q10.119 23.077 9.568 23.020Q9.017 22.963 8.751 22.925L8.751 22.925L8.447 22.849Q7.573 22.659 7.193 22.659L7.193 22.659Q6.433 22.545 4.875 22.260Q3.317 21.975 2.557 21.823L2.557 21.823Q2.063 21.633 0.923 21.405L0.923 21.405L0.657 21.367Q0.543 21.367 0.543 21.139L0.543 21.139Q0.771 19.923 0.999 17.377L0.999 17.377L1.037 17.073Q1.037 16.959 1.094 16.674Q1.151 16.389 1.151 16.275L1.151 16.275Q1.189 16.199 1.189 16.047Q1.189 15.895 1.265 15.781L1.265 15.781Q1.303 15.477 1.379 14.850Q1.455 14.223 1.493 13.881L1.493 13.881Q1.721 12.703 1.721 11.981L1.721 11.981Q2.177 9.017 2.329 7.231L2.329 7.231Q2.329 6.775 2.785 6.509L2.785 6.509L4.951 5.825Q5.027 5.825 5.103 5.730Q5.179 5.635 5.179 5.445L5.179 5.445Q5.483 3.735 6.737 1.873L6.737 1.873Q7.497 0.733 8.485 0.239L8.485 0.239Q8.751 0.239 9.169 0.182Q9.587 0.125 9.815 0.125L9.815 0.125Q10.803 0.125 11.221 0.695L11.221 0.695L11.335 0.809L11.449 0.809Q11.943 0.809 12.342 0.999Q12.741 1.189 13.007 1.531L13.007 1.531Q13.387 2.025 13.843 2.975L13.843 2.975Q13.843 3.089 14.071 3.089L14.071 3.089L14.679 2.823Q14.869 2.823 14.888 2.861Q14.907 2.899 14.907 3.089L14.907 3.089Q14.907 3.621 14.850 4.628Q14.793 5.635 14.793 6.167L14.793 6.167Q14.717 6.813 14.660 8.181Q14.603 9.549 14.565 10.195L14.565 10.195Q14.489 11.031 14.432 12.627Q14.375 14.223 14.299 15.059L14.299 15.059L14.413 15.173ZM5.635 14.717L5.635 14.717Q6.129 15.173 6.471 15.439L6.471 15.439Q7.155 15.857 7.307 16.503L7.307 16.503Q7.421 16.959 7.231 17.282Q7.041 17.605 6.585 17.681L6.585 17.681L6.015 17.681Q5.141 17.529 4.229 16.845L4.229 16.845Q4.153 16.769 4.115 16.788Q4.077 16.807 4.001 16.959L4.001 16.959Q3.735 17.909 3.545 18.403Q3.355 18.897 3.735 19.125L3.735 19.125Q5.369 20.341 7.193 20.189L7.193 20.189Q8.181 20.075 8.865 19.562Q9.549 19.049 9.910 18.156Q10.271 17.263 10.271 16.389Q10.271 15.515 9.701 14.717L9.701 14.717Q9.701 14.641 9.644 14.584Q9.587 14.527 9.587 14.489L9.587 14.489Q9.321 14.071 8.903 13.729L8.903 13.729Q8.637 13.539 8.029 13.159L8.029 13.159Q7.801 13.045 7.307 12.589L7.307 12.589Q7.041 12.095 7.231 11.525Q7.421 10.955 8.029 10.803L8.029 10.803Q9.093 10.651 10.271 11.031L10.271 11.031Q10.347 11.107 10.423 11.050Q10.499 10.993 10.499 10.917L10.499 10.917L11.221 8.789Q11.335 8.523 11.297 8.447Q11.259 8.371 10.993 8.295L10.993 8.295Q9.891 7.991 8.485 8.181L8.485 8.181Q6.965 8.447 5.977 9.397L5.977 9.397Q4.913 10.461 4.837 11.981L4.837 11.981Q4.647 12.741 4.875 13.444Q5.103 14.147 5.635 14.717ZM10.385 1.037L10.385 1.037Q10.309 0.961 10.271 0.961L10.271 0.961L10.157 0.923Q10.005 0.847 9.663 0.923L9.663 0.923L9.321 0.923Q8.371 1.265 7.687 2.139L7.687 2.139Q6.661 3.279 6.015 5.445L6.015 5.445Q6.015 5.521 6.034 5.540Q6.053 5.559 6.129 5.559L6.129 5.559Q6.433 5.521 6.965 5.331Q7.497 5.141 7.801 5.103L7.801 5.103Q7.877 5.103 7.877 5.027L7.877 5.027L7.915 4.989Q7.915 4.837 7.972 4.609Q8.029 4.381 8.029 4.267L8.029 4.267Q8.447 3.127 8.903 2.443L8.903 2.443Q9.511 1.531 10.385 1.037ZM11.221 3.545L11.221 3.545Q11.221 2.975 10.993 1.987L10.993 1.987Q10.917 1.835 10.879 1.816Q10.841 1.797 10.765 1.873L10.765 1.873Q10.195 2.177 10.043 2.481L10.043 2.481Q9.359 3.317 8.979 4.495L8.979 4.495L8.979 4.609L9.093 4.609Q9.435 4.533 10.119 4.343L10.119 4.343L10.765 4.153Q11.107 4.039 11.164 3.963Q11.221 3.887 11.221 3.545ZM12.171 3.773L12.171 3.773Q12.437 3.659 13.121 3.431L13.121 3.431L13.121 3.317Q12.931 2.519 12.437 1.987L12.437 1.987Q12.171 1.759 11.943 1.645L11.943 1.645L11.829 1.645L11.829 1.759Q11.829 1.835 11.886 2.006Q11.943 2.177 11.943 2.253L11.943 2.253Q12.057 2.595 12.057 3.545L12.057 3.545Q12.057 3.773 12.171 3.773ZM22.735 18.289L23.115 19.581L23.457 22.089L23.457 22.203L23.343 22.317Q22.621 22.431 21.253 22.735Q19.885 23.039 19.201 23.153L19.201 23.153Q17.415 23.571 16.237 23.723L16.237 23.723L16.123 23.761Q16.047 23.761 16.047 23.742Q16.047 23.723 16.085 23.609L16.085 23.609Q16.085 22.773 16.199 22.431L16.199 22.431Q16.275 21.595 16.332 19.999Q16.389 18.403 16.465 17.567L16.465 17.567L16.465 16.389Q16.465 15.553 16.579 15.173L16.579 15.173L16.579 14.337L16.921 8.067L16.921 6.889Q17.035 5.939 17.035 4.153L17.035 4.153L17.035 2.937Q17.149 2.937 17.149 2.956Q17.149 2.975 17.149 3.089L17.149 3.089L17.643 3.545L17.909 3.773Q18.175 4.001 18.251 4.153Q18.327 4.305 18.460 4.343Q18.593 4.381 18.840 4.381Q19.087 4.381 19.619 4.438Q20.151 4.495 20.379 4.495L20.379 4.495Q20.455 4.495 20.531 4.571Q20.607 4.647 20.607 4.723L20.607 4.723Q20.683 4.989 20.740 5.464Q20.797 5.939 20.835 6.167L20.835 6.167L21.215 8.523Q21.329 9.131 21.500 10.385Q21.671 11.639 21.785 12.209L21.785 12.209Q21.975 13.235 22.279 15.249Q22.583 17.263 22.735 18.289L22.735 18.289Z\"/>',\n\t'seti:livescript':\n\t\t'<path d=\"M12.222 2.158L12.222 2.158L12.222 10.853Q12.296 10.853 12.314 10.835Q12.333 10.816 12.333 10.742L12.333 10.742L12.407 10.705Q12.444 10.668 12.444 10.594L12.444 10.594L12.518 10.594Q12.555 10.557 12.555 10.483L12.555 10.483Q12.666 10.483 12.684 10.483Q12.703 10.483 12.703 10.372L12.703 10.372Q12.777 10.372 12.795 10.354Q12.814 10.335 12.814 10.261L12.814 10.261Q12.925 10.261 12.925 10.039L12.925 10.039L12.925 2.972Q12.925 2.639 13.258 2.639L13.258 2.639L13.961 2.639Q14.220 2.639 14.257 2.676Q14.294 2.713 14.294 2.972L14.294 2.972L14.294 8.744L14.405 8.744Q14.516 8.744 14.534 8.744Q14.553 8.744 14.553 8.633L14.553 8.633L14.553 8.522L14.775 8.300Q14.849 8.226 14.812 8.152L14.812 8.152L14.775 8.078L14.775 3.675L16.033 3.675L16.033 6.894Q16.107 6.894 16.163 6.838Q16.218 6.783 16.255 6.783L16.255 6.783L17.550 5.525Q17.661 5.525 17.661 5.303L17.661 5.303L17.661 3.564Q17.809 3.490 18.105 3.527L18.105 3.527L18.364 3.564L18.623 3.527Q18.956 3.490 19.030 3.564Q19.104 3.638 19.067 4.008L19.067 4.008L19.030 4.267L19.030 4.822L19.067 4.933L19.178 5.044L19.104 5.044L20.214 5.044Q20.325 5.044 20.380 5.100Q20.436 5.155 20.436 5.303L20.436 5.303L20.436 6.450L20.103 6.450Q20.140 6.376 20.251 6.376Q20.362 6.376 20.436 6.339L20.436 6.339L18.808 6.339Q18.734 6.339 18.715 6.358Q18.697 6.376 18.697 6.450L18.697 6.450L18.216 6.894Q17.513 7.560 17.180 7.967L17.180 7.967Q17.180 8.078 17.439 8.078L17.439 8.078L20.214 8.078Q20.362 8.078 20.399 8.097Q20.436 8.115 20.436 8.300L20.436 8.300L20.436 9.114Q20.436 9.299 20.380 9.373Q20.325 9.447 20.103 9.447L20.103 9.447L15.922 9.447L15.848 9.447Q15.774 9.410 15.737 9.428Q15.700 9.447 15.700 9.558L15.700 9.558L14.294 10.964L20.436 10.964L20.436 12Q20.436 12.148 20.362 12.185Q20.288 12.222 20.103 12.222L20.103 12.222L12.814 12.222Q12.703 12.222 12.703 12.241Q12.703 12.259 12.703 12.333L12.703 12.333Q12.592 12.333 12.573 12.352Q12.555 12.370 12.555 12.444L12.555 12.444Q12.481 12.444 12.463 12.463Q12.444 12.481 12.444 12.592L12.444 12.592Q12.370 12.592 12.351 12.592Q12.333 12.592 12.333 12.703L12.333 12.703Q12.259 12.703 12.240 12.722Q12.222 12.740 12.222 12.814L12.222 12.814Q12.148 12.814 12.129 12.832Q12.111 12.851 12.111 12.925L12.111 12.925Q12.037 12.925 12.018 12.944Q12.000 12.962 12.037 13.036Q12.074 13.110 12.148 13.073L12.148 13.073L12.222 13.036L20.880 13.036Q20.991 13.036 21.009 13.055Q21.028 13.073 21.028 13.147L21.028 13.147L21.028 13.961Q21.028 14.220 20.972 14.276Q20.917 14.331 20.658 14.331L20.658 14.331L10.372 14.331L10.261 14.442Q10.187 14.553 10.021 14.720Q9.854 14.886 9.780 14.997L9.780 14.997Q9.854 15.071 10.039 15.034L10.039 15.034L10.150 14.997L21.583 14.997Q21.694 14.997 21.749 15.034Q21.805 15.071 21.805 15.256L21.805 15.256L21.805 16.403L8.633 16.403Q8.411 16.403 8.300 16.514L8.300 16.514L7.005 17.772Q7.005 17.846 7.153 17.883L7.153 17.883L7.264 17.883L22.989 17.883Q23.137 17.883 23.174 17.920Q23.211 17.957 23.211 18.142L23.211 18.142L23.211 20.325Q23.211 20.584 23.174 20.621Q23.137 20.658 22.841 20.658L22.841 20.658L5.858 20.658Q5.784 20.732 5.821 20.843L5.821 20.843L5.858 20.917L5.858 23.544L3.194 23.544Q3.046 23.544 3.009 23.489Q2.972 23.433 2.972 23.322L2.972 23.322L2.972 20.658Q2.898 20.584 2.842 20.621Q2.787 20.658 2.750 20.658L2.750 20.658L0.419 20.658Q0.271 20.658 0.234 20.639Q0.197 20.621 0.197 20.436L0.197 20.436L0.197 18.142Q0.197 17.957 0.252 17.920Q0.308 17.883 0.419 17.883L0.419 17.883L2.972 17.883L0.197 17.883Q0.271 17.809 0.326 17.846Q0.382 17.883 0.419 17.883L0.419 17.883L3.083 17.883Q3.194 17.772 3.083 17.661L3.083 17.661L3.083 0.900Q3.083 0.715 3.120 0.697Q3.157 0.678 3.305 0.678L3.305 0.678L5.525 0.678Q5.784 0.678 5.821 0.715Q5.858 0.752 5.858 1.011L5.858 1.011L5.858 16.958Q6.006 16.884 6.228 16.625L6.228 16.625L6.672 16.144Q7.042 15.737 7.264 15.589L7.264 15.589Q7.375 15.589 7.375 15.367L7.375 15.367L7.375 2.158Q7.523 2.084 7.930 2.158L7.930 2.158L8.189 2.158L8.411 2.158Q8.670 2.084 8.744 2.158Q8.818 2.232 8.781 2.491L8.781 2.491L8.744 2.750L8.744 13.961Q9.669 13.036 10.261 12.592L10.261 12.592Q10.335 12.518 10.372 12.407L10.372 12.407L10.372 12.333L10.372 2.158L11.630 2.158Q11.741 2.158 11.759 2.177Q11.778 2.195 11.778 2.269L11.778 2.269Q12.037 2.121 12.148 2.158L12.148 2.158L12.222 2.158Q12.111 5.044 12.111 10.594L12.111 10.594L12.111 10.853Q12.185 10.779 12.148 10.594L12.148 10.594L12.111 10.483L12.111 2.417Q12.148 2.417 12.148 2.325Q12.148 2.232 12.222 2.158ZM23.544 20.658L6.450 20.658Q6.524 20.584 6.672 20.658L6.672 20.658L6.783 20.658L23.433 20.658Q23.692 20.658 23.747 20.621Q23.803 20.584 23.803 20.325L23.803 20.325L23.803 18.142Q23.803 17.957 23.766 17.920Q23.729 17.883 23.544 17.883L23.544 17.883L7.819 17.883L7.745 17.920Q7.671 17.920 7.634 17.901Q7.597 17.883 7.597 17.772L7.597 17.772L23.655 17.772Q23.766 17.772 23.785 17.809Q23.803 17.846 23.803 17.994L23.803 17.994L23.803 20.436Q23.803 20.621 23.766 20.639Q23.729 20.658 23.544 20.658L23.544 20.658ZM6.450 0.678L6.450 16.736Q6.376 16.662 6.413 16.514L6.413 16.514L6.450 16.403L6.450 0.789Q6.450 0.530 6.394 0.475Q6.339 0.419 6.080 0.419L6.080 0.419L3.897 0.419Q3.712 0.419 3.693 0.456Q3.675 0.493 3.675 0.678L3.675 0.678L3.675 0.567Q3.564 0.456 3.582 0.438Q3.601 0.419 3.786 0.419L3.786 0.419L6.228 0.419Q6.339 0.419 6.394 0.475Q6.450 0.530 6.450 0.678L6.450 0.678ZM15.108 7.967L15.108 7.967Q15.034 7.893 15.071 7.782L15.071 7.782L15.108 7.708L15.108 2.158Q15.108 1.899 15.071 1.862Q15.034 1.825 14.775 1.825L14.775 1.825L14.072 1.825Q13.739 1.825 13.739 2.158L13.739 2.158L13.739 9.225Q13.739 9.447 13.591 9.447L13.591 9.447Q13.739 9.336 13.739 9.114L13.739 9.114L13.739 1.936Q13.739 1.751 13.757 1.733Q13.776 1.714 13.961 1.714L13.961 1.714L14.997 1.714Q15.108 1.714 15.163 1.751Q15.219 1.788 15.219 1.936L15.219 1.936L15.219 7.486Q15.108 7.708 15.108 7.967ZM11.741 13.369L11.778 13.406Q11.852 13.295 12.000 13.369L12.000 13.369L12.111 13.369L22.064 13.369Q22.323 13.406 22.360 13.351Q22.397 13.295 22.397 13.036L22.397 13.036L22.397 12.222Q22.397 12.111 22.378 12.074Q22.360 12.037 22.286 12.111L22.286 12.111L13.369 12.111L22.286 12.111Q22.471 12.111 22.489 12.148Q22.508 12.185 22.508 12.333L22.508 12.333L22.508 13.258Q22.508 13.406 22.471 13.462Q22.434 13.517 22.286 13.517L22.286 13.517L12.111 13.517Q11.889 13.369 11.741 13.369L11.741 13.369ZM7.930 2.010L7.930 1.936Q8.004 2.010 7.967 2.066Q7.930 2.121 7.930 2.158L7.930 2.158L7.930 15.108Q7.930 15.367 7.819 15.367L7.819 15.367L7.819 2.158Q7.893 2.084 7.930 2.010L7.930 2.010ZM22.286 16.292L22.286 16.292L22.286 15.108Q22.286 14.997 22.249 14.942Q22.212 14.886 22.064 14.886L22.064 14.886L10.261 14.886L21.805 14.886Q22.064 14.886 22.119 14.923Q22.175 14.960 22.175 15.256L22.175 15.256L22.175 16.181Q22.249 16.181 22.304 16.236Q22.360 16.292 22.286 16.292ZM10.853 1.936L10.853 1.936L10.853 12.111Q10.853 12.333 10.705 12.333L10.705 12.333L10.705 2.047Q10.779 2.047 10.779 1.992Q10.779 1.936 10.853 1.936ZM17.550 7.634L17.550 7.708Q17.550 7.597 17.772 7.597L17.772 7.597L21.953 7.597Q22.286 7.597 22.286 7.264L22.286 7.264L22.286 6.450Q22.286 6.265 22.249 6.247Q22.212 6.228 22.064 6.228L22.064 6.228L19.289 6.228Q19.030 6.228 19.030 6.117L19.030 6.117L22.397 6.117Q22.471 6.117 22.508 6.154L22.508 6.154L22.508 6.228L22.508 7.597L18.105 7.597Q17.846 7.597 17.735 7.616Q17.624 7.634 17.550 7.708L17.550 7.708L17.550 7.634ZM12.222 1.936L12.222 2.158Q12.148 2.232 12.185 2.343L12.185 2.343L12.222 2.417L12.222 10.594Q12.037 6.487 12.222 2.158L12.222 2.158Q12.148 2.158 12.148 2.084Q12.148 2.010 12.222 1.936L12.222 1.936ZM14.664 10.483L14.664 10.483Q14.738 10.409 14.812 10.446L14.812 10.446L14.886 10.483L21.953 10.483Q22.138 10.483 22.212 10.446Q22.286 10.409 22.286 10.261L22.286 10.261L22.286 9.225Q22.397 9.225 22.397 9.447L22.397 9.447L22.397 10.483Q22.397 10.594 22.360 10.668Q22.323 10.742 22.175 10.742L22.175 10.742Q20.473 10.557 18.401 10.520L18.401 10.520Q17.180 10.483 14.664 10.483ZM0.678 20.436L0.678 17.772L3.453 17.772L0.900 17.772Q0.715 17.772 0.696 17.809Q0.678 17.846 0.678 17.994L0.678 17.994L0.678 20.325Q0.678 20.510 0.696 20.529Q0.715 20.547 0.900 20.547L0.900 20.547L3.453 20.547L0.789 20.547Q0.678 20.547 0.678 20.529Q0.678 20.510 0.678 20.436L0.678 20.436ZM16.625 1.936L16.625 1.936L16.625 2.047Q16.625 2.010 16.588 1.955Q16.551 1.899 16.625 1.936ZM22.286 7.597L17.661 7.597L22.286 7.597ZM17.994 1.936L17.994 5.044Q17.920 5.044 17.957 4.970L17.957 4.970L17.994 4.822L17.994 1.825Q18.068 1.825 18.031 1.899L18.031 1.899L17.994 1.936ZM21.805 4.711L20.436 4.711Q20.436 4.600 20.547 4.600L20.547 4.600L22.175 4.600Q22.101 4.674 21.990 4.674Q21.879 4.674 21.805 4.711L21.805 4.711ZM19.030 6.228L22.286 6.228L19.030 6.228ZM6.339 23.581L6.339 23.544L6.339 20.658L6.339 23.322Q6.413 23.433 6.413 23.451Q6.413 23.470 6.339 23.581L6.339 23.581ZM22.323 4.267L22.286 4.711L22.286 3.564Q22.286 3.453 22.249 3.398Q22.212 3.342 22.064 3.342L22.064 3.342L20.954 3.342L22.286 3.342Q22.360 3.638 22.323 4.267L22.323 4.267ZM19.511 1.936L19.511 1.936Q19.585 1.936 19.548 2.047L19.548 2.047L19.511 2.158L19.511 3.675L19.474 3.786L19.400 3.897Q19.400 3.453 19.474 2.750L19.474 2.750Q19.511 2.195 19.511 1.936ZM14.294 10.964L14.294 10.964Q14.294 10.890 14.312 10.872Q14.331 10.853 14.405 10.853L14.405 10.853L14.368 10.853Q14.294 10.890 14.294 10.964ZM13.480 11.667L13.480 11.667Q13.480 11.556 13.498 11.538Q13.517 11.519 13.628 11.519L13.628 11.519L13.591 11.593Q13.554 11.667 13.480 11.667ZM14.183 11.038L14.183 11.075Q14.183 11.038 14.183 11.001L14.183 11.001L14.183 11.075L14.183 11.038ZM12.296 10.890L12.333 10.853Q12.333 10.890 12.333 10.927L12.333 10.927L12.333 10.853L12.296 10.890ZM14.405 10.742L14.405 10.742Q14.405 10.631 14.460 10.631Q14.516 10.631 14.405 10.742L14.405 10.742Q14.516 10.631 14.516 10.687Q14.516 10.742 14.405 10.742ZM13.850 11.297L13.850 11.297Q13.850 11.223 13.887 11.223Q13.924 11.223 13.850 11.297L13.850 11.297Q13.924 11.223 13.924 11.260Q13.924 11.297 13.850 11.297ZM12.962 10.224L13.036 10.150Q13.036 10.224 12.999 10.224Q12.962 10.224 13.036 10.150L13.036 10.150L12.962 10.224ZM13.184 10.002L13.258 9.928Q13.258 10.002 13.221 10.002Q13.184 10.002 13.258 9.928L13.258 9.928L13.184 10.002ZM13.369 11.778L13.369 11.778ZM13.258 11.926L13.258 12Q13.258 11.963 13.258 11.926L13.258 11.926L13.258 12L13.258 11.926ZM15.589 7.560L15.700 7.486Q15.700 7.560 15.644 7.560Q15.589 7.560 15.700 7.486L15.700 7.486L15.589 7.560ZM12.444 10.742L12.444 10.742ZM12.518 10.594L12.555 10.594Q12.555 10.631 12.555 10.668L12.555 10.668L12.555 10.594L12.518 10.594ZM10.742 2.158L10.853 2.158Q10.779 2.158 10.742 2.158L10.742 2.158ZM22.286 3.342L22.286 3.342ZM10.853 14.294L10.853 14.294ZM14.072 11.075L14.072 11.075ZM14.072 11.149L14.072 11.186Q14.072 11.186 14.072 11.149L14.072 11.149L14.072 11.186L14.072 11.149ZM10.964 14.294L10.964 14.294ZM13.998 11.186L13.961 11.186Q13.961 11.186 13.998 11.186L13.998 11.186L13.961 11.186L13.998 11.186ZM10.964 14.183L10.964 14.183ZM13.850 11.297L13.850 11.297ZM13.850 11.371L13.850 11.408Q13.850 11.408 13.850 11.371L13.850 11.371L13.850 11.408L13.850 11.371ZM13.776 11.408L13.739 11.408Q13.739 11.408 13.776 11.408L13.776 11.408L13.739 11.408L13.776 11.408ZM11.038 14.183L10.964 14.183Q11.001 14.183 11.038 14.183L11.038 14.183L10.964 14.183L11.038 14.183ZM13.739 11.445L13.739 11.519Q13.739 11.482 13.739 11.445L13.739 11.445L13.739 11.519L13.739 11.445ZM13.702 11.519L13.591 11.519Q13.665 11.519 13.702 11.519L13.702 11.519L13.591 11.519L13.702 11.519ZM11.075 14.109L11.075 14.183Q11.075 14.146 11.075 14.109L11.075 14.109L11.075 14.183L11.075 14.109ZM11.112 14.072L11.075 14.072Q11.075 14.072 11.112 14.072L11.112 14.072L11.075 14.072L11.112 14.072ZM13.480 11.667L13.480 11.778Q13.480 11.741 13.480 11.667L13.480 11.667L13.480 11.778L13.480 11.667ZM11.260 13.961L11.186 13.961Q11.223 13.961 11.260 13.961L11.260 13.961L11.186 13.961L11.260 13.961ZM16.514 6.635L16.514 6.561Q16.514 6.598 16.514 6.635L16.514 6.635L16.514 6.561L16.514 6.635ZM11.778 13.517L11.778 13.517ZM11.630 13.517L11.630 13.517ZM11.630 13.591L11.630 13.628Q11.630 13.591 11.630 13.591L11.630 13.591L11.630 13.628L11.630 13.591ZM11.519 13.628L11.519 13.628ZM11.482 13.739L11.408 13.739Q11.445 13.739 11.482 13.739L11.482 13.739L11.408 13.739L11.482 13.739ZM11.408 13.739L11.408 13.739ZM11.408 13.813L11.408 13.850Q11.408 13.813 11.408 13.813L11.408 13.813L11.408 13.850L11.408 13.813ZM11.334 13.850L11.297 13.850Q11.334 13.850 11.334 13.850L11.334 13.850L11.297 13.850L11.334 13.850ZM15.219 7.967L15.219 7.967ZM15.293 7.893L15.330 7.819Q15.330 7.856 15.330 7.856L15.330 7.856L15.293 7.893L15.330 7.819L15.293 7.893ZM15.367 7.819L15.478 7.708Q15.404 7.782 15.367 7.819L15.367 7.819L15.478 7.708L15.367 7.819ZM15.478 7.708L15.478 7.708ZM15.478 7.708L15.478 7.708ZM16.255 6.931L16.255 6.931ZM15.589 7.597L15.589 7.597ZM15.700 7.486L15.700 7.486ZM15.700 7.449L15.700 7.375Q15.700 7.375 15.700 7.375L15.700 7.375L15.700 7.449L15.700 7.375L15.700 7.449ZM15.811 7.375L15.811 7.375ZM15.885 7.301L15.922 7.264Q15.922 7.264 15.885 7.301L15.885 7.301L15.885 7.301L15.922 7.264L15.885 7.301ZM15.922 7.264L15.922 7.264ZM15.959 7.227L16.033 7.153Q16.033 7.153 16.033 7.153L16.033 7.153L15.959 7.227L16.033 7.153L15.959 7.227ZM16.033 7.153L16.033 7.153ZM16.107 7.079L16.144 7.042Q16.144 7.042 16.144 7.042L16.144 7.042L16.107 7.079L16.144 7.042L16.107 7.079ZM16.144 7.042L16.144 7.042ZM16.181 7.005L16.255 6.931Q16.218 6.968 16.181 7.005L16.181 7.005L16.255 6.931L16.181 7.005ZM14.220 10.964L14.183 10.964Q14.220 10.964 14.220 10.964L14.220 10.964L14.183 10.964L14.220 10.964ZM12.407 10.779L12.444 10.742Q12.444 10.742 12.444 10.742L12.444 10.742L12.407 10.779L12.444 10.742L12.407 10.779ZM14.405 10.742L14.405 10.742ZM14.553 10.631L14.553 10.631ZM12.629 10.557L12.703 10.483Q12.666 10.520 12.666 10.520L12.666 10.520L12.629 10.557L12.703 10.483L12.629 10.557ZM12.703 10.483L12.703 10.483ZM12.814 10.372L12.814 10.372ZM12.925 10.298L12.925 10.261Q12.925 10.298 12.925 10.298L12.925 10.298L12.925 10.261L12.925 10.298ZM13.036 10.150L13.036 10.150ZM13.036 10.150L13.036 10.150ZM13.258 9.928L13.258 9.928ZM13.295 9.891L13.369 9.817Q13.332 9.854 13.295 9.891L13.295 9.891L13.369 9.817L13.295 9.891ZM13.369 9.817L13.369 9.817ZM13.443 9.743L13.480 9.706Q13.480 9.706 13.480 9.706L13.480 9.706L13.443 9.743L13.480 9.706L13.443 9.743ZM13.480 9.706L13.480 9.706ZM13.628 9.558L13.628 9.558ZM16.514 6.672L16.514 6.672Z\"/>',\n\t'seti:lua':\n\t\t'<path d=\"M10.423 23.875L10.423 23.875Q7.649 23.875 5.274 22.488Q2.899 21.101 1.512 18.726Q0.125 16.351 0.125 13.539Q0.125 10.727 1.531 8.333Q2.937 5.939 5.293 4.609L5.293 4.609Q7.763 3.165 10.689 3.241L10.689 3.241Q13.425 3.279 15.743 4.666Q18.061 6.053 19.391 8.371L19.391 8.371Q20.759 10.765 20.759 13.558Q20.759 16.351 19.372 18.726Q17.985 21.101 15.610 22.488Q13.235 23.875 10.423 23.875ZM14.717 6.205L14.717 6.205Q13.463 6.205 12.551 7.117Q11.639 8.029 11.639 9.283Q11.639 10.537 12.513 11.449Q13.387 12.361 14.679 12.361Q15.971 12.361 16.883 11.487Q17.795 10.613 17.795 9.321Q17.795 8.029 16.902 7.117Q16.009 6.205 14.717 6.205ZM20.759 6.205L20.759 6.205Q19.505 6.205 18.612 5.312Q17.719 4.419 17.795 3.127L17.795 3.127Q17.795 1.873 18.669 0.999Q19.543 0.125 20.873 0.125L20.873 0.125Q22.127 0.125 23.001 1.056Q23.875 1.987 23.875 3.222Q23.875 4.457 22.944 5.331Q22.013 6.205 20.759 6.205Z\"/>',\n\t'seti:markdown':\n\t\t'<path d=\"M16.660 1.240L17.940 0.360L17.940 12.760L22.700 12.760Q19.060 16.480 11.820 23.760L11.820 23.760L1.300 12.880L5.700 12.880L5.700 0.240L6.180 0.600Q8.380 2.120 9.500 2.840L9.500 2.840Q11.500 4.120 11.920 4.120Q12.340 4.120 13.860 3.120L13.860 3.120Q14.780 2.560 16.660 1.240L16.660 1.240Z\"/>',\n\t'seti:argdown':\n\t\t'<path d=\"M17.421 0.261L17.421 12.780L21.750 12.780L12 23.739L2.250 12.780L6.579 12.780L6.579 0.261L10.245 2.562L10.245 16.875L12 18.864L13.755 16.875L13.755 2.562L17.421 0.261Z\"/>',\n\t'seti:info':\n\t\t'<path d=\"M23.780 10.803L23.818 10.803Q23.628 8.029 21.918 5.331L21.918 5.331Q20.664 3.469 18.916 2.234Q17.168 0.999 15.002 0.467L15.002 0.467Q13.748 0.125 12.646 0.125L12.646 0.125L10.746 0.125Q7.326 0.467 4.438 2.595L4.438 2.595Q1.132 5.369 0.296 9.245L0.296 9.245Q0.068 10.423 0.068 11.145L0.068 11.145L0.068 13.045Q0.448 16.351 2.082 18.631L2.082 18.631Q4.172 21.709 7.288 22.925L7.288 22.925Q9.454 23.685 11.202 23.875L11.202 23.875L13.102 23.875Q17.434 23.495 20.474 20.303L20.474 20.303Q22.944 17.833 23.666 14.375L23.666 14.375Q23.742 14.071 23.799 13.539Q23.856 13.007 23.932 12.703L23.932 12.703L23.932 11.411Q23.780 11.145 23.780 10.803L23.780 10.803ZM11.924 21.975L11.924 21.975Q9.188 21.975 6.870 20.569L6.870 20.569Q4.590 19.239 3.279 16.921Q1.968 14.603 1.968 11.867Q1.968 9.131 3.317 6.813Q4.666 4.495 6.984 3.165L6.984 3.165Q9.378 1.759 12.152 1.759L12.152 1.759Q14.850 1.835 17.149 3.184Q19.448 4.533 20.778 6.813L20.778 6.813Q22.146 9.131 22.108 11.867Q22.070 14.603 20.683 16.921Q19.296 19.239 17.016 20.569L17.016 20.569Q14.660 21.975 11.924 21.975ZM15.496 18.289L14.774 18.289Q14.432 18.289 14.166 18.175L14.166 18.175Q14.014 18.175 13.900 17.947L13.900 17.947Q13.862 17.833 13.824 17.795L13.824 17.795L13.824 10.081Q12.874 10.157 11.031 10.214Q9.188 10.271 8.238 10.309L8.238 10.309L8.238 11.259L9.416 11.259Q9.758 11.259 9.948 11.487Q10.138 11.715 10.138 12.095L10.138 12.095L10.138 17.567Q10.138 18.289 9.416 18.289L9.416 18.289L8.352 18.289L8.352 19.239L15.496 19.239L15.496 18.289ZM11.696 8.675L11.696 8.675Q12.570 8.675 13.140 8.067Q13.710 7.459 13.710 6.642Q13.710 5.825 13.102 5.217Q12.494 4.609 11.658 4.609Q10.822 4.609 10.252 5.217Q9.682 5.825 9.682 6.642Q9.682 7.459 10.290 8.067Q10.898 8.675 11.696 8.675Z\"/>',\n\t'seti:clock':\n\t\t'<path d=\"M11.328 0.198L11.328 0.198L12.924 0.198Q13.260 0.240 13.848 0.303Q14.436 0.366 14.772 0.450L14.772 0.450Q17.124 1.038 19.098 2.424L19.098 2.424Q20.694 3.642 21.576 5.028L21.576 5.028Q23.298 7.296 23.676 10.278L23.676 10.278Q23.676 10.404 23.739 10.698Q23.802 10.992 23.802 11.202L23.802 11.202L23.802 12.924Q23.760 13.176 23.697 13.659Q23.634 14.142 23.550 14.352L23.550 14.352Q22.920 16.914 21.597 18.804Q20.274 20.694 18.048 22.122L18.048 22.122Q16.788 22.794 15.822 23.130L15.822 23.130Q14.604 23.592 13.428 23.676L13.428 23.676Q13.302 23.676 13.050 23.739Q12.798 23.802 12.672 23.802L12.672 23.802L10.950 23.802Q10.698 23.760 10.215 23.697Q9.732 23.634 9.522 23.550L9.522 23.550Q6.960 22.920 5.070 21.597Q3.180 20.274 1.752 18.048L1.752 18.048Q1.080 16.788 0.744 15.822L0.744 15.822Q0.282 14.604 0.198 13.428L0.198 13.428Q0.282 13.344 0.240 13.071Q0.198 12.798 0.198 12.672L0.198 12.672L0.198 11.076Q0.240 10.740 0.303 10.152Q0.366 9.564 0.450 9.228L0.450 9.228Q1.038 6.876 2.424 4.902L2.424 4.902Q3.642 3.138 5.952 1.752L5.952 1.752Q8.052 0.492 10.278 0.324L10.278 0.324Q10.488 0.324 10.824 0.261Q11.160 0.198 11.328 0.198ZM2.172 12.252L2.172 12.252Q2.214 14.898 3.579 17.124Q4.944 19.350 7.212 20.673Q9.480 21.996 12.168 21.975Q14.856 21.954 17.145 20.589Q19.434 19.224 20.736 16.872Q22.038 14.520 21.996 11.748L21.996 11.748Q21.912 9.144 20.526 6.918Q19.140 4.692 16.872 3.432L16.872 3.432Q14.478 2.088 11.748 2.172L11.748 2.172Q9.144 2.214 6.918 3.558Q4.692 4.902 3.432 7.170L3.432 7.170Q2.088 9.522 2.172 12.252ZM12.546 11.958L12.546 12Q12.672 11.874 12.987 11.685Q13.302 11.496 13.428 11.328L13.428 11.328Q15.822 9.774 16.872 8.976L16.872 8.976L16.998 8.892Q17.124 8.808 17.124 8.724L17.124 8.724Q17.292 8.514 17.565 8.577Q17.838 8.640 18.027 8.829Q18.216 9.018 18.174 9.291Q18.132 9.564 17.922 9.774L17.922 9.774L15.780 11.244Q14.352 12.210 13.722 12.798L13.722 12.798L12.252 13.848Q11.958 14.016 11.643 13.890Q11.328 13.764 11.328 13.428L11.328 13.428L11.328 4.902Q11.328 4.272 11.874 4.272L11.874 4.272Q12.084 4.146 12.315 4.314Q12.546 4.482 12.546 4.776L12.546 4.776L12.546 12L12.546 11.958Z\"/>',\n\t'seti:maven':\n\t\t'<path d=\"M12.464 0.865L12.226 1.001Q11.818 1.341 11.138 1.851L11.138 1.851Q9.982 2.769 9.540 3.177L9.540 3.177Q8.894 3.789 8.316 4.605L8.316 4.605Q7.772 5.387 7.636 6.849Q7.500 8.311 7.908 9.671L7.908 9.671Q8.384 11.201 9.370 11.949L9.370 11.949Q9.030 11.949 7.840 11.303L7.840 11.303L6.718 10.657L8.282 12.833Q10.016 15.145 10.764 15.859L10.764 15.859Q13.280 19.531 13.926 23.747L13.926 23.747Q14.402 20.245 12.668 17.049L12.668 17.049L12.668 16.947Q12.566 16.505 12.685 16.182Q12.804 15.859 13.178 15.553L13.178 15.553Q13.722 15.247 15.966 13.105L15.966 13.105L15.966 13.105L14.708 13.819Q13.382 14.533 12.974 14.601L12.974 14.601L13.756 13.785Q14.334 13.173 14.606 12.901L14.606 12.901Q15.082 12.459 15.524 12.153L15.524 12.153Q17.598 10.487 17.224 7.699L17.224 7.699Q17.224 7.359 17.054 6.747L17.054 6.747L17.020 6.543Q16.238 3.143 13.178 0.253L13.178 0.253Q12.940 0.559 12.464 0.865L12.464 0.865Z\"/>',\n\t'seti:nim':\n\t\t'<path d=\"M13.806 5.334L12.086 3.829L10.237 5.291Q9.463 5.248 8.345 5.420L8.345 5.420Q7.098 5.592 6.367 5.850L6.367 5.850Q5.808 5.463 5.163 4.990L5.163 4.990L4.647 4.603L3.486 6.495Q2.282 7.140 1.594 7.785L1.594 7.785L0.089 7.140L0.562 8.129Q1.207 9.591 1.637 10.236L1.637 10.236Q2.325 11.311 3.228 11.913L3.228 11.913Q4.174 10.408 6.711 9.634L6.711 9.634Q9.033 8.903 11.979 8.946Q14.924 8.989 17.289 9.720L17.289 9.720Q19.826 10.537 20.815 11.870L20.815 11.870Q21.890 11.311 22.707 9.978L22.707 9.978Q23.137 9.204 23.825 7.441L23.825 7.441L23.911 7.226Q23.051 7.527 22.277 7.785L22.277 7.785Q22.019 7.527 21.524 7.140Q21.030 6.753 20.600 6.495L20.600 6.495L19.482 4.560L17.762 5.764Q15.053 5.248 13.806 5.334L13.806 5.334ZM1.035 11.053L1.035 11.053L3.185 16.299Q4.561 18.105 6.926 19.137L6.926 19.137Q9.205 20.126 11.849 20.169Q14.494 20.212 16.816 19.266L16.816 19.266Q19.267 18.277 20.772 16.385L20.772 16.385L23.137 11.010Q21.890 12.816 19.224 14.321L19.224 14.321Q18.751 14.579 17.676 14.794L17.676 14.794L16.687 14.966L12.043 12.558L7.356 14.923L6.367 14.751Q5.292 14.493 4.819 14.278L4.819 14.278Q3.658 13.676 2.755 12.859L2.755 12.859Q1.895 12.128 1.035 11.053L1.035 11.053Z\"/>',\n\t'seti:github':\n\t\t'<path d=\"M14.250 17.360L14.250 17.360Q14.516 17.284 14.972 17.227Q15.428 17.170 15.694 17.132L15.694 17.132Q18.088 16.524 18.886 14.738L18.886 14.738Q19.912 12.686 19.380 10.330L19.380 10.330Q19.266 9.874 19.038 9.418L19.038 9.418Q18.848 9.114 18.430 8.582L18.430 8.582Q18.278 8.430 18.278 8.202L18.278 8.202Q18.658 6.758 18.164 5.466L18.164 5.466Q18.164 5.390 18.069 5.314Q17.974 5.238 17.822 5.238L17.822 5.238Q17.442 5.238 17.024 5.390L17.024 5.390Q16.758 5.466 16.283 5.713Q15.808 5.960 15.086 6.416L15.086 6.416Q14.972 6.530 14.744 6.530L14.744 6.530Q11.932 5.846 9.158 6.530L9.158 6.530Q8.930 6.530 8.778 6.416L8.778 6.416L8.512 6.264Q7.790 5.846 7.448 5.694L7.448 5.694Q6.878 5.428 6.308 5.352L6.308 5.352Q5.852 5.276 5.757 5.333Q5.662 5.390 5.586 5.846L5.586 5.846Q5.244 7.062 5.586 8.316L5.586 8.316L5.586 8.544Q4.826 9.418 4.560 10.558L4.560 10.558Q4.332 11.584 4.522 12.724L4.522 12.724Q4.598 12.952 4.655 13.370Q4.712 13.788 4.750 14.016L4.750 14.016Q5.206 15.270 6.080 16.011Q6.954 16.752 8.322 17.132L8.322 17.132Q9.272 17.360 9.880 17.474L9.880 17.474Q9.386 17.930 9.158 18.880L9.158 18.880Q9.158 18.994 9.044 19.032L9.044 19.032L9.044 19.032Q8.056 19.412 7.220 19.222L7.220 19.222Q6.308 18.994 5.700 18.082L5.700 18.082Q5.092 17.018 4.028 16.866L4.028 16.866L3.458 16.866Q3.268 16.866 3.249 16.999Q3.230 17.132 3.344 17.246L3.344 17.246L3.572 17.474Q4.522 18.044 4.864 19.146L4.864 19.146Q5.244 19.906 5.852 20.305Q6.460 20.704 7.372 20.780L7.372 20.780Q8.436 20.780 9.044 20.666L9.044 20.666L9.044 22.946Q9.044 23.212 8.797 23.345Q8.550 23.478 8.208 23.402L8.208 23.402Q6.764 22.870 5.358 21.996L5.358 21.996Q2.622 20.096 1.330 17.512L1.330 17.512Q-0.038 14.890 0.114 11.660L0.114 11.660Q0.228 8.962 1.520 6.663Q2.812 4.364 4.959 2.806Q7.106 1.248 9.728 0.716L9.728 0.716Q12.768 0.146 15.694 1.096L15.694 1.096Q18.506 2.046 20.596 4.231Q22.686 6.416 23.522 9.380L23.522 9.380Q24.282 12.306 23.522 15.232L23.522 15.232Q22.762 18.082 20.748 20.267Q18.734 22.452 15.922 23.402L15.922 23.402Q15.466 23.592 15.219 23.402Q14.972 23.212 14.972 22.680L14.972 22.680L14.972 19.830Q15.086 19.070 14.934 18.500L14.934 18.500Q14.782 17.854 14.250 17.360Z\"/>',\n\t'seti:notebook':\n\t\t'<path d=\"M4.314 0.198L16.746 0.198Q17.838 0.198 18.615 0.975Q19.392 1.752 19.392 2.844L19.392 2.844L19.392 21.156Q19.392 22.248 18.615 23.025Q17.838 23.802 16.746 23.802L16.746 23.802L4.314 23.802Q3.222 23.802 2.445 23.025Q1.668 22.248 1.668 21.156L1.668 21.156L1.668 2.844Q1.668 1.752 2.445 0.975Q3.222 0.198 4.314 0.198L4.314 0.198ZM21.450 15.528L20.568 15.528L21.450 15.528Q21.786 15.528 22.038 15.759Q22.290 15.990 22.332 16.326L22.332 16.326L22.332 18.216Q22.332 18.552 22.122 18.783Q21.912 19.014 21.576 19.098L21.576 19.098L20.568 19.098L20.568 15.528L21.450 15.528ZM21.450 10.824L20.568 10.824L21.450 10.824Q21.786 10.824 22.038 11.034Q22.290 11.244 22.332 11.580L22.332 11.580L22.332 13.470Q22.332 13.806 22.122 14.058Q21.912 14.310 21.576 14.352L21.576 14.352L20.568 14.352L20.568 10.824L21.450 10.824ZM21.450 6.078L20.568 6.078L21.450 6.078Q21.786 6.078 22.038 6.309Q22.290 6.540 22.332 6.876L22.332 6.876L22.332 8.766Q22.332 9.102 22.122 9.333Q21.912 9.564 21.576 9.648L21.576 9.648L20.568 9.648L20.568 6.078L21.450 6.078ZM14.352 4.314L14.352 4.314L6.708 4.314Q6.372 4.314 6.120 4.545Q5.868 4.776 5.826 5.070L5.826 5.070L5.784 7.002Q5.784 7.296 6.015 7.548Q6.246 7.800 6.582 7.842L6.582 7.842L14.352 7.884Q14.688 7.884 14.940 7.653Q15.192 7.422 15.234 7.086L15.234 7.086L15.276 5.196Q15.276 4.818 15.003 4.566Q14.730 4.314 14.352 4.314Z\"/>',\n\t'seti:nunjucks':\n\t\t'<path d=\"M23.800 21.475L19.600 6.625L17.700 7.225Q17.050 5.075 16.050 3.425L16.050 3.425Q14.400 0.825 12.400 0.825Q10.400 0.825 8.700 3.525L8.700 3.525Q7.650 5.125 6.950 7.225L6.950 7.225L4.750 6.625L0.200 21.275L5.850 23.025L10.400 8.325L8.350 7.725Q8.800 6.025 9.850 4.425L9.850 4.425Q11.100 2.375 12.250 2.375Q13.400 2.375 14.600 4.425L14.600 4.425Q15.550 5.975 16.000 7.725L16.000 7.725L13.650 8.475L18.200 23.175L23.800 21.475Z\"/>',\n\t'seti:npm':\n\t\t'<path d=\"M24 7.296L0 7.296L0 15.296L6.816 15.296L6.816 16.704L12.096 16.704L12.096 15.392L24 15.392L24 7.296ZM6.592 8.704L6.592 13.984L5.312 13.984L5.312 10.112L4 10.112L4 13.984L1.312 13.984L1.312 8.704L6.592 8.704ZM13.184 13.984L13.216 13.984L10.496 13.984L10.496 15.392L7.808 15.392L7.808 8.800L13.088 8.800Q13.216 10.400 13.184 13.984L13.184 13.984ZM22.592 8.704L22.592 13.984L21.312 13.984L21.312 10.112L20 10.112L20 13.984L18.592 13.984L18.592 10.112L17.312 10.112L17.312 13.984L14.592 13.984L14.592 8.704L22.592 8.704ZM11.904 12.704L11.904 10.112L10.592 10.112L10.592 12.704L11.904 12.704Z\"/>',\n\t'seti:ocaml':\n\t\t'<path d=\"M23.306 21.102L23.343 21.028L23.343 5.414Q23.343 4.489 23.195 4.008L23.195 4.008Q23.047 3.083 22.048 2.306L22.048 2.306Q21.715 2.047 21.530 1.973L21.530 1.973Q21.197 1.825 20.901 1.825L20.901 1.825Q20.827 1.825 20.808 1.806Q20.790 1.788 20.790 1.714L20.790 1.714L2.623 1.714Q2.623 1.936 2.179 1.936L2.179 1.936Q0.884 2.417 0.329 3.897L0.329 3.897Q0.218 4.156 0.218 4.600L0.218 4.600L0.218 9.928Q0.218 10.187 0.255 10.206Q0.292 10.224 0.551 10.150L0.551 10.150L1.143 9.928Q1.439 9.854 1.735 9.373L1.735 9.373Q1.846 9.188 1.920 9.114L1.920 9.114L2.031 8.966Q2.327 8.485 2.623 8.300L2.623 8.300Q2.808 8.115 2.993 8.115Q3.178 8.115 3.437 8.300L3.437 8.300L3.696 8.485Q4.029 8.707 4.251 8.781L4.251 8.781Q4.991 9.003 5.398 8.892L5.398 8.892Q5.620 8.892 5.731 8.707L5.731 8.707Q5.842 8.633 5.953 8.411L5.953 8.411L5.990 8.300Q6.212 7.856 6.360 7.745Q6.508 7.634 6.619 7.634L6.619 7.634L7.026 7.597Q7.470 7.671 7.914 8.078L7.914 8.078Q8.173 8.300 8.654 8.892L8.654 8.892Q8.802 9.188 9.098 9.336L9.098 9.336Q9.320 9.447 9.801 9.928L9.801 9.928Q10.134 10.335 10.948 10.964L10.948 10.964Q11.503 11.408 11.762 11.667L11.762 11.667Q12.391 12.259 13.168 12.592L13.168 12.592Q13.760 12.777 14.130 12.740L14.130 12.740Q14.611 12.666 15.018 12.111L15.018 12.111Q15.758 11.186 15.943 10.150L15.943 10.150Q16.017 9.928 16.313 9.521L16.313 9.521L16.498 9.225L16.609 9.225Q17.275 9.114 18.237 9.336L18.237 9.336Q18.570 9.336 18.829 9.447L18.829 9.447L19.273 9.558Q19.902 9.669 20.216 9.817Q20.531 9.965 20.642 10.279Q20.753 10.594 20.679 10.964L20.679 10.964Q20.605 11.186 20.549 11.223Q20.494 11.260 20.383 11.223Q20.272 11.186 20.198 11.186L20.198 11.186L19.865 11.186Q19.384 11.297 18.459 11.778L18.459 11.778Q18.274 11.852 18.144 12.018Q18.015 12.185 18.015 12.370L18.015 12.370Q17.830 13.036 17.312 13.406L17.312 13.406L17.238 13.480Q17.090 13.554 17.090 13.739L17.090 13.739Q16.979 13.813 16.849 13.980Q16.720 14.146 16.609 14.220L16.609 14.220Q15.536 15.293 13.612 15.478L13.612 15.478L11.762 15.478Q11.614 15.478 11.577 15.534Q11.540 15.589 11.540 15.700L11.540 15.700Q11.577 15.922 11.577 16.384Q11.577 16.847 11.669 17.087Q11.762 17.328 11.762 17.772L11.762 17.772Q11.762 18.475 12.354 19.067L12.354 19.067Q12.465 19.178 12.465 19.400L12.465 19.400Q12.465 20.214 12.576 20.547L12.576 20.547L12.613 20.806Q12.835 21.694 13.020 22.064L13.020 22.064Q13.168 22.175 13.242 22.212Q13.316 22.249 13.408 22.194Q13.501 22.138 13.723 22.064Q13.945 21.990 14.093 21.953L14.093 21.953Q14.463 21.805 15.018 21.731L15.018 21.731Q15.351 21.731 16.054 21.731L16.054 21.731Q16.424 21.731 17.201 21.694L17.201 21.694Q18.163 21.620 18.644 21.657L18.644 21.657Q19.421 21.657 20.087 21.842L20.087 21.842Q20.309 21.879 20.716 21.934Q21.123 21.990 21.345 22.064L21.345 22.064Q21.826 22.212 22.344 22.249L22.344 22.249Q22.677 22.286 23.343 22.286L23.343 22.286Q23.787 22.286 23.787 21.842L23.787 21.842Q23.454 21.583 23.380 21.435Q23.306 21.287 23.306 21.102L23.306 21.102ZM5.620 18.031L5.620 17.772L5.768 17.661Q5.953 16.662 6.545 16.033L6.545 16.033L6.434 16.033Q5.361 16.033 4.140 15.700L4.140 15.700Q2.993 15.478 2.401 14.886L2.401 14.886Q2.142 14.627 1.827 14.701Q1.513 14.775 1.365 15.145L1.365 15.145Q1.365 15.182 1.309 15.348Q1.254 15.515 1.254 15.589L1.254 15.589Q0.884 16.292 0.440 16.736L0.440 16.736Q0.181 17.217 0.218 17.550L0.218 17.550L0.218 19.733Q0.218 20.103 0.551 20.140Q0.884 20.177 1.439 20.362L1.439 20.362Q1.846 20.510 2.031 20.547L2.031 20.547L2.586 20.732Q3.622 21.139 4.140 21.139L4.140 21.139Q4.473 21.139 4.547 21.084Q4.621 21.028 4.695 20.658L4.695 20.658Q4.695 20.621 4.769 20.566Q4.843 20.510 4.843 20.436L4.843 20.436L4.695 20.436L4.843 20.436L4.843 20.214L4.843 20.325Q4.843 20.288 4.898 20.214Q4.954 20.140 4.954 20.103L4.954 20.103L4.954 19.992Q5.065 19.733 5.065 19.400L5.065 19.400Q5.065 18.956 5.176 18.808L5.176 18.808L5.176 18.697Q5.361 18.623 5.583 18.179L5.583 18.179L5.620 18.031ZM11.429 19.992L11.429 19.992Q11.133 19.696 10.948 19.178L10.948 19.178Q10.948 18.808 10.837 18.808L10.837 18.808Q10.837 18.549 10.689 18.105L10.689 18.105L10.615 17.883Q10.245 16.699 9.209 15.811L9.209 15.811Q8.765 15.552 8.524 15.571Q8.284 15.589 7.951 15.922L7.951 15.922Q7.877 15.996 7.821 16.107Q7.766 16.218 7.729 16.292L7.729 16.292Q7.026 17.439 6.693 18.142L6.693 18.142L6.619 18.253Q6.434 18.549 6.434 18.845L6.434 18.845Q6.360 18.882 6.304 19.085Q6.249 19.289 6.212 19.400L6.212 19.400Q5.916 19.696 5.546 20.510L5.546 20.510L5.398 20.806Q5.398 20.843 5.361 20.898Q5.324 20.954 5.398 21.028L5.398 21.028L5.620 21.028Q6.545 20.806 7.026 20.547L7.026 20.547Q8.025 20.066 9.209 20.436L9.209 20.436Q9.690 20.621 10.134 20.880L10.134 20.880Q10.430 21.065 10.948 21.472L10.948 21.472Q11.318 21.731 11.984 21.953L11.984 21.953L12.243 21.953L12.243 21.842Q12.058 21.509 11.873 20.954L11.873 20.954Q11.614 20.288 11.429 19.992Z\"/>',\n\t'seti:odata':\n\t\t'<path d=\"M20.586 0.324L20.586 0.324L3.534 0.324Q2.148 0.324 1.203 1.269Q0.258 2.214 0.258 3.600L0.258 3.600L0.258 20.400Q0.258 21.786 1.203 22.731Q2.148 23.676 3.534 23.676L3.534 23.676L20.460 23.676Q21.846 23.676 22.791 22.731Q23.736 21.786 23.736 20.400L23.736 20.400L23.736 3.600Q23.820 2.214 22.896 1.269Q21.972 0.324 20.586 0.324ZM6.684 21.072L6.684 21.072Q5.298 21.072 4.290 20.043Q3.282 19.014 3.282 17.649Q3.282 16.284 4.290 15.255Q5.298 14.226 6.684 14.226Q8.070 14.226 9.078 15.255Q10.086 16.284 10.086 17.670Q10.086 19.056 9.099 20.064Q8.112 21.072 6.684 21.072ZM11.682 12.504L11.808 12.504L3.408 12.504L3.408 10.278L11.682 10.278L11.682 12.504ZM11.682 9.228L11.808 9.228L3.408 9.228L3.408 7.002L11.682 7.002L11.682 9.228ZM11.682 5.952L11.808 5.952L3.408 5.952L3.408 3.852L11.682 3.852L11.682 5.952ZM20.712 13.302L20.712 15.528L12.438 15.528L12.438 13.302L20.712 13.302ZM20.712 10.152L20.712 12.378L12.438 12.378L12.438 10.152L20.712 10.152ZM20.712 6.876L20.712 9.102L12.438 9.102L12.438 6.876L20.712 6.876ZM20.712 3.726L20.712 5.952L12.438 5.952L12.438 3.726L20.712 3.726Z\"/>',\n\t'seti:perl':\n\t\t'<path d=\"M9.194 22.393L8.966 23.875L7.180 23.875Q7.218 23.723 7.275 23.362Q7.332 23.001 7.408 22.925L7.408 22.925Q8.054 22.697 8.206 22.165L8.206 22.165Q8.320 21.861 8.282 21.101L8.282 21.101L8.244 20.569L8.244 13.197Q7.826 13.121 6.952 13.045L6.952 13.045Q6.268 12.969 5.926 12.931L5.926 12.931Q5.394 12.817 5.052 12.627L5.052 12.627Q3.114 11.373 2.563 9.454Q2.012 7.535 2.886 5.369L2.886 5.369Q3.494 4.001 2.050 3.811L2.050 3.811Q1.822 3.811 1.100 3.583L1.100 3.583L1.100 3.355Q1.480 3.165 2.126 2.747Q2.772 2.329 3.152 2.177L3.152 2.177Q3.228 2.101 3.494 2.101L3.494 2.101Q3.950 2.063 4.102 1.911L4.102 1.911Q5.090 1.417 5.584 1.341Q6.078 1.265 6.458 1.626Q6.838 1.987 7.408 2.975L7.408 2.975Q7.826 3.849 7.788 4.685Q7.750 5.521 7.294 6.433L7.294 6.433Q6.914 7.079 6.876 7.421L6.876 7.421Q6.800 7.991 7.408 8.219L7.408 8.219L8.168 7.155Q9.536 5.179 10.296 4.267L10.296 4.267Q11.512 2.747 12.766 1.835L12.766 1.835Q14.248 0.695 15.958 0.125L15.958 0.125L16.794 0.125Q17.022 0.467 17.592 1.075L17.592 1.075Q18.428 1.949 18.694 2.519L18.694 2.519Q19.302 3.507 20.366 5.559L20.366 5.559Q21.544 7.839 22.266 8.941L22.266 8.941Q22.722 9.625 22.855 10.689Q22.988 11.753 22.722 12.722Q22.456 13.691 21.886 14.147L21.886 14.147L21.886 10.005L21.430 10.005Q20.898 10.765 20.822 11.791L20.822 11.791Q20.746 12.399 20.860 13.691L20.860 13.691Q20.936 14.641 20.936 15.097L20.936 15.097Q20.936 16.617 20.898 19.581L20.898 19.581Q20.822 22.469 20.822 23.875L20.822 23.875L19.986 23.875L19.986 23.571Q19.910 23.191 19.986 22.925L19.986 22.925Q20.708 17.491 18.694 12.019L18.694 12.019Q17.212 13.349 17.022 15.591L17.022 15.591Q16.946 16.313 16.718 17.187L16.718 17.187Q16.566 17.719 16.186 18.783L16.186 18.783Q16.110 19.011 15.882 19.429L15.882 19.429Q15.540 20.113 15.502 20.455L15.502 20.455Q15.464 21.025 16.072 21.519L16.072 21.519Q16.148 21.595 16.053 22.070Q15.958 22.545 15.958 22.811L15.958 22.811L15.502 22.241Q15.160 21.785 15.008 21.633L15.008 21.633L15.008 22.013Q14.590 22.127 13.697 22.298Q12.804 22.469 12.386 22.583L12.386 22.583Q12.386 22.545 12.329 22.355Q12.272 22.165 12.272 22.127L12.272 22.127Q12.766 21.861 12.994 21.633L12.994 21.633Q13.868 21.101 13.963 20.550Q14.058 19.999 13.336 19.391L13.336 19.391Q11.018 17.567 12.044 14.983L12.044 14.983Q12.196 14.679 12.234 13.919L12.234 13.919Q12.234 13.349 12.272 12.969L12.272 12.969L11.588 13.349Q11.094 13.577 10.942 13.691L10.942 13.691Q10.676 13.881 10.638 14.033L10.638 14.033Q10.182 15.705 9.726 19.201L9.726 19.201L9.688 19.505Q9.422 20.569 9.194 22.393L9.194 22.393ZM12.994 17.605L14.438 19.391Q15.654 17.795 15.844 16.123Q16.034 14.451 15.122 13.197L15.122 13.197Q13.716 16.047 12.994 17.605L12.994 17.605Z\"/>',\n\t'seti:php':\n\t\t'<path d=\"M8.154 16.514L8.154 16.514Q7.964 15.298 6.976 14.956L6.976 14.956L6.140 14.462Q5.418 14.234 5.190 14.120L5.190 14.120Q4.848 13.968 4.468 14.348L4.468 14.348Q4.316 14.690 4.582 14.956L4.582 14.956Q4.696 15.184 5.076 15.564L5.076 15.564Q5.304 15.906 5.912 16.514L5.912 16.514L5.988 16.628Q6.178 16.894 6.216 17.046L6.216 17.046Q6.330 17.274 6.254 17.578L6.254 17.578Q6.064 18.946 5.190 19.820L5.190 19.820Q5.114 19.896 5.000 19.934L5.000 19.934L4.962 19.934Q4.696 19.934 4.373 19.801Q4.050 19.668 3.860 19.478L3.860 19.478Q3.632 19.212 3.670 18.965Q3.708 18.718 4.012 18.528L4.012 18.528Q4.088 18.528 4.164 18.452Q4.240 18.376 4.240 18.262L4.240 18.262Q4.620 17.882 4.468 17.464L4.468 17.464Q4.392 17.388 4.335 17.274Q4.278 17.160 4.240 17.084L4.240 17.084Q3.746 16.628 2.568 15.678L2.568 15.678Q0.364 13.968 0.212 11.384L0.212 11.384Q0.022 8.648 1.504 6.064L1.504 6.064Q2.150 4.962 3.062 4.506L3.062 4.506Q3.518 4.278 4.468 4.012L4.468 4.012Q6.862 3.366 8.610 3.670L8.610 3.670Q10.624 4.088 11.118 5.684L11.118 5.684Q11.498 7.052 11.346 8.078L11.346 8.078Q11.270 9.294 10.415 10.282Q9.560 11.270 8.382 11.612L8.382 11.612Q7.850 11.802 7.546 11.707Q7.242 11.612 6.976 11.156L6.976 11.156L6.786 10.890Q6.330 10.206 6.140 9.864L6.140 9.864L6.140 9.712Q6.140 9.674 6.083 9.617Q6.026 9.560 6.026 9.484L6.026 9.484Q6.140 10.206 6.216 10.548L6.216 10.548Q6.368 11.118 6.596 11.498L6.596 11.498Q6.824 11.802 7.033 11.954Q7.242 12.106 7.546 12.106L7.546 12.106Q9.864 12.296 11.232 10.548L11.232 10.548Q12.562 8.724 12.182 6.064L12.182 6.064Q12.182 5.760 12.030 5.304L12.030 5.304L11.954 5.114Q11.954 4.886 11.992 4.791Q12.030 4.696 12.182 4.620L12.182 4.620Q12.410 4.620 12.410 4.506L12.410 4.506Q14.462 4.088 16.096 4.278L16.096 4.278Q18.452 4.468 20.010 5.570L20.010 5.570Q22.746 7.280 23.696 10.662L23.696 10.662Q23.810 11.042 23.810 11.612L23.810 11.612Q23.810 11.802 23.734 11.821Q23.658 11.840 23.487 11.745Q23.316 11.650 23.088 11.422L23.088 11.422L23.012 11.270Q22.936 11.232 22.822 11.042Q22.708 10.852 22.632 10.776Q22.556 10.700 22.480 10.700Q22.404 10.700 22.404 10.776Q22.404 10.852 22.347 10.966Q22.290 11.080 22.290 11.156L22.290 11.156Q21.682 13.208 20.618 14.234L20.618 14.234Q20.542 14.348 20.466 14.481Q20.390 14.614 20.390 14.728L20.390 14.728L20.352 15.184Q20.314 15.830 20.390 16.134L20.390 16.134L20.732 19.098Q20.808 19.516 20.675 19.744Q20.542 19.972 20.124 20.162L20.124 20.162Q19.440 20.428 18.946 20.428L18.946 20.428L17.046 20.428Q17.046 19.706 16.970 19.402L16.970 19.402Q16.856 18.870 16.476 18.642L16.476 18.642Q16.704 17.692 16.837 17.084Q16.970 16.476 16.590 15.906L16.590 15.906Q16.286 15.450 15.640 15.678L15.640 15.678Q14.348 16.476 12.676 15.906L12.676 15.906Q12.258 15.792 12.011 15.868Q11.764 15.944 11.612 16.248L11.612 16.248Q11.232 16.970 11.232 17.692Q11.232 18.414 11.460 19.592L11.460 19.592Q11.460 19.896 11.422 19.991Q11.384 20.086 11.118 20.162L11.118 20.162Q9.978 20.504 8.762 20.314L8.762 20.314L8.610 20.314Q8.610 19.592 8.534 19.288L8.534 19.288Q8.344 18.756 7.812 18.528L7.812 18.528Q8.382 17.920 8.154 16.514ZM3.290 13.778L3.290 13.778Q3.518 13.512 3.518 13.284L3.518 13.284Q3.594 13.018 3.404 12.676Q3.214 12.334 2.910 12.220L2.910 12.220Q2.758 12.144 2.663 12.182Q2.568 12.220 2.454 12.334L2.454 12.334Q2.264 12.524 1.979 12.885Q1.694 13.246 1.504 13.398L1.504 13.398Q1.504 13.474 1.447 13.531Q1.390 13.588 1.390 13.664L1.390 13.664Q1.276 13.816 1.352 13.968Q1.428 14.120 1.618 14.120L1.618 14.120Q1.960 14.120 2.112 14.234L2.112 14.234Q2.796 14.234 3.290 13.778ZM3.290 9.864L3.290 9.864Q3.290 9.674 3.138 9.522Q2.986 9.370 2.796 9.370Q2.606 9.370 2.435 9.541Q2.264 9.712 2.340 9.978L2.340 9.978Q2.340 10.168 2.511 10.301Q2.682 10.434 2.910 10.434L2.910 10.434Q3.290 10.092 3.290 9.864Z\"/>',\n\t'seti:pipeline':\n\t\t'<path d=\"M15.948 15.948L15.948 18.888L7.926 18.888Q7.800 18.384 7.506 17.922L7.506 17.922L17.922 7.506Q18.804 8.052 19.896 8.052L19.896 8.052Q21.408 8.052 22.542 7.023Q23.676 5.994 23.802 4.461Q23.928 2.928 22.983 1.710Q22.038 0.492 20.526 0.240Q19.014-0.012 17.733 0.828Q16.452 1.668 16.074 3.138L16.074 3.138L7.926 3.138Q7.548 1.710 6.330 0.870Q5.112 0.030 3.621 0.219Q2.130 0.408 1.164 1.521Q0.198 2.634 0.198 4.125Q0.198 5.616 1.164 6.729Q2.130 7.842 3.621 8.031Q5.112 8.220 6.330 7.380Q7.548 6.540 7.926 5.112L7.926 5.112L16.074 5.112Q16.200 5.616 16.494 6.078L16.494 6.078L6.078 16.494Q5.196 15.948 4.146 15.948L4.146 15.948Q2.592 15.948 1.458 16.977Q0.324 18.006 0.198 19.539Q0.072 21.072 1.017 22.290Q1.962 23.508 3.474 23.760Q4.986 24.012 6.267 23.172Q7.548 22.332 7.926 20.862L7.926 20.862L15.948 20.862L15.948 23.802L23.802 23.802L23.802 15.948L15.948 15.948ZM4.146 6.078L4.146 6.078Q3.432 6.078 2.886 5.637Q2.340 5.196 2.214 4.503Q2.088 3.810 2.403 3.201Q2.718 2.592 3.369 2.319Q4.020 2.046 4.692 2.256Q5.364 2.466 5.763 3.033Q6.162 3.600 6.099 4.314Q6.036 5.028 5.532 5.532L5.532 5.532Q4.944 6.078 4.146 6.078ZM19.854 2.172L19.854 2.172Q20.484 2.172 20.967 2.487Q21.450 2.802 21.681 3.369Q21.912 3.936 21.807 4.524Q21.702 5.112 21.282 5.532Q20.862 5.952 20.274 6.057Q19.686 6.162 19.119 5.931Q18.552 5.700 18.237 5.217Q17.922 4.734 17.922 4.104L17.922 4.104Q17.922 3.306 18.489 2.739Q19.056 2.172 19.854 2.172ZM4.146 21.828L4.146 21.828Q3.306 21.828 2.739 21.261Q2.172 20.694 2.172 19.875Q2.172 19.056 2.739 18.489Q3.306 17.922 4.125 17.922Q4.944 17.922 5.532 18.489Q6.120 19.056 6.120 19.875Q6.120 20.694 5.532 21.261Q4.944 21.828 4.146 21.828ZM17.922 21.828L17.922 17.922L21.828 17.922L21.828 21.828L17.922 21.828Z\"/>',\n\t'seti:pddl':\n\t\t'<path d=\"M4.608 21.520L4.608 21.800L2.480 21.800Q2.004 20.792 1.598 19.728Q1.192 18.664 0.912 17.432L0.912 17.432Q0.632 16.312 0.478 14.912Q0.324 13.512 0.324 12L0.324 12Q0.324 10.376 0.492 9.032Q0.660 7.688 0.912 6.596L0.912 6.596Q1.192 5.392 1.584 4.314Q1.976 3.236 2.480 2.200L2.480 2.200L4.608 2.200L4.608 2.508Q4.132 3.236 3.712 4.146Q3.292 5.056 2.942 6.274Q2.592 7.492 2.368 8.934Q2.144 10.376 2.144 12L2.144 12Q2.144 13.708 2.354 15.094Q2.564 16.480 2.928 17.712L2.928 17.712Q3.264 18.888 3.698 19.854Q4.132 20.820 4.608 21.520L4.608 21.520ZM23.676 12L23.676 12Q23.676 13.512 23.522 14.898Q23.368 16.284 23.088 17.432L23.088 17.432Q22.808 18.664 22.402 19.728Q21.996 20.792 21.520 21.800L21.520 21.800L19.392 21.800L19.392 21.520Q19.840 20.820 20.288 19.854Q20.736 18.888 21.086 17.684Q21.436 16.480 21.646 15.094Q21.856 13.708 21.856 12L21.856 12Q21.856 10.376 21.632 8.934Q21.408 7.492 21.058 6.274Q20.708 5.056 20.288 4.146Q19.868 3.236 19.392 2.508L19.392 2.508L19.392 2.200L21.520 2.200Q22.024 3.236 22.416 4.314Q22.808 5.392 23.088 6.596L23.088 6.596Q23.340 7.688 23.508 9.032Q23.676 10.376 23.676 12ZM6.260 10.068L6.260 10.068Q5.476 10.068 4.958 9.522Q4.440 8.976 4.440 8.150Q4.440 7.324 4.972 6.792Q5.504 6.260 6.316 6.260Q7.128 6.260 7.646 6.792Q8.164 7.324 8.164 8.164Q8.164 9.004 7.632 9.536Q7.100 10.068 6.260 10.068ZM6.260 18.552L6.260 18.552Q5.476 18.552 4.958 17.992Q4.440 17.432 4.440 16.606Q4.440 15.780 4.972 15.248Q5.504 14.716 6.316 14.716Q7.128 14.716 7.646 15.262Q8.164 15.808 8.164 16.634Q8.164 17.460 7.632 18.006Q7.100 18.552 6.260 18.552ZM18.972 11.160L18.972 18.244L15.976 18.244L15.976 16.564L15.920 16.564Q14.884 18.552 12.868 18.552L12.868 18.552Q11.384 18.552 10.530 17.572Q9.676 16.592 9.676 14.968L9.676 14.968Q9.676 11.496 13.204 10.964L13.204 10.964L15.976 10.544Q15.976 8.612 14.156 8.612Q12.336 8.612 10.684 9.872L10.684 9.872L10.684 7.100Q11.328 6.708 12.476 6.414Q13.624 6.120 14.576 6.120L14.576 6.120Q18.972 6.120 18.972 11.160L18.972 11.160ZM16.004 13.428L16.004 13.428L16.004 12.644L14.128 12.924Q12.588 13.148 12.588 14.520L12.588 14.520Q12.588 15.164 12.966 15.556Q13.344 15.948 13.988 15.948L13.988 15.948Q14.856 15.948 15.430 15.234Q16.004 14.520 16.004 13.428Z\"/>',\n\t'seti:plan':\n\t\t'<path d=\"M0.127 6.839L0.127 0.608L16.929 0.608L16.929 6.839L0.127 6.839ZM7.040 15.116L7.040 8.885L23.873 8.885L23.873 15.116L7.040 15.116ZM17.053 23.393L17.053 17.162L23.873 17.162L23.873 23.393L17.053 23.393Z\"/>',\n\t'seti:happenings':\n\t\t'<path d=\"M7.635 6.765L7.635 0.735L23.925 0.735L23.925 6.765L7.635 6.765ZM0.015 3.735L0.015 3.735Q0.015 4.845 0.735 5.610Q1.455 6.375 2.475 6.375Q3.495 6.375 4.230 5.610Q4.965 4.845 4.965 3.750Q4.965 2.655 4.230 1.890Q3.495 1.125 2.475 1.125Q1.455 1.125 0.735 1.890Q0.015 2.655 0.015 3.735ZM7.695 15.015L7.695 8.985L23.985 8.985L23.985 15.015L7.695 15.015ZM0.075 11.985L0.075 11.985Q0.075 13.065 0.795 13.830Q1.515 14.595 2.550 14.595Q3.585 14.595 4.305 13.830Q5.025 13.065 5.025 11.985Q5.025 10.905 4.305 10.140Q3.585 9.375 2.550 9.375Q1.515 9.375 0.795 10.140Q0.075 10.905 0.075 11.985ZM7.695 23.265L7.695 17.205L23.985 17.205L23.985 23.265L7.695 23.265ZM0.075 20.235L0.075 20.235Q0.075 21.315 0.795 22.080Q1.515 22.845 2.550 22.845Q3.585 22.845 4.305 22.080Q5.025 21.315 5.025 20.235Q5.025 19.155 4.305 18.390Q3.585 17.625 2.550 17.625Q1.515 17.625 0.795 18.390Q0.075 19.155 0.075 20.235Z\"/>',\n\t'seti:powershell':\n\t\t'<path d=\"M0.090 22.968L0.090 17.023L9.438 11.980L0.131 6.322L0.131 0.336L15.875 10.504L15.875 13.948L0.090 22.968ZM23.910 20.262L23.910 23.665L10.258 23.665L10.258 20.262L23.910 20.262Z\"/>',\n\t'seti:prisma':\n\t\t'<path d=\"M20.093 18.987L20.093 18.987L9.113 22.155Q8.861 22.227 8.663 22.047Q8.465 21.867 8.537 21.651L8.537 21.651L12.461 3.327Q12.497 3.075 12.785 3.039Q13.073 3.003 13.181 3.255L13.181 3.255L20.417 18.267Q20.525 18.483 20.435 18.699Q20.345 18.915 20.093 18.987ZM22.001 18.231L22.001 18.231L13.577 0.843Q13.397 0.519 13.091 0.321Q12.785 0.123 12.425 0.087Q12.065 0.051 11.723 0.231Q11.381 0.411 11.201 0.699L11.201 0.699L2.093 15.099Q1.877 15.423 1.877 15.819Q1.877 16.215 2.093 16.539L2.093 16.539L6.557 23.271Q6.737 23.595 7.061 23.757Q7.385 23.919 7.745 23.919L7.745 23.919Q7.961 23.919 8.177 23.847L8.177 23.847L21.101 20.139Q21.713 19.959 21.983 19.383Q22.253 18.807 22.001 18.231Z\"/>',\n\t'seti:pug':\n\t\t'<path d=\"M16.267 5.578L16.267 5.578L16.267 5.578L16.267 5.578ZM13.717 12.276L13.717 12.276Q13.343 12.106 13.207 11.885Q13.071 11.664 12.782 11.562Q12.493 11.460 12.170 11.460Q11.847 11.460 11.558 11.562Q11.269 11.664 11.116 11.885Q10.963 12.106 10.623 12.276L10.623 12.276Q9.569 12.752 8.957 13.755Q8.345 14.758 8.413 15.914L8.413 15.914Q8.379 15.914 8.464 16.509Q8.549 17.104 8.651 17.546L8.651 17.546Q8.821 18.158 8.991 18.277Q9.161 18.396 10.147 18.566L10.147 18.566Q11.337 18.804 12.170 18.804Q13.003 18.804 14.227 18.566L14.227 18.566Q15.247 18.396 15.417 18.277Q15.587 18.158 15.723 17.546L15.723 17.546Q15.825 17.104 15.893 16.458L15.893 16.458Q15.961 15.914 15.927 15.914L15.927 15.914Q15.961 14.758 15.366 13.755Q14.771 12.752 13.717 12.276ZM22.455 8.094L22.455 8.094Q22.285 7.958 21.979 7.652L21.979 7.652Q21.027 6.700 20.449 6.326Q19.871 5.952 19.021 5.782L19.021 5.782Q18.443 5.646 17.457 5.578L17.457 5.578Q17.355 5.748 17.525 5.952L17.525 5.952L18.069 6.496Q19.633 8.094 19.973 9.964L19.973 9.964L20.007 10.406Q20.381 13.092 20.449 13.160L20.449 13.160Q20.585 13.534 20.721 13.568Q20.857 13.602 21.129 13.296L21.129 13.296Q21.163 13.262 21.401 12.786L21.401 12.786Q21.945 11.596 22.183 11.290Q22.421 10.984 23.237 10.304L23.237 10.304Q23.781 9.896 23.815 9.794Q23.849 9.692 23.356 9.097Q22.863 8.502 22.455 8.094ZM5.897 6.496L6.407 5.986Q6.407 5.952 6.441 5.952L6.441 5.952Q6.611 5.748 6.509 5.578L6.509 5.578Q5.523 5.646 4.945 5.782L4.945 5.782Q4.095 5.952 3.517 6.326Q2.939 6.700 1.987 7.652L1.987 7.652L1.511 8.094Q1.103 8.502 0.661 9.080L0.661 9.080Q0.151 9.692 0.185 9.794Q0.219 9.896 0.729 10.304L0.729 10.304Q1.545 10.984 1.800 11.290Q2.055 11.596 2.599 12.786L2.599 12.786Q2.803 13.262 2.837 13.296L2.837 13.296Q3.109 13.602 3.245 13.568Q3.381 13.534 3.517 13.160L3.517 13.160Q3.585 13.092 3.959 10.406L3.959 10.406L4.027 9.930Q4.333 8.094 5.897 6.496L5.897 6.496ZM19.259 13.160L19.259 13.160L19.259 13.160ZM16.879 6.496L16.879 6.496Q16.811 6.428 16.709 6.326L16.709 6.326L16.335 5.918Q16.165 5.748 16.267 5.578L16.267 5.578L15.111 5.442Q13.989 5.306 13.411 5.238L13.411 5.238Q10.827 5.068 8.243 5.578L8.243 5.578L8.175 5.612Q7.903 5.646 7.767 5.578L7.767 5.578L7.733 5.646Q7.767 5.782 7.631 5.918L7.631 5.918L7.087 6.496Q5.523 8.094 5.217 9.930L5.217 9.930L5.047 10.984L5.047 11.120L5.047 11.086Q4.979 11.596 4.928 12.276Q4.877 12.956 4.911 13.160L4.911 13.160L4.945 13.228Q5.047 13.976 5.285 14.282L5.285 14.282Q5.387 14.452 5.625 14.758L5.625 14.758Q6.033 15.268 6.067 15.608L6.067 15.608Q6.339 17.036 7.767 17.546L7.767 17.546Q7.563 17.138 7.393 16.152L7.393 16.152Q7.257 15.234 7.325 15.200L7.325 15.200Q7.257 13.942 8.022 12.854Q8.787 11.766 10.147 11.256L10.147 11.256Q10.589 11.086 10.776 10.848Q10.963 10.610 11.320 10.508Q11.677 10.406 12.102 10.406Q12.527 10.406 12.884 10.508Q13.241 10.610 13.428 10.848Q13.615 11.086 14.057 11.256L14.057 11.256Q15.417 11.766 16.182 12.854Q16.947 13.942 16.879 15.200L16.879 15.200Q16.947 15.234 16.811 16.118L16.811 16.118Q16.641 17.070 16.437 17.512L16.437 17.512Q17.729 16.968 17.967 15.744L17.967 15.744Q18.069 15.234 18.375 14.860L18.375 14.860L18.409 14.792Q18.613 14.554 18.647 14.384L18.647 14.384Q18.783 14.282 18.919 13.976L18.919 13.976L18.987 13.738Q19.123 13.330 19.259 13.160L19.259 13.160Q19.191 13.024 18.851 10.440L18.851 10.440L18.783 9.930Q18.443 8.094 16.879 6.496ZM6.747 8.060L6.747 8.060Q6.713 8.026 6.730 8.026Q6.747 8.026 6.747 8.060ZM7.699 11.630L7.699 11.630Q7.087 11.630 6.662 11.205Q6.237 10.780 6.237 10.168Q6.237 9.556 6.662 9.114Q7.087 8.672 7.699 8.672Q8.311 8.672 8.753 9.114Q9.195 9.556 9.195 10.168Q9.195 10.780 8.753 11.205Q8.311 11.630 7.699 11.630ZM15.315 11.052L15.315 11.052Q15.315 11.052 15.315 11.052L15.315 11.052L15.315 11.052ZM16.607 11.630L16.607 11.630Q15.995 11.630 15.553 11.205Q15.111 10.780 15.111 10.168Q15.111 9.556 15.553 9.114Q15.995 8.672 16.607 8.672Q17.219 8.672 17.644 9.114Q18.069 9.556 18.069 10.168Q18.069 10.780 17.644 11.205Q17.219 11.630 16.607 11.630ZM18.171 10.746L18.171 10.746Q18.171 10.712 18.171 10.678L18.171 10.678L18.171 10.576L18.171 10.610Q18.171 10.678 18.171 10.746ZM18.171 10.406L18.171 10.406Q18.171 10.406 18.171 10.440L18.171 10.440L18.171 10.406Q18.205 10.372 18.171 10.406Z\"/>',\n\t'seti:puppet':\n\t\t'<path d=\"M4.426 7.310L4.426 0.338L11.398 0.338L11.426 5.658L14.254 8.486L14.282 8.486L19.574 8.486L19.574 15.486L14.310 15.486Q14.254 15.458 14.170 15.570L14.170 15.570L11.426 18.314L11.398 23.662L4.426 23.662L4.426 16.662L9.774 16.662L12.630 13.806L12.630 10.166L9.774 7.310L4.426 7.310ZM6.778 4.986L6.778 2.662L9.074 2.662L9.074 4.986L6.778 4.986ZM6.778 21.310L6.778 19.014L9.074 19.014L9.074 21.310L6.778 21.310Z\"/>',\n\t'seti:purescript':\n\t\t'<path d=\"M16.968 17.184L15.132 15.456L7.032 15.456L8.868 17.184L16.968 17.184ZM8.868 11.136L7.032 12.864L15.132 12.864L16.968 11.136L8.868 11.136ZM16.968 8.544L15.132 6.816L7.032 6.816L8.868 8.544L16.968 8.544ZM2.244 14.160L6.168 10.236L4.944 9.012L0.408 13.548Q0.156 13.800 0.156 14.160Q0.156 14.520 0.408 14.772L0.408 14.772L4.944 19.308L6.168 18.084L2.244 14.160ZM23.592 9.228L23.592 9.228L19.056 4.692L17.832 5.916L21.756 9.840L17.832 13.764L19.056 14.988L23.592 10.452Q23.844 10.200 23.844 9.840Q23.844 9.480 23.592 9.228Z\"/>',\n\t'seti:python':\n\t\t'<path d=\"M11.460 11.300L11.460 11.300L8.688 11.300Q7.236 11.300 6.378 12.158Q5.520 13.016 5.520 14.468L5.520 14.468L5.520 16.932Q5.520 17.372 5.124 17.372L5.124 17.372L3.892 17.372Q1.956 17.372 1.120 15.700L1.120 15.700Q0.460 14.336 0.460 13.236L0.460 13.236Q0.196 10.508 0.856 8.704L0.856 8.704Q1.560 6.592 3.452 6.240L3.452 6.240L11.416 6.240Q11.856 6.240 11.856 6.108L11.856 6.108L11.856 5.404L11.680 5.316Q11.504 5.272 11.460 5.272L11.460 5.272L6.752 5.272Q6.444 5.272 6.334 5.140Q6.224 5.008 6.224 4.700L6.224 4.700L6.224 2.940Q6.224 1.400 7.456 1.004L7.456 1.004Q8.820 0.432 9.524 0.300L9.524 0.300Q12.164-0.140 14.452 0.432L14.452 0.432Q15.596 0.696 16.388 1.268L16.388 1.268Q16.872 1.752 17.048 2.148L17.048 2.148Q17.312 2.632 17.224 3.204L17.224 3.204L17.224 8.132Q17.224 9.584 16.432 10.376Q15.640 11.168 14.188 11.168L14.188 11.168Q13.220 11.300 11.460 11.300ZM7.588 3.072L7.588 3.072Q7.588 3.512 7.896 3.842Q8.204 4.172 8.644 4.172Q9.084 4.172 9.436 3.820Q9.788 3.468 9.788 3.072Q9.788 2.676 9.458 2.368Q9.128 2.060 8.688 1.972L8.688 1.972Q8.204 1.972 7.896 2.302Q7.588 2.632 7.588 3.072ZM12.560 12.708L12.560 12.708L15.288 12.708Q16.740 12.708 17.598 11.850Q18.456 10.992 18.456 9.540L18.456 9.540L18.456 7.032Q18.456 6.636 18.852 6.636L18.852 6.636L20.084 6.636Q22.020 6.636 22.856 8.308L22.856 8.308Q23.560 9.672 23.560 10.772L23.560 10.772Q23.780 13.500 23.120 15.304L23.120 15.304Q22.416 17.416 20.524 17.768L20.524 17.768L12.560 17.768Q12.120 17.768 12.120 17.900L12.120 17.900L12.120 18.604L12.296 18.692Q12.472 18.736 12.560 18.736L12.560 18.736L17.224 18.736Q17.532 18.736 17.642 18.868Q17.752 19.000 17.752 19.308L17.752 19.308L17.752 21.068Q17.752 22.608 16.520 23.004L16.520 23.004Q15.156 23.532 14.452 23.708L14.452 23.708Q11.812 24.148 9.524 23.532L9.524 23.532Q8.380 23.312 7.588 22.740L7.588 22.740Q7.104 22.256 6.928 21.860L6.928 21.860Q6.664 21.376 6.752 20.804L6.752 20.804L6.752 15.832Q6.752 14.424 7.544 13.632Q8.336 12.840 9.788 12.840L9.788 12.840Q10.756 12.708 12.560 12.708ZM16.388 20.936L16.388 20.936Q16.388 20.496 16.080 20.166Q15.772 19.836 15.332 19.836Q14.892 19.836 14.540 20.188Q14.188 20.540 14.188 20.936Q14.188 21.332 14.518 21.640Q14.848 21.948 15.288 22.036L15.288 22.036Q15.772 22.036 16.080 21.706Q16.388 21.376 16.388 20.936Z\"/>',\n\t'seti:react':\n\t\t'<path d=\"M18.955 15.993L18.993 15.993Q18.993 16.107 18.993 16.297L18.993 16.297Q19.221 18.615 19.221 19.774Q19.221 20.933 18.651 21.693Q18.081 22.453 17.207 22.529L17.207 22.529Q16.333 22.719 15.307 22.301L15.307 22.301Q14.547 21.921 13.179 21.009L13.179 21.009Q12.305 20.439 11.849 20.173L11.849 20.173Q10.671 21.123 9.949 21.579L9.949 21.579Q9.341 21.997 8.771 22.187L8.771 22.187Q7.707 22.643 6.795 22.472Q5.883 22.301 5.332 21.579Q4.781 20.857 4.762 19.679Q4.743 18.501 4.971 15.879L4.971 15.879Q3.527 15.423 2.805 14.929L2.805 14.929Q1.855 14.473 0.905 13.523L0.905 13.523Q0.145 12.725 0.183 11.794Q0.221 10.863 1.057 10.065L1.057 10.065Q1.665 9.343 2.691 8.849L2.691 8.849Q3.299 8.507 4.591 8.051L4.591 8.051Q4.667 8.051 4.838 7.994Q5.009 7.937 5.085 7.937L5.085 7.937L4.743 5.315Q4.705 4.479 4.971 3.073L4.971 3.073Q5.237 2.085 6.054 1.667Q6.871 1.249 7.935 1.515L7.935 1.515Q8.885 1.743 9.835 2.351L9.835 2.351Q10.519 2.769 11.393 3.529L11.393 3.529Q11.545 3.605 11.735 3.871L11.735 3.871L11.849 4.023Q13.749 2.579 14.813 1.971L14.813 1.971Q16.029 1.173 17.207 1.515L17.207 1.515Q18.081 1.705 18.594 2.446Q19.107 3.187 19.221 4.365L19.221 4.365L19.221 6.265Q19.221 6.645 19.031 7.405L19.031 7.405Q18.917 7.899 18.841 8.165L18.841 8.165Q19.905 8.507 20.874 8.982Q21.843 9.457 22.261 9.761L22.261 9.761Q22.983 10.255 23.401 10.882Q23.819 11.509 23.819 12.193Q23.819 12.877 23.401 13.504Q22.983 14.131 22.261 14.625L22.261 14.625Q21.805 14.929 20.855 15.423L20.855 15.423Q20.171 15.537 18.955 15.993L18.955 15.993ZM10.861 15.841L12.077 15.879Q12.267 15.879 12.685 15.841L12.685 15.841Q13.255 15.765 13.521 15.765L13.521 15.765Q14.015 15.765 14.357 15.271L14.357 15.271Q15.535 13.371 15.991 12.307L15.991 12.307Q16.105 12.155 16.105 11.927Q16.105 11.699 15.991 11.623L15.991 11.623Q15.421 10.521 14.205 8.659L14.205 8.659Q14.053 8.279 13.635 8.279L13.635 8.279Q11.621 8.279 10.557 8.165L10.557 8.165Q9.607 8.165 9.113 9.001L9.113 9.001Q8.657 9.723 8.391 10.179L8.391 10.179L7.859 11.167Q7.517 11.661 7.460 11.870Q7.403 12.079 7.460 12.288Q7.517 12.497 7.859 13.029L7.859 13.029L8.391 13.979L8.847 14.739Q9.227 15.347 9.379 15.499L9.379 15.499Q9.607 15.765 9.911 15.803L9.911 15.803Q10.139 15.841 10.861 15.841L10.861 15.841ZM5.199 14.929L5.199 14.929Q5.921 13.029 6.263 12.193L6.263 12.193L6.263 11.737L5.199 9.001Q3.071 9.647 1.893 10.521L1.893 10.521Q1.057 11.167 1.057 11.908Q1.057 12.649 1.893 13.257L1.893 13.257Q2.501 13.903 3.451 14.321L3.451 14.321Q4.021 14.587 5.199 14.929ZM18.613 8.887L18.613 8.887Q18.157 9.951 17.435 11.851L17.435 11.851L17.397 11.965Q17.359 12.079 17.435 12.079L17.435 12.079Q18.157 13.979 18.613 15.157L18.613 15.157Q19.069 14.929 20.019 14.435L20.019 14.435Q21.273 13.865 21.805 13.523L21.805 13.523Q22.793 12.877 22.793 12.079Q22.793 11.281 21.843 10.673L21.843 10.673Q21.197 10.103 19.753 9.419L19.753 9.419Q18.955 9.077 18.613 8.887ZM5.883 7.481L5.921 7.671Q7.821 7.481 8.885 7.215L8.885 7.215Q8.961 7.215 9.037 7.139L9.037 7.139L9.113 7.101Q9.797 6.151 11.013 4.707L11.013 4.707Q10.063 3.909 9.569 3.529L9.569 3.529Q8.733 2.883 7.935 2.579L7.935 2.579Q6.947 2.237 6.339 2.598Q5.731 2.959 5.541 4.023L5.541 4.023Q5.465 4.745 5.579 5.695L5.579 5.695Q5.655 6.265 5.883 7.481L5.883 7.481ZM17.777 7.709L17.777 7.671Q17.777 7.633 17.834 7.462Q17.891 7.291 17.891 7.215L17.891 7.215Q18.157 6.037 18.157 5.391L18.157 5.391Q18.233 4.327 17.891 3.529L17.891 3.529Q17.739 2.921 17.321 2.655Q16.903 2.389 16.371 2.465L16.371 2.465Q15.421 2.617 14.509 3.225L14.509 3.225Q13.939 3.567 12.951 4.479L12.951 4.479L12.685 4.707Q13.293 5.543 14.471 6.987L14.471 6.987L14.813 7.329L17.777 7.709ZM5.921 16.221L5.921 16.221Q5.883 16.449 5.807 16.867L5.807 16.867Q5.579 17.931 5.541 18.425L5.541 18.425Q5.465 19.299 5.693 20.059L5.693 20.059Q5.845 20.933 6.434 21.275Q7.023 21.617 7.935 21.351L7.935 21.351Q8.695 21.123 9.493 20.591L9.493 20.591Q9.949 20.249 10.823 19.489L10.823 19.489L11.127 19.223Q10.443 18.273 9.227 16.829L9.227 16.829Q8.999 16.601 8.885 16.601L8.885 16.601Q7.517 16.601 5.921 16.221ZM12.571 19.223L12.571 19.223Q13.065 19.869 14.015 20.553L14.015 20.553Q14.889 21.199 15.649 21.465L15.649 21.465Q17.549 22.035 17.891 20.173L17.891 20.173Q18.043 19.337 17.967 18.349L17.967 18.349Q17.929 17.741 17.701 16.525L17.701 16.525L17.663 16.373L16.903 16.449Q15.307 16.563 14.585 16.829L14.585 16.829Q14.243 17.019 13.939 17.361L13.939 17.361Q13.749 17.589 13.445 18.045L13.445 18.045Q13.179 18.425 13.046 18.634Q12.913 18.843 12.571 19.223ZM10.443 7.101L13.293 7.101Q13.027 6.797 12.552 6.265Q12.077 5.733 11.849 5.429L11.849 5.429L11.659 5.695Q10.937 6.607 10.443 7.101L10.443 7.101ZM12.837 17.361L13.293 16.829L10.557 16.829Q10.785 17.133 11.260 17.665Q11.735 18.197 11.963 18.501L11.963 18.501Q12.229 18.083 12.837 17.361L12.837 17.361ZM8.391 15.651L8.391 15.651Q8.239 15.385 7.935 14.853L7.935 14.853Q7.289 13.789 6.985 13.143L6.985 13.143L6.263 15.157Q6.909 15.423 8.391 15.651ZM16.713 13.143L16.713 13.143L15.307 15.651L17.435 15.271Q17.283 14.511 16.713 13.143ZM6.985 10.787L6.985 10.787L8.391 8.279L6.263 8.621L6.415 9.115Q6.681 10.217 6.985 10.787ZM15.307 8.279L15.307 8.279Q15.535 8.773 16.010 9.476Q16.485 10.179 16.713 10.673L16.713 10.673Q17.207 9.229 17.435 8.659L17.435 8.659Q16.523 8.621 15.307 8.279Z\"/>',\n\t'seti:rescript':\n\t\t'<path d=\"M23.825 5.486L23.825 5.486Q23.825 6.861 23.115 8.066Q22.406 9.270 21.202 10.000Q19.998 10.732 18.557 10.732Q17.117 10.732 15.913 10.044Q14.709 9.355 14.021 8.151Q13.333 6.948 13.333 5.486Q13.333 4.023 14.021 2.820Q14.709 1.616 15.913 0.928Q17.117 0.239 18.536 0.239Q19.955 0.239 21.159 0.928Q22.363 1.616 23.094 2.820Q23.825 4.023 23.825 5.486ZM4.862 0.111L9.291 0.111L9.291 19.332Q9.291 20.492 9.248 20.922L9.248 20.922Q9.205 21.568 9.011 22.041Q8.818 22.514 8.366 22.965Q7.915 23.416 7.399 23.632L7.399 23.632Q7.055 23.803 6.410 23.847L6.410 23.847Q6.023 23.890 4.733 23.890L4.733 23.890Q3.572 23.890 3.142 23.847L3.142 23.847Q2.497 23.803 2.024 23.610Q1.551 23.416 1.099 22.965Q0.648 22.514 0.433 21.998L0.433 21.998Q0.261 21.654 0.175 21.009L0.175 21.009Q0.175 20.579 0.175 19.332L0.175 19.332L0.175 4.797Q0.175 3.378 0.175 2.863L0.175 2.863Q0.261 2.088 0.454 1.701Q0.648 1.315 1.013 0.949Q1.379 0.584 1.809 0.390Q2.239 0.197 2.970 0.154L2.970 0.154Q3.443 0.111 4.862 0.111L4.862 0.111Z\"/>',\n\t'seti:R':\n\t\t'<path d=\"M12.000 18.678L12.000 18.678Q8.808 18.678 6.120 17.628Q3.432 16.578 1.836 14.772Q0.240 12.966 0.240 10.824Q0.240 8.682 1.836 6.876Q3.432 5.070 6.120 3.999Q8.808 2.928 12.000 2.928Q15.192 2.928 17.880 3.999Q20.568 5.070 22.164 6.876Q23.760 8.682 23.760 10.824Q23.760 12.966 22.164 14.772Q20.568 16.578 17.880 17.628Q15.192 18.678 12.000 18.678ZM13.806 6.036L13.806 6.036Q11.370 6.036 9.312 6.750Q7.254 7.464 6.057 8.703Q4.860 9.942 4.860 11.391Q4.860 12.840 6.057 14.079Q7.254 15.318 9.312 16.053Q11.370 16.788 13.806 16.788L13.806 16.788Q17.712 16.788 19.938 15.486L19.938 15.486Q22.374 14.100 22.374 11.412Q22.374 8.724 19.938 7.296L19.938 7.296Q17.712 6.036 13.806 6.036ZM18.678 15.024L18.132 15.108Q18.342 15.192 18.552 15.276L18.552 15.276Q19.014 15.402 19.266 15.570L19.266 15.570Q19.602 15.738 19.854 15.948L19.854 15.948Q19.938 16.074 20.022 16.200L20.022 16.200L22.920 21.030L18.384 21.030L16.242 17.082L15.990 16.662Q15.738 16.242 15.570 16.116L15.570 16.116Q15.234 15.864 15.024 15.864L15.024 15.864L13.932 15.864L13.932 21.072L9.942 21.072L9.942 7.842L18.006 7.842L18.552 7.884Q19.266 8.010 19.812 8.304L19.812 8.304Q20.610 8.682 21.072 9.354L21.072 9.354Q21.660 10.194 21.660 11.391Q21.660 12.588 21.114 13.428L21.114 13.428Q20.652 14.142 19.896 14.562L19.896 14.562Q19.350 14.856 18.678 15.024L18.678 15.024ZM16.410 10.698L16.410 10.698L13.974 10.698L13.974 12.966L16.410 12.924L16.704 12.882Q17.040 12.798 17.250 12.588L17.250 12.588Q17.544 12.294 17.502 11.790L17.502 11.790Q17.544 11.076 16.956 10.824L16.956 10.824Q16.662 10.698 16.410 10.698L16.410 10.698Z\"/>',\n\t'seti:ruby':\n\t\t'<path d=\"M13.913 0.300L14.453 0.030L20.212 0.030Q20.302 0.030 20.438 0.098Q20.573 0.165 20.663 0.165L20.663 0.165Q23.363 0.840 23.768 3.270L23.768 3.270Q23.768 3.405 23.835 3.698Q23.902 3.990 23.902 4.125L23.902 4.125L23.902 5.250Q23.902 5.385 23.835 5.678Q23.768 5.970 23.768 6.105L23.768 6.105L23.272 12.855Q22.732 18.975 22.643 22.125L22.643 22.125Q22.598 22.530 22.463 22.688Q22.327 22.845 21.923 22.845L21.923 22.845Q21.157 22.890 19.672 22.957Q18.188 23.025 17.422 23.115L17.422 23.115Q15.892 23.160 12.788 23.385Q9.683 23.610 8.152 23.655L8.152 23.655Q7.297 23.655 5.588 23.970L5.588 23.970L4.193 23.970Q4.148 23.970 3.990 23.880Q3.832 23.790 3.787 23.790L3.787 23.790Q2.482 23.610 1.605 22.845Q0.728 22.080 0.413 20.865L0.413 20.865Q0.322 20.640 0.255 20.212Q0.188 19.785 0.098 19.605L0.098 19.605L0.098 18.885Q0.098 18.840 0.188 18.660Q0.277 18.480 0.277 18.345L0.277 18.345Q0.277 17.760 0.345 16.567Q0.413 15.375 0.413 14.835L0.413 14.835Q0.413 13.530 0.548 12.990L0.548 12.990Q1.402 10.785 2.213 9.345L2.213 9.345Q3.248 7.455 4.598 6.105L4.598 6.105Q6.532 4.035 8.468 2.775L8.468 2.775Q10.672 1.335 13.058 0.750L13.058 0.750Q13.328 0.570 13.913 0.300L13.913 0.300Z\"/>',\n\t'seti:rust':\n\t\t'<path d=\"M18.419 2.539L18.419 2.983L18.419 3.686L18.530 3.797L18.642 3.797Q18.826 3.760 19.178 3.704Q19.529 3.649 19.696 3.575Q19.863 3.501 19.992 3.630Q20.122 3.760 20.047 3.926Q19.973 4.093 19.918 4.444Q19.863 4.796 19.825 4.981L19.825 4.981L19.825 5.092L19.936 5.203Q19.936 5.314 20.159 5.314L20.159 5.314L21.194 5.314Q21.564 5.314 21.527 5.647L21.527 5.647L21.527 5.906Q21.453 6.128 21.232 6.572L21.232 6.572L21.084 6.831L21.084 6.942L21.157 7.016Q21.194 7.090 21.194 7.164L21.194 7.164L21.305 7.164Q21.491 7.238 21.842 7.293Q22.194 7.349 22.360 7.367Q22.526 7.386 22.601 7.589Q22.674 7.793 22.601 7.978L22.601 7.978L22.305 8.385Q22.082 8.644 22.008 8.792L22.008 8.792L22.008 9.014Q22.008 9.088 22.119 9.162L22.119 9.162L22.230 9.236L23.155 9.606Q23.340 9.680 23.396 9.846Q23.451 10.013 23.267 10.161L23.267 10.161Q22.822 10.642 22.489 10.864L22.489 10.864L22.489 11.197L22.526 11.271Q22.637 11.308 22.712 11.308L22.712 11.308L22.970 11.493Q23.303 11.715 23.488 11.789Q23.674 11.863 23.674 12.066Q23.674 12.270 23.526 12.381L23.526 12.381L22.601 12.936L22.601 13.047Q22.601 13.232 22.601 13.269Q22.601 13.306 22.712 13.417L22.712 13.417Q23.155 13.861 23.415 13.972L23.415 13.972Q23.563 14.157 23.507 14.305Q23.451 14.453 23.267 14.564L23.267 14.564Q22.601 14.786 22.341 14.897L22.341 14.897L22.305 14.934Q22.230 14.971 22.194 15.045Q22.156 15.119 22.230 15.267L22.230 15.267L22.230 15.378Q22.415 15.526 22.637 15.933L22.637 15.933L22.822 16.192Q22.896 16.266 22.878 16.414Q22.860 16.562 22.712 16.636L22.712 16.636L22.601 16.673L22.453 16.747Q22.305 16.747 22.008 16.821Q21.712 16.895 21.564 16.895L21.564 16.895L21.416 16.895Q21.416 16.932 21.361 16.987Q21.305 17.043 21.305 17.117L21.305 17.117L21.343 17.228L21.416 17.339L21.787 18.153Q21.860 18.227 21.805 18.375Q21.749 18.523 21.675 18.597L21.675 18.597L20.270 18.597Q20.159 18.745 20.159 18.967L20.159 18.967Q20.232 19.115 20.288 19.411Q20.343 19.707 20.418 19.873Q20.491 20.040 20.343 20.188Q20.195 20.336 19.936 20.336L19.936 20.336Q19.752 20.299 19.400 20.243Q19.049 20.188 18.901 20.114L18.901 20.114L18.790 20.114L18.604 20.299Q18.567 20.336 18.642 20.336L18.642 20.336L18.642 21.372Q18.642 21.742 18.308 21.742L18.308 21.742L18.087 21.742Q17.605 21.594 17.162 21.261L17.162 21.261L17.050 21.261Q16.939 21.261 16.866 21.335Q16.791 21.409 16.791 21.520L16.791 21.520Q16.791 21.668 16.736 21.964Q16.680 22.260 16.680 22.426Q16.680 22.593 16.477 22.667Q16.273 22.741 16.125 22.667L16.125 22.667Q15.756 22.297 15.311 22.075L15.311 22.075L15.200 22.075Q15.090 22.075 15.015 22.149Q14.942 22.223 14.942 22.297L14.942 22.297L14.608 23.222Q14.497 23.481 14.276 23.481L14.276 23.481Q14.201 23.481 14.165 23.407L14.165 23.407L14.128 23.370Q13.683 22.889 13.462 22.556L13.462 22.556L13.203 22.556Q13.128 22.556 13.055 22.630Q12.980 22.704 12.980 22.778L12.980 22.778L12.537 23.592Q12.425 23.740 12.222 23.740Q12.018 23.740 11.945 23.592L11.945 23.592L11.352 22.667L11.020 22.667L10.206 23.481Q10.095 23.592 9.762 23.592L9.762 23.592Q9.651 23.592 9.651 23.370L9.651 23.370L9.280 22.260Q9.243 22.186 9.169 22.186L9.169 22.186L8.837 22.186Q8.651 22.371 8.281 22.593L8.281 22.593L8.023 22.778L7.800 22.667Q7.467 22.519 7.338 22.426Q7.208 22.334 7.208 22.186L7.208 22.186Q7.097 21.853 7.097 21.150L7.097 21.150L7.060 21.113Q7.060 21.039 6.986 21.039Q6.912 21.039 6.857 20.983Q6.801 20.928 6.764 20.928L6.764 20.928L6.616 20.928L6.505 21.039L5.691 21.372Q5.543 21.483 5.395 21.409Q5.247 21.335 5.247 21.150L5.247 21.150L5.247 19.892L5.136 19.781L5.026 19.781Q4.840 19.818 4.489 19.873Q4.138 19.929 3.971 20.003Q3.804 20.077 3.675 19.947Q3.545 19.818 3.619 19.651Q3.693 19.485 3.749 19.133Q3.804 18.782 3.841 18.597L3.841 18.597L3.841 18.486L3.731 18.375L2.583 18.375Q2.250 18.375 2.250 18.042L2.250 18.042L2.250 17.783Q2.324 17.561 2.583 17.117L2.583 17.117L2.694 16.858L2.694 16.710Q2.657 16.636 2.583 16.636L2.583 16.636Q2.583 16.562 2.510 16.562L2.510 16.562L2.472 16.525Q2.287 16.525 1.936 16.469Q1.584 16.414 1.418 16.414Q1.251 16.414 1.177 16.210Q1.103 16.007 1.214 15.822L1.214 15.822Q1.548 15.489 1.769 15.008L1.769 15.008L1.769 14.786L1.584 14.638Q1.510 14.564 1.436 14.564L1.436 14.564L0.622 14.231Q0.437 14.120 0.382 13.953Q0.326 13.787 0.511 13.639L0.511 13.639Q0.696 13.565 1.066 13.269L1.066 13.269Q1.288 13.047 1.436 12.936L1.436 12.936L1.436 12.714Q1.436 12.492 1.325 12.492L1.325 12.492L0.511 12.011Q0.326 11.937 0.326 11.733Q0.326 11.530 0.511 11.456L0.511 11.456L1.436 10.864L1.436 10.642L0.622 9.828Q0.548 9.754 0.548 9.606Q0.548 9.458 0.622 9.347L0.622 9.347L0.659 9.347Q0.733 9.310 0.733 9.236L0.733 9.236L1.769 8.903L1.769 8.792Q1.843 8.718 1.806 8.533L1.806 8.533L1.769 8.422Q1.658 8.311 1.492 8.089Q1.325 7.867 1.214 7.719Q1.103 7.571 1.177 7.367Q1.251 7.164 1.418 7.127Q1.584 7.090 1.936 7.034Q2.287 6.979 2.472 6.942L2.472 6.942L2.583 6.942Q2.583 6.868 2.639 6.812Q2.694 6.757 2.694 6.683L2.694 6.683L2.694 6.572L2.583 6.461L2.250 5.647Q2.139 5.499 2.213 5.351Q2.287 5.203 2.472 5.203L2.472 5.203L3.731 5.203L3.767 5.129Q3.767 5.092 3.841 5.092L3.841 5.092L3.841 4.981Q3.619 4.278 3.619 3.945L3.619 3.945Q3.545 3.760 3.675 3.630Q3.804 3.501 3.989 3.575L3.989 3.575Q4.322 3.575 5.026 3.797L5.026 3.797L5.136 3.797L5.284 3.649Q5.321 3.575 5.247 3.575L5.247 3.575L5.247 2.539Q5.247 2.206 5.580 2.206L5.580 2.206L5.840 2.206Q6.061 2.280 6.505 2.539L6.505 2.539L6.764 2.650L6.912 2.650Q6.986 2.613 6.986 2.539L6.986 2.539Q7.060 2.539 7.060 2.465L7.060 2.465L7.097 2.428Q7.097 2.243 7.153 1.891Q7.208 1.540 7.208 1.373Q7.208 1.207 7.412 1.133Q7.615 1.059 7.800 1.133L7.800 1.133Q8.133 1.503 8.614 1.725L8.614 1.725L8.837 1.725L8.947 1.614L8.947 1.540Q8.985 1.466 9.058 1.392L9.058 1.392L9.392 0.578Q9.503 0.430 9.576 0.393Q9.651 0.356 9.872 0.356L9.872 0.356L9.983 0.467Q10.058 0.652 10.354 1.022L10.354 1.022Q10.576 1.244 10.686 1.392L10.686 1.392L10.797 1.392Q10.872 1.392 10.945 1.318L10.945 1.318L11.020 1.281L11.500 0.467Q11.648 0.282 11.815 0.263Q11.982 0.245 12.055 0.356L12.055 0.356L12.166 0.467L12.759 1.392L13.091 1.392Q13.166 1.392 13.203 1.318L13.203 1.318L13.239 1.281Q13.683 0.800 13.794 0.578L13.794 0.578Q13.869 0.504 14.017 0.504Q14.165 0.504 14.276 0.578L14.276 0.578L14.276 0.689Q14.497 1.392 14.608 1.614L14.608 1.614L14.720 1.725L14.942 1.725Q15.459 1.540 15.866 1.133L15.866 1.133Q16.052 1.059 16.255 1.133Q16.459 1.207 16.459 1.373Q16.459 1.540 16.514 1.891Q16.569 2.243 16.569 2.428L16.569 2.428L16.607 2.465Q16.607 2.539 16.680 2.539L16.680 2.539L16.791 2.539Q16.902 2.613 16.976 2.613Q17.050 2.613 17.050 2.539L17.050 2.539Q17.273 2.428 17.976 2.206L17.976 2.206Q18.160 2.095 18.290 2.187Q18.419 2.280 18.419 2.539L18.419 2.539ZM5.580 6.128L5.580 6.128L14.386 6.128Q15.090 6.128 15.422 6.239L15.422 6.239Q16.532 6.609 17.050 7.275L17.050 7.275Q17.605 7.867 17.605 8.681L17.605 8.681Q17.605 9.347 17.273 10.050L17.273 10.050Q16.902 10.605 16.348 10.975L16.348 10.975Q16.125 11.197 15.977 11.197L15.977 11.197Q16.052 11.271 16.163 11.326Q16.273 11.382 16.348 11.456L16.348 11.456L16.532 11.604Q16.939 11.974 17.050 12.233L17.050 12.233Q17.383 12.788 17.383 13.417L17.383 13.417Q17.383 13.528 17.605 13.750L17.605 13.750Q17.753 13.898 18.049 13.972L18.049 13.972Q18.197 13.972 18.549 13.972Q18.901 13.972 19.122 13.750L19.122 13.750Q19.270 13.602 19.381 13.306L19.381 13.306L19.456 13.047L19.456 12.492Q19.456 12.381 19.474 12.381Q19.492 12.381 19.566 12.381L19.566 12.381L20.380 12.381L20.380 10.864Q20.232 10.790 19.881 10.623Q19.529 10.457 19.345 10.383L19.345 10.383Q19.308 10.346 19.122 10.290Q18.938 10.235 18.901 10.161L18.901 10.161Q18.346 9.976 18.530 9.236L18.530 9.236Q19.011 8.089 19.345 7.497L19.345 7.497L19.345 7.386Q18.901 6.683 18.642 6.387L18.642 6.387Q18.197 5.832 17.716 5.425L17.716 5.425Q16.014 3.871 13.573 3.353L13.573 3.353L13.462 3.353Q12.759 4.056 12.314 4.389L12.314 4.389Q12.130 4.574 11.834 4.574Q11.538 4.574 11.352 4.389L11.352 4.389L10.317 3.353L10.206 3.353Q9.762 3.464 9.058 3.686L9.058 3.686Q7.245 4.352 5.802 5.758L5.802 5.758Q5.580 6.017 5.580 6.128ZM18.642 16.747L18.642 16.747L15.200 16.747Q14.831 16.747 14.720 16.636L14.720 16.636Q14.128 16.340 13.905 15.489L13.905 15.489L13.869 15.267Q13.683 14.601 13.683 14.231L13.683 14.231Q13.683 13.972 13.480 13.528Q13.276 13.084 12.944 12.843Q12.610 12.603 12.166 12.603L12.166 12.603L10.095 12.603L10.095 14.083L12.166 14.083Q12.277 14.083 12.296 14.101Q12.314 14.120 12.314 14.231L12.314 14.231L12.314 16.525Q12.314 16.599 12.296 16.617Q12.277 16.636 12.166 16.636L12.166 16.636L5.247 16.636Q5.469 17.006 5.950 17.450L5.950 17.450L6.061 17.450Q6.986 17.228 7.430 17.228L7.430 17.228Q7.689 17.154 7.930 17.283Q8.171 17.413 8.244 17.672L8.244 17.672L8.614 19.189L8.614 19.300Q9.540 19.670 9.983 19.781L9.983 19.781Q10.761 20.003 11.352 20.003L11.352 20.003Q11.907 20.077 12.573 20.040L12.573 20.040Q12.980 20.003 13.794 19.892L13.794 19.892Q14.386 19.781 15.534 19.300L15.534 19.300L15.534 19.189L15.866 17.672Q16.088 17.080 16.680 17.228L16.680 17.228L18.087 17.561L18.197 17.561Q18.197 17.228 18.642 16.747ZM6.283 8.681L4.914 8.681L5.247 9.347L5.284 9.458Q5.321 9.643 5.247 9.717L5.247 9.717Q5.247 9.902 4.988 10.087L4.988 10.087L4.914 10.161Q3.989 10.642 3.508 10.753L3.508 10.753L3.434 10.753Q3.397 10.790 3.397 10.864L3.397 10.864L3.397 11.456Q3.397 12.603 3.731 13.972L3.731 13.972L3.767 14.046Q3.767 14.120 3.841 14.120L3.841 14.120L6.283 14.120L6.283 8.681ZM9.872 10.272L9.872 10.272Q9.910 10.272 9.947 10.272L9.947 10.272L9.872 10.272L12.537 10.272Q12.980 10.272 13.239 10.161L13.239 10.161L13.351 10.087Q13.498 9.976 13.573 9.809Q13.646 9.643 13.628 9.402Q13.610 9.162 13.462 9.014L13.462 9.014Q13.091 8.681 12.648 8.681L12.648 8.681L9.762 8.681Q9.872 9.236 9.872 10.272ZM2.917 9.125L2.917 9.125Q2.917 9.384 3.138 9.606Q3.360 9.828 3.619 9.828Q3.878 9.828 4.100 9.606Q4.322 9.384 4.322 9.125Q4.322 8.866 4.100 8.644Q3.878 8.422 3.619 8.422Q3.360 8.422 3.138 8.644Q2.917 8.866 2.917 9.125ZM17.605 18.856L17.605 18.856Q17.605 18.597 17.402 18.375Q17.198 18.153 16.921 18.153Q16.643 18.153 16.440 18.375Q16.236 18.597 16.236 18.856Q16.236 19.115 16.440 19.318Q16.643 19.522 16.921 19.522Q17.198 19.522 17.402 19.337Q17.605 19.152 17.605 18.856ZM6.764 18.153L6.764 18.153Q6.468 18.153 6.265 18.375Q6.061 18.597 6.061 18.856Q6.061 19.115 6.265 19.318Q6.468 19.522 6.746 19.522Q7.023 19.522 7.227 19.318Q7.430 19.115 7.430 18.856Q7.430 18.597 7.227 18.375Q7.023 18.153 6.764 18.153ZM12.537 2.983L12.537 2.983Q12.537 2.724 12.352 2.520Q12.166 2.317 11.852 2.317Q11.538 2.317 11.334 2.502Q11.131 2.687 11.131 2.983Q11.131 3.279 11.316 3.482Q11.500 3.686 11.834 3.686L11.834 3.686Q12.314 3.686 12.537 2.983ZM20.047 9.939L20.047 9.939Q20.306 9.939 20.529 9.717Q20.750 9.495 20.750 9.236Q20.750 8.977 20.529 8.755Q20.306 8.533 20.047 8.533Q19.788 8.533 19.566 8.755Q19.345 8.977 19.345 9.254Q19.345 9.532 19.529 9.735Q19.715 9.939 20.047 9.939Z\"/>',\n\t'seti:sass':\n\t\t'<path d=\"M14.594 13.680L14.594 13.806Q13.838 14.268 12.284 15.234L12.284 15.234L9.974 16.662Q10.730 18.384 10.100 20.232L10.100 20.232Q9.638 21.744 8.525 22.647Q7.412 23.550 5.900 23.760L5.900 23.760Q5.312 23.928 4.472 23.634L4.472 23.634Q3.800 23.466 3.548 22.710L3.548 22.710Q2.750 20.946 3.926 19.560L3.926 19.560L4.346 19.140Q5.144 18.300 5.648 18.006L5.648 18.006Q6.026 17.670 6.950 17.166L6.950 17.166Q7.664 16.788 8.000 16.536L8.000 16.536Q8.084 16.452 8.294 16.410L8.294 16.410L8.546 16.284Q8.378 16.200 8.147 15.990Q7.916 15.780 7.748 15.780L7.748 15.780Q5.984 14.646 4.724 13.386L4.724 13.386Q3.674 12.336 3.296 11.706L3.296 11.706Q2.414 11.034 2.267 10.110Q2.120 9.186 2.624 8.262L2.624 8.262Q3.254 6.750 4.976 4.986L4.976 4.986Q8.000 2.340 12.074 0.912L12.074 0.912Q14.972-0.096 17.576 0.282L17.576 0.282Q17.870 0.408 18.521 0.597Q19.172 0.786 19.424 0.912L19.424 0.912Q20.936 1.542 21.482 2.676Q22.028 3.810 21.524 5.406L21.524 5.406Q20.684 8.094 18.374 9.438L18.374 9.438Q16.778 10.446 15.140 10.824Q13.502 11.202 11.696 10.908L11.696 10.908Q10.604 10.740 9.596 9.732L9.596 9.732Q9.344 9.186 9.176 9.060L9.176 9.060L9.218 8.976Q9.260 8.892 9.344 8.808L9.344 8.808L9.386 8.808Q9.512 8.850 9.596 8.934L9.596 8.934Q10.940 10.320 12.998 9.858L12.998 9.858Q14.804 9.522 16.106 8.892L16.106 8.892Q17.618 8.178 18.626 6.960Q19.634 5.742 19.970 4.608L19.970 4.608Q20.306 3.138 18.794 2.256L18.794 2.256Q17.534 1.626 15.644 1.836L15.644 1.836Q12.116 2.382 8.546 4.608L8.546 4.608Q6.572 5.700 5.522 7.086L5.522 7.086Q5.144 7.632 4.598 8.682L4.598 8.682Q4.304 9.480 4.430 10.236Q4.556 10.992 5.144 11.538L5.144 11.538Q6.194 12.588 6.698 13.008L6.698 13.008L9.176 15.108L9.344 15.150Q9.512 15.192 9.596 15.108L9.596 15.108Q10.100 14.772 10.646 14.562L10.646 14.562Q12.620 13.470 14.300 13.134L14.300 13.134Q14.300 13.428 14.342 13.533Q14.384 13.638 14.510 13.722L14.510 13.722L14.594 13.806L14.594 13.680ZM8.924 17.082L8.924 17.082Q7.160 18.048 5.900 19.308L5.900 19.308Q5.186 20.022 4.976 20.988L4.976 20.988Q4.892 21.576 5.228 21.870Q5.564 22.164 6.194 22.038L6.194 22.038Q7.664 21.702 8.546 20.358L8.546 20.358Q9.008 19.602 9.092 18.804L9.092 18.804Q9.134 18.132 8.924 17.082Z\"/>',\n\t'seti:spring':\n\t\t'<path d=\"M21.571 1.813L21.655 1.603Q22.915 5.005 23.377 7.483L23.377 7.483Q24.007 10.675 23.629 13.531L23.629 13.531Q23.041 18.067 19.555 21.175L19.555 21.175Q17.749 22.561 15.775 23.275L15.775 23.275Q13.675 24.031 11.575 23.905L11.575 23.905Q6.451 23.737 3.154 19.873Q-0.143 16.009 0.277 11.053L0.277 11.053Q0.487 7.231 3.049 4.501L3.049 4.501Q5.569 1.057 10.651 0.175L10.651 0.175Q15.187-0.413 19.177 2.527L19.177 2.527Q19.975 3.199 20.227 3.283L20.227 3.283Q20.563 3.451 20.815 3.157L20.815 3.157Q20.983 2.947 21.403 1.981L21.403 1.981Q21.487 1.981 21.571 1.813L21.571 1.813ZM20.605 9.205L20.605 9.205Q19.723 13.153 15.775 16.429L15.775 16.429Q14.767 17.269 13.339 17.941L13.339 17.941Q12.457 18.361 10.609 19.033L10.609 19.033L9.853 19.327Q9.601 19.411 9.076 19.600Q8.551 19.789 8.299 19.873L8.299 19.873Q8.299 20.125 8.425 20.251L8.425 20.251Q9.223 20.209 10.903 20.167L10.903 20.167Q13.045 20.125 14.095 20.083L14.095 20.083Q15.817 19.957 17.203 19.705L17.203 19.705Q19.807 19.327 21.214 17.857Q22.621 16.387 23.125 13.699L23.125 13.699Q23.629 10.507 22.579 6.727L22.579 6.727Q22.369 6.055 21.991 4.669L21.991 4.669L21.529 3.031Q21.193 3.577 21.025 3.577L21.025 3.577Q19.471 5.971 16.153 7.525L16.153 7.525Q15.187 7.945 13.843 8.155L13.843 8.155Q13.003 8.281 11.365 8.365L11.365 8.365L10.525 8.449Q7.585 8.659 5.905 10.003L5.905 10.003Q4.897 10.759 4.288 12.019Q3.679 13.279 3.658 14.602Q3.637 15.925 4.225 16.975L4.225 16.975Q4.687 17.941 5.317 18.214Q5.947 18.487 6.955 18.277L6.955 18.277Q7.291 18.193 7.942 18.004Q8.593 17.815 8.929 17.731L8.929 17.731Q13.045 16.681 15.775 14.833L15.775 14.833Q18.841 12.691 20.605 9.205ZM4.729 18.781L4.729 18.781Q4.351 18.781 4.078 19.117Q3.805 19.453 3.805 19.852Q3.805 20.251 4.141 20.503Q4.477 20.755 4.855 20.755Q5.233 20.755 5.506 20.440Q5.779 20.125 5.779 19.663Q5.779 19.201 5.548 18.991Q5.317 18.781 4.729 18.781Z\"/>',\n\t'seti:slim':\n\t\t'<path d=\"M0.135 20.463L0.135 20.463L0.135 3.663Q0.135 3.579 0.198 3.516Q0.261 3.453 0.261 3.411L0.261 3.411Q0.723 1.731 1.983 1.017L1.983 1.017L3.537 0.261L20.337 0.261Q20.421 0.261 20.484 0.324Q20.547 0.387 20.589 0.387L20.589 0.387Q22.311 0.975 23.361 2.613L23.361 2.613Q23.571 3.201 23.865 3.537L23.865 3.537L23.865 20.337Q23.865 20.379 23.802 20.463Q23.739 20.547 23.739 20.589L23.739 20.589Q23.319 22.017 22.059 22.941L22.059 22.941Q21.219 23.571 20.337 23.739L20.337 23.739L3.537 23.739Q3.453 23.739 3.390 23.676Q3.327 23.613 3.285 23.613L3.285 23.613Q1.563 23.025 0.513 21.387L0.513 21.387Q0.387 21.261 0.135 20.463ZM3.789 6.687L3.789 6.687Q4.209 7.191 5.049 8.241L5.049 8.241Q6.435 10.047 7.233 10.887L7.233 10.887Q7.317 10.971 7.485 11.055Q7.653 11.139 7.737 11.139L7.737 11.139L16.137 11.139Q16.221 11.139 16.410 11.076Q16.599 11.013 16.683 11.013L16.683 11.013L17.901 9.459Q19.329 7.653 20.085 6.813L20.085 6.813Q18.111 4.461 15.087 3.579Q12.063 2.697 9.081 3.453L9.081 3.453Q5.889 4.251 3.789 6.687ZM14.037 6.015L14.961 4.713Q14.709 5.553 14.310 7.401Q13.911 9.249 13.659 10.089L13.659 10.089L11.139 10.089Q12.231 8.703 14.037 6.015L14.037 6.015Z\"/>',\n\t'seti:smarty':\n\t\t'<path d=\"M11.882 16.016L11.882 16.016L9.572 16.016Q8.977 16.016 8.837 15.911Q8.697 15.806 8.592 15.246L8.592 15.246Q8.102 12.761 6.947 10.661L6.947 10.661Q6.387 9.681 5.862 8.246L5.862 8.246Q5.232 6.461 5.547 4.501L5.547 4.501Q5.687 3.241 6.439 2.279Q7.192 1.316 8.382 0.791L8.382 0.791Q10.202-0.014 12.057 0.004Q13.912 0.021 15.697 0.896L15.697 0.896Q17.342 1.631 18.059 3.206Q18.777 4.781 18.462 6.706L18.462 6.706Q18.287 7.826 17.832 8.981L17.832 8.981Q17.517 9.786 16.817 11.081L16.817 11.081Q15.662 13.391 15.382 15.141L15.382 15.141Q15.277 15.666 15.189 15.841Q15.102 16.016 14.909 16.069Q14.717 16.121 14.192 16.121L14.192 16.121Q13.317 16.016 11.882 16.016ZM11.777 23.996L11.777 23.996L9.152 23.996Q8.802 23.996 8.644 23.856Q8.487 23.716 8.487 23.331L8.487 23.331L8.487 22.876Q8.487 22.386 8.522 22.229Q8.557 22.071 8.749 22.036Q8.942 22.001 9.502 22.001L9.502 22.001L14.507 22.036Q14.892 22.001 15.032 22.159Q15.172 22.316 15.172 22.666L15.172 22.666Q15.172 23.331 15.119 23.559Q15.067 23.786 14.839 23.839Q14.612 23.891 13.947 23.891L13.947 23.891Q13.317 23.996 11.777 23.996ZM14.227 20.041L11.882 20.041L9.152 20.041Q8.837 20.041 8.714 19.936Q8.592 19.831 8.592 19.516L8.592 19.516L8.592 19.201Q8.592 18.641 8.627 18.466Q8.662 18.291 8.837 18.239Q9.012 18.186 9.537 18.186L9.537 18.186L14.507 18.186Q15.172 18.186 15.172 18.851L15.172 18.851L15.172 19.131Q15.172 19.656 15.137 19.814Q15.102 19.971 14.927 20.006Q14.752 20.041 14.227 20.041L14.227 20.041Z\"/>',\n\t'seti:sbt':\n\t\t'<path d=\"M23.839 13.145L23.877 13.145Q23.877 13.677 23.687 14.551L23.687 14.551L23.611 14.817L7.955 14.817Q6.397 14.817 6.397 16.375L6.397 16.375Q6.397 16.983 6.834 17.325Q7.271 17.667 8.069 17.667L8.069 17.667L22.547 17.667Q21.141 20.365 18.842 21.923Q16.543 23.481 13.484 23.842Q10.425 24.203 7.537 23.025L7.537 23.025Q4.763 21.847 2.825 19.529Q0.887 17.211 0.355 14.209L0.355 14.209Q-0.253 11.131 0.735 8.205L0.735 8.205Q1.723 5.355 3.927 3.284Q6.131 1.213 9.133 0.453L9.133 0.453Q11.603-0.193 14.130 0.263Q16.657 0.719 18.785 2.163Q20.913 3.607 22.205 5.773L22.205 5.773Q21.939 5.925 21.255 5.925L21.255 5.925L13.541 5.925Q12.895 5.925 12.496 6.324Q12.097 6.723 12.097 7.350Q12.097 7.977 12.439 8.300Q12.781 8.623 13.427 8.623L13.427 8.623L23.383 8.623Q23.725 9.535 23.725 10.409L23.725 10.409L10.805 10.409Q9.133 10.409 9.133 11.853L9.133 11.853Q9.133 12.461 9.570 12.803Q10.007 13.145 10.805 13.145L10.805 13.145L22.775 13.145Q23.307 12.993 23.839 13.145L23.839 13.145Z\"/>',\n\t'seti:scala':\n\t\t'<path d=\"M19.350 0.198L19.350 0.198L19.350 5.700L19.224 5.826Q19.182 5.952 19.098 5.952L19.098 5.952Q18.510 6.540 17.628 6.750L17.628 6.750Q16.662 7.086 14.520 7.548L14.520 7.548L13.974 7.674Q11.622 8.094 7.800 8.598L7.800 8.598Q5.700 8.850 4.650 9.102L4.650 9.102L4.650 3.600Q4.776 3.474 5.196 3.474L5.196 3.474Q8.220 2.970 10.152 2.802L10.152 2.802L11.622 2.592Q13.596 2.298 14.604 2.088L14.604 2.088Q16.242 1.794 17.502 1.374L17.502 1.374L17.670 1.290Q18.300 1.038 18.594 0.870L18.594 0.870Q19.098 0.576 19.350 0.198ZM19.350 7.548L19.350 7.548L19.350 13.050L19.098 13.302Q18.510 13.764 17.250 14.268L17.250 14.268L16.998 14.352Q14.898 14.898 13.848 15.024L13.848 15.024Q13.176 15.150 11.937 15.339Q10.698 15.528 10.026 15.654L10.026 15.654L8.304 15.906Q5.910 16.200 4.650 16.452L4.650 16.452L4.650 10.950Q4.776 10.824 5.028 10.824L5.028 10.824Q6.162 10.698 8.388 10.362Q10.614 10.026 11.748 9.900L11.748 9.900Q14.772 9.480 17.376 8.724L17.376 8.724Q18.090 8.472 18.468 8.262L18.468 8.262Q18.972 7.968 19.350 7.548ZM5.028 23.760L4.776 23.802L4.650 23.802L4.650 18.300Q4.776 18.174 5.028 18.174L5.028 18.174Q5.826 18.048 7.338 17.838Q8.850 17.628 9.648 17.502L9.648 17.502L11.034 17.292Q13.134 16.998 14.142 16.830L14.142 16.830Q15.864 16.494 17.250 16.074L17.250 16.074Q18.048 15.822 18.384 15.612L18.384 15.612Q19.014 15.318 19.350 14.898L19.350 14.898L19.350 20.400Q19.266 20.568 18.972 20.820L18.972 20.820L18.678 21.072Q17.712 21.534 16.200 21.954L16.200 21.954Q14.814 22.332 11.916 22.794L11.916 22.794L11.328 22.878Q7.926 23.424 6.246 23.550L6.246 23.550Q5.784 23.550 5.028 23.760L5.028 23.760Z\"/>',\n\t'seti:ethereum':\n\t\t'<path d=\"M6.740 12.560L7.460 13L11.940 15.640L12.060 15.640Q16.580 12.880 18.940 11.640L18.940 11.640L18.940 11.520Q18.100 10 16.940 8.240L16.940 8.240L12.060 0.120L11.060 1.760Q10.020 3.480 9.580 4.360L9.580 4.360Q9.260 4.920 8.560 6.060Q7.860 7.200 7.580 7.760L7.580 7.760Q6.060 10.240 5.300 11.360L5.300 11.360L5.300 11.520Q5.660 11.920 6.740 12.560L6.740 12.560ZM12.820 17.760L12.820 17.760Q12.700 17.800 12.380 18L12.380 18L11.940 18.240Q11.460 17.920 10.460 17.360Q9.460 16.800 8.920 16.500Q8.380 16.200 7.260 15.500Q6.140 14.800 5.580 14.480L5.580 14.480Q5.500 14.440 5.300 14.360L5.300 14.360L5.060 14.240L6.060 15.640Q7.820 18.120 8.820 19.360L8.820 19.360Q9.300 20.120 10.360 21.560Q11.420 23 11.940 23.760L11.940 23.760L11.940 23.800Q11.980 23.880 12.060 23.880L12.060 23.880L16.700 17.360Q17.060 16.880 17.820 15.820Q18.580 14.760 18.940 14.240L18.940 14.240Q15.500 16.080 12.820 17.760Z\"/>',\n\t'seti:stylus':\n\t\t'<path d=\"M20.197 0.406L20.197 0.406Q19.936 0.406 19.559 0.580L19.559 0.580Q19.037 0.754 18.515 1.218L18.515 1.218Q18.225 1.479 17.674 2.117L17.674 2.117Q16.079 3.828 14.948 6.119L14.948 6.119Q13.556 8.555 12.686 11.542L12.686 11.542Q12.309 12.992 12.309 13.920L12.309 13.920Q12.338 14.239 12.556 14.427Q12.773 14.616 13.034 14.558L13.034 14.558Q13.556 14.500 13.962 14.152L13.962 14.152Q14.223 13.920 14.658 13.427L14.658 13.427L14.774 13.282L14.774 12.992Q14.223 13.543 13.962 13.717L13.962 13.717Q13.759 13.862 13.629 13.804Q13.498 13.746 13.498 13.543L13.498 13.543L13.498 12.644Q13.498 11.223 14.136 9.454L14.136 9.454Q14.861 7.482 15.325 6.583L15.325 6.583Q16.427 4.176 17.848 2.755L17.848 2.755Q18.573 1.624 19.849 1.508L19.849 1.508Q20.197 1.508 20.400 1.595Q20.603 1.682 20.661 1.943L20.661 1.943Q20.661 2.030 20.748 2.030L20.748 2.030L20.806 2.001Q20.864 1.943 20.922 1.943L20.922 1.943Q21.125 1.392 21.125 1.116Q21.125 0.841 20.835 0.594Q20.545 0.348 20.197 0.406ZM11.149 9.570L11.149 9.570Q10.685 9.570 10.076 9.990Q9.467 10.411 9.235 10.817L9.235 10.817L9.235 11.020L9.322 10.991L9.438 10.904L9.496 10.817Q9.670 10.614 9.786 10.556L9.786 10.556Q9.960 10.382 10.076 10.324L10.076 10.324Q10.250 10.237 10.424 10.295Q10.598 10.353 10.642 10.556Q10.685 10.759 10.598 10.904L10.598 10.904Q10.511 11.890 10.221 13.572L10.221 13.572L10.134 13.920Q9.525 16.298 8.713 17.806L8.713 17.806Q7.495 20.213 5.987 21.605L5.987 21.605Q5.465 22.040 5.088 22.243L5.088 22.243Q4.566 22.504 4.073 22.504L4.073 22.504Q3.725 22.504 3.566 22.402Q3.406 22.301 3.348 21.982L3.348 21.982Q3.348 21.924 3.261 21.866L3.261 21.866L3.174 21.779Q2.768 22.330 2.913 22.968Q3.058 23.606 3.435 23.606L3.435 23.606Q3.899 23.606 4.450 23.432L4.450 23.432Q4.798 23.316 5.175 23.026L5.175 23.026Q5.378 22.852 5.784 22.446L5.784 22.446L5.987 22.243Q7.553 20.677 8.713 18.531L8.713 18.531Q10.482 15.486 11.410 12.180L11.410 12.180L11.468 12.035Q11.613 11.397 11.671 11.078L11.671 11.078Q11.758 10.556 11.700 10.092L11.700 10.092Q11.700 9.889 11.526 9.729Q11.352 9.570 11.149 9.570Z\"/>',\n\t'seti:svelte':\n\t\t'<path d=\"M10.924 1.248L5.422 4.734L10.924 1.248Q12.436 0.240 14.284 0.177Q16.132 0.114 17.812 0.933Q19.492 1.752 20.584 3.306L20.584 3.306Q21.382 4.398 21.676 5.721Q21.970 7.044 21.739 8.367Q21.508 9.690 20.794 10.740L20.794 10.740Q21.844 12.798 21.424 15.024L21.424 15.024Q21.214 16.242 20.542 17.292Q19.870 18.342 18.862 19.056L18.862 19.056L13.066 22.752Q11.554 23.760 9.706 23.823Q7.858 23.886 6.178 23.067Q4.498 22.248 3.406 20.694L3.406 20.694Q2.650 19.602 2.335 18.258Q2.020 16.914 2.272 15.612Q2.524 14.310 3.238 13.218L3.238 13.218Q2.146 11.202 2.566 8.976L2.566 8.976Q2.776 7.758 3.448 6.708Q4.120 5.658 5.128 4.944L5.128 4.944L10.924 1.248ZM18.316 4.776L18.316 4.776Q17.518 3.642 16.237 3.159Q14.956 2.676 13.612 3.012L13.612 3.012Q13.192 3.138 12.772 3.348L12.772 3.348L7.018 7.002Q6.304 7.422 5.863 8.094Q5.422 8.766 5.275 9.564Q5.128 10.362 5.317 11.160Q5.506 11.958 5.968 12.630L5.968 12.630Q6.766 13.764 8.047 14.226Q9.328 14.688 10.672 14.352L10.672 14.352Q11.092 14.226 11.512 14.016L11.512 14.016L13.864 12.546Q14.032 12.420 14.200 12.378L14.200 12.378Q14.620 12.294 15.019 12.420Q15.418 12.546 15.628 12.924L15.628 12.924Q15.922 13.302 15.838 13.806L15.838 13.806Q15.754 14.226 15.460 14.478L15.460 14.478L9.832 18.090Q9.664 18.216 9.496 18.258L9.496 18.258Q9.076 18.342 8.698 18.195Q8.320 18.048 8.089 17.733Q7.858 17.418 7.858 17.082L7.858 17.082L7.858 16.704L7.648 16.620Q6.682 16.326 5.842 15.780L5.842 15.780L5.212 15.360L5.128 15.654Q5.044 15.906 5.002 16.200L5.002 16.200Q4.834 16.998 5.023 17.796Q5.212 18.594 5.674 19.224L5.674 19.224Q6.430 20.316 7.627 20.799Q8.824 21.282 10.126 21.030L10.126 21.030L10.378 20.988Q10.840 20.862 11.218 20.652L11.218 20.652L17.014 16.998Q17.686 16.578 18.127 15.906Q18.568 15.234 18.715 14.436Q18.862 13.638 18.673 12.840Q18.484 12.042 18.022 11.370L18.022 11.370Q17.224 10.236 15.943 9.774Q14.662 9.312 13.318 9.648L13.318 9.648Q12.898 9.774 12.478 9.984L12.478 9.984L10.126 11.454Q9.958 11.580 9.790 11.622L9.790 11.622Q9.370 11.706 8.992 11.580Q8.614 11.454 8.362 11.076L8.362 11.076Q8.068 10.698 8.152 10.194L8.152 10.194Q8.236 9.774 8.530 9.522L8.530 9.522L14.158 5.910Q14.326 5.784 14.494 5.742L14.494 5.742Q14.914 5.658 15.292 5.805Q15.670 5.952 15.901 6.267Q16.132 6.582 16.132 6.918L16.132 6.918L16.132 7.296L16.342 7.380Q17.308 7.674 18.148 8.220L18.148 8.220L18.778 8.640L18.862 8.346Q18.946 8.052 18.988 7.800L18.988 7.800Q19.156 7.002 18.967 6.204Q18.778 5.406 18.316 4.776Z\"/>',\n\t'seti:swift':\n\t\t'<path d=\"M4.419 7.307L4.419 7.307Q4.419 7.231 4.362 7.174Q4.305 7.117 4.305 7.041L4.305 7.041Q4.191 6.889 3.887 6.528Q3.583 6.167 3.469 5.977L3.469 5.977Q3.013 5.559 2.861 5.331L2.861 5.331Q2.557 4.989 2.405 4.685L2.405 4.685Q2.861 5.179 3.583 5.635L3.583 5.635Q3.697 5.711 3.887 5.882Q4.077 6.053 4.191 6.091L4.191 6.091Q4.419 6.281 4.837 6.642Q5.255 7.003 5.483 7.193L5.483 7.193Q5.825 7.497 6.585 8.067L6.585 8.067L7.041 8.371Q8.219 9.321 8.827 9.663L8.827 9.663Q9.283 10.043 10.233 10.689Q11.183 11.335 11.677 11.677L11.677 11.677L11.677 11.563Q10.233 9.929 9.397 9.093L9.397 9.093Q8.447 7.877 7.991 7.421L7.991 7.421L6.813 5.863Q6.813 5.825 6.737 5.768Q6.661 5.711 6.661 5.635L6.661 5.635L5.597 4.191L5.445 3.925Q5.217 3.621 5.141 3.393L5.141 3.393Q5.445 3.621 5.977 4.153Q6.509 4.685 6.794 4.951Q7.079 5.217 7.744 5.806Q8.409 6.395 8.675 6.699L8.675 6.699Q8.941 6.889 9.416 7.307Q9.891 7.725 10.119 7.877L10.119 7.877L11.563 8.941Q11.715 9.093 12.019 9.321Q12.323 9.549 12.475 9.663L12.475 9.663Q15.097 11.563 16.541 12.513L16.541 12.513Q16.617 12.627 16.693 12.608Q16.769 12.589 16.769 12.399L16.769 12.399Q17.377 10.917 17.377 8.713L17.377 8.713Q17.377 7.117 16.769 5.521L16.769 5.521Q16.769 5.293 16.541 4.571L16.541 4.571L16.351 4.191Q15.971 3.393 15.705 3.013L15.705 3.013Q15.553 2.405 14.869 1.721L14.869 1.721Q14.869 1.645 14.812 1.588Q14.755 1.531 14.755 1.493L14.755 1.493Q15.211 1.721 15.933 2.177L15.933 2.177Q18.479 4.077 20.341 6.813L20.341 6.813L20.607 7.383Q21.215 8.523 21.405 9.093L21.405 9.093Q22.241 11.145 22.241 12.893L22.241 12.893L22.241 13.007Q22.241 14.071 22.127 14.527L22.127 14.527Q22.051 14.793 21.994 15.268Q21.937 15.743 21.899 15.971Q21.861 16.199 21.975 16.313L21.975 16.313Q23.419 18.099 23.761 20.227L23.761 20.227Q23.875 20.607 23.875 21.443L23.875 21.443Q23.875 22.013 23.761 22.393L23.761 22.393Q23.761 22.469 23.761 22.488Q23.761 22.507 23.666 22.507Q23.571 22.507 23.571 22.431L23.571 22.431L23.533 22.393Q22.735 20.759 21.291 20.607L21.291 20.607Q20.607 20.417 19.923 20.569L19.923 20.569Q19.391 20.645 18.669 20.949L18.669 20.949Q18.403 21.063 17.947 21.291L17.947 21.291Q17.377 21.595 17.111 21.671L17.111 21.671Q14.603 22.735 11.905 22.393L11.905 22.393Q9.929 22.165 8.333 21.557L8.333 21.557L8.219 21.557Q7.877 21.367 7.155 21.063Q6.433 20.759 6.091 20.607L6.091 20.607Q2.633 18.707 0.277 15.249L0.277 15.249L0.125 15.135L0.277 15.135L0.391 15.249Q1.075 15.743 1.569 15.971L1.569 15.971L2.025 16.237Q2.671 16.617 2.975 16.693L2.975 16.693Q3.849 17.187 5.027 17.377L5.027 17.377Q5.065 17.453 5.122 17.453Q5.179 17.453 5.255 17.529L5.255 17.529Q6.851 17.985 9.055 17.985L9.055 17.985Q11.487 17.795 13.311 16.807L13.311 16.807Q13.501 16.693 13.501 16.674Q13.501 16.655 13.311 16.579L13.311 16.579Q12.779 16.161 11.715 15.211L11.715 15.211L10.955 14.527Q10.651 14.299 10.119 13.767Q9.587 13.235 9.283 13.007L9.283 13.007Q8.827 12.627 8.105 11.677L8.105 11.677Q7.991 11.525 7.687 11.221Q7.383 10.917 7.269 10.727L7.269 10.727Q7.041 10.499 6.566 9.967Q6.091 9.435 5.882 9.188Q5.673 8.941 5.312 8.542Q4.951 8.143 4.761 7.877L4.761 7.877Q4.761 7.649 4.419 7.307Z\"/>',\n\t'seti:db':\n\t\t'<path d=\"M12.546 9.327L12.546 9.327Q9.858 9.327 7.884 9.201L7.884 9.201Q5.448 9.033 3.348 8.571L3.348 8.571Q2.844 8.487 1.920 8.109L1.920 8.109Q1.416 7.857 1.122 7.773L1.122 7.773Q0.576 7.605 0.576 6.975L0.576 6.975L0.576 2.397Q0.576 2.187 0.681 2.019Q0.786 1.851 0.954 1.851L0.954 1.851L1.374 1.683Q2.802 1.053 3.600 0.927L3.600 0.927Q5.784 0.423 8.472 0.255L8.472 0.255Q10.068 0.171 13.302 0.171L13.302 0.171Q17.712 0.171 21.198 1.095L21.198 1.095Q21.996 1.263 22.878 1.725L22.878 1.725Q23.424 1.893 23.424 2.523L23.424 2.523L23.424 7.101Q23.424 7.395 23.046 7.773L23.046 7.773Q21.996 8.277 21.450 8.445L21.450 8.445L19.602 8.739Q17.166 9.117 15.948 9.201L15.948 9.201Q15.360 9.285 14.205 9.285Q13.050 9.285 12.546 9.327ZM23.424 9.621L23.424 9.621L23.424 14.325Q23.424 14.535 23.193 14.766Q22.962 14.997 22.752 14.997L22.752 14.997L22.080 15.249Q20.820 15.669 20.274 15.795L20.274 15.795Q15.360 16.635 8.976 16.425L8.976 16.425L8.892 16.425Q6.792 16.299 5.700 16.173L5.700 16.173Q3.936 15.963 2.550 15.501L2.550 15.501Q2.004 15.375 0.996 14.871L0.996 14.871Q0.576 14.661 0.576 14.199L0.576 14.199L0.576 9.621Q3.180 10.503 6.288 10.839L6.288 10.839Q8.514 11.049 12 11.049L12 11.049Q15.150 11.175 17.712 10.881L17.712 10.881Q20.736 10.545 23.424 9.621ZM23.424 16.971L23.424 16.971L23.424 21.549Q23.424 21.759 23.193 21.990Q22.962 22.221 22.752 22.347L22.752 22.347Q21.702 22.725 21.198 22.893L21.198 22.893Q20.316 23.187 19.602 23.271L19.602 23.271Q11.580 24.447 3.726 23.145L3.726 23.145Q3.054 23.019 1.710 22.557L1.710 22.557L1.122 22.347Q0.828 22.263 0.702 22.053Q0.576 21.843 0.576 21.423L0.576 21.423L0.576 16.971Q3.096 17.853 6.204 18.189L6.204 18.189Q8.430 18.399 12 18.399L12 18.399Q15.486 18.399 17.712 18.189L17.712 18.189Q20.820 17.853 23.424 16.971Z\"/>',\n\t'seti:terraform':\n\t\t'<path d=\"M8.850 11.880L8.850 4.200L15.150 8.040L15.150 15.720L8.850 11.880ZM22.590 4.200L15.900 8.040L15.900 15.720L22.590 11.880L22.590 4.200ZM8.100 3.840L1.410 0.120L1.410 7.560L8.100 11.280L8.100 3.840ZM8.850 12.750L8.850 20.160L15.150 23.880L15.150 16.470L8.850 12.750Z\"/>',\n\t'seti:tex':\n\t\t'<path d=\"M3.651 6.951L3.651 13.650L2.199 13.650L2.199 14.970L6.753 14.970L6.753 13.650L5.400 13.650L5.400 6.951L5.829 6.951Q6.456 6.951 6.753 7.017Q7.050 7.083 7.198 7.281Q7.347 7.479 7.479 7.974L7.479 7.974L8.700 7.974L8.502 5.598L0.450 5.598L0.153 9.327L1.374 9.327L1.374 8.799Q1.473 7.842 1.605 7.545L1.605 7.545Q1.770 7.149 2.100 7.050L2.100 7.050Q2.397 6.951 3.222 6.951L3.222 6.951L3.651 6.951ZM11.802 17.346L11.802 17.346L10.053 17.346L10.053 14.475L10.647 14.475Q11.076 14.475 11.191 14.508Q11.307 14.541 11.340 14.706Q11.373 14.871 11.373 15.399L11.373 15.399L11.373 15.927L12.627 15.927L12.627 11.670L11.373 11.670L11.373 12.198Q11.373 12.726 11.340 12.891Q11.307 13.056 11.191 13.089Q11.076 13.122 10.647 13.122L10.647 13.122L10.053 13.122L10.053 10.647L11.703 10.647Q12.462 10.647 12.809 10.812Q13.155 10.977 13.287 11.373L13.287 11.373Q13.386 11.703 13.452 12.495L13.452 12.495L14.673 12.495L14.376 9.195L7.149 9.195L7.149 10.548L8.172 10.548L8.172 17.148L7.149 17.148L7.149 18.501L14.475 18.501L14.871 15.795L13.551 15.795Q13.551 16.686 13.171 17.016Q12.792 17.346 11.802 17.346ZM17.874 14.970L17.874 13.749L17.379 13.749L18.897 11.472L20.448 13.749L20.151 13.749L20.151 14.871L23.847 14.871L23.847 13.650L23.352 13.650Q22.890 13.650 22.791 13.617Q22.692 13.584 22.626 13.452L22.626 13.452L20.151 9.822L21.801 7.446L21.900 7.347Q22.098 7.116 22.263 7.017L22.263 7.017Q22.527 6.852 22.923 6.852L22.923 6.852L23.451 6.852L23.451 5.499L20.250 5.499L20.250 6.720L20.778 6.720L20.679 6.852L19.425 8.601L18.072 6.951L18.402 6.951L18.402 5.499L14.673 5.499L14.673 6.951L15.201 6.951Q15.663 6.951 15.745 6.967Q15.828 6.984 15.927 7.149L15.927 7.149L18.072 10.350L16.224 13.122L15.927 13.452Q15.597 13.749 15.102 13.749L15.102 13.749L14.574 13.749L14.574 14.970L17.874 14.970Z\"/>',\n\t'seti:default':\n\t\t'<path d=\"M1.082 16.876L1.082 14.014L22.918 14.014L22.918 16.876L1.082 16.876ZM1.082 9.986L1.082 7.071L13.272 7.071L13.272 9.986L1.082 9.986ZM1.082 3.096L1.082 0.181L22.918 0.181L22.918 3.096L1.082 3.096ZM1.082 23.819L1.082 20.904L17.300 20.904L17.300 23.819L1.082 23.819Z\"/>',\n\t'seti:twig':\n\t\t'<path d=\"M17.640 0.080L17.640 0.120Q17.680 0.120 17.760 0.120L17.760 0.120L18.080 0.120L18.080 0.120L17.640 0.120L17.640 0.080ZM16.680 0.720L16.360 0.720Q17.080 0.160 17.640 0.120L17.640 0.120L18.160 0.120Q18.280 0.120 18.280 0.160L18.280 0.160L17.920 0.160Q17.400 0.160 16.680 0.720L16.680 0.720ZM16.680 0.720L16.680 0.720Q17.400 0.160 17.920 0.160L17.920 0.160Q17.360 0.360 17.040 0.640L17.040 0.640Q16.880 0.760 16.640 1.040L16.640 1.040L16.440 1.200Q16.240 1.160 16.080 1.320L16.080 1.320Q16.320 1 16.680 0.720ZM17.920 0.160L17.920 0.160Q18.040 0.160 18.280 0.160L18.280 0.160Q18.920 0.280 19.560 1.040L19.560 1.040L19.520 1.120Q18.880 0.480 18.240 0.360L18.240 0.360Q18.080 0.320 18 0.360L18 0.360Q17.640 0.440 17.400 0.600L17.400 0.600Q17.280 0.640 17.120 0.800L17.120 0.800L17 0.920Q16.880 0.840 17 0.680L17 0.680L17.040 0.640Q17.360 0.360 17.920 0.160ZM18 0.360L18 0.360Q18.080 0.320 18.240 0.360L18.240 0.360Q18.120 0.440 18 0.360ZM16 1.800L16.440 1.200Q16.480 1.160 16.640 1.040L16.640 1.040Q16.880 0.760 17.040 0.640L17.040 0.640L17 0.680Q16.880 0.840 17 0.920L17 0.920Q16.600 1.440 16 2.560L16 2.560L15.960 2.680Q15.920 2.800 15.840 2.840L15.840 2.840L15.880 2.680L15.520 2.640L15.520 2.600L16 1.800ZM15.440 1.640L15.480 1.520Q15.600 1.440 15.800 1.200L15.800 1.200Q16.160 0.840 16.360 0.720L16.360 0.720L16.680 0.720Q16.360 0.960 16.080 1.320L16.080 1.320L15.840 1.600Q15.760 1.640 15.600 1.560L15.600 1.560L15.560 1.520L15.520 1.560Q15.480 1.640 15.440 1.640L15.440 1.640ZM19.520 1.120L19.560 1.040Q19.680 1.160 19.720 1.280L19.720 1.280L19.720 1.320L19.640 1.240Q19.520 1.160 19.520 1.120L19.520 1.120ZM15.840 1.600L16.080 1.320Q16.240 1.160 16.440 1.200L16.440 1.200L16 1.800L15.840 1.920L15.640 1.880L15.840 1.600ZM19.720 1.320L19.720 1.280Q19.840 1.360 19.880 1.520L19.880 1.520Q19.760 1.440 19.720 1.320L19.720 1.320ZM15.520 1.560L15.560 1.520Q15.560 1.560 15.600 1.560L15.600 1.560Q15.760 1.640 15.840 1.600L15.840 1.600L15.640 1.880Q15.280 2.440 14.960 3L14.960 3L14.600 3.760L14.080 3.720L13.920 3.880Q14.480 2.800 15.240 1.840L15.240 1.840L15.280 1.760Q15.360 1.640 15.420 1.640Q15.480 1.640 15.520 1.560L15.520 1.560ZM15.640 1.880L15.840 1.920Q15.880 1.920 15.920 1.880L15.920 1.880L16 1.800L15.560 2.600L15.400 2.680L15.240 2.640Q15.200 2.680 15.120 2.840Q15.040 3 14.960 3L14.960 3Q15.280 2.400 15.640 1.880L15.640 1.880ZM15.240 2.640L15.400 2.680Q15.440 2.680 15.480 2.640L15.480 2.640L15.560 2.600L15.520 2.640Q15.320 3.040 14.960 3.920L14.960 3.920L14.720 4.560Q14.520 5.040 14.480 5.280L14.480 5.280L14.440 5.400Q14.360 5.440 14.280 5.640Q14.200 5.840 14.120 5.880Q14.040 5.920 13.980 5.780Q13.920 5.640 13.840 5.640L13.840 5.640L13.920 5.280L14.560 3.760L14.960 3Q15.040 3 15.120 2.840Q15.200 2.680 15.240 2.640L15.240 2.640ZM15.520 2.640L15.520 2.640Q15.760 2.640 15.880 2.680L15.880 2.680L15.840 2.840Q15.320 4.080 14.880 5.360L14.880 5.360L14.800 5.320Q14.560 5.320 14.480 5.280L14.480 5.280Q14.520 5.040 14.720 4.560L14.720 4.560L14.960 3.920Q15.320 3.040 15.520 2.640ZM13.920 3.880L13.920 3.880Q13.960 3.840 14.040 3.760L14.040 3.760L14.080 3.720L14.560 3.760L13.920 5.280Q13.880 5.320 13.840 5.420Q13.800 5.520 13.760 5.520L13.760 5.520L13.720 5.440Q13.560 5.240 13.560 5.120L13.560 5.120Q13.440 5.080 13.400 5.180Q13.360 5.280 13.300 5.460Q13.240 5.640 13.160 5.720L13.160 5.720Q13.160 5.600 13.240 5.400L13.240 5.400L13.360 5.120Q13.720 4.280 13.920 3.880ZM13.400 5.200L13.400 5.200Q13.440 5.080 13.560 5.120L13.560 5.120Q13.560 5.240 13.720 5.440L13.720 5.440L13.760 5.520Q13.800 5.520 13.840 5.420Q13.880 5.320 13.920 5.280L13.920 5.280L13.840 5.640Q13.400 6.800 13.240 7.480L13.240 7.480Q13.120 7.840 12.960 8.560L12.960 8.560L12.840 9.160L12.760 9.200Q12.640 9.280 12.560 9.280L12.560 9.280Q12.240 9.440 12.160 9.720L12.160 9.720Q12.160 9.640 12.160 9.560L12.160 9.560Q12.560 7.520 13.080 5.880L13.080 5.880L13.160 5.720Q13.240 5.640 13.300 5.460Q13.360 5.280 13.400 5.200ZM14.240 5.920L14.480 5.280Q14.560 5.320 14.800 5.320L14.800 5.320L14.880 5.360Q14.800 5.400 14.800 5.520L14.800 5.520L14.800 5.560Q14.760 5.800 14.600 6.280L14.600 6.280L14.440 6.760Q14.240 7.520 14.040 8.320L14.040 8.320L14.040 8.400Q14.040 8.440 14 8.440L14 8.440Q13.840 8.440 13.640 8.440L13.640 8.440Q13.560 8.520 13.480 8.680L13.480 8.680Q13.560 8.240 13.800 7.400L13.800 7.400L13.920 6.920Q14.040 6.560 14.240 5.920L14.240 5.920ZM14.800 5.560L14.800 5.560Q14.800 5.560 14.800 5.520L14.800 5.520Q14.800 5.400 14.880 5.360L14.880 5.360L14.840 5.400Q14.840 5.560 14.800 5.560ZM14.160 5.920L14.160 5.920Q14.200 5.840 14.280 5.640Q14.360 5.440 14.440 5.400L14.440 5.400L13.920 6.920Q13.880 6.960 13.760 7.120L13.760 7.120L13.760 7.200Q13.640 7.200 13.580 7.180Q13.520 7.160 13.520 7.100Q13.520 7.040 13.440 7.080L13.440 7.080L13.440 7.120Q13.360 7.400 13.240 7.480L13.240 7.480Q13.400 6.800 13.840 5.640L13.840 5.640Q13.920 5.640 13.980 5.780Q14.040 5.920 14.160 5.920ZM13.760 7.200L13.760 7.200Q13.760 7.160 13.760 7.120L13.760 7.120Q13.880 6.960 13.920 6.920L13.920 6.920L13.800 7.440Q13.560 8.240 13.480 8.680L13.480 8.680L13.320 9.240Q13.240 9.360 13.180 9.600Q13.120 9.840 13.080 9.920L13.080 9.920L13 9.960Q12.800 10 12.720 10.040L12.720 10.040L12.640 10.440Q12.640 10.320 12.660 10.160Q12.680 10 12.760 9.660Q12.840 9.320 12.840 9.160L12.840 9.160L12.960 8.560Q13.120 7.840 13.240 7.480L13.240 7.480Q13.360 7.400 13.440 7.120L13.440 7.120L13.440 7.080Q13.520 7.040 13.520 7.100Q13.520 7.160 13.580 7.180Q13.640 7.200 13.760 7.200ZM13.640 8.440L13.640 8.440Q13.840 8.440 14 8.440L14 8.440L13.840 9.480Q13.640 10.440 13.560 10.920L13.560 10.920L13.560 10.920Q13.520 10.760 13.600 10.440L13.600 10.440L13.600 10.400Q13.520 10.360 13.360 10.420Q13.200 10.480 13.140 10.500Q13.080 10.520 13.080 10.600L13.080 10.600L13.040 10.640Q13.080 10.360 13.200 9.840L13.200 9.840L13.320 9.240L13.480 8.680Q13.560 8.520 13.640 8.440ZM12.560 9.280L12.560 9.280Q12.640 9.280 12.760 9.200L12.760 9.200L12.840 9.160Q12.840 9.320 12.760 9.660Q12.680 10 12.640 10.200L12.640 10.200Q12.360 10.120 12.080 10.200L12.080 10.200Q12.080 10.080 12.120 9.940Q12.160 9.800 12.160 9.720L12.160 9.720Q12.240 9.440 12.560 9.280ZM13 9.960L13.080 9.920Q13.120 9.840 13.180 9.600Q13.240 9.360 13.320 9.240L13.320 9.240L13.200 9.840Q13.080 10.360 13.040 10.640L13.040 10.640L13.040 10.800L13 10.680Q12.960 10.680 12.960 10.760Q12.960 10.840 12.940 10.880Q12.920 10.920 12.760 10.880L12.760 10.880L12.640 10.880L12.560 11.040L12.560 10.800L12.720 10.040Q12.800 10 13 9.960L13 9.960ZM12.080 10.200L12.080 10.200Q12.400 10.120 12.640 10.200L12.640 10.200Q12.640 10.320 12.640 10.440L12.640 10.440L12.560 10.800Q12.480 10.840 12.320 10.820Q12.160 10.800 12.080 10.840Q12 10.880 11.960 11.040L11.960 11.040L11.960 11.080Q11.960 10.640 12.080 10.200ZM13.120 10.480L13.120 10.480Q13.200 10.480 13.360 10.420Q13.520 10.360 13.600 10.400L13.600 10.400L13.600 10.480Q13.520 10.760 13.560 10.920L13.560 10.920Q13.520 11.200 13.480 11.680L13.480 11.680L13.440 11.680Q13.480 11.560 13.360 11.440L13.360 11.440L13 11.440L12.880 11.720L12.920 11.360L13.080 10.560Q13.120 10.520 13.120 10.480ZM12.960 10.840L12.960 10.840Q12.960 10.840 12.960 10.760Q12.960 10.680 13 10.680L13 10.680L13.040 10.800L12.920 11.360L12.800 11.600L12.560 11.560L12.440 11.760L12.480 11.320Q12.520 11.120 12.560 11.040L12.560 11.040L12.640 10.880L12.760 10.880Q12.920 10.920 12.960 10.840ZM12.080 10.840L12.080 10.840Q12.160 10.800 12.320 10.820Q12.480 10.840 12.560 10.800L12.560 10.800L12.560 11.040Q12.520 11.120 12.480 11.320L12.480 11.320L12.440 11.520Q12.360 11.600 12.160 11.560L12.160 11.560L12 11.520L11.840 11.800Q11.840 11.440 11.960 11.080L11.960 11.080L11.960 11.040Q12 10.880 12.080 10.840ZM16 11L16 11Q16.040 11 16.080 10.960L16.080 10.960Q16.240 10.840 16.320 10.880L16.320 10.880Q14.800 12.480 14.240 15.560L14.240 15.560L14.200 15.520Q14.040 15.480 13.880 15.480L13.880 15.480L13.880 15.320Q14.160 14.200 14.360 13.640L14.360 13.640Q14.640 12.720 15.080 12.040L15.080 12.040L15.120 12Q15.200 11.880 15.180 11.820Q15.160 11.760 15.240 11.640L15.240 11.640L15.400 11.520Q15.760 11.160 16 11ZM12.560 11.560L12.800 11.600Q12.800 11.600 12.800 11.560L12.800 11.560L12.920 11.360L12.880 11.720Q12.840 11.800 12.760 12L12.760 12L12.720 12.160L12.480 12.120L12.360 12.400L12.360 12.360Q12.400 12.040 12.440 11.760L12.440 11.760L12.560 11.560ZM12.880 11.720L13 11.440L13.360 11.440Q13.480 11.560 13.440 11.680Q13.400 11.800 13.400 11.940Q13.400 12.080 13.360 12.400Q13.320 12.720 13.320 12.840L13.320 12.840L13.280 12.840L13.280 12.720Q13.320 12.520 13.240 12.500Q13.160 12.480 13.040 12.440L13.040 12.440Q12.800 12.400 12.800 12.560L12.800 12.560L12.760 12.520L12.760 12.400L12.800 12.120Q12.880 11.880 12.880 11.720L12.880 11.720ZM11.840 11.800L12 11.520Q12.040 11.520 12.160 11.560L12.160 11.560Q12.360 11.600 12.440 11.520L12.440 11.520L12.440 11.760Q12.400 12.040 12.360 12.360L12.360 12.360L12.280 12.560Q12.240 12.600 12.040 12.540Q11.840 12.480 11.800 12.640Q11.760 12.800 11.720 13L11.720 13Q11.680 12.800 11.760 12.400L11.760 12.400L11.800 12.160Q11.800 11.920 11.840 11.800L11.840 11.800ZM13.400 11.920L13.400 11.920Q13.400 11.800 13.440 11.680L13.440 11.680Q13.520 11.880 13.400 11.920ZM12.480 12.120L12.720 12.160Q12.720 12.120 12.760 12L12.760 12Q12.840 11.800 12.880 11.720L12.880 11.720Q12.880 11.880 12.800 12.120L12.800 12.120L12.760 12.400L12.640 12.680L12.440 12.640Q12.400 12.680 12.360 12.840L12.360 12.840L12.360 12.880L12.360 12.400L12.480 12.120ZM14.800 12.280L14.800 12.280Q14.840 12.240 14.880 12.120L14.880 12.120Q15.040 11.880 15.160 11.800L15.160 11.800Q15.200 11.880 15.120 12L15.120 12L15.080 12.040Q14.640 12.720 14.360 13.640L14.360 13.640Q14.160 14.200 13.880 15.320L13.880 15.320L13.880 15.480Q13.760 15.640 13.720 16.040L13.720 16.040L13.680 16.320Q13.600 16.320 13.480 16.200L13.480 16.200L13.400 16.080Q13.360 16.080 13.240 16.120L13.240 16.120L13.200 16.120Q13.280 15.720 13.520 15L13.520 15L13.640 14.640Q14.200 13.160 14.800 12.280ZM12.280 12.560L12.280 12.560Q12.320 12.520 12.320 12.440L12.320 12.440L12.360 12.360L12.280 13.560L12.200 15.320Q12.160 15.640 12.160 16.200L12.160 16.200L12.160 17.760L12.120 17.760L11.920 17.360Q11.760 17.040 11.640 16.880L11.640 16.880L11.600 16.880Q11.560 16.800 11.520 16.820Q11.480 16.840 11.540 17.020Q11.600 17.200 11.720 17.600L11.720 17.600Q11.920 18.200 11.920 18.560L11.920 18.560L11.840 18.320Q11.720 17.680 11.480 17.120L11.480 17.120L11.400 16.880Q11.440 16.680 11.440 16.560L11.440 16.560L11.440 16.360Q11.480 16.080 11.480 15.840L11.480 15.840L11.480 15.600Q11.480 15.440 11.520 15.120L11.520 15.120L11.560 14.600Q11.600 14.480 11.600 14.360L11.600 14.360L11.600 14.120Q11.600 13.760 11.680 13.400L11.680 13.400L11.720 13Q11.760 12.800 11.800 12.640Q11.840 12.480 12.040 12.540Q12.240 12.600 12.280 12.560ZM12.640 12.720L12.640 12.680Q12.640 12.680 12.640 12.680L12.640 12.680L12.760 12.400L12.760 12.520L12.680 12.880Q12.640 13.200 12.560 13.400L12.560 13.400L12.400 13.360Q12.360 13.480 12.280 13.720L12.280 13.720L12.280 13.760L12.360 12.840Q12.400 12.680 12.440 12.640L12.440 12.640L12.640 12.720ZM12.800 12.560L12.800 12.560Q12.800 12.400 13.040 12.440L13.040 12.440Q13.160 12.480 13.240 12.500Q13.320 12.520 13.280 12.720L13.280 12.720L13.280 12.840Q13.320 13.280 13.200 13.720L13.200 13.720L13.200 14Q13.160 14.200 13.160 14.400L13.160 14.400L13.160 14.960L13.120 15.240Q13.080 15.440 13.080 15.680L13.080 15.680L13.040 16.640L13 16.760Q12.920 16.960 12.920 17.040L12.920 17.040Q12.880 17.040 12.880 17.120L12.880 17.120L12.880 17.200Q12.760 17.280 12.680 17.360L12.680 17.360Q12.720 17.120 12.680 16.600L12.680 16.600L12.640 16.120Q12.560 15.480 12.640 14.160L12.640 14.160L12.640 13.520Q12.680 13.200 12.800 12.560ZM12.680 12.880L12.760 12.520L12.800 12.560Q12.680 13.200 12.640 13.520L12.640 13.520Q12.480 15 12.600 16.520L12.600 16.520Q12.560 16.840 12.400 17.440L12.400 17.440L12.280 18.040Q12.280 18.080 12.200 18.120L12.200 18.120L12.160 18.120L12.160 16.440Q12.200 16.160 12.200 15.600L12.200 15.600L12.200 15.320L12.280 13.720Q12.360 13.480 12.400 13.360L12.400 13.360L12.560 13.400Q12.640 13.200 12.680 12.880L12.680 12.880ZM5.360 13.080L5.360 12.680Q5.440 12.680 5.440 12.800L5.440 12.800L5.400 13.080L5.360 13.080ZM5.400 13.080L5.400 13.080L5.440 12.840L5.640 13.840Q5.680 13.920 5.640 14L5.640 14Q5.600 14.200 5.680 14.600L5.680 14.600Q5.760 15.200 5.600 15.440L5.600 15.440L5.520 15.360L5.520 15.200Q5.520 15.040 5.480 14.960L5.480 14.960L5.480 14.400Q5.520 14.280 5.480 14.160Q5.440 14.040 5.440 13.880L5.440 13.880L5.440 13.760Q5.440 13.560 5.420 13.400Q5.400 13.240 5.400 13.080ZM11.680 13.080L11.680 13.400Q11.680 13.360 11.660 13.220Q11.640 13.080 11.680 13.080L11.680 13.080ZM5.440 13.440L5.440 13.440Q5.440 13.560 5.440 13.760L5.440 13.760L5.440 13.880L5.400 13.880Q5.400 13.680 5.440 13.440ZM12.600 16.520L12.600 16.520Q12.480 15 12.640 13.520L12.640 13.520L12.640 14.160Q12.560 15.480 12.640 16.120L12.640 16.120L12.680 16.600Q12.720 17.120 12.680 17.360L12.680 17.360Q12.760 17.280 12.880 17.200L12.880 17.200L12.880 17.280L12.600 18.640L12.560 18.600Q12.520 18.560 12.480 18.600L12.480 18.600L12.360 18.600Q12.200 18.640 12.160 18.720L12.160 18.720L12.160 18.120L12.200 18.120Q12.280 18.080 12.280 18.040L12.280 18.040L12.400 17.440Q12.560 16.840 12.600 16.520ZM13.200 14.040L13.200 14Q13.200 14 13.200 13.960L13.200 13.960L13.200 13.720Q13.240 13.880 13.200 14.040L13.200 14.040ZM7.680 14.040L7.680 14.040Q7.640 14.080 7.540 14.020Q7.440 13.960 7.460 13.920Q7.480 13.880 7.640 13.960L7.640 13.960Q8.280 13.920 9 14.160L9 14.160L9 14.160L8.720 14.480L8.400 14.320Q7.920 14.080 7.680 14.040ZM5.520 15.360L5.640 15.440Q5.760 15.200 5.680 14.600L5.680 14.600Q5.600 14.200 5.640 14L5.640 14Q5.720 14.200 5.800 14.600L5.800 14.600L5.840 14.920Q5.760 14.840 5.720 14.940Q5.680 15.040 5.720 15.080L5.720 15.080Q5.760 15.320 5.720 15.800L5.720 15.800L5.720 16.080L5.640 16.240Q5.560 16.200 5.520 16.040L5.520 16.040L5.520 15.360ZM13.200 14.400L13.160 14.400Q13.160 14.200 13.200 14.040L13.200 14.040L13.200 14.400ZM11.600 14.360L11.560 14.360Q11.480 14.160 11.600 14.120L11.600 14.120L11.600 14.360ZM5.480 14.160L5.480 14.160Q5.520 14.280 5.480 14.400L5.480 14.400L5.480 14.400Q5.400 14.280 5.480 14.160ZM8.720 14.480L9 14.160Q9.240 14.240 9.440 14.360L9.440 14.360L9.360 14.480L9.480 14.560Q9.640 14.680 9.720 14.760Q9.800 14.840 9.820 15.080Q9.840 15.320 9.960 15.400L9.960 15.400Q9.920 15.440 9.960 15.560L9.960 15.560L9.960 15.720Q9.880 15.640 9.800 15.480L9.800 15.480L9.680 15.320L9.480 15.120Q9.040 14.720 8.800 14.560L8.800 14.560L8.760 14.520Q8.720 14.480 8.720 14.480L8.720 14.480ZM11.560 14.360L11.560 14.360L11.600 14.360Q11.600 14.480 11.560 14.600L11.560 14.600Q11.520 14.440 11.560 14.360ZM9.480 14.560L9.360 14.480Q9.400 14.400 9.440 14.360L9.440 14.360Q10.320 14.880 11 16.080L11 16.080Q10.920 16.160 10.800 16.240L10.800 16.240Q10.800 16.440 10.960 16.720L10.960 16.720Q11.080 17.320 10.880 17.880L10.880 17.880L10.920 18.280L10.880 18.280Q10.760 17.080 9.960 15.720L9.960 15.720L9.960 15.560Q9.920 15.440 9.960 15.400L9.960 15.400Q9.840 15.320 9.820 15.080Q9.800 14.840 9.720 14.760Q9.640 14.680 9.480 14.560L9.480 14.560ZM13.160 14.680L13.160 14.400L13.200 14.400L13.200 14.480Q13.200 14.640 13.160 14.680L13.160 14.680ZM5.480 14.400L5.480 14.400L5.480 14.400L5.480 14.680L5.440 14.680Q5.400 14.520 5.480 14.400ZM11.560 14.640L11.520 14.920Q11.520 14.880 11.520 14.840L11.520 14.840Q11.480 14.640 11.560 14.640L11.560 14.640ZM5.440 14.680L5.440 14.680L5.480 14.680L5.480 14.920L5.440 14.960Q5.440 14.800 5.440 14.680ZM5.720 15.080L5.720 15.080Q5.680 15.040 5.720 14.940Q5.760 14.840 5.840 14.920L5.840 14.920L5.920 15.520L5.880 15.520Q5.760 15.600 5.840 15.720L5.840 15.720Q5.800 15.840 5.840 16.040Q5.880 16.240 5.840 16.360L5.840 16.360L5.760 16.520L5.720 16.080L5.720 15.800Q5.760 15.320 5.720 15.080ZM5.480 14.960L5.480 14.960Q5.520 15.040 5.520 15.200L5.520 15.200L5.520 15.280L5.480 15.280Q5.480 15.120 5.480 14.960ZM13.120 15.280L13.120 15.240Q13.120 15.200 13.120 15.160L13.120 15.160L13.160 14.960L13.160 15.280L13.120 15.280ZM13.120 15.680L13.080 15.680Q13.080 15.360 13.120 15.240L13.120 15.240L13.120 15.360Q13.120 15.560 13.120 15.680L13.120 15.680ZM5.480 15.600L5.480 15.280L5.520 15.280L5.520 15.600L5.480 15.600ZM12.160 16.440L12.160 16.440Q12.160 16.360 12.160 16.200L12.160 16.200Q12.160 15.640 12.200 15.320L12.200 15.320L12.200 15.600Q12.200 16.160 12.160 16.440ZM13.680 16.320L13.680 16.320Q13.720 16.240 13.720 16.040L13.720 16.040Q13.760 15.640 13.880 15.480L13.880 15.480Q14 15.480 14.200 15.520L14.200 15.520Q14.160 15.680 14.160 15.920L14.160 15.920L14.160 15.960Q14.120 16.120 14.080 16.280Q14.040 16.440 14.060 16.540Q14.080 16.640 14.040 16.760L14.040 16.760L14 16.840Q13.880 16.840 13.840 16.880L13.840 16.880L13.680 17.040Q13.600 17.240 13.560 17.600L13.560 17.600Q13.480 18.120 13.360 18.360L13.360 18.360Q13.320 18.360 13.200 18.320L13.200 18.320L13.120 18.280L13.040 18.280Q12.920 18.240 12.840 18.200Q12.760 18.160 12.840 18.040L12.840 18.040Q12.880 17.480 13.080 16.680L13.080 16.680Q13.120 16.560 13.080 16.480L13.080 16.480L13.080 16.360Q13.160 16.280 13.200 16.120L13.200 16.120L13.240 16.120Q13.360 16.080 13.400 16.080L13.400 16.080L13.480 16.200Q13.600 16.320 13.680 16.320ZM5.840 15.720L5.840 15.720Q5.760 15.600 5.880 15.520L5.880 15.520L5.920 15.520L6.080 16.400Q6 16.360 5.960 16.440L5.960 16.440L5.960 17L5.880 17.080Q5.840 17.160 5.840 17.240L5.840 17.240Q5.800 17.080 5.720 16.960L5.720 16.960L5.760 16.520L5.840 16.360Q5.880 16.240 5.840 16.040Q5.800 15.840 5.840 15.720ZM14.160 15.960L14.160 15.960Q14.160 15.960 14.160 15.920L14.160 15.920Q14.160 15.680 14.200 15.520L14.200 15.520Q14.200 15.600 14.200 15.760Q14.200 15.920 14.160 15.960ZM5.480 15.920L5.480 15.600L5.520 15.600L5.520 15.920L5.480 15.920ZM11.480 15.840L11.480 15.840Q11.400 15.640 11.480 15.600L11.480 15.600L11.480 15.840ZM13.080 15.920L13.080 15.680L13.120 15.680Q13.160 15.800 13.120 15.920L13.120 15.920L13.080 15.920ZM11.480 15.840L11.480 15.840L11.480 15.840Q11.480 16.160 11.440 16.360L11.440 16.360L11.440 16.120Q11.440 15.920 11.480 15.840ZM5.480 16.040L5.480 15.920L5.520 15.920L5.520 16.280Q5.440 16.240 5.480 16.040L5.480 16.040ZM13.080 16.400L13.080 15.920L13.120 15.920L13.120 16.160Q13.120 16.200 13.140 16.160Q13.160 16.120 13.200 16.120L13.200 16.120Q13.160 16.280 13.080 16.400L13.080 16.400ZM13.080 16.360L13.040 16.640Q13.040 16.480 13.040 16.240L13.040 16.240L13.080 15.920L13.080 16.360ZM5.520 16.600L5.520 16Q5.520 16 5.520 16.040L5.520 16.040Q5.560 16.200 5.640 16.240L5.640 16.240L5.720 16.080L5.760 16.520L5.720 16.960Q5.640 16.920 5.560 16.920L5.560 16.920L5.560 16.800Q5.560 16.680 5.520 16.600L5.520 16.600ZM10.800 16.240L10.800 16.240Q10.920 16.160 11 16.080L11 16.080Q11.160 16.320 11.320 16.680L11.320 16.680L11.160 16.800L11.240 17.080Q11.360 17.520 11.320 17.760L11.320 17.760L11.240 18Q11.040 18.440 11 18.680L11 18.680L10.960 18.680Q10.960 18.600 10.920 18.480L10.920 18.480L10.880 17.880Q11.080 17.320 10.960 16.720L10.960 16.720Q10.840 16.480 10.800 16.240ZM11.440 16.560L11.400 16.560Q11.440 16.520 11.420 16.360Q11.400 16.200 11.440 16.200L11.440 16.200L11.440 16.560ZM14.080 16.600L14.080 16.560Q14.040 16.440 14.080 16.320L14.080 16.320Q14.200 16.520 14.080 16.600L14.080 16.600ZM5.920 17L5.960 16.440Q6 16.360 6.040 16.420Q6.080 16.480 6.120 16.660Q6.160 16.840 6.100 16.940Q6.040 17.040 6.080 17.160L6.080 17.160Q6.080 17.800 6.160 18.400L6.160 18.400L6.200 18.640Q6.200 18.920 6.280 19.040L6.280 19.040L6.280 19.040L6.200 19.160L6.120 19.160Q5.960 19.120 5.920 19.160L5.920 19.160L5.880 18.520Q5.880 18.280 5.840 17.800L5.840 17.800L5.840 17.240Q5.840 17.160 5.880 17.080L5.880 17.080L5.920 17ZM13 16.760L13.040 16.640L13.080 16.480Q13.120 16.560 13.080 16.680L13.080 16.680Q12.880 17.360 12.840 18.040L12.840 18.040Q12.760 18.160 12.840 18.200Q12.920 18.240 13.040 18.280L13.040 18.280L13.120 18.280L13.200 18.320Q13.320 18.360 13.360 18.360L13.360 18.360Q13.480 18.120 13.560 17.600L13.560 17.600Q13.600 17.240 13.680 17.040L13.680 17.040L13.840 16.880Q13.880 16.840 14 16.840L14 16.840L14 17.080Q14 17.200 13.960 17.480L13.960 17.480L13.880 18.040Q13.800 18.160 13.640 18.160L13.640 18.160Q13.520 18.280 13.480 18.560L13.480 18.560L13.400 18.800Q13.280 18.800 13.120 18.640Q12.960 18.480 12.800 18.500Q12.640 18.520 12.680 18.240L12.680 18.240L12.920 17.040Q12.920 16.960 13 16.760L13 16.760ZM11.400 16.880L11.400 16.560L11.440 16.560Q11.440 16.680 11.400 16.880L11.400 16.880ZM5.520 16.600L5.520 16.600Q5.560 16.680 5.560 16.840L5.560 16.840L5.560 16.960L5.520 16.960L5.520 16.960Q5.520 16.720 5.520 16.600ZM11.240 17.080L11.160 16.800Q11.200 16.760 11.320 16.680L11.320 16.680L11.320 16.760Q11.360 16.840 11.400 16.880L11.400 16.880L11.480 17.120L11.480 17.240Q11.400 17.280 11.440 17.480L11.440 17.480L11.480 17.760Q11.520 18.080 11.520 18.240L11.520 18.240L11.360 18.520Q11.160 18.880 11.120 19.080L11.120 19.080Q11.080 19.160 11.100 19.360Q11.120 19.560 11.120 19.640L11.120 19.640L11.080 19.640L11.040 19.360Q11.080 19.240 11.040 19.120Q11 19 11 18.720L11 18.720L11 18.680Q11.040 18.440 11.240 18L11.240 18L11.320 17.760Q11.360 17.520 11.240 17.080L11.240 17.080ZM14 17.080L14 16.840Q14 16.800 14.040 16.800L14.040 16.800L14.040 16.880Q14.080 17.040 14 17.080L14 17.080ZM11.560 17L11.560 17Q11.480 16.840 11.520 16.820Q11.560 16.800 11.600 16.880L11.600 16.880L11.640 16.880Q11.760 17.040 11.920 17.360L11.920 17.360L12.120 17.760L12.160 17.760L12.160 18.320Q12.080 18.440 11.960 18.560L11.960 18.560L11.920 18.800L11.800 18.800Q11.920 18.640 11.920 18.560L11.920 18.560Q11.920 18.200 11.720 17.600L11.720 17.600Q11.600 17.200 11.560 17ZM5.560 18.200L5.560 16.920Q5.640 16.920 5.720 16.960L5.720 16.960Q5.800 17.080 5.840 17.240L5.840 17.240L5.840 17.800Q5.880 18.280 5.880 18.520L5.880 18.520L5.840 18.360Q5.800 18.120 5.760 18L5.760 18Q5.680 18 5.640 18.120L5.640 18.120L5.560 18.200ZM6.080 17.160L6.080 17.160Q6.040 17.040 6.160 16.920L6.160 16.920Q6.200 17.320 6.320 18.080L6.320 18.080L6.400 18.640Q6.360 18.680 6.400 18.840Q6.440 19 6.440 19.080L6.440 19.080L6.360 18.960L6.280 19.040Q6.200 18.920 6.200 18.640L6.200 18.640L6.160 18.400Q6.080 17.800 6.080 17.160ZM5.520 17.360L5.520 16.960L5.560 16.960L5.560 17.360L5.520 17.360ZM12.880 17.280L12.880 17.200Q12.880 17.160 12.880 17.100Q12.880 17.040 12.920 17.040L12.920 17.040L12.880 17.280ZM11.480 17.240L11.480 17.120Q11.720 17.640 11.840 18.320L11.840 18.320L11.760 18.280L11.760 18.160Q11.640 17.680 11.480 17.240L11.480 17.240ZM11.480 17.760L11.480 17.560Q11.480 17.520 11.440 17.480L11.440 17.480Q11.400 17.280 11.480 17.240L11.480 17.240Q11.680 17.760 11.760 18.160L11.760 18.160L11.640 18.240Q11.600 18.280 11.620 18.380Q11.640 18.480 11.600 18.520L11.600 18.520L11.520 18.640Q11.320 18.840 11.320 18.960L11.320 18.960Q11.240 19.200 11.340 19.600Q11.440 20 11.400 20.200L11.400 20.200Q11.400 20.320 11.280 20.560L11.280 20.560L11.240 20.600Q11.200 20.680 11.180 20.840Q11.160 21 11.120 21.080L11.120 21.080L11.120 19.640Q11.120 19.560 11.100 19.360Q11.080 19.160 11.120 19.080L11.120 19.080Q11.160 18.880 11.360 18.520L11.360 18.520L11.520 18.240Q11.520 18.080 11.480 17.760L11.480 17.760ZM5.520 17.800L5.520 17.360L5.560 17.360L5.560 17.800L5.520 17.800ZM13.960 17.920L13.920 17.920Q13.920 17.880 13.920 17.840L13.920 17.840L13.960 17.560L13.960 17.920ZM13.920 18.400L13.880 18.040L13.920 17.920L13.920 18.400L13.920 18.400ZM5.600 18.880L5.560 18.200Q5.600 18.160 5.640 18.080Q5.680 18 5.760 18L5.760 18Q5.800 18.120 5.840 18.360L5.840 18.360L5.880 18.520L5.880 19Q5.880 19.240 5.800 19.200Q5.720 19.160 5.640 19L5.640 19L5.600 18.880ZM13.640 18.160L13.640 18.160Q13.800 18.160 13.880 18.040L13.880 18.040L13.880 18.640Q13.840 18.640 13.740 18.760Q13.640 18.880 13.520 18.840L13.520 18.840Q13.440 18.920 13.400 19.120L13.400 19.120L13.360 19.240Q13.360 19.040 13.220 18.860Q13.080 18.680 12.880 18.660Q12.680 18.640 12.640 18.440L12.640 18.440L12.680 18.240Q12.640 18.520 12.800 18.500Q12.960 18.480 13.120 18.640Q13.280 18.800 13.400 18.800L13.400 18.800L13.480 18.560Q13.520 18.280 13.640 18.160ZM11.760 18.280L11.640 18.240Q11.680 18.200 11.760 18.160L11.760 18.160L11.760 18.280ZM5.560 18.200L5.560 18.200Q5.600 18.320 5.600 18.560L5.600 18.560L5.600 18.640L5.560 18.640Q5.560 18.440 5.560 18.200ZM11.520 18.640L11.600 18.520Q11.640 18.480 11.620 18.380Q11.600 18.280 11.640 18.240L11.640 18.240L11.800 18.320Q11.720 18.360 11.660 18.500Q11.600 18.640 11.560 18.680L11.560 18.680Q11.440 18.840 11.380 19.020Q11.320 19.200 11.380 19.360Q11.440 19.520 11.600 19.760L11.600 19.760L11.680 19.920Q11.720 20.200 11.520 20.720L11.520 20.720Q11.440 21.040 11.400 21.200L11.400 21.200L11.400 21.280Q11.400 22.240 11.520 22.680L11.520 22.680L11.280 22.640Q11.240 22.600 11.240 22.600L11.240 22.600L11.040 21.800Q11.080 21.760 11.080 21.640L11.080 21.640L11.120 21.080Q11.160 21 11.180 20.840Q11.200 20.680 11.240 20.600L11.240 20.600L11.280 20.560Q11.400 20.320 11.400 20.200L11.400 20.200Q11.440 20 11.340 19.600Q11.240 19.200 11.320 18.960L11.320 18.960Q11.320 18.840 11.520 18.640L11.520 18.640ZM11.560 18.680L11.560 18.680Q11.600 18.640 11.660 18.500Q11.720 18.360 11.800 18.320L11.800 18.320L11.840 18.320Q11.880 18.440 11.720 18.560L11.720 18.560Q11.680 18.640 11.560 18.780Q11.440 18.920 11.440 19.040L11.440 19.040Q11.360 19.240 11.460 19.420Q11.560 19.600 11.760 19.760L11.760 19.760Q11.840 19.760 11.840 19.880L11.840 19.880L11.840 19.920Q11.920 20.400 11.840 21.160L11.840 21.160L11.800 21.480Q11.800 22.160 11.840 22.520L11.840 22.520Q11.880 22.640 11.840 22.760L11.840 22.760L11.520 22.680Q11.400 22.240 11.400 21.240L11.400 21.240L11.400 21.200Q11.440 21.040 11.520 20.720L11.520 20.720Q11.720 20.200 11.680 19.920L11.680 19.920L11.600 19.760Q11.440 19.520 11.380 19.360Q11.320 19.200 11.380 19.020Q11.440 18.840 11.560 18.680ZM11.840 18.320L11.840 18.320L11.920 18.560Q11.920 18.640 11.800 18.800L11.800 18.800Q11.520 18.920 11.520 19.200L11.520 19.200Q11.480 19.360 11.720 19.560L11.720 19.560L11.880 19.680Q11.920 19.920 11.960 20.360L11.960 20.360L11.960 20.680Q12.040 21.480 12 22.760L12 22.760L11.840 22.760Q11.880 22.640 11.840 22.520L11.840 22.520Q11.800 22.160 11.800 21.520L11.800 21.520L11.840 21.160Q11.920 20.400 11.840 19.920L11.840 19.920L11.840 19.880Q11.840 19.760 11.760 19.760L11.760 19.760Q11.560 19.600 11.460 19.420Q11.360 19.240 11.440 19.040L11.440 19.040Q11.440 18.920 11.560 18.780Q11.680 18.640 11.720 18.560L11.720 18.560Q11.880 18.440 11.840 18.320ZM11.920 18.800L11.960 18.560Q12.080 18.440 12.160 18.320L12.160 18.320L12.160 18.720L12.080 18.840L11.920 18.800ZM13.880 18.920L13.920 18.400L13.920 18.400L13.920 18.920L13.880 18.920ZM12.760 18.720L12.600 18.640L12.640 18.440Q12.680 18.640 12.880 18.660Q13.080 18.680 13.220 18.860Q13.360 19.040 13.360 19.240L13.360 19.240L13.400 19.120Q13.440 18.920 13.520 18.840L13.520 18.840Q13.640 18.880 13.740 18.760Q13.840 18.640 13.880 18.640L13.880 18.640L13.880 18.920L13.880 18.960Q13.720 19.080 13.640 19.240L13.640 19.240L13.600 19.200Q13.520 19.160 13.480 19.120L13.480 19.120L13.480 19.240Q13.440 19.440 13.440 19.520L13.440 19.520Q13.440 19.720 13.380 20.120Q13.320 20.520 13.320 20.760L13.320 20.760L13.240 21.080Q13.120 21.040 12.920 21.080Q12.720 21.120 12.640 21.120L12.640 21.120Q12.520 21 12.520 20.800L12.520 20.800L12.640 21L12.720 20.920Q12.880 20.800 12.960 20.820Q13.040 20.840 13.160 20.760L13.160 20.760L13.240 20.720Q13.320 20.520 13.320 20.080L13.320 20.080L13.360 19.800Q13.400 19.560 13.320 19.360L13.320 19.360L13.120 19.640L13.040 19.680Q12.800 19.760 12.700 19.740Q12.600 19.720 12.440 19.720L12.440 19.720L12.440 19.720L12.440 19.720Q12.520 19.640 12.720 19.620Q12.920 19.600 13 19.560L13 19.560Q13.160 19.440 13.180 19.220Q13.200 19 13.040 18.840L13.040 18.840Q13 18.760 12.760 18.720L12.760 18.720ZM4.240 18.560L4.120 18.480Q4.320 18.560 4.680 18.920L4.680 18.920L4.680 18.960Q4.600 18.960 4.600 19.080L4.600 19.080L4.560 19.080Q4.480 18.880 4.240 18.560L4.240 18.560ZM12.080 18.880L12.160 18.720Q12.200 18.640 12.360 18.600L12.360 18.600L12.480 18.600Q12.520 18.560 12.560 18.600L12.560 18.600L12.600 18.640L12.600 18.840Q12.440 18.880 12.400 19.060Q12.360 19.240 12.480 19.360L12.480 19.360Q12.480 19.480 12.440 19.580Q12.400 19.680 12.320 19.680Q12.240 19.680 12.200 19.600L12.200 19.600L12.080 19.360Q12.200 19.240 12.200 19.100Q12.200 18.960 12.080 18.880L12.080 18.880ZM5.560 19.120L5.560 18.640L5.600 18.640L5.600 19.120L5.560 19.120ZM12.600 18.840L12.600 18.640L12.600 18.640L12.760 18.720Q13 18.760 13.040 18.840L13.040 18.840Q13.200 19 13.180 19.220Q13.160 19.440 13 19.560L13 19.560Q12.920 19.600 12.720 19.620Q12.520 19.640 12.440 19.720L12.440 19.720L12.440 19.600Q12.480 19.480 12.480 19.360L12.480 19.360Q12.600 19.480 12.780 19.440Q12.960 19.400 13.040 19.240Q13.120 19.080 12.940 18.920Q12.760 18.760 12.600 18.840L12.600 18.840ZM11.520 19.200L11.520 19.200Q11.520 18.920 11.800 18.800L11.800 18.800L11.920 18.800L12.080 18.840Q12.200 18.960 12.200 19.100Q12.200 19.240 12.080 19.360L12.080 19.360L12 19.440Q11.680 19.520 11.520 19.200ZM12.600 18.840L12.600 18.840Q12.760 18.760 12.940 18.920Q13.120 19.080 13.040 19.240Q12.960 19.400 12.780 19.440Q12.600 19.480 12.480 19.360Q12.360 19.240 12.400 19.060Q12.440 18.880 12.600 18.840ZM5.600 19.280L5.600 18.880Q5.640 18.920 5.680 19.040Q5.720 19.160 5.800 19.200Q5.880 19.240 5.880 19L5.880 19L5.880 18.960L5.920 19.320L5.920 19.560L5.720 19.480Q5.680 19.440 5.640 19.360L5.640 19.360L5.600 19.280ZM13.880 19.240L13.840 18.960L13.880 18.920L13.880 19.240L13.880 19.240ZM4.680 19.280L4.600 19.080Q4.600 18.960 4.680 18.960L4.680 18.960Q5.040 19.280 5.280 19.800L5.280 19.800L5.200 19.880Q5.040 20.040 5 20.120L5 20.120L5 20.120Q4.920 19.840 4.680 19.280L4.680 19.280ZM6.280 19.080L6.280 19.040Q6.320 19 6.360 18.960L6.360 18.960L6.440 19.080L6.440 19.200Q6.480 19.360 6.480 19.440L6.480 19.440Q6.360 19.520 6.340 19.700Q6.320 19.880 6.300 19.960Q6.280 20.040 6.080 20.080L6.080 20.080L5.960 20.120L5.960 19.840Q5.960 19.760 6.100 19.760Q6.240 19.760 6.280 19.720L6.280 19.720L6.280 19.320Q6.320 19.160 6.280 19.080L6.280 19.080ZM13.600 19.200L13.640 19.240Q13.720 19.080 13.840 18.960L13.840 18.960L13.880 20.240Q13.760 20.160 13.620 20.300Q13.480 20.440 13.400 20.440L13.400 20.440L13.320 20.760Q13.320 20.520 13.380 20.120Q13.440 19.720 13.440 19.520L13.440 19.520Q13.440 19.440 13.480 19.240L13.480 19.240L13.480 19.120Q13.520 19.160 13.600 19.200L13.600 19.200ZM6.120 19.160L6.200 19.160Q6.200 19.160 6.240 19.120L6.240 19.120L6.280 19.040Q6.320 19.160 6.320 19.320L6.320 19.320L6.280 19.440Q6.240 19.480 6.100 19.460Q5.960 19.440 5.920 19.560L5.920 19.560L5.920 19.200Q5.960 19.120 6.120 19.160L6.120 19.160ZM5.560 19.600L5.560 19.120L5.600 19.120L5.600 19.600L5.560 19.600ZM11.040 19.120L11.040 19.120Q11.080 19.240 11.040 19.360L11.040 19.360L11.040 19.360Q11 19.240 11.040 19.120ZM11.880 19.680L11.880 19.680Q11.840 19.640 11.720 19.560L11.720 19.560Q11.480 19.360 11.520 19.200L11.520 19.200Q11.680 19.520 12 19.440L12 19.440L11.840 19.480Q11.880 19.520 11.960 19.580Q12.040 19.640 12.080 19.680L12.080 19.680L12.120 19.840Q12.160 19.960 12.200 20.040L12.200 20.040Q12.240 20.280 12.240 20.520L12.240 20.520L12.120 20.480L12.120 20.600Q12.080 20.760 12.080 20.840L12.080 20.840Q12.080 21.120 12.080 21.720L12.080 21.720Q12.080 22.440 12.120 22.800L12.120 22.800L12 22.800L12 22.760Q12.040 21.480 11.960 20.680L11.960 20.680L11.960 20.360Q11.920 19.920 11.880 19.680ZM13.880 20.080L13.880 19.240L13.880 19.240L13.920 19.520Q13.920 19.880 13.880 20.080L13.880 20.080L13.880 20.080ZM5.600 19.720L5.600 19.280Q5.600 19.280 5.640 19.360Q5.680 19.440 5.720 19.480L5.720 19.480L5.920 19.560L5.920 19.680L5.920 19.720Q5.880 19.960 5.680 19.880L5.680 19.880Q5.640 19.880 5.640 19.800L5.640 19.800L5.600 19.720ZM11.040 19.440L11.040 19.360L11.040 19.360L11.080 19.640Q11 19.640 11.040 19.440L11.040 19.440ZM11.840 19.480L12 19.440Q12.040 19.360 12.080 19.360L12.080 19.360L12.200 19.600L12.200 20.040Q12.160 19.960 12.120 19.840L12.120 19.840L12.080 19.680Q12.040 19.640 11.960 19.580Q11.880 19.520 11.840 19.480L11.840 19.480ZM13.120 19.680L13.120 19.640Q13.160 19.600 13.280 19.440L13.280 19.440L13.320 19.360Q13.360 19.480 13.360 19.800L13.360 19.800L13.320 20.080Q13.320 20.520 13.240 20.720L13.240 20.720L13.160 20.760Q13.040 20.840 12.960 20.820Q12.880 20.800 12.720 20.920L12.720 20.920L12.640 21L12.520 20.800Q12.440 20.600 12.400 20.200L12.400 20.200L12.400 20Q12.400 19.880 12.440 19.760L12.440 19.760L12.440 19.720Q12.600 19.720 12.700 19.740Q12.800 19.760 13.040 19.680L13.040 19.680L13.120 19.680ZM5.960 19.840L5.920 19.560Q5.960 19.440 6.100 19.460Q6.240 19.480 6.280 19.440L6.280 19.440L6.280 19.720Q6.240 19.760 6.100 19.760Q5.960 19.760 5.960 19.840L5.960 19.840ZM6.280 19.960L6.320 19.960Q6.320 19.880 6.340 19.700Q6.360 19.520 6.480 19.440L6.480 19.440L6.480 19.480Q6.520 19.720 6.520 19.800L6.520 19.800L6.480 19.840Q6.400 19.880 6.400 19.920L6.400 19.920L6.360 20.160Q6.360 20.440 6.280 20.600L6.280 20.600L6 20.840L5.960 20.120L6.080 20.080Q6.280 20.040 6.280 19.960L6.280 19.960ZM12.200 20.040L12.200 19.600Q12.240 19.680 12.320 19.680Q12.400 19.680 12.440 19.600L12.440 19.600L12.440 19.720Q12.400 19.880 12.400 20L12.400 20L12.360 20.240Q12.320 20.520 12.280 20.640L12.280 20.640L12.240 20.520Q12.240 20.280 12.200 20.040L12.200 20.040ZM11.080 19.960L11.080 19.640L11.120 19.640L11.080 19.960L11.080 19.960ZM5.920 19.720L5.920 19.680Q5.920 19.760 5.960 19.840L5.960 19.840L5.960 20.200L5.640 20.240L5.600 20.080Q5.560 19.920 5.600 19.720L5.600 19.720L5.640 19.800Q5.640 19.880 5.680 19.880L5.680 19.880Q5.880 19.960 5.920 19.720L5.920 19.720ZM5.080 20.520L5 20.120Q5.040 20.040 5.200 19.880L5.200 19.880L5.280 19.800Q5.480 20.120 5.560 20.400L5.560 20.400L5.640 20.600Q5.760 20.880 5.800 21.040L5.800 21.040Q5.680 21.080 5.700 21.280Q5.720 21.480 5.680 21.520L5.680 21.520L5.600 21.560Q5.320 21.720 5.320 21.840L5.320 21.840L5.320 21.840Q5.320 21.400 5.080 20.520L5.080 20.520ZM6.360 20.160L6.400 19.920Q6.400 19.880 6.480 19.840L6.480 19.840L6.520 19.800Q6.560 20.080 6.600 20.600L6.600 20.600L6.600 20.760Q6.600 21 6.500 21.060Q6.400 21.120 6.320 21.360L6.320 21.360L6.320 21.360Q6.280 21.440 6.240 21.640L6.240 21.640L6.240 21.680L6.160 21.800L6.080 22.280L6.040 22.320Q6 22.120 6 21.960L6 21.960L6 21.760Q6.040 21.600 6 21.300Q5.960 21 6 20.840L6 20.840L6.280 20.600Q6.360 20.440 6.360 20.160L6.360 20.160ZM11.040 19.960L11.080 19.960L11.080 19.960L11.080 21.040L11.080 21.040L11.040 19.960ZM12.320 20.680L12.280 20.640Q12.320 20.520 12.360 20.240L12.360 20.240L12.400 20L12.400 20.200Q12.440 20.600 12.480 20.800Q12.520 21 12.640 21.120L12.640 21.120Q12.720 21.120 12.920 21.080Q13.120 21.040 13.240 21.080L13.240 21.080L13.320 20.800L13.320 21L13.240 21.560Q13.160 21.520 12.980 21.600Q12.800 21.680 12.720 21.640L12.720 21.640L12.560 21.280Q12.520 21.240 12.440 21.200L12.440 21.200L12.320 21.120L12.320 20.960Q12.360 20.800 12.320 20.680L12.320 20.680ZM13.880 20.240L13.880 20.080L13.880 20.080Q13.920 20.120 13.920 20.240L13.920 20.240L13.880 20.320L13.880 20.240ZM5.560 20.400L5.600 20.080Q5.600 20.160 5.640 20.240L5.640 20.240L5.600 20.520L5.560 20.400ZM5.600 20.520L5.640 20.240Q5.680 20.240 5.840 20.240L5.840 20.240L5.960 20.200L6 20.840Q5.960 21 6 21.300Q6.040 21.600 6 21.760L6 21.760L5.880 21.720Q5.920 21.560 5.840 21.240L5.840 21.240L5.800 21.040Q5.760 20.880 5.640 20.600L5.640 20.600L5.600 20.520ZM13.320 20.800L13.400 20.440Q13.480 20.440 13.620 20.300Q13.760 20.160 13.880 20.240L13.880 20.240L13.880 20.320Q13.840 20.520 13.880 20.720L13.880 20.720L13.880 20.880L13.840 20.720L13.720 20.760Q13.520 20.840 13.420 20.900Q13.320 20.960 13.320 21.120L13.320 21.120L13.320 20.800ZM13.880 20.760L13.880 20.280L13.920 20.280L13.920 20.760L13.880 20.760ZM13.880 20.320L13.880 20.720Q13.840 20.520 13.880 20.320L13.880 20.320ZM7.560 20.440L7.560 20.400L7.960 20.400L7.960 20.440L7.560 20.440ZM6.600 20.640L6.640 20.880Q7.040 20.440 7.560 20.440L7.560 20.440L8.040 20.440Q8.160 20.400 8.120 20.440L8.120 20.440Q8 20.440 7.880 20.480L7.880 20.480Q7.400 20.560 7.160 21.040L7.160 21.040Q7.120 21.080 7.080 21.200L7.080 21.200L7 21.280Q6.920 21.600 6.880 22.040L6.880 22.040L6.840 22.040Q6.800 21.840 6.680 21.640L6.680 21.640L6.560 21.600Q6.320 21.480 6.360 21.360L6.360 21.360L6.400 21.280Q6.480 21.160 6.540 21.080Q6.600 21 6.600 20.760L6.600 20.760L6.600 20.640L6.600 20.640ZM7.880 20.480L7.880 20.480Q8 20.440 8.120 20.440L8.120 20.440Q8.120 20.520 8.040 20.520Q7.960 20.520 7.880 20.480ZM12.120 20.640L12.120 20.480Q12.200 20.520 12.240 20.520L12.240 20.520L12.320 20.680L12.320 20.880Q12.280 21.080 12.320 21.160Q12.360 21.240 12.360 21.360L12.360 21.360L12.400 21.920L12.400 22.200Q12.440 22.520 12.480 22.640L12.480 22.640Q12.360 22.640 12.360 22.740Q12.360 22.840 12.320 22.840L12.320 22.840L12.120 22.800Q12.080 22.440 12.080 21.720L12.080 21.720Q12.080 21.120 12.080 20.840L12.080 20.840Q12.080 20.760 12.120 20.640L12.120 20.640ZM12.320 20.880L12.320 20.680Q12.360 20.800 12.320 20.960L12.320 20.960L12.320 21.160Q12.280 21.080 12.320 20.880L12.320 20.880ZM13.320 21.120L13.320 21.120Q13.320 20.960 13.420 20.900Q13.520 20.840 13.720 20.760L13.720 20.760L13.840 20.720L13.880 20.880L13.880 21.040L13.720 21.160Q13.480 21.280 13.400 21.400L13.400 21.400Q13.320 21.760 13.320 22.520L13.320 22.520L13.320 22.640L13.280 22.680L13.280 22.520Q13.280 21.600 13.320 21.120ZM13.880 21.040L13.880 20.760L13.920 20.720L13.920 21.080L13.880 21.040ZM10.760 21.480L10.760 21.040Q10.880 21.080 10.840 21.120L10.840 21.120Q10.800 21.280 10.800 21.440L10.800 21.440L10.800 21.480L10.760 21.480ZM11.080 21.480L11.080 21.040L11.080 21.040L11.080 21.480ZM13.240 21.560L13.240 21.560Q13.280 21.520 13.280 21.400L13.280 21.400L13.320 21L13.320 21.120Q13.280 21.600 13.280 22.520L13.280 22.520L13.280 22.680L12.760 22.680L12.720 22.320Q12.680 21.800 12.560 21.560L12.560 21.560Q12.400 21.520 12.360 21.360L12.360 21.360L12.360 21.360Q12.360 21.240 12.320 21.160L12.320 21.160L12.320 21.120L12.440 21.200Q12.520 21.240 12.560 21.280L12.560 21.280L12.720 21.640Q12.800 21.680 12.980 21.600Q13.160 21.520 13.240 21.560ZM5.600 21.560L5.680 21.520Q5.720 21.480 5.700 21.280Q5.680 21.080 5.800 21.040L5.800 21.040L5.840 21.240Q5.920 21.560 5.880 21.720L5.880 21.720Q5.760 21.680 5.680 21.720Q5.600 21.760 5.600 21.880L5.600 21.880Q5.600 22.080 5.840 22.120L5.840 22.120L5.720 22.160Q5.840 22.240 5.880 22.320L5.880 22.320L5.880 22.440Q5.880 22.640 5.840 22.720L5.840 22.720L5.400 22.720L5.400 22.600L5.320 21.840Q5.320 21.680 5.600 21.560L5.600 21.560ZM6.360 21.360L6.320 21.360Q6.320 21.360 6.320 21.360L6.320 21.360Q6.400 21.120 6.520 21.040L6.520 21.040Q6.480 21.160 6.400 21.280L6.400 21.280L6.360 21.360ZM13.400 21.400L13.400 21.400Q13.480 21.280 13.720 21.160L13.720 21.160L13.880 21.040L13.880 21.120Q13.880 21.320 13.920 21.440L13.920 21.440L13.920 21.600L13.880 21.560L13.800 21.680Q13.600 21.880 13.560 22.040L13.560 22.040Q13.560 22.120 13.480 22.120L13.480 22.120L13.400 22.160Q13.400 22.320 13.320 22.560L13.320 22.560L13.320 22.520Q13.320 21.760 13.400 21.400ZM13.880 21.120L13.880 21.040L13.920 21.080L13.920 21.440Q13.880 21.320 13.880 21.120L13.880 21.120ZM10.800 21.440L10.800 21.440Q10.800 21.280 10.840 21.120L10.840 21.120Q10.880 21.400 11 21.720L11 21.720Q11.040 21.760 11.020 21.660Q11 21.560 11.040 21.600L11.040 21.600L11.080 21.640Q11.080 21.760 11.040 21.800L11.040 21.800L11.240 22.600Q11.240 22.600 11.280 22.640L11.280 22.640L11.360 22.640Q11.240 22.640 11.280 22.800L11.280 22.800L11.360 23L11.160 23Q10.920 23 10.840 22.920L10.840 22.920L10.840 22.360Q10.840 21.760 10.800 21.440ZM13.920 21.560L13.920 21.280Q14 21.280 13.960 21.440L13.960 21.440L13.960 21.560L13.920 21.560ZM6.320 21.360L6.320 21.360L6.360 21.360Q6.320 21.480 6.560 21.600L6.560 21.600L6.680 21.640Q6.800 21.840 6.840 22.040L6.840 22.040L6.800 22.520Q6.640 22.520 6.580 22.580Q6.520 22.640 6.520 22.840L6.520 22.840L6.520 22.920L6.440 23.160Q6.280 23.200 6.080 23.200L6.080 23.200L6.040 22.720L6.160 22.840L6.440 22.760Q6.520 22.520 6.560 22.240L6.560 22.240L6.400 22.240Q6.160 22.200 6.080 22.280L6.080 22.280L6.160 21.800Q6.040 22 6.240 22.080Q6.440 22.160 6.520 22.020Q6.600 21.880 6.480 21.760Q6.360 21.640 6.240 21.720L6.240 21.720L6.240 21.640Q6.280 21.440 6.320 21.360ZM12.400 21.920L12.360 21.360Q12.400 21.520 12.560 21.560L12.560 21.560Q12.680 21.800 12.720 22.320L12.720 22.320L12.760 22.680L12.720 22.680L12.600 22.200Q12.520 22.240 12.480 22.080Q12.440 21.920 12.400 21.920L12.400 21.920ZM10.800 22.960L10.800 21.440Q10.840 21.760 10.840 22.360L10.840 22.360L10.840 22.920L10.800 22.960ZM13.800 21.680L13.880 21.560L13.920 21.600L13.920 21.640Q13.920 21.840 13.960 21.920L13.960 21.920L13.960 22Q14 22.120 14 22.200L14 22.200L14.040 22.640L13.320 22.640L13.360 22.560Q13.400 22.320 13.400 22.120L13.400 22.120L13.480 22.120Q13.560 22.120 13.560 22.040L13.560 22.040Q13.600 21.880 13.800 21.680L13.800 21.680ZM13.920 21.680L13.920 21.560L13.960 21.560L13.960 21.600Q13.960 21.840 13.960 21.920L13.960 21.920Q13.920 21.840 13.920 21.680L13.920 21.680ZM6.160 21.800L6.240 21.680Q6.360 21.640 6.480 21.760Q6.600 21.880 6.520 22.020Q6.440 22.160 6.240 22.080Q6.040 22 6.160 21.800L6.160 21.800ZM5.600 21.880L5.600 21.880Q5.600 21.760 5.680 21.720Q5.760 21.680 5.880 21.720L5.880 21.720L6 21.800L6 21.960Q5.960 22.040 5.840 22.120L5.840 22.120Q5.600 22.080 5.600 21.880ZM14.080 22.880L14.360 22.360Q14.400 22.320 14.480 22.160L14.480 22.160Q14.640 21.840 14.760 21.680L14.760 21.680Q14.720 21.880 14.600 22.280L14.600 22.280L14.400 22.760Q14.360 22.920 14.320 22.960L14.320 22.960L14.040 22.960Q14.040 22.920 14.080 22.880L14.080 22.880ZM12.400 22.200L12.360 21.920Q12.440 21.920 12.480 22.080Q12.520 22.240 12.600 22.200L12.600 22.200L12.720 22.680L12.480 22.640Q12.440 22.520 12.400 22.200L12.400 22.200ZM5.720 22.160L5.840 22.120Q5.960 22.040 6 21.960L6 21.960Q6 22.160 6.040 22.320L6.040 22.320L6.040 22.720Q6 22.960 6.040 23.320L6.040 23.320L5.920 23.200Q5.880 23.040 5.900 22.760Q5.920 22.480 5.880 22.360Q5.840 22.240 5.720 22.160L5.720 22.160ZM9.760 22L9.760 22Q9.800 22.040 9.920 22.200L9.920 22.200L10.240 22.600Q10.080 22.680 9.960 22.440L9.960 22.440L9.840 22.240Q9.760 22.120 9.760 22ZM15.360 22.240L15.440 22.160L15.480 22.160Q15.360 22.400 15.240 22.640L15.240 22.640L15 22.640Q15.080 22.520 15.360 22.240L15.360 22.240ZM10 22.320L10.040 22.320Q10.160 22.360 10.420 22.280Q10.680 22.200 10.800 22.200L10.800 22.200L10.800 22.280Q10.600 22.280 10.440 22.360L10.440 22.360L10.440 22.360Q10.280 22.480 10.320 22.600L10.320 22.600Q10.480 22.720 10.800 22.800L10.800 22.800L10.800 22.880Q10.160 22.760 9.800 22.600L9.800 22.600Q9.800 22.400 9.880 22.360L9.880 22.360L9.960 22.440Q10.080 22.680 10.240 22.600L10.240 22.600L10 22.320ZM14.040 22.680L14 22.200Q14.040 22.200 14.040 22.260Q14.040 22.320 14.080 22.320L14.080 22.320L14.360 22.400L14.160 22.720Q14.160 22.640 14.040 22.680L14.040 22.680L14.040 22.680ZM6.040 22.320L6.080 22.280Q6.160 22.200 6.400 22.240L6.400 22.240L6.560 22.240Q6.520 22.520 6.440 22.760L6.440 22.760L6.160 22.840L6.040 22.720L6.040 22.320ZM6.800 22.600L6.800 22.520Q6.800 22.360 6.840 22.240L6.840 22.240L6.840 22.320Q6.840 22.520 6.840 22.600L6.840 22.600L6.800 22.600ZM10.440 22.360L10.440 22.360Q10.600 22.280 10.800 22.280L10.800 22.280L10.800 22.320Q10.600 22.360 10.600 22.520Q10.600 22.680 10.800 22.720L10.800 22.720L10.800 22.800Q10.480 22.720 10.320 22.600L10.320 22.600Q10.280 22.480 10.440 22.360L10.440 22.360ZM5.880 22.440L5.880 22.320Q5.920 22.480 5.900 22.760Q5.880 23.040 5.920 23.200L5.920 23.200L5.960 23.680Q5.840 23.680 5.720 23.680L5.720 23.680L5.480 23.640L5.400 23.360L5.400 22.720L5.840 22.720Q5.880 22.640 5.880 22.440L5.880 22.440ZM10.560 22.520L10.560 22.520Q10.600 22.360 10.800 22.320L10.800 22.320L10.800 22.720Q10.600 22.680 10.560 22.520ZM5.040 22.560L5.040 22.440Q5.080 22.440 5.120 22.560L5.120 22.560L5.080 22.720Q5 22.760 5.040 22.560L5.040 22.560ZM6.560 22.560L6.560 22.560Q6.640 22.520 6.800 22.520L6.800 22.520L6.800 22.800Q6.600 22.800 6.520 22.920L6.520 22.920L6.520 22.840Q6.520 22.640 6.560 22.560ZM5.200 23.880L5.080 22.720Q5.080 22.640 5.120 22.600L5.120 22.600L5.560 23.840L5.400 23.880L5.200 23.880ZM6.800 22.840L6.800 22.600L6.840 22.600Q6.840 22.720 6.840 22.840L6.840 22.840L6.800 22.840ZM5.400 22.600L5.400 22.600L5.400 22.720L5.400 23.040L5.360 23.040Q5.360 22.840 5.400 22.600ZM11.720 22.840L11.280 22.800Q11.240 22.640 11.360 22.640L11.360 22.640L11.400 22.720Q11.760 22.800 12.120 22.800L12.120 22.800L12.320 22.840L12.320 22.880Q12.080 22.880 11.720 22.840L11.720 22.840ZM11.400 22.720L11.360 22.640Q11.440 22.680 11.520 22.680L11.520 22.680L12 22.760L12 22.800Q11.680 22.800 11.400 22.720L11.400 22.720ZM12.560 22.720L12.360 22.760Q12.360 22.640 12.480 22.640L12.480 22.640L12.760 22.680Q12.720 22.720 12.560 22.720L12.560 22.720ZM12.760 22.680L12.760 22.680Q12.920 22.680 13.040 22.680L13.040 22.680Q12.760 22.800 12.400 22.760L12.400 22.760L12.560 22.720Q12.720 22.720 12.760 22.680ZM13.040 22.680L13.040 22.680Q13.160 22.680 13.280 22.680L13.280 22.680Q13.200 22.720 13 22.760L13 22.760L12.800 22.800L12.440 22.800L12.400 22.880L12.320 22.880L12.320 22.840Q12.360 22.840 12.360 22.760L12.360 22.760L12.400 22.760Q12.800 22.800 13.040 22.680ZM13.280 22.680L13.280 22.680L13.280 22.680L14.040 22.640L14.040 22.680L13.920 22.720L13.640 22.720Q13.520 22.680 13.240 22.740Q12.960 22.800 12.840 22.800L12.840 22.800L12.800 22.800L13 22.760Q13.200 22.720 13.280 22.680ZM13.920 22.720L14.040 22.680Q14.040 22.680 14.040 22.680L14.040 22.680Q14.160 22.640 14.160 22.720L14.160 22.720L14.080 22.880L14.080 22.800Q12.720 23 11.320 22.920L11.320 22.920L11.280 22.800L11.720 22.840Q12.080 22.880 12.320 22.880L12.320 22.880L12.400 22.880Q12.880 22.880 13.160 22.840L13.160 22.840Q13.560 22.880 13.920 22.720L13.920 22.720ZM7.280 23.760L7.640 22.720L7.680 22.680Q7.680 22.920 7.560 23.360L7.560 23.360L7.480 23.760L7.280 23.760ZM12.840 22.800L12.840 22.800Q12.960 22.800 13.240 22.740Q13.520 22.680 13.680 22.720L13.680 22.720Q13.520 22.760 13.240 22.800L13.240 22.800L13.160 22.840Q12.760 22.880 12.400 22.880L12.400 22.880L12.400 22.840L12.560 22.840Q12.760 22.840 12.840 22.800ZM13.640 22.720L13.680 22.720L13.920 22.720Q13.560 22.880 13.160 22.840L13.160 22.840L13.240 22.800Q13.520 22.760 13.640 22.720L13.640 22.720ZM6.040 23.560L6.040 23.320Q6 23 6.040 22.720L6.040 22.720L6.080 23.200Q6.280 23.200 6.440 23.160L6.440 23.160L6.520 22.920L6.560 23.440L6.040 23.560ZM6.560 23.360L6.520 22.920Q6.600 22.800 6.800 22.800L6.800 22.800L6.800 22.920Q6.800 23.120 6.840 23.200L6.840 23.200L6.840 23.320L6.800 23.320Q6.720 23.240 6.640 23.280L6.640 23.280L6.560 23.360ZM12.400 22.840L12.440 22.800L12.800 22.800L12.840 22.800Q12.760 22.840 12.560 22.840L12.560 22.840L12.400 22.840ZM6.800 22.920L6.800 22.840L6.840 22.840Q6.840 23 6.840 23.200L6.840 23.200Q6.800 23.120 6.800 22.920L6.800 22.920ZM8.120 23.120L8.120 23.120L8.160 23.120L8.080 23.480L7.880 23.480Q7.960 23.360 8.120 23.120ZM4.400 23.160L4.400 23.160Q4.480 23.200 4.600 23.320L4.600 23.320L4.680 23.440Q4.920 23.640 4.720 23.640L4.720 23.640Q4.640 23.600 4.560 23.440L4.560 23.440L4.480 23.320Q4.440 23.200 4.400 23.160ZM5.960 23.680L5.920 23.200Q6 23.240 6.040 23.320L6.040 23.320L6.040 23.720L5.960 23.720L5.960 23.680ZM6.840 23.520L6.840 23.200Q6.880 23.160 6.880 23.280L6.880 23.280L6.840 23.520ZM5.120 23.360L5.160 23.240Q5.160 23.520 5.200 23.760L5.200 23.760Q5.080 23.760 5.020 23.680Q4.960 23.600 5 23.520L5 23.520L5.040 23.480Q5.120 23.440 5.120 23.360L5.120 23.360ZM6.600 23.760L6.560 23.360Q6.560 23.360 6.600 23.320L6.600 23.320L6.640 23.280Q6.720 23.240 6.800 23.320L6.800 23.320L6.920 23.680Q6.840 23.720 6.640 23.760L6.640 23.760L6.600 23.760ZM4.680 23.440L4.680 23.440Q4.760 23.440 4.920 23.400L4.920 23.400L5.120 23.360Q5.120 23.440 5.040 23.480L5.040 23.480L5 23.520Q4.960 23.600 5.020 23.680Q5.080 23.760 5.200 23.760L5.200 23.760L5.200 23.840L5.120 23.800Q4.840 23.760 4.720 23.640L4.720 23.640Q4.920 23.640 4.680 23.440ZM6.880 23.440L6.880 23.360Q6.920 23.400 7.040 23.400L7.040 23.400Q7.280 23.440 7.360 23.480L7.360 23.480L7.280 23.760Q6.760 23.880 6.200 23.840L6.200 23.840L6.240 23.840Q6.480 23.800 6.600 23.800L6.600 23.800L6.720 23.800Q7 23.760 7.080 23.640Q7.160 23.520 6.880 23.440L6.880 23.440L6.880 23.440ZM4.320 23.520L4.360 23.520Q4.400 23.480 4.520 23.400L4.520 23.400L4.520 23.400L4.560 23.440Q4.640 23.600 4.740 23.680Q4.840 23.760 5.120 23.800L5.120 23.800L5.200 23.840L5.200 23.880Q5.040 23.880 4.760 23.800L4.760 23.800L4.560 23.760Q4.360 23.760 4.240 23.600L4.240 23.600Q4.200 23.560 4.320 23.520L4.320 23.520ZM6.040 23.880L6.040 23.560Q6.160 23.520 6.400 23.480L6.400 23.480L6.560 23.440L6.600 23.800Q6.480 23.800 6.240 23.840L6.240 23.840L6.040 23.880ZM6.840 23.520L6.880 23.440Q6.880 23.440 6.880 23.440L6.880 23.440Q7.160 23.520 7.080 23.640Q7 23.760 6.720 23.800L6.720 23.800L6.600 23.800L6.600 23.760L6.600 23.760Q6.800 23.720 6.920 23.680L6.920 23.680L6.840 23.520ZM5.520 23.720L5.480 23.640Q5.600 23.680 5.720 23.720Q5.840 23.760 5.960 23.720L5.960 23.720L6.040 23.720L6.040 23.760L5.520 23.720ZM5.720 23.680L5.720 23.680Q5.840 23.680 5.960 23.680L5.960 23.680L5.960 23.720Q5.840 23.760 5.720 23.680ZM5.560 23.800L5.520 23.720Q5.560 23.720 5.600 23.720L5.600 23.720L6.040 23.760L6.040 23.800L5.840 23.800Q5.640 23.760 5.560 23.800L5.560 23.800ZM5.560 23.840L5.560 23.800Q5.640 23.760 5.840 23.800L5.840 23.800L6.040 23.800L6.040 23.840L5.560 23.840ZM5.400 23.880L5.560 23.840Q5.680 23.840 5.960 23.840L5.960 23.840L6.040 23.840L6.040 23.880L6 23.880Q5.600 23.880 5.400 23.920L5.400 23.920L5.400 23.880Z\"/>',\n\t'seti:typescript':\n\t\t'<path d=\"M11.376 6.833L11.532 6.833L7.359 6.833L7.359 19.235L4.317 19.235L4.317 6.833L0.183 6.833L0.183 4.610L11.376 4.610L11.376 6.833ZM20.892 15.491L20.892 15.491Q20.892 15.062 20.736 14.750Q20.580 14.438 20.307 14.126L20.307 14.126Q20.073 13.931 19.605 13.697L19.605 13.697Q19.293 13.580 18.513 13.268L18.513 13.268L18.201 13.151Q15.666 12.410 14.184 11.318L14.184 11.318Q12.858 10.187 12.858 8.510L12.858 8.510Q12.858 6.677 14.457 5.585L14.457 5.585Q15.900 4.492 18.299 4.492Q20.697 4.492 22.257 5.858L22.257 5.858Q23.700 7.145 23.700 9.017L23.700 9.017L20.892 9.017Q20.892 8.471 20.716 8.081Q20.541 7.691 20.151 7.301L20.151 7.301Q19.371 6.716 18.201 6.716Q17.031 6.716 16.407 7.184Q15.783 7.652 15.783 8.510L15.783 8.510Q15.783 9.212 16.524 9.758L16.524 9.758Q16.836 9.992 17.460 10.226L17.460 10.226Q17.928 10.421 18.942 10.733L18.942 10.733Q21.321 11.434 22.569 12.507Q23.817 13.580 23.817 15.491L23.817 15.491Q23.817 16.427 23.447 17.187Q23.076 17.948 22.374 18.416L22.374 18.416Q20.892 19.508 18.474 19.508L18.474 19.508Q15.900 19.508 14.301 18.260L14.301 18.260Q12.468 16.817 12.624 14.750L12.624 14.750L15.549 14.750Q15.549 15.998 16.407 16.700L16.407 16.700Q16.758 16.973 17.343 17.129Q17.928 17.285 18.591 17.285L18.591 17.285Q19.917 17.285 20.424 16.817L20.424 16.817Q20.892 16.076 20.892 15.491Z\"/>',\n\t'seti:tsconfig':\n\t\t'<path d=\"M1.668 0.198L1.668 0.198L22.332 0.198Q22.962 0.198 23.382 0.618Q23.802 1.038 23.802 1.668L23.802 1.668L23.802 22.332Q23.802 22.962 23.382 23.382Q22.962 23.802 22.332 23.802L22.332 23.802L1.668 23.802Q1.038 23.802 0.618 23.382Q0.198 22.962 0.198 22.332L0.198 22.332L0.198 1.668Q0.198 1.038 0.618 0.618Q1.038 0.198 1.668 0.198ZM10.572 12.546L13.470 12.546L13.470 10.530L5.364 10.530L5.364 12.546L8.262 12.546L8.262 21.576L10.572 21.576L10.572 12.546ZM14.268 19.140L14.268 21.702Q14.898 22.038 15.675 22.185Q16.452 22.332 17.313 22.332Q18.174 22.332 18.951 22.164Q19.728 21.996 20.337 21.576Q20.946 21.156 21.282 20.526Q21.618 19.896 21.618 18.930L21.618 18.930Q21.618 18.216 21.408 17.712Q21.198 17.208 20.820 16.788Q20.442 16.368 19.917 16.032Q19.392 15.696 18.804 15.444L18.804 15.444Q18.342 15.234 17.985 15.045Q17.628 14.856 17.355 14.667Q17.082 14.478 16.914 14.226Q16.746 13.974 16.746 13.701Q16.746 13.428 16.893 13.218Q17.040 13.008 17.271 12.861Q17.502 12.714 17.838 12.630Q18.174 12.546 18.594 12.546L18.594 12.546Q19.602 12.546 20.526 13.008L20.526 13.008Q20.862 13.134 21.114 13.344L21.114 13.344L21.114 10.950Q20.526 10.740 19.938 10.614L19.938 10.614Q19.182 10.530 18.426 10.530L18.426 10.530Q17.586 10.530 16.809 10.719Q16.032 10.908 15.465 11.328Q14.898 11.748 14.562 12.378Q14.226 13.008 14.226 13.932L14.226 13.932Q14.226 15.066 14.835 15.864Q15.444 16.662 16.704 17.208L16.704 17.208L17.628 17.628Q18.048 17.838 18.363 18.069Q18.678 18.300 18.867 18.573Q19.056 18.846 19.056 19.140L19.056 19.140Q19.056 19.686 18.594 19.980L18.594 19.980Q18.384 20.148 18.006 20.232Q17.628 20.316 17.208 20.316L17.208 20.316Q16.410 20.316 15.654 20.022Q14.898 19.728 14.268 19.140L14.268 19.140Z\"/>',\n\t'seti:vala':\n\t\t'<path d=\"M13.452 8.241L13.490 8.241Q15.010 8.241 16.454 8.583L16.454 8.583L17.024 8.849Q18.050 9.343 18.278 10.255L18.278 10.255Q18.506 11.053 18.088 11.927L18.088 11.927Q17.974 12.307 17.670 12.725L17.670 12.725Q17.480 12.953 17.024 13.333L17.024 13.333L16.796 13.599Q16.112 14.169 14.592 15.157L14.592 15.157L13.718 15.727Q13.414 15.917 12.806 16.297L12.806 16.297Q11.704 17.019 11.210 17.513L11.210 17.513Q11.096 17.627 10.925 17.855Q10.754 18.083 10.602 18.197L10.602 18.197Q10.450 18.463 10.431 18.786Q10.412 19.109 10.488 19.413L10.488 19.413Q10.754 19.945 11.267 20.363Q11.780 20.781 12.445 20.952Q13.110 21.123 13.604 20.743Q14.098 20.363 14.174 19.641L14.174 19.641L14.174 19.147Q14.174 18.691 14.554 18.349L14.554 18.349Q15.162 17.893 15.846 17.741L15.846 17.741Q17.100 17.475 17.746 17.627L17.746 17.627Q17.974 17.627 18.088 17.741L18.088 17.741Q18.544 17.931 18.658 18.197Q18.772 18.463 18.696 18.919L18.696 18.919Q18.202 20.895 16.568 22.377L16.568 22.377Q15.124 23.631 13.338 23.783L13.338 23.783Q11.932 23.973 10.488 23.441L10.488 23.441Q5.814 21.655 4.218 16.791L4.218 16.791Q3.686 15.005 4.180 13.447Q4.674 11.889 6.118 10.749L6.118 10.749Q7.752 9.229 10.754 8.583L10.754 8.583Q11.704 8.355 12.160 8.355L12.160 8.355Q12.768 8.241 13.452 8.241L13.452 8.241ZM15.010 4.783L15.010 4.783Q15.200 1.933 17.518 0.641L17.518 0.641Q18.924-0.081 20.368 0.299L20.368 0.299Q21.166 0.451 21.470 0.907Q21.774 1.363 21.641 2.199Q21.508 3.035 20.824 3.966Q20.140 4.897 19.722 5.353L19.722 5.353Q19.038 6.075 18.354 6.455L18.354 6.455Q17.594 7.025 16.796 7.177L16.796 7.177Q16.226 7.253 15.827 7.063Q15.428 6.873 15.238 6.341L15.238 6.341Q15.010 5.619 15.010 4.783ZM13.338 2.769L13.338 2.769Q13.338 4.479 12.502 5.619L12.502 5.619Q12.350 5.961 12.046 6.113L12.046 6.113Q11.780 6.379 11.457 6.379Q11.134 6.379 10.868 6.113L10.868 6.113Q10.488 5.733 10.260 5.049L10.260 5.049Q9.842 3.605 10.146 2.427L10.146 2.427Q10.526 1.097 11.932 1.097L11.932 1.097Q12.388 1.097 12.711 1.287Q13.034 1.477 13.110 1.819L13.110 1.819Q13.224 1.933 13.224 2.199L13.224 2.199Q13.224 2.541 13.338 2.769ZM8.474 4.897L8.474 5.277Q8.474 5.847 8.436 6.151L8.436 6.151Q8.360 6.607 8.132 6.949L8.132 6.949Q7.942 7.405 7.600 7.462Q7.258 7.519 6.840 7.177L6.840 7.177Q5.814 6.341 5.624 5.049L5.624 5.049L5.624 4.213Q5.738 3.643 6.137 3.263Q6.536 2.883 7.068 2.883L7.068 2.883Q7.676 2.883 8.018 3.377L8.018 3.377Q8.474 4.023 8.474 4.897L8.474 4.897ZM5.168 8.735L5.168 8.697Q5.168 9.077 4.940 9.533L4.940 9.533Q4.750 10.065 4.218 9.913L4.218 9.913L4.104 9.875L3.990 9.799Q3.268 9.533 2.793 8.716Q2.318 7.899 2.318 7.177L2.318 7.177Q2.318 6.759 2.584 6.417Q2.850 6.075 3.268 5.847L3.268 5.847Q3.800 5.695 4.332 6.227L4.332 6.227Q4.636 6.531 4.788 7.025L4.788 7.025Q4.902 7.291 5.016 7.899L5.016 7.899L5.054 8.013Q5.168 8.355 5.168 8.735L5.168 8.735Z\"/>',\n\t'seti:vite':\n\t\t'<path d=\"M16.232 0.179L16.232 0.179L6.392 2.139Q6.152 2.179 6.112 2.459L6.112 2.459L5.512 12.659Q5.512 12.859 5.652 12.979Q5.792 13.099 5.952 13.059L5.952 13.059L8.672 12.419Q8.872 12.379 9.012 12.519Q9.152 12.659 9.112 12.819L9.112 12.819L8.312 16.819Q8.272 17.019 8.432 17.159Q8.592 17.299 8.752 17.219L8.752 17.219L10.472 16.699Q10.672 16.659 10.812 16.799Q10.952 16.939 10.912 17.139L10.912 17.139L9.632 23.379Q9.552 23.699 9.832 23.799Q10.112 23.899 10.272 23.659L10.272 23.659L10.432 23.419L18.432 7.459Q18.552 7.259 18.412 7.079Q18.272 6.899 18.032 6.939L18.032 6.939L15.232 7.459Q15.032 7.499 14.892 7.359Q14.752 7.219 14.832 7.019L14.832 7.019L16.672 0.659Q16.712 0.459 16.572 0.299Q16.432 0.139 16.232 0.179Z\"/>',\n\t'seti:vue':\n\t\t'<path d=\"M6.117 1.659L12.000 11.870L17.883 1.659L14.775 1.659L12.000 6.469L9.225 1.659L6.117 1.659ZM23.951 1.659L19.178 1.659L12.000 14.090L4.822 1.659L0.049 1.659L12.000 22.341L23.951 1.659Z\"/>',\n\t'seti:wasm':\n\t\t'<path d=\"M9.396 0.198L0.198 0.198L0.198 23.802L23.802 23.802L23.802 0.198L14.646 0.198Q14.646 1.164 13.806 1.983Q12.966 2.802 12 2.802Q11.034 2.802 10.215 1.983Q9.396 1.164 9.396 0.198L9.396 0.198ZM8.976 21.198L7.002 13.302L8.598 13.302L9.774 17.838L10.950 13.302L12.126 13.302L13.302 17.838L14.478 13.302L16.074 13.302L14.100 21.198L12.714 21.198L11.538 16.074L11.328 17.166Q10.782 19.770 10.362 21.198L10.362 21.198L8.976 21.198ZM15.486 21.198L17.460 13.302L19.812 13.302L21.786 21.198L20.022 21.198L19.602 19.224L17.628 19.224L17.250 21.198L15.486 21.198ZM19.434 17.838L18.846 14.898L18.426 14.898L17.838 17.838L19.434 17.838Z\"/>',\n\t'seti:wat':\n\t\t'<path d=\"M11.702 18.563L9.532 18.563L7.327 9.813L5.157 18.563L2.952 18.563L0.222 5.438L2.532 5.438L4.072 13.102L6.242 5.438L8.447 5.438L10.617 13.102L12.158 5.438L14.467 5.438L11.702 18.563ZM23.778 18.563L21.013 5.438L16.638 5.438L13.943 18.563L16.638 18.563L17.197 15.308L20.453 15.308L21.013 18.563L23.778 18.563ZM20.208 13.102L17.443 13.102L18.282 8.203L19.367 8.203L20.208 13.102Z\"/>',\n\t'seti:xml':\n\t\t'<path d=\"M0.372 0.202L0.372 0.202Q6.803 0.084 12.408 3.506L12.408 3.506Q17.718 6.751 20.845 12.208Q23.972 17.666 23.795 23.625L23.795 23.625L19.016 23.625Q18.839 19.731 17.541 16.486Q16.243 13.241 13.677 10.527Q11.110 7.813 7.688 6.456L7.688 6.456Q4.384 5.158 0.549 5.158L0.549 5.158Q0.372 3.506 0.372 0.202ZM16.243 23.802L16.243 23.802L11.464 23.802L10.874 20.852Q9.753 17.489 7.600 15.542Q5.446 13.595 2.024 12.946L2.024 12.946Q1.670 12.710 0.726 12.710L0.726 12.710Q0.490 12.710 0.431 12.621Q0.372 12.533 0.372 12.356L0.372 12.356L0.372 7.754Q5.977 7.754 10.520 11.471L10.520 11.471Q15.948 16.368 16.243 23.802ZM6.862 20.498L6.862 20.498Q6.862 21.855 5.889 22.828Q4.915 23.802 3.529 23.802Q2.142 23.802 1.228 22.858Q0.313 21.914 0.195 20.498L0.195 20.498Q0.195 19.082 1.169 18.108Q2.142 17.135 3.529 17.135Q4.915 17.135 5.889 18.108Q6.862 19.082 6.862 20.498Z\"/>',\n\t'seti:yml':\n\t\t'<path d=\"M13.615 15.116L13.415 16.076Q13.095 16.076 12.575 16.156L12.575 16.156Q12.215 16.196 12.055 16.196L12.055 16.196L9.815 16.196Q10.055 13.516 10.495 8.196Q10.935 2.876 11.175 0.196L11.175 0.196L12.655 0.196Q14.375 0.116 15.175 0.196L15.175 0.196Q15.455 0.196 15.735 0.556Q16.015 0.916 15.935 1.196L15.935 1.196Q15.655 3.196 14.935 7.236L14.935 7.236L14.415 9.956Q14.215 11.636 13.615 15.116L13.615 15.116ZM13.935 21.076L13.935 21.076Q14.015 22.116 13.255 22.936Q12.495 23.756 11.295 23.836Q10.095 23.916 9.115 23.256Q8.135 22.596 8.055 21.596L8.055 21.596Q7.975 20.436 8.735 19.676Q9.495 18.916 10.815 18.836Q12.135 18.756 12.995 19.356Q13.855 19.956 13.935 21.076Z\"/>',\n\t'seti:prolog':\n\t\t'<path d=\"M20.167 0.198L20.167 0.198Q19.915 0.450 19.621 0.744L19.621 0.744Q18.907 1.374 18.067 1.962L18.067 1.962Q16.849 2.718 15.505 3.138L15.505 3.138Q13.783 3.684 11.977 3.684Q10.171 3.684 8.491 3.138L8.491 3.138Q7.147 2.718 5.929 1.962L5.929 1.962Q5.089 1.374 4.375 0.744L4.375 0.744L3.829 0.198L3.367 0.996Q2.821 2.004 2.359 3.054L2.359 3.054Q1.729 4.566 1.435 5.994L1.435 5.994Q1.015 7.800 1.099 9.354L1.099 9.354Q1.267 11.790 1.981 13.722L1.981 13.722Q2.863 16.116 4.753 18.258L4.753 18.258Q7.357 21.156 11.977 23.802L11.977 23.802Q16.219 21.408 18.739 18.804L18.739 18.804Q20.923 16.536 21.931 13.974L21.931 13.974Q22.729 11.916 22.897 9.354L22.897 9.354Q23.065 6.540 21.637 3.054L21.637 3.054Q20.923 1.332 20.167 0.198L20.167 0.198ZM4.249 2.676L4.249 2.676Q5.047 3.264 5.887 3.684L5.887 3.684Q4.207 4.272 3.115 5.658L3.115 5.658Q3.577 4.104 4.249 2.676ZM19.369 15.528L19.369 15.528Q18.277 17.208 16.555 18.720L16.555 18.720L16.555 17.334L15.001 17.334L15.001 20.022Q13.993 20.778 12.775 21.534L12.775 21.534L12.775 19.266L11.221 19.266L11.221 21.534Q10.087 20.820 8.995 20.022L8.995 20.022L8.995 17.334L7.441 17.334L7.441 18.720Q6.643 18.048 5.929 17.250L5.929 17.250Q4.417 15.528 3.619 13.638L3.619 13.638Q4.795 14.772 6.391 15.171Q7.987 15.570 9.541 15.066L9.541 15.066L11.977 17.544L14.455 15.066Q16.009 15.570 17.605 15.171Q19.201 14.772 20.377 13.638L20.377 13.638Q19.957 14.646 19.369 15.528ZM11.977 15.318L11.053 14.394Q11.557 14.058 11.977 13.638L11.977 13.638Q12.439 14.058 12.943 14.394L12.943 14.394L11.977 15.318ZM16.177 13.806L16.177 13.806Q14.749 13.806 13.594 12.966Q12.439 12.126 11.977 10.782L11.977 10.782Q11.473 12.336 10.087 13.155Q8.701 13.974 7.084 13.722Q5.467 13.470 4.417 12.231Q3.367 10.992 3.367 9.375Q3.367 7.758 4.417 6.519Q5.467 5.280 7.084 5.007Q8.701 4.734 10.087 5.574Q11.473 6.414 11.977 7.926L11.977 7.926Q12.523 6.330 14.014 5.511Q15.505 4.692 17.164 5.049Q18.823 5.406 19.810 6.792Q20.797 8.178 20.608 9.858Q20.419 11.538 19.159 12.672Q17.899 13.806 16.177 13.806ZM18.109 3.684L18.109 3.684Q18.949 3.264 19.747 2.676L19.747 2.676Q20.419 4.104 20.881 5.658L20.881 5.658Q19.789 4.272 18.109 3.684ZM5.593 9.354L5.593 9.354Q5.593 10.278 6.244 10.929Q6.895 11.580 7.798 11.580Q8.701 11.580 9.352 10.929Q10.003 10.278 10.003 9.354Q10.003 8.430 9.352 7.800Q8.701 7.170 7.798 7.170Q6.895 7.170 6.244 7.800Q5.593 8.430 5.593 9.354ZM13.993 9.354L13.993 9.354Q13.993 10.278 14.644 10.929Q15.295 11.580 16.198 11.580Q17.101 11.580 17.752 10.929Q18.403 10.278 18.403 9.354Q18.403 8.430 17.752 7.800Q17.101 7.170 16.198 7.170Q15.295 7.170 14.644 7.800Q13.993 8.430 13.993 9.354Z\"/>',\n\t'seti:zig':\n\t\t'<path d=\"M3.120 5.821L7.301 4.600L4.526 8.004L3.120 5.821ZM0.197 4.600L7.301 4.600L5.303 6.302L4.526 8.004L3.601 8.004L3.601 15.848L4.970 15.848L3.305 16.625L2.047 19.252L0.197 19.252L0.197 4.600ZM0.826 17.550L4.970 15.848L2.047 19.252L0.826 17.550ZM5.895 8.004L8.855 4.600L9.780 6.746L5.895 8.004ZM8.855 6.154L8.855 4.600L17.328 4.600L17.328 8.004L5.895 8.004L8.855 6.154ZM14.072 17.254L18.105 15.848L15.145 19.252L14.072 17.254ZM6.672 15.848L18.105 15.848L15.626 17.254L15.145 19.252L6.672 19.252L6.672 15.848ZM15.774 4.600L23.322 1.196L8.226 19.252L0.678 22.804L15.774 4.600ZM19.030 8.152L21.953 4.600L21.805 7.375L19.030 8.152ZM21.953 4.600L23.803 4.600L23.803 19.252L16.551 19.252L18.697 17.402L19.474 15.848L20.399 15.848L20.399 8.152L19.030 8.152L20.547 6.746L21.953 4.600ZM16.551 19.252L19.474 15.848L20.251 18.179L16.551 19.252Z\"/>',\n\t'seti:zip':\n\t\t'<path d=\"M21.348 23.894L21.348 23.894L2.652 23.894Q2.386 23.894 2.234 23.723Q2.082 23.552 2.082 23.324L2.082 23.324L2.082 0.714Q2.082 0.486 2.234 0.315Q2.386 0.144 2.652 0.144L2.652 0.144L21.348 0.144Q21.576 0.144 21.747 0.315Q21.918 0.486 21.918 0.714L21.918 0.714L21.918 23.324Q21.918 23.552 21.766 23.723Q21.614 23.894 21.348 23.894ZM11.620 14.546L11.620 0.106L12.380 0.106L12.380 14.546L11.620 14.546ZM11.810 1.778L11.810 0.980L14.128 0.980L14.128 1.778L11.810 1.778ZM11.810 3.412L11.810 2.614L14.128 2.614L14.128 3.412L11.810 3.412ZM11.810 5.084L11.810 4.286L14.128 4.286L14.128 5.084L11.810 5.084ZM11.810 6.718L11.810 5.920L14.128 5.920L14.128 6.718L11.810 6.718ZM11.810 8.390L11.810 7.592L14.128 7.592L14.128 8.390L11.810 8.390ZM11.810 10.024L11.810 9.264L14.128 9.264L14.128 10.024L11.810 10.024ZM11.810 11.696L11.810 10.898L14.128 10.898L14.128 11.696L11.810 11.696ZM11.810 13.330L11.810 12.532L14.128 12.532L14.128 13.330L11.810 13.330ZM9.796 2.538L9.796 1.740L12.152 1.740L12.152 2.538L9.796 2.538ZM9.796 4.172L9.796 3.412L12.152 3.412L12.152 4.172L9.796 4.172ZM9.796 5.844L9.796 5.046L12.152 5.046L12.152 5.844L9.796 5.844ZM9.796 7.516L9.796 6.718L12.152 6.718L12.152 7.516L9.796 7.516ZM9.796 9.150L9.796 8.352L12.152 8.352L12.152 9.150L9.796 9.150ZM9.796 10.822L9.796 10.024L12.152 10.024L12.152 10.822L9.796 10.822ZM9.796 12.456L9.796 11.658L12.152 11.658L12.152 12.456L9.796 12.456ZM9.796 0.904L9.796 0.106L12.152 0.106L12.152 0.904L9.796 0.904ZM9.796 14.090L9.796 13.330L12.152 13.330L12.152 14.090L9.796 14.090ZM14.812 19.410L14.812 19.410Q14.736 18.726 14.470 17.358L14.470 17.358L14.318 16.408L14.280 16.294Q14.090 15.914 13.558 15.724Q13.026 15.534 12.019 15.534Q11.012 15.534 10.461 15.724Q9.910 15.914 9.720 16.294L9.720 16.294L9.682 16.408L9.530 17.358Q9.264 18.726 9.188 19.410L9.188 19.410Q9.112 19.942 9.530 20.265Q9.948 20.588 10.746 20.626L10.746 20.626L13.254 20.626Q14.052 20.588 14.470 20.265Q14.888 19.942 14.812 19.410ZM12.038 18.308L12.038 18.308L11.962 18.308Q11.392 18.308 11.050 18.061Q10.708 17.814 10.689 17.377Q10.670 16.940 11.031 16.655Q11.392 16.370 11.962 16.408L11.962 16.408L11.962 16.408L12.038 16.370Q12.608 16.370 12.969 16.655Q13.330 16.940 13.311 17.377Q13.292 17.814 12.950 18.061Q12.608 18.308 12.038 18.308Z\"/>',\n\t'seti:wgt':\n\t\t'<path d=\"M11.751 0.458L11.751 0.458Q11.681 0.458 11.646 0.493L11.646 0.493L11.436 0.633Q9.826 1.613 9.056 2.208L9.056 2.208Q7.761 3.293 7.446 4.273L7.446 4.273Q7.341 4.588 7.341 4.833L7.341 4.833Q7.341 4.973 7.411 5.288L7.411 5.288L7.446 5.393Q7.691 6.233 8.251 7.108L8.251 7.108Q8.391 7.388 8.793 7.895Q9.196 8.403 9.231 8.403L9.231 8.403Q11.156 7.178 12.136 6.233L12.136 6.233L12.136 3.538Q12.136 1.823 12.101 1.298Q12.066 0.773 11.978 0.633Q11.891 0.493 11.751 0.458ZM7.061 5.008L7.061 5.008Q6.956 5.008 6.868 5.463Q6.781 5.918 6.448 7.230Q6.116 8.543 5.906 9.208L5.906 9.208Q5.766 9.733 5.713 10.223Q5.661 10.713 5.766 10.853L5.766 10.853Q5.906 11.098 9.476 12.218L9.476 12.218Q10.246 12.463 10.491 12.445Q10.736 12.428 10.876 12.148L10.876 12.148L10.911 12.113Q11.051 11.833 11.033 11.640Q11.016 11.448 10.666 10.923L10.666 10.923Q10.386 10.538 9.406 9.208L9.406 9.208L9.266 8.963Q8.076 7.353 7.743 6.793Q7.411 6.233 7.271 5.498L7.271 5.498Q7.131 5.008 7.061 5.008ZM17.946 5.673L17.946 5.673Q17.806 5.673 17.666 5.673L17.666 5.673Q17.211 5.743 15.846 5.830Q14.481 5.918 13.816 5.953L13.816 5.953Q13.256 5.953 12.766 6.058Q12.276 6.163 12.171 6.303L12.171 6.303Q11.996 6.513 12.066 10.258L12.066 10.258Q12.066 11.063 12.153 11.290Q12.241 11.518 12.521 11.588L12.521 11.588L12.591 11.588Q12.906 11.658 13.081 11.570Q13.256 11.483 13.641 10.958L13.641 10.958Q13.921 10.608 14.901 9.278L14.901 9.278L15.076 9.033Q16.231 7.423 16.668 6.933Q17.106 6.443 17.736 6.058L17.736 6.058Q18.191 5.813 18.156 5.743Q18.121 5.673 17.946 5.673ZM18.961 5.883L18.961 5.883Q18.646 5.883 18.436 5.953L18.436 5.953Q18.261 5.988 18.016 6.163L18.016 6.163L17.911 6.198Q17.176 6.688 16.511 7.493L16.511 7.493Q16.301 7.738 15.933 8.280Q15.566 8.823 15.601 8.858L15.601 8.858Q17.351 10.328 18.576 10.923L18.576 10.923L21.131 10.118Q22.741 9.593 23.248 9.400Q23.756 9.208 23.861 9.068Q23.966 8.928 23.931 8.788L23.931 8.788Q23.896 8.718 23.861 8.718L23.861 8.718L23.686 8.543Q22.251 7.318 21.411 6.793L21.411 6.793Q20.011 5.883 18.961 5.883ZM0.411 9.243L0.411 9.243Q0.341 9.243 0.306 9.278L0.306 9.278Q0.131 9.313 0.096 9.453L0.096 9.453Q0.061 9.488 0.061 9.558L0.061 9.558L0.131 9.768Q0.586 11.623 0.901 12.533L0.901 12.533Q1.531 14.108 2.371 14.703L2.371 14.703Q2.651 14.913 2.861 14.983L2.861 14.983Q3.001 15.018 3.316 15.053L3.316 15.053L3.421 15.053Q4.296 15.088 5.311 14.843L5.311 14.843Q5.626 14.773 6.238 14.545Q6.851 14.318 6.851 14.283L6.851 14.283Q6.291 12.078 5.696 10.853L5.696 10.853L3.106 10.013Q0.761 9.243 0.411 9.243ZM18.506 11.028L18.471 10.993Q17.981 10.993 14.726 12.113L14.726 12.113Q13.956 12.358 13.763 12.515Q13.571 12.673 13.606 12.988L13.606 12.988L13.606 13.023Q13.641 13.338 13.781 13.478Q13.921 13.618 14.516 13.863L14.516 13.863Q14.936 14.003 16.546 14.493L16.546 14.493L16.791 14.598Q18.716 15.193 19.311 15.473Q19.906 15.753 20.466 16.243L20.466 16.243Q20.851 16.558 20.921 16.505Q20.991 16.453 20.798 16.033Q20.606 15.613 20.098 14.353Q19.591 13.093 19.381 12.463L19.381 12.463Q19.171 11.938 18.926 11.500Q18.681 11.063 18.506 11.028L18.506 11.028ZM10.421 13.583L10.421 13.583Q10.176 13.583 9.546 13.758L9.546 13.758Q9.126 13.898 7.656 14.353L7.656 14.353L7.411 14.458Q5.486 15.088 4.838 15.210Q4.191 15.333 3.456 15.263L3.456 15.263Q2.966 15.228 2.931 15.315Q2.896 15.403 3.316 15.630Q3.736 15.858 4.873 16.593Q6.011 17.328 6.571 17.713L6.571 17.713Q7.026 17.993 7.481 18.203Q7.936 18.413 8.111 18.378L8.111 18.378Q8.391 18.308 10.526 15.228L10.526 15.228Q11.016 14.563 11.086 14.335Q11.156 14.108 10.911 13.898L10.911 13.898L10.876 13.863Q10.736 13.688 10.648 13.635Q10.561 13.583 10.421 13.583ZM12.871 14.353L12.871 14.353Q12.766 14.353 12.591 14.423L12.591 14.423L12.556 14.458Q12.276 14.563 12.188 14.738Q12.101 14.913 12.066 15.543L12.066 15.543Q12.031 15.998 12.066 17.678L12.066 17.678L12.066 17.958Q12.066 19.953 11.996 20.618Q11.926 21.283 11.611 21.948L11.611 21.948Q11.436 22.403 11.506 22.455Q11.576 22.508 11.926 22.193Q12.276 21.878 13.308 21.020Q14.341 20.163 14.901 19.743L14.901 19.743Q15.321 19.393 15.653 19.025Q15.986 18.658 15.986 18.483L15.986 18.483Q16.021 18.203 13.781 15.228L13.781 15.228Q13.396 14.703 13.203 14.528Q13.011 14.353 12.871 14.353ZM17.141 15.053L17.141 15.053Q17.141 15.053 17.141 15.053L17.141 15.053L17.141 15.053Q16.301 17.188 16.091 18.518L16.091 18.518L17.666 20.688Q18.646 22.053 18.996 22.473Q19.346 22.893 19.503 22.963Q19.661 23.033 19.801 22.928L19.801 22.928Q19.836 22.928 19.836 22.858L19.836 22.858L19.941 22.648Q20.676 20.898 20.921 19.953L20.921 19.953Q21.341 18.343 21.026 17.328L21.026 17.328Q20.921 17.013 20.781 16.838L20.781 16.838Q20.676 16.698 20.466 16.488L20.466 16.488L20.361 16.418Q19.696 15.893 18.716 15.508L18.716 15.508Q18.436 15.403 17.841 15.210Q17.246 15.018 17.141 15.053ZM9.896 18.343L9.896 18.343Q8.881 18.378 8.111 18.483L8.111 18.483L6.536 20.653Q5.521 22.053 5.223 22.490Q4.926 22.928 4.926 23.103Q4.926 23.278 5.031 23.383L5.031 23.383Q5.066 23.418 5.136 23.418L5.136 23.418L5.381 23.418Q7.271 23.593 8.216 23.523L8.216 23.523Q9.896 23.453 10.736 22.823L10.736 22.823Q11.016 22.613 11.156 22.438L11.156 22.438Q11.226 22.298 11.366 22.018L11.366 22.018L11.401 21.913Q11.716 21.108 11.786 20.058L11.786 20.058Q11.821 19.743 11.786 19.095Q11.751 18.448 11.716 18.413L11.716 18.413Q10.666 18.343 9.896 18.343Z\"/>',\n\t'seti:illustrator':\n\t\t'<path d=\"M23.802 23.151L23.802 0.849L0.198 0.849L0.198 23.151L23.802 23.151ZM13.848 16.977L13.848 16.977L12.546 16.977Q12.336 16.977 12.273 16.956Q12.210 16.935 12.126 16.725L12.126 16.725Q12 16.263 11.664 15.276Q11.328 14.289 11.202 13.827L11.202 13.827Q11.202 13.743 11.097 13.659Q10.992 13.575 10.824 13.575L10.824 13.575L8.052 13.575Q7.884 13.575 7.758 13.743L7.758 13.743L7.674 13.827L6.876 16.725Q6.876 16.809 6.792 16.893Q6.708 16.977 6.624 16.977L6.624 16.977L5.196 16.977Q4.986 16.977 4.944 16.914Q4.902 16.851 4.902 16.725L4.902 16.725Q5.154 15.885 5.616 14.289L5.616 14.289Q6.078 12.441 6.372 11.601L6.372 11.601Q6.624 10.635 7.212 8.598Q7.800 6.561 8.052 5.595L8.052 5.595L8.136 5.469Q8.178 5.301 8.241 5.259Q8.304 5.217 8.472 5.301L8.472 5.301L10.446 5.301Q10.572 5.301 10.635 5.343Q10.698 5.385 10.698 5.595L10.698 5.595Q11.034 6.561 11.622 8.598Q12.210 10.635 12.546 11.601L12.546 11.601L14.100 16.725Q14.184 16.935 14.163 16.956Q14.142 16.977 13.848 16.977ZM17.796 16.347L17.796 16.347Q17.796 16.809 17.712 16.893Q17.628 16.977 17.124 16.977L17.124 16.977L16.200 16.977Q15.990 16.977 15.969 16.956Q15.948 16.935 15.948 16.725L15.948 16.725L15.948 8.199Q15.948 7.989 16.032 7.905Q16.116 7.821 16.200 7.947L16.200 7.947L17.376 7.947Q17.544 7.947 17.586 8.010Q17.628 8.073 17.628 8.199L17.628 8.199Q17.754 10.971 17.796 16.347ZM16.872 7.023L16.872 7.023Q16.452 7.023 16.137 6.750Q15.822 6.477 15.822 6.078Q15.822 5.679 16.095 5.364Q16.368 5.049 16.809 5.049Q17.250 5.049 17.586 5.322Q17.922 5.595 17.922 5.973L17.922 5.973Q17.922 6.477 17.628 6.750Q17.334 7.023 16.872 7.023ZM9.648 7.821L9.648 7.821Q9.648 7.695 9.564 7.695Q9.480 7.695 9.396 7.821L9.396 7.821Q9.396 7.863 9.312 7.989Q9.228 8.115 9.228 8.199L9.228 8.199Q8.724 10.551 8.304 11.601L8.304 11.601Q8.346 11.895 8.472 11.895L8.472 11.895L10.278 11.895Q10.488 11.895 10.509 11.811Q10.530 11.727 10.446 11.601L10.446 11.601Q10.236 9.543 9.648 7.821Z\"/>',\n\t'seti:photoshop':\n\t\t'<path d=\"M23.802 23.172L23.802 0.702L23.802 0.198L0.198 0.198L0.198 23.802L23.172 23.802Q23.550 23.802 23.676 23.676Q23.802 23.550 23.802 23.172L23.802 23.172ZM9.900 13.428L9.900 13.428Q9.144 13.638 7.716 13.764L7.716 13.764L6.750 13.848L6.750 18.552L4.398 18.552Q4.314 18.552 4.230 18.342Q4.146 18.132 4.146 17.922L4.146 17.922L4.146 5.448Q4.146 5.364 4.839 5.259Q5.532 5.154 6.078 5.154L6.078 5.154Q8.472 5.154 9.522 5.322L9.522 5.322Q11.118 5.490 12.042 6.477Q12.966 7.464 13.050 8.976L13.050 8.976Q13.134 10.656 12.294 11.853Q11.454 13.050 9.900 13.428ZM20.022 16.074L20.022 16.074Q19.812 17.796 18.174 18.426L18.174 18.426Q15.948 19.350 13.428 18.300L13.428 18.300Q13.344 18.300 13.260 18.153Q13.176 18.006 13.176 17.922L13.176 17.922Q13.428 16.872 13.554 16.452L13.554 16.452Q14.100 16.536 15.108 16.746L15.108 16.746Q15.864 16.914 16.242 16.998Q16.620 17.082 16.956 16.872Q17.292 16.662 17.376 16.200L17.376 16.200Q17.544 15.486 16.578 14.898L16.578 14.898Q16.284 14.688 15.528 14.436L15.528 14.436L15.276 14.352Q14.184 13.848 13.701 13.134Q13.218 12.420 13.302 11.328L13.302 11.328Q13.428 10.446 14.079 9.795Q14.730 9.144 15.822 8.850L15.822 8.850Q17.502 8.472 19.224 9.102L19.224 9.102Q19.308 9.102 19.434 9.333Q19.560 9.564 19.476 9.648L19.476 9.648L19.098 11.076Q17.796 10.824 17.250 10.698L17.250 10.698Q16.578 10.698 16.326 10.824L16.326 10.824Q15.822 11.034 15.738 11.496Q15.654 11.958 16.074 12.252L16.074 12.252Q16.200 12.420 16.515 12.546Q16.830 12.672 17.019 12.798Q17.208 12.924 17.796 13.176L17.796 13.176Q18.216 13.344 18.426 13.428L18.426 13.428Q20.022 14.016 20.022 16.074ZM9.228 7.422L9.228 7.422Q8.724 7.338 7.842 7.254L7.842 7.254L6.750 7.128L6.750 11.622Q7.800 11.706 8.325 11.664Q8.850 11.622 9.480 11.349Q10.110 11.076 10.362 10.446L10.362 10.446Q10.572 9.984 10.572 9.270Q10.572 8.556 10.215 8.073Q9.858 7.590 9.228 7.422Z\"/>',\n\t'seti:pdf':\n\t\t'<path d=\"M0.051 21.261L0.051 21.261Q0.135 20.715 0.471 20.211L0.471 20.211Q0.681 19.917 1.227 19.392Q1.773 18.867 3.201 18.069L3.201 18.069L3.579 17.817Q4.503 17.313 6.351 16.515L6.351 16.515Q6.435 16.515 6.519 16.431Q6.603 16.347 6.603 16.263L6.603 16.263Q8.157 13.113 8.829 11.517L8.829 11.517Q9.039 11.055 9.354 10.152Q9.669 9.249 9.879 8.787L9.879 8.787L9.879 8.661Q9.123 6.855 8.955 5.637L8.955 5.637Q8.619 4.209 8.829 2.487L8.829 2.487Q8.997 1.647 9.879 0.765L9.879 0.765Q10.005 0.639 10.551 0.639L10.551 0.639L11.601 0.639Q12.063 0.639 12.231 1.017L12.231 1.017Q12.651 1.311 13.155 2.067L13.155 2.067Q13.323 2.445 13.449 3.117L13.449 3.117L13.575 3.663Q13.575 4.713 13.407 5.931L13.407 5.931Q13.281 6.687 13.029 8.115L13.029 8.115L12.651 9.585L12.651 9.711Q13.617 11.475 15.549 13.617L15.549 13.617Q15.633 13.743 15.675 13.764Q15.717 13.785 15.927 13.743L15.927 13.743Q17.355 13.491 20.127 13.491L20.127 13.491Q21.555 13.491 22.605 14.037L22.605 14.037Q23.403 14.541 23.655 14.961L23.655 14.961Q23.949 15.465 23.949 15.717L23.949 15.717L23.949 16.641Q23.949 17.103 23.529 17.313L23.529 17.313L23.403 17.439Q23.193 17.691 23.025 17.775L23.025 17.775Q22.773 17.985 22.479 17.943L22.479 17.943Q22.353 17.985 22.017 18.048Q21.681 18.111 21.555 18.111L21.555 18.111Q20.043 18.195 18.657 17.733L18.657 17.733Q17.397 17.313 16.053 16.389L16.053 16.389Q15.927 16.263 15.591 15.990Q15.255 15.717 15.129 15.591L15.129 15.591Q15.087 15.591 15.003 15.528Q14.919 15.465 14.877 15.465L14.877 15.465Q13.281 15.717 12.525 16.011L12.525 16.011Q11.979 16.137 10.929 16.452Q9.879 16.767 9.375 16.893L9.375 16.893Q9.291 16.893 9.228 16.977Q9.165 17.061 9.081 17.061L9.081 17.061Q8.157 18.741 6.855 20.463L6.855 20.463Q5.847 21.681 4.629 22.689L4.629 22.689Q3.915 23.151 3.201 23.361L3.201 23.361L2.151 23.361Q1.731 23.361 1.479 23.193L1.479 23.193Q0.975 23.025 0.618 22.647Q0.261 22.269 0.177 21.765L0.177 21.765Q0.051 21.765 0.051 21.261ZM12.273 12.945L11.601 12.063Q11.391 12.567 10.929 13.617Q10.467 14.667 10.299 15.213L10.299 15.213L13.449 14.415Q13.029 13.953 12.273 12.945L12.273 12.945ZM17.229 15.465L17.229 15.465Q18.363 16.221 19.875 16.641L19.875 16.641Q20.127 16.725 20.379 16.683L20.379 16.683Q20.505 16.641 20.736 16.536Q20.967 16.431 21.030 16.200Q21.093 15.969 20.925 15.843L20.925 15.843Q20.925 15.717 20.631 15.717L20.631 15.717Q20.505 15.675 20.148 15.612Q19.791 15.549 19.581 15.465L19.581 15.465Q18.405 15.255 17.229 15.465ZM4.377 19.749L4.881 19.161Q4.881 19.161 4.839 19.161L4.839 19.161L4.881 19.161Q3.747 19.707 2.655 20.589L2.655 20.589Q2.277 20.841 1.731 21.639L1.731 21.639L1.731 21.891L1.899 22.017L2.025 22.017L2.445 21.681Q2.781 21.471 2.949 21.387L2.949 21.387Q3.495 20.883 4.377 19.749L4.377 19.749ZM10.425 5.511L10.551 5.889Q10.593 5.889 10.635 5.889L10.635 5.889L10.551 5.889Q10.929 4.713 10.929 3.663L10.929 3.663Q10.929 2.865 10.803 2.487L10.803 2.487Q10.803 2.361 10.551 2.361L10.551 2.361Q10.467 2.361 10.404 2.424Q10.341 2.487 10.299 2.487L10.299 2.487Q10.089 2.739 10.047 3.075L10.047 3.075Q10.005 3.285 10.005 3.789L10.005 3.789Q10.005 4.461 10.131 4.839L10.131 4.839Q10.341 5.007 10.425 5.511L10.425 5.511Z\"/>',\n\t'seti:font':\n\t\t'<path d=\"M19.565 16.346L18.663 14.952L14.440 14.952L13.948 16.100L13.825 16.387Q13.661 16.797 13.661 17.002L13.661 17.002Q13.702 17.576 14.071 17.781L14.071 17.781Q14.153 17.863 14.645 17.904L14.645 17.904L15.219 18.027L15.219 18.396L11.242 18.396L11.242 18.027Q12.021 17.781 12.267 17.494L12.267 17.494Q12.718 17.043 13.292 15.731L13.292 15.731L17.515 6.219L17.638 6.219L21.902 15.977Q22.476 17.330 22.927 17.781L22.927 17.781Q23.419 18.027 23.788 18.027L23.788 18.027L23.788 18.396L18.047 18.396L18.047 18.027L18.540 18.027Q19.073 18.027 19.565 17.781L19.565 17.781Q19.852 17.494 19.811 17.371L19.811 17.371L19.811 17.002L19.565 16.346ZM14.850 14.173L18.416 14.173L16.613 10.073L14.850 14.173ZM0.213 20.856L0.213 20.200Q1.074 20.200 1.709 19.298Q2.345 18.396 2.796 17.125L2.796 17.125L8.413 3.144L9.192 3.144L14.973 16.592Q15.465 17.822 16.162 19.175L16.162 19.175L16.367 19.544Q16.695 20.200 17.802 20.200L17.802 20.200L17.802 20.856L9.438 20.856L9.438 20.200Q10.586 20.200 11.119 19.954L11.119 19.954Q11.488 19.749 11.488 19.175L11.488 19.175Q11.488 18.970 11.365 18.560L11.365 18.560L11.242 18.273Q10.996 17.781 10.873 17.371L10.873 17.371L10.217 15.854L4.477 15.854L3.698 17.904Q3.288 18.847 3.288 19.298L3.288 19.298Q3.288 19.872 4.067 20.077L4.067 20.077Q4.313 20.200 5.338 20.200L5.338 20.200L5.338 20.856L0.213 20.856ZM4.846 14.829L9.848 14.829L7.388 8.925L7.142 8.925L4.846 14.829Z\"/>',\n\t'seti:image':\n\t\t'<path d=\"M18.640 13.480L18.640 13.480Q19.560 13.480 20.220 12.840Q20.880 12.200 20.880 11.260Q20.880 10.320 20.220 9.660Q19.560 9 18.620 9Q17.680 9 17.020 9.660Q16.360 10.320 16.360 11.260Q16.360 12.200 17.020 12.840Q17.680 13.480 18.640 13.480ZM23.880 6.640L4.760 6.640L4.760 22.120L23.880 22.120L23.880 6.640ZM5.640 19L5.640 7.480L23 7.480L23 21L18.520 15.880L15.760 19.120L10.240 13.240L5.640 19ZM17.880 4L17.880 1.880L0.120 1.880L0.120 15.360L2.520 15.360L2.520 4L17.880 4Z\"/>',\n\t'seti:svg':\n\t\t'<path d=\"M6.360 13.120L6.360 6.240L13.240 6.240Q13.240 5 12.360 3.540Q11.480 2.080 10.080 1.160L10.080 1.160Q8.520 0.120 6.840 0.120Q5.160 0.120 3.620 1.020Q2.080 1.920 1.160 3.420Q0.240 4.920 0.240 6.600Q0.240 8.280 1.280 9.880L1.280 9.880Q2.200 11.280 3.660 12.200Q5.120 13.120 6.360 13.120L6.360 13.120ZM9.000 23.880L9.000 9.120L23.760 9.120L23.760 23.880L9.000 23.880Z\"/>',\n\t'seti:sublime':\n\t\t'<path d=\"M23.787 11.131L23.787 11.131L23.787 10.501Q23.409 8.023 22.233 5.881L22.233 5.881Q21.057 4.117 19.461 2.731L19.461 2.731Q18.159 1.639 16.563 1.051L16.563 1.051Q11.355-0.965 6.483 1.555L6.483 1.555Q3.669 3.151 2.031 5.755L2.031 5.755Q1.107 7.183 0.645 8.569L0.645 8.569Q0.183 10.039 0.183 11.677L0.183 11.677L0.183 13.105Q0.561 16.171 2.031 18.481L2.031 18.481Q3.585 20.749 5.433 21.883L5.433 21.883Q6.903 22.891 8.163 23.227L8.163 23.227Q11.061 24.109 13.413 23.731L13.413 23.731Q19.965 22.765 22.737 16.801L22.737 16.801Q23.997 14.281 23.787 11.131ZM14.883 17.431L14.883 17.431Q14.169 17.809 13.287 17.893L13.287 17.893Q12.783 17.977 11.733 17.977L11.733 17.977Q9.717 17.977 7.533 16.633L7.533 16.633Q7.155 16.465 7.407 16.255L7.407 16.255L8.163 14.701L8.331 14.575L8.457 14.533Q9.927 15.667 11.985 15.877L11.985 15.877Q12.783 15.877 13.161 15.751L13.161 15.751Q13.833 15.583 14.085 14.827L14.085 14.827Q14.085 14.113 13.413 13.777L13.413 13.777Q12.657 13.357 11.061 12.853L11.061 12.853L10.893 12.811Q10.011 12.559 9.591 12.349L9.591 12.349Q8.835 12.013 8.457 11.551L8.457 11.551Q7.785 10.879 7.785 9.451L7.785 9.451Q7.869 7.939 8.835 7.057L8.835 7.057Q9.675 6.217 11.061 6.007L11.061 6.007Q13.665 5.629 15.933 7.057L15.933 7.057Q16.059 7.183 16.059 7.477L16.059 7.477L15.261 8.905Q15.219 9.031 15.177 9.052Q15.135 9.073 15.051 9.073L15.051 9.073L14.883 9.031Q14.001 8.527 13.245 8.275L13.245 8.275Q12.405 8.023 11.481 8.107L11.481 8.107Q11.313 8.107 11.061 8.170Q10.809 8.233 10.683 8.233L10.683 8.233Q10.347 8.485 10.179 8.716Q10.011 8.947 10.011 9.325L10.011 9.325Q10.011 9.997 10.578 10.375Q11.145 10.753 11.733 10.753L11.733 10.753Q12.153 10.879 13.077 11.131L13.077 11.131L14.211 11.425Q14.337 11.257 14.757 11.257L14.757 11.257Q14.337 11.257 14.211 11.383L14.211 11.383L15.219 11.803Q15.261 11.845 15.261 11.929L15.261 11.929L15.345 11.929Q15.387 11.971 15.387 12.055L15.387 12.055L15.555 12.181Q15.639 12.223 15.639 12.307L15.639 12.307L15.807 12.475Q16.395 12.979 16.584 13.987Q16.773 14.995 16.353 15.919L16.353 15.919Q15.933 16.969 14.883 17.431Z\"/>',\n\t'seti:code-search':\n\t\t'<path d=\"M15.067 0.258L15.067 0.223Q12.792 0.223 10.849 1.500Q8.907 2.777 7.979 4.895Q7.052 7.012 7.384 9.305Q7.717 11.598 9.257 13.348L9.257 13.348L1.627 22.098L2.677 23.043L10.272 14.328Q11.777 15.518 13.632 15.867Q15.487 16.218 17.289 15.675Q19.092 15.133 20.457 13.803Q21.822 12.473 22.434 10.670Q23.047 8.867 22.784 6.995Q22.522 5.122 21.419 3.565Q20.317 2.008 18.637 1.133Q16.957 0.258 15.067 0.258L15.067 0.258ZM15.067 14.572L15.067 14.572Q13.142 14.572 11.532 13.470Q9.922 12.367 9.187 10.565Q8.452 8.762 8.837 6.855Q9.222 4.947 10.587 3.565Q11.952 2.183 13.842 1.797Q15.732 1.412 17.517 2.165Q19.302 2.918 20.369 4.545Q21.437 6.172 21.437 8.098L21.437 8.098Q21.437 9.392 20.964 10.582Q20.492 11.773 19.599 12.683Q18.707 13.593 17.534 14.082Q16.362 14.572 15.067 14.572ZM15.102 1.657L15.102 1.657Q13.212 1.657 11.602 2.690Q9.992 3.723 9.204 5.473Q8.417 7.223 8.714 9.113Q9.012 11.003 10.272 12.438L10.272 12.438L3.937 19.648L4.847 20.453L11.112 13.277Q12.372 14.258 13.912 14.537Q15.452 14.818 16.939 14.380Q18.427 13.943 19.564 12.840Q20.702 11.738 21.209 10.250Q21.717 8.762 21.489 7.205Q21.262 5.648 20.352 4.370Q19.442 3.092 18.059 2.375Q16.677 1.657 15.102 1.657L15.102 1.657ZM15.102 13.453L15.102 13.453Q13.492 13.453 12.162 12.560Q10.832 11.668 10.219 10.180Q9.607 8.692 9.922 7.117Q10.237 5.543 11.374 4.387Q12.512 3.232 14.087 2.918Q15.662 2.602 17.132 3.232Q18.602 3.863 19.512 5.192L19.512 5.192Q20.562 6.767 20.369 8.657Q20.177 10.547 18.847 11.913L18.847 11.913Q18.112 12.648 17.132 13.050Q16.152 13.453 15.102 13.453ZM3.622 23.778L1.137 21.608L9.782 11.808L12.232 13.978L3.622 23.778Z\"/>',\n\t'seti:shell':\n\t\t'<path d=\"M9.977 3.222L9.935 3.222Q9.305 3.432 8.717 3.810L8.717 3.810Q8.549 3.894 8.213 4.146L8.213 4.146Q7.709 4.524 7.289 5.070L7.289 5.070L6.995 5.448Q6.575 6.120 6.449 6.666L6.449 6.666Q6.365 6.918 6.323 7.254L6.323 7.254Q6.239 7.884 6.323 8.556L6.323 8.556Q6.365 8.976 6.449 9.396L6.449 9.396Q6.617 10.026 6.995 10.614L6.995 10.614L7.289 10.992Q7.667 11.454 8.255 11.874L8.255 11.874L8.885 12.336Q9.347 12.672 10.061 12.966L10.061 12.966L11.069 13.386L11.531 13.554Q12.119 13.764 12.329 13.848L12.329 13.848L12.413 13.932Q13.757 14.310 14.345 15.360L14.345 15.360Q14.597 15.864 14.597 16.284L14.597 16.284L14.597 16.452Q14.681 16.998 14.555 17.250L14.555 17.250Q14.051 18.258 13.253 18.552L13.253 18.552Q12.749 18.720 11.447 18.678L11.447 18.678Q10.565 18.468 10.061 17.922L10.061 17.922Q9.599 17.418 9.305 16.494L9.305 16.494L9.263 16.326Q9.221 16.032 9.032 15.843Q8.843 15.654 8.549 15.654L8.549 15.654L6.449 15.654Q6.155 15.654 5.966 15.843Q5.777 16.032 5.819 16.326L5.819 16.326Q5.819 16.956 5.966 17.544Q6.113 18.132 6.449 18.804L6.449 18.804Q6.659 19.140 6.911 19.455Q7.163 19.770 7.877 20.316L7.877 20.316Q8.213 20.568 8.465 20.694L8.465 20.694Q8.843 20.904 9.683 21.240L9.683 21.240L9.977 21.324Q10.271 21.408 10.460 21.597Q10.649 21.786 10.649 22.080L10.649 22.080L10.649 23.130Q10.649 23.424 10.838 23.613Q11.027 23.802 11.321 23.802L11.321 23.802L12.749 23.802Q13.043 23.802 13.232 23.613Q13.421 23.424 13.421 23.130L13.421 23.130L13.421 22.080Q13.421 21.786 13.610 21.555Q13.799 21.324 14.051 21.282L14.051 21.282Q14.219 21.240 14.513 21.114L14.513 21.114Q15.269 20.820 15.731 20.568L15.731 20.568Q16.613 20.022 17.411 18.888L17.411 18.888Q17.873 18.216 17.999 17.712L17.999 17.712Q18.125 17.418 18.167 16.998L18.167 16.998Q18.209 16.326 18.167 15.696L18.167 15.696Q18.125 15.276 17.999 14.982L17.999 14.982Q17.831 14.310 17.453 13.764L17.453 13.764Q16.907 12.882 15.563 11.916L15.563 11.916Q14.975 11.538 14.387 11.244L14.387 11.244Q13.925 11.034 12.959 10.656L12.959 10.656L12.035 10.320Q11.153 9.984 10.859 9.732L10.859 9.732Q10.355 9.312 10.124 8.913Q9.893 8.514 9.851 8.094L9.851 8.094L9.851 8.052Q9.851 7.590 9.914 7.254Q9.977 6.918 10.313 6.414L10.313 6.414Q10.565 6.078 10.943 5.994L10.943 5.994Q11.405 5.826 11.825 5.826L11.825 5.826Q12.539 5.784 12.896 5.847Q13.253 5.910 13.694 6.267Q14.135 6.624 14.345 7.044L14.345 7.044Q14.597 7.590 14.597 8.094L14.597 8.094L14.597 8.094Q14.639 8.388 14.828 8.577Q15.017 8.766 15.269 8.766L15.269 8.766L17.369 8.766Q17.663 8.766 17.852 8.556Q18.041 8.346 17.999 8.094L17.999 8.094Q17.957 7.590 17.852 7.107Q17.747 6.624 17.411 5.868L17.411 5.868Q17.285 5.574 17.117 5.322L17.117 5.322Q16.697 4.650 16.277 4.272L16.277 4.272Q16.067 4.062 15.857 3.894Q15.647 3.726 15.017 3.390L15.017 3.390L14.681 3.222L14.345 3.054Q14.051 2.928 13.862 2.697Q13.673 2.466 13.673 2.214L13.673 2.214L13.673 0.870Q13.673 0.576 13.484 0.387Q13.295 0.198 13.043 0.198L13.043 0.198L11.573 0.198Q11.279 0.198 11.090 0.387Q10.901 0.576 10.901 0.870L10.901 0.870L10.901 2.340Q10.901 2.802 10.313 3.054L10.313 3.054Q10.229 3.138 9.977 3.222L9.977 3.222Z\"/>',\n\t'seti:video':\n\t\t'<path d=\"M12.000 23.802L12.000 23.802Q8.808 23.802 6.078 22.185Q3.348 20.568 1.752 17.838L1.752 17.838Q0.114 15.024 0.198 11.748L0.198 11.748Q0.240 8.598 1.836 5.952Q3.432 3.306 6.078 1.794L6.078 1.794Q8.850 0.198 12.126 0.198L12.126 0.198Q15.318 0.198 18.027 1.815Q20.736 3.432 22.290 6.162L22.290 6.162Q23.886 8.976 23.802 12.252L23.802 12.252Q23.760 15.360 22.143 18.027Q20.526 20.694 17.880 22.206L17.880 22.206Q15.150 23.802 12.000 23.802ZM7.800 4.650L7.800 4.650L7.800 19.224L7.926 19.224L18.552 12.126L18.678 12Q18.678 12 18.552 12L18.552 12Q11.496 7.254 7.800 4.902L7.800 4.902Q7.884 4.818 7.884 4.734Q7.884 4.650 7.800 4.650Z\"/>',\n\t'seti:audio':\n\t\t'<path d=\"M12.475 21.282L12.323 21.282Q12.171 21.282 11.867 21.054L11.867 21.054L11.639 20.902Q8.067 17.824 6.167 16.266L6.167 16.266L6.053 16.190L5.939 16.152L0.923 16.152Q0.315 16.152 0.125 15.582L0.125 15.582L0.125 8.818Q0.125 8.362 0.372 8.115Q0.619 7.868 1.075 7.868L1.075 7.868L6.053 7.868Q6.281 7.868 6.281 7.716L6.281 7.716L11.753 2.966Q12.095 2.700 12.437 2.719Q12.779 2.738 13.045 3.118L13.045 3.118Q13.159 3.232 13.159 3.574L13.159 3.574L13.159 20.332Q13.159 20.978 12.703 21.168L12.703 21.168L12.589 21.168L12.475 21.282ZM23.837 12.124L23.875 12.124Q23.875 12.846 23.723 13.188L23.723 13.188Q23.571 15.354 22.659 17.482L22.659 17.482Q21.899 19.382 20.759 20.674L20.759 20.674Q20.607 20.978 20.075 21.168L20.075 21.168Q19.581 21.168 19.239 20.788L19.239 20.788Q19.049 20.636 19.049 20.389Q19.049 20.142 19.239 19.952L19.239 19.952Q19.277 19.838 19.467 19.610Q19.657 19.382 19.695 19.268L19.695 19.268Q21.481 16.760 21.823 13.910L21.823 13.910Q22.545 8.514 19.239 4.296L19.239 4.296Q18.783 3.650 19.239 3.232L19.239 3.232Q19.505 2.852 19.904 2.852Q20.303 2.852 20.645 3.232L20.645 3.232Q22.089 5.056 22.659 6.652L22.659 6.652Q23.571 8.780 23.723 11.174L23.723 11.174L23.723 12.010Q23.799 12.010 23.837 12.086L23.837 12.086L23.837 12.124ZM20.645 11.896L20.645 11.896Q20.645 15.468 18.631 18.432L18.631 18.432Q18.479 18.698 17.909 18.888L17.909 18.888Q17.453 18.888 17.073 18.546L17.073 18.546Q16.921 18.356 16.921 18.109Q16.921 17.862 17.073 17.596L17.073 17.596Q17.073 17.520 17.149 17.463Q17.225 17.406 17.187 17.368L17.187 17.368Q18.517 15.582 18.859 13.188L18.859 13.188Q19.391 9.502 17.073 6.310L17.073 6.310Q16.845 5.816 16.845 5.588L16.845 5.588Q16.959 5.284 17.073 5.132Q17.187 4.980 17.453 4.866L17.453 4.866Q18.023 4.676 18.384 5.189Q18.745 5.702 19.467 6.918L19.467 6.918Q20.531 9.236 20.531 11.060L20.531 11.060Q20.645 11.554 20.645 11.896ZM17.339 12.010L17.339 12.010Q17.339 14.366 16.123 16.038L16.123 16.038Q15.971 16.380 15.648 16.475Q15.325 16.570 14.983 16.380Q14.641 16.190 14.546 15.867Q14.451 15.544 14.717 15.202L14.717 15.202Q15.325 14.290 15.553 12.846L15.553 12.846Q15.705 11.820 15.477 10.775Q15.249 9.730 14.717 8.818L14.717 8.818Q14.527 8.438 14.603 8.115Q14.679 7.792 14.945 7.621Q15.211 7.450 15.591 7.526Q15.971 7.602 16.123 7.868L16.123 7.868Q16.655 8.666 17.073 10.110L17.073 10.110L17.149 10.414Q17.339 11.516 17.339 12.010Z\"/>',\n\t'seti:windows':\n\t\t'<path d=\"M0.125 11.544L0.125 3.450L9.815 2.120L9.815 11.506L0.125 11.544ZM0.125 12.532L9.815 12.570L9.815 21.956L0.125 20.626L0.125 12.532ZM10.993 11.468L10.993 1.968L23.837 0.106L23.837 11.392L10.993 11.468ZM10.955 12.646L23.875 12.684L23.837 23.894L10.993 22.108L10.955 12.646Z\"/>',\n\t'seti:jenkins':\n\t\t'<path d=\"M13.216 8.608L13.216 8.608Q13.248 8.608 13.344 8.608L13.344 8.608L13.376 8.608Q14.176 8.544 15.200 7.904L15.200 7.904Q15.232 7.872 15.264 7.776L15.264 7.776L15.360 7.488Q15.488 7.200 15.232 6.976L15.232 6.976Q14.496 6.304 13.952 5.440L13.952 5.440Q13.888 5.312 13.792 5.088L13.792 5.088L13.696 4.864Q13.664 4.832 13.600 4.768L13.600 4.768L13.568 4.736L13.504 4.896Q13.504 4.992 13.536 5.152L13.536 5.152Q13.664 5.728 14.208 6.400L14.208 6.400Q14.528 6.816 14.848 7.168L14.848 7.168Q14.976 7.328 14.944 7.552Q14.912 7.776 14.688 7.840L14.688 7.840Q14.592 7.872 14.400 7.968L14.400 7.968L14.176 8.096Q13.632 8.320 13.344 8.320L13.344 8.320Q13.152 8.288 13.056 8.224Q12.960 8.160 12.928 8L12.928 8L12.864 7.712Q12.608 7.968 12.800 8.384L12.800 8.384Q12.832 8.544 12.944 8.576Q13.056 8.608 13.216 8.608ZM11.552 9.088L11.680 9.312Q11.936 9.568 12.416 9.728L12.416 9.728Q13.120 9.920 13.824 9.888L13.824 9.888L13.824 9.536Q13.824 9.312 13.632 9.312L13.632 9.312Q12.864 9.312 12.416 9.248L12.416 9.248Q12.224 9.248 11.840 9.152L11.840 9.152L11.552 9.088ZM11.168 5.760L11.136 5.760Q11.008 5.760 10.960 5.808Q10.912 5.856 10.960 5.984Q11.008 6.112 11.104 6.176L11.104 6.176Q11.616 6.400 12.080 6.208Q12.544 6.016 12.384 5.440L12.384 5.440Q12.320 5.184 12.032 4.800Q11.744 4.416 11.632 4.464Q11.520 4.512 11.776 5.184L11.776 5.184Q11.904 5.664 11.888 5.744Q11.872 5.824 11.168 5.760L11.168 5.760ZM10.176 4.352L10.176 4.352Q10.208 4.288 10.272 4.192L10.272 4.192Q10.752 3.232 11.616 3.424L11.616 3.424Q11.776 3.424 11.776 3.296Q11.776 3.168 11.680 3.072Q11.584 2.976 11.456 2.944L11.456 2.944Q11.072 2.848 10.720 3.104L10.720 3.104Q10.080 3.520 10.016 4.096L10.016 4.096Q10.016 4.320 10.176 4.352ZM14.112 9.280L14.112 9.280Q14.048 9.280 14.048 9.376L14.048 9.376L14.048 9.856Q14.528 9.856 14.928 9.536Q15.328 9.216 15.552 8.704L15.552 8.704L15.456 8.736L15.232 8.864Q14.528 9.248 14.112 9.280ZM15.136 6.016L15.136 6.016Q15.616 6.080 16 5.728L16 5.728Q16.096 5.632 16.032 5.504L16.032 5.504L15.840 5.120Q15.552 4.416 15.456 4.432Q15.360 4.448 15.424 4.768L15.424 4.768Q15.488 4.992 15.616 5.248L15.616 5.248Q15.680 5.440 15.648 5.488Q15.616 5.536 15.424 5.568L15.424 5.568L15.296 5.568Q14.912 5.536 14.864 5.632Q14.816 5.728 14.896 5.872Q14.976 6.016 15.136 6.016ZM19.264 16.960L19.264 16.960Q19.072 16.320 18.688 16.112Q18.304 15.904 17.632 16.064L17.632 16.064Q17.408 16.128 16.896 16.320L16.896 16.320L16.384 16.544L17.600 12.896Q17.856 12.128 17.248 11.648L17.248 11.648L16.608 11.136L16.224 10.816Q16 10.592 15.984 10.496Q15.968 10.400 16.128 10.112L16.128 10.112Q16.640 9.248 16.880 7.808Q17.120 6.368 17.088 5.056L17.088 5.056Q16.992 3.264 16.160 2.144L16.160 2.144Q14.848 0.352 12.800 0.128L12.800 0.128L12.480 0.096L11.776 0.096L10.944 0.224Q9.536 0.448 8.128 1.312L8.128 1.312L7.744 1.568Q7.008 2.016 6.720 2.816L6.720 2.816Q6.656 2.912 6.528 3.008L6.528 3.008Q5.888 3.456 5.856 4.320L5.856 4.320Q5.856 4.608 5.856 5.120L5.856 5.120L5.888 5.664Q5.888 5.728 5.824 5.856L5.824 5.856L5.728 6.144Q5.600 6.432 5.568 6.592L5.568 6.592Q5.408 7.840 6.208 8.736L6.208 8.736Q6.464 9.024 6.976 9.152L6.976 9.152Q7.104 9.216 7.136 9.344L7.136 9.344Q7.232 9.920 7.552 10.400L7.552 10.400Q7.648 10.528 7.680 10.688L7.680 10.688Q7.712 10.944 7.488 11.072L7.488 11.072L5.248 12.448Q5.120 12.512 5.024 12.576L5.024 12.576Q4.544 12.736 4.416 13.120L4.416 13.120L4.416 13.344L4.672 14.016Q5.056 15.040 5.216 15.584L5.216 15.584Q5.568 16.640 5.696 18.304L5.696 18.304L5.728 19.616Q5.760 19.936 6.048 20.064L6.048 20.064L8.000 20.928Q8.544 21.152 9.120 21.120L9.120 21.120Q9.248 21.120 9.312 21.248L9.312 21.248L10.016 23.488Q10.048 23.616 10.208 23.648L10.208 23.648L11.616 23.904L12.928 23.904L13.664 23.840Q14.080 23.744 14.368 23.648L14.368 23.648Q14.752 23.520 14.688 23.040L14.688 23.040Q14.656 22.816 14.704 22.768Q14.752 22.720 14.976 22.656L14.976 22.656L14.976 22.656Q15.424 22.528 15.536 22.336Q15.648 22.144 15.552 21.696L15.552 21.696L15.360 20.672L15.360 20.416Q15.360 20.256 15.536 20.208Q15.712 20.160 15.776 19.936L15.776 19.936Q15.776 19.840 15.840 19.808L15.840 19.808Q16.352 19.584 16.928 19.648L16.928 19.648Q16.992 19.648 16.992 19.648L16.992 19.648Q17.056 19.840 17.184 19.888Q17.312 19.936 17.536 19.936L17.536 19.936L17.536 19.936Q18.144 19.968 18.528 19.936L18.528 19.936Q19.232 19.872 19.488 19.008L19.488 19.008L19.584 18.560L19.584 18.176Q19.392 17.440 19.264 16.960ZM8.160 10.848L8.192 10.816Q7.744 9.920 7.616 9.216L7.616 9.216Q7.584 8.992 7.744 8.832Q7.904 8.672 7.936 8.592Q7.968 8.512 7.904 8.352L7.904 8.352Q7.904 8.352 7.840 8.352L7.840 8.352L7.744 8.384Q7.680 8.416 7.584 8.480L7.584 8.480L7.520 8.512Q7.104 8.800 6.688 8.512L6.688 8.512Q6.208 8.192 6.016 7.456L6.016 7.456Q5.856 6.848 6.144 6.400L6.144 6.400Q6.528 5.888 7.104 5.952L7.104 5.952Q7.456 6.016 7.648 6.400L7.648 6.400Q7.712 6.528 7.776 6.848L7.776 6.848L7.776 6.848Q7.808 6.976 7.936 6.976L7.936 6.976L8.288 6.912Q8.480 6.880 8.544 6.784Q8.608 6.688 8.544 6.464L8.544 6.464L8.384 5.728Q8.320 5.376 8.416 4.736L8.416 4.736L8.416 4.704Q8.576 3.936 8.608 3.552L8.608 3.552Q8.608 3.456 8.544 3.168L8.544 3.168L8.512 2.976Q8.512 2.816 8.544 2.720L8.544 2.720Q8.864 2.112 9.536 1.664Q10.208 1.216 10.944 0.832L10.944 0.832Q11.488 0.544 12.480 0.640Q13.472 0.736 14.464 1.248L14.464 1.248Q14.944 1.472 15.488 2.144L15.488 2.144L15.936 2.720Q15.648 2.688 15.488 2.720L15.488 2.720Q15.232 2.752 15.104 2.944L15.104 2.944Q15.040 3.040 15.008 3.296L15.008 3.296Q15.424 3.008 15.920 3.264Q16.416 3.520 16.576 3.936L16.576 3.936Q16.672 4.256 16.704 4.512L16.704 4.512Q16.832 5.952 16.736 6.880L16.736 6.880Q16.576 8.672 15.840 9.824L15.840 9.824Q15.360 10.624 15.008 10.912L15.008 10.912Q14.464 11.456 13.408 11.712L13.408 11.712Q12.704 11.872 12.096 11.680L12.096 11.680Q11.360 11.424 10.784 10.912L10.784 10.912Q10.336 10.528 9.696 9.632L9.696 9.632L9.568 9.472Q9.504 9.696 9.696 10.016L9.696 10.016Q9.984 10.528 10.656 11.200L10.656 11.200Q10.784 11.328 11.072 11.552L11.072 11.552L11.264 11.680L10.976 11.712Q9.408 11.808 9.088 11.712L9.088 11.712Q8.544 11.584 8.160 10.848L8.160 10.848ZM12.288 14.432L12.480 14.240Q12.768 14.528 13.184 15.328L13.184 15.328Q13.536 15.936 13.760 16.480L13.760 16.480L13.568 16.448Q13.312 16.384 13.184 16.320L13.184 16.320L11.488 15.616Q11.360 15.552 11.424 15.456L11.424 15.456L11.488 15.296Q11.680 14.976 11.808 14.832Q11.936 14.688 12.288 14.432L12.288 14.432ZM13.696 15.328L13.696 15.328Q13.568 15.072 13.280 14.560L13.280 14.560L13.056 14.144Q12.992 14.016 13.024 13.952L13.024 13.952Q13.088 13.792 13.216 13.824L13.216 13.824Q13.312 13.856 13.472 13.760L13.472 13.760Q13.728 13.632 14.016 13.824L14.016 13.824Q14.112 13.888 14.336 13.984L14.336 13.984L14.464 14.048Q14.464 14.592 14.400 15.328L14.400 15.328Q14.336 16.256 14.176 16.448L14.176 16.448L14.080 16.160Q13.824 15.584 13.696 15.328ZM13.152 23.200L13.152 23.200Q12.224 23.328 11.264 23.136L11.264 23.136Q10.976 23.104 10.528 22.976L10.528 22.976Q10.432 22.976 10.400 22.880L10.400 22.880L10.240 22.304Q9.952 21.184 9.824 20.608Q9.696 20.032 9.536 18.848L9.536 18.848L9.440 18.144L9.440 17.984L10.112 17.888Q11.648 17.664 12.768 17.728L12.768 17.728Q12.960 17.728 13.280 17.760L13.280 17.760L13.568 17.824Q13.664 17.824 13.696 17.952L13.696 17.952L13.952 22.240Q13.952 22.432 14.016 22.784L14.016 22.784L14.048 23.040L13.792 23.104Q13.376 23.168 13.152 23.200ZM14.944 22.176L14.944 22.176Q14.848 22.208 14.688 22.240L14.688 22.240L14.432 22.304L14.368 20.448L14.784 20.352Q14.816 20.352 14.864 20.368Q14.912 20.384 14.912 20.416L14.912 20.416L14.944 20.544Q15.104 21.408 15.168 21.856L15.168 21.856L15.168 21.856Q15.168 22.080 15.152 22.112Q15.136 22.144 14.944 22.176L14.944 22.176ZM14.624 16.416L14.624 16.384Q14.592 16.416 14.592 16.416Q14.592 16.416 14.592 16.416L14.592 16.416L14.816 14.656Q14.848 14.464 14.848 14.176L14.848 14.176Q14.848 14.016 15.040 13.968Q15.232 13.920 15.472 14.032Q15.712 14.144 15.808 14.336L15.808 14.336Q15.872 14.400 15.808 14.432L15.808 14.432L14.624 16.416ZM18.112 19.296L18.144 19.296Q18.112 19.296 18.048 19.296L18.048 19.296Q17.632 19.296 17.440 19.264L17.440 19.264L17.440 19.200L18.208 19.040L18.208 19.008L17.952 18.944L16.992 18.976Q16.864 19.008 16.832 18.976Q16.800 18.944 16.800 18.816L16.800 18.816L16.608 17.344Q16.576 17.280 16.640 17.248L16.640 17.248L17.344 16.928L17.824 16.736Q18.048 16.608 18.272 16.672Q18.496 16.736 18.592 16.992L18.592 16.992Q18.848 17.856 18.912 18.336L18.912 18.336Q18.976 18.720 18.752 18.992Q18.528 19.264 18.112 19.296L18.112 19.296ZM15.616 2.912L15.520 3.168Q15.584 3.168 15.648 3.200L15.648 3.200Q15.808 3.264 15.936 3.392L15.936 3.392Q16.160 3.552 16.320 3.808L16.320 3.808Q16.544 4.192 16.672 4.864L16.672 4.864L16.704 5.440L16.704 6.016Q16.704 6.720 16.608 7.296L16.608 7.296Q16.480 8.256 16 9.280L16 9.280Q15.616 10.016 15.104 10.688L15.104 10.688L14.656 11.232L15.808 10.272L16.608 8.640L16.928 6.688L16.928 4.768L16.640 3.136L15.616 2.912Z\"/>',\n\t'seti:babel':\n\t\t'<path d=\"M18.766 8.158L18.766 8.158L20.738 6.356Q21.554 5.064 21.554 3.534L21.554 3.534L21.554 3.296Q21.554 2.990 21.418 2.718L21.418 2.718Q20.874 1.630 19.582 1.120L19.582 1.120Q18.562 0.440 15.570 0.304L15.570 0.304Q12.170 0.780 9.450 1.970L9.450 1.970Q8.226 2.820 7.104 3.432L7.104 3.432L7.104 3.602Q7.138 3.602 7.206 3.568Q7.274 3.534 7.308 3.534L7.308 3.534Q7.444 3.534 7.444 3.602L7.444 3.602L7.614 3.534L7.682 3.534L7.682 3.602L7.614 3.704Q7.512 3.806 7.308 3.908Q7.104 4.010 6.424 4.520L6.424 4.520L5.982 4.860L6.220 5.030L6.050 4.928Q6.050 5.030 5.846 5.030L5.846 5.030L5.846 5.098L5.982 5.302Q5.846 5.302 5.744 5.234L5.744 5.234Q5.098 5.336 4.724 5.982L4.724 5.982L4.724 6.288L4.758 6.254Q5.030 5.982 5.234 5.846L5.234 5.846L5.234 6.050L5.166 6.050L5.030 6.152L5.030 6.220L5.166 6.424L5.166 6.526Q5.268 6.356 5.404 6.220L5.404 6.220L5.982 5.608L6.356 5.404Q6.662 5.506 6.662 5.608L6.662 5.608L6.798 5.608Q8.566 4.316 10.334 3.670L10.334 3.670L10.334 3.806Q10.266 3.976 10.028 4.180L10.028 4.180Q9.960 4.282 9.892 4.282L9.892 4.282Q9.892 4.418 10.028 4.554L10.028 4.554Q9.314 6.866 8.430 8.600L8.430 8.600Q5.880 15.298 2.446 21.588L2.446 21.588Q2.446 21.622 2.480 21.707Q2.514 21.792 2.514 21.826L2.514 21.826L2.718 21.758Q2.956 21.656 3.262 21.520L3.262 21.520L3.330 21.520L3.330 21.656L3.466 21.656L3.636 21.588L3.772 21.588L3.772 21.894L3.534 22.438Q3.092 23.152 3.024 23.628L3.024 23.628L3.024 23.696L3.262 23.696L3.466 23.390Q4.350 22.370 4.724 21.520L4.724 21.520L5.472 21.282Q6.220 21.078 6.798 20.840L6.798 20.840L6.934 20.772Q8.430 20.194 9.144 19.820L9.144 19.820Q9.756 19.820 10.385 19.599Q11.014 19.378 11.524 18.970L11.524 18.970L11.524 18.902L11.150 19.072L11.082 19.072L11.082 18.902Q12.034 18.800 12.612 18.392L12.612 18.392Q14.278 17.134 15.944 15.910L15.944 15.910Q19.378 13.360 19.276 10.980L19.276 10.980Q18.630 9.926 17.950 9.348L17.950 9.348L17.712 9.042Q17.712 8.770 18.766 8.158ZM15.196 13.836L15.196 13.836L13.156 15.468Q11.524 16.522 10.708 16.964L10.708 16.964Q8.702 18.188 6.492 18.970L6.492 18.970L6.220 19.072L6.118 19.072Q6.186 18.834 7.818 15.536L7.818 15.536L9.382 12.408Q11.592 12.068 13.666 11.150L13.666 11.150L14.176 11.082Q14.788 10.946 15.366 11.116Q15.944 11.286 16.386 11.660L16.386 11.660L16.386 11.966Q15.842 13.496 15.196 13.836ZM16.454 7.036L16.454 7.036Q15.672 7.818 14.686 8.362L14.686 8.362Q12.680 9.008 11.218 9.790L11.218 9.790Q11.150 9.790 11.082 9.722L11.082 9.722L10.844 9.722L10.844 9.586Q10.844 8.940 11.218 8.464L11.218 8.464Q11.456 7.138 11.728 7.036L11.728 7.036L13.428 3.228Q13.428 3.058 13.717 2.905Q14.006 2.752 14.550 2.718L14.550 2.718L14.754 2.718L14.754 2.922Q15.502 2.786 16.114 2.786L16.114 2.786Q16.998 2.684 17.525 2.854Q18.052 3.024 18.154 3.432L18.154 3.432L18.154 3.602L18.324 3.602L18.324 3.092L18.460 3.092Q18.936 3.330 19.038 3.738L19.038 3.738L19.038 3.908Q19.038 4.350 18.698 4.724L18.698 4.724Q18.528 4.724 18.528 4.486L18.528 4.486L18.392 4.486L18.392 4.928Q17.406 6.526 16.896 6.526L16.896 6.526Q16.726 6.798 16.454 7.036Z\"/>',\n\t'seti:bower':\n\t\t'<path d=\"M22.745 11.388L22.745 11.388Q22.631 11.350 22.327 11.198L22.327 11.198Q21.871 11.008 21.567 10.932L21.567 10.932Q19.971 10.476 16.779 9.982L16.779 9.982L15.145 9.754Q14.993 9.754 14.613 9.678Q14.233 9.602 13.967 9.640L13.967 9.640Q14.081 9.146 14.309 8.956Q14.537 8.766 15.145 8.652L15.145 8.652Q15.221 8.728 15.278 8.899Q15.335 9.070 15.373 9.165Q15.411 9.260 15.487 9.298Q15.563 9.336 15.639 9.260L15.639 9.260L16.475 9.260Q17.539 9.070 18.261 8.500Q18.983 7.930 19.439 6.904L19.439 6.904Q19.667 5.954 19.781 5.574L19.781 5.574Q20.009 3.940 20.959 2.952L20.959 2.952L21.225 2.724Q20.351 2.496 19.211 2.781Q18.071 3.066 17.045 3.788L17.045 3.788Q15.943 4.624 15.411 5.802L15.411 5.802Q15.297 5.764 14.993 5.707Q14.689 5.650 14.575 5.612Q14.461 5.574 14.404 5.498Q14.347 5.422 14.347 5.346L14.347 5.346Q13.587 3.332 11.725 2.154L11.725 2.154Q11.041 1.736 10.091 1.584L10.091 1.584Q9.255 1.432 8.381 1.546L8.381 1.546Q4.695 2.078 2.339 5.004L2.339 5.004Q1.199 6.410 0.648 8.120Q0.097 9.830 0.211 11.654L0.211 11.654Q0.363 15.340 2.567 18.646L2.567 18.646Q2.947 19.178 4.011 20.204L4.011 20.204L4.467 20.660Q5.113 21.040 5.835 20.983Q6.557 20.926 7.089 20.432L7.089 20.432Q7.127 20.356 7.260 20.185Q7.393 20.014 7.431 19.938L7.431 19.938Q7.431 20.014 7.488 20.071Q7.545 20.128 7.545 20.204L7.545 20.204Q7.659 20.394 7.849 20.926L7.849 20.926Q8.039 21.534 8.153 21.724L8.153 21.724Q8.343 22.104 8.856 22.294Q9.369 22.484 9.825 22.332L9.825 22.332Q9.977 22.142 10.167 22.332L10.167 22.332Q10.889 22.674 11.611 22.332L11.611 22.332Q11.687 22.294 11.839 22.142L11.839 22.142Q12.029 21.990 12.181 21.933Q12.333 21.876 12.675 21.952L12.675 21.952L12.903 21.990Q13.435 21.876 13.701 21.648L13.701 21.648Q14.081 21.344 14.081 20.774L14.081 20.774L14.119 20.736Q14.119 20.660 14.195 20.660L14.195 20.660Q14.385 20.584 14.537 20.413Q14.689 20.242 14.689 20.052L14.689 20.052Q14.841 19.558 14.689 18.874L14.689 18.874L14.309 18.228Q13.815 17.240 13.511 16.860L13.511 16.860L13.397 16.746Q14.081 16.974 14.575 17.088L14.575 17.088Q15.639 17.316 16.209 16.746L16.209 16.746Q16.285 16.746 16.342 16.803Q16.399 16.860 16.475 16.860L16.475 16.860Q17.159 17.316 18.033 17.164Q18.907 17.012 19.439 16.404L19.439 16.404L19.667 16.404Q20.769 16.556 21.567 15.910L21.567 15.910Q21.795 15.682 21.909 15.530L21.909 15.530Q22.061 15.302 22.061 15.074L22.061 15.074L22.061 14.960Q22.973 14.884 23.391 14.466Q23.809 14.048 23.809 13.174L23.809 13.174Q23.733 12.490 23.505 12.072Q23.277 11.654 22.745 11.388ZM17.539 8.196L17.539 8.196Q16.817 8.538 15.867 8.538L15.867 8.538L15.829 8.538Q15.753 8.500 15.715 8.405Q15.677 8.310 15.620 8.082Q15.563 7.854 15.544 7.740Q15.525 7.626 15.544 7.607Q15.563 7.588 15.639 7.588L15.639 7.588Q15.943 7.588 16.019 7.664Q16.095 7.740 16.095 8.082L16.095 8.082Q16.285 7.816 16.361 7.778Q16.437 7.740 16.703 7.854L16.703 7.854Q16.817 7.892 17.064 7.949Q17.311 8.006 17.425 8.082L17.425 8.082L17.615 8.196Q17.615 8.196 17.539 8.196ZM15.981 5.688L15.981 5.688Q16.475 4.814 17.767 3.788L17.767 3.788Q18.755 3.218 19.667 3.218L19.667 3.218Q19.439 3.446 19.325 3.674L19.325 3.674Q19.059 4.016 18.907 4.890L18.907 4.890L18.831 5.346Q18.603 6.524 18.375 7.132L18.375 7.132Q18.375 7.284 18.223 7.436L18.223 7.436Q18.147 7.550 18.147 7.588L18.147 7.588Q17.843 7.322 17.235 6.866L17.235 6.866L16.931 6.638L16.931 6.410L17.083 6.144Q17.539 5.384 17.805 5.004L17.805 5.004Q18.223 4.396 18.717 4.054L18.717 4.054Q17.919 4.358 17.387 5.004L17.387 5.004Q17.007 5.422 16.475 6.410L16.475 6.410L15.867 6.068Q15.867 5.992 15.924 5.878Q15.981 5.764 15.981 5.688ZM12.789 7.702L12.789 7.702Q12.789 6.676 13.245 6.068L13.245 6.068Q13.245 5.992 13.397 5.954L13.397 5.954L13.511 5.954Q15.221 6.106 16.589 7.132L16.589 7.132Q16.703 7.170 16.874 7.360Q17.045 7.550 17.159 7.588L17.159 7.588Q17.045 7.550 16.760 7.493Q16.475 7.436 16.361 7.360L16.361 7.360Q15.297 7.132 14.461 7.474L14.461 7.474Q13.853 7.930 12.789 7.702L12.789 7.702Q13.017 7.968 13.302 8.006Q13.587 8.044 13.967 7.968L13.967 7.968Q14.271 7.968 14.727 7.778L14.727 7.778L14.917 7.740Q14.917 7.778 14.879 7.892L14.879 7.892L14.803 7.968Q14.423 8.234 13.359 8.652L13.359 8.652L13.017 8.804L12.903 8.804Q12.789 8.424 12.789 7.702ZM9.217 4.852L9.217 4.852Q10.053 4.852 10.699 5.498Q11.345 6.144 11.345 7.018Q11.345 7.892 10.737 8.519Q10.129 9.146 9.236 9.146Q8.343 9.146 7.716 8.519Q7.089 7.892 7.089 6.980Q7.089 6.068 7.697 5.460Q8.305 4.852 9.217 4.852ZM14.689 12.110L14.689 12.110Q15.639 12.338 16.361 12.338L16.361 12.338L18.489 12.718Q18.679 12.718 18.717 12.813Q18.755 12.908 18.603 13.060L18.603 13.060Q18.413 13.326 18.033 13.478Q17.653 13.630 17.311 13.554L17.311 13.554L17.045 13.554Q17.159 13.706 17.083 13.934Q17.007 14.162 16.817 14.238L16.817 14.238Q16.361 14.732 15.639 14.732L15.639 14.732Q15.525 14.732 15.221 14.675Q14.917 14.618 14.803 14.618L14.803 14.618Q14.727 14.960 14.347 15.150Q13.967 15.340 13.454 15.302Q12.941 15.264 12.561 14.960L12.561 14.960Q12.789 15.682 12.903 16.138L12.903 16.138L12.903 16.518Q12.713 17.848 11.763 18.741Q10.813 19.634 9.445 19.710L9.445 19.710Q6.861 19.900 4.695 18.304L4.695 18.304Q2.833 17.088 1.845 14.960L1.845 14.960Q1.845 14.922 1.788 14.846Q1.731 14.770 1.731 14.732L1.731 14.732Q2.681 14.960 3.061 15.074L3.061 15.074Q3.783 15.188 4.277 15.112L4.277 15.112Q4.961 14.998 5.531 14.618L5.531 14.618L5.797 14.618Q7.165 14.998 8.381 14.846L8.381 14.846Q9.331 14.694 10.129 14.162L10.129 14.162Q10.851 13.668 11.611 12.832L11.611 12.832Q11.953 12.490 12.447 11.540L12.447 11.540Q12.675 11.160 13.131 10.704L13.131 10.704L13.397 10.704L16.361 11.046L17.615 11.274Q19.325 11.616 20.275 11.768L20.275 11.768L20.313 11.768Q20.389 11.806 20.389 11.882L20.389 11.882L19.173 11.958Q16.171 12.224 14.689 12.110ZM9.217 8.196L9.217 8.196Q9.787 8.196 10.148 7.873Q10.509 7.550 10.528 7.018Q10.547 6.486 10.167 6.144Q9.787 5.802 9.236 5.802Q8.685 5.802 8.305 6.182Q7.925 6.562 7.925 7.018Q7.925 7.474 8.286 7.835Q8.647 8.196 9.217 8.196ZM8.495 6.068L8.495 6.068Q8.571 5.992 8.780 5.935Q8.989 5.878 9.103 5.802L9.103 5.802L9.711 6.068Q9.863 6.144 9.863 6.315Q9.863 6.486 9.692 6.657Q9.521 6.828 9.141 6.828Q8.761 6.828 8.381 6.638L8.381 6.638Q8.191 6.220 8.495 6.068Z\"/>',\n\t'seti:docker':\n\t\t'<path d=\"M10.109 6.473L10.109 4.427L12.188 4.427L12.188 6.473L10.109 6.473ZM10.109 8.849L10.109 6.803L12.188 6.803L12.188 8.849L10.109 8.849ZM10.109 11.324L10.109 9.277L12.188 9.277L12.188 11.324L10.109 11.324ZM7.733 8.849L7.733 6.803L9.812 6.803L9.812 8.849L7.733 8.849ZM7.733 11.324L7.733 9.277L9.812 9.277L9.812 11.324L7.733 11.324ZM5.258 8.849L5.258 6.803L7.337 6.803L7.337 8.849L5.258 8.849ZM5.258 11.324L5.258 9.277L7.337 9.277L7.337 11.324L5.258 11.324ZM2.882 11.324L2.882 9.277L4.961 9.277L4.961 11.324L2.882 11.324ZM12.584 11.324L12.584 9.277L14.663 9.277L14.663 11.324L12.584 11.324ZM23.111 9.575L23.111 9.575Q22.286 9.377 21.758 9.377L21.758 9.377Q21.032 9.509 20.735 8.750Q20.438 7.991 19.811 7.495L19.811 7.495Q19.349 6.968 18.920 7.083Q18.491 7.198 18.260 7.826L18.260 7.826Q17.963 8.816 18.161 10.103L18.161 10.103Q18.326 10.796 18.177 11.043Q18.029 11.291 17.319 11.456Q16.610 11.620 8.492 11.654L8.492 11.654Q4.433 11.654 0.539 11.620L0.539 11.620L0.539 11.620Q0.374 11.620 0.291 11.934Q0.209 12.248 0.209 12.677L0.209 12.677Q0.209 13.238 0.308 13.997L0.308 13.997Q0.407 14.987 0.638 15.449L0.638 15.449Q1.430 17.230 2.717 18.287L2.717 18.287Q4.136 19.442 5.984 19.574L5.984 19.574L10.637 19.574Q12.782 19.508 14.696 18.485L14.696 18.485Q16.412 17.593 18.062 15.878L18.062 15.878Q19.382 14.360 20.108 12.776L20.108 12.776Q20.405 12.215 21.065 12.082Q21.725 11.951 22.055 11.852L22.055 11.852Q22.550 11.720 22.913 11.422L22.913 11.422Q23.012 11.225 23.309 10.928L23.309 10.928Q23.837 10.532 23.787 10.136Q23.738 9.739 23.111 9.575ZM7.733 14.822L7.733 14.822Q8.063 14.822 8.310 15.053Q8.558 15.284 8.558 15.663Q8.558 16.043 8.343 16.257Q8.129 16.471 7.766 16.471Q7.403 16.471 7.155 16.257Q6.908 16.043 6.908 15.663Q6.908 15.284 7.155 15.053Q7.403 14.822 7.733 14.822ZM2.684 17.099L2.684 17.099Q3.047 17.066 3.740 17.099L3.740 17.099Q4.664 17.132 5.060 17.000L5.060 17.000Q5.819 16.736 6.330 16.917Q6.842 17.099 7.238 17.825L7.238 17.825Q7.403 18.188 7.799 18.485L7.799 18.485Q8.030 18.650 8.624 18.980L8.624 18.980L8.987 19.178Q7.271 19.409 5.555 18.831Q3.839 18.254 2.684 17.099Z\"/>',\n\t'seti:code-climate':\n\t\t'<path d=\"M15.627 15.494L15.813 15.742Q15.782 15.804 15.689 15.897L15.689 15.897L13.364 18.098Q13.271 18.222 13.178 18.207Q13.085 18.191 12.992 18.067L12.992 18.067L8.094 13.386L3.320 18.005Q3.134 18.160 3.010 18.160Q2.886 18.160 2.731 18.005L2.731 18.005Q2.204 17.478 1.057 16.393L1.057 16.393L0.282 15.649L0.499 15.494Q0.778 15.339 0.871 15.215L0.871 15.215Q4.653 11.619 7.846 8.550L7.846 8.550Q8.001 8.395 8.110 8.395Q8.218 8.395 8.373 8.519L8.373 8.519L15.627 15.494ZM11.535 10.038L15.968 5.791Q16.681 6.473 18.107 7.837L18.107 7.837L23.718 13.200L21.083 15.742L15.968 10.875L14.170 12.580L11.535 10.038Z\"/>',\n\t'seti:eslint':\n\t\t'<path d=\"M23.862 12.000L18.570 1.884L5.862 1.884L0.138 12.000L5.862 22.116L18.570 22.116L23.862 12.000ZM19.146 16.176L12.054 20.208L4.962 16.176L4.962 8.184L12.054 3.792L19.146 8.184L19.146 16.176ZM12.054 6.492L7.338 9.408L7.338 14.808L12.054 17.508L16.770 14.808L16.770 9.408L12.054 6.492Z\"/>',\n\t'seti:firebase':\n\t\t'<path d=\"M12.297 4.025L14.640 8.777L12.297 10.955L10.119 6.566L11.241 4.025Q11.472 3.662 11.769 3.662Q12.066 3.662 12.297 4.025L12.297 4.025ZM10.119 6.566L12.297 10.955L3.552 19.073L10.119 6.566ZM3.552 19.073L17.214 5.444Q17.511 5.114 17.792 5.213Q18.072 5.312 18.171 5.741L18.171 5.741L20.448 18.974L12.891 23.495Q12.726 23.594 12.297 23.660L12.297 23.660L11.934 23.693L11.571 23.660Q11.208 23.594 11.043 23.495L11.043 23.495L3.552 19.073ZM7.215 0.659L10.119 6.566L3.552 19.073L6.489 0.791Q6.555 0.362 6.770 0.312Q6.984 0.263 7.215 0.659L7.215 0.659Z\"/>',\n\t'seti:firefox':\n\t\t'<path d=\"M23.928 10.888L23.886 10.594Q23.718 9.544 23.634 8.998L23.634 8.998L23.214 9.880L23.130 9.880Q23.550 6.058 20.946 3.790L20.946 3.790L20.862 4.000Q20.442 3.454 19.434 2.740L19.434 2.740Q19.308 3.118 19.644 3.454L19.644 3.454Q20.862 4.546 21.576 5.806L21.576 5.806L21.702 6.016Q21.282 5.428 20.232 4.420L20.232 4.420L19.812 4.000Q18.636 2.740 16.746 2.152L16.746 2.152L16.200 1.984L16.662 2.404Q17.544 3.076 17.964 3.454L17.964 3.454Q18.468 3.958 18.909 4.525Q19.350 5.092 19.392 5.302L19.392 5.302Q18.342 4.462 17.376 4.378L17.376 4.378Q19.980 6.730 19.686 10.132L19.686 10.132Q19.308 9.418 18.720 9.040L18.720 9.040L18.888 10.636Q19.014 11.854 18.888 12.484L18.888 12.484L18.636 13.618L18.342 13.030L18.216 13.618Q17.964 14.542 17.838 15.004L17.838 15.004Q17.544 16.054 16.872 16.684L16.872 16.684Q16.704 16.810 16.494 16.936L16.494 16.936Q16.452 16.978 16.368 16.978L16.368 16.978L16.284 16.978L16.284 16.684L16.200 16.684Q16.074 16.684 16.032 16.726L16.032 16.726Q15.864 16.894 15.696 17.062L15.696 17.062Q15.402 17.482 14.898 17.776L14.898 17.776L14.982 17.440L13.890 17.692Q13.218 17.818 12.882 17.860L12.882 17.860Q12.336 17.944 11.874 17.860L11.874 17.860Q11.160 17.734 10.824 17.356L10.824 17.356L11.538 17.356Q11.412 17.230 11.286 17.188L11.286 17.188L10.026 16.894Q9.438 16.726 9.165 16.558Q8.892 16.390 8.430 15.886L8.430 15.886L8.346 15.802Q7.044 14.542 7.338 12.484L7.338 12.484Q7.422 11.854 7.506 11.560L7.506 11.560Q7.674 11.098 8.052 10.804L8.052 10.804Q7.632 10.510 7.170 10.510L7.170 10.510Q6.498 10.468 5.910 10.909Q5.322 11.350 5.112 12.022L5.112 12.022Q4.566 13.618 5.742 15.172L5.742 15.172L5.868 15.340Q5.112 14.710 4.839 13.639Q4.566 12.568 4.986 11.602L4.986 11.602Q5.322 10.804 6.015 10.405Q6.708 10.006 7.485 10.132Q8.262 10.258 8.850 10.846L8.850 10.846L9.060 11.098Q9.186 10.720 9.165 10.237Q9.144 9.754 8.934 9.460L8.934 9.460Q7.884 8.032 8.199 6.457Q8.514 4.882 9.774 3.664L9.774 3.664L9.942 3.496Q9.060 3.286 8.094 3.874Q7.128 4.462 6.204 5.722L6.204 5.722L6.372 4.714L6.036 4.672Q4.104 4.420 2.634 5.638L2.634 5.638Q1.206 6.772 0.576 8.830L0.576 8.830L0.072 10.636L0.114 10.636L0.450 10.132Q0.282 11.056 0.198 12.148Q0.114 13.240 0.156 13.786L0.156 13.786L0.408 13.030Q0.954 17.062 3.516 19.750L3.516 19.750Q4.776 21.052 6.855 22.249Q8.934 23.446 9.858 23.362L9.858 23.362L9.396 23.068Q9.648 23.110 10.110 23.236L10.110 23.236L10.530 23.320L11.916 23.656L12.672 23.656L12.294 23.320L13.596 23.236Q15.528 23.110 17.418 22.438L17.418 22.438Q17.964 22.270 18.468 21.808L18.468 21.808Q18.804 21.472 19.266 20.884L19.266 20.884Q19.434 20.632 19.644 20.506L19.644 20.506Q21.492 19.498 22.542 17.608L22.542 17.608Q23.046 16.726 22.794 15.592L22.794 15.592Q22.752 15.382 22.836 15.172L22.836 15.172L23.088 14.584Q23.382 13.996 23.487 13.681Q23.592 13.366 23.760 12.694L23.760 12.694L23.928 12.064L23.928 10.888ZM8.178 12.946L7.800 12.694Q7.632 13.618 8.052 14.542Q8.472 15.466 9.270 15.928L9.270 15.928Q9.354 15.970 9.480 15.970L9.480 15.970Q11.412 16.096 12.798 14.962L12.798 14.962L13.092 14.710Q13.470 14.374 14.058 14.458L14.058 14.458Q14.268 14.500 14.373 14.395Q14.478 14.290 14.436 14.080L14.436 14.080Q14.352 13.618 13.932 13.366L13.932 13.366Q12.630 12.694 11.412 13.366L11.412 13.366Q11.076 13.576 10.740 13.660L10.740 13.660Q9.942 13.870 9.018 13.408L9.018 13.408Q8.766 13.282 8.178 12.946L8.178 12.946ZM3.894 4.126L3.978 4.084Q4.062 4.000 4.104 3.958L4.104 3.958Q8.430-0.074 14.100 1.102L14.100 1.102Q15.024 1.312 16.830 1.858L16.830 1.858L18.006 2.194Q15.822 0.682 13.134 0.409Q10.446 0.136 7.905 1.018Q5.364 1.900 3.558 3.832L3.558 3.832L3.894 4.126ZM11.664 7.234L11.664 7.234Q11.706 6.982 11.622 6.898Q11.538 6.814 11.328 6.772L11.328 6.772Q10.908 6.730 10.026 6.730L10.026 6.730L9.816 6.730Q9.438 6.688 9.270 6.646L9.270 6.646Q8.934 6.604 8.724 6.352L8.724 6.352Q8.472 6.940 8.682 7.864Q8.892 8.788 9.354 9.208L9.354 9.208L9.858 8.914Q10.614 8.494 11.034 8.284L11.034 8.284Q11.622 7.990 11.664 7.234ZM3.642 4.420L3.642 4.420Q2.844 3.748 2.760 2.614L2.760 2.614Q2.130 3.286 1.836 4.168L1.836 4.168Q1.626 4.882 1.584 5.890L1.584 5.890Q2.550 4.882 3.642 4.420Z\"/>',\n\t'seti:gitlab':\n\t\t'<path d=\"M8.048 9.283L12.000 23.799L15.952 9.283L8.048 9.283ZM2.538 9.283L12.000 23.799L8.048 9.283L2.538 9.283ZM12.000 23.799L2.538 9.283L1.360 13.691Q1.284 13.995 1.360 14.299Q1.436 14.603 1.664 14.793L1.664 14.793L12.000 23.799ZM4.932 0.543L2.538 9.283L8.048 9.283L5.692 0.543Q5.616 0.201 5.312 0.201Q5.008 0.201 4.932 0.543L4.932 0.543ZM21.462 9.283L12.000 23.799L15.952 9.283L21.462 9.283ZM12.000 23.799L21.462 9.283L22.640 13.691Q22.716 13.995 22.640 14.299Q22.564 14.603 22.336 14.793L22.336 14.793L12.000 23.799ZM19.068 0.543L21.462 9.283L15.952 9.283L18.308 0.543Q18.384 0.201 18.688 0.201Q18.992 0.201 19.068 0.543L19.068 0.543Z\"/>',\n\t'seti:grunt':\n\t\t'<path d=\"M19.485 12.265L19.485 12.265Q19.596 12.228 19.855 12.117L19.855 12.117Q20.632 11.747 21.002 11.488L21.002 11.488Q21.483 11.118 21.668 10.637Q21.853 10.156 21.668 9.712Q21.483 9.268 21.002 9.046L21.002 9.046Q20.706 8.898 20.521 8.343L20.521 8.343Q20.373 7.344 20.891 6.493L20.891 6.493Q21.483 5.642 21.113 5.013Q20.743 4.384 19.596 4.310L19.596 4.310L19.485 4.310L19.374 4.199L19.374 4.125Q19.263 3.607 19.300 3.385L19.300 3.385Q19.374 2.978 19.818 2.682L19.818 2.682Q20.077 2.571 20.743 2.349L20.743 2.349Q21.002 2.238 21.335 2.238L21.335 2.238Q21.335 2.090 21.224 2.090L21.224 2.090L21.224 2.090Q20.669 1.461 19.818 1.202L19.818 1.202Q19.078 0.980 18.227 1.054L18.227 1.054Q17.265 1.165 16.488 1.646L16.488 1.646Q15.896 2.016 15.193 2.793L15.193 2.793Q15.045 3.126 14.638 2.904L14.638 2.904Q14.416 2.793 14.416 2.571L14.416 2.571Q14.416 2.460 14.471 2.220Q14.527 1.979 14.527 1.868L14.527 1.868L14.860 1.165Q14.453 1.165 14.046 1.350L14.046 1.350Q13.824 1.461 13.343 1.757L13.343 1.757L13.121 1.868Q12.862 1.461 12.843 1.091Q12.825 0.721 13.010 0.240L13.010 0.240Q12.159 0.536 11.715 0.906L11.715 0.906Q11.197 1.387 11.049 2.090L11.049 2.090Q10.864 1.942 10.827 1.646L10.827 1.646Q10.790 1.461 10.790 1.128L10.790 1.128L10.827 0.832L10.716 0.832Q10.642 0.832 10.586 0.888Q10.531 0.943 10.457 0.943L10.457 0.943Q9.606 1.646 9.421 2.571L9.421 2.571Q9.421 2.645 9.365 2.756Q9.310 2.867 9.310 2.904L9.310 2.904Q9.273 2.867 9.143 2.812Q9.014 2.756 8.977 2.682L8.977 2.682Q8.755 2.497 8.292 2.109Q7.830 1.720 7.571 1.535L7.571 1.535Q6.535 0.943 5.554 0.925Q4.574 0.906 3.538 1.424L3.538 1.424L2.613 2.349Q2.835 2.460 3.297 2.571Q3.760 2.682 3.982 2.793L3.982 2.793Q4.611 2.978 4.777 3.348Q4.944 3.718 4.685 4.421L4.685 4.421Q4.574 4.421 4.352 4.477Q4.130 4.532 3.982 4.532L3.982 4.532Q3.390 4.717 3.131 5.106Q2.872 5.494 2.946 6.049L2.946 6.049Q3.020 6.197 3.186 6.549Q3.353 6.900 3.427 7.085L3.427 7.085Q3.427 7.122 3.482 7.252Q3.538 7.381 3.538 7.418L3.538 7.418L3.538 8.602Q3.538 8.824 3.057 9.268L3.057 9.268L2.872 9.453Q2.465 9.823 2.391 10.082L2.391 10.082Q2.132 10.489 2.317 10.970Q2.502 11.451 2.946 11.821L2.946 11.821Q3.538 12.265 4.463 12.635L4.463 12.635Q5.388 13.079 5.388 14.004L5.388 14.004Q5.388 15.299 5.277 15.965L5.277 15.965Q5.277 16.076 5.203 16.372L5.203 16.372Q5.166 16.705 5.166 16.890L5.166 16.890Q4.574 16.446 4.259 15.984Q3.945 15.521 3.871 14.929L3.871 14.929Q2.835 15.817 2.835 16.779L2.835 16.779Q2.650 18.000 3.168 18.962L3.168 18.962Q3.723 19.998 5.055 20.479L5.055 20.479Q5.166 20.479 5.388 20.738L5.388 20.738Q6.424 22.440 8.496 22.810L8.496 22.810Q8.755 22.810 8.755 22.921L8.755 22.921Q9.643 23.476 10.642 23.661L10.642 23.661Q11.530 23.809 12.677 23.735L12.677 23.735Q13.417 23.624 13.787 23.513L13.787 23.513Q14.342 23.365 14.749 23.032L14.749 23.032Q14.823 22.995 14.989 22.921Q15.156 22.847 15.193 22.810L15.193 22.810Q17.117 22.477 18.227 20.849L18.227 20.849L18.560 20.479Q19.707 20.109 20.188 19.332L20.188 19.332Q20.595 18.777 20.743 18.000L20.743 18.000Q20.854 17.334 20.780 16.668L20.780 16.668Q20.669 16.187 20.428 15.799Q20.188 15.410 19.707 14.929L19.707 14.929Q19.559 15.558 19.263 16.021Q18.967 16.483 18.449 16.890L18.449 16.890Q18.338 15.854 18.338 13.893L18.338 13.893Q18.523 13.227 18.782 12.839Q19.041 12.450 19.485 12.265ZM14.971 18.407L9.791 18.407L14.971 18.407Z\"/>',\n\t'seti:gulp':\n\t\t'<path d=\"M15.840 18.343L15.840 18.343Q15.660 18.643 15.465 19.018Q15.270 19.393 15.270 19.663L15.270 19.663L15 22.573Q15 23.083 14.520 23.233L14.520 23.233Q14.190 23.323 13.500 23.563Q12.810 23.803 12.480 23.893L12.480 23.893L11.430 23.893Q11.400 23.893 11.250 23.848Q11.100 23.803 11.070 23.803L11.070 23.803Q9.930 23.413 9.480 23.143L9.480 23.143Q9.180 22.843 9.180 22.663L9.180 22.663Q9.090 21.913 9.090 20.503L9.090 20.503Q9.090 19.753 9.030 19.423L9.030 19.423Q8.910 18.823 8.520 18.463L8.520 18.463L8.520 18.343Q12.060 19.333 15.840 18.343ZM17.250 6.463L17.250 6.463Q17.190 7.093 17.055 8.413Q16.920 9.733 16.890 10.393L16.890 10.393L16.230 17.233Q16.230 18.103 15.270 18.253L15.270 18.253Q14.250 18.463 12.570 18.643L12.570 18.643Q10.470 18.793 8.640 18.253L8.640 18.253Q8.310 18.193 8.160 17.953L8.160 17.953Q8.070 17.803 7.980 17.413L7.980 17.413Q7.980 16.843 7.770 16.123L7.770 16.123L7.410 12.643Q7.410 12.583 7.365 12.493Q7.320 12.403 7.320 12.373L7.320 12.373Q7.260 11.383 6.960 9.493L6.960 9.493L6.750 7.963Q6.660 7.483 6.660 6.553L6.660 6.553Q12.120 7.693 17.250 6.463ZM6.660 5.893L6.660 5.893Q7.020 5.713 7.230 5.713L7.230 5.713L9.840 5.413Q9.900 5.413 10.080 5.323L10.080 5.323L10.230 5.233Q10.140 4.993 10.020 4.513L10.020 4.513Q9.900 3.943 9.780 3.643L9.780 3.643Q9.600 3.193 9.270 2.893L9.270 2.893Q8.910 2.503 8.205 1.708Q7.500 0.913 7.140 0.553L7.140 0.553Q7.440 0.223 7.560 0.148Q7.680 0.073 7.785 0.133Q7.890 0.193 8.160 0.463L8.160 0.463L8.520 0.823Q8.940 1.303 9.180 1.483L9.180 1.483Q10.770 2.773 11.070 4.753L11.070 4.753Q11.190 5.173 11.400 5.293Q11.610 5.413 12 5.413L12 5.413L16.680 5.713Q16.830 5.713 17.130 5.803L17.130 5.803L17.340 5.893Q16.590 6.463 14.070 6.673L14.070 6.673Q11.760 6.883 9.480 6.673L9.480 6.673Q7.110 6.433 6.660 5.893Z\"/>',\n\t'seti:ionic':\n\t\t'<path d=\"M22.555 6.765L22.555 6.765Q23.277 6.081 23.277 4.789Q23.277 3.497 22.384 2.585Q21.491 1.673 20.199 1.673L20.199 1.673Q19.477 1.673 18.527 2.167L18.527 2.167Q15.753 0.115 11.991 0.115L11.991 0.115Q8.761 0.115 6.025 1.749L6.025 1.749Q3.327 3.345 1.731 6.043L1.731 6.043Q0.097 8.779 0.116 12.009Q0.135 15.239 1.731 17.975L1.731 17.975Q3.327 20.673 6.025 22.269L6.025 22.269Q8.761 23.903 11.991 23.884Q15.221 23.865 17.957 22.269L17.957 22.269Q20.655 20.673 22.251 17.975L22.251 17.975Q23.885 15.239 23.885 12.009L23.885 12.009Q23.885 9.197 22.555 6.765ZM11.991 21.965L11.991 21.965Q9.255 22.003 6.937 20.597L6.937 20.597Q4.657 19.305 3.346 17.006Q2.035 14.707 2.035 12.009Q2.035 9.311 3.384 7.031Q4.733 4.751 7.013 3.402Q9.293 2.053 11.991 2.053L11.991 2.053Q15.069 2.015 17.463 3.687L17.463 3.687Q17.235 4.409 17.197 4.751L17.197 4.751Q17.235 6.043 18.109 6.955Q18.983 7.867 20.313 7.867L20.313 7.867Q20.769 7.867 21.035 7.715L21.035 7.715Q21.985 9.843 21.985 12.009L21.985 12.009Q21.985 14.745 20.655 17.044Q19.325 19.343 17.026 20.654Q14.727 21.965 11.991 21.965ZM11.991 7.145L11.991 7.145Q10.661 7.145 9.540 7.791Q8.419 8.437 7.735 9.539Q7.051 10.641 7.032 11.990Q7.013 13.339 7.659 14.460Q8.305 15.581 9.407 16.265Q10.509 16.949 11.858 16.968Q13.207 16.987 14.328 16.341Q15.449 15.695 16.133 14.593Q16.817 13.491 16.836 12.142Q16.855 10.793 16.209 9.634Q15.563 8.475 14.442 7.810Q13.321 7.145 11.991 7.145Z\"/>',\n\t'seti:platformio':\n\t\t'<path d=\"M16.482 5.484L16.482 5.484L17.166 3.036Q17.706 3 18.084 2.604Q18.462 2.208 18.462 1.668Q18.462 1.128 18.048 0.714Q17.634 0.300 17.058 0.300Q16.482 0.300 16.068 0.714Q15.654 1.128 15.654 1.668L15.654 1.668Q15.654 2.028 15.834 2.352Q16.014 2.676 16.338 2.856L16.338 2.856L15.618 5.268Q14.754 5.052 13.818 4.944L13.818 4.944Q13.134 4.836 12.486 4.800L12.486 4.800L11.982 4.800L11.694 4.944L11.694 23.592L11.982 23.700Q12.738 23.700 14.358 22.764L14.358 22.764Q16.086 21.756 17.706 20.244L17.706 20.244Q19.614 18.516 20.694 16.644L20.694 16.644Q22.026 14.448 22.026 12.396L22.026 12.396Q22.026 9.552 20.262 7.716L20.262 7.716Q18.858 6.240 16.482 5.484ZM14.502 17.508L14.502 17.508Q13.710 16.248 13.728 14.232Q13.746 12.216 14.718 10.668L14.718 10.668Q15.798 8.904 17.742 8.508L17.742 8.508Q18.678 8.364 19.506 8.940L19.506 8.940Q20.406 9.516 20.694 10.668L20.694 10.668Q21.054 11.856 20.334 13.368L20.334 13.368Q19.686 14.664 18.426 15.888L18.426 15.888Q17.238 17.004 16.086 17.508Q14.934 18.012 14.502 17.508ZM17.022 12.252L17.022 12.252Q16.482 12.252 16.104 12.612Q15.726 12.972 15.726 13.494Q15.726 14.016 16.104 14.376Q16.482 14.736 17.004 14.736Q17.526 14.736 17.904 14.376Q18.282 14.016 18.282 13.494Q18.282 12.972 17.904 12.612Q17.526 12.252 17.022 12.252ZM17.382 13.404L17.382 13.404Q17.274 13.404 17.166 13.314Q17.058 13.224 17.058 13.098Q17.058 12.972 17.166 12.864Q17.274 12.756 17.400 12.756Q17.526 12.756 17.616 12.864Q17.706 12.972 17.706 13.098Q17.706 13.224 17.616 13.314Q17.526 13.404 17.382 13.404ZM8.346 5.304L8.346 5.304L7.662 2.820Q7.950 2.640 8.130 2.334Q8.310 2.028 8.310 1.668L8.310 1.668Q8.310 1.092 7.896 0.696Q7.482 0.300 6.888 0.300Q6.294 0.300 5.898 0.696Q5.502 1.092 5.502 1.650Q5.502 2.208 5.898 2.622Q6.294 3.036 6.906 3.036L6.906 3.036L7.590 5.520Q5.178 6.312 3.774 7.752L3.774 7.752Q1.974 9.588 1.974 12.396L1.974 12.396Q2.010 14.484 3.306 16.680L3.306 16.680Q4.422 18.552 6.330 20.280L6.330 20.280Q7.914 21.792 9.642 22.800L9.642 22.800Q11.262 23.700 11.982 23.700L11.982 23.700L11.982 4.800L11.478 4.836Q10.830 4.872 10.182 4.944L10.182 4.944Q9.210 5.088 8.346 5.304ZM9.750 17.508L9.750 17.508Q9.354 18.012 8.202 17.508Q7.050 17.004 5.862 15.888L5.862 15.888Q4.602 14.664 3.954 13.368L3.954 13.368Q3.234 11.856 3.594 10.668L3.594 10.668Q3.882 9.516 4.782 8.940L4.782 8.940Q5.610 8.364 6.546 8.508L6.546 8.508Q8.454 8.904 9.570 10.668L9.570 10.668Q10.506 12.216 10.542 14.232Q10.578 16.248 9.750 17.508ZM7.194 12.288L7.194 12.288Q6.654 12.288 6.276 12.648Q5.898 13.008 5.898 13.530Q5.898 14.052 6.276 14.412Q6.654 14.772 7.194 14.772Q7.734 14.772 8.112 14.412Q8.490 14.052 8.490 13.530Q8.490 13.008 8.112 12.648Q7.734 12.288 7.194 12.288ZM6.798 13.440L6.798 13.440Q6.654 13.440 6.564 13.350Q6.474 13.260 6.474 13.134Q6.474 13.008 6.564 12.900Q6.654 12.792 6.798 12.792Q6.942 12.792 7.032 12.900Q7.122 13.008 7.122 13.134Q7.122 13.260 7.032 13.350Q6.942 13.440 6.798 13.440Z\"/>',\n\t'seti:rollup':\n\t\t'<path d=\"M20.108 22.374L20.108 22.374L16.580 15.276Q16.454 15.066 16.475 14.961Q16.496 14.856 16.706 14.772L16.706 14.772Q17.882 14.184 18.680 13.176L18.680 13.176Q19.730 11.916 20.213 10.362Q20.696 8.808 20.486 7.149Q20.276 5.490 19.562 4.272L19.562 4.272Q19.520 4.188 19.457 4.062Q19.394 3.936 19.310 3.852L19.310 3.852Q18.974 3.516 18.428 3.222L18.428 3.222Q18.050 3.054 17.336 2.802L17.336 2.802Q16.538 2.676 16.160 2.676L16.160 2.676Q15.530 2.634 15.026 2.781Q14.522 2.928 14.249 3.054Q13.976 3.180 13.808 3.474L13.808 3.474Q12.884 4.566 13.262 5.952L13.262 5.952Q13.472 6.666 13.934 7.296L13.934 7.296Q14.270 7.758 14.984 8.304L14.984 8.304Q15.530 8.598 15.908 8.724L15.908 8.724Q16.160 8.724 16.307 8.661Q16.454 8.598 16.580 8.304L16.580 8.304Q16.706 8.178 16.706 7.800L16.706 7.800Q16.706 7.296 16.412 6.498L16.412 6.498Q16.160 5.952 16.160 5.700L16.160 5.700Q16.328 6.162 16.790 7.002L16.790 7.002L17.084 7.548Q17.210 7.842 17.168 8.199Q17.126 8.556 16.958 8.850L16.958 8.850L16.706 9.102Q16.370 9.354 15.719 9.963Q15.068 10.572 14.732 10.824L14.732 10.824Q13.346 12 12.380 13.176L12.380 13.176Q11.288 14.478 9.482 17.124L9.482 17.124Q8.978 17.796 8.180 19.140L8.180 19.140L7.634 20.022Q7.382 20.400 6.920 21.177Q6.458 21.954 6.227 22.353Q5.996 22.752 5.492 23.508L5.492 23.508L5.282 23.802L20.486 23.802Q20.780 23.802 20.885 23.634Q20.990 23.466 20.906 23.172L20.906 23.172Q20.528 22.962 20.360 22.752L20.360 22.752Q20.234 22.626 20.108 22.374ZM7.508 10.824L7.508 10.824Q7.802 10.320 8.348 9.270L8.348 9.270Q9.818 6.666 10.658 5.448L10.658 5.448Q11.708 3.852 12.212 3.222L12.212 3.222Q13.052 2.382 13.934 2.004L13.934 2.004Q15.320 1.752 16.160 1.878L16.160 1.878Q17.798 2.088 18.932 3.222L18.932 3.222L19.184 3.474L19.184 3.348Q16.664 0.198 12.884 0.198L12.884 0.198L3.434 0.198Q3.056 0.198 3.056 0.702L3.056 0.702L3.056 19.602Q3.686 17.922 5.534 14.478L5.534 14.478Q6.206 13.176 7.508 10.824Z\"/>',\n\t'seti:stylelint':\n\t\t'<path d=\"M10.540 4.860L10.540 3.060L13.540 3.060L13.540 4.860L10.540 4.860ZM17.060 1.060L17.100 7.020L13.940 5.020Q13.860 4.980 13.860 3.900L13.860 3.900L13.900 2.820L17.060 1.060ZM6.980 0.980L6.900 6.940L10.100 4.980Q10.180 4.900 10.180 3.820L10.180 3.820L10.140 2.780L6.980 0.980ZM11.140 7.540L11.140 7.540Q11.140 7.220 11.380 6.980Q11.620 6.740 11.960 6.740Q12.300 6.740 12.520 6.980Q12.740 7.220 12.740 7.560Q12.740 7.900 12.520 8.140Q12.300 8.380 11.960 8.380Q11.620 8.380 11.380 8.140Q11.140 7.900 11.140 7.540ZM11.140 12.540L11.140 12.540Q11.140 12.180 11.380 11.940Q11.620 11.700 11.960 11.700Q12.300 11.700 12.520 11.940Q12.740 12.180 12.740 12.520Q12.740 12.860 12.520 13.100Q12.300 13.340 11.960 13.340Q11.620 13.340 11.380 13.100Q11.140 12.860 11.140 12.540ZM11.140 17.500L11.140 17.500Q11.140 17.180 11.380 16.940Q11.620 16.700 11.960 16.700Q12.300 16.700 12.520 16.940Q12.740 17.180 12.740 17.520Q12.740 17.860 12.520 18.100Q12.300 18.340 11.960 18.340Q11.620 18.340 11.380 18.100Q11.140 17.860 11.140 17.500ZM21.780 5.580L23.940 3.620L20.500 0.420L18.580 0.420L17.580 3.980L17.500 7.740L16.580 7.220L12.220 23.620L23.300 7.380L21.780 5.580ZM2.220 5.540L0.060 3.540L3.500 0.380L5.460 0.380L6.460 3.900L6.500 7.700L7.420 7.140L11.780 23.540L0.700 7.340L2.220 5.540ZM7.300 7.420L7.300 7.420Z\"/>',\n\t'seti:yarn':\n\t\t'<path d=\"M6.729 21.545L6.729 21.545Q6.393 21.377 6.225 21.041L6.225 21.041Q6.099 20.915 6.078 20.915Q6.057 20.915 5.973 21.041Q5.889 21.167 5.826 21.419Q5.763 21.671 5.679 21.839L5.679 21.839Q5.385 22.805 4.839 23.141Q4.293 23.477 3.327 23.267L3.327 23.267Q3.075 23.267 2.529 23.015L2.529 23.015Q1.731 22.595 2.151 21.839L2.151 21.839Q2.151 21.755 2.214 21.629Q2.277 21.503 2.277 21.419L2.277 21.419Q1.563 21.419 1.353 20.789L1.353 20.789Q0.849 19.445 1.017 18.416Q1.185 17.387 2.151 16.421L2.151 16.421L2.235 16.253Q2.403 15.959 2.403 15.791L2.403 15.791Q2.403 14.363 2.697 13.313L2.697 13.313Q3.033 12.053 3.831 11.045L3.831 11.045Q4.503 10.163 5.427 9.617L5.427 9.617Q5.595 9.533 5.616 9.407Q5.637 9.281 5.553 9.071L5.553 9.071Q4.839 8.189 4.629 6.971L4.629 6.971Q4.545 6.635 4.671 6.215L4.671 6.215Q4.755 5.921 5.007 5.417L5.007 5.417L5.175 5.039Q5.427 4.745 5.553 4.745L5.553 4.745Q5.931 4.661 6.561 4.199L6.561 4.199L6.855 3.989Q8.157 2.645 10.131 2.645L10.131 2.645Q10.341 2.645 10.446 2.582Q10.551 2.519 10.551 2.393L10.551 2.393Q10.719 1.595 11.307 0.839L11.307 0.839L11.727 0.419Q11.937 0.209 12.168 0.230Q12.399 0.251 12.525 0.545L12.525 0.545Q12.777 1.007 13.155 1.847L13.155 1.847L13.407 2.393Q13.617 2.729 13.827 2.519L13.827 2.519Q14.331 2.309 14.499 2.288Q14.667 2.267 14.751 2.393Q14.835 2.519 15.003 3.065L15.003 3.065Q16.011 7.265 13.701 10.919L13.701 10.919Q13.617 11.045 13.428 11.318Q13.239 11.591 13.155 11.759Q13.071 11.927 13.092 12.053Q13.113 12.179 13.281 12.389L13.281 12.389Q14.163 13.145 14.751 14.195Q15.339 15.245 15.507 16.421L15.507 16.421Q15.717 17.891 15.507 19.319L15.507 19.319Q15.423 19.697 15.507 19.760Q15.591 19.823 15.927 19.739L15.927 19.739Q17.397 19.277 18.531 18.521L18.531 18.521L18.867 18.353Q19.623 17.891 20.043 17.723L20.043 17.723Q20.673 17.429 21.303 17.345L21.303 17.345L21.681 17.345Q22.101 17.261 22.374 17.366Q22.647 17.471 22.836 17.723Q23.025 17.975 23.025 18.269L23.025 18.269Q23.025 18.857 22.353 19.067L22.353 19.067Q20.631 19.403 18.804 20.726Q16.977 22.049 14.457 22.721L14.457 22.721Q14.373 22.721 14.205 22.805Q14.037 22.889 13.953 23.015L13.953 23.015Q13.659 23.225 13.323 23.309L13.323 23.309Q13.113 23.393 12.651 23.435L12.651 23.435L12.231 23.519L11.895 23.561Q9.375 23.771 8.031 23.771L8.031 23.771Q7.275 23.771 6.729 23.645L6.729 23.645Q5.973 23.393 5.805 22.889L5.805 22.889Q5.595 22.091 6.225 21.671L6.225 21.671Q6.351 21.671 6.519 21.608Q6.687 21.545 6.729 21.545Z\"/>',\n\t'seti:webpack':\n\t\t'<path d=\"M18.403 16.275L21.975 18.441L12.475 23.875L12.475 19.619L18.403 16.275ZM19.125 15.819L22.697 17.833L22.697 6.433L19.125 8.447L19.125 15.819ZM5.711 16.275L2.025 18.441L11.639 23.875L11.639 19.619L5.711 16.275ZM4.875 15.819L1.303 17.833L1.303 6.433L4.875 8.447L4.875 15.819ZM5.217 7.611L1.683 5.711L11.525 0.125L11.525 4.191L5.217 7.611ZM18.783 7.611L22.317 5.711L12.475 0.125L12.475 4.191L18.783 7.611ZM11.525 12.513L11.525 18.669L5.597 15.477L5.597 9.055L11.525 12.513ZM12.475 12.513L12.475 18.669L18.403 15.477L18.403 9.055L12.475 12.513ZM12.019 11.677L6.053 8.219L12.019 5.027L17.947 8.219L12.019 11.677Z\"/>',\n\t'seti:lock':\n\t\t'<path d=\"M18.838 10.302L22.270 10.302L22.270 23.979L1.730 23.979L1.730 10.302L5.162 10.302L5.162 6.918Q5.162 6.354 5.255 5.884L5.255 5.884Q5.490 4.051 6.642 2.618Q7.793 1.184 9.509 0.503Q11.224-0.179 13.104 0.103L13.104 0.103Q15.548 0.526 17.194 2.453Q18.838 4.380 18.838 6.871L18.838 6.871L18.838 10.302ZM8.545 10.302L15.407 10.302Q15.407 10.255 15.407 10.208L15.407 10.208L15.407 6.777Q15.407 6.542 15.360 6.213L15.360 6.213Q15.078 4.850 14.021 4.098Q12.963 3.346 11.553 3.487L11.553 3.487Q10.284 3.581 9.415 4.592Q8.545 5.602 8.545 6.918L8.545 6.918L8.545 10.302Z\"/>',\n\t'seti:license':\n\t\t'<path d=\"M14.768 8.750L14.768 8.750Q14.636 7.067 13.431 5.994Q12.227 4.922 10.445 4.823L10.445 4.823L8.894 4.823Q8.795 3.602 9.356 2.710L9.356 2.710Q10.016 1.556 11.484 1.407Q12.953 1.259 13.943 2.248L13.943 2.248Q14.768 3.206 14.768 4.724L14.768 4.724Q14.768 5.021 14.900 5.152L14.900 5.152L15.824 6.076Q16.253 4.756 15.923 3.271L15.923 3.271Q15.527 1.919 14.454 1.093Q13.382 0.268 11.897 0.203L11.897 0.203Q10.940 0.203 10.280 0.401L10.280 0.401Q9.488 0.631 8.894 1.226L8.894 1.226Q8.168 1.919 7.821 3.041Q7.475 4.163 7.739 5.318L7.739 5.318Q8.036 6.572 8.993 7.495L8.993 7.495Q9.323 7.826 10.148 8.222L10.148 8.222Q10.511 8.387 10.791 8.172Q11.072 7.957 11.072 7.628L11.072 7.628Q11.072 6.934 10.346 6.803L10.346 6.803Q10.247 6.803 10.049 6.572L10.049 6.572L10.049 6.473Q10.346 6.407 10.692 6.489Q11.039 6.572 11.270 6.803L11.270 6.803Q11.732 7.265 11.567 8.024L11.567 8.024Q11.435 8.585 10.989 8.799Q10.544 9.014 9.917 8.849L9.917 8.849Q8.366 8.419 7.574 6.803L7.574 6.803Q7.508 6.637 7.359 6.324Q7.211 6.011 7.145 5.846L7.145 5.846Q6.518 6.506 6.188 6.968L6.188 6.968Q5.759 7.628 5.594 8.321L5.594 8.321Q5.000 11.093 7.046 12.974L7.046 12.974Q7.145 13.073 7.178 13.155Q7.211 13.238 7.145 13.403L7.145 13.403Q6.980 14.822 6.749 15.746L6.749 15.746Q6.584 16.637 6.320 18.336Q6.056 20.035 5.907 20.894Q5.759 21.752 5.924 22.577L5.924 22.577Q6.089 23.236 6.749 23.501L6.749 23.501Q6.782 23.303 6.831 22.890Q6.881 22.478 6.947 22.247L6.947 22.247Q7.112 21.389 7.359 19.689Q7.607 17.989 7.772 17.099L7.772 17.099Q7.871 16.571 8.019 15.498Q8.168 14.426 8.300 13.898L8.300 13.898Q8.300 13.601 8.597 13.601L8.597 13.601Q8.729 13.601 8.762 13.700Q8.795 13.799 8.795 13.997L8.795 13.997Q8.663 15.218 8.300 17.000L8.300 17.000Q8.135 18.089 7.805 20.102L7.805 20.102Q7.409 22.412 7.244 23.501L7.244 23.501Q7.244 23.567 7.310 23.633L7.310 23.633L7.343 23.699Q7.607 23.699 8.069 23.748Q8.531 23.797 8.795 23.797L8.795 23.797Q9.422 23.797 10.049 22.973L10.049 22.973Q10.478 22.544 10.148 21.851L10.148 21.851Q9.950 21.653 9.950 21.521L9.950 21.521Q9.488 20.927 10.148 20.597L10.148 20.597Q10.181 20.564 10.346 20.514Q10.511 20.465 10.544 20.399L10.544 20.399Q10.709 20.333 10.758 20.135Q10.808 19.937 10.643 19.772L10.643 19.772L10.346 19.474Q10.016 19.079 9.851 18.947Q9.686 18.815 9.735 18.567Q9.785 18.320 9.933 18.221Q10.082 18.122 10.445 17.973Q10.808 17.825 10.973 17.726Q11.138 17.627 11.171 17.495Q11.204 17.363 11.072 17.198L11.072 17.198Q10.874 17.000 10.742 16.802L10.742 16.802Q10.544 16.571 10.577 16.241Q10.610 15.911 10.841 15.729Q11.072 15.548 11.270 15.548L11.270 15.548Q11.567 15.416 11.600 15.152L11.600 15.152Q11.567 14.987 11.633 14.673Q11.699 14.360 11.699 14.228L11.699 14.228Q11.699 14.129 11.765 13.997Q11.831 13.865 11.897 13.799L11.897 13.799Q12.194 13.502 13.019 13.073L13.019 13.073Q14.075 12.314 14.504 11.208Q14.933 10.103 14.768 8.750ZM18.497 20.498L18.497 20.498Q18.497 20.102 18.068 20.102L18.068 20.102Q17.837 20.102 17.738 20.069L17.738 20.069Q17.540 20.003 17.474 19.772L17.474 19.772Q17.309 19.309 17.474 18.848L17.474 18.848Q17.804 18.188 17.045 18.023L17.045 18.023Q16.946 18.023 16.715 17.973Q16.484 17.924 16.319 17.924L16.319 17.924Q16.022 17.924 16.022 17.627L16.022 17.627Q16.022 17.528 15.972 17.313Q15.923 17.099 15.923 17.000L15.923 17.000Q15.923 16.802 16.055 16.604L16.055 16.604Q16.121 16.471 16.302 16.257Q16.484 16.043 16.467 15.927Q16.451 15.812 16.220 15.647L16.220 15.647Q16.187 15.614 16.022 15.515Q15.857 15.416 15.824 15.350L15.824 15.350Q15.527 15.185 15.477 15.020Q15.428 14.855 15.593 14.525L15.593 14.525Q15.824 14.327 15.824 14.228L15.824 14.228Q16.088 13.931 15.923 13.601L15.923 13.601L15.824 13.436Q15.659 13.040 15.494 12.875L15.494 12.875Q15.362 12.578 15.494 12.248L15.494 12.248Q16.814 9.640 15.824 7.198L15.824 7.198Q15.032 5.515 13.118 5.053L13.118 5.053L13.019 5.152Q13.085 5.186 13.184 5.351Q13.283 5.515 13.349 5.549L13.349 5.549Q15.296 7.198 15.296 9.575L15.296 9.575Q15.296 12.248 13.019 13.898L13.019 13.898Q12.854 13.997 12.804 14.129Q12.755 14.261 12.821 14.426L12.821 14.426Q13.019 14.921 13.382 15.911Q13.745 16.901 13.943 17.396L13.943 17.396L16.418 23.171L16.484 23.270Q16.583 23.402 16.748 23.402L16.748 23.402Q17.672 22.940 18.167 22.148Q18.662 21.356 18.497 20.498ZM12.293 14.426L12.293 14.426Q12.260 14.624 12.210 14.954Q12.161 15.284 12.095 15.449L12.095 15.449L12.062 15.647Q12.029 15.944 12.095 16.076L12.095 16.076L14.273 21.422Q14.636 22.346 15.296 22.873L15.296 22.873Q15.692 23.072 15.824 23.204L15.824 23.204L15.923 23.072Q14.768 20.201 12.293 14.426Z\"/>',\n\t'seti:makefile':\n\t\t'<path d=\"M12 9.249L4.398 0.975L0.198 2.025L0.198 23.025L5.700 23.025L5.700 10.005L10.404 15.129L13.596 15.129L18.300 10.005L18.300 23.025L23.802 23.025L23.802 2.025L19.602 0.975L12 9.249Z\"/>',\n\t'seti:heroku':\n\t\t'<path d=\"M8.143 20.493L4.267 23.875L4.267 17.111L8.143 20.493ZM18.213 10.157L18.213 10.157Q19.163 11.069 19.505 12.399L19.505 12.399Q19.733 13.121 19.695 13.729L19.695 13.729L19.695 23.875L16.275 23.875L16.275 13.767Q16.275 13.045 15.857 12.589L15.857 12.589Q15.325 12.019 14.147 12.019L14.147 12.019Q11.943 12.019 9.131 12.893L9.131 12.893Q7.535 13.387 6.699 13.767L6.699 13.767L4.267 14.869L4.267 0.125L7.725 0.125L7.725 9.777Q11.297 8.637 14.147 8.637L14.147 8.637Q16.693 8.637 18.213 10.157ZM17.149 5.673L13.729 5.673Q15.705 3.051 16.275 0.125L16.275 0.125L19.733 0.125Q19.353 3.203 17.149 5.673L17.149 5.673Z\"/>',\n\t'seti:todo':\n\t\t'<path d=\"M3.726 0.261L12 0.261L19.476 0.261Q21.030 0.261 22.206 1.227Q23.382 2.193 23.676 3.663L23.676 3.663Q23.802 3.915 23.802 4.461L23.802 4.461L23.802 19.413Q23.802 21.009 22.815 22.206Q21.828 23.403 20.274 23.613L20.274 23.613Q20.148 23.613 19.875 23.676Q19.602 23.739 19.476 23.739L19.476 23.739L4.650 23.739Q2.676 23.739 1.437 22.521Q0.198 21.303 0.198 19.413L0.198 19.413L0.198 4.587Q0.198 2.907 1.164 1.731Q2.130 0.555 3.726 0.261L3.726 0.261ZM3.726 12.063L3.726 12.063L3.726 19.539Q3.726 20.337 4.650 20.337L4.650 20.337L19.350 20.337Q19.728 20.337 20.001 20.064Q20.274 19.791 20.274 19.413L20.274 19.413L20.274 4.713Q20.148 4.209 19.917 3.999Q19.686 3.789 19.224 3.789L19.224 3.789L4.524 3.789Q4.020 3.789 3.810 3.999Q3.600 4.209 3.600 4.713L3.600 4.713Q3.726 7.065 3.726 12.063ZM6.876 10.341L6.876 10.341Q7.002 10.425 7.212 10.467L7.212 10.467L7.548 10.635Q8.010 10.803 8.913 11.265Q9.816 11.727 10.278 11.937L10.278 11.937Q10.488 12.021 10.530 12.021Q10.572 12.021 10.698 11.937L10.698 11.937L12.420 10.635Q15.066 8.661 16.452 7.737L16.452 7.737Q16.620 7.569 16.914 7.443L16.914 7.443Q17.082 7.401 17.124 7.359L17.124 7.359Q17.418 7.233 17.607 7.464Q17.796 7.695 17.796 7.989L17.796 7.989L17.628 8.241Q17.502 8.451 17.502 8.535L17.502 8.535Q16.830 9.417 15.528 11.307L15.528 11.307L14.226 13.113Q12.672 15.255 11.874 16.263L11.874 16.263Q11.370 17.019 10.698 16.977Q10.026 16.935 9.522 16.137L9.522 16.137L6.498 11.685Q6.204 11.391 6.204 11.265L6.204 11.265Q6.120 10.887 6.309 10.614Q6.498 10.341 6.876 10.341Z\"/>',\n\t'seti:ignored':\n\t\t'<path d=\"M4.661 20.860L21.083 4.480Q21.335 3.934 21.335 3.682L21.335 3.682Q21.167 2.968 20.600 2.779Q20.033 2.590 19.361 3.010L19.361 3.010L16.715 5.656Q16.589 5.782 16.211 5.782L16.211 5.782Q13.943 4.900 11.738 4.921Q9.533 4.942 7.307 5.908L7.307 5.908Q3.191 7.714 0.335 12.082L0.335 12.082Q0.125 12.502 0.146 12.817Q0.167 13.132 0.461 13.384L0.461 13.384Q1.469 14.602 2.687 15.610L2.687 15.610Q3.485 16.282 5.039 17.332L5.039 17.332Q4.997 17.332 4.934 17.395Q4.871 17.458 4.787 17.458L4.787 17.458L4.115 18.088Q3.317 18.886 2.939 19.306L2.939 19.306Q2.519 19.768 2.687 20.482L2.687 20.482Q2.897 21.112 3.611 21.280L3.611 21.280Q4.283 21.280 4.661 20.860L4.661 20.860ZM7.811 14.560L6.887 15.484L6.635 15.484Q4.241 14.182 2.687 12.502L2.687 12.502Q4.955 9.520 7.559 8.134L7.559 8.134Q5.627 11.410 7.811 14.560L7.811 14.560ZM12.809 8.302L12.809 8.302Q12.599 8.932 11.885 8.932L11.885 8.932Q11.003 8.932 10.373 9.478Q9.743 10.024 9.659 10.906L9.659 10.906L9.659 11.284Q9.659 11.662 9.428 11.872Q9.197 12.082 8.840 12.082Q8.483 12.082 8.273 11.830Q8.063 11.578 8.063 11.158L8.063 11.158Q8.063 9.562 9.197 8.407Q10.331 7.252 11.885 7.252L11.885 7.252Q12.347 7.252 12.620 7.567Q12.893 7.882 12.809 8.302ZM23.561 11.956L23.561 11.956Q23.225 11.578 22.574 10.717Q21.923 9.856 21.587 9.478L21.587 9.478Q21.335 9.142 20.684 8.554Q20.033 7.966 19.739 7.630L19.739 7.630L18.185 9.184Q20.033 10.654 21.335 12.502L21.335 12.502Q21.083 12.754 20.957 12.754L20.957 12.754Q20.159 13.510 19.739 13.804L19.739 13.804Q15.623 17.122 10.961 16.534L10.961 16.534Q10.583 16.534 10.415 16.702L10.415 16.702Q9.911 17.206 9.659 17.584L9.659 17.584L8.861 18.382L8.987 18.382Q12.137 19.054 14.615 18.508L14.615 18.508Q19.739 17.626 23.561 13.132L23.561 13.132Q24.149 12.754 23.561 11.956Z\"/>',\n\t'custom:terragrunt':\n\t\t'<style>.icon-light{display:none}.icon-dark{display:block}:root[data-theme=\\'light\\'] .icon-light{display:block}:root[data-theme=\\'light\\'] .icon-dark{display:none}</style><g class=\"icon-light\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12.0404 0.513672L22.4616 6.514V18.514L12.0404 24.514L1.61914 18.514V6.514L12.0404 0.513672ZM21.3366 6.832L12.0404 1.35L2.74414 6.832V18.696L12.0404 23.178L21.3366 18.696V6.832Z\" fill=\"#160C56\"></path><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12.0404 23.179V18.697V12.515L7.39226 9.423L2.74414 6.332V12.515V18.697L12.0404 23.179Z\" fill=\"white\"></path><path d=\"M16.6885 20.488L21.3366 18.697L16.6885 15.606L12.0404 18.697V23.179L16.6885 20.488Z\" fill=\"#87E0E1\"></path><path d=\"M12.0404 12.515V18.697L16.6885 15.606L12.0404 12.515Z\" fill=\"#1B46DD\"></path><path d=\"M16.6885 9.423L12.0404 12.515L16.6885 15.606L21.3366 12.515L16.6885 9.423Z\" fill=\"#B068E9\"></path><path d=\"M16.6885 4.241L12.0404 1.15L7.39226 4.241L2.74414 6.332L7.39226 9.423L12.0404 6.332L16.6885 9.423L21.3366 6.332L16.6885 4.241Z\" fill=\"#F9DB4E\"></path><path d=\"M12.0404 12.515L16.6885 9.423L12.0404 6.332L7.39226 9.423L12.0404 12.515Z\" fill=\"#E94A5D\"></path></g><g class=\"icon-dark\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12.0404 0.513672L22.4616 6.514V18.514L12.0404 24.514L1.61914 18.514V6.514L12.0404 0.513672ZM21.3366 6.832L12.0404 1.35L2.74414 6.832V18.696L12.0404 23.178L21.3366 18.696V6.832Z\" fill=\"white\"></path><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12.0404 23.179V18.697V12.515L7.39226 9.423L2.74414 6.332V12.515V18.697L12.0404 23.179Z\" fill=\"white\"></path><path d=\"M16.6885 20.488L21.3366 18.697L16.6885 15.606L12.0404 18.697V23.179L16.6885 20.488Z\" fill=\"#87E0E1\"></path><path d=\"M12.0404 12.515V18.697L16.6885 15.606L12.0404 12.515Z\" fill=\"#1B46DD\"></path><path d=\"M16.6885 9.423L12.0404 12.515L16.6885 15.606L21.3366 12.515L16.6885 9.423Z\" fill=\"#B068E9\"></path><path d=\"M16.6885 4.241L12.0404 1.15L7.39226 4.241L2.74414 6.332L7.39226 9.423L12.0404 6.332L16.6885 9.423L21.3366 6.332L16.6885 4.241Z\" fill=\"#F9DB4E\"></path><path d=\"M12.0404 12.515L16.6885 9.423L12.0404 6.332L7.39226 9.423L12.0404 12.515Z\" fill=\"#E94A5D\"></path></g>',\n\t'custom:opentofu':\n\t\t'<g><path d=\"M11.8144 1.3393C11.9303 1.27492 12.0694 1.27492 12.1853 1.3393L20.4151 5.90989C20.6817 6.05619 20.6817 6.44244 20.4151 6.58875L12.1853 11.1593C12.0694 11.2237 11.9303 11.2237 11.8144 11.1593L3.58471 6.58875C3.31812 6.44244 3.31812 6.05619 3.58471 5.90989L11.8144 1.3393Z\" fill=\"#E6C220\"></path><path d=\"M21.1505 7.65992C21.4055 7.51947 21.7184 7.70674 21.7184 7.99935V17.1347C21.7184 17.2751 21.6431 17.4097 21.5214 17.4741L13.2105 22.0857C12.9555 22.2261 12.6426 22.0389 12.6426 21.7462V12.6109C12.6426 12.4705 12.7179 12.3359 12.8396 12.2715L21.1505 7.65992Z\" fill=\"white\"></path><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M2.2793 7.99935C2.2793 7.70674 2.59226 7.51947 2.84726 7.65992L11.1581 12.2715C11.2798 12.3417 11.3552 12.4705 11.3552 12.6109V21.7462C11.3552 22.0389 11.0422 22.2261 10.7872 22.0857L2.48214 17.48C2.36043 17.4097 2.28509 17.281 2.28509 17.1405V7.99935H2.2793ZM5.76958 15.1706V15.1823L3.82227 14.1464V14.1347C3.86863 13.5202 4.33807 13.2569 4.87706 13.5436C5.41605 13.8304 5.81594 14.5561 5.76958 15.1706ZM8.83599 16.9655V16.9538C8.88235 16.3393 8.48245 15.6136 7.94347 15.3268C7.40448 15.0401 6.93504 15.3034 6.88867 15.9179V15.9296L8.83599 16.9655Z\" fill=\"#FEDA15\"></path></g>',\n};\n"
  },
  {
    "path": "docs/src/components/vendored/starlight/rehype-file-tree.ts",
    "content": "import { AstroError } from 'astro/errors';\nimport type { Element, ElementContent, Text } from 'hast';\nimport { type Child, h, s } from 'hastscript';\nimport { select } from 'hast-util-select';\nimport { fromHtml } from 'hast-util-from-html';\nimport { toString } from 'hast-util-to-string';\nimport { rehype } from 'rehype';\nimport { CONTINUE, SKIP, visit } from 'unist-util-visit';\nimport { Icons, type StarlightIcon } from './Icons';\nimport { definitions } from './file-tree-icons';\n\ndeclare module 'vfile' {\n\tinterface DataMap {\n\t\tdirectoryLabel: string;\n\t}\n}\n\nconst folderIcon = makeSVGIcon(Icons['seti:folder']);\nconst defaultFileIcon = makeSVGIcon(Icons['seti:default']);\n\n/**\n * Process the HTML for a file tree to create the necessary markup for each file and directory\n * including icons.\n * @param html Inner HTML passed to the `<FileTree>` component.\n * @param directoryLabel The localized label for a directory.\n * @returns The processed HTML for the file tree.\n */\nexport function processFileTree(html: string, directoryLabel: string) {\n\tconst file = fileTreeProcessor.processSync({ data: { directoryLabel }, value: html });\n\n\treturn file.toString();\n}\n\n/** Rehype processor to extract file tree data and turn each entry into its associated markup. */\nconst fileTreeProcessor = rehype()\n\t.data('settings', { fragment: true })\n\t.use(function fileTree() {\n\t\treturn (tree: Element, file) => {\n\t\t\tconst { directoryLabel } = file.data;\n\n\t\t\tvalidateFileTree(tree);\n\n\t\t\tvisit(tree, 'element', (node) => {\n\t\t\t\t// Strip nodes that only contain newlines.\n\t\t\t\tnode.children = node.children.filter(\n\t\t\t\t\t(child) => child.type === 'comment' || child.type !== 'text' || !/^\\n+$/.test(child.value)\n\t\t\t\t);\n\n\t\t\t\t// Skip over non-list items.\n\t\t\t\tif (node.tagName !== 'li') return CONTINUE;\n\n\t\t\t\tconst [firstChild, ...otherChildren] = node.children;\n\n\t\t\t\t// Keep track of comments associated with the current file or directory.\n\t\t\t\tconst comment: Child[] = [];\n\n\t\t\t\t// Extract text comment that follows the file name, e.g. `README.md This is a comment`\n\t\t\t\tif (firstChild?.type === 'text') {\n\t\t\t\t\tconst [filename, ...fragments] = firstChild.value.split(' ');\n\t\t\t\t\tfirstChild.value = filename || '';\n\t\t\t\t\tconst textComment = fragments.join(' ').trim();\n\t\t\t\t\tif (textComment.length > 0) {\n\t\t\t\t\t\tcomment.push(fragments.join(' '));\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Comments may not always be entirely part of the first child text node,\n\t\t\t\t// e.g. `README.md This is an __important__ comment` where the `__important__` and `comment`\n\t\t\t\t// nodes would also be children of the list item node.\n\t\t\t\tconst subTreeIndex = otherChildren.findIndex(\n\t\t\t\t\t(child) => child.type === 'element' && child.tagName === 'ul'\n\t\t\t\t);\n\t\t\t\tconst commentNodes =\n\t\t\t\t\tsubTreeIndex > -1 ? otherChildren.slice(0, subTreeIndex) : [...otherChildren];\n\t\t\t\totherChildren.splice(0, subTreeIndex > -1 ? subTreeIndex : otherChildren.length);\n\t\t\t\tcomment.push(...commentNodes);\n\n\t\t\t\tconst firstChildTextContent = firstChild ? toString(firstChild) : '';\n\n\t\t\t\t// Decide a node is a directory if it ends in a `/` or contains another list.\n\t\t\t\tconst isDirectory =\n\t\t\t\t\t/\\/\\s*$/.test(firstChildTextContent) ||\n\t\t\t\t\totherChildren.some((child) => child.type === 'element' && child.tagName === 'ul');\n\t\t\t\t// A placeholder is a node that only contains 3 dots or an ellipsis.\n\t\t\t\tconst isPlaceholder = /^\\s*(\\.{3}|…)\\s*$/.test(firstChildTextContent);\n\t\t\t\t// A node is highlighted if its first child is bold text, e.g. `**README.md**`.\n\t\t\t\tconst isHighlighted = firstChild?.type === 'element' && firstChild.tagName === 'strong';\n\n\t\t\t\t// Create an icon for the file or directory (placeholder do not have icons).\n\t\t\t\tconst icon = h('span', isDirectory ? folderIcon : getFileIcon(firstChildTextContent));\n\t\t\t\tif (isDirectory) {\n\t\t\t\t\t// Add a screen reader only label for directories before the icon so that it is announced\n\t\t\t\t\t// as such before reading the directory name.\n\t\t\t\t\ticon.children.unshift(h('span', { class: 'sr-only' }, directoryLabel));\n\t\t\t\t}\n\n\t\t\t\t// Add classes and data attributes to the list item node.\n\t\t\t\tnode.properties.class = isDirectory ? 'directory' : 'file';\n\t\t\t\tif (isPlaceholder) node.properties.class += ' empty';\n\n\t\t\t\t// Create the tree entry node that contains the icon, file name and comment which will end up\n\t\t\t\t// as the list item’s children.\n\t\t\t\tconst treeEntryChildren: Child[] = [\n\t\t\t\t\th('span', { class: isHighlighted ? 'highlight' : '' }, [\n\t\t\t\t\t\tisPlaceholder ? null : icon,\n\t\t\t\t\t\tfirstChild,\n\t\t\t\t\t]),\n\t\t\t\t];\n\n\t\t\t\tif (comment.length > 0) {\n\t\t\t\t\ttreeEntryChildren.push(makeText(' '), h('span', { class: 'comment' }, ...comment));\n\t\t\t\t}\n\n\t\t\t\tconst treeEntry = h('span', { class: 'tree-entry' }, ...treeEntryChildren);\n\n\t\t\t\tif (isDirectory) {\n\t\t\t\t\tconst hasContents = otherChildren.length > 0;\n\n\t\t\t\t\tnode.children = [\n\t\t\t\t\t\th('details', { open: hasContents }, [\n\t\t\t\t\t\t\th('summary', treeEntry),\n\t\t\t\t\t\t\t...(hasContents ? otherChildren : [h('ul', h('li', '…'))]),\n\t\t\t\t\t\t]),\n\t\t\t\t\t];\n\n\t\t\t\t\t// Continue down the tree.\n\t\t\t\t\treturn CONTINUE;\n\t\t\t\t}\n\n\t\t\t\tnode.children = [treeEntry, ...otherChildren];\n\n\t\t\t\t// Files can’t contain further files or directories, so skip iterating children.\n\t\t\t\treturn SKIP;\n\t\t\t});\n\t\t};\n\t});\n\n/** Make a text node with the pass string as its contents. */\nfunction makeText(value = ''): Text {\n\treturn { type: 'text', value };\n}\n\n/** Make a node containing an SVG icon from the passed HTML string. */\nfunction makeSVGIcon(svgString: string) {\n\treturn s(\n\t\t'svg',\n\t\t{\n\t\t\twidth: 16,\n\t\t\theight: 16,\n\t\t\tclass: 'tree-icon',\n\t\t\t'aria-hidden': 'true',\n\t\t\tviewBox: '0 0 24 24',\n\t\t},\n\t\tfromHtml(svgString, { fragment: true })\n\t);\n}\n\n/** Return the icon for a file based on its file name. */\nfunction getFileIcon(fileName: string) {\n\tconst name = getFileIconName(fileName);\n\tif (!name) return defaultFileIcon;\n\tif (name in Icons) {\n\t\tconst path = Icons[name as StarlightIcon];\n\t\treturn makeSVGIcon(path);\n\t}\n\treturn defaultFileIcon;\n}\n\n/** Return the icon name for a file based on its file name. */\nfunction getFileIconName(fileName: string) {\n\tlet icon: string | undefined = definitions.files[fileName];\n\tif (icon) return icon;\n\ticon = getFileIconTypeFromExtension(fileName);\n\tif (icon) return icon;\n\tfor (const [partial, partialIcon] of Object.entries(definitions.partials)) {\n\t\tif (fileName.includes(partial)) return partialIcon;\n\t}\n\treturn icon;\n}\n\n/**\n * Get an icon from a file name based on its extension.\n * Note that an extension in Seti is everything after a dot, so `README.md` would be `.md` and\n * `name.with.dots` will try to look for an icon for `.with.dots` and then `.dots` if the first one\n * is not found.\n */\nfunction getFileIconTypeFromExtension(fileName: string) {\n\tconst firstDotIndex = fileName.indexOf('.');\n\tif (firstDotIndex === -1) return;\n\tlet extension = fileName.slice(firstDotIndex);\n\twhile (extension !== '') {\n\t\tconst icon = definitions.extensions[extension];\n\t\tif (icon) return icon;\n\t\tconst nextDotIndex = extension.indexOf('.', 1);\n\t\tif (nextDotIndex === -1) return;\n\t\textension = extension.slice(nextDotIndex);\n\t}\n\treturn;\n}\n\n/** Validate that the user provided HTML for a file tree is valid. */\nfunction validateFileTree(tree: Element) {\n\tconst rootElements = tree.children.filter(isElementNode);\n\tconst [rootElement] = rootElements;\n\n\tif (rootElements.length === 0) {\n\t\tthrowFileTreeValidationError(\n\t\t\t'The `<FileTree>` component expects its content to be a single unordered list but found no child elements.'\n\t\t);\n\t}\n\n\tif (rootElements.length !== 1) {\n\t\tthrowFileTreeValidationError(\n\t\t\t`The \\`<FileTree>\\` component expects its content to be a single unordered list but found multiple child elements: ${rootElements\n\t\t\t\t.map((element) => `\\`<${element.tagName}>\\``)\n\t\t\t\t.join(' - ')}.`\n\t\t);\n\t}\n\n\tif (!rootElement || rootElement.tagName !== 'ul') {\n\t\tthrowFileTreeValidationError(\n\t\t\t`The \\`<FileTree>\\` component expects its content to be an unordered list but found the following element: \\`<${rootElement?.tagName}>\\`.`\n\t\t);\n\t}\n\n\tconst listItemElement = select('li', rootElement);\n\n\tif (!listItemElement) {\n\t\tthrowFileTreeValidationError(\n\t\t\t'The `<FileTree>` component expects its content to be an unordered list with at least one list item.'\n\t\t);\n\t}\n}\n\nfunction isElementNode(node: ElementContent): node is Element {\n\treturn node.type === 'element';\n}\n\n/** Throw a validation error for a file tree linking to the documentation. */\nfunction throwFileTreeValidationError(message: string): never {\n\tthrow new AstroError(\n\t\tmessage,\n\t\t'To learn more about the `<FileTree>` component, see https://starlight.astro.build/components/file-tree/'\n\t);\n}\n\nexport interface Definitions {\n\tfiles: Record<string, string>;\n\textensions: Record<string, string>;\n\tpartials: Record<string, string>;\n}\n"
  },
  {
    "path": "docs/src/content/docs/01-getting-started/01-quick-start.mdx",
    "content": "---\ntitle: Quick Start\ndescription: Start using Terragrunt today!\nslug: getting-started/quick-start\nsidebar:\n  order: 1\n---\n\nimport { Steps } from '@astrojs/starlight/components';\n\n## Install Terragrunt\n\nIf you haven't already installed Terragrunt, you can do so by following the instructions in the [Install Terragrunt](/getting-started/install/) guide.\n\n## Add `terragrunt.hcl` to your project\n\nIf you are currently using OpenTofu or Terraform, and you want to start using Terragrunt in your project, simply run the following where your OpenTofu project is located:\n\n```shell\ntouch terragrunt.hcl\n```\n\nThis creates an empty Terragrunt configuration file in the directory where you are using OpenTofu. You can now start using `terragrunt` instead of `tofu` or `terraform` to run your OpenTofu/Terraform commands as if you were simply using OpenTofu or Terraform.\n\nDepending on why you're looking to adopt Terragrunt, this may be all you need to do!\n\nWith just this empty file, you've already made it so that you no longer need to run `tofu init` or `terraform init` before running `tofu apply` or `terraform apply`. Terragrunt will automatically run `init` for you if necessary. This is a feature called [Auto-init](/features/units/auto-init/).\n\nThis might not be very impressive so far, so you may be wondering _why_ one might want to start using Terragrunt to manage their OpenTofu/Terraform projects. The next section will give you a very gentle introduction to using Terragrunt, and show you how you can start to leverage Terragrunt to manage your OpenTofu/Terraform projects more effectively.\n\n## Tutorial\n\nWhat follows is a gentle step-by-step guide to integrating Terragrunt into a new (or existing) OpenTofu/Terraform project.\n\nFor the sake of this tutorial, a minimal set of OpenTofu configurations will be used so that you can follow along. Following these steps will give you an idea of how to integrate Terragrunt into an existing project, even if yours is more complex.\n\nThis tutorial will assume the following:\n\n1. You have [OpenTofu](https://opentofu.org/docs/intro/install/) or [Terraform](https://developer.hashicorp.com/terraform/install) installed\\*.\n2. You have a basic understanding of what OpenTofu/Terraform do.\n3. You are using a Unix-like operating system.\n\nThis tutorial will not assume the following:\n\n1. You have any subscriptions to any cloud providers.\n2. You have any experience with Terragrunt.\n3. You have any existing Terragrunt, OpenTofu or Terraform projects.\n\n\\* Note that if you have _both_ OpenTofu and Terraform installed, you'll want to read the [tf-path](/reference/cli/commands/run#tf-path) docs to understand how Terragrunt determines which binary to use.\n\nIf you would like a less gentle introduction geared towards users with an active AWS account, familiarity with OpenTofu/Terraform, and potentially a team actively using Terragrunt, consider starting with the [Overview](/getting-started/overview/).\n\nIf you start to feel lost, or don't understand a concept, consider reading the [Terminology](/getting-started/terminology/) page before continuing with this tutorial. It has a brief overview of most of the common terms used when discussing Terragrunt.\n\nFinally, note that all of the files created in this tutorial can be copied directly from the code block, none of them are partial files, so you don't have to worry about figuring out where to put the code. Just copy and paste!\n\nYou can also see what to expect in your filesystem at each step [here](https://github.com/gruntwork-io/terragrunt/tree/main/test/fixtures/docs/01-quick-start).\n\n<Steps>\n\n1. **Create a new Terragrunt project**\n\n   Let's say you have the following `main.tf` in directory `foo`:\n\n   ```hcl\n   # foo/main.tf\n   resource \"local_file\" \"file\" {\n     content  = \"Hello, World!\"\n     filename = \"${path.module}/hi.txt\"\n   }\n   ```\n\n   As we learned above, integrating this OpenTofu project with Terragrunt is as simple as creating a `terragrunt.hcl` file in the same directory:\n\n   ```bash\n   touch foo/terragrunt.hcl\n   ```\n\n   You can now run `terragrunt` commands within the `foo` directory, as if you were using `tofu` or `terraform`.\n\n   ```bash\n   $ cd foo\n   $ terragrunt apply -auto-approve\n   18:44:26.066 STDOUT tofu: Initializing the backend...\n   18:44:26.067 STDOUT tofu: Initializing provider plugins...\n   18:44:26.067 STDOUT tofu: - Finding latest version of hashicorp/local...\n   18:44:26.717 STDOUT tofu: - Installing hashicorp/local v2.5.2...\n   18:44:27.033 STDOUT tofu: - Installed hashicorp/local v2.5.2 (signed, key ID 0C0AF313E5FD9F80)\n   18:44:27.033 STDOUT tofu: Providers are signed by their developers.\n   18:44:27.033 STDOUT tofu: If you'd like to know more about provider signing, you can read about it here:\n   18:44:27.033 STDOUT tofu: https://opentofu.org/docs/cli/plugins/signing/\n   18:44:27.034 STDOUT tofu: OpenTofu has created a lock file .terraform.lock.hcl to record the provider\n   18:44:27.034 STDOUT tofu: selections it made above. Include this file in your version control repository\n   18:44:27.034 STDOUT tofu: so that OpenTofu can guarantee to make the same selections by default when\n   18:44:27.034 STDOUT tofu: you run \"tofu init\" in the future.\n   18:44:27.034 STDOUT tofu: OpenTofu has been successfully initialized!\n   18:44:27.035 STDOUT tofu:\n   18:44:27.035 STDOUT tofu: You may now begin working with OpenTofu. Try running \"tofu plan\" to see\n   18:44:27.035 STDOUT tofu: any changes that are required for your infrastructure. All OpenTofu commands\n   18:44:27.035 STDOUT tofu: should now work.\n   18:44:27.035 STDOUT tofu: If you ever set or change modules or backend configuration for OpenTofu,\n   18:44:27.035 STDOUT tofu: rerun this command to reinitialize your working directory. If you forget, other\n   18:44:27.035 STDOUT tofu: commands will detect it and remind you to do so if necessary.\n   18:44:27.362 STDOUT tofu: OpenTofu used the selected providers to generate the following execution\n   18:44:27.362 STDOUT tofu: plan. Resource actions are indicated with the following symbols:\n   18:44:27.362 STDOUT tofu:   + create\n   18:44:27.362 STDOUT tofu: OpenTofu will perform the following actions:\n   18:44:27.362 STDOUT tofu:   # local_file.file will be created\n   18:44:27.362 STDOUT tofu:   + resource \"local_file\" \"file\" {\n   18:44:27.362 STDOUT tofu:       + content              = \"Hello, World!\"\n   18:44:27.362 STDOUT tofu:       + content_base64sha256 = (known after apply)\n   18:44:27.362 STDOUT tofu:       + content_base64sha512 = (known after apply)\n   18:44:27.362 STDOUT tofu:       + content_md5          = (known after apply)\n   18:44:27.362 STDOUT tofu:       + content_sha1         = (known after apply)\n   18:44:27.362 STDOUT tofu:       + content_sha256       = (known after apply)\n   18:44:27.362 STDOUT tofu:       + content_sha512       = (known after apply)\n   18:44:27.362 STDOUT tofu:       + directory_permission = \"0777\"\n   18:44:27.362 STDOUT tofu:       + file_permission      = \"0777\"\n   18:44:27.362 STDOUT tofu:       + filename             = \"./hi.txt\"\n   18:44:27.362 STDOUT tofu:       + id                   = (known after apply)\n   18:44:27.362 STDOUT tofu:     }\n   18:44:27.362 STDOUT tofu: Plan: 1 to add, 0 to change, 0 to destroy.\n   18:44:27.362 STDOUT tofu:\n   18:44:27.383 STDOUT tofu: local_file.file: Creating...\n   18:44:27.384 STDOUT tofu: local_file.file: Creation complete after 0s [id=0a0a9f2a6772942557ab5355d76af442f8f65e01]\n   18:44:27.392 STDOUT tofu:\n   18:44:27.392 STDOUT tofu: Apply complete! Resources: 1 added, 0 changed, 0 destroyed.\n   18:44:27.392 STDOUT tofu:\n   ```\n\n   You might notice that this is a little more verbose than the output you're used to seeing from running `tofu` or `terraform` directly. This is because Terragrunt does a bit of work behind the scenes to make sure that you can scale your OpenTofu/Terraform usage without running into common problems. As you get more comfortable with using Terragrunt on larger projects, you may find the extra information helpful.\n\n   If you prefer that Terragrunt terminal output look more like that from `tofu` or `terraform`, you can use the `--log-format bare` flag (or set the environment variable `TG_LOG_FORMAT=bare`) to reduce the verbosity of the output.\n\n   e.g.\n\n   ```bash\n   $ terragrunt --log-format bare apply\n   local_file.file: Refreshing state... [id=0a0a9f2a6772942557ab5355d76af442f8f65e01]\n\n   No changes. Your infrastructure matches the configuration.\n\n   OpenTofu has compared your real infrastructure against your configuration and\n   found no differences, so no changes are needed.\n\n   Apply complete! Resources: 0 added, 0 changed, 0 destroyed.\n   ```\n\n   The way dynamicity is handled in OpenTofu is via `variable` configuration blocks. Let's add one to our `main.tf` so that we can control the content of the file we're creating:\n\n   ```hcl\n   # foo/main.tf\n   variable \"content\" {}\n\n   resource \"local_file\" \"file\" {\n     content  = var.content\n     filename = \"${path.module}/hi.txt\"\n   }\n   ```\n\n   Now, just like when using `tofu` alone, you can pass in the value for the `content` variable using the `-var` flag:\n\n   ```bash\n   terragrunt apply -auto-approve -var content='Hello, Terragrunt!'\n   ```\n\n   This is a common pattern when working with Infrastructure as Code (IaC). You typically create IaC that is relatively static, and then as you need to make configurations dynamic, you add variables to your configuration files to introduce dynamicity.\n\n2. **Add a new Terragrunt unit**\n\n   In the context of Terragrunt, a \"unit\" is a directory that contains a `terragrunt.hcl` file, and it represents a single piece of infrastructure. You can think of a unit as a single instance of an OpenTofu/Terraform module.\n\n   Let's create a copy of the `foo` directory and call it `bar`:\n\n   ```bash\n   cd ..\n   cp -r foo bar\n   ```\n\n   We now have two identical units in our project, `foo` and `bar`. We also have identical code in each of these directories, which is not ideal if we want to be able to avoid duplicating effort when we make changes to our infrastructure.\n\n3. **Create a shared module**\n\n   To avoid this duplication, we can introduce a new `shared` directory, and reference that directory from both `foo` and `bar`. This way, we can make changes to our infrastructure in one place and have those changes apply to both units.\n\n   Let's create a new directory called `shared`:\n\n   ```bash\n   mkdir shared\n   ```\n\n   Now, copy the `main.tf` file from `foo` to `shared`:\n\n   ```bash\n   cp foo/main.tf shared/main.tf\n   ```\n\n   Finally, let's update the `foo` and `bar` main.tf files to reference the `shared` directory. Update the `main.tf` files in both `foo` and `bar` to look like this:\n\n   ```hcl\n   # foo/main.tf\n   variable \"content\" {}\n\n   module \"shared\" {\n     source = \"../shared\"\n\n     content = var.content\n   }\n   ```\n   ```hcl\n   # bar/main.tf\n   variable \"content\" {}\n\n   module \"shared\" {\n     source = \"../shared\"\n\n     content = var.content\n   }\n   ```\n\n   There's now one place where the logic for the resource `local_file.file` is defined, and both `foo` and `bar` reference that logic. You can imagine that as your infrastructure grows, it can become more and more advantageous to put repeated logic into shared modules like this.\n\n   This setup does have some problems, however. While you could keep navigating to the different units and running `terragrunt apply` in each one with the appropriate `-var` flags, this can quickly become tedious, as you have to know which units require which set of vars applied. You might decide to work around this by creating a file named `terraform.tfvars` in each unit directory, but this also comes with some limitations that Terragrunt can help you avoid.\n\n4. **Use Terragrunt to manage your units**\n\n   Luckily, Terragrunt has a built-in feature to control the inputs passed to your OpenTofu/Terraform configurations. This feature is called (aptly enough) [inputs](/reference/hcl/attributes/#inputs).\n\n   Let's add inputs to both `terragrunt.hcl` files in the `foo` and `bar` directories:\n\n   ```hcl\n   # foo/terragrunt.hcl\n   inputs = {\n     content = \"Hello from foo, Terragrunt!\"\n   }\n   ```\n\n   ```hcl\n   # bar/terragrunt.hcl\n   inputs = {\n     content = \"Hello from bar, Terragrunt!\"\n   }\n   ```\n\n   You don't have to maintain the extra `main.tf` files just to instantiate the `module` blocks. You can use the `terraform` block to handle this for you. Update the `terragrunt.hcl` files in `foo` and `bar` to look like this:\n\n   ```hcl\n   # foo/terragrunt.hcl\n   terraform {\n     source = \"../shared\"\n   }\n\n   inputs = {\n     content = \"Hello from foo, Terragrunt!\"\n   }\n   ```\n\n   ```hcl\n   # bar/terragrunt.hcl\n   terraform {\n     source = \"../shared\"\n   }\n\n   inputs = {\n     content = \"Hello from bar, Terragrunt!\"\n   }\n   ```\n\n   And you can delete the `main.tf` files from both `foo` and `bar`:\n\n   ```bash\n   rm foo/main.tf bar/main.tf\n   ```\n\n   This saves you some duplicated content, as you no longer need to maintain that extra `content` variable in each `main.tf` file. You can imagine that for especially large modules, the ability to define inputs in the `terragrunt.hcl` file can save you a lot of time and effort. The patterns for your infrastructure are exclusively defined in `.tf` files now, and the `terragrunt.hcl` files are used to manage the instances of those patterns as units.\n\n   If you run `terragrunt apply -auto-approve` in the `foo` and `bar` directories, you'll see that the `content` variable is set to the value you defined in the `inputs` block of the `terragrunt.hcl` file. You might also notice that there's now a special `.terragrunt-cache` directory generated for you in each unit directory. This is where Terragrunt copies the contents of modules, and performs any necessary additional code generation to make sure that your OpenTofu/Terraform code is ready to be run.\n\n   The `.terragrunt-cache` directory is typically added to `.gitignore` files, similar to the `.terraform` directory that OpenTofu generates.\n\n5. **Use Terragrunt to manage your stacks**\n\n   In the context of Terragrunt, a \"stack\" is a collection of units that are managed together. You can think of a stack as a single environment, such as `dev`, `staging`, or `prod`, or an entire project.\n\n   One of the main reasons users adopt Terragrunt is that it can help manage the complexity of managing multiple units across multiple environments.\n\n   e.g. Let's say we wanted to update both our `foo` and `bar` environments simultaneously.\n\n   In the directory above `foo` and `bar`, run the following:\n\n   ```bash\n   $ terragrunt run --all apply\n   08:42:00.150 INFO   The stack at . will be processed in the following order for command apply:\n   Group 1\n   - Module ./bar\n   - Module ./foo\n\n\n   Are you sure you want to run 'terragrunt apply' in each folder of the stack described above? (y/n) y\n   08:43:10.702 STDOUT [foo] tofu: local_file.file: Refreshing state... [id=c4ae21736a6297f44ea86791e528338e9d14a0e9]\n   08:43:10.702 STDOUT [bar] tofu: local_file.file: Refreshing state... [id=f855394a0316da09618c8b1fde7b91e00e759f80]\n   08:43:10.708 STDOUT [bar] tofu: No changes. Your infrastructure matches the configuration.\n   08:43:10.708 STDOUT [bar] tofu: OpenTofu has compared your real infrastructure against your configuration and\n   08:43:10.708 STDOUT [bar] tofu: found no differences, so no changes are needed.\n   08:43:10.708 STDOUT [foo] tofu: No changes. Your infrastructure matches the configuration.\n   08:43:10.708 STDOUT [foo] tofu: OpenTofu has compared your real infrastructure against your configuration and\n   08:43:10.708 STDOUT [foo] tofu: found no differences, so no changes are needed.\n   08:43:10.716 STDOUT [foo] tofu:\n   08:43:10.716 STDOUT [foo] tofu: Apply complete! Resources: 0 added, 0 changed, 0 destroyed.\n   08:43:10.716 STDOUT [foo] tofu:\n   08:43:10.720 STDOUT [bar] tofu:\n   08:43:10.720 STDOUT [bar] tofu: Apply complete! Resources: 0 added, 0 changed, 0 destroyed.\n   08:43:10.720 STDOUT [bar] tofu:\n   ```\n\n   This is where that additional verbosity in Terragrunt logging is really handy. You can see that Terragrunt concurrently ran `apply -auto-approve` in both the `foo` and `bar` units. The extra logging for Terragrunt also included information on the names of the units that were processed, and disambiguated the output from each unit.\n\n   When Terragrunt runs these commands, it creates a `.terragrunt-cache` directory in each unit's directory. This cache directory serves as Terragrunt's scratch directory where it:\n\n   - Downloads your remote OpenTofu/Terraform configurations\n   - Runs your OpenTofu/Terraform commands\n   - Stores downloaded modules and providers\n   - Stores generated files (in this case, the `hi.txt` file will be created under `.terragrunt-cache/[HASH]/[HASH]/hi.txt` rather than directly in the `foo` or `bar` directories)\n\n   The `.terragrunt-cache` directory is typically added to `.gitignore` files, similar to the `.terraform` directory that OpenTofu generates. You can safely delete this folder at any time, and Terragrunt will recreate it as necessary.\n\n   If you want to control where the files are created, you can modify the module to accept an output directory parameter. For example, you can update the `shared/main.tf` file to:\n\n   ```hcl\n   variable \"content\" {}\n   variable \"output_dir\" {}\n\n   resource \"local_file\" \"file\" {\n     content  = var.content\n     filename = \"${var.output_dir}/hi.txt\"\n   }\n   ```\n\n   Then in your `foo/terragrunt.hcl` and `bar/terragrunt.hcl` files, you can use the `get_terragrunt_dir()` built-in function to get the directory where the `terragrunt.hcl` file is located:\n\n   ```hcl\n   terraform {\n     source = \"../shared\"\n   }\n\n   inputs = {\n     output_dir = get_terragrunt_dir()\n     content = \"Hello from bar, Terragrunt!\"\n   }\n   ```\n   With this configuration, the `hi.txt` files will be created directly in the `foo` and `bar` directories instead of the `.terragrunt-cache` directory.\n\n   Similar to the `tofu` CLI, there is a prompt to confirm that you are sure you want to run the command in each unit when performing a command that's potentially destructive. You can skip this prompt by using the `--non-interactive` flag, just as you would with `-auto-approve` in OpenTofu.\n\n   ```bash\n   terragrunt run --all --non-interactive apply\n   ```\n\n6. **Use Terragrunt to manage your DAG**\n\n   In the context of Terragrunt, a Directed Acyclic Graph (DAG) is a data structure that represents the relationship between units in your stack, as determined by their dependencies.\n\n   Don't worry if that doesn't make sense right now. The important thing to know is that Terragrunt uses the DAG to determine the order in which it performs runs across your stack. Once you see how Terragrunt uses the DAG to determine the order in which to run commands across your stack, you'll understand why this is important.\n\n   For example, let's say that the `content` of the `bar` unit depended on the `content` of the `foo` unit. You can express this dependency first by adding an `output` block to the `shared` module:\n\n   ```hcl\n   # shared/output.tf\n   output \"content\" {\n     value = local_file.file.content\n   }\n   ```\n\n   Then, you can update the `bar` unit to depend on the `foo` unit by using the `dependencies` block in the `terragrunt.hcl` file:\n\n   ```hcl\n   # bar/terragrunt.hcl\n   terraform {\n    source = \"../shared\"\n   }\n\n   dependency \"foo\" {\n    config_path = \"../foo\"\n   }\n\n   inputs = {\n    output_dir = get_terragrunt_dir()\n    content = \"Foo content: ${dependency.foo.outputs.content}\"\n   }\n   ```\n\n   Being good citizens of the IaC world, we should run a `plan` before an `apply` to see what changes Terragrunt will make to our infrastructure (note that you will get an error here. This is expected, and we'll fix it in the next step):\n\n   ```bash\n   $ terragrunt run --all plan\n   08:57:09.271 INFO   The stack at . will be processed in the following order for command plan:\n   Group 1\n   - Module ./foo\n\n   Group 2\n   - Module ./bar\n\n   ...\n\n   08:57:09.936 ERROR  [bar] Module ./bar has finished with an error\n   08:57:09.936 ERROR  error occurred:\n\n   * ./foo/terragrunt.hcl is a dependency of ./bar/terragrunt.hcl but detected no outputs. Either the target module has not been applied yet, or the module has no outputs.\n\n     If this dependency is accessed before the outputs are ready (which can happen during the planning phase of an unapplied stack), consider using mock_outputs:\n\n     dependency \"foo\" {\n         config_path = \"../foo\"\n\n         mock_outputs = {\n             foo_output = \"mock-foo-output\"\n         }\n     }\n\n     For more info, see:\n     https://docs.terragrunt.com/features/stacks/#unapplied-dependency-and-mock-outputs\n\n     If you do not require outputs from your dependency, consider using the dependencies block instead:\n     https://docs.terragrunt.com/reference/config-blocks-and-attributes/#dependencies\n   ```\n\n   Oh no! We got an error. This happens because the way in which dependencies are resolved by default in Terragrunt is to run `terragrunt output` within the dependency for use in the dependent unit. In this case, the `foo` unit has not been applied yet, so there are no outputs to fetch.\n\n   You should notice, however, that Terragrunt has already figured out the order in which to run the `plan` command across the units in your stack. This is what we mean when we say that Terragrunt uses a DAG to determine the order of runs in your stack. Terragrunt analyzes the dependencies in your stack, and determines an order for runs so that outputs are ready to be used as inputs in dependent units.\n\n   If you decided to run `terragrunt run --all apply` instead, you would instead see Terragrunt complete the `apply` in the `foo` unit first, and then complete the `apply` in the `bar` unit, as it's aware that the `bar` unit might need some outputs from the `foo` unit.\n\n7. **Use mocks to handle unavailable outputs**\n\n   In this scenario, most Terragrunt users leverage `mock_outputs` to handle unavailable outputs (see [limitations on accessing exposed config](https://docs.terragrunt.com/reference/config-blocks-and-attributes/#limitations-on-accessing-exposed-config)). Given that it's expected that the `foo` unit won't be able to provide outputs until it's applied, you can use the `mock_outputs` block to provide a placeholder value for the `content` output during the `plan` phase.\n\n   ```hcl\n   # bar/terragrunt.hcl\n   terraform {\n     source = \"../shared\"\n   }\n\n   dependency \"foo\" {\n     config_path = \"../foo\"\n     mock_outputs = {\n       content = \"Mocked content from foo\"\n     }\n   }\n\n   inputs = {\n     output_dir = get_terragrunt_dir()\n     content = \"Foo content: ${dependency.foo.outputs.content}\"\n   }\n   ```\n\n   Re-running the `plan` command should now complete successfully:\n\n   ```bash\n   $ terragrunt run --all plan\n   09:29:03.461 INFO   The stack at . will be processed in the following order for command plan:\n   Group 1\n   - Module ./foo\n\n   Group 2\n   - Module ./bar\n\n   ...\n\n   09:29:03.644 WARN   [bar] Config ./foo/terragrunt.hcl is a dependency of ./bar/terragrunt.hcl that has no outputs, but mock outputs provided and returning those in dependency output.\n\n   ...\n\n   09:29:03.898 STDOUT [bar] tofu:   + resource \"local_file\" \"file\" {\n   09:29:03.898 STDOUT [bar] tofu:       + content              = \"Foo content: Mocked content from foo\"\n   09:29:03.898 STDOUT [bar] tofu:       + content_base64sha256 = (known after apply)\n   09:29:03.898 STDOUT [bar] tofu:       + content_base64sha512 = (known after apply)\n   09:29:03.898 STDOUT [bar] tofu:       + content_md5          = (known after apply)\n   09:29:03.898 STDOUT [bar] tofu:       + content_sha1         = (known after apply)\n   09:29:03.898 STDOUT [bar] tofu:       + content_sha256       = (known after apply)\n   09:29:03.898 STDOUT [bar] tofu:       + content_sha512       = (known after apply)\n   09:29:03.898 STDOUT [bar] tofu:       + directory_permission = \"0777\"\n   09:29:03.898 STDOUT [bar] tofu:       + file_permission      = \"0777\"\n   09:29:03.898 STDOUT [bar] tofu:       + filename             = \"./hi.txt\"\n   09:29:03.898 STDOUT [bar] tofu:       + id                   = (known after apply)\n   09:29:03.898 STDOUT [bar] tofu:     }\n   ```\n\n   If you're concerned about the `mock_outputs` attribute resulting in invalid configurations, note that during an apply, the outputs of `foo` will be known, and Terragrunt won't use `mock_outputs` to resolve the outputs of `foo`.\n\n   ```bash\n   $ terragrunt run --all --non-interactive apply\n\n   ...\n\n   09:31:21.587 STDOUT [bar] tofu:   + resource \"local_file\" \"file\" {\n   09:31:21.587 STDOUT [bar] tofu:       + content              = \"Foo content: Hello from foo, Terragrunt!\"\n   09:31:21.587 STDOUT [bar] tofu:       + content_base64sha256 = (known after apply)\n   09:31:21.587 STDOUT [bar] tofu:       + content_base64sha512 = (known after apply)\n   09:31:21.587 STDOUT [bar] tofu:       + content_md5          = (known after apply)\n   09:31:21.587 STDOUT [bar] tofu:       + content_sha1         = (known after apply)\n   09:31:21.587 STDOUT [bar] tofu:       + content_sha256       = (known after apply)\n   09:31:21.587 STDOUT [bar] tofu:       + content_sha512       = (known after apply)\n   09:31:21.587 STDOUT [bar] tofu:       + directory_permission = \"0777\"\n   09:31:21.587 STDOUT [bar] tofu:       + file_permission      = \"0777\"\n   09:31:21.587 STDOUT [bar] tofu:       + filename             = \"./hi.txt\"\n   09:31:21.587 STDOUT [bar] tofu:       + id                   = (known after apply)\n   09:31:21.587 STDOUT [bar] tofu:     }\n\n   ...\n   ```\n\n   You can also be explicit about the fact that you only want to use `mock_outputs` during the `plan` phase by specifying that in your `dependency` configuration:\n\n   ```hcl\n   # bar/terragrunt.hcl\n   terraform {\n     source = \"../shared\"\n   }\n\n   dependency \"foo\" {\n     config_path = \"../foo\"\n     mock_outputs = {\n       content = \"Mocked content from foo\"\n     }\n\n     mock_outputs_allowed_terraform_commands = [\"plan\"]\n   }\n\n   inputs = {\n     output_dir = get_terragrunt_dir()\n     content = \"Foo content: ${dependency.foo.outputs.content}\"\n   }\n   ```\n\n   Something a little subtle just happened there. Note that the `inputs` attribute is dynamic. This addresses some of the limitations mentioned earlier about using `terraform.tfvars` files to manage inputs for units. Given that the `bar` unit is dependent on output values from the `foo` unit, you wouldn't be able to use a `terraform.tfvars` file to populate this variable without some additional tooling to populate it dynamically.\n\n   Terragrunt was spawned organically out of supporting Gruntwork customers using Terraform at scale, and features in the product are designed to address common problems like these that arise when managing OpenTofu/Terraform projects at scale in production.\n\n8. **Continue learning and exploring**\n\n   Hopefully, following this simple tutorial has given you confidence in integrating Terragrunt into your existing OpenTofu/Terraform projects. Starting small, and gradually introducing more complex Terragrunt features is a great way to learn how Terragrunt can help you manage your infrastructure more effectively.\n\n   The next step of the Getting Started guide is to follow the [Overview](/getting-started/overview/) guide. This guide will introduce you to more advanced Terragrunt features, and show you how to use Terragrunt to manage your infrastructure across multiple environments in a real-world AWS account.\n\n   If you're ready to get your hands dirty with more advanced Terragrunt features yourself, you can skip ahead to the [Features](/features/units) section of the documentation.\n\n   If you ever need help with a particular problem, take a look at the resources available to you in the [Support](/community/support/) section. You are especially encouraged to join the [Terragrunt Discord](/community/invite) server, and become part of the Terragrunt community.\n\n</Steps>\n"
  },
  {
    "path": "docs/src/content/docs/01-getting-started/02-overview.mdx",
    "content": "---\ntitle: Overview\ndescription: Get a high level overview of the most important Terragrunt features.\nslug: getting-started/overview\nsidebar:\n  order: 2\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\n\nThe following is a simple overview of the main features in Terragrunt.\n\nIt includes configurations that are a bit more complex than the ones found in the [Quick Start](/getting-started/quick-start/), but don't panic!\n\nWe'll walk you through each one, and you don't need to understand everything right away. Knowing that these features are available as you start to use Terragrunt can give you a tool to reach for when you encounter common problems that typically require one or more of these solutions.\n\nThis guide is geared towards users who have either already gone through the [Quick Start](/getting-started/quick-start/) or are joining a team of users that are already using Terragrunt. As a consequence, we'll be using more complex configurations, discussing more advanced features, and showing how to use Terragrunt to manage real AWS infrastructure.\n\nIf you are unfamiliar with OpenTofu/Terraform, you may want to also read [OpenTofu](https://opentofu.org/docs/intro/) or [Terraform](https://developer.hashicorp.com/terraform/intro) documentation after reading this guide.\n\n## Following Along\n\nWhat follows isn't a tutorial in the same sense as the [Quick Start](/getting-started/quick-start/), but more of a guided tour of some of the more commonly used features of Terragrunt. You don't need to follow along to understand the concepts, but if you want to, you can.\n\nThe code samples provided here are available as individual \"steps\" [here](https://github.com/gruntwork-io/terragrunt/tree/main/test/fixtures/docs/02-overview).\n\n{/* NOTE: We also test this continuously in `tests/integration_docs_aws_test.go` */}\n\nIf you would prefer it, you can clone the [Terragrunt repository](https://github.com/gruntwork-io/terragrunt.git), and follow along with the examples in your own environment without any copy + paste.\n\nJust make sure to replace the values prefixed `__FILL_IN_` with values relevant to your AWS account.\n\nIf you don't have an AWS account, you can either sign up for a free tier account at [aws.amazon.com](https://aws.amazon.com/) or adapt the examples to use a different cloud provider.\n\n## Example\n\nHere is a typical `terragrunt.hcl` file you might find in a Terragrunt project\\*:\n\n```hcl\n# terragrunt.hcl\n\n# Configure the remote backend\nremote_state {\n  backend = \"s3\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    bucket = \"my-tofu-state\"\n\n    key            = \"tofu.tfstate\"\n    region         = \"us-east-1\"\n    encrypt        = true\n    dynamodb_table = \"my-lock-table\"\n  }\n}\n\n# Configure the AWS provider\ngenerate \"provider\" {\n  path = \"provider.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\nEOF\n}\n\n# Configure the module\n#\n# The URL used here is a shorthand for\n# \"tfr://registry.terraform.io/terraform-aws-modules/vpc/aws?version=5.16.0\".\n#\n# You can find the module at:\n# https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest\n#\n# Note the extra `/` after the `tfr` protocol is required for the shorthand\n# notation.\nterraform {\n  source = \"tfr:///terraform-aws-modules/vpc/aws?version=5.16.0\"\n}\n\n# Configure the inputs for the module\ninputs = {\n  name = \"my-vpc\"\n  cidr = \"10.0.0.0/16\"\n\n  azs             = [\"us-east-1a\", \"us-east-1b\", \"us-east-1c\"]\n  private_subnets = [\"10.0.1.0/24\", \"10.0.2.0/24\", \"10.0.3.0/24\"]\n  public_subnets  = [\"10.0.101.0/24\", \"10.0.102.0/24\", \"10.0.103.0/24\"]\n\n  enable_nat_gateway = false\n  enable_vpn_gateway = false\n\n  tags = {\n    IaC = \"true\"\n    Environment = \"dev\"\n  }\n}\n```\n\n### Try it out\n\nIf you want to try this configuration locally:\n\n1. Copy the contents above into a `terragrunt.hcl` file in an empty directory.\n2. Change the value of `bucket` in the `remote_state` block to a unique name.\n\n   This has to be globally unique, so you might want to include today's date in the name.\n\n3. Ensure that you are authenticated with AWS and have the necessary permissions to create resources.\n\n   Running `aws sts get-caller-identity` in the AWS CLI is a good way to confirm this.\n\n4. Run `terragrunt apply -auto-approve` in the directory where you created the `terragrunt.hcl` file.\n\nIf you're familiar with OpenTofu/Terraform, this should be a pretty familiar experience.\n\nFor the most part, when you use Terragrunt, you are simply setting up configurations in `terragrunt.hcl` files that have analogues to what you would define with `.tf` files, then running `terragrunt` instead of `tofu`/`terraform` on the command line.\n\n### `terragrunt.hcl`\n\nThe `terragrunt.hcl` file above does the following:\n\n#### Remote state backend configuration\n\nThe `remote_state` configuration block controls how Terragrunt should store backend OpenTofu/Terraform state.\n\nIn this example, Terragrunt is being configured to store state in an S3 bucket named `my-tofu-state` in the `us-east-1` region. The state file will be named `tofu.tfstate`, and Terragrunt will use a DynamoDB table named `my-lock-table` for locking.\n\nIf you run the following, you can see how Terragrunt generates a `backend.tf` file to tell OpenTofu/Terraform to do this:\n\n```bash\n$ find .terragrunt-cache -name backend.tf -exec cat {} \\;\n# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa\nterraform {\n  backend \"s3\" {\n    bucket         = \"my-tofu-state\"\n    dynamodb_table = \"my-lock-table\"\n    encrypt        = true\n    key            = \"tofu.tfstate\"\n    region         = \"us-east-1\"\n  }\n}\n```\n\nRight before running any OpenTofu/Terraform command that might store state, Terragrunt will ensure that the appropriate `backend.tf` file is present in the working directory where OpenTofu/Terraform will run, so that state is persisted appropriately when `tofu` or `terraform` are invoked.\n\nNote that while following the example above, you didn't need to manually create that `my-tofu-state` S3 bucket, the `my-lock-table` DynamoDB table, or run `tofu/terraform init` to perform initialization.\n\nThese are just a few of the things that Terragrunt does automatically when orchestrating OpenTofu/Terraform commands because it knows how OpenTofu/Terraform work, and it can take care of some busy work for you to make your life easier.\n\n#### Provider configuration\n\nThe `generate` block is used to inject arbitrary files into the OpenTofu/Terraform module before running any OpenTofu/Terraform commands.\n\nIn this example, Terragrunt is being configured to inject a `provider.tf` file into the module that configures the AWS provider to use the `us-east-1` region.\n\nIf you run the following, you can see the `provider.tf` file that Terragrunt generates:\n\n```bash\n$ find .terragrunt-cache -name provider.tf -exec cat {} \\;\n# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\n```\n\nThis is the most common use case for the `generate` block, but you can use it to inject any file you want into the OpenTofu/Terraform module. This can be useful for injecting any configurations that aren't part of the generic module you want to reuse, or aren't easy to generate dynamically (such as `provider` blocks, which can't be dynamic in OpenTofu/Terraform). You can imagine that it may be convenient to have one set of modules, but dynamically inject different provider configurations based on the AWS region you're deploying to, etc.\n\nYou want to be mindful not to do too much with this configuration block, as it can make your OpenTofu/Terraform code harder to reproduce, understand and maintain. But it can be a powerful tool when used judiciously.\n\n#### Module configuration\n\nThe `terraform` block is used to indicate where to source the OpenTofu/Terraform module from (it's called `terraform` for historical reasons, but it controls behavior pertinent to both OpenTofu and Terraform).\n\nIn this example, all it is doing is controlling where Terragrunt should fetch the OpenTofu/Terraform module from. The configuration block [can do a lot more](/reference/hcl/blocks#terraform), but the `source` attribute is the most common attribute you'll set on the `terraform` block.\n\nYou'll notice that in the examples above, we were using `find` to locate the `.tf` files being generated and placed within the OpenTofu/Terraform module being downloaded here within the `.terragrunt-cache` directory. This is because Terragrunt aims to operate as an orchestrator, at a level of abstraction higher than OpenTofu/Terraform.\n\nOver the years supporting customers managing IaC at scale, the patterns that we've seen emerge for really successful organizations is to treat OpenTofu/Terraform modules as versioned, generic, well tested patterns of infrastructure, and to deploy them in as close to the exact same way as possible across all uses of them.\n\nTerragrunt supports this pattern by treating each [unit](/getting-started/terminology/#unit) of Terragrunt configuration (a directory with a `terragrunt.hcl` file in it) as a hermetic container of infrastructure that can be reasoned about in isolation, and then composed together to form a larger system of one or more [stacks](/getting-started/terminology/#stack) (each stack being a collection of units).\n\nTo that end, the way that Terragrunt loads OpenTofu/Terraform configurations is to download them into a subdirectory of the `.terragrunt-cache` directory, and then to orchestrate OpenTofu/Terraform commands from that directory. This ensures that the OpenTofu/Terraform modules are treated as immutable, versioned, and hermetic, and that the OpenTofu/Terraform runs are reliably reproducible.\n\n<FileTree>\n\n- .terragrunt-cache/\n  - tnIp4Am20T3Q8-6FuPqfof-kRGU\n    - ThyYwttwki6d6AS3aD5OwoyqIWA\n      - CHANGELOG.md\n      - LICENSE\n      - README.md\n      - UPGRADE-3.0.md\n      - UPGRADE-4.0.md\n      - backend.tf\n      - examples\n      - main.tf\n      - modules\n      - outputs.tf\n      - provider.tf\n      - terragrunt.hcl\n      - variables.tf\n      - versions.tf\n      - vpc-flow-logs.tf\n\n</FileTree>\n\nAny file that isn't part of the OpenTofu/Terraform module (like the `backend.tf` and `provider.tf` files Terragrunt generated) get a special little `Generated by Terragrunt` comment at the top of their files by default to make sure it's clear that Terragrunt generated them (and that they might not be there for other users of the same module).\n\n#### Inputs configuration\n\nThe `inputs` block is used to indicate what variable values should be passed to OpenTofu/Terraform when running `tofu` or `terraform` commands.\n\nIn this example, Terragrunt is being configured to pass in a bunch of variables to the OpenTofu/Terraform module. These variables are used to configure the VPC module, such as the name of the VPC, the CIDR block, the availability zones, the subnets, and so on.\n\nUnder the hood, what happens here is that Terragrunt sets relevant `TF_VAR_` prefixed environment variables, which are automatically detected by OpenTofu/Terraform as values for variables defined in `.tf` files.\n\n#### Further Reading\n\nYou can learn more about all the configuration [blocks](/reference/hcl/blocks) and [attributes](/reference/hcl/attributes) available in Terragrunt in the docs.\n\n## Core Patterns\n\nThis statement above is kind of a lie:\n\n\\*  Here is a typical `terragrunt.hcl` file you might find in a Terragrunt project.\n\nThe truth is, you'll almost never see configuration like that outside of some tests or examples. The reason for this is that one of the main responsibilities Terragrunt has is to scale IaC, and the configuration above would result in quite a lot of code duplication across a project. In an AWS project for example, you will probably use the same (or very similar) `provider` configuration across all your units, and you'll probably use the same `backend` configuration across all your units (with the only exception being the `key` for where in S3 your state should be stored).\n\nAware of this pattern, Terragrunt is designed to leverage a hierarchy of reusable configurations so that your code can be [DRY (Don't Repeat Yourself)](/getting-started/terminology#dont-repeat-yourself-dry).\n\n### The `include` block\n\nIn almost every `terragrunt.hcl` file you see, there will be a section that looks like this:\n\n```hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n```\n\nThis block configures Terragrunt to _include_ configuration found in a parent folder named `root.hcl` into it. This is a way to share configuration across all units of infrastructure in your project.\n\nThe `root` _label_ being applied here is the idiomatic way to reference the `root.hcl` file that is common to all other configurations in the project. This is a convention, not a requirement, but it's a good one to follow to make your code more readable and maintainable.\n\nRewriting the example above to use the `include` block so that it looks more like the kind of thing you'd see in a real project would look like this:\n\n```hcl\n# root.hcl\nremote_state {\n  backend = \"s3\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    bucket = \"my-tofu-state\"\n\n    key            = \"tofu.tfstate\"\n    region         = \"us-east-1\"\n    encrypt        = true\n    dynamodb_table = \"my-lock-table\"\n  }\n}\n\ngenerate \"provider\" {\n  path = \"provider.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\nEOF\n}\n```\n\n```hcl\n# vpc/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"tfr:///terraform-aws-modules/vpc/aws?version=5.16.0\"\n}\n\ninputs = {\n  name = \"my-vpc\"\n  cidr = \"10.0.0.0/16\"\n\n  azs             = [\"us-east-1a\", \"us-east-1b\", \"us-east-1c\"]\n  private_subnets = [\"10.0.1.0/24\", \"10.0.2.0/24\", \"10.0.3.0/24\"]\n  public_subnets  = [\"10.0.101.0/24\", \"10.0.102.0/24\", \"10.0.103.0/24\"]\n\n  enable_nat_gateway = false\n  enable_vpn_gateway = false\n\n  tags = {\n    IaC = \"true\"\n    Environment = \"dev\"\n  }\n}\n```\n\nBy doing this, you can see that it's become easier to introduce new units of infrastructure, as you only need to define the unique parts of the configuration for that unit in the new `terragrunt.hcl` file. The shared configuration is inherited from the `root.hcl` file.\n\nWhen you see `include` blocks in Terragrunt, remember that they result in the configuration being _inlined_ into the configuration file that includes them. For the most part, you can simply replace the relevant `include` block with the configuration it is including to see the full configuration that Terragrunt will use.\n\nThe exception to this is when you are using directives that explicitly leverage the fact that configurations are being included.\n\n### Building out the stack\n\nFor example, say you wanted to add another unit of infrastructure into the _stack_ that you're building out here. You could create a new directory named `ec2`, and add a `terragrunt.hcl` file to it like this:\n\n```hcl\n# ec2/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"tfr:///terraform-aws-modules/ec2-instance/aws?version=5.7.1\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n\ninputs = {\n  name = \"single-instance\"\n\n  instance_type = \"t2.micro\"\n  monitoring    = true\n  subnet_id     = dependency.vpc.outputs.private_subnets[0]\n\n  tags = {\n    IaC         = \"true\"\n    Environment = \"dev\"\n  }\n}\n```\n\n### Key Collisions\n\nIf you tried to run `terragrunt plan` in that new `ec2` directory, you'd get an error that looked like this:\n\n```bash\n$ terragrunt plan\n...\n* Failed to execute \"tofu init\" in ./.terragrunt-cache/I6Os-7-mjDhv4uQ5iCoGcOrDYhI/pfgqyj3TsBEWff7a1El6tYu6LEE\n  ╷\n  │ Error: Backend configuration changed\n  │\n  │ A change in the backend configuration has been detected, which may require\n  │ migrating existing state.\n  │\n  │ If you wish to attempt automatic migration of the state, use \"tofu init\n  │ -migrate-state\".\n  │ If you wish to store the current configuration with no changes to the\n  │ state, use \"tofu init -reconfigure\".\n  ╵\n\n\n  exit status 1\n```\n\nWhat's happening here is that when Terragrunt invoked OpenTofu/Terraform, it generated exactly the same `backend.tf` file for the new unit of infrastructure as it did for the VPC unit.\n\nYou can see that in the newly generated `backend.tf` file in the `.terragrunt-cache` directory under `ec2`:\n\n```bash\n$ find .terragrunt-cache -name backend.tf -exec cat {} \\;\n# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa\nterraform {\n  backend \"s3\" {\n    bucket         = \"my-tofu-state\"\n    dynamodb_table = \"my-lock-table\"\n    encrypt        = true\n    key            = \"tofu.tfstate\"\n    region         = \"us-east-1\"\n  }\n}\n```\n\n### Dynamic keys\n\nWhat most folks would really prefer here is to have the state for the `ec2` unit stored in a different, but predictable, location relative to the `vpc` unit.\n\nThe pattern that we've found to be most effective is to store state so that the location in the remote backend, like S3 mirrors the location of the unit on the filesystem.\n\nSo this filesystem layout:\n\n<FileTree>\n\n- root.hcl\n  - vpc\n    - terragrunt.hcl\n  - ec2\n    - terragrunt.hcl\n\n</FileTree>\n\nWould result in this state layout in S3:\n\n<FileTree>\n\n- s3://my-tofu-state\n  - vpc\n    - tofu.tfstate\n  - ec2\n    - tofu.tfstate\n\n</FileTree>\n\nTo achieve this, we can take advantage of the `path_relative_to_include()` Terragrunt HCL function to generate a `key` dynamically based on the position of the unit relative to the `root.hcl` file within the filesystem.\n\n```hcl\n# root.hcl\nremote_state {\n  backend = \"s3\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    bucket = \"my-tofu-state\"\n\n    key            = \"${path_relative_to_include()}/tofu.tfstate\" # <--\n    region         = \"us-east-1\"\n    encrypt        = true\n    dynamodb_table = \"my-lock-table\"\n  }\n}\n\ngenerate \"provider\" {\n  path = \"provider.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\nEOF\n}\n```\n\nWhat this does is set the `key` attribute of the generated `backend.tf` file to be the relative path from the `root.hcl` file to the `terragrunt.hcl` file that is being processed.\n\n### Migrating state\n\nYou have to be careful when adjusting the `key` attribute of units (including when moving units around in the filesystem, if you use something like `path_relative_to_include` to drive the value of the `key` attribute) because it can result in state being stored in a different location in the remote backend.\n\nThere's native tooling in OpenTofu/Terraform to support these procedures, but you want to be confident you know what you're doing when you run them. By default, Terragrunt will provision a remote backend that uses versioning, so you can always roll back to a previous state if you need to.\n\n```bash\n# First, we'll migrate state to the new location\n$ terragrunt init -migrate-state\n# Then, let's take a look at the generated backend.tf file\n$ find .terragrunt-cache -name backend.tf -exec cat {} \\;\n# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa\nterraform {\n  backend \"s3\" {\n    bucket         = \"my-tofu-state\"\n    dynamodb_table = \"my-lock-table\"\n    encrypt        = true\n    key            = \"vpc/tofu.tfstate\"\n    region         = \"us-east-1\"\n  }\n}\n```\n\nNow, we can run the plan in the `ec2` directory without any issues:\n\n```bash\n# Within the ec2 directory\n$ terragrunt plan\n...\n$ find .terragrunt-cache -name backend.tf -exec cat {} \\;\n# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa\nterraform {\n  backend \"s3\" {\n    bucket         = \"my-tofu-state\"\n    dynamodb_table = \"my-lock-table\"\n    encrypt        = true\n    key            = \"ec2/tofu.tfstate\"\n    region         = \"us-east-1\"\n  }\n}\n```\n\nFollowing this pattern, you can create as many units of infrastructure in your project as you like without worrying about collisions in remote state keys.\n\nNote that while this is the idiomatic approach for defining the `key` attribute for your `backend` configuration, it is not a requirement. You can set the `key` attribute to any value you like, and you can use any Terragrunt HCL function to generate that value dynamically such that you avoid collisions in your remote state.\n\nAnother completely valid approach, for example, is to utilize [get_repo_root](/reference/hcl/functions#get_repo_root), which returns a path relative to the root of the git repository. This, of course, has the drawback that it will not work if you are not using git.\n\nJust make sure to test your configuration carefully, and document your approach so that others can understand what you're doing.\n\n### The `dependency` block\n\nYou might have noticed that the `ec2` unit of infrastructure has a `dependency` block in its configuration:\n\n```hcl\n# ec2/terragrunt.hcl\n# ...\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n\ninputs = {\n  name = \"single-instance\"\n\n  instance_type = \"t2.micro\"\n  monitoring    = true\n  subnet_id     = dependency.vpc.outputs.private_subnets[0]\n\n  tags = {\n    IaC         = \"true\"\n    Environment = \"dev\"\n  }\n}\n```\n\nThis block tells Terragrunt that the `ec2` unit _depends_ on the output of the `vpc` unit. You can also see that it references that dependency within the `inputs` block as `dependency.vpc.outputs.private_subnets[0]`.\n\nWhen Terragrunt is performing a run for a dependency, it will first run `terragrunt output` in the dependency, then expose the values from that output as values that can be used in the dependent unit.\n\nThis is a very useful mechanism, as it keeps each unit isolated, while allowing for message passing between units when they need to interact.\n\n### The Directed Acyclic Graph (DAG)\n\nDependencies also give Terragrunt a way to reason about the order in which units of infrastructure should be run. It uses what's called a [Directed Acyclic Graph (DAG)](/getting-started/terminology/#directed-acyclic-graph-dag) to determine the order in which units should be run, and then runs them in that order.\n\nFor example, let's go ahead and destroy all the infrastructure that we've created so far:\n\n```bash\n# From the root directory\n$ terragrunt run --all destroy\n16:32:08.944 INFO   The stack at . will be processed in the following order for command destroy:\nGroup 1\n- Module ./ec2\n\nGroup 2\n- Module ./vpc\n\n\nWARNING: Are you sure you want to run `terragrunt destroy` in each folder of the stack described above? There is no undo! (y/n)\n```\n\nFirst, notice that we're using a special `run --all` command for Terragrunt. This command tells Terragrunt that we're operating on a stack of units, and that we want to run a given OpenTofu/Terraform command on all of them.\n\nSecond, notice that the `ec2` unit is being run _before_ the `vpc` unit. Terragrunt knows that the `ec2` unit depends on the `vpc` unit, so it plans to run the `ec2` unit first, followed by the `vpc` unit.\n\nThis is a simple example, but as you build out more complex stacks of infrastructure, you'll find that Terragrunt's dependency resolution is a powerful tool for getting infrastructure provisioned correctly.\n\nGo ahead and answer `y` to the prompt to allow destruction to proceed, and notice that the logging has also changed slightly:\n\n```txt\n16:33:28.820 STDOUT [ec2] tofu: aws_instance.this[0]: Destruction complete after 1m11s\n16:33:28.936 STDOUT [ec2] tofu:\n16:33:28.936 STDOUT [ec2] tofu: Destroy complete! Resources: 1 destroyed.\n16:33:28.936 STDOUT [ec2] tofu:\n16:33:30.713 STDOUT [vpc] tofu: aws_vpc.this[0]: Refreshing state... [id=vpc-063d11b72a2c9f8b3]\n16:33:31.510 STDOUT [vpc] tofu: aws_default_security_group.this[0]: Refreshing state... [id=sg-060d402b95a2cd935]\n16:33:31.511 STDOUT [vpc] tofu: aws_default_route_table.default[0]: Refreshing state... [id=rtb-05adb3ee7f48640f0]\n```\n\nTerragrunt will give you the contextual information you need to understand what's happening in your stack as it's being run. That `[ec2]` and `[vpc]` prefix is a great way to quickly disambiguate what's happening in one unit of infrastructure from another.\n\n### Mock outputs\n\nNow that the stack has been destroyed, take a look at the error you get when you try to run `terragrunt run --all plan` again:\n\n```bash\n$ terragrunt run --all plan\n...\n16:50:22.153 STDOUT [vpc] tofu: Note: You didn't use the -out option to save this plan, so OpenTofu can't\n16:50:22.153 STDOUT [vpc] tofu: guarantee to take exactly these actions if you run \"tofu apply\" now.\n16:50:22.854 ERROR  [ec2] Module ./ec2 has finished with an error\n16:50:22.855 ERROR  error occurred:\n\n* ./vpc/terragrunt.hcl is a dependency of ./ec2/terragrunt.hcl but detected no outputs. Either the target module has not been applied yet, or the module has no outputs. If this is expected, set the skip_outputs flag to true on the dependency block.\n\n16:50:22.855 ERROR  Unable to determine underlying exit code, so Terragrunt will exit with error code 1\n```\n\nThe error emitted here tells us that the `vpc` unit doesn't have any outputs available for the `ec2` unit to consume as a dependency.\n\nThe pattern most commonly used to address this is to simply mock the unavailable output during plans.\n\nAdjust the `vpc` dependency in the `ec2` unit like so:\n\n```hcl\n# ...\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n\n  mock_outputs = {\n    private_subnets = [\"mock-subnet\"]\n  }\n\n  mock_outputs_allowed_terraform_commands = [\"plan\"]\n}\n# ...\n```\n\nThen run the plan again:\n\n```bash\n$ terragrunt run --all plan\n...\n16:53:04.037 STDOUT [ec2] tofu:       + source_dest_check                    = true\n16:53:04.037 STDOUT [ec2] tofu:       + spot_instance_request_id             = (known after apply)\n16:53:04.037 STDOUT [ec2] tofu:       + subnet_id                            = \"mock-subnet\"\n16:53:04.037 STDOUT [ec2] tofu:       + tags                                 = {\n16:53:04.038 STDOUT [ec2] tofu:           + \"Environment\" = \"dev\"\n...\n```\n\nAs you can see, the plan for the EC2 instance now includes a `subnet_id` value of `mock-subnet`, which is the value we provided in the `mock_outputs` block.\n\nTerragrunt only uses these mock values when the output is unavailable, so a `terragrunt run --all apply` would succeed, but it's best practice to explicitly tell Terragrunt that it should only use these mock values during a plan (or any other command where you are okay with the output being mocked).\n\nAlso note that when you run `terragrunt run --all apply`:\n\n```bash\n$ terragrunt run --all apply\n16:57:32.297 INFO   The stack at . will be processed in the following order for command apply:\nGroup 1\n- Module ./vpc\n\nGroup 2\n- Module ./ec2\n\n\nAre you sure you want to run 'terragrunt apply' in each folder of the stack described above? (y/n)\n```\n\nThat the order of units has flipped. Terragrunt knows that during applies, dependencies actually need to be run _before_ the dependent unit, so it's flipped the order of the units in the stack, relative to destroys.\n\nYou can answer `y` to allow the apply to proceed and see that the EC2 instance is placed into a real subnet (not the mock value) as expected.\n\n### Configuration hierarchy\n\nTerragrunt also provides tooling for constructing a hierarchy of configurations that can be used to manage multiple environments, regions, or accounts.\n\nSay, for example, you wanted to provision the same resources you've provisioned so far, but in multiple AWS regions, with a filesystem layout like this:\n\n<FileTree>\n\n- root.hcl\n- us-east-1\n  - vpc\n    - terragrunt.hcl\n  - ec2\n    - terragrunt.hcl\n- us-west-2\n  - vpc\n    - terragrunt.hcl\n  - ec2\n    - terragrunt.hcl\n\n</FileTree>\n\nWith Terragrunt, that's pretty easy to achieve. You would first create a `us-east-1` directory like so:\n\n```bash\nmkdir us-east-1\n```\n\nThen move the contents you have in the `vpc` and `ec2` directories into the `us-east-1` directory:\n\n```bash\nmv vpc/ ec2/ us-east-1/\n```\n\nRemember that now you'll need to migrate state, as changing the location of the units in the filesystem will result in a change in the remote state path:\n\n(In production scenarios, you likely want to carefully manage state by migrating over one unit at a time, but for the sake of this tutorial, you can learn about this shortcut)\n\n```bash\nterragrunt run --all -- init -migrate-state\n```\n\nWe want the AWS region used by our units to be determined dynamically, so we can add a configuration file to the `us-east-1` directory that looks like this:\n\n```hcl\n# us-east-1/region.hcl\nlocals {\n  region = \"us-east-1\"\n}\n```\n\nThen update the `root.hcl` like so:\n\n```hcl\n# root.hcl\nlocals {\n  region_hcl = find_in_parent_folders(\"region.hcl\")\n  region     = read_terragrunt_config(local.region_hcl).locals.region\n}\n\n# Configure the remote backend\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n  config = {\n    bucket = \"my-tofu-state\"\n\n    key            = \"${path_relative_to_include()}/tofu.tfstate\"\n    region         = \"us-east-1\"\n    encrypt        = true\n    dynamodb_table = \"my-lock-table\"\n  }\n}\n\n# Configure the AWS provider\ngenerate \"provider\" {\n  path = \"provider.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nprovider \"aws\" {\n  region = \"${local.region}\"\n}\nEOF\n}\n```\n\nNow, when the configurations in the `us-east-1` directory include the `root.hcl`, they'll automatically parse the first `region.hcl` file they find while traversing up the filesystem, and use the `region` value defined in that file to set the AWS region for the provider.\n\n**NOTE** In the generate block, we're using `\"${local.region}\"`, rather than `local.region`. This is because the `generate` block is going to generate a file directly into the OpenTofu/Terraform module. We need to ensure that when the value is interpolated, it's done so in a way that OpenTofu/Terraform can understand, so we wrap it in quotes.\n\n**ALSO NOTE** The `remote_state` block is still storing all state in the `us-east-1` region by design. We don't have to do this, and you could easily set it to store state in multiple regions. For the sake of simplicity, and demonstration, we're keeping it in one region.\n\n### Exposed includes\n\nBefore moving on, take note of one thing, the `azs` attribute in the `vpc` unit of the `us-east-1` stack is hardcoded to `[\"us-east-1a\", \"us-east-1b\", \"us-east-1c\"]`.\n\nThis would cause issues if we were to try to deploy the `vpc` unit in the `us-west-2` stack, as those availability zones don't exist in the `us-west-2` region. What we need to do is make the `azs` attribute dynamic and use the resolved region to determine the correct availability zones.\n\nTo do this, we can _expose_ the attributes on the included `root` configuration by setting the `expose` attribute to `true`:\n\n```hcl\ninclude \"root\" {\n  path   = find_in_parent_folders(\"root.hcl\")\n  expose = true\n}\n\nlocals {\n  region = include.root.locals.region\n}\n\n# Configure the module\n# The URL used here is a shorthand for\n# \"tfr://registry.terraform.io/terraform-aws-modules/vpc/aws?version=5.16.0\".\n# Note the extra `/` after the protocol is required for the shorthand\n# notation.\nterraform {\n  source = \"tfr:///terraform-aws-modules/vpc/aws?version=5.16.0\"\n}\n\n# Configure the inputs for the module\ninputs = {\n  name = \"my-vpc\"\n  cidr = \"10.0.0.0/16\"\n\n  azs             = [\"${local.region}a\", \"${local.region}b\", \"${local.region}c\"] # <--\n  private_subnets = [\"10.0.1.0/24\", \"10.0.2.0/24\", \"10.0.3.0/24\"]\n  public_subnets  = [\"10.0.101.0/24\", \"10.0.102.0/24\", \"10.0.103.0/24\"]\n\n  enable_nat_gateway = false\n  enable_vpn_gateway = false\n\n  tags = {\n    IaC = \"true\"\n    Environment = \"dev\"\n  }\n}\n```\n\nThis makes it so that the values of the `azs` attribute are determined dynamically based on the region that the unit is being deployed to.\n\nNow that you've set up the `us-east-1` directory, you can repeat the process for the `us-west-2` directory:\n\n```bash\ncp -R us-east-1/ us-west-2/\n```\n\nThen update the `region.hcl` file in the `us-west-2` directory to set the region to `us-west-2`:\n\n```hcl\n# us-west-2/region.hcl\nlocals {\n  region = \"us-west-2\"\n}\n```\n\n### Tightening the blast radius\n\nRun the `terragrunt run --all apply` command after changing your current working directory to the `us-west-2` directory:\n\n```bash\ncd us-west-2\nterragrunt run --all apply\n```\n\nYou should see the VPC and EC2 instances being provisioned in the `us-west-2` region.\n\nThis showcases three superpowers you gain when you leverage Terragrunt:\n\n1. **Automatic DAG Resolution**: No configuration file had to be updated or modified to ensure that the `ec2` unit was run after the `vpc` unit when provisioning the `us-west-2` stack. Terragrunt automatically resolved the dependency graph and ran the units in the correct order.\n2. **Dynamic Configuration**: The code you copied from the `us-east-1` directory to the `us-west-2` directory didn't need to be modified at all to provision resources in a different region (with the exception of naming the region in the `region.hcl` file). Terragrunt was able to dynamically resolve the correct configuration based on context, and apply it to the OpenTofu/Terraform modules as generic patterns of infrastructure.\n3. **Reduced Blast Radius**: By applying Terragrunt within the `us-west-2` directory, you were able to confidently target only the resources in that region, without affecting the resources in the `us-east-1` region. This is a powerful tool for safely managing multiple environments, regions, or accounts with a single codebase. Your current working directory when using Terragrunt is your [blast radius](/getting-started/terminology/#blast-radius), and Terragrunt makes it easy to manage that blast radius effectively.\n\n### Cleanup\n\nIf you still have all the resources that were provisioned as part of this tutorial active, this is a reminder that you might want to clean them up.\n\nTo destroy all the resources you've provisioned thus far, run the following:\n\n```bash\n# From the root directory\n$ terragrunt run --all destroy\n```\n\nIn real-world scenarios, it's generally advised that you plan your destroys first before cleaning them up:\n\n```bash\n# From the root directory\n$ terragrunt run --all -- plan -destroy\n```\n\nYou won't need to run any more Terragrunt commands for the rest of this guide.\n\n### Recommended Repository Patterns\n\nOutside of the patterns used for setting up Terragrunt configurations within a project, there are also some patterns that we recommend for managing one or more repositories used to manage infrastructure. At Gruntwork, we refer to this as your \"Infrastructure Estate\".\n\nThese recommendations are merely guidelines, and you should adapt them to suit your team's needs and constraints.\n\n#### `infrastructure-live`\n\nThe core of the infrastructure you manage with a Terragrunt is typically stored in a repository named `infrastructure-live` (or some variant of it). This repository is where you store the Terragrunt configurations used for infrastructure that is intended to be \"live\" (i.e. provisioned and active).\n\nMost successful teams stick to [Trunk Based Development](https://trunkbaseddevelopment.com/), perform plans on any change being proposed via a pull request / merge request, and only apply changes to live infrastructure after a successful plan and review.\n\nThis repository is generally concerned with the configuration of reliably reproducible, immutable and versioned infrastructure. You generally don't author OpenTofu/Terraform code directly into it, and you apply appropriate branch protection rules to ensure that changes are merged only if they get the appropriate sign-off from responsible parties.\n\nWhat's on the default branch for this repository is generally considered the source of truth for the infrastructure you have provisioned. That default branch is generally the only version that matters when considering the state of your infrastructure.\n\nYou can see an example of this in the [terragrunt-infrastructure-live-stacks-example](https://github.com/gruntwork-io/terragrunt-infrastructure-live-stacks-example) repository maintained by Gruntwork.\n\n#### `infrastructure-modules`\n\nThe patterns for your infrastructure you want to reliably reproduce. This repository is where you store the OpenTofu/Terraform modules that you use in your `infrastructure-live` repository.\n\nThis repository is generally concerned with maintaining versioned, well tested and vetted patterns of infrastructure, ready to be consumed by the `infrastructure-live` repository.\n\nYou typically integrate this repository with tools like [Terratest](https://terratest.gruntwork.io/) to ensure that every change to a module is well tested and reliable.\n\n[Semantic Versioning](https://semver.org/) is widely used to manage communicating the impact of updates to this repository, and you typically pin the version of a consumed module in the `infrastructure-live` repository to a specific tag.\n\nYou can see an example of this in the [terragrunt-infrastructure-catalog-example](https://github.com/gruntwork-io/terragrunt-infrastructure-catalog-example) repository maintained by Gruntwork.\n\n### Atomic Deployments\n\nFollowing the repository patterns outlined above, you typically see Terragrunt repositories that have configurations which look like this:\n\n```hcl\n# infrastructure-live/qa/app/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"github.com:foo/infrastructure-modules.git//app?ref=v0.0.1\"\n}\n\ninputs = {\n  instance_count = 3\n  instance_type  = \"t2.micro\"\n}\n```\n\nWhere `app` is an opinionated module in the `infrastructure-modules` repository, maintained by the team managing infrastructure for the `foo` organization.\n\nThe code in that module might be hand-rolled, it may wrap a community maintained module, or it might wrap a module like one found in the [Gruntwork IaC Library](https://www.gruntwork.io/platform/iac-library).\n\nRegardless, the module is something that the team managing infrastructure for an organization has vetted for deployment.\n\nWhen deploying a change to live infrastructure, the team would typically make a change like the following:\n\n```hcl\n# infrastructure-live/qa/app/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"github.com:foo/infrastructure-modules.git//app?ref=v0.0.2\" # <--\n}\n\ninputs = {\n  instance_count = 1\n  instance_type  = \"t3.micro\"\n}\n```\n\nGiven that all the changes here are part of one atomic deployment, it's fairly easy to determine the impact of the change, and to roll back if necessary.\n\nAfter that, they would propagate the change up however many intermediary environments they have, and finally to production.\n\n```hcl\n# infrastructure-live/prod/app/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"github.com:foo/infrastructure-modules.git//app?ref=v0.0.2\" # <--\n}\n\ninputs = {\n  instance_count = 3\n  instance_type  = \"t3.large\"\n}\n```\n\nNote that the two `terragrunt.hcl` files here have different `inputs` values, as those values are specific to the environment they are being deployed to.\n\nThe end result of this process is that _infrastructure changes_ are atomic and reproduceable, and that the infrastructure being deployed is versioned and immutable.\n\n![Using Terragrunt to promote immutable Infrastructure as Code across environments](../../../assets/img/collections/documentation/promote-immutable-Terraform-code-across-envs.png)\n\nIf at any point during this process a change is found to be problematic, the team can simply roll back to the previous version of the module for a single unit in a given environment.\n\nThat's the power of reducing your blast radius with Terragrunt!\n\n### Keep It Simple, Silly\n\nOne last pattern to internalize is the general tendency to prefer simple configurations over complex ones when possible.\n\nTerragrunt provides a lot of power and flexibility, but it's generally best to use that power to make your configurations more readable and maintainable. Keep in mind that you're writing code that will be read by other humans, and that you might not be around to explain any complexity you introduce.\n\nAs an example, consider one potential solution to a step outlined in the [Exposed includes](#exposed-includes) section, the requirement to update the `region` local in the `region.hcl` file:\n\n```hcl\n# us-west-2/region.hcl\nlocals {\n  region = \"us-west-2\"\n}\n```\n\nYou might think to yourself \"Hey, I know a lot about Terragrunt functionality, I can make this more dynamic, such that I don't even need to create a `region.hcl` file!\" and come up with a solution like this:\n\n```hcl\n# root.hcl\nlocals {\n  region = split(\"/\", path_relative_to_include())[0]\n}\n\n# Configure the remote backend\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n  config = {\n    bucket = \"my-tofu-state\"\n\n    key            = \"${path_relative_to_include()}/tofu.tfstate\"\n    region         = \"us-east-1\"\n    encrypt        = true\n    dynamodb_table = \"my-lock-table\"\n  }\n}\n\n# Configure the AWS provider\ngenerate \"provider\" {\n  path = \"provider.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nprovider \"aws\" {\n  region = \"${local.region}\"\n}\nEOF\n}\n```\n\nThis would result in the `region` local being set to the name of the first directory in the path to the `terragrunt.hcl` file that is being run during an include (`us-east-1` and `us-west-1` in each respective stack). This would allow you to remove the `region.hcl` file from both the `us-east-1` and `us-west-2` directories.\n\nConsider, though, that this might make the configuration harder to understand for someone who is not as familiar with Terragrunt as you are. You've now tightly coupled the name of the directory to the region that the infrastructure is being deployed to, and you've made it harder for someone to understand if they run into issues.\n\nSay a user tries to deploy infrastructure while on a Windows machine, where the path separator is `\\` instead of `/`. Using this configuration would result in the `region` local being set to something like `us-east-1\\vpc`, which is confusing and not what you want.\n\nIn this case, you might prefer to have kept the `region.hcl` file, as it makes the configuration more explicit and easier to understand.\n\nOn the other hand, maybe you work with a team that's very comfortable with Terragrunt, exclusively using Unix-based systems, and you've all agreed and documented this as a good pattern to follow. In that case, this might be a perfectly acceptable solution.\n\nYou have to exercise your best judgment when deciding how much complexity to introduce into your Terragrunt configurations. As a general rule, the best patterns to follow are the ones that are easiest to reproduce, understand, and maintain.\n\n## Next steps\n\nNow that you’ve learned the basics of Terragrunt, here is some further reading to learn more:\n\n1. [Features](/features/units): Learn about the core features Terragrunt supports.\n\n2. [Documentation](/reference/hcl): Check out the detailed Terragrunt reference documentation.\n\n3. [_Fundamentals of DevOps and Software Delivery_](https://www.gruntwork.io/fundamentals-of-devops): Learn the fundamentals of DevOps and Software Delivery from one of the founders of Gruntwork!\n\n4. [_Terraform: Up & Running_](https://www.terraformupandrunning.com/): Terragrunt is a direct implementation of many of the ideas from this book.\n"
  },
  {
    "path": "docs/src/content/docs/01-getting-started/03-install.mdx",
    "content": "---\ntitle: Install\ndescription: Install Terragrunt!\nslug: getting-started/install\nsidebar:\n  order: 3\n---\n\nimport InstallTabs from '@components/InstallTabs.astro'\nimport { Code } from '@astrojs/starlight/components'\nimport { getLatestRelease } from '@lib/github';\nexport const releaseData = await getLatestRelease('gruntwork-io', 'terragrunt');\nexport const version = releaseData?.tag_name || 'v0.99.0';\n\n## Quick Install\n\nThe quickest way to install Terragrunt on Linux or macOS:\n\n```bash\ncurl -sL https://docs.terragrunt.com/install | bash\n```\n\n:::tip[Script Options]\nRun `curl -sL https://docs.terragrunt.com/install | bash -s -- --help` to see all options, or [view the script on GitHub](https://github.com/gruntwork-io/terragrunt/blob/main/docs/public/install).\n:::\n\n## Download from releases page\n\n1. Go to the [Releases Page](https://github.com/gruntwork-io/terragrunt/releases).\n2. Download the archive for your operating system: e.g., if you're on a Mac, download `terragrunt_darwin_amd64.tar.gz`; if you're on Windows, download `terragrunt_windows_amd64.exe.zip`, etc.\n3. Download `SHA256SUMS` and optionally `SHA256SUMS.gpgsig` for signature verification.\n4. Verify the checksum and optionally the signature (see [Verifying the checksum](#verifying-the-checksum) below).\n5. Extract the archive: e.g., `tar -xzf terragrunt_darwin_amd64.tar.gz` or unzip on Windows.\n6. Add execute permissions to the binary (Linux/Mac): `chmod u+x terragrunt`.\n7. Put the binary somewhere on your `PATH`: e.g., On Linux and Mac: `mv terragrunt /usr/local/bin/terragrunt`.\n\n### Verifying the checksum\n\nWhen you download the binary from the releases page, you can also use the checksum file to verify the integrity of the binary. This can be useful for ensuring that you have an intact binary and that it has not been tampered with.\n\nTo verify the integrity of the file, do the following:\n\n1. Have the binary downloaded, and accessible.\n2. Generate the SHA256 checksum of the binary.\n3. Download the `SHA256SUMS` file from the releases page.\n4. Find the expected checksum for the binary you downloaded.\n5. If the checksums match, the binary is intact and has not been tampered with.\n6. Optionally, verify the GPG signature:\n\n```bash\n# Import Gruntwork's public key (first time only)\ncurl -s https://gruntwork.io/.well-known/pgp-key.txt | gpg --import\n\n# Verify signature\ngpg --verify SHA256SUMS.gpgsig SHA256SUMS\n```\n\n:::caution[Verify Key Fingerprint]\nAfter importing the key, verify its fingerprint matches exactly:\n\n```bash\ngpg --fingerprint 577774ACA847CC49\n```\n\nExpected output:\n```\npub   ed25519 2026-01-12 [SC]\n      68C8 0F86 DF98 E710 C0F2  2E2E 5777 74AC A847 CC49\nuid           [ unknown] Gruntwork (Code Signing Key) <security@gruntwork.io>\n```\n:::\n\n7. Alternatively, verify with Cosign:\n\n```bash\ncosign verify-blob SHA256SUMS \\\n  --bundle SHA256SUMS.sigstore.json \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n  --certificate-identity-regexp \"github.com/gruntwork-io/terragrunt\"\n```\n\n### Convenience Scripts\n\n<InstallTabs version={version} />\n\n:::note\nThese scripts automatically verify the SHA256 checksum and GPG signature before completing.\n:::\n\n## Install via a package manager\n\nNote that all the different package managers are third party. The third party Terragrunt packages may not be updated with the latest version, but are often close. Please check your version against the latest available on the [Releases Page](https://github.com/gruntwork-io/terragrunt/releases).\nIf you  want the latest version, the recommended installation option is to [download from the releases page](https://github.com/gruntwork-io/terragrunt/releases).\n\n* **Windows**: You can install Terragrunt on Windows using [Chocolatey](https://chocolatey.org/)\n\n  ```bash\n  choco install terragrunt\n  ```\n\n* **macOS**: You can install Terragrunt on macOS using [Homebrew](https://brew.sh/):\n\n  ```bash\n  brew install terragrunt\n  ```\n\n* **Linux (Homebrew)**: Most Linux users can use [Homebrew](https://docs.brew.sh/Homebrew-on-Linux):\n\n  ```bash\n  brew install terragrunt\n  ```\n* **Linux (Pacman)**: Arch Linux users can use [pacman](https://archlinux.org/packages/extra/x86_64/terragrunt/):\n\n  ```bash\n  pacman -S terragrunt\n  ```\n\n* **Linux (Gentoo)**: Gentoo users can use [emerge](https://repology.org/project/terragrunt/versions):\n\n  ```bash\n  emerge -a app-admin/terragrunt-bin\n  ```\n\n* **FreeBSD**: You can install Terragrunt on FreeBSD using [Pkg](https://www.freebsd.org/cgi/man.cgi?pkg(7)):\n\n  ```bash\n  pkg install terragrunt\n  ```\n\n## Install via tool manager\n\nA best practice when using Terragrunt is to pin the version you are using to ensure that you, your colleagues and your CI/CD pipelines are all using the same version. This also allows you to easily upgrade to new versions and rollback to previous versions if needed.\n\nYou can use a tool manager to install and manage Terragrunt versions.\n\n* **mise**: You can install Terragrunt using [mise](https://mise.jdx.dev)\n\n  <Code\n    lang=\"bash\"\n    code={`mise install terragrunt ${version}`}\n    frame=\"terminal\"\n  >\n  </Code>\n\n* **asdf**: You can install Terragrunt using [asdf](https://asdf-vm.com)\n\n  <Code\n    lang=\"bash\"\n    code={`asdf plugin add terragrunt\\nasdf install terragrunt ${version}`}\n    frame=\"terminal\"\n  >\n  </Code>\n\nBoth of these tools allow you to pin the version of Terragrunt you are using in a `.tool-versions` (and `.mise.toml` for mise) file in your project directory.\n\nColleagues and CI/CD pipelines can then install the associated tool manager, and run using the pinned version.\n\nNote that the tools Terragrunt integrates with, such as OpenTofu and Terraform, can also be managed by these tool managers, so you can also pin the versions of those tools in the same file.\n\n**Backend details:**\n\n- **mise** uses [aqua](https://aquaproj.github.io/) as its default backend to install Terragrunt.\n- **asdf** uses the asdf-terragrunt plugin, which is maintained by Gruntwork: https://github.com/gruntwork-io/asdf-terragrunt\n\n## Building from source\n\nIf you'd like to build from source, you can use `go` to build Terragrunt yourself, and install it:\n\n```shell\ngit clone https://github.com/gruntwork-io/terragrunt.git\ncd terragrunt\n# Feel free to checkout a particular tag, etc if you want here.\ngo install\n```\n\n## Enable tab completion\n\nIf you use either Bash or Zsh, you can enable tab completion for Terragrunt commands. To enable autocomplete, first ensure that a config file exists for your chosen shell.\n\nFor Bash shell.\n\n```shell\ntouch ~/.bashrc\n```\n\nFor Zsh shell.\n\n```shell\ntouch ~/.zshrc\n```\n\nThen install the autocomplete package.\n\n``` shell\nterragrunt --install-autocomplete\n```\n\nOnce the autocomplete support is installed, you will need to restart your shell.\n\n## Gruntwork Pipelines\n\nGruntwork offers a commercial CI/CD solution for Terragrunt called [Pipelines](https://www.gruntwork.io/platform/pipelines). Pipelines is a fully managed CI/CD service that is designed to work seamlessly with Terragrunt. It provides an out of the box solution for running Terragrunt in CI/CD without the need to setup and maintain your own CI/CD infrastructure.\n\n## Terragrunt GitHub Action\n\nTerragrunt is also available as a GitHub Action.\n\nInstructions on how to use it can be found at [https://github.com/gruntwork-io/terragrunt-action](https://github.com/gruntwork-io/terragrunt-action).\n"
  },
  {
    "path": "docs/src/content/docs/01-getting-started/04-terminology.md",
    "content": "---\ntitle: Terminology\ndescription: Quickly understand commonly use terms in Terragrunt.\nslug: getting-started/terminology\nsidebar:\n  order: 4\n---\n\nInfrastructure as Code (IaC) tooling necessarily requires a lot of terminology to describe various concepts and features due to the breadth of the domain.\n\nWhenever possible, Terragrunt terminology attempts to align with wider industry standards, but there are always exceptions. There are going to be times when certain terms are used in different tools, but have special meaning in Terragrunt, and there are times when the same term might have different meaning in different contexts.\n\nThis document aims to provide a quick reference for the most important and commonly used terms in Terragrunt and generally in Gruntwork products. Whenever terminology used in Terragrunt deviates from this document, it should either be explained or adjusted to align with this document.\n\n## Terms\n\n---\n\n### Terragrunt\n\nTerragrunt is a flexible orchestration tool that allows Infrastructure as Code written in [OpenTofu](https://opentofu.org/)/[Terraform](https://www.terraform.io/) to scale.\n\nIt differs from many other IaC tools in that it is designed to be an orchestrator for OpenTofu/Terraform execution, rather than primarily provisioning infrastructure itself. Terragrunt users write OpenTofu/Terraform code to define high-level patterns of infrastructure that they want to create, then use Terragrunt to dynamically apply those generic patterns in particular ways.\n\nBecause of this separation of concerns, most of what Terragrunt does is designed to extend the capabilities of OpenTofu/Terraform, rather than replace them. Most of the [features](/features/units) of Terragrunt are designed to make it easier to manage large infrastructure estates, or to provide additional capabilities that are inconvenient or impossible to achieve with OpenTofu/Terraform alone.\n\n### OpenTofu\n\n[OpenTofu](https://opentofu.org/) is an open-source Infrastructure as Code tool spawned as a fork of [Terraform](https://www.terraform.io/) after the license change from the [Mozilla Public License (MPL)](https://en.wikipedia.org/wiki/Mozilla_Public_License) to the [Business Source License (BSL)](https://en.wikipedia.org/wiki/Business_Source_License).\n\nOpenTofu was created as a drop-in replacement for Terraform (as it was forked from the same MPL source code), and is designed to be fully compatible with Terraform configurations and modules.\n\nYou may notice that Terragrunt documentation uses the phrase \"OpenTofu/Terraform\" to refer to the IaC tooling that Terragrunt orchestrates. This is because Terragrunt is generally agnostic to the specific IaC tooling that is being used to drive infrastructure updates. When relevant, Terragrunt documentation endeavors to explicitly indicate that functionality is specific to one tool or the other. From the perspective of Terragrunt, the two are usually interchangeable, though Terragrunt will default to using OpenTofu if both are available.\n\nNote that some documentation refers to Terraform alone in some instances as a consequence of the historical context in which Terragrunt was created, as it predates the creation of OpenTofu. Conversely, some documentation may refer to OpenTofu alone simply because of the fact that OpenTofu is the default IaC tool that Terragrunt uses.\n\n### Unit\n\nA unit is a single instance of infrastructure managed by Terragrunt. It has its own state, and can be detected by the presence of a `terragrunt.hcl` file in a directory.\n\nUnits typically represent a minimal useful piece of infrastructure that should be independently managed.\n\ne.g. A unit might represent a single VPC, a single database, or a single server.\n\nWhile not a requirement, a general tendency experienced when working with Terragrunt is that units tend to decrease in size. This is because Terragrunt makes it easy to segment pieces of infrastructure into their own state, and to have them interact with each other through the use of [dependency blocks](/reference/hcl/blocks#dependency). Smaller units are quicker to update, easier to reason about and safer to work with.\n\nA common pattern used in the repository structure for Terragrunt projects is to have a single `root.hcl` file located at the root of the repository, and multiple subdirectories each containing their own `terragrunt.hcl` file. This is typically done to promote code-reuse, as it allows for any configuration common to all units to be defined in the `root.hcl` file, and for unit-specific configuration to be defined in child directories. In this pattern, the `root.hcl` file is not considered a unit, while all the child directories containing `terragrunt.hcl` files are.\n\nNote that units don't technically need to call their configuration files `terragrunt.hcl` (that's configurable via the [--config](/reference/cli/commands/run#config)), and users don't technically need to use `root.hcl` as the root configuration file or to name it that. This is the most common pattern followed by the community, however, and deviation from this pattern should be justified in the context of the project. It can help others with Terragrunt experience understand the project more easily if industry standard patterns are followed.\n\n### Stack\n\nA stack is a collection of units managed by Terragrunt. There is ([as of writing](https://github.com/gruntwork-io/terragrunt/issues/3313)) work underway to provide a top level artifact for interacting with stacks via a `terragrunt.stack.hcl` file, but as of now, stacks are generally defined by a directory with a tree of units. Units within a stack can be dependent on each other, and can be updated in a specific order to ensure that dependencies are resolved in the correct order.\n\nStacks typically represent a collection of units that need to be managed in concert.\n\ne.g. A stack might represent a collection of units that together form a single application environment, a business unit, or a region.\n\nThe design of `terragrunt.stack.hcl` files is to ensure that they function entirely as a convenient shorthand for an equivalent directory structure of units. This is to ensure that users are able to easily transition between the two paradigms, and are able to decide for themselves which approach to structuring infrastructure is most appropriate for their use case.\n\n### Component\n\nComponent is a generic term to refer to something that is either a unit or a stack.\n\nCertain Terragrunt commands operate on components (e.g. [`find`](/reference/cli/commands/find) and [`list`](/reference/cli/commands/list)) while others operate on particular types of components (e.g. [`run`](/reference/cli/commands/run) only runs units whereas [`stack generate`](/reference/cli/commands/stack/generate) and [`stack output`](/reference/cli/commands/stack/output) commands run on stacks).\n\n### Module\n\nA module is an [OpenTofu/Terraform construct](https://opentofu.org/docs/language/modules/) defined using a collection of OpenTofu/Terraform configurations ending in `.tf` (or `.tofu` in the case of OpenTofu) that represent a general pattern of infrastructure that can be instantiated multiple times.\n\nModules typically represent a generic pattern of infrastructure that can be instantiated multiple times, with different configurations exposed as variables.\n\ne.g. A module might represent a generic pattern for a VPC, a database, or a server. Note that this differs from a unit, which represents a single instance of a provisioned VPC, database, or server.\n\nModules can be located either in the local filesystem, in a remote repository, or in any of [these supported locations](https://opentofu.org/docs/language/modules/sources/).\n\nTo integrate a module into a Terragrunt unit, reference the module using the `source` attribute of the [terraform block](/reference/hcl/blocks#terraform).\n\nTerragrunt users typically spend a good deal of time authoring modules, as they are the primary way of defining the infrastructure patterns that Terragrunt is going to be orchestrating. Using tooling like [Terratest](https://github.com/gruntwork-io/terratest) can help to ensure that modules are well-tested and reliable.\n\nA common pattern in Terragrunt usage is to only ever provision versioned, immutable modules. This is because Terragrunt is designed to be able to manage infrastructure over long periods of time, and it is important to be able to reproduce the state of infrastructure at any point in time.\n\n### Resource\n\nA resource is a low level building block of infrastructure that is defined in OpenTofu/Terraform configurations.\n\nResources are typically defined in modules, but don't have to be. Terragrunt can provision resources defined with `.tf` files that are not part of a module, located adjacent to the `terragrunt.hcl` file of a unit.\n\ne.g. A resource might represent a single S3 bucket, or a single load balancer.\n\nResources generally correspond to the smallest piece of infrastructure that can be managed by OpenTofu/Terraform, and each resource has a specific address in state.\n\n### State\n\nTerragrunt stores the current state of infrastructure in one or more OpenTofu/Terraform [state files](https://opentofu.org/docs/language/state/).\n\nState is an extremely important concept in the context of OpenTofu/Terraform, and it's helpful to read the relevant documentation there to understand what Terragrunt does to it.\n\nTerragrunt has myriad capabilities that are designed to make working with state easier, including tooling to bootstrap state backend resources on demand, managing unit interaction with external state, and segmenting state.\n\nThe most common way in which state is segmented in Terragrunt projects is to take advantage of filesystem directory structures. Most Terragrunt projects are configured to store state in remote backends like S3 with keys that correspond to the relative path to the unit directory within a project, relative to the root `terragrunt.hcl` file.\n\n### Directed Acyclic Graph (DAG)\n\nThe way in which units are resolved within a stack is via a [Directed Acyclic Graph (DAG)](https://en.wikipedia.org/wiki/Directed_acyclic_graph#:~:text=A%20directed%20acyclic%20graph%20is,a%20path%20with%20zero%20edges).\n\nThis graph is also used to determine the order in which resources are resolved within a unit. Dependencies in a DAG determine the order in which resources are created, updated, or destroyed.\n\nFor creations and updates, resources are updated such that dependencies are always resolved before their dependents. For destructions, resources are destroyed such that dependents are always destroyed before their dependencies.\n\nThis is still true even when working with multiple units in a stack. Terragrunt will resolve the dependencies of all units in a stack (resolving the DAG within each unit first), and then apply the changes to all units in the stack in the correct order.\n\nNote that DAGs are _Acyclic_, meaning that there are no loops in the graph. This is because loops would create circular dependencies, which would make it impossible to determine the correct order to resolve resources.\n\n### Don't Repeat Yourself (DRY)\n\nThe [Don't Repeat Yourself (DRY)](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) principle is a software development principle that states that duplication in code should be avoided.\n\nEarly on, a lot of Terragrunt functionality was designed to make it easier to follow the DRY principle. This was because Terraform users at the time found that they were often repeating the same, or very similar code across multiple configurations. Examples of this included the limitation that remote state and provider configurations needed to be repeated in every root module, and that there were limitations in the dynamicity of these configurations.\n\nOver time, Terragrunt has evolved to provide more features that make it easier to manage infrastructure at scale, and the focus has shifted to offering more tooling for _orchestrating_ infrastructure, rather than simply making it easier to avoid repeating yourself. Many of the features still serve to make it easier to follow the DRY principle, but this is no longer the primary focus of the tool.\n\nMuch of the marketing around Terragrunt still emphasizes the DRY principle, as it is a useful way to explain the value of Terragrunt to new users. However, you might miss the forest for the trees if you focus too much on the DRY principle when evaluating Terragrunt. Terragrunt is a powerful tool that can be used to manage infrastructure at scale, and it is worth evaluating it based on its capabilities to do so.\n\n### Blast Radius\n\n[Blast Radius](https://en.wikipedia.org/wiki/Blast_radius) is a term used in software development to describe the potential impact of a change, derived from the term used to describe the potential impact of an explosion.\n\nIn the context of infrastructure management, blast radius is used to describe the potential impact (negative or positive) of a change to infrastructure. The larger the blast radius, the more potential impact a change has.\n\nTerragrunt was born out of a need to reduce the blast radii of infrastructure changes. By making it easier to segment state in infrastructure, and to manage dependencies between units, Terragrunt makes it easier to reason about the impact of changes to infrastructure, and to ensure that changes can be made safely.\n\nWhen using Terragrunt, there is very frequently a mapping between your filesystem and the infrastructure you have provisioned with OpenTofu/Terraform. As such, when changing your current working directory in a Terragrunt project, you end up implicitly changing the blast radius of Terragrunt commands. The more units you have as children of your current working directory (the units in your stack), the more infrastructure you are likely to impact with a Terragrunt command.\n\nAs an adage, you can generally think of this property as: \"Your current working directory is your blast radius\".\n\n### Run\n\nA run is a single invocation of OpenTofu/Terraform by Terragrunt.\n\nRuns are the primary way that Terragrunt does work. When you run `terragrunt plan` or `terragrunt apply`, Terragrunt will invoke OpenTofu/Terraform to drive the infrastructure update accordingly.\n\nNote that runs abstract away a lot of the complexity that comes from working with OpenTofu/Terraform directly. Terragrunt might automatically perform some code generation, provision requisite resources, or add/modify to the underlying OpenTofu/Terraform configuration to ensure that day to day operations are as smooth as possible.\n\nThe way in which these complexities are abstracted is via Terragrunt configuration files (`terragrunt.hcl`), which can be used to define how Terragrunt should forward commands to OpenTofu/Terraform.\n\nThere is an explicit list of [supported shortcuts](https://docs.terragrunt.com/reference/cli/commands/opentofu-shortcuts/) that Terragrunt will forward to OpenTofu/Terraform by default. For all other commands that need to be forwarded to OpenTofu/Terraform, use the `run` command (e.g., `terragrunt run -- workspace ls`).\n\nIn the simplest case, a run in a unit with an empty `terragrunt.hcl` file will be equivalent to running OpenTofu/Terraform directly in the unit directory (with some small additional features like automatic initialization and logging adjustments).\n\n### Execution\n\nAn execution is a single command run by Terragrunt, which does not necessarily have anything to do with OpenTofu/Terraform.\n\nWays in which Terragrunt can perform executions are limited to features like [hooks](/features/units/hooks/), [run_cmd](/reference/hcl/functions#run_cmd), etc.\n\nThese utilities are part of what makes Terragrunt so powerful, as they allow users to move infrastructure management complexity out of modules.\n\n### Run Queue\n\nThe Run Queue is the queue of all units that Terragrunt will do work on over one or more runs.\n\nCertain commands like [run --all](/reference/cli/commands/run#all) populate the Run Queue with all units in a stack, while other commands like `plan` or `apply` will only populate the Run Queue with the unit that the command was run in.\n\nCertain flags like [--include-dir](/reference/cli/commands/run#include-dir) can be used to adjust the Run Queue to include additional units. Conversely, there are flags like [--exclude-dir](/reference/cli/commands/run#exclude-dir) that can be used to adjust the Run Queue to exclude units.\n\nTerragrunt will always attempt to run until the Run Queue is empty.\n\n### Runner Pool\n\nThe Runner Pool is the pool of available resources that Terragrunt can use to execute runs.\n\nUnits are dequeued from the Run Queue into the Runner Pool depending on factors like [parallelism](/reference/cli/commands/run#parallelism) and the DAG.\n\nUnits are only considered \"running\" when they are in the Runner Pool.\n\n### Dependency\n\nA dependency is a relationship between two units in a stack that results in data being passed from the dependency to the dependent unit.\n\nDependencies are defined in Terragrunt configuration files using the [dependency block](/reference/hcl/blocks#dependency).\n\nDependencies are important for resolving the DAG, and the DAG is one of the most important properties to understand with Terragrunt. In an effort to avoid confusing users, Terragrunt maintainers attempt to overload the term \"dependency\" as little as possible. Other relationships may be described as \"reading\" or \"including\" to avoid any ambiguity as to what is relevant to the DAG.\n\n### Include\n\nThe term \"include\" is used in two different contexts in Terragrunt.\n\n1. **Include in configuration**: This is when one configuration file is included as partial configuration in another configuration file. This is done using the [include block](/reference/hcl/blocks#include) in Terragrunt configuration files.\n2. **Include in the Run Queue**: This is when a unit is included in the Run Queue. There are multiple ways for a unit to be included in the Run Queue.\n\n### Exclude\n\nThe term \"exclude\" is only used in the context of excluding units from the Run Queue.\n\n### Variable\n\nA variable is a named dynamic value that is exposed by OpenTofu/Terraform configurations.\n\nTo avoid ambiguity, Terragrunt maintainers try to avoid using the term \"variable\" in Terragrunt documentation.\n\n### Input\n\nAn input is a value configured in Terragrunt configurations to set the value of OpenTofu/Terraform variables.\n\nInputs are defined in Terragrunt configuration files using the [inputs attribute](/reference/hcl/attributes#inputs). Under the hood, these inputs result in `TF_VAR_` prefixed environment variables being populated before initiating a run.\n\n### Output\n\nAn output is a value that is returned by OpenTofu/Terraform after a run is completed.\n\nBy default, Terragrunt will interact with OpenTofu/Terraform in order to retrieve these outputs via [dependency blocks](/reference/hcl/blocks#dependency).\n\nTerragrunt does have the ability to mock outputs, which is useful when dependencies do not yet have outputs to be consumed (e.g. during the run of a unit with a dependency that has not been applied).\n\nTerragrunt also has the ability to fetch outputs without interacting with OpenTofu/Terraform via [--fetch-dependency-output-from-state](/reference/cli/commands/run#fetch-dependency-output-from-state) for dependencies where state is stored in AWS. This is an experimental feature, and more tooling is planned to make this easier to use.\n\n### Feature\n\nA [feature](/reference/cli/commands/run#feature) is a configuration that can be dynamically controlled in Terragrunt configurations.\n\nThey operate very similarly to variables, but are designed to be used to dynamically adjust the behavior of Terragrunt configurations, rather than OpenTofu/Terraform configurations.\n\nFeatures can be adjusted using feature flags, which are set in Terragrunt configurations using the [feature block](/reference/hcl/blocks#feature) and the [feature flag](/reference/cli/commands/run#feature) attribute.\n\nLike all good feature flags, you are encouraged to use them with good judgement and to avoid using them as a crutch to avoid making decisions about permanent adjustments to your infrastructure.\n\n### IaC Engine\n\n[IaC Engines](/features/units/engine/) (typically abbreviated \"Engines\") are a way to extend the capabilities of Terragrunt by allowing users to control exactly how Terragrunt performs runs.\n\nEngines allow Terragrunt users to author custom logic for how runs are to be executed in plugins, including defining exactly how OpenTofu/Terraform is to be invoked, where OpenTofu/Terraform is to be invoked, etc.\n\n### Infrastructure Estate\n\nAn infrastructure estate is all the infrastructure that a person or organization manages. This can be as small as a single resource, or as large as a collection of repositories containing one or more stacks.\n\nGenerally speaking, the larger the infrastructure estate, the more important it is to have good tooling for managing it. Terragrunt is designed to be able to manage infrastructure estates of any size, and is used by organizations of all sizes to manage their infrastructure efficiently.\n\n## CLI Redesign\n\nNote that some of the language used in this page may be adjusted in the near future due to RFC [#3445](https://github.com/gruntwork-io/terragrunt/issues/3445).\n\nTo make terminology and overall UI/UX of using Terragrunt more consistent and easier to understand, the RFC proposes a number of changes to the CLI. This includes renaming some flags, reorganizing some commands, and adjusting some terminology.\n\nAs of this writing, the RFC is still in the proposal stage, so share your thoughts on the RFC if you have any opinions on the proposed changes.\n"
  },
  {
    "path": "docs/src/content/docs/02-guides/01-terralith-to-terragrunt/01-introduction.md",
    "content": "---\ntitle: Introduction\ndescription: Introduction to the Terralith to Terragrunt guide\nslug: guides/terralith-to-terragrunt\nsidebar:\n  order: 1\nprev: false\n---\n\nA common challenge that emerges as infrastructure grows is the \"Terralith,\" a portmanteau of Terraform and Monolith. This pattern, also referred to as a \"Megamodule\" or an \"All In One State\" configuration, describes a scenario where a large, complex infrastructure estate is managed within a single state file.\n\nImagine you're a platform engineer, and what once felt like an instant `tofu apply` to update your infrastructure now drags on for minutes. You once had confidence that you could reliably update exactly the infrastructure you cared about changing in every `tofu apply`, but things have gotten complicated. You now have to sift through a massive wall of plan text to confirm that your intended tag update on a resource doesn't bring down production. You're seeing irrelevant timestamp updates, changes introduced out-of-band from colleagues in the AWS console, and more.\n\nMaybe it's faster (and safer) to just go ahead and make the update out-of-band yourself instead of dealing with this monstrosity, exacerbating the issue. This is the scenario that platform engineers find themselves in when they're struggling to deal with a Terralith. This isn't a hypothetical scenario, it's daily life for teams that take what once worked perfectly fine at smaller scales, and incrementally added more and more complexity and technical debt until they find themselves in the position where they can no effectively longer use the Infrastructure as Code (IaC) that served them so well in the past.\n\nThis is a comprehensive, hands-on guide that will demonstrate how you can naturally find yourself managing a Terralith, and how you can get yourself out, using Terragrunt. Along the way, you'll learn skills like:\n\n- Strategies for effectively organizing your IaC for maximum productivity and safety.\n- Principles of state manipulation, and a look under the hood as to how OpenTofu/Terraform store state.\n- Modern best practices for authoring scalable and reliable Terragrunt configuration.\n\nThe guide will do this by having you:\n\n1. Set up a local development environment for managing IaC.\n2. Build a fun toy project, and provision it in a real AWS account.\n3. Expand that toy project until you can start to see the impact of managing infrastructure following a Terralith architecture pattern.\n4. Break down that Terralith to gain improvements in scalability and reliability.\n5. Add Terragrunt to improve your ability to orchestrate your IaC.\n6. Leverage more of Terragrunt to further improve your DevEx with IaC.\n\nThis guide will not assume a significant amount of technical skill with Terragrunt, OpenTofu, AWS or NodeJS, but you will use these tools along the way. The guide will gently guide you through their usage, focusing on teaching you lessons as they pertain to IaC. In the next step of this guide, we'll make sure you have all of these tools installed (and that you're signed up for an AWS account).\n\nThe guide will assume that you're comfortable using a terminal, and that you have access to a Unix-like environment, either by using a Linux/macOS workstation, or by using Windows Subsystem for Linux (WSL) on Windows. It will also assume that you're OK with not worrying about certain technical details like how the NodeJS application that you deploy as part of this guide works, as some of those technical details will be glossed over in the interest of focus on the technical details that are relevant in this guide.\n\nWhile not a requirement, it would be good to have a basic understanding of how Git works, so that you can commit updates to your copy of the project as you go along.\n\nIf you get lost or confused at any point, ask for help in the [Terragrunt Discord](/community/invite)! There are plenty of passionate Terragrunt community members that are more than happy to help.\n"
  },
  {
    "path": "docs/src/content/docs/02-guides/01-terralith-to-terragrunt/02-overview.md",
    "content": "---\ntitle: Overview\ndescription: Overview of the Terralith to Terragrunt guide\nslug: guides/terralith-to-terragrunt/overview\nsidebar:\n  order: 2\n---\n\nTo demonstrate the journey from a Terralith to a scalable Terragrunt setup, we will build and deploy a complete, real-world application early on in this guide, then spend the rest of the guide refactoring the IaC that manages the infrastructure hosting it.\n\nThe architecture for our sample project is a simple serverless web application hosted in AWS, which consists of three main components:\n\n1. A [Lambda](https://aws.amazon.com/lambda/)-backed website.\n2. An [S3 bucket](https://aws.amazon.com/s3/) to store static assets.\n3. A [DynamoDB table](https://aws.amazon.com/dynamodb/) to store metadata on those assets.\n\nThis application will allow users to view and vote on their favorite AI-generated images of cats. We've intentionally chosen these AWS serverless offerings as they are cost-effective and should be very cheap, if not free, for anyone following along with a new AWS account.\n\n## What You'll Need\n\nTo provision the application we build as part of this guide, you will need an AWS account, and permissions to provision resources within it. If you don't have one, you can follow the official [instructions to sign up](https://signin.aws.amazon.com/signup?request_type=register) for one for free.\n\nTo manage the development dependencies for this project, this guide uses [mise](https://mise.jdx.dev/), a tool that helps manage project-specific runtimes and tools. You are welcome to install the required tools manually, but using `mise` (or another tool manager) is recommended when working with IaC, as reproducibility is paramount for ensuring that you can work effectively with colleagues (including future you) on shared infrastructure.\n\nIf you are happy to install development dependencies with `mise`, you can install it using the official [Mise](https://mise.jdx.dev/getting-started.html) installation guide.\n\nIf you would like to manually install all the development dependencies for this guide, you can install them here:\n\n- [Terragrunt](https://docs.terragrunt.com/getting-started/install/)\n- [OpenTofu](https://opentofu.org/docs/intro/install/)\n- [NodeJS](https://nodejs.org/en/download)\n- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)\n\nWe will build this project from the ground up, but if you get lost at any point or want to skip ahead, you can find the completed project (as of each step) [here](https://github.com/gruntwork-io/terragrunt/tree/main/docs/src/fixtures/terralith-to-terragrunt).\n\nNote that the content shown in code fences in this project will always be displayed in totality, so you can either copy them directly into the filename that's labeled at the top of the code fence for a file, or run the command directly in your terminal for commands. If a command starts with a `$`, the intent of the code fence is to demonstrate expected output, so you aren't expected to copy and paste it directly into your terminal.\n"
  },
  {
    "path": "docs/src/content/docs/02-guides/01-terralith-to-terragrunt/03-setup.mdx",
    "content": "---\ntitle: Setup\ndescription: Setup of the Terralith to Terragrunt guide\nslug: guides/terralith-to-terragrunt/setup\nsidebar:\n  order: 3\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Code, Tabs, TabItem } from '@astrojs/starlight/components';\n\nDuring the setup phase, you're going to:\n\n- Setup a Git repository where this project will live.\n- Install all required dependencies (if you haven't already).\n- Set up the NodeJS application that you're going to deploy to the cloud.\n- Acquire the assets used for the application.\n\nLet's get building!\n\n## Create the Git repository\n\nCreate a new Git project where we're going to store the IaC for our live infrastructure.\n\n```bash\nmkdir terralith-to-terragrunt\ncd terralith-to-terragrunt\ngit init\n```\n\n## Install dependencies with `mise`\n\n(Assuming you are using it) Use `mise` to download, install and pin the version of tools you're going to use in this project.\n\n```bash\nmise use terragrunt@0.95.0\nmise use opentofu@1.11.1\nmise use aws@2.27.63\nmise use node@22.17.1\n```\n\nYou should now have a local `mise.toml` file that looks like the following with all the tools pinned that you need.\n\nimport miseToml from '../../../../fixtures/terralith-to-terragrunt/mise.toml?raw';\n\n<Code title=\"mise.toml\" lang=\"toml\" code={miseToml} />\n\n## Setting up the app\n\nNow that you have the tools installed that are going to be used for this project, you'll want to setup the application we're going to be managing throughout the project.\n\nIt's not a very interesting application (it was vibe coded pretty quickly), and the details of how it works aren't that important to the topic of this blog post. Corners were also cut when designing the application to minimize the resources you have to provision, so don't design any of your applications based on what you see there.\n\n### Create the application directory structure\n\nFirst, create the application directory structure:\n\n```bash\nmkdir -p app/best-cat\ncd app/best-cat\n```\n\nNext, copy the application files into the new directory you just created.\n\nimport packageJson from '../../../../fixtures/terralith-to-terragrunt/app/best-cat/package.json?raw';\nimport indexJs from '../../../../fixtures/terralith-to-terragrunt/app/best-cat/index.js?raw';\nimport templateHtml from '../../../../fixtures/terralith-to-terragrunt/app/best-cat/template.html?raw';\nimport stylesCss from '../../../../fixtures/terralith-to-terragrunt/app/best-cat/styles.css?raw';\nimport scriptJs from '../../../../fixtures/terralith-to-terragrunt/app/best-cat/script.js?raw';\nimport packageLockJson from '../../../../fixtures/terralith-to-terragrunt/app/best-cat/package-lock.json?raw';\n\n<Tabs syncKey=\"app-code\">\n    <TabItem label=\"package.json\">\n        <Code lang=\"json\" code={packageJson} />\n    </TabItem>\n    <TabItem label=\"index.js\">\n        <Code lang=\"javascript\" code={indexJs} />\n    </TabItem>\n    <TabItem label=\"template.html\">\n        <Code lang=\"html\" code={templateHtml} />\n    </TabItem>\n    <TabItem label=\"styles.css\">\n        <Code lang=\"css\" code={stylesCss} />\n    </TabItem>\n    <TabItem label=\"script.js\">\n        <Code lang=\"javascript\" code={scriptJs} />\n    </TabItem>\n    <TabItem label=\"package-lock.json\">\n        <Code lang=\"json\" code={packageLockJson} />\n    </TabItem>\n</Tabs>\n\nYou should end up with an `app/best-cat` directory that looks like this:\n\n<FileTree>\n- app\n  - best-cat\n    - index.js\n    - package-lock.json\n    - package.json\n    - script.js\n    - styles.css\n    - template.html\n</FileTree>\n\n## Packaging the app\n\nOnce you have the app stored in `app/best-cat`, you'll want to create the `dist` directory, then package the application for delivery to a lambda function.\n\n```bash\nmkdir dist\ncd app/best-cat\nnpm i\nnpm run package\n```\n\nI also recommend adding the following `.gitignore` file to your `dist` directory so you don't accidentally commit any other content in this directory to your repository:\n\n```bash\n# dist/.gitignore\n\n*\n!.gitignore\n```\n\n## Generating assets\n\nYou'll also want some assets to use in this project to make it more fun. I generated a bunch of cat pictures using [Gemini](https://gemini.google.com/), but feel free to use stock photos or something else to generate the assets.\n\nI would recommend that you place the images in the same location I did (`dist/static`), so that the convenience scripts I wrote work out of the box without modification.\n\nThis is what my `dist` directory looks like after following these steps:\n\n<FileTree>\n- dist\n  - best-cat.zip\n  - static\n    - 01-cat.png\n    - 02-cat.png\n    - 03-cat.png\n    - 04-cat.png\n    - 05-cat.png\n    - 06-cat.png\n    - 07-cat.png\n    - 08-cat.png\n    - 09-cat.png\n    - 10-cat.png\n</FileTree>\n\nOur end goal is to host a site that looks like this in AWS using these artifacts:\n\n![terralith-to-terragrunt-app-goal](../../../../assets/img/guides/terralith-to-terragrunt/app-goal.png)\n"
  },
  {
    "path": "docs/src/content/docs/02-guides/01-terralith-to-terragrunt/04-step-1-starting-the-terralith.mdx",
    "content": "---\ntitle: \"Step 1: Starting the Terralith\"\ndescription: Starting the Terralith\nslug: guides/terralith-to-terragrunt/step-1-starting-the-terralith\nsidebar:\n  order: 4\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Code, Aside } from '@astrojs/starlight/components';\n\nDuring this step we're going to take the contents of the `dist` directory, and deploy the application in AWS using OpenTofu. The content that we're going to create here is not intended to be a best-practices IaC setup, but rather what one might do if they were to naively put together the IaC for this application from scratch without knowledge of best practices, or planning for future modifications.\n\nThis will be reiterated at the end of this step, but note that this statement is not intended to be judgemental. There are perfectly valid reasons for building an MVP quickly without worrying about architecting the most optimally scaling IaC setup from day 1, and everyone has to start somewhere in their IaC journey.\n\nAs you progress through this guide, you'll be exposed to the challenges that might arise if you design your IaC as prescribed at each step, and this guide will do its best to highlight those trade-offs. The goal is for you to be able to make judgements around the IaC design that is best suited to your infrastructure management.\n\n## Tutorial\n\nTo start, create the `live` directory where you're going to be provisioning some infrastructure. Creating a dedicated `live` directory/repository separate from reusable infrastructure patterns is an established recommended best practice from Gruntwork that will become important in later iterations.\n\n```bash\nmkdir live\n```\n\nTo get started, we'll want to define our [providers](https://opentofu.org/docs/language/providers/), which is how OpenTofu will actually effectuate infrastructure in AWS.\n\nimport providersTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/providers.tf?raw';\n\n<Code title=\"live/providers.tf\" lang=\"hcl\" code={providersTf} />\n\nDon't worry about that `var.aws_region` bit there. We'll define it later. When we do, we'll be defining the region in which AWS will have resources provisioned.\n\nIt's best practice to tell OpenTofu what your version constraints are, so that your IaC is reliably reusable. You'll pin version `>= 1.10` of OpenTofu here, as you're going to be using an OpenTofu 1.10+ feature later on, and you'll pin version `~> 6.0` because any change that makes the `resource` definitions we define here break *should* be in a future major release of the `aws` provider.\n\nimport versionsTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/versions.tf?raw';\n\n<Code title=\"live/versions.tf\" lang=\"hcl\" code={versionsTf} />\n\nNow define the resources that are provisioned in this step.\n\nFirst, we'll add the database that we use to provision resources in this project. For this guide, we'll be using DynamoDB, which is a fast, NoSQL database offered by AWS. The primary construct in DynamoDB is a table. All DynamoDB tables have a name, and a primary key (also called the hash key). This is what we'll use to uniquely reference items in the table.\n\nIn this project, DynamoDB tables will be used to store the metadata about cats and the number of votes they've gotten as being the best cat.\n\nimport ddbTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/ddb.tf?raw';\n\n<Code title=\"live/ddb.tf\" lang=\"hcl\" code={ddbTf} />\n\nNext, we'll add our object store. For this guide, we'll be using S3, which is a cheap, scalable object store provided by AWS. The primary construct of S3 is a bucket. All buckets must have a name that is globally unique, so we'll want to make sure that the value we select later on for `name` is appropriately unique.\n\nimport s3Tf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/s3.tf?raw';\n\n<Code title=\"live/s3.tf\" lang=\"hcl\" code={s3Tf} />\n\nThat `force_destroy` attribute there is important. It determines whether we can destroy the S3 bucket without getting rid of all its contents. You typically want this to be set to `false` in production environments, but it can be convenient to set this to `true` in test/ephemeral environments, where you expect the bucket to be short-lived.\n\nIn addition to provisioning resources using OpenTofu, you can also lookup data using `data` configuration blocks. These can be useful ways to access frequently needed data in AWS resources, like AWS account IDs.\n\nimport dataTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/data.tf?raw';\n\n<Code title=\"live/data.tf\" lang=\"hcl\" code={dataTf} />\n\nUsing that `data` block, let's provision the IAM role that's used for our Lambda function. We'll want it to trust the Lambda service, so that the Lambda function is allowed to assume it, and have permissions to:\n\n1. Get and list the objects in S3 (our cat images).\n2. Interact with the DynamoDB table used for storing metadata on our assets (the votes for best cat).\n3. Basic permissions required to operate a Lambda function (the ability to log to [CloudWatch Logs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/WhatIsCloudWatchLogs.html)).\n\nimport iamTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/iam.tf?raw';\n\n<Code title=\"live/iam.tf\" lang=\"hcl\" code={iamTf} />\n\nThe final resource you're going to provision is the Lambda function. Lambda functions are a form of cheap, ephemeral compute that are especially useful for demo guides like this where you might forget to clean up some dummy resources. They won't cost you anything while they're not doing anything!\n\nimport lambdaTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/lambda.tf?raw';\n\n<Code title=\"live/lambda.tf\" lang=\"hcl\" code={lambdaTf} />\n\nNow let's add some variables that we want to specify for this project. You can think of these as the *inputs* that you supply to your generically defined IaC to get infrastructure customized to your needs. As a matter of best practice, we're going to separate the required variables from the optional ones.\n\nimport varsRequiredTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/vars-required.tf?raw';\n\n<Code title=\"live/vars-required.tf\" lang=\"hcl\" code={varsRequiredTf} />\n\nimport varsOptionalTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/vars-optional.tf?raw';\n\n<Code title=\"live/vars-optional.tf\" lang=\"hcl\" code={varsOptionalTf} />\n\nAlso add an `.auto.tfvars` file to define values for these variables automatically. This isn't strictly required as you'll be prompted for the values of required variables interactively if you don't supply them here, but it does make your life easier.\n\n<Aside type=\"note\">\nYou'll want to make sure that `name` is set to something globally unique, as it'll be used as part of an S3 bucket name, which might conflict with a bucket created by somebody else otherwise (a simple way to decrease your odds of a collision is to use something like the date in your bucket name).\n</Aside>\n\n```hcl\n# live/.auto.tfvars\n\n# Required: Name used for all resources (must be unique)\nname = \"best-cat-2025-09-24-2359\"\n\n# Required: Path to your Lambda function zip file\nlambda_zip_file = \"../dist/best-cat.zip\"\n```\n\nYou'll also want to add some *outputs* so that you can easily interact with the infrastructure you create.\n\nimport outputsTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/outputs.tf?raw';\n\n<Code title=\"live/outputs.tf\" lang=\"hcl\" code={outputsTf} />\n\nAs a best practice, you'll want to add a `backend` configuration so that state isn't stored locally. This is important if you want to collaborate with others in infrastructure management. Note that you'll likely want to change the name of the bucket you use here, as it also has to be globally unique to avoid conflicts with anyone else.\n\nimport backendTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/backend.tf?raw';\n\n<Code title=\"live/backend.tf\" lang=\"hcl\" code={backendTf} />\n\n<Aside type=\"note\">\nWe're using the special new ✨ lockfile-based state locking ✨ made available in OpenTofu 1.10! This is especially convenient for this guide, as it saves us from provisioning an additional DynamoDB table to handle state locking.\n</Aside>\n\nUnfortunately, OpenTofu will not provision this bucket for us automatically, so we will have to provision that manually.\n\n```bash\naws s3api create-bucket --bucket 'terragrunt-to-terralith-tfstate-2025-09-24-2359' --region 'us-east-1'\naws s3api put-bucket-versioning --bucket 'terragrunt-to-terralith-tfstate-2025-09-24-2359' --versioning-configuration 'Status=Enabled'\n```\n\n## Project Layout Check-in\n\nAt this stage, you should have a `live` directory that looks like this:\n\n<FileTree>\n- live\n  - backend.tf\n  - data.tf\n  - ddb.tf\n  - iam.tf\n  - lambda.tf\n  - outputs.tf\n  - providers.tf\n  - s3.tf\n  - vars-optional.tf\n  - vars-required.tf\n  - versions.tf\n</FileTree>\n\n\n## Applying Updates\n\nWe can now apply our live infrastructure!\n\n```bash\ncd live\ntofu init\ntofu apply\n```\n\nYou'll receive a prompt to approve the apply (type `yes` then enter to continue).\n\nMake sure to review the plan thoroughly, then approve it. Assuming everything went well, you'll see a bunch of outputs at the end of the apply. One of them will be an output that looks like the following:\n\n```text\nlambda_function_url = \"https://somerandomcharacters.lambda-url.us-east-1.on.aws/\"\n```\n\nCopy that link, and paste it into your browser to see a page like the following:\n\n![app-without-images](../../../../assets/img/guides/terralith-to-terragrunt/app-without-images.png)\n\nCongratulations! You've got live infrastructure you built yourself running in AWS!\n\nWe can see an error that the site doesn't have any images, and a prompt to upload some cat pictures to get started. To get those pictures uploaded, we'll want to grab the name of the S3 bucket, and use the AWS CLI to upload the assets.\n\n```bash\n# (Assuming you're using bash or zsh and you're in the `live` directory).\n\n# Grab the bucket name into the `bucket_name` variable.\nbucket_name=\"$(tofu output -raw s3_bucket_name)\"\n\n# Navigate to the root of the git repository.\ncd \"$(git rev-parse --show-toplevel)\"\n\n# Navigate to the directory where you stored your cat pictures.\ncd dist/static\n\n# Use the AWS CLI to sync the assets to the bucket.\naws s3 sync . \"s3://${bucket_name}/\"\n```\n\nIf you reload the website, you should be able to see the cat images you uploaded.\n\n![app-with-images](../../../../assets/img/guides/terralith-to-terragrunt/app-goal.png)\n\n## Wrap Up\n\nYou've successfully built and deployed a complete, serverless web application using OpenTofu!\n\nAll of your infrastructure including:\n\n- An S3 bucket\n- A DynamoDB table\n- An IAM role\n- A Lambda function\n\nAre defined and managed within a single root module. This configuration, could be called a \"Terralith\" or \"Megamodule,\" but it's probably not obvious that there's anything wrong with this setup. This a common and perfectly acceptable starting point for many projects. It's simple and direct, but as you continue to adjust and refactor this project, its monolithic nature will present challenges in reusability and safe environment management as you scale. In the next step, you'll begin to address these challenges by refactoring your code into reusable modules.\n"
  },
  {
    "path": "docs/src/content/docs/02-guides/01-terralith-to-terragrunt/05-step-2-refactoring.mdx",
    "content": "---\ntitle: \"Step 2: Refactoring\"\ndescription: Refactoring\nslug: guides/terralith-to-terragrunt/step-2-refactoring\nsidebar:\n  order: 5\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Code, Aside } from '@astrojs/starlight/components';\n\nOne of the most important skills you can learn when managing IaC is learning how to refactor IaC. The code you write directly relates to infrastructure that delivers value to your team and wider organization, so knowing how to safely reorganize your code so that it's easier to reuse and reason about without incurring risk for the infrastructure you support is invaluable.\n\nSome of the most common reasons you might engage in this kind of refactoring includes:\n\n- Rewriting bespoke IaC as consumption of a reusable module so that you can repurpose the common IaC in other environments/projects.\n- Standardizing IaC patterns for consistent application of security/cost best practices.\n- Abstracting away the implementation details of one or more resources as a module so that you can focus on the higher level abstraction of how that module integrates with the rest of your infrastructure.\n- Creating a generic module with a well defined API for a component in your infrastructure so that you can easily swap out the module with another module that shares a compatible (or close enough to compatible) API.\n\nIn this step, we'll start going down the road of making our infrastructure components modular so that we are well prepared for the next step, when we introduce a secondary environment as a replica of the environment we provisioned in the last step.\n\n## Tutorial\n\nThe Gruntwork recommended best practice for creating reusable IaC is to create a dedicated `catalog` directory (or a dedicated `catalog` repository) outside the `live` directory (or `live` repository) where reusable IaC patterns like OpenTofu/Terraform modules are stored.\n\nTo reorganize the resources that we've created so far into reusable modules, we'll create a directory called `catalog/modules` where we can store our modules for reusability. We'll create an OpenTofu module for each piece of high-level functionality that we are provisioning in our current environment (`s3`, `lambda`, `iam` and `ddb`).\n\n```bash\nmkdir -p catalog/modules/{s3, lambda, iam, ddb}\n```\n\nNow we can move over the files that were provisioning these independent resources into their own modules so we can establish APIs for them and start reusing some of this code. It's a pretty standard convention to name the core file used in a module `main.tf`. Good modules do one thing, and if you can't figure out what a module does by the name of the module, it's probably indicative that you're making an odd abstraction.\n\n```bash\nmv live/ddb.tf catalog/modules/ddb/main.tf\nmv live/iam.tf catalog/modules/iam/main.tf\nmv live/data.tf catalog/modules/iam/data.tf\nmv live/lambda.tf catalog/modules/lambda/main.tf\nmv live/s3.tf catalog/modules/s3/main.tf\n```\n\nThe contents of some of these files need a little massaging, however, as the IaC didn't have clear boundaries between the constituent components. Let's fix that by providing an API for each of these modules in the form of variables for inputs and outputs…. for outputs.\n\nimport ddbVarsRequiredTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/ddb/vars-required.tf?raw';\n\n<Code title=\"catalog/modules/ddb/vars-required.tf\" lang=\"hcl\" code={ddbVarsRequiredTf} />\n\nimport ddbOutputsTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/ddb/outputs.tf?raw';\n\n<Code title=\"catalog/modules/ddb/outputs.tf\" lang=\"hcl\" code={ddbOutputsTf} />\n\nimport s3VarsRequiredTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/s3/vars-required.tf?raw';\n\n<Code title=\"catalog/modules/s3/vars-required.tf\" lang=\"hcl\" code={s3VarsRequiredTf} />\n\nimport s3VarsOptionalTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/s3/vars-optional.tf?raw';\n\n<Code title=\"catalog/modules/s3/vars-optional.tf\" lang=\"hcl\" code={s3VarsOptionalTf} />\n\nimport s3OutputsTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/s3/outputs.tf?raw';\n\n<Code title=\"catalog/modules/s3/outputs.tf\" lang=\"hcl\" code={s3OutputsTf} />\n\nimport iamVarsRequiredTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/iam/vars-required.tf?raw';\n\n<Code title=\"catalog/modules/iam/vars-required.tf\" lang=\"hcl\" code={iamVarsRequiredTf} />\n\nimport iamOutputsTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/iam/outputs.tf?raw';\n\n<Code title=\"catalog/modules/iam/outputs.tf\" lang=\"hcl\" code={iamOutputsTf} />\n\nFor the `iam` module, we're also going to need to make adjustments to the `main.tf` file to account for previous tight coupling between resources. The updates here take advantage of those new `s3_bucket_arn` and `dynamodb_table_arn` variables for message passing between modules, exposed by the outputs of the `ddb` and `s3` modules.\n\nimport iamMainTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/iam/main.tf?raw';\n\n<Code title=\"catalog/modules/iam/main.tf\" lang=\"hcl\" code={iamMainTf} />\n\nimport lambdaVarsOptionalTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/lambda/vars-optional.tf?raw';\n\n<Code title=\"catalog/modules/lambda/vars-optional.tf\" lang=\"hcl\" code={lambdaVarsOptionalTf} />\n\nimport lambdaVarsRequiredTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/lambda/vars-required.tf?raw';\n\n<Code title=\"catalog/modules/lambda/vars-required.tf\" lang=\"hcl\" code={lambdaVarsRequiredTf} />\n\nimport lambdaOutputsTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/lambda/outputs.tf?raw';\n\n<Code title=\"catalog/modules/lambda/outputs.tf\" lang=\"hcl\" code={lambdaOutputsTf} />\n\nAgain, for the Lambda module we're going to need to make updates to the `main.tf` file to account for the tight coupling between resources now that we're wiring them together via variables and outputs.\n\nimport lambdaMainTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/lambda/main.tf?raw';\n\n<Code title=\"catalog/modules/lambda/main.tf\" lang=\"hcl\" code={lambdaMainTf} />\n\nLet's make sure that our modules have a copy of the `versions.tf` file that was in the root module (if you're not comfortable with using the `find` command below, you can just copy the `versions.tf` file into each of the modules you've created so far manually). It's a best practice to have reusable modules define their version constraints so that they can explicitly signal to module consumers when they use features in newer provider versions that might require a provider upgrade or are dodging a bug in a particular version of a provider that consumers should avoid.\n\n```bash\nfind catalog/modules -mindepth 1 -type d -exec cp live/versions.tf {}/versions.tf \\;\n```\n\nTo use these modules, we need to use OpenTofu `module` blocks to reference them in a new `main.tf` file placed in the `live` directory (the OpenTofu root module).\n\nWhat we're doing here is simply instantiating each of the modules that we've created so far by referencing them using a relative path to the module in the `source` attribute, setting values for their required inputs (some of which are acquired as outputs from other modules).\n\nimport liveMainTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/live/main.tf?raw';\n\n<Code title=\"live/main.tf\" lang=\"hcl\" code={liveMainTf} />\n\nWe also want to forward outputs from these modules into our root module so that we can access them from the `tofu` CLI.\n\nimport liveOutputsTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/live/outputs.tf?raw';\n\n<Code title=\"live/outputs.tf\" lang=\"hcl\" code={liveOutputsTf} />\n\nWe can also reduce the amount of content in the optional variables file, now that each of the modules define the variables that matter to them. This keeps the API of each module clean, as each module exposes the variables that specifically control them.\n\nimport liveVarsOptionalTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/live/vars-optional.tf?raw';\n\n<Code title=\"live/vars-optional.tf\" lang=\"hcl\" code={liveVarsOptionalTf} />\n\nAfter all this refactoring, we'll want to run a `plan` to make sure we can safely apply our changes.\n\n<Aside type=\"note\">\nYou'll need to re-initialize now that you're using modules here.\n</Aside>\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`$ tofu init\n$ tofu plan\n...\nPlan: 11 to add, 0 to change, 11 to destroy.\n...\n`} />\n\nOh no! After all our refactors, we've introduced changes that would *completely destroy* all of the infrastructure we've created so far!\n\nThis is a common scenario that you need to become comfortable with as you learn how to refactor and adjust IaC for scalability and maintainability. You leveraged the built-in protections of plans to give you a dry-run of your infrastructure updates, and can reason about why OpenTofu is trying to do what it's doing here to avoid catastrophe.\n\nWe, as authors of the IaC, know that all we've done in this step is move some files into different directories, but as far as OpenTofu is concerned, we've deleted resources at addresses like the following:\n\n```bash\n  # aws_lambda_function.main will be destroyed\n  # (because aws_lambda_function.main is not in configuration)\n```\n\nAnd introduced resources at addresses the like the following:\n\n```bash\n  # module.lambda.aws_lambda_function.main will be created\n```\n\nThe reason for this is that OpenTofu doesn't really have a way of knowing the difference between moving a file like that for the sake of reorganization and completely removing infrastructure in one place and adding it in another without some help from IaC authors.\n\nThe way we communicate to OpenTofu that a resource at one address has simply moved to a new address is to introduce `moved` blocks.\n\nFor each resource that we want to move, we'll want to introduce a `moved` block with a `from` of the old address (what OpenTofu reports as being destroyed in our plan) and a `to` of the equivalent new address (what OpenTofu reports as being created in our plan).\n\nimport liveMovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/live/moved.tf?raw';\n\n<Code title=\"live/moved.tf\" lang=\"hcl\" code={liveMovedTf} />\n\nIt's worth noting that we haven't been working with any infrastructure that's important to preserve in this demo so far. We can easily reproduce this infrastructure without much effort. It's important to know how to perform refactors without having to recreate infrastructure, though, as we need to be able to avoid paying the penalty of outages or data loss — especially when working with production infrastructure.\n\nIf, for example, the database or s3 bucket being managed here had real customer information, it would be *extremely* important to avoid recreating these resources. OpenTofu doesn't always know that recreating a stateful resource can cause permanent data loss. If you want the benefits we mentioned earlier of refactored IaC, you'll want to know how to carefully handle state manipulation in OpenTofu and understand what it's trying to do.\n\nSo, as a small tangent, let's discuss what actually happened when we introduced these `moved` blocks. There are multiple ways to configure OpenTofu backend state configurations, but the way that we've configured it here is to have the state files stored in S3 as JSON files. What we did under the hood with our `moved` blocks was update the content of that JSON file in `s3://[your-state-bucket]/tofu.tfstate` so that each of the `resources` in your state file used updated values for their resource addresses.\n\nIn the example of this move:\n\n```hcl\nmoved {\n  from = aws_dynamodb_table.asset_metadata\n  to   = module.ddb.aws_dynamodb_table.asset_metadata\n}\n```\n\nWe updated one of the JSON objects in the state file from one that had these values:\n\n```json\n      # Some stuff\n      \"mode\": \"managed\",\n      \"type\": \"aws_dynamodb_table\",\n      \"name\": \"asset_metadata\",\n      \"provider\": \"provider[\\\"registry.opentofu.org/hashicorp/aws\\\"]\",\n      # More stuff\n```\n\nTo one that had these values:\n\n```json\n      # Some stuff\n      \"module\": \"module.ddb\",\n      \"mode\": \"managed\",\n      \"type\": \"aws_dynamodb_table\",\n      \"name\": \"asset_metadata\",\n      \"provider\": \"provider[\\\"registry.opentofu.org/hashicorp/aws\\\"]\",\n      # More stuff\n```\n\nWhen OpenTofu wants to know the current state of `aws_dynamodb_table.asset_metadata`, it can look it up using the first value, and when it wants to lookup the state of `module.ddb.aws_dynamodb_table.asset_metadata` it uses the second value.\n\nBy moving the value in state, we're just telling OpenTofu that we're calling the resource by a different name now, without actually changing anything in AWS.\n\n## Project Layout Check-in\n\nYou should have a filesystem layout that look like the following for your IaC now:\n\n<FileTree>\n- catalog\n  - modules\n    - ddb\n      - main.tf\n      - outputs.tf\n      - vars-required.tf\n      - versions.tf\n    - iam\n      - data.tf\n      - main.tf\n      - outputs.tf\n      - vars-required.tf\n      - versions.tf\n    - lambda\n      - main.tf\n      - outputs.tf\n      - vars-optional.tf\n      - vars-required.tf\n      - versions.tf\n    - s3\n      - main.tf\n      - outputs.tf\n      - vars-optional.tf\n      - vars-required.tf\n      - versions.tf\n- live\n  - backend.tf\n  - main.tf\n  - moved.tf\n  - outputs.tf\n  - providers.tf\n  - vars-optional.tf\n  - vars-required.tf\n  - versions.tf\n</FileTree>\n\n\n## Applying Updates\n\nYou can now run `tofu apply` with no changes (don't worry, you'll get a chance to confirm you want to proceed before you have to commit to anything).\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`$ tofu apply\n...\nPlan: 0 to add, 0 to change, 0 to destroy.\n...\n\nDo you want to perform these actions?\n  OpenTofu will perform the actions described above.\n  Only 'yes' will be accepted to approve.\n\n  Enter a value:\n`} />\n\n## Trade-Offs\n\nBefore moving on to the next step, where we'll duplicate our entire infrastructure estate to introduce a new development environment, it's important to pause here and evaluate the trade-offs of this refactor.\n\nBoth the infrastructure in step 1 and step 2 provisioned the exact same infrastructure (remember that there were `0 to add, 0 to change, 0 to destroy.`). In fact, with the exception of the next step where we introduce the new `dev` environment, every step will result in the exact same infrastructure being provisioned.\n\nWhy then is this refactor valuable? What do we gain by refactoring our IaC like this? What do we trade away in exchange?\n\n### Pros\n\n- **Abstraction by encapsulation**. Instead of one large set of variables that could be used by any resource, or one large set of resources that could interact in ways that are difficult to understand, there are modules that encapsulate subsets of infrastructure so that they have explicit interfaces via variables and outputs.\n- **More code reusability**. Each of these modules can be reused in `live` infrastructure or in other `catalog` modules (which we'll see in the next step).\n\n### Cons\n\n- **Increased complexity**. Instead of one self-contained directory with files directly defining resources to be provisioned, there's a layer of indirection via modules. As someone consuming the module, you have to either trust it has been authored well (and that it's well documented, tested, etc.) or vet the module yourself.\n- **State Adjustment**. State manipulation or resource recreation is required to migrate to this pattern.\n\nEvery subsequent stage is going to continue incurring trade-offs. You (or someone experienced you trust) must to decide whether these trade-offs are appropriate for your organization and your infrastructure estate.\n\n## Wrap Up\n\nThis was a significant refactoring step. You've transformed your flat configuration into a set of distinct, reusable modules, each with a well-defined API of variables and outputs.\n\nThe most critical lesson here was mastering the `moved` block. This powerful feature allowed you to completely reorganize your code's structure without OpenTofu needing to destroy and recreate your existing infrastructure, a vital skill for managing real-world infrastructure. While this adds a layer of indirection, the trade-off is greater code reusability and clearer separation of concerns. With this new modular structure, you're now perfectly positioned to create a second environment with ease.\n"
  },
  {
    "path": "docs/src/content/docs/02-guides/01-terralith-to-terragrunt/06-step-3-adding-dev.mdx",
    "content": "---\ntitle: \"Step 3: Adding dev\"\ndescription: Adding dev\nslug: guides/terralith-to-terragrunt/step-3-adding-dev\nsidebar:\n  order: 6\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Code, Aside } from '@astrojs/starlight/components';\n\nIn the last step, you engaged in the foundational work of refactoring your monolithic configuration into a set of reusable modules, still instantiated in a single root module. Now it's time to leverage those newly develop skills to create new infrastructure.\n\nOne of the main advantages gained in creating infrastructure using IaC is improved reproducibility. The naive approach to creating new infrastructure is to directly copy and paste IaC to duplicate it, but there's frequently advantage in packaging the infrastructure you're going to replicate as a new pattern in your `catalog` so that you have a single source of truth for your shared IaC patterns.\n\nIn this step, you'll take the infrastructure you've created so far, do one more refactor to encapsulate it as a single reusable module, then instantiate it a second time as a second `dev` environment.\n\n## Tutorial\n\nLet's introduce that new higher level module as a new module named `best_cat`. It will provision the `s3`, `ddb`, `lambda` and `iam` modules we added in the last step, and wire them together. This will give us a single entity that we can duplicate across environments.\n\n<Aside type=\"note\">\nWe're basically taking all the stuff in `live` and shoving it into this new `best_cat` module.\n</Aside>\n\nimport bestCatMainTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/best_cat/main.tf?raw';\n\n<Code title=\"catalog/modules/best_cat/main.tf\" lang=\"hcl\" code={bestCatMainTf} />\n\nimport bestCatOutputsTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/best_cat/outputs.tf?raw';\n\n<Code title=\"catalog/modules/best_cat/outputs.tf\" lang=\"hcl\" code={bestCatOutputsTf} />\n\nimport bestCatVarsOptionalTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/best_cat/vars-optional.tf?raw';\n\n<Code title=\"catalog/modules/best_cat/vars-optional.tf\" lang=\"hcl\" code={bestCatVarsOptionalTf} />\n\nimport bestCatVarsRequiredTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/best_cat/vars-required.tf?raw';\n\n<Code title=\"catalog/modules/best_cat/vars-required.tf\" lang=\"hcl\" code={bestCatVarsRequiredTf} />\n\nSimilar to what we did before with the constituent modules, we can simply replace the content in `live` with a reference to our new `best_cat` module.\n\nimport prodMainTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/live/main.tf?raw';\n\n<Code title=\"live/main.tf\" lang=\"hcl\" code={prodMainTf} />\n\nOnce again, we get the scary `tofu plan` that tells us we would recreate all our infrastructure if we were to naively apply here:\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`$ tofu plan\n...\nPlan: 11 to add, 0 to change, 11 to destroy.\n...\n`} />\n\n\nLuckily, we already know how to handle this. We're going to update our `moved.tf` file to declare all the moves that need to be performed to transition the old addresses of resources to their new addresses.\n\nimport movedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/live/moved.tf?raw';\n\n<Code title=\"live/moved.tf\" lang=\"hcl\" code={movedTf} />\n\nOur apply now successfully completes without doing anything!\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`$ tofu apply\n...\nApply complete! Resources: 0 added, 0 changed, 0 destroyed.\n...\n`} />\n\nNow the stage is set to add the additional `dev` environment. We can do that by duplicating the `prod` module, and labeling the new `module` block `dev` (you'll also want to add a little suffix to the end of the `name` input to avoid naming collisions).\n\nimport mainTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/live/main.tf?raw';\n\n<Code title=\"live/main.tf\" lang=\"hcl\" code={mainTf} />\n\nWe also need to expose some of the outputs of the new `dev` module, but if we just duplicated all the `prod` outputs, we'd end up with a massive wall of outputs that would be hard to parse. Luckily, we only need two outputs to be externally accessible per environment, so we can drop a bunch of outputs to streamline things.\n\nimport outputsTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/live/outputs.tf?raw';\n\n<Code title=\"live/outputs.tf\" lang=\"hcl\" code={outputsTf} />\n\n## Project Layout Check-in\n\nWe should have a project layout that looks like this now:\n\n<FileTree>\n- catalog\n  - modules\n    - **best_cat**\n      - **main.tf**\n      - **outputs.tf**\n      - **vars-optional.tf**\n      - **vars-required.tf**\n    - ddb\n      - main.tf\n      - outputs.tf\n      - vars-required.tf\n      - versions.tf\n    - iam\n      - data.tf\n      - main.tf\n      - outputs.tf\n      - vars-required.tf\n      - versions.tf\n    - lambda\n      - main.tf\n      - outputs.tf\n      - vars-optional.tf\n      - vars-required.tf\n      - versions.tf\n    - s3\n      - main.tf\n      - outputs.tf\n      - vars-optional.tf\n      - vars-required.tf\n      - versions.tf\n- live\n  - backend.tf\n  - main.tf\n  - moved.tf\n  - outputs.tf\n  - providers.tf\n  - vars-optional.tf\n  - vars-required.tf\n  - versions.tf\n</FileTree>\n\n\n## Applying Updates\n\nNow we can deploy our changes.\n\n<Aside type=\"note\">\nWe need to re-initialize here as we've added a new module.\n</Aside>\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`tofu init\ntofu apply\n`} />\n\nWe now have our new, fresh dev environment!\n\n![fresh-dev-environment](../../../../assets/img/guides/terralith-to-terragrunt/app-without-images.png)\n\n<Aside type=\"note\">\nThis environment is *very* fresh. We don't have the static assets uploaded, and we're working with a fresh database. This guide isn't going to into the ways in which Terragrunt could integrate into your build system to do this for you automatically, but know that it *is* part of Terragrunt's feature set to handle this kind of thing. There are hints at the end of this guide to point those capabilities out and encourage your own exploration.\n</Aside>\n\n## Trade-offs\n\n### Pros\n\nWe have officially reached the stage where we're hitting **risk increase** due to our ***Terralith***! This is the configuration of IaC that a lot of infrastructure estates grow to naturally as they tack on more resources and add environments. It's a tipping point in maintainability that is best caught early, and addressed.\n\nWe gained the ability to easily provision new infrastructure via reusable modules and could simply copy and paste (then season to taste) some configuration in our `live/main.tf` file. We also had a single source of truth for representing all the infrastructure that we were provisioning, in both the reusable module, and our `live` OpenTofu root module.\n\nWe traded that for additional risk incurred, as every `apply` or `destroy` now has the potential to modify or destroy multiple environments, and you have to carefully avoid misconfiguration by reading plans (and trusting that they're accurate) to avoid accidentally damaging the wrong environment. Furthermore, you also have to be very careful that you only modify the resources that you intend to modify *within* a given environment when you make updates to it (are you accidentally destroying your database when attempting a tagging update for your Lambda function?). The reason for this is that all your resources are in the same state file. OpenTofu has to make one atomic change to that single state file with every update, so all the resources in state are at risk when any change is made.\n\nFor your information, there are tools out there, like [OPA](https://www.openpolicyagent.org/) that enable automated reasoning about plan risk, but those tools are typically adopted by more advanced infrastructure teams, and there is typically a significant amount of overhead in authoring and maintaining the policies that assess plan risk (and driving behavior off those assessments). There are hints at the end of this guide to point those capabilities out and encourage your own exploration on this topic.\n\nGenerally, the approach that teams take to structurally reduce this risk is to start to ***break down the Terralith*** into separate root modules, each with their own state. This gives teams confidence that they ***can only*** modify `dev` when they set their current working directory to the `dev` root module, and `prod` when their current working directory is the `prod` root module. When thinking through access control, this can also be convenient, as you can segment the access control that you use for one root module from another. Teams frequently configure their setups so that they need to explicitly use different credentials via role assumption, etc. when running commands in root modules related to different environments (e.g. `dev` vs `prod` ) to avoid accidental updates in the wrong environment.\n\n### Cons\n\nThe downside to that approach, as we'll see in the next step, is that it *does* increase the management burden of orchestrating and maintaining your IaC, and additional tooling like Terragrunt is a good way to handle that additional orchestration burden.\n\n## Wrap Up\n\nYou've successfully spun up a second, isolated development environment by reusing your new `best_cat` module. However, this is also the point where the *Terralith* design pattern starts to incur some serious drawbacks. At this stage, all your infrastructure for both your environments (`dev` and `prod`) now lives in a single state file. This introduces significant risk. A small mistake intended for `dev` could potentially damage or destroy your `prod` environment because OpenTofu sees it all as one atomic unit to manage, and you're responsible for reasoning about the generated plan to see if you should proceed with an apply. The next step is the most critical step in maturing your IaC estate (as far as this guide is concerned) as you break this monolith apart to limit the blast radius of your updates.\n"
  },
  {
    "path": "docs/src/content/docs/02-guides/01-terralith-to-terragrunt/07-step-4-breaking-the-terralith.mdx",
    "content": "---\ntitle: \"Step 4: Breaking the Terralith\"\ndescription: Breaking the Terralith\nslug: guides/terralith-to-terragrunt/step-4-breaking-the-terralith\nsidebar:\n  order: 7\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Code, Aside } from '@astrojs/starlight/components';\n\nIn the previous step, you successfully duplicated your `prod` environment as a new `dev` environment by replicating your `prod` module declaration as a new `dev` module instance in your `live/main.tf` file. While this demonstrated the power of reusable modules, it also introduced significant risk: the ***Terralith***. You've tightly coupled management of both your `dev` and `prod` environments in a single state file, and you introduce risk to one whenever you attempt to make changes to another.\n\nIn this step, you'll solve this problem by breaking apart your Terralith apart. You'll refactor your `live` root module into two distinct `dev` and `prod` root modules. Each will have its own state file, completely eliminating the risk of accidental cross-environment changes.\n\n## Tutorial\n\nBreaking down your Terralith so that you have multiple root modules is fairly simple now that you understand state manipulation a bit better.\n\nFirst, let's create a top-level directory for `prod` in `live`.\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`mkdir prod`} />\n\nNext, let's move everything into the `prod` directory (If you're not comfortable with using the `find` command here, you can just drag the content into the `prod` directory).\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`find . -mindepth 1 -maxdepth 1 -not -name 'prod' -exec mv {} prod/ \\;`} />\n\nTo complete our new multi-environment setup, let's duplicate that `prod` directory to a new `dev` directory.\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`cp -R prod dev`} />\n\nWe need to edit the contents of the `dev` and `prod` directories to make some key adjustments. First, we'll want to make sure that the `backend.tf` files are updated to use new keys so that the two root modules don't conflict with each other.\n\nimport devBackendTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/dev/backend.tf?raw';\n\n<Code title=\"live/dev/backend.tf\" lang=\"hcl\" code={devBackendTf} />\n\nimport prodBackendTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/prod/backend.tf?raw';\n\n<Code title=\"live/prod/backend.tf\" lang=\"hcl\" code={prodBackendTf} />\n\nWe'll also want to update the references to the shared module, update the `.auto.tfvars` file and edit the outputs to handle all the changes necessary for this project.\n\nimport devMainTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/dev/main.tf?raw';\n\n<Code title=\"live/dev/main.tf\" lang=\"hcl\" code={devMainTf} />\n\nimport prodMainTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/prod/main.tf?raw';\n\n<Code title=\"live/prod/main.tf\" lang=\"hcl\" code={prodMainTf} />\n\n<Aside type=\"note\">\nThese two files are now *exactly the same*. This will be important to keep in mind later.\n</Aside>\n\nGiven that we've renamed the the module, we'll also need to add `moved` blocks to handle the state moves that need to take place here. If you're not sure what we're doing here, consider reviewing earlier steps.\n\nimport devMovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/dev/moved.tf?raw';\n\n<Code title=\"live/dev/moved.tf\" lang=\"hcl\" code={devMovedTf} />\n\nimport prodMovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/prod/moved.tf?raw';\n\n<Code title=\"live/prod/moved.tf\" lang=\"hcl\" code={prodMovedTf} />\n\nNext, we'll update the outputs, just like we did for the `main.tf` files.\n\nimport devOutputsTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/dev/outputs.tf?raw';\n\n<Code title=\"live/dev/outputs.tf\" lang=\"hcl\" code={devOutputsTf} />\n\nimport prodOutputsTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/prod/outputs.tf?raw';\n\n<Code title=\"live/prod/outputs.tf\" lang=\"hcl\" code={prodOutputsTf} />\n\n<Aside type=\"note\">\nThese two files are *also exactly the same*. This will be important to keep in mind later.\n</Aside>\n\nFinally, we need to update the `.auto.tfvars` files to reflect the difference in inputs passed to variables in these two root modules.\n\n```hcl\n# live/prod/.auto.tfvars\n\n# Required: Name used for all resources (must be unique)\nname = \"best-cat-2025-09-24-2359\"\n\n# Required: Path to your Lambda function zip file\nlambda_zip_file = \"../../dist/best-cat.zip\"\n```\n\n```hcl\n# live/dev/.auto.tfvars\n\n# Required: Name used for all resources (must be unique)\nname = \"best-cat-2025-09-24-2359-dev\"\n\n# Required: Path to your Lambda function zip file\nlambda_zip_file = \"../../dist/best-cat.zip\"\n```\n<Aside type=\"note\">\nThese two files differ in that they have different `name` values. They're not identical, but they're very similar.\n</Aside>\n\nIt's time for some more state manipulation! We currently have a single state file in S3 at `s3://[your-state-bucket]/tofu.tfstate`. Our plan for splitting the state is to basically duplicate state for both the `dev` and `prod` root modules, then remove resources that we don't need from state in each of the root modules.\n\nIn addition to having the state in S3, we also have a local copy of state in each root module. Running the `tofu init -migrate-state` command with the `.terraform` directory populated by copy of state from the previous configuration of the project will copy state to the new location in each new root module.\n\n<Code title=\"live/dev\" lang=\"bash\" frame=\"terminal\" code={`$ tofu init -migrate-state\n\nInitializing the backend...\nBackend configuration changed!\n\nOpenTofu has detected that the configuration specified for the backend\nhas changed. OpenTofu will now check for existing state in the backends.\n\nDo you want to copy existing state to the new backend?\n  Pre-existing state was found while migrating the previous \"s3\" backend to the\n  newly configured \"s3\" backend. No existing state was found in the newly\n  configured \"s3\" backend. Do you want to copy this state to the new \"s3\"\n  backend? Enter \"yes\" to copy and \"no\" to start with an empty state.\n\n  Enter a value: yes\n\nSuccessfully configured the backend \"s3\"! OpenTofu will automatically\nuse this backend unless the backend configuration changes.\n`} />\n\n<Code title=\"live/prod\" lang=\"bash\" frame=\"terminal\" code={`$ tofu init -migrate-state\n\nInitializing the backend...\nBackend configuration changed!\n\nOpenTofu has detected that the configuration specified for the backend\nhas changed. OpenTofu will now check for existing state in the backends.\n\nDo you want to copy existing state to the new backend?\n  Pre-existing state was found while migrating the previous \"s3\" backend to the\n  newly configured \"s3\" backend. No existing state was found in the newly\n  configured \"s3\" backend. Do you want to copy this state to the new \"s3\"\n  backend? Enter \"yes\" to copy and \"no\" to start with an empty state.\n\n  Enter a value: yes\n\nSuccessfully configured the backend \"s3\"! OpenTofu will automatically\nuse this backend unless the backend configuration changes.\n`} />\n\nWe now have the state in `s3://[your-state-bucket]/tofu.tfstate` copied to both:\n\n- `s3://[your-state-bucket]/dev/tofu.tfstate`\n- `s3://[your-state-bucket]/prod/tofu.tfstate`\n\nWe need to remove the resources from state that aren't relevant in the new root modules, now so that we don't deploy `prod` resources in the `dev` root module and vice versa.\n\nimport devRemovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/dev/removed.tf?raw';\n\n<Code title=\"live/dev/removed.tf\" lang=\"hcl\" code={devRemovedTf} />\n\nimport prodRemovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/prod/removed.tf?raw';\n\n<Code title=\"live/prod/removed.tf\" lang=\"hcl\" code={prodRemovedTf} />\n\n## Project Layout Check-in\n\nAt this stage, we should have a `live` directory that looks like the following (the `catalog` directory shouldn't have changed at all):\n\n<FileTree>\n- live\n  - dev\n    - backend.tf\n    - main.tf\n    - moved.tf\n    - outputs.tf\n    - providers.tf\n    - removed.tf\n    - vars-optional.tf\n    - vars-required.tf\n    - versions.tf\n  - prod\n    - backend.tf\n    - main.tf\n    - moved.tf\n    - outputs.tf\n    - providers.tf\n    - removed.tf\n    - vars-optional.tf\n    - vars-required.tf\n    - versions.tf\n</FileTree>\n\n## Applying Updates\n\nWe should now see that we're simply going to forget the removed resources instead of destroying them.\n\n<Code title=\"live/dev\" lang=\"bash\" frame=\"terminal\" code={`$ tofu plan\n...\nPlan: 0 to add, 1 to change, 0 to destroy, 11 to forget.\n...\n`} />\n\nLet's apply both `dev` and `prod` to finalize the moves and removals.\n\n<Code title=\"live/dev\" lang=\"bash\" frame=\"terminal\" code={`$ tofu apply\n...\nApply complete! Resources: 0 added, 1 changed, 0 destroyed, 11 forgotten.\n...\n`} />\n\n<Code title=\"live/prod\" lang=\"bash\" frame=\"terminal\" code={`$ tofu apply\n...\nApply complete! Resources: 0 added, 1 changed, 0 destroyed, 11 forgotten.\n...\n`} />\n\n## Trade-offs\n\n### Pros\n\nWe did it! We successfully broke apart our Terralith using OpenTofu alone. Some organizations get to this stage in their IaC journey, and are perfectly happy with managing their infrastructure like this.\n\nYou can limit the blast radius of your `dev` and `prod` environments this way, and it's fairly straightforward to adjust your current working directory to the `dev` root module when making modifications to the `dev` environment, and adjusting your working directory to the `prod` root module when making modifications to the  `prod` environment. This is actually the pattern that Gruntwork was initially helping customers achieve early on to make their infrastructure safer, and more manageable by teams.\n\n### Cons\n\nThere are, however, some downsides to how we're managing infrastructure here.\n\n1. There's some annoying boilerplate that's inconvenient to create and maintain. The following files are identical in each environment, but need to be present just to get OpenTofu to provision the same module:\n    1. `main.tf`\n    2. `outputs.tf`\n    3. `providers.tf`\n    4. `vars-optional.tf`\n    5. `vars-required.tf`\n2. We also have *almost* the same file in each of these, and their values aren't really that interesting.\n    1. `backend.tf`\n    2. `.auto.tfvars`\n3. We also don't have a convenient way to run multiple root modules at once. What if we want to update both `dev` *and* `prod` at once? What if we want to break down the environments further?\n- As you might have guessed, the next step is to introduce Terragrunt to address some of these downsides, and unlock even more capabilities for managing infrastructure at scale.\n\n## Wrap Up\n\nThis is a pivotal moment in this guide. You have successfully started to break down the Terralith!\n\nBy migrating your state and refactoring your configuration, you have split your single, high-risk state file into two separate ones: one for `dev` and one for `prod`. The primary benefit is safety. You've drastically reduced the blast radius, as running `tofu apply` in the `dev` directory can now *only* affect development resources and running `tofu apply` in the `prod` directory can *only* affect production resources. However, this safety has come at the cost of duplication. Your `dev` and `prod` directories contain a lot of identical, boilerplate `.tf` files, and it isn't very scalable. What if you have twice as many environments? What if you have ten times as many? How are you going to handle making all those updates?\n\nHelping customers solve these problems and more at scale is what Terragrunt was designed for, which we'll introduce next to streamline your workflow.\n"
  },
  {
    "path": "docs/src/content/docs/02-guides/01-terralith-to-terragrunt/08-step-5-adding-terragrunt.mdx",
    "content": "---\ntitle: \"Step 5: Adding Terragrunt\"\ndescription: Adding Terragrunt\nslug: guides/terralith-to-terragrunt/step-5-adding-terragrunt\nsidebar:\n  order: 8\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Aside, Code } from '@astrojs/starlight/components';\n\nIn the last step, you took a massive leap forward in safety by breaking your Terralith into separate `dev` and `prod` root modules. The trade-off, however, is that you've created a significant amount of boilerplate and duplication. Your `dev` and `prod` directories are filled with nearly identical `.tf` files (if not completely identical), and managing them involves a lot of careful copy-pasting. You also can't conveniently manage multiple root modules at once. This isn't scalable and is prone to error.\n\nThis is the problem Terragrunt was created to solve. It acts as an orchestrator for OpenTofu/Terraform, helping you write DRY (Don't Repeat Yourself) infrastructure code that scales.\n\nIn this step, you'll introduce Terragrunt to drastically reduce that boilerplate. You will:\n\n- Replace the duplicated `.tf` and `.auto.tfvars` files in each environment with a single, concise `terragrunt.hcl` file.\n- Use Terragrunt's `terraform`, `inputs`, and `generate` blocks to define the module source, pass variables, and create configuration files on the fly.\n- Centralize common configurations (like your S3 `backend` configuration) in a single `root.hcl` file using the `include` block, ensuring your setup is easy to maintain.\n\nBy the end of this step, your `live` directory will be dramatically leaner, paving the way for easier management and scaling.\n\n## Tutorial\n\nNow that we've structured our project to segment environments into their own root modules (and their own state files), it's pretty simple to convert our root modules to Terragrunt units. In Terragrunt terminology, a [unit](https://docs.terragrunt.com/getting-started/terminology/#unit) is a single instance of infrastructure managed by Terragrunt. They're easy to manage, and they come with a lot of tooling to support common IaC needs, like code generation, authentication, error handling, and more.\n\nThe process of converting an OpenTofu root module to a Terragrunt unit simply involves adding an empty `terragrunt.hcl` file to each root module (that's all the `find` command below does). This allows Terragrunt to recognize the contents of the directory as a Terragrunt unit, and orchestrate infrastructure updates within it.\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`find . -mindepth 1 -maxdepth 1 -type dir -exec touch {}/terragrunt.hcl \\;`} />\n\nNow, we can use Terragrunt to orchestrate runs across both of these units.\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`$ terragrunt run --all plan\n15:07:02.593 INFO   The runner at . will be processed in the following order for command plan:\nGroup 1\n- Unit ./dev\n- Unit ./prod\n...\n`} />\n\nWe can also selectively run the plan for the `dev` environment by changing the working directory to `dev`, or using the [`--queue-include-dir`](https://docs.terragrunt.com/reference/cli-options/#queue-include-dir) flag.\n\n<Code title=\"live/dev\" lang=\"bash\" frame=\"terminal\" code={`$ terragrunt plan`} />\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`$ terragrunt run --all --queue-include-dir dev plan\n15:09:17.090 INFO   The runner at . will be processed in the following order for command plan:\nGroup 1\n- Unit ./dev\n\n...\n`} />\n\nTerragrunt is frequently adopted gradually in this manner. If you have an infrastructure problem you want addressed, you can gradually introduce more and more Terragrunt tooling to address those problems.\n\nWe can also simplify things significantly now that we're using Terragrunt. Terragrunt is designed to work well in this pattern where the majority of logic is abstracted away to a shared module. We can eliminate the need for some boilerplate now that we have access to the `terraform` block in `terragrunt.hcl` files (It's named `terraform` for legacy reasons. It's 100% compatible with OpenTofu).\n\n```hcl\n# live/dev/terragrunt.hcl\n\nterraform {\n    source = \"../../catalog/modules//best_cat\"\n}\n```\n\n```hcl\n# live/prod/terragrunt.hcl\n\nterraform {\n    source = \"../../catalog/modules//best_cat\"\n}\n```\n\n<Aside type=\"note\">\nThose `//` are there on purpose. They're how `go-getter`, the library that Terragrunt uses (just like OpenTofu), indicates that it's working with a directory *within* a module source. This allows relative references like `../s3` to work within the `best_cat` module.\n</Aside>\n\nWith those changes, we can now remove the unnecessary boilerplate related to invoking the shared module.\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`rm -f ./*/main.tf ./*/outputs.tf ./*/vars-*.tf ./*/versions.tf`} />\n\nWe can also leverage the `inputs` attribute in the `terragrunt.hcl` file to set inputs instead of relying on the separate `.auto.tfvars` file.\n\n```hcl\n# live/dev/terragrunt.hcl\n\nterraform {\n    source = \"../../catalog/modules//best_cat\"\n}\n\ninputs = {\n    name = \"best-cat-2025-09-24-2359-dev\"\n\n    lambda_zip_file = \"${get_repo_root()}/dist/best-cat.zip\"\n}\n```\n\n```hcl\n# live/prod/terragrunt.hcl\n\nterraform {\n    source = \"../../catalog/modules//best_cat\"\n}\n\ninputs = {\n    name = \"best-cat-2025-09-24-2359\"\n\n    lambda_zip_file = \"${get_repo_root()}/dist/best-cat.zip\"\n}\n```\n\nNote the use of `get_repo_root()`. This is a simple convenience function you can use to get the path to the root of your Git repository.\n\nYou can use almost all of the same HCL functions you can use in OpenTofu, with some additional functions supplied by Terragrunt for tasks that are more useful in the context of Terragrunt (you can see the full list in the official Terragrunt [HCL functions](https://docs.terragrunt.com/reference/built-in-functions/) reference here).\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`rm -f ./*/.auto.tfvars ./*/.auto.tfvars.example`} />\n\nWe can also get Terragrunt to generate that `backend.tf` file for us on-demand using the `remote_state` block.\n\n```bash\n# live/dev/terragrunt.hcl\n\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    bucket         = \"terragrunt-to-terralith-tfstate-2025-09-24-2359\"\n    key            = \"dev/tofu.tfstate\"\n    region         = \"us-east-1\"\n    encrypt        = true\n    use_lockfile   = true\n  }\n}\n\nterraform {\n    source = \"../../catalog/modules//best_cat\"\n}\n\ninputs = {\n    name = \"best-cat-2025-09-24-2359-dev\"\n\n    lambda_zip_file = \"${get_repo_root()}/dist/best-cat.zip\"\n}\n```\n\n```bash\n# live/prod/terragrunt.hcl\n\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    bucket         = \"terragrunt-to-terralith-tfstate-2025-09-24-2359\"\n    key            = \"prod/tofu.tfstate\"\n    region         = \"us-east-1\"\n    encrypt        = true\n    use_lockfile   = true\n  }\n}\n\nterraform {\n    source = \"../../catalog/modules//best_cat\"\n}\n\ninputs = {\n    name = \"best-cat-2025-09-24-2359\"\n\n    lambda_zip_file = \"${get_repo_root()}/dist/best-cat.zip\"\n}\n```\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`rm -f ./*/backend.tf`} />\n\nIn fact, we can have Terragrunt generate any arbitrary file we need on-demand, including boilerplate files like we had in the `providers.tf` file.\n\n```bash\n# live/dev/terragrunt.hcl\n\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    bucket       = \"terragrunt-to-terralith-tfstate-2025-09-24-2359\"\n    key          = \"dev/tofu.tfstate\"\n    region       = \"us-east-1\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n\ngenerate \"providers\" {\n  path      = \"providers.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents  = <<EOF\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\nEOF\n}\n\nterraform {\n  source = \"../../catalog/modules//best_cat\"\n}\n\ninputs = {\n  name = \"best-cat-2025-09-24-2359-dev\"\n\n  lambda_zip_file = \"${get_repo_root()}/dist/best-cat.zip\"\n}\n```\n\n```bash\n# live/prod/terragrunt.hcl\n\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    bucket       = \"terragrunt-to-terralith-tfstate-2025-09-24-2359\"\n    key          = \"prod/tofu.tfstate\"\n    region       = \"us-east-1\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n\ngenerate \"providers\" {\n  path      = \"providers.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents  = <<EOF\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\nEOF\n}\n\nterraform {\n  source = \"../../catalog/modules//best_cat\"\n}\n\ninputs = {\n  name = \"best-cat-2025-09-24-2359\"\n\n  lambda_zip_file = \"${get_repo_root()}/dist/best-cat.zip\"\n}\n```\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`rm -f ./*/providers.tf`} />\n\nWhat basically all Terragrunt users do at this stage is refactor out that core shared configuration (`backend` and `provider` configurations in this case), into a shared `root.hcl` file that all `terragrunt.hcl` files `include`*.* This allows for greater reuse of configuration that's common to all Terragrunt units.\n\nimport rootHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/live/root.hcl?raw';\n\n<Code title=\"live/root.hcl\" lang=\"hcl\" code={rootHcl} />\n\nNote the use of `path_relative_to_include()`  in the `key`. This tells Terragrunt to use the *path* *relative* *to the include* of the `root.hcl` file.\n\nThis can be a little confusing for new users, so just to make it very explicit:\n\nThe `live/root.hcl` file is going to be included by the `live/dev/terragrunt.hcl` file. As such, the path of the including unit (`live/dev`) *relative* to the path of the directory for the included file (`live`) is `dev`. We therefore expect `${path_relative_to_include()}` to resolve to `dev` in the `live/dev` unit, and `prod` in the `live/prod` unit (which is coincidentally how we setup our state keys before).\n\nNow we can add the `include` block that actually performs this include in each of the unit configuration files, which is just three lines.\n\nimport devTerragruntHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/live/dev/terragrunt.hcl?raw';\n\n<Code title=\"live/dev/terragrunt.hcl\" lang=\"hcl\" code={devTerragruntHcl} />\n\nimport prodTerragruntHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/live/prod/terragrunt.hcl?raw';\n\n<Code title=\"live/prod/terragrunt.hcl\" lang=\"hcl\" code={prodTerragruntHcl} />\n\nNote the addition of `find_in_parent_folders()` in the added `include` block. As you might expect, it returns the path to the `root.hcl` file found in the parent folders of `live/prod` (which is `live/root.hcl`).\n\nWe just need to do a little more state manipulation using `moved` blocks, which we should be very familiar with at this stage. When we removed the indirection of the `main` module in the `main.tf` file, we also changed the addresses of resources in state. Let's take care of that by updating the `moved.tf` file.\n\nimport devMovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/live/dev/moved.tf?raw';\n\n<Code title=\"live/dev/moved.tf\" lang=\"hcl\" code={devMovedTf} />\n\nimport prodMovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/live/prod/moved.tf?raw';\n\n<Code title=\"live/prod/moved.tf\" lang=\"hcl\" code={prodMovedTf} />\n\n<Aside type=\"note\">\nThis is an important movement to take note of. Even though we introduced the new abstraction of the Terragrunt unit, we actually reduced indirection in OpenTofu. As we'll see again later in this guide, creating granular Terragrunt units results in simpler OpenTofu modules, reducing the complexity of each unit of infrastructure.\n</Aside>\n\nWe can also remove the `removed.tf` files now that we've already “forgotten” them.\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`rm -f ./*/removed.tf`} />\n\n## Project Layout Check-in\n\nWe should now have a file layout like the following in the `live` directory:\n\n<FileTree>\n- live\n  - dev\n    - moved.tf\n    - **terragrunt.hcl**\n  - prod\n    - moved.tf\n    - **terragrunt.hcl**\n  - root.hcl\n</FileTree>\n\n\n## Applying Updates\n\nWe're ready to run a `plan` across *both units* to see if things are working correctly after all our refactors!\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`terragrunt run --all plan`} />\n\nWhen we're ready, we can `apply` our changes as well.\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`terragrunt run --all apply`} />\n\n## Trade-offs\n\n### Pros\n\n- **Significantly Reduced Duplication**: We've eliminated the need to have the following files in every environment (along with their contents):\n    - `main.tf`\n    - `providers.tf`\n    - `versions.tf`\n    - `outputs.tf`\n- **Centralized Configuration**: You know have a central location for storing common configurations like `backend` and `provider` configurations in your `root.hcl` file.\n- **Scalable IaC Growth**: Adding new environments and more is scalable now. You simply add a new Terragrunt unit, and you get isolated infrastructure that can be managed independently of the rest of your infrastructure estate.\n- **Orchestration**: You can now manage all your environments from the root of the live directory using commands like `terragrunt run --all apply`, which was not possible before without custom scripting or other additional tooling.\n\n### Cons\n\n- **Additional Tooling**: You and your team now depend on Terragrunt for critical workflows. You need to make sure you have the tool is installed and supported everywhere you want to manage infrastructure, and that your team is educated on how it works.\n- **Added Abstraction**: Although the OpenTofu code that you manage in each unit is now simpler, you now have to reason about Terragrunt configurations and commands when considering how they'll be used.\n\n## Wrap Up\n\nWith the introduction of Terragrunt, you've remediated the duplication and boilerplate created in the last step. You replaced numerous `.tf` and `.tfvars` files in each environment with a single, concise `terragrunt.hcl` file. In this step, you learned how to use the `terraform` block to specify a module source to generate a root module on demand, the `inputs` block to pass variables to that root module, and the `generate` block to inject additional files on the fly. Finally, you used the powerful `include` block to create a central `root.hcl`, ensuring your configuration is DRY (Don't Repeat Yourself). Your live infrastructure code is now dramatically leaner and easier to manage across many environments.\n"
  },
  {
    "path": "docs/src/content/docs/02-guides/01-terralith-to-terragrunt/09-step-6-breaking-the-terralith-further.mdx",
    "content": "---\ntitle: \"Step 6: Breaking the Terralith Further\"\ndescription: Breaking the Terralith Further\nslug: guides/terralith-to-terragrunt/step-6-breaking-the-terralith-further\nsidebar:\n  order: 9\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Code, Aside } from '@astrojs/starlight/components';\n\nYou've successfully added Terragrunt to your project, eliminating significant boilerplate from each of your `dev` and `prod` environments. While your environments are now isolated from each other, the resources *within* each environment (your S3 bucket, DynamoDB table, IAM role, and Lambda function) are still managed together in a single state file. This is essentially a smaller-scale Terralith within each environment.\n\nThis tight coupling poses its own risks. Do you really want a routine update to your Lambda function's application code to require a plan that also evaluates your production database? Stateful resources like databases and storage buckets change infrequently and require maximum stability, while stateless application code changes constantly. Coupling them in the same state file means a mistake in one could still impact the other, increasing the blast radius of any single change.\n\nIn this step, you will break the Terralith down even further. You will transform each environment from a single large unit into a collection of smaller, independent units, one for each core component (S3, DDB, IAM, and Lambda). This granular approach provides far more safety and flexibility, and is common in Terragrunt projects. To connect these newly independent components, you'll learn one of Terragrunt's most powerful features: the `dependency` block, which allows units to share outputs, such as passing the ARN of your S3 bucket to your IAM policy, and control the order of updates in your infrastructure units.\n\n## Tutorial\n\nWe're going to follow a very similar process to what we did when breaking apart the Terralith into two environments.\n\nFirst, we'll create a directory for each of the new units we want to create for all the constituent modules of the `best_cat` megamodule. In each of our environments (`dev` and `prod`).\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`mkdir -p {dev, prod}/{s3, ddb, iam, lambda}`} />\n\nNext, we'll create the `terragrunt.hcl` files in each of these directories.\n\nimport devDdbTerragruntHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/ddb/terragrunt.hcl?raw';\n\n<Code title=\"live/dev/ddb/terragrunt.hcl\" lang=\"hcl\" code={devDdbTerragruntHcl} />\n\nimport prodDdbTerragruntHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/ddb/terragrunt.hcl?raw';\n\n<Code title=\"live/prod/ddb/terragrunt.hcl\" lang=\"hcl\" code={prodDdbTerragruntHcl} />\n\nimport devS3TerragruntHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/s3/terragrunt.hcl?raw';\n\n<Code title=\"live/dev/s3/terragrunt.hcl\" lang=\"hcl\" code={devS3TerragruntHcl} />\n\nimport prodS3TerragruntHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/s3/terragrunt.hcl?raw';\n\n<Code title=\"live/prod/s3/terragrunt.hcl\" lang=\"hcl\" code={prodS3TerragruntHcl} />\n\nIn units where we need to integrate with other units (like the `iam` unit), we'll need to add a `dependency` block to tell Terragrunt how it can fetch outputs from relevant dependencies for use as inputs. Terragrunt has to integrate different units like this, as they don't have the same state file, so OpenTofu needs an external tool, like Terragrunt to pull outputs out of state from one unit and pass in inputs to another unit.\n\nimport devIamTerragruntHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/iam/terragrunt.hcl?raw';\n\n<Code title=\"live/dev/iam/terragrunt.hcl\" lang=\"hcl\" code={devIamTerragruntHcl} />\n\nimport prodIamTerragruntHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/iam/terragrunt.hcl?raw';\n\n<Code title=\"live/prod/iam/terragrunt.hcl\" lang=\"hcl\" code={prodIamTerragruntHcl} />\n\n<Aside type=\"note\">\nUnfortunately, OpenTofu doesn't have a way of specifying certain inputs as “unknown” [as of yet](https://github.com/opentofu/opentofu/issues/812), but we can work around this limitation by taking advantage of Terragrunt's ability to mock outputs from dependencies using `mock_outputs`. This allows us to plan successfully without worrying about getting errors that required variables aren't passed in.\n</Aside>\n\nNote that some providers like the AWS provider require these inputs to be well formed (in this case, they have to be valid AWS ARNs). In these scenarios, it can be important to provide valid looking ARNs as a consequence to satisfy provider validations. If you just passed `mock-bucket-arn` as the value of the input `s3_bucket_arn`, the AWS provider might throw an error during plans, as it expects the value to look more like `arn:aws:s3:::mock-bucket-name`, and it assumes that the user made an error.\n\nWe've also set the `mock_outputs_allowed_terraform_commands` attribute. By default, Terragrunt will use mocked outputs whenever a dependency returns no outputs. This is typically only the case for plans, but we can be explicit about when Terragrunt is allowed to mock outputs to avoid any accidental applies with mocked values. Other commands that might benefit from mocking are commands like `destroy` and `validate`. I don't anticipate needing them mocked here, so I've only allowed mocking for commands where I know we're going to need them mocked during this guide (you'll see why `state` can get mocked outputs in a bit).\n\nFinally, note that we've also set the `mock_outputs_merge_strategy_with_state` attribute. By default, Terragrunt treats mocking as something binary: Either outputs are mocked, or they're not. This is because you typically don't have a need to partially mock some outputs and not others. In our use-case, where we're migrating over state we will need to do this, as we'll be pushing existing state to units, but their outputs are also changing. We'll see what that looks like later.\n\nimport devLambdaTerragruntHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/lambda/terragrunt.hcl?raw';\n\n<Code title=\"live/dev/lambda/terragrunt.hcl\" lang=\"hcl\" code={devLambdaTerragruntHcl} />\n\nimport prodLambdaTerragruntHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/lambda/terragrunt.hcl?raw';\n\n<Code title=\"live/prod/lambda/terragrunt.hcl\" lang=\"hcl\" code={prodLambdaTerragruntHcl} />\n\n## Project Layout Check-in\n\nWe should now have a file tree that looks like the following (we'll be getting rid of the two top-level `terragrunt.hcl` and `moved.tf` files in each environment soon):\n\n<FileTree>\n- live\n  - dev\n    - ddb\n      - terragrunt.hcl\n    - iam\n      - terragrunt.hcl\n    - lambda\n      - terragrunt.hcl\n    - s3\n      - terragrunt.hcl\n    - terragrunt.hcl (This is being removed soon)\n    - moved.tf (This is being removed soon)\n  - prod\n    - ddb\n      - terragrunt.hcl\n    - iam\n      - terragrunt.hcl\n    - lambda\n      - terragrunt.hcl\n    - s3\n      - terragrunt.hcl\n    - terragrunt.hcl (This is being removed soon)\n    - moved.tf (This is being removed soon)\n  - root.hcl\n</FileTree>\n\n\n## Migrating State to Individual Units\n\nIt's time to engage in our favorite solution for IaC refactoring, state manipulation!\n\nWe're going to use the tools we've learned so far, and *pull* state from those two top-level units, then *push* them into the constituent units we've broken the megamodule down into. We expect to need to both move resource addresses in state, and forget particular resources to avoid accidentally destroying anything.\n\n<Code title=\"live/dev\" lang=\"bash\" frame=\"terminal\" code={`terragrunt state pull > /tmp/tofu.tfstate\ncd ddb && terragrunt state push /tmp/tofu.tfstate\ncd ../iam && terragrunt state push /tmp/tofu.tfstate\ncd ../lambda && terragrunt state push /tmp/tofu.tfstate\ncd ../s3 && terragrunt state push /tmp/tofu.tfstate\n`} />\n\n<Code title=\"live/prod\" lang=\"bash\" frame=\"terminal\" code={`terragrunt state pull > /tmp/tofu.tfstate\ncd ddb && terragrunt state push /tmp/tofu.tfstate\ncd ../iam && terragrunt state push /tmp/tofu.tfstate\ncd ../lambda && terragrunt state push /tmp/tofu.tfstate\ncd ../s3 && terragrunt state push /tmp/tofu.tfstate\n`} />\n\nWe can now clean up the extraneous files mentioned earlier at the root of the environments.\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`rm -f {dev, prod}/{terragrunt.hcl, moved.tf}`} />\n\nGo ahead and run the following to see very similar plan output to what we've seen in the past when we needed to make state moves & removes.\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`terragrunt run --all plan\n\n# Lots of destroys!`} />\n\nThe following moves and removes will handle the state transitions necessary here.\n\nimport devDdbMovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/ddb/moved.tf?raw';\n\n<Code title=\"live/dev/ddb/moved.tf\" lang=\"hcl\" code={devDdbMovedTf} />\n\nimport devDdbRemovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/ddb/removed.tf?raw';\n\n<Code title=\"live/dev/ddb/removed.tf\" lang=\"hcl\" code={devDdbRemovedTf} />\n\nimport devIamMovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/iam/moved.tf?raw';\n\n<Code title=\"live/dev/iam/moved.tf\" lang=\"hcl\" code={devIamMovedTf} />\n\nimport devIamRemovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/iam/removed.tf?raw';\n\n<Code title=\"live/dev/iam/removed.tf\" lang=\"hcl\" code={devIamRemovedTf} />\n\nimport devLambdaMovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/lambda/moved.tf?raw';\n\n<Code title=\"live/dev/lambda/moved.tf\" lang=\"hcl\" code={devLambdaMovedTf} />\n\nimport devLambdaRemovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/lambda/removed.tf?raw';\n\n<Code title=\"live/dev/lambda/removed.tf\" lang=\"hcl\" code={devLambdaRemovedTf} />\n\nimport devS3MovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/s3/moved.tf?raw';\n\n<Code title=\"live/dev/s3/moved.tf\" lang=\"hcl\" code={devS3MovedTf} />\n\nimport devS3RemovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/s3/removed.tf?raw';\n\n<Code title=\"live/dev/s3/removed.tf\" lang=\"hcl\" code={devS3RemovedTf} />\n\nimport prodDdbMovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/ddb/moved.tf?raw';\n\n<Code title=\"live/prod/ddb/moved.tf\" lang=\"hcl\" code={prodDdbMovedTf} />\n\nimport prodDdbRemovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/ddb/removed.tf?raw';\n\n<Code title=\"live/prod/ddb/removed.tf\" lang=\"hcl\" code={prodDdbRemovedTf} />\n\nimport prodIamMovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/iam/moved.tf?raw';\n\n<Code title=\"live/prod/iam/moved.tf\" lang=\"hcl\" code={prodIamMovedTf} />\n\nimport prodIamRemovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/iam/removed.tf?raw';\n\n<Code title=\"live/prod/iam/removed.tf\" lang=\"hcl\" code={prodIamRemovedTf} />\n\nimport prodLambdaMovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/lambda/moved.tf?raw';\n\n<Code title=\"live/prod/lambda/moved.tf\" lang=\"hcl\" code={prodLambdaMovedTf} />\n\nimport prodLambdaRemovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/lambda/removed.tf?raw';\n\n<Code title=\"live/prod/lambda/removed.tf\" lang=\"hcl\" code={prodLambdaRemovedTf} />\n\nimport prodS3MovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/s3/moved.tf?raw';\n\n<Code title=\"live/prod/s3/moved.tf\" lang=\"hcl\" code={prodS3MovedTf} />\n\nimport prodS3RemovedTf from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/s3/removed.tf?raw';\n\n<Code title=\"live/prod/s3/removed.tf\" lang=\"hcl\" code={prodS3RemovedTf} />\n\nThat was a ton of work! The effort of making these state moves might encourage you to do some early planning to avoid the need to do these kinds of state moves down the line as you plan your infrastructure estate.\n\nFolks sometimes feel like they don't really want or need to adopt Terragrunt before they reach a point where scaling up IaC further becomes painful. Deciding to avoid learning Terragrunt before this point is a form of tech debt accrual. Doing the work up-front to follow the patterns that Terragrunt enables (like segmenting state at granular levels) helps to mitigate the severity of refactor work down the line. If we had architected our IaC ahead of time to use small, focused units, we never would have had to do the work of these state moves.\n\nHopefully, going through these state moves in this guide gives you confidence that you *can* do it if you need to, however. As long as you move carefully, and know what you're doing, you can break down even the largest Terraliths with time!\n\n## Applying Updates\n\nNow, let's repeat our plan to confirm that we won't destroy anything important. If you *do* see any destroys, you probably have something misconfigured in one of your `moved.tf` or `removed.tf` files. Review them carefully.\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`terragrunt run --all plan\n\n# No destroys!\n# You might see some creates, but that's a side-effect of how\n# OpenTofu tracks state internally. You are safe to ignore them.\n`} />\n\nThankfully, now that we've segmented state we can carefully run across the `dev` units before running in `prod`, with *zero risk* that we're going to accidentally break anything there. We can actually perform our updates *even more carefully* by updating one unit at a time, but that's not really necessary for our use-case here.\n\nConsider when it might make sense to do that for your own real infrastructure, however. If you are doing state manipulation like this on stateful production resources like databases or blob stores for example, it's a good idea to move slower to avoid data loss or outages.\n\n<Code title=\"live/dev\" lang=\"bash\" frame=\"terminal\" code={`terragrunt run --all apply\n\n# Migration complete!`} />\n\n<Code title=\"live/prod\" lang=\"bash\" frame=\"terminal\" code={`terragrunt run --all apply\n\n# Migration complete!`} />\n\n## Trade-offs\n\nYou've now reached the most granular and arguably the safest way to structure a Terragrunt project (while remaining practical about avoiding over-segmenting resources). By breaking down each environment into component-specific units, you've moved from a \"one state file per environment\" model to a \"one state file per component, per environment\" model. This is a common and highly recommended pattern for mature Infrastructure as Code (IaC) management, but it comes with its own set of trade-offs.\n\n- Pros\n    - **Safety and Granular Blast Radius**: This is the single biggest advantage. A change to a stateless resource that changes frequently, like the **Lambda function**, now has **zero chance of impacting a stateful resource** that changes rarely, like the **DynamoDB table** or **S3 bucket**.\n    - **Reduced Lock Contention**: State locks are now per-component, meaning an `apply` on the Lambda function won't block a simultaneous `apply` on the IAM role, enabling more concurrent infrastructure work by platform teams.\n    - **Faster Feedback Loops**: When you run `terragrunt plan` inside a specific component directory (e.g., `live/dev/lambda`), OpenTofu only needs to refresh the state for that single component. This is significantly faster than refreshing the state for the entire environment, which is a huge productivity win on large projects.\n- Cons\n    - **Increased Configuration Complexity**: The number of directories and `terragrunt.hcl` files has multiplied. While each file is simple, managing the overall structure requires more discipline. The cognitive load shifts from understanding a single large module to understanding how many small, interconnected modules form a complete system.\n    - **Explicit Dependency Management**: You now *must* explicitly define the relationships between your components using `dependency` blocks. This is powerful but also creates another layer of configuration to maintain. Forgetting a dependency or referencing it incorrectly will cause failures.\n    - **Mocking Outputs**: As demonstrated in the tutorial, you can't `plan` a component that depends on another component that doesn't exist yet. This necessitates using `mock_outputs` if you want to perform a `run --all plan` against a stack with unapplied dependencies, which is a powerful workaround but adds another concept that engineers must learn and manage correctly.\n\n## Wrap Up\n\nYou've now taken modularity to the next level. Instead of one state file per environment, you now have one state file per *component* (S3, DDB, IAM, Lambda) within each environment. This provides the ultimate level of granular control and safety. You can now update your application's Lambda function with zero risk of accidentally modifying your stateful database or storage bucket.\n\nThe core lesson here was learning how to use the `dependency` block, Terragrunt's mechanism for wiring together independent units by passing outputs from one unit as inputs to another. You also learned to use `mock_outputs` to solve the problem that arises when planning interdependent infrastructure that doesn't exist yet.\n\nHowever, this safety came with a trade-off: a proliferation of `terragrunt.hcl` files across your codebase. In the next step, you will eliminate this final piece of boilerplate by using Terragrunt Stacks, which allow you to generate entire collections of units on-demand from a single `terragrunt.stack.hcl` file.\n"
  },
  {
    "path": "docs/src/content/docs/02-guides/01-terralith-to-terragrunt/10-step-7-taking-advantage-of-terragrunt-stacks.mdx",
    "content": "---\ntitle: \"Step 7: Taking advantage of Terragrunt Stacks\"\ndescription: Taking advantage of Terragrunt Stacks\nslug: guides/terralith-to-terragrunt/step-7-taking-advantage-of-terragrunt-stacks\nsidebar:\n  order: 10\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Code, Aside } from '@astrojs/starlight/components';\n\nUp until relatively recently in the history of Terragrunt, the proliferation of `terragrunt.hcl` files was the trade-off platform engineers had to accept for state segmentation. Luckily, with the advent of [Terragrunt Stacks](/features/stacks/), that's not the case anymore. Collections of units, like the ones you created in the last step, can be generated on-demand using `terragrunt.stack.hcl` files. This saves you from having to duplicate `terragrunt.hcl` files across your codebase.\n\nIn this step, you will migrate to this modern pattern. You'll start by persisting your unit definitions in your `catalog`. Then, you'll replace the numerous `terragrunt.hcl` files in your `live` directory with a single `terragrunt.stack.hcl` file in each environment. To handle the slight configuration differences between `dev` and `prod`, you'll use Terragrunt's `values` attributes, which allow you to parameterize your reusable unit definitions.\n\n## Tutorial\n\nLet's start migrating to Terragrunt Stacks by persisting unit definitions in the `catalog`.\n\n<Aside type=\"tip\">\nWe copy over `.terraform.lock.hcl` files in addition to `terragrunt.hcl` files here. Persisting `.terraform.lock.hcl` files is a best practice to ensure reproducibility of units, and to maximize the performance of OpenTofu (as OpenTofu relies on the availability of this file when determining whether it can safely reuse content in the provider cache directory).\n</Aside>\n\n```bash\nmkdir -p catalog/units/{ddb, iam, lambda, s3}\ncp live/dev/ddb/{terragrunt.hcl, .terraform.lock.hcl} catalog/units/ddb/\ncp live/dev/iam/{terragrunt.hcl, .terraform.lock.hcl} catalog/units/iam/\ncp live/dev/lambda/{terragrunt.hcl, .terraform.lock.hcl} catalog/units/lambda/\ncp live/dev/s3/{terragrunt.hcl, .terraform.lock.hcl} catalog/units/s3/\n```\n\nYou might remember that the unit configurations differed very slightly between `dev` and `prod`. Luckily, Terragrunt has special tooling to handle that via the usage of `values` variables.\n\nimport catalogUnitsDdbTerragruntHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/units/ddb/terragrunt.hcl?raw';\n\n<Code title=\"catalog/units/ddb/terragrunt.hcl\" lang=\"hcl\" code={catalogUnitsDdbTerragruntHcl} />\n\nBy specifying `values.name` there, we're allowing values to be used in our unit configurations from `terragrunt.stack.hcl` files. You'll see how these values are set later in this step.\n\nimport catalogUnitsIamTerragruntHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/units/iam/terragrunt.hcl?raw';\n\n<Code title=\"catalog/units/iam/terragrunt.hcl\" lang=\"hcl\" code={catalogUnitsIamTerragruntHcl} />\n\n<Aside type=\"note\">\nWe're not just using the `values` variable for the inputs that differ between stacks, like the `name` and `aws_region`. We can use them for any value we want to substitute in our `terragrunt.hcl` files, including the relative paths to other units (which we might want to be dynamic if we're going to refactor or add additional dependencies, etc.)\n</Aside>\n\nimport catalogUnitsLambdaTerragruntHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/units/lambda/terragrunt.hcl?raw';\n\n<Code title=\"catalog/units/lambda/terragrunt.hcl\" lang=\"hcl\" code={catalogUnitsLambdaTerragruntHcl} />\n\nimport catalogUnitsS3TerragruntHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/units/s3/terragrunt.hcl?raw';\n\n<Code title=\"catalog/units/s3/terragrunt.hcl\" lang=\"hcl\" code={catalogUnitsS3TerragruntHcl} />\n\nNow we can replace the `terragrunt.hcl` files in `live` with a single `terragrunt.stack.hcl` file in each environment to generate them on-demand using `unit` blocks. By default, units generated by Terragrunt are generated into `.terragrunt-stack` directories. We opt out of that by setting `no_dot_terragrunt_stack` to `true`.\n\nimport liveDevTerragruntStackHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/live/dev/terragrunt.stack.hcl?raw';\n\n<Code title=\"live/dev/terragrunt.stack.hcl\" lang=\"hcl\" code={liveDevTerragruntStackHcl} />\n\nWe'll also add this `.gitignore` file to avoid recommitting the generated files in our repository, as they'll be regenerated whenever we need them. We'll see how we can remove the need for this in a future step.\n\nimport liveDevGitignore from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/live/dev/.gitignore?raw';\n\n<Code title=\"live/dev/.gitignore\" lang=\"hcl\" code={liveDevGitignore} />\n\nNow that we can generate these unit configurations on demand, we can remove the copies that we created manually!\n\n<Code title=\"live/dev\" lang=\"bash\" frame=\"terminal\" code={`rm -rf .terraform.lock.hcl ddb iam lambda s3\nterragrunt run --all plan`} />\n\nAll that's left now is to repeat the same thing for prod.\n\nimport liveProdTerragruntStackHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/live/prod/terragrunt.stack.hcl?raw';\n\n<Code title=\"live/prod/terragrunt.stack.hcl\" lang=\"hcl\" code={liveProdTerragruntStackHcl} />\n\nimport liveProdGitignore from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/live/prod/.gitignore?raw';\n\n<Code title=\"live/prod/.gitignore\" lang=\"hcl\" code={liveProdGitignore} />\n\n<Code title=\"live/prod\" lang=\"bash\" frame=\"terminal\" code={`rm -rf .terraform.lock.hcl ddb iam lambda s3\nterragrunt run --all plan`} />\n\n## Project Layout Check-in\n\nIf you clean out the `.gitignore`'ed files and take a look at the file tree, you should see that your `live` file count has shrunk down again!\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`rm -rf ./***/ddb ./***/iam ./***/lambda ./***/s3`} />\n\n<FileTree>\n- live\n  - dev\n    - terragrunt.stack.hcl\n  - prod\n    - terragrunt.stack.hcl\n  - root.hcl\n</FileTree>\n\n\n## Trade-offs\n\nYou've now adopted one of Terragrunt's most advanced features, **Terragrunt Stacks**, to achieve an exceptionally clean and DRY (Don't Repeat Yourself) infrastructure codebase. By generating your component configurations on the fly from a central catalog, you've eliminated the last major source of boilerplate in your Terragrunt configurations. However, this abstraction comes with its own set of trade-offs.\n\n### Pros\n\n- **Maximum Reusability and Deduplication**: This is the most significant benefit of using Terragrunt Stacks. Instead of having multiple `terragrunt.hcl` files scattered across each environment's subdirectories, you now have a single, reusable unit definition for each component in your `catalog`. Adding a new environment is as simple as creating a new `terragrunt.stack.hcl` and defining its unique inputs.\n- **Simplified `live` Directory**: Your `live` directory is now incredibly lean and easy to navigate. Each environment is represented by a single `terragrunt.stack.hcl` file, which serves as a clear manifest of all the components that make up that environment. This is a similar layout to that which was achieved in step 5, but we've gained the ability to retain state segmentation and operate granularly.\n- **Centralized Configuration Catalog**: If you need to update the configuration for a component across *all* environments (e.g., add a new dependency or change a `mock_output`), you only need to edit the corresponding file in `catalog/units`. This drastically reduces the chance of configuration drift and makes maintenance much easier.\n\n### Cons\n\n- **Increased Abstraction**: The biggest trade-off is the added layer of indirection. Engineers no longer have all the configuration for their Terragrunt units committed to their repository. If they want to read through their configurations, they need to generate the stack, or read the contents stored in their `catalog`.\n- **Steeper Learning Curve**: The concepts of `terragrunt.stack.hcl`, `unit` blocks, and the `values` attribute are powerful but are also more advanced Terragrunt features. Onboarding new team members may require more time to explain this higher level of abstraction compared to the more explicit file-based approach from the previous step.\n\n## Wrap Up\n\nYou've conquered the final major source of Terragrunt boilerplate! In this step, you adopted one of Terragrunt's most powerful features: **Terragrunt Stacks**.\n\nBy centralizing your generic unit configurations into the `catalog`, you were able to replace the numerous `terragrunt.hcl` files in each environment with a single, clean `terragrunt.stack.hcl` file. The core concept you mastered was using the `values` object to **parameterize** these reusable units, allowing you to define a component's configuration once and deploy it many times with environment-specific values.\n\nHowever, there's one piece of technical debt left from our migration: the state paths in your S3 backend don't yet align with Terragrunt's default conventions, requiring those annoying `.gitignore` files. In the final step, you'll perform one last set of state migrations to finalize your layout, fully mastering this guide.\n"
  },
  {
    "path": "docs/src/content/docs/02-guides/01-terralith-to-terragrunt/11-step-8-refactoring-state-with-terragrunt-stacks.mdx",
    "content": "---\ntitle: \"Step 8: Refactoring state with Terragrunt Stacks\"\ndescription: Refactoring state with Terragrunt Stacks\nslug: guides/terralith-to-terragrunt/step-8-refactoring-state-with-terragrunt-stacks\nsidebar:\n  order: 11\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Code, Aside } from '@astrojs/starlight/components';\n\nYou've just completed a major refactor using **Terragrunt Stacks**.\n\nHowever, there's one final piece of technical debt remaining to complete this guide. To make the transition in the previous step smoother, we used the `no_dot_terragrunt_stack` attribute, which generated the unit configurations directly into directories like `dev/s3` and `prod/lambda`.\n\nWhile this worked perfectly fine for our migration, and is a recommended first step to adopting Terragrunt Stacks, it's not the standard configuration you would arrive at if you wrote the configurations by hand. By default, Terragrunt generates unit configurations into a hidden `.terragrunt-stack` directory within each environment. This keeps your generated code is neatly tucked away and easily ignored by version control. Our current setup requires `.gitignore` files in each stack directory to prevent committing this generated code.\n\nIn this final step, you will perform one last state migration to align your project with Terragrunt's best practices. You will remove the `no_dot_terragrunt_stack` attribute and move your state to match the default, conventional directory structure.\n\n## Tutorial\n\nTo review, this is what our S3 layout looks like for our state (ignoring the state that we've left behind during our refactors):\n\n```bash\n$ aws s3 ls --recursive s3://[your-state-bucket] | awk '{print $4}' | rg -v '^tofu.tfstate$' | rg -v '^dev/tofu.tfstate$' | rg -v '^prod/tofu.tfstate$'\ndev/ddb/tofu.tfstate\ndev/iam/tofu.tfstate\ndev/lambda/tofu.tfstate\ndev/s3/tofu.tfstate\nprod/ddb/tofu.tfstate\nprod/iam/tofu.tfstate\nprod/lambda/tofu.tfstate\nprod/s3/tofu.tfstate\n```\n\nWhat we'd like our state keys to look like is the following, which is how it would look if we provisioned our stack without usage of `no_dot_terragrunt_stack` from the beginning:\n\n```bash\ndev/.terragrunt-stack/ddb/tofu.tfstate\ndev/.terragrunt-stack/iam/tofu.tfstate\ndev/.terragrunt-stack/lambda/tofu.tfstate\ndev/.terragrunt-stack/s3/tofu.tfstate\nprod/.terragrunt-stack/ddb/tofu.tfstate\nprod/.terragrunt-stack/iam/tofu.tfstate\nprod/.terragrunt-stack/lambda/tofu.tfstate\nprod/.terragrunt-stack/s3/tofu.tfstate\n```\n\nGiven that there's a close relationship between filesystem layout and backend state keys, we can achieve this by having our units generated into the default `.terragrunt-stack` directories instead of being generated directly adjacent to `terragrunt.stack.hcl` files.\n\nWhat we'll want to do is perform state migration while having both unit layouts generated locally. If you remember earlier steps, the way to that this is to use the `state pull` and `state push` commands.\n\nFirst, let's make sure we have our stack generated as-is without removing the `no_dot_terragrunt_stack` attribute.\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`terragrunt stack generate\n16:36:50.794 INFO   Generating stack from ./dev/terragrunt.stack.hcl\n16:36:50.797 INFO   Generating stack from ./prod/terragrunt.stack.hcl\n16:36:50.798 INFO   Processing unit s3 from ./dev/terragrunt.stack.hcl\n16:36:50.798 INFO   Processing unit ddb from ./dev/terragrunt.stack.hcl\n16:36:50.798 INFO   Processing unit lambda from ./dev/terragrunt.stack.hcl\n16:36:50.798 INFO   Processing unit iam from ./dev/terragrunt.stack.hcl\n16:36:50.798 INFO   Processing unit lambda from ./prod/terragrunt.stack.hcl\n16:36:50.798 INFO   Processing unit iam from ./prod/terragrunt.stack.hcl\n16:36:50.798 INFO   Processing unit ddb from ./prod/terragrunt.stack.hcl\n16:36:50.798 INFO   Processing unit s3 from ./prod/terragrunt.stack.hcl\n`} />\n\nNow let's edit our `terragrunt.stack.hcl` files to remove the `no_dot_terragrunt_stack` attribute. This will generate units into the desired final directory structure.\n\nimport liveDevTerragruntStackHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/live/dev/terragrunt.stack.hcl?raw';\n\n<Code title=\"live/dev/terragrunt.stack.hcl\" lang=\"hcl\" code={liveDevTerragruntStackHcl} />\n\nimport liveProdTerragruntStackHcl from '../../../../fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/live/prod/terragrunt.stack.hcl?raw';\n\n<Code title=\"live/prod/terragrunt.stack.hcl\" lang=\"hcl\" code={liveProdTerragruntStackHcl} />\n\nNow let's generate again to get that generated `.terragrunt-stack` directory.\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`terragrunt stack generate`} />\n\n## Project Layout Check-in\n\nShould see a layout like the following now, with both a stack generated within `.terragrunt-stack` and one generated outside of it:\n\n<FileTree>\n- live\n  - dev\n    - **.terragrunt-stack**\n      - **ddb**\n        - **.terraform.lock.hcl**\n        - **.terragrunt-stack-manifest**\n        - **terragrunt.hcl**\n        - **terragrunt.values.hcl**\n      - **iam**\n        - **.terraform.lock.hcl**\n        - **.terragrunt-stack-manifest**\n        - **terragrunt.hcl**\n        - **terragrunt.values.hcl**\n      - **lambda**\n        - **.terraform.lock.hcl**\n        - **.terragrunt-stack-manifest**\n        - **terragrunt.hcl**\n        - **terragrunt.values.hcl**\n      - **s3**\n        - **.terraform.lock.hcl**\n        - **.terragrunt-stack-manifest**\n        - **terragrunt.hcl**\n        - **terragrunt.values.hcl**\n    - ddb\n      - .terraform.lock.hcl\n      - .terragrunt-stack-manifest\n      - terragrunt.hcl\n      - terragrunt.values.hcl\n    - iam\n      - .terraform.lock.hcl\n      - .terragrunt-stack-manifest\n      - terragrunt.hcl\n      - terragrunt.values.hcl\n    - lambda\n      - .terraform.lock.hcl\n      - .terragrunt-stack-manifest\n      - terragrunt.hcl\n      - terragrunt.values.hcl\n    - s3\n      - .terraform.lock.hcl\n      - .terragrunt-stack-manifest\n      - terragrunt.hcl\n      - terragrunt.values.hcl\n    - terragrunt.stack.hcl\n  - prod\n    - **.terragrunt-stack**\n      - **ddb**\n        - **.terraform.lock.hcl**\n        - **.terragrunt-stack-manifest**\n        - **terragrunt.hcl**\n        - **terragrunt.values.hcl**\n      - **iam**\n        - **.terraform.lock.hcl**\n        - **.terragrunt-stack-manifest**\n        - **terragrunt.hcl**\n        - **terragrunt.values.hcl**\n      - **lambda**\n        - **.terraform.lock.hcl**\n        - **.terragrunt-stack-manifest**\n        - **terragrunt.hcl**\n        - **terragrunt.values.hcl**\n      - **s3**\n        - **.terraform.lock.hcl**\n        - **.terragrunt-stack-manifest**\n        - **terragrunt.hcl**\n        - **terragrunt.values.hcl**\n    - ddb\n      - .terraform.lock.hcl\n      - .terragrunt-stack-manifest\n      - terragrunt.hcl\n      - terragrunt.values.hcl\n    - iam\n      - .terraform.lock.hcl\n      - .terragrunt-stack-manifest\n      - terragrunt.hcl\n      - terragrunt.values.hcl\n    - lambda\n      - .terraform.lock.hcl\n      - .terragrunt-stack-manifest\n      - terragrunt.hcl\n      - terragrunt.values.hcl\n    - s3\n      - .terraform.lock.hcl\n      - .terragrunt-stack-manifest\n      - terragrunt.hcl\n      - terragrunt.values.hcl\n    - terragrunt.stack.hcl\n</FileTree>\n\n\n## Applying Updates\n\nTo migrate state from the old unit paths to the new paths, we can run the following:\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`# Migrate dev state\ncd dev/ddb\nterragrunt state pull > /tmp/tofu.tfstate\ncd ../.terragrunt-stack/ddb\nterragrunt state push /tmp/tofu.tfstate\ncd ../../s3\nterragrunt state pull > /tmp/tofu.tfstate\ncd ../.terragrunt-stack/s3\nterragrunt state push /tmp/tofu.tfstate\ncd ../../iam\nterragrunt state pull > /tmp/tofu.tfstate\ncd ../.terragrunt-stack/iam\nterragrunt state push /tmp/tofu.tfstate\ncd ../../lambda\nterragrunt state pull > /tmp/tofu.tfstate\ncd ../.terragrunt-stack/lambda\nterragrunt state push /tmp/tofu.tfstate\n\n# Migrate prod state\ncd ../../../prod/ddb\nterragrunt state pull > /tmp/tofu.tfstate\ncd ../.terragrunt-stack/ddb\nterragrunt state push /tmp/tofu.tfstate\ncd ../../s3\nterragrunt state pull > /tmp/tofu.tfstate\ncd ../.terragrunt-stack/s3\nterragrunt state push /tmp/tofu.tfstate\ncd ../../iam\nterragrunt state pull > /tmp/tofu.tfstate\ncd ../.terragrunt-stack/iam\nterragrunt state push /tmp/tofu.tfstate\ncd ../../lambda\nterragrunt state pull > /tmp/tofu.tfstate\ncd ../.terragrunt-stack/lambda\nterragrunt state push /tmp/tofu.tfstate\n`} />\n\nWe can now remove the `.gitignore` files, and prove to ourselves that state has migrated successfully!\n\n<Code title=\"live\" lang=\"bash\" frame=\"terminal\" code={`rm -rf ./***/.gitignore ./***/ddb ./***/iam ./***/lambda ./***/s3\nterragrunt run --all plan\n\n# No changes!\n`} />\n\n## Trade-offs\n\nThis final refactor brings your project into alignment with Terragrunt's standard conventions, but there are some minor trade-offs to consider.\n\n### Pros\n\n- **Cleaner Working Directory**: The most significant advantage is the cleanliness of your `live` directory. All generated code now resides in a hidden `.terragrunt-stack` directory, leaving your environment folders (e.g., `live/dev`) containing only your manually-managed `terragrunt.stack.hcl` file.\n- **Simplified Version Control**: You can now remove the environment-specific `.gitignore` files. A single, global entry to ignore `.terragrunt-stack` and `.terragrunt-cache` is all that's needed, making your version control rules simpler and more reliable.\n\n### Cons\n\n- **State Migration**: The primary cost is the one-time effort of performing the state migration. While powerful, any direct state manipulation requires careful execution to avoid errors. This refactor is an investment of time and attention to detail.\n- **Tooling Requirements**: If you currently use a CI/CD tool that supports Terragrunt, it has to have built-in awareness of how Terragrunt Stack generation, like [Gruntwork Pipelines](https://www.gruntwork.io/platform/pipelines). CI/CD tools that have been around for a long while might not prioritize handling stack generation, and lack support as a consequence. Placing all generated units in a `.gitignore` file, CI/CD tools might not be able to track when units change, and make selective changes to IaC.\n\n## Wrap Up\n\nThis final step was about aligning with Terragrunt's standard conventions. By removing the `no_dot_terragrunt_stack` attribute, you enabled Terragrunt's default behavior of generating code into a hidden `.terragrunt-stack` directory.\n\nThis required one last, careful state migration. You used **`terragrunt state pull`** to download state from old unit keys and **`terragrunt state push`** to the new, conventional backend keys that matched the updated directory structure from stack generation. Your project is now not only easy to manage but also immediately familiar to any engineer experienced with Terragrunt, featuring a state backend structure aligned with your filesystem.\n"
  },
  {
    "path": "docs/src/content/docs/02-guides/01-terralith-to-terragrunt/12-wrap-up.mdx",
    "content": "---\ntitle: \"Wrap Up\"\ndescription: Wrap Up\nslug: guides/terralith-to-terragrunt/wrap-up\nsidebar:\n  order: 12\nnext: false\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nCongratulations on making it through the Terralith to Terragrunt guide!\n\nYou've successfully navigated the entire journey from a cumbersome Terralith to a clean, scalable, and maintainable IaC setup managed by Terragrunt. This process mirrors the real-world challenges many teams face as their infrastructure grows, and the skills you've developed are invaluable for managing IaC effectively at any scale.\n\nAlong the way, you've practiced and mastered critical techniques that will serve you well on your path to IaC mastery. You now have hands-on experience with:\n\n- **Advanced State Manipulation**: Safely refactoring your infrastructure's state without destroying and recreating critical resources. You've used OpenTofu's `moved` and `removed` blocks and `state pull` and `state push` commands to migrate state between different file system layouts and backend configurations.\n- **Modular Infrastructure Design**: Breaking down a monolithic configuration into smaller, reusable modules with clear, encapsulated APIs. You learned to structure your project with a `catalog` of components that can be consumed by your `live` infrastructure.\n- **Boilerplate Reduction with Terragrunt**: Eliminating repetitive code by using `terragrunt.hcl` to generate provider and backend configurations, define module sources, and pass inputs dynamically using functions like `get_repo_root()` and `path_relative_to_include()`.\n- **Dependency Management**: Orchestrating complex deployments across multiple, isolated state files. You learned to use Terragrunt's `dependency` blocks to pass outputs from one component as inputs to another, and how to use `mock_outputs` to enable successful planning even before dependencies have been applied.\n- **Dynamic Stack Generation**: Transitioning from manually managing individual `terragrunt.hcl` files to defining entire environments on-demand with a single `terragrunt.stack.hcl` file, making your infrastructure definitions radically simpler and easier to maintain.\n\nIf you'd like to continue practicing on your own, consider how you might continue experimenting with this configuration.\n\n- **Nested Stacks**: What if you wanted to manage both the `dev` and `prod` environments from a single, top-level file using the [Environment-based Stacks pattern](https://docs.terragrunt.com/features/stacks/explicit)? Think about how you would refactor your current setup to achieve this. Would you need to perform more state manipulation? If so, which techniques from this guide would you leverage to accomplish it safely?\n\n    <Aside type=\"tip\">\n      Review the state migration steps throughout this guide!\n    </Aside>\n\n- **Integrating Terragrunt Into Your Build System**: In this guide, the artifact that's deployed to Lambda is manually packaged by you before provisioning any infrastructure. Terragrunt has special tooling in [hooks](https://docs.terragrunt.com/features/units/hooks/) to automate manual tasks like this for you on-demand as you interact with your infrastructure. How would you integrate that into the configurations you've built so far?\n\n    <Aside type=\"tip\">\n      You might find an example of this in our [example catalog](https://github.com/gruntwork-io/terragrunt-infrastructure-catalog-example/tree/main/examples/terragrunt)!\n    </Aside>\n\n- **Testing Our IaC**: If you were to use this infrastructure in production, you might want the underlying patterns (and perhaps the live infrastructure) tested automatically, rather than just applying the IaC and manually verifying that it is working correctly. How would you go about doing that?\n\n    <Aside type=\"tip\">\n      You might find a helpful tool to accomplish that in [Terratest](https://terratest.gruntwork.io/)!\n    </Aside>\n\n- **Automating Plan Assessments**: Throughout this guide, you engaged in careful manual evaluation of plans, and it was hinted early on that tools like [OPA](https://www.openpolicyagent.org/) provide a way to automatically assess the risk of plans. How would you integrate them into Terragrunt?\n\n    <Aside type=\"tip\">\n      You might find it useful to review the documentation on [hooks](https://docs.terragrunt.com/features/units/hooks/)!\n    </Aside>\n\nThis was a simple example, but if you're struggling with a Terralith, your use-case is almost definitely more complex! Gruntwork offers support for assisting customers transition out of Terraliths into maintainable, best practices IaC codebases.\n\nSend an email to sales@gruntwork.io for [Terragrunt Enterprise Support](https://www.gruntwork.io/services/terragrunt) for more information.\n"
  },
  {
    "path": "docs/src/content/docs/03-features/01-units/02-includes.mdx",
    "content": "---\ntitle: Includes\ndescription: Learn how to reuse partial Terragrunt configurations to DRY up your configurations.\nslug: features/units/includes\nsidebar:\n  order: 2\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\n\n## Motivation\n\nAs covered in [Units](/features/units) and [State Backend](/features/units/state-backend),\nit quickly becomes important to define base Terragrunt configuration files that are included in units. This is to ensure\nthat all units have a consistent configuration, and to avoid repeating the same configuration across multiple units.\n\nFor example, you might have a **root** Terragrunt configuration that defines the remote state and provider configurations for all your units:\n\n```hcl\n# root.hcl\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket         = \"my-tofu-state\"\n    key            = \"${path_relative_to_include()}/tofu.tfstate\"\n    region         = \"us-east-1\"\n    encrypt        = true\n    dynamodb_table = \"my-lock-table\"\n  }\n}\n\ngenerate \"provider\" {\n  path = \"provider.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nprovider \"aws\" {\n  assume_role {\n    role_arn = \"arn:aws:iam::0123456789:role/terragrunt\"\n  }\n}\nEOF\n}\n```\n\nYou can then include this in each of your **unit** `terragrunt.hcl` files using the `include` block for each\ninfrastructure module you need to deploy:\n\n```hcl\n# app/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n```\n\nThis pattern is useful for global configuration blocks that need to be included in all of your modules, but what if you\nhave Terragrunt configurations that are only relevant to subsets of your stack?\n\nFor example, consider the following terragrunt file structure, which defines three environments (`prod`, `qa`, and `stage`)\nwith the same infrastructure in each one (an app, a MySQL database, and a VPC):\n\n<FileTree>\n\n- live\n  - root.hcl\n  - prod\n    - app\n      - terragrunt.hcl\n    - mysql\n      - terragrunt.hcl\n    - vpc\n      - terragrunt.hcl\n  - qa\n    - app\n      - terragrunt.hcl\n    - mysql\n      - terragrunt.hcl\n    - vpc\n      - terragrunt.hcl\n  - stage\n    - app\n      - terragrunt.hcl\n    - mysql\n      - terragrunt.hcl\n    - vpc\n      - terragrunt.hcl\n\n</FileTree>\n\nMore often than not, each of the services will look similar across the different environments, only requiring small\ntweaks.\n\nFor example, the `app/terragrunt.hcl` files may be identical across all three environments except for an\nadjustment to the `instance_type` parameter for each environment. These identical settings don't belong in the root\n`terragrunt.hcl` configuration because they are only relevant to the `app` configurations, and not `mysql` or `vpc`.\n\nTo solve this, you can use [multiple include blocks](/reference/hcl/blocks#include).\n\n## Using multiple includes\n\nSuppose your `qa/app/terragrunt.hcl` configuration looks like the following:\n\n```hcl\n# qa/app/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"github.com/<org>/modules.git//app?ref=v0.1.0\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n\ndependency \"mysql\" {\n  config_path = \"../mysql\"\n}\n\ninputs = {\n  env            = \"qa\"\n  basename       = \"example-app\"\n  vpc_id         = dependency.vpc.outputs.vpc_id\n  subnet_ids     = dependency.vpc.outputs.subnet_ids\n  mysql_endpoint = dependency.mysql.outputs.endpoint\n}\n```\n\nIn this example, the only thing that is different between the environments is the `env` input variable. This means that\nexcept for one line, everything in the config is duplicated across `prod`, `qa`, and `stage`.\n\nTo [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) this up, we will introduce a new folder called `_env`\nwhich will contain the common configurations across the three environments (we prefix with `_` to indicate that this\nfolder doesn't contain deployable configurations, and so that it is lexically sorted first in the directory listing):\n\n<FileTree>\n\n- live\n  - root.hcl\n  - _env\n    - app.hcl\n    - mysql.hcl\n    - vpc.hcl\n  - prod\n    - app\n      - terragrunt.hcl\n    - mysql\n      - terragrunt.hcl\n    - vpc\n      - terragrunt.hcl\n  - qa\n    - app\n      - terragrunt.hcl\n    - mysql\n      - terragrunt.hcl\n    - vpc\n      - terragrunt.hcl\n  - stage\n    - app\n      - terragrunt.hcl\n    - mysql\n      - terragrunt.hcl\n    - vpc\n      - terragrunt.hcl\n\n</FileTree>\n\nIn our example, the contents of `_env/app.hcl` would look like the following:\n\n```hcl\n# _env/app.hcl\nterraform {\n  source = \"github.com/<org>/modules.git//app?ref=v0.1.0\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n\ndependency \"mysql\" {\n  config_path = \"../mysql\"\n}\n\ninputs = {\n  basename       = \"example-app\"\n  vpc_id         = dependency.vpc.outputs.vpc_id\n  subnet_ids     = dependency.vpc.outputs.subnet_ids\n  mysql_endpoint = dependency.mysql.outputs.endpoint\n}\n```\n\nNote that everything is defined except for the `env` input variable. We now modify `qa/app/terragrunt.hcl` to include\nthis alongside the root configuration by using multiple `include` blocks, significantly reducing our per\nenvironment configuration:\n\n```hcl\n# qa/app/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ninclude \"env\" {\n  path = \"${get_terragrunt_dir()}/../../_env/app.hcl\"\n}\n\ninputs = {\n  env = \"qa\"\n}\n```\n\n## Using exposed includes\n\nIn the previous section, we covered using `include` to DRY common component configurations. While powerful, `include` has\na limitation where the included configuration is statically merged into the child configuration.\n\nIn our example, note that the `_env/app.hcl` file hardcodes the `app` module version to `v0.1.0` (relevant section\npasted below for convenience):\n\n```hcl\n# _env/app.hcl\nterraform {\n  source = \"github.com/<org>/modules.git//app?ref=v0.1.0\"\n}\n\n# ... other blocks omitted for brevity ...\n```\n\nWhat if we want to deploy a different version for each environment? One way you can do this is by redefining the\n`terraform` block in the unit. For instance, to deploy `v0.2.0` in the `qa` environment, you can perform\nthe following:\n\n```hcl\n# qa/app/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ninclude \"env\" {\n  path = \"${get_terragrunt_dir()}/../../_env/app.hcl\"\n}\n\n# Override the terraform.source attribute to v0.2.0\nterraform {\n  source = \"github.com/<org>/modules.git//app?ref=v0.2.0\"\n}\n\ninputs = {\n  env = \"qa\"\n}\n```\n\nWhile this works, we now have duplicated the source URL. To avoid repeating the source URL, we can use exposed includes\nto reference data defined in the parent configurations. To do this, modify the parent configuration to export\nthe source URL as a local variable instead of defining it into the `terraform` block:\n\n```hcl\n# _env/app.hcl\nlocals {\n  source_base_url = \"github.com/<org>/modules.git//app\"\n}\n\n# ... other blocks and attributes omitted for brevity ...\n```\n\nWe then set the `expose` attribute to `true` on the `include` block in the child configuration so that we can reference\nthe data defined in the included configuration. Using that, we can construct the terraform source URL without having to\nrepeat the module source:\n\n```hcl\n# qa/app/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ninclude \"env\" {\n  path   = \"${get_terragrunt_dir()}/../../_env/app.hcl\"\n  expose = true\n}\n\n# Construct the terraform.source attribute using the source_base_url and custom version v0.2.0\nterraform {\n  source = \"${include.env.locals.source_base_url}?ref=v0.2.0\"\n}\n\ninputs = {\n  env = \"qa\"\n}\n```\n\n## Using `read_terragrunt_config`\n\nIn the previous two sections, we covered using `include` to merge Terragrunt configurations through static merges\nwith unit configuration. What if you want included configurations to be dynamic in the context of the unit where they\nare being used?\n\nIn our example, the unit configuration defines the `env` input in its configuration (pasted below for convenience):\n\n```hcl\n# qa/app/terragrunt.hcl\n# ... other blocks omitted for brevity ...\n\ninputs = {\n  env = \"qa\"\n}\n```\n\nWhat if some inputs depend on this `env` input? For example, what if we want to append the `env` to the `name` input\nbefore passing to OpenTofu/Terraform?\n\nOne way to do this is to define the override parameters in the child config instead of the parent:\n\n```hcl\n# qa/app/terragrunt.hcl\n# ... other blocks omitted for brevity ...\n\ninclude \"env\" {\n  path   = \"${get_terragrunt_dir()}/../../_env/app.hcl\"\n  expose = true\n}\n\ninputs = {\n  env      = \"qa\"\n  basename = \"${include.env.locals.basename}-qa\"\n}\n```\n\nWhile this works, you could lose all the DRY advantages of the include block if you have many configurations that depend\non the `env` input. Instead, you can use `read_terragrunt_config` to load additional context when including configurations\nby taking advantage of the folder structure, and define the env-based logic in the included configuration.\n\nTo show this, let's introduce a new `env.hcl` configuration in each environment:\n\n<FileTree>\n\n- live\n  - root.hcl\n  - _env\n    - app.hcl\n    - mysql.hcl\n    - vpc.hcl\n  - prod\n    - env.hcl\n    - app\n      - terragrunt.hcl\n    - mysql\n      - terragrunt.hcl\n    - vpc\n      - terragrunt.hcl\n  - qa\n    - env.hcl\n    - app\n      - terragrunt.hcl\n    - mysql\n      - terragrunt.hcl\n    - vpc\n      - terragrunt.hcl\n  - stage\n    - env.hcl\n    - app\n      - terragrunt.hcl\n    - mysql\n      - terragrunt.hcl\n    - vpc\n      - terragrunt.hcl\n\n</FileTree>\n\nThe `env.hcl` configuration will look like the following:\n\n```hcl\n# qa/env.hcl\nlocals {\n  env = \"qa\" # this will be prod in the prod folder, and stage in the stage folder.\n}\n```\n\nWe can then read the `env.hcl` file in the included `_env/app.hcl` file and use the `env` local:\n\n```hcl\n# _env/app.hcl\nlocals {\n  # Load the relevant env.hcl file based on where the including unit is.\n  # This works because find_in_parent_folders always runs in the context of the unit.\n  env_vars = read_terragrunt_config(find_in_parent_folders(\"env.hcl\"))\n  env_name = local.env_vars.locals.env\n\n  source_base_url = \"github.com/<org>/modules.git//app\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n\ndependency \"mysql\" {\n  config_path = \"../mysql\"\n}\n\ninputs = {\n  env            = local.env_name\n  basename       = \"example-app-${local.env_name}\"\n  vpc_id         = dependency.vpc.outputs.vpc_id\n  subnet_ids     = dependency.vpc.outputs.subnet_ids\n  mysql_endpoint = dependency.mysql.outputs.endpoint\n}\n```\n\nWith this configuration, the `env_vars` local is set based on the location of the unit.\n\nFor example, when Terragrunt is run in the context of the `prod/app` unit, `prod/env.hcl` is read,\nwhile `qa/env.hcl` is read when Terragrunt is run in the `qa/app` unit.\n\nNow we can clean up the child config to eliminate the `env` input variable, since that is loaded in the `env.hcl` context:\n\n```hcl\n# qa/app/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ninclude \"env\" {\n  path   = \"${get_terragrunt_dir()}/../../_env/app.hcl\"\n  expose = true\n}\n\n# Construct the terraform.source attribute using the source_base_url and custom version v0.2.0\nterraform {\n  source = \"${include.env.locals.source_base_url}?ref=v0.2.0\"\n}\n```\n\n## Considerations for CI/CD Pipelines\n\nFor infrastructure CI/CD pipelines, it is common to only want to run the workflow on the modules that were updated. For\nexample, if you only changed the unit configuration for the RDS database in the dev account, then you only\nwant to run `plan` and `apply` on that module, not other components or other accounts.\n\nIf you did not take advantage of `include` or `read_terragrunt_config`, then implementing this pipeline is\nstraightforward: you can use `git diff` to collect all the files that changed, and for those `terragrunt.hcl` files that\nwere updated, you can run `terragrunt plan` or `terragrunt apply` in that unit.\n\nHowever, if you use `include` or `read_terragrunt_config`, then a single file change may need to be reflected on\nmultiple files that were not touched at all in the commit. In our previous example, when a configuration is updated in\nthe `_env/app.hcl` file, we need to apply the change to all the modules that `include` that common environment\nconfiguration.\n\nThe most comprehensive approach to managing this is to use the [--queue-include-units-reading](https://docs.terragrunt.com/reference/cli-options/#queue-include-units-reading)\nflag. This flag will automatically add all units that read the file to the queue of units to be run. This includes\nboth units that include the file, and units that read the file using something like `read_terragrunt_config` (make\nto read the documentation on this so that you know the limitations of this flag).\n\nIn the previous example, your CI/CD pipeline can run:\n\n```bash\nterragrunt run --all plan --queue-include-units-reading _env/app.hcl\n```\n\nThis will:\n\n- Recursively find all Terragrunt units in the current directory tree.\n- Filter out any units that don't include `_env/app.hcl` so that they won't be run.\n- Run `plan` on any modules remaining (which will be the set of units in the current tree that include\n  `_env/app.hcl`).\n\nThereby allowing you to only run those modules that need to be updated by the code change.\n\nAlternatively, you can implement a promotion workflow if you have multiple environments that depend on the\n`_env/app.hcl` configuration. In the above example, suppose you wanted to progressively roll out the changes through the\nenvironments, `qa`, `stage`, and `prod` in order. In this case, you can use `--working-dir` to scope down the\nupdates from the common file:\n\n```bash\n# Roll out the change to the qa environment first\nterragrunt run --all plan --queue-include-units-reading _env/app.hcl --working-dir qa\nterragrunt run --all apply --queue-include-units-reading _env/app.hcl --working-dir qa\n# If the apply succeeds to qa, move on to the stage environment\nterragrunt run --all plan --queue-include-units-reading _env/app.hcl --working-dir stage\nterragrunt run --all apply --queue-include-units-reading _env/app.hcl --working-dir stage\n# And finally, prod.\nterragrunt run --all plan --queue-include-units-reading _env/app.hcl --working-dir prod\nterragrunt run --all apply --queue-include-units-reading _env/app.hcl --working-dir prod\n```\n\nThis allows you to have flexibility in how changes are rolled out. For example, you can add extra validation stages\nin-between the roll-out to each environment, or add in manual approval between the stages.\n\n**NOTE**: If you identify an issue with rolling out the change in a downstream environment, and want to abort, you will\nneed to make sure that that environment uses the older version of the common configuration. This is because the common\nconfiguration is now partially rolled out, where some environments need to use the new updated common configuration,\nwhile other environments need the old one. The best way to handle this situation is to create a new copy of the common\nconfiguration at the old version and have the environments that depend on the older version point to that version.\n"
  },
  {
    "path": "docs/src/content/docs/03-features/01-units/03-state-backend.mdx",
    "content": "---\ntitle: State Backend\ndescription: Learn how Terragrunt can create and manage remote state backends.\nslug: features/units/state-backend\nsidebar:\n  order: 3\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Aside } from '@astrojs/starlight/components';\n\nOpenTofu/Terraform supports [remote state storage](https://opentofu.org/docs/language/state/remote/) via various [backends](https://opentofu.org/docs/language/settings/backends/configuration/) that you normally configure in your `.tf` files as follows:\n\n```hcl\n# main.tf\nterraform {\n  backend \"s3\" {\n    bucket         = \"my-tofu-state\"\n    key            = \"frontend-app/tofu.tfstate\"\n    region         = \"us-east-1\"\n    encrypt        = true\n    dynamodb_table = \"my-lock-table\"\n  }\n}\n```\n\nUnfortunately, the `backend` configuration does not currently support expressions, variables, or functions. This makes it hard to keep your code [DRY](/getting-started/terminology/#dont-repeat-yourself-dry) if you have multiple OpenTofu/Terraform modules. For example, consider the following filesystem layout, which uses different OpenTofu/Terraform modules to deploy a backend app, frontend app, MySQL database, and a VPC:\n\n<FileTree>\n\n- backend-app\n  - main.tf\n- frontend-app\n  - main.tf\n- mysql\n  - main.tf\n- vpc\n  - main.tf\n\n</FileTree>\n\nTo use remote state with each of these modules, you would have to copy/paste the identical `backend` configuration into each of the `main.tf` files. The only thing that would differ between the configurations would be the `key` parameter: e.g., the `key` for `mysql/main.tf` might be `mysql/terraform.tfstate` and the `key` for `frontend-app/main.tf` might be `frontend-app/terraform.tfstate`.\n\nIn addition, the resources used for remote state will be provisioned _somewhere else_, and that _somewhere else_ needs to be managed. Most users end up using \"click-ops\" to provision the S3 bucket and DynamoDB table used for AWS remote state (clicking around in the AWS console until they have what they need). This is error-prone, difficult to reproduce, and makes it hard to do the _right thing_ consistently (e.g., enabling versioning, encryption, and access logging).\n\nLuckily, Terragrunt has built-in tooling to make it easy to manage remote state.\n\n## Generating remote state settings with Terragrunt\n\nTo fill in the settings via Terragrunt, create a `root.hcl` file in the root folder, plus one `terragrunt.hcl` file in each of the OpenTofu/Terraform modules:\n\n<FileTree>\n\n- root.hcl\n- backend-app\n  - main.tf\n  - terragrunt.hcl\n- frontend-app\n  - main.tf\n  - terragrunt.hcl\n- mysql\n  - main.tf\n  - terragrunt.hcl\n- vpc\n  - main.tf\n  - terragrunt.hcl\n\n</FileTree>\n\nIn your `root.hcl` file, you can define your entire remote state configuration just once in a `generate` block, to generate a `backend.tf` file that includes the backend configuration:\n\n```hcl\n# root.hcl\ngenerate \"backend\" {\n  path      = \"backend.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nterraform {\n  backend \"s3\" {\n    bucket         = \"my-tofu-state\"\n    key            = \"${path_relative_to_include()}/tofu.tfstate\"\n    region         = \"us-east-1\"\n    encrypt        = true\n    dynamodb_table = \"my-lock-table\"\n  }\n}\nEOF\n}\n```\n\n<Aside type=\"tip\" title=\"Windows compatibility\">\n\nTo ensure compatibility with Windows systems, you may want to use `replace` to ensure that `\\` characters are replaced with `/` characters in the `key` parameter.\n\n```hcl\n# root.hcl\n\ngenerate \"backend\" {\n  path      = \"backend.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nterraform {\n  backend \"s3\" {\n    bucket         = \"my-tofu-state\"\n    key            = \"${replace(path_relative_to_include(), \"\\\\\", \"/\")}/tofu.tfstate\"\n    region         = \"us-east-1\"\n    encrypt        = true\n    dynamodb_table = \"my-lock-table\"\n  }\n}\nEOF\n}\n```\n\n</Aside>\n\nThis instructs Terragrunt to create the file `backend.tf` in the working directory (where Terragrunt calls `tofu`/`terraform`)\nbefore it runs any OpenTofu/Terraform commands, including `init`. This allows you to inject this backend configuration\nin all the units that include the root file and have `terragrunt` properly initialize the backend configuration with\ninterpolated values.\n\nTo inherit this configuration in each unit, such as `mysql/terragrunt.hcl`, you can\nconfigure Terragrunt units to automatically include all the settings from the root `root.hcl` file as follows:\n\n```hcl\n# mysql/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n```\n\nThe `include` block here configures the `mysql` Terragrunt unit to merge in partial configurations from the `root.hcl` file specified via the `path` parameter. It behaves exactly as if you had copy/pasted the `generate` configuration into `mysql/terragrunt.hcl`, but this approach is much easier to maintain\\!\n\nThe next time you run `terragrunt`, it will automatically configure all the settings for the backend, if they aren't configured already, by calling [tofu/terraform init](https://opentofu.org/docs/cli/commands/init/).\n\nThe `terragrunt.hcl` files above use two Terragrunt built-in functions:\n\n- `find_in_parent_folders()`: This function returns the absolute path to the first file it finds in the parent folders above the current unit with a given file name. In the example above, the call to `find_in_parent_folders(\"root.hcl\")` in `mysql/terragrunt.hcl` will return `/absolute/path/to/root.hcl`. This way, you don't have to hard code the `path` parameter in every unit.\n\n- `path_relative_to_include()`: This function returns the relative path between the unit and the path specified in its `include` block. We typically use this in a root `root.hcl` file so that each unit stores its OpenTofu/Terraform state at a different `key`. For example, the `mysql` unit will have its `key` parameter resolve to `mysql/tofu.tfstate` and the `frontend-app` module will have its `key` parameter resolve to `frontend-app/tofu.tfstate`.\n\nRead [Functions docs](/reference/hcl/functions) for more info.\n\n## Create remote state resources automatically\n\nThe `generate` block is useful for allowing you to set up the remote state backend configuration automatically, but\nthis introduces a bootstrapping problem: how do you create and manage the underlying storage resources for the remote\nstate? For example, when using the [s3 backend](https://opentofu.org/docs/language/settings/backends/s3/), OpenTofu/Terraform\nexpects that the S3 bucket already exists for it to upload/download the state objects.\n\nIdeally, you can manage the S3 bucket using OpenTofu/Terraform, but what about the state object for the module managing the S3\nbucket? How do you create the S3 bucket, before you run `tofu`/`terraform`, if you need to run `tofu`/`terraform` to create the\nbucket?\n\nTo handle this, Terragrunt supports a different block for managing the backend configuration: the [remote_state\nblock](/reference/hcl/blocks/#remote_state).\n\n<Aside type=\"note\" title=\"Remote state vs generate\">\n\n`remote_state` is an alternative way of managing the OpenTofu/Terraform backend compared to `generate`. You cannot use both\nmethods at the same time to manage the remote state configuration. When using `remote_state`, be sure not also to use\nan equivalent `generate` block for managing the backend.\n\n</Aside>\n\nThe following backends are currently supported by `remote_state`:\n\n- [s3 backend](https://opentofu.org/docs/language/settings/backends/s3)\n- [gcs backend](https://opentofu.org/docs/language/settings/backends/gcs)\n\nFor all other backends, the `remote_state` block operates in the same manner as `generate`, currently. If you are not intending for Terragrunt to automatically perform\nautomated bootstrapping of remote state resources, you are advised to use `generate` blocks to configure the OpenTofu/Terraform backend instead.\n\nHere is an example of using the `remote_state` block to configure the S3 backend using both an S3 bucket and a DynamoDB table:\n\n```hcl\n# root.hcl\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    bucket         = \"my-terraform-state\"\n    key            = \"${path_relative_to_include()}/terraform.tfstate\"\n    region         = \"us-east-1\"\n    encrypt        = true\n    dynamodb_table = \"my-lock-table\"\n  }\n}\n```\n\nLike the approach with `generate` blocks, this will generate a `backend.tf` file that contains the remote state\nconfiguration. However, in addition to that, Terragrunt will also now manage the S3 bucket and DynamoDB table for you.\nThis means that if the S3 bucket `my-terraform-state` and DynamoDB table `my-lock-table` does not exist in your account,\nTerragrunt will automatically create these resources before calling `terraform` and configure them based on the\nspecified configuration parameters.\n\nWhen you run `terragrunt` with a `remote_state` configuration, it will automatically create the following resources if they don't already exist:\n\n### S3 Backend\n\n- **S3 bucket**: If you are using the [S3 backend](https://opentofu.org/docs/language/settings/backends/s3) for remote state storage and the `bucket` you specify in `remote_state.config` doesn't already exist, Terragrunt will create it automatically, with [versioning](https://docs.aws.amazon.com/AmazonS3/latest/dev/Versioning.html), [server-side encryption](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingServerSideEncryption.html), and [access logging](https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerLogs.html) enabled.\n\n  In addition, you can let Terragrunt tag the bucket with custom tags that you specify in `remote_state.config.s3_bucket_tags`.\n\n- **DynamoDB table**: If you are using the [S3 backend](https://opentofu.org/docs/language/settings/backends/s3) for remote state storage and/or you specify a `dynamodb_table` (a [DynamoDB table used for locking](https://opentofu.org/docs/language/settings/backends/s3/#dynamodb-state-locking)) in `remote_state.config`, Terragrunt will create them automatically if they don't already exist. They will be created with [server-side encryption](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/EncryptionAtRest.html) enabled, and the DynamoDB table will use the primary key `LockID`.\n\n  You may configure a custom endpoint for the AWS DynamoDB API using `remote_state.config.dynamodb_endpoint`.\n\n  In addition, you can let Terragrunt tag the DynamoDB table with custom tags that you specify in `remote_state.config.dynamodb_table_tags`.\n\n- **Access logging bucket**: If you specify an `accesslogging_bucket_name` in `remote_state.config` and that bucket doesn't already exist, Terragrunt will create it automatically. You can tag the access logging bucket with custom tags using `remote_state.config.accesslogging_bucket_tags`, and control the log object prefix using `accesslogging_target_prefix`.\n\n<Aside type=\"tip\" title=\"Using profiles\">\n\nIf you specify a `profile` key in `remote_state.config`, Terragrunt will automatically use this AWS profile when creating the S3 bucket, DynamoDB table, or access logging bucket.\n\n</Aside>\n\nIf you are using an OpenTofu/Terraform version that supports S3-based lockfiles, you can also use the following to only provision the S3 backend:\n\n```hcl\n# root.hcl\n\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket       = \"my-tofu-state\"\n    key          = \"${path_relative_to_include()}/tofu.tfstate\"\n    region       = \"us-east-1\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n```\n\nAdditionally, for **the S3 backend only**, Terragrunt will automatically update the S3 resource to match the\nconfiguration specified in the `remote_state` bucket. For example, if you require versioning in the `remote_state`\nblock, but the underlying state bucket doesn't have versioning enabled, Terragrunt will automatically turn on versioning\non the bucket to match the configuration.\n\nIf you do not want Terragrunt to automatically apply changes, you can configure the following:\n\n```hcl\n# root.hcl\nremote_state {\n  # ... other args omitted for brevity ...\n  config = {\n    # ... other config omitted for brevity ...\n    disable_bucket_update = true\n  }\n}\n```\n\n#### Advanced configuration\n\n<Aside type=\"caution\" title=\"DEPRECATED\">\nThe `skip_bucket_accesslogging` option, which previously disabled access logging on the state bucket, is now deprecated. Use `accesslogging_bucket_name` instead to explicitly configure the target logging bucket.\n</Aside>\n\nFor the `s3` backend, the following additional config options can be used for S3-compatible object stores, as necessary:\n\n```hcl\n# root.hcl\nremote_state {\n  # ...\n\n  config = {\n    skip_bucket_versioning         = true\n    skip_bucket_ssencryption       = true\n    skip_bucket_root_access        = true\n    skip_bucket_enforced_tls       = true\n    skip_credentials_validation    = true\n    enable_lock_table_ssencryption = true\n    accesslogging_bucket_name      = \"my-logs-bucket\"\n    accesslogging_target_prefix    = \"TFStateLogs/\"\n    shared_credentials_file        = \"/path/to/credentials/file\"\n    skip_metadata_api_check        = true\n    force_path_style               = true\n  }\n}\n```\n\n- `skip_bucket_versioning` — Skip versioning on the state bucket. Use only if the object store does not support versioning.\n- `skip_bucket_ssencryption` — Skip server-side encryption on the state bucket. Use only if non-encrypted state is required or the object store does not support server-side encryption.\n- `skip_bucket_root_access` — Skip granting the AWS account root user access to the state bucket.\n- `skip_bucket_enforced_tls` — Skip enforcing TLS on the state bucket. Use only if you need to access the S3 bucket without TLS being enforced.\n- `skip_credentials_validation` — Skip validation of AWS credentials. Useful when using S3-compatible object stores other than AWS.\n- `enable_lock_table_ssencryption` — Enable server-side encryption on the DynamoDB lock table.\n- `accesslogging_bucket_name` — Name of the target bucket for server access logging on the state bucket.\n- `accesslogging_target_prefix` — Prefix for access log objects. Defaults to `TFStateLogs/`. Set to an empty string to disable the prefix.\n- `shared_credentials_file` — Path to a shared credentials file for AWS authentication.\n- `skip_metadata_api_check` — Skip the AWS metadata API check.\n- `force_path_style` — Force S3 path-style URLs instead of virtual-hosted-style.\n\nIf you experience an error for any of these configurations, confirm you are using OpenTofu or Terraform v0.12.2 or greater.\n\nThe following config options are only valid for the `s3` backend and are used by Terragrunt — they are **not** passed on to OpenTofu/Terraform:\n\n- `s3_bucket_tags`\n- `dynamodb_table_tags`\n- `accesslogging_bucket_tags`\n- `skip_bucket_versioning`\n- `skip_bucket_ssencryption`\n- `skip_bucket_root_access`\n- `skip_bucket_enforced_tls`\n- `skip_bucket_public_access_blocking`\n- `accesslogging_bucket_name`\n- `accesslogging_target_prefix`\n- `enable_lock_table_ssencryption`\n\n### GCS Backend\n\n- **GCS bucket**: If you are using the [GCS backend](https://opentofu.org/docs/language/settings/backends/gcs) for remote state storage and the `bucket` you specify in `remote_state.config` doesn't already exist, Terragrunt will create it automatically, with [versioning](https://cloud.google.com/storage/docs/object-versioning) enabled. For this to work correctly you must also specify `project` and `location` keys in `remote_state.config`, so Terragrunt knows where to create the bucket. You will also need to supply valid credentials using either `remote_state.config.credentials` or by setting the `GOOGLE_APPLICATION_CREDENTIALS` environment variable. If you want to skip creating the bucket entirely, simply set `skip_bucket_creation` to `true` and Terragrunt will assume the bucket has already been created. If you don't specify `bucket` in `remote_state` then Terragrunt will assume that you will pass `bucket` through `-backend-config` in `extra_arguments`.\n\n  You are strongly advised to enable [Cloud Audit Logs](https://cloud.google.com/storage/docs/access-logs) to audit and track API operations performed against the state bucket.\n\n  In addition, you can let Terragrunt label the bucket with custom labels that you specify in `remote_state.config.gcs_bucket_labels`.\n\n#### Advanced configuration\n\nFor the `gcs` backend, the following additional config options can be used for GCS-compatible object stores, as necessary:\n\n```hcl\n# root.hcl\nremote_state {\n  # ...\n\n  config = {\n    skip_bucket_versioning    = true\n    enable_bucket_policy_only = false\n    encryption_key            = \"GOOGLE_ENCRYPTION_KEY\"\n  }\n}\n```\n\n- `skip_bucket_versioning` — Skip versioning on the state bucket. Use only if the object store does not support versioning.\n- `enable_bucket_policy_only` — Enable uniform bucket-level access. See [uniform bucket-level access](https://cloud.google.com/storage/docs/uniform-bucket-level-access) for details.\n- `encryption_key` — A Cloud KMS key name to use for encrypting state objects.\n\nIf you experience an error for any of these configurations, confirm you are using Terraform v0.12.0 or greater.\n\nThe following config options are only valid for the `gcs` backend and are used by Terragrunt — they are **not** passed on to OpenTofu/Terraform:\n\n- `gcs_bucket_labels`\n- `skip_bucket_versioning`\n- `enable_bucket_policy_only`\n\n### Disabling automatic remote state bootstrapping\n\nYou can disable automatic remote state bootstrapping by setting `remote_state.disable_init` (it's called this for legacy reasons). This will skip the automatic creation of remote state resources (S3 buckets, DynamoDB tables, GCS buckets) by Terragrunt, while still allowing OpenTofu/Terraform to initialize already provisioned backends normally. This can be handy when running commands such as `run --all validate` as part of a CI process where you do not want Terragrunt to create or modify remote state resources.\n\n<Aside type=\"caution\">\nWhen `disable_init` is `true`, backend resources must already exist, and valid credentials to access the backend are still required by OpenTofu/Terraform.\n</Aside>\n\nIn previous versions, `disable_init = true` passed `-backend=false` to `terraform init`, preventing OpenTofu/Terraform from initializing the backend entirely. The new behavior passes `-backend-config=KEY=VALUE` arguments instead — OpenTofu/Terraform **will** attempt to connect to the backend. Ensure backend resources exist and credentials are valid before using `disable_init = true`.\n\nNote that `--backend-bootstrap` defaults to `false`, so Terragrunt does not create backend resources by default regardless of `disable_init`. The `disable_init` flag additionally prevents bootstrap even when `--backend-bootstrap` is explicitly set.\n\n### Skipping backend initialization entirely\n\nIf you need to skip backend initialization entirely (the previous behavior of `disable_init`), pass `-backend=false` directly to OpenTofu/Terraform via `extra_arguments`:\n\n```hcl\nterraform {\n  extra_arguments \"no_backend_init\" {\n    commands  = [\"init\"]\n    arguments = [\"-backend=false\"]\n  }\n}\n```\n\nOr directly on the command line: `terragrunt run -- init -backend=false`\n\nThe following example demonstrates using an environment variable to configure `disable_init`:\n\n```hcl\n# root.hcl\nremote_state {\n  # ...\n\n  disable_init = tobool(get_env(\"TG_DISABLE_INIT\", \"false\"))\n}\n```\n\n## Further reading\n\n- Managing your remote state like this is really valuable when you organize your units into a [stack](/features/stacks). Reading about those concepts will help you understand how to organize your infrastructure such that different units stored in isolated state can interact with each other.\n- See the [`remote_state` block reference](/reference/hcl/blocks/#remote_state) for the full list of supported attributes.\n- See the [`generate` block reference](/reference/hcl/blocks/#generate) for details on how code generation works.\n- Check out the [terragrunt-infrastructure-catalog-example](https://github.com/gruntwork-io/terragrunt-infrastructure-catalog-example) and [terragrunt-infrastructure-live-stacks-example](https://github.com/gruntwork-io/terragrunt-infrastructure-live-stacks-example) repos for fully-working sample code that demonstrates how to use Terragrunt to manage remote state.\n"
  },
  {
    "path": "docs/src/content/docs/03-features/01-units/04-extra-arguments.mdx",
    "content": "---\ntitle: Extra Arguments\ndescription: Learn how to pass extra arguments to every OpenTofu/Terraform run.\nslug: features/units/extra-arguments\nsidebar:\n  order: 4\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Aside } from '@astrojs/starlight/components';\n\nSometimes you need to pass extra CLI arguments every time you run certain `tofu`/`terraform` commands.\n\nFor example, you may want to set the `lock-timeout` setting to 20 minutes for all commands that may modify remote state so that OpenTofu/Terraform will keep trying to acquire a lock for up to 20 minutes if someone else already has the lock rather than immediately exiting with an error.\n\nYou can configure Terragrunt to pass specific CLI arguments for specific commands using an `extra_arguments` block in your `terragrunt.hcl` file:\n\n``` hcl\n# terragrunt.hcl\n\nterraform {\n  # Force OpenTofu/Terraform to keep trying to acquire a lock for\n  # up to 20 minutes if someone else already has the lock\n  extra_arguments \"retry_lock\" {\n    commands = [\n      \"init\",\n      \"apply\",\n      \"refresh\",\n      \"import\",\n      \"plan\",\n      \"taint\",\n      \"untaint\"\n    ]\n\n    arguments = [\n      \"-lock-timeout=20m\"\n    ]\n\n    env_vars = {\n      TF_VAR_var_from_environment = \"value\"\n    }\n  }\n}\n```\n\nEach `extra_arguments` block includes:\n\n- A **label** — an arbitrary name for the block (in the example above, `retry_lock`)\n- **`commands`** — a list of OpenTofu/Terraform commands to which the extra arguments should be added\n- **`arguments`**, **`required_var_files`**, or **`optional_var_files`** — the extra arguments or var-files to pass\n\nYou can also pass custom environment variables using the `env_vars` attribute, which stores environment variables in key value pairs. With the configuration above, when you run `terragrunt apply`, Terragrunt will call OpenTofu/Terraform as follows:\n\n```bash\n$ terragrunt apply\n# tofu apply -lock-timeout=20m\n```\n\nYou can even use built-in functions such as [`get_terraform_commands_that_need_locking`](/reference/hcl/functions/#get_terraform_commands_that_need_locking) to conveniently populate the list of OpenTofu/Terraform commands that need locking:\n\n``` hcl\n# terragrunt.hcl\n\nterraform {\n  # Force OpenTofu/Terraform to keep trying to acquire a lock for up to 20 minutes if someone else already has the lock\n  extra_arguments \"retry_lock\" {\n    commands  = get_terraform_commands_that_need_locking()\n    arguments = [\"-lock-timeout=20m\"]\n  }\n}\n```\n\nTerragrunt provides four similar helper functions for populating the `commands` list:\n\n- [`get_terraform_commands_that_need_locking`](/reference/hcl/functions/#get_terraform_commands_that_need_locking)\n- [`get_terraform_commands_that_need_vars`](/reference/hcl/functions/#get_terraform_commands_that_need_vars)\n- [`get_terraform_commands_that_need_input`](/reference/hcl/functions/#get_terraform_commands_that_need_input)\n- [`get_terraform_commands_that_need_parallelism`](/reference/hcl/functions/#get_terraform_commands_that_need_parallelism)\n\n## Multiple extra_arguments blocks\n\nYou can specify one or more `extra_arguments` blocks. The `arguments` in each block will be applied any time you call `terragrunt` with one of the commands in the `commands` list. If more than one `extra_arguments` block matches a command, the arguments will be added in the order of appearance in the configuration. For example, in addition to lock settings, you may also want to pass custom `-var-file` arguments to several commands:\n\n``` hcl\n# terragrunt.hcl\n\nterraform {\n  # Force OpenTofu/Terraform to keep trying to acquire a lock for\n  # up to 20 minutes if someone else already has the lock\n  extra_arguments \"retry_lock\" {\n    commands  = get_terraform_commands_that_need_locking()\n    arguments = [\"-lock-timeout=20m\"]\n  }\n\n  # Pass custom var files to OpenTofu/Terraform\n  extra_arguments \"custom_vars\" {\n    commands = [\n      \"apply\",\n      \"plan\",\n      \"import\",\n      \"push\",\n      \"refresh\"\n    ]\n\n    arguments = [\n      \"-var\", \"foo=bar\",\n      \"-var\", \"region=us-west-1\"\n    ]\n  }\n}\n```\n\nWith the configuration above, when you run `terragrunt apply`, Terragrunt will call OpenTofu/Terraform as follows:\n\n```bash\n$ terragrunt apply\n# tofu apply -lock-timeout=20m -var foo=bar -var region=us-west-1\n```\n\n## `extra_arguments` for `init`\n\nExtra arguments for the `init` command have some additional behavior and constraints.\n\nIn addition to being appended to the `tofu init`/`terraform init` command that is run when you explicitly run `terragrunt init`, `extra_arguments` for `init` will also be appended to the `init` commands that are automatically run during other commands (see [Auto-Init](/features/units/auto-init)).\n\n<Aside type=\"caution\">\nYou must *not* specify the `-from-module` option or the `DIR` argument in the `extra_arguments` for `init`. This option and argument will be provided automatically by Terragrunt.\n</Aside>\n\nHere’s an example of configuring `extra_arguments` for `init` in an environment in which OpenTofu/Terraform plugins are manually installed, rather than relying on OpenTofu/Terraform to automatically download them.\n\n``` hcl\n# terragrunt.hcl\n\nterraform {\n  # ...\n\n  extra_arguments \"init_args\" {\n    commands = [\n      \"init\"\n    ]\n\n    arguments = [\n      \"-plugin-dir=/my/tofu/plugin/dir\",\n    ]\n  }\n}\n```\n\n<Aside type=\"tip\">\nYou're encouraged to use one of the [Automatic Provider Cache Dir](/features/caching/auto-provider-cache-dir) or [Provider Cache Server](/features/caching/provider-cache-server) features instead of manually installing plugins in most cases.\n</Aside>\n\n## Required and optional var-files\n\nOne common usage of `extra_arguments` is to include tfvars files. Instead of using `arguments`, you can use `required_var_files` or `optional_var_files`. Both add `-var-file=<your file>` for each file specified, but they differ in how they handle missing files:\n\n- **`required_var_files`** — all listed files must exist. Terragrunt will exit with an error if any are missing.\n- **`optional_var_files`** — files that don’t exist are silently skipped. This is useful for conditional configurations based on environment variables.\n\nHere’s an example:\n\n<FileTree>\n\n- root.hcl\n- prod.tfvars\n- us-west-2.tfvars\n- backend-app\n  - main.tf\n  - dev.tfvars\n  - terragrunt.hcl\n- frontend-app\n  - main.tf\n  - us-east-1.tfvars\n  - terragrunt.hcl\n\n</FileTree>\n\n``` hcl\n# backend-app/terragrunt.hcl\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  extra_arguments \"conditional_vars\" {\n    commands = [\n      \"apply\",\n      \"plan\",\n      \"import\",\n      \"push\",\n      \"refresh\"\n    ]\n\n    required_var_files = [\n      \"${get_parent_terragrunt_dir()}/tofu.tfvars\"\n    ]\n\n    optional_var_files = [\n      \"${get_parent_terragrunt_dir(\"root\")}/${get_env(\"TF_VAR_env\", \"dev\")}.tfvars\",\n      \"${get_parent_terragrunt_dir(\"root\")}/${get_env(\"TF_VAR_region\", \"us-east-1\")}.tfvars\",\n      \"${get_terragrunt_dir()}/${get_env(\"TF_VAR_env\", \"dev\")}.tfvars\",\n      \"${get_terragrunt_dir()}/${get_env(\"TF_VAR_region\", \"us-east-1\")}.tfvars\"\n    ]\n  }\n}\n```\n\nSee the [`get_terragrunt_dir()`](/reference/hcl/functions/#get_terragrunt_dir) and [`get_parent_terragrunt_dir()`](/reference/hcl/functions/#get_parent_terragrunt_dir) documentation for more details.\n\nWith the configuration above, when you run `terragrunt run --all apply`, Terragrunt will call OpenTofu/Terraform as follows:\n\n```bash\n$ terragrunt run --all apply\n[backend-app]  tofu apply -var-file=/my/tf/tofu.tfvars -var-file=/my/tf/backend-app/dev.tfvars\n[frontend-app] tofu apply -var-file=/my/tf/tofu.tfvars -var-file=/my/tf/frontend-app/us-east-1.tfvars\n\n$ TF_VAR_env=prod terragrunt run --all apply\n[backend-app]  tofu apply -var-file=/my/tf/tofu.tfvars -var-file=/my/tf/prod.tfvars\n[frontend-app] tofu apply -var-file=/my/tf/tofu.tfvars -var-file=/my/tf/prod.tfvars -var-file=/my/tf/frontend-app/us-east-1.tfvars\n\n$ TF_VAR_env=prod TF_VAR_region=us-west-2 terragrunt run --all apply\n[backend-app]  tofu apply -var-file=/my/tf/tofu.tfvars -var-file=/my/tf/prod.tfvars -var-file=/my/tf/us-west-2.tfvars\n[frontend-app] tofu apply -var-file=/my/tf/tofu.tfvars -var-file=/my/tf/prod.tfvars -var-file=/my/tf/us-west-2.tfvars\n```\n\n## Handling whitespace\n\nThe list of arguments cannot include whitespace, so if you need to pass command line arguments that include spaces (e.g. `-var bucket=example.bucket.name`), then each of the arguments will need to be a separate item in the `arguments` list:\n\n``` hcl\n# terragrunt.hcl\n\nterraform {\n  extra_arguments \"bucket\" {\n    commands = [\n      \"apply\",\n      \"plan\",\n      \"import\",\n      \"push\",\n      \"refresh\"\n    ]\n\n    arguments = [\n      \"-var\", \"bucket=example.bucket.name\",\n    ]\n  }\n}\n```\n\nWith the configuration above, when you run `terragrunt apply`, Terragrunt will call OpenTofu/Terraform as follows:\n\n```bash\n$ terragrunt apply\n# tofu apply -var bucket=example.bucket.name\n```\n\n## Further reading\n\n- [`extra_arguments` block reference](/reference/hcl/blocks/#terraform)\n- [Helper functions reference](/reference/hcl/functions/) — includes the `get_terraform_commands_that_need_*` family\n- [Auto-Init feature](/features/units/auto-init) — how Terragrunt automatically runs `init`\n"
  },
  {
    "path": "docs/src/content/docs/03-features/01-units/05-authentication.mdx",
    "content": "---\ntitle: Authentication\ndescription: Learn how Terragrunt helps you automate authentication workflows.\nslug: features/units/authentication\nsidebar:\n  order: 5\n---\n\nimport { Aside, Code } from '@astrojs/starlight/components';\n\nimport authProviderCmdSchema from '../../../../../public/schemas/auth-provider-cmd/v2/schema.json?raw';\n\nAWS is by far the most popular OpenTofu/Terraform provider, and most Terragrunt users are using it to manage AWS infrastructure, at least in part. As a consequence, Terragrunt has a number of features that make it easier to work with AWS.\n\nThe most secure way to manage multiple AWS accounts is to segment infrastructure between [multiple AWS accounts](https://aws.amazon.com/organizations/getting-started/best-practices). Segmenting infrastructure in this way can ensure that developers are not granted permissions they don't need on infrastructure they don't manage. It's also a best practice from a safety perspective, as it helps to prevent accidental changes to sensitive resources like production infrastructure.\n\nWhen working with multiple AWS accounts, a best practice is to temporarily assume roles within those AWS accounts to perform actions using mechanisms like [IAM Identity Center](https://aws.amazon.com/iam/identity-center/) or [OIDC](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_oidc.html). When using these technologies, users don't need any static users or credentials. All access is temporary, and permissions are determined by the role they assume.\n\nThese technologies allow you to securely (and temporarily) acquire secrets for least privilege access to a target AWS account, and perform actions that can only impact that AWS account, limiting blast radius.\n\nThere are a few ways to assume IAM roles when using AWS CLI tools, such as OpenTofu/Terraform:\n\n1. One option is to create a named [profile](http://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html), each with a different [role_arn](http://docs.aws.amazon.com/cli/latest/userguide/cli-roles.html) parameter. You then tell OpenTofu/Terraform which profile to use via the `AWS_PROFILE` environment variable.\n\n   The downside to using profiles is that they can vary between users. One user might have a profile named `dev` that assumes a role in the `dev` account, while another user might have a profile named `development` that assumes the same role. This can lead to confusion and errors when sharing code between users. It also results in a requirement that all users have profiles set up on their local machines.\n\n   Finally, this also presents a problem in CI/CD pipelines, where you typically avoid storing AWS credentials on disk so they are less likely to leak.\n\n2. Another option is to use the [AWS CLI](https://aws.amazon.com/cli/). As a standard operating procedure, users are required to assume a role _before_ invoking OpenTofu/Terraform by running something like `aws sts assume-role --role-arn <ROLE>`, use the output of that command to set the appropriate environment variables, and the tool is run with those temporary credentials stored as environment variables.\n\n   The downside to this approach is that it requires that users know this process and remember to do it correctly every time they want to use OpenTofu/Terraform. It's also a tedious process, and requires several steps to complete correctly.\n\n   Worse yet, it requires that users repeat this process often, as the credentials you get back from the `assume-role` command expire. This is especially problematic if the OpenTofu/Terraform run is expected to take longer than the role assumption duration, and can expire mid-run.\n\n3. A final option is to modify your AWS provider with the [assume_role configuration block](https://search.opentofu.org/provider/hashicorp/aws/latest/docs#assume_role-configuration-block) and your S3 backend with the [role_arn parameter](https://opentofu.org/docs/language/settings/backends/s3/#assume-role-configuration).\n\n   The downside to managing your role assumption with the AWS provider is that all runs have to be performed with the same IAM role. This can be problematic if you have different users who assume different roles, depending on their need for elevated access, and as a best practice, the role assumed by CI/CD pipelines should be different from the role assumed by developers.\n\n   The _way_ in which these roles are assumed also differ, as developers might use a web-based SSO portal to acquire temporary credentials, while CI/CD pipelines might use OIDC and assume a role using a web identity token.\n\n<Aside type=\"caution\">\nTemporary credentials obtained via `assume-role` can expire during long-running OpenTofu/Terraform operations. Terragrunt's built-in role assumption (described below) handles automatic credential refresh, avoiding this problem.\n</Aside>\n\nTo avoid these frustrating trade-offs, you can configure Terragrunt to assume an IAM role for you.\n\n## Configuring Terragrunt to assume an IAM role\n\nTo tell Terragrunt to assume an IAM role, just set the [`--iam-assume-role`](/reference/cli/commands/run#iam-assume-role) command line argument:\n\n```bash\nterragrunt apply --iam-assume-role \"arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME\"\n```\n\nAlternatively, you can set the `TG_IAM_ASSUME_ROLE` environment variable:\n\n```bash\nexport TG_IAM_ASSUME_ROLE=\"arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME\"\nterragrunt apply\n```\n\nAdditionally, you can specify an `iam_role` property in the terragrunt config:\n\n```hcl\n# terragrunt.hcl\n\niam_role = \"arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME\"\n```\n\nTerragrunt will resolve the value of the option by first looking for the cli argument, then looking for the environment variable, then defaulting to the value specified in the config file.\n\nTerragrunt will call the `sts assume-role` API on your behalf and expose the credentials it gets back as environment variables when running OpenTofu/Terraform. The advantage of this approach is that you can store your AWS credentials in a secret store and never write them to disk in plaintext, you get fresh credentials on every run of Terragrunt, without the complexity of calling `assume-role` yourself, and you don't have to modify your OpenTofu/Terraform code or backend configuration at all.\n\n## Leveraging OIDC role assumption\n\nIn addition, you can combine the `--iam-assume-role` flag with the [`--iam-assume-role-web-identity-token`](/reference/cli/commands/run#iam-assume-role-web-identity-token) to use the `AssumeRoleWithWebIdentity` API instead of the `AssumeRole` API.\n\nThis is especially convenient in the context of CI/CD pipelines, as it's generally a best practice to assume roles there via OIDC.\n\nConfiguring OIDC role assumption largely works like the `--iam-assume-role` flag, with the addition of the `--iam-assume-role-web-identity-token` flag.\n\n<Aside type=\"tip\">\nThe `--iam-assume-role-web-identity-token` flag accepts both a raw token value and the path to a file containing the token. Terragrunt will automatically detect which one you've provided.\n</Aside>\n\nAs a command line argument:\n\n```bash\nterragrunt apply --iam-assume-role \"arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME\" --iam-assume-role-web-identity-token \"$TOKEN\"\n```\n\nAs environment variables:\n\n```bash\nexport TG_IAM_ASSUME_ROLE=\"arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME\"\nexport TG_IAM_ASSUME_ROLE_WEB_IDENTITY_TOKEN=\"$TOKEN\"\nterragrunt apply\n```\n\nIn the Terragrunt configuration:\n\n```hcl\n# terragrunt.hcl\n\niam_role = \"arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME\"\niam_web_identity_token = get_env(\"AN_OIDC_TOKEN\")\n```\n\n## Auth provider command\n\nFinally, there is also a special flag that allows you to use an external command to provide the role assumption credentials. This is the most powerful and flexible option for setting up Terragrunt authentication, but it does require a bit more setup.\n\nThis technique is especially useful in the following circumstances:\n\n- In a CI/CD pipelines, where you might want to use different role assumption mechanisms for different stages of the pipeline (like a read-only, plan role during pull request open, and a read-write, apply role during merge).\n- On a shared development repository, where you might want to use different roles for different developers, or even different roles for the same developer, depending on the task at hand.\n- In a setup where units in different accounts depend on each other, and you want to assume a different role for each account.\n\nThe [`--auth-provider-cmd`](/reference/cli/commands/run#auth-provider-cmd) flag allows you to specify a command that can be executed by Terragrunt to fetch credentials at runtime.\n\n```bash\nterragrunt apply --auth-provider-cmd /path/to/auth-script.sh\n```\n\nAs with all other flags, you can also set this as an environment variable:\n\n```bash\nexport TG_AUTH_PROVIDER_CMD=\"/path/to/auth-script.sh\"\nterragrunt apply\n```\n\nWhen Terragrunt executes this script, it will expect a response in stdout that obeys the following schema:\n\n<Code title=\"auth-provider-cmd/v2/schema.json\" lang=\"json\" code={authProviderCmdSchema} />\n\nAll top-level objects are optional, and you can provide multiple.\n\n- `awsCredentials` is the standard AWS credential object, which can be used to set the `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and (optionally) `AWS_SESSION_TOKEN` environment variables before running OpenTofu/Terraform.\n- `awsRole` is the role assumption object, which can be used to dynamically perform role assumption on the `roleARN` role with the `roleSessionName` session name, for a `duration` of time, and with a `webIdentityToken` if needed. Terragrunt will automatically refresh this role assumption when the duration expires.\n- `envs` is a map of environment variables that will be set before running OpenTofu/Terraform.\n\n<Aside type=\"note\">\nThe auth provider command runs in the context of the Terragrunt working directory. You can use this to author logic in your script that determines which credentials to return based on the context of the Terragrunt run.\n</Aside>\n\nThis feature is integrated with the [Gruntwork Pipelines](https://www.gruntwork.io/platform/pipelines) product to provide a secure and flexible way to manage assumption of different roles in different accounts based on context.\n\n## Required Permissions\n\nYou are ultimately responsible for ensuring that the IAM role you are assuming has the minimal and necessary permissions required to perform the activity you are attempting.\n\nAt a minimum, however there is some guidance that you can follow for ensuring that you have sufficient permissions.\n\nGranting the following permissions on an IAM role:\n\n```json\n# permissions.json\n\n{\n    \"Version\": \"2012-10-17\",\n    \"Statement\": [\n        {\n            \"Sid\": \"AllowAllDynamoDBActionsOnAllTerragruntTables\",\n            \"Effect\": \"Allow\",\n            \"Action\": \"dynamodb:*\",\n            \"Resource\": [\n                \"arn:aws:dynamodb:*:1234567890:table/terragrunt*\"\n            ]\n        },\n        {\n            \"Sid\": \"AllowAllS3ActionsOnTerragruntBuckets\",\n            \"Effect\": \"Allow\",\n            \"Action\": \"s3:*\",\n            \"Resource\": [\n                \"arn:aws:s3:::terragrunt*\",\n                \"arn:aws:s3:::terragrunt*/*\"\n            ]\n        }\n    ]\n}\n```\n\nWill grant Terragrunt more than enough permissions to perform what it needs to do in AWS (replacing `1234567890` with your AWS account ID, and `terragrunt*` with the desired names of your Terragrunt resources).\n\nNote that these permissions might be too broad for your circumstances, however. A more minimal policy might look like the following:\n\n```json\n# permissions.json\n\n{\n    \"Version\": \"2012-10-17\",\n    \"Statement\": [\n        {\n            \"Sid\": \"AllowCreateAndListS3ActionsOnSpecifiedTerragruntBucket\",\n            \"Effect\": \"Allow\",\n            \"Action\": [\n                \"s3:ListBucket\",\n                \"s3:GetBucketVersioning\",\n                \"s3:GetBucketAcl\",\n                \"s3:GetBucketLogging\",\n                \"s3:CreateBucket\",\n                \"s3:PutBucketPublicAccessBlock\",\n                \"s3:PutBucketTagging\",\n                \"s3:PutBucketPolicy\",\n                \"s3:PutBucketVersioning\",\n                \"s3:PutEncryptionConfiguration\",\n                \"s3:PutBucketAcl\",\n                \"s3:PutBucketLogging\",\n                \"s3:GetEncryptionConfiguration\",\n                \"s3:GetBucketPolicy\",\n                \"s3:GetBucketPublicAccessBlock\",\n                \"s3:PutLifecycleConfiguration\",\n                \"s3:PutBucketOwnershipControls\"\n            ],\n            \"Resource\": \"arn:aws:s3:::BUCKET_NAME\"\n        },\n        {\n            \"Sid\": \"AllowGetAndPutS3ActionsOnSpecifiedTerragruntBucketPath\",\n            \"Effect\": \"Allow\",\n            \"Action\": [\n                \"s3:PutObject\",\n                \"s3:GetObject\"\n            ],\n            \"Resource\": \"arn:aws:s3:::BUCKET_NAME/some/path/here\"\n        },\n        {\n            \"Sid\": \"AllowCreateAndUpdateDynamoDBActionsOnSpecifiedTerragruntTable\",\n            \"Effect\": \"Allow\",\n            \"Action\": [\n                \"dynamodb:PutItem\",\n                \"dynamodb:GetItem\",\n                \"dynamodb:DescribeTable\",\n                \"dynamodb:DeleteItem\",\n                \"dynamodb:CreateTable\"\n            ],\n            \"Resource\": \"arn:aws:dynamodb:*:*:table/TABLE_NAME\"\n        }\n    ]\n}\n```\n\nAs you can see, the permissions are getting locked down, and the risk you run by adopting these permissions is that you might not realize that you need certain permissions until you run into an error.\n\n<Aside type=\"tip\">\nStart with permissions that are too narrow, and expand them as necessary. It's easier to grant additional permissions when you encounter an error than to audit overly broad permissions later.\n</Aside>\n\nAdditionally, while Terragrunt _can_ provision the S3 bucket and DynamoDB table it uses for S3 state storage, it doesn't _need_ to. You can create these resources manually, then grant Terragrunt permissions to interact with them (but not create them). A policy that allows this would look like the following:\n\n```json\n# permissions.json\n\n{\n    \"Version\": \"2012-10-17\",\n    \"Statement\": [\n        {\n            \"Action\": [\n                \"s3:GetBucketLocation\",\n                \"s3:List*\"\n            ],\n            \"Resource\": [\n                \"arn:aws:s3:::<BucketName>\"\n            ],\n            \"Effect\": \"Allow\"\n        },\n        {\n            \"Action\": [\n                \"s3:DeleteObject\",\n                \"s3:GetObject\",\n                \"s3:PutObject\",\n                \"s3:ListBucket\"\n            ],\n            \"Resource\": [\n                \"arn:aws:s3:::<BucketName>/*\"\n            ],\n            \"Effect\": \"Allow\"\n        },\n        {\n            \"Sid\": \"AllowCreateAndUpdateDynamoDBActionsOnSpecifiedTerragruntTable\",\n            \"Effect\": \"Allow\",\n            \"Action\": [\n                \"dynamodb:PutItem\",\n                \"dynamodb:GetItem\",\n                \"dynamodb:DescribeTable\",\n                \"dynamodb:DeleteItem\",\n            ],\n            \"Resource\": \"arn:aws:dynamodb:*:*:table/TABLE_NAME\"\n        }\n    ]\n}\n```\n\nYou'll want to make sure that you set configurations like `skip_bucket_versioning` in [remote_state](/reference/hcl/blocks#remote_state) to prevent Terragrunt from attempting to validate the bucket or table is in the proper configuration without requisite permissions.\n\n## Further Reading\n\n- [Units Overview](/features/units) — for context on how units work with remote modules and source URLs\n- [State Backend](/features/units/state-backend) — remote state also involves AWS credentials and role assumption\n- [`--iam-assume-role` CLI reference](/reference/cli/commands/run#iam-assume-role) — CLI flag for IAM role assumption\n- [`--auth-provider-cmd` CLI reference](/reference/cli/commands/run#auth-provider-cmd) — CLI flag for external auth provider commands\n"
  },
  {
    "path": "docs/src/content/docs/03-features/01-units/06-hooks.mdx",
    "content": "---\ntitle: Hooks\ndescription: Learn how to execute custom code before or after running OpenTofu/Terraform, or when errors occur.\nslug: features/units/hooks\nsidebar:\n  order: 6\n---\n\nimport { Aside } from \"@astrojs/starlight/components\";\n\n_Before Hooks_, _After Hooks_ and _Error Hooks_ are a feature of terragrunt that make it possible to define custom actions that will be called before/after running an `tofu`/`terraform` command.\n\nThey allow you to _orchestrate_ certain operations around IaC updates so that you have a consistent way to run custom code before or after running OpenTofu/Terraform.\n\nHere’s an example:\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  before_hook \"before_hook\" {\n    commands     = [\"apply\", \"plan\"]\n    execute      = [\"echo\", \"Running OpenTofu\"]\n  }\n\n  after_hook \"after_hook\" {\n    commands     = [\"apply\", \"plan\"]\n    execute      = [\"echo\", \"Finished running OpenTofu\"]\n    run_on_error = true\n  }\n\n  error_hook \"import_resource\" {\n    commands  = [\"apply\"]\n    execute   = [\"echo\", \"Error Hook executed\"]\n    on_errors = [\n      \".*\",\n    ]\n  }\n}\n```\n\nIn this example configuration, whenever Terragrunt runs `tofu apply` or `tofu plan` (or the `terraform` equivalent), three things will happen:\n\n- Before Terragrunt runs `tofu`/`terraform`, it will output `Running OpenTofu` to the console.\n- After Terragrunt runs `tofu`/`terraform`, it will output `Finished running OpenTofu`, regardless of whether the\n  command failed.\n- If an error occurs during the `tofu apply` command, Terragrunt will output `Error Hook executed`.\n\nYou can learn more about all the various configuration options supported in [the reference docs for the terraform\nblock](/reference/hcl/blocks#terraform).\n\n## Hook Context\n\nAll hooks add extra environment variables when executing the hook's run command:\n\n- `TG_CTX_TF_PATH`\n- `TG_CTX_COMMAND`\n- `TG_CTX_HOOK_NAME`\n\nFor example:\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  before_hook \"test_hook\" {\n    commands     = [\"apply\"]\n    execute      = [\"hook.sh\"]\n  }\n}\n```\n\nWhere `hook.sh` is:\n\n```bash\n# hook.sh\n\necho \"TF_PATH=${TG_CTX_TF_PATH} COMMAND=${TG_CTX_COMMAND} HOOK_NAME=${TG_CTX_HOOK_NAME}\"\n```\n\nWill result in the following output when Terragrunt runs `tofu apply`/`terraform apply`:\n\n```bash\nTF_PATH=tofu COMMAND=apply HOOK_NAME=test_hook\n```\n\nNote that hooks are executed within the working directory where OpenTofu/Terraform would be run.\n\nIf using the `source` attribute for the `terraform` block, this will result in the hook running in\nthe hidden `.terragrunt-cache` directory.\n\nThis also means that you can use `tofu`/`terraform` commands within hooks to access any outputs needed\nfor hook logic.\n\nFor example:\n\n```bash\n# Get the bucket_name output from OpenTofu/Terraform state\nBUCKET_NAME=\"$(\"$TG_CTX_TF_PATH\" output -raw bucket_name)\"\n\n# Use the AWS CLI to list the contents of the bucket\naws s3 ls \"s3://$BUCKET_NAME\"\n```\n\nNote that the `TG_CTX_TF_PATH` environment variable is used here to ensure compatibility, regardless of the\nvalue of [tf-path](/reference/cli/commands/run#tf-path). This can be a useful practice\nif you are migrating between OpenTofu or Terraform.\n\nYou will also have access to all the `inputs` set in the `terragrunt.hcl` file as environment variables prefixed\nby `TF_VAR_`, as that's how the variables are set for use in OpenTofu/Terraform.\n\nFor example, if you have the following `inputs` block in your `terragrunt.hcl` file:\n\n```hcl\n# terragrunt.hcl\n\ninputs = {\n  bucket_name = \"my-bucket\"\n}\n```\n\nYou can access the `bucket_name` input in your hook as follows:\n\n```bash\n# Get the bucket_name input from the terragrunt.hcl file\nBUCKET_NAME=\"$TF_VAR_bucket_name\"\n\n# Use the AWS CLI to list the contents of the bucket\naws s3 ls \"s3://$BUCKET_NAME\"\n```\n\n## Orchestrating execution outside IaC\n\nHooks can be used to handle operations that need to happen, but are not directly related to the OpenTofu/Terraform.\n\nFor example, you may be using Terragrunt to manage an [AWS ECS service](https://aws.amazon.com/ecs/).\n\nYou can use a `before_hook` to build and push a new image to the [Elastic Container Registry (ECR)](https://aws.amazon.com/ecr/) before running `tofu apply`.\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  before_hook \"build_and_push_image\" {\n    commands     = [\"plan\", \"apply\"]\n    execute      = [\"./build_and_push_image.sh\"]\n  }\n}\n```\n\nWhere `build_and_push_image.sh` is something like:\n\n```bash\n# build_and_push_image.sh\n\n#!/usr/bin/env bash\n\nset -eou pipefail\n\nACCOUNT_ID=\"123456789012\"\nREGION=\"us-east-1\"\nREPOSITORY=\"my-repository\"\nTAG=\"latest\"\n\nIMAGE_TAG=\"${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPOSITORY}:${TAG}\"\n\n# Build the Docker image\ndocker build -t \"$IMAGE_TAG\" .\n\n# Push the Docker image to ECR\naws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com\ndocker push \"$IMAGE_TAG\"\n```\n\nThe hard-coding of values in the script could be replaced with context, as shown in the previous section.\n\nSimilarly, you may want to smoke-test newly deployed infrastructure after running `tofu apply`.\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  after_hook \"smoke_test\" {\n    commands     = [\"apply\"]\n    execute      = [\"./smoke_test.sh\"]\n    run_on_error = true\n  }\n}\n```\n\nWhere `smoke_test.sh` is something like:\n\n```bash\n# smoke_test.sh\n\n#!/usr/bin/env bash\n\nset -eou pipefail\n\n# Get the URL for the service from OpenTofu/Terraform state\nSERVICE_URL=\"$(\"$TG_CTX_TF_PATH\" output -raw service_url)\"\n\n# Use curl to check the service is up\ncurl -sSf \"$SERVICE_URL\"\n```\n\nYou might even decide to integrate with a product like [Terratest](https://github.com/gruntwork-io/terratest) for more complex testing.\n\n## Hook Ordering\n\nYou can have multiple before and after hooks. Each hook will execute in the order they are defined.\n\nFor example:\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  before_hook \"before_hook_1\" {\n    commands     = [\"apply\", \"plan\"]\n    execute      = [\"echo\", \"Will run OpenTofu\"]\n  }\n\n  before_hook \"before_hook_2\" {\n    commands     = [\"apply\", \"plan\"]\n    execute      = [\"echo\", \"Running OpenTofu\"]\n  }\n}\n```\n\nThis configuration will cause Terragrunt to output `Will run OpenTofu` and then `Running OpenTofu` before the call\nto OpenTofu/Terraform.\n\n## Tflint hook\n\n_Before Hooks_ or _After Hooks_ natively support _tflint_, a linter for OpenTofu/Terraform code. It will validate the\nOpenTofu/Terraform code used by Terragrunt, and its inputs.\n\nThis support includes automatically running `tflint init`, and passing in variables.\n\nHere's an example:\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  before_hook \"before_hook\" {\n    commands     = [\"apply\", \"plan\"]\n    execute      = [\"tflint\"]\n  }\n}\n```\n\nThe `.tflint.hcl` should exist in the same folder as `terragrunt.hcl` or one of it's parents. If Terragrunt can't find\na `.tflint.hcl` file, it won't execute tflint and return an error. All configurations should be in a `config` block in this\nfile, as per [Tflint's docs](https://github.com/terraform-linters/tflint/blob/master/docs/user-guide/config.md).\n\n```hcl\n# .tflint.hcl\n\nplugin \"aws\" {\n    enabled = true\n    version = \"0.21.0\"\n    source  = \"github.com/terraform-linters/tflint-ruleset-aws\"\n}\n\nconfig {\n  module = true\n}\n```\n\n### Configuration\n\nAny desired extra configuration should be added in the `.tflint.hcl` file.\nIt will work with a `.tflint.hcl` file in the current folder or any parent folder.\nTo utilize an alternative configuration file, use the `--config` flag with the path to the configuration file.\n\nFor example:\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n    before_hook \"tflint\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"tflint\", \"--minimum-failure-severity=error\", \"--config\", \"custom.tflint.hcl\"]\n  }\n}\n```\n\nIf you need to bypass the integration behavior (auto init or passing in variables), you can specify an absolute path to `tflint`, as this integration works by checking the first value in the `execute` array:\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  before_hook \"before_hook\" {\n    commands     = [\"apply\", \"plan\"]\n    execute      = [\"/usr/local/bin/tflint\", \"some\", \"args\"]\n  }\n}\n```\n\n### Authentication for tflint rulesets\n\n_Public rulesets_\n\n`tflint` works without any authentication for public rulesets (hosted on public repositories).\n\n_Private rulesets_\n\nIf you want to run the `tflint` hook with custom rulesets defined in a private repository, you will need to export a valid `GITHUB_TOKEN` token.\n\n### Troubleshooting\n\n**`flag provided but not defined: -act-as-bundled-plugin` error**\n\nIf you have an `.tflint.hcl` file that is empty, or uses the `terraform` ruleset without version or source constraint, it can return the following error:\n\n```log\nFailed to initialize plugins; Unrecognized remote plugin message: Incorrect Usage. flag provided but not defined: -act-as-bundled-plugin\n```\n\nTo fix this, make sure that the configuration for the `terraform` ruleset, in the `.tflint.hcl` file contains a version constraint:\n\n```hcl\n# .tflint.hcl\n\nplugin \"terraform\" {\n    enabled = true\n    version = \"0.2.1\"\n    source  = \"github.com/terraform-linters/tflint-ruleset-terraform\"\n}\n```\n"
  },
  {
    "path": "docs/src/content/docs/03-features/01-units/07-auto-init.mdx",
    "content": "---\ntitle: Auto-init\ndescription: Learn how Terragrunt makes it so that you don't have to explicitly call `init` when using it.\nslug: features/units/auto-init\nsidebar:\n  order: 7\n---\n\n*Auto-Init* is a feature of Terragrunt that makes it so that `terragrunt init` does not need to be called explicitly before other terragrunt commands.\n\nWhen Auto-Init is enabled (the default), terragrunt will automatically call [`tofu init`](https://opentofu.org/docs/cli/commands/init/)/[`terraform init`](https://www.terraform.io/docs/commands/init.html) before other commands (e.g. `terragrunt plan`) when terragrunt detects that any of the following are true:\n\n- `init` has never been called.\n- `source` needs to be downloaded.\n- The `.terragrunt-init-required` file is in the downloaded module directory (`.terragrunt-cache/aaa/bbb/modules/<module>`).\n- The modules or remote state used by a module have changed since the previous call to `init`.\n\nAs [mentioned](/features/units/extra-arguments/#extra_arguments-for-init), `extra_arguments` can be configured to allow customization of the `tofu init` command.\n\nNote that there might be cases where Terragrunt does not detect that `tofu init` needs to be called. In such cases, OpenTofu/Terraform may fail, and re-running `terragrunt init` can resolve the issue.\n\n## Disabling Auto-Init\n\nIn some cases, it might be desirable to disable Auto-Init.\n\nFor example, you might want to specify a different `-plugin-dir` option to `tofu init` (and don't want to have it set in `extra_arguments`).\n\nTo disable Auto-Init, use the `--no-auto-init` command line option or set the `TG_NO_AUTO_INIT` environment variable to `true`.\n\nDisabling Auto-Init requires you to explicitly run `terragrunt init` before executing any other Terragrunt commands for that configuration. If Auto-Init is disabled and Terragrunt detects that `init` should have been run, it will throw an error.\n"
  },
  {
    "path": "docs/src/content/docs/03-features/01-units/08-runtime-control.mdx",
    "content": "---\ntitle: Feature Flags, Errors and Excludes\ndescription: Learn how Terragrunt allows for runtime control using feature flags, error handling, and excludes.\nslug: features/units/runtime-control\nsidebar:\n  order: 8\n---\n\nSometimes, you need to have Terragrunt behave differently at runtime due to specific context that you have in your environment.\n\nThe following configuration blocks have been designed to work together in concert to provide you a great deal of flexibility in how Terragrunt behaves at runtime.\n\n## Feature Flags\n\nDefined using the [feature](/reference/hcl/blocks#feature) configuration block, Terragrunt allows for the control of specific features at runtime using feature flags.\n\nFor example:\n\n```hcl\n# terragrunt.hcl\n\nfeature \"s3_version\" {\n  default = \"v1.0.0\"\n}\n\nterraform {\n  source = \"git::git@github.com:acme/infrastructure-modules.git//storage/s3?ref=${feature.s3_version.value}\"\n}\n```\n\nThe configuration above allows you to set a default version for the `s3_version` feature flag, controlling the tag used for fetching the `s3` module from the `infrastructure-modules` repository.\n\nAt runtime, you can override the default value by doing one of the following:\n\n```bash\nterragrunt apply --feature s3_version=v1.1.0\n```\n\nOr by setting the corresponding environment variable:\n\n```bash\nexport TG_FEATURE=\"s3_version=v1.1.0\"\nterragrunt apply\n```\n\nThis can be a useful way to opt in to new features or to test changes in your infrastructure.\n\nSetting a different version of an OpenTofu/Terraform module in a lower environment can be useful for testing changes before rolling them out to production. Users will always use the default version unless they explicitly set a different value.\n\n## Errors\n\nDefined using the [errors](/reference/hcl/blocks#errors) configuration block, Terragrunt allows for fine-grained control of errors at runtime.\n\nFor example:\n\n```hcl\n# terragrunt.hcl\n\nerrors {\n    # Retry block for transient errors\n    retry \"transient_errors\" {\n        retryable_errors = [\".*Error: transient network issue.*\"]\n        max_attempts = 3\n        sleep_interval_sec = 5\n    }\n\n    # Ignore block for known safe-to-ignore errors\n    ignore \"known_safe_errors\" {\n        ignorable_errors = [\n            \".*Error: safe warning.*\",\n            \"!.*Error: do not ignore.*\"\n        ]\n        message = \"Ignoring safe warning errors\"\n        signals = {\n            alert_team = false\n        }\n    }\n}\n```\n\nThis configuration allows for control over how Terragrunt handles errors at runtime.\n\nIn the example above, Terragrunt will retry up to three times with a five-second pause between each retry for any error that matches the regex `.*Error: transient network issue.*`.\n\nIt will also ignore any error that matches the regex `.*Error: safe warning.*`, but will not ignore any error that matches the regex `.*Error: do not ignore.*`.\n\nWhen it ignores an error that it can safely ignore, it will output the message `Ignoring safe warning errors`, and will generate a file named `error-signals.json` in the working directory with the following content:\n\n```json\n# error-signals.json\n\n{\n    \"alert_team\": false\n}\n```\n\nYou can learn more about how this configuration block works in the documentation linked above, but for now, what's important to know is that it allows you to determine what Terragrunt should do when it encounters an error at runtime.\n\nNote that these configurations can also be adjusted dynamically. You can use a combination of feature flags and errors to control how Terragrunt behaves at runtime.\n\nSay, for example, a developer was trying to roll out a new version of your module that is known to be potentially flaky. You want to integrate your new module update with the rest of your team, but you don't want to break runs that aren't ready for the new module.\n\nYou could use a feature flag to control introduction of that new module, and an error block to ignore any errors that you know are safe to ignore.\n\n```hcl\n# terragrunt.hcl\n\nfeature \"enable_flaky_module\" {\n  default = false\n}\n\nlocals {\n  version = feature.enable_flaky_module.value ? \"v1.0.0\" : \"v1.1.0\"\n}\n\nterraform {\n  source = \"git::git@github.com:acme/infrastructure-modules.git//storage/s3?ref=${local.version}\"\n}\n\nerrors {\n    # Ignore errors when set\n    ignore \"flaky_module_errors\" {\n        ignorable_errors = feature.enable_flaky_module.value ? [\n            \".*Error: flaky module error.*\"\n        ] : []\n        message = \"Ignoring flaky module error\"\n        signals = {\n            send_notification = true\n        }\n    }\n}\n```\n\nIn this example, the `enable_flaky_module` feature flag sets _both_ the version of the module, and the error handling behavior for the unit that consumes it. This would allow the developer to integrate the unit configuration update with the rest of the codebase, enable the flag that introduces the module update in a lower environment, and then ignore any errors that are known to be safe to ignore.\n\nThis pattern allows for greater speed of integration with larger codebases, and can be a useful tool for managing risk in your infrastructure.\n\n## Excludes\n\nDefined using the [exclude](/reference/hcl/blocks#exclude) configuration block, Terragrunt allows for the exclusion of specific units at runtime.\n\nFor example:\n\n```hcl\n# terragrunt.hcl\n\nlocals {\n  day_of_week = formatdate(\"EEE\", timestamp())\n  ban_deploy  = contains([\"Fri\", \"Sat\", \"Sun\"], local.day_of_week)\n}\n\nexclude {\n    if = local.ban_deploy\n    actions = [\"apply\", \"destroy\"]\n}\n```\n\nIn this example, the `exclude` block will prevent the `apply` command from running in a given unit on Fridays, Saturdays, and Sundays, as all good DevOps engineers know that deploying that close to a weekend is a recipe for disaster.\n\nWhile a toy example, this demonstrates how you can use the `exclude` block to use dynamic information at runtime to control the [run queue](/getting-started/terminology/#run-queue).\n\nYou can use this block to prevent certain units from running in certain environments, or to prevent certain commands from running in certain units.\n\nNote that, just like with the other blocks mentioned so far, you can use a combination of configurations mentioned here to ensure that Terragrunt behaves exactly as you need it to at runtime.\n\nA more practical use of the `exclude` block would be to control which environments are run in `run --all` commands.\n\nFor example:\n\n```hcl\n# dev/root.hcl\nfeature \"dev\" {\n  default = true\n}\n\nexclude {\n    if = !feature.dev.value\n    actions = [\"all_except_output\"]\n}\n```\n\n```hcl\n# stage/root.hcl\nfeature \"stage\" {\n  default = false\n}\n\nexclude {\n    if = !feature.stage.value\n    actions = [\"all_except_output\"]\n}\n```\n\n```hcl\n# prod/root.hcl\nfeature \"prod\" {\n  default = false\n}\n\nexclude {\n    if = !feature.prod.value\n    actions = [\"all_except_output\"]\n}\n```\n\nIn this example, the `dev`, `stage` and `prod` directories have their own root configurations that are included by all units in their respective environments. The assumption of a configuration like this is that each environment is fully self-contained, and that the team has a desire to always update `dev` units, but wants to opt in changes to `stage` and `prod` units.\n\nIn this setup, any `run --all` command like the following:\n\n```bash\nterragrunt run --all plan\n```\n\nWill exclude all units in both the `stage` and `prod` directories, as the `feature` block in each of those directories is set to `false` by default. As a result, the only units that are run are those in the `dev` directory.\n\nWhen a user wants to opt in to updates for the `stage` environment, they could do something like this:\n\n```bash\nterragrunt run --all --feature stage=true plan\n```\n\nThey can even mix and match feature flags to opt-in/out of multiple environments at once:\n\n```bash\nterragrunt run --all --feature dev=false --feature stage=true --feature prod=true plan\n```\n\nThis allows for a great deal of flexibility in how you programmatically control the behavior of Terragrunt at runtime.\n\n### Exclusion from the Run Queue\n\nThe `exclude` block will only exclude the unit from the run queue, which is only relevant in the context of a `run --all` command.\n\nA user could still explicitly navigate to the unit directory and run the command manually.\n\nIf you would like to explicitly prevent a command from being run, even if a user was to navigate to the unit directory and run the command manually, you can use a combination of the `exclude` block and a `before_hook` block to prevent the command from running.\n\nFor example:\n\n```hcl\n# terragrunt.hcl\n\nlocals {\n  day_of_week = formatdate(\"EEE\", timestamp())\n  ban_deploy  = contains([\"Fri\", \"Sat\", \"Sun\"], local.day_of_week)\n}\n\nexclude {\n    if = local.ban_deploy\n    actions = [\"apply\", \"destroy\"]\n}\n\nterraform {\n  before_hook \"prevent_deploy\" {\n    commands = [\"apply\", \"destroy\"]\n    execute  = local.ban_deploy ? [\"bash\", \"-c\", \"echo 'Deploying on weekends is not allowed. Go home.' && exit 1\"] : []\n  }\n}\n```\n\nNote that this will result in an exit code of 1, rather than merely excluding the unit from the run queue, which is slightly different behavior.\n"
  },
  {
    "path": "docs/src/content/docs/03-features/01-units/09-engine.mdx",
    "content": "---\ntitle: IaC Engines\ndescription: Learn how to dynamically control OpenTofu/Terraform runs using IaC engines.\nslug: features/units/engine\nsidebar:\n  order: 9\n---\n\nimport { Code } from '@astrojs/starlight/components'\nimport { getLatestRelease } from '@lib/github';\nexport const releaseData = await getLatestRelease('gruntwork-io', 'terragrunt-engine-opentofu');\nexport const version = releaseData?.tag_name || 'v0.1.0';\n\nIaC engines allow you to customize and configure how IaC updates are orchestrated by Terragrunt. This feature is still experimental and not recommended for general production usage.\n\nTo try it out, all you need to do is include the following in your `terragrunt.hcl`:\n\n<Code\n  lang=\"hcl\"\n  title=\"terragrunt.hcl\"\n  code={`\nengine {\n    source  = \"github.com/gruntwork-io/terragrunt-engine-opentofu\"\n    version = \"${version}\"\n}\n`}\n>\n</Code>\n\nThis example leverages the official OpenTofu engine, [publicly available on GitHub](https://github.com/gruntwork-io/terragrunt-engine-opentofu).\n\nThis engine currently leverages the locally available installation of the `tofu` binary, just like Terragrunt does by default without use of engine configurations. It provides a convenient example of how to build engines for Terragrunt.\n\nIn the future, this engine will expand in capability to include additional features and configurations.\n\nSince this functionality is experimental and not recommended for production, set the following environment variable to enable it:\n\n```sh\nexport TG_EXPERIMENTAL_ENGINE=1\n```\n\n## Use Cases\n\nIaC Engines were introduced to offer advanced users of Terragrunt a level of customization over how exactly IaC updates are performed with a given set of Terragrunt configurations.\n\nWithout usage of IaC Engines, Terragrunt will determine how IaC updates will be performed by doing things like invoking the `tofu` or `terraform` binary directly. For most users, this is fine.\n\nHowever, advanced users have more complex use cases that require more control over how those IaC updates are executed, given certain Terragrunt configurations.\n\ne.g.\n\n* Emitting custom logging or metrics whenever the `tofu` binary is executed.\n* Running `tofu` in a remote environment, such as a separate Kubernetes pod from the one executing Terragrunt.\n* Using different versions of `tofu` for different Terragrunt configurations in the same `run --all` execution.\n\n## HTTPS Sources\n\nUse an HTTP(S) URL to specify the path to the engine:\n\n<Code\n  lang=\"hcl\"\n  title=\"terragrunt.hcl\"\n  code={`\nengine {\n    source = \"https://github.com/gruntwork-io/terragrunt-engine-opentofu/releases/download/${version}/terragrunt-iac-engine-opentofu_rpc_${version}_linux_amd64.zip\"\n}\n`}\n>\n</Code>\n\n## Local Sources\n\nSpecify a local absolute path as the source:\n\n<Code\n  lang=\"hcl\"\n  title=\"terragrunt.hcl\"\n  code={`\nengine {\n    source = \"/home/users/iac-engines/terragrunt-iac-engine-opentofu_${version}\"\n}\n`}\n>\n</Code>\n\n## Parameters\n\n* `source`: (Required) The source of the plugin. Multiple engine approaches are supported, including GitHub repositories, HTTP(S) paths, and local absolute paths.\n* `version`: (Optional) The version of the engine to download from GitHub releases. If not specified, the latest release is always downloaded.\n* `type`: (Optional) Currently, the only supported type is `rpc`.\n* `meta`: (Optional) A block for setting engine-specific metadata. This can include various configuration settings required by the engine.\n\n## Caching\n\nEngines are cached locally by default to enhance performance and minimize repeated downloads.\n\nThe cached engines are stored in the following directory by default:\n\n`~/.cache/terragrunt/plugins/iac-engine/rpc/<version>`\n\nIf you need to use a different path, set the environment variable `TG_ENGINE_CACHE_PATH` accordingly.\n\nDownloaded engines are checked for integrity using the SHA256 checksum GPG key.\nIf the checksum does not match, the engine is not executed.\nTo disable this feature, set the environment variable:\n\n```sh\nexport TG_ENGINE_SKIP_CHECK=0\n```\n\nTo configure a custom log level for the engine, set the `TG_ENGINE_LOG_LEVEL` environment variable to one of `debug`, `info`, `warn`, `error`.\n\n```sh\nexport TG_ENGINE_LOG_LEVEL=debug\n```\n\n## Engine Metadata\n\nThe `meta` block is used to pass metadata to the engine. This metadata can be used to configure the engine or pass additional information to the engine.\n\nThe metadata block is a map of key-value pairs. Engines can read the information passed via the metadata map to configure themselves.\n\n```hcl\n# terragrunt.hcl\n\nengine {\n   source = \"github.com/gruntwork-io/terragrunt-engine-opentofu\"\n   # Optionally set metadata for the engine.\n   meta = {\n     key_1 = [\"value1\", \"value2\"]\n     key_2 = \"1.6.0\"\n   }\n}\n```\n\nConfigurations you might want to set with `meta` include:\n\n* Connection configurations\n* Tool versions\n* Feature flags\n* Other configurations that the engine might want to be variable in different `terragrunt.hcl` files\n"
  },
  {
    "path": "docs/src/content/docs/03-features/01-units/index.mdx",
    "content": "---\ntitle: Units\ndescription: Learn how Terragrunt units result in atomic deployments and immutable infrastructure.\nslug: features/units\nsidebar:\n  label: Overview\n  order: 1\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Aside } from '@astrojs/starlight/components';\n\nConsider the following file structure in a typical OpenTofu/Terraform project, which defines three environments (prod, qa, stage) with the same infrastructure in each one (an app, a MySQL database, and a VPC):\n\n<FileTree>\n\n- live\n  - prod\n    - app\n      - main.tf\n    - mysql\n      - main.tf\n    - vpc\n      - main.tf\n  - qa\n    - app\n      - main.tf\n    - mysql\n      - main.tf\n    - vpc\n      - main.tf\n  - stage\n    - app\n      - main.tf\n    - mysql\n      - main.tf\n    - vpc\n      - main.tf\n\n</FileTree>\n\nThe contents of each environment could be more or less identical, except perhaps for a few settings (e.g. the prod environment may run more or bigger servers). As the size of the infrastructure grows, having to maintain all of this duplicated code between environments becomes more error prone. You can reduce the amount of copy paste using [OpenTofu/Terraform modules](https://blog.gruntwork.io/how-to-create-reusable-infrastructure-with-terraform-modules-25526d65f73d), but even the code to instantiate a module and set up input variables, output variables, providers, and remote state can still create a lot of maintenance overhead.\n\nHow can you keep your OpenTofu/Terraform code [DRY](/getting-started/terminology/#dont-repeat-yourself-dry) so that you can maximize code reuse and minimize maintenance overhead?\n\nMoreover, how can you ensure that you are reproducing as close to the same infrastructure as possible across environments, so that you can be confident that what you test in qa will work when you deploy to prod?\n\n## Terragrunt units\n\nA unit in Terragrunt is a directory containing a `terragrunt.hcl` file. This hermetic unit of infrastructure is the smallest deployable entity in Terragrunt. It's also the most important feature Terragrunt has.\n\nUnits are designed to be contained, and can be operated on independently of other units. Infrastructure changes to units are also meant to be atomic. The interface you have with a unit is a `terragrunt.hcl` file, and any change you make to it should result in one reproducible change to a limited subset of your infrastructure.\n\nUnits are designed to work with immutable OpenTofu/Terraform modules. As a best practice, OpenTofu/Terraform modules referenced by a unit should be versioned, and the version of that module should be immutable. This ensures that the infrastructure you deploy is consistent across environments, and that you are confident you can reproduce the same pattern of infrastructure as many times as you need.\n\n## Remote OpenTofu/Terraform modules\n\nTerragrunt has the ability to download remote OpenTofu/Terraform configurations. The idea is that you define the OpenTofu/Terraform code for your infrastructure just once, in a single repo, called, for example, `modules`:\n\n<FileTree>\n\n- modules\n  - app\n    - main.tf\n  - mysql\n    - main.tf\n  - vpc\n    - main.tf\n\n</FileTree>\n\nThis repo contains typical OpenTofu/Terraform code, with one important design constraint: anything in your OpenTofu/Terraform code that should be different between environments should be exposed as an [input variable](https://opentofu.org/docs/language/values/variables/). For example, the `app` module might expose the following variables:\n\n```hcl\n# variables.tf\nvariable \"instance_count\" {\n  description = \"How many servers to run\"\n}\nvariable \"instance_type\" {\n  description = \"What kind of servers to run (e.g. t3.large)\"\n}\n```\n\nThese variables allow you to run smaller/fewer servers in qa and stage to save money and larger/more servers in prod to ensure availability and scalability. They also define the _variability_ of this infrastructure pattern. When instantiating the `app` module as a Terragrunt unit, you can be fairly confident that the only variance you are likely to see between environments is in the values of these variables.\n\nIn a separate repo, called, for example, `live`, you define the code for all of your environments, which now consists of just one `terragrunt.hcl` file per unit (e.g. `app/terragrunt.hcl`, `mysql/terragrunt.hcl`, etc). This gives you the following file layout:\n\n<FileTree>\n\n- live\n  - prod\n    - app\n      - terragrunt.hcl\n    - mysql\n      - terragrunt.hcl\n    - vpc\n      - terragrunt.hcl\n  - qa\n    - app\n      - terragrunt.hcl\n    - mysql\n      - terragrunt.hcl\n    - vpc\n      - terragrunt.hcl\n  - stage\n    - app\n      - terragrunt.hcl\n    - mysql\n      - terragrunt.hcl\n    - vpc\n      - terragrunt.hcl\n\n</FileTree>\n\nNotice how there are no OpenTofu/Terraform configurations (`.tf` files) in any of the folders. Instead, each `terragrunt.hcl` file specifies a `terraform { …​ }` block that specifies from where to download the OpenTofu/Terraform code, as well as the environment-specific values for the input variables in that OpenTofu/Terraform code. For example, `stage/app/terragrunt.hcl` may look like this:\n\n```hcl\n# terragrunt.hcl\nterraform {\n  # Deploy version v0.0.3 in stage\n  source = \"git::git@github.com:foo/modules.git//app?ref=v0.0.3\"\n}\n\ninputs = {\n  instance_count = 3\n  instance_type  = \"t4g.micro\"\n}\n```\n\n<Aside type=\"note\">\nThe double slash (`//`) in the `source` parameter is intentional and required. It's part of OpenTofu/Terraform's Git syntax for [module sources](https://opentofu.org/docs/language/modules/sources/). OpenTofu/Terraform may display a \"OpenTofu/Terraform initialized in an empty directory\" warning, but you can safely ignore it.\n</Aside>\n\nAnd `prod/app/terragrunt.hcl` may look like this:\n\n```hcl\n# terragrunt.hcl\nterraform {\n  # Deploy version v0.0.1 in prod\n  source = \"git::git@github.com:foo/modules.git//app?ref=v0.0.1\"\n}\n\ninputs = {\n  instance_count = 10\n  instance_type  = \"m8g.large\"\n}\n```\n\nYou can now deploy the modules in your `live` repo. For example, to deploy the `app` module in stage, you would do the following:\n\n```bash\ncd live/stage/app\nterragrunt apply\n```\n\nWhen Terragrunt finds the `terraform` block with a `source` parameter in `live/stage/app/terragrunt.hcl` file, it will:\n\n1. Download the configurations specified via the `source` parameter into the `--download-dir` folder (by default `.terragrunt-cache` in the working directory, which we recommend adding to `.gitignore`). The `source` parameter supports the same syntax as the OpenTofu/Terraform [module source](https://opentofu.org/docs/language/modules/sources/) parameter (via [go-getter](https://github.com/hashicorp/go-getter)), including local file paths, Git URLs, and `ref` parameters for pinning a tag, commit, or branch. Terragrunt downloads all the code before the double-slash `//` so that relative paths between modules work correctly.\n\n2. Copy all files from the current working directory into the temporary folder.\n\n3. Execute the OpenTofu/Terraform command you specified in that temporary folder (assuming you are performing a [run](/getting-started/terminology/#run)).\n\n4. Pass any variables defined in the `inputs = { …​ }` block as environment variables (prefixed with `TF_VAR_`) when running OpenTofu/Terraform. Notice how the `inputs` block in `stage/app/terragrunt.hcl` deploys fewer and smaller instances than prod.\n\nCheck out the [terragrunt-infrastructure-modules-example](https://github.com/gruntwork-io/terragrunt-infrastructure-modules-example) and [terragrunt-infrastructure-live-example](https://github.com/gruntwork-io/terragrunt-infrastructure-live-example) repos for fully-working sample code that demonstrates our recommended folder structure for successful infrastructure management.\n\n## Immutable modules and atomic deployments\n\nWith this approach, copy/paste between environments is minimized. The `terragrunt.hcl` files contain solely the `source` URL of the module to deploy and the `inputs` to set for that module in the current environment. To create a new unit, you copy an old one and update just the environment-specific `inputs` in the `terragrunt.hcl` files, which is about as close to the \"essential complexity\" of the problem as you can get.\n\nJust as importantly, since the OpenTofu/Terraform module code is now defined in a single repo, you can version it (e.g., using Git tags and referencing them using the `ref` parameter in the `source` URL, as in the `stage/app/terragrunt.hcl` and `prod/app/terragrunt.hcl` examples above), and promote a single, immutable version through each environment (e.g., qa → stage → prod).\n\nThis is especially powerful when thinking about how the pattern is deployed. Because all of the configuration for a unit is defined using a versioned URL and a set of inputs, it's easy to reliably promote an infrastructure change across environments as one atomic change. It's also easy to roll back to a previous version of the infrastructure by changing the `ref` parameter in the `source` URL.\n\nThis idea is inspired by Kief Morris' blog post [Using Pipelines to Manage Environments with Infrastructure as Code](https://medium.com/@kief/https-medium-com-kief-using-pipelines-to-manage-environments-with-infrastructure-as-code-b37285a1cbf5).\n\n## Working locally\n\nIf you’re testing changes to a local copy of the `modules` repo, you can use the `--source` command-line option or the `TG_SOURCE` environment variable to override the `source` parameter. This is useful to point Terragrunt at a local checkout of your code so you can do rapid, iterative, make-a-change-and-rerun development:\n\n```bash\ncd live/stage/app\nterragrunt apply --source ../../../modules//app\n```\n\nNote: the double slash (`//`) is required here too — see the note above for details.\n\n## Working with lock files\n\nOpenTofu/Terraform lock files (`.terraform.lock.hcl`) are supported by Terragrunt. The lock file will be generated next to your `terragrunt.hcl`, and you should check it into version control.\n\nSee the [Lock File Handling docs](/reference/lock-files) for more details.\n\n## Terragrunt caching\n\nThe first time you set the `source` parameter to a remote URL, Terragrunt will download the code from that URL into a tmp folder. It will _NOT_ download it again afterwards unless you change that URL.\n\nTherefore, when working locally, you should use the `--source` parameter and point it at a local file path as described in the previous section. Terragrunt will copy the local files every time you run it, which is nearly instantaneous, and doesn’t require reinitializing everything, so you’ll be able to iterate quickly.\n\nIf you need to force Terragrunt to redownload something from a remote URL, run Terragrunt with the `--source-update` flag, and it’ll delete the tmp folder, download the files from scratch, and reinitialize everything. This can take a while, so avoid it and use `--source` when you can!\n\n## Working with relative file paths\n\n<Aside type=\"caution\">\nWhen you run `terragrunt apply` in folder `foo`, OpenTofu/Terraform actually runs in a temporary folder such as `.terragrunt-cache/foo`. Relative file paths will be relative to that temporary folder, not the folder where you ran Terragrunt!\n</Aside>\n\nIn particular:\n\n- **Command line**: When using file paths on the command line, such as passing an extra `-var-file` argument, you should use absolute paths:\n\n    ``` bash\n    # Use absolute file paths on the CLI!\n    terragrunt apply -var-file /foo/bar/extra.tfvars\n    # Or use the PWD environment variable to construct\n    # an absolute path before passing it to Terragrunt\n    # $ terragrunt apply -var-file \"$PWD/extra.tfvars\"\n    ```\n\n- **Terragrunt configuration**: When using file paths directly in your Terragrunt configuration (`terragrunt.hcl`), such as in an `extra_arguments` block, you can’t use hard-coded absolute file paths, or it won’t work on your teammates' computers. Therefore, you should utilize the Terragrunt built-in function `get_terragrunt_dir()` to use a relative file path:\n\n    ``` hcl\n    # terragrunt.hcl\n    terraform {\n      source = \"git::git@github.com:foo/modules.git//frontend-app?ref=v0.0.3\"\n      extra_arguments \"custom_vars\" {\n        commands = [\n          \"apply\",\n          \"plan\",\n          \"import\",\n          \"push\",\n          \"refresh\"\n        ]\n        # With the get_terragrunt_dir() function, you can use relative paths!\n        arguments = [\n          \"-var-file=${get_terragrunt_dir()}/../common.tfvars\",\n          \"-var-file=example.tfvars\"\n        ]\n      }\n    }\n    ```\n    See the [`get_terragrunt_dir()`](/reference/hcl/functions/#get_terragrunt_dir) documentation for more details.\n\n## Using Terragrunt with private Git repos\n\nThe easiest way to use Terragrunt with private Git repos is to use SSH authentication. Configure your Git account so you can use it with SSH (see the [guide for GitHub here](https://help.github.com/articles/connecting-to-github-with-ssh/)) and use the SSH URL for your repo:\n\n``` hcl\n# terragrunt.hcl\nterraform {\n  source = \"git@github.com:foo/modules.git//path/to/module?ref=v0.0.1\"\n}\n```\n\nLook up the Git repo for your repository to find the proper format.\n\n<Aside type=\"note\">\nIn automated pipelines, you may need to run the following command for your Git repository prior to calling `terragrunt` to ensure that the SSH host is registered locally:\n\n```bash\nssh -T -oStrictHostKeyChecking=accept-new git@github.com || true\n```\n</Aside>\n\n## Generate blocks\n\nIn an ideal world, all that units do would be to run versioned, immutable OpenTofu/Terraform modules with environment-specific inputs.\nIn the real world, however, certain scenarios arise when you have to inject additional configurations to the immutable OpenTofu/Terraform\nmodules you use. This is where [generate blocks](/reference/hcl/blocks#generate) prove useful.\nWhen you define a `generate` block, Terragrunt will do the following before any run:\n1. Fetch any module referenced in a source URL in the `terraform` block into the `.terragrunt-cache` folder (if there is none, it will run in the current working directory).\n2. Generate the file specified in the `generate` block into the directory where Terragrunt will run OpenTofu/Terraform.\n3. Run the OpenTofu/Terraform command.\n\nThe most common example of this is to dynamically generate a `provider.tf` file that includes provider configurations. Most OpenTofu/Terraform modules leave provider configuration to the consumer, which is a good practice — it lets each consumer define the provider in a way that suits their needs.\n\nConsider a setup where you want to always assume a specific IAM role when calling a given OpenTofu/Terraform module. Not all modules expose the right variables for configuring the `aws` provider, so you can use a Terragrunt `generate` block to create a `provider.tf` with the correct configuration. Add an `env.hcl` file for each environment in the `live` repo:\n\n<FileTree>\n\n- live\n  - prod\n    - env.hcl\n    - app\n      - terragrunt.hcl\n    - mysql\n      - terragrunt.hcl\n    - vpc\n      - terragrunt.hcl\n  - qa\n    - env.hcl\n    - app\n      - terragrunt.hcl\n    - mysql\n      - terragrunt.hcl\n    - vpc\n      - terragrunt.hcl\n  - stage\n    - env.hcl\n    - app\n      - terragrunt.hcl\n    - mysql\n      - terragrunt.hcl\n    - vpc\n      - terragrunt.hcl\n\n</FileTree>\n\nEach `env.hcl` file (the one at the environment level, e.g `prod/env.hcl`) should define a\n`generate` block to generate the AWS provider configuration to assume the role for that environment. For example,\nif you wanted to assume the role `arn:aws:iam::0123456789:role/terragrunt` in all the units for the prod account, you\nwould put the following in `prod/env.hcl`:\n\n```hcl\n# prod/env.hcl\ngenerate \"provider\" {\n  path = \"provider.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nprovider \"aws\" {\n  assume_role {\n    role_arn = \"arn:aws:iam::0123456789:role/terragrunt\"\n  }\n}\nEOF\n}\n```\n\nThis instructs Terragrunt to create the file `provider.tf` in the working directory where Terragrunt calls `tofu`/`terraform`\nbefore it runs any of the OpenTofu/Terraform commands (e.g `plan`, `apply`, `validate`, etc). This allows you to inject this\nprovider configuration for any unit that includes the `env.hcl` file.\nTo include this in the child configurations (e.g `app/terragrunt.hcl`), you would update all the units to\ninclude this configuration using the `include` block:\n\n```hcl\n# prod/app/terragrunt.hcl\ninclude \"env\" {\n  path = find_in_parent_folders(\"env.hcl\")\n}\n```\n\nThe `include` block tells Terragrunt to use the exact same Terragrunt configuration from the `env.hcl` file\nspecified via the `path` parameter. It behaves exactly as if you had copy/pasted the OpenTofu/Terraform configuration from the\nincluded file `generate` configuration into the child, but this approach is much easier to maintain!\n\n## Further Reading\n\n- [State Backend](/features/units/state-backend) — configure remote state for your units\n- [Includes](/features/units/includes) — share common configuration across units\n- [Extra Arguments](/features/units/extra-arguments) — pass additional CLI arguments to OpenTofu/Terraform\n- [Hooks](/features/units/hooks) — run custom commands before or after OpenTofu/Terraform\n- [Authentication](/features/units/authentication) — work with multiple AWS accounts and dynamic provider authentication\n- [`terraform` block reference](/reference/hcl/blocks/#terraform) — full reference for the `terraform` block\n- [Lock File Handling](/reference/lock-files) — details on how Terragrunt manages `.terraform.lock.hcl` files\n"
  },
  {
    "path": "docs/src/content/docs/03-features/02-stacks/02-implicit.mdx",
    "content": "---\ntitle: Implicit Stacks\ndescription: Create stacks by organizing units in a directory structure.\nslug: features/stacks/implicit\nsidebar:\n  order: 2\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\n\nThe simplest way to create a stack is to organize your units in a directory structure in your repository. When you have multiple units in a directory, Terragrunt automatically treats that directory as a stack for the purposes of commands like `terragrunt run --all apply`.\n\n### Converting Terraform Modules to Units\n\nLet's say your infrastructure is defined across multiple OpenTofu/Terraform root modules:\n\n<FileTree>\n\n- root\n  - backend-app\n    - main.tf\n  - frontend-app\n    - main.tf\n  - mysql\n    - main.tf\n  - valkey\n    - main.tf\n  - vpc\n    - main.tf\n\n</FileTree>\n\nTo convert these to Terragrunt units, simply add a `terragrunt.hcl` file to each directory:\n\n<FileTree>\n\n- root\n  - backend-app\n    - main.tf\n    - terragrunt.hcl\n  - frontend-app\n    - main.tf\n    - terragrunt.hcl\n  - mysql\n    - main.tf\n    - terragrunt.hcl\n  - valkey\n    - main.tf\n    - terragrunt.hcl\n  - vpc\n    - main.tf\n    - terragrunt.hcl\n\n</FileTree>\n\nNow you have an **implicit stack**! The `root` directory contains all your units and can be managed as a single stack.\n\n### Working with Implicit Stacks\n\nUse the [`--all` flag](/reference/cli/commands/run/#all) to run an OpenTofu/Terraform command on all units in the implicit stack in the current working directory:\n\n```bash\n# Deploy all units discovered in the current working directory\nterragrunt run --all apply\n\n# Plan changes across all units discovered in the current working directory\nterragrunt run --all plan\n\n# Destroy all units discovered in the current working directory\nterragrunt run --all destroy\n\n# View outputs from all units discovered in the current working directory\nterragrunt run --all output\n```\n\nYou can also use the [`--graph` flag](/reference/cli/commands/run/#graph) to run an OpenTofu/Terraform command on all units in the [DAG](/getting-started/terminology/#directed-acyclic-graph-dag) of the unit in the current working directory.\n\n```bash\n# Run an OpenTofu/Terraform command on all units in the DAG of the unit in the current working directory\nterragrunt run --graph apply\n```\n\n### Advantages of Implicit Stacks\n\n- **Simple**: Just organize units in directory trees.\n- **Familiar**: Organized following best practices for OpenTofu/Terraform repository structures.\n- **Flexible**: Easy to add/remove units by creating/deleting directories.\n- **Version Control Friendly**: Each unit is a separate directory with its own history.\n- **Backwards Compatible**: This has been the default way to work with Terragrunt for over eight years, and the majority of existing Terragrunt configurations use this approach.\n\n### Limitations of Implicit Stacks\n\n- **Manual Management**: Each unit must be manually created and configured.\n- **No Reusability**: Patterns can't be easily shared across environments.\n- **Repetitive**: Similar configurations must be duplicated or referenced from [includes](/features/units/includes).\n"
  },
  {
    "path": "docs/src/content/docs/03-features/02-stacks/03-explicit.mdx",
    "content": "---\ntitle: Explicit Stacks\ndescription: Define stacks using terragrunt.stack.hcl files for reusable patterns.\nslug: features/stacks/explicit\nsidebar:\n  order: 3\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Aside } from '@astrojs/starlight/components';\n\nFor an alternate approach (that is more flexible, but not necessarily always the better solution), you can define explicit stacks using `terragrunt.stack.hcl` files. These are **blueprints** that programmatically generate units at runtime.\n\n### What is a terragrunt.stack.hcl file?\n\nA `terragrunt.stack.hcl` file is a blueprint that defines how to generate Terragrunt configurations programmatically. It tells Terragrunt:\n\n- What units to create.\n- Where to get their configurations from.\n- Where to place them in the directory structure.\n- What values to pass to each unit.\n\n<Aside type=\"caution\">\nIt is invalid for a unit to contain a `terragrunt.stack.hcl` file and for a stack to contain a `terragrunt.hcl` file. If either is true, Terragrunt will throw an error.\n\nThis is to prevent ambiguity in deciding whether a component is a unit or a stack.\n</Aside>\n\n### Supported Configuration Blocks\n\n#### `unit` blocks - Define Individual Infrastructure Components\n\n- **Purpose**: Define a single, deployable piece of infrastructure.\n- **Use case**: When you want to create a single piece of isolated infrastructure (e.g. a specific VPC, database, or application).\n- **Result**: Generates a directory with a single `terragrunt.hcl` file in the specified `path` from the specified `source`.\n\n#### `stack` blocks - Define Reusable Infrastructure Patterns\n\n- **Purpose**: Define a stack of units to be deployed together.\n- **Use case**: When you have a common, multi-unit pattern (like \"dev environment\" or \"three-tier web application\") that you want to deploy multiple times.\n- **Result**: Generates a directory with another `terragrunt.stack.hcl` file in the specified `path` from the specified `source`.\n\n### Example: Simple Stack with Units\n\n```hcl\n# terragrunt.stack.hcl\n\nunit \"vpc\" {\n  source = \"git::git@github.com:acme/infrastructure-catalog.git//units/vpc?ref=v0.0.1\"\n  path   = \"vpc\"\n  values = {\n    vpc_name = \"main\"\n    cidr     = \"10.0.0.0/16\"\n  }\n}\n\nunit \"database\" {\n  source = \"git::git@github.com:acme/infrastructure-catalog.git//units/database?ref=v0.0.1\"\n  path   = \"database\"\n  values = {\n    engine   = \"postgres\"\n    version  = \"13\"\n    vpc_path = \"../vpc\"\n  }\n}\n```\n\nRunning `terragrunt stack generate` in the directory containing that `terragrunt.stack.hcl` file generates:\n\n<FileTree>\n\n- .terragrunt-stack\n  - vpc\n    - terragrunt.hcl\n    - terragrunt.values.hcl\n  - database\n    - terragrunt.hcl\n    - terragrunt.values.hcl\n\n</FileTree>\n\n<Aside type=\"note\">\nNote that the contents are generated into a `.terragrunt-stack` directory. This is to make it convenient to add `.terragrunt-stack` to your `.gitignore` file, and always generate the stack on demand instead of checking it in.\n\nAlso note the `terragrunt.values.hcl` files generated next to the `terragrunt.hcl` files of the units. These files contain the values specified in the `values` block for the unit.\n</Aside>\n\n### Example: Nested Stack with Reusable Patterns\n\n```hcl\n# terragrunt.stack.hcl\n\nstack \"dev\" {\n  source = \"git::git@github.com:acme/infrastructure-catalog.git//stacks/environment?ref=v0.0.1\"\n  path   = \"dev\"\n  values = {\n    environment = \"development\"\n    cidr        = \"10.0.0.0/16\"\n  }\n}\n\nstack \"prod\" {\n  source = \"git::git@github.com:acme/infrastructure-catalog.git//stacks/environment?ref=v0.0.1\"\n  path   = \"prod\"\n  values = {\n    environment = \"production\"\n    cidr        = \"10.1.0.0/16\"\n  }\n}\n```\n\nThe referenced stack might contain:\n\n```hcl\n# stacks/environment/terragrunt.stack.hcl\n\nunit \"vpc\" {\n  source = \"git::git@github.com:acme/infrastructure-catalog.git//units/vpc?ref=v0.0.1\"\n  path   = \"vpc\"\n  values = {\n    vpc_name = values.environment\n    cidr     = values.cidr\n  }\n}\n\nunit \"database\" {\n  source = \"git::git@github.com:acme/infrastructure-catalog.git//units/database?ref=v0.0.1\"\n  path   = \"database\"\n  values = {\n    environment = values.environment\n    vpc_path    = \"../vpc\"\n  }\n}\n```\n\nRunning `terragrunt stack generate` in the directory containing that `terragrunt.stack.hcl` file generates:\n\n<FileTree>\n\n- .terragrunt-stack\n  - dev\n    - terragrunt.stack.hcl\n    - .terragrunt-stack\n      - vpc\n        - terragrunt.hcl\n        - terragrunt.values.hcl\n      - database\n        - terragrunt.hcl\n        - terragrunt.values.hcl\n  - prod\n    - terragrunt.stack.hcl\n    - .terragrunt-stack\n      - vpc\n        - terragrunt.hcl\n        - terragrunt.values.hcl\n      - database\n        - terragrunt.hcl\n        - terragrunt.values.hcl\n\n</FileTree>\n\n### Working with Explicit Stacks\n\n```bash\n# Generate units from the `terragrunt.stack.hcl` file in the current\n# working directory (and all stacks in child directories).\nterragrunt stack generate\n\n# Deploy all generated units defined using the `terragrunt.stack.hcl` file\n# in the current working directory (and any units generated by stacks in this file).\n#\n# Note that this will also automatically generate the stack if it is not already generated.\nterragrunt stack run apply\n```\n\n### Advantages of Explicit Stacks\n\n- **Reusability**: Define patterns once, reuse them across environments.\n- **Consistency**: Ensure all environments follow the same structure.\n- **Version Control**: Version collections of infrastructure patterns alongside the units of infrastructure that make them up.\n- **Automation**: Generate complex infrastructure from simple blueprints.\n- **Flexibility**: Easy to create variations with different values.\n\n### Limitations of Explicit Stacks\n\n- **Complexity**: Requires understanding another configuration file.\n- **Generation Overhead**: Units must be generated before use.\n- **Debugging**: Generated files can be harder to debug if you accidentally generate files that are not what you intended.\n\n## Stack Outputs\n\nWhen defining a stack using a `terragrunt.stack.hcl` file, you also have the ability to interact with the aggregated outputs of all the units in the stack from the CLI.\n\nTo do this, use the [`stack output`](/reference/cli/commands/stack/output) command (not the [`stack run output`](/reference/cli/commands/stack/run) command).\n\n```bash\n$ terragrunt stack output\nbackend_app = {\n  domain = \"backend-app.example.com\"\n}\nfrontend_app = {\n  domain = \"frontend-app.example.com\"\n}\nmysql = {\n  endpoint = \"terraform-20250504140737772400000001.abcdefghijkl.us-east-1.rds.amazonaws.com\"\n}\nvalkey = {\n  endpoint = \"serverless-valkey-01.amazonaws.com\"\n}\nvpc = {\n  vpc_id = \"vpc-1234567890\"\n}\n```\n\nThis returns a single aggregated HCL object aggregating all the outputs for all the units within the stack.\n\n<Aside type=\"tip\">\nYou can use the `--format json` flag to get the output in JSON format, which can be useful for accessing values programmatically.\n</Aside>\n\n## Using Local State with Stacks\n\nWhen using Explicit Stacks, you might want to use local state files instead of remote state for development, testing, or specific use cases. However, this presents a challenge because the generated `.terragrunt-stack` directory can be safely deleted and regenerated using `terragrunt stack clean && terragrunt stack generate`, which would normally cause local state files to be lost.\n\nTo solve this problem, you can configure your stack to store state files outside of the `.terragrunt-stack` directory, in a persistent location that survives stack regeneration.\n\n### Configuration\n\nHere's how to configure local state that persists across stack regeneration:\n\n**1. Create a `root.hcl` file with local backend configuration:**\n\n```hcl\n# root.hcl\nremote_state {\n  backend = \"local\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    path = \"${get_parent_terragrunt_dir()}/.terragrunt-local-state/${path_relative_to_include()}/tofu.tfstate\"\n  }\n}\n```\n\n**2. Create your stack definition:**\n\n```hcl\n# live/terragrunt.stack.hcl\nunit \"vpc\" {\n  source = \"${find_in_parent_folders(\"units/vpc\")}\"\n  path   = \"vpc\"\n}\n\nunit \"database\" {\n  source = \"${find_in_parent_folders(\"units/database\")}\"\n  path   = \"database\"\n}\n\nunit \"app\" {\n  source = \"${find_in_parent_folders(\"units/app\")}\"\n  path   = \"app\"\n}\n```\n\n**3. Configure your units to include the root configuration:**\n\n```hcl\n# units/vpc/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \".\"\n}\n```\n\n**4. Add a `.gitignore` file to exclude state files from version control:**\n\n```text\n# .gitignore\n.terragrunt-local-state\n```\n\n**Important:** Local state files should never be committed to version control as they may contain sensitive information and can cause conflicts when multiple developers work on the same infrastructure.\n\n### How It Works\n\nThe key insight is using `path_relative_to_include()` in the state path configuration. This function returns the relative path from each unit to the `root.hcl` file, creating unique state file paths like:\n\n```text\n.terragrunt-local-state/live/.terragrunt-stack/vpc/tofu.tfstate\n.terragrunt-local-state/live/.terragrunt-stack/database/tofu.tfstate\n.terragrunt-local-state/live/.terragrunt-stack/app/tofu.tfstate\n```\n\nSince these state files are stored in `.terragrunt-local-state/` (outside of `.terragrunt-stack/`), they persist when you run:\n\n```bash\nterragrunt stack clean && terragrunt stack generate\n```\n\n### Directory Structure\n\nAfter running the stack, your directory structure will look like this:\n\n<FileTree>\n\n- .\n  - root.hcl\n  - .gitignore (Excludes .terragrunt-local-state)\n  - .terragrunt-local-state/ (Persistent state files - ignored by git)\n    - live/\n      - .terragrunt-stack/\n        - vpc/\n          - tofu.tfstate\n        - database/\n          - tofu.tfstate\n        - app/\n          - tofu.tfstate\n  - live/\n    - terragrunt.stack.hcl\n    - .terragrunt-stack/ (Generated stack - can be deleted)\n      - vpc/\n        - terragrunt.hcl\n        - main.tf\n      - database/\n        - terragrunt.hcl\n        - main.tf\n      - app/\n        - terragrunt.hcl\n        - main.tf\n  - units/ (Reusable unit definitions)\n    - vpc/\n    - database/\n    - app/\n\n</FileTree>\n\n## Known Limitations of Explicit Stacks\n\nThere are currently some known limitations with explicit stacks that you should be aware of as you start to adopt them.\n\n### Dependencies cannot be set on stacks\n\nThe `dependency` block cannot set the value of the `config_path` attribute to that of a stack. This is functionality that is planned for the future, but is not currently supported.\n\nAs such, if you currently have multiple stacks that need to depend on each other, or on units within each other's stacks, you will need to either use implicit stacks, or work around this limitation by setting the `config_path` attribute to the path of the unit within the stack, and carefully ensuring that all stacks are generated before any units are run.\n\n### Deeply nested stack generation can be slow\n\nEvery generation of a stack from a `terragrunt.stack.hcl` file can potentially result in network traffic to fetch the source for the stack and filesystem traffic to copy the generated units to the `.terragrunt-stack` directory. This can result in slow stack generation if you have very deeply nested stacks.\n\nThe planned solution for this in the future is to allow for some deduplication in stack generation, but this is not currently implemented.\n\n### Includes are not supported in `terragrunt.stack.hcl` files\n\nThe `include` block is not supported in `terragrunt.stack.hcl` files. This isn't functionality that is planned for future implementation, but may change based on community feedback, and proven use-cases.\n\nThe current design of explicit stacks is that, when necessary, stacks can be nested into other stacks making them better organized and reusable without relying on includes to share configuration between stacks.\n"
  },
  {
    "path": "docs/src/content/docs/03-features/02-stacks/04-stack-operations.mdx",
    "content": "---\ntitle: Stack Operations\ndescription: Work with dependencies, visualize the DAG, control parallelism, and manage stack runs.\nslug: features/stacks/stack-operations\nsidebar:\n  order: 4\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Aside } from '@astrojs/starlight/components';\n\n## Passing outputs between units\n\nConsider the following file structure:\n\n<FileTree>\n\n- root\n  - backend-app\n    - terragrunt.hcl\n  - mysql\n    - terragrunt.hcl\n  - valkey\n    - terragrunt.hcl\n  - vpc\n    - terragrunt.hcl\n\n</FileTree>\n\nSuppose that you wanted to pass in the VPC ID of the VPC that is created from the `vpc` unit in the directory structure above to the `mysql` unit as an input variable. Or that you wanted to pass in the subnet IDs of the private subnet that is allocated as part of the `vpc` unit.\n\nYou can use the `dependency` block to extract those outputs and use them as `inputs` to the `mysql` unit.\n\nFor example, suppose the `vpc` unit outputs the ID under the output named `vpc_id`. To access that output, you would specify in `mysql/terragrunt.hcl`:\n\n```hcl\n# mysql/terragrunt.hcl\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n\ninputs = {\n  vpc_id = dependency.vpc.outputs.vpc_id\n}\n```\n\nWhen you apply this unit, the output will be read from the `vpc` unit and passed in as an input to the `mysql` unit right before calling `tofu apply`/`terraform apply`.\n\nYou can also specify multiple `dependency` blocks to access the outputs of multiple units.\n\nFor example, in the above folder structure, you might want to reference the `domain` output of the `valkey` and `mysql` units for use as `inputs` in the `backend-app` unit. To access those outputs, you would specify the following in `backend-app/terragrunt.hcl`:\n\n```hcl\n# backend-app/terragrunt.hcl\ndependency \"mysql\" {\n  config_path = \"../mysql\"\n}\n\ndependency \"valkey\" {\n  config_path = \"../valkey\"\n}\n\ninputs = {\n  mysql_url = dependency.mysql.outputs.domain\n  valkey_url = dependency.valkey.outputs.domain\n}\n```\n\nNote that each `dependency` block results in a relevant status in the Terragrunt [DAG](/getting-started/terminology/#directed-acyclic-graph-dag). This means that when you run `run --all apply` on a config that has `dependency` blocks, Terragrunt will not attempt to deploy the config until all the units referenced in `dependency` blocks have been applied. So for the above example, the order for the `run --all apply` command would be:\n\n1. Deploy the VPC\n\n2. Deploy MySQL and valkey in parallel\n\n3. Deploy the backend-app\n\nIf any of the units failed to deploy, then Terragrunt will not attempt to deploy the units that depend on them.\n\n**Note**: Not all blocks can access outputs passed by `dependency` blocks. See the section on [Configuration parsing order](/reference/hcl#configuration-parsing-order) for more information.\n\n### Unapplied dependency and mock outputs\n\nTerragrunt will return an error if the unit referenced in a `dependency` block has not been applied yet. This is because you cannot actually fetch outputs out of an unapplied unit, even if there are no resources being created in the unit.\n\nThis is most problematic when running commands that do not modify state (e.g `run --all plan` and `run --all validate`) on a completely new setup where no infrastructure has been deployed. You won't be able to `plan` or `validate` a unit if you can't determine the `inputs`. If the unit depends on the outputs of another unit that hasn't been applied yet, you won't be able to compute the `inputs` unless the dependencies are all applied.\n\nOf course, in real life usage, you typically need the ability to run `run --all validate` or `run --all plan` on a completely new set of infrastructure.\n\nTo address this, you can provide mock outputs to use when a unit hasn't been applied yet. This is configured using the `mock_outputs` attribute on the `dependency` block and it corresponds to a map that will be injected in place of the actual dependency outputs if the target config hasn't been applied yet.\n\nUsing a mock output is typically the best solution here, as you typically don't actually care that an _accurate_ value is used for a given value at this stage, just that it will plan successfully. When you actually apply the unit, that's when you want to be sure that a real value is used.\n\nFor example, in the previous scenario with a `mysql` unit and `vpc` unit, suppose you wanted to mock a value for the `vpc_id` during a `run --all validate` for the `mysql` unit.\n\nYou can specify that in `mysql/terragrunt.hcl`:\n\n```hcl\n# mysql/terragrunt.hcl\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n\n  mock_outputs = {\n    vpc_id = \"mock-vpc-id\"\n  }\n}\n\ninputs = {\n  vpc_id = dependency.vpc.outputs.vpc_id\n}\n```\n\nYou can now run `validate` on this config before the `vpc` unit is applied because Terragrunt will use the map `{vpc_id = \"mock-vpc-id\"}` as the `outputs` attribute on the dependency instead of erroring out.\n\nWhat if you wanted to restrict this behavior to only the `validate` command? For example, you might not want to use the defaults for a `plan` operation because the plan doesn't give you any indication of what is actually going to be created.\n\nYou can use the `mock_outputs_allowed_terraform_commands` attribute to indicate that the `mock_outputs` should only be used when running those OpenTofu/Terraform commands. So to restrict the `mock_outputs` to only when `validate` is being run, you can modify the above `terragrunt.hcl` file to:\n\n```hcl\n# mysql/terragrunt.hcl\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n\n  mock_outputs = {\n    vpc_id = \"temporary-dummy-id\"\n  }\n\n  mock_outputs_allowed_terraform_commands = [\"validate\"]\n}\n\ninputs = {\n  vpc_id = dependency.vpc.outputs.vpc_id\n}\n```\n\nNote that indicating `validate` means that the `mock_outputs` will be used either with `validate` or with `run --all validate`.\n\nYou can also use `skip_outputs` on the `dependency` block to specify the dependency without pulling in the outputs:\n\n```hcl\n# mysql/terragrunt.hcl\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n\n  skip_outputs = true\n}\n```\n\nWhen `skip_outputs` is used with `mock_outputs`, mocked outputs will be returned without attempting to load outputs from OpenTofu/Terraform.\n\nThis can be useful when you disable backend initialization (`remote_state.disable_init`) in CI for example.\n\n```hcl\n# mysql/terragrunt.hcl\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n\n  mock_outputs = {\n    vpc_id = \"temporary-dummy-id\"\n  }\n\n  skip_outputs = true\n}\n```\n\nYou can also use `mock_outputs_merge_strategy_with_state` on the `dependency` block to merge mocked outputs and real outputs:\n\n```hcl\n# mysql/terragrunt.hcl\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n\n  mock_outputs = {\n    vpc_id     = \"temporary-dummy-id\"\n    new_output = \"temporary-dummy-value\"\n  }\n\n  mock_outputs_merge_strategy_with_state = \"shallow\"\n}\n```\n\nIf real outputs only contain `vpc_id`, `dependency.outputs` will contain a real value for `vpc_id` and a mocked value for `new_output`.\n\n### Passing outputs between units in explicit stacks\n\nWhen defining units using a `terragrunt.stack.hcl` file, you might need to perform some indirection to pass outputs between units, as the dependency relationship of each unit is explicitly defined in each unit's `terragrunt.hcl` file.\n\nFor example, say you wanted to generate the stack above using the following `terragrunt.stack.hcl` file:\n\n```hcl\n# terragrunt.stack.hcl\n\nunit \"vpc\" {\n  source = \"github.com/acme/infrastructure-catalog//units/vpc?ref=v1.0.0\"\n  path   = \"vpc\"\n}\n\nunit \"mysql\" {\n  source = \"github.com/acme/infrastructure-catalog//units/mysql?ref=v1.0.0\"\n  path   = \"mysql\"\n}\n\nunit \"valkey\" {\n  source = \"github.com/acme/infrastructure-catalog//units/valkey?ref=v1.0.0\"\n  path   = \"valkey\"\n}\n\nunit \"backend_app\" {\n  source = \"github.com/acme/infrastructure-catalog//units/backend-app?ref=v1.0.0\"\n  path   = \"backend-app\"\n}\n```\n\nGenerating this stack would generate the following:\n\n<FileTree>\n\n- .terragrunt-stack\n  - vpc\n    - terragrunt.hcl\n  - mysql\n    - terragrunt.hcl\n  - valkey\n    - terragrunt.hcl\n  - backend-app\n    - terragrunt.hcl\n\n</FileTree>\n\nThe `backend-app` unit would need to access the outputs of the `mysql` and `valkey` units to use as inputs. To do this, you can use the `dependency` block to access the outputs of the `mysql` and `backend-app` units.\n\n```hcl\n# github.com/acme/infrastructure-catalog//units/mysql/terragrunt.hcl\ndependency \"vpc\" {\n  config_path = values.vpc_path\n}\n\ninputs = {\n  vpc_id = dependency.vpc.outputs.vpc_id\n}\n```\n\n```hcl\n# github.com/acme/infrastructure-catalog//units/backend-app/terragrunt.hcl\ndependency \"mysql\" {\n  config_path = values.mysql_path\n}\n\ndependency \"valkey\" {\n  config_path = values.valkey_path\n}\n\ninputs = {\n  mysql_url = dependency.mysql.outputs.domain\n  valkey_url = dependency.valkey.outputs.domain\n}\n```\n\nAnd update the `terragrunt.stack.hcl` file to:\n\n```hcl\n# terragrunt.stack.hcl\n\nunit \"vpc\" {\n  source = \"github.com/acme/infrastructure-catalog//units/vpc?ref=v1.0.0\"\n  path   = \"vpc\"\n}\n\nunit \"mysql\" {\n  source = \"github.com/acme/infrastructure-catalog//units/mysql?ref=v1.0.0\"\n  path   = \"mysql\"\n  values = {\n    vpc_path = \"../vpc\"\n  }\n}\n\nunit \"valkey\" {\n  source = \"github.com/acme/infrastructure-catalog//units/valkey?ref=v1.0.0\"\n  path   = \"valkey\"\n  values = {\n    vpc_path = \"../vpc\"\n  }\n}\n\nunit \"backend_app\" {\n  source = \"github.com/acme/infrastructure-catalog//units/backend-app?ref=v1.0.0\"\n  path   = \"backend-app\"\n  values = {\n    mysql_path  = \"../mysql\"\n    valkey_path = \"../valkey\"\n  }\n}\n```\n\nFollowing this pattern, the path to dependencies are passed in as `values` to the unit, and units themselves define dependency blocks that utilize those values.\n\n<Aside type=\"note\">\nYou might not like this design!\n\nTake a look at RFC [#4067](https://github.com/gruntwork-io/terragrunt/issues/4067) for an alternate proposal from a member of the Terragrunt community, and follow the conversation there.\n</Aside>\n\n## Dependencies between units\n\nYou can also specify dependencies without accessing any of the outputs of units. Consider the following file structure:\n\n<FileTree>\n\n- root\n  - backend-app\n    - terragrunt.hcl\n  - frontend-app\n    - terragrunt.hcl\n  - mysql\n    - terragrunt.hcl\n  - valkey\n    - terragrunt.hcl\n  - vpc\n    - terragrunt.hcl\n\n</FileTree>\n\nLet's assume you have the following dependencies between OpenTofu/Terraform units:\n\n- `backend-app` depends on `mysql`, `valkey`, and `vpc`\n\n- `frontend-app` depends on `backend-app` and `vpc`\n\n- `mysql` depends on `vpc`\n\n- `valkey` depends on `vpc`\n\n- `vpc` has no dependencies\n\nYou can express these dependencies in your `terragrunt.hcl` config files using a `dependencies` block. For example, in `backend-app/terragrunt.hcl` you would specify:\n\n``` hcl\n# backend-app/terragrunt.hcl\ndependencies {\n  paths = [\"../vpc\", \"../mysql\", \"../valkey\"]\n}\n```\n\nSimilarly, in `frontend-app/terragrunt.hcl`, you would specify:\n\n``` hcl\n# frontend-app/terragrunt.hcl\ndependencies {\n  paths = [\"../vpc\", \"../backend-app\"]\n}\n```\n\nOnce you've specified these dependencies in each `terragrunt.hcl` file, Terragrunt will be able to perform updates respecting the [DAG](/getting-started/terminology/#directed-acyclic-graph-dag) of dependencies.\n\nFor the example at the start of this section, the order of runs for the `run --all apply` command would be:\n\n1. Deploy the VPC\n\n2. Deploy MySQL and valkey in parallel\n\n3. Deploy the backend-app\n\n4. Deploy the frontend-app\n\nAny error encountered in an individual unit during a `run --all` command will prevent Terragrunt from proceeding with the deployment of any dependent units.\n\nTo check all of your dependencies and validate the code in them, you can use the `run --all validate` command.\n\n<Aside type=\"note\">\nDuring `destroy` runs, Terragrunt will try to find all dependent units and show a confirmation prompt with a list of detected dependencies.\n\nThis is because Terragrunt knows that once resources in a dependency are destroyed, any commands run on dependent units may fail.\n\nFor example, if `destroy` was called on the `Valkey` unit, you'd be asked for confirmation, as the `backend-app` depends on `Valkey`. You can suppress the prompt by using the `--non-interactive` flag.\n</Aside>\n\n## Visualizing the DAG\n\nTo visualize the dependency graph you can use the `dag graph` command (similar to the `tofu/terraform graph` command), or its equivalent `list --format=dot --dependencies --external` command.\n\nThe graph is output in DOT format. The typical program used to render this file format is GraphViz, but many web services are available that can do this as well.\n\n```bash\nterragrunt dag graph | dot -Tsvg > graph.svg\n# Or equivalently:\nterragrunt list --format=dot --dependencies --external | dot -Tsvg > graph.svg\n```\n\nThe example above generates the following graph:\n\n![terragrunt dag graph](../../../../assets/img/collections/documentation/graph.png)\n\nNote that this graph shows the dependency relationship in the direction of the arrow, with the tip pointing to the dependency (e.g. `frontend-app` depends on `backend-app`).\n\nFor plans and applies, Terragrunt will run units in the opposite direction, however (e.g. `backend-app` would be applied before `frontend-app`).\n\nThe exception to this rule is during the `destroy` (and `plan/apply -destroy`) commands, where Terragrunt will run in the direction of the arrow (e.g. `frontend-app` would be destroyed before `backend-app`).\n\n## Testing multiple units locally\n\nIf you are using Terragrunt to download [remote OpenTofu/Terraform modules](/features/units/#remote-opentofuterraform-modules) and all of your units have the `source` parameter set to a Git URL, but you want to test with a local checkout of the code, you can use the `--source` parameter to override that value:\n\n```bash\nterragrunt run --all --source /source/modules -- plan\n```\n\nIf you set the `--source` parameter, the `run --all` command will assume that parameter is pointing to a folder on your local file system that has a local checkout of all of your OpenTofu/Terraform modules.\n\nFor each unit that is being processed via a `run --all` command, Terragrunt will:\n\n1. Read in the `source` parameter in that unit's `terragrunt.hcl` file.\n2. Parse out the path (the portion after the double-slash).\n3. Append the path to the `--source` parameter to create the final local path for that unit.\n\nFor example, consider the following `terragrunt.hcl` file:\n\n``` hcl\n# terragrunt.hcl\n\nterraform {\n  source = \"git::git@github.com:acme/infrastructure-modules.git//networking/vpc?ref=v0.0.1\"\n}\n```\n\nRunning the following:\n\n```bash\nterragrunt run --all --source /source/infrastructure-modules -- apply\n```\n\nWill result in a unit with the configuration for the source above being resolved to `/source/infrastructure-modules//networking/vpc`.\n\n## Limiting run parallelism\n\nBy default, Terragrunt will not impose a limit on the number of units it executes when it traverses the dependency graph,\nmeaning that if it finds 5 units without dependencies, it'll run OpenTofu/Terraform 5 times in parallel, once in each unit.\n\nSometimes, this can create a problem if there are a lot of units in the dependency graph, like hitting a rate limit on a\ncloud provider.\n\nTo limit the maximum number of unit executions at any given time use the `--parallelism [number]` flag\n\n```sh\nterragrunt run --all --parallelism 4 -- apply\n```\n\n## Saving OpenTofu/Terraform plan output\n\nA powerful feature of OpenTofu/Terraform is the ability to save the result of a plan as a binary file using the [-out flag](https://opentofu.org/docs/cli/commands/plan/).\n\nTerragrunt provides special tooling in `run --all` execution to ensure that the saved plan for a `run --all` against a stack has\na corresponding entry for each unit in the stack in a directory structure that mirrors the stack structure.\n\nTo save every plan generated when running a stack, use the `--out-dir` flag (or `TG_OUT_DIR` environment variable) as demonstrated below:\n\n```bash\n$ terragrunt run --all --out-dir /tmp/tfplan -- plan\n```\n\n<FileTree>\n\n- app1\n  - tfplan.tfplan\n- app2\n  - tfplan.tfplan\n- app3\n  - tfplan.tfplan\n- project-2\n  - project-2-app1\n    - tfplan.tfplan\n\n</FileTree>\n\nThis integration also exists for the `apply` command, where the generated plan will be used when performing the apply.\n\n```bash\n$ terragrunt run --all --out-dir /tmp/tfplan -- apply\n```\n\nFor planning a destroy operation, use the following commands:\n\n```bash\nterragrunt run --all --out-dir /tmp/tfplan -- plan -destroy\nterragrunt run --all --out-dir /tmp/tfplan -- apply\n```\n\n<Aside type=\"caution\">\n\nIf you are leveraging [mock outputs](#unapplied-dependency-and-mock-outputs) in your stack, you may get unexpected results when applying saved plans, as the plans will have mock outputs in them.\n\nThere's a workaround documented [here](https://github.com/gruntwork-io/terragrunt/issues/2178#issuecomment-2615842856), but no first-class feature in Terragrunt to address this issue, currently.\n\nIf you would like Terragrunt to have first-class support for a solution to this, please [create an RFC](https://github.com/gruntwork-io/terragrunt/issues/new?template=03-rfc.yml) to propose it.\n\n</Aside>\n\nTo save plans in json format use the `--json-out-dir` flag:\n\n```bash\nterragrunt run --all --json-out-dir /tmp/json -- plan\n```\n\n<FileTree>\n\n- app1\n  - tfplan.json\n- app2\n  - tfplan.json\n- app3\n  - tfplan.json\n- project-2\n  - project-2-app1\n    - tfplan.json\n\n</FileTree>\n\n\n```bash\nterragrunt run --all --out-dir /tmp/all --json-out-dir /tmp/all -- plan\n```\n\n<FileTree>\n\n- app1\n  - tfplan.json\n  - tfplan.tfplan\n- app2\n  - tfplan.json\n  - tfplan.tfplan\n- app3\n  - tfplan.json\n  - tfplan.tfplan\n- project-2\n  - project-2-app1\n    - tfplan.json\n    - tfplan.tfplan\n\n</FileTree>\n\n## Nested Stacks\n\nNote that you can also have nested stacks.\n\nFor example, consider the following file structure:\n\n<FileTree>\n\n- root\n  - us-east-1\n    - app\n      - terragrunt.hcl\n    - db\n      - terragrunt.hcl\n  - us-west-2\n    - app\n      - terragrunt.hcl\n    - db\n      - terragrunt.hcl\n\n</FileTree>\n\nIn this example, there's the `root` stack, that contains all the infrastructure you've defined so far,\nand there's also the `us-east-1` and `us-west-2` stacks, that contain the infrastructure for the `app` and `db` units in those regions.\n\nYou can run `run --all` commands at any depth of the stack to run the units in that stack and all of its children.\n\nFor example, to run all the units in the `us-east-1` stack, you can run:\n\n```sh\ncd root/us-east-1\nterragrunt run --all apply\n```\n\nTerragrunt will only include the units in the `us-east-1` stack and its children in the queue of units to run.\n\nThis is the primary tool Terragrunt users use to control the blast radius of their changes. For the most part, it is the current working directory that determines the blast radius of a `run --all` command.\n\nIn addition to using your working directory to control what's included in a [run queue](/getting-started/terminology/#run-queue), you can also use [Filters](/features/filter/) to control this.\n\nThere are more flags that control the behavior of the `run` command, which you can find in the [`run` docs](/reference/cli/commands/run).\n"
  },
  {
    "path": "docs/src/content/docs/03-features/02-stacks/06-run-queue.mdx",
    "content": "---\ntitle: Run Queue\ndescription: Learn how Terragrunt orchestrates multiple concurrent OpenTofu/Terraform runs.\nslug: features/stacks/run-queue\nsidebar:\n  order: 5\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Aside, Tabs, TabItem } from '@astrojs/starlight/components';\n\nTerragrunt's \"Run Queue\" is the mechanism it uses to manage the run order and concurrency when running OpenTofu/Terraform commands across multiple Terragrunt [units](/features/units). This is particularly relevant when using the [`run --all`](/reference/cli/commands/run#all) or [`run --graph`](/reference/cli/commands/run#graph) commands.\n\n## How it Works: The Dependency Graph (DAG)\n\nAt its core, the Run Queue relies on a [Directed Acyclic Graph (DAG)](/getting-started/terminology#directed-acyclic-graph-dag) built from the dependencies defined between your Terragrunt units. These dependencies are typically established using [`dependency`](/reference/hcl/blocks#dependency) or [`dependencies`](/reference/hcl/blocks#dependencies) blocks in your `terragrunt.hcl` files.\n\nTerragrunt analyzes these dependencies to determine the correct order of operations:\n\n1.  **Discovery:** Terragrunt discovers configurations that might be relevant to a run based on the current working directory.\n2.  **Constructing the Queue:** Based on the command being run, Terragrunt creates an ordered queue.\n    *   For commands like `plan` or `apply`, dependencies are run *before* the units that depend on them.\n    *   For commands like `destroy`, dependent units are run *before* their dependencies.\n3.  **Runs:** Terragrunt dequeues the units in the queue and runs them, respecting the queue order. By default, it runs units concurrently up to a certain limit (controlled by the [`--parallelism`](/reference/cli/commands/run#parallelism) flag), but it will always wait for a unit's dependencies (or dependents for destroys) to complete successfully before running that unit.\n\n### Example DAG\n\nConsider a setup where:\n\n- Unit \"dependent\" depends on unit \"dependency\".\n- Unit \"dependency\" depends on unit \"ancestor-dependency\".\n- Unit \"independent\" has no dependencies nor dependents.\n\n<FileTree>\n\n- root\n  - subtree\n    - dependent\n      - terragrunt.hcl\n    - dependency\n      - terragrunt.hcl\n  - ancestor-dependency\n    - terragrunt.hcl\n  - independent\n    - terragrunt.hcl\n\n</FileTree>\n\n\n```d2\ndirection: right\n\n# Define the nodes\ndependent: dependent {\n  shape: rectangle\n}\n\ndependency: dependency {\n  shape: rectangle\n}\n\nancestor-dependency: ancestor-dependency {\n  shape: rectangle\n}\n\nindependent: independent {\n  shape: rectangle\n}\n\n# Define the connections\ndependent -> dependency: depends on\ndependency -> ancestor-dependency: depends on\n```\n\nAssuming a current working directory of the `root` directory, Terragrunt would run units in the following order:\n\n-   **`run --all plan` Order:** Terragrunt would run `independent` and `ancestor-dependency` concurrently. Once `ancestor-dependency` finishes, `dependency` would run. Once `dependency` finishes, `dependent` would run.\n-   **`run --all destroy` Order:** Terragrunt would run `dependent` and `independent` concurrently. Once `dependent` finishes, `dependency` would run. Once `dependency` finishes, `ancestor-dependency` would run.\n\n## Controlling the Queue\n\nSeveral flags allow you to customize how Terragrunt builds and executes the run queue. By default, Terragrunt will include all units that are in the current working directory.\n\n### Include by default\n\nBy default, when using the `--all` flag, Terragrunt will include all units that are in the current working directory.\n\nUsing any positive filter triggers \"Exclude by default\" behavior, meaning that Terragrunt will no longer automatically include all units in the current working directory, and will instead selectively include units only if they match a positive filter (and don't match any negative filters).\n\nMore on this in the [Filtering Units](#filtering-units) section.\n\n### Filtering Units\n\nYou can control which units are included or excluded from the queue using the [`--filter` flag](/features/filter).\n\n#### Positive Filters\n\nAny filter that doesn't start with a `!` prefix is considered a positive filter. When Terragrunt sees that any positive filter is present, it will evaluate every path it encounters while walking the directory tree, and only include units that match a positive filter.\n\n```bash\nterragrunt run --all --filter './subtree/**' -- plan\n```\n\nThis will tell Terragrunt only to run the units that match the glob pattern `./subtree/**` (any directory found in the current working directory starting with `subtree`).\n\nThere are many more types of filters you can use, and those details are covered in the [Filter feature documentation](/features/filter).\n\n#### Negative Filters\n\nAny filter that starts with a `!` prefix is considered a negative filter. Negative filters are always evaluated after positive filters (if present).\n\n```bash\nterragrunt run --all --filter '!./subtree/**' -- plan\n```\n\nThis will tell Terragrunt to run all units in the current working directory except those that match the glob pattern `./subtree/**` (any directory found in the current working directory starting with `subtree`).\n\n#### Combining Positive and Negative Filters\n\nYou can combine positive and negative filters to ensure that only the units you want are run.\n\n```bash\nterragrunt run --all --filter './subtree/**' --filter '!./subtree/dependency/**' -- plan\n```\n\nThis will tell Terragrunt to run all units in the directory `subtree` except those that match the glob pattern `./subtree/dependency/**` (any directory found in the current working directory starting with `subtree/dependency`).\n\n### Modifying Order and Error Handling\n\n- [`--queue-construct-as`](/reference/cli/commands/list#queue-construct-as) (`--as`): Build the run queue *as if* a particular command was run. Useful for performing dry-runs of [`run`](/reference/cli/commands/run) using discovery commands, like [`find`](/reference/cli/commands/find) and [`list`](/reference/cli/commands/list).\n\n  e.g. `terragrunt list --queue-construct-as destroy`\n\n  This lists the units in the order they'd be processed for `run --all destroy`:\n\n  ```bash\n  $ terragrunt list --as destroy -l\n  Type  Path\n  unit  independent\n  unit  subtree/dependent\n  unit  subtree/dependency\n  unit  ancestor-dependency\n  ```\n\n  ```bash\n  $ terragrunt list --as plan -l\n  Type  Path\n  unit  ancestor-dependency\n  unit  independent\n  unit  subtree/dependency\n  unit  subtree/dependent\n  ```\n\n- [`--queue-ignore-dag-order`](/reference/cli/commands/run#queue-ignore-dag-order): Run units in the queue concurrently without respecting the dependency order.\n\n  e.g. `terragrunt run --all plan --queue-ignore-dag-order`\n\n  Run `plan` on `ancestor-dependency`, `subtree/dependency`, `subtree/dependent`, and `independent` all concurrently, without waiting for their defined dependencies. For instance, `subtree/dependent`'s plan would not wait for `subtree/dependency`'s plan to complete.\n\n  <Aside type=\"caution\">\n  This flag is useful for faster runs in stateless commands like `validate` or `plan`, but is **dangerous** for commands that modify state like `apply` or `destroy`.\n\n  You might encounter failed applies if unit dependencies are not applied before dependents, and conversely, failed destroys if unit dependents are not destroyed before dependencies.\n  </Aside>\n\n- [`--queue-ignore-errors`](/reference/cli/commands/run#queue-ignore-errors): Continue processing the queue even if some units fail.\n\n  e.g. `terragrunt run --all plan --queue-ignore-errors`\n\n  If `ancestor-dependency`'s plan fails, Terragrunt will still attempt to run `plan` for `subtree/dependency`, then `subtree/dependent`, and also for `independent`.\n\n  <Aside type=\"caution\">\n  This flag is useful for identifying all errors at once, but can lead to inconsistent state if used with `apply` or `destroy`.\n\n  You might encounter failed applies if unit dependencies are not applied successfully before dependents, and conversely, failed destroys if unit dependents are not destroyed successfully before dependencies.\n  </Aside>\n\n- [`--fail-fast`](/reference/cli/commands/run#fail-fast): Fail the run if any unit fails, stopping all remaining units immediately.\n\n  e.g. `terragrunt run --all plan --fail-fast`\n\n  If `independent`'s plan fails, Terragrunt will stop running any more units and fail the run, even if `ancestor-dependency`'s plan succeeds.\n\n  <Aside type=\"tip\">\n  This flag is useful when you want to fail the run as soon as any unit fails, and stop running any more units.\n  </Aside>\n\n## Important Considerations\n\n<Aside type=\"caution\">\nWhen using `run --all plan` with units that have dependencies (e.g. via `dependency` or `dependencies` blocks), the command will fail if those dependencies have never been deployed. This is because Terragrunt cannot resolve dependency outputs without existing state.\n\nTo work around this issue, use [mock outputs in dependency blocks](/reference/hcl/blocks/#dependency).\n</Aside>\n\n<Aside type=\"caution\">\nDo not set `TF_PLUGIN_CACHE_DIR` when using `run --all` (unless using OpenTofu >= 1.10).\n\nThis can cause concurrent access issues with the provider cache. Instead, use Terragrunt's built-in [Provider Cache Server](/features/caching/provider-cache-server/).\n</Aside>\n\n<Aside type=\"caution\">\nWhen using `run --all` with `apply` or `destroy`, Terragrunt automatically adds the `-auto-approve` flag due to limitations with shared stdin making individual approvals impossible. Use [`--no-auto-approve`](/reference/cli/commands/run#no-auto-approve) to override this, but be aware you might need alternative approval workflows.\n</Aside>\n"
  },
  {
    "path": "docs/src/content/docs/03-features/02-stacks/07-run-report.mdx",
    "content": "---\ntitle: Run Report\ndescription: Learn how Terragrunt provides detailed reports of runs, and at-a-glance summaries of them.\nslug: features/stacks/run-report\nsidebar:\n  order: 6\n---\n\nimport { Aside, Code } from '@astrojs/starlight/components';\n\nimport runReportSchema from '../../../../../public/schemas/run/report/v4/schema.json?raw';\n\nTerragrunt uses an internal data store to track the results of runs when multiple are done at once. You can view this data, both with a high-level summary that is displayed at the end of each run, and via a detailed report that can be requested on-demand (coming soon).\n\n## Run Summary\n\nBy default, when performing a queue-based run (e.g. `terragrunt run --all plan`), Terragrunt will emit some additional information to the console after the run is complete.\n\n```bash\n$ terragrunt run --all plan\n\n# Omitted for brevity...\n\n❯❯ Run Summary  3 units  62ms\n   ────────────────────────────\n   Succeeded    3\n```\n\nThis output is called the \"Run Summary\". It provides at-a-glance information about the run that was just performed, including the following (as relevant):\n\n- Duration: The duration of the run.\n- Units: The number of units that were run.\n- Succeeded: The number of units that succeeded (if any did).\n- Failed: The number of units that failed (if any did).\n- Excluded: The number of units that were excluded from the run (if any were).\n- Early Exits: The number of units that exited early, due to a failure in a dependency (if any did).\n\n### Showing Unit Durations\n\nYou can enable showing the duration of each unit in the run summary by using the `--summary-per-unit` flag.\n\n```bash\n$ terragrunt run --all plan --summary-per-unit\n\n# Omitted for brevity...\n\n❯❯ Run Summary  3 units     10m\n   ──────────────────────────────\n   Succeeded (3)\n      long-running-unit     10m\n      medium-running-unit   12s\n      short-running-unit    5ms\n```\n\nThe units are sorted by duration, with the longest-running units shown first.\n\n### Disabling the summary\n\nYou can disable the summary output by using the `--summary-disable` flag.\n\n```bash\nterragrunt run --all plan --summary-disable\n```\n\nThe internal report will still be tracked, and is available for generation if requested.\n\n## Run Report\n\nOptionally, you can also generate a detailed report of the run, which has all the information used to generate the run summary.\n\n```bash\nterragrunt run --all plan --report-file report.csv\n```\n\nYou can specify the format of the report using the `--report-format` flag, which supports either `csv` or `json`:\n\n```bash\nterragrunt run --all plan --report-file report.json --report-format json\n```\n\nThe format can also be inferred from the file extension. If no format is specified and the file has no extension, CSV will be used by default:\n\n```bash\n# Will generate a CSV report\nterragrunt run --all plan --report-file report\n\n# Will generate a JSON report\nterragrunt run --all plan --report-file report.json\n\n# Will generate a CSV report\nterragrunt run --all plan --report-file report.csv\n```\n\nThe report will be generated in the specified format at the given path in the current working directory. Here's an example of what the CSV format looks like:\n\n```csv\nName,Started,Ended,Result,Reason,Cause\nfirst-exclude,2025-06-05T16:28:41-04:00,2025-06-05T16:28:41-04:00,excluded,exclude block,\nsecond-exclude,2025-06-05T16:28:41-04:00,2025-06-05T16:28:41-04:00,excluded,exclude block,\nfirst-failure,2025-06-05T16:28:41-04:00,2025-06-05T16:28:42-04:00,failed,run error,\nfirst-success,2025-06-05T16:28:41-04:00,2025-06-05T16:28:41-04:00,succeeded,,\nsecond-failure,2025-06-05T16:28:41-04:00,2025-06-05T16:28:42-04:00,failed,run error,\nsecond-success,2025-06-05T16:28:41-04:00,2025-06-05T16:28:41-04:00,succeeded,,\nsecond-early-exit,2025-06-05T16:28:42-04:00,2025-06-05T16:28:42-04:00,early exit,run error,\nfirst-early-exit,2025-06-05T16:28:42-04:00,2025-06-05T16:28:42-04:00,early exit,run error,\n```\n\nAnd here's an example of what the JSON format looks like:\n\n```json\n[\n  {\n    \"Name\": \"first-exclude\",\n    \"Started\": \"2025-06-05T16:28:41-04:00\",\n    \"Ended\": \"2025-06-05T16:28:41-04:00\",\n    \"Result\": \"excluded\",\n    \"Reason\": \"exclude block\"\n  },\n  {\n    \"Name\": \"first-success\",\n    \"Started\": \"2025-06-05T16:28:41-04:00\",\n    \"Ended\": \"2025-06-05T16:28:41-04:00\",\n    \"Result\": \"succeeded\"\n  }\n]\n```\n\nYou can use this file to determine details for each unit run, including the name of the unit, the start and end times, the result, the reason for that result, and the cause for that reason. Note that in the JSON format, empty fields (Reason and Cause) are omitted entirely rather than being set to empty values.\n\nIn general, the schema for this report should change infrequently, but we'll try to keep it up to date here.\n\nYou can also generate a JSON schema file for the report, so that you have a programmatic way to validate that the report is going to conform to an expected schema.\n\n```bash\nterragrunt run --all plan --report-schema-file report.schema.json\n```\n\nThe schema will be generated at the given path in the current working directory. The generated schema conforms to the [JSON Schema](https://json-schema.org/) standard.\n\nThis generated schema will look like the following:\n\n<Code title=\"run/report/v4/schema.json\" lang=\"json\" code={runReportSchema} />\n\nNote the `$id` field, which is used to identify the schema. This is useful to quickly determine which version of the schema is being used. You can also fetch the schema remotely from that URL.\n\n### Results\n\nResults are high level outcomes of a unit run, and will always be one of the following:\n\n- `succeeded`: The unit run succeeded.\n- `failed`: The unit run failed.\n- `excluded`: The unit was excluded from the run.\n- `early exit`: The unit exited early, due to a failure in a dependency.\n\n### Reasons\n\nReasons are more granular details of those results, and will always be one of the following, based on the result of the unit run:\n\n- `succeeded`:\n  - ``: When the unit run succeeded without any special conditions, an empty string will be found here.\n  - `retry succeeded`: When the unit run initially failed, but was retried due to a `retry` block, and succeeded on a subsequent attempt, you can expect to see a value of `retry succeeded` here.\n  - `error ignored`: When the unit run failed, but the error was ignored due to an `ignore` block, you can expect to see a value of `error ignored` here.\n- `failed`:\n  - `run error`: When the unit run failed due to a run error, you can expect to see a value of `run error` here.\n- `excluded`:\n  - `exclude block`: When the unit was excluded from the run due to an `exclude` block, you can expect to see a value of `exclude block` here.\n- `early exit`:\n  - `ancestor error`: When the unit exited early due to an error in the run of a dependency, you can expect to see a value of `ancestor error` here.\n\n### Causes\n\nCauses indicate the specific reason for a given result, and are generally not guessable. These provide information on the exact mechanism that caused the result.\n\n- `error ignored`: You will find the name of the `ignore` block that resulted in the error being ignored.\n- `run error`: You will find the actual error message of the unit that failed.\n- `ancestor error`: You will find the name of the unit that failed.\n"
  },
  {
    "path": "docs/src/content/docs/03-features/02-stacks/index.mdx",
    "content": "---\ntitle: Stacks\ndescription: Learn how to work with multiple units at once using implicit and explicit stacks.\nslug: features/stacks\nsidebar:\n  label: Overview\n  order: 1\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nA **stack** in Terragrunt is a collection of related units that can be managed together. Stacks provide a way to:\n\n- Deploy multiple infrastructure components with a single command\n- Manage dependencies between units automatically\n- Control the blast radius of changes\n- Organize infrastructure into logical groups\n\nTerragrunt supports two approaches to defining stacks:\n\n1. **Implicit Stacks**: Created by organizing units in a directory structure.\n2. **Explicit Stacks**: Defined using `terragrunt.stack.hcl` files.\n\n## Choosing Between Implicit and Explicit Stacks\n\n### Use Implicit Stacks When:\n\n- You have a small number of units.\n- Each unit is unique and not repeated across environments.\n- You don't mind a high file count.\n- You're just getting started with Terragrunt.\n- You need maximum explicitness and transparency.\n\n### Use Explicit Stacks When:\n\n- You have multiple environments (dev, staging, prod).\n- You want to reuse collections of related infrastructure patterns.\n- You have many similar units that differ only in values.\n- You want to version collections of infrastructure patterns.\n- You're building infrastructure catalogs or templates.\n\n<Aside type=\"tip\">\nStart with implicit stacks to get familiar with the concept, then gradually introduce explicit stacks for reusable patterns as your infrastructure grows.\n</Aside>\n\n## Examples\n\nFor detailed examples, see the [Gruntwork Terragrunt Infrastructure Catalog Stack Examples](https://github.com/gruntwork-io/terragrunt-infrastructure-catalog-example/tree/main/examples/terragrunt/stacks). These have full-featured examples of stacks that deploy real, stateful infrastructure in an AWS account.\n"
  },
  {
    "path": "docs/src/content/docs/03-features/06-catalog/02-tui.mdx",
    "content": "---\ntitle: Catalog TUI\ndescription: Browse and search your module catalog with Terragrunt's interactive terminal UI.\nslug: features/catalog/tui\nsidebar:\n  order: 2\n---\n\nLaunch the user interface for searching and managing your module catalog.\n\n```bash\nterragrunt catalog <repo-url> [--no-include-root] [--root-file-name]\n```\n\n![screenshot](../../../../assets/img/screenshots/catalog-screenshot.png)\n\nIf `<repo-url>` is provided, the repository will be cloned into a temporary directory, otherwise:\n\n1. The repository list are searched in the config file `terragrunt.hcl`. if `terragrunt.hcl` does not exist in the current directory, the config are searched in the parent directories.\n1. If the repository list is not found in the configuration file, the modules are looked for in the current directory.\n\nAn example of how to define the optional default template and the list of repositories for the `catalog` command in the `terragrunt.hcl` configuration file:\n\n``` hcl\n# terragrunt.hcl\ncatalog {\n  default_template = \"git@github.com/acme/example.git//path/to/template\"  # Optional default template to use for scaffolding\n  urls = [\n    \"relative/path/to/repo\", # will be converted to the absolute path, relative to the path of the configuration file.\n    \"/absolute/path/to/repo\",\n    \"github.com/gruntwork-io/terraform-aws-lambda\", # url to remote repository\n    \"http://github.com/gruntwork-io/terraform-aws-lambda\", # same as above\n  ]\n  no_shell = true  # Optional: disable shell commands in boilerplate templates for security\n  no_hooks = true  # Optional: disable hooks in boilerplate templates for security\n}\n```\n\nThis will recursively search for OpenTofu/Terraform modules in the root of the repo and the `modules` directory and show a table with all the modules. You can then:\n\n1. Search and filter the table: `/` and start typing.\n1. Select a module in the table: use the arrow keys to go up and down and next/previous page.\n1. See the docs for a selected module: `ENTER`.\n1. Use [`terragrunt scaffold`](/features/catalog/scaffold) to render a `terragrunt.hcl` for using the module: `S`.\n\n## Scaffolding Flags\n\nThe following `catalog` flags control behavior of the underlying `scaffold` command when the `S` key is pressed in a catalog entry:\n\n- `--no-include-root` - Do not include the root configuration file in any generated `terragrunt.hcl` during scaffolding.\n- `--root-file-name` - The name of the root configuration file to include in any generated `terragrunt.hcl` during scaffolding. This value also controls the name of the root configuration file to search for when trying to determine Catalog urls.\n"
  },
  {
    "path": "docs/src/content/docs/03-features/06-catalog/03-scaffold.mdx",
    "content": "---\ntitle: Scaffold\ndescription: Learn how to scaffold Terragrunt units.\nslug: features/catalog/scaffold\nsidebar:\n  order: 3\n---\n\nTerragrunt scaffolding can generate files for you automatically using [boilerplate](https://github.com/gruntwork-io/boilerplate) templates.\n\nCurrently, one boilerplate template is supported out-of-the-box, which you can use to generate a best-practices `terragrunt.hcl` that configures an OpenTofu/Terraform module for deployment:\n\n```bash\nterragrunt scaffold <MODULE_URL> [TEMPLATE_URL] [--var] [--var-file] [--no-include-root] [--root-file-name] [--no-dependency-prompt]\n```\n\nDescription:\n\n- `MODULE_URL` - This parameter specifies the URL to an OpenTofu/Terraform module. It can be a local file path, git URL, registry URL, or any other [module source URL](https://developer.hashicorp.com/terraform/language/modules/sources).\n- `TEMPLATE_URL` - This optional parameter specifies the URL to a custom boilerplate template to generate HCL files. It can be a local file path, git URL, registry URL, or any other [module source URL](https://developer.hashicorp.com/terraform/language/modules/sources). If not specified, Terragrunt will:\n  - Look for a `.boilerplate` folder in the module at `MODULE_URL`, and if found, use the boilerplate template in that folder.\n  - Failing to find that, Terragrunt will use a default boilerplate template that is built-in, which creates a simple Terragrunt unit for deploying that OpenTofu/Terraform module.\n\nFor example, here's how you can generate a `terragrunt.hcl` file to instantiate an [example MySQL OpenTofu/Terraform module](https://github.com/gruntwork-io/terragrunt-infrastructure-catalog-example/tree/main/modules/mysql) for deployment:\n\n```bash\nterragrunt scaffold github.com/gruntwork-io/terragrunt-infrastructure-modules-example//modules/mysql\n```\n\nThis will create a `terragrunt.hcl` in your current working directory, with roughly the following contents:\n\n```hcl\n# terragrunt.hcl\n\n# This is a Terragrunt unit generated by Gruntwork Boilerplate (https://github.com/gruntwork-io/boilerplate).\nterraform {\n  source = \"git::https://github.com/gruntwork-io/terragrunt-infrastructure-modules-example.git//modules/mysql?ref=v0.8.1\"\n}\n\ninputs = {\n  # --------------------------------------------------------------------------------------------------------------------\n  # Required input variables\n  # --------------------------------------------------------------------------------------------------------------------\n\n  # Type: string\n  # Description: The AWS region to deploy to (e.g. us-east-1)\n  aws_region = \"\" # TODO: fill in value\n\n  # Type: string\n  # Description: The name of the DB\n  name = \"\" # TODO: fill in value\n\n  # Type: string\n  # Description: The instance class of the DB (e.g. db.t2.micro)\n  instance_class = \"\" # TODO: fill in value\n\n  # (... full list of inputs omitted for brevity ...)\n}\n```\n\nImportant notes:\n\n- The `source` URL is configured for you automatically, with the `ref` pointing to the latest \"release\" tag of the module (found by scanning git tags).\n- The `inputs` section is generated for you automatically, and will list all required and optional variables from the module, with their types, descriptions, and defaults, so you can easily fill them in to configure the unit as you like.\n\n## Custom templates for scaffolding\n\nTerragrunt has a basic template built-in for rendering `terragrunt.hcl` files, but you can provide your own templates to customize what code is generated! Scaffolding is done via [boilerplate](https://github.com/gruntwork-io/boilerplate), and Terragrunt allows you to specify custom boilerplate templates via three mechanisms - listed in order of priority:\n\n1. You can specify a custom boilerplate template to use as the second argument of the `scaffold` command.\n2. You can define a custom boilerplate template in a `.boilerplate` subfolder of your module.\n3. You can define a default custom boilerplate template in the [catalog config](/features/catalog/tui).\n\nIf you define input variables in your boilerplate template, Terragrunt will prompt users for the values. Those values can also be passed in via `--var` and `--var-file` arguments.\nThere are also a set of variables that Terragrunt will automatically expose to your boilerplate templates for rendering:\n\n- `sourceUrl` - URL to module\n- `requiredVariables` - list of required variables in the unit being scaffolded (see below)\n- `optionalVariables` - list of optional variables in the unit being scaffolded (see below)\n\nThe elements in the `requiredVariables` and `optionalVariables` lists are structs with the following fields:\n\n- `Name` - variable name\n- `Description` - variable description\n- `Type` - variable type (string, number, bool, list, map, object) [Type Constants](https://developer.hashicorp.com/packer/docs/templates/hcl_templates/variables#type-constraints)\n- `DefaultValue` - variable default value\n- `DefaultValuePlaceholder` - default value placeholder (e.g. `\"\"` for a string or `0` for a number)\n\nOptional variables which can be passed to `scaffold` command:\n\n- `Ref` - git tag or branch name for module to be used\n- `EnableRootInclude` - add in default `terragrunt.hcl` inclusion for the root unit, by default `true`\n- `RootFileName` - name of the root configuration file, by default `terragrunt.hcl` \\*\n- `SourceUrlType` - if set to `git-ssh` module url will be converted to Git/SSH format\n- `SourceGitSshUser` - git user for Git/SSH format, by default `git`\n\n\\* **NOTE**: `RootFileName` is set to `terragrunt.hcl` by default to ensure backwards compatibility, but the pattern of using a `terragrunt.hcl` file at the root of Terragrunt projects has since been deprecated.\n\n   When the [root-terragrunt-hcl](/reference/strict-controls#root-terragrunt-hcl) strict control is enabled, the default configuration file will change to `root.hcl`, which is considered a better practice. For more details, see [Migrating from root `terragrunt.hcl`](/migrate/migrating-from-root-terragrunt-hcl).\n\n### Convenience flags\n\n- `--no-include-root` - Disable inclusion of the root include in the generated `terragrunt.hcl` file (equivalent to using `--var=EnableRootInclude=false`, and will be overridden if the corresponding `var` value is set).\n- `--root-file-name` - Set the name of the root configuration file to include in the generated `terragrunt.hcl` file (equivalent to using `--var=RootFileName=<name>`, and will be overridden if the corresponding `var` value is set).\n- `--no-dependency-prompt` - Disable dependency confirmation, but keep the interactive mode enabled (skip asking for confirmation about including dependencies defined in the boilerplate template).\n\n\\* **NOTE**: `RootFileName` is set to `terragrunt.hcl` by default to ensure backwards compatibility, but the pattern of using a `terragrunt.hcl` file at the root of Terragrunt projects has since been deprecated.\n\n   See the note above on the [root-terragrunt-hcl](/reference/strict-controls#root-terragrunt-hcl) strict control for more information.\n\n## Examples\n\nScaffold new project but use specific module version:\n\n```bash\nterragrunt scaffold github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs --var=Ref=v0.68.4\n```\n\nScaffold new project but use Git/SSH URLs:\n\n```bash\nterragrunt scaffold github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs --var=SourceUrlType=git-ssh\n```\n\n```hcl\n# terragrunt.hcl\nterraform {\n  source = \"git::ssh://git@github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=v0.68.4\"\n}\n```\n\nScaffold new project using a template from a Git repository:\n\n```bash\nterragrunt scaffold github.com/gruntwork-io/terragrunt.git//test/fixtures/scaffold/module-with-template\n# The template from the .boilerplate directory will be used to generate terragrunt.hcl\n```\n\n**NOTE**: Scaffolding infrastructure from an external repository might introduce security or stability risks. Always review code from trusted external sources before running it.\n\nScaffold new project using an external template:\n\n```bash\nterragrunt scaffold github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs git@github.com:gruntwork-io/terragrunt.git//test/fixtures/scaffold/external-template\n# The files external-template.txt and terragrunt.hcl will be created from that external template\n```\n"
  },
  {
    "path": "docs/src/content/docs/03-features/06-catalog/index.mdx",
    "content": "---\ntitle: Catalog\ndescription: Learn how to search, browse, and scaffold modules with Terragrunt's catalog ecosystem.\nslug: features/catalog\nsidebar:\n  label: Overview\n  order: 1\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nTerragrunt's catalog ecosystem provides two complementary tools for working with OpenTofu/Terraform modules:\n\n- **[Catalog TUI](/features/catalog/tui)** — A terminal user interface for browsing and searching your module catalog.\n- **[Scaffold](/features/catalog/scaffold)** — A command for generating `terragrunt.hcl` files from module templates.\n\nYou can use Scaffold standalone, or launch it directly from the Catalog TUI by pressing `S` on a selected module.\n\n## Security Configuration\n\nThe `catalog` block supports security-related configuration options:\n\n- `no_shell` (bool): When set to `true`, disables shell command execution in boilerplate templates during scaffolding. This is useful for organizations that want to prevent the use of shell commands in templates for security reasons.\n- `no_hooks` (bool): When set to `true`, disables hook execution in boilerplate templates during scaffolding. This is useful for organizations that want to prevent the use of hooks in templates for security reasons.\n\n<Aside type=\"caution\">\n\nDo not use catalog/scaffold to scaffold untrusted templates. IaC configurations are inherently powerful, as they\ncan run arbitrary code on your system, so make sure to only use trusted templates you have reviewed and approved.\n\n</Aside>\n\nThese configuration values can be overridden by their corresponding CLI flags:\n\n```bash\nterragrunt catalog --no-shell --no-hooks\n\nterragrunt scaffold module-url --no-shell\n```\n\n**Priority order**: CLI flags > catalog configuration > defaults (both default to `false`, allowing `shell` and `hooks` to be used by default)\n\n## Custom templates for scaffolding\n\nTerragrunt has a basic template built-in for rendering `terragrunt.hcl` files, but you can provide your own templates to customize how code is generated! Scaffolding is done via [boilerplate](https://github.com/gruntwork-io/boilerplate), and Terragrunt allows you to specify custom boilerplate templates via multiple mechanisms:\n\n1. You can define a custom Boilerplate template in a `.boilerplate` sub-directory of any OpenTofu/Terraform module.\n2. You can specify a custom Boilerplate template in the catalog configuration using the `default_template` option.\n3. You can specify a custom boilerplate template to use as the second argument of the [`scaffold` command](/features/catalog/scaffold).\n\nSee the [Catalog TUI](/features/catalog/tui) and [Scaffold](/features/catalog/scaffold) pages for more details on configuring templates.\n"
  },
  {
    "path": "docs/src/content/docs/03-features/07-caching/02-provider-cache-server.mdx",
    "content": "---\ntitle: Provider Cache Server\ndescription: Learn how to use the Terragrunt provider cache server.\nslug: features/caching/provider-cache-server\nsidebar:\n  order: 2\n---\n\nimport { Aside } from '@astrojs/starlight/components'\n\n<Aside type=\"tip\">\nIf you're using OpenTofu >= 1.10, you'll use the [Automatic Provider Cache Dir](/features/caching/auto-provider-cache-dir) feature by default on the latest version of Terragrunt.\n\nYou might not necessarily get any performance benefits when using this feature if so. Only use this feature if you are using an older version of OpenTofu, if you are using Terraform or if you are reaching a performance bottleneck that is not addressed by the Automatic Provider Cache Dir feature.\n</Aside>\n\nTerragrunt has the ability to cache OpenTofu/Terraform providers across all OpenTofu/Terraform runs. The Provider Cache Server feature ensures that each provider is only ever downloaded and stored on disk exactly once by running a local provider cache server while Terragrunt runs OpenTofu/Terraform commands.\n\nThe Provider Cache Server is a performance optimization. For more details on performance optimizations, their tradeoffs, and other performance tips, read the dedicated [Performance documentation](/troubleshooting/performance).\n\n## Why caching is useful\n\nLet's imagine that your project consists of 50 Terragrunt units, and each of them uses the same `aws` provider. Without caching, each of them will download the provider from the Internet, and store it in its own `.terraform` directory. For clarity, the downloadable archive `terraform-provider-aws_5.36.0_darwin_arm64.zip` has a size of ~100MB, and when unzipped it takes up ~450MB of disk space. It's easy to calculate that initializing such a project with 50 modules will cost you 5GB of traffic and 22.5GB of free space instead of 100MB and 450MB using the cache.\n\n## Why OpenTofu/Terraform's built-in provider caching doesn't work\n\nOpenTofu/Terraform has a provider caching feature, the [Provider Plugin Cache](https://opentofu.org/docs/cli/config/config-file/#provider-plugin-cache), that does the job well... unless you run multiple OpenTofu/Terraform processes simultaneously, such as when you use `terragrunt run --all`. Then the OpenTofu/Terraform processes begin conflict by overwriting each other's cache, which causes an error such as `Error: Failed to install provider`. As a result, Terragrunt previously had to disable concurrency for `init` steps in `run --all`, which is significantly slower. If you enable Terragrunt Provider Caching, as described in this section, that will no longer be necessary, and you should see significant performance improvements with `init`, as well as significant savings in terms of bandwidth and disk space usage.\n\n<Aside type=\"note\">\nThis isn't necessarily true for the latest version of OpenTofu anymore! For more information, see the [Automatic Provider Cache Dir](/features/caching/auto-provider-cache-dir) feature documentation.\n</Aside>\n## Usage\n\nThe Terragrunt Provider Cache Server is disabled by default. To enable it, you need to use the flag [`provider-cache`](https://docs.terragrunt.com/reference/cli/commands/run#provider-cache):\n\n```shell\nterragrunt run --all --provider-cache apply\n```\n\nor the environment variable `TG_PROVIDER_CACHE`:\n\n```shell\nTG_PROVIDER_CACHE=1 terragrunt run --all apply\n```\n\nBy default, cached providers are stored in `terragrunt/providers` folder, which is located in the user cache directory:\n\n- `$HOME/.terragrunt-cache/terragrunt/providers` on Unix systems\n- `$HOME/Library/Caches/terragrunt/providers` on Darwin\n- `%LocalAppData%\\terragrunt\\providers` on Windows\n\nThe file structure of the cache directory is identical to the OpenTofu/Terraform [plugin_cache_dir](https://opentofu.org/docs/cli/config/config-file/#provider-plugin-cache) directory. If you already have a directory with providers cached by OpenTofu/Terraform [plugin_cache_dir](https://opentofu.org/docs/cli/config/config-file/#provider-plugin-cache), you can set this path using the flag [`provider-cache-dir`](/reference/cli/commands/run#provider-cache-dir), to enable the Provider Cache Server to reuse existing cached providers.\n\n```shell\nterragrunt plan \\\n--provider-cache \\\n--provider-cache-dir /new/path/to/cache/dir\n```\n\nor the environment variable `TG_PROVIDER_CACHE_DIR`:\n\n```shell\nTG_PROVIDER_CACHE=1 \\\nTG_PROVIDER_CACHE_DIR=/new/path/to/cache/dir \\\nterragrunt plan\n```\n\nBy default, Terragrunt only caches providers from the following registries: `registry.terraform.io`, `registry.opentofu.org`. You can override this list using the flag [`provider-cache-registry-names`](https://docs.terragrunt.com/reference/cli/commands/run#provider-cache-registry-names):\n\n```shell\nterragrunt apply \\\n--provider-cache \\\n--provider-cache-registry-names example1.com \\\n--provider-cache-registry-names example2.com\n```\n\nor the environment variable `TG_PROVIDER_CACHE_REGISTRY_NAMES`:\n\n```shell\nTG_PROVIDER_CACHE=1 \\\nTG_PROVIDER_CACHE_REGISTRY_NAMES=example1.com,example2.com \\\nterragrunt apply\n```\n\n## How Terragrunt Provider Caching works\n\n- Start a server on localhost. This is the _Terragrunt Provider Cache server_.\n- Configure OpenTofu/Terraform instances to use the Terragrunt Provider Cache server as a remote registry:\n\n  - Create local CLI config file `.terraformrc` for each module that concatenates the user configuration from the OpenTofu/Terraform [CLI config file](https://opentofu.org/docs/cli/config/config-file/) with additional sections:\n\n    - [provider-installation](https://opentofu.org/docs/cli/config/config-file/#provider-installation) forces OpenTofu/Terraform to look for the required providers in the cache directory and create symbolic links to them, if not found, then request them from the remote registry.\n    - [host](https://github.com/hashicorp/terraform/issues/28309) forces OpenTofu/Terraform to [forward](#how-forwarding-requests-through-the-provider-cache-server-works) all provider requests through the Terragrunt Provider Cache server. The address link contains [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) and is unique for each module, used by Terragrunt Provider Cache server to associate modules with the requested providers.\n  - Set environment variables:\n    - [TF_CLI_CONFIG_FILE](https://opentofu.org/docs/cli/config/environment-variables/#tf_plugin_cache_dir) sets to use just created local CLI config `.terragrunt-cache/.terraformrc`\n    - [TF*TOKEN*\\*](https://opentofu.org/docs/cli/config/config-file/#environment-variable-credentials) sets per-remote-registry tokens for authentication to Terragrunt Provider Cache server.\n\n- Any time Terragrunt is going to run `init`:\n  - Call `tofu/terraform init`. This gets OpenTofu/Terraform to request all the providers it needs from the Terragrunt Provider Cache server.\n  - The Terragrunt Provider Cache server will download the provider from the remote registry, unpack and store it into the cache directory or [create a symlink](#reusing-providers-from-the-user-plugins-directory) if the required provider exists in the user plugins directory. Note that the Terragrunt Provider Cache server will ensure that each unique provider is only ever downloaded and stored on disk once, handling concurrency (from multiple OpenTofu/Terraform and Terragrunt instances) correctly. Along with the provider, the cache server downloads hashes and signatures of the providers to check that the files are not corrupted.\n  - The Terragrunt Provider Cache server returns the HTTP status [_423 Locked_](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/423) to OpenTofu/Terraform. This is because we do _not_ want OpenTofu/Terraform to actually download any providers as a result of calling `tofu/terraform init`; we only use that command to request the Terragrunt Provider Cache Server to start caching providers.\n  - At this point, all providers are downloaded and cached, so finally, we run `terragrunt init` a second time, which will find all the providers it needs in the cache, and it'll create symlinks to them nearly instantly, with no additional downloading.\n  - Note that if a OpenTofu/Terraform module doesn't have a lock file, OpenTofu/Terraform does _not_ use the cache, so it would end up downloading all the providers from scratch. To work around this, we generate `.terraform.lock.hcl` based on the request made by `tofu/terraform init` to the Terragrunt Provider Cache server. Since `terraform init` only requests the providers that need to be added/updated, we can keep track of them using the Terragrunt Provider Cache server and update the OpenTofu/Terraform lock file with the appropriate hashes without having to parse `tf` configs.\n\n### Reusing providers from the user plugins directory\n\nSome plugins for some operating systems may not be available in the remote registries. Thus, the cache server will not be able to download the requested provider. As an example, plugin `template v2.2.0` for `darwin-arm64`, see [Template v2.2.0 does not have a package available - Mac M1](https://discuss.hashicorp.com/t/template-v2-2-0-does-not-have-a-package-available-mac-m1/35099). The workaround is to compile the plugin from source code and put it into the user plugins directory or use the automated solution [https://github.com/kreuzwerker/m1-terraform-provider-helper](https://github.com/kreuzwerker/m1-terraform-provider-helper). For this reason, the cache server first tries to create a symlink from the user's plugin directory if the required provider already exists there:\n\n- %APPDATA%\\terraform.d\\plugins on Windows\n- ~/.terraform.d/plugins on other systems\n\n### How forwarding requests through the Provider Cache Server works\n\nOpenTofu/Terraform has an official documented setting [network_mirror](https://developer.hashicorp.com/terraform/cli/config/config-file#network_mirror), that works great, but has one major drawback for the local cache server - the need to use an HTTPS connection with a trusted certificate. Fortunately, there is another way - using the undocumented [host](https://github.com/hashicorp/terraform/issues/28309) setting, which allows OpenTofu/Terraform to create connections to the caching server over HTTP.\n\n### Provider Cache with `providers lock` command\n\nIf you run `providers lock` with enabled Terragrunt Provider Cache, Terragrunt creates the provider cache and generates the lock file.\n\n```shell\nterragrunt run --provider-cache -- providers lock -platform=linux_amd64 -platform=darwin_arm64 -platform=freebsd_amd64\n```\n\n## Configure the Provider Cache Server\n\nSince the Provider Cache Server is essentially a Private Registry server that accepts requests from OpenTofu/Terraform, downloads and saves providers to the cache directory, there are a few more flags that are unlikely to be needed, but are useful to know about:\n\n- [`provider-cache-hostname`](https://docs.terragrunt.com/reference/cli/commands/run#provider-cache-hostname) - Default: `localhost`.\n- [`provider-cache-port`](https://docs.terragrunt.com/reference/cli/commands/run#provider-cache-port) - Default: Assigned random port automatically.\n- [`provider-cache-token`](https://docs.terragrunt.com/reference/cli/commands/run#provider-cache-token) - Default: Generated randomly.\n\nTo enhance security, the Terragrunt Provider Cache has authentication to prevent unauthorized connections from third-party applications. You can set your own token using any character set.\n\n```shell\nterragrunt apply \\\n--provider-cache \\\n--provider-cache-host 192.168.0.100 \\\n--provider-cache-port 5758 \\\n--provider-cache-token my-secret\n```\n\nor using environment variables:\n\n```shell\nTG_PROVIDER_CACHE=1 \\\nTG_PROVIDER_CACHE_HOST=192.168.0.100 \\\nTG_PROVIDER_CACHE_PORT=5758 \\\nTG_PROVIDER_CACHE_TOKEN=my-secret \\\nterragrunt apply\n```\n"
  },
  {
    "path": "docs/src/content/docs/03-features/07-caching/03-auto-provider-cache-dir.mdx",
    "content": "---\ntitle: Automatic Provider Cache Dir\ndescription: Learn how Terragrunt automatically configures OpenTofu's native provider caching to improve performance and reduce bandwidth usage.\nslug: features/caching/auto-provider-cache-dir\nsidebar:\n  order: 3\n---\n\nThis feature has been stabilized and is now enabled by default when using OpenTofu >= 1.10.\n\n*Automatic Provider Cache Dir* is a feature of Terragrunt that automatically configures OpenTofu's native provider caching mechanism by setting the `TF_PLUGIN_CACHE_DIR` environment variable. This enables efficient provider caching without the need to manually configure provider cache directories or use Terragrunt's provider cache server.\n\nWhen using OpenTofu >= 1.10, Terragrunt will automatically configure OpenTofu to use a shared provider cache directory, which provides several benefits:\n\n- **Improved performance**: Providers are downloaded once and reused across multiple configurations\n- **Reduced bandwidth usage**: Eliminates redundant provider downloads\n- **Better concurrency**: OpenTofu 1.10+ handles concurrent access to the provider cache safely\n- **Simplified setup**: No need for manual provider cache configuration\n\n## Requirements\n\nThe Automatic Provider Cache Dir feature has specific requirements:\n\n- **OpenTofu version >= 1.10** is required\n- **Only works with OpenTofu** (not Terraform)\n- If requirements are not met, the experiment silently does nothing\n\n## Usage\n\nWhen using OpenTofu >= 1.10, this feature is enabled by default. No additional configuration is required:\n\n```bash\nterragrunt run --all apply\n```\n\n## How it Works\n\nWhen enabled, Terragrunt automatically:\n\n1. **Detects OpenTofu version** and ensures it meets the minimum requirement (>= 1.10)\n2. **Sets up provider cache directory** using the default cache location or a custom path\n3. **Configures TF_PLUGIN_CACHE_DIR** environment variable for OpenTofu processes\n4. **Ensures directory exists** with proper permissions\n\nThe default provider cache directory is located at:\n\n- `$HOME/.terragrunt-cache/providers` on Unix systems\n- `$HOME/Library/Caches/terragrunt/providers` on macOS\n- `%LocalAppData%\\terragrunt\\providers` on Windows\n\n## Customizing the Cache Directory\n\nYou can customize the provider cache directory using the `--provider-cache-dir` flag:\n\n```bash\nterragrunt apply --provider-cache-dir /custom/path/to/cache\n```\n\nOr with environment variables:\n\n```bash\nTG_PROVIDER_CACHE_DIR='/custom/path/to/cache' terragrunt apply\n```\n\n## Disabling Auto Provider Cache Dir\n\nYou can disable the feature for specific runs using the `--no-auto-provider-cache-dir` flag:\n\n```bash\nterragrunt run --all apply --no-auto-provider-cache-dir\n```\n\nThis is particularly useful when:\n- You want manual control over provider caching for specific environments\n- Testing configurations without provider caching\n- Using custom provider cache configurations\n\n## Comparison with Provider Cache Server\n\nTerragrunt also provides a [Provider Cache Server](/features/caching/provider-cache-server) feature. Here's when to use each:\n\n**Use Auto Provider Cache Dir when:**\n\n- Using OpenTofu 1.10+\n- You want a simple, low-maintenance caching solution\n- You prefer native OpenTofu caching mechanisms\n- You need good concurrent access handling\n\n**Use Provider Cache Server when:**\n\n- Using older versions of OpenTofu/Terraform\n- You need advanced caching features\n- You want to share providers across different filesystems\n- You need custom registry configurations\n\n## Troubleshooting\n\nIf the feature doesn't seem to be working:\n\n1. **Check OpenTofu version**: Ensure you're using OpenTofu 1.10 or later\n2. **Check cache directory**: Ensure the cache directory is accessible and has proper permissions\n3. **Review environment variables**: Verify `TF_PLUGIN_CACHE_DIR` is not already set by another tool\n\nYou can enable debug logging to see more information:\n\n```bash\nterragrunt apply --log-level debug\n```\n"
  },
  {
    "path": "docs/src/content/docs/03-features/07-caching/04-cas.mdx",
    "content": "---\ntitle: Content Addressable Store (CAS)\ndescription: Learn how Terragrunt supports deduplication of content using a Content Addressable Store (CAS).\nslug: features/caching/cas\nsidebar:\n  order: 4\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\n\nTerragrunt supports a Content Addressable Store (CAS) to deduplicate content across multiple Terragrunt configurations. This feature is still experimental and not recommended for general production usage.\n\nThe CAS is used to speed up both catalog cloning and OpenTofu/Terraform source cloning by avoiding redundant downloads of Git repositories.\n\nTo use the CAS, you will need to enable the [cas](/reference/experiments/#cas) experiment.\n\n## Usage\n\nWhen you enable the `cas` experiment, Terragrunt will automatically use the CAS when cloning any compatible source (Git repositories).\n\n### Catalog Usage\n\n```hcl\n# root.hcl\n\ncatalog {\n  urls = [\n    \"git@github.com:acme/modules.git\"\n  ]\n}\n```\n\n### OpenTofu/Terraform Source Usage\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  source = \"git@github.com:acme/infrastructure-modules.git//vpc?ref=v1.0.0\"\n}\n```\n\nWhen Terragrunt clones a repository while using the CAS, if the repository is not found in the CAS, Terragrunt will clone the repository from the original URL and store it in the CAS for future use.\n\nWhen generating a repository from the CAS, Terragrunt will hard link entries from the CAS to the new repository. This allows Terragrunt to deduplicate content across multiple repositories.\n\nIn the event that hard linking fails due to some operating system / host incompatibility with hard links, Terragrunt will fall back to performing copies of the content from the CAS.\n\n## Storage\n\nThe CAS is stored in the `~/.cache/terragrunt/cas` directory. This directory can be safely deleted at any time, as Terragrunt will automatically regenerate the CAS as needed.\n\nAvoid partial deletions of the CAS directory without care, as that might result in partially cloned repositories and unexpected behavior.\n\n## How it works\n\nTerragrunt's CAS uses a content-addressable storage model to deduplicate repository content from Git clones to save disk space and improve performance. Each Git object is identified by its SHA1 hash, allowing identical content to be shared across multiple cloned repositories and repeated clones.\n\n### Content Addressing\n\nCAS uses Git's native content addressing scheme where each object is uniquely identified by its SHA1 hash. This means:\n\n- **Identical content** across different repositories shares the same hash\n- **Same commit hash** always represents the same content\n- **Storage is partitioned** by the first two characters of the hash (e.g., `ab/abc123...`)\n\n### Storage Structure\n\nThe CAS store is organized in a partitioned structure to optimize file system performance:\n\n<FileTree>\n\n- ~/.cache/terragrunt/cas/store/\n  - ab/\n    - abc123...xyz (blob)\n    - abc123...xyz.lock (lock file)\n    - abd456...xyz (tree)\n  - cd/\n    - cd7890...xyz (blob)\n    - cd7890...xyz.lock (lock file)\n  - ...\n\n</FileTree>\n\nEach content object is stored at `{hash[:2]}/{hash}`, where the first two characters create a partition directory. This prevents having thousands of files in a single directory, which can degrade file system performance.\n\n### Clone Flow\n\nWhen Terragrunt needs to clone a repository using the CAS it does the following, depending on whether the content is already in the CAS or not:\n\n#### Cold Clones\n\nFor cold clones, where the content is not already in the CAS:\n\n1. Terragrunt resolves the Git reference (branch/tag) to a commit hash\n2. The tree related to the commit hash is not found in the CAS\n3. Terragrunt clones the repository to a temporary directory\n4. All blobs and trees required to reproduce the repository are extracted\n5. Content is stored in the CAS, partitioned by hash prefix\n6. The tree structure is read from the CAS and hard links are created to the target directory\n\n#### Warm Clones\n\nFor warm clones, where the content is already in the CAS:\n\n1. Terragrunt resolves the Git reference to a commit hash\n2. CAS checks if the content exists\n3. The tree structure is read directly from the CAS\n4. Hard links are created from CAS to the target directory\n\n#### Flow Diagram\n\n```d2\ndirection: down\n\n# Source\ngit_repo: \"Git Repository\\n\\ngit@github.com:acme/modules.git?ref=v1.0.0\" {\n  shape: cylinder\n}\n\n# Decision Point\ncheck_cas: \"In CAS?\\n\\nhash = 123abc...\" {\n  shape: diamond\n}\n\n# First Clone Path (Content Not in CAS)\nclone_store: \"Clone & Store\\n(git clone → extract → store)\" {\n  shape: rectangle\n}\n\n# Subsequent Clone Path (Content Already in CAS)\nread_cas: \"Read from CAS\\n\\n123abc...\" {\n  shape: rectangle\n}\n\n# Link Step\nlink_step: \"Link to Targets\\n\\nblob abc123... main.tf\\nblob cd7890... variables.tf\" {\n  shape: rectangle\n}\n\n# Linked Targets\nlinked_target1: \"Linked Target\\n\\n.terragrunt-cache/.../main.tf -->\\n~/.cache/terragrunt/cas/store/ab/abc123...\" {\n  shape: rectangle\n}\n\nlinked_target2: \"Linked Target\\n\\n.terragrunt-cache/.../variables.tf -->\\n~/.cache/terragrunt/cas/store/cd/cd7890...\" {\n  shape: rectangle\n}\n\n# Flow\ngit_repo -> check_cas\ncheck_cas -> clone_store\ncheck_cas -> read_cas\nclone_store -> read_cas\nread_cas -> link_step\nlink_step -> linked_target1\nlink_step -> linked_target2\n```\n\n### Deduplication Mechanism\n\nCAS achieves deduplication through hard links, which allows multiple files to use the same physical space on disk, avoiding duplicated content in repositories cloned by Terragrunt.\n\n- **Hard Links**: When the same content is requested multiple times, CAS creates hard links from the read-only store to each target directory\n- **Automatic Fallback**: If hard linking fails (e.g., cross-filesystem boundaries, operating system limitations), CAS automatically falls back to copying the content instead\n\n### Performance Benefits\n\nCAS provides significant performance improvements:\n\n- **Faster Subsequent Clones**: Once content is in CAS, subsequent clones skip the network download and Git clone operations entirely\n- **Reduced Disk Usage**: Hard links share the same inode, so duplicate content only consumes disk space once, regardless of how many times the file is used in clones by Terragrunt\n"
  },
  {
    "path": "docs/src/content/docs/03-features/07-caching/index.mdx",
    "content": "---\ntitle: Caching\ndescription: Learn how Terragrunt optimizes performance through provider caching and content deduplication.\nslug: features/caching\nsidebar:\n  label: Overview\n  order: 1\n---\n\nTerragrunt provides several caching mechanisms to improve performance and reduce bandwidth usage when working with OpenTofu/Terraform. These features ensure that providers and content are downloaded and stored efficiently, avoiding redundant work across multiple units.\n\nTerragrunt's caching ecosystem includes:\n\n- **Provider caching** to avoid redundant downloads of large provider binaries\n- **Content deduplication** to share identical content across multiple configurations\n"
  },
  {
    "path": "docs/src/content/docs/03-features/08-filter/02-name.mdx",
    "content": "---\ntitle: Name Expressions\ndescription: Match units and stacks by their name\nslug: features/filter/name\nsidebar:\n  order: 2\n---\n\nimport { Aside } from '@astrojs/starlight/components';\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\n\nMatch units and stacks by their name. This is the simplest form of filtering.\n\n```bash\n# Exact match\nterragrunt find --filter app1\n\n# Glob pattern\nterragrunt find --filter 'app*'\n```\n\n<FileTree>\n- apps\n  - **app1** \\<-- Matched by the first and second filter\n    - terragrunt.hcl\n  - **app2** \\<-- Matched only by the second filter\n    - terragrunt.hcl\n  - other\n    - terragrunt.hcl\n</FileTree>\n\n<Aside type=\"note\">\nNote that `app1` and `app2` were selected _within_ the `apps` directory. Filtering on names will match _any_ unit/stack that has a directory basename name matching a filter.\n</Aside>\n"
  },
  {
    "path": "docs/src/content/docs/03-features/08-filter/03-path.mdx",
    "content": "---\ntitle: Path Expressions\ndescription: Match units and stacks by their file system path\nslug: features/filter/path\nsidebar:\n  order: 3\n---\n\nimport { Aside } from '@astrojs/starlight/components';\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\n\nMatch units and stacks by their file system path.\n\n```bash\n# Relative paths\nterragrunt find --filter './envs/prod/apps/app1'\nterragrunt find --filter './envs/stage/**'\n\n# Absolute paths\nterragrunt find --filter '/absolute/path/to/envs/dev/apps/*'\n\n# Wrapped paths (useful for explicitly indicating that this is a path expression)\nterragrunt find --filter '{./envs/prod/apps/app2}'\n```\n\n<FileTree>\n- envs\n  - prod\n    - apps\n      - **app1** \\<-- Matched by the first filter\n        - terragrunt.hcl\n      - **app2** \\<-- Matched by the fourth filter\n        - terragrunt.hcl\n  - stage\n    - apps\n      - **app1** \\<-- Matched by the second filter\n        - terragrunt.hcl\n      - **app2** \\<-- Also matched by the second filter\n        - terragrunt.hcl\n  - dev\n    - apps\n      - **app1** \\<-- Matched by the third filter\n        - terragrunt.hcl\n      - **app2** \\<-- Also matched by the third filter\n        - terragrunt.hcl\n</FileTree>\n\n<Aside type=\"note\">\nNote that globs used in path-based expressions will not recursively match nested directories unless you use the `**` wildcard.\n\n(That's why `./envs/stage/**` is used above)\n</Aside>\n\n<Aside type=\"note\">\n\nGlob patterns must use Unix forward slashes `/` to separate directories, even on Windows machines.\n\n</Aside>\n"
  },
  {
    "path": "docs/src/content/docs/03-features/08-filter/04-attributes.mdx",
    "content": "---\ntitle: Attribute Expressions\ndescription: Match units and stacks by their configuration attributes\nslug: features/filter/attributes\nsidebar:\n  order: 4\n---\n\nimport { Aside } from '@astrojs/starlight/components';\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\n\nMatch units and stacks by their configuration attributes.\n\n```bash\n# Filter by component type\nterragrunt find --filter 'type=unit'\nterragrunt find --filter 'type=stack'\n\n# Filter by external dependency status\nterragrunt find --filter '{./**}... | external=false'\nterragrunt find --filter '{./**}... | external=true'\n\n# Explicitly filter by name (useful for explicitly indicating that this is a name expression)\nterragrunt find --filter 'name=stack*'\n```\n\n<FileTree>\n- .\n  - **unit1** \\<-- Matched by the first filter\n    - terragrunt.hcl\n  - **stack1** \\<-- Matched by the second and fifth filter\n    - terragrunt.stack.hcl\n- ..\n  - dependencies \\<-- Note that this directory is sibling to the current working directory\n    - **dependency-of-app1** \\<-- Matched by the fourth filter, but not the third filter\n        - terragrunt.hcl\n</FileTree>\n\nThe following are the attributes supported for attribute-based expressions:\n\n| Attribute | Description |\n|-----------|-------------|\n| name | Match units and stacks by their directory basename. |\n| type | Match units and stacks by their type. |\n| external | Match units and stacks if they are external to the current working directory. |\n| reading | Match units and stacks by the files they read. |\n| source | Match units and stacks by their Terraform source URL or path specified in the `terraform` block of `terragrunt.hcl` files. |\n\n## Reading-Based Expressions\n\nMatch units and stacks by the files they read.\n\nConsider the following file tree:\n\n<FileTree>\n\n- reading-shared-hcl\n  - terragrunt.hcl\n- also-reading-shared-hcl\n  - terragrunt.hcl\n- not-reading-shared-hcl\n  - terragrunt.hcl\n- shared.hcl\n\n</FileTree>\n\nSuppose that `reading-shared-hcl` and `also-reading-shared-hcl` both read `shared.hcl` in their configurations, like so:\n\n```hcl\n# terragrunt.hcl\n\nlocals {\n shared = read_terragrunt_config(find_in_parent_folders(\"shared.hcl\"))\n}\n```\n\nIf you run the command `terragrunt run --all --filter 'reading=shared.hcl' -- plan` from the root folder, both\n`reading-shared-hcl` and `also-reading-shared-hcl` will be run; not `not-reading-shared-hcl`.\n\nThis is because the `read_terragrunt_config` HCL function has a special hook that allows Terragrunt to track that it has\nread the file `shared.hcl`. This hook is used by all native HCL functions that Terragrunt supports which read files.\n\nNote, however, that there are certain scenarios where Terragrunt may not be able to track that a file has been read this way.\n\nFor example, you may be using a bash script to read a file via [`run_cmd`](/reference/hcl/functions/#run_cmd), or reading the file via OpenTofu/Terraform code. To support these\nuse-cases, the [`mark_as_read`](/reference/hcl/functions/#mark_as_read) function can be used to explicitly mark a file as read in the unit.\n\nThat would look something like this:\n\n```hcl\n# terragrunt.hcl\n\nlocals {\n  filename = mark_as_read(\"file-read-by-tofu.txt\")\n}\n\ninputs = {\n  filename = local.filename\n}\n```\n<Aside type=\"caution\">\nDue to how Terragrunt parses configurations during `run --all`, functions will only properly mark files as read if they are used outside the `inputs` attribute.\n\nReading a file directly in the `inputs` attribute will not mark the file as read, as the `inputs` attribute is not parsed until after the queue has already been populated to support rendering of dependency outputs, which are only available after dependencies have been run.\n</Aside>\n\n## Source-Based Expressions\n\nMatch units and stacks by their Terraform source URL or path specified in the `terraform` block of `terragrunt.hcl` files.\n\n```bash\n# Filter by exact source match\nterragrunt find --filter 'source=github.com/acme/foo'\nterragrunt find --filter 'source=gitlab.com/example/baz'\nterragrunt find --filter 'source=./module'\n\n# Filter by source using glob patterns\nterragrunt find --filter 'source=*github.com**acme/*'\nterragrunt find --filter 'source=git::git@github.com:acme/**'\nterragrunt find --filter 'source=**github.com**'\nterragrunt find --filter 'source=gitlab.com/**'\n```\n\n<FileTree>\n- .\n  - **github-acme-foo** \\<-- Matched by source=github.com/acme/foo and source=*github.com**acme/*\n    - terragrunt.hcl (source: github.com/acme/foo)\n  - **github-acme-bar** \\<-- Matched by source=*github.com**acme/* and source=git::git@github.com:acme/**\n    - terragrunt.hcl (source: git::git@github.com:acme/bar)\n  - **gitlab-example-baz** \\<-- Matched by source=gitlab.com/example/baz and source=gitlab.com/**\n    - terragrunt.hcl (source: gitlab.com/example/baz)\n  - **local-module** \\<-- Matched by source=./module\n    - terragrunt.hcl (source: ./module)\n    - module\n      - main.tf\n  - other-unit\n    - terragrunt.hcl (source: s3://bucket/module)\n</FileTree>\n\n<Aside type=\"note\">\nThe `source=` filter matches against the Terraform source URL or path specified in the `terraform` block of `terragrunt.hcl` files in units. It supports glob patterns, allowing you to match multiple sources with patterns like `*github.com**` or `gitlab.com/**`. This is useful for filtering units that use specific module sources, such as all units using a particular GitHub organization's modules or all local modules.\n\nThis attribute may be supported on stacks in the future.\n</Aside>\n"
  },
  {
    "path": "docs/src/content/docs/03-features/08-filter/05-graph.mdx",
    "content": "---\ntitle: Graph Expressions\ndescription: Filter units based on their dependency relationships using graph traversal operators\nslug: features/filter/graph\nsidebar:\n  order: 5\n---\n\nimport { Aside } from '@astrojs/starlight/components';\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\n\nFilter units and stacks based on their dependency relationships using graph traversal operators. This allows you to find components that depend on a target, or components that a target depends on.\n\nGraph-based expressions use the ellipsis (`...`) operator to indicate graph traversal direction and the caret (`^`) operator to exclude the target from results.\n\n## Include Dependencies\n\nUse `...` _after_ a target expression to include the target and all of its dependencies:\n\n```bash\n# Find 'service' and everything it depends on\nterragrunt find --filter 'service...'\n```\n\n<FileTree>\n- .\n  - **service** \\<-- Matched (target)\n    - terragrunt.hcl (depends on: db, cache, vpc)\n  - **db** \\<-- Matched (dependency of service)\n    - terragrunt.hcl (depends on: vpc)\n  - **cache** \\<-- Matched (dependency of service)\n    - terragrunt.hcl (depends on: vpc)\n  - **vpc** \\<-- Matched (dependency of service, db, cache)\n    - terragrunt.hcl\n</FileTree>\n\n## Include Dependents\n\nUse `...` _before_ a target expression to include the target and all components that depend on it:\n\n```bash\n# Find 'vpc' and everything that depends on it\nterragrunt find --filter '...vpc'\n```\n\n<FileTree>\n- .\n  - **vpc** \\<-- Matched (target)\n    - terragrunt.hcl\n  - **db** \\<-- Matched (depends on vpc)\n    - terragrunt.hcl (depends on: vpc)\n  - **cache** \\<-- Matched (depends on vpc)\n    - terragrunt.hcl (depends on: vpc)\n  - **service** \\<-- Matched (depends on vpc via db and cache)\n    - terragrunt.hcl (depends on: db, cache)\n</FileTree>\n\n## Include Both Directions\n\nUse `...` _before and after_ a target expression to include a target, all its dependencies, and all its dependents:\n\n```bash\n# Find 'db' and its complete dependency graph\nterragrunt find --filter '...db...'\n```\n\n<FileTree>\n- .\n  - **vpc** \\<-- Matched (dependency of db)\n    - terragrunt.hcl\n  - **db** \\<-- Matched (target)\n    - terragrunt.hcl (depends on: vpc)\n  - **service** \\<-- Matched (depends on db)\n    - terragrunt.hcl (depends on: db, cache)\n</FileTree>\n\n## Exclude Target\n\nUse `^` before a target expression to exclude the target from results. This is useful when you want only the dependencies or dependents, but not the target itself:\n\n```bash\n# Find all dependents of 'vpc' but exclude 'vpc' itself\nterragrunt find --filter '...^vpc'\n```\n\n<FileTree>\n- .\n  - vpc \\<-- Not matched (due to '^' operator)\n    - terragrunt.hcl\n  - **db** \\<-- Matched (depends on vpc)\n    - terragrunt.hcl (depends on: vpc)\n  - **cache** \\<-- Matched (depends on vpc)\n    - terragrunt.hcl (depends on: vpc)\n  - **service** \\<-- Matched (depends on vpc via db and cache)\n    - terragrunt.hcl (depends on: db, cache)\n</FileTree>\n\n## Depth-Limited Traversal\n\nYou can limit how many levels of dependencies or dependents to traverse by adding a numeric depth before or after the ellipsis (`...`) operator. This is useful when you only want immediate or nearby relationships rather than the full transitive closure.\n\n```bash\n# Find 'service' and only its direct dependencies (1 level deep)\nterragrunt find --filter 'service...1'\n\n# Find 'vpc' and only components that directly depend on it (1 level)\nterragrunt find --filter '1...vpc'\n\n# Find 'db' with 2 levels of dependencies and 1 level of dependents\nterragrunt find --filter '1...db...2'\n```\n\nGiven this dependency graph where service depends on db and cache, which both depend on vpc:\n\n<FileTree>\n- .\n  - vpc\n    - terragrunt.hcl\n  - db\n    - terragrunt.hcl (depends on: vpc)\n  - cache\n    - terragrunt.hcl (depends on: vpc)\n  - service\n    - terragrunt.hcl (depends on: db, cache)\n</FileTree>\n\nUsing `service...1` (dependencies with depth 1):\n\n<FileTree>\n- .\n  - vpc \\<-- Not matched (2 hops away, beyond depth limit)\n    - terragrunt.hcl\n  - **db** \\<-- Matched (1 hop from service)\n    - terragrunt.hcl (depends on: vpc)\n  - **cache** \\<-- Matched (1 hop from service)\n    - terragrunt.hcl (depends on: vpc)\n  - **service** \\<-- Matched (target)\n    - terragrunt.hcl (depends on: db, cache)\n</FileTree>\n\n<Aside type=\"note\" title=\"Multiple Targets and Depth\">\nWhen a filter matches multiple targets, the depth limit applies independently to each target. If a component is reachable from multiple targets at different distances, it will be included if it is within the depth limit of _any_ target.\n\nFor example, if target A can reach component X in 3 hops and target B can reach X in 1 hop, with a depth limit of 2, X will be included because it is within 2 hops of target B.\n</Aside>\n\n<Aside type=\"tip\">\nGraph expressions require dependency/dependent information to work correctly.\n\nWhen using graph expressions, Terragrunt automatically discovers dependency relationships between components to enable graph traversal. This may add some overhead compared to simple name or path filters, as Terragrunt will need to recursively parse and evaluate HCL files to determine whether there are more dependencies or dependents to include.\n\nNote that this overhead is especially noticeable in _dependent_ graph expressions, as Terragrunt will need to recursively parse _all_ units that could _possibly_ depend on the target. Use this expression judiciously.\n</Aside>\n"
  },
  {
    "path": "docs/src/content/docs/03-features/08-filter/06-git.mdx",
    "content": "---\ntitle: Git Expressions\ndescription: Filter units and stacks based on Git diffs using Git expressions\nslug: features/filter/git\nsidebar:\n  order: 6\n---\n\nimport { Aside } from '@astrojs/starlight/components';\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\n\nMatch units and stacks based on changes between Git references. This is useful for targeting infrastructure that has been modified, added, or removed between commits, branches, or tags.\n\nGit-based expressions are written between `[` and `]` characters, and use the `...` operator to indicate the range of changes to compare.\n\n```bash\n# Compare between two references\nterragrunt find --filter '[main...HEAD]'\n\n# Shorthand: compare reference to HEAD\nterragrunt find --filter '[main]'\n\n# Compare between specific commits\nterragrunt find --filter '[abc123...def456]'\n\n# Compare between tags\nterragrunt find --filter '[v1.0.0...v2.0.0]'\n\n# Compare using relative references\nterragrunt find --filter '[HEAD~1...HEAD]'\n\n# Compare between branches\nterragrunt find --filter '[feature-branch...main]'\n```\n\n<FileTree>\n- .\n  - **modified-unit** \\<-- Matched by [main...HEAD] (terragrunt.hcl was modified)\n    - terragrunt.hcl (modified)\n  - **new-unit** \\<-- Matched by [main...HEAD] (terragrunt.hcl was added)\n    - terragrunt.hcl (added)\n  - **removed-unit** \\<-- Matched by [main...HEAD] (terragrunt.hcl was removed)\n    - (directory removed)\n  - unchanged-unit\n    - terragrunt.hcl (unchanged)\n</FileTree>\n\n## How it works\n\nWhen evaluating a Git-based filter, Terragrunt will first generate a worktree for every reference that needs to be evaluated, and assess the diffs between Git references.\n\ne.g. For a filter like `[main...HEAD]`, Terragrunt will generate a worktree for `main` and one for `HEAD` in temporary directories, and use `git diff` to assess the diffs between the two references.\n\nThen, for any unit that is discovered within those worktrees, Terragrunt will enqueue that unit for a run in the run queue _in the worktree where it was discovered_.\n\nIn the example above, the `modified-unit` will be discovered in a \"to\" temporary directory (e.g. `/tmp/.../terragrunt-worktree-HEAD.../modified-unit`), whereas the `removed-unit` would be discovered in the \"from\" temporary directory (e.g. `/tmp/.../terragrunt-worktree-main.../removed-unit`).\n\nThis is important to recognize, as it's how destroys will be possible despite the unit no longer being present in the current working directory. As a consequence, however, you may find that paths don't behave how you expect, as you will be performing runs in the temporary directories created for the relevant worktrees.\n\n<Aside type=\"caution\" title=\"Remote State Recommended\">\n\nWhen using Git-based filter expressions (e.g. `[HEAD~1...HEAD]`), it is **strongly recommended** to use remote state configurations. Units discovered using Git-based filter expressions may not properly detect dependency outputs when using local state, which can lead to unexpected outcomes such as mock outputs being used instead of actual dependency outputs.\n\n</Aside>\n\n<Aside type=\"tip\" title=\"Testing with Local State\">\n\nIf you need to test with local state while using Git-based filter expressions, you can work around the limitations by placing state files in a separate location using absolute paths in your `remote_state` configuration. This ensures that state files are stored consistently regardless of which worktree directory Terragrunt is operating in.\n\nFor example, instead of using a relative path:\n\n```hcl\nremote_state {\n  backend = \"local\"\n  config = {\n    path = \"${get_parent_terragrunt_dir()}/.state/${path_relative_to_include()}/tofu.tfstate\"\n  }\n}\n```\n\nConsider using an absolute path to a shared location:\n\n```hcl\nremote_state {\n  backend = \"local\"\n  config = {\n    path = \"/tmp/terragrunt-state/${path_relative_to_include()}/tofu.tfstate\"\n  }\n}\n```\n\nNote that this is a workaround for testing purposes only. For production use, remote state backends (such as S3, GCS, or Azure Storage) are strongly recommended.\n\n</Aside>\n\n## Interaction with the run command\n\nWhen using Git-based expressions and the `run` command, you are required to use one of the `plan` or `apply` commands, and not the `-destroy` flag.\n\nThis is because whether a unit will be destroyed is determined by logic relevant to inspecting changes in Git.\n\nWhen units are added or modified between two Git references, they will be planned or applied. When the units are removed between two Git references, they will be planned for destruction (with `plan -destroy`) or destroyed (with `apply -destroy`).\n\nIn the scenario above, running the following:\n\n```bash\nterragrunt run --all --filter '[main...HEAD]' -- plan\n```\n\nWill result in the following:\n\n- `modified-unit` will be planned (`tofu plan`)\n- `new-unit` will be planned (`tofu plan`)\n- `removed-unit` will be planned for destruction (`tofu plan -destroy`)\n- `unchanged-unit` will be ignored\n\n<Aside type=\"tip\" title=\"Allowing destroys\">\n\nWhen using Git-based filter expressions and the run command, Terragrunt won't destroy units that are removed between the two Git references unless you use the `--filter-allow-destroy` flag.\n\n```bash\nterragrunt run --all --filter '[main...HEAD]' --filter-allow-destroy -- destroy\n```\n\nThis is a safeguard to prevent unintended destruction of infrastructure.\n\n</Aside>\n\n## The `--filter-affected` flag\n\nFor the common use case of comparing the default branch (typically `main`) with `HEAD`, you can use the `--filter-affected` flag as a convenient shorthand:\n\n```bash\n# Find components affected by changes between main and HEAD\nterragrunt find --filter-affected\n\n# Equivalent to:\nterragrunt find --filter '[main...HEAD]'\n```\n\nThe `--filter-affected` flag automatically detects your repository's default branch and compares it with `HEAD`.\n\n"
  },
  {
    "path": "docs/src/content/docs/03-features/08-filter/07-combining.mdx",
    "content": "---\ntitle: Combining Expressions\ndescription: Combine filter expressions using negation, intersection, and union operators\nslug: features/filter/combining\nsidebar:\n  order: 7\n---\n\nimport { Aside } from '@astrojs/starlight/components';\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\n\n## Negated Expressions\n\nNegate filter expressions using the `!` prefix. Negated expressions are always evaluated after all positive expressions have been evaluated.\n\n```bash\n# Exclude by name\nterragrunt find --filter '!app1'\n\n# Exclude by path\nterragrunt find --filter '!./prod/**'\n\n# Exclude by type\nterragrunt find --filter '!type=stack'\n```\n\n<FileTree>\n- envs\n  - prod\n    - apps\n      - app1 \\<-- Excluded by _both_ the first and second filter\n        - terragrunt.hcl\n      - app2 \\<-- Matched by all filters _except_ the second filter\n        - terragrunt.hcl\n    - stacks\n      - **stack1** \\<-- Excluded by _both_ the second and third filter\n        - terragrunt.stack.hcl\n  - stage\n    - apps\n      - **app1** \\<-- Matched by all filters _except_ the first filter\n        - terragrunt.hcl\n      - **app2** \\<-- Matched by all filters\n        - terragrunt.hcl\n    - stacks\n      - **stack1** \\<-- Matched by all filters _except_ the third filter\n        - terragrunt.stack.hcl\n\n</FileTree>\n\n## Intersection Expressions\n\nUse the `|` operator to refine results from left to right. Results must match all filters in the chain to be included.\n\n```bash\n# Find all components in ./prod/** that are also units\nterragrunt find --filter './prod/** | type=unit'\n\n# Find all components in ./prod/** that are not units\nterragrunt find --filter './prod/** | !type=unit'\n\n# You can chain as many filters as you want to further refine the results\nterragrunt find --filter './dev/** | type=unit | !name=unit1'\n```\n\n<FileTree>\n- prod\n  - units\n    - **unit1** \\<-- Matched by the first filter\n      - terragrunt.hcl\n    - **unit2** \\<-- Matched by the first filter\n      - terragrunt.hcl\n  - stacks\n    - **stack1** \\<-- Matched by the second filter\n      - terragrunt.stack.hcl\n    - **stack2** \\<-- Matched by the second filter\n      - terragrunt.stack.hcl\n- dev\n  - units\n    - unit1\n      - terragrunt.hcl\n    - **unit2** \\<-- Matched by the third filter\n      - terragrunt.hcl\n  - stacks\n    - stack1\n      - terragrunt.stack.hcl\n    - stack2\n      - terragrunt.stack.hcl\n</FileTree>\n\n### Path resolution in intersection chains\n\nIn an intersection chain (`A | B | C`), the left-most expression determines which [**components**](/getting-started/terminology#component) flow through the rest of the chain. Each component carries its own **discovery context**, including a working directory where the component was discovered.\n\nRelative path expressions (like `./apps/*`) in any part of the chain resolve against the working directory of the component's discovery context — **not** the user's current working directory. These two are the same when discovering components in the current working directory.\n\n```bash\n# Referencing the file tree above\nterragrunt run --working-dir prod --filter './units/** | unit1'\n```\n\nWhen using [Git expressions](/features/filter/git), components are discovered in temporary Git worktrees (see [How it works](/features/filter/git#how-it-works) for more details. As such, relative paths resolve relative to the root of the Git worktree where the component was discovered.\n\n```bash\n# Note that we still need to specify 'prod' here in the path.\nterragrunt run --working-dir prod --filter '[main...HEAD] | ./prod/units/**'\n```\n\n## Union Expressions\n\nSpecify multiple `--filter` flags to merge results from multiple filters.\n\n```bash\n# Find components named 'unit1' and 'stack1'\nterragrunt find --filter unit1 --filter stack1\n\n# Find components in ./envs/prod/* and ./envs/stage/*\nterragrunt find --filter './envs/prod/*' --filter './envs/stage/*'\n\n# Find components named 'stack2' _except_ those in ./envs/prod/* and ./envs/stage/*\nterragrunt find --filter stack2 --filter '!./envs/prod/**' --filter '!./envs/stage/**'\n```\n\n<FileTree>\n- envs\n  - prod\n    - **unit1** \\<-- Matched by the first filter _and_ the second filter\n      - terragrunt.hcl\n    - **unit2** \\<-- Matched by the second filter\n      - terragrunt.hcl\n    - **stack1** \\<-- Matched by the first filter _and_ the second filter\n      - terragrunt.stack.hcl\n    - **stack2** \\<-- Matched by the second filter\n      - terragrunt.stack.hcl\n  - stage\n    - **unit1** \\<-- Matched by the first filter _and_ the second filter\n      - terragrunt.hcl\n    - **unit2** \\<-- Matched by the second filter\n      - terragrunt.hcl\n    - **stack1** \\<-- Matched by the first filter _and_ the second filter\n      - terragrunt.stack.hcl\n    - **stack2** \\<-- Matched by the second filter\n      - terragrunt.stack.hcl\n  - dev\n    - **unit1** \\<-- Matched by the first filter\n      - terragrunt.hcl\n    - unit2\n      - terragrunt.hcl\n    - **stack1** \\<-- Matched by the first filter\n      - terragrunt.stack.hcl\n    - **stack2** \\<-- Matched by the third filter\n      - terragrunt.stack.hcl\n</FileTree>\n\n### Unions of negated filters\n\nWhen a filter query starts with a negation (`!`), the result is applied after _all_ positive filters have been applied.\n\nThis means that if you have a filter query like this:\n\n```bash\nterragrunt find --filter '!type=unit' --filter 'name=unit1'\n```\n\nThe result will be the components that are not units _and_ are named `unit1`.\n\nThis means that you should be able to expect negative filters to take effect regardless of how other positive filters may result in the addition of results.\n\n<Aside type=\"tip\" title=\"Using Negated Expressions\">\nIf you have infrastructure that you _never_ want to run, you can consider leveraging the [`--filters-file`](/features/filter/filters-file) to automatically negate them.\n</Aside>\n\n### Unions with Git expressions\n\nUnion deduplication in filter results is based on the **absolute path** of each discovered component. When combining [Git expressions](/features/filter/git) with non-Git expressions in a union, it might seem like the same unit has appeared twice.\ne.g.\n\n```bash\n$ terragrunt find --filter '[main...HEAD]' --filter ./live/foo\nlive/foo\nlive/foo\n```\n\nThe reason for this is usually that a unit with the same name has been discovered twice: once discovered from your current working directory and once from a git worktree. From Terragrunt's perspective, these are two different units and can be operated on independently.\n\nIf your intent is not to use Terragrunt in this way (to operate on units in temporary Git worktrees and units in your current working directory independently), you can use the `find` command instead to perform the logic of discovering components that have changed between Git references, then separately perform a `run` against units in your current working directory.\n\n```bash\nterragrunt find --filter '[main...HEAD]' | awk '{printf \"{%s}\\n\", $0}' > /tmp/diffs.txt\nterragrunt run --all --filters-file /tmp/diffs.txt -- plan\n```\n\n<Aside type=\"tip\" title=\"Explicit path filters\">\n\nNote the use of `awk` here to wrap paths in `{}`. This is to explicitly mark the paths as [path expressions](/features/filter/path). This is only necessary for disambiguation between [path expressions](/features/filter/path) and [name expressions](/features/filter/name). If you aren't concerned with this ambiguity, you can omit this step.\n\n</Aside>\n\n<Aside type=\"caution\" title=\"Supporting destroys\">\n\nNote that this approach won't support destroys. For Git expressions to support destroys, Terragrunt needs to be able to perform runs on units that might not exist in the current working directory (as they've been removed in a commit). If you want to support the full infrastructure lifecycle with Git expressions, you will want to use Git expressions directly in runs, or add additional tooling.\n\n</Aside>\n\n"
  },
  {
    "path": "docs/src/content/docs/03-features/08-filter/08-filters-file.mdx",
    "content": "---\ntitle: Filters File\ndescription: Use a filters file to define reusable filter expressions for Terragrunt\nslug: features/filter/filters-file\nsidebar:\n  order: 8\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nIf you want to ensure that certain units are always included or excluded, you can use a filters file.\n\nFilters files are simple text files that contain filter expressions delimited by newlines. Empty lines and lines starting with `#` are ignored.\n\n```text\n# filters.txt\n\n./subtree/**\n!./subtree/dependency/**\n```\n\n```bash\nterragrunt run --all --filters-file filters.txt -- plan\n```\n\nRunning Terragrunt like this is equivalent to running it with the following flags:\n\n```bash\nterragrunt run --all --filter './subtree/**' --filter '!./subtree/dependency/**' -- plan\n```\n\n<Aside type=\"tip\">\n\nIf you want to automatically configure certain filters to be applied by default, you can name the filters file `.terragrunt-filters`.\n\nTerragrunt will automatically read filters from this file if it exists in the current working directory.\n\n</Aside>\n"
  },
  {
    "path": "docs/src/content/docs/03-features/08-filter/index.mdx",
    "content": "---\ntitle: Overview\ndescription: Learn how to use the --filter flag to target specific infrastructure\nslug: features/filter\nsidebar:\n  order: 1\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Aside } from '@astrojs/starlight/components';\n\nThe `--filter` flag provides a sophisticated querying syntax for targeting specific [units](/features/units) and [stacks](/features/stacks) in Terragrunt commands. This unified approach offers powerful filtering capabilities using a flexible query language.\n\n## Filter Syntax Overview\n\nThe filter syntax allows you to target units and stacks using several different approaches. Usage of the filter flag in a command can look like this:\n\n```bash\n$ terragrunt find --filter './prod/** | name=web'\nprod/services/web\n```\n\nWhere the result of `find` above might have been:\n\n```bash\n$ terragrunt find\nprod/services/web\nprod/services/api\nprod/data/db\ndev/services/web\ndev/services/api\ndev/data/db\n```\n\nFor the following file tree:\n\n<FileTree>\n- prod\n  - services\n    - **web** \\<-- Matched by the filter\n      - terragrunt.hcl\n    - api\n      - terragrunt.hcl\n  - data\n    - db\n      - terragrunt.hcl\n- dev\n  - services\n    - web\n      - terragrunt.hcl\n    - api\n      - terragrunt.hcl\n  - data\n    - db\n      - terragrunt.hcl\n</FileTree>\n\n## Filter Expressions\n\nThere are several different types of filter expressions, and particular ways in which they can be combined to achieve different results. You can learn more about that below.\n\n| Filter Type | Description |\n|-------------|-------------|\n| [Name](/features/filter/name) | Match units and stacks by their name. |\n| [Path](/features/filter/path) | Match units and stacks by their file system path. |\n| [Attribute](/features/filter/attributes) | Match units and stacks by their configuration attributes. |\n| [Negated](/features/filter/combining#negated-expressions) | Exclude units and stacks using the `!` prefix. |\n| [Intersection](/features/filter/combining#intersection-expressions) | Use the `\\|` operator to refine results. |\n| [Union](/features/filter/combining#union-expressions) | Combine filter results using multiple `--filter` flags. |\n| [Graph](/features/filter/graph) | Filter units based on their dependency relationships using graph traversal operators. |\n| [Git](/features/filter/git) | Filter units and stacks based on Git diffs using Git expressions. |\n\n## Usage with Commands\n\nThe following commands all support the `--filter` flag using the same filtering syntax (note the section below on [special interactions](#special-interactions)):\n\n- [find](/reference/cli/commands/find)\n- [list](/reference/cli/commands/list)\n- [run](/reference/cli/commands/run)\n- [hcl fmt](/reference/cli/commands/hcl/fmt)\n- [hcl validate](/reference/cli/commands/hcl/validate)\n- [stack run](/reference/cli/commands/stack/run)\n- [stack generate](/reference/cli/commands/stack/generate)\n\nThis flag is intended to be a flexible way to target specific infrastructure that allows you to dry-run infrastructure targeting using discovery commands (like `find` and `list`) before running a command that actually affects infrastructure (like `run`).\n\n## Comparison with Queue Control Flags\n\nThe `--filter` flag provides a unified alternative to multiple queue control flags.\n\n| Legacy Flag | Filter Equivalent |\n|-------------|-------------------|\n| `--queue-include-dir=./path` | `--filter='./path'` |\n| `--queue-exclude-dir=./path` | `--filter='!./path'` |\n| `--queue-include-external` | `--filter='{./**}...'` |\n| `--queue-include-units-reading=shared.hcl` | `--filter='reading=shared.hcl'` |\n\n<Aside type=\"tip\" title=\"Queue flag aliases\">\n\nIf you are currently using those queue control flags, note that you are actually already using the equivalent filter expressions, as they are aliased internally.\n\nYou will generally have a better experience using the filter flag instead, as it provides a more powerful and flexible way to target infrastructure.\n\n</Aside>\n\n## Special Interactions\n\nCertain commands have special interactions with the `--filter` flag that are worth noting.\n\n### `hcl fmt`\n\nUnlike when used for most commands, the `--filter` flag is used to filter on individual HCL files when used with the `hcl fmt`.\n\nAll other commands use `--filter` to filter on units and/or stacks (which are directories). As a result, only path-based filter expressions are supported. Attribute-based filters like `type=unit` or `name=my-app` are not applicable to file-level operations.\n\nExample:\n\n```bash\n# Supported: Path-based expressions\nterragrunt hcl fmt --filter './prod/**/*.hcl'\n\n# Not supported: Attribute-based expressions\nterragrunt hcl fmt --filter 'type=unit'  # This will not work\n```\n\n### `stack generate`\n\nWhen using `--filter` with `stack generate`, filter expressions will only be recognized if they explicitly target stacks. This is to ensure that filters are not over-applied, preventing any stack generation from occurring.\n\n```bash\n# Supported: Only generate the stacks that match the filter, as we are explicitly indicating that we are targeting stacks.\nterragrunt stack generate --filter 'name=prod | type=stack'\n\n# Not supported: This filter will be ignored, as we are not explicitly indicating that we are targeting stacks.\nterragrunt stack generate --filter 'name=prod'  # This will not work\n```\n\nThe reason for this is that stack generation can also be done automatically as part of other commands, like `run`, and thus we need to make it clear that we're trying to control stack generation rather than run behavior.\n\n```bash\n# This will run any unit named 'vpc'\nterragrunt run --all --filter 'vpc' -- plan\n\n# This will run any unit named 'vpc', and prevent stack generation in any stack not named 'dev' (including any stacks named 'vpc')\nterragrunt stack run --filter 'vpc' --filter 'name=dev | type=stack' -- apply\n```\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/01-hcl/01-overview.mdx",
    "content": "---\ntitle: Overview\ndescription: Learn how to configure Terragrunt\nslug: reference/hcl\nsidebar:\n  order: 1\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\n\nTerragrunt configuration is defined in [HCL](https://github.com/hashicorp/hcl) files. This uses the same HCL syntax as OpenTofu/Terraform itself.\n\nHere's an example:\n\n```hcl\n# terragrunt.hcl\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../vpc\", \"../mysql\", \"../redis\"]\n}\n```\n\nThe core of Terragrunt configuration is that of the [unit](/getting-started/terminology#unit), which is canonically defined using `terragrunt.hcl` files.\n\nTerragrunt also supports [JSON-serialized HCL](https://github.com/hashicorp/hcl/blob/hcl2/json/spec.md) defined in `terragrunt.hcl.json` files. Where `terragrunt.hcl` is mentioned in documentation, you can always use `terragrunt.hcl.json` instead.\n\nWhen determining the configuration for a unit, Terragrunt figures out the path to its configuration file according to the following rules:\n\n1. The value of the `--config` command-line option, if specified.\n\n2. The value of the `TG_CONFIG` environment variable, if defined.\n\n3. A `terragrunt.hcl` file in the current working directory, if it exists.\n\n4. A `terragrunt.hcl.json` file in the current working directory, if it exists.\n\n5. If none of these are found, exit with an error.\n\nRefer to the following pages for a complete reference of supported features in the terragrunt configuration file:\n\n- [Blocks](/reference/hcl/blocks)\n- [Attributes](/reference/hcl/attributes)\n- [Functions](/reference/hcl/functions)\n\n## Configuration parsing order\n\nIt is important to be aware of the terragrunt configuration parsing order when using features like [locals](/reference/hcl/blocks/#locals) and [dependency outputs](/features/stacks/stack-operations#passing-outputs-between-units), where you can reference attributes of other blocks in the config in your `inputs`. For example, because `locals` are evaluated before `dependency` blocks, you cannot bind outputs from `dependency` into `locals`. On the other hand, for the same reason, you can use `locals` in the `dependency` blocks.\n\nCurrently terragrunt parses the config in the following order:\n\n1. `include` block\n\n2. `locals` block\n\n3. Evaluation of values for `iam_role`, `iam_assume_role_duration`, `iam_assume_role_session_name`, and `iam_web_identity_token` attributes, if defined\n\n4. `dependencies` block\n\n5. `dependency` blocks, including calling `terragrunt output` on the dependent units to retrieve the outputs\n\n6. Everything else\n\n7. The config referenced by `include`\n\n8. A merge operation between the config referenced by `include` and the current config.\n\nBlocks that are parsed earlier in the process will be made available for use in the parsing of later blocks. Similarly, you cannot use blocks that are parsed later earlier in the process (e.g you can't reference `dependency` in `locals`, `include`, or `dependencies` blocks).\n\nNote that the parsing order is slightly different when using the `--all` flag of the [`run`](/reference/cli/commands/run) command. When using the `--all` flag, Terragrunt parses the configuration twice. In the first pass, it follows the following parsing order:\n\n1. `include` block of all configurations in the tree\n\n2. `locals` block of all configurations in the tree\n\n3. `dependency` blocks of all configurations in the tree, but does NOT retrieve the outputs\n\n4. `terraform` block of all configurations in the tree\n\n5. `dependencies` block of all configurations in the tree\n\nThe results of this pass are then used to build the dependency graph of the units in the stack. Once the graph is constructed, Terragrunt will loop through the units and run the specified command. It will then revert to the single configuration parsing order specified above for each unit as it runs the command.\n\nThis allows Terragrunt to avoid resolving `dependency` on units that haven't been applied yet when doing a clean deployment from scratch with `run --all apply`.\n\n## Stacks\n\nWhen multiple units, each with their own `terragrunt.hcl` file exist in child directories of a single parent directory, that parent directory becomes a [stack](/getting-started/terminology#stack).\n\n> **New to stacks?** For a comprehensive introduction to the concept, see our [Stacks](/features/stacks) guide.\n\n### What is a terragrunt.stack.hcl file?\n\nA `terragrunt.stack.hcl` file is a **blueprint** that defines how to generate Terragrunt configuration programmatically.\n\nIt tells Terragrunt:\n\n- What units to create.\n- Where to get their configurations from.\n- Where to place them in the directory structure.\n- What values to pass to each unit.\n\n### The Two Types of Blocks\n\n#### `unit` blocks - Define Individual Infrastructure Components\n\n- **Purpose**: Define a single, deployable piece of infrastructure.\n- **Use case**: When you want to create a single piece of isolated infrastructure (e.g. a specific VPC, database, or application).\n- **Result**: Generates a single `terragrunt.hcl` file in the specified path.\n\n#### `stack` blocks - Define Reusable Infrastructure Patterns\n\n- **Purpose**: Define a collection of related units that can be reused.\n- **Use case**: When you have a common, multi-unit pattern (like \"dev environment\" or \"three-tier web application\") that you want to deploy multiple times.\n- **Result**: Generates another `terragrunt.stack.hcl` file that can contain more units or stacks.\n\n### Comparison: unit vs stack blocks\n\n| Aspect | `unit` block | `stack` block |\n|--------|-------------|---------------|\n| **Purpose** | Define a single infrastructure component | Define a reusable collection of components |\n| **When to use** | For specific, one-off infrastructure pieces | For patterns of infrastructure pieces that you want provisioned together |\n| **Generated output** | A directory with a single `terragrunt.hcl` file | A directory with a `terragrunt.stack.hcl` file |\n\n### The Complete Workflow\n\n1. **Author**: Write a `terragrunt.stack.hcl` file with `unit` and/or `stack` blocks.\n2. **Generate**: Run `terragrunt stack generate` to create the actual units\\*.\n3. **Deploy**: Run `terragrunt stack run apply` to deploy all units\\*\\*.\n\n\\* Multiple commands (like `stack run` or `run --all`) automatically generate units from `terragrunt.stack.hcl` files for you.\n\n\\*\\* You can also just use `run --all apply` to deploy all units in the stack.\n\n### Example: Simple Stack with Units\n\n```hcl\n# terragrunt.stack.hcl\n\nunit \"vpc\" {\n  source = \"git::git@github.com:acme/infrastructure-catalog.git//units/vpc?ref=v0.0.1\"\n  path   = \"vpc\"\n  values = {\n    vpc_name = \"main\"\n    cidr     = \"10.0.0.0/16\"\n  }\n}\n\nunit \"database\" {\n  source = \"git::git@github.com:acme/infrastructure-catalog.git//units/database?ref=v0.0.1\"\n  path   = \"database\"\n  values = {\n    engine   = \"postgres\"\n    version  = \"13\"\n\n    vpc_path = \"../vpc\"\n  }\n}\n```\n\nRunning `terragrunt stack generate` creates:\n\n<FileTree>\n\n- terragrunt.stack.hcl\n- .terragrunt-stack\n  - vpc\n    - terragrunt.hcl\n    - terragrunt.values.hcl\n  - database\n    - terragrunt.hcl\n    - terragrunt.values.hcl\n\n</FileTree>\n\n### Example: Nested Stack with Reusable Patterns\n\n```hcl\n# terragrunt.stack.hcl\n\nstack \"dev\" {\n  source = \"git::git@github.com:acme/infrastructure-catalog.git//stacks/environment?ref=v0.0.1\"\n  path   = \"dev\"\n  values = {\n    environment = \"development\"\n    cidr        = \"10.0.0.0/16\"\n  }\n}\n\nstack \"prod\" {\n  source = \"git::git@github.com:acme/infrastructure-catalog.git//stacks/environment?ref=v0.0.1\"\n  path   = \"prod\"\n  values = {\n    environment = \"production\"\n    cidr        = \"10.1.0.0/16\"\n  }\n}\n```\n\nThe referenced stack might contain:\n\n```hcl\n# stacks/environment/terragrunt.stack.hcl\n\nunit \"vpc\" {\n  source = \"git::git@github.com:acme/infrastructure-catalog.git//units/vpc?ref=v0.0.1\"\n  path   = \"vpc\"\n  values = {\n    vpc_name = values.environment\n    cidr     = values.cidr\n  }\n}\n\nunit \"database\" {\n  source = \"git::git@github.com:acme/infrastructure-catalog.git//units/database?ref=v0.0.1\"\n  path   = \"database\"\n  values = {\n    environment = values.environment\n\n    vpc_path = \"../vpc\"\n  }\n}\n```\n\nFor more information on these configuration blocks, see:\n\n- [unit](/reference/hcl/blocks#unit)\n- [stack](/reference/hcl/blocks#stack)\n- [locals](/reference/hcl/blocks#locals)\n\nThese special configurations are used by the [stack generate command](/reference/cli/commands/stack/generate) (and all the other `stack` prefixed commands) to generate units programmatically, on demand. The units they generate are valid unit configurations, and can be read and used as if they were manually authored.\n\n## Included Configurations\n\nWhen configurations are _included_ via the [include](/reference/hcl/blocks#include) configuration block, Terragrunt expects configurations to be valid unit configurations.\n\nGenerally speaking, any HCL file found in a Terragrunt project that isn't named `terragrunt.hcl`, `terragrunt.stack.hcl` or `.terraform.lock.hcl` is expected to be partial unit configurations that will be included by a Terragrunt unit.\n\n## Formatting HCL files\n\nYou can rewrite the HCL files to a canonical format using the `hclfmt` command built into `terragrunt`. Similar to `tofu fmt`, this command applies a subset of [the OpenTofu/Terraform language style conventions](https://www.terraform.io/docs/configuration/style.html), along with other minor adjustments for readability.\n\nBy default, this command will recursively search for hcl files and format all of them for a given stack. Consider the following file structure:\n\n<FileTree>\n\n- root\n  - root.hcl\n  - prod\n    - terragrunt.hcl\n  - dev\n    - terragrunt.hcl\n  - qa\n    - terragrunt.hcl\n    - services\n      - services.hcl\n      - service01\n        - terragrunt.hcl\n\n</FileTree>\n\nIf you run `terragrunt hcl fmt` at the `root`, this will update:\n\n- `root/root.hcl`\n\n- `root/prod/terragrunt.hcl`\n\n- `root/dev/terragrunt.hcl`\n\n- `root/qa/terragrunt.hcl`\n\n- `root/qa/services/services.hcl`\n\n- `root/qa/services/service01/terragrunt.hcl`\n\nYou can set `--diff` option. `terragrunt hcl fmt --diff` will output the diff in a unified format which can be redirected to your favourite diff tool. `diff` utility must be presented in PATH.\n\nAdditionally, there's a flag `--check`. `terragrunt hcl fmt --check` will only verify if the files are correctly formatted **without rewriting** them. The command will return exit status 1 if any matching files are improperly formatted, or 0 if all matching `.hcl` files are correctly formatted.\n\nYou can exclude directories from the formatting process by using the `--exclude-dir` flag. For example, `terragrunt hcl fmt --exclude-dir=qa/services`.\n\nIf you want to format a single file, you can use the `--file` flag. For example, `terragrunt hcl fmt --file qa/services/services.hcl`.\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/01-hcl/02-blocks.mdx",
    "content": "---\ntitle: Blocks\ndescription: Learn about Terragrunt HCL configuration blocks\nslug: reference/hcl/blocks\nsidebar:\n  order: 2\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Aside } from '@astrojs/starlight/components';\n\nTerragrunt HCL configuration uses [configuration blocks](https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#blocks) when there's a structural configuration that needs to be defined for Terragrunt.\n\nThink of configuration blocks as a way to control different systems used by Terragrunt, whereas [attributes](/reference/hcl/attributes) are used to define values for those systems.\n\n## terraform\n\nThe `terraform` block is used to configure how Terragrunt will interact with OpenTofu/Terraform. This includes specifying where\nto find the OpenTofu/Terraform configuration files, any extra arguments to pass to the `tofu`/`terraform` binary, and any hooks to run\nbefore or after calling OpenTofu/Terraform.\n\nThe `terraform` block supports the following arguments:\n\n- `source` (attribute): Specifies where to find OpenTofu/Terraform configuration files. This parameter supports the same syntax as the\n  [module source](https://opentofu.org/docs/language/modules/sources/) parameter for OpenTofu/Terraform `module` blocks **except\n  for the Terraform registry** (see below note), including local file paths, Git URLs, and Git URLS with `ref`\n  parameters. Terragrunt will download all the code in the repo (i.e. the part before the double-slash `//`) so that\n  relative paths work correctly between modules in that repo.\n\n  - The `source` parameter can be configured to pull OpenTofu/Terraform modules from any Terraform module registry using\n    the `tfr` protocol. The `tfr` protocol expects URLs to be provided in the format\n    `tfr://REGISTRY_HOST/MODULE_SOURCE?version=VERSION`. For example, to pull the `terraform-aws-modules/vpc/aws`\n    module from the public Terraform registry, you can use the following as the source parameter:\n    `tfr://registry.terraform.io/terraform-aws-modules/vpc/aws?version=3.3.0`.\n  - If you wish to access a private module registry (e.g., [Terraform Cloud/Enterprise](https://www.terraform.io/docs/cloud/registry/index.html)),\n    you can provide the authentication to Terragrunt as an environment variable with the key `TG_TF_REGISTRY_TOKEN`.\n    This token can be any registry API token.\n  - The `tfr` protocol supports a shorthand notation where the `REGISTRY_HOST` can be omitted to default to the public\n    registry. The default registry depends on the wrapped executable: for Terraform, it is `registry.terraform.io`,\n    and for Opentofu, it is `registry.opentofu.org`. Additionally, if the environment variable `TG_TF_DEFAULT_REGISTRY_HOST`\n    is set, this value will be used as the default registry host instead, overriding the standard defaults for the wrapped executable.\n  - If you use `tfr:///` (note the three `/`). For example, the following will\n    fetch the `terraform-aws-modules/vpc/aws` module from the public registry:\n    `tfr:///terraform-aws-modules/vpc/aws?version=3.3.0`.\n  - You can also use submodules from the registry using `//`. For example, to use the `iam-policy` submodule from the\n    registry module\n    [terraform-aws-modules/iam](https://registry.terraform.io/modules/terraform-aws-modules/iam/aws/latest), you can\n    use the following: `tfr:///terraform-aws-modules/iam/aws//modules/iam-policy?version=4.3.0`.\n\n- `include_in_copy` (attribute): A list of glob patterns (e.g., `[\"*.txt\"]`) that should always be copied from the same directory containing `terragrunt.hcl` into the\n  OpenTofu/Terraform working directory. When you use the `source` param in your Terragrunt config and run `terragrunt <command>`,\n  Terragrunt will download the code specified at source into a scratch folder (`.terragrunt-cache`, by default), copy\n  the code in your current working directory into the same scratch folder, and then run `tofu <command>` (or `terraform <command>`) in that\n  scratch folder. By default, Terragrunt excludes hidden files and folders during the copy step. This feature allows you\n  to specify glob patterns of files that should always be copied from the Terragrunt working directory. Additional\n  notes:\n\n  - The path should be specified relative to the source directory.\n  - This list is also used when using a local file source (e.g., `source = \"../modules/vpc\"`). For example, if your\n    OpenTofu/Terraform module source contains a hidden file that you want to copy over (e.g., a `.python-version` file), you\n    can specify that in this list to ensure it gets copied over to the scratch copy\n    (e.g., `include_in_copy = [\".python-version\"]`).\n\n- `exclude_from_copy` (attribute): A list of glob patterns (e.g., `[\"*.txt\"]`) that should always be skipped from the same directory containing `terragrunt.hcl` when copying into the\n  OpenTofu/Terraform working directory. All examples valid for `include_in_copy` can be used here.\n\n  *Note that using `include_in_copy` and `exclude_from_copy` are not mutually exclusive.*\n  If a file matches a pattern in both `include_in_copy` and `exclude_from_copy`, it will not be included. If you would like to ensure that the file *is* included, make sure the patterns you use for `include_in_copy` do not match the patterns in `exclude_from_copy`.\n\n  *Note that if you wish to exclude files from being copied from a terraform module source, you should use the [before_hook](/features/units/hooks) feature.*\n\n- `copy_terraform_lock_file` (attribute): In certain use cases, you don't want to check the terraform provider lock\n  file into your source repository from your working directory as described in\n  [Lock File Handling](/reference/lock-files). This attribute allows you to disable the copy\n  of the generated or existing `.terraform.lock.hcl` from the temp folder into the working directory. Default is `true`.\n\n- `extra_arguments` (block): Nested blocks used to specify extra CLI arguments to pass to the `tofu`/`terraform` binary. Learn more\n  about its usage in the [Keep your CLI flags DRY](/features/units/extra-arguments) use case overview. Supports\n  the following arguments:\n\n  - `arguments` (required) : A list of CLI arguments to pass to `tofu`/`terraform`.\n  - `commands` (required) : A list of `tofu`/`terraform` sub commands that the arguments will be passed to.\n  - `env_vars` (optional) : A map of key value pairs to set as environment variables when calling `tofu`/`terraform`.\n  - `required_var_files` (optional): A list of file paths to OpenTofu/Terraform vars files (`.tfvars`) that will be passed in to\n    `terraform` as `-var-file=<your file>`.\n  - `optional_var_files` (optional): A list of file paths to OpenTofu/Terraform vars files (`.tfvars`) that will be passed in to\n    `tofu`/`terraform` like `required_var_files`, only any files that do not exist are ignored.\n\n- `before_hook` (block): Nested blocks used to specify command hooks that should be run before `tofu`/`terraform` is called.\n  Hooks run from the directory with the OpenTofu/Terraform module, except for hooks related to `read-config` and\n  `init-from-module`. These hooks run in the terragrunt configuration directory (the directory where `terragrunt.hcl`\n  lives).\n  Supports the following arguments:\n\n  - `commands` (required) : A list of `tofu`/`terraform` sub commands for which the hook should run before.\n  - `execute` (required) : A list of command and arguments that should be run as the hook. For example, if `execute` is set as\n    `[\"echo\", \"Foo\"]`, the command `echo Foo` will be run.\n  - `working_dir` (optional) : The path to set as the working directory of the hook. Terragrunt will switch directory\n    to this path before running the hook command. Defaults to the terragrunt configuration directory for\n    `read-config` and `init-from-module` hooks, and the OpenTofu/Terraform module directory for other command hooks.\n  - `run_on_error` (optional) : If set to true, this hook will run even if a previous hook hit an error, or in the\n    case of \"after\" hooks, if the OpenTofu/Terraform command hit an error. Default is false.\n  - `suppress_stdout` (optional) : If set to true, the stdout output of the executed commands will be suppressed. This can be useful when there are scripts relying on OpenTofu/Terraform's output and any other output would break their parsing.\n  - `if` (optional) : hook will be skipped when the argument is set or evaluates to `false`.\n\n\n- `after_hook` (block): Nested blocks used to specify command hooks that should be run after `tofu`/`terraform` is called.\n  Hooks run from the terragrunt configuration directory (the directory where `terragrunt.hcl` lives). Supports the same\n  arguments as `before_hook`.\n- `error_hook` (block): Nested blocks used to specify command hooks that run when an error is thrown. The\n  error must match one of the expressions listed in the `on_errors` attribute. Error hooks are executed after the before/after hooks.\n  To handle errors during source download (when using the `source` attribute), use `init-from-module` in the `commands` list.\n\nIn addition to supporting before and after hooks for all OpenTofu/Terraform commands, the following specialized hooks are also\nsupported:\n\n- `read-config` (after hook only): `read-config` is a special hook command that you can use with\n  the `after_hook` subblock to run an action immediately after terragrunt finishes loading the config. This hook will\n  run on every invocation of terragrunt. Note that you can only use this hook with `after_hooks`. Any `before_hooks`\n  with the command `read-config` will be ignored. The working directory for hooks associated with this\n  command will be the terragrunt config directory.\n\n- `init-from-module` and `init`: Terragrunt has two stages of initialization: one is to download [remote\n  configurations](/features/units) using `go-getter`; the other\n  is [Auto-Init](/features/units/auto-init), which configures the backend and downloads\n  provider plugins and modules. If you wish to run a hook when Terragrunt is using `go-getter` to download remote\n  configurations, use `init-from-module` for the command. This includes `error_hook` blocks to handle download failures.\n  If you wish to execute a hook when Terragrunt is using\n  `tofu init`/`terraform init` for Auto-Init, use `init` for the command. For example, an `after_hook` for the command\n  `init-from-module` will run after terragrunt clones the module, while an `after_hook` for the command `init` will run\n  after terragrunt runs `tofu init`/`terraform init` on the cloned module.\n  - Hooks for both `init-from-module` and `init` only run if the requisite stage needs to run. That is, if terragrunt\n    detects that the module is already cloned in the terragrunt cache, this stage will be skipped and thus the hooks\n    will not run. Similarly, if terragrunt detects that it does not need to run `init` in the auto init feature, the\n    `init` stage is skipped along with the related hooks.\n  - The working directory for hooks associated with `init-from-module` will run in the terragrunt config directory,\n    while the working directory for hooks associated with `init` will be the OpenTofu/Terraform module.\n\nComplete Example:\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  # Pull the OpenTofu/Terraform configuration at the github repo \"acme/infrastructure-modules\", under the subdirectory\n  # \"networking/vpc\", using the git tag \"v0.0.1\".\n  source = \"git::git@github.com:acme/infrastructure-modules.git//networking/vpc?ref=v0.0.1\"\n\n  # For any OpenTofu/Terraform commands that use locking, make sure to configure a lock timeout of 20 minutes.\n  extra_arguments \"retry_lock\" {\n    commands  = get_terraform_commands_that_need_locking()\n    arguments = [\"-lock-timeout=20m\"]\n  }\n\n  # You can also specify multiple extra arguments for each use case. Here we configure terragrunt to always pass in the\n  # `common.tfvars` var file located by the parent terragrunt config.\n  extra_arguments \"custom_vars\" {\n    commands = [\n      \"apply\",\n      \"plan\",\n      \"import\",\n      \"push\",\n      \"refresh\"\n    ]\n\n    required_var_files = [\"${get_parent_terragrunt_dir()}/common.tfvars\"]\n  }\n\n  # The following are examples of how to specify hooks\n\n  # Before apply or plan, run \"echo Foo\".\n  before_hook \"before_hook_1\" {\n    commands     = [\"apply\", \"plan\"]\n    execute      = [\"echo\", \"Foo\"]\n  }\n\n  # Before apply, run \"echo Bar\". Note that blocks are ordered, so this hook will run after the previous hook to\n  # \"echo Foo\". In this case, always \"echo Bar\" even if the previous hook failed.\n  before_hook \"before_hook_2\" {\n    commands     = [\"apply\"]\n    execute      = [\"echo\", \"Bar\"]\n    run_on_error = true\n  }\n\n  # Note that you can use interpolations in subblocks. Here, we configure it so that before apply or plan, print out the\n  # environment variable \"HOME\".\n  before_hook \"interpolation_hook_1\" {\n    commands     = [\"apply\", \"plan\"]\n    execute      = [\"echo\", get_env(\"HOME\", \"HelloWorld\")]\n    run_on_error = false\n  }\n\n  # After running apply or plan, run \"echo Baz\". This hook is configured so that it will always run, even if the apply\n  # or plan failed.\n  after_hook \"after_hook_1\" {\n    commands     = [\"apply\", \"plan\"]\n    execute      = [\"echo\", \"Baz\"]\n    run_on_error = true\n  }\n\n  # After an error occurs during apply or plan, run \"echo Error Hook executed\". This hook is configured so that it will run\n  # after any error, with the \".*\" expression.\n  error_hook \"error_hook_1\" {\n    commands  = [\"apply\", \"plan\"]\n    execute   = [\"echo\", \"Error Hook executed\"]\n    on_errors = [\n      \".*\",\n    ]\n  }\n\n  # Handle errors during source download (e.g., when the source URL is invalid or unreachable).\n  # Use \"init-from-module\" as the command to catch errors during the go-getter download phase.\n  error_hook \"source_download_error\" {\n    commands  = [\"init-from-module\"]\n    execute   = [\"echo\", \"Source download failed\"]\n    on_errors = [\".*\"]\n  }\n\n  # A special after hook to always run after the init-from-module step of the Terragrunt pipeline. In this case, we will\n  # copy the \"foo.tf\" file located by the parent terragrunt.hcl file to the current working directory.\n  after_hook \"init_from_module\" {\n    commands = [\"init-from-module\"]\n    execute  = [\"cp\", \"${get_parent_terragrunt_dir()}/foo.tf\", \".\"]\n  }\n\n  # A special after_hook. Use this hook if you wish to run commands immediately after terragrunt finishes loading its\n  # configurations. If \"read-config\" is defined as a before_hook, it will be ignored as this config would\n  # not be loaded before the action is done.\n  after_hook \"read-config\" {\n    commands = [\"read-config\"]\n    execute  = [\"bash\", \"script/get_aws_credentials.sh\"]\n  }\n}\n```\n\nLocal File Path Example with allowed hidden files:\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  # Pull the OpenTofu/Terraform configuration from the local file system. Terragrunt will make a copy of the source folder in the\n  # Terragrunt working directory (typically `.terragrunt-cache`).\n  source = \"../modules/networking/vpc\"\n\n  # Always include the following file patterns in the Terragrunt copy.\n  include_in_copy = [\n    \".security_group_rules.json\",\n    \"*.yaml\",\n  ]\n}\n```\n\n### A note about using modules from the registry\n\nThe key design of Terragrunt is to act as a preprocessor to convert **shared service modules** in the registry into a **root\nmodule**. In OpenTofu/Terraform, modules can be loosely categorized into two types:\n\n- **Root Module**: An OpenTofu/Terraform module that is designed for running `tofu init`/`terraform init` and the other workflow commands\n  (`apply`, `plan`, etc.). This is the entrypoint module for deploying your infrastructure. Root modules are identified\n  by the presence of key blocks that setup configuration about how OpenTofu/Terraform behaves, like `backend` blocks (for\n  configuring state) and `provider` blocks (for configuring how OpenTofu/Terraform interacts with the cloud APIs).\n- **Shared Module**: A OpenTofu/Terraform module that is designed to be included in other OpenTofu/Terraform modules through `module`\n  blocks. These modules are missing many of the key blocks that are required for running the workflow commands of\n  OpenTofu/Terraform.\n\nTerragrunt further distinguishes shared modules between **service modules** and **modules**:\n\n- **Shared Service Module**: An OpenTofu/Terraform module that is designed to be standalone and applied directly. These modules\n  are not root modules in that they are still missing the key blocks like `backend` and `provider`, but aside from that\n  do not need any additional configuration or composition to deploy. For example, the\n  [terraform-aws-modules/vpc](https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest) module can be\n  deployed by itself without composing with other modules or resources.\n- **Shared Module**: An OpenTofu/Terraform module that is designed to be composed with other modules. That is, these modules must\n  be embedded in another OpenTofu/Terraform module and combined with other resources or modules. For example, the\n  [consul-security-group-rules\n  module](https://registry.terraform.io/modules/hashicorp/consul/aws/latest/submodules/consul-security-group-rules)\n\nTerragrunt started off with features that help directly deploy **Root Modules**, but over the years have implemented\nmany features that allow you to turn **Shared Service Modules** into **Root Modules** by injecting the key configuration\nblocks that are necessary for OpenTofu/Terraform modules to act as **Root Modules**.\n\nModules on the Terraform Registry are primarily designed to be used as **Shared Modules**. That is, you won't be able to\n`git clone` the underlying repository and run `tofu init`/`terraform init` or `apply` directly on the module without modification.\nUnless otherwise specified, almost all the modules will require composition with other modules/resources to deploy.\nWhen using modules in the registry, it helps to think about what blocks and resources are necessary to operate the\nmodule, and translating those into Terragrunt blocks that generate them.\n\nNote that often, Terragrunt may not be able to deploy modules from the registry. While Terragrunt has features\nto turn any **Shared Module** into a **Root Module**, there are two key technical limitations that prevent Terragrunt\nfrom converting ALL shared modules:\n\n- Every complex input must have a `type` associated with it. Otherwise, OpenTofu/Terraform will interpret the input that\n  Terragrunt passes through as `string`. This includes `list` and `map`.\n- Derived sensitive outputs must be marked as `sensitive`. Refer to the [terraform tutorial on sensitive\n  variables](https://learn.hashicorp.com/tutorials/terraform/sensitive-variables#reference-sensitive-variables) for more\n  information on this requirement.\n\n**If you run into issues deploying a module from the registry, chances are that module is not a Shared Service Module,\nand thus not designed for use with Terragrunt. Depending on the technical limitation, Terragrunt may be able to\nsupport the transition to root module. Please always file [an issue on the terragrunt\nrepository](https://github.com/gruntwork-io/terragrunt/issues) with the module + error message you are encountering,\ninstead of the module repository.**\n\n## remote_state\n\nThe `remote_state` block is used to configure how Terragrunt will set up the remote state configuration of your\nOpenTofu/Terraform code. You can read more about Terragrunt's remote state functionality in [Keep your remote state configuration\nDRY](/features/units/state-backend/) use case overview.\n\nThe `remote_state` block supports the following arguments:\n\n- `backend` (attribute): Specifies which remote state backend will be configured. This should be one of the\n  [available backends](https://opentofu.org/docs/language/settings/backends/configuration/#available-backends) that Opentofu/Terraform supports.\n\n- `disable_init` (attribute): When `true`, skip automatic creation and management of remote state resources by Terragrunt.\n  Some backends can be automatically created if the storage backend does not already exist. Currently, `s3` and `gcs` are the\n  two backends with support for automatic creation. Setting this to `true` prevents Terragrunt from creating or modifying these resources,\n  but OpenTofu/Terraform will still initialize the backend normally. Defaults to `false`.\n\n  **Note:** When using `generate` with `disable_init = true`, the backend configuration is written to the generated `.tf` file. OpenTofu/Terraform will still attempt to connect to the backend during init.\n\n  The `--backend-bootstrap` flag controls whether Terragrunt creates backend resources (e.g., S3 buckets) before running `init`. It defaults to `false`.\n\n  | `disable_init` | `--backend-bootstrap` | Backend exists | Result                                                        |\n  |----------------|-----------------------|----------------|---------------------------------------------------------------|\n  | `false`        | `true`                | No             | Terragrunt creates backend resources, init succeeds           |\n  | `false`        | `true`                | Yes            | Terragrunt verifies backend config, init succeeds             |\n  | `false`        | `false`               | No             | No creation, OpenTofu/Terraform init fails (bucket not found) |\n  | `false`        | `false`               | Yes            | No creation, init succeeds                                    |\n  | `true`         | any                   | Yes            | No creation, OpenTofu/Terraform inits normally                |\n  | `true`         | any                   | No             | No creation, OpenTofu/Terraform init fails (bucket not found) |\n\n- `disable_dependency_optimization` (attribute): When `true`, disable optimized dependency fetching for terragrunt\n  modules using this `remote_state` block. See the documentation for [dependency block](#dependency) for more details.\n\n- `generate` (attribute): Configure Terragrunt to automatically generate a `.tf` file that configures the remote state\n  backend. This is a map that expects two properties:\n\n  - `path`: The path where the generated file should be written. If a relative path, it'll be relative to the Terragrunt\n    working dir (where the OpenTofu/Terraform code lives).\n  - `if_exists` (attribute): What to do if a file already exists at `path`.\n\n    Valid values are:\n\n    - `overwrite` (overwrite the existing file)\n    - `overwrite_terragrunt` (overwrite the existing file if it was generated by terragrunt; otherwise, error)\n    - `skip` (skip code generation and leave the existing file as-is)\n    - `error` (exit with an error)\n\n- `config` (attribute): An arbitrary map that is used to fill in the backend configuration in OpenTofu/Terraform. All the\n  properties will automatically be included in the OpenTofu/Terraform backend block (with a few exceptions: see below).\n\n- `encryption` (attribute): A map that is used to configure state and plan encryption in OpenTofu. The properties will be transformed\n  into an `encryption` block in the OpenTofu terraform block. The properties are specific to the respective `key_provider` (see below).\n\n  For example, if you had the following `remote_state` block:\n\n  ```hcl\n  # terragrunt.hcl\n\n  remote_state {\n    backend = \"s3\"\n    config = {\n      bucket = \"mybucket\"\n      key    = \"path/to/my/key\"\n      region = \"us-east-1\"\n    }\n  }\n  ```\n\n  This is equivalent to the following OpenTofu/Terraform code:\n\n  ```hcl\n  # main.tf\n\n  terraform {\n    backend \"s3\" {\n      bucket = \"mybucket\"\n      key    = \"path/to/my/key\"\n      region = \"us-east-1\"\n    }\n  }\n  ```\n\nNote that `remote_state` can also be set as an attribute. This is useful if you want to set `remote_state` dynamically.\nFor example, if in `common.hcl` you had:\n\n```hcl\n# common.hcl\n\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket = \"mybucket\"\n    key    = \"path/to/my/key\"\n    region = \"us-east-1\"\n  }\n}\n```\n\nThen in a `terragrunt.hcl` file, you could dynamically set `remote_state` as an attribute as follows:\n\n```hcl\n# terragrunt.hcl\n\nlocals {\n  # Load the data from common.hcl\n  common = read_terragrunt_config(find_in_parent_folders(\"common.hcl\"))\n}\n\n# Set the remote_state config dynamically to the remote_state config in common.hcl\nremote_state = local.common.remote_state\n```\n\n### backend\n\nNote that Terragrunt does special processing of the `config` attribute for the `s3` and `gcs` remote state backends, and\nsupports additional keys that are used to configure the automatic initialization feature of Terragrunt.\n\nFor the `s3` backend, the following additional properties are supported in the `config` attribute:\n\n- `region` - (Optional) The region of the S3 bucket.\n- `profile` - (Optional) This is the AWS profile name as set in the shared credentials file.\n- `endpoint` - (Optional) A custom endpoint for the S3 API.\n- `endpoints`: (Optional) A configuration `map` for custom service API (starting with Terraform 1.6).\n  - `s3` - (Optional) A custom endpoint for the S3 API. Overrides `endpoint` argument.\n  - `dynamodb` - (Optional) A custom endpoint for the DynamoDB API. Overrides `dynamodb_endpoint` argument.\n- `encrypt` - (Optional) Whether to enable server-side encryption of the state file. If disabled, a log warning will be issued in the console output to notify the user. If `skip_bucket_ssencryption` is enabled, the log will be written as a debug log.\n- `role_arn` - (Optional) The role to be assumed.\n- `shared_credentials_file` - (Optional) This is the path to the shared credentials file. If this is not set and a profile is specified, `~/.aws/credentials` will be used.\n- `external_id` - (Optional) The external ID to use when assuming the role.\n- `session_name` - (Optional) The session name to use when assuming the role.\n- `dynamodb_table` - (Optional) The name of a DynamoDB table to use for state locking and consistency. The table must have a primary key named LockID. If not present, locking will be disabled.\n- `use_lockfile` - (Optional) When `true`, enables native S3 locking using S3 object conditional writes for state locking. This feature requires OpenTofu >= 1.10. Can be used simultaneously with `dynamodb_table` during migration (both locks must be acquired successfully), but typically used as a replacement for DynamoDB locking.\n- `skip_bucket_versioning`: When `true`, the S3 bucket that is created to store the state will not be versioned.\n- `skip_bucket_ssencryption`: When `true`, the S3 bucket that is created to store the state will not be configured with server-side encryption.\n- `skip_bucket_accesslogging`: *DEPRECATED* If provided, will be ignored. A log warning will be issued in the console output to notify the user.\n- `skip_bucket_root_access`: When `true`, the S3 bucket that is created will not be configured with bucket policies that allow access to the root AWS user.\n- `skip_bucket_enforced_tls`: When `true`, the S3 bucket that is created will not be configured with a bucket policy that enforces access to the bucket via a TLS connection.\n- `skip_bucket_public_access_blocking`: When `true`, the S3 bucket that is created will not have public access blocking enabled.\n- `disable_bucket_update`: When `true`, disable update S3 bucket if not equal configured in config block\n- `enable_lock_table_ssencryption`: When `true`, the synchronization lock table in DynamoDB used for remote state concurrent access will be configured with server-side encryption.\n- `s3_bucket_tags`: A map of key value pairs to associate as tags on the created S3 bucket.\n- `dynamodb_table_tags`: A map of key value pairs to associate as tags on the created DynamoDB remote state lock table.\n- `accesslogging_bucket_tags`: A map of key value pairs to associate as tags on the created S3 bucket to store de access logs.\n- `disable_aws_client_checksums`: When `true`, disable computing and checking checksums on the request and response,\n  such as the CRC32 check for DynamoDB. See [#1059](https://github.com/gruntwork-io/terragrunt/issues/1059) for issue where this is a useful workaround.\n- `accesslogging_bucket_name`: (Optional) When provided as a valid `string`, create an S3 bucket with this name to store the access logs for the S3 bucket used to store OpenTofu/Terraform state. If not provided, or string is empty or invalid S3 bucket name, then server access logging for the S3 bucket storing the Opentofu/Terraform state will be disabled. **Note:** When access logging is enabled supported encryption for state bucket is only `AES256`. Reference: [S3 server access logging](https://docs.aws.amazon.com/AmazonS3/latest/userguide/enable-server-access-logging.html)\n- `accesslogging_target_object_partition_date_source`: (Optional) When provided as a valid `string`, it configures the `PartitionDateSource` option. This option is part of the `TargetObjectKeyFormat` and `PartitionedPrefix` AWS configurations, allowing you to configure the log object key format for the access log files. Reference: [Logging requests with server access logging](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerLogs.html).\n- `accesslogging_target_prefix`: (Optional) When provided as a valid `string`, set the `TargetPrefix` for the access log objects in the S3 bucket used to store Opentofu/Terraform state. If set to **empty**`string`, then `TargetPrefix` will be set to **empty** `string`. If attribute is not provided at all, then `TargetPrefix` will be set to **default** value `TFStateLogs/`. This attribute won't take effect if the `accesslogging_bucket_name` attribute is not present.\n- `skip_accesslogging_bucket_acl`: When set to `true`, the S3 bucket where access logs are stored will not be configured with bucket ACL.\n- `skip_accesslogging_bucket_enforced_tls`: When set to `true`, the S3 bucket where access logs are stored will not be configured with a bucket policy that enforces access to the bucket via a TLS connection.\n- `skip_accesslogging_bucket_public_access_blocking`: When set to `true`, the S3 bucket where access logs are stored will not have public access blocking enabled.\n- `skip_accesslogging_bucket_ssencryption`: When set to `true`, the S3 bucket where access logs are stored will not be configured with server-side encryption.\n- `bucket_sse_algorithm`: (Optional) The algorithm to use for server-side encryption of the state bucket. Defaults to `aws:kms`.\n- `bucket_sse_kms_key_id`: (Optional) The KMS Key to use when the encryption algorithm is `aws:kms`. Defaults to the AWS Managed `aws/s3` key.\n- `assume_role`: (Optional) A configuration `map` to use when assuming a role (starting with Terraform 1.6 for Terraform). Override top level arguments\n  - `role_arn` - (Required) The role to be assumed.\n  - `duration` - (Optional) The duration the credentials will be valid.\n  - `external_id` - (Optional) The external ID to use when assuming the role.\n  - `policy` - (Optional) Policy JSON to further restrict the role.\n  - `policy_arns` - (Optional) A list of policy ARNs to further restrict the role.\n  - `session_name` - (Optional) The session name to use when assuming the role.\n  - `source_identity` - (Optional) The source identity to use when assuming the role.\n  - `tags` - (Optional) A map of key value pairs used as assume role session tags.\n  - `transitive_tag_keys` - (Optional) A list of tag keys that to be passed.\n- `assume_role_with_web_identity` - (Optional) A configuration `map` to use when assuming a role with a web identity token.\n  - `role_arn` - (Required) The role to be assumed.\n  - `duration` - (Optional) The duration the credentials will be valid.\n  - `policy` - (Optional) Policy JSON to further restrict the role.\n  - `policy_arns` - (Optional) A list of policy ARNs to further restrict the role.\n  - `session_name` - (Optional) The session name to use when assuming the role.\n  - `web_identity_token` - (Required) The web identity token to use when assuming the role.\n  - `web_identity_token_file` - (Optional) The path to the file containing the web identity token to use when assuming the role.\n\nFor the `gcs` backend, the following additional properties are supported in the `config` attribute:\n\n- `skip_bucket_creation`: When `true`, Terragrunt will skip the auto initialization routine for setting up the GCS\n  bucket for use with remote state.\n- `skip_bucket_versioning`: When `true`, the GCS bucket that is created to store the state will not be versioned.\n- `enable_bucket_policy_only`: When `true`, the GCS bucket that is created to store the state will be configured to use uniform bucket-level access.\n- `project`: The GCP project where the bucket will be created.\n- `location`: The GCP location where the bucket will be created.\n- `gcs_bucket_labels`: A map of key value pairs to associate as labels on the created GCS bucket.\n- `credentials`: Local path to Google Cloud Platform account credentials in JSON format.\n- `access_token`: A temporary [OAuth 2.0 access token] obtained from the Google Authorization server.\n  Example with S3:\n\n```hcl\n# root.hcl\n\n# Configure OpenTofu/Terraform state to be stored in S3, in the bucket \"my-tofu-state\" in us-east-1 under a key that is\n# relative to included terragrunt config. For example, if you had the following filesystem layout:\n#\n# .\n# ├── root.hcl\n# └── child\n#     ├── main.tf\n#     └── terragrunt.hcl\n#\n# And the following is defined in the root terragrunt.hcl config that is included in the child, the state file for the\n# child module will be stored at the key \"child/tofu.tfstate\".\n#\n# Note that since we are not using any of the skip args, this will automatically create the S3 bucket\n# \"my-tofu-state\" and DynamoDB table \"my-lock-table\" if it does not already exist.\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket         = \"my-tofu-state\"\n    key            = \"${path_relative_to_include()}/tofu.tfstate\"\n    region         = \"us-east-1\"\n    encrypt        = true\n    dynamodb_table = \"my-lock-table\"\n  }\n}\n```\n\n```hcl\n# child/terragrunt.hcl\ninclude \"root\" {\n  path   = find_in_parent_folders(\"root.hcl\")\n}\n```\n\n```hcl\n# child/main.tf\nterraform {\n  backend \"s3\" {}\n}\n```\n\nExample with GCS:\n\n```hcl\n# root.hcl\n\n# Configure OpenTofu/Terraform state to be stored in GCS, in the bucket \"my-tofu-state\" in the \"my-tofu\" GCP project in\n# the eu region under a key that is relative to included terragrunt config. This will also apply the labels\n# \"owner=terragrunt_test\" and \"name=tofu_state_storage\" to the bucket if it is created by Terragrunt.\n#\n# For example, if you had the following filesystem layout:\n#\n# .\n# ├── root.hcl\n# └── child\n#     ├── main.tf\n#     └── terragrunt.hcl\n#\n# And the following is defined in the root terragrunt.hcl config that is included in the child, the state file for the\n# child module will be stored at the key \"child/tofu.tfstate\".\n#\n# Note that since we are not using any of the skip args, this will automatically create the GCS bucket\n# \"my-tofu-state\" if it does not already exist.\nremote_state {\n  backend = \"gcs\"\n\n  config = {\n    project  = \"my-tofu\"\n    location = \"eu\"\n    bucket   = \"my-tofu-state\"\n    prefix   = \"${path_relative_to_include()}/tofu.tfstate\"\n\n    gcs_bucket_labels = {\n      owner = \"terragrunt_test\"\n      name  = \"tofu_state_storage\"\n    }\n  }\n}\n```\n\n```hcl\n# child/terragrunt.hcl\ninclude \"root\" {\n  path   = find_in_parent_folders(\"root.hcl\")\n}\n```\n\n```hcl\n# child/main.tf\nterraform {\n  backend \"gcs\" {}\n}\n```\n\nExample with S3 using native S3 locking (OpenTofu >= 1.10):\n\n```hcl\n# Configure OpenTofu/Terraform state to be stored in S3 with native S3 locking instead of DynamoDB.\n# This uses S3 object conditional writes for state locking, which requires OpenTofu >= 1.10.\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket       = \"my-tofu-state\"\n    key          = \"${path_relative_to_include()}/tofu.tfstate\"\n    region       = \"us-east-1\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n```\n\nExample with S3 using both DynamoDB and native S3 locking during migration (OpenTofu >= 1.10):\n\n```hcl\n# Configure OpenTofu/Terraform state with dual locking during migration from DynamoDB to S3 native locking.\n# Both locks must be successfully acquired before operations can proceed.\n# After the migration period, remove dynamodb_table to use only S3 native locking.\n# Note: This won't delete the DynamoDB table, it will just be unused.\n# You can delete it manually after the migration period.\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket         = \"my-tofu-state\"\n    key            = \"${path_relative_to_include()}/tofu.tfstate\"\n    region         = \"us-east-1\"\n    encrypt        = true\n    dynamodb_table = \"my-lock-table\"  # Remove this after migration period\n    use_lockfile   = true             # New native S3 locking\n  }\n}\n```\n\n### encryption\n\nThe encryption map needs a `key_provider` property, which can be set to any provider [supported by OpenTofu](https://opentofu.org/docs/language/state/encryption/#key-providers); including: `pbkdf2`, `aws_kms`, `gcp_kms`, or `openbao`.\n\nDocumentation for each provider type and its possible configuration can be found in the [OpenTofu docs](https://opentofu.org/docs/language/state/encryption/#key-providers).\n\nA `terragrunt.hcl` file configuring PBKDF2 encryption could look like this:\n\n```hcl\n# terragrunt.hcl\n\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket = \"mybucket\"\n    key    = \"path/to/my/key\"\n    region = \"us-east-1\"\n  }\n\n  encryption = {\n    key_provider = \"pbkdf2\"\n    passphrase   = get_env(\"PBKDF2_PASSPHRASE\")\n  }\n}\n```\n\nThis would result in the following OpenTofu code:\n\n```hcl\n# main.tf\n\nterraform {\n  backend \"s3\" {\n    bucket = \"mybucket\"\n    key    = \"path/to/my/key\"\n    region = \"us-east-1\"\n  }\n  encryption {\n    key_provider \"pbkdf2\" \"default\" {\n      passphrase = \"SUPERSECRETPASSPHRASE\"\n    }\n    method \"aes_gcm\" \"default\" {\n      keys = key_provider.pbkdf2.default\n    }\n    state {\n      method = method.aes_gcm.default\n    }\n    plan {\n      method = method.aes_gcm.default\n    }\n  }\n}\n```\n\n## include\n\nThe `include` block is used to specify inheritance of Terragrunt configuration files. The included config (also called\nthe `parent`) will be merged with the current configuration (also called the `child`) before processing. You can learn\nmore about the inheritance properties of Terragrunt in the [Filling in remote state settings with Terragrunt\nsection](/features/units/state-backend/#generating-remote-state-settings-with-terragrunt) of the\n\"Keep your remote state configuration DRY\" use case overview.\n\nYou can have more than one `include` block, but each one must have a unique label. It is recommended to always label\nyour `include` blocks. Bare includes (`include` block with no label - e.g., `include {}`) are currently supported for\nbackward compatibility, but is deprecated usage and support may be removed in the future.\n\n`include` blocks support the following arguments:\n\n- `name` (label): You can define multiple `include` blocks in a single terragrunt config. Each include block\n  must be labeled with a unique name to differentiate it from the other includes. e.g., if you had a block `include\n\"remote\" {}`, you can reference the relevant exposed data with the expression `include.remote`.\n- `path` (attribute): Specifies the path to a Terragrunt configuration file (the `parent` config) that should be merged\n  with this configuration (the `child` config).\n- `expose` (attribute, optional): Specifies whether or not the included config should be parsed and exposed as a\n  variable. When `true`, you can reference the data of the included config under the variable `include`. Defaults to\n  `false`. Note that the `include` variable is a map of `include` labels to the parsed configuration value.\n- `merge_strategy` (attribute, optional): Specifies how the included config should be merged. Valid values are:\n  `no_merge` (do not merge the included config), `shallow` (do a shallow merge - default), `deep` (do a deep merge of\n  the included config).\n\n**NOTE**: At this time, Terragrunt only supports a single level of `include` blocks. That is, Terragrunt will error out\nif an included config also has an `include` block defined. If you are interested in this feature, please follow\n[#1566](https://github.com/gruntwork-io/terragrunt/issues/1566) to be notified when nested `include` blocks are supported.\n\n**Special case for shallow merge**: When performing a shallow merge, all attributes and blocks are merged shallowly with\nreplacement, except for `dependencies` blocks (NOT `dependency` block). `dependencies` blocks are deep merged: that is,\nall the lists of paths from included configurations are concatenated together, rather than replaced in override fashion.\n\nExamples:\n\n### Single include\n\n```hcl\n# root.hcl\n\n# If you have the following filesystem layout, and the following contents for ./child/terragrunt.hcl, this will include\n# and merge the configurations in the root.hcl file.\n#\n# .\n# ├── root.hcl\n# └── child\n#     ├── main.tf\n#     └── terragrunt.hcl\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket         = \"my-tofu-state\"\n    key            = \"${path_relative_to_include()}/tofu.tfstate\"\n    region         = \"us-east-1\"\n    encrypt        = true\n    dynamodb_table = \"my-lock-table\"\n  }\n}\n```\n\n```hcl\n# child/terragrunt.hcl\ninclude \"root\" {\n  path   = find_in_parent_folders(\"root.hcl\")\n  expose = true\n}\n\ninputs = {\n  remote_state_config = include.root.remote_state\n}\n```\n\n```hcl\n# child/main.tf\nterraform {\n  backend \"s3\" {}\n}\n```\n\n### Multiple includes\n\n```hcl\n# root.hcl\n\n# If you have the following filesystem layout, and the following contents for ./child/terragrunt.hcl, this will include\n# and merge the configurations in the root.hcl, while only loading the data in the region.hcl\n# configuration.\n#\n# .\n# ├── root.hcl\n# ├── region.hcl\n# └── child\n#     └── terragrunt.hcl\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket         = \"my-tofu-state\"\n    key            = \"${path_relative_to_include()}/tofu.tfstate\"\n    region         = \"us-east-1\"\n    encrypt        = true\n    dynamodb_table = \"my-lock-table\"\n  }\n}\n```\n\n```hcl\n# region.hcl\nlocals {\n  region = \"production\"\n}\n```\n\n```hcl\n# child/terragrunt.hcl\ninclude \"remote_state\" {\n  path   = find_in_parent_folders(\"root.hcl\")\n  expose = true\n}\n\ninclude \"region\" {\n  path           = find_in_parent_folders(\"region.hcl\")\n  expose         = true\n  merge_strategy = \"no_merge\"\n}\n\ninputs = {\n  remote_state_config = include.remote_state.remote_state\n  region              = include.region.locals.region\n}\n```\n\n```hcl\n# child/main.tf\nterraform {\n  backend \"s3\" {}\n}\n```\n\n### Limitations on accessing exposed config\n\nIn general, you can access all attributes on `include` when they are exposed (e.g., `include.locals`, `include.inputs`,\netc.).\n\nHowever, to support `run --all`, Terragrunt is unable to expose all attributes when the included config has a `dependency`\nblock. To understand this, consider the following example:\n\n```hcl\n# root.hcl\ndependency \"vpc\" {\n  config_path = \"${get_terragrunt_dir()}/../vpc\"\n}\n\ninputs = {\n  vpc_name = dependency.vpc.outputs.name\n}\n```\n\n```hcl\n# terragrunt.hcl\ninclude \"root\" {\n  path   = find_in_parent_folders(\"root.hcl\")\n  expose = true\n}\n\ndependency \"alb\" {\n  config_path = (\n    include.root.inputs.vpc_name == \"mgmt\"\n    ? \"../alb-public\"\n    : \"../alb-private\"\n  )\n}\n\ninputs = {\n  alb_id = dependency.alb.outputs.id\n}\n```\n\nIn the child `terragrunt.hcl`, the `dependency` path for the `alb` depends on whether the VPC is the `mgmt` VPC or not,\nwhich is determined by the `dependency.vpc` in the root config. This means that the output from `dependency.vpc` must be\navailable to parse the `dependency.alb` config.\n\nThis causes problems when performing a `run --all apply` operation. During a `run --all` operation, Terragrunt first parses\nall the `dependency` blocks to build a dependency tree of the Terragrunt modules to figure out the order of operations.\nIf all the paths are static references, then Terragrunt can determine all the dependency paths before any module has\nbeen applied. In this case there is no problem even if other config blocks access `dependency`, as by the time\nTerragrunt needs to parse those blocks, the upstream dependencies would have been applied during the `run --all apply`.\n\nHowever, if those `dependency` blocks depend on upstream dependencies, then there is a problem as Terragrunt would not\nbe able to build the dependency tree without the upstream dependencies being applied.\n\nTherefore, to ensure that Terragrunt can build the dependency tree in a `run --all` operation, Terragrunt enforces the\nfollowing limitation to exposed `include` config:\n\nIf the included configuration has any `dependency` blocks, only `locals` and `include` are exposed and available to the\nchild `include` and `dependency` blocks. There are no restrictions for other blocks in the child config (e.g., you can\nreference `inputs` from the included config in child `inputs`).\n\nOtherwise, if the included config has no `dependency` blocks, there is no restriction on which exposed attributes you\ncan access.\n\nFor example, the following alternative configuration is valid even if the alb dependency is still accessing the `inputs`\nattribute from the included config:\n\n```hcl\n# root.hcl\ninputs = {\n  vpc_name = \"mgmt\"\n}\n```\n\n```hcl\n# terragrunt.hcl\ninclude \"root\" {\n  path   = find_in_parent_folders(\"root.hcl\")\n  expose = true\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n\ndependency \"alb\" {\n  config_path = (\n    include.root.inputs.vpc_name == \"mgmt\"\n    ? \"../alb-public\"\n    : \"../alb-private\"\n  )\n}\n\ninputs = {\n  vpc_name = dependency.vpc.outputs.name\n  alb_id   = dependency.alb.outputs.id\n}\n```\n\n**What is deep merge?**\n\nWhen the `merge_strategy` for the `include` block is set to `deep`, Terragrunt will perform a deep merge of the included\nconfig. For Terragrunt config, deep merge is defined as follows:\n\n- For simple types, the child overrides the parent.\n- For lists, the two attribute lists are combined together in concatenation.\n- For maps, the two maps are combined together recursively. That is, if the map keys overlap, then a deep merge is\n  performed on the map value.\n- For blocks, if the label is the same, the two blocks are combined together recursively. Otherwise, the blocks are\n  appended like a list. This is similar to maps, with block labels treated as keys.\n\nHowever, due to internal implementation details, some blocks are not deep mergeable. This will change in the future, but\nfor now, terragrunt performs a shallow merge (that is, block definitions in the child completely override the parent\ndefinition). The following blocks have this limitation: - `remote_state` - `generate`\n\nSimilarly, the `locals` block is deliberately omitted from the merge operation by design. That is, you will not be able\nto access parent config `locals` in the child config, and vice versa in a merge. However, you can access the parent\nlocals in child config if you use the `expose` feature.\n\nFinally, `dependency` blocks have special treatment. When doing a `deep` merge, `dependency` blocks from **both** child\nand parent config are accessible in **both** places. For example, consider the following setup:\n\n```hcl\n# root.hcl\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n\ninputs = {\n  vpc_id = dependency.vpc.outputs.vpc_id\n  db_id  = dependency.mysql.outputs.db_id\n}\n```\n\n```hcl\n# terragrunt.hcl\ninclude \"root\" {\n  path           = find_in_parent_folders(\"root.hcl\")\n  merge_strategy = \"deep\"\n}\n\ndependency \"mysql\" {\n  config_path = \"../mysql\"\n}\n\ninputs = {\n  security_group_id = dependency.vpc.outputs.security_group_id\n}\n```\n\nIn the example, note how the parent is accessing the outputs of the `mysql` dependency even though it is not defined in\nthe parent. Similarly, the child is accessing the outputs of the `vpc` dependency even though it is not defined in the\nchild.\n\nFull example:\n\n```hcl\n# root.hcl\nremote_state {\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"${path_relative_to_include()}/tofu.tfstate\"\n    region = \"us-west-2\"\n  }\n}\n\ndependency \"vpc\" {\n  # This will get overridden by child terragrunt.hcl configs\n  config_path = \"\"\n\n  mock_outputs = {\n    attribute     = \"hello\"\n    old_attribute = \"old val\"\n    list_attr     = [\"hello\"]\n    map_attr = {\n      foo = \"bar\"\n    }\n  }\n  mock_outputs_allowed_terraform_commands = [\"apply\", \"plan\", \"destroy\", \"output\"]\n}\n\ninputs = {\n  attribute     = \"hello\"\n  old_attribute = \"old val\"\n  list_attr     = [\"hello\"]\n  map_attr = {\n    foo = \"bar\"\n    test = dependency.vpc.outputs.new_attribute\n  }\n}\n```\n\n```hcl\n# terragrunt.hcl\ninclude \"root\" {\n  path           = find_in_parent_folders(\"root.hcl\")\n  merge_strategy = \"deep\"\n}\n\nremote_state {\n  backend = \"local\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n  mock_outputs = {\n    attribute     = \"mock\"\n    new_attribute = \"new val\"\n    list_attr     = [\"mock\"]\n    map_attr = {\n      bar = \"baz\"\n    }\n  }\n}\n\ninputs = {\n  attribute     = \"mock\"\n  new_attribute = \"new val\"\n  list_attr     = [\"mock\"]\n  map_attr = {\n    bar = \"baz\"\n  }\n\n  dep_out = dependency.vpc.outputs\n}\n```\n\n```hcl\n# Merged terragrunt.hcl\n\n# Child override parent completely due to deep merge limitation\nremote_state {\n  backend = \"local\"\n}\n\n# mock_outputs are merged together with deep merge\ndependency \"vpc\" {\n  config_path = \"../vpc\"       # Child overrides parent\n  mock_outputs = {\n    attribute     = \"mock\"     # Child overrides parent\n    old_attribute = \"old val\"  # From parent\n    new_attribute = \"new val\"  # From child\n    list_attr     = [\n      \"hello\",                 # From parent\n      \"mock\",                  # From child\n    ]\n    map_attr = {\n      foo = \"bar\"              # From parent\n      bar = \"baz\"              # From child\n    }\n  }\n\n  # From parent\n  mock_outputs_allowed_terraform_commands = [\"apply\", \"plan\", \"destroy\", \"output\"]\n}\n\n# inputs are merged together with deep merge\ninputs = {\n  attribute     = \"mock\"       # Child overrides parent\n  old_attribute = \"old val\"    # From parent\n  new_attribute = \"new val\"    # From child\n  list_attr     = [\n    \"hello\",                 # From parent\n    \"mock\",                  # From child\n  ]\n  map_attr = {\n    foo = \"bar\"                                   # From parent\n    bar = \"baz\"                                   # From child\n    test = dependency.vpc.outputs.new_attribute   # From parent, referencing dependency mock output from child\n  }\n\n  dep_out = dependency.vpc.outputs                # From child\n}\n```\n\n## locals\n\nThe `locals` block is used to define aliases for Terragrunt expressions that can be referenced elsewhere in configuration.\n\nThe `locals` block does not have a defined set of arguments that are supported. Instead, all the arguments passed into\n`locals` are available under the reference `local.<local name>` throughout the file where the `locals` block is defined.\n\nExample:\n\n```hcl\n# terragrunt.hcl\n\n# Make the AWS region a reusable variable within the configuration\nlocals {\n  aws_region = \"us-east-1\"\n}\n\ninputs = {\n  region = local.aws_region\n  name   = \"${local.aws_region}-bucket\"\n}\n```\n\n### Complex locals\n\nSome `local` variables can be complex types, such as `list` or `map`.\n\nFor example:\n\n```hcl\n# terragrunt.hcl\n\nlocals {\n  # Define a list of regions\n  regions = [\"us-east-1\", \"us-west-2\", \"eu-west-1\"]\n\n  # Define a map of regions to their corresponding bucket names\n  region_to_bucket_name = {\n    us-east-1 = \"east-bucket\"\n    us-west-2 = \"west-bucket\"\n    eu-west-1 = \"eu-bucket\"\n  }\n\n  # The first region is accessed like this\n  first_region = local.regions[0]\n\n  # The bucket name for us-east-1 is accessed like this\n  us_east_1_bucket = local.region_to_bucket_name[\"us-east-1\"]\n}\n```\n\nThese complex types can also arise when using values derived from reading other files.\n\nFor example:\n\n```hcl\n# region.hcl\n\nlocals {\n  region = \"us-east-1\"\n}\n```\n\n```hcl\n# unit/terragrunt.hcl\n\nlocals {\n  # Load the data from region.hcl\n  region_hcl = read_terragrunt_config(find_in_parent_folders(\"region.hcl\"))\n\n  # Access the region from the loaded file\n  region = local.region_hcl.locals.region\n}\n\ninputs = {\n  bucket_name = \"${local.region}-bucket\"\n}\n```\n\nSimilarly, you might want to define this shared data using other serialization formats, like JSON or YAML:\n\n```yaml\n# region.yml\n\nregion: us-east-1\n```\n\n```hcl\n# unit/terragrunt.hcl\n\nlocals {\n  # Load the data from region.json\n  region_yml = yamldecode(file(find_in_parent_folders(\"region.yml\")))\n\n  # Access the region from the loaded file\n  region = local.region_yml.region\n}\n\ninputs = {\n  bucket_name = \"${local.region}-bucket\"\n}\n```\n\n### Computed locals\n\nWhen reading Terragrunt HCL configurations, you might read in a computed configuration:\n\n```hcl\n# computed.hcl\n\nlocals {\n  computed_value = run_cmd(\"--terragrunt-quiet\", \"python3\", \"-c\", \"print('Hello,')\")\n}\n```\n\n```hcl\n# unit/terragrunt.hcl\n\nlocals {\n  # Load the data from computed.hcl\n  computed = read_terragrunt_config(find_in_parent_folders(\"computed.hcl\"))\n\n  # Access the computed value from the loaded file\n  computed_value = \"${local.computed.locals.computed_value} world!\" # <-- This will be \"Hello, world!\"\n}\n```\n\nNote that this can be a powerful feature, but it can easily lead to performance issues if you are not careful,\nas each read will require a full parse of the HCL file and potentially execute expensive computation.\n\nUse this feature judiciously.\n\n## dependency\n\nThe `dependency` block is used to configure module dependencies. Each dependency block exports the outputs of the target\nmodule as block attributes you can reference throughout the configuration. You can learn more about `dependency` blocks\nin the [Dependencies between modules\nsection](/features/stacks/stack-operations#dependencies-between-units) of the\n\"Execute Opentofu/Terraform commands on multiple modules at once\" use case overview.\n\nYou can define more than one `dependency` block. Each label you provide to the block identifies another `dependency`\nthat you can reference in your config.\n\nThe `dependency` block supports the following arguments:\n\n- `name` (label): You can define multiple `dependency` blocks in a single terragrunt config. As such, each block needs a\n  name to differentiate between the other blocks, which is what the first label of the block is used for. You can\n  reference the specific dependency output by the name. E.g if you had a block `dependency \"vpc\"`, you can reference the\n  outputs and inputs of this dependency with the expressions `dependency.vpc.outputs` and `dependency.vpc.inputs`.\n- `config_path` (attribute): Path to a Terragrunt module (folder with a `terragrunt.hcl` file) that should be included\n  as a dependency in this configuration.\n- `enabled` (attribute): When `false`, excludes the dependency from execution. Defaults to `true`.\n- `skip_outputs` (attribute): When `true`, skip calling `terragrunt output` when processing this dependency. If\n  `mock_outputs` is configured, set `outputs` to the value of `mock_outputs`. Otherwise, `outputs` will be set to an\n  empty map. Put another way, setting `skip_outputs` means \"use mocks all the time if `mock_outputs` are set.\"\n- `mock_outputs` (attribute): A map of arbitrary key value pairs to use as the `outputs` attribute when no outputs are\n  available from the target module, or if `skip_outputs` is `true`. However, it's generally recommended not to set\n  `skip_outputs` if using `mock_outputs`, because `skip_outputs` means \"use mocks all the time if they are set\" whereas\n  `mock_outputs` means \"use mocks only if real outputs are not available.\" Use `locals` instead when `skip_outputs = true`.\n- `mock_outputs_allowed_terraform_commands` (attribute): A list of Terraform commands for which `mock_outputs` are\n  allowed. If a command is used where `mock_outputs` is not allowed, and no outputs are available in the target module,\n  Terragrunt will throw an error when processing this dependency.\n- `mock_outputs_merge_with_state` (attribute): DEPRECATED. Use `mock_outputs_merge_strategy_with_state`. When `true`,\n  `mock_outputs` and the state outputs will be merged. That is, the `mock_outputs` will be treated as defaults and the\n  real state outputs will overwrite them if the keys clash.\n- `mock_outputs_merge_strategy_with_state` (attribute): Specifies how any existing state should be merged into the\n  mocks. Valid values are\n  - `no_merge` (default) - any existing state will be used as is. If the dependency does not have an existing state (it\n    hasn't been applied yet), then the mocks will be used\n  - `shallow` - the existing state will be shallow merged into the mocks. Mocks will only be used where the output does\n    not already exist in the dependency's state\n  - `deep_map_only` - the existing state will be deeply merged into the mocks. If an output is a map, the mock key\n    will be used where that key does not exist in the state. Lists will not be merged\n\nExample:\n\n```hcl\n# terragrunt.hcl\n\n# Run `terragrunt output` on the module at the relative path `../vpc` and expose them under the attribute\n# `dependency.vpc.outputs`\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n\n  # Configure mock outputs for the `validate` command that are returned when there are no outputs available (e.g the\n  # module hasn't been applied yet.\n  mock_outputs_allowed_terraform_commands = [\"validate\"]\n  mock_outputs = {\n    vpc_id = \"fake-vpc-id\"\n  }\n}\n\n# Another dependency, available under the attribute `dependency.rds.outputs`\ndependency \"rds\" {\n  config_path = \"../rds\"\n}\n\ninputs = {\n  vpc_id = dependency.vpc.outputs.vpc_id\n  db_url = dependency.rds.outputs.db_url\n}\n```\n**IMPORTANT**: The `dependency.<name>.inputs` field has been deprecated and removed. You can only access dependency outputs via `dependency.<name>.outputs`. If you were previously using `dependency.<name>.inputs`, you should refactor your configuration to use `dependency.<name>.outputs` instead.\n\n**Can I speed up dependency fetching?**\n\n`dependency` blocks are fetched in parallel at each source level, but will serially parse each recursive dependency. For\nexample, consider the following chain of dependencies:\n\n```text\naccount --> vpc --> securitygroup --> ecs\n                                      ^\n                                     /\n                              ecr --\n```\n\nIn this chain, the `ecr` and `securitygroup` module outputs will be fetched concurrently when applying the `ecs` module,\nbut the outputs for `account` and `vpc` will be fetched serially as terragrunt needs to recursively walk through the\ntree to retrieve the outputs at each level.\n\nThis recursive parsing happens due to the necessity to parse the entire `terragrunt.hcl` configuration (including\n`dependency` blocks) in full before being able to call `tofu output`/`terraform output`.\n\nHowever, terragrunt includes an optimization to only fetch the lowest level outputs (`securitygroup` and `ecr` in this\nexample) provided that the following conditions are met in the immediate dependencies:\n\n- The remote state is managed using `remote_state` blocks.\n- The dependency optimization feature flag is enabled (`disable_dependency_optimization = false`, which is the default).\n- The `remote_state` block itself does not depend on any `dependency` outputs (`locals` and `include` are ok).\n- You are not relying on `before_hook`, `after_hook`, or `extra_arguments` to the `tofu init`/`terraform init` call. NOTE:\n  terragrunt will not automatically detect this and you will need to explicitly opt out of the dependency optimization\n  flag.\n\nIf these conditions are met, terragrunt will only parse out the `remote_state` blocks and use that to pull down the\nstate for the target module without parsing the `dependency` blocks, avoiding the recursive dependency retrieval.\n\n## dependencies\n\nThe `dependencies` block is used to enumerate all the Terragrunt modules that need to be applied in order for this\nmodule to be able to apply. Note that this is purely for ordering the operations when using `run --all` commands of\nOpenTofu/Terraform. This does not expose or pull in the outputs like `dependency` blocks.\n\nThe `dependencies` block supports the following arguments:\n\n- `paths` (attribute): A list of paths to modules that should be marked as a dependency.\n\nExample:\n\n```hcl\n# terragrunt.hcl\n\n# When applying this terragrunt config in an `run --all` command, make sure the modules at \"../vpc\" and \"../rds\" are\n# handled first.\ndependencies {\n  paths = [\"../vpc\", \"../rds\"]\n}\n```\n\n## generate\n\nThe `generate` block can be used to arbitrarily generate a file in the terragrunt working directory (where `tofu`/`terraform`\nis called). This can be used to generate common OpenTofu/Terraform configurations that are shared across multiple OpenTofu/Terraform\nmodules. For example, you can use `generate` to generate the provider blocks in a consistent fashion by defining a\n`generate` block in the parent terragrunt config.\n\nThe `generate` block supports the following arguments:\n\n- `name` (label): You can define multiple `generate` blocks in a single terragrunt config. As such, each block needs a\n  name to differentiate between the other blocks.\n- `path` (attribute): The path where the generated file should be written. If a relative path, it'll be relative to the\n  Terragrunt working dir (where the OpenTofu/Terraform code lives).\n- `if_exists` (attribute): What to do if a file already exists at `path`.\n\n  Valid values are:\n  - `overwrite` (overwrite the existing file)\n  - `overwrite_terragrunt` (overwrite the existing file if it was generated by terragrunt; otherwise, error)\n  - `skip` (skip code generation and leave the existing file as-is)\n  - `error` (exit with an error)\n- `if_disabled` (attribute): What to do if a file already exists at `path` and `disable` is set to `true` (`skip` by default)\n\n  Valid values are:\n  - `remove` (remove the existing file)\n  - `remove_terragrunt` (remove the existing file if it was generated by terragrunt; otherwise, error)\n  - `skip` (skip removing and leave the existing file as-is).\n- `comment_prefix` (attribute): A prefix that can be used to indicate comments in the generated file. This is used by\n  terragrunt to write out a signature for knowing which files were generated by terragrunt. Defaults to `#`. Optional.\n- `disable_signature` (attribute): When `true`, disables including a signature in the generated file. This means that\n  there will be no difference between `overwrite_terragrunt` and `overwrite` for the `if_exists` setting. Defaults to\n  `false`. Optional.\n- `contents` (attribute): The contents of the generated file.\n- `disable` (attribute): Disables this generate block.\n\nExample:\n\n```hcl\n# terragrunt.hcl\n\n# When using this terragrunt config, terragrunt will generate the file \"provider.tf\" with the aws provider block before\n# calling to OpenTofu/Terraform. Note that this will overwrite the `provider.tf` file if it already exists.\ngenerate \"provider\" {\n  path      = \"provider.tf\"\n  if_exists = \"overwrite\"\n  contents = <<EOF\nprovider \"aws\" {\n  region              = \"us-east-1\"\n  version             = \"= 2.3.1\"\n  allowed_account_ids = [\"1234567890\"]\n}\nEOF\n}\n```\n\nNote that `generate` can also be set as an attribute. This is useful if you want to set `generate` dynamically.\nFor example, if in `common.hcl` you had:\n\n```hcl\n# common.hcl\n\ngenerate \"provider\" {\n  path      = \"provider.tf\"\n  if_exists = \"overwrite\"\n  contents = <<EOF\nprovider \"aws\" {\n  region              = \"us-east-1\"\n  version             = \"= 2.3.1\"\n  allowed_account_ids = [\"1234567890\"]\n}\nEOF\n}\n```\n\nThen in a `terragrunt.hcl` file, you could dynamically set `generate` as an attribute as follows:\n\n```hcl\n# terragrunt.hcl\n\nlocals {\n  # Load the data from common.hcl\n  common = read_terragrunt_config(find_in_parent_folders(\"common.hcl\"))\n}\n\n# Set the generate config dynamically to the generate config in common.hcl\ngenerate = local.common.generate\n```\n\n## engine\n\nThe `engine` block is used to configure experimental Terragrunt engine configuration.\nMore details in [engine section](https://docs.terragrunt.com/features/units/engine/).\n\n## feature\n\nThe `feature` block is used to configure feature flags in HCL for a specific Terragrunt Unit.\n\nEach feature flag must include a default value.\n\nFeature flags can be overridden via the [`--feature`](/reference/cli/commands/run#feature) CLI option.\n\n```hcl\n# terragrunt.hcl\n\nfeature \"string_flag\" {\n  default = \"test\"\n}\n\nfeature \"run_hook\" {\n  default = false\n}\n\nterraform {\n  before_hook \"feature_flag\" {\n    commands = [\"apply\", \"plan\", \"destroy\"]\n    execute  = feature.run_hook.value ? [\"sh\", \"-c\", \"feature_flag_script.sh\"] : [ \"sh\", \"-c\", \"exit\", \"0\" ]\n  }\n}\n\ninputs = {\n  string_feature_flag = feature.string_flag.value\n}\n```\n\nSetting feature flags through CLI:\n\n```bash\nterragrunt --feature run_hook=true apply\n\nterragrunt --feature run_hook=true --feature string_flag=dev apply\n```\n\nSetting feature flags through env variables:\n\n```bash\nexport TG_FEATURE=run_hook=true\nterragrunt apply\n\nexport TG_FEATURE=run_hook=true,string_flag=dev\nterragrunt apply\n```\n\nNote that the `default` value of the `feature` block is evaluated as an expression dynamically.\n\nWhat this means is that the value of the flag can be set via a Terragrunt expression at runtime. This is useful for scenarios where you want to integrate\nwith external feature flag services like [LaunchDarkly](https://launchdarkly.com/), [AppConfig](https://docs.aws.amazon.com/appconfig/latest/userguide/what-is-appconfig.html), etc.\n\n```hcl\n# terragrunt.hcl\n\nfeature \"feature_name\" {\n  default = run_cmd(\"--terragrunt-quiet\", \"<command-to-fetch-feature-flag-value>\")\n}\n```\n\nFeature flags are used to conditionally control Terragrunt behavior at runtime, including the inclusion or exclusion of units. More on that in the [exclude](#exclude) block.\n\n## exclude\n\nThe `exclude` block in Terragrunt provides advanced configuration options to dynamically determine when and how specific\nunits in the Terragrunt dependency graph are excluded. This feature allows for fine-grained control over which actions\nare executed and can conditionally exclude dependencies.\n\nSyntax:\n\n```hcl\n# terragrunt.hcl\n\nexclude {\n    if                   = <boolean>         # Boolean to determine exclusion.\n    no_run               = <boolean>         # Boolean to prevent the unit from running (ignored for `--all` commands).\n    actions              = [\"<action>\", ...] # List of actions to exclude (e.g., \"plan\", \"apply\", \"all\", \"all_except_output\").\n    exclude_dependencies = <boolean>         # Boolean to determine if dependencies should also be excluded.\n}\n```\n\nAttributes:\n\n| Attribute              | Type         | Description                                                                                                             |\n|------------------------|--------------|-------------------------------------------------------------------------------------------------------------------------|\n| `if`                   | boolean      | Condition to dynamically determine whether the unit should be excluded.                                                 |\n| `actions`              | list(string) | Specifies which actions to exclude when the condition is met. Options: `plan`, `apply`, `all`, `all_except_output` etc. |\n| `exclude_dependencies` | boolean      | Indicates whether the dependencies of the excluded unit should also be excluded (default: `false`).                     |\n| `no_run`               | boolean      | When `true` and `if` is `true`, prevents the unit from running entirely for single unit commands (e.g., `terragrunt run plan`), but only when the current action matches the `actions` list. This attribute is ignored for `run --all` commands. |\n\nExamples:\n\n```hcl\n# terragrunt.hcl\n\nexclude {\n    if = feature.feature_name.value # Dynamically exclude based on a feature flag.\n    actions = [\"plan\", \"apply\"]     # Exclude `plan` and `apply` actions.\n    exclude_dependencies = false    # Do not exclude dependencies.\n}\n```\n\nIn this example, the unit is excluded for the `plan` and `apply` actions only when `feature.feature_name.value`\nevaluates to `true`. Dependencies are not excluded.\n\n```hcl\n# terragrunt.hcl\n\nexclude {\n    if = feature.is_dev_environment.value # Exclude only for development environments.\n    actions = [\"all\"]                     # Exclude all actions.\n    exclude_dependencies = true           # Exclude dependencies along with the unit.\n}\n```\n\nThis configuration ensures the unit and its dependencies are excluded from all actions in the Terragrunt graph when the\nfeature `is_dev_environment` evaluates to `true`.\n\n```hcl\n# terragrunt.hcl\n\nexclude {\n    if = true                       # Explicitly exclude.\n    actions = [\"all_except_output\"] # Allow `output` actions nonetheless.\n    exclude_dependencies = false    # Dependencies remain active.\n}\n```\n\nThis setup is useful for scenarios where output evaluation is still needed, even if other actions like `plan` or `apply` are excluded.\n\n```hcl\n# terragrunt.hcl\n\nexclude {\n    if      = true\n    no_run  = true\n    actions = [\"plan\"]\n}\n```\n\nThis configuration prevents the unit from running when `if` is `true` AND the current action is \"plan\". The `no_run` attribute only applies to single unit commands (e.g., `terragrunt run plan`) and is ignored for `run --all` commands. The exclusion only takes effect when the current action matches the `actions` list.\n\nConsider using this for units that are expensive to continuously update, and can be opted in when necessary.\n\n## errors\n\nThe `errors` block contains all the configurations for handling errors.\n\nIt supports different nested configuration blocks like `retry` and `ignore` to define specific error-handling strategies.\n\n### Retry Configuration\n\nThe `retry` block within the `errors` block defines rules for retrying operations when specific errors occur.\nThis is useful for handling intermittent errors that may resolve after a short delay or multiple attempts.\n\nExample: Retry Configuration\n\n```hcl\n# terragrunt.hcl\n\nerrors {\n    retry \"retry_example\" {\n        retryable_errors = [\".*Error: transient.*\"] # Matches errors containing 'Error: transient'\n        max_attempts = 5                           # Retry up to 5 times\n        sleep_interval_sec = 10                    # Wait 10 seconds between retries\n    }\n}\n```\n\nParameters:\n\n- `retryable_errors`: A list of regex patterns to match errors that are eligible to be retried.\n\n  e.g. `\".*Error: transient.*\"` matches errors containing `Error: transient`.\n\n- `max_attempts`: The maximum number of retry attempts.\n\n  e.g. `5` retries.\n\n- `sleep_interval_sec`: Time (in seconds) to wait between retries.\n\n  e.g. `10` seconds.\n\n### Ignore Configuration\n\nThe `ignore` block within the `errors` block defines rules for ignoring specific errors. This is useful when certain\nerrors are known to be safe and should not prevent the run from proceeding.\n\nExample: Ignore Configuration\n\n```hcl\n# terragrunt.hcl\n\nerrors {\n    ignore \"ignore_example\" {\n        ignorable_errors = [\n            \".*Error: safe-to-ignore.*\", # Ignore errors containing 'Error: safe-to-ignore'\n            \"!.*Error: critical.*\"      # Do not ignore errors containing 'Error: critical'\n        ]\n        message = \"Ignoring safe-to-ignore errors\" # Optional message displayed when ignoring errors\n        signals = {\n            safe_to_revert = true # Indicates the operation is safe to revert on failure\n        }\n    }\n}\n```\n\nParameters:\n\n- `ignorable_errors`: A list of regex patterns to define errors to ignore.\n  - `\"Error: safe-to-ignore.*\"`: Ignores errors containing `Error: safe-to-ignore`.\n  - `\"!Error: critical.*\"`: Ensures errors containing `Error: critical` are not ignored.\n- `message` (Optional): A warning message displayed when an error is ignored.\n  - Example: `\"Ignoring safe-to-ignore errors\"`.\n- `signals` (Optional): Key-value pairs used to emit signals to external systems.\n  - Example: `safe_to_revert = true` indicates it is safe to revert the operation if it fails.\n\nPopulating values into the `signals` attribute results in a JSON file named `error-signals.json` being emitted on failure.\nThis file can be inspected in CI/CD systems to determine the recommended course of action to address the failure.\n\nExample:\n\nIf an error occurs and the author of the unit has signaled `safe_to_revert = true`, the CI/CD system could follow a standard process:\n\n- Identify all units with files named `error-signals.json`.\n- Checkout the previous commit for those units.\n- Apply the units in their previous state, effectively reverting their updates.\n\nThis approach ensures consistent and automated error handling in complex pipelines.\n\n### Combined Example\n\nBelow is a combined example showcasing both retry and ignore configurations within the `errors` block.\n\n```hcl\n# terragrunt.hcl\n\nerrors {\n    # Retry block for transient errors\n    retry \"transient_errors\" {\n        retryable_errors = [\".*Error: transient network issue.*\"]\n        max_attempts = 3\n        sleep_interval_sec = 5\n    }\n\n    # Ignore block for known safe-to-ignore errors\n    ignore \"known_safe_errors\" {\n        ignorable_errors = [\n            \".*Error: safe warning.*\",\n            \"!.*Error: do not ignore.*\"\n        ]\n        message = \"Ignoring safe warning errors\"\n        signals = {\n            alert_team = false\n        }\n    }\n}\n```\n\nTake note that:\n\n- All retry and ignore configurations must be defined within a single `errors` block.\n- Conditional logic can be used within `ignorable_errors` to enable or disable rules dynamically.\n\nEvaluation Order:\n\n- **Ignore Rules:** Errors are checked against the **ignore** rules first. If an error matches, it is ignored and will not trigger a retry.\n\n- **Retry Rules:** Once ignore rules are applied, the **retry** rules handle any remaining errors.\n\n> **Note:**\n> Only the **first matching rule** is applied. If there are multiple conflicting rules, any matches after the first one are ignored.\n\n#### Errors during source fetching\n\nIn addition to handling errors during OpenTofu/Terraform runs, the `errors` block will also handle errors that occur during source fetching.\n\nThis can be particularly useful when fetching from artifact repositories that may be temporarily unavailable.\n\nExample:\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  source = \"https://unreliable-source.com/module.zip\"\n}\n\nerrors {\n    retry \"source_fetch\" {\n        retryable_errors = [\".*Error: transient network issue.*\"]\n        max_attempts = 3\n        sleep_interval_sec = 5\n    }\n}\n```\n\n## unit\n\nThe `unit` block is used to define a deployment unit within a Terragrunt stack file (`terragrunt.stack.hcl`). Each unit represents a distinct infrastructure component that should be deployed as part of the stack.\n\n**Purpose**: Define a single, deployable piece of infrastructure.\n**Use case**: When you want to create a single piece of isolated infrastructure (e.g. a specific VPC, database, or application).\n**Result**: Generates a single `terragrunt.hcl` file in the specified path.\n\nThe `unit` block supports the following arguments:\n\n- `name` (label): A unique identifier for the unit. This is used to reference the unit elsewhere in your configuration.\n- `source` (attribute): Specifies where to find the Terragrunt configuration files for this unit. This follows the same syntax as the `source` parameter in the `terraform` block.\n- `path` (attribute): The relative path where this unit should be deployed within the stack directory (`.terragrunt-stack`). Also take note of the `no_dot_terragrunt_stack` attribute below, which can impact this.\n- `values` (attribute, optional): A map of values that will be passed to the unit as inputs.\n- `no_dot_terragrunt_stack` (attribute, optional): A boolean flag (`true` or `false`). When set to `true`, the unit **will not** be placed inside the `.terragrunt-stack` directory but will instead be generated in the same directory where `terragrunt.stack.hcl` is located. This allows for a **soft adoption** of stacks, making it easier for users to start using `terragrunt.stack.hcl` without modifying existing directory structures, or performing state migrations.\n- `no_validation` (attribute, optional): A boolean flag (`true` or `false`) that controls whether Terragrunt should validate the unit's configuration. When set to `true`, Terragrunt will skip validation checks for this unit.\n\nExample:\n\n```hcl\n# terragrunt.stack.hcl\n\nunit \"vpc\" {\n  source = \"git::git@github.com:acme/infrastructure-units.git//networking/vpc?ref=v0.0.1\"\n  path   = \"vpc\"\n  values = {\n    vpc_name = \"main\"\n    cidr     = \"10.0.0.0/16\"\n  }\n}\n```\n\nNote that each unit must have a unique name and path within the stack.\n\nWhen `values` are specified, generated units will have access to those values via a special `terragrunt.values.hcl` file generated next to the `terragrunt.hcl` file of the unit.\n\n<FileTree>\n\n- terragrunt.stack.hcl\n- .terragrunt-stack\n  - vpc\n    - **terragrunt.values.hcl**\n    - terragrunt.hcl\n\n</FileTree>\n\nThe `terragrunt.values.hcl` file will contain the values specified in the `values` block as top-level attributes:\n\n```hcl\n# .terragrunt-stack/vpc/terragrunt.values.hcl\n\nvpc_name = \"main\"\ncidr     = \"10.0.0.0/16\"\n```\n\nThe unit will be able to leverage those values via `values` variables.\n\n```hcl\n# .terragrunt-stack/vpc/terragrunt.hcl\n\ninputs = {\n  vpc_name = values.vpc_name\n  cidr     = values.cidr\n}\n```\n\nExample usage of `no_dot_terragrunt_stack` attribute:\n\n```hcl\n# terragrunt.stack.hcl\n\nunit \"vpc\" {\n  source = \"git::git@github.com:acme/infrastructure-units.git//networking/vpc?ref=v0.0.1\"\n  path   = \"vpc\"\n  values = {\n    vpc_name = \"main\"\n    cidr     = \"10.0.0.0/16\"\n  }\n}\n\nunit \"rds\" {\n  source = \"git::git@github.com:acme/infrastructure-units.git//database/rds?ref=v0.0.1\"\n  path   = \"rds\"\n  values = {\n    engine   = \"postgres\"\n    version  = \"13\"\n  }\n  no_dot_terragrunt_stack = true\n}\n```\n\nWith the above configuration, the resulting directory structure will be:\n\n<FileTree>\n\n- terragrunt.stack.hcl\n- .terragrunt-stack\n  - vpc\n    - terragrunt.values.hcl\n    - terragrunt.hcl\n- rds\n  - terragrunt.values.hcl\n  - terragrunt.hcl\n\n</FileTree>\n\nThe `vpc` unit is placed inside `.terragrunt-stack`, as expected.\nThe `rds` unit is generated in the **same directory as `terragrunt.stack.hcl`**, rather than inside `.terragrunt-stack`, due to `no_dot_terragrunt_stack = true`.\n\n**Notes:**\n\n- The `source` value can be updated dynamically using the `--source-map` flag, just like `terraform.source`.\n\n- A pre-created `terragrunt.values.hcl` file can be provided in the unit source (sibling to the `terragrunt.hcl` file used as the source of the unit). If present, this file will be used as the default values for the unit. However, if the values attribute is defined in the unit block, the generated `terragrunt.values.hcl` will replace the pre-existing file.\n\n### Comparison: unit vs stack blocks\n\n| Aspect | `unit` block | `stack` block |\n|--------|-------------|---------------|\n| **Purpose** | Define a single infrastructure component | Define a reusable collection of components |\n| **When to use** | For specific, one-off infrastructure pieces | For patterns of infrastructure pieces that you want provisioned together |\n| **Generated output** | A directory with a single `terragrunt.hcl` file | A directory with a `terragrunt.stack.hcl` file |\n\n## stack\n\nThe `stack` block is used to define a stack of deployment units in a Terragrunt configuration file (`terragrunt.stack.hcl`).\nStacks allow for nesting, enabling the organization of infrastructure components into modular, reusable groups, reducing redundancy and improving maintainability.\n\n**Purpose**: Define a collection of related units that can be reused.\n**Use case**: When you have a common, multi-unit pattern (like \"dev environment\" or \"three-tier web application\") that you want to deploy multiple times.\n**Result**: Generates another `terragrunt.stack.hcl` file that can contain more units or stacks.\n\nStacks are designed to be nestable, helping to mitigate the risk of stacks becoming too large or too repetitive.\nWhen a stack is generated, it can include nested stacks, ensuring that the configuration scales efficiently.\n\nThe `stack` block supports the following arguments:\n\n- `name` (label): A unique identifier for the stack. This is used to reference the stack elsewhere in your configuration.\n- `source` (attribute): Specifies where to find the Terragrunt configuration files for this stack. This follows the same syntax as the `source` parameter in the `terraform` block.\n- `path` (attribute): The relative path within `.terragrunt-stack` where this stack should be generated.If an absolute path is provided here, Terragrunt will generate the stack in that location, instead of generating it in a path relative to the `.terragrunt-stack` directory. Also take note of the `no_dot_terragrunt_stack` attribute below, which can impact this.\n- `values` (attribute, optional): A map of custom values that can be passed to the stack. These values can be referenced within the stack's configuration files, allowing for customization without modifying the stack source.\n- `no_dot_terragrunt_stack` (attribute, optional): A boolean flag (`true` or `false`). When set to `true`, the stack **will not** be placed inside the `.terragrunt-stack` directory but will instead be generated in the same directory where `terragrunt.stack.hcl` is located. This allows for a **soft adoption** of stacks, making it easier for users to start using `terragrunt.stack.hcl` without modifying existing directory structures, or performing state migrations.\n- `no_validation` (attribute, optional): A boolean flag (`true` or `false`) that controls whether Terragrunt should validate the stack's configuration. When set to `true`, Terragrunt will skip validation checks for this stack.\n\nExample:\n\n```hcl\n# terragrunt.stack.hcl\nstack \"services\" {\n    source = \"github.com/gruntwork-io/terragrunt-stacks//stacks/mock/services?ref=v0.0.1\"\n    path   = \"services\"\n    values = {\n        project = \"dev-services\"\n        cidr    = \"10.0.0.0/16\"\n    }\n}\n```\n\n```hcl\n# github.com/gruntwork-io/terragrunt-stacks//stacks/mock/services/terragrunt.stack.hcl\n# ...\nunit \"vpc\" {\n  # ...\n  values = {\n    cidr = values.cidr\n  }\n}\n```\n\nIn this example, the `services` stack is defined with path `services`, which will be generated at `.terragrunt-stack/services`.\nThe stack is also provided with custom values for `project` and `cidr`, which can be used within the stack's configuration files.\nTerragrunt will recursively generate a stack using the contents of the `.terragrunt-stack/services/terragrunt.stack.hcl` file until the entire stack is fully generated.\n\n**Notes:**\n\n- The `source` value can be updated dynamically using the `--source-map` flag, just like `terraform.source`.\n\n- A pre-created `terragrunt.values.hcl` file can be provided in the stack source (sibling to the `terragrunt.stack.hcl` file used as the source of the stack). If present, this file will be used as the default values for the stack. However, if the values attribute is defined in the stack block, the generated `terragrunt.values.hcl` will replace the pre-existing file.\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/01-hcl/03-attributes.mdx",
    "content": "---\ntitle: Attributes\ndescription: Learn about terragrunt hcl attributes\nslug: reference/hcl/attributes\nsidebar:\n  order: 3\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\n\nTerragrunt HCL configuration uses [attributes](https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#attribute-definitions) when there are values that need to be defined for Terragrunt as a whole.\n\nThink of attributes as the values used for Terragrunt configuration, such as the inputs to pass an orchestrated OpenTofu/Terraform binary, or the download directory to use.\n\n## inputs\n\nThe `inputs` attribute is a map that is used to specify the input variables and their values to pass in to OpenTofu/Terraform.\nEach entry of the map is passed to OpenTofu/Terraform using [the environment variable\nmechanism](https://opentofu.org/docs/language/values/variables/#environment-variables). This means that each input\nwill be set using the form `TF_VAR_variablename`, with the value in `json` encoded format.\n\nNote that because the values are being passed in with environment variables and `json`, the type information is lost\nwhen crossing the boundary between Terragrunt and OpenTofu/Terraform. You must specify the proper [type\nconstraint](https://opentofu.org/docs/language/values/variables/#type-constraints) on the variable in OpenTofu/Terraform in\norder for OpenTofu/Terraform to process the inputs as the right type.\n\nExample:\n\n```hcl\n# terragrunt.hcl\n\ninputs = {\n  string      = \"string\"\n  number      = 42\n  bool        = true\n  list_string = [\"a\", \"b\", \"c\"]\n  list_number = [1, 2, 3]\n  list_bool   = [true, false]\n\n  map_string = {\n    foo = \"bar\"\n  }\n\n  map_number = {\n    foo = 42\n    bar = 12345\n  }\n\n  map_bool = {\n    foo = true\n    bar = false\n    baz = true\n  }\n\n  object = {\n    str  = \"string\"\n    num  = 42\n    list = [1, 2, 3]\n\n    map = {\n      foo = \"bar\"\n    }\n  }\n\n  from_env = get_env(\"FROM_ENV\", \"default\")\n}\n```\n\nUsing this attribute is roughly equivalent to setting the corresponding `TF_VAR_` attribute.\n\nFor example, setting this in your `terragrunt.hcl`:\n\n```hcl\n# terragrunt.hcl\ninputs = {\n  instance_type  = \"t2.micro\"\n  instance_count = 10\n\n  tags = {\n    Name = \"example-app\"\n  }\n}\n```\n\nAnd running:\n\n```bash\nterragrunt apply\n```\n\nThis is roughly equivalent to running:\n\n```bash\nTF_VAR_instance_type=\"t2.micro\" \\\nTF_VAR_instance_count=10 \\\nTF_VAR_tags='{\"Name\":\"example-app\"}' \\\ntofu apply # or terraform apply\n```\n\n### Variable Precedence\n\nVariables loaded in OpenTofu/Terraform will consequently use the following precedence order (with the highest precedence being lowest on the list):\n\n1. `inputs` set in `terragrunt.hcl` files.\n2. Explicitly set `TF_VAR_` environment variables (these will override the `inputs` set in `terragrunt.hcl` if they conflict).\n3. `terraform.tfvars` files if present.\n4. `terraform.tfvars.json` files if present.\n5. Any `*.auto.tfvars` or `*.auto.tfvars.json` files, processed in lexical order of their filenames.\n6. Any `-var` and `-var-file` options on the command line, in the order they are provided.\n\n## download_dir\n\nThe terragrunt `download_dir` string option can be used to override the default download directory.\n\nThe precedence is as follows: `--download-dir` command line option → `TG_DOWNLOAD_DIR` env variable →\n`download_dir` attribute of the `terragrunt.hcl` file in the module directory → `download_dir` attribute of the included\n`terragrunt.hcl`.\n\nIt supports all terragrunt functions, i.e. `path_relative_from_include()`.\n\n## prevent_destroy\n\nTerragrunt `prevent_destroy` boolean flag allows you to protect selected OpenTofu/Terraform module. It will prevent `destroy` or\n`run --all destroy` command from actually destroying resources of the protected module. This is useful for modules you want to\ncarefully protect, such as a database, or a module that provides auth.\n\nExample:\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  source = \"git::git@github.com:foo/modules.git//app?ref=v0.0.3\"\n}\n\nprevent_destroy = true\n```\n\n## iam_role\n\nThe `iam_role` attribute can be used to specify an IAM role that Terragrunt should assume before invoking OpenTofu/Terraform.\n\nThe precedence is as follows: `--iam-assume-role` command line option → `TG_IAM_ASSUME_ROLE` env variable →\n`iam_role` attribute of the `terragrunt.hcl` file in the module directory → `iam_role` attribute of the included\n`terragrunt.hcl`.\n\nExample:\n\n```hcl\n# terragrunt.hcl\n\niam_role = \"arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME\"\n```\n\n**Notes:**\n\n- Value of `iam_role` can reference local variables\n- Definitions of `iam_role` included from other HCL files through `include`\n\n## iam_assume_role_duration\n\nThe `iam_assume_role_duration` attribute can be used to specify the STS session duration, in seconds, for the IAM role that Terragrunt should assume before invoking OpenTofu/Terraform.\n\nThe precedence is as follows: `--iam-assume-role-duration` command line option → `TG_IAM_ASSUME_ROLE_DURATION` env variable →\n`iam_assume_role_duration` attribute of the `terragrunt.hcl` file in the module directory → `iam_assume_role_duration` attribute of the included\n`terragrunt.hcl`.\n\nExample:\n\n```hcl\n# terragrunt.hcl\n\niam_assume_role_duration = 14400\n```\n\n## iam_assume_role_session_name\n\nThe `iam_assume_role_session_name` attribute can be used to specify the STS session name, for the IAM role that Terragrunt should assume before running OpenTofu/Terraform.\n\nThe precedence is as follows: `--iam-assume-role-session-name` command line option → `TG_IAM_ASSUME_ROLE_SESSION_NAME` env variable →\n`iam_assume_role_session_name` attribute of the `terragrunt.hcl` file in the module directory → `iam_assume_role_session_name` attribute of the included\n`terragrunt.hcl`.\n\n## iam_web_identity_token\n\nThe `iam_web_identity_token` attribute can be used along with `iam_role` to assume a role using AssumeRoleWithWebIdentity. `iam_web_identity_token` can be set to either the token value (typically using `get_env()`), or the path to a file on disk.\n\nThe precedence is as follows: `--iam-assume-role-web-identity-token` command line option → `TG_IAM_ASSUME_ROLE_WEB_IDENTITY_TOKEN` env variable →\n`iam_web_identity_token` attribute of the `terragrunt.hcl` file in the module directory → `iam_web_identity_token` attribute of the included\n`terragrunt.hcl`.\n\nThe primary benefit of using AssumeRoleWithWebIdentity over regular AssumeRole is that it enables you to run terragrunt in your CI/CD pipelines without static AWS credentials.\n\n### Git Provider Configuration\n\nTo use AssumeRoleWithWebIdentity in your CI/CD environment, you must first configure an AWS [OpenID Connect\nprovider](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html) to trust the OIDC service\nprovided by your git provider.\n\nFollow the instructions below for whichever Git provider you use:\n\n- GitLab: [Configure OpenID Connect in AWS to retrieve temporary credentials](https://docs.gitlab.com/ee/ci/cloud_services/aws/)\n- GitHub: [Configuring OpenID Connect in Amazon Web Services](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services)\n- CircleCI: [Using OpenID Connect tokens in jobs](https://circleci.com/docs/openid-connect-tokens/)\n\nOnce you have configured your OpenID Connect Provider and configured the trust policy of your IAM role according to the above instructions, you\ncan configure Terragrunt to use the Web Identity Token in the following manner.\n\nIf your Git provider provides the OIDC token as an environment variable, pass it in to the `iam_web_identity_token` as follows\n\n```hcl\n# terragrunt.hcl\n\niam_role = \"arn:aws:iam::<AWS account number>:role/<IAM role name>\"\n\niam_web_identity_token = get_env(\"<variable name>\")\n```\n\nIf your Git provider provides the OIDC token as a file, simply pass the file path to `iam_web_identity_token`\n\n```hcl\n# terragrunt.hcl\n\niam_role = \"arn:aws:iam::<AWS account number>:role/<IAM role name>\"\n\niam_web_identity_token = \"/path/to/token/file\"\n```\n\n## terraform_binary\n\nThe terragrunt `terraform_binary` string option can be used to override the default binary Terragrunt calls (which is\n`tofu`).\n\nThe precedence is as follows: `--tf-path` command line option → `TG_TF_PATH` env variable →\n`terragrunt.hcl` in the module directory → included `terragrunt.hcl`\n\n## terraform_version_constraint\n\nThe terragrunt `terraform_version_constraint` string overrides the default minimum supported version of OpenTofu/Terraform.\nTerragrunt usually only officially supports the latest version of OpenTofu/Terraform, however, in some cases an older version of OpenTofu/Terraform is needed.\n\nExample:\n\n```hcl\n# terragrunt.hcl\n\nterraform_version_constraint = \">= 0.11\"\n```\n\n## terragrunt_version_constraint\n\nThe terragrunt `terragrunt_version_constraint` string can be used to specify which versions of the Terragrunt CLI can be used with your configuration. If the running version of Terragrunt doesn't match the constraints specified, Terragrunt will produce an error and exit without taking any further actions.\n\nExample:\n\n```hcl\n# terragrunt.hcl\n\nterragrunt_version_constraint = \">= 0.23\"\n```\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/01-hcl/04-functions.mdx",
    "content": "---\ntitle: Functions\ndescription: Learn about the built-in functions available in Terragrunt.\nslug: reference/hcl/functions\nsidebar:\n  order: 4\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\n\nTerragrunt allows you to use built-in functions anywhere in `terragrunt.hcl`, just like OpenTofu/Terraform\\!\n\n## OpenTofu/Terraform built-in functions\n\nAll [OpenTofu/Terraform built-in functions (as of v0.15.3)](https://opentofu.org/docs/language/functions/) are supported in Terragrunt config files:\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  source = \"../modules/${basename(get_terragrunt_dir())}\"\n}\n\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket = trimspace(\"   my-tofu-bucket     \")\n    region = join(\"-\", [\"us\", \"east\", \"1\"])\n    key    = format(\"%s/tofu.tfstate\", path_relative_to_include())\n  }\n}\n```\n\nNote: Any `file*` functions (`file`, `fileexists`, `filebase64`, etc.) are relative to the directory containing the `terragrunt.hcl` file they’re used in.\n\nGiven the following structure:\n\n<FileTree>\n\n- terragrunt\n  - common.tfvars\n  - assets\n    - mysql\n      - assets.txt\n  - terragrunt.hcl\n\n</FileTree>\n\nThen `assets.txt` could be read with the following function call:\n\n```hcl\nfile(\"assets/mysql/assets.txt\")\n```\n\n**Note:**\n\nTerragrunt was originally able to take advantage of built-in OpenTofu/Terraform built-in functions automatically, as they were exposed via an exported package. Since `v0.15.3`, however, these functions are now `internal` to the respective codebases.\n\nAs a result, Terragrunt users typically use different functions to resolve the same problems. e.g. Terragrunt users can execute arbitrary shell commands with [run\\_cmd](#run_cmd) in whatever language they like instead of using a bespoke HCL function to solve a given problem. In the future, OpenTofu may expose these functions via a public package, which would allow Terragrunt to access them directly. Until such a time, Terragrunt will continue to provide its own set of functions to solve problems relevant to Terragrunt users.\n\nIf there is a specific function you would like to see supported directly in Terragrunt, please [open an issue](https://github.com/gruntwork-io/terragrunt/issues) requesting it. Make sure to include the use case you have in mind so we can understand the problem you are trying to solve, and why existing Terragrunt functions are not sufficient.\n\n## find_in_parent_folders\n\n`find_in_parent_folders` searches up the directory tree from the current `terragrunt.hcl` file, and returns the absolute path to the first file in a parent folder with a given name, or exits with an error if no such file is found. This is primarily useful in an `include` block to automatically find the path to a parent Terragrunt configuration:\n\n```hcl\n# some/folder/terragrunt.hcl\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n```\n\nThe function can also be used to find parent folders.\n\n```hcl\n# some/folder/terragrunt.hcl\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"some\")\n}\n```\n\nYou can also pass an optional second `fallback` parameter, which causes the function to return the fallback value (instead of exiting with an error) if the file in the `name` parameter cannot be found:\n\n```hcl\n# some/folder/terragrunt.hcl\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"some-other-file-name.hcl\", \"fallback.hcl\")\n}\n```\n\nNote that this function searches relative to the `terragrunt.hcl` file when called from a parent config. For\nexample, if you had the following filesystem layout:\n\n<FileTree>\n\n- root.hcl\n- prod\n  - env.hcl\n  - mysql\n    - terragrunt.hcl\n\n</FileTree>\n\nAnd the root `root.hcl` contained the following:\n\n```hcl\n# root.hcl\n\nlocals {\n  env_vars = read_terragrunt_config(find_in_parent_folders(\"env.hcl\"))\n}\n```\n\nThe `find_in_parent_folders` will search from the **child `terragrunt.hcl`** (`prod/mysql/terragrunt.hcl`) config,\nfinding the `env.hcl` file in the `prod` directory.\n\n**NOTE:** This function has undocumented behavior that has since been deprecated. To learn more about this, see the [Migrating from root `terragrunt.hcl`](/migrate/migrating-from-root-terragrunt-hcl) guide.\n\n## path_relative_to_include\n\n`path_relative_to_include()` returns the relative path between the current `terragrunt.hcl` file and the `path` specified in its `include` block. For example, consider the following filesystem layout:\n\n<FileTree>\n\n- root.hcl\n- prod\n  - mysql\n    - terragrunt.hcl\n- stage\n  - mysql\n    - terragrunt.hcl\n\n</FileTree>\n\nImagine `prod/mysql/terragrunt.hcl` and `stage/mysql/terragrunt.hcl` include all settings from the root `root.hcl` file:\n\n```hcl\n# prod/mysql/terragrunt.hcl\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n```\n\nThe root `root.hcl` can use the `path_relative_to_include()` in its `remote_state` configuration to ensure each child stores its remote state at a different `key`:\n\n```hcl\n# root.hcl\n\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket = \"my-tofu-bucket\"\n    region = \"us-east-1\"\n    key    = \"${path_relative_to_include()}/tofu.tfstate\"\n  }\n}\n```\n\nThe resulting `key` will be `prod/mysql/tofu.tfstate` for the prod `mysql` module and `stage/mysql/tofu.tfstate` for the stage `mysql` module.\n\nIf you have `include` blocks, this function requires a `name` parameter when used in the child config to specify which\n`include` block to base the relative path on.\n\nExample:\n\n```hcl\n# prod/mysql/terragrunt.hcl\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ninclude \"region\" {\n  path = find_in_parent_folders(\"region.hcl\")\n}\n\nterraform {\n  source = \"../modules/${path_relative_to_include(\"root\")}\"\n}\n```\n\n## path_relative_from_include\n\n`path_relative_from_include()` returns the relative path between the `path` specified in its `include` block and the current `terragrunt.hcl` file (it is the counterpart of `path_relative_to_include()`). For example, consider the following filesystem layout:\n\n<FileTree>\n\n- sources\n  - mysql\n    - \\*.tf\n  - secrets\n    - mysql\n      - \\*.tf\n- terragrunt\n  - root.hcl\n  - common.tfvars\n  - mysql\n    - terragrunt.hcl\n  - secrets\n    - mysql\n      - terragrunt.hcl\n\n</FileTree>\n\nImagine `terragrunt/mysql/terragrunt.hcl` and `terragrunt/secrets/mysql/terragrunt.hcl` include all settings from the root `root.hcl` file:\n\n```hcl\n# terragrunt/mysql/terragrunt.hcl\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n```\n\nThe root `root.hcl` can use the `path_relative_from_include()` in combination with `path_relative_to_include()` in its `source` configuration to retrieve the relative OpenTofu/Terraform source code from the terragrunt configuration file:\n\n```hcl\n# root.hcl\n\nterraform {\n  source = \"${path_relative_from_include()}/../sources//${path_relative_to_include()}\"\n}\n```\n\nThe resulting `source` will be `../../sources//mysql` for `mysql` module and `../../../sources//secrets/mysql` for `secrets/mysql` module.\n\nAnother use case would be to add an extra argument to include the `common.tfvars` file for all subdirectories:\n\n```hcl\n# root.hcl\n\nterraform {\n  extra_arguments \"common_var\" {\n    commands = [\n      \"apply\",\n      \"plan\",\n      \"import\",\n      \"push\",\n      \"refresh\"\n    ]\n\n    arguments = [\n      \"-var-file=${get_terragrunt_dir()}/${path_relative_from_include()}/common.tfvars\",\n    ]\n  }\n}\n```\n\nThis allows proper retrieval of the `common.tfvars` from whatever the level of subdirectories we have.\n\nIf you have `include` blocks, this function requires a `name` parameter when used in the child config to specify which\n`include` block to base the relative path on.\n\nExample:\n\n```hcl\n# prod/mysql/terragrunt.hcl\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ninclude \"region\" {\n  path = find_in_parent_folders(\"region.hcl\")\n}\n\nterraform {\n  source = \"../modules/${path_relative_from_include(\"root\")}\"\n}\n```\n\n## get_env\n\n`get_env(NAME)` return the value of variable named `NAME` or throws exceptions if that variable is not set. Example:\n\n```hcl\n# terragrunt.hcl\n\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket = get_env(\"BUCKET\")\n  }\n}\n```\n\n`get_env(NAME, DEFAULT)` returns the value of the environment variable named `NAME` or `DEFAULT` if that environment variable is not set. Example:\n\n```hcl\n# terragrunt.hcl\n\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket = get_env(\"BUCKET\", \"my-tofu-bucket\")\n  }\n}\n```\n\nNote that [OpenTofu/Terraform will read environment variables](https://opentofu.org/docs/cli/config/environment-variables/#tf_var_name) that start with the prefix `TF_VAR_`, so one way to share a variable named `foo` between OpenTofu/Terraform and Terragrunt is to set its value as the environment variable `TF_VAR_foo` and to read that value in using this `get_env()` built-in function.\n\n## get_platform\n\n`get_platform()` returns the current Operating System. Example:\n\n```hcl\n# terragrunt.hcl\n\ninputs = {\n  platform = get_platform()\n}\n```\n\nThis function can also be used in a comparison to evaluate what to do based on the current operating system. Example:\n\n```hcl\n# outputs.tf\n\noutput \"platform\" {\n  value = var.platform == \"darwin\" ? \"(value for MacOS)\" : \"(value for other OS's)\"\n}\n```\n\nSome of the returned values can be:\n\n- `darwin`\n- `freebsd`\n- `linux`\n- `windows`\n\n## get_repo_root\n\n`get_repo_root()` returns the absolute path to the root of the Git repository:\n\n```hcl\n# terragrunt.hcl\n\ninputs {\n  very_important_config = \"${get_repo_root()}/config/strawberries.conf\"\n}\n```\n\nThis function will error if the file is not located in a Git repository.\n\n## get_path_from_repo_root\n\n`get_path_from_repo_root()` returns the path from the root of the Git repository to the current directory:\n\n```hcl\n# terragrunt.hcl\n\nremote_state {\n  backend = \"s3\"\n\n  config = {\n    bucket         = \"tofu\"\n    dynamodb_table = \"tofu\"\n    encrypt        = true\n    key            = \"${get_path_from_repo_root()}/tofu.tfstate\"\n    session_name   = \"tofu\"\n    region         = \"us-east-1\"\n  }\n}\n```\n\nThis function will error if the file is not located in a Git repository.\n\n## get_path_to_repo_root\n\n`get_path_to_repo_root()` returns the relative path to the root of the Git repository:\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  source = \"${get_path_to_repo_root()}//modules/example\"\n}\n```\n\nThis function will error if the file is not located in a Git repository.\n\n## get_terragrunt_dir\n\n`get_terragrunt_dir()` returns the directory where the Terragrunt configuration file (by default `terragrunt.hcl`) lives. This is useful when you need to use relative paths with [remote OpenTofu/Terraform configurations](/features/units/#remote-opentofuterraform-modules) and you want those paths relative to your Terragrunt configuration file and not relative to the temporary directory where Terragrunt downloads the code.\n\nFor example, imagine you have the following file structure:\n\n<FileTree>\n\n- common.tfvars\n- frontend-app\n  - terragrunt.hcl\n\n</FileTree>\n\nInside `tofu-code/frontend-app/terragrunt.hcl` you might try to write code that looks like this:\n\n```hcl\n# tofu-code/frontend-app/terragrunt.hcl\n\nterraform {\n  source = \"git::git@github.com:foo/modules.git//frontend-app?ref=v0.0.3\"\n\n  extra_arguments \"custom_vars\" {\n    commands = [\n      \"apply\",\n      \"plan\",\n      \"import\",\n      \"push\",\n      \"refresh\"\n    ]\n\n    arguments = [\n      \"-var-file=../common.tfvars\" # Note: This relative path will NOT work correctly!\n    ]\n  }\n}\n```\n\nNote how the `source` parameter is set, so Terragrunt will download the `frontend-app` code from the `modules` repo into a temporary folder and run `tofu`/`terraform` in that temporary folder. Note also that there is an `extra_arguments` block that is trying to allow the `frontend-app` to read some shared variables from a `common.tfvars` file. Unfortunately, the relative path (`../common.tfvars`) won’t work, as it will be relative to the temporary folder\\! Moreover, you can’t use an absolute path, or the code won’t work on any of your teammates' computers.\n\nTo make the relative path work, you need to use `get_terragrunt_dir()` to combine the path with the folder where the `terragrunt.hcl` file lives:\n\n```hcl\n# tofu-code/frontend-app/terragrunt.hcl\n\nterraform {\n  source = \"git::git@github.com:foo/modules.git//frontend-app?ref=v0.0.3\"\n\n  extra_arguments \"custom_vars\" {\n    commands = [\n      \"apply\",\n      \"plan\",\n      \"import\",\n      \"push\",\n      \"refresh\"\n    ]\n\n    # With the get_terragrunt_dir() function, you can use relative paths!\n    arguments = [\n      \"-var-file=${get_terragrunt_dir()}/../common.tfvars\"\n    ]\n  }\n}\n```\n\n## get_working_dir\n\n`get_working_dir()` returns the absolute path where Terragrunt runs OpenTofu/Terraform commands. This is useful when you need to manage substitutions of vars inside a \\*.tfvars file located right inside terragrunt's tmp dir.\n\n## get_parent_terragrunt_dir\n\n`get_parent_terragrunt_dir()` returns the absolute directory where the Terragrunt parent configuration file lives (regardless of what it's called). This is useful when you need to use relative paths with [remote OpenTofu/Terraform configurations](/features/units/#remote-opentofuterraform-modules) and you want those paths relative to your parent Terragrunt configuration file and not relative to the temporary directory where Terragrunt downloads the code.\n\nThis function is very similar to [get_terragrunt_dir()](#get_terragrunt_dir) except it returns the root instead of the leaf of your terragrunt configurations.\n\n<FileTree>\n\n- root.hcl\n- common.tfvars\n- app1\n  - terragrunt.hcl\n- tests\n  - app2\n    - terragrunt.hcl\n  - app3\n    - terragrunt.hcl\n\n</FileTree>\n\n```hcl\n# root.hcl\n\nterraform {\n  extra_arguments \"common_vars\" {\n    commands = [\n      \"apply\",\n      \"plan\",\n      \"import\",\n      \"push\",\n      \"refresh\"\n    ]\n\n    arguments = [\n      \"-var-file=${get_parent_terragrunt_dir()}/common.tfvars\"\n    ]\n  }\n}\n```\n\nThe common.tfvars located in the root folder will be included by all applications, whatever their relative location to the root.\n\nIf you have `include` blocks, this function requires a `name` parameter when used in the child config to specify which\n`include` block to base the parent dir on.\n\nExample:\n\n```hcl\n# prod/mysql/terragrunt.hcl\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ninclude \"region\" {\n  path = find_in_parent_folders(\"region.hcl\")\n}\n\nterraform {\n  source = \"${get_parent_terragrunt_dir(\"root\")}/modules/vpc\"\n}\n```\n\n## get_original_terragrunt_dir\n\n`get_original_terragrunt_dir()` returns the directory where the original Terragrunt configuration file (by default\n`terragrunt.hcl`) lives. This is primarily useful when one Terragrunt config is being read from another: e.g., if\n`/tofu-code/terragrunt.hcl` calls `read_terragrunt_config(\"/foo/bar.hcl\")`, and within `bar.hcl`, you call\n`get_original_terragrunt_dir()`, you'll get back `/tofu-code`.\n\nDuring stack generation, this function returns the\ndirectory of the `terragrunt.stack.hcl` currently being read, even when invoked from other Terragrunt\nconfigs via `read_terragrunt_config()`.\n\n## get_terraform_commands_that_need_vars\n\n`get_terraform_commands_that_need_vars()` returns the list of OpenTofu/Terraform commands that accept `-var` and `-var-file` parameters. This function is used when defining [extra_arguments](/features/units/extra-arguments/#multiple-extra_arguments-blocks).\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  extra_arguments \"common_var\" {\n    commands  = get_terraform_commands_that_need_vars()\n    arguments = [\"-var-file=${get_aws_account_id()}.tfvars\"]\n  }\n}\n```\n\n## get_terraform_commands_that_need_input\n\n`get_terraform_commands_that_need_input()` returns the list of OpenTofu/Terraform commands that accept the `-input=(true or false)` parameter. This function is used when defining [extra_arguments](/features/units/extra-arguments/#multiple-extra_arguments-blocks).\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  # Force OpenTofu/Terraform to not ask for input value if some variables are undefined.\n  extra_arguments \"disable_input\" {\n    commands  = get_terraform_commands_that_need_input()\n    arguments = [\"-input=false\"]\n  }\n}\n```\n\n## get_terraform_commands_that_need_locking\n\n`get_terraform_commands_that_need_locking()` returns the list of terraform commands that accept the `-lock-timeout` parameter. This function is used when defining [extra_arguments](/features/units/extra-arguments/#multiple-extra_arguments-blocks).\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  # Force OpenTofu/Terraform to keep trying to acquire a lock for up to 20 minutes if someone else already has the lock\n  extra_arguments \"retry_lock\" {\n    commands  = get_terraform_commands_that_need_locking()\n    arguments = [\"-lock-timeout=20m\"]\n  }\n}\n```\n\n## get_terraform_commands_that_need_parallelism\n\n`get_terraform_commands_that_need_parallelism()` returns the list of terraform commands that accept the `-parallelism` parameter. This function is used when defining [extra_arguments](/features/units/extra-arguments/#multiple-extra_arguments-blocks).\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  # Force OpenTofu/Terraform to run with reduced parallelism\n  extra_arguments \"parallelism\" {\n    commands  = get_terraform_commands_that_need_parallelism()\n    arguments = [\"-parallelism=5\"]\n  }\n}\n```\n\n## get_aws_account_alias\n\n`get_aws_account_alias()` returns the AWS account alias associated with the current set of credentials. If the alias cannot be found, it will return an empty string. Example:\n\n```hcl\n# terragrunt.hcl\n\ninputs = {\n  account_alias = get_aws_account_alias()\n}\n```\n\n**Note:** value returned by `get_aws_account_alias()` can change during parsing of HCL code, for example after evaluation of `iam_role` attribute.\n\n## get_aws_account_id\n\n`get_aws_account_id()` returns the AWS account id associated with the current set of credentials. Example:\n\n```hcl\n# terragrunt.hcl\n\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket = \"mycompany-${get_aws_account_id()}\"\n  }\n}\n```\n\n**Note:** value returned by `get_aws_account_id()` can change during parsing of HCL code, for example after evaluation of `iam_role` attribute.\n\n## get_aws_caller_identity_arn\n\n`get_aws_caller_identity_arn()` returns the ARN of the AWS identity associated with the current set of credentials. Example:\n\n```hcl\n# terragrunt.hcl\n\ninputs = {\n  caller_arn = get_aws_caller_identity_arn()\n}\n```\n\n**Note:** value returned by `get_aws_caller_identity_arn()` can change during parsing of HCL code, for example after evaluation of `iam_role` attribute.\n\n## get_terraform_command\n\n`get_terraform_command()` returns the current terraform command in execution. Example:\n\n```hcl\n# terragrunt.hcl\n\ninputs = {\n  current_command = get_terraform_command()\n}\n```\n\n## get_terraform_cli_args\n\n`get_terraform_cli_args()` returns cli args for the current terraform command in execution. Example:\n\n```hcl\n# terragrunt.hcl\n\ninputs = {\n  current_cli_args = get_terraform_cli_args()\n}\n```\n\n## get_default_retryable_errors\n\n`get_default_retryable_errors()` returns the default list of retryable error patterns Terragrunt uses for transient failures. Use it within the `errors` block to seed a `retry` configuration.\n\n```hcl\n# terragrunt.hcl\n\nerrors {\n  retry \"default_errors\" {\n    retryable_errors   = get_default_retryable_errors()\n    max_attempts       = 3\n    sleep_interval_sec = 5\n  }\n\n  retry \"custom_errors\" {\n    retryable_errors   = [\".*my custom error.*\"]\n    max_attempts       = 5\n    sleep_interval_sec = 10\n  }\n}\n```\n\n## get_aws_caller_identity_user_id\n\n`get_aws_caller_identity_user_id()` returns the UserId of the AWS identity associated with the current set of credentials. Example:\n\n```hcl\n# terragrunt.hcl\n\ninputs = {\n  caller_user_id = get_aws_caller_identity_user_id()\n}\n```\n\nThis allows uniqueness of the storage bucket per AWS account (since bucket name must be globally unique).\n\nIt is also possible to configure variables specifically based on the account used:\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  extra_arguments \"common_var\" {\n    commands = get_terraform_commands_that_need_vars()\n    arguments = [\"-var-file=${get_aws_account_id()}.tfvars\"]\n  }\n}\n```\n\n**Note:** value returned by `get_aws_caller_identity_user_id()` can change during parsing of HCL code, for example after evaluation of `iam_role` attribute.\n\n## run_cmd\n\n`run_cmd(command, arg1, arg2…​)` runs a shell command and returns the stdout as the result of the interpolation. The command is executed at the same folder as the `terragrunt.hcl` file. This is useful whenever you want to dynamically fill in arbitrary information in your Terragrunt configuration.\n\n### Basic Usage\n\nAs an example, you could write a script that determines the bucket and DynamoDB table name based on the AWS account, instead of hardcoding the name of every account:\n\n```hcl\n# terragrunt.hcl\n\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket         = run_cmd(\"./get_names.sh\", \"bucket\")\n    dynamodb_table = run_cmd(\"./get_names.sh\", \"dynamodb\")\n  }\n}\n```\n\n### Special Parameters\n\nThe `run_cmd` function accepts some special flags that can alter how the function evaluates commands on your behalf. Placing these `--terragrunt-` prefixed flags as the first argument(s) of a `run_cmd` call will result in the behavior of `run_cmd` being adjusted. You can mix and match the flags in any order, so long as they precede the command you are running with `run_cmd`.\n\n| Parameter                   | Description                                                                                                                                                                                   | Example                                                                                                               |\n|-----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|\n| `--terragrunt-quiet`        | Redacts `run_cmd` stdout from Terragrunt logs while still returning the value to HCL. This keeps sensitive information out of log files.                                                      | `run_cmd(\"--terragrunt-quiet\", \"./decrypt_secret.sh\", \"foo\")`                                                         |\n| `--terragrunt-global-cache` | Stores and reuses results in a global cache so the command only runs once per set of arguments, no matter which configuration references it. Useful when the output is directory-independent. | `run_cmd(\"--terragrunt-global-cache\", \"aws\", \"sts\", \"get-caller-identity\", \"--query\", \"Account\", \"--output\", \"text\")` |\n| `--terragrunt-no-cache`     | Skips the cache entirely and forces the command to run on every evaluation. Use this when the output changes frequently (timestamps, tokens, random IDs, etc.).                               | `run_cmd(\"--terragrunt-no-cache\", \"date\", \"+%s\")`                                                                     |\n\nTerragrunt caches `run_cmd` results by default to avoid running the same command multiple times during parsing. The cache key includes the directory of the `terragrunt.hcl` file and the command arguments unless you opt into global caching or disable caching entirely.\nParameters `--terragrunt-global-cache` and `--terragrunt-no-cache` are mutually exclusive, Terragrunt will return an error if both are provided.\n\n#### Examples\n\n**Suppress output for sensitive values:**\n\n```hcl\n# Output is redacted in logs, but still available to Terragrunt\nsuper_secret_value = run_cmd(\"--terragrunt-quiet\", \"./decrypt_secret.sh\", \"foo\")\n```\n\n**Note:** This will prevent terragrunt from displaying the output from the command in its output. However, the value could still be displayed in the OpenTofu/Terraform output if OpenTofu/Terraform does not treat it as a [sensitive value](https://www.terraform.io/docs/configuration/outputs.html#sensitive-suppressing-values-in-cli-output).\n\n**Use global cache for directory-independent commands:**\n\n```hcl\n# Same result regardless of which directory run_cmd is called from\naccount_id = run_cmd(\"--terragrunt-global-cache\", \"aws\", \"sts\", \"get-caller-identity\", \"--query\", \"Account\", \"--output\", \"text\")\n```\n\n**Disable caching for dynamic values:**\n\n```hcl\n# Generates a new UUID every time run_cmd is evaluated\nbuild_id = run_cmd(\"--terragrunt-no-cache\", \"uuidgen\")\n\n# Gets current timestamp on each parse of the Terragrunt configuration\ntimestamp = run_cmd(\"--terragrunt-no-cache\", \"date\", \"+%s\")\n```\n\n**Combine multiple parameters:**\n\n```hcl\n# Disable cache AND suppress output for a sensitive dynamic value\nsession_token = run_cmd(\"--terragrunt-no-cache\", \"--terragrunt-quiet\", \"./generate-temp-token.sh\")\n\n### Caching Behavior\n\nBy default, invocations of `run_cmd` are cached based on the current directory and executed command, so cached values are reused later rather than executed multiple times. Here's an example:\n\n```hcl\n# terragrunt.hcl\n\nlocals {\n  uuid = run_cmd(\"echo\", \"uuid1\",  uuid())\n  uuid2 = run_cmd(\"echo\", \"uuid2\", uuid())\n  uuid3 = run_cmd(\"echo\", \"uuid3\", uuid())\n  potato = run_cmd(\"echo\", \"potato\")\n  potato2 = run_cmd(\"echo\", \"potato\")\n  carrot = run_cmd(\"echo\", \"carrot\")\n}\ninputs = {\n  potato3 = run_cmd(\"echo\", \"potato\")\n  uuid3 = run_cmd(\"echo\", \"uuid3\", uuid())\n  uuid4 = run_cmd(\"echo\", \"uuid4\", uuid())\n  carrot2 = run_cmd(\"echo\", \"carrot\")\n}\n```\n\nOutput:\n\n```bash\n$ terragrunt init\nuuid1 b48379e1-924d-2403-8789-c72d50be964c\nuuid1 9f3a8398-b11f-5314-7783-dad176ee487d\nuuid1 649ac501-e5db-c935-1499-c59fb7a75625\nuuid2 2d65972b-3fa9-181f-64fe-dcd574d944d0\nuuid3 e345de60-9cfa-0455-79b7-af0d053a15a5\npotato\nuuid3 7f90a4ed-96e3-1dd8-5fee-91b8c8e07650\nuuid2 8638fe79-c589-bebd-2a2a-3e6b96f7fc34\nuuid3 310d0447-f0a6-3f67-efda-e6b1521fa1fb\nuuid4 f8e80cc6-1892-8db7-bd63-6089fef00c01\nuuid2 289ff371-8021-54c6-2254-72de9d11392a\nuuid3 baa19863-1d99-e0ef-11f2-ede830d1c58a\ncarrot\n```\n\n**Key observations from the output:**\n\n- `carrot` and `potato` appear once because subsequent invocations used cached values\n- `uuid1`, `uuid2`, and `uuid3` appear multiple times because each call to `uuid()` generates a different cache key\n- `uuid3` appears one extra time because it's declared in both `locals` and `inputs`\n- `uuid4` appears once since it's declared in `inputs`, which is evaluated once\n\nThis caching behavior can be modified using the special parameters described in the [Special Parameters](#special-parameters) section above.\n\n## read_terragrunt_config\n\n`read_terragrunt_config(config_path, [default_val])` parses the terragrunt config at the given path and serializes the\nresult into a map that can be used to reference the values of the parsed config. This function will expose all blocks\nand attributes of a terragrunt config.\n\nFor example, suppose you had a config file called `common.hcl` that contains common input variables:\n\n```hcl\n# common.hcl\n\ninputs = {\n  stack_name = \"staging\"\n  account_id = \"1234567890\"\n}\n```\n\nYou can read these inputs in another config by using `read_terragrunt_config`, and merge them into the inputs:\n\n```hcl\n# terragrunt.hcl\n\nlocals {\n  common_vars = read_terragrunt_config(find_in_parent_folders(\"common.hcl\"))\n}\n\ninputs = merge(\n  local.common_vars.inputs,\n  {\n    # additional inputs\n  }\n)\n```\n\nThis function also takes in an optional second parameter which will be returned if the file does not exist:\n\n```hcl\n# terragrunt.hcl\n\nlocals {\n  common_vars = read_terragrunt_config(find_in_parent_folders(\"i-dont-exist.hcl\", \"i-dont-exist.hcl\"), {inputs = {}})\n}\n\ninputs = merge(\n  local.common_vars.inputs, # This will be {}\n  {\n    # additional inputs\n  }\n)\n```\n\nNote that this function will also render `dependency` blocks. That is, the parsed config will make the outputs of the\n`dependency` blocks available. For example, suppose you had the following config in a file called `common_deps.hcl`:\n\n```hcl\n# common_deps.hcl\n\ndependency \"vpc\" {\n  config_path = \"${get_terragrunt_dir()}/../vpc\"\n}\n```\n\nYou can access the outputs of the vpc dependency through the parsed outputs of `read_terragrunt_config`:\n\n```hcl\n# terragrunt.hcl\n\nlocals {\n  common_deps = read_terragrunt_config(find_in_parent_folders(\"common_deps.hcl\"))\n}\n\ninputs = {\n  vpc_id = local.common_deps.dependency.vpc.outputs.vpc_id\n}\n```\n\nNotes:\n\n- `read_terragrunt_config` can be also used to read `terragrunt.stack.hcl` and `terragrunt.values.hcl` files.\n\n## sops_decrypt_file\n\n`sops_decrypt_file(file_path)` decrypts a yaml, json, ini, env or \"raw text\" file encrypted with `sops`.\n\n[sops](https://github.com/getsops/sops) is an editor of encrypted files that supports YAML, JSON, ENV, INI and\nBINARY formats and encrypts with AWS KMS, GCP KMS, Azure Key Vault, Hashicorp Vault and PGP.\n\nThis allows static secrets to be stored encrypted within your Terragrunt repository.\n\nFor example, suppose you have some static secrets required to bootstrap your\ninfrastructure in `secrets.yaml`, you can decrypt and merge them into the inputs\nby using `sops_decrypt_file`:\n\n```hcl\n# terragrunt.hcl\n\nlocals {\n  secret_vars = yamldecode(sops_decrypt_file(find_in_parent_folders(\"secrets.yaml\")))\n}\n\ninputs = merge(\n  local.secret_vars,\n  {\n    # additional inputs\n  }\n)\n```\n\nIf you absolutely need to fallback to a default value you can make use of the OpenTofu/Terraform `try` function:\n\n```hcl\n# terragrunt.hcl\n\nlocals {\n  secret_vars = try(jsondecode(sops_decrypt_file(find_in_parent_folders(\"no-secrets-here.json\"))), {})\n}\n\ninputs = merge(\n  local.secret_vars, # This will be {}\n  {\n    # additional inputs\n  }\n)\n```\n\n## get_terragrunt_source_cli_flag\n\n`get_terragrunt_source_cli_flag()` returns the value passed in via the CLI `--source` or an environment variable `TG_SOURCE`. Note that this will return an empty string when either of those values are not provided.\n\nThis is useful for constructing before and after hooks, or TF flags that only apply to local development (e.g., setting up debug flags, or adjusting the `iam_role` parameter).\n\nSome example use cases are:\n\n- Setting debug logging when doing local development.\n- Adjusting the kubernetes provider configuration so that it targets minikube instead of real clusters.\n- Providing special mocks pulled in from the local dev source (e.g., something like `mock_outputs = jsondecode(file(\"${get_terragrunt_source_cli_arg()}/dependency_mocks/vpc.json\"))`).\n\n## read_tfvars_file\n\n`read_tfvars_file(file_path)` reads a `.tfvars` or `.tfvars.json` file and returns a map of the variables defined in it.\n\nThis is useful for reading variables from a `.tfvars` file, merging them into the inputs, or using them in a `locals` block:\n\n```hcl\n# terragrunt.hcl\n\nlocals {\n  inputs_from_tfvars = jsondecode(read_tfvars_file(\"common.tfvars\"))\n}\n\ninputs = merge(\n  local.inputs_from_tfvars,\n  {\n    # additional inputs\n  }\n)\n```\n\nAnother example:\n\n```hcl\n# terragrunt.hcl\n\nlocals {\n  backend = jsondecode(read_tfvars_file(\"backend.tfvars\"))\n}\n\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket         = \"${get_env(\"TG_BUCKET_PREFIX\", \"tf-bucket\")}-${get_aws_account_id()}\"\n    key            = \"${path_relative_to_include()}/terraform-${local.aws_region}.tfstate\"\n    region         = local.backend.region\n  }\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n}\n```\n\n## mark_as_read\n\n`mark_as_read(file_path)` marks a file as read so that it can be picked up for inclusion by the [queue-include-units-reading](/reference/cli/commands/run#queue-include-units-reading) flag.\n\nThis is useful for situations when you want to mark a file as read, but are not reading it using a native Terragrunt HCL function.\n\nFor example:\n\n```hcl\n# terragrunt.hcl\n\nlocals {\n  filename   = mark_as_read(\"/path/to/my/file-read-by-tofu.txt\")\n  many_files = [for f in fileset(\"./config\", \"*.yaml\") : file(mark_as_read(abspath(\"${get_terragrunt_dir()}/config/${f}\")))]\n}\n\ninputs = {\n  filename   = local.filename\n  many_files = local.many_files\n}\n```\n\nBy using `mark_as_read` on `file-read-by-tofu.txt`, you can ensure that the `terragrunt.hcl` file passing in the `file-read-by-tofu.txt` file as an input will be included in\nany `run --all` run where the flag `--queue-include-units-reading file-read-by-tofu.txt` is set.\n\nThe same technique can be used to mark a file as read when a file is read using code in `run_cmd`.\n\n**NOTE**: Due to the way that Terragrunt enqueues files we require an absolute path for mark_as_read to avoid multiple inclusions.\n\n**NOTE**: Due to the way that Terragrunt parses configurations during a `run --all`, functions will only properly mark files as read\nif they are used in the `locals` block. Reading a file directly in the `inputs` block will not mark the file as read, as the `inputs`\nblock is not evaluated until *after* the queue has been populated with units to run.\n\n## constraint_check\n\n`constraint_check(version, constraint)` checks if a given version satisfies a given constraint.\n\nThis particularly is useful for situations where you want to change the runtime behavior of Terragrunt based on the version of an OpenTofu/Terraform module.\n\nFor example:\n\n```hcl\nfeature \"module_version\" {\n  default = \"1.2.3\"\n}\n\nlocals {\n  module_version       = feature.module_version.value\n  needs_v2_adjustments = constraint_check(local.module_version, \">= 2.0.0\")\n}\n\nterraform {\n  source = \"github.com/my-org/my-module.git//?ref=v${local.module_version}\"\n}\n\ninputs = !local.needs_v2_adjustments ? {\n  old_module_input_name = \"old_module_input_value\"\n} : {\n  new_module_input_name = \"new_module_input_value\"\n}\n```\n\nIn this example, the `v2.0.0` version of the module made a breaking change to rename an input variable from `old_module_input_name` to `new_module_input_name`.\n\nInstead of carefully coordinating the version update with the corresponding input change, users can set a feature flag to control opt-in of the new module version, and have Terragrunt dynamically adjust the input variable name based on the constraint check, that the module version is greater than or equal to `2.0.0`.\n\nThe HCL function supports all the same constraints that you can use for version constraints in [terragrunt_version_constraint](/reference/hcl/attributes/#terragrunt_version_constraint) and [terraform_version_constraint](/reference/hcl/attributes/#terraform_version_constraint).\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/01-overview.mdx",
    "content": "---\ntitle: Overview\ndescription: Learn how the Terragrunt CLI works\nslug: reference/cli\nsidebar:\n  order: 1\n---\n\nimport { Aside, Badge, LinkCard } from '@astrojs/starlight/components';\nimport { getCollection, getEntry } from 'astro:content';\nexport const commands = await getCollection('commands');\n\nexport const openTofuShortcutsEntry = commands.filter((command) => command.data.path === 'opentofu-shortcuts')[0];\n\nexport const mainCommands = commands.filter((command) => {\n    return command.data.category === 'main'\n});\n\nexport const backendCommands = commands.filter((command) => {\n    return command.data.category === 'backend'\n});\n\nexport const stackCommands = commands.filter((command) => {\n    return command.data.category === 'stack'\n});\n\nexport const catalogCommands = commands.filter((command) => {\n    return command.data.category === 'catalog'\n});\n\nexport const discoveryCommands = commands.filter((command) => {\n    return command.data.category === 'discovery'\n});\n\nexport const configurationCommands = commands.filter((command) => {\n    return command.data.category === 'configuration'\n});\n\nexport const globalFlags = await getEntry('docs', 'reference/cli/global-flags');\n\nThe Terragrunt CLI is designed to make it as easy as possible to manage infrastructure at any scale.\n\nTo support that design, there are certain patterns that are used throughout the CLI. This document will help you understand those patterns so you can use the CLI more effectively.\n\n## Usage\n\nMost of the time, if you are trying to use Terragrunt to run a command that you would normally run with OpenTofu/Terraform, you can just replace `tofu`/ `terraform` with `terragrunt`.\n\nTerragrunt will pass the command to `tofu`/ `terraform` with the same arguments.\n\n```bash\nterragrunt plan\n```\n\nTerragrunt doesn't always _just_ pass the command. It frequently does some additional processing to make it easier to manage infrastructure at scale.\n\nFor example, in the previous `plan` command, you wouldn't have to explicitly run `init` like you would with `tofu`/ `terraform`. Terragrunt takes advantage of a feature called [Auto-init](/features/units/auto-init) to automatically run `init` when necessary.\n\nUsing Terragrunt in this way is taking advantage of the **OpenTofu Shortcuts** that Terragrunt provides.\n\n<LinkCard title={openTofuShortcutsEntry.data.name} href={`/reference/cli/commands/${openTofuShortcutsEntry.id}`} description={openTofuShortcutsEntry.data.description} />\n\nTerragrunt also has some other commands that are unique to Terragrunt.\n\n## Main Commands\n\nThese are the main commands you will use with Terragrunt:\n\n{\n    mainCommands.map((doc) => (\n        <LinkCard title={doc.data.name} href={`/reference/cli/commands/${doc.id}`} description={doc.data.description} />\n    ))\n}\n\n## Backend Commands\n\nThese are the commands that are used when working with OpenTofu/Terraform state backends:\n\n{\n    backendCommands.map((doc) => (\n        <LinkCard title={\"backend \" + doc.data.name} href={`/reference/cli/commands/${doc.id}`} description={doc.data.description} />\n    ))\n}\n\n## Stack Commands\n\nThese are the commands that are used when working with a `terragrunt.stack.hcl` file:\n\n{\n    stackCommands.map((doc) => (\n        <LinkCard title={\"stack \" + doc.data.name} href={`/reference/cli/commands/${doc.id}`} description={doc.data.description} />\n    ))\n}\n\n## Catalog Commands\n\nThese are the commands that are used when working with a Terragrunt catalog:\n\n{\n    catalogCommands.map((doc) => (\n        <LinkCard title={doc.data.name} href={`/reference/cli/commands/${doc.id}`} description={doc.data.description} />\n    ))\n}\n\n## Discovery Commands\n\nThese are the commands that are used to discover units in your Terragrunt project:\n\n{\n    discoveryCommands.map((doc) => (\n        <LinkCard title={doc.data.name} href={`/reference/cli/commands/${doc.id}`} description={doc.data.description} />\n    ))\n}\n\n## Configuration Commands\n\nThese are the commands that are used to interact directly with Terragrunt configuration:\n\n{\n    configurationCommands.map((doc) => (\n        <LinkCard title={doc.data.path.split('/').join(' ')} href={`/reference/cli/commands/${doc.id}`} description={doc.data.description} />\n    ))\n}\n\n## Global Flags\n\nThere are some flags that are available to all Terragrunt commands:\n\n<LinkCard title={globalFlags.data.title} href={`/${globalFlags.id}`} description={globalFlags.data.description} />\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/0-opentofu-shortcuts.md",
    "content": "---\ntitle: OpenTofu Shortcuts\ndescription: Interact with OpenTofu/Terraform backend infrastructure.\nslug: reference/cli/commands/opentofu-shortcuts\nsidebar:\n  order: 0\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/0100-run.md",
    "content": "---\ntitle: run\ndescription: Run OpenTofu/Terraform commands.\nslug: reference/cli/commands/run\nsidebar:\n  order: 100\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/0200-exec.md",
    "content": "---\ntitle: exec\ndescription: Execute an arbitrary command, wrapped by Terragrunt.\nslug: reference/cli/commands/exec\nsidebar:\n  order: 200\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/0500-catalog.md",
    "content": "---\ntitle: catalog\ndescription: Launch a Terminal User Interface (TUI) to browse and use OpenTofu/Terraform modules.\nslug: reference/cli/commands/catalog\nsidebar:\n  order: 500\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/0600-scaffold.md",
    "content": "---\ntitle: scaffold\ndescription: Generate Terragrunt configuration files from a catalog.\nslug: reference/cli/commands/scaffold\nsidebar:\n  order: 600\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/0700-find.md",
    "content": "---\ntitle: find\ndescription: Find relevant Terragrunt configurations.\nslug: reference/cli/commands/find\nsidebar:\n  order: 700\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/0800-list.md",
    "content": "---\ntitle: list\ndescription: List Terragrunt configurations.\nslug: reference/cli/commands/list\nsidebar:\n  order: 800\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/1100-render.md",
    "content": "---\ntitle: render\ndescription: Render a simplified, but equivalent Terragrunt config.\nslug: reference/cli/commands/render\nsidebar:\n  order: 1100\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/backend/0300-bootstrap.md",
    "content": "---\ntitle: bootstrap\ndescription: Interact with OpenTofu/Terraform backend infrastructure.\nslug: reference/cli/commands/backend/bootstrap\nsidebar:\n  order: 300\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/backend/0301-migrate.md",
    "content": "---\ntitle: migrate\ndescription: Migrate OpenTofu/Terraform state from one location to another.\nslug: reference/cli/commands/backend/migrate\nsidebar:\n  order: 301\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/backend/0302-delete.md",
    "content": "---\ntitle: delete\ndescription: Delete OpenTofu/Terraform state.\nslug: reference/cli/commands/backend/delete\nsidebar:\n  order: 302\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/dag/1000-graph.md",
    "content": "---\ntitle: graph\ndescription: Graph the Directed Acyclic Graph (DAG) in DOT language.\nslug: reference/cli/commands/dag/graph\nsidebar:\n  order: 1000\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/hcl/0900-fmt.md",
    "content": "---\ntitle: fmt\ndescription: Recursively find HashiCorp Configuration Language (HCL) files and rewrite them into a canonical format.\nslug: reference/cli/commands/hcl/fmt\nsidebar:\n  order: 900\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/hcl/0901-validate.md",
    "content": "---\ntitle: validate\ndescription: Recursively find HashiCorp Configuration Language (HCL) files and validate them.\nslug: reference/cli/commands/hcl/validate\nsidebar:\n  order: 900\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/info/1200-print.md",
    "content": "---\ntitle: print\ndescription: Print out a short description of Terragrunt context.\nslug: reference/cli/commands/info/print\nsidebar:\n  order: 1200\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/stack/0400-generate.md",
    "content": "---\ntitle: generate\ndescription: Generate a stack of units based on configurations in a terragrunt.stack.hcl file.\nslug: reference/cli/commands/stack/generate\nsidebar:\n  order: 400\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/stack/0401-run.md",
    "content": "---\ntitle: run\ndescription: Run a command against a stack of units defined in a terragrunt.stack.hcl file.\nslug: reference/cli/commands/stack/run\nsidebar:\n  order: 401\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/stack/0402-output.md",
    "content": "---\ntitle: output\ndescription: Retrieve outputs from units defined in a terragrunt.stack.hcl file as an aggregated output.\nslug: reference/cli/commands/stack/output\nsidebar:\n  order: 402\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/02-commands/stack/0403-clean.md",
    "content": "---\ntitle: clean\ndescription: Remove the auto-generated `.terragrunt-stack` directories created by `stack` commands.\nslug: reference/cli/commands/stack/clean\nsidebar:\n  order: 402\n---\n\n<!-- This page is intentionally empty. Commands are defined in `src/pages/reference/cli/commands/[...slug].astro -->\n<!-- This file is a placeholder to ensure that other pages see commands in their sidebars, and so that the data is accessible in the docs collection. -->\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/02-cli/98-global-flags.mdx",
    "content": "---\ntitle: Global Flags\ndescription: Global flags for the Terragrunt CLI.\nslug: reference/cli/global-flags\nsidebar:\n  order: 98\n---\n\nimport Flag from '@components/Flag.astro'\n\nThe Terragrunt CLI supports the following global flags:\n\n## --experiment\n\n<Flag slug=\"experiment\" />\n\n## --experiment-mode\n\n<Flag slug=\"experiment-mode\" />\n\n## --log-custom-format\n\n<Flag slug=\"log-custom-format\" />\n\n## --log-disable\n\n<Flag slug=\"log-disable\" />\n\n## --log-format\n\n<Flag slug=\"log-format\" />\n\n## --log-level\n\n<Flag slug=\"log-level\" />\n\n## --log-show-abs-paths\n\n<Flag slug=\"log-show-abs-paths\" />\n\n## --no-color\n\n<Flag slug=\"no-color\" />\n\n## --no-tip\n\n<Flag slug=\"no-tip\" />\n\n## --no-tips\n\n<Flag slug=\"no-tips\" />\n\n## --non-interactive\n\n<Flag slug=\"non-interactive\" />\n\n## --strict-control\n\n<Flag slug=\"strict-control\" />\n\n## --strict-mode\n\n<Flag slug=\"strict-mode\" />\n\n## --working-dir\n\n<Flag slug=\"working-dir\" />\n\n## --help\n\n<Flag slug=\"help\" />\n\n## --version\n\n<Flag slug=\"version\" />\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/03-strict-controls.mdx",
    "content": "---\ntitle: Strict Controls\ndescription: Opt-in to strict controls to avoid deprecated features and ensure your code is future-proof.\nslug: reference/strict-controls\nsidebar:\n  order: 3\n---\n\nimport { Aside } from \"@astrojs/starlight/components\";\n\nTerragrunt supports operating in a mode referred to as \"Strict Mode\".\n\nStrict Mode is a set of controls that can be enabled to ensure that your Terragrunt usage is future-proof\nby making deprecated features throw errors instead of warnings. This can be useful when you want to ensure\nthat your Terragrunt code is up-to-date with the latest conventions to avoid breaking changes in\nfuture versions of Terragrunt.\n\nWhenever possible, Terragrunt will initially provide you with a warning when you use a deprecated feature, without throwing an error.\nHowever, in Strict Mode, these warnings will be converted to errors, which will cause the Terragrunt command to fail.\n\nA good practice for using strict controls is to enable Strict Mode in your CI/CD pipelines for lower environments\nto catch any deprecated features early on. This allows you to fix them before they become a problem\nin production in a future Terragrunt release.\n\nIf you are unsure about the impact of enabling strict controls, you can enable them for specific controls to\ngradually increase your confidence in the future compatibility of your Terragrunt usage.\n\n## Controlling Strict Mode\n\nThe simplest way to enable strict mode is to set the [strict-mode](/reference/strict-controls) flag.\n\nThis will enable strict mode for all Terragrunt commands, for all strict mode controls.\n\n```bash\n$ terragrunt run-all plan\n15:26:08.585 WARN   The `run-all plan` command is deprecated and will be removed in a future version. Use `terragrunt run --all plan` instead.\n```\n\n```bash\n$ terragrunt --strict-mode run-all plan\n15:26:23.685 ERROR  The `run-all plan` command is no longer supported. Use `terragrunt run --all plan` instead.\n```\n\nYou can also use the environment variable, which can be more useful in CI/CD pipelines:\n\n```bash\n$ TG_STRICT_MODE='true' terragrunt run-all plan\n15:26:23.685 ERROR  The `run-all plan` command is no longer supported. Use `terragrunt run --all plan` instead.\n```\n\nInstead of enabling strict mode like this, you can also enable specific strict controls by setting the [strict-control](/reference/strict-controls)\nflag to a value that's specific to a particular strict control.\nThis can allow you to gradually increase your confidence in the future compatibility of your Terragrunt usage.\n\n```bash\n$ terragrunt run-all plan --strict-control cli-redesign\n15:26:08.585 WARN   The `run-all plan` command is deprecated and will be removed in a future version. Use `terragrunt run --all plan` instead.\n```\n\n```bash\n$ terragrunt run-all plan --strict-control cli-redesign\n15:26:23.685 ERROR  The `run-all plan` command is no longer supported. Use `terragrunt run --all plan` instead.\n```\n\nAgain, you can also use the environment variable, which might be more useful in CI/CD pipelines:\n\n```bash\n$ TG_STRICT_CONTROL='cli-redesign' terragrunt run-all plan\n15:26:23.685 ERROR  The `run-all plan` command is no longer supported. Use `terragrunt run --all plan` instead.\n```\n\nYou can enable multiple strict controls at once:\n\n```bash\n$ terragrunt run-all plan --strict-control cli-redesign --strict-control default-command\n15:26:23.685 ERROR  The `run-all plan` command is no longer supported. Use `terragrunt run --all plan` instead.\n15:26:46.521 ERROR  Unable to determine underlying exit code, so Terragrunt will exit with error code 1\n```\n\n```bash\n$ terragrunt run-all apply --strict-control cli-redesign --strict-control default-command\n15:26:46.564 ERROR  The `run-all apply` command is no longer supported. Use `terragrunt run --all apply` instead.\n15:26:46.564 ERROR  Unable to determine underlying exit code, so Terragrunt will exit with error code 1\n```\n\nYou can also enable multiple strict controls at once when using the environment variable by using a comma-delimited list.\n\n```bash\n$ TG_STRICT_CONTROL='cli-redesign,default-command' bash -c 'terragrunt run-all plan; terragrunt run-all apply'\n15:26:46.521 ERROR  The `run-all plan` command is no longer supported. Use `terragrunt run --all plan` instead.\n15:26:46.521 ERROR  Unable to determine underlying exit code, so Terragrunt will exit with error code 1\n15:26:46.564 ERROR  The `run-all apply` command is no longer supported. Use `terragrunt run --all apply` instead.\n15:26:46.564 ERROR  Unable to determine underlying exit code, so Terragrunt will exit with error code 1\n```\n\nYou can also use [control categories](#control-categories) to enable certain categories of strict controls.\n\n```bash\n$ terragrunt run-all plan --strict-control deprecated-commands\n15:26:23.685 ERROR  The `run-all plan` command is no longer supported. Use `terragrunt run --all plan` instead.\n```\n\n## Strict Mode Controls\n\nThe following strict mode controls are available:\n\n### root-terragrunt-hcl\n\nThrow an error when users try to reference a root `terragrunt.hcl` file using `find_in_parent_folders`.\n\nThis control will also try to find other scenarios where users may be using `terragrunt.hcl` as the root configuration, including when using commands like `scaffold` and `catalog`, which can generate a `terragrunt.hcl` file expecting a `terragrunt.hcl` file at the root of the project. Enabling this flag adjusts the defaults for those commands so that they expect a recommended `root.hcl` file by default, and will throw an error if a `terragrunt.hcl` file is explicitly set.\n\n**Reason**: Using a root `terragrunt.hcl` file was previously the recommended pattern to use with Terragrunt, but that is no longer the case. For more information see [Migrating from root `terragrunt.hcl`](/migrate/migrating-from-root-terragrunt-hcl/).\n\n### terragrunt-prefix-env-vars\n\nThrow an error when using the `TERRAGRUNT_` prefix for environment variables.\n\n**Reason**: This prefix has been renamed to `TG_` to shorten the prefix, due to the work in RFC [#3445](https://github.com/gruntwork-io/terragrunt/issues/3445).\n**Example**: The `TERRAGRUNT_LOG_LEVEL` env var is deprecated and will be removed in a future version. Use `TG_LOG_LEVEL=info` instead.\n\n### default-command\n\nThrow an error when using the Terragrunt default command.\n\n**Reason**: Terragrunt now supports a special `run` command that can be used to explicitly forward commands to OpenTofu/Terraform when no shortcut exists in the Terragrunt CLI.\n**Example**: The default command is deprecated and will be removed in a future version. Use `terragrunt run` instead.\n\n### cli-redesign\n\nThrow an error when using commands that were deprecated as part of the CLI redesign.\n\n**Commands**:\n\n- `run-all`\n- `graph`\n- `graph-dependencies`\n- `hclfmt`\n- `hclvalidate`\n- `output-module-groups`\n- `render-json`\n- `terragrunt-info`\n- `validate-inputs`\n\n**Reason**: These commands have been deprecated in favor of more consistent and intuitive commands as part of the CLI redesign. For more information, see the [CLI Redesign Migration Guide](/migrate/cli-redesign/).\n\n### bare-include\n\nThrow an error when using a bare include.\n\n**Reason**: Backwards compatibility for supporting bare includes results in a performance penalty for Terragrunt, and deprecating support provides a significant performance improvement. For more information, see the [Bare Include Migration Guide](/migrate/bare-include/).\n\n\n### queue-exclude-external\n\nThrow an error when using the deprecated `--queue-exclude-external` flag.\n\n**Reason**: External dependencies are now excluded by default. The `--queue-exclude-external` flag is no longer needed and has been deprecated. Use `--queue-include-external` if you need to include external dependencies.\n\n**Example**:\n\n```bash\n# This will show a warning (or error with strict control enabled)\n$ terragrunt run --all plan --queue-exclude-external\nWARN  The `--queue-exclude-external` flag is deprecated and will be removed in a future version of Terragrunt. External dependencies are now excluded by default.\n```\n\n### queue-strict-include\n\nThrow an error when using the deprecated `--queue-strict-include` flag.\n\n**Reason**: The behavior of Terragrunt when using `--queue-strict-include` is now the default behavior. The `--queue-strict-include` flag is no longer needed and has been deprecated.\n\n**Example**:\n\n```bash\n# This will show a warning (or error with strict control enabled)\n$ terragrunt run --all plan --queue-strict-include\nWARN  The `--queue-strict-include` flag is deprecated and will be removed in a future version of Terragrunt. The behavior of Terragrunt when using `--queue-strict-include` is now the default behavior.\n```\n\n### units-that-include\n\nThrow an error when using the deprecated `--units-that-include` flag.\n\n**Reason**: The `--units-that-include` flag has been deprecated. Use `--filter='reading=<path>'` to include units that include or read the specified configuration.\n\n**Example**:\n\n```bash\n# This will show a warning (or error with strict control enabled)\n$ terragrunt run --all plan --units-that-include=root.hcl\nWARN  The `--units-that-include` flag is deprecated and will be removed in a future version of Terragrunt. Use `--filter='reading=<path>'` to include units that include or read the specified configuration.\n```\n\n### disable-command-validation\n\nThrow an error when using the deprecated `--disable-command-validation` flag.\n\n**Reason**: Command validation has been removed entirely from Terragrunt. The `run` command now accepts any command and passes it through to the underlying OpenTofu/Terraform binary. The `--disable-command-validation` flag is no longer needed and does nothing.\n\n### no-destroy-dependencies-check\n\nThrow an error when using the deprecated `--no-destroy-dependencies-check` flag.\n\n**Reason**: The `--no-destroy-dependencies-check` flag is deprecated and no longer affects Terragrunt's behavior. Dependency checks are now disabled by default during destroy operations. Use `--destroy-dependencies-check` to explicitly enable dependency checks when needed.\n\n### legacy-internal-tflint\n\nForce the use of external tflint binary.\n\n**Reason**: The embedded version of tflint has been held back by upstream's adoption of the BUSL license. As a result, the embedded version is horribly out of date, and we are deprecating it for removal in a future version of Terragrunt. You may use `--terragrunt-external-tflint` in your `tflint` hook to opt in to the use of an external tflint binary, or enable this strict control.\n\n### deprecated-hidden-flag\n\nThrow an error when using the deprecated `--hidden` flag.\n\n**Reason**: Hidden directories are now included by default in `find` and `list` command results. The `--hidden` flag is no longer needed and has been deprecated. Use `--no-hidden` if you need to exclude hidden directories.\n\n**Example**:\n\n```bash\n# This will show a warning (or error with strict control enabled)\n$ terragrunt find --hidden\nWARN  The `--hidden` flag is deprecated and will be removed in a future version of Terragrunt. Hidden directories are now included by default. Use `--no-hidden` to exclude them.\n```\n\n### disable-dependent-modules\n\nThrow an error when using the deprecated `--disable-dependent-modules` flag.\n\n**Reason**: Dependent modules discovery has been removed from `terragrunt render`. The `--disable-dependent-modules` flag is no longer needed and has no effect.\n\n**Example**:\n\n```bash\n# This will show a warning (or error with strict control enabled)\n$ terragrunt render --format=json --disable-dependent-modules\nWARN  The `--disable-dependent-modules` flag is deprecated and will be removed in a future version of Terragrunt. Dependent modules discovery has been removed from `terragrunt render`, so this flag has no effect.\n```\n\n## Control Categories\n\nCertain strict controls are grouped into categories to make it easier to enable multiple strict controls at once.\n\nThese categories change over time, so you might want to use the specific strict controls if you want to ensure that only certain controls are enabled.\n\n### deprecated-commands\n\nThrow an error when using the deprecated commands.\n\n**Controls**:\n\n- [default-command](#default-command)\n- [cli-redesign](#cli-redesign)\n\n**Note**: The individual `*-all` commands (`plan-all`, `apply-all`, `destroy-all`, `output-all`, `validate-all`, `spin-up`, `tear-down`) have been removed from Terragrunt and are no longer available as strict controls. Use `terragrunt run --all` for the modern equivalent.\n\n### deprecated-flags\n\nThrow an error when using the deprecated flags.\n\n**Controls**:\n\n- [queue-exclude-external](#queue-exclude-external)\n- [no-destroy-dependencies-check](#no-destroy-dependencies-check)\n- [deprecated-hidden-flag](#deprecated-hidden-flag)\n- [queue-strict-include](#queue-strict-include)\n- [units-that-include](#units-that-include)\n- [disable-command-validation](#disable-command-validation)\n- [disable-dependent-modules](#disable-dependent-modules)\n\n### deprecated-env-vars\n\nThrow an error when using the deprecated environment variables.\n\n**Controls**:\n\n- [terragrunt-prefix-env-vars](#terragrunt-prefix-env-vars)\n\n## Completed Controls\n\nThe following strict controls have been completed and are no longer needed:\n\n- [legacy-all](#legacy-all)\n- [spin-up](#spin-up)\n- [tear-down](#tear-down)\n- [plan-all](#plan-all)\n- [apply-all](#apply-all)\n- [destroy-all](#destroy-all)\n- [output-all](#output-all)\n- [validate-all](#validate-all)\n- [skip-dependencies-inputs](#skip-dependencies-inputs)\n- [terragrunt-prefix-flags](#terragrunt-prefix-flags)\n- [double-star](#double-star)\n\n### skip-dependencies-inputs\n\n**Status**: Completed - Dependency inputs are now disabled by default.\n\nReading inputs from dependencies has been deprecated and is now disabled by default for performance. Use dependency outputs instead.\n\n### terragrunt-prefix-flags\n\n**Status**: Completed - The `--terragrunt-` prefix for flags has been removed from Terragrunt.\n\n**Reason**: The `--terragrunt-` prefix for flags is no longer necessary, due to the work in RFC [#3445](https://github.com/gruntwork-io/terragrunt/issues/3445). Use the flag name without the prefix instead (e.g., `--non-interactive` instead of `--terragrunt-non-interactive`).\n\n### double-star\n\n**Status**: Completed - The `**` glob pattern now matches all subdirectories regardless of depth by default.\n\nThe `**` glob pattern in [queue-exclude-dir](/reference/cli/commands/run#queue-exclude-dir) and [queue-include-dir](/reference/cli/commands/run#queue-include-dir) now matches all subdirectories regardless of depth, and `**/*` matches subdirectories with a depth of at least one. This behavior is now the default and this strict control is no longer needed.\n\n### require-explicit-bootstrap\n\n**Status**: Completed - Backend provisioning is no longer performed automatically by default.\n\nTerragrunt now requires explicit opt-in to bootstrap backend infrastructure. Use `terragrunt backend bootstrap` or pass `--backend-bootstrap` to a `run` command (e.g., `terragrunt run apply --backend-bootstrap`) to provision or update backend resources referenced by the [`remote_state`](/reference/hcl/blocks/#remote_state) block.\n\nThis strict control is no longer necessary because the default behavior already requires explicit bootstrapping.\n\n### legacy-all\n\n**Status**: Completed - The legacy `*-all` commands have been removed from Terragrunt.\n\nThis control was previously used to throw an error when using any of the legacy commands that were replaced by `run-all`. These commands have now been completely removed from Terragrunt as part of the deprecation schedule.\n\n**Previously controlled commands** (now removed):\n\n- `plan-all` - Use `terragrunt run --all plan` instead\n- `apply-all` - Use `terragrunt run --all apply` instead\n- `destroy-all` - Use `terragrunt run --all destroy` instead\n- `output-all` - Use `terragrunt run --all output` instead\n- `validate-all` - Use `terragrunt run --all validate` instead\n- `spin-up` - Use `terragrunt run --all apply` instead\n- `tear-down` - Use `terragrunt run --all destroy` instead\n\n### spin-up\n\n**Status**: Completed - This command has been completely removed from Terragrunt.\n\n**Reason**: The `spin-up` command was deprecated and has now been removed as part of the deprecation schedule. Use `terragrunt run --all apply` instead.\n\n### tear-down\n\n**Status**: Completed - This command has been completely removed from Terragrunt.\n\n**Reason**: The `tear-down` command was deprecated and has now been removed as part of the deprecation schedule. Use `terragrunt run --all destroy` instead.\n\n### plan-all\n\n**Status**: Completed - This command has been completely removed from Terragrunt.\n\n**Reason**: The `plan-all` command was deprecated and has now been removed as part of the deprecation schedule. Use `terragrunt run --all plan` instead.\n\n### apply-all\n\n**Status**: Completed - This command has been completely removed from Terragrunt.\n\n**Reason**: The `apply-all` command was deprecated and has now been removed as part of the deprecation schedule. Use `terragrunt run --all apply` instead.\n\n### destroy-all\n\n**Status**: Completed - This command has been completely removed from Terragrunt.\n\n**Reason**: The `destroy-all` command was deprecated and has now been removed as part of the deprecation schedule. Use `terragrunt run --all destroy` instead.\n\n### output-all\n\n**Status**: Completed - This command has been completely removed from Terragrunt.\n\n**Reason**: The `output-all` command was deprecated and has now been removed as part of the deprecation schedule. Use `terragrunt run --all output` instead.\n\n### validate-all\n\n**Status**: Completed - This command has been completely removed from Terragrunt.\n\n**Reason**: The `validate-all` command was deprecated and has now been removed as part of the deprecation schedule. Use `terragrunt run --all validate` instead.\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/04-experiments.md",
    "content": "---\ntitle: Experiments\ndescription: Opt-in to experimental features before they're stable.\nslug: reference/experiments\nsidebar:\n  order: 4\n---\n\nTerragrunt supports operating in a mode referred to as \"Experiment Mode\".\n\nExperiment Mode is a set of controls that can be enabled to opt in to experimental features before they're stable.\nThese features are subject to change and may be removed or altered at any time.\nThey generally provide early access to new features or changes that are being considered for inclusion in future releases.\n\nThose experiments will be documented here so that you know the following:\n\n1. What the experiment is.\n2. What the experiment does.\n3. How to provide feedback on the experiment.\n4. What criteria must be met for the experiment to be considered stable.\n\nSometimes, the criteria for an experiment to be considered stable is unknown, as there may not be a clear path to stabilization. In that case, this will be noted in the experiment documentation, and collaboration with the community will be encouraged to help determine the future of the experiment.\n\n## Controlling Experiment Mode\n\nThe simplest way to enable experiment mode is to set the [experiment-mode](/reference/experiments) flag.\n\nThis will enable experiment mode for all Terragrunt commands, for all experiments (note that this isn't generally recommended, unless you are following Terragrunt development closely and are prepared for the possibility of breaking changes).\n\n```bash\nterragrunt plan --experiment-mode\n```\n\nYou can also use the environment variable, which can be more useful in CI/CD pipelines:\n\n```bash\nTG_EXPERIMENT_MODE='true' terragrunt plan\n```\n\nInstead of enabling experiment mode, you can also enable specific experiments by setting the [experiment](/reference/experiments)\nflag to a value that's specific to an experiment.\nThis can allow you to experiment with a specific unstable feature that you think might be useful to you.\n\n```bash\nterragrunt plan --experiment symlinks\n```\n\nAgain, you can also use the environment variable, which can be more useful in CI/CD pipelines:\n\n```bash\nTG_EXPERIMENT='symlinks' terragrunt plan\n```\n\nYou can also enable multiple experiments at once.\n\n```bash\nterragrunt --experiment symlinks plan\n```\n\nIncluding the environment variable:\n\n```bash\nTG_EXPERIMENT='symlinks,stacks' terragrunt plan\n```\n\n## Active Experiments\n\nThe following experiments are available:\n\n- [symlinks](#symlinks)\n- [cas](#cas)\n- [filter-flag](#filter-flag)\n- [iac-engine](#iac-engine)\n- [dependency-fetch-output-from-state](#dependency-fetch-output-from-state)\n\n### symlinks\n\nSupport symlink resolution for Terragrunt units.\n\n#### symlinks - What it does\n\nBy default, Terragrunt will ignore symlinks when determining which units it should run. By enabling this experiment, Terragrunt will resolve symlinks and add them to the list of units being run.\n\n#### symlinks - How to provide feedback\n\nProvide your feedback on the [Experiment: Symlinks](https://github.com/gruntwork-io/terragrunt/discussions/3671) discussion.\n\n#### symlinks - Criteria for stabilization\n\nTo stabilize this feature, the following need to be resolved, at a minimum:\n\n- [ ] Ensure that symlink support continues to work for users referencing symlinks in flags. See [#3622](https://github.com/gruntwork-io/terragrunt/issues/3622).\n  - [ ] Add integration tests for all filesystem flags to confirm support with symlinks (or document the fact that they cannot be supported).\n- [ ] Ensure that MacOS integration tests still work. See [#3616](https://github.com/gruntwork-io/terragrunt/issues/3616).\n  - [ ] Add integration tests for MacOS in CI.\n\n### `cas`\n\nSupport for Terragrunt Content Addressable Storage (CAS).\n\n#### `cas` - What it does\n\nAllow Terragrunt to store and retrieve Git repositories from a Content Addressable Storage (CAS) system.\n\nThe CAS is used to speed up both catalog cloning and OpenTofu/Terraform source cloning by avoiding redundant downloads of Git repositories.\n\n#### `cas` - How to provide feedback\n\nShare your experience with this feature in the [CAS](https://github.com/gruntwork-io/terragrunt/discussions/3939) Feedback GitHub Discussion.\nFeedback is crucial for ensuring the feature meets real-world use cases. Please include:\n\n- Any bugs or issues encountered (including logs or stack traces if possible).\n- Suggestions for additional improvements or enhancements.\n\n#### `cas` - Criteria for stabilization\n\nTo transition the `cas` feature to a stable release, the following must be addressed:\n\n- [x] Add support for storing and retrieving catalog repositories from the CAS.\n- [x] Add support for storing and retrieving OpenTofu/Terraform modules from the CAS.\n- [ ] Add support for storing and retrieving Unit/Stack configurations from the CAS.\n\n### `filter-flag`\n\nSupport for sophisticated unit and stack filtering using the `--filter` flag.\n\n#### `filter-flag` - What it does\n\nThe `--filter` flag provides a sophisticated querying syntax for targeting units and stacks in Terragrunt commands. This unified approach replaces the need for multiple queue control flags and offers powerful filtering capabilities.\n\n**Current Support Status:**\n\n- ✅ Available in `find`, `list`, and `run` commands\n\n**Supported Filtering Types:**\n\n1. **Name-based filtering**: Target units/stacks by their directory name (exact match or glob patterns)\n2. **Path-based filtering**: Target units/stacks by their file system path (relative, absolute, or glob patterns)\n3. **Attribute-based filtering**: Target units by configuration attributes:\n   - `type=unit` or `type=stack` - Filter by component type\n   - `external=true` or `external=false` - Filter by whether the unit/stack is an external dependency (outside the current working directory)\n   - `name=pattern` - Filter by name using glob patterns\n4. **Negation filters**: Exclude units using the `!` prefix\n5. **Filter intersection**: Combine filters using the `|` operator for results pruning\n6. **Multiple filters**: Specify multiple `--filter` flags to union results\n\n**Not Yet Implemented:**\n\n- Git-based filtering (`[ref...ref]` syntax)\n- Dependency/dependent traversal (`...` syntax)\n\n#### `filter-flag` - How to provide feedback\n\nProvide your feedback on the [Filter Flag RFC](https://github.com/gruntwork-io/terragrunt/issues/4060) GitHub issue.\n\n#### `filter-flag` - Criteria for stabilization\n\nTo transition the `filter-flag` feature to a stable release, the following must be addressed, at a minimum:\n\n- [x] Add support for name-based filtering\n- [x] Add support for path-based filtering (relative, absolute, glob)\n- [x] Add support for attribute-based filtering (type, external, name)\n- [x] Add support for negation filters (!)\n- [x] Add support for filter intersection (|)\n- [x] Add support for multiple filters (union/OR semantics)\n- [x] Integrate with the `find` command\n- [x] Integrate with the `list` command\n- [x] Integrate with the `run` command\n- [x] Add support for git-based filtering ([ref...ref] syntax)\n- [x] Add support for dependency/dependent traversal (... syntax)\n- [x] Add support for `--filters-file` flag\n- [x] Add support for `--filter-allow-destroy` flag\n- [x] Add support for `--filter-affected` shorthand\n- [x] Comprehensive integration testing across all commands\n- [ ] Backport legacy queue control flags (queue-exclude-dir, queue-include-dir, etc.) into equivalent filter patterns as aliases.\n\n**Future Deprecations:**\n\nWhen this experiment stabilizes, the following queue control flags will be deprecated in favor of the unified `--filter` flag:\n\n- `--queue-exclude-dir`\n- `--queue-excludes-file`\n- `--queue-exclude-external`\n- `--queue-include-dir`\n- `--queue-include-external`\n- `--queue-include-units-including`\n- `--queue-strict-include`\n\nThe current plan is to continue to support the flags as aliases for particular `--filter` patterns.\n\n### `iac-engine`\n\nSupport for Terragrunt IaC engines.\n\n#### `iac-engine` - What it does\n\nEnables usage of [Terragrunt IaC engines](/features/units/engine) for running IaC operations. This allows Terragrunt to use pluggable engines to execute Terraform/OpenTofu commands, providing enhanced functionality and extensibility.\n\nIaC engines are still experimental, as the API is unstable and may change in future minor versions of Terragrunt.\n\nYou can disable engine usage on a per-command basis using the [`--no-engine`](/reference/cli/commands/run#no-engine) flag, even when the experiment is enabled globally.\n\n#### `iac-engine` - How to provide feedback\n\nProvide your feedback on the [Terragrunt IaC Engines](https://github.com/gruntwork-io/terragrunt/discussions/5202) GitHub discussion.\n\n#### `iac-engine` - Criteria for stabilization\n\nTo transition the `iac-engine` feature to a stable release, the following must be addressed, at a minimum:\n\n- [ ] API stability and backward compatibility guarantees\n- [ ] Comprehensive integration testing across all supported operations\n- [ ] Documentation of engine development and integration process\n- [ ] Performance benchmarks and optimization\n- [ ] Security review of engine execution and isolation mechanisms\n- [ ] Community feedback on real-world usage and edge cases\n\n### `dependency-fetch-output-from-state`\n\nSupport for fetching dependency outputs directly from state files.\n\n#### `dependency-fetch-output-from-state` - What it does\n\nBy default, Terragrunt retrieves dependency outputs by running `tofu output` or `terraform output` commands, which requires initializing the dependency unit and can be slow. When this experiment is enabled, Terragrunt will attempt to fetch dependency outputs directly from the remote state file, bypassing the need to initialize the dependency and significantly speeding up dependency processing.\n\n**Current Backend Support:**\n\n- ✅ S3 backend: Fully supported\n- ⚠️ Other backends: Falls back to the normal method (using `tofu/terraform output`)\n\nWhen an unsupported backend is encountered, Terragrunt will automatically fall back to the default method of using `tofu/terraform output`.\n\n**Disabling the feature:**\n\nYou can disable the dependency-fetch-output-from-state feature using the `--no-dependency-fetch-output-from-state` flag, even when the experiment is enabled:\n\n```bash\nterragrunt run --all --experiment-mode --no-dependency-fetch-output-from-state -- plan\n```\n\n#### `dependency-fetch-output-from-state` - How to provide feedback\n\nProvide your feedback in the dedicated [GitHub discussion](https://github.com/gruntwork-io/terragrunt/discussions/5200) page. When reporting issues or providing feedback, please include:\n\n- The backend type you're using\n- Any performance improvements you've observed\n- Any issues or edge cases you've encountered\n\n#### `dependency-fetch-output-from-state` - Criteria for stabilization\n\nTo transition the `dependency-fetch-output-from-state` feature to a stable release, the following must be addressed, at a minimum:\n\n- [ ] Add support for additional backends (e.g., GCS, etc.)\n- [ ] Comprehensive integration testing across different backend types\n- [ ] Performance benchmarking to validate speed improvements\n- [ ] Error handling and edge case testing\n- [ ] Documentation of supported backends and limitations\n- [ ] Community feedback on real-world usage\n\n## Completed Experiments\n\n- [cli-redesign](#cli-redesign)\n- [stacks](#stacks)\n- [runner-pool](#runner-pool)\n- [report](#report)\n- [auto-provider-cache-dir](#auto-provider-cache-dir)\n\n### `cli-redesign`\n\nSupport for the new Terragrunt CLI design.\n\n#### `cli-redesign` - What it does\n\nEnabled features from the CLI Redesign RFC.\n\nThis experiment flag is no longer needed, as the CLI Redesign is now the default.\n\n#### `cli-redesign` - How to provide feedback\n\nNow that the CLI Redesign experiment is complete, please provide feedback in the form of standard [GitHub issues](https://github.com/gruntwork-io/terragrunt/issues).\n\n#### `cli-redesign` - Criteria for stabilization\n\nTo transition `cli-redesign` features to a stable the following have been completed:\n\n- [x] Add support for `run` command.\n  - [x] Add support for basic usage of the `run` command (e.g., `terragrunt run plan`, `terragrunt run -- plan -no-color`).\n  - [x] Add support for the `--all` flag.\n  - [x] Add support for the `--graph` flag.\n- [x] Add support for `exec` command.\n- [x] Rename legacy `--terragrunt-` prefixed flags so that they no longer need the prefix.\n- [x] Add the `hcl` command, replacing commands like `hclfmt`, `hclvalidate` and `validate-inputs`.\n- [x] Add OpenTofu commands as explicit shortcuts in the CLI instead of forwarding all unknown commands to OpenTofu/Terraform.\n- [x] Add support for the `backend` command.\n- [x] Add support for the `render` command.\n- [x] Add support for the `info` command.\n- [x] Add support for the `dag` command.\n- [x] Add support for the `find` command.\n  - [x] Add support for `find` without flags.\n  - [x] Add support for `find` with colorful output.\n  - [x] Add support for `find` with `--format=json` flag.\n  - [x] Add support for `find` with stdout redirection detection.\n  - [x] Add support for `find` with `--hidden` flag.\n  - [x] Add support for `find` with `--sort=alpha` flag.\n  - [x] Add support for `find` with `--sort=dag` flag.\n  - [x] Add support for `find` with the `exclude` block used to exclude units from the search.\n  - [x] Add integration with `symlinks` experiment to support finding units/stacks via symlinks.\n  - [x] Add handling of broken configurations or configurations requiring authentication.\n  - [x] Add integration test for `find` with `--sort=dag` flag on all the fixtures in the `test/fixtures` directory.\n- [x] Add support for the `list` command.\n  - [x] Add support for `list` without flags.\n  - [x] Add support for `list` with colorful output.\n  - [x] Add support for `list` with `--format=tree` flag.\n  - [x] Add support for `list` with `--format=long` flag.\n  - [x] Add support for `list` with stdout redirection detection.\n  - [x] Add support for `list` with `--hidden` flag.\n  - [x] Add support for `list` with `--sort=alpha` flag.\n  - [x] Add support for `list` with `--sort=dag` flag.\n  - [x] Add support for `list` with `--group-by=fs` flag.\n  - [x] Add support for `list` with `--group-by=dag` flag.\n  - [x] Add support for `list` with the `exclude` block used to exclude units from the search.\n  - [x] Add integration with `symlinks` experiment to support listing units/stacks via symlinks.\n  - [x] Add handling of broken configurations or configurations requiring authentication.\n  - [x] Add integration test for `list` with `--sort=dag` flag on all the fixtures in the `test/fixtures` directory.\n\n### stacks\n\nSupport for Terragrunt stacks.\n\n#### What it does\n\nEnable `stack` command to manage Terragrunt stacks.\n\n#### stacks - Criteria for stabilization\n\nTo transition the `stacks` feature to a stable release, the following must be addressed:\n\n- [x] Add support for `stack run *` command\n- [x] Add support for `stack output` commands to extend stack-level operations.\n- [x] Integration testing for recursive stack handling across typical workflows, ensuring smooth transitions during `plan`, `apply`, and `destroy` operations.\n- [x] Confirm compatibility with parallelism flags (e.g., `--parallel`), especially for stacks with dependencies.\n- [x] Ensure that error handling and failure recovery strategies work as intended across large and nested stacks.\n\n### `runner-pool`\n\nProposes replacing Terragrunt's group-based execution with a dynamic runner pool that schedules Units as soon as dependencies are resolved.\nThis improves efficiency, reduces bottlenecks, and limits the impact of individual failures.\n\n#### `runner-pool` - What it does\n\nAllow usage of experimental runner pool implementation for units execution.\n\n#### `runner-pool` - How to provide feedback\n\nProvide your feedback on the [Runner Pool](https://github.com/gruntwork-io/terragrunt/issues/3629).\n\n#### `runner-pool` - Criteria for stabilization\n\nTo transition the `runner-pool` feature to a stable release, the following must be addressed:\n\n- [x] Use new discovery and queue packages to discover units.\n- [x] Add support for including/excluding external units in the discovery process.\n- [x] Add runner pool implementation to execute discovered units.\n- [x] Add integration tests to track that the runner pool works in the same way as the current implementation.\n- [x] Add performance tests to track that the runner pool implementation is faster than the current implementation.\n- [x] Add support for fail fast behavior in the runner pool.\n- [x] Improve the UI to queue to apply.\n- [x] Add OpenTelemetry support to the runner pool.\n\n### `report`\n\nSupport for Terragrunt Run Reports and Summaries.\n\n#### `report` - What it does\n\nAllows generation of run reports and summary displays. This experiment flag is no longer needed, as the report feature is now stable and available by default.\n\n#### `report` - How to provide feedback\n\nNow that the report experiment is complete, please provide feedback in the form of standard [GitHub issues](https://github.com/gruntwork-io/terragrunt/issues).\n\n#### `report` - Criteria for stabilization\n\nTo transition the `report` feature to stable, the following have been completed:\n\n- [x] Add support for generating reports (in CSV format by default).\n- [x] Add support for displaying summaries of runs.\n- [x] Add ability to disable summary display.\n- [x] Add support for generating reports in JSON format.\n- [x] Add comprehensive integration tests for the `report` experiment.\n- [x] Finalize the design of run summaries and reports.\n\n### `auto-provider-cache-dir`\n\nEnable native OpenTofu provider caching by setting `TF_PLUGIN_CACHE_DIR` instead of using Terragrunt's internal provider cache server.\n\n#### `auto-provider-cache-dir` - What it does\n\nThis experiment automatically configures OpenTofu to use its built-in provider caching mechanism by setting the `TF_PLUGIN_CACHE_DIR` environment variable. This approach leverages OpenTofu's native provider caching capabilities, which are more robust for concurrent operations in OpenTofu 1.10+.\n\nThis experiment flag is no longer needed, as the auto-provider-cache-dir feature is now enabled by default when using OpenTofu >= 1.10.\n\n**Requirements:**\n\n- OpenTofu version >= 1.10 is required\n- Only works when using OpenTofu (not Terraform)\n\n**Disabling the feature:**\n\nYou can disable the auto-provider-cache-dir feature using the `--no-auto-provider-cache-dir` flag:\n\n```bash\nterragrunt run --all apply --no-auto-provider-cache-dir\n```\n\n#### `auto-provider-cache-dir` - How to provide feedback\n\nNow that the auto-provider-cache-dir experiment is complete, please provide feedback in the form of standard [GitHub issues](https://github.com/gruntwork-io/terragrunt/issues).\n\n#### `auto-provider-cache-dir` - Criteria for stabilization\n\nTo transition the `auto-provider-cache-dir` feature to stable, the following have been completed:\n\n- [x] Comprehensive testing to confirm the safety of concurrent runs using the same provider cache directory.\n- [x] Performance comparison with the existing provider cache server approach.\n- [x] Documentation and examples of best practices for usage.\n- [x] Community feedback on real-world usage and any edge cases discovered.\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/05-supported-versions.mdx",
    "content": "---\ntitle: OpenTofu and Terraform Version Compatibility Table\ndescription: Learn which Terraform and OpenTofu versions are compatible with which versions of Terragrunt.\nslug: reference/supported-versions\nsidebar:\n  order: 5\n---\n\nimport CompatibilityTable from '@components/CompatibilityTable.astro';\n\n{/* Compatibility data source: src/data/compatibility/compatibility.json */}\n\n## Supported OpenTofu Versions\n\nThe officially supported versions are:\n\n<CompatibilityTable tool=\"opentofu\" />\n\n## Supported Terraform Versions\n\nThe officially supported versions are:\n\n<CompatibilityTable tool=\"terraform\" />\n\n**Note 1:** Terragrunt lists support for BSL versions of Terraform (>= 1.6.x) and core IaC functionality will work as expected.\nHowever, support for BSL Terraform-specific features is not guaranteed even if that version is in this table.\n\n**Note 2:** This table lists versions that are officially tested in the CI process. In practice, the version\ncompatibility is more relaxed than documented above. For example, we've found that Terraform 0.13 works with any version\nabove 0.19.0, and we've also found that terraform 0.11 works with any version above 0.19.18 as well.\n\nIf you wish to use Terragrunt against an untested Terraform version, you can use the\n[terraform_version_constraint](https://docs.terragrunt.com/reference/config-blocks-and-attributes/#terraform_version_constraint)\n(introduced in Terragrunt [v0.19.18](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.19.18)) attribute to\nrelax the version constraint.\n\n## Compatibility API\n\nVersion compatibility data is available via JSON API:\n\n| Endpoint | Description |\n|----------|-------------|\n| `/api/v1/compatibility` | All entries |\n| `/api/v1/compatibility/opentofu` | OpenTofu only |\n| `/api/v1/compatibility/terraform` | Terraform only |\n\n**Example:**\n\n```bash\ncurl https://docs.terragrunt.com/api/v1/compatibility/opentofu\n```\n\n**Response format:**\n\n```json\n[\n  {\n    \"tool\": \"opentofu\",\n    \"version\": \"1.11.x\",\n    \"terragrunt_min\": \"0.95.0\",\n    \"terragrunt_max\": null\n  }\n]\n```\n\n| Field            | Description                                                  |\n|------------------|--------------------------------------------------------------|\n| `tool`           | IaC tool name: `opentofu` or `terraform`                     |\n| `version`        | Tool version pattern (e.g., `1.11.x`)                        |\n| `terragrunt_min` | Minimum compatible Terragrunt version                        |\n| `terragrunt_max` | Maximum compatible version, or `null` for open-ended support |\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/06-lock-files.mdx",
    "content": "---\ntitle: Lock File Handling\ndescription: Learn how Terragrunt handles OpenTofu/Terraform lock files\nslug: reference/lock-files\nsidebar:\n  order: 6\n---\n\nimport FileTree from \"@components/vendored/starlight/FileTree.astro\";\n\n## How to use lock files with Terragrunt\n\nTo use [OpenTofu/Terraform lock files](https://opentofu.org/docs/language/files/dependency-lock/) with Terragrunt, you\nneed to:\n\n1. Run Terragrunt as usual (e.g., run `terragrunt plan`, `terragrunt apply`, etc.).\n1. Check the `.terraform.lock.hcl` file into version control.\n\nEverything else with OpenTofu/Terraform and Terragrunt should work as expected. To learn the details of how this works, read on!\n\n## How Terragrunt handles lock files\n\n### What's a lock file?\n\n[Terraform 0.14 added support for a\n_lock file_](https://www.hashicorp.com/blog/terraform-0-14-introduces-a-dependency-lock-file-for-providers)\nwhich gets created or updated every time you run `tofu init`/`terraform init`. The file is typically generated into your working\ndirectory (i.e., the folder in which you ran `tofu init`/`terraform init`) and is called `.terraform.lock.hcl`.\nIt captures the versions of all the OpenTofu/Terraform providers you're using. Normally, you want to check this file into\nversion control so that when your team members run OpenTofu/Terraform, they get the identical provider versions.\n\n### The problem with mixing remote OpenTofu/Terraform configurations in Terragrunt and lock files\n\nLet's say you are using Terragrunt with [remote OpenTofu/Terraform\nconfigurations](/features/units/) and you have the following folder\nstructure:\n\n<FileTree>\n\n- live\n  - prod\n    - vpc\n      - terragrunt.hcl\n  - stage\n    - vpc\n      - terragrunt.hcl\n\n</FileTree>\n\nImagine that in `live/stage/vpc/terragrunt.hcl`, you have the following contents:\n\n```hcl\n# live/stage/vpc/terragrunt.hcl\n\nterraform {\n  source = \"git::git@github.com:acme/infrastructure-modules.git//networking/vpc?ref=v0.0.1\"\n}\n```\n\nIf you ran `terragrunt apply` in the `/live/stage/vpc` folder, Terragrunt will:\n\n1. `git clone` the VPC module in the `source` URL into a temp folder in `.terragrunt-cache/xxx/vpc`, where `xxx` is\n   dynamically determined based on the URL.\n1. Run `tofu apply`/`terraform apply` in the `.terragrunt-cache/xxx/vpc` temp folder.\n\nAs a result, the `.terraform.lock.hcl` file will be generated in the `.terragrunt-cache/xxx/vpc` temp folder, rather\nthan in `/live/stage/vpc`.\n\n### How Terragrunt solves this problem\n\nTo solve this problem, since version v0.27.0, Terragrunt implements the following logic for lock files:\n\n1. If Terragrunt finds a `.terraform.lock.hcl` file in your working directory (e.g., in `/live/stage/vpc`), before\n   running OpenTofu/Terraform, Terragrunt will copy that lock file into the temp folder it uses when running your OpenTofu/Terraform code\n   (e.g., `.terragrunt-cache/xxx/vpc`). This way, if you had a lock file checked into version control, Terragrunt will\n   respect and use it with your OpenTofu/Terraform code as you'd expect.\n1. After running OpenTofu/Terraform, if Terragrunt finds a `.terraform.lock.hcl` in the temp folder (e.g.,\n   `.terragrunt-cache/xxx/vpc`), it will copy that lock file back to your working directory (e.g., to `/live/stage/vpc`).\n   That way, you can commit the lock file (or the changes to the lock file) to version control as usual.\n\n### Check the lock file in!\n\nAfter running Terragrunt on each of your modules, you should check your lock files in! That means your folder structure\nshould end up looking something like this:\n\n<FileTree>\n\n- live\n  - prod\n    - vpc\n      - .terraform.lock.hcl\n      - terragrunt.hcl\n  - stage\n    - vpc\n      - .terraform.lock.hcl\n      - terragrunt.hcl\n\n</FileTree>\n\nAlso, any time you change the providers you're using, and re-run `init`, the lock file will be updated, so make sure\nto check the updates into version control too.\n\n### Disabling the copy of the generated lock file\n\nIn certain use cases, like when using a remote module containing a lock file within it, you probably\ndon't want Terragrunt to also copy the lock file into your working directory. In these scenarios, you can opt out of copying\nthe `.terraform.lock.hcl` file by using `copy_terraform_lock_file = false` in the `terraform` configuration block as follows:\n\n```hcl\n# terragrunt.hcl\n\nterraform {\n  ...\n  copy_terraform_lock_file = false\n}\n```\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/07-logging/01-overview.md",
    "content": "---\ntitle: Overview\ndescription: Learn how Terragrunt decides what to log, and when.\nslug: reference/logging\nsidebar:\n  order: 1\n---\n\nTerragrunt logs messages as it runs to help you understand what it's doing. Given that Terragrunt is an IaC orchestrator, this can result in messages that are surprising if you don't understand what Terragrunt is doing behind the scenes.\n\n## Log Levels\n\nTo start with, Terragrunt has the following log levels:\n\n- `STDERR`\n- `STDOUT`\n- `ERROR`\n- `WARN`\n- `INFO`\n- `DEBUG`\n- `TRACE`\n\nThe `STDOUT` and `STDERR` log levels are non-standard, and exist due to Terragrunt's special responsibility as an IaC orchestrator.\n\nFor the most part, whenever you use Terragrunt to run something using another tool (like OpenTofu or Terraform), Terragrunt will capture the stdout and stderr terminal output from that tool, enrich it with additional information, then _log_ it as `STDOUT` or `STDERR` respectively.\n\nThe exception to this is when Terragrunt is running a process in \"Headless Mode\", where it will instead emit stdout and stderr terminal output to `INFO` and `ERROR` log levels respectively.\n\nAll other log levels are standard, and are used by Terragrunt to log its own messages.\n\nFor example:\n\n```bash\n$ terragrunt --log-level debug plan\n14:20:38.431 DEBUG  Terragrunt Version: 0.0.0\n14:20:38.431 DEBUG  Did not find any locals block: skipping evaluation.\n14:20:38.431 DEBUG  Running command: tofu --version\n14:20:38.431 DEBUG  Engine is not enabled, running command directly in .\n14:20:38.451 DEBUG  tofu version: 1.8.5\n14:20:38.451 DEBUG  Reading Terragrunt config file at ./terragrunt.hcl\n14:20:38.451 DEBUG  Did not find any locals block: skipping evaluation.\n14:20:38.451 DEBUG  Did not find any locals block: skipping evaluation.\n14:20:38.452 DEBUG  Running command: tofu init\n14:20:38.452 DEBUG  Engine is not enabled, running command directly in .\n14:20:38.469 INFO   tofu: Initializing the backend...\n14:20:38.470 INFO   tofu: Initializing provider plugins...\n14:20:38.470 INFO   tofu: OpenTofu has been successfully initialized!\n14:20:38.470 INFO   tofu:\n14:20:38.470 INFO   tofu: You may now begin working with OpenTofu. Try running \"tofu plan\" to see\n14:20:38.470 INFO   tofu: any changes that are required for your infrastructure. All OpenTofu commands\n14:20:38.470 INFO   tofu: should now work.\n14:20:38.470 INFO   tofu: If you ever set or change modules or backend configuration for OpenTofu,\n14:20:38.470 INFO   tofu: rerun this command to reinitialize your working directory. If you forget, other\n14:20:38.470 INFO   tofu: commands will detect it and remind you to do so if necessary.\n14:20:38.470 DEBUG  Running command: tofu plan\n14:20:38.470 DEBUG  Engine is not enabled, running command directly in .\n14:20:38.490 STDOUT tofu: No changes. Your infrastructure matches the configuration.\n14:20:38.490 STDOUT tofu: OpenTofu has compared your real infrastructure against your configuration and\n14:20:38.490 STDOUT tofu: found no differences, so no changes are needed.\n```\n\nHere, we have three types of log messages:\n\n1. `DEBUG` messages from Terragrunt itself. By default, Terragrunt's log level is `INFO`, but we've set it to `DEBUG` using the `--log-level` flag.\n2. `STDOUT` messages from OpenTofu. These are messages that OpenTofu would normally print directly to the terminal, but instead, Terragrunt captures them and logs them as `STDOUT` log messages, along with timestamps and other metadata.\n3. `INFO` messages from Terragrunt [auto-init](/features/units/auto-init). These were initially emitted by OpenTofu. However, the user did not specifically ask for them, so Terragrunt logs them as `INFO` messages.\n\n## Enrichment\n\nThe reason Terragrunt enriches stdout/stderr from the processes is that it is often very useful to have this extra metadata.\n\nFor example:\n\n```bash\n$ terragrunt run --all plan\n14:27:45.359 INFO   The stack at . will be processed in the following order for command plan:\nGroup 1\n- Module ./unit-1\n- Module ./unit-2\n\n\n14:27:45.399 INFO   [unit-2] tofu: Initializing the backend...\n14:27:45.399 INFO   [unit-1] tofu: Initializing the backend...\n14:27:45.400 INFO   [unit-2] tofu: Initializing provider plugins...\n14:27:45.400 INFO   [unit-2] tofu: OpenTofu has been successfully initialized!\n14:27:45.400 INFO   [unit-2] tofu:\n14:27:45.400 INFO   [unit-2] tofu: You may now begin working with OpenTofu. Try running \"tofu plan\" to see\n14:27:45.400 INFO   [unit-2] tofu: any changes that are required for your infrastructure. All OpenTofu commands\n14:27:45.400 INFO   [unit-2] tofu: should now work.\n14:27:45.400 INFO   [unit-2] tofu: If you ever set or change modules or backend configuration for OpenTofu,\n14:27:45.400 INFO   [unit-2] tofu: rerun this command to reinitialize your working directory. If you forget, other\n14:27:45.400 INFO   [unit-2] tofu: commands will detect it and remind you to do so if necessary.\n14:27:45.400 INFO   [unit-1] tofu: Initializing provider plugins...\n14:27:45.400 INFO   [unit-1] tofu: OpenTofu has been successfully initialized!\n14:27:45.400 INFO   [unit-1] tofu:\n14:27:45.400 INFO   [unit-1] tofu: You may now begin working with OpenTofu. Try running \"tofu plan\" to see\n14:27:45.400 INFO   [unit-1] tofu: any changes that are required for your infrastructure. All OpenTofu commands\n14:27:45.400 INFO   [unit-1] tofu: should now work.\n14:27:45.400 INFO   [unit-1] tofu: If you ever set or change modules or backend configuration for OpenTofu,\n14:27:45.400 INFO   [unit-1] tofu: rerun this command to reinitialize your working directory. If you forget, other\n14:27:45.400 INFO   [unit-1] tofu: commands will detect it and remind you to do so if necessary.\n14:27:45.422 STDOUT [unit-2] tofu: No changes. Your infrastructure matches the configuration.\n14:27:45.423 STDOUT [unit-2] tofu: OpenTofu has compared your real infrastructure against your configuration and\n14:27:45.423 STDOUT [unit-2] tofu: found no differences, so no changes are needed.\n14:27:45.423 STDOUT [unit-1] tofu: No changes. Your infrastructure matches the configuration.\n14:27:45.423 STDOUT [unit-1] tofu: OpenTofu has compared your real infrastructure against your configuration and\n14:27:45.423 STDOUT [unit-1] tofu: found no differences, so no changes are needed.\n```\n\nHere you see two different units being run by Terragrunt concurrently, and stdout/stderr for each being emitted in real time. This is really helpful when managing IaC at scale, as it lets you know exactly what each unit in your stack is doing, and how long it is taking.\n\nIt's easier to see the impact of this enrichment if we turn it off, so let's use the [bare](/reference/logging/formatting#bare) preset described in [Log Formatting](/reference/logging/formatting).\n\n```bash\n$ terragrunt run --all --log-format bare plan\nINFO[0000] The stack at /Users/yousif/tmp/testing-stdout-stderr-split will be processed in the following order for command plan:\nGroup 1\n- Module /Users/yousif/tmp/testing-stdout-stderr-split/unit-1\n- Module /Users/yousif/tmp/testing-stdout-stderr-split/unit-2\n\n\n\nInitializing the backend...\n\nInitializing provider plugins...\n\nOpenTofu has been successfully initialized!\n\nYou may now begin working with OpenTofu. Try running \"tofu plan\" to see\nany changes that are required for your infrastructure. All OpenTofu commands\nshould now work.\n\nIf you ever set or change modules or backend configuration for OpenTofu,\nrerun this command to reinitialize your working directory. If you forget, other\ncommands will detect it and remind you to do so if necessary.\n\nNo changes. Your infrastructure matches the configuration.\n\nOpenTofu has compared your real infrastructure against your configuration and\nfound no differences, so no changes are needed.\n\nInitializing the backend...\n\nInitializing provider plugins...\n\nOpenTofu has been successfully initialized!\n\nYou may now begin working with OpenTofu. Try running \"tofu plan\" to see\nany changes that are required for your infrastructure. All OpenTofu commands\nshould now work.\n\nIf you ever set or change modules or backend configuration for OpenTofu,\nrerun this command to reinitialize your working directory. If you forget, other\ncommands will detect it and remind you to do so if necessary.\n\nNo changes. Your infrastructure matches the configuration.\n\nOpenTofu has compared your real infrastructure against your configuration and\nfound no differences, so no changes are needed.\n```\n\nThis tells Terragrunt to log messages from OpenTofu/Terraform without any enrichment. As you can see, it's not as easy to disambiguate the messages from the two units, so it helps to use Terragrunt's default log format when managing IaC at scale.\n\n## Exceptions to enrichment\n\nThere are exceptions to the general rule that Terragrunt logs stdout/stderr from the processes it runs as `STDOUT` and `STDERR` respectively when not in Headless Mode.\n\nBecause Terragrunt is an IaC orchestrator, it uses its awareness of OpenTofu/Terraform usage to recognize certain circumstances when a user is likely to want stdout/stderr to be emitted exactly as it would when running a process directly.\n\nAn example of this is `terragrunt output`:\n\n```bash\n$ terragrunt output -json\n15:20:07.759 INFO   tofu: Initializing the backend...\n15:20:07.759 INFO   tofu: Initializing provider plugins...\n15:20:07.759 INFO   tofu: OpenTofu has been successfully initialized!\n15:20:07.759 INFO   tofu:\n15:20:07.759 INFO   tofu: You may now begin working with OpenTofu. Try running \"tofu plan\" to see\n15:20:07.759 INFO   tofu: any changes that are required for your infrastructure. All OpenTofu commands\n15:20:07.759 INFO   tofu: should now work.\n15:20:07.759 INFO   tofu: If you ever set or change modules or backend configuration for OpenTofu,\n15:20:07.759 INFO   tofu: rerun this command to reinitialize your working directory. If you forget, other\n15:20:07.759 INFO   tofu: commands will detect it and remind you to do so if necessary.\n{\n  \"something\": {\n    \"sensitive\": false,\n    \"type\": \"string\",\n    \"value\": \"Hello, World!\"\n  },\n  \"something_else\": {\n    \"sensitive\": false,\n    \"type\": \"string\",\n    \"value\": \"Goodbye, World!\"\n  }\n}\n```\n\nAs you can see, the output from OpenTofu here isn't being enriched, even though the user explicitly asked Terragrunt to run `output -json`.\n\nThis is because the user is pretty likely to want to programmatically interact with the output of `output`, and so Terragrunt doesn't enrich it.\n\nIf, for example, you wanted to use a tool like `jq` to parse the output of `terragrunt output -json`, you could do that without having to worry about Terragrunt's metadata getting in the way, or disabling anything with an extra flag.\n\n```bash\n$ terragrunt output -json | jq '.something'\n15:24:40.310 INFO   tofu: Initializing the backend...\n15:24:40.311 INFO   tofu: Initializing provider plugins...\n15:24:40.311 INFO   tofu: OpenTofu has been successfully initialized!\n15:24:40.311 INFO   tofu:\n15:24:40.311 INFO   tofu: You may now begin working with OpenTofu. Try running \"tofu plan\" to see\n15:24:40.311 INFO   tofu: any changes that are required for your infrastructure. All OpenTofu commands\n15:24:40.311 INFO   tofu: should now work.\n15:24:40.311 INFO   tofu: If you ever set or change modules or backend configuration for OpenTofu,\n15:24:40.311 INFO   tofu: rerun this command to reinitialize your working directory. If you forget, other\n15:24:40.311 INFO   tofu: commands will detect it and remind you to do so if necessary.\n{\n  \"sensitive\": false,\n  \"type\": \"string\",\n  \"value\": \"Hello, World!\"\n}\n```\n\n## Streaming and buffering\n\nWhile Terragrunt logs stdout from OpenTofu/Terraform in real time, it buffers each line of stdout before logging it. This is because Terragrunt needs to be able to buffer stdout to prevent different units from interleaving their log messages.\n\nDepending on what you're doing with Terragrunt, this might occasionally result in issues when multiple units are running concurrently, and they are each producing multi-line output that is more convenient to be read independently. In these cases, you can do some post-processing on the logs to read the units in isolation.\n\nFor example:\n\n```bash\n$ terragrunt run --all apply --no-color --non-interactive > logs\n16:01:51.164 INFO   The stack at . will be processed in the following order for command apply:\nGroup 1\n- Module ./unit1\n- Module ./unit2\n\n```\n\n```bash\n$ grep '\\[unit1\\]' < logs\n16:01:51.272 STDOUT [unit1] tofu: null_resource.empty: Refreshing state... [id=3335573617542340690]\n16:01:51.279 STDOUT [unit1] tofu: OpenTofu used the selected providers to generate the following execution\n16:01:51.279 STDOUT [unit1] tofu: plan. Resource actions are indicated with the following symbols:\n16:01:51.279 STDOUT [unit1] tofu: -/+ destroy and then create replacement\n16:01:51.279 STDOUT [unit1] tofu: OpenTofu will perform the following actions:\n16:01:51.279 STDOUT [unit1] tofu:   # null_resource.empty must be replaced\n16:01:51.279 STDOUT [unit1] tofu: -/+ resource \"null_resource\" \"empty\" {\n16:01:51.279 STDOUT [unit1] tofu:       ~ id       = \"3335573617542340690\" -> (known after apply)\n16:01:51.279 STDOUT [unit1] tofu:       ~ triggers = { # forces replacement\n16:01:51.280 STDOUT [unit1] tofu:           ~ \"always_run\" = \"2025-01-09T21:01:17Z\" -> (known after apply)\n16:01:51.280 STDOUT [unit1] tofu:         }\n16:01:51.280 STDOUT [unit1] tofu:     }\n16:01:51.280 STDOUT [unit1] tofu: Plan: 1 to add, 0 to change, 1 to destroy.\n16:01:51.280 STDOUT [unit1] tofu:\n16:01:51.297 STDOUT [unit1] tofu: null_resource.empty: Destroying... [id=3335573617542340690]\n16:01:51.297 STDOUT [unit1] tofu: null_resource.empty: Destruction complete after 0s\n16:01:51.300 STDOUT [unit1] tofu: null_resource.empty: Creating...\n16:01:51.301 STDOUT [unit1] tofu: null_resource.empty: Provisioning with 'local-exec'...\n16:01:51.301 STDOUT [unit1] tofu: null_resource.empty (local-exec): Executing: [\"/bin/sh\" \"-c\" \"echo 'sleeping...'; sleep 1; echo 'done sleeping'\"]\n16:01:51.304 STDOUT [unit1] tofu: null_resource.empty (local-exec): sleeping...\n16:01:52.311 STDOUT [unit1] tofu: null_resource.empty (local-exec): done sleeping\n16:01:52.312 STDOUT [unit1] tofu: null_resource.empty: Creation complete after 1s [id=4749136145104485309]\n16:01:52.322 STDOUT [unit1] tofu:\n16:01:52.322 STDOUT [unit1] tofu: Apply complete! Resources: 1 added, 0 changed, 1 destroyed.\n16:01:52.322 STDOUT [unit1] tofu:\n```\n\n```bash\n$ grep '\\[unit2\\]' < logs\n16:01:51.273 STDOUT [unit2] tofu: null_resource.empty: Refreshing state... [id=7532622543468447677]\n16:01:51.280 STDOUT [unit2] tofu: OpenTofu used the selected providers to generate the following execution\n16:01:51.280 STDOUT [unit2] tofu: plan. Resource actions are indicated with the following symbols:\n16:01:51.280 STDOUT [unit2] tofu: -/+ destroy and then create replacement\n16:01:51.280 STDOUT [unit2] tofu: OpenTofu will perform the following actions:\n16:01:51.280 STDOUT [unit2] tofu:   # null_resource.empty must be replaced\n16:01:51.280 STDOUT [unit2] tofu: -/+ resource \"null_resource\" \"empty\" {\n16:01:51.280 STDOUT [unit2] tofu:       ~ id       = \"7532622543468447677\" -> (known after apply)\n16:01:51.280 STDOUT [unit2] tofu:       ~ triggers = { # forces replacement\n16:01:51.280 STDOUT [unit2] tofu:           ~ \"always_run\" = \"2025-01-09T21:01:17Z\" -> (known after apply)\n16:01:51.280 STDOUT [unit2] tofu:         }\n16:01:51.280 STDOUT [unit2] tofu:     }\n16:01:51.280 STDOUT [unit2] tofu: Plan: 1 to add, 0 to change, 1 to destroy.\n16:01:51.280 STDOUT [unit2] tofu:\n16:01:51.297 STDOUT [unit2] tofu: null_resource.empty: Destroying... [id=7532622543468447677]\n16:01:51.297 STDOUT [unit2] tofu: null_resource.empty: Destruction complete after 0s\n16:01:51.300 STDOUT [unit2] tofu: null_resource.empty: Creating...\n16:01:51.301 STDOUT [unit2] tofu: null_resource.empty: Provisioning with 'local-exec'...\n16:01:51.301 STDOUT [unit2] tofu: null_resource.empty (local-exec): Executing: [\"/bin/sh\" \"-c\" \"echo 'sleeping...'; sleep 1; echo 'done sleeping'\"]\n16:01:51.303 STDOUT [unit2] tofu: null_resource.empty (local-exec): sleeping...\n16:01:52.311 STDOUT [unit2] tofu: null_resource.empty (local-exec): done sleeping\n16:01:52.312 STDOUT [unit2] tofu: null_resource.empty: Creation complete after 1s [id=6569505210291935319]\n16:01:52.322 STDOUT [unit2] tofu:\n16:01:52.322 STDOUT [unit2] tofu: Apply complete! Resources: 1 added, 0 changed, 1 destroyed.\n16:01:52.322 STDOUT [unit2] tofu:\n```\n\n## Disabling logs\n\nFinally, you can also disable logs entirely, like so:\n\n```bash\n$ terragrunt --log-disable plan\n\nInitializing the backend...\n\nInitializing provider plugins...\n\nOpenTofu has been successfully initialized!\n\nYou may now begin working with OpenTofu. Try running \"tofu plan\" to see\nany changes that are required for your infrastructure. All OpenTofu commands\nshould now work.\n\nIf you ever set or change modules or backend configuration for OpenTofu,\nrerun this command to reinitialize your working directory. If you forget, other\ncommands will detect it and remind you to do so if necessary.\n\nNo changes. Your infrastructure matches the configuration.\n\nOpenTofu has compared your real infrastructure against your configuration and\nfound no differences, so no changes are needed.\n```\n\nThis will give you the closest experience to using OpenTofu/Terraform directly, with Terragrunt doing all of its work in the background.\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/07-logging/02-formatting.md",
    "content": "---\ntitle: Formatting\ndescription: Learn how to customize Terragrunt logging.\nslug: reference/logging/formatting\nsidebar:\n  order: 2\n---\n\nUsing the `--log-custom-format <format>` flag you can customize the way Terragrunt logs with total control over the logging format.\n\nThe argument passed to this flag is a Terragrunt native format string that has special syntax, as described below.\n\n## Placeholders\n\nThe format string consists of placeholders and text. Placeholders start with the `%` sign.\n\ne.g.\n\n```shell\n--log-custom-format \"%time %level %msg\"\n```\n\nOutput:\n\n```shell\n10:09:19.809 debug Running command: tofu --version\n```\n\nTo escape the `%` character, use `%%`.\n\ne.g.\n\n```shell\n--log-custom-format \"%time %level %%msg\"\n```\n\nOutput:\n\n```shell\n10:09:19.809 debug %msg\n```\n\nPlaceholders have preset names:\n\n* `%time` - Current time.\n\n* `%interval` - Seconds elapsed since Terragrunt started.\n\n* `%level` - Log level.\n\n* `%prefix` - Path to the working directory were Terragrunt is running.\n\n* `%msg` - Log message.\n\n* `%tf-path` - Path to the OpenTofu/Terraform executable (as defined by [tf-path](https://docs.terragrunt.com/reference/cli-options/#tf-path)).\n\n* `%tf-command` - Executed OpenTofu/Terraform command, e.g. `apply`.\n\n* `%tf-command-args` - Arguments of the executed OpenTofu/Terraform command, e.g. `apply -auto-approve`.\n\n* `%t` - Indent.\n\n* `%n` - Newline.\n\nAny other text is considered plain text. The parser always tries to find the longest name. For example, tofu command \"apply -auto-approve\" with format \"%tf-command-args\" will be replaced with \"apply -auto-approve\", but not \"apply-args\". If you need to replace it with \"apply-args\", use empty brackets \"%tf-command()-args\". More examples: \"%tf-path\" will be replaced with \"tofu\", `%t()-path` will be replaced with \"   -path\".\n\ne.g.\n\n```shell\n--log-custom-format \"time=%time level=%level message=%msg\"\n```\n\nOutput:\n\n```shell\ntime=00:10:44.716 level=debug message=Running command: tofu --version\n```\n\nUsing the placeholder as shown above will display the value simply. If you would like to format the value, you can pass options to the placeholder.\n\nPlaceholder formatting uses the following syntax:\n\n`%placeholder-name(option-name=option-value, option-name=option-value,...)`\n\ne.g.\n\n```shell\n--log-custom-format \"%time(format='Y-m-d H:i:sv') %level(format=short,case=upper) %msg\"\n```\n\nOutput:\n\n```shell\n2024-11-12 11:52:20.214 DEB Running command: tofu --version\n```\n\nIn this example, the timestamp (as referenced by the `%time` placeholder) has been formatted with the `format` string `Y-m-d H:i:sv`. Similarly, the log level (as referenced by the `%level` placeholder), has been formatted to use the `short` `format`, and `upper` `case`.\n\nEven if you don't pass options, the empty parenthesis are added implicitly. Thus `%time` is equivalent to `%time()`. Parenthesis are considered part of the syntax for specifying  parameters to placeholders by default. Any parenthesis following a placeholder will be interpreted as specifying the parameters for the placeholder function.\n\ne.g.\n\n```shell\n--log-custom-format \"%time(plain-text)\"\n```\n\nOutput:\n\n```shell\ninvalid option name \"plain-text\" for placeholder \"time\"\n```\n\nIf you would like to escape parentheses so that they appear as plain text in logs, make sure to use empty parentheses after a placeholder so that the following parentheses are not evaluated as specifying parameters for the placeholder function.\n\ne.g.\n\n```shell\n--log-custom-format \"%time()(plain-text)\"\n```\n\nOutput:\n\n```shell\n12:33:08.513(plain-text)\n```\n\nYou can format plain text as well by using an unnamed placeholder.\n\ne.g.\n\n```shell\n--log-custom-format \"%(content='time=',color=magenta)%time %(content='level=',color=light-blue)%level %(content='msg=',color=green)%msg\"\n```\n\nOutput:\n\n```shell\ntime=12:33:08.513 level=debug msg=Running command: tofu --version\n```\n\n*Unfortunately, it is not possible to display color in a Markdown document, but in the above output, `time=` is colored magenta, `level=` is colored light blue and `msg=` is colored green.*\n\n![screenshot](../../../../assets/img/screenshots/custom-log-format-1.jpg)\n\n## Options\n\nOptions can be divided into common ones, which can be passed to any placeholder, and specific ones for each placeholder.\n\nCommon options:\n\n* `content=<text>` - Sets a placeholder value, typically used to set the initial value of an unnamed placeholder.\n\n* `case=[upper|lower|capitalize]` - Sets the case of the text.\n\n* `width=<number>` - Sets the column width.\n\n* `align=[left|center|right]` - Aligns content relative to the edges of the column, used in conjunction with `width`.\n\n* `prefix=<text>` - Prepends the prefix to the content. If the content of the placeholder is empty, the prefix will not be prepended.\n\n* `suffix=<text>`-  Appends the suffix to the content. If the content of the placeholder is empty, the suffix will not be appended.\n\n* `escape=[json]` - Escapes content for use as a value in a JSON string.\n\n* `color=[red|white|yellow|green|cayn|magenta|blue|...]` - Sets the color for the content.\n\n  * `1..255` - Specifies a color using a [number](https://www.hackitu.de/termcolor256/), 1 to 255\n\n  * `red|white|yellow|green|cyan|magenta|blue|light-blue|light-black|light-red|light-green|light-yellow|light-magenta|light-cyan|light-white` - Specifies a color using a word\n\n  * `gradient` - Specifies to use a different color each time the placeholder content changes.\n\n  * `preset` - Specifies to use preset colors. For example, each log level name has its own preset color.\n\n  * `disable` - Disables color, also removes colors set in terraform/tofu output.\n\nSpecific options for placeholders:\n\n* `%level`\n\n  * `format=[full|short|tiny]` - Specifies the format for log level names.\n\n    * `full` - `stdout`, `stderr`, `error`, `warn`, `info`, `debug`, `trace`\n\n    * `short` - `std`, `err`, `wrn`, `inf`, `deb`, `trc`\n\n    * `tiny` - `s`, `e`, `w`, `i`, `d`, `t`\n\n* `%time`\n\n  * `format=<time-format>` - Sets the time format.\n\n    Preset formats:\n\n    * `date-time` - e.g. `2006-01-02 15:04:05`\n\n    * `date-only` - e.g. `2006-01-02`\n\n    * `time-only` - e.g. `15:04:05`\n\n    * `rfc3339` - e.g. `2006-01-02T15:04:05Z07:00`\n\n    * `rfc3339-nano` - e.g. `2006-01-02T15:04:05.999999999Z07:00`\n\n    Custom format string characters:\n\n    * `H` - 24-hour format of an hour with leading zeros, `00` to `23`\n\n    * `h` - 12-hour format of an hour with leading zeros, `01` to `12`\n\n    * `g` - 12-hour format of an hour without leading zeros, `1` to `12`\n\n    * `i` - Minutes with leading zeros, `00` to `59`\n\n    * `s` - Seconds with leading zeros, `00` to `59`\n\n    * `v` - Milliseconds. e.g. `.654`\n\n    * `u` - Microseconds, e.g. `.654321`\n\n    * `Y` - A full numeric representation of a year, e.g. `1999`, `2003`\n\n    * `y` - A two digit representation of a year, e.g. `99`, `03`\n\n    * `m` - Numeric representation of a month, with leading zeros, `01` to `12`\n\n    * `n` - Numeric representation of a month, without leading zeros, `1` to `12`\n\n    * `M` - A short textual representation of a month, three letters, `Jan` to `Dec`\n\n    * `d` - Day of the month, 2 digits with leading zeros, `01` to `31`\n\n    * `j` - Day of the month without leading zeros, `1` to `31`\n\n    * `D` - A textual representation of a day, three letters, `Mon` to `Sun`\n\n    * `A` - Uppercase Ante meridiem and Post meridiem, `AM` or `PM`\n\n    * `a` - Lowercase Ante meridiem and Post meridiem, `am` or `pm`\n\n    * `T` - Timezone abbreviation, e.g. `EST`, `MDT`\n\n    * `P` - Difference to Greenwich time (GMT) with colon between hours and minutes, e.g. `+02:00`\n\n    * `O` - Difference to Greenwich time (GMT) without colon between hours and minutes, e.g. `+0200`\n\n* `%prefix`\n\n  * `path=[relative|short-relative|short]`\n\n    * `relative` - Outputs a relative path to the working directory.\n\n    * `short-relative` - Outputs a relative path to the working directory, trims the leading slash `./` and hides the working directory path `.`\n\n    * `short` - Outputs an absolute path, but hides the working directory path.\n\n* `%tf-path`\n\n  * `path=[filename|dir]`\n\n    * `filename` - Outputs the name of the executable.\n\n    * `dir` - Outputs the directory name of the executable.\n\n* `%msg`\n\n  * `path=[relative]`\n\n    * `relative` - Converts all absolute paths to paths relative to the working directory.\n\n## Presets\n\nThe examples below replicate the preset formats specified with `--log-format`. They can be useful if you need to change existing formats to suit your needs.\n\n### Pretty\n\n`--log-format pretty`\n\n```shell\n--log-custom-format \"%time(color=light-black) %level(case=upper,width=6,color=preset) %prefix(path=short-relative,color=gradient,suffix=' ')%tf-path(color=cyan,suffix=': ')%msg(path=relative)\"\n```\n\n### Bare\n\n`--log-format bare`\n\n```shell\n--tf-forward-stdout --log-custom-format \"%level(case=upper,width=4)[%interval] %msg %prefix(path=short,prefix='prefix=[',suffix=']')\"\n```\n\n### Key-value\n\n`--log-format key-value`\n\n```shell\n--log-custom-format \"time=%time(format=rfc3339) level=%level prefix=%prefix(path=short-relative) tf-path=%tf-path(path=filename) msg=%msg(path=relative,color=disable)\"\n```\n\n### JSON\n\n`--log-format json`\n\n```shell\n--log-custom-format '{\"time\":\"%time(format=rfc3339,escape=json)\", \"level\":\"%level(escape=json)\", \"prefix\":\"%prefix(path=short-relative,escape=json)\", \"tf-path\":\"%tf-path(path=filename,escape=json)\", \"msg\":\"%msg(path=relative,escape=json,color=disable)\"}'\n```\n"
  },
  {
    "path": "docs/src/content/docs/04-reference/08-terragrunt-cache.md",
    "content": "---\ntitle: Terragrunt Cache\ndescription: Learn what the `.terragrunt-cache` directory is and how to manage it.\nslug: reference/terragrunt-cache\nsidebar:\n  order: 10\n---\n\nTerragrunt uses a cache directory (`.terragrunt-cache`) to store downloaded modules when using the `source` attribute in the `terraform` block.\n\nThis cache directory is created whenever Terragrunt downloads a module from a remote source, and where it runs the OpenTofu/Terraform commands. It also stores any modules and providers that are downloaded as part of these commands by default.\n\n## Clearing the Terragrunt cache\n\nTerragrunt creates a `.terragrunt-cache` folder in the current working directory as its scratch directory. It downloads your remote OpenTofu/Terraform configurations into this folder, runs your OpenTofu/Terraform commands in this folder, and any modules and providers those commands download also get stored in this folder. You can safely delete this folder any time, and Terragrunt will recreate it as necessary.\n\nIf you need to clean up a lot of these folders (e.g., after `terragrunt run --all apply`), you can use the following commands on Mac and Linux:\n\nRecursively find all the `.terragrunt-cache` folders that are children of the current folder:\n\n``` bash\nfind . -type d -name \".terragrunt-cache\"\n```\n\nIf you are **SURE** you want to delete all the folders that come up in the previous command, you can recursively delete all of them as follows:\n\n``` bash\nfind . -type d -name \".terragrunt-cache\" -prune -exec rm -rf {} \\;\n```\n\nAlso consider setting the `TG_DOWNLOAD_DIR` environment variable if you wish to place the cache directories somewhere else.\n\nIf the reason you are clearing out your Terragrunt cache is that you are struggling with running out of disk space, consider using the [Provider Cache](/features/caching/provider-cache-server) feature to store OpenTofu/Terraform provider plugins in a shared location, as those are typically the largest files stored in the `.terragrunt-cache` directory.\n"
  },
  {
    "path": "docs/src/content/docs/05-community/01-contributing.mdx",
    "content": "---\ntitle: Contributing\ndescription: Contributing to Terragrunt\nslug: community/contributing\nsidebar:\n  order: 1\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n## Contribution Guidelines\n\nContributions to Terragrunt are very welcome! We follow a fairly standard [pull request\nprocess](https://help.github.com/articles/about-pull-requests/) for contributions, subject to the following guidelines:\n\n### File a GitHub issue or write an RFC\n\nBefore starting any work, we recommend filing a GitHub issue in this repo. This is your chance to ask questions and\nget feedback from the maintainers and the community before you sink a lot of time into writing (possibly the wrong)\ncode. If there is anything you're unsure about, just ask!\n\nSometimes, the scope of the feature proposal is large enough that it requires major updates to the code base to\nimplement. In these situations, a maintainer may suggest writing up an RFC that describes the feature in more details\nthan what can be reasonably captured in an enhancement.\n\nTo write an RFC, click the [RFC](https://github.com/gruntwork-io/terragrunt/issues/new?assignees=&labels=rfc%2Cpending-decision&projects=&template=02-rfc.yml) button in the issues tab.\n\nThis will present you a template you can fill out to describe the feature you want to propose.\n\n### Update the documentation\n\nWe recommend updating the documentation _before_ updating any code (see [Readme Driven\nDevelopment](http://tom.preston-werner.com/2010/08/23/readme-driven-development.html)). This ensures the documentation\nstays up to date and allows you to think through the problem at a high level before you get lost in the weeds of\ncoding.\n\nThe documentation is built with [Starlight](https://github.com/withastro/starlight) and\nhosted on [Vercel](https://vercel.com/) from the `docs` folder on `main` branch. Read this\n[README.md](https://github.com/gruntwork-io/terragrunt/tree/main/docs#terragrunt-docs-rewrite) to\nlearn more about making updates to the docs.\n\n### Update the tests\n\nWe also recommend updating the automated tests _before_ updating any code (see [Test Driven\nDevelopment](https://en.wikipedia.org/wiki/Test-driven_development)). That means you add or update a test case,\nverify that it's failing with a clear error message, and _then_ make the code changes to get that test to pass. This\nensures the tests stay up to date and verify all the functionality in this Module, including whatever new\nfunctionality you're adding in your contribution. Check out [Developing Terragrunt](#developing-terragrunt)\nfor instructions on running the automated tests.\n\n### Update the code\n\nAt this point, make your code changes and use your new test case to verify that everything is working. Check out\n[Developing Terragrunt](#developing-terragrunt) for instructions on how to build and run Terragrunt locally.\n\nWe have a [style guide](https://gruntwork.io/guides/style%20guides/golang-style-guide/) for the Go programming language,\nin which we documented some best practices for writing Go code. Please ensure your code adheres to the guidelines\noutlined in the guide.\n\n### Create a pull request\n\n[Create a pull request](https://help.github.com/articles/creating-a-pull-request/) with your changes. Please make sure\nto include the following:\n\n1. A description of the change, including a link to your GitHub issue.\n1. The output of your automated test run, preferably in a [GitHub Gist](https://gist.github.com/).\n   We cannot run automated tests for pull requests automatically due to\n   [security concerns](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#using-secrets-in-a-workflow),\n   so we need you to manually provide this test output so we can verify that everything is working.\n1. Any notes on backwards incompatibility.\n\n### Merge and release\n\nThe maintainers for this repo will review your code and provide feedback. If everything looks good, they will merge the\ncode and release a new version, which you'll be able to find in the [releases page](https://github.com/gruntwork-io/terragrunt/releases).\n\n## Developing Terragrunt\n\n### Running locally\n\nTo run Terragrunt locally, use the `go run` command:\n\n```bash\ngo run main.go plan\n```\n\n### Testing on Windows\n\nRunning tests on Windows is currently limited. Not all tests pass reliably, and additional configuration is required for proper functionality.\nSpecifically:\n\n- Long file paths must be enabled on Windows.\n- A Bash shell (e.g., via Git Bash or WSL) must be available in the environment.\n\nFor setup instructions and requirements, `.github/scripts/windows-setup.ps1`.\n\n\n### Dependencies\n\nTerragrunt uses go modules (read more about the modules system in [the official\nwiki](https://github.com/golang/go/wiki/Modules)). This means that dependencies are automatically installed when you use\nany go command that compiles the code (`build`, `run`, `test`, etc.).\n\n### Linting\n\nTerragrunt uses [golangci-lint](https://golangci-lint.run/) to lint the golang code in the codebase. This is a helpful form of static analysis that can catch common bugs and issues related to performance, style and maintainability.\n\nWe use the linter as a guide to learn about how we can improve the Terragrunt codebase. We do not enforce 100% compliance with the linter. If you believe that an error thrown by the linter is irrelevant, use the documentation on [false-positives](https://golangci-lint.run/usage/false-positives/) to suppress that error, along with an explanation of why you believe the error is a false positive.\n\nIf you feel like the linter is missing a check that would be useful for improving the code quality of Terragrunt, please open an issue to discuss it, then open a pull request to add the check.\n\nThere are two lint configurations currently in use:\n\n- **Default linter**\n\n  This is the default configuration that is used when running `golangci-lint run`. The configuration for this lint is defined in the `.golangci.yml` file.\n\n  These lints **must** pass before any code is merged into the `main` branch.\n\n- **Strict linter**\n\n  This is the more strict configuration that is used to check for additional issues in pull requests. This configuration is defined in the `.strict.golanci.yml` file.\n\n  These lints **do not have to pass** before code is merged into the `main` branch, but the results are useful to look at to improve code quality.\n\n#### Default linter\n\nBefore any tests run in our continuous integration suite, they must pass the default linter. This is to ensure an acceptable floor for code quality in the codebase.\n\nTo run the default linter directly, use:\n\n```bash\ngolangci-lint run\n```\n\nThere's also a Makefile recipe that runs the default linter:\n\n```bash\nmake run-lint\n```\n\nIf possible, you are advised to [integrate the linter into your code editor](https://golangci-lint.run/welcome/integrations/) to get immediate feedback as edit Terragrunt code.\n\n#### Markdownlint\n\nIn addition to the golang linter, we also use [markdownlint](https://github.com/DavidAnson/markdownlint) to lint the markdown files in the codebase. This is to ensure that the documentation is consistent and easy to read.\n\nYou'll want to check that the markdown files are linted correctly before submitting a pull request to update the docs. You can do this by running:\n\n```bash\nmarkdownlint \\\n    --disable MD013 MD024 \\\n    -- \\\n    docs\n```\n\n### Running tests\n\nThere are multiple different kinds of tests in the Terragrunt codebase, and each serves a different purpose.\n\n#### Unit tests\n\nThese are tests that test individual functions in the codebase. They are located in the same package as the code they are testing and are suffixed `*_test.go`.\n\nThey use a package directive that is suffixed `_test` of the package they test to force them to only test exported functions of that package, while residing in the same directory.\n\nThe idea behind this practice is to keep the tests close to the code they are testing, and to force them to only test the public API of the package. This allows implementation details of particular functions to change without breaking tests, as long as the public API behaves the same.\n\nIn general, if you are editing Terragrunt code, and there isn't a unit test that covers the code you are updating, it's probably a good idea to add one. If there is a unit test for the code you are updating, you should make sure that you run that test after any update to ensure that you haven't broken anything.\n\nWhen possible, introduce new tests for code _before_ you start making changes. This is a practice known as [Test Driven Development](https://en.wikipedia.org/wiki/Test-driven_development).\n\nYou can run the unit tests for a particular package by running:\n\n```bash\ngo test ./path/to/package\n```\n\nTo specifically run a single test, you can use the `-run` flag:\n\n```bash\ngo test -run TestFunctionName ./path/to/package\n```\n\nThere are many ways to customize the `go test` command, including using flags like `-v` to get more verbose output. To learn more about go testing, read the [official documentation](https://pkg.go.dev/testing).\n\n#### Integration tests\n\nThese are tests that test integrations between multiple parts of the Terragrunt codebase, and external services. They generally invoke Terragrunt as if you were using it from the command line.\n\nThese tests are located in the `test` directory, and are suffixed `*_test.go`.\n\nOften, these tests run against test fixtures, which are small Terragrunt configurations that emulate specific real-world scenarios. These test fixtures are located in the `test/fixtures` directory.\n\nTo run the integration tests, you can use the `go test` command:\n\n```bash\ngo test ./test\n```\n\nNote that integration tests can be slow, as they often involve running full Terragrunt commands, and that frequently involves spawning new processes. As a result, you may want to run only a subset of the tests while developing. You can do this by using the `-run` flag:\n\n```bash\ngo test -run 'TestBeginningOfFunctionName*' ./test\n```\n\nThis will run all tests that start with `TestBeginningOfFunctionName`.\n\nNote that some tests may require that you opt-in for them to be tested. This is because they may require access to external services that you need to authenticate with or use a specific external tool that you might not have installed. In these cases, we use [golang build tags](https://pkg.go.dev/go/build) to conditionally compile the tests. You can run these tests by setting the appropriate build tag before testing.\n\nFor example, AWS tests are tagged using the `aws` build tag. To run these tests, you can use the `-tags` flag set in the `GOFLAGS` environment variable like so:\n\n```bash\nGOFLAGS='-tags=aws' go test -run 'TestAwsInitHookNoSourceWithBackend' .\n```\n\nDepending on how you've configured your editor, you may need to make sure that your editor has the `GOFLAGS` environment variable set before starting for the best development experience:\n\n```bash\nexport GOFLAGS='-tags=aws'\nneovim .\n```\n\nIn general, we try to make sure that any test that requires a build tag is also consistently prefixed a certain way so that they can be tested independently.\n\nFor example, all AWS tests are prefixed with `TestAws*`.\n\nTerragrunt also includes integration tests for Google Cloud Platform (GCP). These tests are prefixed with `TestGcp*` and are tagged with the `gcp` build tag. To run these tests, you can use the `-tags` flag set in the `GOFLAGS` environment variable, similar to AWS tests:\n\n```bash\nGOFLAGS='-tags=gcp' go test -run 'TestGcp*' .\n```\n\nTo successfully run the GCP tests, you must set the following environment variables:\n\n- `GCLOUD_SERVICE_KEY`: The service account JSON key used for authentication.\n- `GOOGLE_CLOUD_PROJECT` or `GOOGLE_PROJECT_ID`: The GCP project name.\n- `GOOGLE_COMPUTE_ZONE`: The compute zone name.\n- `GOOGLE_IDENTITY_EMAIL`: The service account identity email.\n- `GCLOUD_SERVICE_KEY_IMPERSONATOR`: (Optional) An additional service account key used in impersonation tests.\n\nMake sure these environment variables are set in your shell before running the tests. For example:\n\n```bash\nexport GCLOUD_SERVICE_KEY=\"/path/to/service-account.json\"\nexport GOOGLE_CLOUD_PROJECT=\"your-gcp-project\"\nexport GOOGLE_COMPUTE_ZONE=\"us-central1-a\"\nexport GOOGLE_IDENTITY_EMAIL=\"service-account@your-gcp-project.iam.gserviceaccount.com\"\nexport GCLOUD_SERVICE_KEY_IMPERSONATOR=\"/path/to/impersonator-service-account.json\"\n```\n\nThe service account used for GCP tests must have the following IAM roles in your GCP project:\n\n- `roles/storage.admin`\n- `roles/iam.serviceAccountTokenCreator`\n\nYou can assign these roles using the following gcloud commands:\n\n```bash\ngcloud projects add-iam-policy-binding <gcp-project> \\\n  --member=\"<service-account>\" \\\n  --role=\"roles/storage.admin\"\n\ngcloud projects add-iam-policy-binding <gcp-project> \\\n  --member=\"<service-account>\" \\\n  --role=\"roles/iam.serviceAccountTokenCreator\"\n```\n\n#### Race tests\n\nGiven that Terragrunt is a tool that frequently involves concurrently running multiple things at once, there's always a risk for race conditions to occur. As such, there are dedicated tests that are run with the `-race` flag in CI to use golang's built-in tooling for identifying race conditions.\n\nIn general, when encountering a bug caused by a race condition in the wild, we endeavor to write a test for it, and add it to the `./test/race_test.go` file to avoid regressions in the future. If you want to make sure that new code you are writing doesn't introduce a race condition, add a test for it in the `race_test.go` file.\n\nWe can do a better job of finding candidates for additional testing here, so if you are interested in helping out, please open an issue to discuss it.\n\nThe convention we use for race tests is to prefix them with `WithRacing`. The Terragrunt Continuous Integration workflow will run these tests with the `-race` flag as part of the test suite.\n\n#### Benchmark tests\n\nBenchmark tests are tests that are run with the `-bench` flag to the `go test` command. They are used to measure the performance of a particular function or set of functions.\n\nYou can find them by looking for tests that start with `Benchmark*` instead of `Test*` in the codebase.\n\nFor more information on Terragrunt performance, read the dedicated [Performance documentation](/troubleshooting/performance).\n\nIn general, we have inadequate benchmark testing in the Terragrunt codebase, and want to improve this. If you are interested in helping out, please open an issue to discuss it.\n\nPrior to the release of Terragrunt 1.0, we will have a concerted effort to improve the benchmark testing in the codebase.\n\n#### Continuous Integration\n\nAll of the testing mentioned above is run automatically as part of our continuous integration suite in GitHub Actions.\n\n### Debug logging\n\nIf you set the `TG_DEBUG_INPUTS` environment variable to \"true\", the stack trace for any error will be printed to stdout when you run the app.\n\nAdditionally, newer features introduced in v0.19.0 (such as `locals` and `dependency` blocks) can output more verbose logging if you set the `TG_LOG` environment variable to `debug`.\n\n### Error handling\n\nIn this project, we try to ensure that:\n\n1. Every error has a stacktrace. This makes debugging easier.\n\n2. Every error generated by our own code (as opposed to errors from Go built-in functions or errors from 3rd party libraries) has a custom type. This makes error handling more precise, as we can decide to handle different types of errors differently.\n\nTo accomplish these two goals, we have created an `errors` package that has several helper methods, such as `errors.New(err error)`, which wraps the given `error` in an Error object that contains a stacktrace. Under the hood, the `errors` package is using the [go-errors](https://github.com/go-errors/errors) library, but this may change in the future, so the rest of the code should not depend on `go-errors` directly.\n\nHere is how the `errors` package should be used:\n\n1. Any time you want to create your own error, create a custom type for it, and when instantiating that type, wrap it with a call to `errors.New`. That way, any time you call a method defined in the Terragrunt code, you know the error it returns already has a stacktrace and you don't have to wrap it yourself.\n\n2. Any time you get back an error object from a function built into Go or a 3rd party library, immediately wrap it with `errors.New`. This gives us a stacktrace as close to the source as possible.\n\n3. If you need to get back the underlying error, you can use the `errors.IsError` and `errors.Unwrap` functions.\n\n### Formatting\n\nEvery source file in this project should be formatted with `go fmt`. There are few helper scripts and targets in the Makefile that can help with this (mostly taken from the [terraform repo](https://github.com/hashicorp/terraform/) when it was MPL licensed):\n\n1. `make fmtcheck` Checks to see if all source files are formatted. Exits 1 if there are unformatted files.\n\n2. `make fmt` Formats all source files with `gofmt`.\n\n3. `make install-pre-commit-hook`\n\n    Installs a git pre-commit hook that will run all of the source files through `gofmt`.\n\nTo ensure that your changes get properly formatted, please install the git pre-commit hook with `make install-pre-commit-hook`.\n\n### Development Containers\n\n[Development Containers](https://containers.dev/) enable you to capture an entire development environment within a container. They can specify the required binaries, languages, extensions, and settings for a project. They can even define commands to run when entering the container. The [Dev Container spec](https://containers.dev/implementors/spec/) is met by a number of [supporting tools and editors](https://containers.dev/supporting), but here we demonstrate a Visual Studio Code example for contributing to the Terragrunt project.\n\n1. Install the [Dev Containers VSCode extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers).\n\n2. Create a `.devcontainer.json` file at the project root.\n\n   The example below:\n   - Launches a container configured with the appropriate version of Go.\n   - Integrates `golangci-lint` (the standard Golang linter) into the editor.\n   - Installs the `markdownlint` extension with specific rules disabled:\n     - `MD013`\n     - `MD024`\n   - Includes Node.js and OpenTofu.\n   - Starts the Astro Starlight docs upon container startup.\n\n    ```json\n    # .devcontainer.json\n\n    {\n      \"name\": \"Terragrunt Contributing IDE\",\n      \"image\": \"mcr.microsoft.com/devcontainers/go:1-1.23-bookworm\",\n      \"runArgs\": [\"--network=host\"],\n      \"customizations\": {\n        \"vscode\": {\n          \"settings\": {\n            \"go.lintTool\": \"golangci-lint\",\n            \"go.lintFlags\": [\n              \"--fast\"\n            ],\n            \"markdownlint.config\": {\n              \"MD013\": false,\n              \"MD024\": false\n            }\n          },\n          \"extensions\": [\n            \"davidanson.vscode-markdownlint\"\n          ]\n        }\n      },\n      \"features\": {\n        \"ghcr.io/devcontainers/features/node:1\": {},\n        \"ghcr.io/robbert229/devcontainer-features/opentofu:1\": {}\n      },\n      \"postCreateCommand\": \"cd docs && npm install && npm run dev\"\n    }\n    ```\n\n3. Open the project as a VSCode workspace and select `Reopen in Container` when prompted.\n\n   <Aside type=\"tip\">\n\n     If you miss the prompt, just open the command palette and run:\n\n     ```\n     Dev Containers: Rebuild and Reopen in Container\n     ```\n\n   </Aside>\n\n## Releasing your changes\n\nTo learn about how changes get released, read the [Releases documentation](/process/releases).\n"
  },
  {
    "path": "docs/src/content/docs/05-community/02-support.md",
    "content": "---\ntitle: Support\ndescription: Get help with Terragrunt\nslug: community/support\nsidebar:\n  order: 2\n---\n\n## Github Discussions\n\nSearch [Terragrunt GitHub Discussions](https://gruntwork-io/terragrunt/discussions) for existing questions or ask your own. [Gruntwork GitHub Discussions](https://github.com/gruntwork-io/knowledge-base/discussions) is a good place for general discussions and questions about Gruntwork tools.\n\n## Join the Discord Community\n\nJoin the [Gruntwork Discord Community](/community/invite) to chat with maintainers and members of the community.\n\n## Github Issues\n\nRead through [existing issues](https://github.com/gruntwork-io/terragrunt/issues) or post a new one. Github issues is a good place to:\n\n- Report a bug\n\n- Ask for improvements\n\n- Propose a change to how Terragrunt works\n\n- Start contributing by solving issues\n\n## Commercial support\n\nDoes your company rely on Terragrunt in production? If so, you can get commercial support directly from Gruntwork, the creators of Terragrunt! Check out the [Gruntwork Support Page](https://gruntwork.io/support) for more details.\n"
  },
  {
    "path": "docs/src/content/docs/05-community/03-license.md",
    "content": "---\ntitle: License\ndescription: This code is released under the MIT License. Read more here.\nslug: community/license\nsidebar:\n  order: 3\n---\n\nThis code is released under the MIT License. See [LICENSE.txt](https://github.com/gruntwork-io/terragrunt/blob/main/LICENSE.txt).\n"
  },
  {
    "path": "docs/src/content/docs/06-troubleshooting/01-debugging.mdx",
    "content": "---\ntitle: Debugging\ndescription: Debugging Terragrunt and OpenTofu/Terraform\nslug: troubleshooting/debugging\nsidebar:\n  order: 1\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Aside } from '@astrojs/starlight/components';\n\n## Support\n\nIf you are running into issues with Terragrunt, join the [Terragrunt Community Discord Server](/community/invite).\n\n<Aside type=\"tip\" title=\"Enterprise Support\">\n\n[Terragrunt Enterprise Support](https://www.gruntwork.io/services/terragrunt) is available for users who need additional support beyond the Terragrunt Community Discord Server.\n\n</Aside>\n\n## Logging\n\nThe easiest way to get more information for debugging an error you are experiencing in Terragrunt is to increase the log level.\n\n```bash\nterragrunt run --log-level debug -- plan\n```\n\nIncreasing the log level using the [`--log-level`](/reference/cli/global-flags/#log-level) flag will output more information about what Terragrunt is doing and why. This is useful information for your debugging, and for sharing with others if you want their help in understanding what went wrong in your run.\n\nTerragrunt has a fairly sophisticated logging system, and it can be helpful to read through the documentation on [Logging](/reference/logging/) to understand how it works.\n\n## Telemetry\n\nIf you have [Docker](https://www.docker.com/) installed, and you want to gain deeper insight as to what Terragrunt is doing, you can also enable the [OpenTelemetry](https://opentelemetry.io/) integration in Terragrunt to send telemetry data to a locally running container to better visualize what Terragrunt is doing and when.\n\nFirst, run the following in a terminal:\n\n```bash\ndocker run --rm --name jaeger -e COLLECTOR_OTLP_ENABLED=true -p 16686:16686 -p 4317:4317 -p 4318:4318 jaegertracing/all-in-one:1.54.0\n```\n\nThis spins up a local instance of [Jaeger](https://www.jaegertracing.io/), which is a distributed tracing system that can be used to see what Terragrunt is doing.\n\nNext, run the following in the terminal where you want to run Terragrunt:\n\n```bash\nexport TG_TELEMETRY_TRACE_EXPORTER=http\nexport TG_TELEMETRY_TRACE_EXPORTER_HTTP_ENDPOINT=localhost:4318\nexport TG_TELEMETRY_TRACE_EXPORTER_INSECURE_ENDPOINT=true\n```\n\nThis tells Terragrunt that it should send telemetry data to the Jaeger instance running in the Docker container.\n\nNow you'll want to do whatever you want to trace with Terragrunt by running Terragrunt normally.\n\n```bash\nterragrunt run -- plan\n```\n\nFinally, visit the Jaeger UI at [http://localhost:16686](http://localhost:16686/) to see traces of your runs.\n\n<Aside type=\"tip\" title=\"Exporting traces\">\n\nYou can export traces using the \"Download Results\" button in the UI and upload them using the \"Upload\" pane on the left.\n\nThis can be useful if you want to share traces with others, though you should be mindful that you might have sensitive data in these traces you don't want shared with untrusted parties. Sharing traces with other trusted members of your team can be a good way to visually analyze the results of runs in addition to reading through logs.\n\n</Aside>\n\n<Aside type=\"note\" title=\"Learn More About OpenTelemetry\">\n\nTo learn more about Terragrunt's integration with OpenTelemetry, see the dedicated [OpenTelemetry documentation](/troubleshooting/open-telemetry/).\n\n</Aside>\n\n## Debugging OpenTofu/Terraform Behavior\n\nTerragrunt and OpenTofu/Terraform usually play well together in helping you write scalable, reusable infrastructure. But how can you figure out what went wrong in the rare case that things _do_ go wrong?\n\nTerragrunt provides a way to configure its logging level through the [`--log-level`](https://docs.terragrunt.com/reference/cli/global-flags/#log-level) flag. Additionally, Terragrunt provides an [`--inputs-debug`](https://docs.terragrunt.com/reference/cli/commands/run/#inputs-debug) flag that can generate a `terragrunt-debug.tfvars.json` file to help you understand what inputs it is setting when calling OpenTofu/Terraform.\n\nFor example, you could use it like this to debug a `plan` that's producing unexpected outcomes.\n\n```shell\nterragrunt run --log-level debug --inputs-debug -- plan\n```\n\nRunning this command will do two things for you:\n\n- Output a file named `terragrunt-debug.tfvars.json` to your current working directory (the same one containing your `terragrunt.hcl`)\n- Print instructions on how to invoke `tofu`/`terraform` against the generated file to reproduce exactly the same `tofu`/`terraform` output as you saw when invoking `terragrunt`. This will help you to determine where the problem's root cause lies.\n\nUsing those features is helpful when you want determine which of these three major areas is the\nroot cause of your problem:\n\n1. Misconfiguration of your infrastructure code.\n2. An error in `terragrunt`.\n3. An error in `tofu/terraform`.\n\nConsider this file structure for a fictional production environment where we\nhave configured an application to deploy as many tasks as there are minimum\nnumber of machines in some cluster.\n\n<FileTree>\n\n- live\n  - prod\n    - app\n      - vars.tf\n      - main.tf\n      - outputs.tf\n      - terragrunt.hcl\n    - ecs-cluster\n      - outputs.tf\n\n</FileTree>\n\nThe files contain this text (`app/main.tf` and `ecs-cluster/outputs.tf` omitted\nfor brevity):\n\n```hcl\n# app/vars.tf\nvariable \"image_id\" {\n  type = string\n}\nvariable \"num_tasks\" {\n  type = number\n}\n# app/outputs.tf\noutput \"task_ids\" {\n  value = module.app_infra_module.task_ids\n}\n# app/terragrunt.hcl\nlocals {\n  image_id = \"acme/myapp:1\"\n}\ndependency \"cluster\" {\n  config_path = \"../ecs-cluster\"\n}\ninputs = {\n  image_id = locals.image_id\n  num_tasks = dependency.cluster.outputs.cluster_min_size\n}\n```\n\nYou perform a `terragrunt plan`, and find that `outputs.task_ids` has 7\nelements, but you know that the cluster only has 4 VMs in it! What's happening?\nLet's figure it out. Run this:\n\n```shell\nterragrunt plan --log-level debug --inputs-debug\n```\n\nAfter planning, you will see output like this in stderr:\n\n```log\n[terragrunt] Variables passed to tofu/terraform are located in \"~/live/prod/app/terragrunt-debug.tfvars\"\n[terragrunt] Run this command to replicate how tofu/terraform was invoked:\n[terragrunt]     tofu plan -var-file=\"~/live/prod/app/terragrunt-debug.tfvars.json\" \"~/live/prod/app\"\n```\n\nWell we may have to do all that, but first let's just take a look at `terragrunt-debug.tfvars.json`\n\n```json\n{\n  \"image_id\": \"acme/myapp:1\",\n  \"num_tasks\": 7\n}\n```\n\nSo this gives us the clue -- we expected `num_tasks` to be 4, not 7! Looking into\n`ecs-cluster/outputs.tf` we see this text:\n\n```hcl\n# ecs-cluster/outputs.tf\noutput \"cluster_min_size\" {\n  value = module.my_cluster_module.cluster_max_size\n}\n```\n\nOops! It says `max` when it should be `min`. If we fix `ecs-cluster/outputs.tf`\nwe should be golden!\n\n## Additional OpenTofu/Terraform Debugging\n\nIf you're still having trouble, you may want to try adjusting the `TF_LOG` environment variables to instruct OpenTofu/Terraform to emit more detailed logs.\n\n```bash\nTF_LOG=debug terragrunt run -- plan\n```\n\nYou can learn more about OpenTofu/Terraform debug logging [here](https://opentofu.org/docs/internals/debugging/).\n"
  },
  {
    "path": "docs/src/content/docs/06-troubleshooting/02-open-telemetry.md",
    "content": "---\ntitle: OpenTelemetry\ndescription: Learn how to integrate Terragrunt with OpenTelemetry\nslug: troubleshooting/open-telemetry\nsidebar:\n  order: 2\n---\n\nTerragrunt can be configured to emit telemetry in [OpenTelemetry](https://opentelemetry.io/) format in the form of traces and metrics.\n\nOpenTelemetry tracing is typically used in Terragrunt to analyze performance. For more details on analyzing and optimizing performance, read the dedicated [Performance documentation](/troubleshooting/performance).\n\n## High-level overview\n\nThe following are concepts and technologies that are important to be aware of when using OpenTelemetry with Terragrunt.\n\n- [OpenTelemetry](https://opentelemetry.io/)\n- [Traces](https://opentelemetry.io/docs/concepts/signals/traces/)\n- [Metrics](https://opentelemetry.io/docs/concepts/signals/metrics/)\n- [Jaeger](https://www.jaegertracing.io/)\n\nTracing configuration:\n\n- `TG_TELEMETRY_TRACE_EXPORTER` - traces exporter type to be used. Currently supported values are:\n  - `none` - no trace exporting, default value.\n  - `console` - to export traces to console as JSON\n  - `otlpHttp` - to export traces to an OpenTelemetry collector over HTTP [otlptracehttp](https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp)\n  - `otlpGrpc` - to export traces over gRPC [otlptracegrpc](https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc)\n  - `http` - to export traces to a custom HTTP endpoint using [otlptracehttp](https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp)\n- `TG_TELEMETRY_TRACE_EXPORTER_HTTP_ENDPOINT` - in case of `http` exporter, this is the endpoint to which traces will be sent.\n- `TG_TELEMETRY_TRACE_EXPORTER_INSECURE_ENDPOINT` - if set to true, the exporter will not validate the server's certificate, helpful for local traces collection.\n- `TRACEPARENT` - if set, the value will be used as a parent trace context, format `TRACEPARENT=00-<hex_trace_id>-<hex_span_id>-<trace_flags>`, example: `TRACEPARENT=00-xxx-yyy-01`\n\nMetrics configuration:\n\n- `TG_TELEMETRY_METRIC_EXPORTER` - metrics exporter type to be used. Currently supported values are:\n  - `none` - no metric exporting, default value.\n  - `console` - write metrics to console as JSONs.\n  - `otlpHttp` - export metrics to an OpenTelemetry collector over HTTP [otlpmetrichttp](https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp)\n  - `grpcHttp` - export metrics to an OpenTelemetry collector over gRPC [otlpmetricgrpc](https://pkg.go.dev/go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc)\n- `TG_TELEMETRY_METRIC_EXPORTER_INSECURE_ENDPOINT` - if set to true, the exporter will not validate the server's certificate, helpful for local metrics collection.\n\n## Example configurations for trace collection\n\nCollection of examples how to configure Terragrunt to emit traces and metrics in OpenTelemetry format.\n\n## Example traces collection with Jaeger\n\n- Start a Jaeger instance with docker:\n\n```bash\ndocker run --rm --name jaeger -e COLLECTOR_OTLP_ENABLED=true -p 16686:16686 -p 4317:4317 -p 4318:4318 jaegertracing/all-in-one:1.54.0\n```\n\n- Verify that UI is available at <http://localhost:16686/>\n- Define environment variables for Terragrunt to report traces to Jaeger:\n\n```bash\nexport TG_TELEMETRY_TRACE_EXPORTER=http\nexport TG_TELEMETRY_TRACE_EXPORTER_HTTP_ENDPOINT=localhost:4318\nexport TG_TELEMETRY_TRACE_EXPORTER_INSECURE_ENDPOINT=true\n```\n\n- Run terragrunt\n- Verify that traces are available in Jaeger UI\n\n## Configurations to collect traces in Grafana Tempo\n\n- Start a Grafana Tempo instance [example](https://grafana.com/docs/tempo/latest/getting-started/docker-example/)\n- Define environment variables for Terragrunt to report traces to Tempo:\n\n```bash\nexport TG_TELEMETRY_TRACE_EXPORTER=otlpHttp\n# Replace with your tempo instance\nexport OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318\nexport TG_TELEMETRY_TRACE_EXPORTER_INSECURE_ENDPOINT=true\n````\n\n- Run terragrunt\n- Check for traces in Tempo UI for service \"terragrunt\"\n\n## Example traces collection in console\n\n- Set env variable to enable telemetry:\n\n```bash\nexport TG_TELEMETRY_TRACE_EXPORTER=console\n```\n\n- Run terragrunt\n- Check produced traces in console, like:\n\n```json\n{\"Name\":\"run_bash\",\"SpanContext\":{\"TraceID\":\"bdf3cb9078706b7f0b4f1d92428eedc0\",\"SpanID\":\"f91587247524593b\",\"TraceFlags\":\"01\",\"TraceState\":\"\",\"Remote\":false},\"Parent\":{\"TraceID\":\"bdf3cb9078706b7f0b4f1d92428eedc0\",\"SpanID\":\"b0b007770f852066\",\"TraceFlags\":\"01\",\"TraceState\":\"\",\"Remote\":false},\"SpanKind\":1,\"StartTime\":\"2024-02-08T12:32:30.564217484Z\",\"EndTime\":\"2024-02-08T12:32:31.570666395Z\",\"Attributes\":[{\"Key\":\"command\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"bash\"}},{\"Key\":\"args\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"[-c sleep 1]\"}},{\"Key\":\"dir\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"/projects/gruntwork/terragrunt-tests/trace-test/mod2\"}}],\"Events\":null,\"Links\":null,\"Status\":{\"Code\":\"Unset\",\"Description\":\"\"},\"DroppedAttributes\":0,\"DroppedEvents\":0,\"DroppedLinks\":0,\"ChildSpanCount\":0,\"Resource\":[{\"Key\":\"service.name\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"terragrunt\"}},{\"Key\":\"service.version\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"v0.55.0-29-g66bfa07b756e-dirty\"}},{\"Key\":\"telemetry.sdk.language\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"go\"}},{\"Key\":\"telemetry.sdk.name\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"opentelemetry\"}},{\"Key\":\"telemetry.sdk.version\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"1.23.0\"}}],\"InstrumentationLibrary\":{\"Name\":\"terragrunt\",\"Version\":\"\",\"SchemaURL\":\"\"}}\n{\"Name\":\"parse_config_file\",\"SpanContext\":{\"TraceID\":\"bdf3cb9078706b7f0b4f1d92428eedc0\",\"SpanID\":\"d2823047fb469bdf\",\"TraceFlags\":\"01\",\"TraceState\":\"\",\"Remote\":false},\"Parent\":{\"TraceID\":\"bdf3cb9078706b7f0b4f1d92428eedc0\",\"SpanID\":\"b0b007770f852066\",\"TraceFlags\":\"01\",\"TraceState\":\"\",\"Remote\":false},\"SpanKind\":1,\"StartTime\":\"2024-02-08T12:32:30.380054129Z\",\"EndTime\":\"2024-02-08T12:32:31.570899286Z\",\"Attributes\":[{\"Key\":\"config_path\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"/projects/gruntwork/terragrunt-tests/trace-test/mod2/terragrunt.hcl\"}}],\"Events\":null,\"Links\":null,\"Status\":{\"Code\":\"Unset\",\"Description\":\"\"},\"DroppedAttributes\":0,\"DroppedEvents\":0,\"DroppedLinks\":0,\"ChildSpanCount\":0,\"Resource\":[{\"Key\":\"service.name\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"terragrunt\"}},{\"Key\":\"service.version\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"v0.55.0-29-g66bfa07b756e-dirty\"}},{\"Key\":\"telemetry.sdk.language\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"go\"}},{\"Key\":\"telemetry.sdk.name\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"opentelemetry\"}},{\"Key\":\"telemetry.sdk.version\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"1.23.0\"}}],\"InstrumentationLibrary\":{\"Name\":\"terragrunt\",\"Version\":\"\",\"SchemaURL\":\"\"}}\n{\"Name\":\"run_terraform\",\"SpanContext\":{\"TraceID\":\"bdf3cb9078706b7f0b4f1d92428eedc0\",\"SpanID\":\"152d873a18559f07\",\"TraceFlags\":\"01\",\"TraceState\":\"\",\"Remote\":false},\"Parent\":{\"TraceID\":\"bdf3cb9078706b7f0b4f1d92428eedc0\",\"SpanID\":\"b0b007770f852066\",\"TraceFlags\":\"01\",\"TraceState\":\"\",\"Remote\":false},\"SpanKind\":1,\"StartTime\":\"2024-02-08T12:32:31.57161757Z\",\"EndTime\":\"2024-02-08T12:32:31.688157882Z\",\"Attributes\":[{\"Key\":\"command\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"tofu\"}},{\"Key\":\"args\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"[init]\"}},{\"Key\":\"dir\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"/projects/gruntwork/terragrunt-tests/trace-test/mod2\"}}],\"Events\":null,\"Links\":null,\"Status\":{\"Code\":\"Unset\",\"Description\":\"\"},\"DroppedAttributes\":0,\"DroppedEvents\":0,\"DroppedLinks\":0,\"ChildSpanCount\":0,\"Resource\":[{\"Key\":\"service.name\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"terragrunt\"}},{\"Key\":\"service.version\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"v0.55.0-29-g66bfa07b756e-dirty\"}},{\"Key\":\"telemetry.sdk.language\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"go\"}},{\"Key\":\"telemetry.sdk.name\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"opentelemetry\"}},{\"Key\":\"telemetry.sdk.version\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"1.23.0\"}}],\"InstrumentationLibrary\":{\"Name\":\"terragrunt\",\"Version\":\"\",\"SchemaURL\":\"\"}}\n{\"Name\":\"run_terraform\",\"SpanContext\":{\"TraceID\":\"bdf3cb9078706b7f0b4f1d92428eedc0\",\"SpanID\":\"29341bdb65f66b1e\",\"TraceFlags\":\"01\",\"TraceState\":\"\",\"Remote\":false},\"Parent\":{\"TraceID\":\"bdf3cb9078706b7f0b4f1d92428eedc0\",\"SpanID\":\"b0b007770f852066\",\"TraceFlags\":\"01\",\"TraceState\":\"\",\"Remote\":false},\"SpanKind\":1,\"StartTime\":\"2024-02-08T12:32:31.688240673Z\",\"EndTime\":\"2024-02-08T12:32:31.793377642Z\",\"Attributes\":[{\"Key\":\"command\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"tofu\"}},{\"Key\":\"args\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"[apply -auto-approve -input=false]\"}},{\"Key\":\"dir\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"/projects/gruntwork/terragrunt-tests/trace-test/mod2\"}}],\"Events\":null,\"Links\":null,\"Status\":{\"Code\":\"Unset\",\"Description\":\"\"},\"DroppedAttributes\":0,\"DroppedEvents\":0,\"DroppedLinks\":0,\"ChildSpanCount\":0,\"Resource\":[{\"Key\":\"service.name\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"terragrunt\"}},{\"Key\":\"service.version\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"v0.55.0-29-g66bfa07b756e-dirty\"}},{\"Key\":\"telemetry.sdk.language\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"go\"}},{\"Key\":\"telemetry.sdk.name\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"opentelemetry\"}},{\"Key\":\"telemetry.sdk.version\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"1.23.0\"}}],\"InstrumentationLibrary\":{\"Name\":\"terragrunt\",\"Version\":\"\",\"SchemaURL\":\"\"}}\n{\"Name\":\"run_module\",\"SpanContext\":{\"TraceID\":\"bdf3cb9078706b7f0b4f1d92428eedc0\",\"SpanID\":\"8a01522bc65e0f1b\",\"TraceFlags\":\"01\",\"TraceState\":\"\",\"Remote\":false},\"Parent\":{\"TraceID\":\"bdf3cb9078706b7f0b4f1d92428eedc0\",\"SpanID\":\"b0b007770f852066\",\"TraceFlags\":\"01\",\"TraceState\":\"\",\"Remote\":false},\"SpanKind\":1,\"StartTime\":\"2024-02-08T12:32:30.290680776Z\",\"EndTime\":\"2024-02-08T12:32:31.793392803Z\",\"Attributes\":[{\"Key\":\"path\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"/projects/gruntwork/terragrunt-tests/trace-test/mod2\"}},{\"Key\":\"terraformCommand\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"apply\"}}],\"Events\":null,\"Links\":null,\"Status\":{\"Code\":\"Unset\",\"Description\":\"\"},\"DroppedAttributes\":0,\"DroppedEvents\":0,\"DroppedLinks\":0,\"ChildSpanCount\":0,\"Resource\":[{\"Key\":\"service.name\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"terragrunt\"}},{\"Key\":\"service.version\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"v0.55.0-29-g66bfa07b756e-dirty\"}},{\"Key\":\"telemetry.sdk.language\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"go\"}},{\"Key\":\"telemetry.sdk.name\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"opentelemetry\"}},{\"Key\":\"telemetry.sdk.version\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"1.23.0\"}}],\"InstrumentationLibrary\":{\"Name\":\"terragrunt\",\"Version\":\"\",\"SchemaURL\":\"\"}}\n{\"Name\":\"run --all apply\",\"SpanContext\":{\"TraceID\":\"bdf3cb9078706b7f0b4f1d92428eedc0\",\"SpanID\":\"b0b007770f852066\",\"TraceFlags\":\"01\",\"TraceState\":\"\",\"Remote\":false},\"Parent\":{\"TraceID\":\"00000000000000000000000000000000\",\"SpanID\":\"0000000000000000\",\"TraceFlags\":\"00\",\"TraceState\":\"\",\"Remote\":false},\"SpanKind\":1,\"StartTime\":\"2024-02-08T12:32:26.388519019Z\",\"EndTime\":\"2024-02-08T12:32:31.793405603Z\",\"Attributes\":[{\"Key\":\"terraformCommand\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"apply\"}},{\"Key\":\"args\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"[apply]\"}},{\"Key\":\"dir\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"/projects/gruntwork/terragrunt-tests/trace-test\"}}],\"Events\":null,\"Links\":null,\"Status\":{\"Code\":\"Unset\",\"Description\":\"\"},\"DroppedAttributes\":0,\"DroppedEvents\":0,\"DroppedLinks\":0,\"ChildSpanCount\":28,\"Resource\":[{\"Key\":\"service.name\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"terragrunt\"}},{\"Key\":\"service.version\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"v0.55.0-29-g66bfa07b756e-dirty\"}},{\"Key\":\"telemetry.sdk.language\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"go\"}},{\"Key\":\"telemetry.sdk.name\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"opentelemetry\"}},{\"Key\":\"telemetry.sdk.version\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"1.23.0\"}}],\"InstrumentationLibrary\":{\"Name\":\"terragrunt\",\"Version\":\"\",\"SchemaURL\":\"\"}}\n```\n\n## Collection of metrics with OpenTelemetry collector and Prometheus\n\n- Start OpenTelemetry collector with Prometheus receiver.\n\n  Example setup through `docker-compose.yml`:\n\n  ```yaml\n  version: '3'\n  services:\n    otel-collector:\n      image: otel/opentelemetry-collector:0.94.0\n      volumes:\n        - ./otel-collector-config.yaml:/etc/otelcol/config.yaml\n      ports:\n        - \"4317:4317\" # OTLP gRPC receiver\n        - \"4318:4318\" # OTLP HTTP receiver\n        - \"8889:8889\" # Prometheus exporter\n    prometheus:\n      image: prom/prometheus:v2.45.3\n      volumes:\n        - ./prometheus.yml:/etc/prometheus/prometheus.yml\n      ports:\n        - \"9090:9090\"\n      depends_on:\n        - otel-collector\n  ```\n\nOpenTelemetry collector configuration `otel-collector-config.yaml`:\n\n```yaml\nreceivers:\n  otlp:\n    protocols:\n      grpc:\n      http:\nprocessors:\n  batch:\nexporters:\n  prometheus:\n    endpoint: \"0.0.0.0:8889\" # Prometheus exporter endpoint\nservice:\n  pipelines:\n    metrics:\n      receivers: [otlp]\n      processors: [batch]\n      exporters: [prometheus]\n```\n\nPrometheus configuration file `prometheus.yml`:\n\n```yaml\nglobal:\n  scrape_interval: 15s\nscrape_configs:\n  - job_name: 'opentelemetry'\n    scrape_interval: 5s\n    static_configs:\n      - targets: ['otel-collector:8889']\n```\n\n- Confirm that Prometheus is available at <http://localhost:9090/>\n- Define environment variables for Terragrunt to report metrics to OpenTelemetry collector:\n\n```bash\nexport TG_TELEMETRY_METRIC_EXPORTER=grpcHttp\nexport TG_TELEMETRY_METRIC_EXPORTER_INSECURE_ENDPOINT=true\nexport OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317\n```\n\n- Run terragrunt\n- Verify that metrics are available in Prometheus UI\n\nExample configuration to export metrics to console:\n\n- Set env variable to enable telemetry:\n\n```bash\nexport TG_TELEMETRY_METRIC_EXPORTER=console\n```\n\n- Run terragrunt\n- In output will be printed messages like:\n\n```json\n{\"Resource\":[{\"Key\":\"service.name\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"terragrunt\"}},{\"Key\":\"service.version\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"v0.55.0-41-g7185318bb11b\"}},{\"Key\":\"telemetry.sdk.language\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"go\"}},{\"Key\":\"telemetry.sdk.name\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"opentelemetry\"}},{\"Key\":\"telemetry.sdk.version\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"1.23.1\"}}],\"ScopeMetrics\":[]}\n{\"Resource\":[{\"Key\":\"service.name\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"terragrunt\"}},{\"Key\":\"service.version\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"v0.55.0-41-g7185318bb11b\"}},{\"Key\":\"telemetry.sdk.language\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"go\"}},{\"Key\":\"telemetry.sdk.name\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"opentelemetry\"}},{\"Key\":\"telemetry.sdk.version\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"1.23.1\"}}],\"ScopeMetrics\":[{\"Scope\":{\"Name\":\"terragrunt\",\"Version\":\"\",\"SchemaURL\":\"\"},\"Metrics\":[{\"Name\":\"run_bash_duration\",\"Description\":\"\",\"Unit\":\"\",\"Data\":{\"DataPoints\":[{\"Attributes\":[{\"Key\":\"args\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"[-c sleep 2]\"}},{\"Key\":\"command\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"bash\"}},{\"Key\":\"dir\",\"Value\":{\"Type\":\"STRING\",\"Value\":\"/projects/gruntwork/terragrunt-tests/trace-test/mod3\"}}],\"StartTime\":\"2024-02-12T14:38:14.85578658Z\",\"Time\":\"2024-02-12T14:38:17.853165589Z\",\"Count\":1,\"Bounds\":[0,5,10,25,50,75,100,250,500,750,1000,2500,5000,7500,10000],\"BucketCounts\":[0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0],\"Min\":2005,\"Max\":2005,\"Sum\":2005}],\"Temporality\":\"CumulativeTemporality\"}},{\"Name\":\"run_bash_success_count\",\"Description\":\"\",\"Unit\":\"\",\"Data\":{\"DataPoints\":[{\"Attributes\":[],\"StartTime\":\"2024-02-12T14:38:16.860878555Z\",\"Time\":\"2024-02-12T14:38:17.853169359Z\",\"Value\":1}],\"Temporality\":\"CumulativeTemporality\",\"IsMonotonic\":true}}]}]}\n```\n"
  },
  {
    "path": "docs/src/content/docs/06-troubleshooting/03-performance.mdx",
    "content": "---\ntitle: Performance\ndescription: Learn how to improve the performance of Terragrunt\nslug: troubleshooting/performance\nsidebar:\n  order: 3\n---\n\n## Easy Wins\n\nNormally, it's best practice to start by measuring performance before making any changes. This allows you to understand the impact of your changes, and to identify areas for improvement.\n\nHowever, given the nature of the problems that Terragrunt solves, there are some obvious wins that you can make without measuring performance, if you're aware of the tradeoffs.\n\n### Provider Cache Dir\n\nOne of the most expensive things that OpenTofu/Terraform does, from a bandwidth and disk utilization perspective, is download and install providers. These are large binary files that are downloaded from the internet, and not cached across units by default.\n\nIf you're using OpenTofu >= 1.10 and the latest version of Terragrunt, you'll use the [Automatic Provider Cache Dir](/features/caching/auto-provider-cache-dir) feature by default.\n\nThis feature automatically configures OpenTofu to use its built-in provider caching mechanism by setting the `TF_PLUGIN_CACHE_DIR` environment variable to a central location on the filesystem, allowing reuse of downloaded providers across multiple Terragrunt runs.\n\nFor most users at sensible scales, this is an automatic performance win that you don't need to do anything to enable.\n\n### Provider Cache Dir - Gotchas\n\nAt very large scales, you might find that the filesystem lock contention between OpenTofu processes to synchronize access to the provider cache directory is a bottleneck. You might also find that you can't use the provider cache directory because you are storing your provider cache in a shared NFS mount or are using Terraform or an older version of OpenTofu.\n\nIn these scenarios, you can use the [Provider Cache Server](/features/caching/provider-cache-server) feature to improve performance.\n\n### Provider Cache Server\n\nYou can significantly reduce the amount of time taken by Terragrunt runs by enabling the provider cache server, like this:\n\n```shell\nterragrunt run --all plan --provider-cache\n```\n\n#### Provider Cache - Gotchas\n\nThe provider cache server is a single server that is used by all Terragrunt runs being performed in a given Terragrunt invocation. You will see the most benefit if you are using it in a command that will perform many OpenTofu/Terraform operations, like with the `--all` flag and the `--graph` flag.\n\nWhen performing individual runs, like `terragrunt plan`, the provider cache server can be a net negative to performance, because starting and stopping the server might add more overhead than just downloading the providers (or using the [Automatic Provider Cache Dir](/features/caching/auto-provider-cache-dir) feature). Whether this is the case depends on many factors, including network speed, the number of providers being downloaded, and whether or not the providers are already cached in the Terragrunt provider cache.\n\nWhen in doubt, [measure the performance](#measuring-performance) before and after enabling the provider cache server to see if it's a net win for your use case.\n\n### Fetching Output From State\n\nUnder the hood, Terragrunt dependency blocks leverage the OpenTofu/Terraform `output -json` command to fetch outputs from one unit and leverage them in another.\n\nThe OpenTofu/Terraform `output -json` command does a bit more work than simply fetching output values from state, and a significant portion of that slowdown is loading providers, which it doesn't really need in most cases.\n\nYou can significantly improve the performance of dependency blocks by using the [`dependency-fetch-output-from-state`](https://docs.terragrunt.com/reference/experiments/#dependency-fetch-output-from-state) experiment. When the experiment is active, Terragrunt will resolve outputs by directly fetching the backend state file from S3 and parse it directly, avoiding any overhead incurred by calling the `output -json` command of OpenTofu/Terraform.\n\nFor example:\n\n```bash\nterragrunt run --all plan --experiment=dependency-fetch-output-from-state\n```\n\n#### Fetching Output From State - Gotchas\n\nThe first thing you need to be aware of when considering usage of the `dependency-fetch-output-from-state` experiment is that it only works for S3 backends. If you are using a different backend, this experiment won't do anything.\n\nNext, you should be aware that there is no guarantee that OpenTofu/Terraform will maintain the existing schema of their state files, so there is also no guarantee that the flag will work as expected in future versions of OpenTofu/Terraform.\n\nWe are coordinating with the OpenTofu team to improve the performance of the `output` command, and we hope that this flag will be unnecessary for most users in the future.\n\nSee [#1549](https://github.com/opentofu/opentofu/issues/1549) for more details.\n\n## Measuring Performance\n\nBefore diving into any particular performance optimization, it's important to first measure performance, and to make sure that you measure performance after any changes so that you understand the impact of your changes.\n\nTo measure performance, you can use multiple tools, depending on your role.\n\n### End User\n\nAs an end user, you're advised to use the following tools to get a better understanding of the performance of Terragrunt.\n\n#### OpenTelemetry\n\nUse [OpenTelemetry](/troubleshooting/open-telemetry) to collect traces from Terragrunt runs so that you can analyze the performance of individual operations when using Terragrunt.\n\nThis can be useful both to identify bottlenecks in Terragrunt, and to understand when performance changes can be attributed to integrations with other tools, like OpenTofu or Terraform.\n\n#### Benchmark Usage\n\nUse benchmarking tools like [Hyperfine](https://github.com/sharkdp/hyperfine) to run benchmarks of your Terragrunt usage to compare the performance of different versions of Terragrunt, or with different configurations.\n\nYou can use configurations like the `--warmup` flag to do some warmup runs before the actual benchmarking. This is useful to get a more accurate measurement of the performance of Terragrunt with cache populated, etc.\n\nHere's an example of how to use Hyperfine to benchmark the performance of Terragrunt with two different configurations:\n\n```shell\nhyperfine -w 3 -r 5 'terragrunt run --all plan' 'terragrunt run --all plan --experiment=dependency-fetch-output-from-state'\n```\n\n### Terragrunt Developer\n\nAs a Terragrunt developer, you're advised to use the following tools to improve the performance of Terragrunt when improving the codebase.\n\n#### Benchmark Tests\n\nUse Benchmark tests to measure the performance of particular subroutines in Terragrunt.\n\nThese benchmarks give you a good indication of the performance of a particular part of Terragrunt, and can help you identify areas for improvement. You can run benchmark tests like this:\n\n```shell\ngo test -bench=BenchmarkSomeFunction\n```\n\nYou can also run benchmarks with different configurations, like the following for getting memory allocation information as well:\n\n```shell\ngo test -bench=BenchmarkSomeFunction -benchmem\n```\n\nYou can learn more about benchmarking in Go by reading the [official documentation](https://pkg.go.dev/testing#hdr-Benchmarks).\n\n#### Profiling\n\nUse profiling tools like [pprof](https://github.com/google/pprof) to get a more detailed view of the performance of Terragrunt.\n\nFor example, you could use the following command to profile a particular test:\n\n```shell\ngo test -run 'SomeTest' -cpuprofile=cpu.prof -memprofile=mem.prof\n```\n\nYou can then use the `go tool pprof` command to analyze the profile data:\n\n```shell\ngo tool pprof cpu.prof\n```\n\nIt can be helpful to use the web interface to view the profile data using flame graphs, etc.\n\n```shell\ngo tool pprof -http=:8080 cpu.prof\n```\n\nYou can learn more about profiling in Go by reading the [official documentation](https://pkg.go.dev/cmd/pprof).\n"
  },
  {
    "path": "docs/src/content/docs/07-process/01-1-0-guarantees.mdx",
    "content": "---\ntitle: Terragrunt 1.0 Guarantees\ndescription: Stability and compatibility guarantees for Terragrunt 1.0 and the 1.x release line.\nslug: process/1-0-guarantees\nsidebar:\n  order: 1\n---\n\nThe term \"breaking change\" means something different to different people, and every change can, in some way, be a breaking change to someone.\n\n[![Relevant XKCD](https://imgs.xkcd.com/comics/workflow.png)](https://xkcd.com/1172/)\n\n*Relevant XKCD (#1172)*\n\nWith the release of Terragrunt 1.0, there are some concrete compatibility guarantees that you can rely on for the duration of the 1.x line of Terragrunt, which is intended to remain the latest major version of Terragrunt for the foreseeable future. Any violations of these guarantees is a bug that will be fixed in a future 1.x release, or an oversight on the part of maintainers that will result in an update to this document.\n\nThis is a living document that will be updated as questions are answered regarding what is and isn't considered a breaking change in Terragrunt 1.x.\n\n## What compatibility guarantees are being made in 1.0?\n\n### The CLI\n\nCLI commands and flags present in Terragrunt 1.0 will not be removed or renamed during 1.x. Commands and flags will continue to work the same way throughout 1.x.\n\nYou may see deprecation warnings, and new commands or flags may be introduced that let you opt in to different functionality, but running the same commands will produce the same results. New CLI commands and flags introduced during 1.x will also maintain backwards compatibility for the remainder of 1.x.\n\n### HCL Configurations\n\nTerragrunt HCL configurations valid in 1.0 will remain valid throughout 1.x. The HCL parser will not drop support for any block, attribute, or function during 1.x.\n\nYou may see deprecation warnings, and new HCL configurations will continue to be introduced, but using the same configurations will produce the same results. New HCL configurations introduced during 1.x will also maintain backwards compatibility for the remainder of 1.x.\n\n### Run Report\n\nThe [Run Report](/features/stacks/run-report/) generated using the [`--report-file`](/reference/cli/commands/run/#report-file) flag can be parsed using the schema output by [`--report-schema-file`](/reference/cli/commands/run/#report-schema-file). You can also find the schema at the URL listed in the `$id` field, e.g., https://docs.terragrunt.com/schemas/run/report/v4/schema.json.\n\nAny modifications made to the schema that break parsing of existing report files using a modern JSON parser will only be done on an opt-in basis for the duration of 1.x, and you will be able to use the `$id` field of the generated schema to confirm that you are parsing a file with an expected schema.\n\nNote that no guarantee is made that new fields won’t be added to the generated report file. If you are not using a modern deserialization library that can ignore unknown fields, this may cause issues for you, and you’ll have to take note of the `$id` field of the generated `--report-schema-file` to catch any such cases. Modern deserialization libraries in popular programming languages like Golang have built-in support for ignoring unknown fields, and it's expected that most users will be using one such deserialization library or a tool like `jq` to parse reports. As such, new versions of the report schema that _add_ new fields will not be considered breaking, whereas _removing_ fields will be.\n\n### Find\n\nThe [find](/reference/cli/commands/find/) command provides the ability to discover Terragrunt configurations programmatically. Maintaining a stable schema for `find` output is something you will be able to rely on. For the duration of 1.x, you will be able to expect the same output from any usage of the `find` command. You will never have a field in `find` output disappear, or for the general shape of the JSON schema to change.\n\nNew opt-in flags that allow you to discover more about the configurations in your codebase might be introduced, and backwards compatibility of their behavior will be ensured for the duration of 1.x.\n\n### OpenTofu/Terraform stdout/stderr\n\nThere will be special consideration made to ensure that no onerous changes are made to OpenTofu/Terraform stdout/stderr forwarding. You should expect OpenTofu/Terraform stdout/stderr logs to be [enriched](/reference/logging/) the same way when using the same logging configurations, and consistency in whether the output is forwarded to Terragrunt stdout/stderr.\n\n## What compatibility guarantees aren’t being made in 1.0?\n\n### Experimental Features\n\nTerragrunt supports the ability to utilize certain [experimental features](/reference/experiments/) on an opt-in basis. These are features that will explicitly not be given any stability guarantees, and can change in any given release. Big new features in Terragrunt are very likely to be introduced as experiments and iterated on in collaboration with early adopters before they are stabilized, to ensure that they are feasible to support without breaking changes for the duration of 1.x.\n\nNo guarantees are made regarding the stability of experimental features, or even that any given experiment will ever be generally available. Experiments give maintainers the opportunity to iterate quickly without the same stability guarantees made for the rest of Terragrunt.\n\n### List\n\nThe [list](/reference/cli/commands/list/) command differs from the `find` command in that it is designed to be understood by humans (as opposed to being parsed programmatically in `find`), and as such, changes to how information is presented might be encountered during 1.x releases. These changes might be done to improve the legibility of results, or provide additional useful information by default without requiring complex invocations of the `list` command.\n\nNo guarantees that list output will have the same colors, or that list results will be displayed with the same general shape for the duration of 1.x. Any change made in this regard will be made carefully, however, and maintainers will do their best to ensure that they are communicated effectively and opt-in where possible.\n\n### Catalog\n\nThe [catalog](/reference/cli/commands/catalog/) command has a Terminal User Interface (TUI), and is also designed to be understood and interacted with by humans on the terminal. Changes to how information is displayed in the catalog command is subject to change, and stability in the UI is not guaranteed in 1.x releases. You should be able to expect that you won't lose any capabilities in the catalog TUI, however.\n\nNew buttons and columns might appear in future 1.x releases, and you may have to navigate the TUI in different ways. Major changes to TUIs in Terragrunt will occur gradually over multiple releases as experimental features when possible.\n\n### Stdout/Stderr\n\nWith the exception of OpenTofu/Terraform stdout/stderr and certain structured data, like report files and `find` output, no guarantees are made that you will see the exact same stdout/stderr for the same invocation of the Terragrunt CLI. This includes logging. You should not rely on the same log entries or error messages existing from one release of Terragrunt to another, or that they are worded the same way. You also shouldn't rely on the same run summary being printed with the same content.\n\nLog messages including error messages will be continuously adjusted to improve the user experience of using the Terragrunt CLI. This will be done to make it easier to understand what is happening in Terragrunt runs, and troubleshoot errors.\n\n### Bugs\n\nThe most important guarantee that we cannot make in Terragrunt 1.x is that no bugs will be created. It is possible that we will *accidentally* introduce regressions that result in breaking changes to Terragrunt. Those are bugs that will be fixed in a future release of Terragrunt 1.x.\n\nIf you depend on functionality in Terragrunt that is considered to be a bug (either as of 1.0, or introduced in a 1.x release), it is possible that fixing that bug will result in a breaking change to your workflows. Whenever possible, maintainers will be judicious with any such fix, and will attempt to preserve backwards compatibility despite the bug fix via opt-in functionality.\n\n### Integrations\n\nIt is possible that changes to third party software that Terragrunt integrates with will require a breaking change in Terragrunt. In those scenarios, it might not always be possible for the change to be opt-in. These scenarios will be handled on a case-by-case basis, and maintainers endeavor to minimize the impact of these changes to you.\n\n### Performance\n\nThe fastest thing you can do in software is to do nothing at all. In the pursuit of improving performance, maintainers may prevent Terragrunt from doing work understood to be unnecessary. In a large enough user base, *someone* is potentially going to be impacted by changes like this. Whenever possible, maintainers will strive to err on the side of maintaining exactly the same functionality, and only changing functionality if there's little to no justification for how the behavior would be beneficial to Terragrunt users. In the event changes like this cause disruption to users, maintainers will take this disruption very seriously and work to remediate the disruption, or revert the performance improvement.\n\n### Golang Compatibility\n\nMaintainers will frequently update the build system to use the latest version of Golang when compiling Terragrunt. Newer versions of Golang may drop support for older operating systems, which means that newer versions of compiled Terragrunt binaries might no longer run on older operating systems. When the Golang version is upgraded, the `go.mod` file will also be updated accordingly. If you compile Terragrunt from source, you may need to upgrade your local Golang installation to match the version specified in `go.mod`, or manually downgrade the version in `go.mod` to continue compiling Terragrunt using an older Golang version.\n\nIf you are impacted by a Golang version upgrade and would like to request a policy adjustment, please reach out to the maintainers by starting a GitHub discussion or reaching out in the Terragrunt Discord.\n\n### Golang Library Compatibility\n\nUsing Terragrunt as a Go library has no backwards compatibility guarantees. Most Go code in the Terragrunt repository lives in [`internal`](https://github.com/gruntwork-io/terragrunt/tree/main/internal), and maintainers don't expect external parties to depend on Terragrunt packages directly.\n\nWhen packages are generally useful to internal Gruntwork parties, they will be migrated to [`pkg`](https://github.com/gruntwork-io/terragrunt/tree/main/pkg). Breaking changes to packages in `pkg` are still possible at any time, but maintainers will coordinate directly with known consumers to help prevent upgrade issues.\n\nWhen external parties need a stable dependency on shared code, dedicated libraries will be created in separate, versioned repositories (e.g., [terragrunt-engine-go](https://github.com/gruntwork-io/terragrunt-engine-go)). If you are relying on code in `pkg` or vendoring code from `internal`, you are heavily encouraged to start a conversation with Terragrunt maintainers so that we can plan out a path to an external library that you can rely on to be stable and independently versioned from Terragrunt.\n\n## Versioning Policy\n\nFor the duration of 1.x, only the minor and patch versions of Terragrunt releases will be used in the [semantic versioning scheme](https://semver.org/), just like it was in Terragrunt 0.x.\n\nThe difference as of 1.0 is that no backwards incompatible breaking changes will be introduced in any minor release. Bug fixes will continue to be released as patch versions, and minor releases will be used to introduce new [backward compatible](https://en.wikipedia.org/wiki/Backward_compatibility) major features. All [_forward incompatible changes_](https://en.wikipedia.org/wiki/Forward_compatibility) will be released in minor releases, and will usually be supported on an experimental basis before they are generally available.\n\ne.g. If a new flag is going to be introduced, it will likely be an experimental feature introduced in a minor or patch release, then eventually promoted to a generally available feature in a future minor release. Versions of Terragrunt released before that minor release will not be able to use that flag without enabling the experiment, but every version of Terragrunt released afterwards in 1.x will support it.\n\nRelease candidates of minor releases will be published to give users a chance to test out the stability of new releases before they are fully released.\n\n## Deprecations\n\nIf functionality is deprecated in 1.x, it will likely be removed in 2.x. On a case-by-case basis, maintainers may decide to explicitly extend the lifetime of deprecated functionality beyond 2.x.\n\n## Feedback\n\nIf you have any feedback on these guarantees, or would like clarification on something that isn't covered here, please open a [discussion topic](https://github.com/gruntwork-io/terragrunt/discussions) or [create an issue](https://github.com/gruntwork-io/terragrunt/issues) in the Terragrunt GitHub repository.\n"
  },
  {
    "path": "docs/src/content/docs/07-process/02-cli-rules.mdx",
    "content": "---\ntitle: CLI Rules\ndescription: Learn the rules for how the Terragrunt CLI is designed.\nslug: process/cli-rules\nsidebar:\n  order: 2\n---\n\nimport { Aside, Badge, LinkCard } from '@astrojs/starlight/components';\n\nThese are the rules that Terragrunt maintainers endeavor to follow when working on the CLI.\n\nWhenever maintainers break these rules, that is a bug and should be reported. The maintainers will either fix the behavior, or update the rules to reflect the reason for the discrepancy.\n\nIn addition, if you find that a certain pattern that's reliably followed in the CLI is not documented here, please let us know so we can update this document to encourage consistency.\n\n1. The final argument to a Terragrunt command will always be a verb.\n2. All the arguments preceding the final argument will <Badge text=\"usually\" variant=\"caution\" /> be a noun.\n\n    <details>\n      <summary><Badge text=\"Exception\" variant=\"caution\" /></summary>\n\n             The exceptions to this rule are commands like `terragrunt run`, as these will frequently have two **verbs** in sequence (e.g. `terragrunt run plan`).\n\n             This is an exception to the rule because it is exceptional behavior. The end of Terragrunt’s responsibility (from a CLI design perspective) is to process the `run` command, so what follows is not subject to the rules dictated here.\n    </details>\n\n3. All flags will <Badge text=\"usually\" variant=\"caution\" /> start with a noun.\n\n    <details>\n        <summary><Badge text=\"Exception\" variant=\"caution\" /></summary>\n\n        The exception to this rule will be for negation as the flag will start with `no`/ `non` , as discussed below.\n    </details>\n\n    If the flag is controlling a single configuration for Terragrunt, that configuration will be the name of the flag.\n\n    <details>\n        <summary><Badge text=\"Example\" variant=\"note\" /></summary>\n\n        `--working-dir`: Set the `working directory` configuration for Terragrunt.\n    </details>\n\n    If a Terragrunt system can be controlled entirely by referencing the name of the system, or the flag can control high level behavior of Terragrunt on its own, that will be the name of the flag.\n\n    <details>\n        <summary><Badge text=\"Example\" variant=\"note\" /></summary>\n\n        `--provider-cache`: The system is the `provider cache` server. The Provider Cache Server be enabled if this flag is set.\n\n        For brevity, prefer this to flags like `--provider-cache-enable`.\n    </details>\n\n    If a configuration is being set for a system, another noun will follow the name of the system after a dash. The flag will accept a parameter that sets the configuration for that system.\n\n    <details>\n        <summary><Badge text=\"Example\" variant=\"note\" /></summary>\n\n        - `--log-level`: The system is `log`, the `level` is the configuration.\n\n        - `--provider-cache-dir`: The system is the `provider cache` server. The directory is the configuration.\n    </details>\n\n    If an operation will be performed on a system, a verb will follow. If necessary, a noun will follow the verb to indicate what the parameter for that flag corresponds to, or the setting it controls for the operation.\n\n    <details>\n        <summary><Badge text=\"Example\" variant=\"note\" /></summary>\n\n        `--queue-include-unit` - The system is the runner `queue`. The operation being performed to that system is `include`. The parameter of `unit` indicates that the parameter to the flag will be a `unit` being `included` in the `queue`.\n    </details>\n\n4. Behavior on the same systems will always share the same stem.\n\n    <details>\n        <summary><Badge text=\"Example\" variant=\"note\" /></summary>\n        All flags that have to do with the Terragrunt Provider Cache Server will start with `provider-cache`.\n\n        A user looking through the flags available in Terragrunt sorted in alphabetical order will find them right next to each other.\n    </details>\n\n5. All boolean flags will accept an optional parameter of `true` or `false` .\n\n    `true` will <Badge text=\"usually\" variant=\"caution\" /> correspond to the default behavior of the flag, and `false` will correspond to the inverse.\n\n    <details>\n        <summary><Badge text=\"Exception\" variant=\"caution\" /></summary>\n\n        The exception to this rule is when the default behavior of a flag changes.\n\n        For example, the [terragrunt-include-module-prefix](https://docs.terragrunt.com/reference/cli-options/#terragrunt-include-module-prefix) flag was previously opt-in, but users were better served with the behavior being opt-out. To preserve backwards compatibility until the next release, the flag remained, but to use it, users had to set it via `--terragrunt-include-module-prefix=false`.\n\n        In this scenario, whenever applicable, a different flag will be made available that does obey this rule (like [tf-forward-stdout](https://docs.terragrunt.com/reference/cli-options/#tf-forward-stdout)).\n    </details>\n\n    When a flag prevents something from happening that Terragrunt would do by default, it will be proceeded by the prefix `no`/ `non`.\n\n    <details>\n        <summary><Badge text=\"Example\" variant=\"note\" /></summary>\n\n        `--no-color` has a default value of `true`, and setting it to `false` will make it so that the behavior of not setting the flag is active (Terragrunt will emit colorful output).\n\n        The alternative would be to have a `--color` flag, and using that flag to disable color would require that they do something like `--color=false`.\n\n        This would violate the rule that the default behavior of the flag wouldn’t change anything, as Terragrunt emits color by default.\n    </details>\n\n6. Commands and flags will always be backwards compatible until the next major release of Terragrunt.\n\n    This includes instances where behavior violates one of the other rules listed here.\n\n7. If naming a command or flag following these rules would make it harder to understand or longer than it needs to be, the exception will be allowed and documented.\n8. Flags that specifically control the behavior of OpenTofu/Terraform will be prefixed `tf`.\n\n   <details>\n        <summary><Badge text=\"Example\" variant=\"note\" /></summary>\n\n        `--tf-path` controls the path to the OpenTofu/Terraform binary.\n\n    </details>\n\n9. Every flag will have at least one corresponding environment variable that is exactly the same text as the flag, but converted to `SHOUTY_SNAKE_CASE` instead of `kebab-case`, and prefixed with `TG_`.\n\n   When more than one environment variable controls a flag, it will be to support backwards compatibility.\n\n   <details>\n        <summary><Badge text=\"Example\" variant=\"note\" /></summary>\n\n        `iam-assume-role-duration` —> `TG_IAM_ASSUME_ROLE_DURATION`\n   </details>\n\n"
  },
  {
    "path": "docs/src/content/docs/07-process/03-releases.mdx",
    "content": "---\ntitle: Releases\ndescription: Learn about the Terragrunt release process.\nslug: process/releases\nsidebar:\n  order: 3\n---\n\nTerragrunt releases use [semantic versions (semver)](https://semver.org/).\n\nNote that as of 2026/01/27, Terragrunt is still pre-1.0, so breaking changes may still be introduced in minor releases. We will try to minimize these changes as much as possible, but they may still happen.\n\nOnce 1.0 is released, Terragrunt backwards compatibility will be guaranteed for all minor releases (see [Terragrunt 1.0 Guarantees](/process/1-0-guarantees) for details on what constitutes a breaking change).\n\nThis documentation should be updated at that time to reflect the new policy. If it has not, please file a bug report.\n\n### When to Cut a New Release\n\nWhile Terragrunt is still pre-1.0, maintainers will cut a new release whenever a new feature is added or a bug is fixed. Maintainers will exercise their best judgment to determine when a new release is necessary, and bias towards cutting a new release as frequently as possible when in doubt.\n\n### How to Create a New Release\n\nTo release a new version of Terragrunt, go to the [Releases Page](https://github.com/gruntwork-io/terragrunt/releases) and cut a new pre-release off the `main` branch. Ensure that the new release uses the **Set as a pre-release** checkbox initially.\n\nThe GitHub Actions workflow for this repository has been configured to:\n\n1. Automatically detect new tags.\n\n2. Build assets for every OS using that tag as a version number (including binaries, archives, checksums, signature files, etc.).\n\n3. Upload the assets to the release in GitHub.\n\nSee [`.github/workflows/release.yml`](https://github.com/gruntwork-io/terragrunt/blob/main/.github/workflows/release.yml) for details.\n\nFollow the GitHub Actions logs to ensure that the assets are built and uploaded correctly. Once the job is successful, go back to the release, give the release notes a final review, then uncheck the **Set as a pre-release** checkbox and check the **Set as the latest release** checkbox.\n\n### Pre-releases\n\nOccasionally, Terragrunt maintainers will cut a pre-release to get feedback on the UI/UX for a new feature or to give the community a chance to test it in the wild before making it generally available.\n\nAlpha pre-releases are generally cut off a feature branch, in order to keep the `main` branch stable and releasable at all times.\n\nPre-releases are tagged with a pre-release name that looks like the following: `alpha2025022501`, etc. with the following information:\n\n- Channel: e.g. `alpha` (indicating the stability of the release)\n\n  The `alpha` channel has the following meaning in Terragrunt:\n\n  - `alpha`: This release is recommended for testing in non-production environments only. It is intended for testing out new features with stakeholders external to Gruntwork before a general release.\n\n  At the moment, this is really the only channel we need. In the future, we might adjust this to include more channels, such as `beta`, etc.\n\n- Date: e.g. `20250225` (indicating the date the release was cut without dashes or slashes)\n- Incremental number: e.g. `01` (indicating the number of pre-releases cut on that day)\n\nThis pre-release system is subject to change, and maintainers will update this documentation to reflect any changes.\n\nThe current plan for how maintainers are going to handle pre-releases after 1.0 is that:\n\n1. Pre-releases for the `alpha` channel will continue to be cut from feature branches, and use the same naming convention as before.\n2. Pre-releases for the `rc` channel will be cut from the `main` branch, and use a naming convention that looks like `v1.0.0-rc1`, which is incremented for each release candidate.\n\nRelease candidates in the `rc` channel will undergo more thorough testing, both automated and manual.\n\n"
  },
  {
    "path": "docs/src/content/docs/08-migrate/01-migrating-from-root-terragrunt-hcl.md",
    "content": "---\ntitle: Migrating from root terragrunt.hcl\ndescription: Migrate from using a root `terragrunt.hcl` file.\nslug: migrate/migrating-from-root-terragrunt-hcl\nsidebar:\n  order: 1\n---\n\n## Problem\n\nThe recommended best practice for Terragrunt usage was previously that users defined two types of `terragrunt.hcl` files for any significantly large code base:\n\n1. A root `terragrunt.hcl` file that defined the Terragrunt configuration common to all units in the code base.\n2. Child `terragrunt.hcl` files that defined the Terragrunt configuration specific to each [unit](/getting-started/terminology/#unit) in the code base.\n\nThis was a simple pattern that made it very obvious what these files were used for (Terragrunt), and certain Terragrunt features (like `find_in_parent_folders`) assumed this default structure.\n\nOver time, this caused confusion for users of Terragrunt, however. See [#3181](https://github.com/gruntwork-io/terragrunt/issues/3181) for an example of the confusion this has caused.\n\nAt the end of the day, from a functional perspective, it doesn't actually help users to have the root configuration named `terragrunt.hcl`. It makes it more confusing to determine what is shared configuration and what is configuration for a unit.\n\nIt also complicates Terragrunt usage, as commands like `run --all` need to be run from a directory where all child directories will be `terragrunt.hcl` files corresponding to units that need to be run.\n\n## Recommended Solution\n\nTo simplify Terragrunt usage and make it more clear what the root configuration is, it is now recommended that users rename the root `terragrunt.hcl` file to something else (e.g. `root.hcl`).\n\nThis will simplify Terragrunt usage, as you will no longer need to carefully avoid running Terragrunt commands in a way that might make it think the root `terragrunt.hcl` file is unit configuration, and it will make it more obvious to users what is and isn't a unit.\n\nNote that in addition to renaming the root `terragrunt.hcl` file, you will also need to update any Terragrunt configurations that assume the root configuration will be named `terragrunt.hcl`. The most common example of this would be usage of `find_in_parent_folders` without any arguments. By default, this will look for a `terragrunt.hcl` file, so you will need to update this to look for the new root configuration file name.\n\ne.g.\n\n```hcl\n# /some/path/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders()\n}\n```\n\nTo:\n\n```hcl\n# /some/path/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n```\n\n## Additional Considerations\n\nIf you use [Scaffold](/features/catalog/scaffold) and [Catalog](/features/catalog), you may need to use additional flags to control how new units are generated. It was previously a safe assumption that most users would leverage a root `terragrunt.hcl` file, and thus, the default behavior was to generate a new unit that would look for a `terragrunt.hcl` file above it.\n\nYou can use the `--root-file-name` and `--no-include-root` flags of both `catalog` and `scaffold` to explicitly control how new units are generated, and what they will look for as the root configuration file (or if they should look for one at all).\n\ne.g.\n\n```bash\nterragrunt catalog\n```\n\nTo:\n\n```bash\nterragrunt catalog --root-file-name root.hcl\n```\n\n## Strict Control\n\nTo enforce this recommended pattern, you can also enable the [root-terragrunt-hcl](/reference/strict-controls#root-terragrunt-hcl) strict control to throw an error when Terragrunt detects that a root `terragrunt.hcl` file is being used.\n\ne.g.\n\n```bash\nterragrunt plan\n```\n\nTo:\n\n```bash\nterragrunt plan --strict-control=root-terragrunt-hcl\n```\n\nOr:\n\n```bash\nTG_STRICT_CONTROL=root-terragrunt-hcl terragrunt plan\n```\n\nBy enabling the strict control, you will also have the default behavior of `scaffold` and `catalog` commands changed to use `root.hcl` as the default root configuration file name if none are provided.\n\n## Future Behavior\n\nFor now, warnings will be emitted when this pattern is detected in order to encourage users to change to the new pattern, but this behavior will be an explicit error in a future version of Terragrunt.\n\nGiven how long this has been the standard pattern, we want to assure users that they will have a _very_ long time to migrate to this new pattern. For the most part, using the old pattern results in very little practical difference in Terragrunt behavior, assuming Terragrunt usage is already working fine.\n\nAs an explicit promise, Terragrunt will not remove support for the old pattern until at least Terragrunt 2.0, and even then, it will be a removal with a long warning period. You can take your time to migrate to the new pattern for older codebases, and are encouraged to share any feedback you have on this change so that we can make it as smooth a transition as possible for you.\n\n## Frequently Asked Questions\n\n### Could a different default value be used for `find_in_parent_folders` (e.g. `root.hcl`)?\n\nYes, it could, but this would be a different, immediate breaking change as users might have both `root.hcl` files and `terragrunt.hcl` files in their repositories, and this could result in Terragrunt finding the wrong configuration file.\n\nIt also doesn't address a significant part of the problem, which is that the following frequently confuses new users to Terragrunt:\n\n```hcl\ninclude \"root\" {\n  path = find_in_parent_folders()\n}\n```\n\nIt does not communicate _what_ Terragrunt will look to include in parent folders, and having a hidden extra fallback value is not a good pattern for Terragrunt to encourage.\n\nFurthermore, `find_in_parent_folders` _already_ supports a fallback value in the second parameter, when used. Having two different ways to specify a fallback value would be confusing.\n\nLastly, the `root` include does not have any special meaning in Terragrunt, from a functional perspective, it's merely a convention. Terragrunt users do not have to supply a root include, and users can have as many includes as they like. By requiring that users specify the root include filename explicitly, Terragrunt is encouraging users to think about what the root configuration is, and what they want in it.\n\n### Is it better for the root configuration to be named `root.hcl`?\n\nNaming the root file `root.hcl` is the recommended pattern, but it is not a requirement.\n\nOur documentation and examples are updated to reference this new pattern, and following this pattern will allow users to pattern match when writing their own configurations.\n\n### Is there any name I _shouldn't_ use for the root configuration?\n\nThe only names that we would strongly encourage you don't adopt for root configuration is any name that begins with `terragrunt` (e.g. `terragrunt.hcl` or `terragrunt.stack.hcl`).\n\nIt is not formally a reserved name, but there are currently only two special filenames in Terragrunt:\n\n1. `terragrunt.hcl` - The default configuration file name for a Terragrunt unit.\n2. `terragrunt.stack.hcl` - The default configuration file name for a Terragrunt stack.\n\nUsing a name that begins with `terragrunt` could cause confusion for users, as they might expect that Terragrunt has special behavior for files with that name.\n"
  },
  {
    "path": "docs/src/content/docs/08-migrate/02-upgrading-to-terragrunt-0-19-x.md",
    "content": "---\ntitle: Upgrading to Terragrunt 0.19.x\ndescription: Migration guide to upgrade to terragrunt 0.19.x\nslug: migrate/upgrading_to_terragrunt_0.19.x\nsidebar:\n  order: 2\n---\n\n## Background\n\nTerraform 0.12 was released in May, 2019, and it included a few major changes:\n\n1. More strict rules around what can go in a `.tfvars` file. In particular, any variable defined in a `.tfvars` file\n   that does not match a corresponding `variable` definition in your `.tf` files produces an error.\n1. A shift from HCL to HCL2 as the main syntax. This included support for first-class expressions (i.e., using variables\n   and functions without having to wrap everything in `${...}`).\n\nBefore version 0.19.0, Terragrunt had you define its configuration in a `terragrunt = { ... }` variable in\na `terraform.tfvars` file, but due to item (1) this no longer works with Terraform 0.12 and newer. That means we had to\nmove to a new file format. This requires a migration, which is unfortunate, but as a nice benefit, item (2)\ngives us a nicer syntax and new functionality!\n\n## Migration guide\n\nThe following sections outline the steps you may need to take in order to migrate from Terragrunt \\<= v0.18.x\nto Terragrunt 0.19.x and newer:\n\n- [Background](#background)\n- [Migration guide](#migration-guide)\n  - [Move from terraform.tfvars to terragrunt.hcl](#move-from-terraformtfvars-to-terragrunthcl)\n  - [Move input variables into inputs](#move-input-variables-into-inputs)\n  - [Use first-class expressions](#use-first-class-expressions)\n  - [Check attributes vs blocks usage](#check-attributes-vs-blocks-usage)\n  - [Rename a few built-in functions](#rename-a-few-built-in-functions)\n  - [Use older Terraform](#use-older-terraform)\n\nCheck out [this PR in the terragrunt-infrastructure-live-example\nrepo](https://github.com/gruntwork-io/terragrunt-infrastructure-live-example/pull/17) for an example of what the code\nchanges look like.\n\n### Move from terraform.tfvars to terragrunt.hcl\n\nSince Terraform 0.12 has more strict rules about what can go into `terraform.tfvars` files, you now need to move your\nTerragrunt configuration from `terraform.tfvars` to a `terragrunt.hcl` file, removing the `terragrunt = { ... }`\nwrapping along the way.\n\nFor example, if you had the following in `terraform.tfvars`:\n\n```hcl\n# terraform.tfvars\nterragrunt = {\n  terraform {\n    source = \"git::git@github.com:foo/modules.git//frontend-app?ref=v0.0.3\"\n    extra_arguments \"custom_vars\" {\n      commands  = [\"apply\", \"plan\"]\n      arguments = [\"-var\", \"foo=42\"]\n    }\n  }\n  remote_state {\n    backend = \"s3\"\n    config = {\n      bucket         = \"my-terraform-state\"\n      key            = \"${path_relative_to_include()}/terraform.tfstate\"\n      region         = \"us-east-1\"\n      encrypt        = true\n      dynamodb_table = \"my-lock-table\"\n    }\n  }\n}\n```\n\nYou would migrate this to `terragrunt.hcl` as follows:\n\n```hcl\n# terragrunt.hcl\nterraform {\n  source = \"git::git@github.com:foo/modules.git//frontend-app?ref=v0.0.3\"\n  extra_arguments \"custom_vars\" {\n    commands  = [\"apply\", \"plan\"]\n    arguments = [\"-var\", \"foo=42\"]\n  }\n}\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket         = \"my-terraform-state\"\n    key            = \"${path_relative_to_include()}/terraform.tfstate\"\n    region         = \"us-east-1\"\n    encrypt        = true\n    dynamodb_table = \"my-lock-table\"\n  }\n}\n```\n\n### Move input variables into inputs\n\nWhen we were using `terraform.tfvars` files for Terragrunt configuration, we were piggybacking on the fact that\nTerraform [automatically loads variables from tfvars\nfiles](https://www.terraform.io/docs/configuration/variables.html#variable-definitions-tfvars-files) to set variables\nfor our modules:\n\n```hcl\n# terraform.tfvars\n# Terragrunt configuration\nterragrunt = {\n  terraform {\n    # ...\n  }\n  remote_state {\n    # ...\n  }\n}\n# Input variables to set for your Terraform module\ninstance_type  = \"t2.micro\"\ninstance_count = 10\n```\n\nWith the move to `terragrunt.hcl`, we no longer get this behavior for free. However, Terragrunt can simulate this\nbehavior for you if you define your input variables by specifying `inputs = { ... }`:\n\n```hcl\n# terragrunt.hcl\nterraform {\n  # ...\n}\nremote_state {\n  # ...\n}\n# Input variables to set for your Terraform module\ninputs = {\n  instance_type  = \"t2.micro\"\n  instance_count = 10\n}\n```\n\nWhenever you run a Terragrunt command, such as `terragrunt apply`, Terragrunt will make these variables available to\nyour Terraform module as environment variables.\n\n### Use first-class expressions\n\nTerraform 0.11 only allowed special behavior, such as function calls, using \"interpolation syntax,\" where you wrapped\nthe code with `${...}`. Terragrunt included a handful of functions you could call using interpolation syntax, but\n_only_ within the `terragrunt = { ... }` block:\n\n```hcl\n# terraform.tfvars\nterragrunt = {\n  terraform {\n    extra_arguments \"retry_lock\" {\n      # Using a function within interpolation syntax\n      commands  = \"${get_terraform_commands_that_need_locking()}\"\n      arguments = [\"-lock-timeout=20m\"]\n    }\n  }\n}\n# Using interpolation syntax outside of the terragrunt config did NOT work before\nfoo = \"${get_env(\"FOO\", \"default\")}\"\n```\n\nTerraform 0.12 has moved to HCL2, which has first-class support for expressions. That means you can call functions\nwithout having to wrap them in `${...}`. Terragrunt embraces HCL2, and thanks to HCL2's nice parser, that means we not\nonly get first-class expressions, but we can also use those expressions _everywhere_ in `terragrunt.hcl`!\n\n```hcl\n# terragrunt.hcl\nterraform {\n  extra_arguments \"retry_lock\" {\n    # Using a function within first-class expressions!\n    commands  = get_terraform_commands_that_need_locking()\n    arguments = [\"-lock-timeout=20m\"]\n  }\n}\ninputs = {\n  # This now works with Terragrunt 0.19.x and newer!\n  foo = get_env(\"FOO\", \"default\")\n}\n```\n\n### Check attributes vs blocks usage\n\nHCL2 is more strict about the difference between attributes:\n\n```hcl\n# Attributes use an equals sign before the curly brace\nfoo = {\n  bar = \"baz\"\n}\n```\n\nAnd blocks:\n\n```hcl\n# Blocks do not use equal signs before the curly brace\nfoo {\n  bar = \"baz\"\n}\n```\n\nSince Terragrunt uses HCL2, we now have to be more strict with which parts of the Terragrunt configuration are\nattributes and which are blocks:\n\n```hcl\n# terragrunt.hcl\n# terraform is a block, so make sure NOT to include an equals sign\nterraform {\n  source = \"git::git@github.com:foo/modules.git//frontend-app?ref=v0.0.3\"\n  # extra_arguments is a block, so make sure NOT to include an equals sign\n  extra_arguments \"custom_vars\" {\n    commands  = [\"apply\", \"plan\"]\n    arguments = [\"-var\", \"foo=42\"]\n  }\n}\n# remote_state is a block, so make sure NOT to include an equals sign\nremote_state {\n  backend = \"s3\"\n  # config is an attribute, so an equals sign is REQUIRED\n  config = {\n    bucket = \"foo\"\n    # s3_bucket_tags is an attribute, so an equals sign is REQUIRED\n    s3_bucket_tags = {\n      owner = \"terragrunt integration test\"\n      name = \"Terraform state storage\"\n    }\n    # dynamodb_table_tags is an attribute, so an equals sign is REQUIRED\n    dynamodb_table_tags = {\n      owner = \"terragrunt integration test\"\n      name = \"Terraform lock table\"\n    }\n    # accesslogging_bucket_tags is an attribute, so an equals sign is REQUIRED\n    accesslogging_bucket_tags = {\n      owner = \"terragrunt integration test\"\n      name  = \"Terraform access log storage\"\n    }\n  }\n}\n# include is a block, so make sure NOT to include an equals sign\ninclude {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n# dependencies is a block, so make sure NOT to include an equals sign\ndependencies {\n  paths = [\"../vpc\", \"../mysql\", \"../redis\"]\n}\n# Inputs is an attribute, so an equals sign is REQUIRED\ninputs = {\n  instance_type  = \"t2.micro\"\n  instance_count = 10\n}\n```\n\n### Rename a few built-in functions\n\nTwo built-in functions were renamed:\n\n1. `get_tfvars_dir()` is now called `get_terragrunt_dir()`.\n1. `get_parent_tfvars_dir()` is now called `get_parent_terragrunt_dir()`.\n\nMake sure to make the corresponding updates in your `terragrunt.hcl` file!\n\n### Use older Terraform\n\nAlthough it is not officially supported and not tested, it is still possible to use terraform\\<0.12 with terragrunt >=0.19.\n\nJust install a different version of terraform into a directory of your choice outside of `PATH` and specify path to the binary in `terragrunt.hcl` as `terraform_binary`, plus you need to lower the version check constraint:\n\n```hcl\nterraform_binary = \"~/bin/terraform-v11/terraform\"\nterraform_version_constraint = \">= 0.11\"\n```\n"
  },
  {
    "path": "docs/src/content/docs/08-migrate/03-cli-redesign.md",
    "content": "---\ntitle: CLI Redesign\ndescription: Migration guide to adopt changes from RFC #3445\nslug: migrate/cli-redesign\nsidebar:\n  order: 3\n---\n\n## Background\n\nAs part of the redesign in [#3445](https://github.com/gruntwork-io/terragrunt/issues/3445), several CLI adjustments have been made to improve user experience and consistency. This guide will help you understand the changes and how to adapt to them.\n\nNote that this guide is being written while deprecations are in effect, so some of the changes may not be breaking yet. We'll do our best to keep this guide up to date as the changes are finalized. To opt in to breaking changes early, you can use the relevant [strict control](/reference/strict-controls/).\n\nThe high-level changes made as a part of that RFC that require migration for current users are as follows:\n\n- The `terragrunt-` prefix has been removed from all flags.\n- All environment variables have had their prefixes renamed to `TG_` instead of `TERRAGRUNT_`.\n- A new `run` command has been introduced to the CLI that also handles the responsibilities of deprecated `run-all` and `graph` commands.\n- A new `backend` command has been introduced to support users in interacting with backends.\n- Infrequently used commands have been reorganized into a structure that makes it easier to find them.\n- Arguments are no longer sent to `tofu` / `terraform` by default.\n\n## Migration guide\n\nAll of the changes you need to make to adopt to this new CLI design involve changing how you invoke Terragrunt from the command line.\n\n### Remove `terragrunt-` prefix from flags\n\nIf you are currently using flags that are prefixed with `terragrunt-`, you will need to stop using that flag, and use a differently named one instead (usually the same exact flag with `terragrunt-` removed from the beginning, but not always).\n\nFor example, if you are using the `--terragrunt-non-interactive` flag, you will need to switch to the [`--non-interactive`](/reference/cli/global-flags/#non-interactive) flag instead.\n\nBefore:\n\n```bash\nterragrunt plan --terragrunt-non-interactive\n```\n\nAfter:\n\n```bash\nterragrunt plan --non-interactive\n```\n\nSometimes, the flag change might be slightly more involved than simply removing the `terragrunt-` prefix.\n\nFor example, if you are using the `--terragrunt-debug` flag, you will need to switch to the [`--inputs-debug`](/reference/cli/commands/run/#inputs-debug) flag instead.\n\nBefore:\n\n```bash\nterragrunt plan --terragrunt-debug\n```\n\nAfter:\n\n```bash\nterragrunt plan --inputs-debug\n```\n\nYou can find the new flag names in the [CLI reference](/reference/cli/) (including the deprecated flags they replace).\n\n### CLI Flag Migration Table\n\nBelow is a comprehensive mapping of old CLI flag names to their modern counterparts:\n\n| Old Flag                                          | New Flag                                                  |\n|---------------------------------------------------|-----------------------------------------------------------|\n| `--terragrunt-check`                              | `--check`                                                 |\n| `--terragrunt-config`                             | `--config`                                                |\n| `--terragrunt-debug`                              | `--inputs-debug`                                          |\n| `--terragrunt-diff`                               | `--diff`                                                  |\n| `--terragrunt-disable-bucket-update`              | `--disable-bucket-update`                                 |\n| `--terragrunt-disable-command-validation`         | `--disable-command-validation` (deprecated)               |\n| `--terragrunt-download-dir`                       | `--download-dir`                                          |\n| `--terragrunt-exclude-dir`                        | `--queue-exclude-dir`                                     |\n| `--terragrunt-excludes-file`                      | `--queue-excludes-file`                                   |\n| `--terragrunt-fail-on-state-bucket-creation`      | removed (no equivalent; backend provisioning is explicit) |\n| `--terragrunt-fetch-dependency-output-from-state` | `--dependency-fetch-output-from-state`                    |\n| `--terragrunt-forward-tf-stdout`                  | `--tf-forward-stdout`                                     |\n| `--terragrunt-hclfmt-exclude-dir`                 | `--exclude-dir`                                           |\n| `--terragrunt-hclfmt-file`                        | `--file`                                                  |\n| `--terragrunt-hclfmt-stdin`                       | `--stdin`                                                 |\n| `--terragrunt-hclvalidate-json`                   | `--json`                                                  |\n| `--terragrunt-hclvalidate-show-config-path`       | `--show-config-path`                                      |\n| `--terragrunt-iam-assume-role-duration`           | `--iam-assume-role-duration`                              |\n| `--terragrunt-iam-role`                           | `--iam-assume-role`                                       |\n| `--terragrunt-iam-web-identity-token`             | `--iam-assume-role-web-identity-token`                    |\n| `--terragrunt-ignore-dependency-errors`           | `--queue-ignore-errors`                                   |\n| `--terragrunt-ignore-dependency-order`            | `--queue-ignore-dag-order`                                |\n| `--terragrunt-ignore-external-dependencies`       | `--queue-exclude-external` (deprecated)                   |\n| `--terragrunt-include-dir`                        | `--queue-include-dir`                                     |\n| `--terragrunt-include-external-dependencies`      | `--queue-include-external`                                |\n| `--terragrunt-json-disable-dependent-modules`     | `--disable-dependent-modules` (deprecated)                |\n| `--terragrunt-json-out-dir`                       | `--json-out-dir`                                          |\n| `--terragrunt-json-out`                           | `--out`                                                   |\n| `--terragrunt-log-custom-format`                  | `--log-custom-format`                                     |\n| `--terragrunt-log-disable`                        | `--log-disable`                                           |\n| `--terragrunt-log-format`                         | `--log-format`                                            |\n| `--terragrunt-log-level`                          | `--log-level`                                             |\n| `--terragrunt-log-show-abs-paths`                 | `--log-show-abs-paths`                                    |\n| `--terragrunt-modules-that-include`               | `--units-that-include` (deprecated)                       |\n| `--terragrunt-no-auto-approve`                    | `--no-auto-approve`                                       |\n| `--terragrunt-no-auto-init`                       | `--no-auto-init`                                          |\n| `--terragrunt-no-auto-retry`                      | `--no-auto-retry`                                         |\n| `--terragrunt-no-color`                           | `--no-color`                                              |\n| `--terragrunt-no-destroy-dependencies-check`      | `--destroy-dependencies-check`                            |\n| `--terragrunt-out-dir`                            | `--out-dir`                                               |\n| `--terragrunt-parallelism`                        | `--parallelism`                                           |\n| `--terragrunt-provider-cache-dir`                 | `--provider-cache-dir`                                    |\n| `--terragrunt-provider-cache-hostname`            | `--provider-cache-hostname`                               |\n| `--terragrunt-provider-cache-port`                | `--provider-cache-port`                                   |\n| `--terragrunt-provider-cache-registry-names`      | `--provider-cache-registry-names`                         |\n| `--terragrunt-provider-cache-token`               | `--provider-cache-token`                                  |\n| `--terragrunt-provider-cache`                     | `--provider-cache`                                        |\n| `--terragrunt-queue-include-units-reading`        | `--queue-include-units-reading`                           |\n| `--terragrunt-source-map`                         | `--source-map`                                            |\n| `--terragrunt-source-update`                      | `--source-update`                                         |\n| `--terragrunt-source`                             | `--source`                                                |\n| `--terragrunt-strict-include`                     | `--queue-strict-include` (deprecated)                     |\n| `--terragrunt-strict-validate`                    | `--strict-validate`                                       |\n| `--terragrunt-tfpath`                             | `--tf-path`                                               |\n| `--terragrunt-use-partial-parse-config-cache`     | `--use-partial-parse-config-cache`                        |\n| `--terragrunt-working-dir`                        | `--working-dir`                                           |\n| `--terragrunt-non-interactive`                    | `--non-interactive`                                       |\n\n### Update environment variables\n\nIf you are currently using environment variables to configure Terragrunt, you will need to stop using that environment variable, and use a differently named one instead (usually the same exact environment variable with `TERRAGRUNT_` replaced with `TG_`, but not always).\n\nFor example, if you are using the `TERRAGRUNT_NON_INTERACTIVE` environment variable, you will need to switch to the [`TG_NON_INTERACTIVE`](/reference/cli/global-flags/#non-interactive) environment variable instead.\n\nBefore:\n\n```bash\nexport TERRAGRUNT_NON_INTERACTIVE=true\n```\n\nAfter:\n\n```bash\nexport TG_NON_INTERACTIVE=true\n```\n\nSometimes, the environment variable change might be slightly more involved than simply replacing `TERRAGRUNT_` with `TG_`.\n\nFor example, if you are using the `TERRAGRUNT_DEBUG` environment variable, you will need to switch to the [`TG_INPUTS_DEBUG`](/reference/cli/commands/run/#inputs-debug) environment variable instead.\n\nBefore:\n\n```bash\nexport TERRAGRUNT_DEBUG=true\n```\n\nAfter:\n\n```bash\nexport TG_INPUTS_DEBUG=true\n```\n\nYou can find the new environment variable names in the [CLI reference](/reference/cli/) (including the deprecated environment variables they replace).\n\n### Use the new `run` command\n\nDefault behavior change (v0.88.0): Terragrunt no longer forwards unknown commands to OpenTofu/Terraform by default. If you previously ran commands like `terragrunt workspace ls`, use the explicit `run` form instead:\n\n```bash\nterragrunt run -- workspace ls\n```\n\nThe `run` command has been introduced to the CLI to handle the responsibility currently held by the default command in Terragrunt.\n\nIf you want to tell Terragrunt that what you are running is a command in the orchestrated IaC tool (OpenTofu/Terraform), you can use the `run` command to explicitly indicate this.\n\nFor example, if you are currently using the `terragrunt` command to run `plan`, you can switch to the `run plan` command instead.\n\nBefore:\n\n```bash\nterragrunt plan\n```\n\nAfter:\n\n```bash\nterragrunt run plan\n```\n\nNote that certain shortcuts will be supported out of the box, such as `terragrunt plan`, so you can continue to use most `run` commands without the `run` keyword, as you were doing before.\n\nFor example, the following command will continue to work as expected:\n\n```bash\nterragrunt plan\n```\n\nThe commands that will not receive shortcuts are OpenTofu/Terraform commands that are not recommended for usage with Terragrunt, or have a conflict with the Terragrunt CLI API.\n\nFor example, the `workspace` command will not receive a shortcut, as you are encouraged not to use workspaces when working with Terragrunt. Terragrunt manages state isolation for you, so you don't need to use them.\n\nIf you would like to explicitly run a command that does not have a shortcut, you can use the `run` command to do so. We recommend separating Terragrunt flags from OpenTofu/Terraform arguments with `--`:\n\n```bash\nterragrunt run -- workspace ls\n```\n\nSimilarly, commands like `graph` won't be supported as a shortcut, as `graph` is a now deprecated command in the Terragrunt CLI. Supporting it as a shortcut would be misleading, so you can use the `run` command to run it explicitly:\n\n```bash\nterragrunt run graph\n```\n\nYou might want to explicitly indicate that the flag you are using is one for OpenTofu/Terraform, and not a Terragrunt flag. To do this, you can use the `--` argument to explicitly separate the Terragrunt flags from the OpenTofu/Terraform flags:\n\n```bash\nterragrunt run -- apply -auto-approve\n```\n\nThis usually isn't necessary, except when combining a complicated series of flags and arguments, which can be difficult to parse for the CLI.\n\nIn addition to allowing for explicit invocation of OpenTofu/Terraform instead of using shortcuts, the `run` command also takes on the responsibilities of the now deprecated `run-all` and `graph` commands using flags.\n\nFor example, if you are currently using the `terragrunt run-all` command, you can switch to the `run` command with the `--all` flag instead.\n\nBefore:\n\n```bash\nterragrunt run-all plan\n```\n\nAfter:\n\n```bash\nterragrunt run --all plan\n```\n\n### Take advantage of the new `exec` command\n\nPreviously, Terragrunt only supported orchestrating the `tofu` and `terraform` binaries as the main program being executed when Terragrunt was invoked.\n\nWith the introduction of the new [exec](/reference/cli/commands/exec/) command, this is no longer the case. You can now orchestrate any program you want, and integrate it with Terragrunt's ability to fetch outputs, download OpenTofu/Terraform modules, set `inputs`, and more.\n\nFor example, if you want to use Terragrunt to list the contents of an AWS S3 bucket, you can do the following:\n\n```bash\nterragrunt exec -- bash -c 'aws s3 ls s3://$TF_VAR_bucket_name'\n```\n\nWith the following `terragrunt.hcl` file:\n\n```hcl\ninputs = {\n  bucket_name = \"my-bucket\"\n}\n```\n\nTerragrunt will load the `inputs` for the unit, and make them available as `TF_VAR_` prefixed environment variables for the executed command.\n\nThis offers a flexible way to integrate Terragrunt with other tools, besides just OpenTofu/Terraform for simple operational tasks.\n\n### Use the new `backend` capabilities\n\nPreviously, Terragrunt would automatically provision any backend resources defined in the [remote_state](/reference/hcl/blocks#remote_state) block of a `terragrunt.hcl` file.\n\nThis was a source of confusion for many users, as it was potentially performing additional actions that users did not intend without asking for it.\n\nAs part of the CLI Redesign, Terragrunt now supports a dedicated [backend command](/reference/cli/commands/backend/bootstrap/) to handle processes involved with interacting with OpenTofu/Terraform backends.\n\nThis includes the ability to bootstrap (provision) backend resources, migrate state between backend state files, and delete backend state files.\n\nIn addition, the `--backend-bootstrap` flag has been introduced, which preserves the legacy behavior of automatically provisioning backend resources. As this flag requires explicit opt in, you will want to explicitly set this flag (or the corresponding environment variable `TG_BACKEND_BOOTSTRAP` to `true`) if you want to continue to have Terragrunt automatically provision backend resources.\n\nBefore:\n\n```bash\nterragrunt plan\n```\n\nAfter:\n\n```bash\nterragrunt plan --backend-bootstrap\n```\n\n### Use the new `find` and `list` commands\n\nThe [find](/reference/cli/commands/find/) and [list](/reference/cli/commands/list/) commands have been introduced to help you discover configurations in your Terragrunt projects.\n\nThe `find` command is useful when you want to perform programmatic discovery of a Terragrunt unit or configuration of that unit, and the `list` command is useful when you want to get a high-level overview of the Terragrunt units and configurations in your project.\n\nIf you are currently using the `output-module-groups` command, you can switch to the `find --dag --json` command to get a more fine grained outlook on the nature of your Terragrunt configurations. You can also use the `list --dag --tree` command to get a better overview of how your units interact in the Directed Acyclic Graph (DAG) of Terragrunt units.\n\nBefore:\n\n```bash\nterragrunt output-module-groups\n{\n  \"Group 1\": [\n    \"/absolute/path/to/vpc\"\n  ],\n  \"Group 2\": [\n    \"/absolute/path/to/db\"\n  ],\n  \"Group 3\": [\n    \"/absolute/path/to/ec2\"\n  ]\n}\n```\n\nAfter:\n\n```bash\nterragrunt find --dag --json\n[\n  {\n    \"type\": \"unit\",\n    \"path\": \"vpc\"\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"db\"\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"ec2\"\n  }\n]\n```\n\n```bash\nterragrunt list --dag --tree\n.\n╰── vpc\n    ├── db\n    │   ╰── ec2\n    ╰── ec2\n```\n\nYou might be wondering why these commands no longer reference \"Groups\". That's because the concurrency model of Terragrunt will change in a future release (see [#3629](https://github.com/gruntwork-io/terragrunt/issues/3629)), and at that point, Terragrunt will no longer run units in \"Groups\" of units, but instead run each unit when it is free to run. If you are currently relying on the `output-module-groups` to programmatically determine when units can run, you'll want to switch to using the `find --dag --json --dependencies` command to get a detailed breakdown of dependencies between units, and use that information to determine when units can run.\n\n```bash\nterragrunt find --dag --json --dependencies\n[\n  {\n    \"type\": \"unit\",\n    \"path\": \"vpc\"\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"db\",\n    \"dependencies\": [\n      \"vpc\"\n    ]\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"ec2\",\n    \"dependencies\": [\n      \"vpc\",\n      \"db\"\n    ]\n  }\n]\n```\n\n### Use the newly renamed commands\n\nAside from the adjustments listed above, you'll also want to replace usage of deprecated commands with their newly renamed counterparts.\n\n- `hclfmt` (use `hcl fmt` instead)\n- `hclvalidate` (use `hcl validate` instead)\n- `validate-inputs` (use `hcl validate --inputs` instead)\n- `terragrunt-info` (use `info print` instead)\n- `render-json` (use `render --json -w` instead)\n- `graph-dependencies` (use `dag graph` instead)\n- `run-all` (use `run --all` instead)\n- `graph` (use `run --graph` instead)\n"
  },
  {
    "path": "docs/src/content/docs/08-migrate/04-terragrunt-stacks.mdx",
    "content": "---\ntitle: Terragrunt Stacks\ndescription: Migration guide to migrate to Terragrunt Stacks\nslug: migrate/terragrunt-stacks\nsidebar:\n  order: 4\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\nimport { Aside } from '@astrojs/starlight/components';\n\n## Migrating from the `terragrunt-infrastructure-live-example` repository\n\nIf you have an existing repository that was started using the [terragrunt-infrastructure-live-example](https://github.com/gruntwork-io/terragrunt-infrastructure-live-example) repository as a starting point, follow the steps below to migrate your existing configurations to take advantage of the patterns available using Terragrunt Stacks.\n\n### Step 1: Assess your current infrastructure\n\nBefore you get started adjusting any of your existing configurations, it's important to understand the current state of your infrastructure.\n\nHow much of it do you regularly update? Does any of it result in frustration or difficulty? Why?\n\nDetermine whether it's a good time to be migrating your infrastructure to new patterns, and if so, how much of it you're willing to migrate. If you are happy, and successful with your current patterns, you may not need to migrate any existing configuration, and that's great! Consider this a best practice that you can adopt when you start to introduce new infrastructure, and that you may want to adjust your existing infrastructure configurations over time to take advantage of new patterns.\n\nThe advantages of using the new paradigm with Terragrunt Stacks are:\n\n- You can more easily manage your infrastructure at scale.\n- You can more easily manage your infrastructure in different environments.\n- You can more easily manage your infrastructure across multiple accounts and regions.\n- You can more easily manage your infrastructure across multiple teams and organizations.\n\nWe, at Gruntwork, generally consider this paradigm to be the best practice for managing Infrastructure as Code (IaC) at scale, which is why we've created this migration guide to help you transition to it.\n\nIf you get overwhelmed at any point, read the [Support docs](/community/support/) to learn how you can get help.\n\n### Step 2: Update your Terragrunt version\n\nNow that you've determined that you want to migrate some or all of your infrastructure to new patterns, the next step is to ensure that you have a version of Terragrunt that supports the `terragrunt.stack.hcl` file.\n\nYou can do this by updating the version of Terragrunt you use to the latest available version. If you would like more information on how to update your Terragrunt version, see the [Installation](/getting-started/install/) guide.\n\n### Step 3: Add `.terragrunt-stack` directories to your repository `.gitignore` file\n\nNow that you're adopting Terragrunt Stacks, you'll want to add the `.terragrunt-stack` directories to your repository `.gitignore` file.\n\n```bash\necho \".terragrunt-stack\" >> .gitignore\ngit add .gitignore\ngit commit -m \"Add .terragrunt-stack to .gitignore\"\n```\n\nThis will prevent you from accidentally committing `.terragrunt-stack` directories to your repository, which is good because you can always regenerate them on demand using the `terragrunt stack generate` command.\n\nAll other `terragrunt stack` commands also automatically generate the `.terragrunt-stack` directory on demand, so you can safely ignore it.\n\n### Step 4: Re-define existing infrastructure using `terragrunt.stack.hcl` files\n\nThe infrastructure that you already have can be re-defined using `terragrunt.stack.hcl` files, reducing the amount of code that you need to maintain in your repository.\n\nTo do this, you'll need to:\n\n{/*\n\n    We have a repository here: [infrastructure-catalog](https://github.com/gruntwork-io/terragrunt-infrastructure-catalog-example), but we have not yet published it, so we'll just be vague about this for now.\n\n    Units example link: [units](https://github.com/gruntwork-io/terragrunt-infrastructure-catalog-example/tree/main/units)\n    Usage example link: [terragrunt.stack.hcl examples](https://github.com/gruntwork-io/terragrunt-infrastructure-catalog-example/tree/main/examples/terragrunt/stacks)\n\n*/}\n\n1. Create an `infrastructure-catalog` repository if you don't already have one to store your infrastructure configurations.\n2. Define the units that you want to reproduce from your `infrastructure-catalog` repository in your `infrastructure-live` repository via `terragrunt.stack.hcl` files.\n3. Find a collection of units that you want to abstract into a stack, and define a `terragrunt.stack.hcl` file for them.\n\n   For example, say you have a collection of units like this, that you want to abstract into a stack:\n\n   <FileTree>\n\n   - non-prod\n     - us-east-1\n       - stateful-ec2-asg-service\n         - service\n            - terragrunt.hcl\n         - db\n            - terragrunt.hcl\n         - sgs\n            - asg\n                - terragrunt.hcl\n\n   </FileTree>\n\n   This collection of units can be abstracted into a single stack by creating a `terragrunt.stack.hcl` file in the `stateful-ec2-asg-service` directory that references each unit configuration, as defined in your `infrastructure-catalog` repository (in this example, the `infrastructure-catalog` repository is hosted at `git@github.com:acme/infrastructure-catalog.git`):\n\n   ```hcl\n    ## non-prod/us-east-1/stateful-ec2-asg-service/terragrunt.stack.hcl\n\n   unit \"service\" {\n     source = \"git::git@github.com:acme/infrastructure-catalog.git//units/ec2-asg-stateful-service?ref=v1.0.0\"\n     path   = \"service\"\n\n     no_dot_terragrunt_stack = true\n\n     ## Add any additional configuration for the service unit here\n   }\n\n   unit \"db\" {\n     source = \"git::git@github.com:acme/infrastructure-catalog.git//units/mysql?ref=v1.0.0\"\n     path   = \"db\"\n\n     no_dot_terragrunt_stack = true\n\n     ## Add any additional configuration for the db unit here\n   }\n\n   unit \"asg-sg\" {\n     source = \"git::git@github.com:acme/infrastructure-catalog.git//units/security-group?ref=v1.0.0\"\n     path   = \"sgs/asg\"\n\n     no_dot_terragrunt_stack = true\n\n     ## Add any additional configuration for the asg-sg unit here\n   }\n   ```\n\n   **Note the use of the `no_dot_terragrunt_stack` attribute.** This is used to prevent Terragrunt from automatically generating the units into a `.terragrunt-stack` directory. This is important, because you are probably using `path_relative_to_include()` in the `key` attribute of the `remote_state` block of the root `root.hcl` file, which is included in every unit. By specifying `no_dot_terragrunt_stack = true`, the generated units will be generated into the same directory as they were before, and the `path_relative_to_include()` function will resolve to the same path as before. Migrating to a `terragrunt.stack.hcl` file in this way allows you to migrate your infrastructure to the new patterns outlined here at your own pace, and to migrate state between the old and new patterns if you want to.\n\n   Now, you can remove the existing unit configurations, and regenerate them on demand using the `terragrunt stack generate` command.\n\n   ```bash\n   cd non-prod/us-east-1/stateful-ec2-asg-service\n   rm -rf service db sgs\n   ```\n\n   If you have identical unit configurations after performing the following, you can remove the unit configurations again, add them to a `.gitignore` file, and commit the new `terragrunt.stack.hcl` file.\n\n   ```bash\n   terragrunt stack generate\n   ```\n\n   <FileTree>\n\n   - non-prod\n     - us-east-1\n       - stateful-ec2-asg-service\n         - **terragrunt.stack.hcl**\n         - service\n            - terragrunt.hcl < This should be identical to the unit configuration before\n         - db\n            - terragrunt.hcl < This should be identical to the unit configuration before\n         - sgs\n            - asg\n                - terragrunt.hcl < This should be identical to the unit configuration before\n\n   </FileTree>\n\n\n   Now that you've confirmed generation is working, you can remove the unit configurations again, add them to a `.gitignore` file, and commit the new `terragrunt.stack.hcl` file.\n\n   ```bash\n   rm -rf service db sgs\n   git add terragrunt.stack.hcl service db sgs\n   git commit -m \"Remove unit configurations and add terragrunt.stack.hcl\"\n   echo \"service\" >> .gitignore\n   echo \"db\" >> .gitignore\n   echo \"sgs\" >> .gitignore\n   git add .gitignore\n   git commit -m \"Add unit configurations to .gitignore\"\n   ```\n\n   Your repository should now look like this:\n\n   <FileTree>\n\n   - non-prod\n     - us-east-1\n       - stateful-ec2-asg-service\n          - **.gitignore**\n          - terragrunt.stack.hcl\n\n   </FileTree>\n\n\n   You can repeat this process as much as you want, abstracting more and more of your infrastructure into Terragrunt Stacks.\n\n### Step 5: Remove reliance on the `_envcommon` directory\n\nThe `_envcommon` directory is no longer needed to create \"Don't Repeat Yourself\" (DRY) configurations with Terragrunt, and is no longer recommended as a best practice.\n\nIf you would like to remove usage of the `_envcommon` directory, you can do so by replacing usage of the `include` block referencing the `_envcommon` directory with content directly committed to `terragrunt.hcl` files.\n\nFor example, say you have a `terragrunt.hcl` file that looks like this:\n\n```hcl\n## non-prod/us-east-1/mysql/terragrunt.hcl\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ninclude \"envcommon\" {\n  path = \"${dirname(find_in_parent_folders(\"root.hcl\"))}/_envcommon/mysql.hcl\"\n  expose = true\n}\n\nterraform {\n  source = \"${include.envcommon.locals.base_source_url}?ref=v0.8.0\"\n}\n\ninputs = {\n  instance_class    = \"db.t2.medium\"\n  allocated_storage = 100\n}\n```\n\nand an `_envcommon/mysql.hcl` file that looks like this:\n\n```hcl\n## _envcommon/mysql.hcl\n\nlocals {\n  environment_vars = read_terragrunt_config(find_in_parent_folders(\"env.hcl\"))\n\n  env = local.environment_vars.locals.environment\n\n  base_source_url = \"git::git@github.com:acme/infrastructure-catalog.git//modules/mysql\"\n}\n\ninputs = {\n  name              = \"mysql_${local.env}\"\n  instance_class    = \"db.t2.micro\"\n  allocated_storage = 20\n  storage_type      = \"standard\"\n  master_username   = \"admin\"\n}\n```\n\nThis pattern was previously used to create \"Don't Repeat Yourself\" (DRY) configurations with Terragrunt. However, this pattern is no longer recommended as a best practice, and is no longer needed to create DRY configurations with Terragrunt.\n\nInstead, you can create a `terragrunt.hcl` file in your `infrastructure-catalog` repository that looks like this:\n\n```hcl\n## units/mysql/terragrunt.hcl\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"git::git@github.com:acme/infrastructure-catalog.git//modules/mysql?ref=${values.version}\"\n}\n\ninputs = {\n  ## Required inputs\n  name              = values.name\n  instance_class    = values.instance_class\n  allocated_storage = values.allocated_storage\n  storage_type      = values.storage_type\n  master_username   = values.master_username\n  master_password   = values.master_password\n\n  ## Optional inputs\n  skip_final_snapshot = try(values.skip_final_snapshot, null)\n  engine_version      = try(values.engine_version, null)\n}\n```\n\nThen reference that `terragrunt.hcl` file in your `terragrunt.stack.hcl` files, like so:\n\n```hcl\n## non-prod/us-east-1/terragrunt.stack.hcl\n\nunit \"mysql\" {\n  source = \"git::git@github.com:acme/infrastructure-catalog.git//units/mysql?ref=v1.0.0\"\n  path   = \"mysql\"\n\n  ## As discussed above, this prevents Terragrunt from automatically generating the units into a `.terragrunt-stack` directory.\n  no_dot_terragrunt_stack = true\n\n  values = {\n    version = \"v0.8.0\"\n    name = \"mysql_dev\"\n    instance_class = \"db.t2.micro\"\n    allocated_storage = 20\n    storage_type = \"standard\"\n    master_username = \"admin\"\n  }\n}\n```\n\nNow, all your unit configurations can be found directly in the `terragrunt.hcl` file in the `infrastructure-catalog` repository, without having to bounce around between different included or referenced files, and you have an explicit interface for the values that can be set externally, via the `values` attribute.\n\nDifferent environments can pin different versions of the unit, and that allows for easy atomic updates (and rollbacks) of both OpenTofu/Terraform module versions _and_ Terragrunt unit configurations if needed.\n\n<Aside type=\"tip\">\n\nThis is one of the main reasons why we recommend using Terragrunt Stacks over the old `_envcommon` directory pattern.\n\nIn the old `_envcommon` directory pattern, there was no simple way to version the shared configuration referenced by all units in your `live` repository. All units always referenced the same version of the shared configuration in `_envcommon`. Now that you're using Terragrunt Stacks, you can use Git tags to version the shared configuration you reference in your `terragrunt.stack.hcl` files, and different environments can pin the version of the shared configuration they want to use.\n\n</Aside>\n\n### Step 6: Update your CI/CD pipeline\n\nChances are, if you're currently performing Terragrunt updates via a CI/CD pipeline (and you aren't using [Gruntwork Pipelines](https://www.gruntwork.io/platform/pipelines)), your CI/CD pipeline doesn't yet have integration with Terragrunt Stacks.\n\nThere are a few options for how to proceed here.\n\n{/*\n    Note to maintainers: This is basically ready to go, but we're not announcing it yet, so we'll leave this commented out for now.\n\n1. You can start using [Gruntwork Pipelines](https://www.gruntwork.io/platform/pipelines).\n\n   This is a little self-serving, as it's a paid product that we offer at Gruntwork, but we are constantly refining it as the best way to manage IaC at scale via GitOps, and we've built first-class support for Terragrunt Stacks into it, so it's the most straightforward way to get Terragrunt Stacks support out of the box.\n*/}\n\n1. You can simply commit the generated `.terragrunt-stack` directories to your repository.\n\n   This is the easiest option when managing CI/CD yourself, but it also means that you won't gain some of the benefits that come from using Terragrunt Stacks. When getting started, however, this is a good way to avoid the additional technical debt that comes from having to update your CI/CD pipeline to support Terragrunt Stacks, while learning how to use `terragrunt.stack.hcl` files, and reorganizing your infrastructure configurations.\n\n   To do this, remove the `.terragrunt-stack` entry from your `.gitignore` file, and commit the changes to your repository. You can then manually run the `terragrunt stack generate` command to generate the `.terragrunt-stack` directories on demand, and commit them to your repository, allowing your CI/CD pipeline to completely ignore the fact that you're using Terragrunt Stacks. The units generated by the `terragrunt stack generate` command are completely compatible with units that you can author manually, so you don't have to worry about any incompatibility issues that might arise from this approach.\n\n1. You can configure your CI/CD pipeline to run the `terragrunt stack generate` command whenever your pipeline runs, and leverage the generated `.terragrunt-stack` directories in your pipeline.\n\n   Depending on the complexity of your CI/CD pipeline, this might be as simple as performing the following:\n\n   ```bash\n   terragrunt stack generate\n   terragrunt run --all plan/apply --non-interactive\n   ```\n\n   This doesn't account for destroys or the reduction of blast radius for changes by carefully inspecting Git diffs, but it's a good start for users that don't have CI/CD pipelines that are too complex.\n\n   There is an open RFC in GitHub ([Filter Flag](https://github.com/gruntwork-io/terragrunt/issues/4060)) that would allow for this kind of complex filtering out of the box with Terragrunt, but at the moment, it's still an open RFC.\n"
  },
  {
    "path": "docs/src/content/docs/08-migrate/05-bare-include.md",
    "content": "---\ntitle: Bare Include\ndescription: Migration guide to avoid using bare includes\nslug: migrate/bare-include\nsidebar:\n  order: 5\n---\n\n## Migrating from bare includes\n\nThe earliest form of include support in Terragrunt was a bare include.\n\ne.g.\n\n```hcl\n# terragrunt.hcl\n\ninclude {\n    path = find_in_parent_folders(\"root.hcl\")\n}\n```\n\nOnce Terragrunt supported the ability to define multiple includes, and to expose the values in includes as variables, users could optionally use named includes instead of a bare include.\n\ne.g.\n\n```hcl\n# terragrunt.hcl\n\ninclude \"root\" {\n    path = find_in_parent_folders(\"root.hcl\")\n}\n```\n\nHCL parsing does not support the ability to parse HCL configuration and accept that a configuration block has zero or more attributes, so a workaround in Terragrunt internals was to parse the configuration, then rewrite it internally to avoid breaking backwards compatibility for bare includes.\n\ne.g.\n\n```hcl\n# terragrunt.hcl\n\ninclude {\n    path = find_in_parent_folders(\"root.hcl\")\n}\n```\n\nbecomes:\n\n```hcl\n# terragrunt.hcl\n\ninclude \"\" {\n    path = find_in_parent_folders(\"root.hcl\")\n}\n```\n\nEspecially on large projects, this extra work is not worth the performance penalty, and Terragrunt has deprecated support for bare includes.\n\nIn a future version of Terragrunt, users will be required to use named includes for all includes.\n"
  },
  {
    "path": "docs/src/content/docs/08-migrate/06-deprecated-attributes.mdx",
    "content": "---\ntitle: Migrating from Deprecated Attributes\ndescription: Learn how to migrate from the deprecated skip and retryable_errors attributes to their modern replacements.\nslug: migrate/deprecated-attributes\nsidebar:\n  order: 6\n---\n\nThis guide explains how to migrate from the deprecated `skip` and `retryable_errors` attributes to their modern, more powerful replacements.\n\nTerragrunt has deprecated two attributes in favor of more flexible block-based configurations:\n\n- **`skip`** → Use the **`exclude`** block instead\n- **`retryable_errors`** → Use the **`errors`** block with **`retry`** sub-blocks instead\n\nThese new blocks provide more granular control and composability compared to the simple attributes they replace.\n\n## Migrating from `skip` to `exclude`\n\n### Why was `skip` deprecated?\n\nThe `skip` attribute was a simple boolean that would exclude a unit from the run queue. The new `exclude` block provides much more flexibility:\n\n- Exclude the unit only for specific OpenTofu/Terraform commands (e.g., only `plan` but not `apply`)\n- Use conditional logic to determine when to exclude the unit\n- Combine multiple conditions\n- Better integration with other Terragrunt features\n\n### Basic Migration\n\n**Before:**\n```hcl\nskip = true\n```\n\n**After:**\n```hcl\nexclude {\n  if = true\n  actions = [\"all\"]\n}\n```\n\n### Conditional Skip\n\n**Before:**\n```hcl\nskip = get_env(\"ENVIRONMENT\") == \"production\"\n```\n\n**After:**\n```hcl\nexclude {\n  if = get_env(\"ENVIRONMENT\") == \"production\"\n  actions = [\"all\"]\n}\n```\n\n### Skip Specific Actions\n\nThe new `exclude` block allows you to exclude the unit only for specific OpenTofu/Terraform commands:\n\n```hcl\nexclude {\n  if = get_env(\"SKIP_DESTROY\") == \"true\"\n  actions = [\"destroy\"]\n}\n```\n\nThis wasn't possible with the old `skip` attribute!\n\n## Migrating from `retryable_errors` to `errors` Block\n\n### Why was `retryable_errors` deprecated?\n\nThe `retryable_errors` attribute was a simple list of error patterns. The new `errors` block with `retry` sub-blocks provides:\n\n- **Multiple retry configurations** with different patterns and settings\n- **Named retry blocks** for better documentation\n- **Per-retry configuration** of max attempts and sleep intervals\n- **Composability** - combine multiple retry strategies\n- **Better organization** for complex retry logic\n\n### Basic Migration\n\n**Before:**\n```hcl\nretryable_errors = [\n  \".*Error: transient network issue.*\",\n  \".*Error: timeout.*\"\n]\n\nretry_max_attempts     = 3\nretry_sleep_interval_sec = 5\n```\n\n**After:**\n```hcl\nerrors {\n  retry \"transient_errors\" {\n    retryable_errors = [\n      \".*Error: transient network issue.*\",\n      \".*Error: timeout.*\"\n    ]\n    max_attempts = 3\n    sleep_interval_sec = 5\n  }\n}\n```\n\n### Using Default Retryable Errors\n\nIf you were using the `get_default_retryable_errors()` function:\n\n**Before:**\n```hcl\nretryable_errors = concat(\n  get_default_retryable_errors(),\n  [\".*custom error.*\"]\n)\n```\n\n**After:**\n```hcl\nerrors {\n  retry \"default_errors\" {\n    retryable_errors = get_default_retryable_errors()\n    max_attempts = 3\n    sleep_interval_sec = 5\n  }\n\n  retry \"custom_errors\" {\n    retryable_errors = [\".*custom error.*\"]\n    max_attempts = 5\n    sleep_interval_sec = 10\n  }\n}\n```\n\nNote: The `get_default_retryable_errors()` function still works and returns the default list for use within the `errors` block.\n\n### Multiple Retry Strategies\n\nThe new `errors` block allows you to define different retry strategies for different types of errors:\n\n```hcl\nerrors {\n  # Quick retries for transient network issues\n  retry \"network_errors\" {\n    retryable_errors = [\n      \".*connection reset.*\",\n      \".*timeout.*\"\n    ]\n    max_attempts = 5\n    sleep_interval_sec = 2\n  }\n\n  # Slower retries for rate limiting\n  retry \"rate_limit_errors\" {\n    retryable_errors = [\n      \".*rate limit exceeded.*\",\n      \".*too many requests.*\"\n    ]\n    max_attempts = 10\n    sleep_interval_sec = 30\n  }\n\n  # Few retries for potential transient API issues\n  retry \"api_errors\" {\n    retryable_errors = [\n      \".*internal server error.*\"\n    ]\n    max_attempts = 3\n    sleep_interval_sec = 15\n  }\n}\n```\n\nThis level of granularity wasn't possible with the old `retryable_errors` attribute!\n\n## Error Messages\n\nIf you try to use the deprecated attributes, Terragrunt will fail with an HCL parsing error:\n\n**For `skip` attribute:**\n```\nError: Unsupported argument\n\n  on terragrunt.hcl line 2:\n   2: skip = true\n\nAn argument named \"skip\" is not expected here.\n```\n\n**For `retryable_errors` attribute:**\n```\nError: Unsupported argument\n\n  on terragrunt.hcl line 4:\n   4: retryable_errors = [\".*Error: transient.*\"]\n\nAn argument named \"retryable_errors\" is not expected here.\n```\n\nThese errors indicate that the attributes have been completely removed from Terragrunt. Please refer to the migration examples below.\n\n## How Retry Errors Are Collected\n\nWhen you define multiple `retry` blocks within the `errors` block, Terragrunt automatically collects **all** the `retryable_errors` patterns from all retry blocks and uses them for error matching.\n\n**Example:**\n```hcl\nerrors {\n  retry \"network_errors\" {\n    retryable_errors = [\".*timeout.*\", \".*connection reset.*\"]\n    max_attempts = 5\n    sleep_interval_sec = 2\n  }\n\n  retry \"api_errors\" {\n    retryable_errors = [\".*rate limit.*\", \".*429.*\"]\n    max_attempts = 10\n    sleep_interval_sec = 30\n  }\n}\n```\n\nIn this example, Terragrunt will retry on any error matching:\n- `.*timeout.*`\n- `.*connection reset.*`\n- `.*rate limit.*`\n- `.*429.*`\n\nEach retry block can have its own `max_attempts` and `sleep_interval_sec`,\nallowing fine-grained control over retry behavior for different error types—for example,\none block can retry at 2-second intervals while another uses 30-second intervals.\n\n## Further Reading\n\n- [Exclude Block Reference](/reference/hcl/blocks#exclude)\n- [Errors Block Reference](/reference/hcl/blocks#errors)\n- [All Attributes Reference](/reference/hcl/attributes)\n"
  },
  {
    "path": "docs/src/content.config.ts",
    "content": "import { defineCollection, z } from 'astro:content';\nimport { docsLoader } from '@astrojs/starlight/loaders';\nimport { docsSchema } from '@astrojs/starlight/schema';\nimport { glob, file } from 'astro/loaders';\n\nconst commands = defineCollection({\n\tloader: glob({ pattern: \"**/*.mdx\", base: \"src/data/commands\" }),\n\tschema: z.object({\n\t\tname: z.string(),\n\t\tdescription: z.string(),\n\t\tpath: z.string().regex(/^[a-z0-9-/]+$/),\n\t\tcategory: z.enum([\n\t\t\t\"main\",\n\t\t\t\"backend\",\n\t\t\t\"stack\",\n\t\t\t\"catalog\",\n\t\t\t\"discovery\",\n\t\t\t\"configuration\",\n\t\t\t\"shortcuts\",\n\t\t]),\n\t\tsidebar: z.object({\n\t\t\tparent: z.string().optional(),\n\t\t\torder: z.number(),\n\t\t}),\n\t\tusage: z.string(),\n\t\texamples: z.array(z.object({\n\t\t\tcode: z.string(),\n\t\t\tdescription: z.string().optional(),\n\t\t})),\n\t\tflags: z.array(z.string()).optional(),\n\t\texperiment: z.object({\n\t\t\tcontrol: z.string(),\n\t\t\tname: z.string(),\n\t\t}).optional(),\n\t}),\n});\n\nconst docs = defineCollection({\n\tloader: docsLoader(),\n\tschema: docsSchema(),\n});\n\nconst flags = defineCollection({\n\tloader: glob({ pattern: \"**/*.mdx\", base: \"src/data/flags\" }),\n\tschema: z.object({\n\t\tname: z.string(),\n\t\tdescription: z.string(),\n\t\tdefaultVal: z.string().optional(),\n\t\ttype: z.string(),\n\t\tenv: z.array(z.string()).optional(),\n\t\taliases: z.array(z.string()).optional(),\n\t}),\n});\n\nconst compatibility = defineCollection({\n\tloader: file(\"src/data/compatibility/compatibility.json\"),\n\tschema: z.object({\n\t\tid: z.string(),\n\t\ttool: z.enum([\"opentofu\", \"terraform\"]),\n\t\tversion: z.string(),\n\t\tterragrunt_min: z.string(),\n\t\tterragrunt_max: z.string().nullable(),\n\t\torder: z.number(),\n\t}),\n});\n\nexport const collections = { commands, compatibility, docs, flags };\n"
  },
  {
    "path": "docs/src/data/commands/backend/bootstrap.mdx",
    "content": "---\nname: bootstrap\npath: backend/bootstrap\ncategory: backend\nsidebar:\n  order: 300\ndescription: Bootstrap OpenTofu/Terraform backend infrastructure.\nusage: |\n  Bootstrap OpenTofu/Terraform backend infrastructure.\nexamples:\n  - description: |\n      Provision backend resources defined in remote_state.\n    code: |\n      terragrunt backend bootstrap\nflags:\n  - backend-bootstrap-all\n  - backend-bootstrap-config\n  - backend-bootstrap-download-dir\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n## Provision remote_state\n\nUsing this command bootstraps the resources described in your [`remote_state` block](/reference/hcl/blocks/#remote_state).\n\nIf any of the resources described in the `remote_state` block need provisioning, `bootstrap` will provision them. If they are present, but configured in a way that differs from `remote_state` configuration, Terragrunt will attempt to update them when it is safe to do so.\n\nFor example, if you have the following `remote_state` block:\n\n```hcl\n# terragrunt.hcl\n\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket  = \"mybucket\"\n    key     = \"path/to/my/key\"\n    region  = \"us-east-1\"\n    encrypt = true\n\n    dynamodb_table = \"tf-lock\"\n\n    accesslogging_bucket_name = \"mybucket-logs\"\n  }\n}\n```\n\nThen run the following:\n\n```bash\nterragrunt backend bootstrap\n```\n\nYou'll ensure the availability of:\n\n- An S3 bucket named `mybucket` in the `us-east-1` region with the following enabled:\n  - Server Side Encryption (SSE)\n  - Versioning\n  - TLS Enforcement\n- A DynamoDB table named `tf-lock` in the `us-east-1` region with SSE.\n- An S3 bucket named `mybucket-logs` configured as the access log destination for the `mybucket` bucket.\n\nThe `bootstrap` command is idempotent. If the resources already exist, `bootstrap` will not provision them again.\n\n<Aside type=\"tip\" title=\"--backend-bootstrap\">\n\nThe flag `--backend-bootstrap` is equivalent to explicitly running the `bootstrap` command.\n\nUsing it in conjunction with any `run` command will result in any required bootstrapping to be performed prior to initiating the run.\n\n</Aside>\n"
  },
  {
    "path": "docs/src/data/commands/backend/delete.mdx",
    "content": "---\nname: delete\npath: backend/delete\ncategory: backend\nsidebar:\n  order: 302\ndescription: Delete backend state used by a unit.\nusage: |\n  Delete backend state used by a unit.\nexamples:\n  - description: |\n      Delete backend state for the current unit.\n    code: |\n      terragrunt backend delete\n  - description: |\n      Delete backend state for the current unit without confirmation.\n    code: |\n      terragrunt backend delete --non-interactive\n  - description: |\n      Delete backend state for the current unit, even if it doesn't have versioning enabled.\n    code: |\n      terragrunt backend delete --force\nflags:\n  - backend-delete-all\n  - backend-delete-config\n  - backend-delete-download-dir\n  - backend-delete-force\n---\n\n## Delete State\n\nUsing this command deletes the backend state file for the current Terragrunt unit.\n\nFor example, given the following `remote_state` block:\n\n```hcl\n# terragrunt.hcl\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket  = \"mybucket\"\n    key     = \"path/to/my/key\"\n    region  = \"us-east-1\"\n    encrypt = true\n\n    dynamodb_table = \"tf-lock\"\n\n    accesslogging_bucket_name = \"mybucket-logs\"\n  }\n}\n```\n\nRunning `terragrunt backend delete` will delete the backend state file located at `path/to/my/key` in the `mybucket` bucket.\n"
  },
  {
    "path": "docs/src/data/commands/backend/migrate.mdx",
    "content": "---\nname: migrate\npath: backend/migrate\ncategory: backend\nsidebar:\n  order: 301\ndescription: Migrate OpenTofu/Terraform state from one unit to another.\nusage: |\n  Migrate OpenTofu/Terraform state from one unit to another.\nexamples:\n  - description: |\n      Migrate backend state from `unit` to `unit-renamed`.\n    code: |\n      backend migrate old-unit-name new-unit-name\n  - description: |\n      Force state migration, even if the bucket doesn't have versioning enabled.\n    code: |\n      backend migrate --force old-unit-name new-unit-name\nflags:\n  - backend-migrate-config\n  - backend-migrate-download-dir\n  - backend-migrate-force\n---\n\nimport FileTree from '@components/vendored/starlight/FileTree.astro';\n\nThis command will migrate the OpenTofu/Terraform state backend from one unit to another.\n\nYou will typically want to use this command if you are using a `key` attribute for your `remote_state` block that uses the `path_relative_to_include` function, and you want to rename the unit.\n\nFor example, given the following filesystem structure:\n\n<FileTree>\n\n- old-unit-name\n  - terragrunt.hcl\n- root.hcl\n\n</FileTree>\n\n```hcl\n# root.hcl\n\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    bucket         = \"my-tofu-state\"\n    key            = \"${path_relative_to_include()}/tofu.tfstate\"\n    region         = \"us-east-1\"\n    encrypt        = true\n    dynamodb_table = \"my-lock-table\"\n  }\n}\n```\n\n```hcl\n# old-unit-name/terragrunt.hcl\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n```\n\nYou couldn't simply rename the `old-unit-name` directory to `new-unit-name` and run `terragrunt apply` in `new-unit-name`, because the change in the evaluated value for `path_relative_to_include()` would result in a new state key for the `new-unit-name` unit.\n\nInstead, you can use the `backend migrate` command to migrate the backend state from the `old-unit-name` unit to the `new-unit-name` unit.\n\n```bash\ncp -R old-unit-name new-unit-name\nterragrunt backend migrate old-unit-name new-unit-name\nrm -rf old-unit-name\n```\n\nThis will migrate the backend state from the `old-unit-name` unit to the `new-unit-name` unit, and then delete the `old-unit-name` unit.\n\nTerragrunt performs migrations in one of two ways, depending on the level of support for the backends being migrated, and the state of configuration between the two units.\n\n1. If the backend source for both the source and destination units are the same (both are S3 or GCS), Terragrunt will use the AWS/GCP SDK to move state between the two units transparently without interacting with OpenTofu/Terraform. This is the preferred method, when possible.\n2. If either backend source isn't supported by Terragrunt, or the state of configuration between the two units is different, Terragrunt will instead use the OpenTofu/Terraform CLI to move the state between the two units. This is the fallback method, and will generally be slower. Terragrunt also won't be able to delete the existing state from the source unit in this case, so you'll need to handle that yourself.\n"
  },
  {
    "path": "docs/src/data/commands/catalog.mdx",
    "content": "---\nname: catalog\npath: catalog\ncategory: catalog\nsidebar:\n  order: 500\ndescription: Launch a Terminal User Interface (TUI) to browse and use OpenTofu/Terraform modules.\nusage: |\n  Launch a Terminal User Interface (TUI) to browse and use OpenTofu/Terraform modules.\nexamples:\n  - description: Start up a catalog using the configurations discovered in a parent Terragrunt configuration.\n    code: |\n      terragrunt catalog\n  - description: Explicitly indicate the name of the root configuration being discovered.\n    code: |\n      terragrunt catalog --root-file-name root.hcl\nflags:\n  - catalog-no-include-root\n  - catalog-root-file-name\n  - catalog-no-shell\n  - catalog-no-hooks\n---\n\n```bash\nterragrunt catalog [repo-url] [options]\n```\n\nFor more information on how the catalog works, see the dedicated [catalog documentation](/features/catalog).\n"
  },
  {
    "path": "docs/src/data/commands/dag/graph.mdx",
    "content": "---\nname: graph\npath: dag/graph\ncategory: configuration\nsidebar:\n  order: 1000\ndescription: Graph the Directed Acyclic Graph (DAG) in DOT language.\nusage: |\n  Print a visual representation of the Terragrunt dependency graph in DOT language format.\n  This command analyzes your Terragrunt configuration and outputs a directed acyclic graph (DAG) showing the relationships and dependencies between your Terraform modules.\n\n  **Note:** This command is an alias for `list --format=dot`. Both commands produce identical output.\nexamples:\n  - description: Graph all dependencies in the graph as a DotViz graph.\n    code: |\n      $ terragrunt dag graph\n      digraph {\n        \"alb\" ;\n        \"ecs\" ;\n        \"ecs\" -> \"alb\";\n      }\n  - description: Graph all dependencies in visual diagram.\n    code: |\n      $ terragrunt dag graph  | dot -Tpng > graph.png\n\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n<Aside type=\"note\" title=\"Alias for list --format=dot\">\n\nThe `dag graph` command is an alias for `list --format=dot --dependencies --external`. Both commands produce identical DOT format output:\n\n```bash\nterragrunt dag graph\n# Equivalent to:\nterragrunt list --format=dot --dependencies --external\n```\n\nFor more information about the DOT format and its usage, see the [list command documentation](/reference/cli/commands/list#dot-format).\n\n</Aside>\n"
  },
  {
    "path": "docs/src/data/commands/exec.mdx",
    "content": "---\nname: exec\npath: exec\ncategory: main\nsidebar:\n  order: 200\ndescription: Execute an arbitrary command, wrapped by Terragrunt.\nusage: |\n  Execute an arbitrary command, wrapped by Terragrunt.\nexamples:\n  - description: |\n      Execute 'echo \"Hello, Terragrunt!\"' via Terragrunt.\n    code: |\n      terragrunt exec -- echo \"Hello, Terragrunt!\"\n  - description: |\n      Inspect `main.tf` file of module for Unit\n    code: |\n      terragrunt exec --in-download-dir -- cat main.tf\nflags:\n  - auth-provider-cmd\n  - config\n  - download-dir\n  - iam-assume-role\n  - iam-assume-role-duration\n  - iam-assume-role-session-name\n  - iam-assume-role-web-identity-token\n  - in-download-dir\n  - inputs-debug\n---\n\n## Difference between `run` and `exec`\n\nIn contrast to the `run` command, which will always invoke OpenTofu/Terraform, the `exec` command allows for execution of any arbitrary command via Terragrunt.\n\nThis can be useful, as it allows you full control over the process that is being orchestrated by Terragrunt, while taking advantage of Terragrunt's features such as dependency resolution, inputs, and more.\n\n## Interaction with configuration\n\nWhen using `exec`, you will have almost the exact same configuration context that you have when using `run`, including inputs.\n\n```hcl\n# terragrunt.hcl\n\ninputs = {\n  message = \"Hello, Terragrunt!\"\n}\n```\n\nRunning the following command will show that the `message` input is available to the command:\n\n```bash\n$ terragrunt exec -- env | grep 'TF_VAR_message'\nTF_VAR_message=Hello, Terragrunt!\n```\n\n"
  },
  {
    "path": "docs/src/data/commands/find.mdx",
    "content": "---\nname: find\npath: find\ncategory: discovery\nsidebar:\n  order: 700\ndescription: Find relevant Terragrunt configurations.\nusage: |\n  The `find` command helps you discover Terragrunt configurations in your codebase.\n\n  It recursively searches for `terragrunt.hcl` and `terragrunt.stack.hcl` files and displays them in formatted output.\nexamples:\n  - description: |\n      Find all configurations (units and stacks) in the current directory.\n    code: |\n      terragrunt find\n  - description: |\n      Find all configurations in a different directory.\n    code: |\n      terragrunt find --working-dir /path/to/working/dir\n  - description: |\n      Disable color output.\n    code: |\n      terragrunt find --no-color\n  - description: |\n      Find all configurations in the current directory and emit them as a JSON string.\n    code: |\n      terragrunt find --format 'json'\n  - description: |\n      Find all configurations and output them in JSON format (alias for --format=json).\n    code: |\n      terragrunt find --json\n  - description: |\n      Sort configurations based on their dependencies using DAG mode.\n    code: |\n      terragrunt find --dag\n  - description: |\n      Sort configurations based on dependency graph as if running plan command.\n    code: |\n      $ terragrunt find --queue-construct-as=plan\n      stacks/live/dev\n      stacks/live/prod\n      units/live/dev/vpc\n      units/live/prod/vpc\n      units/live/dev/db\n      units/live/prod/db\n      units/live/dev/ec2\n      units/live/prod/ec2\n\n  - description: |\n      Sort configurations based on dependency graph as if running destroy command.\n    code: |\n      $ terragrunt find --queue-construct-as=destroy\n      stacks/live/dev\n      stacks/live/prod\n      units/live/dev/ec2\n      units/live/prod/ec2\n      units/live/dev/db\n      units/live/prod/db\n      units/live/dev/vpc\n      units/live/prod/vpc\n\n  - description: |\n      Include dependency information in the output.\n    code: |\n      terragrunt find --dependencies --format 'json'\n  - description: |\n      Include exclude configuration in the output.\n    code: |\n      terragrunt find --exclude --format 'json'\n  - description: |\n      Include external dependencies in the output.\n    code: |\n      terragrunt find --dependencies --external --format 'json'\n  - description: |\n      Include the list of files read by each component in the output.\n    code: |\n      terragrunt find --reading --format 'json'\nflags:\n  - find-format\n  - find-json\n  - find-dag\n  - find-no-hidden\n  - find-dependencies\n  - find-exclude\n  - find-include\n  - find-reading\n  - find-external\n  - queue-construct-as\n  - filter\n  - filter-affected\n---\n\nimport { Aside, Badge } from '@astrojs/starlight/components';\n\n## Color Output\n\nWhen used without any flags, all units and stacks discovered in the current working directory are displayed in colorful text format.\n\n![find](../../assets/img/screenshots/find.png)\n\n<Aside type=\"note\" title=\"Color Coding\">\n\nDiscovered configurations are color coded to help you identify them at a glance:\n\n- <Badge text=\"Units\" style={{ backgroundColor: '#1B46DD', color: '#FFFFFF' }} /> are displayed in blue\n- <Badge text=\"Stacks\" style={{ backgroundColor: '#2E8B57', color: '#FFFFFF' }} /> are displayed in green\n\n</Aside>\n\n## Output Formats\n\nThe `find` command supports two output formats:\n\n### Text Format (Default)\n\nThe default text format displays each configuration on a new line, with color coding for different types.\n\n### JSON Format\n\nYou can output the results in JSON format using either:\n\n```bash\nterragrunt find --format=json\n```\n\nor the shorter alias:\n\n```bash\nterragrunt find --json\n```\n\nThe JSON output includes additional metadata about each configuration, such as its type (unit or stack) and path.\n\n## DAG Mode\n\nThe `find` command supports DAG mode to sort output based on dependencies using the `--dag` flag.\n\nWhen using DAG mode, configurations with no dependencies appear first, followed by configurations that depend on them, maintaining the correct dependency order:\n\n```bash\nterragrunt find --dag\nunitA           # no dependencies\nunitB           # no dependencies\nunitC           # depends on unitA\nunitD           # depends on unitC\n```\n\nIf multiple configurations share common dependencies, they will be sorted in lexical order.\n\n## Queue Construct As\n\nThe `find` command supports the `--queue-construct-as` flag (or its shorter alias `--as`) to sort output based on the dependency graph, as if a particular command was run.\n\nFor example, when using the `plan` command:\n\n```bash\nterragrunt find --queue-construct-as=plan\nstacks/live/dev\nstacks/live/prod\nunits/live/dev/vpc\nunits/live/prod/vpc\nunits/live/dev/db\nunits/live/prod/db\nunits/live/dev/ec2\nunits/live/prod/ec2\n```\n\nThis will sort the output based on the dependency graph, as if the `plan` command was run. All dependent units will appear *after* the units they depend on.\n\nWhen using the `destroy` command:\n\n```bash\nterragrunt find --as=destroy\nstacks/live/dev\nstacks/live/prod\nunits/live/dev/ec2\nunits/live/prod/ec2\nunits/live/dev/db\nunits/live/prod/db\nunits/live/dev/vpc\nunits/live/prod/vpc\n```\n\nThis will sort the output based on the dependency graph, as if the `destroy` command was run. All dependent units will appear *before* the units they depend on.\n\n**Note:** The `--queue-construct-as` flag implies the `--dag` flag.\n\n## Dependencies\n\nYou can include dependency information in the output using the `--dependencies` flag. When enabled, the JSON output will include the dependency relationships between configurations:\n\n```bash\nterragrunt find --dependencies --format=json\n[\n  {\n    \"type\": \"unit\",\n    \"path\": \"unitA\",\n    \"dependencies\": []\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"unitB\",\n    \"dependencies\": [\"../unitA\", \"../../external/unitC\"]\n  }\n]\n```\n\n## Exclude Configuration\n\nYou can include exclude configuration in the output using the `--exclude` flag. When enabled, the JSON output will include the configurations of the `exclude` block in the discovered units:\n\n```bash\nterragrunt find --exclude --format=json\n[\n  {\n    \"type\": \"unit\",\n    \"path\": \"action/exclude-apply\",\n    \"exclude\": {\n      \"exclude_dependencies\": true,\n      \"actions\": [\n        \"apply\"\n      ],\n      \"if\": true\n    }\n  }\n]\n```\n\nYou can combine this with the `--queue-construct-as` flag to dry-run behavior relevant to excludes:\n\n```bash\nterragrunt find --exclude --queue-construct-as=plan --format=json\n```\n\n`find` will remove any units that would match the exclude configuration.\n\n## Reading Files\n\nYou can include information about which files are read by each component using the `--reading` flag. This is particularly useful for understanding configuration dependencies and tracking which shared files are consumed by your Terragrunt configurations.\n\nWhen enabled, the JSON output will include a `reading` field that lists all files that were read during the parsing of each component:\n\n```bash\n$ terragrunt find --reading --format=json\n[\n  {\n    \"type\": \"unit\",\n    \"path\": \"app\",\n    \"reading\": [\n      \"path/to/shared.hcl\",\n      \"path/to/shared.tfvars\",\n    ]\n  }\n]\n```\n\nThis includes files read by Terragrunt helper functions such as:\n- [`read_terragrunt_config()`](/reference/hcl/functions/#read_terragrunt_config) - Reading other Terragrunt configuration files\n- [`read_tfvars_file()`](/reference/hcl/functions/#read_tfvars_file) - Reading Terraform variable files\n- [`sops_decrypt_file()`](/reference/hcl/functions/#sops_decrypt_file) - Reading encrypted files via SOPS\n- [`mark_as_read()`](/reference/hcl/functions/#mark_as_read) - Explicitly marking a file as read\n\n## External Dependencies\n\nBy default, external dependencies (those outside the working directory) are not part of the overall results (although, they will be mentioned in the dependency section of the JSON output). Use the `--external` flag to include them as top-level results:\n\n```bash\nterragrunt find --dependencies --external --format=json\n[\n  {\n    \"type\": \"unit\",\n    \"path\": \"internal/unitA\",\n    \"dependencies\": []\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"internal/unitB\",\n    \"dependencies\": [\"../unitA\", \"../../external/unitC\"]\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"external/unitC\",\n    \"dependencies\": []\n  }\n]\n```\n\n<Aside type=\"note\" title=\"Automatic Dependency Discovery\">\n\nThe `--external` flag automatically enables dependency discovery, so you don't need to explicitly pass `--dependencies` when using `--external` unless you want to actually see the dependency information.\n\nThe following commands are equivalent:\n\n```bash\nterragrunt find --external\nterragrunt find --dependencies --external\n```\n\nAs the default text format of `find` won't display dependency information anyways.\n\n</Aside>\n\n## Hidden Configurations\n\nBy default, hidden directories (those starting with `.`) are included in the search results. Use the `--no-hidden` flag to exclude them:\n\n```bash\nterragrunt find --no-hidden\n```\n\n## Disabling Color Output\n\nYou can disable color output by using the global `--no-color` flag:\n\n```bash\nterragrunt find --no-color\n```\n\nWhen stdout is redirected, color output is disabled automatically to prevent undesired interference with other tools.\n\n![find-no-color](../../assets/img/screenshots/find-no-color.png)\n\n## Working Directory\n\nYou can change the working directory for `find` by using the global `--working-dir` flag:\n\n```bash\nterragrunt find --working-dir=/path/to/working/dir\n```\n\n## Filtering Results\n\nThe `find` command supports the `--filter` flag to target specific configurations using a flexible query language. This is particularly useful for discovering configurations that match specific criteria before running operations on them.\n\n### Finding Affected Components\n\nFor the common use case of finding components affected by changes between the default branch and `HEAD`, you can use the `--filter-affected` flag:\n\n```bash\n# Find all components affected by changes between main and HEAD\nterragrunt find --filter-affected\n```\n\nThis is equivalent to `terragrunt find --filter '[main...HEAD]'` and automatically detects your repository's default branch.\n\n### Basic Filtering Examples\n\n```bash\n# Filter by name using glob patterns\nterragrunt find --filter 'app*'\n\n# Filter by path\nterragrunt find --filter './prod/**'\n\n# Filter by type\nterragrunt find --filter 'type=unit'\n\n# Combine filters with intersection\nterragrunt find --filter './prod/** | type=unit'\n```\n\n### Advanced Filtering\n\nThe filter syntax supports negation, multiple filters, and complex queries:\n\n```bash\n# Exclude specific configurations\nterragrunt find --filter '!./test/**'\n\n# Multiple filters (OR logic)\nterragrunt find --filter 'app1' --filter 'app2'\n\n# Complex queries with chaining\nterragrunt find --filter './dev/** | type=unit | !name=unit1'\n```\n\n<Aside type=\"tip\" title=\"Learn More About Filtering\">\n\nFor comprehensive examples and advanced usage patterns, see the [Filters feature documentation](/features/filter).\n\n</Aside>\n"
  },
  {
    "path": "docs/src/data/commands/hcl/fmt.mdx",
    "content": "---\nname: fmt\npath: hcl/fmt\ncategory: configuration\nsidebar:\n  order: 900\ndescription: Recursively find HashiCorp Configuration Language (HCL) files and rewrite them into a canonical format.\nusage: |\n   Recursively find HashiCorp Configuration Language (HCL) files and rewrite them into a canonical format.\nexamples:\n  - description: Recursively format all HCL files in the current directory.\n    code: |\n      terragrunt hcl fmt\nflags:\n  - hcl-fmt-check\n  - hcl-fmt-diff\n  - hcl-fmt-exclude-dir\n  - hcl-fmt-file\n  - hcl-fmt-filter\n  - hcl-fmt-stdin\n  - parallelism\n  - queue-exclude-dir\n  - queue-exclude-external\n  - queue-excludes-file\n  - queue-ignore-dag-order\n  - queue-ignore-errors\n  - queue-include-dir\n  - queue-include-external\n  - queue-include-units-reading\n  - queue-strict-include\n---\n"
  },
  {
    "path": "docs/src/data/commands/hcl/validate.mdx",
    "content": "---\nname: validate\npath: hcl/validate\ncategory: configuration\nsidebar:\n  order: 901\ndescription: Recursively find HashiCorp Configuration Language (HCL) files and validate them.\nusage: |\n  Recursively find HashiCorp Configuration Language (HCL) files and validate them.\nexamples:\n  - description: Discover all HCL files in the current directory, and validate them.\n    code: |\n      terragrunt hcl validate\nflags:\n  - filter\n  - hcl-validate-json\n  - hcl-validate-show-config-path\n  - hcl-validate-inputs\n  - hcl-validate-strict\n  - queue-exclude-dir\n  - queue-exclude-external\n  - queue-excludes-file\n  - queue-ignore-dag-order\n  - queue-ignore-errors\n  - queue-include-dir\n  - queue-include-external\n  - queue-include-units-reading\n  - queue-strict-include\n---\n"
  },
  {
    "path": "docs/src/data/commands/info/print.mdx",
    "content": "---\nname: print\npath: info/print\ncategory: configuration\nsidebar:\n  order: 1200\ndescription: Print out a short description of Terragrunt context.\nusage: |\n  Outputs a JSON object with contextual information such as the configuration file path, current working directory, cache download location, IAM role (if any), the binary used to run Terraform or Tofu, and the command being executed.\nexamples:\n  - description: Print out information about the current context of Terragrunt.\n    code: |\n      $ terragrunt info print\n      {\n        \"config_path\": \"/example/path/terragrunt.hcl\",\n        \"download_dir\": \"/example/path/.terragrunt-cache\",\n        \"iam_role\": \"\",\n        \"terraform_binary\": \"tofu\",\n        \"terraform_command\": \"print\",\n        \"working_dir\": \"/example/path\"\n      }\n\n---\n"
  },
  {
    "path": "docs/src/data/commands/list.mdx",
    "content": "---\nname: list\npath: list\ncategory: discovery\nsidebar:\n  order: 800\ndescription: List Terragrunt configurations in your codebase.\nusage: |\n  The list command helps you discover and display Terragrunt configurations in your codebase. It provides various output formats and options to help you understand the structure and dependencies of your Terragrunt configurations.\nexamples:\n  - description: |\n      List all units in a typical multi-environment infrastructure setup.\n    code: |\n      $ terragrunt list\n      live/dev/db    live/dev/ec2   live/dev/vpc\n      live/prod/db   live/prod/ec2  live/prod/vpc\n\n  - description: |\n      List all units in long format, showing unit types and paths.\n    code: |\n      $ terragrunt list -l\n      Type  Path\n      unit  live/dev/db\n      unit  live/dev/ec2\n      unit  live/dev/vpc\n      unit  live/prod/db\n      unit  live/prod/ec2\n      unit  live/prod/vpc\n\n  - description: |\n      List all units in tree format to visualize the infrastructure hierarchy.\n    code: |\n      $ terragrunt list -T\n      .\n      ╰── live\n          ├── dev\n          │   ├── db\n          │   ├── ec2\n          │   ╰── vpc\n          ╰── prod\n              ├── db\n              ├── ec2\n              ╰── vpc\n\n  - description: |\n      List all units with their dependencies to understand infrastructure relationships.\n    code: |\n      $ terragrunt list -l --dependencies\n      Type  Path           Dependencies\n      unit  live/dev/db    live/dev/vpc\n      unit  live/dev/ec2   live/dev/db, live/dev/vpc\n      unit  live/dev/vpc\n      unit  live/prod/db   live/prod/vpc\n      unit  live/prod/ec2  live/prod/db, live/prod/vpc\n      unit  live/prod/vpc\n\n  - description: |\n      List all units in dependency order (DAG) to understand deployment sequence.\n    code: |\n      $ terragrunt list -l --dag --dependencies\n      Type  Path          Dependencies\n      unit  b-dependency\n      unit  a-dependent   b-dependency\n\n  - description: |\n      List all units in dependency order as if running plan command.\n    code: |\n      $ terragrunt list --queue-construct-as=plan\n      stacks/live/dev      stacks/live/prod     units/live/dev/vpc\n      units/live/prod/vpc  units/live/dev/db    units/live/prod/db\n      units/live/dev/ec2   units/live/prod/ec2\n\n  - description: |\n      List all units in dependency order as if running destroy command.\n    code: |\n      $ terragrunt list --queue-construct-as=destroy\n      stacks/live/dev      stacks/live/prod     units/live/dev/ec2\n      units/live/prod/ec2  units/live/dev/db    units/live/prod/db\n      units/live/dev/vpc   units/live/prod/vpc\n\n  - description: |\n      Generate a DOT format graph of dependencies for visualization.\n    code: |\n      $ terragrunt list --format=dot --dependencies\n      digraph {\n        \"live/dev/vpc\" ;\n        \"live/dev/db\" ;\n        \"live/dev/ec2\" ;\n        \"live/dev/db\" -> \"live/dev/vpc\";\n        \"live/dev/ec2\" -> \"live/dev/db\";\n        \"live/dev/ec2\" -> \"live/dev/vpc\";\n      }\n\nflags:\n  - list-format\n  - list-no-hidden\n  - list-dependencies\n  - list-external\n  - list-tree\n  - list-long\n  - list-dag\n  - queue-construct-as\n  - filter\n  - filter-affected\n---\n\nimport { Aside, Badge } from '@astrojs/starlight/components';\n\n## Output Formats\n\nThe `list` command supports multiple output formats to help you visualize your Terragrunt configurations in different ways:\n\n### Text Format (Default)\n\nThe default text format provides a simple, space-separated list of configurations.\n\n![list](../../assets/img/screenshots/list.png)\n\nIt will display all configurations that fit in the width of your terminal. When configurations exceed the width of your terminal, it will wrap to the next line.\n\n![list-narrow](../../assets/img/screenshots/list-narrow.png)\n\n### Long Format\n\nThe long format provides additional details about each configuration, including its type:\n\n![list-long](../../assets/img/screenshots/list-long.png)\n\n### Tree Format\n\nThe tree format provides a hierarchical view of your configurations:\n\n![list-tree](../../assets/img/screenshots/list-tree.png)\n\nBy default, configurations in tree format are displayed ordered by name and grouped by directory:\n\n```bash\n.\n╰── live\n    ├── dev\n    │   ├── db\n    │   ├── ec2\n    │   ╰── vpc\n    ╰── prod\n        ├── db\n        ├── ec2\n        ╰── vpc\n```\n\n### DOT Format\n\nThe DOT format outputs a directed acyclic graph (DAG) in DOT language format, which can be rendered as a visual dependency graph using tools like GraphViz.\n\n```bash\n$ terragrunt list --format=dot --dependencies\ndigraph {\n  \"live/dev/vpc\" ;\n  \"live/dev/db\" ;\n  \"live/dev/ec2\" ;\n  \"live/dev/db\" -> \"live/dev/vpc\";\n  \"live/dev/ec2\" -> \"live/dev/db\";\n  \"live/dev/ec2\" -> \"live/dev/vpc\";\n  \"live/prod/vpc\" ;\n  \"live/prod/db\" ;\n  \"live/prod/ec2\" ;\n  \"live/prod/db\" -> \"live/prod/vpc\";\n  \"live/prod/ec2\" -> \"live/prod/db\";\n  \"live/prod/ec2\" -> \"live/prod/vpc\";\n}\n```\n\nYou can render the DOT output to an image using GraphViz:\n\n```bash\n$ terragrunt list --format=dot --dependencies | dot -Tpng > graph.png\n$ terragrunt list --format=dot --dependencies | dot -Tsvg > graph.svg\n```\n\n<Aside type=\"note\" title=\"DOT Format Alias\">\n\nThe `dag graph` command is an alias for `list --format=dot`. Both commands produce identical DOT format output:\n\n```bash\nterragrunt dag graph\n# Equivalent to:\nterragrunt list --format=dot --dependencies --external\n```\n\n</Aside>\n\n## DAG Mode\n\nThe `list` command supports DAG mode to sort and group output based on dependencies using the `--dag` flag. When using DAG mode, configurations with no dependencies appear first, followed by configurations that depend on them, maintaining the correct dependency order.\n\nFor example, in default text format:\n\n```bash\n# Default alphabetical order\n$ terragrunt list\na-dependent b-dependency\n\n# DAG mode order\n$ terragrunt list --dag\nb-dependency a-dependent\n```\n\nWhen using `--dag` with the tree format, configurations are sorted by dependency order and grouped by relationship in the dependency graph:\n\n```bash\n$ terragrunt list --tree --dag\n.\n├── live/dev/vpc\n│   ├── live/dev/db\n│   │   ╰── live/dev/ec2\n│   ╰── live/dev/ec2\n╰── live/prod/vpc\n    ├── live/prod/db\n    │   ╰── live/prod/ec2\n    ╰── live/prod/ec2\n```\n\n## Queue Construct As\n\nThe `list` command supports the `--queue-construct-as` flag (or its shorter alias `--as`) to sort output based on the dependency graph, as if a particular command was run.\n\nFor example, when using the `plan` command:\n\n```bash\n$ terragrunt list --queue-construct-as=plan\nstacks/live/dev      stacks/live/prod     units/live/dev/vpc\nunits/live/prod/vpc  units/live/dev/db    units/live/prod/db\nunits/live/dev/ec2   units/live/prod/ec2\n```\n\nThis will sort the output based on the dependency graph, as if the `plan` command was run. All dependent units will appear *after* the units they depend on.\n\nWhen using the `destroy` command:\n\n```bash\n$ terragrunt list --queue-construct-as=destroy\nstacks/live/dev      stacks/live/prod     units/live/dev/ec2\nunits/live/prod/ec2  units/live/dev/db    units/live/prod/db\nunits/live/dev/vpc   units/live/prod/vpc\n```\n\nThis will sort the output based on the dependency graph, as if the `destroy` command was run. All dependent units will appear *before* the units they depend on.\n\n**Note:** The `--queue-construct-as` flag implies the `--dag` flag.\n\n## Dependencies and Discovery\n\n### Dependencies\n\nInclude dependency information in the output using the `--dependencies` flag. When combined with different grouping options, this provides powerful ways to visualize your infrastructure's dependency structure.\n\n### External Dependencies\n\nUse the `--external` flag to discover and include dependencies that exist outside your current working directory. This is particularly useful when working with shared modules or cross-repository dependencies.\n\n<Aside type=\"note\" title=\"Automatic Dependency Discovery\">\n\nThe `--external` flag automatically enables dependency discovery, so you don't need to explicitly pass `--dependencies` when using `--external`. The following commands are equivalent:\n\n```bash\nterragrunt list --external\nterragrunt list --dependencies --external\n```\n\nThis is because the default text format of `list` won't display dependency information anyways (as opposed to the `long` format, which does).\n\n</Aside>\n\n### Hidden Configurations\n\nBy default, Terragrunt includes configurations in hidden directories (those starting with a dot) in the output. Use the `--no-hidden` flag to exclude these configurations.\n\n## Working Directory\n\nYou can change the working directory for `list` by using the global `--working-dir` flag:\n\n```bash\nterragrunt list --working-dir=/path/to/working/dir\n```\n\n## Color Output\n\nWhen used without any flags, all units and stacks discovered in the current working directory are displayed in colorful text format.\n\n<Aside type=\"note\" title=\"Color Coding\">\nDiscovered configurations are color coded to help you identify them at a glance:\n\n- <Badge text=\"Units\" style={{ backgroundColor: '#1B46DD', color: '#FFFFFF' }} /> are displayed in blue\n- <Badge text=\"Stacks\" style={{ backgroundColor: '#2E8B57', color: '#FFFFFF' }} /> are displayed in green\n</Aside>\n\nYou can disable color output by using the global `--no-color` flag.\n\n## Filtering Results\n\nThe `list` command supports the `--filter` flag to target specific configurations using a flexible query language. This is particularly useful for listing configurations that match specific criteria before running operations on them.\n\n### Listing Affected Components\n\nFor the common use case of listing components affected by changes between the default branch and `HEAD`, you can use the `--filter-affected` flag:\n\n```bash\n# List all components affected by changes between main and HEAD\nterragrunt list --filter-affected\n```\n\nThis is equivalent to `terragrunt list --filter '[main...HEAD]'` and automatically detects your repository's default branch.\n\n### Basic Filtering Examples\n\n```bash\n# Filter by name using glob patterns\nterragrunt list --filter 'app*'\n\n# Filter by path\nterragrunt list --filter './prod/**'\n\n# Filter by type\nterragrunt list --filter 'type=unit'\n\n# Combine filters with intersection\nterragrunt list --filter './prod/** | type=unit'\n```\n\n### Advanced Filtering\n\nThe filter syntax supports negation, multiple filters, and complex queries:\n\n```bash\n# Exclude specific configurations\nterragrunt list --filter '!./test/**'\n\n# Multiple filters (OR logic)\nterragrunt list --filter 'app1' --filter 'app2'\n\n# Complex queries with chaining\nterragrunt list --filter './dev/** | type=unit | !name=unit1'\n```\n\n<Aside type=\"tip\" title=\"Learn More About Filtering\">\n\nFor comprehensive examples and advanced usage patterns, see the [Filters feature documentation](/features/filter).\n\n</Aside>\n"
  },
  {
    "path": "docs/src/data/commands/opentofu-shortcuts.mdx",
    "content": "---\nname: OpenTofu Shortcuts\npath: opentofu-shortcuts\ncategory: shortcuts\nsidebar:\n  order: 0\ndescription: Use Terragrunt as a drop-in replacement for OpenTofu/Terraform commands.\nusage: |\n  Terragrunt is an orchestration tool for OpenTofu/Terraform, so with a couple exceptions, you can generally use it as a drop-in replacement for OpenTofu/Terraform.\n\n  Terragrunt has a shortcut for most OpenTofu commands. You can usually just replace `tofu` or `terraform` with `terragrunt` and it will do what you expect.\nexamples:\n  - description: |\n      Run the apply command.\n    code: |\n      terragrunt apply\n  - description: |\n      Run the plan command.\n    code: |\n      terragrunt plan\n  - description: |\n      Run the output command.\n    code: |\n      terragrunt output\n---\n\n## Supported Shortcuts\n\n- `apply`\n- `destroy`\n- `force-unlock`\n- `import`\n- `init`\n- `output`\n- `plan`\n- `refresh`\n- `show`\n- `state`\n- `test`\n- `validate`\n"
  },
  {
    "path": "docs/src/data/commands/render.mdx",
    "content": "---\nname: render\npath: render\ncategory: configuration\nsidebar:\n  order: 1100\ndescription: |\n  Render the Terragrunt configuration in the current working directory, with as much work done as possible beforehand (that is, with all includes merged, dependencies resolved/interpolated, function calls executed, etc).\nusage: |\n  Generate a simplified version of the Terragrunt configuration with all includes and dependencies resolved.\nexamples:\n  - description: Render the configurations for the current unit in JSON format.\n    code: |\n      terragrunt render --format=json\nflags:\n  - render-format\n  - render-write\n  - render-all\n---\n\nRender the Terragrunt configuration in the current working directory, with as much work done as possible beforehand (that is, with all includes merged, dependencies resolved/interpolated, function calls executed, etc).\n\nThe only supported format at the moment is JSON, but support for HCL will be added in a future version.\n\nExample:\n\nThe following `terragrunt.hcl`:\n\n```hcl\nlocals {\n  aws_region = \"us-east-1\"\n}\n\ninputs = {\n  aws_region = local.aws_region\n}\n```\n\n\nRenders to the following HCL by default:\n\n```bash\n$ terragrunt render\nlocals {\n  aws_region = \"us-east-1\"\n}\ninputs = {\n  aws_region = \"us-east-1\"\n}\n```\n\nNote the resolution of the `aws_region` local, making it easier to read the final evaluated configuration at a glance.\n\nRenders to the following JSON when the `--format json` flag is used:\n\n```bash\n$ terragrunt render --format json\n{\n  \"locals\": { \"aws_region\": \"us-east-1\" },\n  \"inputs\": { \"aws_region\": \"us-east-1\" }\n  // NOTE: other attributes are omitted for brevity\n}\n```\n\nYou can also use the `--write` flag to write the rendered configuration to a canonically named file in the same working directory as the `terragrunt.hcl` file.\n\nExample:\n\n```bash\n# Note the use of the `--json` shortcut flag.\nterragrunt render --json --write\n```\n\nThis will write the rendered configuration to `terragrunt.rendered.json` in the current working directory.\n\nThis can be useful when rendering many configurations in a given directory, and you want to keep the rendered configurations in the same directory as the original configurations, without leveraging external tools or scripts.\n\nThis is also useful when combined with the `--all` flag, which will render all configurations discovered from the current working directory.\n\n```bash\n# Note the use of the `-w` alias for the `--write` flag.\nterragrunt render --all --json -w\n```\n\nThis will render all configurations discovered from the current working directory and write the rendered configurations to `terragrunt.rendered.json` files adjacent to the configurations they are derived from.\n"
  },
  {
    "path": "docs/src/data/commands/run.mdx",
    "content": "---\nname: run\npath: run\ncategory: main\nsidebar:\n  order: 100\ndescription: 'Run OpenTofu/Terraform commands.'\nusage: |\n  Run a command, passing arguments to an orchestrated tofu/terraform binary.\n\n  This is the explicit, and most flexible form of running an IaC command with Terragrunt. Shortcuts can be found in \"terragrunt --help\" for common use-cases.\nexamples:\n  - description: |\n      Run the plan command.\n    code: |\n      terragrunt run plan\n      # Shortcut:\n      # terragrunt plan\n  - description: |\n      Run the plan command, and pass additional arguments.\n    code: |\n      terragrunt run -- output -json\n      # Shortcut:\n      # terragrunt output -json\nflags:\n  - all\n  - auth-provider-cmd\n  - config\n  - json-out-dir\n  - dependency-fetch-output-from-state\n  - disable-bucket-update\n  - disable-command-validation\n  - download-dir\n  - out-dir\n  - engine-cache-path\n  - engine-log-level\n  - engine-skip-check\n  - experimental-engine\n  - feature\n  - filter\n  - filter-affected\n  - graph\n  - iam-assume-role\n  - iam-assume-role-duration\n  - iam-assume-role-session-name\n  - iam-assume-role-web-identity-token\n  - inputs-debug\n  - no-auto-approve\n  - no-auto-init\n  - no-auto-provider-cache-dir\n  - no-auto-retry\n  - destroy-dependencies-check\n  - parallelism\n  - provider-cache\n  - provider-cache-dir\n  - provider-cache-hostname\n  - provider-cache-port\n  - provider-cache-registry-names\n  - provider-cache-token\n  - queue-exclude-dir\n  - queue-exclude-external\n  - queue-excludes-file\n  - queue-ignore-dag-order\n  - queue-ignore-errors\n  - queue-include-dir\n  - queue-include-external\n  - queue-include-units-reading\n  - queue-strict-include\n  - report-file\n  - report-format\n  - report-schema-file\n  - source\n  - source-map\n  - source-update\n  - summary-disable\n  - summary-per-unit\n  - tf-forward-stdout\n  - tf-path\n  - units-that-include\n  - use-partial-parse-config-cache\n  - version-manager-file-name\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n## Running multiple units\n\nNote that the `run` command is a more explicit and flexible way to run OpenTofu/Terraform commands in comparison to [OpenTofu shortcuts](/reference/cli/commands/opentofu-shortcuts).\n\nThe `run` command also supports the following flags that can be used to drive runs in multiple units:\n\n- `--all`: Run the provided OpenTofu/Terraform command against all units in the current stack.\n- `--graph`: Run the provided OpenTofu/Terraform command against the graph of dependencies for the unit in the current working directory.\n\n## Filtering Units\n\nThe `run` command supports the `--filter` flag to target specific units using a flexible query language. This is particularly useful when running commands across multiple units with `--all`.\n\n### Running Affected Components\n\nFor the common use case of running commands on components affected by changes between the default branch and `HEAD`, you can use the `--filter-affected` flag:\n\n```bash\n# Plan changes for affected components\nterragrunt run --all --filter-affected -- plan\n\n# Apply changes for affected components\nterragrunt run --all --filter-affected -- apply\n```\n\nThis is equivalent to `terragrunt run --all --filter '[main...HEAD]' -- plan` and automatically detects your repository's default branch. When using `--filter-affected` with the `run` command, you must use one of the `plan` or `apply` commands, and not the `-destroy` flag.\n\n### Basic Filtering Examples\n\n```bash\n# Filter by path with glob patterns\nterragrunt run --all --filter 'prod/**' -- plan\n\n# Filter by name\nterragrunt run --all --filter 'app*' -- apply\n\n# Filter by type\nterragrunt run --all --filter 'type=unit' -- plan\n```\n\n### Advanced Filtering\n\nThe filter syntax supports negation, intersection, and complex queries:\n\n```bash\n# Exclude specific configurations\nterragrunt run --all --filter '!./test/**' -- plan\n\n# Combine filters with intersection (refinement)\nterragrunt run --all --filter './prod/** | type=unit' -- apply\n\n# Complex queries with chaining\nterragrunt run --all --filter './prod/** | type=unit | !name=legacy' -- plan\n\n# Multiple filters (OR logic)\nterragrunt run --all --filter 'app1' --filter 'app2' -- plan\n```\n\n<Aside type=\"tip\" title=\"Learn More About Filtering\">\n\nFor comprehensive examples and advanced usage patterns, see the [Filters feature documentation](/features/filter).\n\n</Aside>\n\n## Separating Arguments\n\nYou may, at times, need to explicitly separate the arguments used for Terragrunt from those used for OpenTofu/Terraform. In those circumstances, you can use the argument `--` to separate the Terragrunt flags from the OpenTofu/Terraform flags.\n\n```bash\nterragrunt run -- plan -no-color\n```\n\n<Aside type=\"caution\">\nWhen using `run --all plan` with units that have dependencies (e.g. via `dependency` or `dependencies` blocks), the command will fail if those dependencies have never been deployed. This is because Terragrunt cannot resolve dependency outputs without existing state.\n\nTo work around this issue, use [mock outputs in dependency blocks](/reference/hcl/blocks/#dependency).\n</Aside>\n\n<Aside type=\"caution\">\nDo not set `TF_PLUGIN_CACHE_DIR` when using `run --all`. This can cause concurrent access issues with the provider cache. Instead, use Terragrunt's built-in [Provider Cache Server](/features/caching/provider-cache-server/).\n\nWe are working with the OpenTofu team to improve this behavior in the future.\n</Aside>\n\n<Aside type=\"caution\">\nWhen using `run --all` with `apply` or `destroy`, Terragrunt automatically adds the `-auto-approve` flag due to limitations with shared stdin making individual approvals impossible.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/commands/scaffold.mdx",
    "content": "---\nname: scaffold\npath: scaffold\ncategory: catalog\ndescription: Generate Terragrunt configuration files from a catalog.\nsidebar:\n  order: 600\nusage: |\n  Generate Terragrunt configuration files from a catalog.\nflags:\n  - scaffold-no-include-root\n  - scaffold-root-file-name\n  - scaffold-var\n  - scaffold-var-file\n  - scaffold-no-shell\n  - scaffold-no-hooks\nexamples:\n  - description: Scaffold a standard MySQL database module as a new unit.\n    code: |\n      terragrunt scaffold github.com/gruntwork-io/terragrunt-infrastructure-modules-example//modules/mysql\n---\n\n```bash\nterragrunt scaffold <MODULE_URL> [TEMPLATE_URL] [--var] [--var-file] [--no-include-root] [--root-file-name]\n```\n\nFor more information on how scaffolding works, see the dedicated [scaffold documentation](/features/catalog/scaffold).\n"
  },
  {
    "path": "docs/src/data/commands/stack/clean.mdx",
    "content": "---\nname: clean\npath: \"stack/clean\"\ncategory: stack\ndescription: Remove `.terragrunt-stack` directories created by `stack` commands.\nusage: |\n  Running `terragrunt stack clean` removes the `.terragrunt-stack` directory, which is generated by the `terragrunt stack generate` or `terragrunt stack run` commands.\n\n  This can be useful when you need to remove generated configurations or troubleshoot issues.\nsidebar:\n  order: 403\nexamples:\n  - description: Remove all auto-generated `.terragrunt-stack` directories created by `stack` commands.\n    code: |\n      terragrunt stack clean\n---\n"
  },
  {
    "path": "docs/src/data/commands/stack/generate.mdx",
    "content": "---\nname: generate\npath: \"stack/generate\"\ncategory: stack\ndescription: Generate a stack of units based on configurations in a terragrunt.stack.hcl file.\nusage: |\n  Generate a stack of units based on configurations in a terragrunt.stack.hcl file.\nsidebar:\n  order: 400\nexamples:\n  - description: Generate stacks of units using the configurations in all terragrunt.stack.hcl files found, starting in the current directory.\n    code: |\n      terragrunt stack generate\nflags:\n  - stack-generate-filter\n---\n\nimport FileTree from \"@components/vendored/starlight/FileTree.astro\";\nimport { Aside } from \"@astrojs/starlight/components\";\n\n## Generating a stack\n\n```hcl\n# terragrunt.stack.hcl\n\nunit \"mother\" {\n\tsource = \"units/chicken\"\n\tpath   = \"mother\"\n}\n\nunit \"father\" {\n\tsource = \"./units/chicken\"\n\tpath   = \"father\"\n}\n\nunit \"chick_1\" {\n\tsource = \"./units/chick\"\n\tpath   = \"chicks/chick-1\"\n}\n\nunit \"chick_2\" {\n\tsource = \"units/chick\"\n\tpath   = \"chicks/chick-2\"\n}\n```\n\nRunning the following:\n\n```bash\nterragrunt stack generate\n```\n\nGenerates the following stack:\n\n<FileTree>\n\n- terragrunt.stack.hcl\n- .terragrunt-stack\n  - mother\n    - terragrunt.hcl\n  - father\n    - terragrunt.hcl\n  - chicks\n    - chick-1\n      - terragrunt.hcl\n    - chick-2\n      - terragrunt.hcl\n\n</FileTree>\n\n<Aside type=\"note\">\nParallel Execution: Stack generation runs concurrently to improve performance. The number of parallel tasks is determined by the `GOMAXPROCS` environment variable and can be explicitly controlled using the `--parallelism` flag:\n\n```bash\nterragrunt stack generate --parallelism 4\n```\n\nAutomatic Discovery: The command automatically discovers all `terragrunt.stack.hcl` files within the directory structure and generates them in parallel.\n\nValidation of Units and Stacks: During the stack generation, the system will validate that each unit and stack's target directory contains the appropriate configuration file (`terragrunt.hcl` for units and `terragrunt.stack.hcl` for stacks). This ensures the directories are correctly structured before proceeding with the stack generation.\nTo **skip this validation**, you can use the `--no-stack-validate` flag:\n\n```bash\nterragrunt stack generate --no-stack-validate\n```\n\n</Aside>\n\n<Aside type=\"caution\">\n  Path Restrictions: If an absolute path is provided as an argument, `generate` will throw an error. Only relative paths\n  within the working directory are supported.\n</Aside>\n\n<Aside type=\"caution\">\nRecreating the stack directory and cleaning stale files: By default, `terragrunt stack generate` does not delete files in `.terragrunt-stack` that are no longer produced by the current `terragrunt.stack.hcl`. If you removed or changed `values` or units and still see old files (e.g., a lingering `terragrunt.values.hcl`), regenerate from a clean state by either refreshing sources or cleaning the directory first:\n\n```bash\n# Refresh sources and regenerate from a clean copy\nterragrunt stack generate --source-update\n\n# Or explicitly remove the generated directory first\nterragrunt stack clean && terragrunt stack generate\n```\n\nYou can also pass `--source-update` when running commands via `stack run`:\n\n```bash\nterragrunt stack run plan --source-update\n```\n\n</Aside>\n"
  },
  {
    "path": "docs/src/data/commands/stack/output.mdx",
    "content": "---\nname: output\npath: stack/output\ncategory: stack\nsidebar:\n  order: 402\ndescription: Get outputs from a stack of units.\nusage: |\n  The `terragrunt stack output` command allows users to retrieve and interact with outputs from multiple units within a Terragrunt stack.\n\n  This feature simplifies handling infrastructure outputs by consolidating them into a single view.\nexamples:\n  - description: Get outputs from a stack of units.\n    code: |\n      terragrunt stack output\n  - description: Get outputs from a stack of units in JSON format.\n    code: |\n      terragrunt stack output --format json\n  - description: Get an output from a stack of units in raw format.\n    code: |\n      terragrunt stack output --format raw app.id\nflags:\n  - stack-output-format\n  - stack-output-json\n  - stack-output-raw\n  - no-stack-generate\n---\n\nExecuting `terragrunt stack output` in a stack directory produces an aggregated output from all units within the stack:\n\n```bash\n$ terragrunt stack output\nservice.output1 = \"output1\"\nservice.output2 = \"output2\"\ndb.output1 = \"output1\"\ndb.output2 = \"output2\"\n```\n\n## Indexing outputs\n\nTo retrieve outputs for a specific unit, specify the unit name:\n\n```bash\n$ terragrunt stack output project1_app1\nproject1_app1 = {\n  complex = {\n    delta     = 0.02\n    id        = 2\n    name      = \"name1\"\n    timestamp = \"2025-02-07T21:05:51Z\"\n  }\n  complex_list = [{\n    delta     = 0.02\n    id        = 10\n    name      = \"name1\"\n    timestamp = \"2025-02-07T21:05:51Z\"\n    }, {\n    delta     = 0.03\n    id        = 20\n    name      = \"name10\"\n    timestamp = \"2025-02-07T21:05:51Z\"\n  }]\n  custom_value1 = \"value1\"\n  data          = \"app1\"\n  list          = [\"1\", \"2\", \"3\"]\n}\n```\n\nYou can also retrieve a specific output from a unit:\n\n```bash\n$ terragrunt stack output project1_app1.custom_value1\nproject1_app1.custom_value1 = \"value1\"\n```\n\n## Output formats\n\nTerragrunt provides multiple output formats for easier parsing and integration with other tools. The desired format can be specified using the `--format` CLI flag.\n\n| Format    | Description                                                                     |\n|-----------|---------------------------------------------------------------------------------|\n| `default` | Format output as HCL.                                                           |\n| `json`    | Format output as JSON. This can be useful for integrations with other tools.    |\n| `raw`     | Format output as a simple raw string. Useful for integration into bash scripts. |\n\nTo retrieve outputs in structured JSON format:\n\n```bash\n$ terragrunt stack output --format json project1_app2\n{\n  \"project1_app2\": {\n    \"complex\": {\n      \"delta\": 0.02,\n      \"id\": 2,\n      \"name\": \"name2\",\n      \"timestamp\": \"2025-02-07T21:05:51Z\"\n    },\n    \"complex_list\": [\n      {\n        \"delta\": 0.02,\n        \"id\": 2,\n        \"name\": \"name2\",\n        \"timestamp\": \"2025-02-07T21:05:51Z\"\n      },\n      {\n        \"delta\": 0.03,\n        \"id\": 2,\n        \"name\": \"name3\",\n        \"timestamp\": \"2025-02-07T21:05:51Z\"\n      }\n    ],\n    \"custom_value2\": \"value2\",\n    \"data\": \"app2\",\n    \"list\": [\n      \"a\",\n      \"b\",\n      \"c\"\n    ]\n  }\n}\n```\n\n### json format\n\nAccessing a specific list inside JSON format:\n\n```bash\n$ terragrunt stack output --format json project1_app2.complex_list\n{\n  \"project1_app2.complex_list\": [\n    {\n      \"delta\": 0.02,\n      \"id\": 2,\n      \"name\": \"name2\",\n      \"timestamp\": \"2025-02-07T21:05:51Z\"\n    },\n    {\n      \"delta\": 0.03,\n      \"id\": 2,\n      \"name\": \"name3\",\n      \"timestamp\": \"2025-02-07T21:05:51Z\"\n    }\n  ]\n}\n```\n\n### raw format\n\nThe `raw` format returns outputs as plain values without additional structure. When accessing lists or structured outputs, indexes are required to extract values.\n\nRetrieving a simple value:\n\n```bash\n$ terragrunt stack output --format raw project1_app2.data\napp2\n```\n"
  },
  {
    "path": "docs/src/data/commands/stack/run.mdx",
    "content": "---\nname: run\npath: stack/run\ncategory: stack\nsidebar:\n  order: 401\ndescription: Run a command against a stack of units defined in a terragrunt.stack.hcl file.\nusage: |\n  Run OpenTofu/Terraform commands against a stack of units.\nexamples:\n  - description: |\n      Run a plan on each unit.\n    code: |\n      terragrunt stack run plan\n  - description: |\n      Apply changes for each unit.\n    code: |\n      terragrunt stack run apply\n  - description: |\n      Destroy all units.\n    code: |\n      terragrunt stack run destroy\nflags:\n  - no-stack-generate\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nThe `stack run *` command allows users to execute IaC commands across all units defined in a `terragrunt.stack.hcl` file.\nThis feature facilitates efficient orchestration of operations on multiple units, simplifying workflows for managing complex infrastructure stacks.\n\n## Automatic stack generation\n\nBefore executing the specified command, the `terragrunt stack run *` command will automatically generate the stack by creating\nthe `.terragrunt-stack` directory using the `terragrunt.stack.hcl` configuration file.\nThis ensures that all units are up-to-date before running the requested operation.\n\n<Aside type=\"note\">\nRefreshing sources and cleaning stale files: If you change or remove units/values and find stale files remain in `.terragrunt-stack`, pass `--source-update` to refresh sources before generation, or clean the directory first:\n\n```bash\nterragrunt stack run plan --source-update\n\n# or\nterragrunt stack clean && terragrunt stack run plan\n```\n</Aside>\n"
  },
  {
    "path": "docs/src/data/compatibility/compatibility.json",
    "content": "[\n  {\n    \"id\": \"opentofu-1.11.x\",\n    \"tool\": \"opentofu\",\n    \"version\": \"1.11.x\",\n    \"terragrunt_min\": \"0.95.0\",\n    \"terragrunt_max\": null,\n    \"order\": 26\n  },\n  {\n    \"id\": \"opentofu-1.10.x\",\n    \"tool\": \"opentofu\",\n    \"version\": \"1.10.x\",\n    \"terragrunt_min\": \"0.82.0\",\n    \"terragrunt_max\": null,\n    \"order\": 25\n  },\n  {\n    \"id\": \"opentofu-1.9.x\",\n    \"tool\": \"opentofu\",\n    \"version\": \"1.9.x\",\n    \"terragrunt_min\": \"0.72.0\",\n    \"terragrunt_max\": null,\n    \"order\": 24\n  },\n  {\n    \"id\": \"opentofu-1.8.x\",\n    \"tool\": \"opentofu\",\n    \"version\": \"1.8.x\",\n    \"terragrunt_min\": \"0.66.0\",\n    \"terragrunt_max\": null,\n    \"order\": 23\n  },\n  {\n    \"id\": \"opentofu-1.7.x\",\n    \"tool\": \"opentofu\",\n    \"version\": \"1.7.x\",\n    \"terragrunt_min\": \"0.58.0\",\n    \"terragrunt_max\": null,\n    \"order\": 22\n  },\n  {\n    \"id\": \"opentofu-1.6.x\",\n    \"tool\": \"opentofu\",\n    \"version\": \"1.6.x\",\n    \"terragrunt_min\": \"0.52.0\",\n    \"terragrunt_max\": null,\n    \"order\": 21\n  },\n  {\n    \"id\": \"terraform-1.14.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"1.14.x\",\n    \"terragrunt_min\": \"0.94.0\",\n    \"terragrunt_max\": null,\n    \"order\": 20\n  },\n  {\n    \"id\": \"terraform-1.13.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"1.13.x\",\n    \"terragrunt_min\": \"0.86.0\",\n    \"terragrunt_max\": null,\n    \"order\": 19\n  },\n  {\n    \"id\": \"terraform-1.12.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"1.12.x\",\n    \"terragrunt_min\": \"0.80.0\",\n    \"terragrunt_max\": null,\n    \"order\": 18\n  },\n  {\n    \"id\": \"terraform-1.11.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"1.11.x\",\n    \"terragrunt_min\": \"0.75.0\",\n    \"terragrunt_max\": null,\n    \"order\": 17\n  },\n  {\n    \"id\": \"terraform-1.10.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"1.10.x\",\n    \"terragrunt_min\": \"0.74.0\",\n    \"terragrunt_max\": null,\n    \"order\": 16\n  },\n  {\n    \"id\": \"terraform-1.9.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"1.9.x\",\n    \"terragrunt_min\": \"0.60.0\",\n    \"terragrunt_max\": null,\n    \"order\": 15\n  },\n  {\n    \"id\": \"terraform-1.8.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"1.8.x\",\n    \"terragrunt_min\": \"0.57.0\",\n    \"terragrunt_max\": null,\n    \"order\": 14\n  },\n  {\n    \"id\": \"terraform-1.7.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"1.7.x\",\n    \"terragrunt_min\": \"0.56.0\",\n    \"terragrunt_max\": null,\n    \"order\": 13\n  },\n  {\n    \"id\": \"terraform-1.6.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"1.6.x\",\n    \"terragrunt_min\": \"0.53.0\",\n    \"terragrunt_max\": null,\n    \"order\": 12\n  },\n  {\n    \"id\": \"terraform-1.5.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"1.5.x\",\n    \"terragrunt_min\": \"0.48.0\",\n    \"terragrunt_max\": null,\n    \"order\": 11\n  },\n  {\n    \"id\": \"terraform-1.4.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"1.4.x\",\n    \"terragrunt_min\": \"0.45.0\",\n    \"terragrunt_max\": null,\n    \"order\": 10\n  },\n  {\n    \"id\": \"terraform-1.3.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"1.3.x\",\n    \"terragrunt_min\": \"0.40.0\",\n    \"terragrunt_max\": null,\n    \"order\": 9\n  },\n  {\n    \"id\": \"terraform-1.2.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"1.2.x\",\n    \"terragrunt_min\": \"0.38.0\",\n    \"terragrunt_max\": null,\n    \"order\": 8\n  },\n  {\n    \"id\": \"terraform-1.1.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"1.1.x\",\n    \"terragrunt_min\": \"0.36.0\",\n    \"terragrunt_max\": null,\n    \"order\": 7\n  },\n  {\n    \"id\": \"terraform-1.0.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"1.0.x\",\n    \"terragrunt_min\": \"0.31.0\",\n    \"terragrunt_max\": null,\n    \"order\": 6\n  },\n  {\n    \"id\": \"terraform-0.15.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"0.15.x\",\n    \"terragrunt_min\": \"0.29.0\",\n    \"terragrunt_max\": null,\n    \"order\": 5\n  },\n  {\n    \"id\": \"terraform-0.14.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"0.14.x\",\n    \"terragrunt_min\": \"0.27.0\",\n    \"terragrunt_max\": null,\n    \"order\": 4\n  },\n  {\n    \"id\": \"terraform-0.13.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"0.13.x\",\n    \"terragrunt_min\": \"0.25.0\",\n    \"terragrunt_max\": null,\n    \"order\": 3\n  },\n  {\n    \"id\": \"terraform-0.12.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"0.12.x\",\n    \"terragrunt_min\": \"0.19.0\",\n    \"terragrunt_max\": \"0.24.4\",\n    \"order\": 2\n  },\n  {\n    \"id\": \"terraform-0.11.x\",\n    \"tool\": \"terraform\",\n    \"version\": \"0.11.x\",\n    \"terragrunt_min\": \"0.14.0\",\n    \"terragrunt_max\": \"0.18.7\",\n    \"order\": 1\n  }\n]\n"
  },
  {
    "path": "docs/src/data/flags/all.mdx",
    "content": "---\nname: all\ndescription: Run the specified OpenTofu/Terraform command on the stack of units in the current directory.\ntype: bool\nenv:\n  - TG_ALL\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nWhen this flag is set, Terragrunt will run the specified OpenTofu/Terraform command against all units in the current stack. This is useful when you want to apply changes across multiple units at once.\n\nFor example:\n\n```bash\nterragrunt run --all plan\n```\n\nThis will run the `plan` command against all units in the current stack.\n\nTo learn more about how to use this flag, see the [Stacks](/features/stacks) feature documentation.\n\n<Aside type=\"note\">\nTerragrunt has a notion of \"external dependencies\". These are units that are not part of the current stack, from the perspective of the current working directory, but are dependencies of units that are part of the current stack.\n\nWhen running an `--all` command, Terragrunt will discover any external dependencies and prompt you to confirm that you want to run them as well.\n\nIf you would like to suppress this prompt, you can use the [`--non-interactive` flag](/reference/cli/commands/run#non-interactive). You can also prevent this behavior by setting  [`--queue-exclude-external`](/reference/cli/commands/run#queue-exclude-external).\n</Aside>\n\n<Aside type=\"danger\">\nWhen running a `run --all destroy` command, Terragrunt will destroy all dependencies of the units under the current working directory, in addition to the units themselves by default!\n\nIf you wish to prevent external dependencies from being destroyed, add the [`--queue-exclude-external` flag](/reference/cli/commands/run#queue-exclude-external), or use the [`--exclude-dir` flag](/reference/cli/commands/run#exclude-dir) once for each directory you wish to exclude.\n</Aside>\n\n<Aside type=\"caution\">\nUse `run --all` with care if you have unapplied dependencies.\n\nIf you have a stack of Terragrunt units with dependencies between them via `dependency` blocks\nand you've never deployed them, then commands like `run --all plan` will fail,\nas it won't be possible to resolve outputs of `dependency` blocks without applying them first.\n\nThe solution for this is to take advantage of [mock outputs in dependency blocks](/reference/hcl/blocks/#dependency).\n</Aside>\n\n<Aside type=\"note\">\nUsing `run --all` with `apply` or `destroy` silently adds the `-auto-approve` flag to the command line\narguments passed to OpenTofu/Terraform due to issues with shared `stdin` making individual approvals impossible.\n\n</Aside>\n\n<Aside type=\"note\">\nUsing the OpenTofu/Terraform [-detailed-exitcode](https://opentofu.org/docs/cli/commands/plan/#other-options)\nflag with the `run --all` command results in an aggregate exit code being returned, rather than the exit code of any particular unit.\n\nThe algorithm for determining the aggregate exit code is as follows:\n\n- If any unit throws a 1, Terragrunt will throw a 1.\n- If any unit throws a 2, but nothing throws a 1, Terragrunt will throw a 2.\n- If nothing throws a non-zero, Terragrunt will throw a 0.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/auth-provider-cmd.mdx",
    "content": "---\nname: auth-provider-cmd\ndescription: Run the provided command and arguments to authenticate Terragrunt dynamically when necessary.\nlookup: true\ntype: string\nenv:\n  - TG_AUTH_PROVIDER_CMD\n---\n\nimport { Aside, Code } from '@astrojs/starlight/components';\n\nimport authProviderCmdSchema from '../../../public/schemas/auth-provider-cmd/v2/schema.json?raw';\n\nThe command and arguments used to obtain authentication credentials dynamically. If specified, Terragrunt runs this command whenever it might need authentication. This includes HCL parsing, where it might be useful to authenticate with a cloud provider _before_ running HCL functions like [`get_aws_account_id`](/reference/hcl/functions#get_aws_account_id) where authentication has to already have taken place. It can also be useful for HCL functions like [`run_cmd`](/reference/hcl/functions#run_cmd) where it may be useful to be authenticated before calling the function.\n\nThe output must be valid JSON of the following schema:\n\n<Code title=\"auth-provider-cmd/v2/schema.json\" lang=\"json\" code={authProviderCmdSchema} />\n\nThis allows Terragrunt to acquire different credentials at runtime without changing any `terragrunt.hcl` configuration. You can use this flag to set arbitrary credentials for continuous integration, authentication with providers other than AWS and more.\n\nAs long as the standard output of the command passed to `auth-provider-cmd` results in JSON matching the schema above, corresponding environment variables will be set (and/or roles assumed) before Terragrunt begins parsing an `terragrunt.hcl` file or running an OpenTofu/Terraform command.\n\nThe simplest approach to leverage this flag is to write a script that fetches desired credentials, and emits them to STDOUT in the JSON format listed above:\n\n```bash\n#!/usr/bin/env bash\n\necho -n '{\"envs\": {\"KEY\": \"a secret\"}}'\n```\n\nYou can use any technology for the authentication provider you'd like, however, as long as Terragrunt can execute it. The expected pattern for using this flag is to author a script/program that will dynamically fetch secret values from a secret store, etc. then emit them to STDOUT for consumption by Terragrunt.\n\nNote that more specific configurations (e.g. `awsCredentials`) take precedence over less specific configurations (e.g. `envs`).\n\nIf you would like to set credentials for AWS with this method, you are encouraged to use `awsCredentials` instead of `envs`, as these keys will be validated to conform to the officially supported environment variables expected by the AWS SDK.\n\nSimilarly, if you would like Terragrunt to assume an AWS role on your behalf, you are encouraged to use the `awsRole` configuration instead of `envs`.\n\nOther credential configurations will be supported in the future, but until then, if your provider authenticates via environment variables, you can use the `envs` field to fetch credentials dynamically from a secret store, etc before Terragrunt executes any IAC.\n\n<Aside type=\"note\">\nThe `awsRole` configuration is only used when the `awsCredentials` configuration is not present. If both are present, the `awsCredentials` configuration will take precedence.\n</Aside>\n\n<Aside type=\"caution\">\nWhen using `iam-assume-role`, AWS authentication takes place right before a Terraform run, after `terragrunt.hcl` files are fully parsed. This means HCL functions like `get_aws_account_id` and `run_cmd` will not run with the assumed role credentials. If you need roles to be assumed prior to parsing configurations, use `auth-provider-cmd` instead.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/backend-bootstrap-all.mdx",
    "content": "---\nname: all\ndescription: When this flag is set, Terragrunt will bootstrap all units discovered in the current working directory.\ntype: bool\nenv:\n  - TG_ALL\n---\n"
  },
  {
    "path": "docs/src/data/flags/backend-bootstrap-config.mdx",
    "content": "---\nname: config\ndescription: Path to the Terragrunt configuration file to use when bootstrapping the resources.\ntype: string\nenv:\n  - TG_CONFIG\n---\n"
  },
  {
    "path": "docs/src/data/flags/backend-bootstrap-download-dir.mdx",
    "content": "---\nname: download-dir\ndescription: Path to download OpenTofu/Terraform modules into. The default is `.terragrunt-cache`.\ntype: string\nenv:\n  - TG_DOWNLOAD_DIR\n---\n"
  },
  {
    "path": "docs/src/data/flags/backend-delete-all.mdx",
    "content": "---\nname: all\ndescription: When this flag is set Terragrunt will delete the backend state for all units discovered in the current working directory.\ntype: bool\nenv:\n  - TG_ALL\n---\n"
  },
  {
    "path": "docs/src/data/flags/backend-delete-config.mdx",
    "content": "---\nname: config\ndescription: Path to the Terragrunt configuration file to use to delete the resources.\ntype: string\nenv:\n  - TG_CONFIG\n---\n"
  },
  {
    "path": "docs/src/data/flags/backend-delete-download-dir.mdx",
    "content": "---\nname: download-dir\ndescription: Path to download OpenTofu/Terraform modules into. The default is `.terragrunt-cache`.\ntype: string\nenv:\n  - TG_DOWNLOAD_DIR\n---\n"
  },
  {
    "path": "docs/src/data/flags/backend-delete-force.mdx",
    "content": "---\nname: force\ndescription: |\n  When this flag is set, Terragrunt will delete the backend state regardless of whether the bucket containing it has versioning enabled.\ntype: bool\nenv:\n  - TG_FORCE\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n<Aside type=\"danger\">\n\nThis flag is dangerous and should be used with caution.\n\nGruntwork recommends always enabling versioning on your backend state resources. Deleting backend state without versioning enabled can result in irreversible data loss.\n\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/backend-migrate-config.mdx",
    "content": "---\nname: config\ndescription: |\n  Path to the Terragrunt configuration file to use to migrate the resources.\n\n  Note that this path is relative to the directory of each of the source and destination units, not the current working directory.\ntype: string\nenv:\n  - TG_CONFIG\n---\n"
  },
  {
    "path": "docs/src/data/flags/backend-migrate-download-dir.mdx",
    "content": "---\nname: download-dir\ndescription: |\n    Path to download OpenTofu/Terraform modules into. The default is `.terragrunt-cache`.\n\n    Note that this path is relative to the directory of each of the source and destination units, not the current working directory.\ntype: string\nenv:\n  - TG_DOWNLOAD_DIR\n---\n"
  },
  {
    "path": "docs/src/data/flags/backend-migrate-force.mdx",
    "content": "---\nname: force\ndescription: |\n  When this flag is set, Terragrunt will force the migration of the backend state, even if the bucket containing it has versioning disabled.\ntype: bool\nenv:\n  - TG_FORCE\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n<Aside type=\"danger\">\n\nThis flag is dangerous and should be used with caution.\n\nGruntwork recommends always enabling versioning on your backend state resources. Migrating backend state without versioning enabled can result in irreversible data loss.\n\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/catalog-no-hooks.mdx",
    "content": "---\nname: no-hooks\ndescription: \"Disable hooks when using boilerplate templates in the catalog.\"\ntype: bool\nenv:\n  - TG_NO_HOOKS\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nWhen enabled, Terragrunt will disable hooks when processing boilerplate templates during catalog operations.\n\nThis is useful for security reasons when you want to prevent templates from executing arbitrary hooks on your system.\n\n<Aside type=\"caution\">\nDo not use catalog/scaffold to scaffold untrusted templates. IaC configurations are inherently powerful, as they\ncan run arbitrary code on your system, so make sure to only use trusted templates you have reviewed and approved.\n</Aside>\n\n<Aside type=\"note\">\nNote that you will otherwise be prompted to confirm the execution of each hook unless you are running in [non-interactive mode](/reference/cli/global-flags/#non-interactive).\n</Aside>\n\nExamples:\n\n```bash\nterragrunt catalog --no-hooks\n```\n"
  },
  {
    "path": "docs/src/data/flags/catalog-no-include-root.mdx",
    "content": "---\nname: no-include-root\ndescription: \"Do not include the root configuration file in any generated terragrunt.hcl file during scaffolding.\"\ntype: bool\nenv:\n  - TG_NO_INCLUDE_ROOT\n---\n\nWhen enabled, Terragrunt will not automatically include the root configuration file in generated `terragrunt.hcl` files during scaffolding operations in the catalog.\n\nThis is useful when you want more control over which configurations are included in newly scaffolded modules.\n\nExamples:\n\n```bash\nterragrunt catalog --no-include-root\n```\n"
  },
  {
    "path": "docs/src/data/flags/catalog-no-shell.mdx",
    "content": "---\nname: no-shell\ndescription: \"Disable shell commands when using boilerplate templates in the catalog.\"\ntype: bool\nenv:\n  - TG_NO_SHELL\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nWhen enabled, Terragrunt will disable shell commands when processing boilerplate templates during catalog operations.\n\nThis is useful for security reasons when you want to prevent templates from executing arbitrary shell commands on your system.\n\n<Aside type=\"caution\">\nDo not use catalog/scaffold to scaffold untrusted templates. IaC configurations are inherently powerful, as they\ncan run arbitrary code on your system, so make sure to only use trusted templates you have reviewed and approved.\n</Aside>\n\n<Aside type=\"note\">\nNote that you will otherwise be prompted to confirm the execution of each shell command unless you are running in [non-interactive mode](/reference/cli/global-flags/#non-interactive).\n</Aside>\n\nExamples:\n\n```bash\nterragrunt catalog --no-shell\n```\n"
  },
  {
    "path": "docs/src/data/flags/catalog-root-file-name.mdx",
    "content": "---\nname: root-file-name\ndescription: The name of the root configuration file to include in any generated terragrunt.hcl during scaffolding.\ntype: string\nenv:\n  - TG_ROOT_FILE_NAME\n---\n\nSpecifies the name of the root configuration file that should be:\n- Included in generated `terragrunt.hcl` files during scaffolding\n- Used when searching for Catalog URLs\n\nThis is particularly useful when your root configuration uses a different naming convention than the default.\n\nExample:\n\n```bash\nterragrunt catalog --root-file-name root.hcl\n```\n"
  },
  {
    "path": "docs/src/data/flags/config.mdx",
    "content": "---\nname: config\ndescription: The path to the Terragrunt config file. Default is terragrunt.hcl.\ntype: string\nenv:\n  - TG_CONFIG\n---\n\nThis flag allows you to specify a custom path to your Terragrunt configuration file. By default, Terragrunt looks for a file named `terragrunt.hcl` in the current directory.\n\nThis is useful when you:\n- Have multiple Terragrunt configurations in the same directory.\n- Want to use a different naming convention for your configuration files.\n- Need to test alternative configurations without modifying the default file.\n\nExample usage:\n\n```bash\nterragrunt run plan --config custom-config.hcl\n```\n"
  },
  {
    "path": "docs/src/data/flags/dependency-fetch-output-from-state.mdx",
    "content": "---\nname: dependency-fetch-output-from-state\ndescription: |\n  Fetch dependency outputs directly from the state file instead of using `tofu output`.\ntype: bool\nenv:\n  - TG_DEPENDENCY_FETCH_OUTPUT_FROM_STATE\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nThis flag modifies how Terragrunt retrieves output values from dependent units. When enabled, Terragrunt will read the outputs directly from the state file instead of running `terraform output` or `tofu output`.\n\n<Aside type=\"note\">\nThis flag is equivalent to enabling the `dependency-fetch-output-from-state` experiment. You can also enable this feature using `--experiment dependency-fetch-output-from-state` or `--experiment-mode`. For more information, see the [Experiments documentation](/reference/experiments#dependency-fetch-output-from-state).\n</Aside>\n\nThe main benefit this flag provides is performance. Reading directly from state is typically faster than executing the OpenTofu/Terraform binary to get the same outputs.\n\nThe limitation of this approach is that it is only supported by the S3 backend, and OpenTofu/Terraform may change the schema of the state file in the future, breaking this functionality.\n\n<Aside type=\"caution\">\nAvoid using this flag without pinning the version of OpenTofu/Terraform you are using.\n\nThere is no guarantee that OpenTofu/Terraform will maintain the existing schema of their state files, so there is also no guarantee that the flag will work as expected in future versions of OpenTofu/Terraform. They have not changed the schema of the state file in a long time, but there is no guarantee that they will not change it in the future. We are coordinating with the OpenTofu team to encourage stability in the state file schema, unless significant performance improvements can be made to OpenTofu output fetching to make this flag unnecessary.\n</Aside>\n\n<Aside type=\"tip\">\nDirect output fetching is a performance optimization. For more details on performance optimizations, their tradeoffs, and other performance tips, read the dedicated [Performance documentation](/troubleshooting/performance).\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/destroy-dependencies-check.mdx",
    "content": "---\nname: destroy-dependencies-check\ndescription: |\n  Enables Terragrunt's dependency validation during destroy operations.\ntype: bool\nenv:\n  - TG_DESTROY_DEPENDENCIES_CHECK\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nWhen enabled, Terragrunt will verify if there are dependent units that would be affected by destroying the current unit and show a warning with the list of dependent modules.\n\n<Aside type=\"tip\">\nEnabling dependency checks during destroy operations helps prevent accidental destruction of infrastructure that other units depend on.\n</Aside>\n\n"
  },
  {
    "path": "docs/src/data/flags/disable-bucket-update.mdx",
    "content": "---\nname: disable-bucket-update\ndescription: |\n  When this flag is set, Terragrunt will not update the remote state resources.\ntype: bool\nenv:\n  - TG_DISABLE_BUCKET_UPDATE\n---\n\nWhen enabled, Terragrunt will throw an error if it detects that remote state resources need to be updated.\n\nThis is useful in scenarios where:\n- You want to ensure state bucket configurations remain unchanged during operations\n- You have separate processes for managing state bucket configurations\n\nThe flag acts as a safety mechanism to prevent unintended modifications to your state storage infrastructure.\n"
  },
  {
    "path": "docs/src/data/flags/disable-command-validation.mdx",
    "content": "---\nname: disable-command-validation\ndescription: When this flag is set, Terragrunt will not validate the tofu/terraform command.\ntype: bool\nenv:\n  - TG_DISABLE_COMMAND_VALIDATION\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n<Aside type=\"caution\">\n**Deprecated**: The `--disable-command-validation` flag is deprecated and will be removed in a future version of Terragrunt. Command validation has been removed entirely from Terragrunt. The `run` command now accepts any command and passes it through to the underlying OpenTofu/Terraform binary without validation. This flag no longer has any effect and can be safely removed from your commands.\n\nTo prepare for this change, you can enable the `disable-command-validation` strict control to ensure you're not using this flag. For more information, see the [Strict Controls documentation](/reference/strict-controls/#disable-command-validation).\n</Aside>\n\nThis flag disables Terragrunt's built-in validation of OpenTofu/Terraform commands. When enabled, Terragrunt will pass commands through to OpenTofu/Terraform without checking if they are valid or supported.\n"
  },
  {
    "path": "docs/src/data/flags/download-dir.mdx",
    "content": "---\nname: download-dir\ndescription: The path to download OpenTofu/Terraform modules into. Default is .terragrunt-cache in the working directory.\ntype: string\nenv:\n  - TG_DOWNLOAD_DIR\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nSpecifies a custom directory where Terragrunt will download and cache OpenTofu/Terraform modules.\n\nBy default, modules are downloaded to `.terragrunt-cache` in the working directory.\n\n<Aside type=\"note\">\nThe download directory (`.terragrunt-cache` by default) can grow significantly over time with multiple versions of modules.\n\nRemember to add this directory to your `.gitignore` file and consider periodic cleanup of old cached content.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/engine-cache-path.mdx",
    "content": "---\nname: engine-cache-path\ndescription: Cache path for Terragrunt engine files.\ntype: string\nenv:\n  - TG_ENGINE_CACHE_PATH\n---\n\nSpecifies the directory where Terragrunt will store its engine cache files. The engine cache helps improve performance by storing compiled configurations and other internal data.\n\nWhen not specified, Terragrunt will use a default location in the system's temporary directory.\n\nYou must also set the [`experimental-engine`](/reference/cli/commands/run#experimental-engine) flag to enable engine usage.\n"
  },
  {
    "path": "docs/src/data/flags/engine-log-level.mdx",
    "content": "---\nname: engine-log-level\ndescription: Terragrunt engine log level.\ntype: string\nenv:\n  - TG_ENGINE_LOG_LEVEL\n---\n\nControls the verbosity of logs from the Terragrunt engine. This is separate from the main Terragrunt logging and specifically affects engine-related operations like configuration parsing and dependency resolution.\n\nBy default, the engine log level is set to that of the main Terragrunt logging level.\n\nYou must also set the [`experimental-engine`](/reference/cli/commands/run#experimental-engine) flag to enable engine usage.\n"
  },
  {
    "path": "docs/src/data/flags/engine-skip-check.mdx",
    "content": "---\nname: engine-skip-check\ndescription: Skip checksum check for Terragrunt engine files.\ntype: bool\nenv:\n  - TG_ENGINE_SKIP_CHECK\n---\n\nWhen enabled, Terragrunt will skip the checksum validation of engine files.\n\nThis can be useful during development or testing, and is currently the only way to use self-developed engines.\n\nTerragrunt currently only has support for verifying the signatures of official engines, though this will change in the future.\n\nIn the meantime, you are encouraged to download engines from a trusted source, verify the integrity of the engine manually, and then reference the local path to the engine in your Terragrunt configuration.\n\nYou must also set the [`experimental-engine`](/reference/cli/commands/run#experimental-engine) flag to use engines.\n"
  },
  {
    "path": "docs/src/data/flags/experiment-mode.mdx",
    "content": "---\nname: experiment-mode\ndescription: Enables experiment mode for Terragrunt.\ntype: boolean\nenv:\n  - TG_EXPERIMENT_MODE\n---\n\nFor more information, see the [experiments documentation](/reference/experiments).\n"
  },
  {
    "path": "docs/src/data/flags/experiment.mdx",
    "content": "---\nname: experiment\ndescription: Enables a specific experiment.\ntype: string\nenv:\n  - TG_EXPERIMENT\n---\n\nFor a list of available experiments, see the [experiments documentation](/reference/experiments).\n"
  },
  {
    "path": "docs/src/data/flags/experimental-engine.mdx",
    "content": "---\nname: experimental-engine\ndescription: Enable Terragrunt experimental engine.\ntype: bool\nenv:\n  - TG_EXPERIMENTAL_ENGINE\n---\n\nEnables the `iac-engine` experiment to use [Terragrunt IaC engines](/features/units/engine).\n\nThis flag is equivalent to using `--experiment iac-engine`. You can also enable this experiment using the environment variable `TG_EXPERIMENT=iac-engine`.\n\nIaC engines are still experimental, as the API is unstable and may change in future minor versions of Terragrunt.\n\nIf you are using a remote custom engine, you must also set the [`engine-skip-check`](/reference/cli/commands/run#engine-skip-check) flag to skip the signature check for the engine.\n"
  },
  {
    "path": "docs/src/data/flags/fail-fast.mdx",
    "content": "---\nname: fail-fast\ndescription: Fail the run if any unit fails, stopping all remaining units immediately.\ntype: bool\nenv:\n  - TG_FAIL_FAST\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nWhen enabled, Terragrunt will fail the entire run as soon as any unit fails. If any unit encounters an error during execution, all remaining units will be skipped and the run will exit with a failure status immediately.\n\nThis is useful for enforcing strict failure handling in CI/CD pipelines or when you want to stop processing on the first error.\n"
  },
  {
    "path": "docs/src/data/flags/feature.mdx",
    "content": "---\nname: feature\ndescription: Set feature flags for the HCL code.\ntype: string\nenv:\n  - TG_FEATURE\n---\n\nAllows enabling or disabling specific features in the HCL code.\n\nTo learn more about feature flags, see the [Feature Flags](/features/units/runtime-control#feature-flags) feature documentation.\n"
  },
  {
    "path": "docs/src/data/flags/filter-affected.mdx",
    "content": "---\nname: filter-affected\ndescription: Filter components affected by changes between the default branch and HEAD\ntype: bool\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nThe `--filter-affected` flag is a convenient shorthand for filtering components that have been modified, added, or removed between the default branch (typically `main`) and `HEAD`. It is equivalent to using `--filter '[main...HEAD]'` (or `--filter '[<defaultBranch>...HEAD]'` if your repository uses a different default branch).\n\n## Usage\n\n```bash\n# Find all components affected by changes between main and HEAD\nterragrunt find --filter-affected\n\n# List affected components\nterragrunt list --filter-affected\n\n# Run plan on affected components\nterragrunt run --all --filter-affected -- plan\n\n# Run apply on affected components\nterragrunt run --all --filter-affected -- apply\n```\n\n## Default Branch Detection\n\nThe flag automatically detects your repository's default branch. It checks:\n\n1. Your Git configuration for `init.defaultBranch`\n2. Falls back to `main` if not configured\n\nTo use a different branch for comparison, use the `--filter` flag directly with a Git-based filter expression:\n\n```bash\n# Compare against a specific branch\nterragrunt find --filter '[develop...HEAD]'\n\n# Compare between two specific references\nterragrunt find --filter '[v1.0.0...v2.0.0]'\n```\n\n## Uncommitted Changes Warning\n\nIf you have uncommitted changes in your working directory, Terragrunt will display a warning:\n\n```\nWarning: You have uncommitted changes. The --filter-affected flag may not include all your local modifications.\n```\n\nThis is because `--filter-affected` compares Git references, not your working directory. To include uncommitted changes, you would need to commit them first or use a different filtering approach.\n\n## Equivalent Filter Expression\n\nThe `--filter-affected` flag is equivalent to:\n\n```bash\nterragrunt find --filter '[main...HEAD]'\n```\n\nOr, if your default branch is different:\n\n```bash\nterragrunt find --filter '[<defaultBranch>...HEAD]'\n```\n\n## Learn More\n\nFor more information about Git-based filtering and advanced usage patterns, see the [Filters feature documentation](/features/filter/git).\n\n"
  },
  {
    "path": "docs/src/data/flags/filter.mdx",
    "content": "---\nname: filter\ndescription: Filter configurations using a flexible query language\ntype: list(string)\n---\n\nimport {Aside} from '@astrojs/starlight/components';\n\nThe `--filter` flag provides a sophisticated querying syntax for targeting specific [units](/features/units) and [stacks](/features/stacks) in Terragrunt commands.\n\n## Usage\n\n```bash\nterragrunt find --filter 'app*'\nterragrunt list --filter './prod/** | type=unit'\nterragrunt run --all --filter './prod/**' -- plan\nterragrunt hcl fmt --filter './prod/**'\nterragrunt hcl validate --filter 'type=unit'\n```\n\n### Name-Based Filtering\n\nMatch configurations by their name using exact matches or glob patterns:\n\n```bash\n# Exact match\nterragrunt find --filter app1\n\n# Glob pattern\nterragrunt find --filter 'app*'\n```\n\n### Path-Based Filtering\n\nMatch configurations by their file system path:\n\n```bash\n# Relative paths with globs\nterragrunt find --filter './envs/prod/**'\n\n# Absolute paths\nterragrunt find --filter '/absolute/path/to/envs/dev/apps/*'\n```\n\n### Attribute-Based Filtering\n\nMatch configurations by their configuration attributes:\n\n```bash\n# Filter by type\nterragrunt find --filter 'type=unit'\nterragrunt find --filter 'type=stack'\n\n# Filter by external dependency status\nterragrunt find --filter 'external=false'\n\n# Filter by files read\nterragrunt find --filter 'reading=shared.hcl'\nterragrunt find --filter 'reading=common/*.hcl' # Globs supported!\nterragrunt find --filter 'reading=config/**' # Double-wildcard globs are required filtering on files nested in subdirectories.\nterragrunt find --filter 'reading=config/vars.tfvars'\n```\n\n<Aside type=\"tip\" title=\"What does 'Reading' mean?\">\n\nThe `reading` attribute filters configurations based on which files they themselves read. This is useful for finding all infrastructure that could potentially be impacted by shared configuration, and is particularly useful when a commonly included configuration (like `root.hcl`) is updated.\n\nSee the [mark_as_read function documentation](/reference/hcl/functions/#mark_as_read) for more information on how Terragrunt tracks files that are read during parsing.\n\n</Aside>\n\n### Negation\n\nExclude configurations using the `!` prefix:\n\n```bash\n# Exclude by name\nterragrunt find --filter '!app1'\n\n# Exclude by path\nterragrunt find --filter '!./prod/**'\n```\n\n### Intersection (Refinement)\n\nUse the `|` operator to refine results:\n\n```bash\n# Find all units in prod directory\nterragrunt find --filter './prod/** | type=unit'\n\n# Chain multiple filters\nterragrunt find --filter './dev/** | type=unit | !name=unit1'\n```\n\n### Git-Based Filtering\n\nFilter configurations based on changes between Git references. For the common use case of comparing the default branch with `HEAD`, you can use the `--filter-affected` flag as a convenient shorthand:\n\n```bash\n# Find components affected by changes between main and HEAD\nterragrunt find --filter-affected\n```\n\nFor more control, use Git-based filter expressions directly:\n\n```bash\n# Compare between two references\nterragrunt find --filter '[main...HEAD]'\n\n# Shorthand: compare reference to HEAD\nterragrunt find --filter '[main]'\n\n# Compare between specific commits\nterragrunt find --filter '[abc123...def456]'\n```\n\nFor more details and examples, see the [Filters feature documentation](/features/filter/git).\n\n### Graph-Based Filtering\n\nFilter configurations based on dependency relationships using graph traversal. Use ellipsis (`...`) to traverse the dependency graph and caret (`^`) to exclude the target from results.\n\n**Syntax variants:**\n- `foo...` - Include target and all dependencies (things it depends on)\n- `...foo` - Include target and all dependents (things that depend on it)\n- `...foo...` - Include target, dependencies, and dependents\n- `^foo...` - Include only dependencies (exclude target)\n- `...^foo` - Include only dependents (exclude target)\n- `...^foo...` - Include dependencies and dependents (exclude target)\n\n```bash\n# Find 'service' and everything it depends on\nterragrunt find --filter 'service...'\n\n# Find 'vpc' and everything that depends on it\nterragrunt find --filter '...vpc'\n\n# Find complete dependency graph for 'db'\nterragrunt find --filter '...db...'\n\n# Find dependencies of 'service' but exclude 'service' itself\nterragrunt find --filter '^service...'\n\n# Combine graph traversal with path filters (note the use of braces to escape the path)\nterragrunt find --filter '{./apps/service}...'\n\n# Combine graph traversal with attribute filters\nterragrunt find --filter '...type=unit'\n\n# Refine graph results with intersection\nterragrunt find --filter 'service... | external=false'\n```\n\n<Aside type=\"note\">\nGraph expressions require dependency discovery to work correctly. When using graph expressions, Terragrunt automatically discovers dependency relationships between components. This may add some overhead compared to simple name or path filters.\n\nFor more detailed examples and explanations, see the [Filters feature documentation](/features/filter/graph).\n</Aside>\n\n### Union (Multiple Filters)\n\nSpecify multiple `--filter` flags to combine results using OR logic:\n\n```bash\n# Find components named 'unit1' OR 'stack1'\nterragrunt find --filter unit1 --filter stack1\n```\n\n### The filters file\n\nInstead of specifying filters on the command line, you can store filter queries in a file. By default, Terragrunt automatically reads filter queries from the `.terragrunt-filters` file in your current working directory if it exists.\n\n```bash\n# Automatically reads .terragrunt-filters (no flag needed)\nterragrunt find\n\n# Use a custom filters file\nterragrunt find --filters-file custom-filters.txt\n\n# Disable automatic file reading\nterragrunt find --no-filters-file\n```\n\nThe filters file should contain one filter query per line. Empty lines and lines starting with `#` are ignored:\n\n```text\n# Production environment filters\ntype=unit\n./prod/**\n\n# Exclude test units\n!name=test-*\n```\n\n## Supported Commands\n\nCurrently supported in:\n- [find](/reference/cli/commands/find)\n- [list](/reference/cli/commands/list)\n- [run](/reference/cli/commands/run)\n- [hcl fmt](/reference/cli/commands/hcl/fmt)\n- [hcl validate](/reference/cli/commands/hcl/validate)\n\nPlanned for future releases:\n- [dag graph](/reference/cli/commands/dag/graph)\n\n## Learn More\n\nFor comprehensive examples and advanced usage patterns, see the [Filters feature documentation](/features/filter).\n"
  },
  {
    "path": "docs/src/data/flags/filters-file.mdx",
    "content": "---\nname: filters-file\ndescription: Path to a file containing filter queries, one per line\ntype: string\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nThe `--filters-file` flag allows you to specify a custom file path containing filter queries. By default, Terragrunt automatically reads filter queries from any `.terragrunt-filters` file in the current working directory. Use this flag to specify a different file path.\n\n## Usage\n\n```bash\n# Use a custom filters file\nterragrunt find --filters-file custom-filters.txt\n\n# Use a filters file in a different directory\nterragrunt find --filters-file /path/to/filters.txt\n\n# Use with run --all\nterragrunt run --all --filters-file prod-filters.txt -- plan\n```\n\n## File Format\n\nThe filters file should contain one filter query per line. Empty lines and lines starting with `#` are ignored:\n\n```text\n# Production environment filters\ntype=unit\n./prod/**\n\n# Exclude test units\n!name=test-*\n```\n\n## Disabling Automatic File Reading\n\nTo disable automatic reading of `.terragrunt-filters`, use the `--no-filters-file` flag:\n\n```bash\n# Disable automatic .terragrunt-filters reading\nterragrunt find --no-filters-file\n```\n"
  },
  {
    "path": "docs/src/data/flags/find-dag.mdx",
    "content": "---\nname: dag\ndescription: |\n  Output in DAG mode.\ntype: boolean\nenv:\n  - TG_DAG\n---\n\nOutputs configurations in DAG mode, which sorts configurations by dependency order by relationship in the dependency graph.\n\nBy default, configurations are sorted alphabetically:\n\n```bash\n$ terragrunt find\nlive/dev/db\nlive/dev/ec2\nlive/dev/vpc\nlive/prod/db\nlive/prod/ec2\nlive/prod/vpc\n```\n\nWhen the `--dag` flag is used, configurations are sorted by dependency order (dependencies before their dependents):\n\n```bash\n$ terragrunt find --dag\nlive/dev/vpc\nlive/prod/vpc\nlive/dev/db\nlive/prod/db\nlive/dev/ec2\nlive/prod/ec2\n```\n\nWhen not used in the JSON format:\n\n```bash\n$ terragrunt find --json --dependencies\n[\n  {\n    \"type\": \"unit\",\n    \"path\": \"live/dev/db\",\n    \"dependencies\": [\n      \"live/dev/vpc\"\n    ]\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"live/dev/ec2\",\n    \"dependencies\": [\n      \"live/dev/vpc\",\n      \"live/dev/db\"\n    ]\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"live/dev/vpc\"\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"live/prod/db\",\n    \"dependencies\": [\n      \"live/prod/vpc\"\n    ]\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"live/prod/ec2\",\n    \"dependencies\": [\n      \"live/prod/vpc\",\n      \"live/prod/db\"\n    ]\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"live/prod/vpc\"\n  }\n]\n```\n\nResults are sorted by path.\n\nWhen combined with the JSON format:\n\n```bash\n$ terragrunt find --json --dependencies --dag\n[\n  {\n    \"type\": \"unit\",\n    \"path\": \"live/dev/vpc\"\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"live/prod/vpc\"\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"live/dev/db\",\n    \"dependencies\": [\n      \"live/dev/vpc\"\n    ]\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"live/prod/db\",\n    \"dependencies\": [\n      \"live/prod/vpc\"\n    ]\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"live/dev/ec2\",\n    \"dependencies\": [\n      \"live/dev/vpc\",\n      \"live/dev/db\"\n    ]\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"live/prod/ec2\",\n    \"dependencies\": [\n      \"live/prod/vpc\",\n      \"live/prod/db\"\n    ]\n  }\n]\n```\n"
  },
  {
    "path": "docs/src/data/flags/find-dependencies.mdx",
    "content": "---\nname: dependencies\ndescription: Include dependency information in the output.\ntype: bool\nenv:\n  - TG_DEPENDENCIES\n---\n\nWhen enabled, the output will include information about dependencies between configurations. This is particularly useful when combined with JSON output format to understand the dependency relationships in your codebase.\n\nExample:\n\n```bash\nterragrunt find --dependencies --format json\n[\n  {\n    \"type\": \"unit\",\n    \"path\": \"unitA\",\n    \"dependencies\": []\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"unitB\",\n    \"dependencies\": [\"../unitA\"]\n  }\n]\n```\n"
  },
  {
    "path": "docs/src/data/flags/find-exclude.mdx",
    "content": "---\nname: exclude\ndescription: Include exclude configuration in the output\ntype: boolean\nenv:\n  - TG_EXCLUDE\n---\n\nInclude exclude configuration in the output. When enabled, the JSON output will include the configurations of the `exclude` block in the discovered units.\n\n## Usage\n\n```bash\n--exclude\n```\n\n## Examples\n\nShow exclude configurations in JSON format:\n```bash\nterragrunt find --exclude --format=json\n```\n\nShow exclude configurations with queue construct simulation:\n```bash\nterragrunt find --exclude --queue-construct-as=plan --format=json\n```\n\n## Behavior\n\nWhen enabled, the JSON output will include any `exclude` block configurations found in the units:\n\n```bash\n$ terragrunt find --exclude --format=json | jq\n[\n  {\n    \"type\": \"unit\",\n    \"path\": \"action/exclude-apply\",\n    \"exclude\": {\n      \"exclude_dependencies\": true,\n      \"actions\": [\n        \"apply\"\n      ],\n      \"if\": true\n    }\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"action/exclude-plan\",\n    \"exclude\": {\n      \"exclude_dependencies\": true,\n      \"actions\": [\n        \"plan\"\n      ],\n      \"if\": true\n    }\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"all-except-output/app1\",\n    \"exclude\": {\n      \"exclude_dependencies\": true,\n      \"actions\": [\n        \"all_except_output\"\n      ],\n      \"if\": true\n    }\n  }\n]\n```\n\nNote that you can combine this with the `--queue-construct-as` flag to dry-run behavior relevant to excludes.\n\n```bash\n$ terragrunt find --exclude --queue-construct-as=plan --format=json | jq\n[\n  {\n    \"type\": \"unit\",\n    \"path\": \"action/exclude-apply\",\n    \"exclude\": {\n      \"exclude_dependencies\": true,\n      \"actions\": [\n        \"apply\"\n      ],\n      \"if\": true\n    }\n  }\n]\n```\n"
  },
  {
    "path": "docs/src/data/flags/find-external.mdx",
    "content": "---\nname: external\ndescription: Include external dependencies in the output.\ntype: bool\nenv:\n  - TG_EXTERNAL\n---\n\nWhen enabled, units outside the working directory can be included as part of the output, if any unit depends on them. This is useful when you need to understand all dependency relationships, including those that don't exist in the current directory.\n\nExample:\n\n```bash\nterragrunt find --dependencies --external --format json\n[\n  {\n    \"type\": \"unit\",\n    \"path\": \"internal/unitA\",\n    \"dependencies\": []\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"internal/unitB\",\n    \"dependencies\": [\"../unitA\", \"../../external/unitC\"]\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"external/unitC\",\n    \"dependencies\": []\n  }\n]\n```\n"
  },
  {
    "path": "docs/src/data/flags/find-format.mdx",
    "content": "---\nname: format\ndescription: |\n  Format the results as specified. Supported values (text, json). Default: text.\ntype: string\nenv:\n  - TG_FORMAT\n---\n\nThis is particularly useful when you need to process the results programmatically or integrate with other tools.\n\nExample:\n\n```bash\n$ terragrunt find --format=json | jq '.[:3]'\n[\n  {\n    \"type\": \"stack\",\n    \"path\": \"basic\"\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"basic/units/chick\"\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"basic/units/chicken\"\n  }\n]\n```\n"
  },
  {
    "path": "docs/src/data/flags/find-hidden.mdx",
    "content": "---\nname: hidden\ndescription: Include hidden directories in find results.\ntype: bool\nenv:\n  - TG_HIDDEN\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n<Aside type=\"caution\" title=\"Deprecated\">\nThis flag is deprecated and will be removed in a future version of Terragrunt. Hidden directories are now included by default, making this flag unnecessary.\n\nUse [`--no-hidden`](/reference/cli/commands/find#no-hidden) to exclude hidden directories from the results.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/find-include.mdx",
    "content": "---\nname: include\ndescription: Include `include` configuration in the output.\ntype: boolean\nenv:\n  - TG_INCLUDE\n---\n\nWhen enabled, JSON output will include the configurations of the `include` block of discovered units.\n\n```bash\n$ terragrunt find --include --format=json | jq\n[\n  {\n    \"type\": \"unit\",\n    \"path\": \"bar\",\n    \"include\": {\n      \"cloud\": \"cloud.hcl\"\n    }\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"foo\"\n  }\n]\n```\n\nYou can use tools like `jq` to filter the output and get all the units that include a specific configuration.\n\n```bash\n$ terragrunt find --include --format=json | jq '[.[] | select(.include.cloud == \"cloud.hcl\")]'\n[\n  {\n    \"type\": \"unit\",\n    \"path\": \"bar\",\n    \"include\": {\n      \"cloud\": \"cloud.hcl\"\n    }\n  }\n]\n```\n"
  },
  {
    "path": "docs/src/data/flags/find-json.mdx",
    "content": "---\nname: json\ndescription: |\n  Output results in JSON format. This is equivalent to using `--format=json`.\ntype: bool\nenv:\n  - TG_JSON\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nThis flag is a convenient shorthand for `--format=json`. It's particularly useful when you need to process the results programmatically or integrate with other tools.\n\nExample:\n\n```bash\n$ terragrunt find --json | jq '.[:3]'\n[\n  {\n    \"type\": \"stack\",\n    \"path\": \"basic\"\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"basic/units/chick\"\n  },\n  {\n    \"type\": \"unit\",\n    \"path\": \"basic/units/chicken\"\n  }\n]\n```\n\n<Aside type=\"note\" title=\"Equivalent Flags\">\n\nThe `--json` flag is equivalent to `--format=json`. You can use either one to get the same JSON output.\n\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/find-no-hidden.mdx",
    "content": "---\nname: no-hidden\ndescription: Exclude hidden directories from find results.\ntype: bool\nenv:\n  - TG_NO_HIDDEN\n---\n\nBy default, Terragrunt includes hidden directories (those starting with a dot) in the find results. Use this flag to exclude them.\n\nExample:\n\n```bash\n# Default: includes hidden directories\n$ terragrunt find\n.hide/unit\nstack\nunit\n\n# With --no-hidden: excludes hidden directories\n$ terragrunt find --no-hidden\nstack\nunit\n```\n"
  },
  {
    "path": "docs/src/data/flags/find-reading.mdx",
    "content": "---\nname: reading\ndescription: Include the list of files that are read by components in the output.\ntype: boolean\nenv:\n  - TG_READING\n---\n\nWhen enabled, JSON output will include a list of files that were read during the parsing of each component. This is useful for understanding configuration dependencies and tracking which shared files are consumed by your Terragrunt configurations.\n\n```bash\n$ terragrunt find --reading --format=json | jq\n[\n  {\n    \"type\": \"unit\",\n    \"path\": \"app\",\n    \"reading\": [\n      \"shared.hcl\",\n      \"shared.tfvars\",\n      \"common/variables.hcl\"\n    ]\n  }\n]\n```\n\nThe `reading` field includes files read by Terragrunt helper functions such as:\n- `read_terragrunt_config()` - Reading other Terragrunt configuration files\n- `read_tfvars_file()` - Reading Terraform variable files\n- `sops_decrypt_file()` - Reading encrypted files via SOPS\n- `mark_as_read()` - Explicitly marking files as read\n\nYou can use tools like `jq` to analyze which components read specific files:\n\n```bash\n$ terragrunt find --reading --format=json | jq '[.[] | select(.reading[]? | contains(\"shared.hcl\"))]'\n[\n  {\n    \"type\": \"unit\",\n    \"path\": \"app\",\n    \"reading\": [\n      \"shared.hcl\",\n      \"shared.tfvars\"\n    ]\n  }\n]\n```\n\n"
  },
  {
    "path": "docs/src/data/flags/graph.mdx",
    "content": "---\nname: graph\ndescription: Run the provided OpenTofu/Terraform command against the graph of dependencies for the unit in the current working directory.\ntype: bool\nenv:\n  - TG_GRAPH\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nWhen this flag is set, Terragrunt will run the specified OpenTofu/Terraform command against the graph of dependencies for the unit in the current working directory. The graph consists of all units that depend on the unit in the current working directory via a `dependency` or `dependencies` blocks, plus all the units that depend on those units, and all the units that depend on those units, and so on, recursively up the tree, up to the Git repository root.\n\nThe command will be executed following the order of dependencies: it'll run on the unit in the current working directory first, then on units that depend on it directly, then on the units that depend on those units, and so on. Note that if the command is `destroy`, it will run in the opposite order (the final dependents, then their dependencies, etc. up to the unit you ran the command in).\n\n## Example Usage\n\nGiven the following dependency graph:\n\n![dependency-graph](../../assets/img/collections/documentation/dependency-graph.png)\n\nRunning `terragrunt run --graph plan` in the `eks` module will lead to the following execution order:\n\n```text\nGroup 1\n- Module project/eks\n\nGroup 2\n- Module project/services/eks-service-1\n- Module project/services/eks-service-2\n\nGroup 3\n- Module project/services/eks-service-2-v2\n- Module project/services/eks-service-3\n- Module project/services/eks-service-5\n\nGroup 4\n- Module project/services/eks-service-3-v2\n- Module project/services/eks-service-4\n\nGroup 5\n- Module project/services/eks-service-3-v3\n```\n\nNotes:\n- `lambda` units aren't included in the graph because they are not dependent on the `eks` unit\n- Execution is from bottom up based on dependencies\n\nRunning `terragrunt run --graph destroy` in the `eks` unit will lead to the following execution order:\n\n```text\nGroup 1\n- Module project/services/eks-service-2-v2\n- Module project/services/eks-service-3-v3\n- Module project/services/eks-service-4\n- Module project/services/eks-service-5\n\nGroup 2\n- Module project/services/eks-service-3-v2\n\nGroup 3\n- Module project/services/eks-service-3\n\nGroup 4\n- Module project/services/eks-service-1\n- Module project/services/eks-service-2\n\nGroup 5\n- Module project/eks\n```\n\nNotes:\n- Execution is in reverse order; first are destroyed \"top\" units and in the end `eks`\n- `lambda` units aren't affected at all\n\nTo learn more about how to use this flag, see the [Stacks](/features/stacks) feature documentation.\n\n<Aside type=\"caution\">\nWhen running `graph destroy`, the execution order is reversed compared to other commands. Dependencies will be destroyed in reverse order to ensure resources are safely removed (dependents are destroyed before their dependencies).\n\nAlways verify the execution plan before running destructive commands.\n</Aside>\n\n<Aside type=\"danger\">\nExternal dependencies (units outside your current working directory) are not automatically included in graph runs.\n\nYou must explicitly include them using [`--queue-include-external`](/reference/cli/commands/run#queue-include-external) if they need to be part of the execution.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/hcl-fmt-check.mdx",
    "content": "---\nname: check\ndescription: Enable check mode in the hclfmt command.\ntype: bool\nenv:\n  - TG_CHECK\n---\n\nWhen enabled, Terragrunt will check if HCL files are properly formatted without making any changes. This is useful in CI/CD pipelines to ensure consistent formatting.\n\nThe command will:\n- Exit with status code 0 if files are properly formatted\n- Exit with status code 1 if any files need formatting\n\nExample:\n\n```bash\nterragrunt hcl fmt --check\n```\n"
  },
  {
    "path": "docs/src/data/flags/hcl-fmt-diff.mdx",
    "content": "---\nname: diff\ndescription: Print diff between original and modified file versions when running with 'hclfmt'.\ntype: bool\nenv:\n  - TG_DIFF\n---\n\nWhen enabled, Terragrunt will show the differences between the original and formatted versions of HCL files. This helps you understand what changes the formatter would make.\n\nExample:\n\n```bash\nterragrunt hcl fmt --diff\n```\n"
  },
  {
    "path": "docs/src/data/flags/hcl-fmt-exclude-dir.mdx",
    "content": "---\nname: exclude-dir\ndescription: Skip HCL formatting in given directories.\ntype: string\nenv:\n  - TG_EXCLUDE_DIR\n---\n\nSpecifies directories to exclude from HCL formatting. This is useful when you want to skip formatting certain parts of your codebase.\n\nExample:\n\n```bash\nterragrunt hcl fmt --exclude-dir=vendor --exclude-dir=.terragrunt-cache\n```\n"
  },
  {
    "path": "docs/src/data/flags/hcl-fmt-file.mdx",
    "content": "---\nname: file\ndescription: The path to a single hcl file that the hclfmt command should run on.\ntype: string\nenv:\n  - TG_FILE\n---\n\nSpecifies a single HCL file to format instead of recursively searching for files. This is useful when you want to format just one specific file.\n\nExample:\n\n```bash\nterragrunt hcl fmt --file=./environments/prod/terragrunt.hcl\n```\n"
  },
  {
    "path": "docs/src/data/flags/hcl-fmt-filter.mdx",
    "content": "---\nname: filter\ndescription: Filter configurations using a flexible query language\ntype: list(string)\n---\n\nimport {Aside} from '@astrojs/starlight/components';\n\n<Aside type=\"note\" title=\"Filter Behavior for HCL Format\">\nThe `--filter` flag works differently for `hcl fmt` compared to other commands. It filters on individual HCL files rather than units or stacks (which are directories). As a result, only path-based filter expressions are supported. Attribute-based filters like `type=unit` or `name=my-app` are not applicable to file-level operations.\n\nExample:\n```bash\n# Supported: Path-based filtering\nterragrunt hcl fmt --filter './prod/**/*.hcl'\n\n# Not supported: Attribute-based filtering\nterragrunt hcl fmt --filter 'type=unit'  # This will not work\n```\n</Aside>\n\nThe `--filter` flag provides a path-based querying syntax for targeting specific HCL files to format.\n\n## Usage\n\n```bash\nterragrunt hcl fmt --filter './prod/**'\n```\n\n### Path-Based Filtering\nMatch HCL files by their file system path:\n\n```bash\n# Relative paths with globs\nterragrunt hcl fmt --filter './envs/prod/**'\n\n# Format all HCL files in a specific directory\nterragrunt hcl fmt --filter './modules/**/*.hcl'\n\n# Absolute paths\nterragrunt hcl fmt --filter '/absolute/path/to/envs/dev/apps/*'\n```\n\n### Negation\nExclude paths using the `!` prefix:\n\n```bash\n# Exclude by path\nterragrunt hcl fmt --filter '!./prod/**'\n\n# Exclude specific directories\nterragrunt hcl fmt --filter '!./test/**'\n```\n\n### Intersection (Refinement)\nUse the `|` operator to refine results:\n\n```bash\n# Format HCL files in prod directory, excluding tests\nterragrunt hcl fmt --filter './prod/** | !./prod/test/**'\n\n# Format specific file patterns\nterragrunt hcl fmt --filter './**/*.hcl | !./**/test/**'\n```\n\n### Union (Multiple Filters)\nSpecify multiple `--filter` flags to combine results using OR logic:\n\n```bash\n# Format files in either dev or staging directories\nterragrunt hcl fmt --filter './dev/**' --filter './staging/**'\n```\n\nFor comprehensive examples and advanced usage patterns, see the [Filters feature documentation](/features/filter).\n\n"
  },
  {
    "path": "docs/src/data/flags/hcl-fmt-stdin.mdx",
    "content": "---\nname: stdin\ndescription: Format HCL from stdin and print result to stdout.\ntype: bool\nenv:\n  - TG_STDIN\n---\n\nWhen enabled, Terragrunt will read HCL content from standard input, format it, and write the result to standard output. This is useful for integrating with text editors or other tools.\n\nExample:\n\n```bash\necho 'locals { foo=\"bar\" }' | terragrunt hcl fmt --stdin\n```\n"
  },
  {
    "path": "docs/src/data/flags/hcl-validate-inputs.mdx",
    "content": "---\nname: inputs\ndescription: Validate that all variables a module requires are set.\ntype: bool\nenv:\n  - TG_INPUTS\n---\n\nWhen enabled, Terragrunt will validate that all variables a module (provisioned by a unit) requires are set.\n\nExample:\n\n```bash\nterragrunt hcl validate --inputs\n```\n"
  },
  {
    "path": "docs/src/data/flags/hcl-validate-json.mdx",
    "content": "---\nname: json\ndescription: Output the result in JSON format.\ntype: bool\nenv:\n  - TG_JSON\n---\n\nWhen enabled, Terragrunt will output the validation results in JSON format, making it easier to parse and process programmatically.\n\nExample:\n\n```bash\nterragrunt hcl validate --json\n```\n"
  },
  {
    "path": "docs/src/data/flags/hcl-validate-show-config-path.mdx",
    "content": "---\nname: show-config-path\ndescription: Show a list of files with invalid configuration.\ntype: bool\nenv:\n  - TG_SHOW_CONFIG_PATH\n---\n\nWhen enabled, Terragrunt will display the full paths of files that contain invalid HCL configurations. This is particularly useful when validating multiple files to quickly identify which files need attention.\n\nExample:\n\n```bash\nterragrunt hcl validate --show-config-path\n```\n"
  },
  {
    "path": "docs/src/data/flags/hcl-validate-strict.mdx",
    "content": "---\nname: strict\ndescription: Throw an error if any inputs are set that are not defined in the module that a unit provisions.\ntype: bool\nenv:\n  - TG_STRICT\n---\n\nWhen enabled, Terragrunt will throw an error if any inputs are set that are not defined in the module that a unit provisions.\n\nExample:\n\n```bash\nterragrunt hcl validate --inputs --strict\n```\n"
  },
  {
    "path": "docs/src/data/flags/help.mdx",
    "content": "---\nname: help\ndescription: Show help information.\ntype: bool\n---\n\nDisplays help information about Terragrunt commands and their usage. Can be used with specific commands to get detailed help about that command.\n"
  },
  {
    "path": "docs/src/data/flags/iam-assume-role-duration.mdx",
    "content": "---\nname: iam-assume-role-duration\ndescription: Session duration for IAM Assume Role session.\ntype: string\nenv:\n  - TG_IAM_ASSUME_ROLE_DURATION\n---\n\nSpecifies how long the temporary credentials should remain valid when assuming an IAM role. This flag is only used when [`iam-assume-role`](/reference/cli/commands/run#iam-assume-role) is specified.\n\nFor more information on how to use this flag, and how it differs from other ways of managing authentication with Terragrunt, see the [Authentication](/features/units/authentication) feature documentation.\n"
  },
  {
    "path": "docs/src/data/flags/iam-assume-role-session-name.mdx",
    "content": "---\nname: iam-assume-role-session-name\ndescription: Name for the IAM Assumed Role session.\ntype: string\nenv:\n  - TG_IAM_ASSUME_ROLE_SESSION_NAME\n---\n\nSpecifies a custom session name when assuming an IAM role. This flag is only used when [`iam-assume-role`](/reference/cli/commands/run#iam-assume-role) is specified.\n\nFor more information on how to use this flag, and how it differs from other ways of managing authentication with Terragrunt, see the [Authentication](/features/units/authentication) feature documentation.\n"
  },
  {
    "path": "docs/src/data/flags/iam-assume-role-web-identity-token.mdx",
    "content": "---\nname: iam-assume-role-web-identity-token\ndescription: For AssumeRoleWithWebIdentity, the WebIdentity token.\ntype: string\nenv:\n  - TG_IAM_ASSUME_ROLE_WEB_IDENTITY_TOKEN\n---\n\nSpecifies the WebIdentity token to use when assuming an IAM role using web identity federation. This flag is only used when [`iam-assume-role`](/reference/cli/commands/run#iam-assume-role) is specified.\n\nFor more information on how to use this flag, and how it differs from other ways of managing authentication with Terragrunt, see the [Authentication](/features/units/authentication) feature documentation.\n"
  },
  {
    "path": "docs/src/data/flags/iam-assume-role.mdx",
    "content": "---\nname: iam-assume-role\ndescription: Assume the specified IAM role before executing OpenTofu/Terraform.\ntype: string\nenv:\n  - TG_IAM_ASSUME_ROLE\n---\n\nSpecifies an IAM role ARN that Terragrunt should assume before executing OpenTofu/Terraform commands. This is useful for managing resources across different AWS accounts or with different permission sets.\n\nFor more information on how to use this flag, and how it differs from other ways of managing authentication with Terragrunt, see the [Authentication](/features/units/authentication) feature documentation.\n"
  },
  {
    "path": "docs/src/data/flags/in-download-dir.mdx",
    "content": "---\nname: in-download-dir\ndescription: Run the provided command in the download directory.\ntype: bool\nenv:\n  - TG_IN_DOWNLOAD_DIR\n---\n\nWhen enabled, Terragrunt will execute the provided command in the temporary download directory where modules are cached, rather than the current working directory.\n\nThis is particularly useful when you need to:\n- Inspect downloaded module contents.\n- Execute commands against the actual module source code.\n- Debug issues with downloaded modules.\n\nExample:\n\n```bash\n# View the contents of main.tf in the downloaded module\nterragrunt exec --in-download-dir -- cat main.tf\n\n# List all files in the downloaded module\nterragrunt exec --in-download-dir -- ls -la\n```\n"
  },
  {
    "path": "docs/src/data/flags/inputs-debug.mdx",
    "content": "---\nname: inputs-debug\ndescription: Write debug.tfvars to working folder to help root-cause issues.\ntype: bool\nenv:\n  - TG_INPUTS_DEBUG\n---\n\nWhen enabled, Terragrunt will write a `debug.tfvars` file to the working directory. This file contains the resolved input values and can be useful for debugging configuration issues.\n\nTo learn more about how to use this flag, see the [Debugging](/troubleshooting/debugging) guide.\n"
  },
  {
    "path": "docs/src/data/flags/json-out-dir.mdx",
    "content": "---\nname: json-out-dir\ndescription: Directory to store JSON plan files generated from plans via show -json.\ntype: string\nenv:\n  - TG_JSON_OUT_DIR\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n<Aside type=\"note\">\n\nThis flag only does anything when used with the `--all` flag for stack runs. It does nothing in single-unit runs.\n\nTo generate JSON plan files for a single unit run, use the standard OpenTofu/Terraform [-out flag](https://opentofu.org/docs/cli/commands/plan) like so:\n\n```bash\nterragrunt run -- plan -out=/tmp/tfplan\n```\n\n```bash\nterragrunt run -- show -json /tmp/tfplan > /tmp/tfplan.json\n```\n\n</Aside>\n\nGenerates machine-readable plan outputs per unit as `tfplan.json` by invoking `show -json` after the plan is created.\n\n- Requires a plan to exist (e.g. created with `--out-dir`).\n- If the directory does not exist, Terragrunt creates it.\n- Relative paths are resolved against the root working directory, and Terragrunt mirrors the unit path under this directory (e.g. `<json-out-dir>/<relative-unit-path>/tfplan.json`).\n\nExamples:\n\n```bash\n# Create native plan files and JSON plans for all units\nterragrunt run --all --out-dir /tmp/all --json-out-dir /tmp/all plan\n\n# Or store JSON plans separately\nterragrunt run --all --out-dir /tmp/plan --json-out-dir /tmp/json plan\n```\n\nSee the [Stacks](/features/stacks/stack-operations#saving-opentofuterraform-plan-output) docs for more usage examples.\n\n\n"
  },
  {
    "path": "docs/src/data/flags/list-dag.mdx",
    "content": "---\nname: dag\ndescription: |\n  Output in DAG mode.\ntype: boolean\nenv:\n  - TG_DAG\n---\n\nOutputs configurations in DAG mode, which sorts configurations by dependency order and groups them by relationship in the dependency graph.\n\nExamples:\n\nBy default, configurations are sorted alphabetically:\n\n```bash\n$ terragrunt list\nlive/dev/db    live/dev/ec2   live/dev/vpc\nlive/prod/db   live/prod/ec2  live/prod/vpc\n```\n\nWhen the `--dag` flag is used, configurations are sorted by dependency order (dependencies before their dependents):\n\n```bash\n$ terragrunt list --dag\nlive/dev/vpc   live/prod/vpc  live/dev/db\nlive/prod/db   live/dev/ec2   live/prod/ec2\n```\n\nWhen not used in the long format:\n\n```bash\n$ terragrunt list -l --dependencies\nType  Path           Dependencies\nunit  live/dev/db    live/dev/vpc\nunit  live/dev/ec2   live/dev/db, live/dev/vpc\nunit  live/dev/vpc\nunit  live/prod/db   live/prod/vpc\nunit  live/prod/ec2  live/prod/db, live/prod/vpc\nunit  live/prod/vpc\n```\n\nResults are sorted by name.\n\nWhen combined with the long format:\n\n```bash\n$ terragrunt list -l --dependencies --dag\nType  Path           Dependencies\nunit  live/dev/vpc\nunit  live/prod/vpc\nunit  live/dev/db    live/dev/vpc\nunit  live/prod/db   live/prod/vpc\nunit  live/dev/ec2   live/dev/db, live/dev/vpc\nunit  live/prod/ec2  live/prod/db, live/prod/vpc\n```\n\nWhen not used in the tree format:\n\n```bash\n$ terragrunt list -T\n.\n╰── live\n    ├── dev\n    │   ├── db\n    │   ├── ec2\n    │   ╰── vpc\n    ╰── prod\n        ├── db\n        ├── ec2\n        ╰── vpc\n```\n\nWhen combined with the tree format:\n\n```bash\n$ terragrunt list -T --dag\n.\n├── live/dev/vpc\n│   ├── live/dev/db\n│   │   ╰── live/dev/ec2\n│   ╰── live/dev/ec2\n╰── live/prod/vpc\n    ├── live/prod/db\n    │   ╰── live/prod/ec2\n    ╰── live/prod/ec2\n```\n"
  },
  {
    "path": "docs/src/data/flags/list-dependencies.mdx",
    "content": "---\nname: dependencies\ndescription: |\n  Include dependencies in list results.\ntype: boolean\nenv:\n  - TG_DEPENDENCIES\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nControls whether dependency information is included in the list output. When enabled, Terragrunt will analyze and display the dependency relationships between configurations.\n\nThis flag is particularly powerful when combined with different output formats and sorting options:\n\n- With `--format=long`: Shows dependencies in a tabular format\n- With `--format=json`: Includes full dependency ancestry in JSON output\n\n<Aside type=\"tip\">\n  Combine `--dependencies` with `--sort=dag` and `--group-by=dag` (or just use `--dag`) to get a complete picture of your infrastructure's deployment sequence.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/list-external.mdx",
    "content": "---\nname: external\ndescription: |\n  Discover external dependencies from initial results.\ntype: boolean\nenv:\n  - TG_EXTERNAL\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nControls whether Terragrunt should discover and include external dependencies in the list results. External dependencies are Terragrunt configurations that are referenced by your configurations but exist outside the current working directory.\n\nThis flag is most useful when:\n- Investigating the complete dependency graph of your infrastructure\n- Determining the full blast radius of a change\n\nExample:\n\n```bash\n$ terragrunt list -l --dependencies\nType  Path          Dependencies\nunit  a-dependent   b-dependency\nunit  b-dependency\n```\n\n```bash\n$ terragrunt list -l --dependencies --external\nType  Path                      Dependencies\nunit  ../external/c-dependency\nunit  a-dependent               ../external/c-dependency, b-dependency\nunit  b-dependency\n```\n\n<Aside type=\"note\">\n  The `--external` flag is typically used in combination with `--dependencies` to show why the external dependency was discovered.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/list-format.mdx",
    "content": "---\nname: format\ndescription: |\n  Format the results as specified. Supported values (text, long, tree, dot). Default: text.\ntype: string\nenv:\n  - TG_FORMAT\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nControls how the list results are displayed:\n\n- `text` (default): Simple space-separated list of configurations. Best for quick overview and scripting.\n- `long`: Detailed view showing type (unit/stack), path, and module information. Useful for auditing and documentation.\n- `tree`: Hierarchical view showing directory structure. Perfect for understanding infrastructure organization.\n- `dot`: Output in DOT format for visualization. Generates a graph using the [GraphViz DOT](https://graphviz.org/doc/info/lang.html) language. Ideal for creating visual dependency graphs.\n\nThese values all have shortcuts as standalone flags:\n\n- `--long` / `-l` for long\n- `--tree` / `-T` for tree\n\nExamples:\n\n```bash\n# Default text format - Great for quick overview\n$ terragrunt list\nlive/dev/db    live/dev/ec2   live/dev/vpc\nlive/prod/db   live/prod/ec2  live/prod/vpc\n```\n\n```bash\n# Long format - Useful for reading structured information quickly\n$ terragrunt list -l\nType  Path           Dependencies\nunit  live/dev/db    live/dev/vpc\nunit  live/dev/ec2   live/dev/db, live/dev/vpc\nunit  live/dev/vpc\nunit  live/prod/db   live/prod/vpc\nunit  live/prod/ec2  live/prod/db, live/prod/vpc\nunit  live/prod/vpc\n```\n\n```bash\n# Tree format - Optimal for visualizing structure\n$ terragrunt list -T\n.\n╰── live\n    ├── dev\n    │   ├── db\n    │   ├── ec2\n    │   ╰── vpc\n    ╰── prod\n        ├── db\n        ├── ec2\n        ╰── vpc\n```\n\n```bash\n# DOT format - Useful for visualizing dependency graphs\n$ terragrunt list --format=dot --dependencies\ndigraph {\n  \"live/dev/vpc\" ;\n  \"live/dev/db\" ;\n  \"live/dev/ec2\" ;\n  \"live/dev/db\" -> \"live/dev/vpc\";\n  \"live/dev/ec2\" -> \"live/dev/db\";\n  \"live/dev/ec2\" -> \"live/dev/vpc\";\n  \"live/prod/vpc\" ;\n  \"live/prod/db\" ;\n  \"live/prod/ec2\" ;\n  \"live/prod/db\" -> \"live/prod/vpc\";\n  \"live/prod/ec2\" -> \"live/prod/db\";\n  \"live/prod/ec2\" -> \"live/prod/vpc\";\n}\n\n# Render the DOT output to an image using GraphViz\n$ terragrunt list --format=dot | dot -Tpng > graph.png\n```\n\nThe examples above demonstrate a typical multi-environment infrastructure setup with networking, compute, and data layers. Each format provides a different perspective on the same infrastructure, making it easier to understand and manage your Terragrunt configurations.\n\n<Aside type=\"tip\" title=\"DOT Format Alias\">\n\nThe `dag graph` command is an alias for `list --format=dot`. Both commands produce identical DOT format output:\n\n```bash\nterragrunt dag graph\n# Equivalent to:\nterragrunt list --format=dot --dependencies --external\n```\n\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/list-hidden.mdx",
    "content": "---\nname: hidden\ndescription: Include hidden directories in list results.\ntype: bool\nenv:\n  - TG_HIDDEN\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n<Aside type=\"caution\" title=\"Deprecated\">\nThis flag is deprecated and will be removed in a future version of Terragrunt. Hidden directories are now included by default, making this flag unnecessary.\n\nUse [`--no-hidden`](/reference/cli/commands/list#no-hidden) to exclude hidden directories from the results.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/list-long.mdx",
    "content": "---\nname: long\ndescription: |\n  Output in long format.\ntype: boolean\nenv:\n  - TG_LONG\naliases:\n  - -l\n---\n\nA shorthand flag that sets `--format=long`. Displays the list results in a detailed tabular format that includes additional information about each configuration.\n"
  },
  {
    "path": "docs/src/data/flags/list-no-hidden.mdx",
    "content": "---\nname: no-hidden\ndescription: Exclude hidden directories from list results.\ntype: bool\nenv:\n  - TG_LIST_NO_HIDDEN\n---\n\nBy default, Terragrunt includes hidden directories (those starting with a dot) in the list results. Use this flag to exclude them.\n\nExample:\n\n```bash\n# Default: includes hidden directories\n$ terragrunt list -l\nType  Path\nunit  .hide/unit\nstack stack\nunit  unit\n\n# With --no-hidden: excludes hidden directories\n$ terragrunt list -l --no-hidden\nType  Path\nstack stack\nunit  unit\n```\n"
  },
  {
    "path": "docs/src/data/flags/list-tree.mdx",
    "content": "---\nname: tree\ndescription: |\n  Output in tree format.\ntype: boolean\nenv:\n  - TG_TREE\naliases:\n  - -T\n---\n\nA shorthand flag that sets `--format=tree`. Displays the list results in a hierarchical tree structure that shows the relationships between your Terragrunt configurations.\n"
  },
  {
    "path": "docs/src/data/flags/log-custom-format.mdx",
    "content": "---\nname: log-custom-format\ndescription: Set the custom log formatting.\ntype: string\nenv:\n  - TG_LOG_CUSTOM_FORMAT\n---\n\nFor more information, see the [log formatting documentation](/reference/logging/formatting).\n"
  },
  {
    "path": "docs/src/data/flags/log-disable.mdx",
    "content": "---\nname: log-disable\ndescription: Disable logging.\ntype: bool\nenv:\n  - TG_LOG_DISABLE\n---\n\nWhen enabled, Terragrunt will disable all logging output. This is useful when you want to see only the OpenTofu/Terraform output or when using Terragrunt in scripts where logging isn't needed.\n\nNote that this automatically enables the `--tf-forward-stdout` flag to ensure OpenTofu/Terraform output is still visible.\n"
  },
  {
    "path": "docs/src/data/flags/log-format.mdx",
    "content": "---\nname: log-format\ndescription: |\n  Set the format for Terragrunt's log output.\ntype: string\nenv:\n  - TG_LOG_FORMAT\n---\n\nFor a list of available formats and their descriptions, see the [log formatting documentation](/reference/logging/formatting).\n"
  },
  {
    "path": "docs/src/data/flags/log-level.mdx",
    "content": "---\nname: log-level\ndescription: Sets the logging level for Terragrunt.\ntype: string\nenv:\n  - TG_LOG_LEVEL\n---\n\nControls the verbosity of Terragrunt's logging output. For more information about available log levels and their meanings, see the [log levels documentation](/reference/logging#log-levels).\n"
  },
  {
    "path": "docs/src/data/flags/log-show-abs-paths.mdx",
    "content": "---\nname: log-show-abs-paths\ndescription: Show absolute paths in logs.\ntype: bool\nenv:\n  - TG_LOG_SHOW_ABS_PATHS\n---\n\nWhen enabled, Terragrunt will show absolute paths in log messages instead of relative paths.\n\nFor more information, see the [log formatting documentation](/reference/logging/formatting).\n"
  },
  {
    "path": "docs/src/data/flags/no-auto-approve.mdx",
    "content": "---\nname: no-auto-approve\ndescription: Don't automatically append '-auto-approve' to the underlying OpenTofu/Terraform commands run with 'run --all'.\ntype: bool\nenv:\n  - TG_NO_AUTO_APPROVE\n---\n\nWhen enabled, Terragrunt will not automatically append the `-auto-approve` flag to destructive commands like `apply` or `destroy` when running with `--all`. This means you'll be prompted for confirmation before making changes.\n"
  },
  {
    "path": "docs/src/data/flags/no-auto-init.mdx",
    "content": "---\nname: no-auto-init\ndescription: Don't automatically run init on tofu/terraform commands.\ntype: bool\nenv:\n  - TG_NO_AUTO_INIT\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nWhen enabled, Terragrunt will not automatically run `init` before other OpenTofu/Terraform commands.\n\nTo learn more about how to use this flag, see the [Auto-init](/features/units/auto-init) feature documentation.\n\n<Aside type=\"caution\">\nWhen `--no-auto-init` is enabled, you must manually run `terragrunt init` if needed. Terragrunt will fail if it detects that initialization is required but auto-init is disabled.\n\nThis is particularly important when using custom plugin directories or when working in CI/CD environments.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/no-auto-provider-cache-dir.mdx",
    "content": "---\nname: no-auto-provider-cache-dir\ndescription: Disable the auto-provider-cache-dir feature even when the experiment is enabled.\ntype: bool\nenv:\n  - TG_NO_AUTO_PROVIDER_CACHE_DIR\n---\n\nWhen enabled, Terragrunt will disable the `auto-provider-cache-dir` feature. This flag allows you to selectively opt-out of the automatic provider caching behavior.\n\nThis is useful when you want to maintain control over provider caching in specific environments or scenarios.\n"
  },
  {
    "path": "docs/src/data/flags/no-auto-retry.mdx",
    "content": "---\nname: no-auto-retry\ndescription: Don't automatically retry commands which fail with transient errors.\ntype: bool\nenv:\n  - TG_NO_AUTO_RETRY\n---\n\nWhen enabled, Terragrunt will not automatically retry commands that fail with transient errors (like network timeouts). By default, Terragrunt will retry such commands a limited number of times.\n\nTo learn more about how Terragrunt handles retries, see the [Errors](/features/units/runtime-control#errors) section of the [Runtime Control](/features/units/runtime-control#errors) feature documentation.\n"
  },
  {
    "path": "docs/src/data/flags/no-color.mdx",
    "content": "---\nname: no-color\ndescription: Disable color output for both Terragrunt and OpenTofu/Terraform.\ntype: bool\nenv:\n  - TG_NO_COLOR\n---\n\nWhen enabled, Terragrunt will disable colored output in both its own logs and in OpenTofu/Terraform output. This is useful when running in environments where color codes might cause issues, such as CI/CD pipelines or when redirecting output to files.\n"
  },
  {
    "path": "docs/src/data/flags/no-dependency-fetch-output-from-state.mdx",
    "content": "---\nname: no-dependency-fetch-output-from-state\ndescription: Disable the dependency-fetch-output-from-state feature even when the experiment is enabled.\ntype: bool\nenv:\n  - TG_NO_DEPENDENCY_FETCH_OUTPUT_FROM_STATE\n---\n\nWhen enabled, Terragrunt will disable the `dependency-fetch-output-from-state` feature. This flag allows you to selectively opt-out of fetching dependency outputs directly from state files.\n\nThis is useful when you want to maintain control over dependency output retrieval in specific environments or scenarios, even when the experiment is enabled globally.\n\n"
  },
  {
    "path": "docs/src/data/flags/no-destroy-dependencies-check.mdx",
    "content": "---\nname: no-destroy-dependencies-check\ndescription: |\n  **Deprecated**: This flag is ignored and no longer affects behavior. Use `--destroy-dependencies-check` to explicitly enable dependency checks during destroy operations.\ntype: bool\nenv:\n  - TG_NO_DESTROY_DEPENDENCIES_CHECK\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n<Aside type=\"warning\">\nThis flag is deprecated and ignored. Dependency checks are disabled by default during destroy operations. Use `--destroy-dependencies-check` to enable them.\n\nTo enable dependency checks during destroy operations, use the `--destroy-dependencies-check` flag instead.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/no-engine.mdx",
    "content": "---\nname: no-engine\ndescription: Disable IaC engines even when the iac-engine experiment is enabled.\ntype: bool\nenv:\n  - TG_NO_ENGINE\n---\n\nWhen enabled, Terragrunt will disable IaC engines. This flag allows you to selectively opt-out of using engines even when the `iac-engine` experiment is enabled.\n\nThis is useful when you want to maintain control over engine usage in specific environments or scenarios, or when you need to troubleshoot issues with engine functionality.\n\n"
  },
  {
    "path": "docs/src/data/flags/no-filters-file.mdx",
    "content": "---\nname: no-filters-file\ndescription: Disable automatic reading of .terragrunt-filters file\ntype: bool\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nThe `--no-filters-file` flag disables automatic reading of the `.terragrunt-filters` file. By default, Terragrunt automatically reads filter queries from any `.terragrunt-filters` file in the current working directory. Use this flag to disable that automatic behavior.\n\n## Usage\n\n```bash\n# Disable automatic .terragrunt-filters reading\nterragrunt find --no-filters-file\n\n# Use with run --all\nterragrunt run --all --no-filters-file -- plan\n```\n"
  },
  {
    "path": "docs/src/data/flags/no-stack-generate.mdx",
    "content": "---\nname: no-stack-generate\ndescription: Disable automatic stack regeneration before running the stack commands.\ntype: bool\nenv:\n  - TG_NO_STACK_GENERATE\n---\n\nWhen enabled, Terragrunt will skip automatic stack regeneration before executing the command.\nThis is useful when you want to run operations using the existing .terragrunt-stack directory without regenerating it, improving execution speed and avoiding unnecessary changes."
  },
  {
    "path": "docs/src/data/flags/no-tip.mdx",
    "content": "---\nname: no-tip\ndescription: Disable specific tips from being displayed.\ntype: string\nenv:\n  - TG_NO_TIP\n---\n\nDisables specific tips from being displayed. This flag can be used multiple times to disable multiple tips. To disable all tips at once, use `--no-tips` instead.\n"
  },
  {
    "path": "docs/src/data/flags/no-tips.mdx",
    "content": "---\nname: no-tips\ndescription: Disable all tips from being displayed.\ntype: bool\nenv:\n  - TG_NO_TIPS\n---\n\nWhen enabled, Terragrunt will not display any tips in the output. To disable only specific tips, use `--no-tip` instead.\n"
  },
  {
    "path": "docs/src/data/flags/non-interactive.mdx",
    "content": "---\nname: non-interactive\ndescription: Assume \"yes\" for all prompts.\ntype: bool\nenv:\n  - TG_NON_INTERACTIVE\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nWhen enabled, Terragrunt will automatically answer \"yes\" to any prompts that would normally require user input. This is particularly useful in automated environments or CI/CD pipelines where user interaction isn't possible.\n\n<Aside type=\"caution\">\nWhen using `--non-interactive`, Terragrunt will automatically answer \"yes\" to all prompts except for external dependency inclusion prompts.\n\nThese will default to \"no\" for safety. Use [`--queue-include-external`](/reference/cli/commands/run#queue-include-external) to explicitly include external dependencies.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/out-dir.mdx",
    "content": "---\nname: out-dir\ndescription: Directory to store plan files generated by plan/show and consumed by apply/destroy.\ntype: string\nenv:\n  - TG_OUT_DIR\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n<Aside type=\"note\">\n\nThis flag only does anything when used with the `--all` flag for stack runs. It does nothing in single-unit runs.\n\nTo generate plan files for a single unit run, use the standard OpenTofu/Terraform [-out flag](https://opentofu.org/docs/cli/commands/plan) like so:\n\n```bash\nterragrunt run -- plan -out=/tmp/tfplan\n```\n\n</Aside>\n\nSpecifies where Terragrunt writes native OpenTofu/Terraform plan files for each unit when running stack operations.\n\n- Plans are written as `tfplan.tfplan` per unit.\n- If the directory does not exist, Terragrunt creates it.\n- Relative paths are resolved against the root working directory, and Terragrunt mirrors the unit path under this directory (e.g. `<out-dir>/<relative-unit-path>/tfplan.tfplan`).\n\nExamples:\n\n```bash\n# Save plans for all units under /tmp/tfplan\nterragrunt run --all plan --out-dir /tmp/tfplan\n\n# Apply using the previously generated plans (per-unit paths are inferred)\nterragrunt run --all apply --out-dir /tmp/tfplan\n```\n\nSee the [Stacks](/features/stacks/stack-operations#saving-opentofuterraform-plan-output) docs for more usage examples.\n\n\n"
  },
  {
    "path": "docs/src/data/flags/parallelism.mdx",
    "content": "---\nname: parallelism\ndescription: Parallelism for --all commands.\ntype: integer\nenv:\n  - TG_PARALLELISM\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nSets the maximum number of concurrent operations when running commands with `--all`. This helps control resource usage and API rate limits when working with multiple units.\n\n<Aside type=\"caution\">\nWhen using `--parallelism` with provider caching, `terraform init` is always executed sequentially if OpenTofu/Terraform provider plugin cache is configured.\n\nTo safely run concurrent operations with provider caching, enable the [Provider Cache Server](/features/caching/provider-cache-server/) instead.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/provider-cache-dir.mdx",
    "content": "---\nname: provider-cache-dir\ndescription: The path to the Terragrunt provider cache directory. By default, 'terragrunt/providers' folder in the user cache directory.\ntype: string\nenv:\n  - TG_PROVIDER_CACHE_DIR\n---\n\nSpecifies the directory where Terragrunt will store cached provider files. This flag is only used when the [Provider Cache Server](/features/caching/provider-cache-server) is enabled.\n"
  },
  {
    "path": "docs/src/data/flags/provider-cache-hostname.mdx",
    "content": "---\nname: provider-cache-hostname\ndescription: The hostname of the Terragrunt Provider Cache server. By default, 'localhost'.\ntype: string\nenv:\n  - TG_PROVIDER_CACHE_HOSTNAME\n---\n\nSets the hostname for connecting to the Provider Cache server. This flag is only used when the [Provider Cache Server](/features/caching/provider-cache-server) is enabled.\n"
  },
  {
    "path": "docs/src/data/flags/provider-cache-port.mdx",
    "content": "---\nname: provider-cache-port\ndescription: The port of the Terragrunt Provider Cache server. By default, assigned automatically.\ntype: string\nenv:\n  - TG_PROVIDER_CACHE_PORT\n---\n\nSpecifies the port number for connecting to the Provider Cache server. This flag is only used when the [Provider Cache Server](/features/caching/provider-cache-server) is enabled.\n"
  },
  {
    "path": "docs/src/data/flags/provider-cache-registry-names.mdx",
    "content": "---\nname: provider-cache-registry-names\ndescription: The list of remote registries to cached by Terragrunt Provider Cache server. By default, 'registry.terraform.io', 'registry.opentofu.org'.\ntype: string\nenv:\n  - TG_PROVIDER_CACHE_REGISTRY_NAMES\n---\n\nSpecifies which provider registries should be cached by the Provider Cache server. This flag is only used when the [Provider Cache Server](/features/caching/provider-cache-server) is enabled.\n"
  },
  {
    "path": "docs/src/data/flags/provider-cache-token.mdx",
    "content": "---\nname: provider-cache-token\ndescription: The token for authentication to the Terragrunt Provider Cache server. By default, assigned automatically.\ntype: string\nenv:\n  - TG_PROVIDER_CACHE_TOKEN\n---\n\nSets the authentication token for connecting to the Provider Cache server. This flag is only used when the [Provider Cache Server](/features/caching/provider-cache-server) is enabled.\n"
  },
  {
    "path": "docs/src/data/flags/provider-cache.mdx",
    "content": "---\nname: provider-cache\ndescription: Enables Terragrunt's provider caching.\ntype: bool\nenv:\n  - TG_PROVIDER_CACHE\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nWhen enabled, Terragrunt will use its provider caching system to speed up provider downloads.\n\nFor more information, see the [Provider Cache Server](/features/caching/provider-cache-server) feature documentation.\n\n<Aside type=\"note\">\nThe `tofu init` command is always executed sequentially if the provider plugin cache is used without the Provider Cache Server.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/queue-construct-as.mdx",
    "content": "---\nname: queue-construct-as\ndescription: |\n  Sort output based on the dependency graph, as if a particular command was run. This flag implies the `--dag` flag.\n\n  The flag has a shorter alias `--as`.\ntype: string\nenv:\n  - TG_QUEUE_CONSTRUCT_AS\naliases:\n  - --as\n---\n\nSort output based on the dependency graph, as if a particular command was run. This flag implies the `--dag` flag.\n\nThe flag has a shorter alias `--as`.\n\n## Usage\n\n```bash\n--queue-construct-as=COMMAND\n--as=COMMAND\n```\n\nWhere `COMMAND` is the command to simulate (e.g., `plan`, `destroy`).\n\n## Examples\n\nSort output as if running plan command (dependencies after dependents):\n```bash\nterragrunt find --queue-construct-as=plan\n```\n\nSort output as if running destroy command (dependencies before dependents):\n```bash\nterragrunt find --queue-construct-as=destroy\n```\n\n## Behavior\n\nWhen using `plan` command, all dependent units will appear *after* the units they depend on.\n\nWhen using `destroy` command, all dependent units will appear *before* the units they depend on.\n\n**Note:** This flag implies the `--dag` flag.\n"
  },
  {
    "path": "docs/src/data/flags/queue-exclude-dir.mdx",
    "content": "---\nname: queue-exclude-dir\ndescription: Unix-style glob of directories to exclude from the queue of Units to run.\ntype: list(string)\nenv:\n  - TG_QUEUE_EXCLUDE_DIR\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n<Aside type=\"tip\" title=\"Alias for --filter\">\n\nThis flag is an alias for the [`--filter` flag](/reference/cli/commands/run#filter).\n\nWhen using this flag, the equivalent filter expression is used instead.\n\ne.g.\n\n```bash\nterragrunt run --all --queue-exclude-dir \"prod/**\" -- plan\n```\n\nis equivalent to:\n\n```bash\nterragrunt run --all --filter \"!{./prod/**}\" -- plan\n```\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/queue-exclude-external.mdx",
    "content": "---\nname: queue-exclude-external\ndescription: Exclude external dependencies from the queue of Units to run.\ntype: bool\nenv:\n  - TG_QUEUE_EXCLUDE_EXTERNAL\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n<Aside type=\"caution\" title=\"Deprecated\">\nThis flag is deprecated and will be removed in a future version of Terragrunt. External dependencies are now excluded by default, making this flag unnecessary.\n\nUse [`--filter` flag](/reference/cli/commands/run#filter) with the expression `{./**}...` if you need to include external dependencies.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/queue-excludes-file.mdx",
    "content": "---\nname: queue-excludes-file\ndescription: Path to a file with a list of directories that need to be excluded when running `run --all` commands.\ntype: string\ndefaultVal: .terragrunt-excludes\nenv:\n  - TG_QUEUE_EXCLUDES_FILE\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n<Aside type=\"tip\" title=\"Alias for --filters-file\">\n\nThis flag leverages the same underlying functionality as the [`--filters-file` flag](/reference/cli/commands/run#filters-file).\n\nWhen using this flag, the `.terragrunt-excludes` file will be parsed, and converted to the equivalent filter expressions.\n\ne.g.\n\n```text\n# excludes-file.txt\nprod\n```\n\n```bash\nterragrunt run --all --queue-excludes-file ./excludes-file.txt -- plan\n```\n\nis equivalent to:\n\n```text\n# filters-file.txt\n!{./prod}\n```\n\n```bash\nterragrunt run --all --filters-file ./filters-file.txt -- plan\n```\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/queue-ignore-dag-order.mdx",
    "content": "---\nname: queue-ignore-dag-order\ndescription: Ignore DAG order for --all commands.\ntype: bool\nenv:\n  - TG_QUEUE_IGNORE_DAG_ORDER\n---\n\nWhen enabled, Terragrunt will ignore the dependency order when running commands with [`--all`](/reference/cli/commands/run#all). This means units will be executed in arbitrary order, which can be useful for read-only operations like `plan`.\n\nNote that this can lead to errors if used with commands that modify state, as dependencies might be processed in the wrong order.\n\nTo learn more about how to use this flag, see the [Stacks](/features/stacks) feature documentation.\n"
  },
  {
    "path": "docs/src/data/flags/queue-ignore-errors.mdx",
    "content": "---\nname: queue-ignore-errors\ndescription: Continue processing Units even if a dependency fails.\ntype: bool\nenv:\n  - TG_QUEUE_IGNORE_ERRORS\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nWhen enabled, Terragrunt will continue processing remaining units even if one fails. This can be useful when you want to see all potential errors in your configuration, rather than stopping at the first failure.\n\nNote that this may lead to incomplete or inconsistent states if used with commands that modify infrastructure.\n\nTo learn more about how to use this flag, see the [Stacks](/features/stacks) feature documentation.\n\n<Aside type=\"danger\">\nUsing `--queue-ignore-errors` can lead to inconsistent state if dependencies fail but dependent modules continue running.\n\nUse this flag with caution, especially with `apply` or `destroy` commands.\n\nFor more fine-grained error handling, see the [errors block](/reference/hcl/blocks#errors) in the HCL reference.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/queue-include-dir.mdx",
    "content": "---\nname: queue-include-dir\ndescription: Unix-style glob of directories to include in the queue of Units to run.\ntype: list(string)\nenv:\n  - TG_QUEUE_INCLUDE_DIR\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n<Aside type=\"tip\" title=\"Alias for --filter\">\n\nThis flag is an alias for the [`--filter` flag](/reference/cli/commands/run#filter).\n\nWhen using this flag, the equivalent filter expression is used instead.\n\ne.g.\n\n```bash\nterragrunt run --all --queue-include-dir \"prod/**\" -- plan\n```\n\nis equivalent to:\n\n```bash\nterragrunt run --all --filter \"{./prod/**}\" -- plan\n```\n\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/queue-include-external.mdx",
    "content": "---\nname: queue-include-external\ndescription: Include external dependencies in the queue of Units to run.\ntype: bool\nenv:\n  - TG_QUEUE_INCLUDE_EXTERNAL\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n<Aside type=\"tip\" title=\"Alias for --filter\">\n\nThis flag is an alias for the [`--filter` flag](/reference/cli/commands/run#filter).\n\nWhen using this flag, the equivalent filter expression is used instead.\n\ne.g.\n\n```bash\nterragrunt run --all --queue-include-external -- plan\n```\n\nis equivalent to:\n\n```bash\nterragrunt run --all --filter \"{./**}...\" -- plan\n```\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/queue-include-units-reading.mdx",
    "content": "---\nname: queue-include-units-reading\ndescription: If flag is set, 'run --all' will only run the command against Terragrunt units that read the specified file via an HCL function or include.\ntype: string\nenv:\n  - TG_QUEUE_INCLUDE_UNITS_READING\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n<Aside type=\"tip\" title=\"Alias for --filter\">\n\nThis flag is an alias for the [`--filter` flag](/reference/cli/commands/run#filter).\n\nWhen using this flag, the equivalent filter expression is used instead.\n\ne.g.\n\n```bash\nterragrunt run --all --queue-include-units-reading shared.hcl -- plan\n```\n\nis equivalent to:\n\n```bash\nterragrunt run --all --filter 'reading=shared.hcl' -- plan\n```\n\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/queue-strict-include.mdx",
    "content": "---\nname: queue-strict-include\ndescription: Only process the directories matched by --queue-include-dir.\ntype: bool\nenv:\n  - TG_QUEUE_STRICT_INCLUDE\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n<Aside type=\"caution\" title=\"Deprecated\">\nThis flag is deprecated and will be removed in a future version of Terragrunt. Terragrunt performs strict inclusion by default, so this flag is no longer needed.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/render-all.mdx",
    "content": "---\nname: all\ndescription: Render all configurations discovered from the current working directory.\ntype: boolean\nenv:\n  - TG_ALL\naliases:\n  - a\n---\n\nExample:\n\n```bash\nterragrunt render --all --json\n```\n\nThis will render all configurations discovered from the current working directory and write the rendered configurations to `terragrunt.rendered.json` files adjacent to the configurations they are derived from.\n\nCombining this with the `--write` flag will write the rendered configurations to `terragrunt.rendered.json` files adjacent to the configurations they are derived from.\n\n```bash\nterragrunt render -a --json --write\n```\n"
  },
  {
    "path": "docs/src/data/flags/render-format.mdx",
    "content": "---\nname: format\ndescription: Use the specified format for the rendered configuration. Supported values (hcl, json). default hcl.\ntype: string\nenv:\n  - TG_FORMAT\n---\n\nControls the output format of the rendered Terragrunt configuration. Supports:\n\n- `hcl` - Output in HCL format (default)\n- `json` - Output in JSON format\n\nThe HCL format is human-readable and matches the original configuration style, while JSON format is useful for programmatic processing.\n\nExample:\n\n```bash\n# Render as HCL (default)\nterragrunt render\n\n# Render as JSON\nterragrunt render --format json\n```\n"
  },
  {
    "path": "docs/src/data/flags/render-write.mdx",
    "content": "---\nname: write\ndescription: Write the rendered configuration to a file (terragrunt.rendered.hcl or terragrunt.rendered.json by default).\ntype: boolean\nenv:\n  - TG_WRITE\naliases:\n  - w\n---\n\nExample:\n\n```bash\nterragrunt render --write --json\n```\n\nThis will write the rendered configuration to `terragrunt.rendered.json` in the current working directory.\n"
  },
  {
    "path": "docs/src/data/flags/report-file.mdx",
    "content": "---\nname: report-file\ndescription: The path where a run report should be generated.\ntype: string\nenv:\n  - TG_REPORT_FILE\n---\n\nBy default, the format of the report will be automatically detected based on the file extension. A `.csv` extension will generate a CSV report, and a `.json` extension will generate a JSON report. Anything else will default to generating a CSV report.\n\nTo explicitly specify the format of the report, use the [report-format](/reference/cli/commands/run/#report-format) flag.\n\nFor more information, see the [Run Report](/features/stacks/run-report) feature.\n"
  },
  {
    "path": "docs/src/data/flags/report-format.mdx",
    "content": "---\nname: report-format\ndescription: The format of the run report.\ntype: string\nenv:\n  - TG_REPORT_FORMAT\n---\n\nThe supported formats are:\n\n- `csv`\n- `json`\n\nThe default is `csv`.\n\nFor more information, see the [Run Report](/features/stacks/run-report) feature.\n"
  },
  {
    "path": "docs/src/data/flags/report-schema-file.mdx",
    "content": "---\nname: report-schema-file\ndescription: |\n  When passed in, a JSON schema for the report will be generated at the specified path.\ntype: string\nenv:\n  - TG_REPORT_SCHEMA_FILE\n---\n\nThis is useful when you need a programmatic way to validate that the report conforms to an expected schema.\n\n### Example\n\n```bash\nterragrunt run --all plan --report-schema-file report.schema.json\n```\n\nThe schema will be generated at the given path in the current working directory.\n\nFor more information, see the [Run Report](/features/stacks/run-report) feature.\n"
  },
  {
    "path": "docs/src/data/flags/scaffold-no-hooks.mdx",
    "content": "---\nname: no-hooks\ndescription: \"Disable hooks when using boilerplate templates during scaffolding.\"\ntype: bool\nenv:\n  - TG_NO_HOOKS\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nWhen enabled, Terragrunt will disable hooks when processing boilerplate templates during scaffolding operations.\n\nThis is useful for security reasons when you want to prevent templates from executing arbitrary hooks on your system.\n\n<Aside type=\"caution\">\nDo not use catalog/scaffold to scaffold untrusted templates. IaC configurations are inherently powerful, as they\ncan run arbitrary code on your system, so make sure to only use trusted templates you have reviewed and approved.\n</Aside>\n\n<Aside type=\"note\">\nNote that you will otherwise be prompted to confirm the execution of each hook unless you are running in [non-interactive mode](/reference/cli/global-flags/#non-interactive).\n</Aside>\n\nExamples:\n\n```bash\nterragrunt scaffold github.com/org/repo//modules/mysql --no-hooks\n```\n"
  },
  {
    "path": "docs/src/data/flags/scaffold-no-include-root.mdx",
    "content": "---\nname: no-include-root\ndescription: Disable inclusion of the root module in the generated `terragrunt.hcl` file (equivalent to using `--var=EnableRootInclude=false`, and will be overridden if the corresponding `var` value is set).\ntype: bool\nenv:\n  - TG_NO_INCLUDE_ROOT\n---\n\nWhen enabled, Terragrunt will not automatically include the root configuration file in generated `terragrunt.hcl` files during scaffolding operations.\n\nThis is equivalent to setting `--var=EnableRootInclude=false` and will be overridden if that variable is explicitly set.\n\nExample:\n\n```bash\nterragrunt scaffold github.com/org/repo//modules/mysql --no-include-root\n```\n"
  },
  {
    "path": "docs/src/data/flags/scaffold-no-shell.mdx",
    "content": "---\nname: no-shell\ndescription: \"Disable shell commands when using boilerplate templates during scaffolding.\"\ntype: bool\nenv:\n  - TG_NO_SHELL\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nWhen enabled, Terragrunt will disable shell commands when processing boilerplate templates during scaffolding operations.\n\nThis is useful for security reasons when you want to prevent templates from executing arbitrary shell commands on your system.\n\n<Aside type=\"caution\">\nDo not use catalog/scaffold to scaffold untrusted templates. IaC configurations are inherently powerful, as they\ncan run arbitrary code on your system, so make sure to only use trusted templates you have reviewed and approved.\n</Aside>\n\n<Aside type=\"note\">\nNote that you will otherwise be prompted to confirm the execution of each shell command unless you are running in [non-interactive mode](/reference/cli/global-flags/#non-interactive).\n</Aside>\n\nExamples:\n\n```bash\nterragrunt scaffold github.com/org/repo//modules/mysql --no-shell\n```\n"
  },
  {
    "path": "docs/src/data/flags/scaffold-root-file-name.mdx",
    "content": "---\nname: root-file-name\ndescription: Set the name of the root configuration file to include in the generated `terragrunt.hcl` file.\ntype: string\nenv:\n  - TG_ROOT_FILE_NAME\n---\n\nSpecifies the name of the root configuration file to include in generated `terragrunt.hcl` files during scaffolding. This is equivalent to using `--var=RootFileName=<name>`, and will be overridden if the corresponding `var` value is set.\n\nExample:\n\n```bash\nterragrunt scaffold github.com/org/repo//modules/mysql --root-file-name root.hcl\n```\n"
  },
  {
    "path": "docs/src/data/flags/scaffold-var-file.mdx",
    "content": "---\nname: var-file\ndescription: Variable file to use for the boilerplate template.\ntype: string\nenv:\n  - TG_VAR_FILE\n---\n\nSpecifies a file containing variables to be used in the boilerplate template during scaffolding. The file should contain variable definitions in HCL format.\n\nExample:\n\n```bash\n# vars.hcl\nenvironment = \"prod\"\nregion = \"us-east-1\"\n\n# Command\nterragrunt scaffold github.com/org/repo//modules/mysql --var-file=vars.hcl\n```\n"
  },
  {
    "path": "docs/src/data/flags/scaffold-var.mdx",
    "content": "---\nname: var\ndescription: Variable to use for the boilerplate template.\ntype: string\nenv:\n  - TG_VAR\n---\n\nSets variables to be used in the boilerplate template during scaffolding. You can specify multiple variables by using the flag multiple times.\n\nExample:\n\n```bash\nterragrunt scaffold github.com/org/repo//modules/mysql \\\n  --var=\"environment=prod\" \\\n  --var=\"region=us-east-1\"\n```\n"
  },
  {
    "path": "docs/src/data/flags/source-map.mdx",
    "content": "---\nname: source-map\ndescription: Replace any source URL (including the source URL of a config pulled in with dependency blocks) that has root source with dest.\ntype: string\nenv:\n  - TG_SOURCE_MAP\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nAllows you to replace source URLs that match a specified pattern with a different URL. This affects both direct source references and sources specified in dependency blocks.\n\nThe format is `source-regex=replacement-source`.\n\nFor example:\n\n```bash\nterragrunt run plan --source-map \"git::ssh://git@github.com/org/repo.git=../local/repo\"\n```\n\nThis will replace any source URL that matches `git::ssh://git@github.com/org/repo.git` with `../local/repo`.\n\n<Aside type=\"caution\">\nSource mapping only performs literal matches on the URL portion. For example, a map key of `ssh://git@github.com/org/repo.git` will not match sources of the form `git::ssh://git@github.com/org/repo.git`. The latter requires a map key of `git::ssh://git@github.com/org/repo.git`.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/source-update.mdx",
    "content": "---\nname: source-update\ndescription: Delete the contents of the temporary folder to clear out any old, cached source code before downloading new source code into it.\ntype: bool\nenv:\n  - TG_SOURCE_UPDATE\n---\n\nWhen enabled, Terragrunt will delete any existing content in the temporary download directory before fetching new source code into it. This ensures you're working with a fresh copy of the source code.\n\nExample:\n\n```bash\nterragrunt run plan --source-update\n```\n"
  },
  {
    "path": "docs/src/data/flags/source.mdx",
    "content": "---\nname: source\ndescription: Download OpenTofu/Terraform configurations from the specified source into a temporary folder, and run Terraform in that temporary folder.\ntype: string\nenv:\n  - TG_SOURCE\n---\n\nSpecifies a source URL where Terragrunt should download the OpenTofu/Terraform configuration. This overrides any `source` parameters specified in the Terragrunt configuration files.\n\nThe configuration will be downloaded into a temporary folder, and Terragrunt will execute OpenTofu/Terraform commands in that folder.\n\nExample:\n\n```bash\nterragrunt run plan --source github.com/example/infrastructure//modules/vpc\n```\n\nThis is particularly useful when:\n\n- Testing changes to modules without modifying the source in your configuration.\n- Using a specific version or branch of your infrastructure modules.\n- Running configurations from a different source than what's specified in your Terragrunt files.\n"
  },
  {
    "path": "docs/src/data/flags/stack-generate-filter.mdx",
    "content": "---\nname: filter\ndescription: Filter configurations using a flexible query language\ntype: list(string)\n---\n\nimport {Aside} from '@astrojs/starlight/components';\n\n<Aside type=\"note\" title=\"Filter Behavior for Stack Generate\">\nThe `--filter` flag works differently for `stack generate` compared to other commands. It requires that you explicitly target stacks using the `type=stack` attribute.\n\nExamples:\n\n```bash\n# Filter by name\nterragrunt stack generate --filter 'name=prod | type=stack'\n```\n\n</Aside>\n\n### Attribute-Based Filtering\nMatch stacks by their configuration attributes:\n\n```bash\n# Filter by name\nterragrunt stack generate --filter 'prod | type=stack'\n\n# Filter by type\nterragrunt stack generate --filter 'type=stack'\n\n# Filter by path\nterragrunt stack generate --filter './envs/prod/** | type=stack'\n```\n\n### Negation\nExclude paths using the `!` prefix:\n\n```bash\n# Exclude by path\nterragrunt stack generate --filter '!./prod/** | type=stack'\n```\n\n### Union (Multiple Filters)\nSpecify multiple `--filter` flags to combine results using OR logic:\n\n```bash\n# Generate the dev and prod stacks\nterragrunt stack generate --filter 'dev | type=stack' --filter 'prod | type=stack'\n```\n"
  },
  {
    "path": "docs/src/data/flags/stack-output-format.mdx",
    "content": "---\nname: format\ndescription: Format stack output. (json, raw).\ntype: string\nenv:\n  - TG_FORMAT\n---\n\nSpecifies the output format for stack outputs. Available formats are:\n\n- `default` - Format output as HCL (default)\n- `json` - Format output as JSON for machine readability\n- `raw` - Format output as raw string for shell script integration\n\nExample:\n\n```bash\n# JSON format\nterragrunt stack output --format json\n\n# Raw format\nterragrunt stack output --format raw project1_app1.custom_value1\n```\n\n{/* TODO: Port over the better docs from the old docs */}\n"
  },
  {
    "path": "docs/src/data/flags/stack-output-json.mdx",
    "content": "---\nname: json\ndescription: Format stack output as JSON. Alias for --format json.\ntype: bool\nenv:\n  - TG_JSON\n---\n\nA convenience flag that formats the stack output as JSON. This is equivalent to using `--format json`.\n\nExample:\n\n```bash\nterragrunt stack output --json\n```\n"
  },
  {
    "path": "docs/src/data/flags/stack-output-raw.mdx",
    "content": "---\nname: raw\ndescription: Format stack output as raw string. Alias for --format raw.\ntype: bool\nenv:\n  - TG_RAW\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nA convenience flag that formats the stack output as a raw string. This is equivalent to using `--format raw`.\n\nThis is particularly useful when you need to use the output in shell scripts or other automation.\n\nExample:\n\n```bash\n# Store the output in a variable\nAPP_ID=$(terragrunt stack output --raw app.id)\n```\n\n<Aside type=\"caution\">\nWhen using `--stack-output-raw`, you must use index-based access. Attempting to access nested structures directly may result in unexpected output or errors.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/strict-control.mdx",
    "content": "---\nname: strict-control\ndescription: Enables specific strict controls.\ntype: string\nenv:\n  - TG_STRICT_CONTROL\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nEnables specific strict controls in Terragrunt. For a list of available controls and their descriptions, run `terragrunt info strict`.\n\nThis flag provides finer-grained control compared to `--strict-mode`, allowing you to enable specific controls rather than all of them.\n\nFor more information, see the [strict controls documentation](/reference/strict-controls).\n\n<Aside type=\"caution\">\nWhen accessing complex data structures using the `raw` format, you must use index-based access.\n\nAttempting to access nested structures directly may result in unexpected output or errors.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/strict-mode.mdx",
    "content": "---\nname: strict-mode\ndescription: Enables strict mode for Terragrunt.\ntype: bool\nenv:\n  - TG_STRICT_MODE\n---\n\nWhen enabled, Terragrunt will operate in strict mode, which opts in the user to all future breaking changes. For more information about what strict mode enables, run `terragrunt info strict`.\n\nFor more information about strict mode, see the [strict controls documentation](/reference/strict-controls).\n"
  },
  {
    "path": "docs/src/data/flags/summary-disable.mdx",
    "content": "---\nname: summary-disable\ndescription: Disables the summary output at the end of a run.\ntype: bool\nenv:\n  - TG_SUMMARY_DISABLE\n---\n\nWhen enabled, Terragrunt will disable the summary output at the end of a run.\n\nFor more information, see the [Run Report](/features/stacks/run-report#disabling-the-summary) feature.\n"
  },
  {
    "path": "docs/src/data/flags/summary-per-unit.mdx",
    "content": "---\nname: summary-per-unit\ndescription: Summarize each unit in the run summary.\ntype: bool\nenv:\n  - TG_SUMMARY_PER_UNIT\n---\n\nWhen enabled, Terragrunt will break down the run summary by unit. The units are sorted by result, then duration, with the longest-running units shown first.\n\nFor more information, see the [Run Report](/features/stacks/run-report) feature.\n"
  },
  {
    "path": "docs/src/data/flags/tf-forward-stdout.mdx",
    "content": "---\nname: tf-forward-stdout\ndescription: If specified, the output of OpenTofu/Terraform commands will be printed as is, without being integrated into the Terragrunt log.\ntype: bool\nenv:\n  - TG_TF_FORWARD_STDOUT\n---\n\nBy default, Terragrunt integrates OpenTofu/Terraform output into its own logging system. When this flag is enabled, OpenTofu/Terraform output will be printed directly to stdout without Terragrunt's logging wrapper.\n\nFor example, without `--tf-forward-stdout`:\n\n```bash\n14:19:25.081 INFO   [app] Running command: tofu plan -input=false\n14:19:25.174 STDOUT [app] tofu: OpenTofu used the selected providers to generate the following execution\n14:19:25.174 STDOUT [app] tofu: plan. Resource actions are indicated with the following symbols:\n14:19:25.174 STDOUT [app] tofu:   + create\n14:19:25.174 STDOUT [app] tofu: OpenTofu will perform the following actions:\n```\n\nWith `--tf-forward-stdout`:\n\n```bash\n14:19:25.081 INFO   [app] Running command: tofu plan -input=false\n\nOpenTofu used the selected providers to generate the following execution\nplan. Resource actions are indicated with the following symbols:\n  + create\n\nOpenTofu will perform the following actions:\n```\n"
  },
  {
    "path": "docs/src/data/flags/tf-path.mdx",
    "content": "---\nname: tf-path\ndescription: Path to the OpenTofu/Terraform binary. Default is tofu (on PATH).\ntype: string\nenv:\n  - TG_TF_PATH\n---\n\nSpecifies the path to the OpenTofu/Terraform binary that Terragrunt should use. By default, Terragrunt will look for a binary named `tofu` in your system's PATH.\n\nThis is useful when:\n\n- You have multiple versions of OpenTofu/Terraform installed.\n- The binary is not in your PATH.\n- You want to use a specific version for certain operations.\n- You want to switch between OpenTofu and Terraform binaries.\n\nExample:\n\n```bash\nterragrunt run plan --tf-path=/usr/local/bin/tofu\n```\n\nNote that if you only have `terraform` installed, and it is available in your `PATH`, Terragrunt will automatically use that binary.\n\nNOTE: This will override the terraform binary that is used by terragrunt in all instances, including dependency lookups. This setting will also override any terraform_binary configuration values specified in the terragrunt.hcl config for both the top level, and dependency lookups.\n"
  },
  {
    "path": "docs/src/data/flags/units-that-include.mdx",
    "content": "---\nname: units-that-include\ndescription: If flag is set, 'run --all' will only run the command against Terragrunt units that include the specified file.\ntype: string\nenv:\n  - TG_UNITS_THAT_INCLUDE\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\n<Aside type=\"tip\" title=\"Alias for --filter\">\n\nThis flag is an alias for the [`--filter` flag](/reference/cli/commands/run#filter).\n\nWhen using this flag, the equivalent filter expression is used instead.\n\ne.g.\n\n```bash\nterragrunt run --all --units-that-include shared.hcl -- plan\n```\n\nis equivalent to:\n\n```bash\nterragrunt run --all --filter 'reading=shared.hcl' -- plan\n```\n\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/use-partial-parse-config-cache.mdx",
    "content": "---\nname: use-partial-parse-config-cache\ndescription: Enables caching of includes during partial parsing operations. Will also be used for the --iam-assume-role option if provided.\ntype: bool\nenv:\n  - TG_USE_PARTIAL_PARSE_CONFIG_CACHE\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nThis flag can be used to drastically decrease time required for parsing Terragrunt configuration files. The effect will only show if a lot of similar includes are expected such as the root terragrunt configuration (e.g. `root.hcl`) include.\n\nNOTE: This is an experimental feature, use with caution.\n\nThe reason you might want to use this flag is that Terragrunt frequently only needs to perform a partial parse of Terragrunt configurations.\n\nThis is the case for scenarios like:\n\n- Building the Directed Acyclic Graph (DAG) during a `run --all` command where only the `dependency` blocks need to be evaluated to determine run order.\n- Parsing the `terraform` block to determine state configurations for fetching `dependency` outputs.\n- Determining whether Terragrunt execution behavior has to change like for `prevent_destroy` or `skip` flags in configuration.\n\nThese configurations are generally safe to cache, but due to the nature of HCL being a dynamic configuration language, there are some edge cases where caching these can lead to incorrect behavior.\n\nOnce this flag has been tested thoroughly, we will consider making it the default behavior.\n\n<Aside type=\"caution\">\nThis is an experimental feature. While it can significantly improve performance with frequently included configurations, the caching behavior may lead to unexpected results in some edge cases due to HCL's dynamic nature.\n\nTest thoroughly in your environment before using in production environments.\n</Aside>\n"
  },
  {
    "path": "docs/src/data/flags/version-manager-file-name.mdx",
    "content": "---\nname: version-manager-file-name\ndescription: File names used during the computation of the cache key for the version manager files.\ntype: string\nenv:\n  - TG_VERSION_MANAGER_FILE_NAME\n---\n\nBy default, terragrunt is specifying:\n\n```shell\n.terraform-version\n.tool-versions\n.mise.toml\nmise.toml\n```"
  },
  {
    "path": "docs/src/data/flags/version.mdx",
    "content": "---\nname: version\ndescription: Show terragrunt version.\ntype: bool\n---\n\nDisplays the current version of Terragrunt.\n"
  },
  {
    "path": "docs/src/data/flags/working-dir.mdx",
    "content": "---\nname: working-dir\ndescription: The path where all commands should be run. Default is current directory.\ntype: string\nenv:\n  - TG_WORKING_DIR\n---\n\nimport { Aside } from '@astrojs/starlight/components';\n\nSpecifies the working directory where Terragrunt should execute its commands. By default, Terragrunt uses the current directory.\n\nExample:\n\n```bash\nterragrunt run plan --working-dir /path/to/infrastructure/prod\n```\n\n<Aside type=\"note\">\nThe `--working-dir` flag behaves differently for `run --all` commands versus single unit commands.\n\nFor `run --all`, Terragrunt will execute in all subdirectories of the working directory, while for `run` commands it will execute only in the specified directory.\n</Aside>\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/.gitignore",
    "content": ".terraform\n.terragrunt-cache\n.terragrunt-stack\nnode_modules\n.auto.tfvars\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/app/best-cat/index.js",
    "content": "import { S3Client, ListObjectsV2Command, GetObjectCommand } from '@aws-sdk/client-s3';\nimport { DynamoDBClient } from '@aws-sdk/client-dynamodb';\nimport { DynamoDBDocumentClient, GetCommand, UpdateCommand, ScanCommand } from '@aws-sdk/lib-dynamodb';\nimport { getSignedUrl } from '@aws-sdk/s3-request-presigner';\nimport { readFileSync } from 'fs';\nimport { join, dirname } from 'path';\nimport { fileURLToPath } from 'url';\nimport Handlebars from 'handlebars';\n\nconst s3Client = new S3Client({\n  maxAttempts: 3,\n  requestHandler: {\n    keepAlive: true\n  }\n});\nconst dynamoClient = new DynamoDBClient({\n  maxAttempts: 3,\n  requestHandler: {\n    keepAlive: true\n  }\n});\nconst dynamodb = DynamoDBDocumentClient.from(dynamoClient);\n\n// Get the directory path for ES modules\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\n// Load static files\nconst templateHtml = readFileSync(join(__dirname, 'template.html'), 'utf8');\nconst stylesCss = readFileSync(join(__dirname, 'styles.css'), 'utf8');\nconst scriptJs = readFileSync(join(__dirname, 'script.js'), 'utf8');\n\n// Compile Handlebars template\nconst template = Handlebars.compile(templateHtml);\n\n// Server-side cache for presigned URLs\nconst presignedUrlCache = new Map();\n\n// Server-side cache for S3 list response\nconst s3ListCache = {\n  data: null,\n  lastUpdated: 0,\n  ttl: 10 * 1000 // 10 seconds\n};\n\n// Cache cleanup interval (every 5 minutes)\nconst CACHE_CLEANUP_INTERVAL = 5 * 60 * 1000;\n\n// Initialize cache cleanup\nsetInterval(cleanupExpiredCache, CACHE_CLEANUP_INTERVAL);\n\n// Function to clean up expired cache entries\nfunction cleanupExpiredCache() {\n  const now = Date.now();\n\n  // Clean up presigned URL cache\n  for (const [key, cacheEntry] of presignedUrlCache.entries()) {\n    if (now > cacheEntry.expiresAt) {\n      presignedUrlCache.delete(key);\n    }\n  }\n\n  // Clean up S3 list cache if expired\n  if (s3ListCache.data && (now - s3ListCache.lastUpdated) > s3ListCache.ttl) {\n    s3ListCache.data = null;\n    s3ListCache.lastUpdated = 0;\n  }\n}\n\n// Function to get or generate presigned URL with caching\nasync function getCachedPresignedUrl(bucketName, imageKey) {\n  const cacheKey = `${bucketName}:${imageKey}`;\n  const now = Date.now();\n\n  // Check if we have a valid cached URL\n  const cached = presignedUrlCache.get(cacheKey);\n  if (cached && now < cached.expiresAt) {\n    return cached.url;\n  }\n\n  // Generate new presigned URL\n  const getObjectCommand = new GetObjectCommand({\n    Bucket: bucketName,\n    Key: imageKey\n  });\n\n  const presignedUrl = await getSignedUrl(s3Client, getObjectCommand, { expiresIn: 3600 });\n\n  // Cache the URL with expiration\n  presignedUrlCache.set(cacheKey, {\n    url: presignedUrl,\n    expiresAt: now + (3600 * 1000) // 1 hour from now\n  });\n\n  return presignedUrl;\n}\n\n// Function to get cached S3 list data\nasync function getCachedS3List(bucketName) {\n  const now = Date.now();\n\n  // Check if we have valid cached data\n  if (s3ListCache.data && (now - s3ListCache.lastUpdated) < s3ListCache.ttl) {\n    console.log('Using cached S3 list data');\n    return s3ListCache.data;\n  }\n\n  // Fetch fresh data from S3\n  console.log('Fetching fresh S3 list data');\n  const listCommand = new ListObjectsV2Command({\n    Bucket: bucketName,\n    MaxKeys: 100\n  });\n  const listResponse = await s3Client.send(listCommand);\n\n  // Cache the response if we got data\n  if (listResponse.Contents && listResponse.Contents.length > 0) {\n    s3ListCache.data = listResponse;\n    s3ListCache.lastUpdated = now;\n    console.log(`Cached S3 list with ${listResponse.Contents.length} objects`);\n  }\n\n  return listResponse;\n}\n\nexport async function handler(event) {\n  const bucketName = process.env.S3_BUCKET_NAME;\n  const tableName = process.env.DYNAMODB_TABLE_NAME;\n\n  try {\n    // Parse the request - Lambda function URLs have a different event structure\n    const method = event.requestContext?.http?.method || event.httpMethod;\n    const path = event.rawPath || event.path || '/';\n\n    console.log('Request:', { method, path, event });\n\n    // Serve static files\n    if (method === 'GET' && path === '/styles.css') {\n      return {\n        statusCode: 200,\n        headers: {\n          'Content-Type': 'text/css',\n          'Cache-Control': 'public, max-age=3600'\n        },\n        body: stylesCss\n      };\n    }\n\n    if (method === 'GET' && path === '/script.js') {\n      return {\n        statusCode: 200,\n        headers: {\n          'Content-Type': 'application/javascript',\n          'Cache-Control': 'public, max-age=3600'\n        },\n        body: scriptJs\n      };\n    }\n\n    // Serve images with caching headers\n    if (method === 'GET' && path.startsWith('/image/')) {\n      const imageKey = path.substring(7); // Remove '/image/' prefix\n\n      try {\n        const presignedUrl = await getCachedPresignedUrl(bucketName, imageKey);\n\n        return {\n          statusCode: 302, // Redirect to presigned URL\n          headers: {\n            'Location': presignedUrl,\n            'Cache-Control': 'public, max-age=3600, immutable'\n          }\n        };\n      } catch (error) {\n        console.error('Error generating presigned URL:', error);\n        return {\n          statusCode: 404,\n          headers: {\n            'Content-Type': 'text/html'\n          },\n          body: '<h1>Image not found</h1>'\n        };\n      }\n    }\n\n    // Main page - serve HTML\n    if (method === 'GET' && (path === '/' || path === '')) {\n      // Get all images from S3 (with caching)\n      const listResponse = await getCachedS3List(bucketName);\n\n      // Get all vote counts from DynamoDB\n      const scanCommand = new ScanCommand({\n        TableName: tableName\n      });\n      const votesResponse = await dynamodb.send(scanCommand);\n      const votesMap = {};\n      if (votesResponse.Items) {\n        votesResponse.Items.forEach(item => {\n          votesMap[item.image_id] = item.votes || 0;\n        });\n      }\n\n      // Filter and prepare image data first\n      const imageObjects = (listResponse.Contents || []).filter(obj =>\n        obj.Key && obj.Key.match(/\\.(jpg|jpeg|png|gif|webp)$/i)\n      );\n\n      // Generate presigned URLs in parallel for better performance\n      const imagesWithUrls = await Promise.all(\n        imageObjects.map(async (obj) => {\n          const presignedUrl = await getCachedPresignedUrl(bucketName, obj.Key);\n          return {\n            key: obj.Key,\n            keyId: obj.Key.replace(/[^a-zA-Z0-9]/g, '-'),\n            url: presignedUrl,\n            votes: votesMap[obj.Key] || 0\n          };\n        })\n      );\n\n      // Sort images by vote count (highest votes first)\n      imagesWithUrls.sort((a, b) => b.votes - a.votes);\n\n      // Render the template with data\n      const html = template({\n        images: imagesWithUrls\n      });\n\n      return {\n        statusCode: 200,\n        headers: {\n          'Content-Type': 'text/html',\n          'Cache-Control': 'public, max-age=60, s-maxage=300'\n        },\n        body: html\n      };\n    }\n\n    // API endpoint for voting\n    if (method === 'POST' && path.startsWith('/vote/')) {\n      const parts = path.split('/');\n      const imageId = parts[2];\n      const voteType = parts[3]; // 'up' or 'down'\n\n      const voteIncrement = voteType === 'up' ? 1 : -1;\n\n      // Update vote count in DynamoDB\n      const updateCommand = new UpdateCommand({\n        TableName: tableName,\n        Key: { image_id: imageId },\n        UpdateExpression: 'SET votes = if_not_exists(votes, :zero) + :inc',\n        ExpressionAttributeValues: {\n          ':inc': voteIncrement,\n          ':zero': 0\n        },\n        ReturnValues: 'UPDATED_NEW'\n      });\n      const result = await dynamodb.send(updateCommand);\n\n              return {\n          statusCode: 200,\n          headers: {\n            'Content-Type': 'application/json'\n          },\n          body: JSON.stringify({\n            message: 'Vote recorded successfully',\n            image_id: imageId,\n            new_vote_count: result.Attributes.votes\n          })\n        };\n    }\n\n    // API endpoint to get current vote counts\n    if (method === 'GET' && path === '/api/votes') {\n      const scanCommand = new ScanCommand({\n        TableName: tableName\n      });\n      const votes = await dynamodb.send(scanCommand);\n\n              return {\n          statusCode: 200,\n          headers: {\n            'Content-Type': 'application/json'\n          },\n          body: JSON.stringify({\n            votes: votes.Items || []\n          })\n        };\n    }\n\n    // For any other GET request, serve the main page (helpful for debugging)\n    if (method === 'GET') {\n      console.log('Serving main page for path:', path);\n\n      // Get all images from S3 (with caching)\n      const listResponse = await getCachedS3List(bucketName);\n\n      // Get all vote counts from DynamoDB\n      const scanCommand = new ScanCommand({\n        TableName: tableName\n      });\n      const votesResponse = await dynamodb.send(scanCommand);\n      const votesMap = {};\n      if (votesResponse.Items) {\n        votesResponse.Items.forEach(item => {\n          votesMap[item.image_id] = item.votes || 0;\n        });\n      }\n\n      // Filter and prepare image data efficiently\n      const imageObjects = (listResponse.Contents || []).filter(obj =>\n        obj.Key && obj.Key.match(/\\.(jpg|jpeg|png|gif|webp)$/i)\n      );\n\n      const imagesWithUrls = imageObjects.map(obj => ({\n        key: obj.Key,\n        keyId: obj.Key.replace(/[^a-zA-Z0-9]/g, '-'),\n        votes: votesMap[obj.Key] || 0\n      }));\n\n      // Sort images by vote count (highest votes first)\n      imagesWithUrls.sort((a, b) => b.votes - a.votes);\n\n      // Render the template with data\n      const html = template({\n        images: imagesWithUrls\n      });\n\n      return {\n        statusCode: 200,\n        headers: {\n          'Content-Type': 'text/html',\n          'Cache-Control': 'public, max-age=60, s-maxage=300'\n        },\n        body: html\n      };\n    }\n\n    // Default response for unknown endpoints\n    return {\n      statusCode: 404,\n      headers: {\n        'Content-Type': 'text/html'\n      },\n      body: `\n        <html>\n          <head><title>404 - Not Found</title></head>\n          <body>\n            <h1>404 - Page Not Found</h1>\n            <p>The requested page could not be found.</p>\n            <p>Method: ${method}, Path: ${path}</p>\n            <a href=\"/\">Go back to the main page</a>\n          </body>\n        </html>\n      `\n    };\n\n  } catch (error) {\n    console.error('Error:', error);\n\n    return {\n      statusCode: 500,\n      headers: {\n        'Content-Type': 'text/html'\n      },\n      body: `\n        <html>\n          <head><title>500 - Server Error</title></head>\n          <body>\n            <h1>500 - Internal Server Error</h1>\n            <p>An error occurred while processing your request.</p>\n            <a href=\"/\">Go back to the main page</a>\n          </body>\n        </html>\n      `\n    };\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/app/best-cat/package.json",
    "content": "{\n  \"name\": \"best-cat\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Vote for the best cat\",\n  \"main\": \"index.js\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"package\": \"zip -r ../../dist/best-cat.zip . -x '*.git*' 'node_modules/.cache/*'\"\n  },\n  \"dependencies\": {\n    \"@aws-sdk/client-s3\": \"^3.993.0\",\n    \"@aws-sdk/client-dynamodb\": \"^3.993.0\",\n    \"@aws-sdk/lib-dynamodb\": \"^3.993.0\",\n    \"@aws-sdk/s3-request-presigner\": \"^3.993.0\",\n    \"handlebars\": \"^4.7.8\"\n  },\n  \"engines\": {\n    \"node\": \">=22.0.0\"\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/app/best-cat/script.js",
    "content": "// Global variable to store the base URL for API calls\nlet baseUrl = '';\n\n// Initialize the application\ndocument.addEventListener('DOMContentLoaded', function() {\n    // Set the base URL from the current location\n    baseUrl = window.location.origin;\n\n    // Add click effects to vote buttons\n    const voteButtons = document.querySelectorAll('.vote-btn');\n    voteButtons.forEach(button => {\n        button.addEventListener('click', function() {\n            // Create a subtle click effect\n            this.style.transform = 'scale(0.9)';\n            setTimeout(() => {\n                this.style.transform = '';\n            }, 150);\n        });\n    });\n});\n\n// Function to handle voting asynchronously\nasync function vote(imageKey, voteType) {\n    const voteElement = document.getElementById(`votes-${imageKey.replace(/[^a-zA-Z0-9]/g, '-')}`);\n    const voteButtons = document.querySelectorAll(`[data-image-key=\"${imageKey}\"] .vote-btn`);\n\n    if (!voteElement) return;\n\n    // Get current vote count\n    const currentVotes = parseInt(voteElement.textContent) || 0;\n    const voteIncrement = voteType === 'up' ? 1 : -1;\n\n    // Optimistically update the UI immediately\n    const newVoteCount = currentVotes + voteIncrement;\n    voteElement.textContent = newVoteCount;\n\n    // Add immediate visual feedback\n    voteElement.style.transform = 'scale(1.2)';\n    voteElement.style.color = voteType === 'up' ? '#4CAF50' : '#f44336';\n\n    // Disable vote buttons to prevent double-clicking\n    voteButtons.forEach(btn => {\n        btn.disabled = true;\n        btn.style.opacity = '0.6';\n    });\n\n    // Send vote request asynchronously\n    const votePromise = fetch(`${baseUrl}/vote/${imageKey}/${voteType}`, {\n        method: 'POST',\n        headers: {\n            'Content-Type': 'application/json'\n        }\n    });\n\n    try {\n        const response = await votePromise;\n\n        if (response.ok) {\n            const result = await response.json();\n\n            // Update with actual server response\n            voteElement.textContent = result.new_vote_count;\n\n            // Success animation\n            voteElement.style.transform = 'scale(1.1)';\n            setTimeout(() => {\n                voteElement.style.transform = 'scale(1)';\n                voteElement.style.color = '';\n            }, 300);\n\n        } else {\n            // Revert optimistic update on error\n            voteElement.textContent = currentVotes;\n            console.error('Vote failed:', response.statusText);\n\n            // Show error feedback\n            voteElement.style.transform = 'scale(1.1)';\n            voteElement.style.color = '#f44336';\n            setTimeout(() => {\n                voteElement.style.transform = 'scale(1)';\n                voteElement.style.color = '';\n            }, 300);\n\n            // Show user-friendly error message\n            showNotification('Vote failed. Please try again.', 'error');\n        }\n    } catch (error) {\n        // Revert optimistic update on network error\n        voteElement.textContent = currentVotes;\n        console.error('Error voting:', error);\n\n        // Show error feedback\n        voteElement.style.transform = 'scale(1.1)';\n        voteElement.style.color = '#f44336';\n        setTimeout(() => {\n            voteElement.style.transform = 'scale(1)';\n            voteElement.style.color = '';\n        }, 300);\n\n        // Show user-friendly error message\n        showNotification('Network error. Please check your connection.', 'error');\n    } finally {\n        // Re-enable vote buttons\n        voteButtons.forEach(btn => {\n            btn.disabled = false;\n            btn.style.opacity = '1';\n        });\n    }\n}\n\n// Function to show notifications\nfunction showNotification(message, type = 'info') {\n    // Remove existing notifications\n    const existingNotification = document.querySelector('.notification');\n    if (existingNotification) {\n        existingNotification.remove();\n    }\n\n    // Create notification element\n    const notification = document.createElement('div');\n    notification.className = `notification notification-${type}`;\n    notification.textContent = message;\n\n    // Add styles\n    notification.style.cssText = `\n        position: fixed;\n        top: 20px;\n        right: 20px;\n        padding: 12px 20px;\n        border-radius: 8px;\n        color: white;\n        font-weight: 500;\n        z-index: 1000;\n        transform: translateX(100%);\n        transition: transform 0.3s ease;\n        max-width: 300px;\n        word-wrap: break-word;\n    `;\n\n    // Set background color based on type\n    if (type === 'error') {\n        notification.style.backgroundColor = '#f44336';\n    } else if (type === 'success') {\n        notification.style.backgroundColor = '#4CAF50';\n    } else {\n        notification.style.backgroundColor = '#2196F3';\n    }\n\n    // Add to page\n    document.body.appendChild(notification);\n\n    // Animate in\n    setTimeout(() => {\n        notification.style.transform = 'translateX(0)';\n    }, 100);\n\n    // Auto-remove after 3 seconds\n    setTimeout(() => {\n        notification.style.transform = 'translateX(100%)';\n        setTimeout(() => {\n            if (notification.parentNode) {\n                notification.remove();\n            }\n        }, 300);\n    }, 3000);\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/app/best-cat/styles.css",
    "content": "* {\n    margin: 0;\n    padding: 0;\n    box-sizing: border-box;\n}\n\nbody {\n    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\n    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);\n    min-height: 100vh;\n    margin: 0;\n    padding: 0;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.container {\n    max-width: 1200px;\n    width: 100%;\n    padding: 20px;\n    text-align: center;\n}\n\nh1 {\n    text-align: center;\n    color: white;\n    margin-bottom: 40px;\n    font-size: 3rem;\n    text-shadow: 2px 2px 4px rgba(0,0,0,0.3);\n    font-weight: 700;\n    letter-spacing: 1px;\n}\n\n.images-grid {\n    display: grid;\n    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));\n    gap: 25px;\n    margin-top: 30px;\n    justify-items: center;\n}\n\n.image-card {\n    background: white;\n    border-radius: 20px;\n    overflow: hidden;\n    box-shadow: 0 15px 35px rgba(0,0,0,0.15);\n    transition: all 0.3s ease;\n    width: 100%;\n    max-width: 350px;\n}\n\n.image-card:hover {\n    transform: translateY(-8px);\n    box-shadow: 0 20px 50px rgba(0,0,0,0.25);\n}\n\n.image-container {\n    position: relative;\n    width: 100%;\n    height: 250px;\n    overflow: hidden;\n}\n\n.image-container img {\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n    transition: transform 0.3s ease;\n}\n\n.image-card:hover .image-container img {\n    transform: scale(1.05);\n}\n\n.voting-section {\n    padding: 20px;\n    text-align: center;\n}\n\n.vote-count {\n    font-size: 1.8rem;\n    font-weight: bold;\n    color: #333;\n    margin-bottom: 20px;\n    background: linear-gradient(135deg, #667eea, #764ba2);\n    -webkit-background-clip: text;\n    -webkit-text-fill-color: transparent;\n    background-clip: text;\n}\n\n.vote-buttons {\n    display: flex;\n    justify-content: center;\n    gap: 10px;\n}\n\n.vote-btn {\n    background: none;\n    border: 2px solid #e0e0e0;\n    border-radius: 50%;\n    width: 55px;\n    height: 55px;\n    font-size: 1.6rem;\n    cursor: pointer;\n    transition: all 0.3s ease;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    position: relative;\n    overflow: hidden;\n}\n\n.vote-btn:hover {\n    border-color: #667eea;\n    background: linear-gradient(135deg, #667eea, #764ba2);\n    color: white;\n    transform: scale(1.1);\n    box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);\n}\n\n.vote-btn:active {\n    transform: scale(0.95);\n}\n\n.vote-btn.up:hover {\n    border-color: #4CAF50;\n    background: linear-gradient(135deg, #4CAF50, #45a049);\n    box-shadow: 0 5px 15px rgba(76, 175, 80, 0.4);\n}\n\n.vote-btn.down:hover {\n    border-color: #f44336;\n    background: linear-gradient(135deg, #f44336, #d32f2f);\n    box-shadow: 0 5px 15px rgba(244, 67, 54, 0.4);\n}\n\n.vote-btn:disabled {\n    cursor: not-allowed;\n    opacity: 0.6;\n    transform: none !important;\n}\n\n.vote-btn:disabled:hover {\n    border-color: #e0e0e0;\n    background: none;\n    box-shadow: none;\n    transform: none;\n}\n\n.loading {\n    text-align: center;\n    color: white;\n    font-size: 1.2rem;\n    margin: 50px 0;\n}\n\n.no-images {\n    text-align: center;\n    color: white;\n    font-size: 1.4rem;\n    margin: 60px auto;\n    line-height: 1.6;\n    background: rgba(255, 255, 255, 0.1);\n    padding: 40px;\n    border-radius: 15px;\n    backdrop-filter: blur(10px);\n    border: 1px solid rgba(255, 255, 255, 0.2);\n    max-width: 500px;\n    width: 100%;\n    grid-column: 1 / -1;\n    justify-self: center;\n}\n\n@media (max-width: 768px) {\n    .images-grid {\n        grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n        gap: 20px;\n    }\n\n    h1 {\n        font-size: 2.2rem;\n        margin-bottom: 30px;\n    }\n\n    .vote-btn {\n        width: 50px;\n        height: 50px;\n        font-size: 1.4rem;\n    }\n\n    .no-images {\n        font-size: 1.2rem;\n        padding: 30px;\n        margin: 40px 0;\n    }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/app/best-cat/template.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Best Cat Voting</title>\n    <link rel=\"stylesheet\" href=\"/styles.css\">\n</head>\n<body>\n    <div class=\"container\">\n        <h1>🐱 Vote for the Best Cat! 🐱</h1>\n\n        <div class=\"images-grid\" id=\"imagesGrid\">\n            {{#if images.length}}\n                {{#each images}}\n                <div class=\"image-card\" data-image-key=\"{{this.key}}\">\n                    <div class=\"image-container\">\n                        <img src=\"/image/{{this.key}}\" alt=\"Cat image\" loading=\"lazy\" decoding=\"async\">\n                    </div>\n                    <div class=\"voting-section\">\n                        <div class=\"vote-count\" id=\"votes-{{this.keyId}}\">{{this.votes}}</div>\n                        <div class=\"vote-buttons\">\n                            <button class=\"vote-btn up\" onclick=\"vote('{{this.key}}', 'up')\" title=\"Vote Up\">⬆️</button>\n                            <button class=\"vote-btn down\" onclick=\"vote('{{this.key}}', 'down')\" title=\"Vote Down\">⬇️</button>\n                        </div>\n\n                    </div>\n                </div>\n                {{/each}}\n            {{else}}\n                <div class=\"no-images\">No images found. Upload some cat pictures to get started!</div>\n            {{/if}}\n        </div>\n    </div>\n\n    <script src=\"/script.js\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/mise.toml",
    "content": "[tools]\naws = \"2.27.63\"\nnode = \"22.17.1\"\nopentofu = \"1.10.3\"\nterragrunt = \"0.83.2\"\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/.auto.tfvars.example",
    "content": "# Example configuration file - copy to terraform.tfvars and update values\n\n# Required: Name used for all resources (must be unique)\nname = \"best-cat-2025-07-31-01\"\n\n# Required: Path to your Lambda function zip file\nlambda_zip_file = \"../../../dist/best-cat.zip\"\n\n# Optional: Force destroy S3 buckets even when they have objects in them.\n# You're generally advised not to do this with important infrastructure,\n# however this makes testing and cleanup easier for this guide.\nforce_destroy = true\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/backend.tf",
    "content": "terraform {\n  backend \"s3\" {\n    bucket       = \"terragrunt-to-terralith-tfstate-2025-09-24-2359\"\n    key          = \"tofu.tfstate\"\n    region       = \"us-east-1\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/data.tf",
    "content": "data \"aws_caller_identity\" \"current\" {}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/ddb.tf",
    "content": "resource \"aws_dynamodb_table\" \"asset_metadata\" {\n  name         = \"${var.name}-asset-metadata\"\n  billing_mode = \"PAY_PER_REQUEST\"\n  hash_key     = \"image_id\"\n\n  attribute {\n    name = \"image_id\"\n    type = \"S\"\n  }\n\n  tags = {\n    Name = \"${var.name}-asset-metadata\"\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/iam.tf",
    "content": "resource \"aws_iam_role\" \"lambda_role\" {\n  name = \"${var.name}-lambda-role\"\n\n  assume_role_policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Action = \"sts:AssumeRole\"\n        Effect = \"Allow\"\n        Principal = {\n          Service = \"lambda.amazonaws.com\"\n        }\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_s3_read\" {\n  name        = \"${var.name}-lambda-s3-read\"\n  description = \"Policy for Lambda to read from S3 bucket\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"s3:GetObject\",\n          \"s3:ListBucket\"\n        ]\n        Resource = [\n          aws_s3_bucket.static_assets.arn,\n          \"${aws_s3_bucket.static_assets.arn}/*\"\n        ]\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_dynamodb\" {\n  name        = \"${var.name}-lambda-dynamodb\"\n  description = \"Policy for Lambda to read/write to DynamoDB table\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"dynamodb:GetItem\",\n          \"dynamodb:PutItem\",\n          \"dynamodb:UpdateItem\",\n          \"dynamodb:DeleteItem\",\n          \"dynamodb:Query\",\n          \"dynamodb:Scan\"\n        ]\n        Resource = aws_dynamodb_table.asset_metadata.arn\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_basic_execution\" {\n  name        = \"${var.name}-lambda-basic-execution\"\n  description = \"Policy for Lambda basic execution (CloudWatch logs)\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"logs:CreateLogGroup\",\n          \"logs:CreateLogStream\",\n          \"logs:PutLogEvents\"\n        ]\n        Resource = \"arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:*\"\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_s3_read\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_s3_read.arn\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_dynamodb\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_dynamodb.arn\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_basic_execution\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_basic_execution.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/lambda.tf",
    "content": "resource \"aws_lambda_function\" \"main\" {\n  function_name = \"${var.name}-function\"\n\n  filename         = var.lambda_zip_file\n  source_code_hash = filebase64sha256(var.lambda_zip_file)\n\n  role = aws_iam_role.lambda_role.arn\n\n  handler       = var.lambda_handler\n  runtime       = var.lambda_runtime\n  timeout       = var.lambda_timeout\n  memory_size   = var.lambda_memory_size\n  architectures = var.lambda_architectures\n\n  environment {\n    variables = {\n      S3_BUCKET_NAME      = aws_s3_bucket.static_assets.bucket\n      DYNAMODB_TABLE_NAME = aws_dynamodb_table.asset_metadata.name\n    }\n  }\n\n  depends_on = [\n    aws_iam_role_policy_attachment.lambda_s3_read,\n    aws_iam_role_policy_attachment.lambda_dynamodb,\n    aws_iam_role_policy_attachment.lambda_basic_execution\n  ]\n}\n\nresource \"aws_lambda_function_url\" \"main\" {\n  function_name      = aws_lambda_function.main.function_name\n  authorization_type = \"NONE\"\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/outputs.tf",
    "content": "output \"lambda_function_url\" {\n  description = \"URL of the Lambda function\"\n  value       = aws_lambda_function_url.main.function_url\n}\n\noutput \"lambda_function_name\" {\n  description = \"Name of the Lambda function\"\n  value       = aws_lambda_function.main.function_name\n}\n\noutput \"s3_bucket_name\" {\n  description = \"Name of the S3 bucket for static assets\"\n  value       = aws_s3_bucket.static_assets.bucket\n}\n\noutput \"s3_bucket_arn\" {\n  description = \"ARN of the S3 bucket for static assets\"\n  value       = aws_s3_bucket.static_assets.arn\n}\n\noutput \"dynamodb_table_name\" {\n  description = \"Name of the DynamoDB table for asset metadata\"\n  value       = aws_dynamodb_table.asset_metadata.name\n}\n\noutput \"dynamodb_table_arn\" {\n  description = \"ARN of the DynamoDB table for asset metadata\"\n  value       = aws_dynamodb_table.asset_metadata.arn\n}\n\noutput \"lambda_role_arn\" {\n  description = \"ARN of the Lambda execution role\"\n  value       = aws_iam_role.lambda_role.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/providers.tf",
    "content": "provider \"aws\" {\n  region = var.aws_region\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/s3.tf",
    "content": "resource \"aws_s3_bucket\" \"static_assets\" {\n  bucket = \"${var.name}-static-assets\"\n\n  force_destroy = var.force_destroy\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/vars-optional.tf",
    "content": "variable \"aws_region\" {\n  description = \"AWS region for all resources\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"lambda_runtime\" {\n  description = \"Lambda function runtime\"\n  type        = string\n  default     = \"nodejs22.x\"\n}\n\nvariable \"lambda_handler\" {\n  description = \"Lambda function handler\"\n  type        = string\n  default     = \"index.handler\"\n}\n\nvariable \"lambda_timeout\" {\n  description = \"Lambda function timeout in seconds\"\n  type        = number\n  default     = 30\n}\n\nvariable \"lambda_memory_size\" {\n  description = \"Lambda function memory size in MB\"\n  type        = number\n  default     = 128\n}\n\nvariable \"lambda_architectures\" {\n  description = \"Lambda function architectures\"\n  type        = list(string)\n  default     = [\"arm64\"]\n}\n\nvariable \"force_destroy\" {\n  description = \"Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)\"\n  type        = bool\n  default     = false\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n\nvariable \"lambda_zip_file\" {\n  description = \"Path to the Lambda function zip file\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-1-starting-the-terralith/live/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/ddb/main.tf",
    "content": "resource \"aws_dynamodb_table\" \"asset_metadata\" {\n  name         = \"${var.name}-asset-metadata\"\n  billing_mode = \"PAY_PER_REQUEST\"\n  hash_key     = \"image_id\"\n\n  attribute {\n    name = \"image_id\"\n    type = \"S\"\n  }\n\n  tags = {\n    Name = \"${var.name}-asset-metadata\"\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/ddb/outputs.tf",
    "content": "output \"name\" {\n  value = aws_dynamodb_table.asset_metadata.name\n}\n\noutput \"arn\" {\n  value = aws_dynamodb_table.asset_metadata.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/ddb/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"The name of the DynamoDB table\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/ddb/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/iam/data.tf",
    "content": "data \"aws_caller_identity\" \"current\" {}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/iam/main.tf",
    "content": "resource \"aws_iam_role\" \"lambda_role\" {\n  name = \"${var.name}-lambda-role\"\n\n  assume_role_policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Action = \"sts:AssumeRole\"\n        Effect = \"Allow\"\n        Principal = {\n          Service = \"lambda.amazonaws.com\"\n        }\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_s3_read\" {\n  name        = \"${var.name}-lambda-s3-read\"\n  description = \"Policy for Lambda to read from S3 bucket\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"s3:GetObject\",\n          \"s3:ListBucket\"\n        ]\n        Resource = [\n          var.s3_bucket_arn,\n          \"${var.s3_bucket_arn}/*\"\n        ]\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_dynamodb\" {\n  name        = \"${var.name}-lambda-dynamodb\"\n  description = \"Policy for Lambda to read/write to DynamoDB table\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"dynamodb:GetItem\",\n          \"dynamodb:PutItem\",\n          \"dynamodb:UpdateItem\",\n          \"dynamodb:DeleteItem\",\n          \"dynamodb:Query\",\n          \"dynamodb:Scan\"\n        ]\n        Resource = var.dynamodb_table_arn\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_basic_execution\" {\n  name        = \"${var.name}-lambda-basic-execution\"\n  description = \"Policy for Lambda basic execution (CloudWatch logs)\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"logs:CreateLogGroup\",\n          \"logs:CreateLogStream\",\n          \"logs:PutLogEvents\"\n        ]\n        Resource = \"arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:*\"\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_s3_read\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_s3_read.arn\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_dynamodb\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_dynamodb.arn\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_basic_execution\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_basic_execution.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/iam/outputs.tf",
    "content": "output \"name\" {\n  value = aws_iam_role.lambda_role.name\n}\n\noutput \"arn\" {\n  value = aws_iam_role.lambda_role.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/iam/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"The name of the IAM role\"\n  type        = string\n}\n\nvariable \"aws_region\" {\n  description = \"The AWS region to deploy the resources to\"\n  type        = string\n}\n\nvariable \"s3_bucket_arn\" {\n  description = \"The ARN of the S3 bucket\"\n  type        = string\n}\n\nvariable \"dynamodb_table_arn\" {\n  description = \"The ARN of the DynamoDB table\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/iam/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/lambda/main.tf",
    "content": "resource \"aws_lambda_function\" \"main\" {\n  function_name = \"${var.name}-function\"\n\n  filename         = var.lambda_zip_file\n  source_code_hash = filebase64sha256(var.lambda_zip_file)\n\n  role = var.lambda_role_arn\n\n  handler       = var.lambda_handler\n  runtime       = var.lambda_runtime\n  timeout       = var.lambda_timeout\n  memory_size   = var.lambda_memory_size\n  architectures = var.lambda_architectures\n\n  environment {\n    variables = {\n      S3_BUCKET_NAME      = var.s3_bucket_name\n      DYNAMODB_TABLE_NAME = var.dynamodb_table_name\n    }\n  }\n}\n\nresource \"aws_lambda_function_url\" \"main\" {\n  function_name      = aws_lambda_function.main.function_name\n  authorization_type = \"NONE\"\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/lambda/outputs.tf",
    "content": "output \"name\" {\n  value = aws_lambda_function.main.function_name\n}\n\noutput \"arn\" {\n  value = aws_lambda_function.main.arn\n}\n\noutput \"url\" {\n  value = aws_lambda_function_url.main.function_url\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/lambda/vars-optional.tf",
    "content": "variable \"lambda_runtime\" {\n  description = \"Lambda function runtime\"\n  type        = string\n  default     = \"nodejs22.x\"\n}\n\nvariable \"lambda_handler\" {\n  description = \"Lambda function handler\"\n  type        = string\n  default     = \"index.handler\"\n}\n\nvariable \"lambda_timeout\" {\n  description = \"Lambda function timeout in seconds\"\n  type        = number\n  default     = 30\n}\n\nvariable \"lambda_memory_size\" {\n  description = \"Lambda function memory size in MB\"\n  type        = number\n  default     = 128\n}\n\nvariable \"lambda_architectures\" {\n  description = \"Lambda function architectures\"\n  type        = list(string)\n  default     = [\"arm64\"]\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/lambda/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n\nvariable \"aws_region\" {\n  description = \"AWS region to deploy the resources to\"\n  type        = string\n}\n\nvariable \"lambda_zip_file\" {\n  description = \"Path to the Lambda function zip file\"\n  type        = string\n}\n\nvariable \"lambda_role_arn\" {\n  description = \"Lambda function role ARN\"\n  type        = string\n}\n\nvariable \"s3_bucket_name\" {\n  description = \"S3 bucket name\"\n  type        = string\n}\n\nvariable \"dynamodb_table_name\" {\n  description = \"DynamoDB table name\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/lambda/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/s3/main.tf",
    "content": "resource \"aws_s3_bucket\" \"static_assets\" {\n  bucket = \"${var.name}-static-assets\"\n\n  force_destroy = var.force_destroy\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/s3/outputs.tf",
    "content": "output \"name\" {\n  value = aws_s3_bucket.static_assets.bucket\n}\n\noutput \"arn\" {\n  value = aws_s3_bucket.static_assets.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/s3/vars-optional.tf",
    "content": "variable \"force_destroy\" {\n  description = \"Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)\"\n  type        = bool\n  default     = false\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/s3/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/catalog/modules/s3/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/live/.auto.tfvars.example",
    "content": "# Example configuration file - copy to terraform.tfvars and update values\n\n# Required: Name used for all resources (must be unique)\nname = \"best-cat-2025-07-31-01\"\n\n# Required: Path to your Lambda function zip file\nlambda_zip_file = \"../../../dist/best-cat.zip\"\n\n# Optional: Force destroy S3 buckets even when they have objects in them.\n# You're generally advised not to do this with important infrastructure,\n# however this makes testing and cleanup easier for this guide.\nforce_destroy = true\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/live/backend.tf",
    "content": "terraform {\n  backend \"s3\" {\n    bucket       = \"terragrunt-to-terralith-tfstate-2025-09-24-2359\"\n    key          = \"tofu.tfstate\"\n    region       = \"us-east-1\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/live/main.tf",
    "content": "module \"s3\" {\n  source = \"../catalog/modules/s3\"\n\n  name = var.name\n\n  force_destroy = var.force_destroy\n}\n\nmodule \"ddb\" {\n  source = \"../catalog/modules/ddb\"\n\n  name = var.name\n}\n\nmodule \"iam\" {\n  source = \"../catalog/modules/iam\"\n\n  name = var.name\n\n  aws_region = var.aws_region\n\n  s3_bucket_arn      = module.s3.arn\n  dynamodb_table_arn = module.ddb.arn\n}\n\nmodule \"lambda\" {\n  source = \"../catalog/modules/lambda\"\n\n  name = var.name\n\n  aws_region = var.aws_region\n\n  s3_bucket_name      = module.s3.name\n  dynamodb_table_name = module.ddb.name\n  lambda_zip_file     = var.lambda_zip_file\n  lambda_role_arn     = module.iam.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/live/moved.tf",
    "content": "moved {\n  from = aws_dynamodb_table.asset_metadata\n  to   = module.ddb.aws_dynamodb_table.asset_metadata\n}\n\nmoved {\n  from = aws_iam_policy.lambda_basic_execution\n  to   = module.iam.aws_iam_policy.lambda_basic_execution\n}\n\nmoved {\n  from = aws_iam_policy.lambda_dynamodb\n  to   = module.iam.aws_iam_policy.lambda_dynamodb\n}\n\nmoved {\n  from = aws_iam_policy.lambda_s3_read\n  to   = module.iam.aws_iam_policy.lambda_s3_read\n}\n\nmoved {\n  from = aws_iam_role.lambda_role\n  to   = module.iam.aws_iam_role.lambda_role\n}\n\nmoved {\n  from = aws_iam_role_policy_attachment.lambda_basic_execution\n  to   = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n}\n\nmoved {\n  from = aws_iam_role_policy_attachment.lambda_dynamodb\n  to   = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n}\n\nmoved {\n  from = aws_iam_role_policy_attachment.lambda_s3_read\n  to   = module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n}\n\nmoved {\n  from = aws_lambda_function.main\n  to   = module.lambda.aws_lambda_function.main\n}\n\nmoved {\n  from = aws_lambda_function_url.main\n  to   = module.lambda.aws_lambda_function_url.main\n}\n\nmoved {\n  from = aws_s3_bucket.static_assets\n  to   = module.s3.aws_s3_bucket.static_assets\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/live/outputs.tf",
    "content": "output \"lambda_function_url\" {\n  description = \"URL of the Lambda function\"\n  value       = module.lambda.url\n}\n\noutput \"lambda_function_name\" {\n  description = \"Name of the Lambda function\"\n  value       = module.lambda.name\n}\n\noutput \"s3_bucket_name\" {\n  description = \"Name of the S3 bucket for static assets\"\n  value       = module.s3.name\n}\n\noutput \"s3_bucket_arn\" {\n  description = \"ARN of the S3 bucket for static assets\"\n  value       = module.s3.arn\n}\n\noutput \"dynamodb_table_name\" {\n  description = \"Name of the DynamoDB table for asset metadata\"\n  value       = module.ddb.name\n}\n\noutput \"dynamodb_table_arn\" {\n  description = \"ARN of the DynamoDB table for asset metadata\"\n  value       = module.ddb.arn\n}\n\noutput \"lambda_role_arn\" {\n  description = \"ARN of the Lambda execution role\"\n  value       = module.iam.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/live/providers.tf",
    "content": "provider \"aws\" {\n  region = var.aws_region\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/live/vars-optional.tf",
    "content": "variable \"aws_region\" {\n  description = \"AWS region for all resources\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"force_destroy\" {\n  description = \"Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)\"\n  type        = bool\n  default     = false\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/live/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n\nvariable \"lambda_zip_file\" {\n  description = \"Path to the Lambda function zip file\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-2-refactoring/live/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/best_cat/main.tf",
    "content": "module \"s3\" {\n  source = \"../s3\"\n\n  name = var.name\n\n  force_destroy = var.force_destroy\n}\n\nmodule \"ddb\" {\n  source = \"../ddb\"\n\n  name = var.name\n}\n\nmodule \"iam\" {\n  source = \"../iam\"\n\n  name = var.name\n\n  aws_region = var.aws_region\n\n  s3_bucket_arn      = module.s3.arn\n  dynamodb_table_arn = module.ddb.arn\n}\n\nmodule \"lambda\" {\n  source = \"../lambda\"\n\n  name = var.name\n\n  aws_region = var.aws_region\n\n  s3_bucket_name      = module.s3.name\n  dynamodb_table_name = module.ddb.name\n  lambda_zip_file     = var.lambda_zip_file\n  lambda_role_arn     = module.iam.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/best_cat/outputs.tf",
    "content": "output \"lambda_function_url\" {\n  description = \"URL of the Lambda function\"\n  value       = module.lambda.url\n}\n\noutput \"lambda_function_name\" {\n  description = \"Name of the Lambda function\"\n  value       = module.lambda.name\n}\n\noutput \"s3_bucket_name\" {\n  description = \"Name of the S3 bucket for static assets\"\n  value       = module.s3.name\n}\n\noutput \"s3_bucket_arn\" {\n  description = \"ARN of the S3 bucket for static assets\"\n  value       = module.s3.arn\n}\n\noutput \"dynamodb_table_name\" {\n  description = \"Name of the DynamoDB table for asset metadata\"\n  value       = module.ddb.name\n}\n\noutput \"dynamodb_table_arn\" {\n  description = \"ARN of the DynamoDB table for asset metadata\"\n  value       = module.ddb.arn\n}\n\noutput \"lambda_role_arn\" {\n  description = \"ARN of the Lambda execution role\"\n  value       = module.iam.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/best_cat/vars-optional.tf",
    "content": "variable \"aws_region\" {\n  description = \"AWS region for all resources\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"force_destroy\" {\n  description = \"Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)\"\n  type        = bool\n  default     = false\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/best_cat/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n\nvariable \"lambda_zip_file\" {\n  description = \"Path to the Lambda function zip file\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/ddb/main.tf",
    "content": "resource \"aws_dynamodb_table\" \"asset_metadata\" {\n  name         = \"${var.name}-asset-metadata\"\n  billing_mode = \"PAY_PER_REQUEST\"\n  hash_key     = \"image_id\"\n\n  attribute {\n    name = \"image_id\"\n    type = \"S\"\n  }\n\n  tags = {\n    Name = \"${var.name}-asset-metadata\"\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/ddb/outputs.tf",
    "content": "output \"name\" {\n  value = aws_dynamodb_table.asset_metadata.name\n}\n\noutput \"arn\" {\n  value = aws_dynamodb_table.asset_metadata.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/ddb/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"The name of the DynamoDB table\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/ddb/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/iam/data.tf",
    "content": "data \"aws_caller_identity\" \"current\" {}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/iam/main.tf",
    "content": "resource \"aws_iam_role\" \"lambda_role\" {\n  name = \"${var.name}-lambda-role\"\n\n  assume_role_policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Action = \"sts:AssumeRole\"\n        Effect = \"Allow\"\n        Principal = {\n          Service = \"lambda.amazonaws.com\"\n        }\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_s3_read\" {\n  name        = \"${var.name}-lambda-s3-read\"\n  description = \"Policy for Lambda to read from S3 bucket\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"s3:GetObject\",\n          \"s3:ListBucket\"\n        ]\n        Resource = [\n          var.s3_bucket_arn,\n          \"${var.s3_bucket_arn}/*\"\n        ]\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_dynamodb\" {\n  name        = \"${var.name}-lambda-dynamodb\"\n  description = \"Policy for Lambda to read/write to DynamoDB table\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"dynamodb:GetItem\",\n          \"dynamodb:PutItem\",\n          \"dynamodb:UpdateItem\",\n          \"dynamodb:DeleteItem\",\n          \"dynamodb:Query\",\n          \"dynamodb:Scan\"\n        ]\n        Resource = var.dynamodb_table_arn\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_basic_execution\" {\n  name        = \"${var.name}-lambda-basic-execution\"\n  description = \"Policy for Lambda basic execution (CloudWatch logs)\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"logs:CreateLogGroup\",\n          \"logs:CreateLogStream\",\n          \"logs:PutLogEvents\"\n        ]\n        Resource = \"arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:*\"\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_s3_read\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_s3_read.arn\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_dynamodb\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_dynamodb.arn\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_basic_execution\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_basic_execution.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/iam/outputs.tf",
    "content": "output \"name\" {\n  value = aws_iam_role.lambda_role.name\n}\n\noutput \"arn\" {\n  value = aws_iam_role.lambda_role.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/iam/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"The name of the IAM role\"\n  type        = string\n}\n\nvariable \"aws_region\" {\n  description = \"The AWS region to deploy the resources to\"\n  type        = string\n}\n\nvariable \"s3_bucket_arn\" {\n  description = \"The ARN of the S3 bucket\"\n  type        = string\n}\n\nvariable \"dynamodb_table_arn\" {\n  description = \"The ARN of the DynamoDB table\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/iam/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/lambda/main.tf",
    "content": "resource \"aws_lambda_function\" \"main\" {\n  function_name = \"${var.name}-function\"\n\n  filename         = var.lambda_zip_file\n  source_code_hash = filebase64sha256(var.lambda_zip_file)\n\n  role = var.lambda_role_arn\n\n  handler       = var.lambda_handler\n  runtime       = var.lambda_runtime\n  timeout       = var.lambda_timeout\n  memory_size   = var.lambda_memory_size\n  architectures = var.lambda_architectures\n\n  environment {\n    variables = {\n      S3_BUCKET_NAME      = var.s3_bucket_name\n      DYNAMODB_TABLE_NAME = var.dynamodb_table_name\n    }\n  }\n}\n\nresource \"aws_lambda_function_url\" \"main\" {\n  function_name      = aws_lambda_function.main.function_name\n  authorization_type = \"NONE\"\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/lambda/outputs.tf",
    "content": "output \"name\" {\n  value = aws_lambda_function.main.function_name\n}\n\noutput \"arn\" {\n  value = aws_lambda_function.main.arn\n}\n\noutput \"url\" {\n  value = aws_lambda_function_url.main.function_url\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/lambda/vars-optional.tf",
    "content": "variable \"lambda_runtime\" {\n  description = \"Lambda function runtime\"\n  type        = string\n  default     = \"nodejs22.x\"\n}\n\nvariable \"lambda_handler\" {\n  description = \"Lambda function handler\"\n  type        = string\n  default     = \"index.handler\"\n}\n\nvariable \"lambda_timeout\" {\n  description = \"Lambda function timeout in seconds\"\n  type        = number\n  default     = 30\n}\n\nvariable \"lambda_memory_size\" {\n  description = \"Lambda function memory size in MB\"\n  type        = number\n  default     = 128\n}\n\nvariable \"lambda_architectures\" {\n  description = \"Lambda function architectures\"\n  type        = list(string)\n  default     = [\"arm64\"]\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/lambda/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n\nvariable \"aws_region\" {\n  description = \"AWS region to deploy the resources to\"\n  type        = string\n}\n\nvariable \"lambda_zip_file\" {\n  description = \"Path to the Lambda function zip file\"\n  type        = string\n}\n\nvariable \"lambda_role_arn\" {\n  description = \"Lambda function role ARN\"\n  type        = string\n}\n\nvariable \"s3_bucket_name\" {\n  description = \"S3 bucket name\"\n  type        = string\n}\n\nvariable \"dynamodb_table_name\" {\n  description = \"DynamoDB table name\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/lambda/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/s3/main.tf",
    "content": "resource \"aws_s3_bucket\" \"static_assets\" {\n  bucket = \"${var.name}-static-assets\"\n\n  force_destroy = var.force_destroy\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/s3/outputs.tf",
    "content": "output \"name\" {\n  value = aws_s3_bucket.static_assets.bucket\n}\n\noutput \"arn\" {\n  value = aws_s3_bucket.static_assets.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/s3/vars-optional.tf",
    "content": "variable \"force_destroy\" {\n  description = \"Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)\"\n  type        = bool\n  default     = false\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/s3/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/catalog/modules/s3/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/live/.auto.tfvars.example",
    "content": "# Example configuration file - copy to terraform.tfvars and update values\n\n# Required: Name used for all resources (must be unique)\nname = \"best-cat-2025-07-31-01\"\n\n# Required: Path to your Lambda function zip file\nlambda_zip_file = \"../../../dist/best-cat.zip\"\n\n# Optional: Force destroy S3 buckets even when they have objects in them.\n# You're generally advised not to do this with important infrastructure,\n# however this makes testing and cleanup easier for this guide.\nforce_destroy = true\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/live/backend.tf",
    "content": "terraform {\n  backend \"s3\" {\n    bucket       = \"terragrunt-to-terralith-tfstate-2025-09-24-2359\"\n    key          = \"tofu.tfstate\"\n    region       = \"us-east-1\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/live/main.tf",
    "content": "module \"dev\" {\n  source = \"../catalog/modules/best_cat\"\n\n  name = \"${var.name}-dev\"\n\n  aws_region = var.aws_region\n\n  lambda_zip_file = var.lambda_zip_file\n  force_destroy   = var.force_destroy\n}\n\nmodule \"prod\" {\n  source = \"../catalog/modules/best_cat\"\n\n  name = var.name\n\n  aws_region = var.aws_region\n\n  lambda_zip_file = var.lambda_zip_file\n  force_destroy   = var.force_destroy\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/live/moved.tf",
    "content": "moved {\n  from = module.ddb.aws_dynamodb_table.asset_metadata\n  to   = module.prod.module.ddb.aws_dynamodb_table.asset_metadata\n}\n\nmoved {\n  from = module.iam.aws_iam_policy.lambda_basic_execution\n  to   = module.prod.module.iam.aws_iam_policy.lambda_basic_execution\n}\n\nmoved {\n  from = module.iam.aws_iam_policy.lambda_dynamodb\n  to   = module.prod.module.iam.aws_iam_policy.lambda_dynamodb\n}\n\nmoved {\n  from = module.iam.aws_iam_policy.lambda_s3_read\n  to   = module.prod.module.iam.aws_iam_policy.lambda_s3_read\n}\n\nmoved {\n  from = module.iam.aws_iam_role.lambda_role\n  to   = module.prod.module.iam.aws_iam_role.lambda_role\n}\n\nmoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n  to   = module.prod.module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n}\n\nmoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n  to   = module.prod.module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n}\n\nmoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n  to   = module.prod.module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n}\n\nmoved {\n  from = module.lambda.aws_lambda_function.main\n  to   = module.prod.module.lambda.aws_lambda_function.main\n}\n\nmoved {\n  from = module.lambda.aws_lambda_function_url.main\n  to   = module.prod.module.lambda.aws_lambda_function_url.main\n}\n\nmoved {\n  from = module.s3.aws_s3_bucket.static_assets\n  to   = module.prod.module.s3.aws_s3_bucket.static_assets\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/live/outputs.tf",
    "content": "output \"dev_lambda_function_url\" {\n  description = \"URL of the Lambda function\"\n  value       = module.dev.lambda_function_url\n}\n\noutput \"dev_s3_bucket_name\" {\n  description = \"Name of the S3 bucket for static assets\"\n  value       = module.dev.s3_bucket_name\n}\n\noutput \"prod_lambda_function_url\" {\n  description = \"URL of the Lambda function\"\n  value       = module.prod.lambda_function_url\n}\n\noutput \"prod_s3_bucket_name\" {\n  description = \"Name of the S3 bucket for static assets\"\n  value       = module.prod.s3_bucket_name\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/live/providers.tf",
    "content": "provider \"aws\" {\n  region = var.aws_region\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/live/vars-optional.tf",
    "content": "variable \"aws_region\" {\n  description = \"AWS region for all resources\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"force_destroy\" {\n  description = \"Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)\"\n  type        = bool\n  default     = false\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/live/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n\nvariable \"lambda_zip_file\" {\n  description = \"Path to the Lambda function zip file\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-3-adding-dev/live/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/best_cat/main.tf",
    "content": "module \"s3\" {\n  source = \"../s3\"\n\n  name = var.name\n\n  force_destroy = var.force_destroy\n}\n\nmodule \"ddb\" {\n  source = \"../ddb\"\n\n  name = var.name\n}\n\nmodule \"iam\" {\n  source = \"../iam\"\n\n  name = var.name\n\n  aws_region = var.aws_region\n\n  s3_bucket_arn      = module.s3.arn\n  dynamodb_table_arn = module.ddb.arn\n}\n\nmodule \"lambda\" {\n  source = \"../lambda\"\n\n  name = var.name\n\n  aws_region = var.aws_region\n\n  s3_bucket_name      = module.s3.name\n  dynamodb_table_name = module.ddb.name\n  lambda_zip_file     = var.lambda_zip_file\n  lambda_role_arn     = module.iam.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/best_cat/outputs.tf",
    "content": "output \"lambda_function_url\" {\n  description = \"URL of the Lambda function\"\n  value       = module.lambda.url\n}\n\noutput \"lambda_function_name\" {\n  description = \"Name of the Lambda function\"\n  value       = module.lambda.name\n}\n\noutput \"s3_bucket_name\" {\n  description = \"Name of the S3 bucket for static assets\"\n  value       = module.s3.name\n}\n\noutput \"s3_bucket_arn\" {\n  description = \"ARN of the S3 bucket for static assets\"\n  value       = module.s3.arn\n}\n\noutput \"dynamodb_table_name\" {\n  description = \"Name of the DynamoDB table for asset metadata\"\n  value       = module.ddb.name\n}\n\noutput \"dynamodb_table_arn\" {\n  description = \"ARN of the DynamoDB table for asset metadata\"\n  value       = module.ddb.arn\n}\n\noutput \"lambda_role_arn\" {\n  description = \"ARN of the Lambda execution role\"\n  value       = module.iam.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/best_cat/vars-optional.tf",
    "content": "variable \"aws_region\" {\n  description = \"AWS region for all resources\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"force_destroy\" {\n  description = \"Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)\"\n  type        = bool\n  default     = false\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/best_cat/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n\nvariable \"lambda_zip_file\" {\n  description = \"Path to the Lambda function zip file\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/ddb/main.tf",
    "content": "resource \"aws_dynamodb_table\" \"asset_metadata\" {\n  name         = \"${var.name}-asset-metadata\"\n  billing_mode = \"PAY_PER_REQUEST\"\n  hash_key     = \"image_id\"\n\n  attribute {\n    name = \"image_id\"\n    type = \"S\"\n  }\n\n  tags = {\n    Name = \"${var.name}-asset-metadata\"\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/ddb/outputs.tf",
    "content": "output \"name\" {\n  value = aws_dynamodb_table.asset_metadata.name\n}\n\noutput \"arn\" {\n  value = aws_dynamodb_table.asset_metadata.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/ddb/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"The name of the DynamoDB table\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/ddb/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/iam/data.tf",
    "content": "data \"aws_caller_identity\" \"current\" {}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/iam/main.tf",
    "content": "resource \"aws_iam_role\" \"lambda_role\" {\n  name = \"${var.name}-lambda-role\"\n\n  assume_role_policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Action = \"sts:AssumeRole\"\n        Effect = \"Allow\"\n        Principal = {\n          Service = \"lambda.amazonaws.com\"\n        }\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_s3_read\" {\n  name        = \"${var.name}-lambda-s3-read\"\n  description = \"Policy for Lambda to read from S3 bucket\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"s3:GetObject\",\n          \"s3:ListBucket\"\n        ]\n        Resource = [\n          var.s3_bucket_arn,\n          \"${var.s3_bucket_arn}/*\"\n        ]\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_dynamodb\" {\n  name        = \"${var.name}-lambda-dynamodb\"\n  description = \"Policy for Lambda to read/write to DynamoDB table\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"dynamodb:GetItem\",\n          \"dynamodb:PutItem\",\n          \"dynamodb:UpdateItem\",\n          \"dynamodb:DeleteItem\",\n          \"dynamodb:Query\",\n          \"dynamodb:Scan\"\n        ]\n        Resource = var.dynamodb_table_arn\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_basic_execution\" {\n  name        = \"${var.name}-lambda-basic-execution\"\n  description = \"Policy for Lambda basic execution (CloudWatch logs)\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"logs:CreateLogGroup\",\n          \"logs:CreateLogStream\",\n          \"logs:PutLogEvents\"\n        ]\n        Resource = \"arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:*\"\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_s3_read\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_s3_read.arn\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_dynamodb\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_dynamodb.arn\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_basic_execution\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_basic_execution.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/iam/outputs.tf",
    "content": "output \"name\" {\n  value = aws_iam_role.lambda_role.name\n}\n\noutput \"arn\" {\n  value = aws_iam_role.lambda_role.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/iam/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"The name of the IAM role\"\n  type        = string\n}\n\nvariable \"aws_region\" {\n  description = \"The AWS region to deploy the resources to\"\n  type        = string\n}\n\nvariable \"s3_bucket_arn\" {\n  description = \"The ARN of the S3 bucket\"\n  type        = string\n}\n\nvariable \"dynamodb_table_arn\" {\n  description = \"The ARN of the DynamoDB table\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/iam/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/lambda/main.tf",
    "content": "resource \"aws_lambda_function\" \"main\" {\n  function_name = \"${var.name}-function\"\n\n  filename         = var.lambda_zip_file\n  source_code_hash = filebase64sha256(var.lambda_zip_file)\n\n  role = var.lambda_role_arn\n\n  handler       = var.lambda_handler\n  runtime       = var.lambda_runtime\n  timeout       = var.lambda_timeout\n  memory_size   = var.lambda_memory_size\n  architectures = var.lambda_architectures\n\n  environment {\n    variables = {\n      S3_BUCKET_NAME      = var.s3_bucket_name\n      DYNAMODB_TABLE_NAME = var.dynamodb_table_name\n    }\n  }\n}\n\nresource \"aws_lambda_function_url\" \"main\" {\n  function_name      = aws_lambda_function.main.function_name\n  authorization_type = \"NONE\"\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/lambda/outputs.tf",
    "content": "output \"name\" {\n  value = aws_lambda_function.main.function_name\n}\n\noutput \"arn\" {\n  value = aws_lambda_function.main.arn\n}\n\noutput \"url\" {\n  value = aws_lambda_function_url.main.function_url\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/lambda/vars-optional.tf",
    "content": "variable \"lambda_runtime\" {\n  description = \"Lambda function runtime\"\n  type        = string\n  default     = \"nodejs22.x\"\n}\n\nvariable \"lambda_handler\" {\n  description = \"Lambda function handler\"\n  type        = string\n  default     = \"index.handler\"\n}\n\nvariable \"lambda_timeout\" {\n  description = \"Lambda function timeout in seconds\"\n  type        = number\n  default     = 30\n}\n\nvariable \"lambda_memory_size\" {\n  description = \"Lambda function memory size in MB\"\n  type        = number\n  default     = 128\n}\n\nvariable \"lambda_architectures\" {\n  description = \"Lambda function architectures\"\n  type        = list(string)\n  default     = [\"arm64\"]\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/lambda/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n\nvariable \"aws_region\" {\n  description = \"AWS region to deploy the resources to\"\n  type        = string\n}\n\nvariable \"lambda_zip_file\" {\n  description = \"Path to the Lambda function zip file\"\n  type        = string\n}\n\nvariable \"lambda_role_arn\" {\n  description = \"Lambda function role ARN\"\n  type        = string\n}\n\nvariable \"s3_bucket_name\" {\n  description = \"S3 bucket name\"\n  type        = string\n}\n\nvariable \"dynamodb_table_name\" {\n  description = \"DynamoDB table name\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/lambda/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/s3/main.tf",
    "content": "resource \"aws_s3_bucket\" \"static_assets\" {\n  bucket = \"${var.name}-static-assets\"\n\n  force_destroy = var.force_destroy\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/s3/outputs.tf",
    "content": "output \"name\" {\n  value = aws_s3_bucket.static_assets.bucket\n}\n\noutput \"arn\" {\n  value = aws_s3_bucket.static_assets.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/s3/vars-optional.tf",
    "content": "variable \"force_destroy\" {\n  description = \"Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)\"\n  type        = bool\n  default     = false\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/s3/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/catalog/modules/s3/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/dev/.auto.tfvars.example",
    "content": "# Example configuration file - copy to terraform.tfvars and update values\n\n# Required: Name used for all resources (must be unique)\nname = \"best-cat-2025-07-31-01-dev\"\n\n# Required: Path to your Lambda function zip file\nlambda_zip_file = \"../../../../dist/best-cat.zip\"\n\n# Optional: Force destroy S3 buckets even when they have objects in them.\n# You're generally advised not to do this with important infrastructure,\n# however this makes testing and cleanup easier for this guide.\nforce_destroy = true\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/dev/backend.tf",
    "content": "terraform {\n  backend \"s3\" {\n    bucket       = \"terragrunt-to-terralith-tfstate-2025-09-24-2359\"\n    key          = \"dev/tofu.tfstate\"\n    region       = \"us-east-1\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/dev/main.tf",
    "content": "module \"main\" {\n  source = \"../../catalog/modules/best_cat\"\n\n  name = var.name\n\n  aws_region = var.aws_region\n\n  lambda_zip_file = var.lambda_zip_file\n  force_destroy   = var.force_destroy\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/dev/moved.tf",
    "content": "moved {\n  from = module.dev.module.ddb.aws_dynamodb_table.asset_metadata\n  to   = module.main.module.ddb.aws_dynamodb_table.asset_metadata\n}\n\nmoved {\n  from = module.dev.module.iam.aws_iam_policy.lambda_basic_execution\n  to   = module.main.module.iam.aws_iam_policy.lambda_basic_execution\n}\n\nmoved {\n  from = module.dev.module.iam.aws_iam_policy.lambda_dynamodb\n  to   = module.main.module.iam.aws_iam_policy.lambda_dynamodb\n}\n\nmoved {\n  from = module.dev.module.iam.aws_iam_policy.lambda_s3_read\n  to   = module.main.module.iam.aws_iam_policy.lambda_s3_read\n}\n\nmoved {\n  from = module.dev.module.iam.aws_iam_role.lambda_role\n  to   = module.main.module.iam.aws_iam_role.lambda_role\n}\n\nmoved {\n  from = module.dev.module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n  to   = module.main.module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n}\n\nmoved {\n  from = module.dev.module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n  to   = module.main.module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n}\n\nmoved {\n  from = module.dev.module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n  to   = module.main.module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n}\n\nmoved {\n  from = module.dev.module.lambda.aws_lambda_function.main\n  to   = module.main.module.lambda.aws_lambda_function.main\n}\n\nmoved {\n  from = module.dev.module.lambda.aws_lambda_function_url.main\n  to   = module.main.module.lambda.aws_lambda_function_url.main\n}\n\nmoved {\n  from = module.dev.module.s3.aws_s3_bucket.static_assets\n  to   = module.main.module.s3.aws_s3_bucket.static_assets\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/dev/outputs.tf",
    "content": "output \"lambda_function_url\" {\n  description = \"URL of the Lambda function\"\n  value       = module.main.lambda_function_url\n}\n\noutput \"s3_bucket_name\" {\n  description = \"Name of the S3 bucket for static assets\"\n  value       = module.main.s3_bucket_name\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/dev/providers.tf",
    "content": "provider \"aws\" {\n  region = var.aws_region\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/dev/removed.tf",
    "content": "removed {\n  from = module.prod.module.s3.aws_s3_bucket.static_assets\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.prod.module.ddb.aws_dynamodb_table.asset_metadata\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.prod.module.iam.aws_iam_role.lambda_role\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.prod.module.iam.aws_iam_policy.lambda_s3_read\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.prod.module.iam.aws_iam_policy.lambda_dynamodb\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.prod.module.iam.aws_iam_policy.lambda_basic_execution\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.prod.module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.prod.module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.prod.module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.prod.module.lambda.aws_lambda_function.main\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.prod.module.lambda.aws_lambda_function_url.main\n  lifecycle {\n    destroy = false\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/dev/vars-optional.tf",
    "content": "variable \"aws_region\" {\n  description = \"AWS region for all resources\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"force_destroy\" {\n  description = \"Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)\"\n  type        = bool\n  default     = false\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/dev/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n\nvariable \"lambda_zip_file\" {\n  description = \"Path to the Lambda function zip file\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/dev/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/prod/.auto.tfvars.example",
    "content": "# Example configuration file - copy to terraform.tfvars and update values\n\n# Required: Name used for all resources (must be unique)\nname = \"best-cat-2025-07-31-01\"\n\n# Required: Path to your Lambda function zip file\nlambda_zip_file = \"../../../../dist/best-cat.zip\"\n\n# Optional: Force destroy S3 buckets even when they have objects in them.\n# You're generally advised not to do this with important infrastructure,\n# however this makes testing and cleanup easier for this guide.\nforce_destroy = true\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/prod/backend.tf",
    "content": "terraform {\n  backend \"s3\" {\n    bucket       = \"terragrunt-to-terralith-tfstate-2025-09-24-2359\"\n    key          = \"prod/tofu.tfstate\"\n    region       = \"us-east-1\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/prod/main.tf",
    "content": "module \"main\" {\n  source = \"../../catalog/modules/best_cat\"\n\n  name = var.name\n\n  aws_region = var.aws_region\n\n  lambda_zip_file = var.lambda_zip_file\n  force_destroy   = var.force_destroy\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/prod/moved.tf",
    "content": "moved {\n  from = module.prod.module.ddb.aws_dynamodb_table.asset_metadata\n  to   = module.main.module.ddb.aws_dynamodb_table.asset_metadata\n}\n\nmoved {\n  from = module.prod.module.iam.aws_iam_policy.lambda_basic_execution\n  to   = module.main.module.iam.aws_iam_policy.lambda_basic_execution\n}\n\nmoved {\n  from = module.prod.module.iam.aws_iam_policy.lambda_dynamodb\n  to   = module.main.module.iam.aws_iam_policy.lambda_dynamodb\n}\n\nmoved {\n  from = module.prod.module.iam.aws_iam_policy.lambda_s3_read\n  to   = module.main.module.iam.aws_iam_policy.lambda_s3_read\n}\n\nmoved {\n  from = module.prod.module.iam.aws_iam_role.lambda_role\n  to   = module.main.module.iam.aws_iam_role.lambda_role\n}\n\nmoved {\n  from = module.prod.module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n  to   = module.main.module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n}\n\nmoved {\n  from = module.prod.module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n  to   = module.main.module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n}\n\nmoved {\n  from = module.prod.module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n  to   = module.main.module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n}\n\nmoved {\n  from = module.prod.module.lambda.aws_lambda_function.main\n  to   = module.main.module.lambda.aws_lambda_function.main\n}\n\nmoved {\n  from = module.prod.module.lambda.aws_lambda_function_url.main\n  to   = module.main.module.lambda.aws_lambda_function_url.main\n}\n\nmoved {\n  from = module.prod.module.s3.aws_s3_bucket.static_assets\n  to   = module.main.module.s3.aws_s3_bucket.static_assets\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/prod/outputs.tf",
    "content": "output \"lambda_function_url\" {\n  description = \"URL of the Lambda function\"\n  value       = module.main.lambda_function_url\n}\n\noutput \"s3_bucket_name\" {\n  description = \"Name of the S3 bucket for static assets\"\n  value       = module.main.s3_bucket_name\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/prod/providers.tf",
    "content": "provider \"aws\" {\n  region = var.aws_region\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/prod/removed.tf",
    "content": "removed {\n  from = module.dev.module.s3.aws_s3_bucket.static_assets\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.dev.module.ddb.aws_dynamodb_table.asset_metadata\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.dev.module.iam.aws_iam_role.lambda_role\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.dev.module.iam.aws_iam_policy.lambda_s3_read\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.dev.module.iam.aws_iam_policy.lambda_dynamodb\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.dev.module.iam.aws_iam_policy.lambda_basic_execution\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.dev.module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.dev.module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.dev.module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.dev.module.lambda.aws_lambda_function.main\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.dev.module.lambda.aws_lambda_function_url.main\n  lifecycle {\n    destroy = false\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/prod/vars-optional.tf",
    "content": "variable \"aws_region\" {\n  description = \"AWS region for all resources\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"force_destroy\" {\n  description = \"Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)\"\n  type        = bool\n  default     = false\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/prod/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n\nvariable \"lambda_zip_file\" {\n  description = \"Path to the Lambda function zip file\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-4-breaking-the-terralith/live/prod/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/best_cat/main.tf",
    "content": "module \"s3\" {\n  source = \"../s3\"\n\n  name = var.name\n\n  force_destroy = var.force_destroy\n}\n\nmodule \"ddb\" {\n  source = \"../ddb\"\n\n  name = var.name\n}\n\nmodule \"iam\" {\n  source = \"../iam\"\n\n  name = var.name\n\n  aws_region = var.aws_region\n\n  s3_bucket_arn      = module.s3.arn\n  dynamodb_table_arn = module.ddb.arn\n}\n\nmodule \"lambda\" {\n  source = \"../lambda\"\n\n  name = var.name\n\n  aws_region = var.aws_region\n\n  s3_bucket_name      = module.s3.name\n  dynamodb_table_name = module.ddb.name\n  lambda_zip_file     = var.lambda_zip_file\n  lambda_role_arn     = module.iam.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/best_cat/outputs.tf",
    "content": "output \"lambda_function_url\" {\n  description = \"URL of the Lambda function\"\n  value       = module.lambda.url\n}\n\noutput \"lambda_function_name\" {\n  description = \"Name of the Lambda function\"\n  value       = module.lambda.name\n}\n\noutput \"s3_bucket_name\" {\n  description = \"Name of the S3 bucket for static assets\"\n  value       = module.s3.name\n}\n\noutput \"s3_bucket_arn\" {\n  description = \"ARN of the S3 bucket for static assets\"\n  value       = module.s3.arn\n}\n\noutput \"dynamodb_table_name\" {\n  description = \"Name of the DynamoDB table for asset metadata\"\n  value       = module.ddb.name\n}\n\noutput \"dynamodb_table_arn\" {\n  description = \"ARN of the DynamoDB table for asset metadata\"\n  value       = module.ddb.arn\n}\n\noutput \"lambda_role_arn\" {\n  description = \"ARN of the Lambda execution role\"\n  value       = module.iam.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/best_cat/vars-optional.tf",
    "content": "variable \"aws_region\" {\n  description = \"AWS region for all resources\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"force_destroy\" {\n  description = \"Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)\"\n  type        = bool\n  default     = false\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/best_cat/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n\nvariable \"lambda_zip_file\" {\n  description = \"Path to the Lambda function zip file\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/ddb/main.tf",
    "content": "resource \"aws_dynamodb_table\" \"asset_metadata\" {\n  name         = \"${var.name}-asset-metadata\"\n  billing_mode = \"PAY_PER_REQUEST\"\n  hash_key     = \"image_id\"\n\n  attribute {\n    name = \"image_id\"\n    type = \"S\"\n  }\n\n  tags = {\n    Name = \"${var.name}-asset-metadata\"\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/ddb/outputs.tf",
    "content": "output \"name\" {\n  value = aws_dynamodb_table.asset_metadata.name\n}\n\noutput \"arn\" {\n  value = aws_dynamodb_table.asset_metadata.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/ddb/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"The name of the DynamoDB table\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/ddb/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/iam/data.tf",
    "content": "data \"aws_caller_identity\" \"current\" {}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/iam/main.tf",
    "content": "resource \"aws_iam_role\" \"lambda_role\" {\n  name = \"${var.name}-lambda-role\"\n\n  assume_role_policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Action = \"sts:AssumeRole\"\n        Effect = \"Allow\"\n        Principal = {\n          Service = \"lambda.amazonaws.com\"\n        }\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_s3_read\" {\n  name        = \"${var.name}-lambda-s3-read\"\n  description = \"Policy for Lambda to read from S3 bucket\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"s3:GetObject\",\n          \"s3:ListBucket\"\n        ]\n        Resource = [\n          var.s3_bucket_arn,\n          \"${var.s3_bucket_arn}/*\"\n        ]\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_dynamodb\" {\n  name        = \"${var.name}-lambda-dynamodb\"\n  description = \"Policy for Lambda to read/write to DynamoDB table\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"dynamodb:GetItem\",\n          \"dynamodb:PutItem\",\n          \"dynamodb:UpdateItem\",\n          \"dynamodb:DeleteItem\",\n          \"dynamodb:Query\",\n          \"dynamodb:Scan\"\n        ]\n        Resource = var.dynamodb_table_arn\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_basic_execution\" {\n  name        = \"${var.name}-lambda-basic-execution\"\n  description = \"Policy for Lambda basic execution (CloudWatch logs)\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"logs:CreateLogGroup\",\n          \"logs:CreateLogStream\",\n          \"logs:PutLogEvents\"\n        ]\n        Resource = \"arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:*\"\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_s3_read\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_s3_read.arn\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_dynamodb\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_dynamodb.arn\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_basic_execution\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_basic_execution.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/iam/outputs.tf",
    "content": "output \"name\" {\n  value = aws_iam_role.lambda_role.name\n}\n\noutput \"arn\" {\n  value = aws_iam_role.lambda_role.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/iam/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"The name of the IAM role\"\n  type        = string\n}\n\nvariable \"aws_region\" {\n  description = \"The AWS region to deploy the resources to\"\n  type        = string\n}\n\nvariable \"s3_bucket_arn\" {\n  description = \"The ARN of the S3 bucket\"\n  type        = string\n}\n\nvariable \"dynamodb_table_arn\" {\n  description = \"The ARN of the DynamoDB table\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/iam/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/lambda/main.tf",
    "content": "resource \"aws_lambda_function\" \"main\" {\n  function_name = \"${var.name}-function\"\n\n  filename         = var.lambda_zip_file\n  source_code_hash = filebase64sha256(var.lambda_zip_file)\n\n  role = var.lambda_role_arn\n\n  handler       = var.lambda_handler\n  runtime       = var.lambda_runtime\n  timeout       = var.lambda_timeout\n  memory_size   = var.lambda_memory_size\n  architectures = var.lambda_architectures\n\n  environment {\n    variables = {\n      S3_BUCKET_NAME      = var.s3_bucket_name\n      DYNAMODB_TABLE_NAME = var.dynamodb_table_name\n    }\n  }\n}\n\nresource \"aws_lambda_function_url\" \"main\" {\n  function_name      = aws_lambda_function.main.function_name\n  authorization_type = \"NONE\"\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/lambda/outputs.tf",
    "content": "output \"name\" {\n  value = aws_lambda_function.main.function_name\n}\n\noutput \"arn\" {\n  value = aws_lambda_function.main.arn\n}\n\noutput \"url\" {\n  value = aws_lambda_function_url.main.function_url\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/lambda/vars-optional.tf",
    "content": "variable \"lambda_runtime\" {\n  description = \"Lambda function runtime\"\n  type        = string\n  default     = \"nodejs22.x\"\n}\n\nvariable \"lambda_handler\" {\n  description = \"Lambda function handler\"\n  type        = string\n  default     = \"index.handler\"\n}\n\nvariable \"lambda_timeout\" {\n  description = \"Lambda function timeout in seconds\"\n  type        = number\n  default     = 30\n}\n\nvariable \"lambda_memory_size\" {\n  description = \"Lambda function memory size in MB\"\n  type        = number\n  default     = 128\n}\n\nvariable \"lambda_architectures\" {\n  description = \"Lambda function architectures\"\n  type        = list(string)\n  default     = [\"arm64\"]\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/lambda/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n\nvariable \"aws_region\" {\n  description = \"AWS region to deploy the resources to\"\n  type        = string\n}\n\nvariable \"lambda_zip_file\" {\n  description = \"Path to the Lambda function zip file\"\n  type        = string\n}\n\nvariable \"lambda_role_arn\" {\n  description = \"Lambda function role ARN\"\n  type        = string\n}\n\nvariable \"s3_bucket_name\" {\n  description = \"S3 bucket name\"\n  type        = string\n}\n\nvariable \"dynamodb_table_name\" {\n  description = \"DynamoDB table name\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/lambda/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/s3/main.tf",
    "content": "resource \"aws_s3_bucket\" \"static_assets\" {\n  bucket = \"${var.name}-static-assets\"\n\n  force_destroy = var.force_destroy\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/s3/outputs.tf",
    "content": "output \"name\" {\n  value = aws_s3_bucket.static_assets.bucket\n}\n\noutput \"arn\" {\n  value = aws_s3_bucket.static_assets.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/s3/vars-optional.tf",
    "content": "variable \"force_destroy\" {\n  description = \"Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)\"\n  type        = bool\n  default     = false\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/s3/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/catalog/modules/s3/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/live/dev/moved.tf",
    "content": "moved {\n  from = module.main.module.ddb.aws_dynamodb_table.asset_metadata\n  to   = module.ddb.aws_dynamodb_table.asset_metadata\n}\n\nmoved {\n  from = module.main.module.iam.aws_iam_policy.lambda_basic_execution\n  to   = module.iam.aws_iam_policy.lambda_basic_execution\n}\n\nmoved {\n  from = module.main.module.iam.aws_iam_policy.lambda_dynamodb\n  to   = module.iam.aws_iam_policy.lambda_dynamodb\n}\n\nmoved {\n  from = module.main.module.iam.aws_iam_policy.lambda_s3_read\n  to   = module.iam.aws_iam_policy.lambda_s3_read\n}\n\nmoved {\n  from = module.main.module.iam.aws_iam_role.lambda_role\n  to   = module.iam.aws_iam_role.lambda_role\n}\n\nmoved {\n  from = module.main.module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n  to   = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n}\n\nmoved {\n  from = module.main.module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n  to   = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n}\n\nmoved {\n  from = module.main.module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n  to   = module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n}\n\nmoved {\n  from = module.main.module.lambda.aws_lambda_function.main\n  to   = module.lambda.aws_lambda_function.main\n}\n\nmoved {\n  from = module.main.module.lambda.aws_lambda_function_url.main\n  to   = module.lambda.aws_lambda_function_url.main\n}\n\nmoved {\n  from = module.main.module.s3.aws_s3_bucket.static_assets\n  to   = module.s3.aws_s3_bucket.static_assets\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/live/dev/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../../catalog/modules//best_cat\"\n}\n\ninputs = {\n  name = \"best-cat-2025-09-24-2359-dev\"\n\n  lambda_zip_file = \"${get_repo_root()}/dist/best-cat.zip\"\n\n  # Optional: Force destroy S3 buckets even when they have objects in them.\n  # You're generally advised not to do this with important infrastructure,\n  # however this makes testing and cleanup easier for this guide.\n  force_destroy = true\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/live/prod/moved.tf",
    "content": "moved {\n  from = module.main.module.ddb.aws_dynamodb_table.asset_metadata\n  to   = module.ddb.aws_dynamodb_table.asset_metadata\n}\n\nmoved {\n  from = module.main.module.iam.aws_iam_policy.lambda_basic_execution\n  to   = module.iam.aws_iam_policy.lambda_basic_execution\n}\n\nmoved {\n  from = module.main.module.iam.aws_iam_policy.lambda_dynamodb\n  to   = module.iam.aws_iam_policy.lambda_dynamodb\n}\n\nmoved {\n  from = module.main.module.iam.aws_iam_policy.lambda_s3_read\n  to   = module.iam.aws_iam_policy.lambda_s3_read\n}\n\nmoved {\n  from = module.main.module.iam.aws_iam_role.lambda_role\n  to   = module.iam.aws_iam_role.lambda_role\n}\n\nmoved {\n  from = module.main.module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n  to   = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n}\n\nmoved {\n  from = module.main.module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n  to   = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n}\n\nmoved {\n  from = module.main.module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n  to   = module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n}\n\nmoved {\n  from = module.main.module.lambda.aws_lambda_function.main\n  to   = module.lambda.aws_lambda_function.main\n}\n\nmoved {\n  from = module.main.module.lambda.aws_lambda_function_url.main\n  to   = module.lambda.aws_lambda_function_url.main\n}\n\nmoved {\n  from = module.main.module.s3.aws_s3_bucket.static_assets\n  to   = module.s3.aws_s3_bucket.static_assets\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/live/prod/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../../catalog/modules//best_cat\"\n}\n\ninputs = {\n  name = \"best-cat-2025-09-24-2359\"\n\n  lambda_zip_file = \"${get_repo_root()}/dist/best-cat.zip\"\n\n  # Optional: Force destroy S3 buckets even when they have objects in them.\n  # You're generally advised not to do this with important infrastructure,\n  # however this makes testing and cleanup easier for this guide.\n  force_destroy = true\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-5-adding-terragrunt/live/root.hcl",
    "content": "remote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    bucket       = \"terragrunt-to-terralith-tfstate-2025-09-24-2359\"\n    key          = \"${path_relative_to_include()}/tofu.tfstate\"\n    region       = \"us-east-1\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n\ngenerate \"providers\" {\n  path      = \"providers.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents  = <<EOF\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\nEOF\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/best_cat/main.tf",
    "content": "module \"s3\" {\n  source = \"../s3\"\n\n  name = var.name\n\n  force_destroy = var.force_destroy\n}\n\nmodule \"ddb\" {\n  source = \"../ddb\"\n\n  name = var.name\n}\n\nmodule \"iam\" {\n  source = \"../iam\"\n\n  name = var.name\n\n  aws_region = var.aws_region\n\n  s3_bucket_arn      = module.s3.arn\n  dynamodb_table_arn = module.ddb.arn\n}\n\nmodule \"lambda\" {\n  source = \"../lambda\"\n\n  name = var.name\n\n  aws_region = var.aws_region\n\n  s3_bucket_name      = module.s3.name\n  dynamodb_table_name = module.ddb.name\n  lambda_zip_file     = var.lambda_zip_file\n  lambda_role_arn     = module.iam.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/best_cat/outputs.tf",
    "content": "output \"lambda_function_url\" {\n  description = \"URL of the Lambda function\"\n  value       = module.lambda.url\n}\n\noutput \"lambda_function_name\" {\n  description = \"Name of the Lambda function\"\n  value       = module.lambda.name\n}\n\noutput \"s3_bucket_name\" {\n  description = \"Name of the S3 bucket for static assets\"\n  value       = module.s3.name\n}\n\noutput \"s3_bucket_arn\" {\n  description = \"ARN of the S3 bucket for static assets\"\n  value       = module.s3.arn\n}\n\noutput \"dynamodb_table_name\" {\n  description = \"Name of the DynamoDB table for asset metadata\"\n  value       = module.ddb.name\n}\n\noutput \"dynamodb_table_arn\" {\n  description = \"ARN of the DynamoDB table for asset metadata\"\n  value       = module.ddb.arn\n}\n\noutput \"lambda_role_arn\" {\n  description = \"ARN of the Lambda execution role\"\n  value       = module.iam.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/best_cat/vars-optional.tf",
    "content": "variable \"aws_region\" {\n  description = \"AWS region for all resources\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"force_destroy\" {\n  description = \"Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)\"\n  type        = bool\n  default     = false\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/best_cat/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n\nvariable \"lambda_zip_file\" {\n  description = \"Path to the Lambda function zip file\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/ddb/main.tf",
    "content": "resource \"aws_dynamodb_table\" \"asset_metadata\" {\n  name         = \"${var.name}-asset-metadata\"\n  billing_mode = \"PAY_PER_REQUEST\"\n  hash_key     = \"image_id\"\n\n  attribute {\n    name = \"image_id\"\n    type = \"S\"\n  }\n\n  tags = {\n    Name = \"${var.name}-asset-metadata\"\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/ddb/outputs.tf",
    "content": "output \"name\" {\n  value = aws_dynamodb_table.asset_metadata.name\n}\n\noutput \"arn\" {\n  value = aws_dynamodb_table.asset_metadata.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/ddb/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"The name of the DynamoDB table\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/ddb/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/iam/data.tf",
    "content": "data \"aws_caller_identity\" \"current\" {}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/iam/main.tf",
    "content": "resource \"aws_iam_role\" \"lambda_role\" {\n  name = \"${var.name}-lambda-role\"\n\n  assume_role_policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Action = \"sts:AssumeRole\"\n        Effect = \"Allow\"\n        Principal = {\n          Service = \"lambda.amazonaws.com\"\n        }\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_s3_read\" {\n  name        = \"${var.name}-lambda-s3-read\"\n  description = \"Policy for Lambda to read from S3 bucket\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"s3:GetObject\",\n          \"s3:ListBucket\"\n        ]\n        Resource = [\n          var.s3_bucket_arn,\n          \"${var.s3_bucket_arn}/*\"\n        ]\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_dynamodb\" {\n  name        = \"${var.name}-lambda-dynamodb\"\n  description = \"Policy for Lambda to read/write to DynamoDB table\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"dynamodb:GetItem\",\n          \"dynamodb:PutItem\",\n          \"dynamodb:UpdateItem\",\n          \"dynamodb:DeleteItem\",\n          \"dynamodb:Query\",\n          \"dynamodb:Scan\"\n        ]\n        Resource = var.dynamodb_table_arn\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_basic_execution\" {\n  name        = \"${var.name}-lambda-basic-execution\"\n  description = \"Policy for Lambda basic execution (CloudWatch logs)\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"logs:CreateLogGroup\",\n          \"logs:CreateLogStream\",\n          \"logs:PutLogEvents\"\n        ]\n        Resource = \"arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:*\"\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_s3_read\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_s3_read.arn\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_dynamodb\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_dynamodb.arn\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_basic_execution\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_basic_execution.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/iam/outputs.tf",
    "content": "output \"name\" {\n  value = aws_iam_role.lambda_role.name\n}\n\noutput \"arn\" {\n  value = aws_iam_role.lambda_role.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/iam/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"The name of the IAM role\"\n  type        = string\n}\n\nvariable \"aws_region\" {\n  description = \"The AWS region to deploy the resources to\"\n  type        = string\n}\n\nvariable \"s3_bucket_arn\" {\n  description = \"The ARN of the S3 bucket\"\n  type        = string\n}\n\nvariable \"dynamodb_table_arn\" {\n  description = \"The ARN of the DynamoDB table\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/iam/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/lambda/main.tf",
    "content": "resource \"aws_lambda_function\" \"main\" {\n  function_name = \"${var.name}-function\"\n\n  filename         = var.lambda_zip_file\n  source_code_hash = filebase64sha256(var.lambda_zip_file)\n\n  role = var.lambda_role_arn\n\n  handler       = var.lambda_handler\n  runtime       = var.lambda_runtime\n  timeout       = var.lambda_timeout\n  memory_size   = var.lambda_memory_size\n  architectures = var.lambda_architectures\n\n  environment {\n    variables = {\n      S3_BUCKET_NAME      = var.s3_bucket_name\n      DYNAMODB_TABLE_NAME = var.dynamodb_table_name\n    }\n  }\n}\n\nresource \"aws_lambda_function_url\" \"main\" {\n  function_name      = aws_lambda_function.main.function_name\n  authorization_type = \"NONE\"\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/lambda/outputs.tf",
    "content": "output \"name\" {\n  value = aws_lambda_function.main.function_name\n}\n\noutput \"arn\" {\n  value = aws_lambda_function.main.arn\n}\n\noutput \"url\" {\n  value = aws_lambda_function_url.main.function_url\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/lambda/vars-optional.tf",
    "content": "variable \"lambda_runtime\" {\n  description = \"Lambda function runtime\"\n  type        = string\n  default     = \"nodejs22.x\"\n}\n\nvariable \"lambda_handler\" {\n  description = \"Lambda function handler\"\n  type        = string\n  default     = \"index.handler\"\n}\n\nvariable \"lambda_timeout\" {\n  description = \"Lambda function timeout in seconds\"\n  type        = number\n  default     = 30\n}\n\nvariable \"lambda_memory_size\" {\n  description = \"Lambda function memory size in MB\"\n  type        = number\n  default     = 128\n}\n\nvariable \"lambda_architectures\" {\n  description = \"Lambda function architectures\"\n  type        = list(string)\n  default     = [\"arm64\"]\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/lambda/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n\nvariable \"aws_region\" {\n  description = \"AWS region to deploy the resources to\"\n  type        = string\n}\n\nvariable \"lambda_zip_file\" {\n  description = \"Path to the Lambda function zip file\"\n  type        = string\n}\n\nvariable \"lambda_role_arn\" {\n  description = \"Lambda function role ARN\"\n  type        = string\n}\n\nvariable \"s3_bucket_name\" {\n  description = \"S3 bucket name\"\n  type        = string\n}\n\nvariable \"dynamodb_table_name\" {\n  description = \"DynamoDB table name\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/lambda/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/s3/main.tf",
    "content": "resource \"aws_s3_bucket\" \"static_assets\" {\n  bucket = \"${var.name}-static-assets\"\n\n  force_destroy = var.force_destroy\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/s3/outputs.tf",
    "content": "output \"name\" {\n  value = aws_s3_bucket.static_assets.bucket\n}\n\noutput \"arn\" {\n  value = aws_s3_bucket.static_assets.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/s3/vars-optional.tf",
    "content": "variable \"force_destroy\" {\n  description = \"Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)\"\n  type        = bool\n  default     = false\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/s3/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/catalog/modules/s3/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/ddb/moved.tf",
    "content": "moved {\n  from = module.ddb.aws_dynamodb_table.asset_metadata\n  to   = aws_dynamodb_table.asset_metadata\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/ddb/removed.tf",
    "content": "removed {\n  from = module.s3.aws_s3_bucket.static_assets\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role.lambda_role\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_policy.lambda_s3_read\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_policy.lambda_dynamodb\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_policy.lambda_basic_execution\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.lambda.aws_lambda_function.main\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.lambda.aws_lambda_function_url.main\n  lifecycle {\n    destroy = false\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/ddb/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${find_in_parent_folders(\"catalog/modules\")}//ddb\"\n}\n\ninputs = {\n  name = \"best-cat-2025-09-24-2359-dev\"\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/iam/moved.tf",
    "content": "moved {\n  from = module.iam.aws_iam_policy.lambda_basic_execution\n  to   = aws_iam_policy.lambda_basic_execution\n}\n\nmoved {\n  from = module.iam.aws_iam_policy.lambda_dynamodb\n  to   = aws_iam_policy.lambda_dynamodb\n}\n\nmoved {\n  from = module.iam.aws_iam_policy.lambda_s3_read\n  to   = aws_iam_policy.lambda_s3_read\n}\n\nmoved {\n  from = module.iam.aws_iam_role.lambda_role\n  to   = aws_iam_role.lambda_role\n}\n\nmoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n  to   = aws_iam_role_policy_attachment.lambda_basic_execution\n}\n\nmoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n  to   = aws_iam_role_policy_attachment.lambda_dynamodb\n}\n\nmoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n  to   = aws_iam_role_policy_attachment.lambda_s3_read\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/iam/removed.tf",
    "content": "removed {\n  from = module.s3.aws_s3_bucket.static_assets\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.ddb.aws_dynamodb_table.asset_metadata\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.lambda.aws_lambda_function.main\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.lambda.aws_lambda_function_url.main\n  lifecycle {\n    destroy = false\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/iam/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${find_in_parent_folders(\"catalog/modules\")}//iam\"\n}\n\ndependency \"s3\" {\n  config_path = \"../s3\"\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    arn = \"arn:aws:s3:::mock-bucket-name\"\n  }\n}\n\ndependency \"ddb\" {\n  config_path = \"../ddb\"\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    arn = \"arn:aws:dynamodb:us-east-1:123456789012:table/mock-table-name\"\n  }\n}\n\ninputs = {\n  name = \"best-cat-2025-09-24-2359-dev\"\n\n  aws_region = \"us-east-1\"\n\n  s3_bucket_arn      = dependency.s3.outputs.arn\n  dynamodb_table_arn = dependency.ddb.outputs.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/lambda/moved.tf",
    "content": "moved {\n  from = module.lambda.aws_lambda_function.main\n  to   = aws_lambda_function.main\n}\n\nmoved {\n  from = module.lambda.aws_lambda_function_url.main\n  to   = aws_lambda_function_url.main\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/lambda/removed.tf",
    "content": "removed {\n  from = module.s3.aws_s3_bucket.static_assets\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.ddb.aws_dynamodb_table.asset_metadata\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role.lambda_role\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_policy.lambda_s3_read\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_policy.lambda_dynamodb\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_policy.lambda_basic_execution\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n  lifecycle {\n    destroy = false\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/lambda/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${find_in_parent_folders(\"catalog/modules\")}//lambda\"\n}\n\ndependency \"s3\" {\n  config_path = \"../s3\"\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    name = \"mock-bucket-name\"\n  }\n}\n\ndependency \"ddb\" {\n  config_path = \"../ddb\"\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    name = \"mock-table-name\"\n  }\n}\n\ndependency \"iam\" {\n  config_path = \"../iam\"\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    arn = \"arn:aws:iam::123456789012:role/mock-role-name\"\n  }\n}\n\ninputs = {\n  name = \"best-cat-2025-09-24-2359-dev\"\n\n  aws_region = \"us-east-1\"\n\n  s3_bucket_name      = dependency.s3.outputs.name\n  dynamodb_table_name = dependency.ddb.outputs.name\n  lambda_role_arn     = dependency.iam.outputs.arn\n\n  lambda_zip_file     = \"${get_repo_root()}/dist/best-cat.zip\"\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/s3/moved.tf",
    "content": "moved {\n  from = module.s3.aws_s3_bucket.static_assets\n  to   = aws_s3_bucket.static_assets\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/s3/removed.tf",
    "content": "removed {\n  from = module.ddb.aws_dynamodb_table.asset_metadata\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role.lambda_role\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_policy.lambda_s3_read\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_policy.lambda_dynamodb\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_policy.lambda_basic_execution\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.lambda.aws_lambda_function.main\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.lambda.aws_lambda_function_url.main\n  lifecycle {\n    destroy = false\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/dev/s3/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${find_in_parent_folders(\"catalog/modules\")}//s3\"\n}\n\ninputs = {\n  name = \"best-cat-2025-09-24-2359-dev\"\n\n  # Optional: Force destroy S3 buckets even when they have objects in them.\n  # You're generally advised not to do this with important infrastructure,\n  # however this makes testing and cleanup easier for this guide.\n  force_destroy = true\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/ddb/moved.tf",
    "content": "moved {\n  from = module.ddb.aws_dynamodb_table.asset_metadata\n  to   = aws_dynamodb_table.asset_metadata\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/ddb/removed.tf",
    "content": "removed {\n  from = module.s3.aws_s3_bucket.static_assets\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role.lambda_role\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_policy.lambda_s3_read\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_policy.lambda_dynamodb\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_policy.lambda_basic_execution\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.lambda.aws_lambda_function.main\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.lambda.aws_lambda_function_url.main\n  lifecycle {\n    destroy = false\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/ddb/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${find_in_parent_folders(\"catalog/modules\")}//ddb\"\n}\n\ninputs = {\n  name = \"best-cat-2025-09-24-2359\"\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/iam/moved.tf",
    "content": "moved {\n  from = module.iam.aws_iam_policy.lambda_basic_execution\n  to   = aws_iam_policy.lambda_basic_execution\n}\n\nmoved {\n  from = module.iam.aws_iam_policy.lambda_dynamodb\n  to   = aws_iam_policy.lambda_dynamodb\n}\n\nmoved {\n  from = module.iam.aws_iam_policy.lambda_s3_read\n  to   = aws_iam_policy.lambda_s3_read\n}\n\nmoved {\n  from = module.iam.aws_iam_role.lambda_role\n  to   = aws_iam_role.lambda_role\n}\n\nmoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n  to   = aws_iam_role_policy_attachment.lambda_basic_execution\n}\n\nmoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n  to   = aws_iam_role_policy_attachment.lambda_dynamodb\n}\n\nmoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n  to   = aws_iam_role_policy_attachment.lambda_s3_read\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/iam/removed.tf",
    "content": "removed {\n  from = module.s3.aws_s3_bucket.static_assets\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.ddb.aws_dynamodb_table.asset_metadata\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.lambda.aws_lambda_function.main\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.lambda.aws_lambda_function_url.main\n  lifecycle {\n    destroy = false\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/iam/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${find_in_parent_folders(\"catalog/modules\")}//iam\"\n}\n\ndependency \"s3\" {\n  config_path = \"../s3\"\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    arn = \"arn:aws:s3:::mock-bucket-name\"\n  }\n}\n\ndependency \"ddb\" {\n  config_path = \"../ddb\"\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    arn = \"arn:aws:dynamodb:us-east-1:123456789012:table/mock-table-name\"\n  }\n}\n\ninputs = {\n  name = \"best-cat-2025-09-24-2359\"\n\n  aws_region = \"us-east-1\"\n\n  s3_bucket_arn      = dependency.s3.outputs.arn\n  dynamodb_table_arn = dependency.ddb.outputs.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/lambda/moved.tf",
    "content": "moved {\n  from = module.lambda.aws_lambda_function.main\n  to   = aws_lambda_function.main\n}\n\nmoved {\n  from = module.lambda.aws_lambda_function_url.main\n  to   = aws_lambda_function_url.main\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/lambda/removed.tf",
    "content": "removed {\n  from = module.s3.aws_s3_bucket.static_assets\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.ddb.aws_dynamodb_table.asset_metadata\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role.lambda_role\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_policy.lambda_s3_read\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_policy.lambda_dynamodb\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_policy.lambda_basic_execution\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n  lifecycle {\n    destroy = false\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/lambda/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${find_in_parent_folders(\"catalog/modules\")}//lambda\"\n}\n\ndependency \"s3\" {\n  config_path = \"../s3\"\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    name = \"mock-bucket-name\"\n  }\n}\n\ndependency \"ddb\" {\n  config_path = \"../ddb\"\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    name = \"mock-table-name\"\n  }\n}\n\ndependency \"iam\" {\n  config_path = \"../iam\"\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    arn = \"arn:aws:iam::123456789012:role/mock-role-name\"\n  }\n}\n\ninputs = {\n  name = \"best-cat-2025-09-24-2359\"\n\n  aws_region = \"us-east-1\"\n\n  s3_bucket_name      = dependency.s3.outputs.name\n  dynamodb_table_name = dependency.ddb.outputs.name\n  lambda_role_arn     = dependency.iam.outputs.arn\n\n  lambda_zip_file     = \"${get_repo_root()}/dist/best-cat.zip\"\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/s3/moved.tf",
    "content": "moved {\n  from = module.s3.aws_s3_bucket.static_assets\n  to   = aws_s3_bucket.static_assets\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/s3/removed.tf",
    "content": "removed {\n  from = module.ddb.aws_dynamodb_table.asset_metadata\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role.lambda_role\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_policy.lambda_s3_read\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_policy.lambda_dynamodb\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_policy.lambda_basic_execution\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_s3_read\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_dynamodb\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.iam.aws_iam_role_policy_attachment.lambda_basic_execution\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.lambda.aws_lambda_function.main\n  lifecycle {\n    destroy = false\n  }\n}\n\nremoved {\n  from = module.lambda.aws_lambda_function_url.main\n  lifecycle {\n    destroy = false\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/prod/s3/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${find_in_parent_folders(\"catalog/modules\")}//s3\"\n}\n\ninputs = {\n  name = \"best-cat-2025-09-24-2359\"\n\n  # Optional: Force destroy S3 buckets even when they have objects in them.\n  # You're generally advised not to do this with important infrastructure,\n  # however this makes testing and cleanup easier for this guide.\n  force_destroy = true\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-6-breaking-the-terralith-further/live/root.hcl",
    "content": "remote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    bucket       = \"terragrunt-to-terralith-tfstate-2025-09-24-2359\"\n    key          = \"${path_relative_to_include()}/tofu.tfstate\"\n    region       = \"us-east-1\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n\ngenerate \"providers\" {\n  path      = \"providers.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents  = <<EOF\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\nEOF\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/best_cat/main.tf",
    "content": "module \"s3\" {\n  source = \"../s3\"\n\n  name = var.name\n\n  force_destroy = var.force_destroy\n}\n\nmodule \"ddb\" {\n  source = \"../ddb\"\n\n  name = var.name\n}\n\nmodule \"iam\" {\n  source = \"../iam\"\n\n  name = var.name\n\n  aws_region = var.aws_region\n\n  s3_bucket_arn      = module.s3.arn\n  dynamodb_table_arn = module.ddb.arn\n}\n\nmodule \"lambda\" {\n  source = \"../lambda\"\n\n  name = var.name\n\n  aws_region = var.aws_region\n\n  s3_bucket_name      = module.s3.name\n  dynamodb_table_name = module.ddb.name\n  lambda_zip_file     = var.lambda_zip_file\n  lambda_role_arn     = module.iam.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/best_cat/outputs.tf",
    "content": "output \"lambda_function_url\" {\n  description = \"URL of the Lambda function\"\n  value       = module.lambda.url\n}\n\noutput \"lambda_function_name\" {\n  description = \"Name of the Lambda function\"\n  value       = module.lambda.name\n}\n\noutput \"s3_bucket_name\" {\n  description = \"Name of the S3 bucket for static assets\"\n  value       = module.s3.name\n}\n\noutput \"s3_bucket_arn\" {\n  description = \"ARN of the S3 bucket for static assets\"\n  value       = module.s3.arn\n}\n\noutput \"dynamodb_table_name\" {\n  description = \"Name of the DynamoDB table for asset metadata\"\n  value       = module.ddb.name\n}\n\noutput \"dynamodb_table_arn\" {\n  description = \"ARN of the DynamoDB table for asset metadata\"\n  value       = module.ddb.arn\n}\n\noutput \"lambda_role_arn\" {\n  description = \"ARN of the Lambda execution role\"\n  value       = module.iam.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/best_cat/vars-optional.tf",
    "content": "variable \"aws_region\" {\n  description = \"AWS region for all resources\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"force_destroy\" {\n  description = \"Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)\"\n  type        = bool\n  default     = false\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/best_cat/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n\nvariable \"lambda_zip_file\" {\n  description = \"Path to the Lambda function zip file\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/ddb/main.tf",
    "content": "resource \"aws_dynamodb_table\" \"asset_metadata\" {\n  name         = \"${var.name}-asset-metadata\"\n  billing_mode = \"PAY_PER_REQUEST\"\n  hash_key     = \"image_id\"\n\n  attribute {\n    name = \"image_id\"\n    type = \"S\"\n  }\n\n  tags = {\n    Name = \"${var.name}-asset-metadata\"\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/ddb/outputs.tf",
    "content": "output \"name\" {\n  value = aws_dynamodb_table.asset_metadata.name\n}\n\noutput \"arn\" {\n  value = aws_dynamodb_table.asset_metadata.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/ddb/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"The name of the DynamoDB table\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/ddb/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/iam/data.tf",
    "content": "data \"aws_caller_identity\" \"current\" {}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/iam/main.tf",
    "content": "resource \"aws_iam_role\" \"lambda_role\" {\n  name = \"${var.name}-lambda-role\"\n\n  assume_role_policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Action = \"sts:AssumeRole\"\n        Effect = \"Allow\"\n        Principal = {\n          Service = \"lambda.amazonaws.com\"\n        }\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_s3_read\" {\n  name        = \"${var.name}-lambda-s3-read\"\n  description = \"Policy for Lambda to read from S3 bucket\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"s3:GetObject\",\n          \"s3:ListBucket\"\n        ]\n        Resource = [\n          var.s3_bucket_arn,\n          \"${var.s3_bucket_arn}/*\"\n        ]\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_dynamodb\" {\n  name        = \"${var.name}-lambda-dynamodb\"\n  description = \"Policy for Lambda to read/write to DynamoDB table\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"dynamodb:GetItem\",\n          \"dynamodb:PutItem\",\n          \"dynamodb:UpdateItem\",\n          \"dynamodb:DeleteItem\",\n          \"dynamodb:Query\",\n          \"dynamodb:Scan\"\n        ]\n        Resource = var.dynamodb_table_arn\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_basic_execution\" {\n  name        = \"${var.name}-lambda-basic-execution\"\n  description = \"Policy for Lambda basic execution (CloudWatch logs)\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"logs:CreateLogGroup\",\n          \"logs:CreateLogStream\",\n          \"logs:PutLogEvents\"\n        ]\n        Resource = \"arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:*\"\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_s3_read\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_s3_read.arn\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_dynamodb\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_dynamodb.arn\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_basic_execution\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_basic_execution.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/iam/outputs.tf",
    "content": "output \"name\" {\n  value = aws_iam_role.lambda_role.name\n}\n\noutput \"arn\" {\n  value = aws_iam_role.lambda_role.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/iam/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"The name of the IAM role\"\n  type        = string\n}\n\nvariable \"aws_region\" {\n  description = \"The AWS region to deploy the resources to\"\n  type        = string\n}\n\nvariable \"s3_bucket_arn\" {\n  description = \"The ARN of the S3 bucket\"\n  type        = string\n}\n\nvariable \"dynamodb_table_arn\" {\n  description = \"The ARN of the DynamoDB table\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/iam/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/lambda/main.tf",
    "content": "resource \"aws_lambda_function\" \"main\" {\n  function_name = \"${var.name}-function\"\n\n  filename         = var.lambda_zip_file\n  source_code_hash = filebase64sha256(var.lambda_zip_file)\n\n  role = var.lambda_role_arn\n\n  handler       = var.lambda_handler\n  runtime       = var.lambda_runtime\n  timeout       = var.lambda_timeout\n  memory_size   = var.lambda_memory_size\n  architectures = var.lambda_architectures\n\n  environment {\n    variables = {\n      S3_BUCKET_NAME      = var.s3_bucket_name\n      DYNAMODB_TABLE_NAME = var.dynamodb_table_name\n    }\n  }\n}\n\nresource \"aws_lambda_function_url\" \"main\" {\n  function_name      = aws_lambda_function.main.function_name\n  authorization_type = \"NONE\"\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/lambda/outputs.tf",
    "content": "output \"name\" {\n  value = aws_lambda_function.main.function_name\n}\n\noutput \"arn\" {\n  value = aws_lambda_function.main.arn\n}\n\noutput \"url\" {\n  value = aws_lambda_function_url.main.function_url\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/lambda/vars-optional.tf",
    "content": "variable \"lambda_runtime\" {\n  description = \"Lambda function runtime\"\n  type        = string\n  default     = \"nodejs22.x\"\n}\n\nvariable \"lambda_handler\" {\n  description = \"Lambda function handler\"\n  type        = string\n  default     = \"index.handler\"\n}\n\nvariable \"lambda_timeout\" {\n  description = \"Lambda function timeout in seconds\"\n  type        = number\n  default     = 30\n}\n\nvariable \"lambda_memory_size\" {\n  description = \"Lambda function memory size in MB\"\n  type        = number\n  default     = 128\n}\n\nvariable \"lambda_architectures\" {\n  description = \"Lambda function architectures\"\n  type        = list(string)\n  default     = [\"arm64\"]\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/lambda/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n\nvariable \"aws_region\" {\n  description = \"AWS region to deploy the resources to\"\n  type        = string\n}\n\nvariable \"lambda_zip_file\" {\n  description = \"Path to the Lambda function zip file\"\n  type        = string\n}\n\nvariable \"lambda_role_arn\" {\n  description = \"Lambda function role ARN\"\n  type        = string\n}\n\nvariable \"s3_bucket_name\" {\n  description = \"S3 bucket name\"\n  type        = string\n}\n\nvariable \"dynamodb_table_name\" {\n  description = \"DynamoDB table name\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/lambda/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/s3/main.tf",
    "content": "resource \"aws_s3_bucket\" \"static_assets\" {\n  bucket = \"${var.name}-static-assets\"\n\n  force_destroy = var.force_destroy\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/s3/outputs.tf",
    "content": "output \"name\" {\n  value = aws_s3_bucket.static_assets.bucket\n}\n\noutput \"arn\" {\n  value = aws_s3_bucket.static_assets.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/s3/vars-optional.tf",
    "content": "variable \"force_destroy\" {\n  description = \"Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)\"\n  type        = bool\n  default     = false\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/s3/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/modules/s3/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/units/ddb/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${find_in_parent_folders(\"catalog/modules\")}//ddb\"\n}\n\ninputs = {\n  name = values.name\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/units/iam/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${find_in_parent_folders(\"catalog/modules\")}//iam\"\n}\n\ndependency \"s3\" {\n  config_path = values.s3_path\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    arn = \"arn:aws:s3:::mock-bucket-name\"\n  }\n}\n\ndependency \"ddb\" {\n  config_path = values.ddb_path\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    arn = \"arn:aws:dynamodb:us-east-1:123456789012:table/mock-table-name\"\n  }\n}\n\ninputs = {\n  name = values.name\n\n  aws_region = values.aws_region\n\n  s3_bucket_arn      = dependency.s3.outputs.arn\n  dynamodb_table_arn = dependency.ddb.outputs.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/units/lambda/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${find_in_parent_folders(\"catalog/modules\")}//lambda\"\n}\n\ndependency \"s3\" {\n  config_path = values.s3_path\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    name = \"mock-bucket-name\"\n  }\n}\n\ndependency \"ddb\" {\n  config_path = values.ddb_path\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    name = \"mock-table-name\"\n  }\n}\n\ndependency \"iam\" {\n  config_path = values.iam_path\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    arn = \"arn:aws:iam::123456789012:role/mock-role-name\"\n  }\n}\n\ninputs = {\n  name = values.name\n\n  aws_region = values.aws_region\n\n  s3_bucket_name      = dependency.s3.outputs.name\n  dynamodb_table_name = dependency.ddb.outputs.name\n  lambda_role_arn     = dependency.iam.outputs.arn\n\n  lambda_zip_file     = \"${get_repo_root()}/dist/best-cat.zip\"\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/catalog/units/s3/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${find_in_parent_folders(\"catalog/modules\")}//s3\"\n}\n\ninputs = {\n  name = values.name\n\n  # Optional: Force destroy S3 buckets even when they have objects in them.\n  force_destroy = try(values.force_destroy, false)\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/live/dev/.gitignore",
    "content": "*\n!.gitignore\n!terragrunt.stack.hcl\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/live/dev/terragrunt.stack.hcl",
    "content": "locals {\n  name       = \"best-cat-2025-09-24-2359-dev\"\n  aws_region = \"us-east-1\"\n\n  units_path = find_in_parent_folders(\"catalog/units\")\n}\n\nunit \"ddb\" {\n  source = \"${local.units_path}/ddb\"\n  path   = \"ddb\"\n\n  no_dot_terragrunt_stack = true\n\n  values = {\n    name = local.name\n  }\n}\n\nunit \"s3\" {\n  source = \"${local.units_path}/s3\"\n  path   = \"s3\"\n\n  no_dot_terragrunt_stack = true\n\n  values = {\n    name = local.name\n\n    # Optional: Force destroy S3 buckets even when they have objects in them.\n    # You're generally advised not to do this with important infrastructure,\n    # however this makes testing and cleanup easier for this guide.\n    force_destroy = true\n  }\n}\n\nunit \"iam\" {\n  source = \"${local.units_path}/iam\"\n  path   = \"iam\"\n\n  no_dot_terragrunt_stack = true\n\n  values = {\n    name = local.name\n\n    aws_region = local.aws_region\n\n    s3_path  = \"../s3\"\n    ddb_path = \"../ddb\"\n  }\n}\n\nunit \"lambda\" {\n  source = \"${local.units_path}/lambda\"\n  path   = \"lambda\"\n\n  no_dot_terragrunt_stack = true\n\n  values = {\n    name = local.name\n\n    aws_region = local.aws_region\n\n    s3_path  = \"../s3\"\n    ddb_path = \"../ddb\"\n    iam_path = \"../iam\"\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/live/prod/.gitignore",
    "content": "*\n!.gitignore\n!terragrunt.stack.hcl\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/live/prod/terragrunt.stack.hcl",
    "content": "locals {\n  name       = \"best-cat-2025-09-24-2359\"\n  aws_region = \"us-east-1\"\n\n  units_path = find_in_parent_folders(\"catalog/units\")\n}\n\nunit \"ddb\" {\n  source = \"${local.units_path}/ddb\"\n  path   = \"ddb\"\n\n  no_dot_terragrunt_stack = true\n\n  values = {\n    name = local.name\n  }\n}\n\nunit \"s3\" {\n  source = \"${local.units_path}/s3\"\n  path   = \"s3\"\n\n  no_dot_terragrunt_stack = true\n\n  values = {\n    name = local.name\n\n    # Optional: Force destroy S3 buckets even when they have objects in them.\n    # You're generally advised not to do this with important infrastructure,\n    # however this makes testing and cleanup easier for this guide.\n    force_destroy = true\n  }\n}\n\nunit \"iam\" {\n  source = \"${local.units_path}/iam\"\n  path   = \"iam\"\n\n  no_dot_terragrunt_stack = true\n\n  values = {\n    name = local.name\n\n    aws_region = local.aws_region\n\n    s3_path  = \"../s3\"\n    ddb_path = \"../ddb\"\n  }\n}\n\nunit \"lambda\" {\n  source = \"${local.units_path}/lambda\"\n  path   = \"lambda\"\n\n  no_dot_terragrunt_stack = true\n\n  values = {\n    name = local.name\n\n    aws_region = local.aws_region\n\n    s3_path  = \"../s3\"\n    ddb_path = \"../ddb\"\n    iam_path = \"../iam\"\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-7-taking-advantage-of-terragrunt-stacks/live/root.hcl",
    "content": "remote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    bucket       = \"terragrunt-to-terralith-tfstate-2025-09-24-2359\"\n    key          = \"${path_relative_to_include()}/tofu.tfstate\"\n    region       = \"us-east-1\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n\ngenerate \"providers\" {\n  path      = \"providers.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents  = <<EOF\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\nEOF\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/best_cat/main.tf",
    "content": "module \"s3\" {\n  source = \"../s3\"\n\n  name = var.name\n\n  force_destroy = var.force_destroy\n}\n\nmodule \"ddb\" {\n  source = \"../ddb\"\n\n  name = var.name\n}\n\nmodule \"iam\" {\n  source = \"../iam\"\n\n  name = var.name\n\n  aws_region = var.aws_region\n\n  s3_bucket_arn      = module.s3.arn\n  dynamodb_table_arn = module.ddb.arn\n}\n\nmodule \"lambda\" {\n  source = \"../lambda\"\n\n  name = var.name\n\n  aws_region = var.aws_region\n\n  s3_bucket_name      = module.s3.name\n  dynamodb_table_name = module.ddb.name\n  lambda_zip_file     = var.lambda_zip_file\n  lambda_role_arn     = module.iam.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/best_cat/outputs.tf",
    "content": "output \"lambda_function_url\" {\n  description = \"URL of the Lambda function\"\n  value       = module.lambda.url\n}\n\noutput \"lambda_function_name\" {\n  description = \"Name of the Lambda function\"\n  value       = module.lambda.name\n}\n\noutput \"s3_bucket_name\" {\n  description = \"Name of the S3 bucket for static assets\"\n  value       = module.s3.name\n}\n\noutput \"s3_bucket_arn\" {\n  description = \"ARN of the S3 bucket for static assets\"\n  value       = module.s3.arn\n}\n\noutput \"dynamodb_table_name\" {\n  description = \"Name of the DynamoDB table for asset metadata\"\n  value       = module.ddb.name\n}\n\noutput \"dynamodb_table_arn\" {\n  description = \"ARN of the DynamoDB table for asset metadata\"\n  value       = module.ddb.arn\n}\n\noutput \"lambda_role_arn\" {\n  description = \"ARN of the Lambda execution role\"\n  value       = module.iam.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/best_cat/vars-optional.tf",
    "content": "variable \"aws_region\" {\n  description = \"AWS region for all resources\"\n  type        = string\n  default     = \"us-east-1\"\n}\n\nvariable \"force_destroy\" {\n  description = \"Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)\"\n  type        = bool\n  default     = false\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/best_cat/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n\nvariable \"lambda_zip_file\" {\n  description = \"Path to the Lambda function zip file\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/ddb/main.tf",
    "content": "resource \"aws_dynamodb_table\" \"asset_metadata\" {\n  name         = \"${var.name}-asset-metadata\"\n  billing_mode = \"PAY_PER_REQUEST\"\n  hash_key     = \"image_id\"\n\n  attribute {\n    name = \"image_id\"\n    type = \"S\"\n  }\n\n  tags = {\n    Name = \"${var.name}-asset-metadata\"\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/ddb/outputs.tf",
    "content": "output \"name\" {\n  value = aws_dynamodb_table.asset_metadata.name\n}\n\noutput \"arn\" {\n  value = aws_dynamodb_table.asset_metadata.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/ddb/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"The name of the DynamoDB table\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/ddb/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/iam/data.tf",
    "content": "data \"aws_caller_identity\" \"current\" {}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/iam/main.tf",
    "content": "resource \"aws_iam_role\" \"lambda_role\" {\n  name = \"${var.name}-lambda-role\"\n\n  assume_role_policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Action = \"sts:AssumeRole\"\n        Effect = \"Allow\"\n        Principal = {\n          Service = \"lambda.amazonaws.com\"\n        }\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_s3_read\" {\n  name        = \"${var.name}-lambda-s3-read\"\n  description = \"Policy for Lambda to read from S3 bucket\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"s3:GetObject\",\n          \"s3:ListBucket\"\n        ]\n        Resource = [\n          var.s3_bucket_arn,\n          \"${var.s3_bucket_arn}/*\"\n        ]\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_dynamodb\" {\n  name        = \"${var.name}-lambda-dynamodb\"\n  description = \"Policy for Lambda to read/write to DynamoDB table\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"dynamodb:GetItem\",\n          \"dynamodb:PutItem\",\n          \"dynamodb:UpdateItem\",\n          \"dynamodb:DeleteItem\",\n          \"dynamodb:Query\",\n          \"dynamodb:Scan\"\n        ]\n        Resource = var.dynamodb_table_arn\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_policy\" \"lambda_basic_execution\" {\n  name        = \"${var.name}-lambda-basic-execution\"\n  description = \"Policy for Lambda basic execution (CloudWatch logs)\"\n\n  policy = jsonencode({\n    Version = \"2012-10-17\"\n    Statement = [\n      {\n        Effect = \"Allow\"\n        Action = [\n          \"logs:CreateLogGroup\",\n          \"logs:CreateLogStream\",\n          \"logs:PutLogEvents\"\n        ]\n        Resource = \"arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:*\"\n      }\n    ]\n  })\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_s3_read\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_s3_read.arn\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_dynamodb\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_dynamodb.arn\n}\n\nresource \"aws_iam_role_policy_attachment\" \"lambda_basic_execution\" {\n  role       = aws_iam_role.lambda_role.name\n  policy_arn = aws_iam_policy.lambda_basic_execution.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/iam/outputs.tf",
    "content": "output \"name\" {\n  value = aws_iam_role.lambda_role.name\n}\n\noutput \"arn\" {\n  value = aws_iam_role.lambda_role.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/iam/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"The name of the IAM role\"\n  type        = string\n}\n\nvariable \"aws_region\" {\n  description = \"The AWS region to deploy the resources to\"\n  type        = string\n}\n\nvariable \"s3_bucket_arn\" {\n  description = \"The ARN of the S3 bucket\"\n  type        = string\n}\n\nvariable \"dynamodb_table_arn\" {\n  description = \"The ARN of the DynamoDB table\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/iam/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/lambda/main.tf",
    "content": "resource \"aws_lambda_function\" \"main\" {\n  function_name = \"${var.name}-function\"\n\n  filename         = var.lambda_zip_file\n  source_code_hash = filebase64sha256(var.lambda_zip_file)\n\n  role = var.lambda_role_arn\n\n  handler       = var.lambda_handler\n  runtime       = var.lambda_runtime\n  timeout       = var.lambda_timeout\n  memory_size   = var.lambda_memory_size\n  architectures = var.lambda_architectures\n\n  environment {\n    variables = {\n      S3_BUCKET_NAME      = var.s3_bucket_name\n      DYNAMODB_TABLE_NAME = var.dynamodb_table_name\n    }\n  }\n}\n\nresource \"aws_lambda_function_url\" \"main\" {\n  function_name      = aws_lambda_function.main.function_name\n  authorization_type = \"NONE\"\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/lambda/outputs.tf",
    "content": "output \"name\" {\n  value = aws_lambda_function.main.function_name\n}\n\noutput \"arn\" {\n  value = aws_lambda_function.main.arn\n}\n\noutput \"url\" {\n  value = aws_lambda_function_url.main.function_url\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/lambda/vars-optional.tf",
    "content": "variable \"lambda_runtime\" {\n  description = \"Lambda function runtime\"\n  type        = string\n  default     = \"nodejs22.x\"\n}\n\nvariable \"lambda_handler\" {\n  description = \"Lambda function handler\"\n  type        = string\n  default     = \"index.handler\"\n}\n\nvariable \"lambda_timeout\" {\n  description = \"Lambda function timeout in seconds\"\n  type        = number\n  default     = 30\n}\n\nvariable \"lambda_memory_size\" {\n  description = \"Lambda function memory size in MB\"\n  type        = number\n  default     = 128\n}\n\nvariable \"lambda_architectures\" {\n  description = \"Lambda function architectures\"\n  type        = list(string)\n  default     = [\"arm64\"]\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/lambda/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n\nvariable \"aws_region\" {\n  description = \"AWS region to deploy the resources to\"\n  type        = string\n}\n\nvariable \"lambda_zip_file\" {\n  description = \"Path to the Lambda function zip file\"\n  type        = string\n}\n\nvariable \"lambda_role_arn\" {\n  description = \"Lambda function role ARN\"\n  type        = string\n}\n\nvariable \"s3_bucket_name\" {\n  description = \"S3 bucket name\"\n  type        = string\n}\n\nvariable \"dynamodb_table_name\" {\n  description = \"DynamoDB table name\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/lambda/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/s3/main.tf",
    "content": "resource \"aws_s3_bucket\" \"static_assets\" {\n  bucket = \"${var.name}-static-assets\"\n\n  force_destroy = var.force_destroy\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/s3/outputs.tf",
    "content": "output \"name\" {\n  value = aws_s3_bucket.static_assets.bucket\n}\n\noutput \"arn\" {\n  value = aws_s3_bucket.static_assets.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/s3/vars-optional.tf",
    "content": "variable \"force_destroy\" {\n  description = \"Force destroy S3 buckets (only set to true for testing or cleanup of demo environments)\"\n  type        = bool\n  default     = false\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/s3/vars-required.tf",
    "content": "variable \"name\" {\n  description = \"Name used for all resources\"\n  type        = string\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/modules/s3/versions.tf",
    "content": "terraform {\n  required_version = \">= 1.10\"\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/units/ddb/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${find_in_parent_folders(\"catalog/modules\")}//ddb\"\n}\n\ninputs = {\n  name = values.name\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/units/iam/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${find_in_parent_folders(\"catalog/modules\")}//iam\"\n}\n\ndependency \"s3\" {\n  config_path = values.s3_path\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    arn = \"arn:aws:s3:::mock-bucket-name\"\n  }\n}\n\ndependency \"ddb\" {\n  config_path = values.ddb_path\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    arn = \"arn:aws:dynamodb:us-east-1:123456789012:table/mock-table-name\"\n  }\n}\n\ninputs = {\n  name = values.name\n\n  aws_region = values.aws_region\n\n  s3_bucket_arn      = dependency.s3.outputs.arn\n  dynamodb_table_arn = dependency.ddb.outputs.arn\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/units/lambda/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${find_in_parent_folders(\"catalog/modules\")}//lambda\"\n}\n\ndependency \"s3\" {\n  config_path = values.s3_path\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    name = \"mock-bucket-name\"\n  }\n}\n\ndependency \"ddb\" {\n  config_path = values.ddb_path\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    name = \"mock-table-name\"\n  }\n}\n\ndependency \"iam\" {\n  config_path = values.iam_path\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"state\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n\n  mock_outputs = {\n    arn = \"arn:aws:iam::123456789012:role/mock-role-name\"\n  }\n}\n\ninputs = {\n  name = values.name\n\n  aws_region = values.aws_region\n\n  s3_bucket_name      = dependency.s3.outputs.name\n  dynamodb_table_name = dependency.ddb.outputs.name\n  lambda_role_arn     = dependency.iam.outputs.arn\n\n  lambda_zip_file     = \"${get_repo_root()}/dist/best-cat.zip\"\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/catalog/units/s3/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${find_in_parent_folders(\"catalog/modules\")}//s3\"\n}\n\ninputs = {\n  name = values.name\n\n  # Optional: Force destroy S3 buckets even when they have objects in them.\n  force_destroy = try(values.force_destroy, false)\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/live/dev/terragrunt.stack.hcl",
    "content": "locals {\n  name       = \"best-cat-2025-09-24-2359-dev\"\n  aws_region = \"us-east-1\"\n\n  units_path = find_in_parent_folders(\"catalog/units\")\n}\n\nunit \"ddb\" {\n  source = \"${local.units_path}/ddb\"\n  path   = \"ddb\"\n\n  values = {\n    name = local.name\n  }\n}\n\nunit \"s3\" {\n  source = \"${local.units_path}/s3\"\n  path   = \"s3\"\n\n  values = {\n    name = local.name\n\n    # Optional: Force destroy S3 buckets even when they have objects in them.\n    # You're generally advised not to do this with important infrastructure,\n    # however this makes testing and cleanup easier for this guide.\n    force_destroy = true\n  }\n}\n\nunit \"iam\" {\n  source = \"${local.units_path}/iam\"\n  path   = \"iam\"\n\n  values = {\n    name = local.name\n\n    aws_region = local.aws_region\n\n    s3_path  = \"../s3\"\n    ddb_path = \"../ddb\"\n  }\n}\n\nunit \"lambda\" {\n  source = \"${local.units_path}/lambda\"\n  path   = \"lambda\"\n\n  values = {\n    name = local.name\n\n    aws_region = local.aws_region\n\n    s3_path  = \"../s3\"\n    ddb_path = \"../ddb\"\n    iam_path = \"../iam\"\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/live/prod/terragrunt.stack.hcl",
    "content": "locals {\n  name       = \"best-cat-2025-09-24-2359\"\n  aws_region = \"us-east-1\"\n\n  units_path = find_in_parent_folders(\"catalog/units\")\n}\n\nunit \"ddb\" {\n  source = \"${local.units_path}/ddb\"\n  path   = \"ddb\"\n\n  values = {\n    name = local.name\n  }\n}\n\nunit \"s3\" {\n  source = \"${local.units_path}/s3\"\n  path   = \"s3\"\n\n  values = {\n    name = local.name\n\n    # Optional: Force destroy S3 buckets even when they have objects in them.\n    # You're generally advised not to do this with important infrastructure,\n    # however this makes testing and cleanup easier for this guide.\n    force_destroy = true\n  }\n}\n\nunit \"iam\" {\n  source = \"${local.units_path}/iam\"\n  path   = \"iam\"\n\n  values = {\n    name = local.name\n\n    aws_region = local.aws_region\n\n    s3_path  = \"../s3\"\n    ddb_path = \"../ddb\"\n  }\n}\n\nunit \"lambda\" {\n  source = \"${local.units_path}/lambda\"\n  path   = \"lambda\"\n\n  values = {\n    name = local.name\n\n    aws_region = local.aws_region\n\n    s3_path  = \"../s3\"\n    ddb_path = \"../ddb\"\n    iam_path = \"../iam\"\n  }\n}\n"
  },
  {
    "path": "docs/src/fixtures/terralith-to-terragrunt/walkthrough/step-8-refactoring-state-with-terragrunt-stacks/live/root.hcl",
    "content": "remote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    bucket       = \"terragrunt-to-terralith-tfstate-2025-09-24-2359\"\n    key          = \"${path_relative_to_include()}/tofu.tfstate\"\n    region       = \"us-east-1\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n\ngenerate \"providers\" {\n  path      = \"providers.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents  = <<EOF\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\nEOF\n}\n"
  },
  {
    "path": "docs/src/layouts/BaseLayout.astro",
    "content": "---\ninterface Props {\n  title?: string;\n  description?: string;\n  url?: string;\n  hasBanner?: boolean;\n}\n\nconst {\n  title = 'Terragrunt | Orchestrate Terraform & OpenTofu at Scale',\n  description = 'Standardize IaC and manage growing infra complexity: define units & stacks, cut repetition with includes/hooks, execute modules in dependency order across environments.',\n  url = 'https://docs.terragrunt.com/',\n  hasBanner = false,\n} = Astro.props;\n---\n<!DOCTYPE html>\n<html lang=\"en\" class=\"h-full\">\n  <head>\n    <!-- Primary Meta Tags -->\n    <meta charset=\"UTF-8\" />\n    <title>{title}</title>\n    <meta name=\"description\" content={description} />\n    <meta name=\"generator\" content={Astro.generator} />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n\n    <!-- Open Graph / Facebook -->\n    <meta property=\"og:description\" content={description} />\n    <meta property=\"og:image\" content=\"https://docs.terragrunt.com/images/terragrunt-og-image-1200x630.png\" />\n    <meta property=\"og:image:alt\" content=\"An image featuring the Gruntwork Mascot and the words Orchestrate Terraform & OpenTofu at Scale\" />\n    <meta property=\"og:image:height\" content=\"630\" />\n    <meta property=\"og:image:secure_url\" content=\"https://docs.terragrunt.com/images/terragrunt-og-image-1200x630.png\" />\n    <meta property=\"og:image:type\" content=\"image/png\" />\n    <meta property=\"og:image:width\" content=\"1200\" />\n    <meta property=\"og:title\" content={title} />\n    <meta property=\"og:type\" content=\"website\" />\n    <meta property=\"og:url\" content={url} />\n\n    <!-- X (Twitter) -->\n    <meta name=\"twitter:card\" content=\"summary_large_image\" />\n    <meta name=\"twitter:description\" content={description} />\n    <meta name=\"twitter:image\" content=\"https://docs.terragrunt.com/images/terragrunt-twitter-image.png\" />\n    <meta name=\"twitter:site\" content=\"@gruntwork_io\" />\n    <meta name=\"twitter:title\" content={title} />\n\n    <link rel=\"canonical\" href={url} />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.svg\" />\n  </head>\n  <body class={`tg bg-[#FAFAFA] min-h-full ${hasBanner ? 'has-banner' : ''}`}>\n    <slot />\n  </body>\n</html>\n"
  },
  {
    "path": "docs/src/lib/commands/headings/index.ts",
    "content": "import { getEntry, type CollectionEntry } from 'astro:content';\n\nexport async function getHeadings(\n\tcommand: CollectionEntry<'commands'>,\n): Promise<{ depth: number; slug: string; text: string }[]> {\n\tconst headings: { depth: number; slug: string; text: string }[] = [];\n\n\theadings.push({ depth: 2, slug: 'usage', text: 'Usage' });\n\n\tif (command.data.examples) {\n\t\theadings.push({ depth: 2, slug: 'examples', text: 'Examples' });\n\t}\n\n\tconst h2HeadingsLines = command.body?.match(/## (.*)/g);\n\tconst h2Headings = h2HeadingsLines?.map((line) => line.replace(/## /g, ''));\n\n\tconst h3HeadingsLines = command.body?.match(/### (.*)/g);\n\tconst h3Headings = h3HeadingsLines?.map((line) => line.replace(/### /g, ''));\n\n\n\tif (h2Headings) {\n\t\th2Headings.forEach((text) => {\n\t\t\tconst slug = text.toLowerCase().replace(/ /g, '-');\n\t\t\theadings.push({ depth: 2, slug, text });\n\t\t});\n\t}\n\n\tif (h3Headings) {\n\t\th3Headings.forEach((text) => {\n\t\t\tconst slug = text.toLowerCase().replace(/ /g, '-');\n\t\t\theadings.push({ depth: 3, slug, text });\n\t\t});\n\t}\n\n\tif (command.data.flags) {\n\t\theadings.push({ depth: 2, slug: 'flags', text: 'Flags' });\n\n\t\tconst flags = await Promise.all(command.data.flags.map(async (flagName: string) => {\n\t\t\tconst flag = (await getEntry('flags', flagName))!;\n\t\t\treturn {\n\t\t\t\tdepth: 3,\n\t\t\t\tslug: flag.data.name,\n\t\t\t\ttext: `--${flag.data.name}`,\n\t\t\t};\n\t\t}));\n\n\t\theadings.push(...flags);\n\t}\n\n\treturn headings;\n};\n"
  },
  {
    "path": "docs/src/lib/commands/sidebar/index.ts",
    "content": "import type { CollectionEntry } from \"astro:content\";\nimport type { SidebarItem } from \"node_modules/@astrojs/starlight/schemas/sidebar\";\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport matter from 'gray-matter';\nimport { sidebar as sidebarTemplate } from '../../../../astro.config.mjs';\n\nfunction createCommandSidebarItem(command: CollectionEntry<'commands'>): SidebarItem & { originalPath: string } {\n  const data = command.data;\n  const sidebarItem = {\n    label: data.name,\n    slug: `reference/cli/commands/${data.path}`,\n    originalPath: data.path,\n  } as SidebarItem & { originalPath: string };\n\n  if (data.experiment) {\n    sidebarItem.badge = {\n      variant: 'tip',\n      text: 'exp',\n    };\n  }\n\n  return sidebarItem;\n}\n\nfunction organizeCommandsIntoGroups(flatCommandItems: (SidebarItem & { originalPath: string })[]): SidebarItem[] {\n  const commandItems: SidebarItem[] = [];\n  const groupedCommands: Record<string, SidebarItem[]> = {};\n\n  flatCommandItems.forEach((item) => {\n    const parts = item.originalPath.split('/');\n\n    if (parts.length === 1) {\n      // Root-level command\n      const { originalPath, ...cleanItem } = item;\n      commandItems.push(cleanItem);\n    } else {\n      // Nested command\n      const groupName = parts[0];\n      if (!groupedCommands[groupName]) {\n        groupedCommands[groupName] = [];\n        commandItems.push({\n          label: groupName,\n          collapsed: true,\n          translations: {},\n          items: groupedCommands[groupName]\n        });\n      }\n      const { originalPath, ...cleanItem } = item;\n      groupedCommands[groupName].push(cleanItem);\n    }\n  });\n\n  return commandItems;\n}\n\nfunction insertCommandsIntoSidebar(\n  sidebar: SidebarItem[],\n  commandItems: SidebarItem[]\n): void {\n  const referenceSection = sidebar.find(item =>\n    typeof item === 'object' && 'label' in item && item.label === 'Reference'\n  ) as { items: SidebarItem[] };\n\n  if (!referenceSection?.items) return;\n\n  const cliSection = referenceSection.items.find(item =>\n    typeof item === 'object' && 'label' in item && item.label === 'CLI'\n  ) as { items: SidebarItem[] };\n\n  if (!cliSection?.items) return;\n\n  // Remove existing Commands section\n  cliSection.items = cliSection.items.filter(item =>\n    !(typeof item === 'object' && 'label' in item && item.label === 'Commands')\n  );\n\n  // Insert new Commands section after Overview\n  const overviewIndex = cliSection.items.findIndex(item =>\n    typeof item === 'object' && 'label' in item && item.label === 'Overview'\n  );\n\n  if (overviewIndex !== -1) {\n    cliSection.items.splice(overviewIndex + 1, 0, {\n      label: 'Commands',\n      collapsed: true,\n      translations: {},\n      items: commandItems\n    });\n  }\n}\n\nfunction populateAutogeneratedSections(items: SidebarItem[]) {\n  for (const item of items) {\n    if (typeof item === 'object' && 'autogenerate' in item) {\n      const dirPath = path.join(process.cwd(), 'src/content/docs', item.autogenerate.directory);\n      if (fs.existsSync(dirPath)) {\n        const files = fs.readdirSync(dirPath)\n          .filter(file => file.endsWith('.mdx') || file.endsWith('.md'));\n\n        (item as SidebarItem & { items: SidebarItem[] }).items = files.map(file => {\n          const content = fs.readFileSync(path.join(dirPath, file), 'utf-8');\n          const { data } = matter(content);\n          return {\n            label: data.title as string,\n            translations: {},\n            slug: data.slug as string,\n            attrs: {}\n          };\n        });\n        delete (item as any).autogenerate;\n      }\n    }\n\n    if (typeof item === 'object' && 'items' in item) {\n      populateAutogeneratedSections(item.items);\n    }\n  }\n}\n\nexport async function getSidebar(commands: CollectionEntry<'commands'>[]): Promise<SidebarItem[]> {\n  // Deep clone the sidebar template to avoid mutations\n  const sidebar = JSON.parse(JSON.stringify(sidebarTemplate));\n\n  // Create flat list of command items\n  const flatCommandItems = commands\n    .sort((a, b) => a.data.sidebar.order - b.data.sidebar.order)\n    .map(createCommandSidebarItem);\n\n  // Organize commands into nested groups\n  const commandItems = organizeCommandsIntoGroups(flatCommandItems);\n\n  // Insert commands into the sidebar\n  insertCommandsIntoSidebar(sidebar, commandItems);\n\n  // Handle autogenerated sections\n  populateAutogeneratedSections(sidebar);\n\n  return sidebar;\n}\n"
  },
  {
    "path": "docs/src/lib/github.ts",
    "content": "const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour in milliseconds\nconst FETCH_TIMEOUT_MS = 10 * 1000; // 10 second timeout for API requests\n\ninterface CacheEntry<T> {\n  data: T;\n  timestamp: number;\n}\n\nconst cache = new Map<string, CacheEntry<unknown>>();\n\n/**\n * Memoized fetch that caches results for 1 hour.\n * Used to avoid rate limiting on GitHub API calls during builds.\n */\nasync function memoizedFetch<T>(\n  cacheKey: string,\n  fetchFn: () => Promise<T>\n): Promise<T> {\n  const now = Date.now();\n  const cached = cache.get(cacheKey) as CacheEntry<T> | undefined;\n\n  if (cached && now - cached.timestamp < CACHE_TTL_MS) {\n    return cached.data;\n  }\n\n  const data = await fetchFn();\n  // Only cache successful (non-null) results to allow retries on failures\n  if (data !== null) {\n    cache.set(cacheKey, { data, timestamp: now });\n  }\n  return data;\n}\n\ninterface GitHubRepoResponse {\n  stargazers_count: number;\n  [key: string]: unknown;\n}\n\ninterface GitHubReleaseResponse {\n  tag_name: string;\n  name: string;\n  html_url: string;\n  published_at: string;\n  [key: string]: unknown;\n}\n\n/**\n * Fetches GitHub repository data with memoization.\n * Results are cached for 1 hour to avoid rate limiting.\n */\nexport async function getGitHubRepo(\n  owner: string,\n  repo: string\n): Promise<GitHubRepoResponse | null> {\n  const cacheKey = `repo:${owner}/${repo}`;\n\n  try {\n    return await memoizedFetch(cacheKey, async () => {\n      const controller = new AbortController();\n      const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);\n\n      try {\n        const response = await fetch(\n          `https://api.github.com/repos/${owner}/${repo}`,\n          {\n            headers: {\n              'User-Agent': 'Terragrunt-Docs',\n            },\n            signal: controller.signal,\n          }\n        );\n\n        if (!response.ok) {\n          console.error(\n            `Failed to fetch GitHub repo ${owner}/${repo}:`,\n            response.status,\n            await response.text()\n          );\n          return null;\n        }\n\n        return response.json();\n      } finally {\n        clearTimeout(timeoutId);\n      }\n    });\n  } catch (error) {\n    console.error(`Error fetching GitHub repo ${owner}/${repo}:`, error);\n    return null;\n  }\n}\n\n/**\n * Fetches the latest release for a GitHub repository with memoization.\n * Results are cached for 1 hour to avoid rate limiting.\n */\nexport async function getLatestRelease(\n  owner: string,\n  repo: string\n): Promise<GitHubReleaseResponse | null> {\n  const cacheKey = `release:${owner}/${repo}`;\n\n  try {\n    return await memoizedFetch(cacheKey, async () => {\n      const controller = new AbortController();\n      const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);\n\n      try {\n        const response = await fetch(\n          `https://api.github.com/repos/${owner}/${repo}/releases/latest`,\n          {\n            headers: {\n              'User-Agent': 'Terragrunt-Docs',\n            },\n            signal: controller.signal,\n          }\n        );\n\n        if (!response.ok) {\n          console.error(\n            `Failed to fetch latest release for ${owner}/${repo}:`,\n            response.status,\n            await response.text()\n          );\n          return null;\n        }\n\n        return response.json();\n      } finally {\n        clearTimeout(timeoutId);\n      }\n    });\n  } catch (error) {\n    console.error(`Error fetching latest release for ${owner}/${repo}:`, error);\n    return null;\n  }\n}\n\n/**\n * Formats a star count for display (e.g., 8600 -> \"8.6k\")\n */\nexport function formatStarCount(stars: number): string {\n  return (stars / 1000).toFixed(1) + 'k';\n}\n"
  },
  {
    "path": "docs/src/lib/utils.ts",
    "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\n// Shadcn has a convention of using the cn function to merge classes\n// https://github.com/shadcn-ui/ui/blob/main/apps/www/lib/utils.ts#L5\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"
  },
  {
    "path": "docs/src/pages/api/v1/compatibility/[tool].ts",
    "content": "import type { APIRoute, GetStaticPaths } from 'astro';\nimport { getCollection } from 'astro:content';\n\nexport const prerender = true;\n\nexport const getStaticPaths: GetStaticPaths = () => [\n\t{ params: { tool: 'index' } },\n\t{ params: { tool: 'opentofu' } },\n\t{ params: { tool: 'terraform' } },\n];\n\nexport const GET: APIRoute = async ({ params }) => {\n\tconst tool = params.tool === 'index' ? undefined : params.tool;\n\tconst entries = (await getCollection('compatibility'))\n\t\t.filter(e => !tool || e.data.tool === tool)\n\t\t.sort((a, b) => {\n\t\t\tif (a.data.tool !== b.data.tool) {\n\t\t\t\treturn a.data.tool === 'opentofu' ? -1 : 1;\n\t\t\t}\n\t\t\treturn b.data.order - a.data.order;\n\t\t})\n\t\t.map(e => ({\n\t\t\ttool: e.data.tool,\n\t\t\tversion: e.data.version,\n\t\t\tterragrunt_min: e.data.terragrunt_min,\n\t\t\tterragrunt_max: e.data.terragrunt_max,\n\t\t}));\n\n\treturn new Response(JSON.stringify(entries), {\n\t\theaders: { 'Content-Type': 'application/json' },\n\t});\n};\n"
  },
  {
    "path": "docs/src/pages/index.astro",
    "content": "---\nimport BaseLayout from '@layouts/BaseLayout.astro';\nimport ConsistencySection from '@components/dv-ConsistencySection.astro';\nimport DrySection from '@components/dv-DrySection.astro';\nimport FeaturedBrands from '@components/dv-FeaturedBrands.astro';\nimport Footer from '@components/dv-Footer.astro';\nimport Header from '@components/Header.astro';\nimport Hero from '@components/dv-Hero.astro';\nimport OrchestrateSection from '@components/dv-OrchestrateSection.astro';\nimport PageContainer from '@components/PageContainer.astro';\nimport PetAdvertise from '@components/dv-PetAdvertise.astro';\nimport Testimonials from '@components/dv-Testimonials.astro';\nimport TopBanner from '@components/TopBanner.astro';\n\nimport '@styles/global.css';\nimport '@styles/custom-page.css';\n---\n\n<BaseLayout hasBanner={true}>\n  <TopBanner />\n  <Header showThemeToggle={false} />\n  <Hero />\n\n  <PageContainer>\n    <FeaturedBrands />\n\n    <div class=\"flex flex-col gap-12 md:gap-38 z-10\">\n      <OrchestrateSection />\n      <ConsistencySection />\n      <DrySection />\n      <Testimonials />\n    </div>\n\n    <div class=\"flex flex-col gap-44 z-10\">\n      <PetAdvertise />\n      <Footer />\n    </div>\n  </PageContainer>\n</BaseLayout>\n"
  },
  {
    "path": "docs/src/pages/reference/cli/commands/[...slug].astro",
    "content": "---\nimport Command from '@components/Command.astro';\nimport StarlightPage from '@astrojs/starlight/components/StarlightPage.astro';\nimport { getCollection } from 'astro:content';\nimport { getHeadings } from '@lib/commands/headings';\nimport { getSidebar } from '@lib/commands/sidebar';\nimport { string } from 'astro:schema';\n\nexport const prerender = true;\n\nexport async function getStaticPaths() {\n\tconst commands = await getCollection('commands');\n\n\tconst sidebar = await getSidebar(commands);\n\n\treturn Promise.all(commands.map(async (command) => {\n\t\tconst headings = await getHeadings(command);\n\n\t\tconst data = command.data;\n\n\t\treturn {\n\t\t\tparams: {\n\t\t\t\tslug: data.path,\n\t\t\t},\n\t\t\tprops: {\n\t\t\t\tname: data.name,\n\t\t\t\tpath: data.path,\n\t\t\t\tdescription: data.description,\n\t\t\t\texperiment: data.experiment,\n\t\t\t\texamples: data.examples,\n\t\t\t\theadings: headings,\n\t\t\t\tsidebar: sidebar,\n\t\t\t},\n\t\t}\n\t}));\n}\n\nconst { name, path, description, headings, sidebar } = Astro.props;\n---\n\n<StarlightPage\n\tfrontmatter={{\n\t\ttitle: name,\n\t\tdescription: description,\n\t}}\n\theadings={headings}\n\tsidebar={sidebar}\n>\n\t<Command path={path} />\n</StarlightPage>\n"
  },
  {
    "path": "docs/src/styles/global.css",
    "content": "@layer base, starlight, theme, components, utilities;\n@import '@astrojs/starlight-tailwind';\n@import \"tailwindcss/theme.css\" layer(theme);\n@import \"tailwindcss/utilities.css\" layer(utilities);\n\n@import \"./lists.css\" layer(components);\n\n/* Starlight Fixes */\n@import \"./starlight-search.css\" layer(components);\n@import \"./starlight-right-sidebar.css\" layer(components);\n\n@font-face {\n  font-family: 'Inter';\n  src: url('/fonts/Inter-VariableFont_opsz,wght.ttf') format('truetype');\n  font-weight: 100 900;\n  font-style: normal;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: 'Inter';\n  src: url('/fonts/Inter-Italic-VariableFont_opsz,wght.ttf') format('truetype');\n  font-weight: 100 900;\n  font-style: italic;\n  font-display: swap;\n}\n\n@font-face {\n  font-family: 'Geist Mono';\n  src: url('/fonts/GeistMono-VariableFont_wght.ttf') format('truetype');\n  font-weight: 100 900;\n  font-style: normal;\n  font-display: swap;\n}\n\n@theme {\n  /* Fonts */\n  --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n  --font-mono: 'Geist Mono', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, monospace;\n\n  /* Buttons */\n  --color-button-bg: #EEECF7;\n  --color-checkout-radio: #eeecf6;\n  --color-checkout-radio-active: #4a31c8;\n\n  /* Colors Nav */\n  --color-nav-link: #A0A1AC;\n  --color-nav-link-hover: #e8eefc;\n\n  /* Colors Text */\n  --color-primary: #0F0934;\n\n  /* Colors Landing Page */\n  --color-accent: #4F2FD0;\n  --color-accent-1: #7d5DFF;\n  --color-accent-2: #87E0E1;\n  --color-accent-3: #BFA6F2;\n  --color-bg-dark: #0F1731;\n  --color-dark-blue-1: #0F0934;\n  --color-gray-1: #A0A1AC;\n  --color-gray-2: #6B6C7A;\n  --color-gray-3: #DBDBDB;\n  --color-gray-4: #6D6F86;\n  --color-stroke-dark: #2E375A;\n  --color-button-primary-border: hsla(0deg, 0%, 100%, 20%);\n\n  /* Colors Contact Form */\n  --color-contact-form-button-bg: rgba(255, 255, 255, 0.15);\n\n  /* Component Colors */\n  --color-opacity-5: rgba(255, 255, 255, 0.05);\n  --color-opacity-10: rgba(255, 255, 255, 0.1);\n\n  /* Dialog */\n  --color-dialog-bg: oklch(0.92 0.004 286.32);\n\n  /* Colors Terragrunt Page */\n  --color-feature-container: #171e3c;\n  --color-feature-container-stroke: #7b7f94;\n  --color-card-border: #2F3547;\n  --color-acc-text: #F48701;\n  --color-gray-border: #E2E3E6;\n  --color-primary-button-light: hsl(33, 99%, 47%);\n\n  /* Starlight Customizations */\n  --sl-color-hairline-shade: #2E375A;\n  --sl-color-secondary: #EBEBEB;\n  --sl-text-sm: 16px;\n  --sl-color-stroke-dark: #2E375A;\n  --sl-z-index-toc: 10;\n\n  /* Strokes */\n  --color-stroke-light: #282A46;\n\n  /* Surfaces */\n  --color-surface-1: #0F0726;\n  --color-surface-3: #1C1833;\n\n  /* Navigation */\n  --sl-color-bg-nav: #0F1731;\n  --sl-color-asides-border: hsl(41, 90%, 60%);\n  --sl-nav-height: 90px;\n}\n\n/* Docs Dark mode colors. */\n:root {\n  --sl-color-bg: #0F1731;\n  --sl-color-bg-sidebar: #0F1731;\n  --sl-color-bg-inline-code: #2b2d4c;\n  --color-code-border: #403f64;\n  --color-code-text: #BFA6F2;\n  --sl-color-accent-low: #1f1d47;\n  --sl-color-accent: #5e46e6;\n  --sl-color-accent-high: #c0c3fa;\n  --sl-color-black: #131824;\n  --sl-color-docs-stroke: #2E375A;\n  --sl-color-gray-1: #e8eefc;\n  --sl-color-gray-2: #bbc2d4;\n  --sl-color-gray-3: #7e8bac;\n  --sl-color-gray-4: #4c5776;\n  --sl-color-gray-5: #2d3754;\n  --sl-color-gray-6: #1c2541;\n  --color-toc-accent: #BFA6F2;\n  --color-toc-background: rgba(191, 166, 242, 0.15);\n  --color-toc-text: #EBEBEB;\n  --sl-color-white: #ffffff;\n}\n\n/* Docs Light mode colors. */\n:root[data-theme='light'] {\n  --sl-color-bg: #ffffff;\n  --sl-color-bg-sidebar: #ffffff;\n  --sl-color-bg-inline-code: #F3F2FF;\n  --color-code-border: #BDADFF;\n  --color-code-text: #7B5AFF;\n  --sl-color-accent-low: #d0d3fc;\n  --sl-color-accent: #6049e8;\n  --sl-color-accent-high: #2c2669;\n  --sl-color-black: #ffffff;\n  --sl-color-docs-stroke: #DBDBDB;\n  --sl-color-gray-1: #1c2541;\n  --sl-color-gray-2: #2d3754;\n  --sl-color-gray-3: #4c5776;\n  --sl-color-gray-4: #7e8bac;\n  --sl-color-gray-5: #bbc2d4;\n  --sl-color-gray-6: #e8eefc;\n  --color-toc-accent: #7B5AFF;\n  --color-toc-background: #E4E9FC;\n  --color-toc-text: #777888;\n  --sl-color-white: #131824;\n}\n\n@layer base {\n  * {\n    box-sizing: border-box;\n  }\n\n  body {\n    margin: 0;\n    padding: 0;\n  }\n\n  /* Hide scrollbar for Chrome, Safari and Opera */\n  .no-scrollbar::-webkit-scrollbar {\n    display: none;\n  }\n\n  /* Hide scrollbar for IE, Edge and Firefox */\n  .no-scrollbar {\n      -ms-overflow-style: none;  /* IE and Edge */\n      scrollbar-width: none;  /* Firefox */\n  }\n\n  @media (width <= 375px) {\n    .primary-button, .secondary-button {\n        padding: 10px 10px;\n    }\n  }\n}\n\n/* Handle all CSS overrides in a single layer to make it easier to manage and update.\n   TODO: Better organize the CSS here and split into appropriate layers as needed.\n*/\n@layer components {\n\n  /* Baseline styles\n  These should really be in the base layer, but for now we want them to apply at this layer.\n  */\n  h1, h2, h3, h4, h5, h6 {\n    font-weight: 400;\n  }\n\n  /* Header */\n  header {\n    background-color: transparent;\n    border: none;\n    height: var(--sl-nav-height);\n  }\n\n  .header {\n    padding: 0 !important;\n  }\n\n  starlight-menu-button button {\n    top: 92px;\n    position: fixed;\n  }\n\n  @media (max-width: 767px) {\n    starlight-menu-button button {\n      top: 70px;\n    }\n  }\n\n  .searchbar {\n    display: flex;\n    align-items: center;\n    justify-content: start;\n    gap: 8px;\n    width: 280px;\n    height: 40px;\n    padding: 6px 12px;\n    background-color: rgba(255,255,255,0.05);\n    border: 1px solid rgba(255,255,255,0.1);\n    border-radius: 6px;\n  }\n\n  .menu-icon {\n      display: none;\n  }\n\n  @media (min-width: 768px) and (max-width: 1023px) {\n      .menu-icon {\n          display: inline-flex;\n      }\n  }\n\n  /* Table of contents (right-hand side in-page guide) */\n\n  mobile-starlight-toc {\n    display: block;\n  }\n\n  mobile-starlight-toc nav {\n    left: 0;\n    position: relative;\n    top: 0;\n  }\n\n  starlight-toc h2 {\n    background-color: var(--color-toc-background);\n    color: var(--color-code-text);\n    display: inline-block;\n    font-family: var(--font-mono);\n    font-size: 12px;\n    text-transform: uppercase;\n    user-select: none;\n    padding: 4px 2px;\n  }\n\n  starlight-toc li {\n    font-family: var(--font-sans);\n    margin: 0.2rem;\n  }\n\n  starlight-toc li a {\n    color: var(--color-toc-text);\n  }\n\n  starlight-toc li a[aria-current=\"true\"],\n  starlight-toc li a:hover {\n    color: var(--color-toc-accent);\n    font-weight: 500;\n  }\n\n  .main-frame {\n    margin-top: var(--sl-nav-height);\n    padding-top: 0;\n  }\n\n  .social-icon {\n    color: var(--color-nav-link) !important;\n    cursor: pointer;\n    text-decoration: none;\n  }\n\n  .social-icon:hover {\n    color: var(--color-nav-link-hover) !important;\n    cursor: pointer;\n  }\n\n  /* Main body */\n\n  .sl-markdown-content h2:not(:first-child) {\n    margin-top: 1.5em;\n  }\n\n  .sl-markdown-content .expressive-code {\n    margin: 1.5em 0;\n  }\n\n  .sl-markdown-content aside {\n    border-left: 5px solid var(--sl-color-asides-border);\n    margin: 1.5em 0;\n  }\n\n  h1 code,\n  h2 code,\n  h3 code,\n  h4 code,\n  h5 code,\n  h6 code,\n  p code {\n    border: 1px solid var(--color-code-border);\n    border-radius: 6px;\n    color: var(--color-code-text);\n  }\n\n  #starlight__sidebar ul li {\n    margin: 0;\n  }\n\n  #starlight__sidebar ul li ul li {\n    padding-left: 1em;\n  }\n\n  #starlight__sidebar details  {\n    padding-left: 0;\n    margin-bottom: 10px;\n  }\n\n  #starlight__sidebar summary {\n    margin-top: 5px;\n    padding-left: 0;\n  }\n\n  #starlight__sidebar details details details {\n    margin-bottom: 0;\n  }\n\n  #starlight__sidebar details details details summary {\n    margin-top: 0;\n    padding: 0.3em var(--sl-sidebar-item-padding-inline);\n  }\n\n  /* Open Source Cards */\n  .opensourcecard {\n    border-left: 0;\n  }\n\n  .opensourcecard:last-child {\n    border-right: 0;\n  }\n\n  /* Main pane */\n\n  .main-pane .sl-container {\n    margin-inline: 0;\n  }\n\n  .main-pane p {\n    line-height: 1.6;\n    margin-bottom: 1.5rem;\n  }\n\n  .main-pane starlight-file-tree {\n    margin-bottom: 1.8rem;\n  }\n\n  .main-pane .code {\n    font-size: calc(14 / 16 * 1rem);\n  }\n\n  /* Sidebar */\n\n  .sidebar-pane {\n    border-inline-end: 1px dashed var(--sl-color-docs-stroke);\n  }\n\n  .content-panel {\n    border-top: 1px dashed var(--sl-color-docs-stroke);\n  }\n\n  .right-sidebar {\n    border-inline-start: 1px dashed var(--sl-color-docs-stroke);\n  }\n\n  /* Cards */\n\n  article.card {\n    background-color: transparent;\n    border: 1px solid var(--sl-color-gray-5);\n    margin: 1.5em 0;\n  }\n}\n\n@media (width < 768px) {\n  :root {\n    --sl-nav-height: 125px;\n  }\n}\n\n@media (1280px > width >= 768px) {\n  :root {\n    --sl-nav-height: 148px;\n  }\n}\n\n/* FAQ */\n\n.accordion-item:last-of-type {\n  border-bottom: dashed 1px var(--color-gray-3);\n}\n"
  },
  {
    "path": "docs/src/styles/lists.css",
    "content": "\n/* List styling to ensure proper rendering */\nol {\n  list-style-type: decimal;\n  margin: 1em 0;\n  padding-left: 3em;\n}\n\nol ol {\n  list-style-type: lower-alpha;\n  margin: 0.5em 0;\n  padding-left: 2.5em;\n}\n\nol ol ol {\n  list-style-type: lower-roman;\n  margin: 0.5em 0;\n  padding-left: 2.5em;\n}\n\n/* Override numbering for sl-steps lists which have their own numbering */\nol.sl-steps {\n  list-style-type: none;\n}\n\nol.sl-steps ol {\n  list-style-type: none;\n}\n\nol.sl-steps ol ol {\n  list-style-type: none;\n}\n\nli {\n  margin: 0.5em 0;\n  line-height: 1.6;\n}\n\n/* Ensure list items have proper spacing and alignment */\nol li {\n  display: list-item;\n  text-align: left;\n}\n"
  },
  {
    "path": "docs/src/styles/starlight-right-sidebar.css",
    "content": ".right-sidebar-panel .sl-container {\n  max-height: calc(100vh - 300px);\n  min-height: 200px;\n  overflow-y: auto;\n  width: 100%;\n}\n"
  },
  {
    "path": "docs/src/styles/starlight-search.css",
    "content": "\n/* Search Box */\n@media (width < 800px) {\n  #search-and-buttons {\n    width: calc(100% - 50px) !important;\n  }\n}\n\n/* Search Box NOT inside Dialog  */\nsite-search>button {\n  display: flex;\n  width: 100%;\n}\n\nsite-search button svg {\n  color: var(--color-gray-1);\n}\n\n/* Search Dialog Box */\nsite-search dialog[open] {\n  background: var(--color-dialog-bg);\n  border-radius: .5rem;\n  display: flex;\n  height: max-content;\n  margin: 4rem auto auto;\n  max-height: calc(100% - 8rem);\n  max-width: 40rem;\n  min-height: 15rem;\n  width: 90%;\n}\n\nbutton[data-close-modal] {\n  color: black;\n  display: none;\n}\n\nsite-search .dialog-frame {\n  overflow-x: hidden;\n  padding: 1.5rem;\n}\n\n#starlight__search > div:nth-of-type(2) {\n  display: none;\n}\n\nsite-search .pagefind-ui {\n  color: oklch(0.37 0.013 285.805);\n}\n\nsite-search .pagefind-ui__drawer {\n  display: flex;\n  flex-direction: row;\n  flex-wrap: wrap;\n}\n\nsite-search .pagefind-ui__results {\n  padding-left: 0;\n}\n\nsite-search .pagefind-ui__result {\n  padding: 20px 10px;\n}\n\n/* Site Search Dark Mode */\nhtml[data-theme=\"dark\"] site-search input,\nhtml[data-theme=\"dark\"] site-search .pagefind-ui__result-link  {\n  color: rgb(19, 24, 36);\n}\n\n\n/*\n * Starlight Overrides\n *\n * Context: Starlight components apply different styles based on dark/light mode state.\n * This causes visual issues because the docs pages have dark mode support, but the homepage nav does not.\n * This override fixes an issue on the homepage nav by styling CMD+K in a consistent way across all pages in both * modes.\n *\n * Future Considerations:\n * - If homepage gains dark mode support, this override may need adjustment\n * - Consider if a more systematic theming approach is needed long-term\n */\nsite-search button kbd {\n  background-color: transparent;\n  font-size: var(--sl-text-sm);\n  gap: 0;\n  margin: 0 0 0 auto;\n}\n\n.pagefind-ui__message {\n  padding: 1.5em 0;\n  height: auto;\n}\n\n#starlight__search .pagefind-ui__form:before {\n  background-color: black !important;\n}\n\ninput.pagefind-ui__search-input {\n  background-color: white;\n  color: black;\n  border: 1px solid oklch(0.871 0.006 286.286);\n  border-radius: 8px;\n  max-width: 590px;\n  padding-left: 50px;\n  width: -webkit-fill-available;\n}\n\ninput.pagefind-ui__search-input::placeholder {\n  color: #666;\n}\n\ninput.pagefind-ui__search-input:focus {\n  outline: 2px solid #448daf;\n  outline-offset: 2px;\n}\n\n.pagefind-ui__search-clear {\n  background-color: transparent;\n  border: none;\n  padding: 10px;\n  right: 0;\n  width: 40px;\n}\n\n.pagefind-ui__search-clear:before {\n  background-color: black;\n  content: \"\";\n  display: block;\n  height: 100%;\n  margin-bottom: 20px;\n  width: 100%;\n  -webkit-mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m13.41 12 6.3-6.29a1 1 0 1 0-1.42-1.42L12 10.59l-6.29-6.3a1 1 0 0 0-1.42 1.42l6.3 6.29-6.3 6.29a1 1 0 0 0 .33 1.64 1 1 0 0 0 1.09-.22l6.29-6.3 6.29 6.3a1 1 0 0 0 1.64-.33 1 1 0 0 0-.22-1.09L13.41 12Z'/%3E%3C/svg%3E\") center / 100% no-repeat;\n  mask: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='m13.41 12 6.3-6.29a1 1 0 1 0-1.42-1.42L12 10.59l-6.29-6.3a1 1 0 0 0-1.42 1.42l6.3 6.29-6.3 6.29a1 1 0 0 0 .33 1.64 1 1 0 0 0 1.09-.22l6.29-6.3 6.29 6.3a1 1 0 0 0 1.64-.33 1 1 0 0 0-.22-1.09L13.41 12Z'/%3E%3C/svg%3E\") center / 100% no-repeat;\n}\n\n#starlight__search .pagefind-ui__result-title:not(:where(.pagefind-ui__result-nested *)),\n#starlight__search .pagefind-ui__result-nested,\n.pagefind-ui__result-title,\n.pagefind-ui__result-nested {\n  background-color: transparent;\n}\n\n.pagefind-ui__result {\n  background: white;\n  color: black;\n  border-radius: 8px;\n  margin-bottom: 20px;\n}\n\n/* Fix search result icons - same approach as text fix */\n.pagefind-ui__result svg,\n.pagefind-ui__result svg path {\n  fill: black;\n}\n\n.pagefind-ui__result-link,\n.pagefind-ui__result-excerpt {\n  color: black;\n}\n\n.pagefind-ui__result-title a {\n  color: black;\n}\n\n.pagefind-ui__result-title a:hover {\n  color: #448daf;\n}\n\n/* Hide Light/Dark Mode Toggle in Hamburger Menu */\ndiv.mobile-preferences {\n  display: none;\n}\n\n/* Fix to site-search button area smaller on mobile */\n@media (width <= 768px) {\n  body.tg site-search > button {\n    width: 100%;\n  }\n}\n"
  },
  {
    "path": "docs/tailwind.config.mjs",
    "content": "/* This is the global Tailwind config for both Starlight standard pages, custom pages, and components.\n\n   The file contents are notably minimal because we're currently defining most of our styles in global.css. At some point, it would be good\n   to migrate the many styles defined there into a proper Tailwind theming defined in this config.\n*/\n\n/** @type {import('tailwindcss').Config} */\nexport default {\n  content: [\n    \"./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}\",\n  ],\n  theme: {\n    extend: {},\n  },\n  plugins: [],\n}\n"
  },
  {
    "path": "docs/tests/install_test.sh",
    "content": "#!/usr/bin/env bash\n# Tests for Terragrunt install script\n#\n# Usage:\n#   ./install_test.sh              # Run all tests\n#   ./install_test.sh --quick      # Skip download tests (faster)\n#\n# Requirements: bash 3.2+\n# Note: Download tests require internet connection\n\n# shellcheck disable=SC2317  # Functions are called indirectly via run_test\n\nset -euo pipefail\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nINSTALL_SCRIPT=\"${SCRIPT_DIR}/../public/install\"\n\n# Test counters\nTESTS_RUN=0\nTESTS_PASSED=0\nTESTS_FAILED=0\n\n# Colors\nif [[ -t 1 ]]; then\n    RED=$'\\033[0;31m'\n    GREEN=$'\\033[0;32m'\n    YELLOW=$'\\033[0;33m'\n    NC=$'\\033[0m'\nelse\n    RED=''\n    GREEN=''\n    YELLOW=''\n    NC=''\nfi\n\n# --- Test Helpers ---\npass() {\n    TESTS_PASSED=$((TESTS_PASSED + 1))\n    printf \"${GREEN}✓${NC} %s\\n\" \"$1\"\n    return 0\n}\n\nfail() {\n    TESTS_FAILED=$((TESTS_FAILED + 1))\n    printf \"${RED}✗${NC} %s\\n\" \"$1\"\n    if [[ -n \"${2:-}\" ]]; then\n        printf \"  ${RED}Error: %s${NC}\\n\" \"$2\"\n    fi\n    return 0\n}\n\nrun_test() {\n    local name=\"$1\"\n    shift\n    TESTS_RUN=$((TESTS_RUN + 1))\n    if \"$@\"; then\n        pass \"$name\"\n        return 0\n    else\n        fail \"$name\"\n        return 1\n    fi\n}\n\nskip_test() {\n    printf \"${YELLOW}○${NC} %s (skipped)\\n\" \"$1\"\n    return 0\n}\n\n# --- Unit Tests ---\n\ntest_script_exists() {\n    [[ -f \"$INSTALL_SCRIPT\" ]]\n}\n\ntest_script_executable_syntax() {\n    bash -n \"$INSTALL_SCRIPT\"\n}\n\ntest_help_output() {\n    local output\n    output=$(bash \"$INSTALL_SCRIPT\" --help 2>&1)\n    [[ \"$output\" == *\"Terragrunt Installer\"* ]] &&\n    [[ \"$output\" == *\"--version\"* ]] &&\n    [[ \"$output\" == *\"--dir\"* ]] &&\n    [[ \"$output\" == *\"--force\"* ]] &&\n    [[ \"$output\" == *\"--no-verify-sig\"* ]] &&\n    [[ \"$output\" == *\"--verify-cosign\"* ]] &&\n    [[ \"$output\" == *\"--no-verify\"* ]]\n}\n\ntest_help_exit_code() {\n    bash \"$INSTALL_SCRIPT\" --help >/dev/null 2>&1\n}\n\ntest_invalid_option_fails() {\n    ! bash \"$INSTALL_SCRIPT\" --invalid-option 2>/dev/null\n}\n\ntest_missing_version_arg_fails() {\n    ! bash \"$INSTALL_SCRIPT\" -v 2>/dev/null\n}\n\ntest_missing_dir_arg_fails() {\n    ! bash \"$INSTALL_SCRIPT\" -d 2>/dev/null\n}\n\n# Test OS detection by sourcing functions\ntest_os_detection() {\n    local os\n    os=$(uname -s)\n    case \"$os\" in\n        Darwin|Linux) return 0 ;;\n        *) return 1 ;;\n    esac\n}\n\n# Test arch detection\ntest_arch_detection() {\n    local arch\n    arch=$(uname -m)\n    case \"$arch\" in\n        x86_64|amd64|aarch64|arm64|i386|i686) return 0 ;;\n        *) return 1 ;;\n    esac\n}\n\n# Test that sha256sum or shasum exists\ntest_checksum_tool_exists() {\n    command -v sha256sum &>/dev/null || command -v shasum &>/dev/null\n}\n\n# Test curl exists\ntest_curl_exists() {\n    command -v curl &>/dev/null\n}\n\n# --- Network Connectivity Check ---\n\ncheck_network_connectivity() {\n    # Quick check if we can reach GitHub\n    curl -fsI --connect-timeout 5 \"https://github.com\" >/dev/null 2>&1\n}\n\n# --- Integration Tests (require network) ---\n\ntest_fetch_latest_version() {\n    local version\n    # Use redirect method (same as install)\n    local redirect_url\n    redirect_url=$(curl -fsI \"https://github.com/gruntwork-io/terragrunt/releases/latest\" 2>/dev/null | grep -i '^location:' | tr -d '\\r')\n    version=$(echo \"$redirect_url\" | grep -oE 'v[0-9]+\\.[0-9]+\\.[0-9]+' | head -1)\n    [[ \"$version\" =~ ^v[0-9]+\\.[0-9]+\\.[0-9]+ ]]\n}\n\ntest_install_specific_version() {\n    local tmpdir\n    tmpdir=$(mktemp -d)\n    # shellcheck disable=SC2064  # Intentional: expand tmpdir now, not at trap time\n    trap \"rm -rf '$tmpdir'\" RETURN\n\n    bash \"$INSTALL_SCRIPT\" -d \"$tmpdir\" -v v0.72.5 --no-verify-sig >/dev/null 2>&1 &&\n    [[ -f \"$tmpdir/terragrunt\" ]] &&\n    [[ -x \"$tmpdir/terragrunt\" ]] &&\n    \"$tmpdir/terragrunt\" --version 2>&1 | grep -q \"v0.72.5\"\n}\n\ntest_install_rc_version() {\n    # Requires gpg (GPG signature verification is default)\n    command -v gpg &>/dev/null || { echo \"gpg required\"; return 1; }\n\n    local tmpdir\n    tmpdir=$(mktemp -d)\n    # shellcheck disable=SC2064  # Intentional: expand tmpdir now, not at trap time\n    trap \"rm -rf '$tmpdir'\" RETURN\n\n    bash \"$INSTALL_SCRIPT\" -d \"$tmpdir\" -v v0.98.0-rc2026011601 >/dev/null 2>&1 &&\n    [[ -f \"$tmpdir/terragrunt\" ]] &&\n    [[ -x \"$tmpdir/terragrunt\" ]] &&\n    \"$tmpdir/terragrunt\" --version 2>&1 | grep -q \"v0.98.0-rc2026011601\"\n}\n\ntest_install_latest_version() {\n    # Requires gpg (GPG signature verification is default)\n    command -v gpg &>/dev/null || { echo \"gpg required\"; return 1; }\n\n    local tmpdir\n    tmpdir=$(mktemp -d)\n    # shellcheck disable=SC2064  # Intentional: expand tmpdir now, not at trap time\n    trap \"rm -rf '$tmpdir'\" RETURN\n\n    bash \"$INSTALL_SCRIPT\" -d \"$tmpdir\" >/dev/null 2>&1 &&\n    [[ -f \"$tmpdir/terragrunt\" ]] &&\n    [[ -x \"$tmpdir/terragrunt\" ]] &&\n    \"$tmpdir/terragrunt\" --version 2>&1 | grep -qE \"^terragrunt version v[0-9]+\"\n}\n\ntest_install_already_exists_fails() {\n    local tmpdir\n    tmpdir=$(mktemp -d)\n    # shellcheck disable=SC2064  # Intentional: expand tmpdir now, not at trap time\n    trap \"rm -rf '$tmpdir'\" RETURN\n\n    # First install\n    bash \"$INSTALL_SCRIPT\" -d \"$tmpdir\" -v v0.72.5 --no-verify-sig >/dev/null 2>&1 || return 1\n\n    # Second install without --force should fail\n    ! bash \"$INSTALL_SCRIPT\" -d \"$tmpdir\" -v v0.72.5 --no-verify-sig 2>/dev/null\n}\n\ntest_install_force_overwrites() {\n    local tmpdir\n    tmpdir=$(mktemp -d)\n    # shellcheck disable=SC2064  # Intentional: expand tmpdir now, not at trap time\n    trap \"rm -rf '$tmpdir'\" RETURN\n\n    # First install\n    bash \"$INSTALL_SCRIPT\" -d \"$tmpdir\" -v v0.72.5 --no-verify-sig >/dev/null 2>&1 || return 1\n\n    # Second install with --force should succeed\n    bash \"$INSTALL_SCRIPT\" -d \"$tmpdir\" -v v0.72.5 --force --no-verify-sig >/dev/null 2>&1\n}\n\ntest_install_creates_directory() {\n    local tmpdir\n    tmpdir=$(mktemp -d)\n    local install_dir=\"${tmpdir}/new/nested/dir\"\n    # shellcheck disable=SC2064  # Intentional: expand tmpdir now, not at trap time\n    trap \"rm -rf '$tmpdir'\" RETURN\n\n    # Should auto-create the directory\n    bash \"$INSTALL_SCRIPT\" -d \"$install_dir\" -v v0.72.5 --no-verify-sig >/dev/null 2>&1 &&\n    [[ -f \"${install_dir}/terragrunt\" ]]\n}\n\ntest_install_invalid_version_fails() {\n    local tmpdir\n    tmpdir=$(mktemp -d)\n    # shellcheck disable=SC2064  # Intentional: expand tmpdir now, not at trap time\n    trap \"rm -rf '$tmpdir'\" RETURN\n\n    ! bash \"$INSTALL_SCRIPT\" -d \"$tmpdir\" -v invalid --no-verify-sig 2>/dev/null\n}\n\ntest_install_no_verify() {\n    local tmpdir\n    tmpdir=$(mktemp -d)\n    # shellcheck disable=SC2064  # Intentional: expand tmpdir now, not at trap time\n    trap \"rm -rf '$tmpdir'\" RETURN\n\n    local output\n    output=$(bash \"$INSTALL_SCRIPT\" -d \"$tmpdir\" -v v0.72.5 --no-verify --no-verify-sig 2>&1)\n    [[ \"$output\" == *\"Skipping checksum verification\"* ]] &&\n    [[ -f \"$tmpdir/terragrunt\" ]]\n}\n\ntest_install_no_verification_at_all() {\n    local tmpdir\n    tmpdir=$(mktemp -d)\n    # shellcheck disable=SC2064  # Intentional: expand tmpdir now, not at trap time\n    trap \"rm -rf '$tmpdir'\" RETURN\n\n    # Install with no checksum and no signature verification\n    local output\n    output=$(bash \"$INSTALL_SCRIPT\" -d \"$tmpdir\" -v v0.72.5 --no-verify --no-verify-sig 2>&1)\n    [[ \"$output\" == *\"Skipping checksum verification\"* ]] &&\n    [[ \"$output\" != *\"SHA256 checksum verified\"* ]] &&\n    [[ \"$output\" != *\"Signature verified\"* ]] &&\n    [[ -f \"$tmpdir/terragrunt\" ]] &&\n    [[ -x \"$tmpdir/terragrunt\" ]]\n}\n\ntest_checksum_verification() {\n    local tmpdir\n    tmpdir=$(mktemp -d)\n    # shellcheck disable=SC2064  # Intentional: expand tmpdir now, not at trap time\n    trap \"rm -rf '$tmpdir'\" RETURN\n\n    local output\n    output=$(bash \"$INSTALL_SCRIPT\" -d \"$tmpdir\" -v v0.72.5 --no-verify-sig 2>&1)\n    [[ \"$output\" == *\"SHA256 checksum verified\"* ]]\n}\n\ntest_old_version_skips_signature() {\n    # Requires gpg (GPG signature verification is default)\n    command -v gpg &>/dev/null || { echo \"gpg required\"; return 1; }\n\n    local tmpdir\n    tmpdir=$(mktemp -d)\n    # shellcheck disable=SC2064  # Intentional: expand tmpdir now, not at trap time\n    trap \"rm -rf '$tmpdir'\" RETURN\n\n    # v0.72.5 is below MIN_SIGNED_VERSION (0.98.0), should skip signature gracefully\n    local output\n    output=$(bash \"$INSTALL_SCRIPT\" -d \"$tmpdir\" -v v0.72.5 2>&1)\n    [[ \"$output\" == *\"Skipping signature verification: not available for versions older than\"* ]]\n}\n\ntest_signature_enabled_by_default() {\n    # Requires gpg (GPG signature verification is default)\n    command -v gpg &>/dev/null || { echo \"gpg required\"; return 1; }\n\n    local tmpdir\n    tmpdir=$(mktemp -d)\n    # shellcheck disable=SC2064  # Intentional: expand tmpdir now, not at trap time\n    trap \"rm -rf '$tmpdir'\" RETURN\n\n    # GPG signature verification is enabled by default\n    # Use RC version which has signatures\n    local output\n    output=$(bash \"$INSTALL_SCRIPT\" -d \"$tmpdir\" -v v0.98.0-rc2026011601 2>&1)\n    [[ \"$output\" == *\"Verifying GPG signature\"* ]] &&\n    [[ \"$output\" == *\"Signature verified\"* ]]\n}\n\ntest_no_verify_sig_skips_signature() {\n    local tmpdir\n    tmpdir=$(mktemp -d)\n    # shellcheck disable=SC2064  # Intentional: expand tmpdir now, not at trap time\n    trap \"rm -rf '$tmpdir'\" RETURN\n\n    # With --no-verify-sig, signature verification should be skipped\n    local output\n    output=$(bash \"$INSTALL_SCRIPT\" -d \"$tmpdir\" -v v0.72.5 --no-verify-sig 2>&1)\n    [[ \"$output\" != *\"Signature verified\"* ]] &&\n    [[ \"$output\" != *\"Using GPG\"* ]] &&\n    [[ \"$output\" != *\"Using Cosign\"* ]]\n}\n\ntest_cosign_signature_verification() {\n    # Requires cosign\n    command -v cosign &>/dev/null || { echo \"cosign required\"; return 1; }\n\n    local tmpdir\n    tmpdir=$(mktemp -d)\n    # shellcheck disable=SC2064  # Intentional: expand tmpdir now, not at trap time\n    trap \"rm -rf '$tmpdir'\" RETURN\n\n    # Use RC version which has signatures\n    local output\n    output=$(bash \"$INSTALL_SCRIPT\" -d \"$tmpdir\" -v v0.98.0-rc2026011601 --verify-cosign 2>&1)\n    [[ \"$output\" == *\"Verifying Cosign signature\"* ]] &&\n    [[ \"$output\" == *\"Signature verified\"* ]]\n}\n\ntest_gpg_is_default_signature_method() {\n    # Requires gpg\n    command -v gpg &>/dev/null || { echo \"gpg required\"; return 1; }\n\n    local tmpdir\n    tmpdir=$(mktemp -d)\n    # shellcheck disable=SC2064  # Intentional: expand tmpdir now, not at trap time\n    trap \"rm -rf '$tmpdir'\" RETURN\n\n    # GPG is default method - verify it's used without any flags\n    local output\n    output=$(bash \"$INSTALL_SCRIPT\" -d \"$tmpdir\" -v v0.98.0-rc2026011601 2>&1)\n    [[ \"$output\" == *\"Verifying GPG signature\"* ]] &&\n    [[ \"$output\" == *\"Signature verified\"* ]]\n}\n\n# --- Platform-Specific Tests ---\n\ntest_macos_shasum_fallback() {\n    # This test verifies the shasum fallback logic works\n    # On Linux with sha256sum, we simulate by checking the code path exists\n    if command -v sha256sum &>/dev/null; then\n        # On Linux, verify sha256sum is used\n        return 0\n    elif command -v shasum &>/dev/null; then\n        # On macOS, verify shasum works\n        echo \"test\" | shasum -a 256 >/dev/null 2>&1\n    else\n        return 1\n    fi\n}\n\ntest_temp_directory_cleanup() {\n    local install_dir\n    install_dir=$(mktemp -d)\n    # shellcheck disable=SC2064  # Intentional: expand install_dir now, not at trap time\n    trap \"rm -rf '$install_dir'\" RETURN\n\n    # Count terragrunt-specific temp dirs before\n    local before\n    before=$(find \"${TMPDIR:-/tmp}\" -maxdepth 1 -name 'terragrunt-install.*' -type d 2>/dev/null | wc -l)\n\n    bash \"$INSTALL_SCRIPT\" -d \"$install_dir\" -v v0.72.5 --no-verify-sig >/dev/null 2>&1\n\n    # Verify no new terragrunt-specific temp dirs remain (script uses trap to cleanup)\n    local after\n    after=$(find \"${TMPDIR:-/tmp}\" -maxdepth 1 -name 'terragrunt-install.*' -type d 2>/dev/null | wc -l)\n    [[ \"$after\" -le \"$before\" ]]\n}\n\n# --- Main ---\n\nmain() {\n    local quick_mode=false\n    if [[ \"${1:-}\" == \"--quick\" ]]; then\n        quick_mode=true\n    fi\n\n    echo \"==========================================\"\n    echo \"Terragrunt Install Script Tests\"\n    echo \"==========================================\"\n    echo \"\"\n\n    echo \"--- Basic Tests ---\"\n    run_test \"Script exists\" test_script_exists\n    run_test \"Script has valid syntax\" test_script_executable_syntax\n    run_test \"Help output contains expected content\" test_help_output\n    run_test \"Help exits with code 0\" test_help_exit_code\n    run_test \"Invalid option fails\" test_invalid_option_fails\n    run_test \"Missing -v argument fails\" test_missing_version_arg_fails\n    run_test \"Missing -d argument fails\" test_missing_dir_arg_fails\n    echo \"\"\n\n    echo \"--- Environment Tests ---\"\n    run_test \"OS is supported ($(uname -s))\" test_os_detection\n    run_test \"Architecture is supported ($(uname -m))\" test_arch_detection\n    run_test \"Checksum tool exists (sha256sum/shasum)\" test_checksum_tool_exists\n    run_test \"curl is installed\" test_curl_exists\n    run_test \"Platform checksum tool works\" test_macos_shasum_fallback\n    echo \"\"\n\n    # Check network connectivity for integration tests\n    local skip_reason=\"\"\n    if [[ \"$quick_mode\" == true ]]; then\n        skip_reason=\"quick mode\"\n    elif ! check_network_connectivity; then\n        skip_reason=\"no network connectivity\"\n    fi\n\n    if [[ -n \"$skip_reason\" ]]; then\n        echo \"--- Integration Tests (SKIPPED - ${skip_reason}) ---\"\n        skip_test \"Fetch latest version from GitHub\"\n        skip_test \"Install specific version\"\n        skip_test \"Install RC version\"\n        skip_test \"Install latest version\"\n        skip_test \"Install fails when already exists\"\n        skip_test \"Install with --force overwrites\"\n        skip_test \"Install creates directory\"\n        skip_test \"Install with invalid version fails\"\n        skip_test \"Install with --no-verify skips checksum\"\n        skip_test \"Install with no verification at all\"\n        skip_test \"Checksum verification works\"\n        skip_test \"Old version skips signature verification\"\n        skip_test \"Signature enabled by default\"\n        skip_test \"--no-verify-sig skips signature\"\n        skip_test \"Cosign signature verification\"\n        skip_test \"GPG is default signature method\"\n        skip_test \"Temp directory cleanup\"\n    else\n        echo \"--- Integration Tests (require network) ---\"\n        run_test \"Fetch latest version from GitHub\" test_fetch_latest_version\n        run_test \"Install specific version (v0.72.5)\" test_install_specific_version\n        run_test \"Install RC version (v0.98.0-rc2026011601)\" test_install_rc_version\n        run_test \"Install latest version\" test_install_latest_version\n        run_test \"Install fails when already exists\" test_install_already_exists_fails\n        run_test \"Install with --force overwrites\" test_install_force_overwrites\n        run_test \"Install creates directory\" test_install_creates_directory\n        run_test \"Install with invalid version fails\" test_install_invalid_version_fails\n        run_test \"Install with --no-verify skips checksum\" test_install_no_verify\n        run_test \"Install with no verification at all\" test_install_no_verification_at_all\n        run_test \"Checksum verification works\" test_checksum_verification\n        run_test \"Old version skips signature verification\" test_old_version_skips_signature\n        run_test \"Signature enabled by default\" test_signature_enabled_by_default\n        run_test \"--no-verify-sig skips signature\" test_no_verify_sig_skips_signature\n        run_test \"Cosign signature verification\" test_cosign_signature_verification\n        run_test \"GPG is default signature method\" test_gpg_is_default_signature_method\n        run_test \"Temp directory cleanup\" test_temp_directory_cleanup\n    fi\n    echo \"\"\n\n    echo \"==========================================\"\n    echo \"Results: ${TESTS_PASSED}/${TESTS_RUN} passed\"\n    if [[ $TESTS_FAILED -gt 0 ]]; then\n        echo \"${RED}${TESTS_FAILED} test(s) failed${NC}\"\n        exit 1\n    else\n        echo \"${GREEN}All tests passed!${NC}\"\n        exit 0\n    fi\n}\n\nmain \"$@\"\n"
  },
  {
    "path": "docs/tsconfig.json",
    "content": "{\n  \"extends\": \"astro/tsconfigs/strict\",\n  \"include\": [\n    \".astro/types.d.ts\",\n    \"**/*\"\n  ],\n  \"exclude\": [\n    \"dist\"\n  ],\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@assets/*\": [\n        \"src/assets/*\"\n      ],\n      \"@components/*\": [\n        \"src/components/*\"\n      ],\n      \"@layouts/*\": [\n        \"src/layouts/*\"\n      ],\n      \"@lib/*\": [\n        \"src/lib/*\"\n      ],\n      \"@styles/*\": [\n        \"src/styles/*\"\n      ],\n      \"@ui/*\": [\n        \"src/components/ui/*\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "docs/vercel.json",
    "content": "{\n  \"buildCommand\": \"bun run build\",\n  \"installCommand\": \"bun install\",\n  \"framework\": \"astro\",\n  \"rewrites\": [\n    {\n      \"source\": \"/api/v1/compatibility\",\n      \"has\": [{ \"type\": \"query\", \"key\": \"tool\", \"value\": \"opentofu\" }],\n      \"destination\": \"/api/v1/compatibility/opentofu\"\n    },\n    {\n      \"source\": \"/api/v1/compatibility\",\n      \"has\": [{ \"type\": \"query\", \"key\": \"tool\", \"value\": \"terraform\" }],\n      \"destination\": \"/api/v1/compatibility/terraform\"\n    },\n    {\n      \"source\": \"/api/v1/compatibility\",\n      \"destination\": \"/api/v1/compatibility/index\"\n    }\n  ],\n  \"redirects\": [\n    {\n      \"source\": \"/lp/:slug*\",\n      \"destination\": \"https://terragrunt.com/lp/:slug*\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/terragrunt-ambassador/:slug*\",\n      \"destination\": \"https://terragrunt.com/terragrunt-ambassador/:slug*\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/terragrunt-scale/:slug*\",\n      \"destination\": \"https://terragrunt.com/terragrunt-scale/:slug*\",\n      \"permanent\": true\n    },\n    {\n      \"source\": \"/contact-tgs/:slug*\",\n      \"destination\": \"https://terragrunt.com/contact-tgs/:slug*\",\n      \"permanent\": true\n    }\n  ],\n  \"headers\": [\n    {\n      \"source\": \"/pagefind/(.*)\",\n      \"headers\": [\n        {\n          \"key\": \"Access-Control-Allow-Origin\",\n          \"value\": \"https://terragrunt.com\"\n        },\n        {\n          \"key\": \"Access-Control-Allow-Methods\",\n          \"value\": \"GET, OPTIONS\"\n        },\n        {\n          \"key\": \"Access-Control-Allow-Headers\",\n          \"value\": \"Content-Type\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/gruntwork-io/terragrunt\n\ngo 1.26\n\nrequire (\n\tcloud.google.com/go/storage v1.61.3\n\tdario.cat/mergo v1.0.2\n\tgithub.com/NYTimes/gziphandler v1.1.1\n\tgithub.com/ProtonMail/go-crypto v1.4.1\n\tgithub.com/aws/aws-sdk-go-v2 v1.41.4\n\tgithub.com/charmbracelet/bubbles v0.21.0\n\tgithub.com/charmbracelet/bubbletea v1.3.5\n\tgithub.com/charmbracelet/glamour v0.8.0\n\tgithub.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834\n\tgithub.com/creack/pty v1.1.24\n\tgithub.com/fatih/structs v1.1.0\n\tgithub.com/getsops/sops/v3 v3.12.2\n\tgithub.com/gitsight/go-vcsurl v1.0.1\n\tgithub.com/go-errors/errors v1.5.1\n\tgithub.com/gofrs/flock v0.13.0\n\tgithub.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/gruntwork-io/boilerplate v0.10.1\n\tgithub.com/gruntwork-io/go-commons v0.17.2\n\tgithub.com/gruntwork-io/terragrunt-engine-go v0.1.0\n\tgithub.com/gruntwork-io/terratest v0.51.0 // indirect\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2\n\tgithub.com/hashicorp/go-getter v1.8.5\n\tgithub.com/hashicorp/go-getter/v2 v2.2.3\n\tgithub.com/hashicorp/go-hclog v1.6.3\n\tgithub.com/hashicorp/go-multierror v1.1.1\n\tgithub.com/hashicorp/go-plugin v1.7.0\n\tgithub.com/hashicorp/go-safetemp v1.0.0\n\tgithub.com/hashicorp/go-version v1.8.0\n\tgithub.com/hashicorp/hcl/v2 v2.24.0\n\n\t// Many functions of terraform was converted to internal to avoid use as a library after v0.15.3. This means that we\n\t// can't use terraform as a library after v0.15.3, so we pull that in here.\n\tgithub.com/hashicorp/terraform v0.15.3\n\tgithub.com/hashicorp/terraform-svchost v0.2.0\n\tgithub.com/huandu/go-clone v1.7.3\n\tgithub.com/labstack/echo/v4 v4.15.1\n\tgithub.com/mattn/go-isatty v0.0.20\n\tgithub.com/mattn/go-zglob v0.0.6\n\tgithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d\n\tgithub.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db\n\tgithub.com/mitchellh/go-homedir v1.1.0\n\tgithub.com/mitchellh/go-wordwrap v1.0.1\n\tgithub.com/mitchellh/mapstructure v1.5.0\n\tgithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/posener/complete v1.2.3\n\tgithub.com/puzpuzpuz/xsync/v3 v3.5.1\n\tgithub.com/sirupsen/logrus v1.9.4\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/urfave/cli/v2 v2.27.7\n\tgithub.com/zclconf/go-cty v1.18.0\n\tgo.opentelemetry.io/otel v1.42.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0\n\tgo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0\n\tgo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0\n\tgo.opentelemetry.io/otel/metric v1.42.0\n\tgo.opentelemetry.io/otel/sdk v1.42.0\n\tgo.opentelemetry.io/otel/sdk/metric v1.42.0\n\tgo.opentelemetry.io/otel/trace v1.42.0\n\tgolang.org/x/mod v0.34.0\n\tgolang.org/x/oauth2 v0.36.0\n\tgolang.org/x/sync v0.20.0\n\tgolang.org/x/sys v0.42.0\n\tgolang.org/x/term v0.41.0\n\tgolang.org/x/text v0.35.0\n\tgoogle.golang.org/api v0.272.0\n\tgoogle.golang.org/grpc v1.79.3\n\tgoogle.golang.org/protobuf v1.36.11\n\tgopkg.in/ini.v1 v1.67.1\n)\n\nrequire (\n\tgithub.com/aws/aws-sdk-go-v2/config v1.32.12\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.19.12\n\tgithub.com/aws/aws-sdk-go-v2/service/dynamodb v1.56.2\n\tgithub.com/aws/aws-sdk-go-v2/service/iam v1.53.6\n\tgithub.com/aws/aws-sdk-go-v2/service/s3 v1.97.1\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.41.9\n\tgithub.com/aws/smithy-go v1.24.2\n\tgithub.com/charmbracelet/x/exp/teatest v0.0.0-20250611152503-f53cdd7e01ef\n\tgithub.com/charmbracelet/x/term v0.2.1\n\tgithub.com/docker/go-connections v0.6.0\n\tgithub.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f\n\tgithub.com/go-git/go-git/v6 v6.0.0-20260209124828-a06215dae685\n\tgithub.com/gobwas/glob v0.2.3\n\tgithub.com/invopop/jsonschema v0.13.0\n\tgithub.com/mattn/go-shellwords v1.0.12\n\tgithub.com/spf13/afero v1.15.0\n\tgithub.com/testcontainers/testcontainers-go v0.41.0\n\tgithub.com/wI2L/jsondiff v0.7.0\n\tgithub.com/xeipuuv/gojsonschema v1.2.0\n\tgo.uber.org/mock v0.6.0\n\tgolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa\n)\n\nrequire (\n\tatomicgo.dev/cursor v0.2.0 // indirect\n\tatomicgo.dev/keyboard v0.2.9 // indirect\n\tatomicgo.dev/schedule v0.1.0 // indirect\n\tcel.dev/expr v0.25.1 // indirect\n\tcloud.google.com/go v0.123.0 // indirect\n\tcloud.google.com/go/auth v0.18.2 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect\n\tcloud.google.com/go/compute/metadata v0.9.0 // indirect\n\tcloud.google.com/go/iam v1.5.3 // indirect\n\tcloud.google.com/go/kms v1.26.0 // indirect\n\tcloud.google.com/go/longrunning v0.8.0 // indirect\n\tcloud.google.com/go/monitoring v1.24.3 // indirect\n\tfilippo.io/age v1.3.1 // indirect\n\tfilippo.io/edwards25519 v1.2.0 // indirect\n\tfilippo.io/hpke v0.4.0 // indirect\n\tgithub.com/AlecAivazis/survey/v2 v2.3.7 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 // indirect\n\tgithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect\n\tgithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect\n\tgithub.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect\n\tgithub.com/Masterminds/goutils v1.1.1 // indirect\n\tgithub.com/Masterminds/semver/v3 v3.4.0 // indirect\n\tgithub.com/Masterminds/sprig/v3 v3.3.0 // indirect\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/agext/levenshtein v1.2.3 // indirect\n\tgithub.com/alecthomas/chroma/v2 v2.15.0 // indirect\n\tgithub.com/apparentlymart/go-cidr v1.1.0 // indirect\n\tgithub.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect\n\tgithub.com/apparentlymart/go-versions v1.0.3 // indirect\n\tgithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect\n\tgithub.com/atotto/clipboard v0.1.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.2 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/kms v1.50.3 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect\n\tgithub.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect\n\tgithub.com/aymanbagabas/go-udiff v0.2.0 // indirect\n\tgithub.com/aymerick/douceur v0.2.0 // indirect\n\tgithub.com/bahlo/generic-list-go v0.2.0 // indirect\n\tgithub.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect\n\tgithub.com/blang/semver v3.5.1+incompatible // indirect\n\tgithub.com/bmatcuk/doublestar v1.3.4 // indirect\n\tgithub.com/buger/jsonparser v1.1.1 // indirect\n\tgithub.com/cenkalti/backoff/v4 v4.3.0 // indirect\n\tgithub.com/cenkalti/backoff/v5 v5.0.3 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/charmbracelet/colorprofile v0.3.0 // indirect\n\tgithub.com/charmbracelet/x/ansi v0.8.0 // indirect\n\tgithub.com/charmbracelet/x/cellbuf v0.0.13 // indirect\n\tgithub.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 // indirect\n\tgithub.com/clipperhouse/uax29/v2 v2.2.0 // indirect\n\tgithub.com/cloudflare/circl v1.6.3 // indirect\n\tgithub.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect\n\tgithub.com/containerd/console v1.0.5 // indirect\n\tgithub.com/containerd/errdefs v1.0.0 // indirect\n\tgithub.com/containerd/errdefs/pkg v0.3.0 // indirect\n\tgithub.com/containerd/log v0.1.0 // indirect\n\tgithub.com/containerd/platforms v0.2.1 // indirect\n\tgithub.com/cpuguy83/dockercfg v0.3.2 // indirect\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect\n\tgithub.com/cyphar/filepath-securejoin v0.6.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/dlclark/regexp2 v1.11.5 // indirect\n\tgithub.com/docker/docker v28.5.2+incompatible // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/ebitengine/purego v0.10.0 // indirect\n\tgithub.com/emirpasic/gods v1.18.1 // indirect\n\tgithub.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect\n\tgithub.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect\n\tgithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect\n\tgithub.com/fatih/color v1.19.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.10 // indirect\n\tgithub.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e // indirect\n\tgithub.com/go-git/gcfg/v2 v2.0.2 // indirect\n\tgithub.com/go-jose/go-jose/v4 v4.1.3 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-ole/go-ole v1.3.0 // indirect\n\tgithub.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect\n\tgithub.com/goccy/go-yaml v1.19.2 // indirect\n\tgithub.com/golang-jwt/jwt/v5 v5.3.1 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // 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-jsonnet v0.21.0 // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.19.0 // indirect\n\tgithub.com/gookit/color v1.6.0 // indirect\n\tgithub.com/gorilla/css v1.0.1 // indirect\n\tgithub.com/goware/prefixer v0.0.0-20160118172347-395022866408 // indirect\n\tgithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect\n\tgithub.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.71 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-retryablehttp v0.7.8 // indirect\n\tgithub.com/hashicorp/go-rootcerts v1.0.2 // indirect\n\tgithub.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect\n\tgithub.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect\n\tgithub.com/hashicorp/go-sockaddr v1.0.7 // indirect\n\tgithub.com/hashicorp/go-uuid v1.0.3 // indirect\n\tgithub.com/hashicorp/hcl v1.0.1-vault-7 // indirect\n\tgithub.com/hashicorp/vault/api v1.22.0 // indirect\n\tgithub.com/hashicorp/yamux v0.1.2 // indirect\n\tgithub.com/huandu/xstrings v1.5.0 // indirect\n\tgithub.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.189 // indirect\n\tgithub.com/inancgumus/screen v0.0.0-20190314163918-06e984b86ed3 // indirect\n\tgithub.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect\n\tgithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect\n\tgithub.com/kevinburke/ssh_config v1.4.0 // indirect\n\tgithub.com/klauspost/compress v1.18.5 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.3.0 // indirect\n\tgithub.com/kylelemons/godebug v1.1.0 // indirect\n\tgithub.com/labstack/gommon v0.4.2 // indirect\n\tgithub.com/lib/pq v1.12.0 // indirect\n\tgithub.com/lithammer/fuzzysearch v1.1.8 // indirect\n\tgithub.com/lucasb-eyer/go-colorful v1.2.0 // indirect\n\tgithub.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect\n\tgithub.com/magiconair/properties v1.8.10 // indirect\n\tgithub.com/mailru/easyjson v0.7.7 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-localereader v0.0.1 // indirect\n\tgithub.com/mattn/go-runewidth v0.0.19 // indirect\n\tgithub.com/microcosm-cc/bluemonday v1.0.27 // 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/panicwrap v1.0.0 // indirect\n\tgithub.com/mitchellh/reflectwalk v1.0.2 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/go-archive v0.2.0 // indirect\n\tgithub.com/moby/patternmatcher v0.6.0 // indirect\n\tgithub.com/moby/sys/sequential v0.6.0 // indirect\n\tgithub.com/moby/sys/user v0.4.0 // indirect\n\tgithub.com/moby/sys/userns v0.1.0 // indirect\n\tgithub.com/moby/term v0.5.2 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect\n\tgithub.com/morikuni/aec v1.1.0 // indirect\n\tgithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect\n\tgithub.com/muesli/cancelreader v0.2.2 // indirect\n\tgithub.com/muesli/reflow v0.3.0 // indirect\n\tgithub.com/muesli/termenv v0.16.0 // indirect\n\tgithub.com/oklog/run v1.2.0 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.1 // indirect\n\tgithub.com/pjbgf/sha1cd v0.5.0 // indirect\n\tgithub.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect\n\tgithub.com/pterm/pterm v0.12.82 // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/ryanuber/go-glob v1.0.0 // indirect\n\tgithub.com/sahilm/fuzzy v0.1.1 // indirect\n\tgithub.com/sergi/go-diff v1.4.0 // indirect\n\tgithub.com/shirou/gopsutil/v4 v4.26.2 // indirect\n\tgithub.com/shopspring/decimal v1.4.0 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/spiffe/go-spiffe/v2 v2.6.0 // indirect\n\tgithub.com/tidwall/gjson v1.18.0 // indirect\n\tgithub.com/tidwall/match v1.1.1 // indirect\n\tgithub.com/tidwall/pretty v1.2.1 // indirect\n\tgithub.com/tidwall/sjson v1.2.5 // indirect\n\tgithub.com/tjfoc/gmsm v1.4.1 // indirect\n\tgithub.com/tklauser/go-sysconf v0.3.16 // indirect\n\tgithub.com/tklauser/numcpus v0.11.0 // indirect\n\tgithub.com/ulikunitz/xz v0.5.15 // indirect\n\tgithub.com/urfave/cli v1.22.17 // indirect\n\tgithub.com/valyala/bytebufferpool v1.0.0 // indirect\n\tgithub.com/valyala/fasttemplate v1.2.2 // indirect\n\tgithub.com/wk8/go-ordered-map/v2 v2.1.8 // indirect\n\tgithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect\n\tgithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgithub.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect\n\tgithub.com/yuin/goldmark v1.7.8 // indirect\n\tgithub.com/yuin/goldmark-emoji v1.0.5 // indirect\n\tgithub.com/yusufpapurcu/wmi v1.2.4 // indirect\n\tgithub.com/zclconf/go-cty-yaml v1.1.0 // indirect\n\tgo.mongodb.org/mongo-driver v1.17.9 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect\n\tgo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect\n\tgo.opentelemetry.io/proto/otlp v1.9.0 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.3 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/crypto v0.49.0 // indirect\n\tgolang.org/x/net v0.52.0 // indirect\n\tgolang.org/x/time v0.15.0 // indirect\n\tgolang.org/x/tools v0.42.0 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tsigs.k8s.io/yaml v1.6.0 // indirect\n)\n\nreplace (\n\t// atomicgo.dev started to return 404\n\tatomicgo.dev/cursor => github.com/atomicgo/cursor v0.2.0\n\tatomicgo.dev/keyboard => github.com/atomicgo/keyboard v0.2.9\n\tatomicgo.dev/schedule => github.com/atomicgo/schedule v0.1.0\n\t// Many functions of terraform was converted to internal to avoid use as a library after v0.15.3. This means that we\n\t// can't use terraform as a library after v0.15.3, so we pull that in here.\n\tgithub.com/hashicorp/terraform => github.com/hashicorp/terraform v0.15.3\n\n\t// This is necessary to workaround go modules error with terraform importing vault incorrectly.\n\t// See https://github.com/hashicorp/vault/issues/7848 for more info\n\tgithub.com/hashicorp/vault => github.com/hashicorp/vault v1.4.2\n\n\t// Fix for missing tencentcloud v3.0.82 tag\n\tgithub.com/tencentcloud/tencentcloud-sdk-go v3.0.82+incompatible => github.com/tencentcloud/tencentcloud-sdk-go v0.0.0-20190816164403-f8fa457a3c72\n\n\t// TFlint introduced a BUSL license in v0.51.0, so we have to be careful not to update past this version.\n\tgithub.com/terraform-linters/tflint => github.com/terraform-linters/tflint v0.50.3\n)\n"
  },
  {
    "path": "go.sum",
    "content": "atomicgo.dev/assert v0.0.2 h1:FiKeMiZSgRrZsPo9qn/7vmr7mCsh5SZyXY4YGYiYwrg=\natomicgo.dev/assert v0.0.2/go.mod h1:ut4NcI3QDdJtlmAxQULOmA13Gz6e2DWbSAS8RUOmNYQ=\nc2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M=\nc2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=\ncel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=\ncel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=\ncloud.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 v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=\ncloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=\ncloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=\ncloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\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/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=\ncloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=\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/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=\ncloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=\ncloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU=\ncloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58=\ncloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA=\ncloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak=\ncloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=\ncloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=\ncloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=\ncloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=\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=\ncloud.google.com/go/storage v1.61.3 h1:VS//ZfBuPGDvakfD9xyPW1RGF1Vy3BWUoVZXgW1KMOg=\ncloud.google.com/go/storage v1.61.3/go.mod h1:JtqK8BBB7TWv0HVGHubtUdzYYrakOQIsMLffZ2Z/HWk=\ncloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=\ncloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=\ndario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\nfilippo.io/age v1.3.1 h1:hbzdQOJkuaMEpRCLSN1/C5DX74RPcNCk6oqhKMXmZi0=\nfilippo.io/age v1.3.1/go.mod h1:EZorDTYUxt836i3zdori5IJX/v2Lj6kWFU0cfh6C0D4=\nfilippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=\nfilippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=\nfilippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=\nfilippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=\ngithub.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=\ngithub.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=\ngithub.com/Azure/azure-sdk-for-go v45.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=\ngithub.com/Azure/azure-sdk-for-go v47.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=\ngithub.com/Azure/azure-sdk-for-go v51.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=\ngithub.com/Azure/azure-sdk-for-go v52.5.0+incompatible h1:/NLBWHCnIHtZyLPc1P7WIqi4Te4CC23kIQyK3Ep/7lA=\ngithub.com/Azure/azure-sdk-for-go v52.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=\ngithub.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=\ngithub.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=\ngithub.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4=\ngithub.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=\ngithub.com/Azure/go-autorest/autorest v0.11.3/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=\ngithub.com/Azure/go-autorest/autorest v0.11.10/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw=\ngithub.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=\ngithub.com/Azure/go-autorest/autorest/azure/cli v0.4.0/go.mod h1:JljT387FplPzBA31vUcvsetLKF3pec5bdAxjVU4kI2s=\ngithub.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM=\ngithub.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=\ngithub.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=\ngithub.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=\ngithub.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=\ngithub.com/Azure/go-autorest/autorest/validation v0.3.0/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E=\ngithub.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E=\ngithub.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=\ngithub.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=\ngithub.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=\ngithub.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=\ngithub.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=\ngithub.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=\ngithub.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 h1:4iB+IesclUXdP0ICgAabvq2FYLXrJWKx1fJQ+GxSo3Y=\ngithub.com/AzureAD/microsoft-authentication-library-for-go v1.7.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4=\ngithub.com/ChrisTrenkamp/goxpath v0.0.0-20190607011252-c5096ec8773d/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=\ngithub.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=\ngithub.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=\ngithub.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=\ngithub.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=\ngithub.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=\ngithub.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/2oUqKc6bF2c=\ngithub.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=\ngithub.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4=\ngithub.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=\ngithub.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=\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 v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=\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 v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=\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/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=\ngithub.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=\ngithub.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=\ngithub.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=\ngithub.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=\ngithub.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=\ngithub.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=\ngithub.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=\ngithub.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=\ngithub.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=\ngithub.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=\ngithub.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM=\ngithub.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw=\ngithub.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=\ngithub.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=\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/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=\ngithub.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=\ngithub.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc=\ngithub.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio=\ngithub.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=\ngithub.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190329064014-6e358769c32a/go.mod h1:T9M45xf79ahXVelWoOBmH0y4aC1t5kXO5BxwyakgIGA=\ngithub.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190103054945-8205d1f41e70/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=\ngithub.com/aliyun/aliyun-tablestore-go-sdk v4.1.2+incompatible/go.mod h1:LDQHRZylxvcg8H7wBIDfvO5g/cy4/sz1iucBlc2l3Jw=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=\ngithub.com/antchfx/xpath v0.0.0-20190129040759-c8489ed3251e/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=\ngithub.com/antchfx/xquery v0.0.0-20180515051857-ad5b8c7a47b0/go.mod h1:LzD22aAzDP8/dyiCKFp31He4m2GPjl0AFyzDtZzUu9M=\ngithub.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=\ngithub.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=\ngithub.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=\ngithub.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0 h1:MzVXffFUye+ZcSR6opIgz9Co7WcDx6ZcY+RjfFHoA0I=\ngithub.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=\ngithub.com/apparentlymart/go-shquot v0.0.1/go.mod h1:lw58XsE5IgUXZ9h0cxnypdx31p9mPFIVEQ9P3c7MlrU=\ngithub.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=\ngithub.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=\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/apparentlymart/go-userdirs v0.0.0-20200915174352-b0c018a67c13/go.mod h1:7kfpUbyCdGJ9fDRCp3fopPQi5+cKNHgTE4ZuNrO71Cw=\ngithub.com/apparentlymart/go-versions v1.0.1/go.mod h1:YF5j7IQtrOAOnsGkniupEA5bfCjzd7i14yu0shZavyM=\ngithub.com/apparentlymart/go-versions v1.0.3 h1:T3b8tumoQLuu1dej2Y9v22J4PWV9IzDLh2A9lIPoVSM=\ngithub.com/apparentlymart/go-versions v1.0.3/go.mod h1:YF5j7IQtrOAOnsGkniupEA5bfCjzd7i14yu0shZavyM=\ngithub.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=\ngithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=\ngithub.com/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/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=\ngithub.com/atomicgo/cursor v0.2.0 h1:nRFeKNcH6uUISSCc1F68IIVUBqXFOkBzL9qHxZ/5DX0=\ngithub.com/atomicgo/cursor v0.2.0/go.mod h1:Lr4ZJB3U7DfPPOkbH7/6TOtJ4vFGHlgj1nc+n900IpU=\ngithub.com/atomicgo/keyboard v0.2.9 h1:3lNinZmrQnFjzI19CiNeKg9w+UCaUa625MtLs8iLOxE=\ngithub.com/atomicgo/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ=\ngithub.com/atomicgo/schedule v0.1.0 h1:0doeK5hjAExAfB4yYXVnSUdJh18+qjmP3tE7uWx4N5E=\ngithub.com/atomicgo/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=\ngithub.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM=\ngithub.com/aws/aws-sdk-go v1.31.9/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=\ngithub.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=\ngithub.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k=\ngithub.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 h1:3kGOqnh1pPeddVa/E37XNTaWJ8W6vrbYV9lJEkCnhuY=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=\ngithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.2 h1:1i1SUOTLk0TbMh7+eJYxgv1r1f47BfR69LL6yaELoI0=\ngithub.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.2/go.mod h1:bo7DhmS/OyVeAJTC768nEk92YKWskqJ4gn0gB5e59qQ=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 h1:SwGMTMLIlvDNyhMteQ6r8IJSBPlRdXX5d4idhIGbkXA=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k=\ngithub.com/aws/aws-sdk-go-v2/service/dynamodb v1.56.2 h1:xi/ECwajy2mixviBD7bKAlGGSwzEaFKX2wIhrZt9NGw=\ngithub.com/aws/aws-sdk-go-v2/service/dynamodb v1.56.2/go.mod h1:dLREOeW66eVaaGIOi2ZlLHDgkR3nuJ02rd00j0YSlBE=\ngithub.com/aws/aws-sdk-go-v2/service/iam v1.53.6 h1:GPQvvxy8+FDnD9xKYzGKJMjIm5xkVM5pd3bFgRldNSo=\ngithub.com/aws/aws-sdk-go-v2/service/iam v1.53.6/go.mod h1:RJNVc52A0K41fCDJOnsCLeWJf8mwa0q30fM3CfE9U18=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 h1:qtJZ70afD3ISKWnoX3xB0J2otEqu3LqicRcDBqsj0hQ=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ=\ngithub.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.20 h1:ru+seMuylHiNZlvgZei83eD8h37hRjm1XIMOEmcV0BU=\ngithub.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.20/go.mod h1:ihZMtPTKoX/ugQRHbui6zNdSgVYN1KY2Dgwb2d3hXlc=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8zjTHSXFhB9L/2OY8Dqs0xXiLjF30jA=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw=\ngithub.com/aws/aws-sdk-go-v2/service/kms v1.50.3 h1:s/zDSG/a/Su9aX+v0Ld9cimUCdkr5FWPmBV8owaEbZY=\ngithub.com/aws/aws-sdk-go-v2/service/kms v1.50.3/go.mod h1:/iSgiUor15ZuxFGQSTf3lA2FmKxFsQoc2tADOarQBSw=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 h1:csi9NLpFZXb9fxY7rS1xVzgPRGMt7MSNWeQ6eo247kE=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=\ngithub.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=\ngithub.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=\ngithub.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=\ngithub.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=\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/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=\ngithub.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=\ngithub.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=\ngithub.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=\ngithub.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=\ngithub.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=\ngithub.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=\ngithub.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=\ngithub.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=\ngithub.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=\ngithub.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=\ngithub.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=\ngithub.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\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/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=\ngithub.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=\ngithub.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=\ngithub.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54=\ngithub.com/charmbracelet/colorprofile v0.3.0 h1:KtLh9uuu1RCt+Hml4s6Hz+kB1PfV3wi++1h5ia65yKQ=\ngithub.com/charmbracelet/colorprofile v0.3.0/go.mod h1:oHJ340RS2nmG1zRGPmhJKJ/jf4FPNNk0P39/wBPA1G0=\ngithub.com/charmbracelet/glamour v0.8.0 h1:tPrjL3aRcQbn++7t18wOpgLyl8wrOHUEDS7IZ68QtZs=\ngithub.com/charmbracelet/glamour v0.8.0/go.mod h1:ViRgmKkf3u5S7uakt2czJ272WSg2ZenlYEZXT2x7Bjw=\ngithub.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=\ngithub.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=\ngithub.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=\ngithub.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=\ngithub.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=\ngithub.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=\ngithub.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=\ngithub.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=\ngithub.com/charmbracelet/x/exp/teatest v0.0.0-20250611152503-f53cdd7e01ef h1:Wcfy1WTykT4c55mCN1a+HiuHXgkv3i9a5Jdo9E+rM1s=\ngithub.com/charmbracelet/x/exp/teatest v0.0.0-20250611152503-f53cdd7e01ef/go.mod h1:MhV4atqUTcHvdaA7Qbkgb0Tvvr+BrH6IW7/i2XW39R8=\ngithub.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=\ngithub.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=\ngithub.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=\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/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=\ngithub.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=\ngithub.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=\ngithub.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=\ngithub.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=\ngithub.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=\ngithub.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/qqsc=\ngithub.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=\ngithub.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=\ngithub.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=\ngithub.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=\ngithub.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=\ngithub.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=\ngithub.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=\ngithub.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=\ngithub.com/coreos/bbolt v1.3.0/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=\ngithub.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=\ngithub.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=\ngithub.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=\ngithub.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=\ngithub.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=\ngithub.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=\ngithub.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=\ngithub.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=\ngithub.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=\ngithub.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=\ngithub.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=\ngithub.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=\ngithub.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=\ngithub.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=\ngithub.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=\ngithub.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=\ngithub.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=\ngithub.com/dylanmei/iso8601 v0.1.0/go.mod h1:w9KhXSgIyROl1DefbMYIE7UVSIvELTbMrCfx+QkYnoQ=\ngithub.com/dylanmei/winrmtest v0.0.0-20190225150635-99b7fe2fddf1/go.mod h1:lcy9/2gH1jn/VCLouHA6tOEwLoNVd4GW6zhuKLmHC2Y=\ngithub.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=\ngithub.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=\ngithub.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=\ngithub.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=\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/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=\ngithub.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=\ngithub.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=\ngithub.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=\ngithub.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=\ngithub.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=\ngithub.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=\ngithub.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=\ngithub.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=\ngithub.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=\ngithub.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=\ngithub.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=\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.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=\ngithub.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=\ngithub.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e h1:y/1nzrdF+RPds4lfoEpNhjfmzlgZtPqyO3jMzrqDQws=\ngithub.com/getsops/gopgagent v0.0.0-20241224165529-7044f28e491e/go.mod h1:awFzISqLJoZLm+i9QQ4SgMNHDqljH6jWV0B36V5MrUM=\ngithub.com/getsops/sops/v3 v3.12.2 h1:4ctEFDNpAAubW8EMICytX8+BFDBSFJkrKvQ9ahSs0a4=\ngithub.com/getsops/sops/v3 v3.12.2/go.mod h1:BACmHQl0J8nPNXBDSJKRT5oUdZx36CkbohGDj9+bD9M=\ngithub.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/gitsight/go-vcsurl v1.0.1 h1:wkijKsbVg9R2IBP97U7wOANeIW9WJJKkBwS9XqllzWo=\ngithub.com/gitsight/go-vcsurl v1.0.1/go.mod h1:qRFdKDa/0Lh9MT0xE+qQBYZ/01+mY1H40rZUHR24X9U=\ngithub.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=\ngithub.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=\ngithub.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=\ngithub.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=\ngithub.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=\ngithub.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=\ngithub.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f h1:Uvbx7nITO3Sd1GdXarX0TbyYmOaSNIJP0mm4LocEyyA=\ngithub.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc=\ngithub.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII=\ngithub.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw=\ngithub.com/go-git/go-git/v6 v6.0.0-20260209124828-a06215dae685 h1:qtAWtG/GhxLMK5J9Nh4WrhwqXq8C1rRLMD45S1iUyNs=\ngithub.com/go-git/go-git/v6 v6.0.0-20260209124828-a06215dae685/go.mod h1:EWlxLBkiFCzXNCadvt05fT9PCAE2sUedgDsvUUIo18s=\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-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=\ngithub.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\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-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=\ngithub.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=\ngithub.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=\ngithub.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=\ngithub.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=\ngithub.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=\ngithub.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=\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.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=\ngithub.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=\ngithub.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=\ngithub.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=\ngithub.com/go-test/deep v1.0.3/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/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=\ngithub.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=\ngithub.com/goccy/go-yaml v1.9.8/go.mod h1:JubOolP3gh0HpiBc4BLRD4YmjEjHAmIIB2aaXKkTfoE=\ngithub.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=\ngithub.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=\ngithub.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=\ngithub.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=\ngithub.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=\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-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\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/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\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.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\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.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\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-jsonnet v0.21.0 h1:43Bk3K4zMRP/aAZm9Po2uSEjY6ALCkYUVIcz9HLGMvA=\ngithub.com/google/go-jsonnet v0.21.0/go.mod h1:tCGAu8cpUpEZcdGMmdOu37nh8bGgqubhI5v2iSk3KJQ=\ngithub.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=\ngithub.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=\ngithub.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=\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/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=\ngithub.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=\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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\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.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\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/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=\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/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE=\ngithub.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA=\ngithub.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=\ngithub.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=\ngithub.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=\ngithub.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=\ngithub.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo=\ngithub.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=\ngithub.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=\ngithub.com/gophercloud/gophercloud v0.6.1-0.20191122030953-d8ac278c1c9d/go.mod h1:ozGNgr9KYOVATV5jsgHl/ceCDXGuguqOZAzoQ/2vcNM=\ngithub.com/gophercloud/gophercloud v0.10.1-0.20200424014253-c3bfe50899e5/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU808e/4mzjjDA76+Ss=\ngithub.com/gophercloud/utils v0.0.0-20200423144003-7c72efc7435d/go.mod h1:ehWUbLQJPqS0Ep+CxeD559hsm9pthPXadJNKwZkp43w=\ngithub.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=\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/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=\ngithub.com/goware/prefixer v0.0.0-20160118172347-395022866408 h1:Y9iQJfEqnN3/Nce9cOegemcy/9Ai5k3huT6E80F3zaw=\ngithub.com/goware/prefixer v0.0.0-20160118172347-395022866408/go.mod h1:PE1ycukgRPJ7bJ9a1fdfQ9j8i/cEcRAoLZzbxYpNB/s=\ngithub.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=\ngithub.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=\ngithub.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=\ngithub.com/gruntwork-io/boilerplate v0.10.1 h1:f+NCks9hMNkaPa1bVVnnNWeN9UoEG7ATS68nyTuPU/A=\ngithub.com/gruntwork-io/boilerplate v0.10.1/go.mod h1:l6lJfbixOrrSurXQ2U98hFCKC04xSYQo6kCnfbgIbec=\ngithub.com/gruntwork-io/go-commons v0.17.2 h1:14dsCJ7M5Vv2X3BIPKeG9Kdy6vTMGhM8L4WZazxfTuY=\ngithub.com/gruntwork-io/go-commons v0.17.2/go.mod h1:zs7Q2AbUKuTarBPy19CIxJVUX/rBamfW8IwuWKniWkE=\ngithub.com/gruntwork-io/terragrunt-engine-go v0.1.0 h1:N/8q099DtaKUgzVGMW2oOYhV8kc5x3Wd/PKlh7G8TRA=\ngithub.com/gruntwork-io/terragrunt-engine-go v0.1.0/go.mod h1:bkjFHAYc8NBV8SkTQyHRhqBebbIsZyFv8mX5r/PAjcM=\ngithub.com/gruntwork-io/terratest v0.51.0 h1:RCXlCwWlHqhUoxgF6n3hvywvbvrsTXqoqt34BrnLekw=\ngithub.com/gruntwork-io/terratest v0.51.0/go.mod h1:evZHXb8VWDgv5O5zEEwfkwMhkx9I53QR/RB11cISrpg=\ngithub.com/hashicorp/aws-sdk-go-base v0.6.0/go.mod h1:2fRjWDv3jJBeN6mVWFHV6hFTNeFBx2gpDLQaZNxUVAY=\ngithub.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.71 h1:3qrWTgbR0uMacRVnE6//G1B20hUJexxqqmQ2OTs1+0s=\ngithub.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.71/go.mod h1:YV27+mh2SLUqeP36G1a9MiqL5eBkFnZQJjNTR9Q9NcY=\ngithub.com/hashicorp/consul v0.0.0-20171026175957-610f3c86a089/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI=\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-azure-helpers v0.12.0/go.mod h1:Zc3v4DNeX6PDdy7NljlYpnrdac1++qNW0I4U+ofGwpg=\ngithub.com/hashicorp/go-azure-helpers v0.14.0/go.mod h1:kR7+sTDEb9TOp/O80ss1UEJg1t4/BHLD/U8wHLS4BGQ=\ngithub.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg=\ngithub.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\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 v1.5.1/go.mod h1:a7z7NPPfNQpJWcn4rSWFtdrSldqLdLPEF3d8nFMsSLM=\ngithub.com/hashicorp/go-getter v1.8.5 h1:DMPV5CSw5JrNg/IK7kDZt3+l2REKXOi3oAw7uYLh2NM=\ngithub.com/hashicorp/go-getter v1.8.5/go.mod h1:WIffejwAyDSJhoVptc3UEshEMkR9O63rw34V7k43O3Q=\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 v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-hclog v0.15.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\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-immutable-radix v0.0.0-20180129170900-7f3cd4390caa/go.mod h1:6ij3Z20p+OhOkCSrA0gImAWoHYQRGbnlcuk6XYTiaRw=\ngithub.com/hashicorp/go-msgpack v0.5.4/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\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-plugin v1.4.1/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ=\ngithub.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA=\ngithub.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8=\ngithub.com/hashicorp/go-retryablehttp v0.5.2/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=\ngithub.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=\ngithub.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=\ngithub.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=\ngithub.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=\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-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=\ngithub.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=\ngithub.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=\ngithub.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=\ngithub.com/hashicorp/go-slug v0.4.1/go.mod h1:I5tq5Lv0E2xcNXNkmx7BSfzi1PsJ2cNjs3cC3LwyhK8=\ngithub.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=\ngithub.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=\ngithub.com/hashicorp/go-tfe v0.14.0/go.mod h1:B71izbwmCZdhEo/GzHopCXN3P74cYv2tsff1mxY4J6c=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=\ngithub.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=\ngithub.com/hashicorp/go-version v1.8.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/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=\ngithub.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=\ngithub.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=\ngithub.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90=\ngithub.com/hashicorp/hcl/v2 v2.10.0/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg=\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/jsonapi v0.0.0-20210420151930-edf82c9774bf/go.mod h1:Yog5+CPEM3c99L1CL2CFCYoSzgWm5vTU58idbRUaLik=\ngithub.com/hashicorp/memberlist v0.1.0/go.mod h1:ncdBp14cuox2iFOq3kDiquKU6fqsTBc3W6JvZwjxxsE=\ngithub.com/hashicorp/serf v0.0.0-20160124182025-e4ec8cc423bb/go.mod h1:h/Ru6tmZazX7WO/GDmwdpS975F019L4t5ng5IgwbNrE=\ngithub.com/hashicorp/terraform v0.15.3 h1:2QWbTj2xJ/8W1gCyIrd0WAqVF4weKPMYjx8nKjbkQjA=\ngithub.com/hashicorp/terraform v0.15.3/go.mod h1:w4eBEsluZfYumXUTLe834eqHh969AabcLqbj2WAYlM8=\ngithub.com/hashicorp/terraform-config-inspect v0.0.0-20210209133302-4fd17a0faac2/go.mod h1:Z0Nnk4+3Cy89smEbrq+sl1bxc9198gIP4I7wcQF6Kqs=\ngithub.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg=\ngithub.com/hashicorp/terraform-svchost v0.2.0 h1:wVc2vMiodOHvNZcQw/3y9af1XSomgjGSv+rv3BMCk7I=\ngithub.com/hashicorp/terraform-svchost v0.2.0/go.mod h1:/98rrS2yZsbppi4VGVCjwYmh8dqsKzISqK7Hli+0rcQ=\ngithub.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0=\ngithub.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM=\ngithub.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=\ngithub.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=\ngithub.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=\ngithub.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=\ngithub.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=\ngithub.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=\ngithub.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=\ngithub.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c=\ngithub.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U=\ngithub.com/huandu/go-clone v1.7.3 h1:rtQODA+ABThEn6J5LBTppJfKmZy/FwfpMUWa8d01TTQ=\ngithub.com/huandu/go-clone v1.7.3/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE=\ngithub.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=\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/huaweicloud/huaweicloud-sdk-go-v3 v0.1.189 h1:YQ+Lx4u4FdStckox6Ecnc/4i1knjXm1r3KhNRmbvqG4=\ngithub.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.189/go.mod h1:M+yna96Fx9o5GbIUnF3OvVvQGjgfVSyeJbV9Yb1z/wI=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=\ngithub.com/inancgumus/screen v0.0.0-20190314163918-06e984b86ed3 h1:fO9A67/izFYFYky7l1pDP5Dr0BTCRkaQJUG6Jm5ehsk=\ngithub.com/inancgumus/screen v0.0.0-20190314163918-06e984b86ed3/go.mod h1:Ey4uAp+LvIl+s5jRbOHLcZpUDnkjLBROl15fZLwPlTM=\ngithub.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=\ngithub.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=\ngithub.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=\ngithub.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94=\ngithub.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8=\ngithub.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=\ngithub.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/joyent/triton-go v0.0.0-20180313100802-d8f9c0314926/go.mod h1:U+RSyWxWd04xTqnuOQxnai7XGS2PrPY2cfGoDKtMHjA=\ngithub.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=\ngithub.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=\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/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=\ngithub.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=\ngithub.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=\ngithub.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=\ngithub.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=\ngithub.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=\ngithub.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=\ngithub.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=\ngithub.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=\ngithub.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=\ngithub.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=\ngithub.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=\ngithub.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/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/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/labstack/echo/v4 v4.15.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs=\ngithub.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=\ngithub.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=\ngithub.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=\ngithub.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=\ngithub.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo=\ngithub.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=\ngithub.com/likexian/gokit v0.0.0-20190309162924-0a377eecf7aa/go.mod h1:QdfYv6y6qPA9pbBA2qXtoT8BMKha6UyNbxWGWl/9Jfk=\ngithub.com/likexian/gokit v0.0.0-20190418170008-ace88ad0983b/go.mod h1:KKqSnk/VVSW8kEyO2vVCXoanzEutKdlBAPohmGXkxCk=\ngithub.com/likexian/gokit v0.0.0-20190501133040-e77ea8b19cdc/go.mod h1:3kvONayqCaj+UgrRZGpgfXzHdMYCAO0KAt4/8n0L57Y=\ngithub.com/likexian/gokit v0.20.15/go.mod h1:kn+nTv3tqh6yhor9BC4Lfiu58SmH8NmQ2PmEl+uM6nU=\ngithub.com/likexian/simplejson-go v0.0.0-20190409170913-40473a74d76d/go.mod h1:Typ1BfnATYtZ/+/shXfFYLrovhFyuKvzwrdOnIDHlmg=\ngithub.com/likexian/simplejson-go v0.0.0-20190419151922-c1f9f0b4f084/go.mod h1:U4O1vIJvIKwbMZKUJ62lppfdvkCdVd2nfMimHK81eec=\ngithub.com/likexian/simplejson-go v0.0.0-20190502021454-d8787b4bfa0b/go.mod h1:3BWwtmKP9cXWwYCr5bkoVDEfLywacOv0s06OBEDpyt8=\ngithub.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=\ngithub.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=\ngithub.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=\ngithub.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=\ngithub.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=\ngithub.com/lusis/go-artifactory v0.0.0-20160115162124-7e4ce345df82/go.mod h1:y54tfGmO3NKssKveTEFFzH8C/akrSOy/iW9qEAUDV84=\ngithub.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=\ngithub.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc=\ngithub.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc=\ngithub.com/masterzen/winrm v0.0.0-20200615185753-c42b5136ff88/go.mod h1:a2HXwefeat3evJHxFXSayvRHpYEPJYtErl4uIzfaUqY=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\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/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=\ngithub.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=\ngithub.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=\ngithub.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=\ngithub.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=\ngithub.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=\ngithub.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\ngithub.com/mattn/go-shellwords v1.0.4/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=\ngithub.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=\ngithub.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=\ngithub.com/mattn/go-zglob v0.0.6 h1:mP8RnmCgho4oaUYDIDn6GNxYk+qJGUs8fJLn+twYj2A=\ngithub.com/mattn/go-zglob v0.0.6/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\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/miekg/dns v1.0.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=\ngithub.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4=\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.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=\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.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\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-linereader v0.0.0-20190213213312-1b945b3263eb/go.mod h1:OaY7UOoTkkrX3wRwjpYRKafIkkyeD0UtweSHAWWiqQM=\ngithub.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\ngithub.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\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 v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=\ngithub.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=\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/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18Ap4z4=\ngithub.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=\ngithub.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/panicwrap v1.0.0 h1:67zIyVakCIvcs69A0FGfZjBdPleaonSgGlXRSRlb6fE=\ngithub.com/mitchellh/panicwrap v1.0.0/go.mod h1:pKvZHwWrZowLUzftuFq7coarnxbBXU4aQh3N0BJOeeA=\ngithub.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/mitchellh/reflectwalk v1.0.1/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=\ngithub.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=\ngithub.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=\ngithub.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=\ngithub.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=\ngithub.com/moby/moby/client v0.2.2 h1:Pt4hRMCAIlyjL3cr8M5TrXCwKzguebPAc2do2ur7dEM=\ngithub.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85pasRz/GmQ=\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/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=\ngithub.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=\ngithub.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=\ngithub.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=\ngithub.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=\ngithub.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=\ngithub.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=\ngithub.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=\ngithub.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=\ngithub.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\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/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=\ngithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=\ngithub.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=\ngithub.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=\ngithub.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=\ngithub.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=\ngithub.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=\ngithub.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=\ngithub.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=\ngithub.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=\ngithub.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=\ngithub.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=\ngithub.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=\ngithub.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=\ngithub.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=\ngithub.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=\ngithub.com/opencontainers/runc v1.2.8 h1:RnEICeDReapbZ5lZEgHvj7E9Q3Eex9toYmaGBsbvU5Q=\ngithub.com/opencontainers/runc v1.2.8/go.mod h1:cC0YkmZcuvr+rtBZ6T7NBoVbMGNAdLa/21vIElJDOzI=\ngithub.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw=\ngithub.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE=\ngithub.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db/go.mod h1:f6Izs6JvFTdnRbziASagjZ2vmf55NSIkC/weStxCHqk=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=\ngithub.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=\ngithub.com/pkg/browser v0.0.0-20201207095918-0426ae3fba23/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=\ngithub.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=\ngithub.com/pkg/errors v0.8.0/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/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 h1:S1hI5JiKP7883xBzZAr1ydcxrKNSVNm7+3+JwjxZEsg=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25/go.mod h1:ZQntvDG8TkPgljxtA0R9frDoND4QORU1VXz015N5Ks4=\ngithub.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/posener/complete v1.2.1/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E=\ngithub.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=\ngithub.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=\ngithub.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=\ngithub.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=\ngithub.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=\ngithub.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=\ngithub.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=\ngithub.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=\ngithub.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=\ngithub.com/pterm/pterm v0.12.82 h1:+D9wYhCaeaK0FIQoZtqbNQuNpe2lB2tajKKsTd5paVQ=\ngithub.com/pterm/pterm v0.12.82/go.mod h1:TyuyrPjnxfwP+ccJdBTeWHtd/e0ybQHkOS/TakajZCw=\ngithub.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=\ngithub.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=\ngithub.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=\ngithub.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=\ngithub.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=\ngithub.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=\ngithub.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=\ngithub.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=\ngithub.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=\ngithub.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=\ngithub.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI=\ngithub.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=\ngithub.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=\ngithub.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=\ngithub.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=\ngithub.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=\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/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=\ngithub.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=\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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\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/stuart-warren/yamlfmt v0.1.2 h1:ojguhYdHpNWy62fLkrQtLFGAzrqFxVaU8f5Z0U8mkMI=\ngithub.com/stuart-warren/yamlfmt v0.1.2/go.mod h1:X5TuPH+hf4O0U1KBvNqygvHbvAnoi9Wyl9BbtPv8SZk=\ngithub.com/tencentcloud/tencentcloud-sdk-go v0.0.0-20190816164403-f8fa457a3c72/go.mod h1:0PfYow01SHPMhKY31xa+EFz2RStxIqj6JFAJS+IkCi4=\ngithub.com/tencentyun/cos-go-sdk-v5 v0.0.0-20190808065407-f07404cefc8c/go.mod h1:wk2XFUg6egk4tSDNZtXeKfe2G6690UVyt163PuUxBZk=\ngithub.com/testcontainers/testcontainers-go v0.41.0 h1:mfpsD0D36YgkxGj2LrIyxuwQ9i2wCKAD+ESsYM1wais=\ngithub.com/testcontainers/testcontainers-go v0.41.0/go.mod h1:pdFrEIfaPl24zmBjerWTTYaY0M6UHsqA1YSvsoU40MI=\ngithub.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=\ngithub.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=\ngithub.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=\ngithub.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=\ngithub.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=\ngithub.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=\ngithub.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=\ngithub.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=\ngithub.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=\ngithub.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=\ngithub.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=\ngithub.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=\ngithub.com/tombuildsstuff/giovanni v0.15.1/go.mod h1:0TZugJPEtqzPlMpuJHYfXY6Dq2uLPrXf98D2XQSxNbA=\ngithub.com/ugorji/go v0.0.0-20180813092308-00b869d2f4a5/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=\ngithub.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=\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/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=\ngithub.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=\ngithub.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=\ngithub.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=\ngithub.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=\ngithub.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=\ngithub.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=\ngithub.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=\ngithub.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=\ngithub.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=\ngithub.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=\ngithub.com/wI2L/jsondiff v0.7.0 h1:1lH1G37GhBPqCfp/lrs91rf/2j3DktX6qYAKZkLuCQQ=\ngithub.com/wI2L/jsondiff v0.7.0/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM=\ngithub.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=\ngithub.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=\ngithub.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=\ngithub.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=\ngithub.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=\ngithub.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=\ngithub.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=\ngithub.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=\ngithub.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg=\ngithub.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngithub.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=\ngithub.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=\ngithub.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=\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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=\ngithub.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=\ngithub.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=\ngithub.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=\ngithub.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=\ngithub.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=\ngithub.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=\ngithub.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=\ngithub.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=\ngithub.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=\ngithub.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=\ngithub.com/zclconf/go-cty v1.8.3/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=\ngithub.com/zclconf/go-cty v1.18.0 h1:pJ8+HNI4gFoyRNqVE37wWbJWVw43BZczFo7KUoRczaA=\ngithub.com/zclconf/go-cty v1.18.0/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg=\ngithub.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=\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=\ngithub.com/zclconf/go-cty-yaml v1.0.2/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0=\ngithub.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0=\ngithub.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=\ngo.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=\ngo.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=\ngo.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=\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.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ=\ngo.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=\ngo.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=\ngo.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 h1:lSZHgNHfbmQTPfuTmWVkEu8J8qXaQwuV30pjCcAUvP8=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0/go.mod h1:so9ounLcuoRDu033MW/E0AD4hhUjVqswrMF5FoZlBcw=\ngo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw=\ngo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs=\ngo.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=\ngo.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=\ngo.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=\ngo.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=\ngo.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=\ngo.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=\ngo.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=\ngo.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=\ngo.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=\ngo.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=\ngo.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=\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.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=\ngo.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=\ngo.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=\ngo.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=\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-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/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-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\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-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=\ngolang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=\ngolang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=\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-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=\ngolang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=\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.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.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=\ngolang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=\ngolang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180811021610-c39426892332/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-20180906233101-161cd47e91fd/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-20181220203305-927f97764cc3/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-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190206173232-65e2d4e15006/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-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-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20191009170851-d66e71096ffb/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/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-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=\ngolang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=\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-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-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=\ngolang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=\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-20201020160332-67f06af15bc9/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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/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-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/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-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/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-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201204225414-ed752295db88/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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.1.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.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=\ngolang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=\ngolang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\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.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\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.5/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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=\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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\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.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=\ngolang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/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-20191203134012-c197fd4bf371/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.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=\ngolang.org/x/tools v0.0.0-20201028111035-eafbe7b904eb/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\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.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=\ngolang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=\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=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\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/api v0.34.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=\ngoogle.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=\ngoogle.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=\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-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\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/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=\ngoogle.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=\ngoogle.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=\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/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=\ngoogle.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=\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.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k=\ngopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=\ngopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=\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.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\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.0-20210107192922-496545a6307b/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=\ngotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=\ngotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=\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=\nk8s.io/api v0.0.0-20190620084959-7cf5895f2711/go.mod h1:TBhBqb1AWbBQbW3XRusr7n7E4v2+5ZY8r8sAMnyFC5A=\nk8s.io/apimachinery v0.0.0-20190612205821-1799e75a0719/go.mod h1:I4A+glKBHiTgiEjQiCCQfCAIcIMFGt291SmsvcrFzJA=\nk8s.io/apimachinery v0.0.0-20190913080033-27d36303b655/go.mod h1:nL6pwRT8NgfF8TT68DBI8uEePRt89cSvoXUVqbkWHq4=\nk8s.io/client-go v10.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=\nk8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=\nk8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=\nk8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=\nk8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=\nk8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=\nk8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc=\nk8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=\nk8s.io/utils v0.0.0-20200411171748-3d5a2fe318e4/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=\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=\nsigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=\nsigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=\nsigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=\nsigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=\n"
  },
  {
    "path": "internal/awshelper/config.go",
    "content": "// Package awshelper provides helper functions for working with AWS services.\npackage awshelper\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/credentials\"\n\tawsiam \"github.com/aws/aws-sdk-go-v2/service/iam\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sts\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sts/types\"\n\t\"github.com/gruntwork-io/go-commons/version\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iam\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst (\n\t// Minimum ARN parts required for a valid ARN\n\tminARNParts = 2\n)\n\n// AwsSessionConfig is a representation of the configuration options for an AWS Config\ntype AwsSessionConfig struct {\n\tTags                    map[string]string\n\tRegion                  string\n\tCustomS3Endpoint        string\n\tCustomDynamoDBEndpoint  string\n\tProfile                 string\n\tRoleArn                 string\n\tCredsFilename           string\n\tExternalID              string\n\tSessionName             string\n\tS3ForcePathStyle        bool\n\tDisableComputeChecksums bool\n}\n\ntype tokenFetcher string\n\n// FetchToken implements the token fetcher interface.\n// Supports providing a token value or the path to a token on disk\nfunc (f tokenFetcher) FetchToken(_ context.Context) ([]byte, error) {\n\t// Check if token is a raw value\n\tif _, err := os.Stat(string(f)); err != nil {\n\t\t// TODO: See if this lint error should be ignored\n\t\treturn []byte(f), nil //nolint: nilerr\n\t}\n\n\ttoken, err := os.ReadFile(string(f))\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\treturn token, nil\n}\n\n// AWSConfigBuilder builds an AWS config using the builder pattern.\n// Use NewAwsConfigBuilder to create, chain With* methods for optional parameters, then call Build().\ntype AWSConfigBuilder struct {\n\tsessionConfig *AwsSessionConfig\n\tenv           map[string]string\n\tiamRoleOpts   iam.RoleOptions\n}\n\n// NewAWSConfigBuilder creates a new builder for AWS config.\nfunc NewAWSConfigBuilder() *AWSConfigBuilder {\n\treturn &AWSConfigBuilder{\n\t\tenv: make(map[string]string),\n\t}\n}\n\n// WithSessionConfig sets the AWS session configuration (region, profile, credentials file, etc.).\nfunc (b *AWSConfigBuilder) WithSessionConfig(cfg *AwsSessionConfig) *AWSConfigBuilder {\n\tb.sessionConfig = cfg\n\treturn b\n}\n\n// WithEnv sets environment variables used for credential and region resolution.\nfunc (b *AWSConfigBuilder) WithEnv(env map[string]string) *AWSConfigBuilder {\n\tb.env = env\n\treturn b\n}\n\n// WithIAMRoleOptions sets IAM role options for assuming a role.\nfunc (b *AWSConfigBuilder) WithIAMRoleOptions(opts iam.RoleOptions) *AWSConfigBuilder {\n\tb.iamRoleOpts = opts\n\treturn b\n}\n\n// Build creates the AWS config from the builder's configuration.\nfunc (b *AWSConfigBuilder) Build(ctx context.Context, l log.Logger) (aws.Config, error) {\n\tvar configOptions []func(*config.LoadOptions) error\n\n\tconfigOptions = append(configOptions, config.WithAppID(\"terragrunt/\"+version.GetVersion()))\n\n\tif envCreds := createCredentialsFromEnv(b.env); envCreds != nil {\n\t\tl.Debugf(\"Using AWS credentials from auth provider command\")\n\n\t\tconfigOptions = append(configOptions, config.WithCredentialsProvider(envCreds))\n\t} else if b.sessionConfig != nil && b.sessionConfig.CredsFilename != \"\" {\n\t\tconfigOptions = append(configOptions, config.WithSharedConfigFiles([]string{b.sessionConfig.CredsFilename}))\n\t}\n\n\t// Prioritize configured region over environment variables\n\t// This fixes the issue where AWS_REGION/AWS_DEFAULT_REGION env vars override the backend config region\n\tvar region string\n\tif b.sessionConfig != nil && b.sessionConfig.Region != \"\" {\n\t\tregion = b.sessionConfig.Region\n\t} else {\n\t\tregion = getRegionFromEnv(b.env)\n\t}\n\n\tif region == \"\" {\n\t\tregion = \"us-east-1\"\n\t}\n\n\tconfigOptions = append(configOptions, config.WithRegion(region))\n\n\tif b.sessionConfig != nil && b.sessionConfig.Profile != \"\" {\n\t\tconfigOptions = append(configOptions, config.WithSharedConfigProfile(b.sessionConfig.Profile))\n\t}\n\n\tcfg, err := config.LoadDefaultConfig(ctx, configOptions...)\n\tif err != nil {\n\t\treturn aws.Config{}, errors.Errorf(\"Error loading AWS config: %w\", err)\n\t}\n\n\tif createCredentialsFromEnv(b.env) != nil {\n\t\treturn cfg, nil\n\t}\n\n\tmergedIAMRoleOptions := getMergedIAMRoleOptions(b.sessionConfig, b.iamRoleOpts)\n\tif mergedIAMRoleOptions.RoleARN == \"\" {\n\t\treturn cfg, nil\n\t}\n\n\tif mergedIAMRoleOptions.WebIdentityToken != \"\" {\n\t\tl.Debugf(\"Assuming role %s using WebIdentity token\", mergedIAMRoleOptions.RoleARN)\n\t\tcfg.Credentials = getWebIdentityCredentialsFromIAMRoleOptions(cfg, mergedIAMRoleOptions)\n\n\t\treturn cfg, nil\n\t}\n\n\tl.Debugf(\"Assuming role %s\", mergedIAMRoleOptions.RoleARN)\n\tcfg.Credentials = getSTSCredentialsFromIAMRoleOptions(cfg, mergedIAMRoleOptions, getExternalID(b.sessionConfig))\n\n\treturn cfg, nil\n}\n\n// BuildS3Client creates an S3 client from the builder's configuration.\n// The session config (set via WithSessionConfig) provides S3-specific options like custom endpoint and path style.\nfunc (b *AWSConfigBuilder) BuildS3Client(ctx context.Context, l log.Logger) (*s3.Client, error) {\n\tcfg, err := b.Build(ctx, l)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tif b.sessionConfig == nil {\n\t\treturn s3.NewFromConfig(cfg), nil\n\t}\n\n\tcustomFN := make([]func(*s3.Options), 0, 2) //nolint:mnd\n\n\tif b.sessionConfig.CustomS3Endpoint != \"\" {\n\t\tcustomFN = append(customFN, func(o *s3.Options) {\n\t\t\to.BaseEndpoint = aws.String(b.sessionConfig.CustomS3Endpoint)\n\t\t})\n\t}\n\n\tif b.sessionConfig.S3ForcePathStyle {\n\t\tcustomFN = append(customFN, func(o *s3.Options) {\n\t\t\to.UsePathStyle = true\n\t\t})\n\t}\n\n\treturn s3.NewFromConfig(cfg, customFN...), nil\n}\n\n// getRegionFromEnv extracts region from environment variables.\nfunc getRegionFromEnv(env map[string]string) string {\n\tif len(env) == 0 {\n\t\treturn \"\"\n\t}\n\n\tif region := env[\"AWS_REGION\"]; region != \"\" {\n\t\treturn region\n\t}\n\n\treturn env[\"AWS_DEFAULT_REGION\"]\n}\n\n// getMergedIAMRoleOptions merges IAM role options from awsCfg and the provided IAM role options.\nfunc getMergedIAMRoleOptions(awsCfg *AwsSessionConfig, iamRoleOpts iam.RoleOptions) iam.RoleOptions {\n\t// Merge in awsCfg role options if available\n\tif awsCfg != nil && awsCfg.RoleArn != \"\" {\n\t\tiamRoleOpts = iam.MergeRoleOptions(\n\t\t\tiamRoleOpts,\n\t\t\tiam.RoleOptions{\n\t\t\t\tRoleARN:               awsCfg.RoleArn,\n\t\t\t\tAssumeRoleSessionName: awsCfg.SessionName,\n\t\t\t},\n\t\t)\n\t}\n\n\treturn iamRoleOpts\n}\n\n// getExternalID returns the external ID from awsCfg if available\nfunc getExternalID(awsCfg *AwsSessionConfig) string {\n\tif awsCfg == nil {\n\t\treturn \"\"\n\t}\n\n\treturn awsCfg.ExternalID\n}\n\n// AssumeIamRole assumes an IAM role and returns the credentials.\nfunc AssumeIamRole(\n\tctx context.Context,\n\tiamRoleOpts iam.RoleOptions,\n\texternalID string,\n\tenv map[string]string,\n) (*types.Credentials, error) {\n\tregion := getRegionFromEnv(env)\n\tif region == \"\" {\n\t\tregion = os.Getenv(\"AWS_REGION\")\n\t}\n\n\tif region == \"\" {\n\t\tregion = os.Getenv(\"AWS_DEFAULT_REGION\")\n\t}\n\n\tif region == \"\" {\n\t\tregion = \"us-east-1\"\n\t}\n\n\t// Set user agent to include terragrunt version\n\tcfg, err := config.LoadDefaultConfig(\n\t\tctx,\n\t\tconfig.WithRegion(region),\n\t\tconfig.WithAppID(\"terragrunt/\"+version.GetVersion()),\n\t)\n\tif err != nil {\n\t\treturn nil, errors.Errorf(\"Error loading AWS config: %w\", err)\n\t}\n\n\tstsClient := sts.NewFromConfig(cfg)\n\n\troleSessionName := iamRoleOpts.AssumeRoleSessionName\n\tif roleSessionName == \"\" {\n\t\troleSessionName = iam.GetDefaultAssumeRoleSessionName()\n\t}\n\n\tduration := time.Duration(iam.DefaultAssumeRoleDuration) * time.Second\n\tif iamRoleOpts.AssumeRoleDuration > 0 {\n\t\tduration = time.Duration(iamRoleOpts.AssumeRoleDuration) * time.Second\n\t}\n\n\tif iamRoleOpts.WebIdentityToken != \"\" {\n\t\t// Use sts AssumeRoleWithWebIdentity\n\t\ttb, err := tokenFetcher(iamRoleOpts.WebIdentityToken).FetchToken(ctx)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Errorf(\"Error reading web identity token file: %w\", err)\n\t\t}\n\n\t\tinput := &sts.AssumeRoleWithWebIdentityInput{\n\t\t\tRoleArn:          aws.String(iamRoleOpts.RoleARN),\n\t\t\tRoleSessionName:  aws.String(roleSessionName),\n\t\t\tWebIdentityToken: aws.String(string(tb)),\n\t\t\tDurationSeconds:  aws.Int32(int32(duration.Seconds())),\n\t\t}\n\n\t\tresult, err := stsClient.AssumeRoleWithWebIdentity(ctx, input)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Errorf(\"Error assuming role with web identity: %w\", err)\n\t\t}\n\n\t\treturn result.Credentials, nil\n\t}\n\n\t// Use regular sts AssumeRole\n\tinput := &sts.AssumeRoleInput{\n\t\tRoleArn:         aws.String(iamRoleOpts.RoleARN),\n\t\tRoleSessionName: aws.String(roleSessionName),\n\t\tDurationSeconds: aws.Int32(int32(duration.Seconds())),\n\t}\n\n\tif externalID != \"\" {\n\t\tinput.ExternalId = aws.String(externalID)\n\t}\n\n\tresult, err := stsClient.AssumeRole(ctx, input)\n\tif err != nil {\n\t\treturn nil, errors.Errorf(\"Error assuming role: %w\", err)\n\t}\n\n\treturn result.Credentials, nil\n}\n\n// GetAWSCallerIdentity gets the caller identity from AWS\nfunc GetAWSCallerIdentity(ctx context.Context, cfg *aws.Config) (*sts.GetCallerIdentityOutput, error) {\n\tstsClient := sts.NewFromConfig(*cfg)\n\treturn stsClient.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})\n}\n\n// ValidateAwsConfig validates that the AWS config has valid credentials\nfunc ValidateAwsConfig(ctx context.Context, cfg *aws.Config) error {\n\t_, err := GetAWSCallerIdentity(ctx, cfg)\n\treturn err\n}\n\n// GetAWSPartition gets the AWS partition from the caller identity\nfunc GetAWSPartition(ctx context.Context, cfg *aws.Config) (string, error) {\n\tresult, err := GetAWSCallerIdentity(ctx, cfg)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Extract partition from ARN\n\tarn := aws.ToString(result.Arn)\n\tif arn == \"\" {\n\t\treturn \"\", errors.New(\"Empty ARN returned from GetCallerIdentity\")\n\t}\n\n\t// ARN format: arn:partition:service:region:account:resource\n\tparts := strings.Split(arn, \":\")\n\tif len(parts) < minARNParts {\n\t\treturn \"\", errors.Errorf(\"Invalid ARN format: %s\", arn)\n\t}\n\n\treturn parts[1], nil\n}\n\n// GetAWSAccountAlias gets the AWS account alias\nfunc GetAWSAccountAlias(ctx context.Context, cfg *aws.Config) (string, error) {\n\tiamClient := awsiam.NewFromConfig(*cfg)\n\n\tresult, err := iamClient.ListAccountAliases(ctx, &awsiam.ListAccountAliasesInput{})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif len(result.AccountAliases) == 0 {\n\t\treturn \"\", nil\n\t}\n\n\treturn result.AccountAliases[0], nil\n}\n\n// GetAWSAccountID gets the AWS account ID from the caller identity\nfunc GetAWSAccountID(ctx context.Context, cfg *aws.Config) (string, error) {\n\tresult, err := GetAWSCallerIdentity(ctx, cfg)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn aws.ToString(result.Account), nil\n}\n\n// GetAWSIdentityArn gets the AWS identity ARN from the caller identity\nfunc GetAWSIdentityArn(ctx context.Context, cfg *aws.Config) (string, error) {\n\tresult, err := GetAWSCallerIdentity(ctx, cfg)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn aws.ToString(result.Arn), nil\n}\n\n// GetAWSUserID gets the AWS user ID from the caller identity\nfunc GetAWSUserID(ctx context.Context, cfg *aws.Config) (string, error) {\n\tresult, err := GetAWSCallerIdentity(ctx, cfg)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn aws.ToString(result.UserId), nil\n}\n\n// ValidatePublicAccessBlock validates the public access block configuration\nfunc ValidatePublicAccessBlock(output *s3.GetPublicAccessBlockOutput) (bool, error) {\n\tif output.PublicAccessBlockConfiguration == nil {\n\t\treturn false, nil\n\t}\n\n\tconfig := output.PublicAccessBlockConfiguration\n\n\treturn aws.ToBool(config.BlockPublicAcls) &&\n\t\taws.ToBool(config.BlockPublicPolicy) &&\n\t\taws.ToBool(config.IgnorePublicAcls) &&\n\t\taws.ToBool(config.RestrictPublicBuckets), nil\n}\n\n//nolint:gocritic // hugeParam: intentionally pass by value to avoid recursive credential resolution\nfunc getWebIdentityCredentialsFromIAMRoleOptions(\n\tcfg aws.Config,\n\tiamRoleOptions iam.RoleOptions,\n) aws.CredentialsProviderFunc {\n\troleSessionName := iamRoleOptions.AssumeRoleSessionName\n\tif roleSessionName == \"\" {\n\t\t// Set a unique session name in the same way it is done in the SDK\n\t\troleSessionName = strconv.FormatInt(time.Now().UTC().UnixNano(), 10)\n\t}\n\n\treturn func(ctx context.Context) (aws.Credentials, error) {\n\t\tstsClient := sts.NewFromConfig(cfg)\n\n\t\ttoken, err := tokenFetcher(iamRoleOptions.WebIdentityToken).FetchToken(ctx)\n\t\tif err != nil {\n\t\t\treturn aws.Credentials{}, err\n\t\t}\n\n\t\tduration := time.Duration(iam.DefaultAssumeRoleDuration) * time.Second\n\t\tif iamRoleOptions.AssumeRoleDuration > 0 {\n\t\t\tduration = time.Duration(iamRoleOptions.AssumeRoleDuration) * time.Second\n\t\t}\n\n\t\tinput := &sts.AssumeRoleWithWebIdentityInput{\n\t\t\tRoleArn:          aws.String(iamRoleOptions.RoleARN),\n\t\t\tRoleSessionName:  aws.String(roleSessionName),\n\t\t\tWebIdentityToken: aws.String(string(token)),\n\t\t\tDurationSeconds:  aws.Int32(int32(duration.Seconds())),\n\t\t}\n\n\t\tresult, err := stsClient.AssumeRoleWithWebIdentity(ctx, input)\n\t\tif err != nil {\n\t\t\treturn aws.Credentials{}, err\n\t\t}\n\n\t\treturn aws.Credentials{\n\t\t\tAccessKeyID:     aws.ToString(result.Credentials.AccessKeyId),\n\t\t\tSecretAccessKey: aws.ToString(result.Credentials.SecretAccessKey),\n\t\t\tSessionToken:    aws.ToString(result.Credentials.SessionToken),\n\t\t\tCanExpire:       true,\n\t\t\tExpires:         aws.ToTime(result.Credentials.Expiration),\n\t\t}, nil\n\t}\n}\n\n//nolint:gocritic // hugeParam: intentionally pass by value to avoid recursive credential resolution\nfunc getSTSCredentialsFromIAMRoleOptions(\n\tcfg aws.Config,\n\tiamRoleOptions iam.RoleOptions,\n\texternalID string,\n) aws.CredentialsProviderFunc {\n\treturn func(ctx context.Context) (aws.Credentials, error) {\n\t\tstsClient := sts.NewFromConfig(cfg)\n\n\t\troleSessionName := iamRoleOptions.AssumeRoleSessionName\n\t\tif roleSessionName == \"\" {\n\t\t\troleSessionName = strconv.FormatInt(time.Now().UTC().UnixNano(), 10)\n\t\t}\n\n\t\tduration := time.Duration(iam.DefaultAssumeRoleDuration) * time.Second\n\t\tif iamRoleOptions.AssumeRoleDuration > 0 {\n\t\t\tduration = time.Duration(iamRoleOptions.AssumeRoleDuration) * time.Second\n\t\t}\n\n\t\tinput := &sts.AssumeRoleInput{\n\t\t\tRoleArn:         aws.String(iamRoleOptions.RoleARN),\n\t\t\tRoleSessionName: aws.String(roleSessionName),\n\t\t\tDurationSeconds: aws.Int32(int32(duration.Seconds())),\n\t\t}\n\n\t\tif externalID != \"\" {\n\t\t\tinput.ExternalId = aws.String(externalID)\n\t\t}\n\n\t\tresult, err := stsClient.AssumeRole(ctx, input)\n\t\tif err != nil {\n\t\t\treturn aws.Credentials{}, err\n\t\t}\n\n\t\treturn aws.Credentials{\n\t\t\tAccessKeyID:     aws.ToString(result.Credentials.AccessKeyId),\n\t\t\tSecretAccessKey: aws.ToString(result.Credentials.SecretAccessKey),\n\t\t\tSessionToken:    aws.ToString(result.Credentials.SessionToken),\n\t\t\tCanExpire:       true,\n\t\t\tExpires:         aws.ToTime(result.Credentials.Expiration),\n\t\t}, nil\n\t}\n}\n\n// createCredentialsFromEnv creates AWS credentials from environment variables.\nfunc createCredentialsFromEnv(env map[string]string) aws.CredentialsProvider {\n\tif len(env) == 0 {\n\t\treturn nil\n\t}\n\n\taccessKeyID := env[\"AWS_ACCESS_KEY_ID\"]\n\tsecretAccessKey := env[\"AWS_SECRET_ACCESS_KEY\"]\n\tsessionToken := env[\"AWS_SESSION_TOKEN\"]\n\n\t// If we don't have at least access key and secret key, return nil\n\tif accessKeyID == \"\" || secretAccessKey == \"\" {\n\t\treturn nil\n\t}\n\n\treturn credentials.NewStaticCredentialsProvider(accessKeyID, secretAccessKey, sessionToken)\n}\n"
  },
  {
    "path": "internal/awshelper/config_test.go",
    "content": "//go:build aws\n\npackage awshelper_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\ts3types \"github.com/aws/aws-sdk-go-v2/service/s3/types\"\n\t\"github.com/gruntwork-io/terragrunt/internal/awshelper\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAwsSessionValidationFail(t *testing.T) {\n\tt.Skip(\"Skipping for now as we need to change the signature of CreateAwsConfig\")\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\t_, err := awshelper.NewAWSConfigBuilder().\n\t\tWithSessionConfig(&awshelper.AwsSessionConfig{\n\t\t\tRegion:        \"not-existing-region\",\n\t\t\tCredsFilename: \"/tmp/not-existing-file\",\n\t\t}).\n\t\tBuild(t.Context(), l)\n\tassert.Error(t, err)\n}\n\n// Test to validate cases when is not possible to read all S3 configurations\n// https://github.com/gruntwork-io/terragrunt/issues/2109\nfunc TestAwsNegativePublicAccessResponse(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tresponse *s3.GetPublicAccessBlockOutput\n\t\tname     string\n\t}{\n\t\t{\n\t\t\tname: \"nil-response\",\n\t\t\tresponse: &s3.GetPublicAccessBlockOutput{\n\t\t\t\tPublicAccessBlockConfiguration: nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"legacy-bucket\",\n\t\t\tresponse: &s3.GetPublicAccessBlockOutput{\n\t\t\t\tPublicAccessBlockConfiguration: &s3types.PublicAccessBlockConfiguration{\n\t\t\t\t\tBlockPublicAcls:       nil,\n\t\t\t\t\tBlockPublicPolicy:     nil,\n\t\t\t\t\tIgnorePublicAcls:      nil,\n\t\t\t\t\tRestrictPublicBuckets: nil,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"false-response\",\n\t\t\tresponse: &s3.GetPublicAccessBlockOutput{\n\t\t\t\tPublicAccessBlockConfiguration: &s3types.PublicAccessBlockConfiguration{\n\t\t\t\t\tBlockPublicAcls:       aws.Bool(false),\n\t\t\t\t\tBlockPublicPolicy:     aws.Bool(false),\n\t\t\t\t\tIgnorePublicAcls:      aws.Bool(false),\n\t\t\t\t\tRestrictPublicBuckets: aws.Bool(false),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresponse, err := awshelper.ValidatePublicAccessBlock(tc.response)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.False(t, response)\n\t\t})\n\t}\n}\n\nfunc TestCreateAwsConfigWithAuthProviderEnv(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\tctx := context.Background()\n\n\tenv := map[string]string{\n\t\t\"AWS_ACCESS_KEY_ID\":     \"test-access-key\",\n\t\t\"AWS_SECRET_ACCESS_KEY\": \"test-secret-key\",\n\t\t\"AWS_SESSION_TOKEN\":     \"test-session-token\",\n\t\t\"AWS_REGION\":            \"us-west-2\",\n\t}\n\n\tcfg, err := awshelper.NewAWSConfigBuilder().\n\t\tWithEnv(env).\n\t\tBuild(ctx, l)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"us-west-2\", cfg.Region)\n\n\tassert.NotNil(t, cfg.Credentials)\n}\n\nfunc TestCreateAwsConfigWithAuthProviderEnvDefaultRegion(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\tctx := context.Background()\n\n\tenv := map[string]string{\n\t\t\"AWS_ACCESS_KEY_ID\":     \"test-access-key\",\n\t\t\"AWS_SECRET_ACCESS_KEY\": \"test-secret-key\",\n\t\t\"AWS_DEFAULT_REGION\":    \"eu-west-1\",\n\t}\n\n\tcfg, err := awshelper.NewAWSConfigBuilder().\n\t\tWithEnv(env).\n\t\tBuild(ctx, l)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"eu-west-1\", cfg.Region)\n\tassert.NotNil(t, cfg.Credentials)\n}\n\nfunc TestAwsConfigRegionTakesPrecedenceOverEnvVars(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\tctx := context.Background()\n\n\t// Simulate env vars; do not mutate process env in parallel tests\n\tenv := map[string]string{\n\t\t\"AWS_REGION\":            \"us-west-1\",\n\t\t\"AWS_DEFAULT_REGION\":    \"us-west-1\",\n\t\t\"AWS_ACCESS_KEY_ID\":     \"test-access-key\",\n\t\t\"AWS_SECRET_ACCESS_KEY\": \"test-secret-key\",\n\t}\n\n\t// Create config with explicit region that should take precedence\n\tawsCfg := &awshelper.AwsSessionConfig{\n\t\tRegion: \"us-east-1\", // This should override the env vars\n\t}\n\n\tcfg, err := awshelper.NewAWSConfigBuilder().\n\t\tWithSessionConfig(awsCfg).\n\t\tWithEnv(env).\n\t\tBuild(ctx, l)\n\trequire.NoError(t, err)\n\n\t// Verify that the config uses the region from awsCfg, not from environment variables\n\tassert.Equal(t, \"us-east-1\", cfg.Region)\n}\n"
  },
  {
    "path": "internal/awshelper/policy.go",
    "content": "package awshelper\n\nimport \"encoding/json\"\n\n// Policy - representation of the policy for AWS\ntype Policy struct {\n\tVersion   string      `json:\"Version\"`\n\tStatement []Statement `json:\"Statement\"`\n}\n\n// Statement - AWS policy statement\n// Action and Resource - can be string OR array of strings\n// https://docs.aws.amazon.com/IAM//latest/UserGuide/reference_policies_elements_action.html\n// https://docs.aws.amazon.com/IAM//latest/UserGuide/reference_policies_elements_resource.html\ntype Statement struct {\n\tPrincipal    any             `json:\"Principal,omitempty\"`\n\tNotPrincipal any             `json:\"NotPrincipal,omitempty\"`\n\tAction       any             `json:\"Action\"`\n\tResource     any             `json:\"Resource\"`\n\tCondition    *map[string]any `json:\"Condition,omitempty\"`\n\tSid          string          `json:\"Sid\"`\n\tEffect       string          `json:\"Effect\"`\n}\n\nfunc UnmarshalPolicy(policy string) (Policy, error) {\n\tvar p Policy\n\n\terr := json.Unmarshal([]byte(policy), &p)\n\tif err != nil {\n\t\treturn p, err\n\t}\n\n\treturn p, nil\n}\n\nfunc MarshalPolicy(policy Policy) ([]byte, error) {\n\tpolicyJSON, err := json.Marshal(policy)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn policyJSON, nil\n}\n"
  },
  {
    "path": "internal/awshelper/policy_test.go",
    "content": "//go:build aws\n\npackage awshelper_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/awshelper\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst simplePolicy = `\n\t\t{\n\t\t\t\"Version\": \"2012-10-17\",\n\t\t\t\"Statement\": [\n\t\t\t\t{\n\t\t\t\t\t\"Sid\": \"StringValues\",\n\t\t\t\t\t\"Effect\": \"Allow\",\n\t\t\t\t\t\"Action\": \"s3:*\",\n\t\t\t\t\t\"Resource\": \"*\"\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t`\n\nconst arraysPolicy = `\n\t\t{\n\t\t\t\"Version\": \"2012-10-17\",\n\t\t\t\"Statement\": [\n\t\t\t\t{\n\t\t\t\t\t\"Sid\": \"Lists\",\n\t\t\t\t\t\"Effect\": \"Allow\",\n\t\t\t\t\t\"Action\": [\n\t\t\t\t\t\t\"s3:ListStorageLensConfigurations\",\n\t\t\t\t\t\t\"s3:ListAccessPointsForObjectLambda\",\n\t\t\t\t\t\t\"s3:ListBucketMultipartUploads\",\n\t\t\t\t\t\t\"s3:ListAllMyBuckets\",\n\t\t\t\t\t\t\"s3:DescribeJob\",\n\t\t\t\t\t\t\"s3:ListAccessPoints\",\n\t\t\t\t\t\t\"s3:ListJobs\",\n\t\t\t\t\t\t\"s3:ListBucketVersions\",\n\t\t\t\t\t\t\"s3:ListBucket\",\n\t\t\t\t\t\t\"s3:ListMultiRegionAccessPoints\",\n\t\t\t\t\t\t\"s3:ListMultipartUploadParts\"\n\t\t\t\t\t],\n\t\t\t\t\t\"Resource\": [\n\t\t\t\t\t\t\"arn:aws:s3:::*\",\n\t\t\t\t\t\t\"arn:aws:s3:*:666:job/*\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t}\n\t`\n\nfunc TestAwsUnmarshalStringActionResource(t *testing.T) {\n\tt.Parallel()\n\n\tbucketPolicy, err := awshelper.UnmarshalPolicy(simplePolicy)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, bucketPolicy)\n\tassert.Len(t, bucketPolicy.Statement, 1)\n\tassert.NotNil(t, bucketPolicy.Statement[0].Action)\n\tassert.NotNil(t, bucketPolicy.Statement[0].Resource)\n\n\tswitch action := bucketPolicy.Statement[0].Action.(type) {\n\tcase string:\n\t\tassert.Equal(t, \"s3:*\", action)\n\tdefault:\n\t\tassert.Fail(t, \"Expected string type for Action\")\n\t}\n\n\tswitch resource := bucketPolicy.Statement[0].Resource.(type) {\n\tcase string:\n\t\tassert.Equal(t, \"*\", resource)\n\tdefault:\n\t\tassert.Fail(t, \"Expected string type for Resource\")\n\t}\n\n\tout, err := awshelper.MarshalPolicy(bucketPolicy)\n\trequire.NoError(t, err)\n\tassert.NotContains(t, string(out), \"null\")\n}\n\nfunc TestAwsUnmarshalActionResourceList(t *testing.T) {\n\tt.Parallel()\n\n\tbucketPolicy, err := awshelper.UnmarshalPolicy(arraysPolicy)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, bucketPolicy)\n\tassert.Len(t, bucketPolicy.Statement, 1)\n\tassert.NotNil(t, bucketPolicy.Statement[0].Action)\n\tassert.NotNil(t, bucketPolicy.Statement[0].Resource)\n\n\tswitch actions := bucketPolicy.Statement[0].Action.(type) {\n\tcase []any:\n\t\tassert.Len(t, actions, 11)\n\t\tassert.Contains(t, actions, \"s3:ListJobs\")\n\tdefault:\n\t\tassert.Fail(t, \"Expected []string type for Action\")\n\t}\n\n\tswitch resource := bucketPolicy.Statement[0].Resource.(type) {\n\tcase []any:\n\t\tassert.Len(t, resource, 2)\n\t\tassert.Contains(t, resource, \"arn:aws:s3:*:666:job/*\")\n\tdefault:\n\t\tassert.Fail(t, \"Expected []string type for Resource\")\n\t}\n\n\tout, err := awshelper.MarshalPolicy(bucketPolicy)\n\trequire.NoError(t, err)\n\tassert.NotContains(t, string(out), \"null\")\n}\n"
  },
  {
    "path": "internal/cache/cache.go",
    "content": "// Package cache provides generic cache.\n// It is used to store values by key and retrieve them later.\npackage cache\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n)\n\n// Cache - generic cache implementation\ntype Cache[V any] struct {\n\tCache map[string]V\n\tMutex *sync.RWMutex\n\tName  string\n}\n\n// NewCache - create new cache with generic type V\nfunc NewCache[V any](name string) *Cache[V] {\n\treturn &Cache[V]{\n\t\tName:  name,\n\t\tCache: make(map[string]V),\n\t\tMutex: &sync.RWMutex{},\n\t}\n}\n\n// Get - fetch value from cache by key\nfunc (c *Cache[V]) Get(ctx context.Context, key string) (V, bool) {\n\tc.Mutex.RLock()\n\tdefer c.Mutex.RUnlock()\n\n\tkeyHash := sha256.Sum256([]byte(key))\n\tcacheKey := hex.EncodeToString(keyHash[:])\n\tvalue, found := c.Cache[cacheKey]\n\n\ttelemetry.TelemeterFromContext(ctx).Count(ctx, c.Name+\"_cache_get\", 1)\n\n\tif found {\n\t\ttelemetry.TelemeterFromContext(ctx).Count(ctx, c.Name+\"_cache_hit\", 1)\n\t} else {\n\t\ttelemetry.TelemeterFromContext(ctx).Count(ctx, c.Name+\"_cache_miss\", 1)\n\t}\n\n\treturn value, found\n}\n\n// Put - put value into cache by key\nfunc (c *Cache[V]) Put(ctx context.Context, key string, value V) {\n\tc.Mutex.Lock()\n\tdefer c.Mutex.Unlock()\n\n\ttelemetry.TelemeterFromContext(ctx).Count(ctx, c.Name+\"_cache_put\", 1)\n\n\tkeyHash := sha256.Sum256([]byte(key))\n\tcacheKey := hex.EncodeToString(keyHash[:])\n\tc.Cache[cacheKey] = value\n}\n\n// ExpiringItem - item with expiration time\ntype ExpiringItem[V any] struct {\n\tValue      V\n\tExpiration time.Time\n}\n\n// ExpiringCache - cache with items with expiration time\ntype ExpiringCache[V any] struct {\n\tCache map[string]ExpiringItem[V]\n\tMutex *sync.RWMutex\n\tName  string\n}\n\n// NewExpiringCache - create new cache with generic type V\nfunc NewExpiringCache[V any](name string) *ExpiringCache[V] {\n\treturn &ExpiringCache[V]{\n\t\tName:  name,\n\t\tCache: make(map[string]ExpiringItem[V]),\n\t\tMutex: &sync.RWMutex{},\n\t}\n}\n\n// Get - fetch value from cache by key\nfunc (c *ExpiringCache[V]) Get(ctx context.Context, key string) (V, bool) {\n\tc.Mutex.Lock()\n\tdefer c.Mutex.Unlock()\n\n\titem, found := c.Cache[key]\n\ttelemetry.TelemeterFromContext(ctx).Count(ctx, c.Name+\"_cache_get\", 1)\n\n\tif !found {\n\t\ttelemetry.TelemeterFromContext(ctx).Count(ctx, c.Name+\"_cache_miss\", 1)\n\t\treturn item.Value, false\n\t}\n\n\tif time.Now().After(item.Expiration) {\n\t\ttelemetry.TelemeterFromContext(ctx).Count(ctx, c.Name+\"_cache_expiry\", 1)\n\t\tdelete(c.Cache, key)\n\n\t\treturn item.Value, false\n\t}\n\n\ttelemetry.TelemeterFromContext(ctx).Count(ctx, c.Name+\"_cache_hit\", 1)\n\n\treturn item.Value, true\n}\n\n// Put - put value into cache by key\nfunc (c *ExpiringCache[V]) Put(ctx context.Context, key string, value V, expiration time.Time) {\n\tc.Mutex.Lock()\n\tdefer c.Mutex.Unlock()\n\n\ttelemetry.TelemeterFromContext(ctx).Count(ctx, c.Name+\"_cache_put\", 1)\n\tc.Cache[key] = ExpiringItem[V]{Value: value, Expiration: expiration}\n}\n\n// ContextCache returns cache from the context. If the cache is nil, it creates a new instance.\nfunc ContextCache[T any](ctx context.Context, key any) *Cache[T] {\n\tcacheInstance, ok := ctx.Value(key).(*Cache[T])\n\tif !ok || cacheInstance == nil {\n\t\tcacheInstance = NewCache[T](fmt.Sprintf(\"%v\", key))\n\t}\n\n\treturn cacheInstance\n}\n"
  },
  {
    "path": "internal/cache/cache_test.go",
    "content": "package cache_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cache\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCacheCreation(t *testing.T) {\n\tt.Parallel()\n\n\tcache := cache.NewCache[string](\"test\")\n\n\tassert.NotNil(t, cache.Mutex)\n\tassert.NotNil(t, cache.Cache)\n\n\tassert.Empty(t, cache.Cache)\n}\n\nfunc TestStringCacheOperation(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\tcache := cache.NewCache[string](\"test\")\n\n\tvalue, found := cache.Get(ctx, \"potato\")\n\n\tassert.False(t, found)\n\tassert.Empty(t, value)\n\n\tcache.Put(ctx, \"potato\", \"carrot\")\n\tvalue, found = cache.Get(ctx, \"potato\")\n\n\tassert.True(t, found)\n\tassert.NotEmpty(t, value)\n\tassert.Equal(t, \"carrot\", value)\n}\n\nfunc TestExpiringCacheCreation(t *testing.T) {\n\tt.Parallel()\n\n\tcache := cache.NewExpiringCache[string](\"test\")\n\n\tassert.NotNil(t, cache.Mutex)\n\tassert.NotNil(t, cache.Cache)\n\n\tassert.Empty(t, cache.Cache)\n}\n\nfunc TestExpiringCacheOperation(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\tcache := cache.NewExpiringCache[string](\"test\")\n\n\tvalue, found := cache.Get(ctx, \"potato\")\n\n\tassert.False(t, found)\n\tassert.Empty(t, value)\n\n\tcache.Put(ctx, \"potato\", \"carrot\", time.Now().Add(1*time.Second))\n\tvalue, found = cache.Get(ctx, \"potato\")\n\n\tassert.True(t, found)\n\tassert.NotEmpty(t, value)\n\tassert.Equal(t, \"carrot\", value)\n}\n\nfunc TestExpiringCacheExpiration(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\tcache := cache.NewExpiringCache[string](\"test\")\n\n\tcache.Put(ctx, \"potato\", \"carrot\", time.Now().Add(-1*time.Second))\n\tvalue, found := cache.Get(ctx, \"potato\")\n\n\tassert.False(t, found)\n\tassert.NotEmpty(t, value)\n\tassert.Equal(t, \"carrot\", value)\n}\n"
  },
  {
    "path": "internal/cache/context.go",
    "content": "package cache\n\nimport (\n\t\"context\"\n)\n\nconst (\n\t// RunCmdCacheContextKey is the context key used to store and retrieve the run command cache\n\tRunCmdCacheContextKey ctxKey = iota\n\n\t// runCmdCacheName is the identifier for the run command cache instance\n\trunCmdCacheName = \"runCmdCache\"\n)\n\n// ctxKey is a type-safe context key type to prevent key collisions\ntype ctxKey byte\n\nfunc ContextWithCache(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, RunCmdCacheContextKey, NewCache[string](runCmdCacheName))\n}\n"
  },
  {
    "path": "internal/cas/.gitignore",
    "content": "*.test\n"
  },
  {
    "path": "internal/cas/benchmark_test.go",
    "content": "package cas_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cas\"\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n)\n\nfunc BenchmarkClone(b *testing.B) {\n\t// Use a small, public repository for consistent results\n\trepo := \"https://github.com/gruntwork-io/terragrunt.git\"\n\n\tl := logger.CreateLogger()\n\n\tb.Run(\"fresh clone\", func(b *testing.B) {\n\t\ttempDir := b.TempDir()\n\n\t\tb.ResetTimer()\n\n\t\tfor i := 0; b.Loop(); i++ {\n\t\t\tb.StopTimer()\n\n\t\t\tstorePath := filepath.Join(tempDir, \"store\", strconv.Itoa(i))\n\t\t\ttargetPath := filepath.Join(tempDir, \"repo\", strconv.Itoa(i))\n\n\t\t\tc, err := cas.New(cas.Options{\n\t\t\t\tStorePath: storePath,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\n\t\t\tb.StartTimer()\n\n\t\t\tif err := c.Clone(b.Context(), l, &cas.CloneOptions{\n\t\t\t\tDir: targetPath,\n\t\t\t}, repo); err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t})\n\n\tb.Run(\"clone with existing store\", func(b *testing.B) {\n\t\ttempDir := b.TempDir()\n\t\tstorePath := filepath.Join(tempDir, \"store\")\n\n\t\t// First clone to populate store\n\t\tc, err := cas.New(cas.Options{\n\t\t\tStorePath: storePath,\n\t\t})\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\n\t\tif err := c.Clone(b.Context(), l, &cas.CloneOptions{\n\t\t\tDir: filepath.Join(tempDir, \"initial\"),\n\t\t}, repo); err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\n\t\tb.ResetTimer()\n\n\t\tfor i := 0; b.Loop(); i++ {\n\t\t\tb.StopTimer()\n\n\t\t\ttargetPath := filepath.Join(tempDir, \"repo\", strconv.Itoa(i))\n\n\t\t\tc, err := cas.New(cas.Options{\n\t\t\t\tStorePath: storePath,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\n\t\t\tb.StartTimer()\n\n\t\t\tif err := c.Clone(b.Context(), l, &cas.CloneOptions{\n\t\t\t\tDir: targetPath,\n\t\t\t}, repo); err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc BenchmarkContent(b *testing.B) {\n\tstore := cas.NewStore(b.TempDir())\n\n\tcontent := cas.NewContent(store)\n\n\t// Prepare test data\n\ttestData := []byte(\"test content for benchmarking\")\n\n\tl := logger.CreateLogger()\n\n\tb.Run(\"store\", func(b *testing.B) {\n\t\tfor i := 0; b.Loop(); i++ {\n\t\t\tb.StopTimer()\n\n\t\t\thash := \"benchmark\" + strconv.Itoa(i)\n\n\t\t\tb.StartTimer()\n\n\t\t\tif err := content.Store(l, hash, testData); err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t})\n\n\tb.Run(\"parallel_store\", func(b *testing.B) {\n\t\tvar mu sync.Mutex\n\n\t\tseen := make(map[string]bool)\n\n\t\tb.RunParallel(func(pb *testing.PB) {\n\t\t\ti := 0\n\t\t\tfor pb.Next() {\n\t\t\t\t// Generate unique hash for each goroutine iteration\n\t\t\t\thash := fmt.Sprintf(\"benchmark%d_%d_%d\", b.N, i, time.Now().UnixNano())\n\n\t\t\t\tmu.Lock()\n\n\t\t\t\tif seen[hash] {\n\t\t\t\t\tmu.Unlock()\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tseen[hash] = true\n\n\t\t\t\tmu.Unlock()\n\n\t\t\t\tif err := content.Store(l, hash, testData); err != nil {\n\t\t\t\t\tb.Fatal(err)\n\t\t\t\t}\n\n\t\t\t\ti++\n\t\t\t}\n\t\t})\n\t})\n}\n\nfunc BenchmarkGitOperations(b *testing.B) {\n\t// Setup a git repository for testing\n\trepoDir := b.TempDir()\n\n\tg, err := git.NewGitRunner()\n\tif err != nil {\n\t\tb.Fatal(err)\n\t}\n\n\tg = g.WithWorkDir(repoDir)\n\n\tctx := b.Context()\n\n\tif err = g.Clone(ctx, \"https://github.com/gruntwork-io/terragrunt.git\", false, 1, \"main\"); err != nil {\n\t\tb.Fatal(err)\n\t}\n\n\tb.Run(\"ls-remote\", func(b *testing.B) {\n\t\tg, err = git.NewGitRunner()\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\n\t\tg = g.WithWorkDir(repoDir)\n\n\t\tb.ResetTimer()\n\n\t\tfor b.Loop() {\n\t\t\t_, err := g.LsRemote(ctx, \"https://github.com/gruntwork-io/terragrunt.git\", \"HEAD\")\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t})\n\n\tb.Run(\"ls-tree -r\", func(b *testing.B) {\n\t\tb.ResetTimer()\n\n\t\tfor b.Loop() {\n\t\t\t_, err := g.LsTreeRecursive(ctx, \"HEAD\")\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t})\n\n\tb.Run(\"cat-file\", func(b *testing.B) {\n\t\t// First get a valid hash\n\t\ttree, err := g.LsTreeRecursive(ctx, \"HEAD\")\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\n\t\tif len(tree.Entries()) == 0 {\n\t\t\tb.Fatal(\"no entries in tree\")\n\t\t}\n\n\t\thash := tree.Entries()[0].Hash\n\n\t\ttmpFile := b.TempDir() + \"/cat-file\"\n\n\t\ttmp, err := os.Create(tmpFile)\n\t\tif err != nil {\n\t\t\tb.Fatal(err)\n\t\t}\n\n\t\tdefer os.Remove(tmpFile)\n\t\tdefer tmp.Close()\n\n\t\tb.ResetTimer()\n\n\t\tfor b.Loop() {\n\t\t\terr := g.CatFile(ctx, hash, tmp)\n\t\t\tif err != nil {\n\t\t\t\tb.Fatal(err)\n\t\t\t}\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/cas/cas.go",
    "content": "// Package cas implements a content-addressable storage for git content.\n//\n// Blobs are copied from cloned repositories to a local store, along with trees.\n// When the same content is requested again, the content is read from the local store,\n// avoiding the need to clone the repository or read from the network.\npackage cas\n\nimport (\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/gofrs/flock\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// Options configures the behavior of CAS\ntype Options struct {\n\t// StorePath specifies a custom path for the content store\n\t// If empty, uses $HOME/.cache/terragrunt/cas/store\n\tStorePath string\n}\n\n// CloneOptions configures the behavior of a specific clone operation\ntype CloneOptions struct {\n\t// Dir specifies the target directory for the clone\n\t// If empty, uses the repository name\n\tDir string\n\n\t// Branch specifies which branch to clone\n\t// If empty, uses HEAD\n\tBranch string\n\n\t// IncludedGitFiles specifies the files to preserve from the .git directory\n\t// If empty, does not preserve any files\n\tIncludedGitFiles []string\n}\n\n// CAS clones a git repository using content-addressable storage.\ntype CAS struct {\n\tstore *Store\n\tgit   *git.GitRunner\n\topts  Options\n}\n\n// New creates a new CAS instance with the given options\n//\n// TODO: Make these options optional\nfunc New(opts Options) (*CAS, error) {\n\tif opts.StorePath == \"\" {\n\t\thome, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\topts.StorePath = filepath.Join(home, \".cache\", \"terragrunt\", \"cas\", \"store\")\n\t}\n\n\tif err := os.MkdirAll(opts.StorePath, DefaultDirPerms); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create CAS store path: %w\", err)\n\t}\n\n\tstore := NewStore(opts.StorePath)\n\n\tg, err := git.NewGitRunner()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &CAS{\n\t\tstore: store,\n\t\tgit:   g,\n\t\topts:  opts,\n\t}, nil\n}\n\n// Clone performs the clone operation\n//\n// TODO: Make options optional\nfunc (c *CAS) Clone(ctx context.Context, l log.Logger, opts *CloneOptions, url string) error {\n\t// Ensure the store path exists\n\tif err := os.MkdirAll(c.store.Path(), DefaultDirPerms); err != nil {\n\t\treturn fmt.Errorf(\"failed to create store path: %w\", err)\n\t}\n\n\t// Acquire global clone lock to ensure only one clone at a time\n\tglobalLock := flock.New(filepath.Join(c.store.Path(), \"clone.lock\"))\n\n\tif err := globalLock.Lock(); err != nil {\n\t\treturn fmt.Errorf(\"failed to acquire global clone lock: %w\", err)\n\t}\n\n\tdefer func() {\n\t\tif unlockErr := globalLock.Unlock(); unlockErr != nil {\n\t\t\tl.Warnf(\"failed to release global clone lock: %v\", unlockErr)\n\t\t}\n\t}()\n\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, \"cas_clone\", map[string]any{\n\t\t\"url\":    url,\n\t\t\"branch\": opts.Branch,\n\t}, func(childCtx context.Context) error {\n\t\thash, err := c.resolveReference(childCtx, url, opts.Branch)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ttargetDir := c.prepareTargetDirectory(opts.Dir, url)\n\n\t\tif c.store.NeedsWrite(hash) {\n\t\t\t// Create a temporary directory for git operations\n\t\t\t_, cleanup, createTempDirErr := c.git.CreateTempDir()\n\t\t\tif createTempDirErr != nil {\n\t\t\t\treturn createTempDirErr\n\t\t\t}\n\n\t\t\tdefer func() {\n\t\t\t\tif cleanupErr := cleanup(); cleanupErr != nil {\n\t\t\t\t\tl.Warnf(\"cleanup error: %v\", cleanupErr)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tif cloneAndStoreErr := c.cloneAndStoreContent(childCtx, l, opts, url, hash); cloneAndStoreErr != nil {\n\t\t\t\treturn cloneAndStoreErr\n\t\t\t}\n\t\t}\n\n\t\tcontent := NewContent(c.store)\n\n\t\ttreeData, err := content.Read(hash)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ttree, err := git.ParseTree(treeData, targetDir)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn LinkTree(childCtx, c.store, tree, targetDir)\n\t})\n}\n\nfunc (c *CAS) prepareTargetDirectory(dir, url string) string {\n\ttargetDir := dir\n\tif targetDir == \"\" {\n\t\ttargetDir = git.ExtractRepoName(url)\n\t}\n\n\treturn filepath.Clean(targetDir)\n}\n\nfunc (c *CAS) resolveReference(ctx context.Context, url, branch string) (string, error) {\n\tresults, err := c.git.LsRemote(ctx, url, branch)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif len(results) == 0 {\n\t\treturn \"\", &WrappedError{\n\t\t\tOp:      \"clone\",\n\t\t\tContext: \"no matching reference\",\n\t\t\tErr:     ErrNoMatchingReference,\n\t\t}\n\t}\n\n\treturn results[0].Hash, nil\n}\n\nfunc (c *CAS) cloneAndStoreContent(\n\tctx context.Context,\n\tl log.Logger,\n\topts *CloneOptions,\n\turl,\n\thash string,\n) error {\n\tif err := c.git.Clone(ctx, url, true, 1, opts.Branch); err != nil {\n\t\treturn err\n\t}\n\n\treturn c.storeRootTree(ctx, l, hash, opts)\n}\n\nfunc (c *CAS) storeRootTree(ctx context.Context, l log.Logger, hash string, opts *CloneOptions) error {\n\ttree, err := c.git.LsTreeRecursive(ctx, hash)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err = c.storeTreeRecursive(ctx, l, hash, tree); err != nil {\n\t\treturn err\n\t}\n\n\tif len(opts.IncludedGitFiles) == 0 {\n\t\treturn nil\n\t}\n\n\tcontent := NewContent(c.store)\n\n\tdata, err := content.Read(hash)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, file := range opts.IncludedGitFiles {\n\t\tstat, err := os.Stat(filepath.Join(c.git.WorkDir, file))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif stat.IsDir() {\n\t\t\tcontinue\n\t\t}\n\n\t\tworkDirPath := filepath.Join(c.git.WorkDir, file)\n\n\t\tincludedHash, err := hashFile(workDirPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tincludedContent := NewContent(c.store)\n\n\t\tif err := includedContent.EnsureCopy(l, includedHash, workDirPath); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tpath := filepath.Join(\".git\", file)\n\n\t\tdata = append(data, fmt.Appendf(nil, \"%06o blob %s\\t%s\\n\", stat.Mode().Perm(), includedHash, path)...)\n\t}\n\n\t// Overwrite the root tree with the new data\n\treturn content.Store(l, hash, data)\n}\n\n// storeTreeRecursive stores a tree fetched from git ls-tree -r\nfunc (c *CAS) storeTreeRecursive(ctx context.Context, l log.Logger, hash string, tree *git.Tree) error {\n\tif !c.store.NeedsWrite(hash) {\n\t\treturn nil\n\t}\n\n\tif err := c.storeBlobs(ctx, tree.Entries()); err != nil {\n\t\treturn err\n\t}\n\n\t// Store the tree object itself\n\tcontent := NewContent(c.store)\n\tif err := content.EnsureWithWait(l, hash, tree.Data()); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// storeBlobs stores blobs in the CAS\nfunc (c *CAS) storeBlobs(ctx context.Context, entries []git.TreeEntry) error {\n\tfor _, entry := range entries {\n\t\tif !c.store.NeedsWrite(entry.Hash) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := c.ensureBlob(ctx, entry.Hash); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// ensureBlob ensures that a blob exists in the CAS.\n// It doesn't use the standard content.Store method because\n// we want to take advantage of the ability to write to the\n// entry using `git cat-file`.\nfunc (c *CAS) ensureBlob(ctx context.Context, hash string) error {\n\tneedsWrite, lock, err := c.store.EnsureWithWait(hash)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// If content already exists or was written by another process, we're done\n\tif !needsWrite {\n\t\treturn nil\n\t}\n\n\t// We have the lock and need to write the content\n\tdefer func() {\n\t\tif unlockErr := lock.Unlock(); unlockErr != nil {\n\t\t\terr = errors.Join(err, unlockErr)\n\t\t}\n\t}()\n\n\tcontent := NewContent(c.store)\n\n\ttmpHandle, err := content.GetTmpHandle(hash)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttmpPath := tmpHandle.Name()\n\n\t// We want to make sure we remove the temporary file\n\t// if we encounter an error\n\tdefer func() {\n\t\tif _, osStatErr := os.Stat(tmpPath); osStatErr == nil {\n\t\t\terr = errors.Join(err, os.Remove(tmpPath))\n\t\t}\n\t}()\n\n\terr = c.git.CatFile(ctx, hash, tmpHandle)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// For Windows, ensure data is synchronized to disk\n\tif runtime.GOOS == \"windows\" {\n\t\tif err = tmpHandle.Sync(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif err = tmpHandle.Close(); err != nil {\n\t\treturn err\n\t}\n\n\tif err = os.Rename(tmpPath, content.getPath(hash)); err != nil {\n\t\treturn err\n\t}\n\n\tif err = os.Chmod(content.getPath(hash), StoredFilePerms); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc hashFile(path string) (string, error) {\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdefer file.Close()\n\n\th := sha1.New()\n\n\tif _, err := io.Copy(h, file); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn hex.EncodeToString(h.Sum(nil)), nil\n}\n"
  },
  {
    "path": "internal/cas/cas_test.go",
    "content": "package cas_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cas\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCAS_Clone(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\tt.Run(\"clone new repository\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttempDir := helpers.TmpDirWOSymlinks(t)\n\t\tstorePath := filepath.Join(tempDir, \"store\")\n\t\ttargetPath := filepath.Join(tempDir, \"repo\")\n\n\t\tc, err := cas.New(cas.Options{\n\t\t\tStorePath: storePath,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\terr = c.Clone(t.Context(), l, &cas.CloneOptions{\n\t\t\tDir: targetPath,\n\t\t}, \"https://github.com/gruntwork-io/terragrunt.git\")\n\t\trequire.NoError(t, err)\n\n\t\t// Verify repository was cloned\n\t\t_, err = os.Stat(filepath.Join(targetPath, \"README.md\"))\n\t\trequire.NoError(t, err)\n\n\t\t// Verify nested files were linked\n\t\t_, err = os.Stat(filepath.Join(targetPath, \"test\", \"integration_test.go\"))\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"clone with specific branch\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttempDir := helpers.TmpDirWOSymlinks(t)\n\t\tstorePath := filepath.Join(tempDir, \"store\")\n\t\ttargetPath := filepath.Join(tempDir, \"repo\")\n\n\t\tc, err := cas.New(cas.Options{\n\t\t\tStorePath: storePath,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\terr = c.Clone(t.Context(), l, &cas.CloneOptions{\n\t\t\tDir:    targetPath,\n\t\t\tBranch: \"main\",\n\t\t}, \"https://github.com/gruntwork-io/terragrunt.git\")\n\t\trequire.NoError(t, err)\n\n\t\t// Verify repository was cloned\n\t\t_, err = os.Stat(filepath.Join(targetPath, \"README.md\"))\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"clone with included git files\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttempDir := helpers.TmpDirWOSymlinks(t)\n\t\tstorePath := filepath.Join(tempDir, \"store\")\n\t\ttargetPath := filepath.Join(tempDir, \"repo\")\n\n\t\tc, err := cas.New(cas.Options{\n\t\t\tStorePath: storePath,\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\terr = c.Clone(t.Context(), l, &cas.CloneOptions{\n\t\t\tDir:              targetPath,\n\t\t\tIncludedGitFiles: []string{\"HEAD\", \"config\"},\n\t\t}, \"https://github.com/gruntwork-io/terragrunt.git\")\n\t\trequire.NoError(t, err)\n\n\t\t// Verify repository was cloned\n\t\t_, err = os.Stat(filepath.Join(targetPath, \".git\", \"HEAD\"))\n\t\trequire.NoError(t, err)\n\n\t\t_, err = os.Stat(filepath.Join(targetPath, \".git\", \"config\"))\n\t\trequire.NoError(t, err)\n\t})\n}\n"
  },
  {
    "path": "internal/cas/content.go",
    "content": "package cas\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// Content manages git object storage and linking\ntype Content struct {\n\tstore *Store\n}\n\nconst (\n\t// DefaultDirPerms represents standard directory permissions (rwxr-xr-x)\n\tDefaultDirPerms = os.FileMode(0755)\n\t// StoredFilePerms represents read-only file permissions (r--r--r--)\n\tStoredFilePerms = os.FileMode(0444)\n\t// RegularFilePerms represents standard file permissions (rw-r--r--)\n\tRegularFilePerms = os.FileMode(0644)\n\t// WindowsOS is the name of the Windows operating system\n\tWindowsOS = \"windows\"\n)\n\n// NewContent creates a new Content instance\nfunc NewContent(store *Store) *Content {\n\treturn &Content{\n\t\tstore: store,\n\t}\n}\n\n// Link creates a hard link from the store to the target path\nfunc (c *Content) Link(ctx context.Context, hash, targetPath string) error {\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, \"cas_link\", map[string]any{\n\t\t\"hash\": hash,\n\t\t\"path\": targetPath,\n\t}, func(childCtx context.Context) error {\n\t\tsourcePath := c.getPath(hash)\n\n\t\t// Try to create hard link directly (most efficient path)\n\t\tif err := os.Link(sourcePath, targetPath); err != nil {\n\t\t\t// Check if it's because target already exists\n\t\t\tif os.IsExist(err) {\n\t\t\t\t// File already exists, which is fine\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\t// If hard link fails for other reasons, try to copy the file\n\t\t\tdata, readErr := os.ReadFile(sourcePath)\n\t\t\tif readErr != nil {\n\t\t\t\treturn &WrappedError{\n\t\t\t\t\tOp:   \"read_source\",\n\t\t\t\t\tPath: sourcePath,\n\t\t\t\t\tErr:  ErrReadFile,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Write to temporary file first\n\t\t\ttempPath := targetPath + \".tmp\"\n\t\t\tif err := os.WriteFile(tempPath, data, RegularFilePerms); err != nil {\n\t\t\t\treturn &WrappedError{\n\t\t\t\t\tOp:   \"write_target\",\n\t\t\t\t\tPath: tempPath,\n\t\t\t\t\tErr:  err,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Atomic rename to final path\n\t\t\tif err := os.Rename(tempPath, targetPath); err != nil {\n\t\t\t\treturn &WrappedError{\n\t\t\t\t\tOp:   \"rename_target\",\n\t\t\t\t\tPath: tempPath,\n\t\t\t\t\tErr:  err,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\n// Store stores a single content item. This is typically used for trees,\n// As blobs are written directly from git cat-file stdout.\nfunc (c *Content) Store(l log.Logger, hash string, data []byte) error {\n\tlock, err := c.store.AcquireLock(hash)\n\tif err != nil {\n\t\treturn wrapError(\"acquire_lock\", hash, err)\n\t}\n\n\tdefer func() {\n\t\tif unlockErr := lock.Unlock(); unlockErr != nil {\n\t\t\tl.Warnf(\"failed to unlock filesystem lock for hash %s: %v\", hash, unlockErr)\n\t\t}\n\t}()\n\n\tif err = os.MkdirAll(c.store.Path(), DefaultDirPerms); err != nil {\n\t\treturn wrapError(\"create_store_dir\", c.store.Path(), ErrCreateDir)\n\t}\n\n\t// Ensure partition directory exists\n\tpartitionDir := c.getPartition(hash)\n\tif err = os.MkdirAll(partitionDir, DefaultDirPerms); err != nil {\n\t\treturn wrapError(\"create_partition_dir\", partitionDir, ErrCreateDir)\n\t}\n\n\treturn c.writeContentToFile(l, hash, data)\n}\n\n// Ensure ensures that a content item exists in the store\nfunc (c *Content) Ensure(l log.Logger, hash string, data []byte) error {\n\tpath := c.getPath(hash)\n\tif c.store.hasContent(path) {\n\t\treturn nil\n\t}\n\n\treturn c.Store(l, hash, data)\n}\n\n// EnsureWithWait ensures that a content item exists in the store, with optimization\n// to wait for concurrent writes instead of doing redundant work\nfunc (c *Content) EnsureWithWait(l log.Logger, hash string, data []byte) error {\n\tneedsWrite, lock, err := c.store.EnsureWithWait(hash)\n\tif err != nil {\n\t\treturn wrapError(\"ensure_with_wait\", hash, err)\n\t}\n\n\t// If content already exists or was written by another process, we're done\n\tif !needsWrite {\n\t\treturn nil\n\t}\n\n\t// We have the lock and need to write the content\n\tdefer func() {\n\t\tif unlockErr := lock.Unlock(); unlockErr != nil {\n\t\t\tl.Warnf(\"failed to unlock filesystem lock for hash %s: %v\", hash, unlockErr)\n\t\t}\n\t}()\n\n\tif err = os.MkdirAll(c.store.Path(), DefaultDirPerms); err != nil {\n\t\treturn wrapError(\"create_store_dir\", c.store.Path(), ErrCreateDir)\n\t}\n\n\t// Ensure partition directory exists\n\tpartitionDir := c.getPartition(hash)\n\tif err = os.MkdirAll(partitionDir, DefaultDirPerms); err != nil {\n\t\treturn wrapError(\"create_partition_dir\", partitionDir, ErrCreateDir)\n\t}\n\n\treturn c.writeContentToFile(l, hash, data)\n}\n\n// writeContentToFile writes data to a temporary file,\n// sets appropriate permissions, and performs an atomic rename.\nfunc (c *Content) writeContentToFile(l log.Logger, hash string, data []byte) error {\n\tpath := c.getPath(hash)\n\ttempPath := path + \".tmp\"\n\n\tf, err := os.OpenFile(tempPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, RegularFilePerms)\n\tif err != nil {\n\t\treturn wrapError(\"create_temp_file\", tempPath, err)\n\t}\n\n\tbuf := bufio.NewWriter(f)\n\n\tif _, err := buf.Write(data); err != nil {\n\t\tf.Close()\n\n\t\tif removeErr := os.Remove(tempPath); removeErr != nil {\n\t\t\tl.Warnf(\"failed to remove temp file %s: %v\", tempPath, removeErr)\n\t\t}\n\n\t\treturn wrapError(\"write_to_store\", tempPath, err)\n\t}\n\n\tif err := buf.Flush(); err != nil {\n\t\tf.Close()\n\n\t\tif removeErr := os.Remove(tempPath); removeErr != nil {\n\t\t\tl.Warnf(\"failed to remove temp file %s: %v\", tempPath, removeErr)\n\t\t}\n\n\t\treturn wrapError(\"flush_buffer\", tempPath, err)\n\t}\n\n\tif err := f.Close(); err != nil {\n\t\tif removeErr := os.Remove(tempPath); removeErr != nil {\n\t\t\tl.Warnf(\"failed to remove temp file %s: %v\", tempPath, removeErr)\n\t\t}\n\n\t\treturn wrapError(\"close_file\", tempPath, err)\n\t}\n\n\t// Set read-only permissions on the temporary file\n\tif err := os.Chmod(tempPath, StoredFilePerms); err != nil {\n\t\tif removeErr := os.Remove(tempPath); removeErr != nil {\n\t\t\tl.Warnf(\"failed to remove temp file %s: %v\", tempPath, removeErr)\n\t\t}\n\n\t\treturn wrapError(\"chmod_temp_file\", tempPath, err)\n\t}\n\n\t// For Windows, handle readonly attributes specifically\n\tif runtime.GOOS == WindowsOS {\n\t\t// Check if a destination file exists and is read-only\n\t\tif _, err := os.Stat(path); err == nil {\n\t\t\t// File exists, make it writable before rename operation\n\t\t\tif err := os.Chmod(path, RegularFilePerms); err != nil {\n\t\t\t\tl.Warnf(\"failed to make destination file writable %s: %v\", path, err)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Atomic rename\n\tif err := os.Rename(tempPath, path); err != nil {\n\t\tif removeErr := os.Remove(tempPath); removeErr != nil {\n\t\t\tl.Warnf(\"failed to remove temp file %s: %v\", tempPath, removeErr)\n\t\t}\n\n\t\treturn wrapError(\"finalize_store\", path, err)\n\t}\n\n\t// For Windows, we need to set the permissions again after rename\n\tif runtime.GOOS == WindowsOS {\n\t\t// Ensure the file has read-only permissions after rename\n\t\tif err := os.Chmod(path, StoredFilePerms); err != nil {\n\t\t\treturn wrapError(\"chmod_final_file\", path, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// EnsureCopy ensures that a content item exists in the store by copying from a file\nfunc (c *Content) EnsureCopy(l log.Logger, hash, src string) error {\n\tpath := c.getPath(hash)\n\tif c.store.hasContent(path) {\n\t\treturn nil\n\t}\n\n\tlock, err := c.store.AcquireLock(hash)\n\tif err != nil {\n\t\treturn wrapError(\"acquire_lock\", hash, err)\n\t}\n\n\tdefer func() {\n\t\tif unlockErr := lock.Unlock(); unlockErr != nil {\n\t\t\tl.Warnf(\"failed to unlock filesystem lock for hash %s: %v\", hash, unlockErr)\n\t\t}\n\t}()\n\n\t// Ensure partition directory exists\n\tpartitionDir := c.getPartition(hash)\n\tif err = os.MkdirAll(partitionDir, DefaultDirPerms); err != nil {\n\t\treturn wrapError(\"create_partition_dir\", partitionDir, ErrCreateDir)\n\t}\n\n\tf, err := os.Create(path)\n\tif err != nil {\n\t\treturn wrapError(\"create_file\", path, err)\n\t}\n\n\tdefer f.Close()\n\n\tr, err := os.Open(src)\n\tif err != nil {\n\t\treturn wrapError(\"open_source\", src, err)\n\t}\n\n\tdefer r.Close()\n\n\tif _, err := io.Copy(f, r); err != nil {\n\t\treturn wrapError(\"copy_file\", src, err)\n\t}\n\n\treturn nil\n}\n\n// GetTmpHandle returns a file handle to a temporary file where content will be stored.\nfunc (c *Content) GetTmpHandle(hash string) (*os.File, error) {\n\tpartitionDir := c.getPartition(hash)\n\tif err := os.MkdirAll(partitionDir, DefaultDirPerms); err != nil {\n\t\treturn nil, wrapError(\"create_partition_dir\", partitionDir, ErrCreateDir)\n\t}\n\n\tpath := c.getPath(hash)\n\ttempPath := path + \".tmp\"\n\n\tf, err := os.Create(tempPath)\n\tif err != nil {\n\t\treturn nil, wrapError(\"create_temp_file\", tempPath, err)\n\t}\n\n\treturn f, err\n}\n\n// Read retrieves content from the store by hash\nfunc (c *Content) Read(hash string) ([]byte, error) {\n\tpath := c.getPath(hash)\n\treturn os.ReadFile(path)\n}\n\n// getPartition returns the partition path for a given hash\nfunc (c *Content) getPartition(hash string) string {\n\treturn filepath.Join(c.store.Path(), hash[:2])\n}\n\n// getPath returns the full path for a given hash\nfunc (c *Content) getPath(hash string) string {\n\treturn filepath.Join(c.getPartition(hash), hash)\n}\n"
  },
  {
    "path": "internal/cas/content_test.go",
    "content": "package cas_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cas\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst testHashValue = \"abcdef123456\"\n\nfunc TestContent_Store(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\tt.Run(\"store new content\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tstore := cas.NewStore(helpers.TmpDirWOSymlinks(t))\n\n\t\tcontent := cas.NewContent(store)\n\t\ttestHash := testHashValue\n\t\ttestData := []byte(\"test content\")\n\n\t\terr := content.Store(l, testHash, testData)\n\t\trequire.NoError(t, err)\n\n\t\t// Verify content was stored\n\t\tpartitionDir := filepath.Join(store.Path(), testHash[:2])\n\t\tstoredPath := filepath.Join(partitionDir, testHash)\n\t\tstoredData, err := os.ReadFile(storedPath)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, testData, storedData)\n\t})\n\n\tt.Run(\"ensure existing content\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tstore := cas.NewStore(helpers.TmpDirWOSymlinks(t))\n\n\t\tcontent := cas.NewContent(store)\n\t\ttestHash := testHashValue\n\t\ttestData := []byte(\"test content\")\n\t\tdifferentData := []byte(\"different content\")\n\n\t\t// Store content twice\n\t\terr := content.Ensure(l, testHash, testData)\n\t\trequire.NoError(t, err)\n\t\terr = content.Ensure(l, testHash, differentData)\n\t\trequire.NoError(t, err)\n\n\t\t// Verify original content remains\n\t\tpartitionDir := filepath.Join(store.Path(), testHash[:2])\n\t\tstoredPath := filepath.Join(partitionDir, testHash)\n\t\tstoredData, err := os.ReadFile(storedPath)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, testData, storedData)\n\t})\n\n\tt.Run(\"overwrite existing content\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tstore := cas.NewStore(helpers.TmpDirWOSymlinks(t))\n\n\t\tcontent := cas.NewContent(store)\n\t\ttestHash := testHashValue\n\t\ttestData := []byte(\"test content\")\n\t\tdifferentData := []byte(\"different content\")\n\n\t\t// Store content twice\n\t\terr := content.Store(l, testHash, testData)\n\t\trequire.NoError(t, err)\n\t\terr = content.Store(l, testHash, differentData)\n\t\trequire.NoError(t, err)\n\n\t\t// Verify original content remains\n\t\tpartitionDir := filepath.Join(store.Path(), testHash[:2])\n\t\tstoredPath := filepath.Join(partitionDir, testHash)\n\t\tstoredData, err := os.ReadFile(storedPath)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, differentData, storedData)\n\t})\n}\n\nfunc TestContent_Link(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\tt.Run(\"create new link\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tstoreDir := helpers.TmpDirWOSymlinks(t)\n\t\tstore := cas.NewStore(storeDir)\n\n\t\tcontent := cas.NewContent(store)\n\t\ttestHash := testHashValue\n\t\ttestData := []byte(\"test content\")\n\n\t\t// First store some content\n\t\terr := content.Store(l, testHash, testData)\n\t\trequire.NoError(t, err)\n\n\t\t// Then create a link to it\n\t\ttargetDir := helpers.TmpDirWOSymlinks(t)\n\t\ttargetPath := filepath.Join(targetDir, \"test.txt\")\n\n\t\terr = content.Link(t.Context(), testHash, targetPath)\n\t\trequire.NoError(t, err)\n\n\t\t// Verify link was created and contains correct content\n\t\tlinkedData, err := os.ReadFile(targetPath)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, testData, linkedData)\n\n\t\t// Verify it's a hard link by checking inode numbers\n\t\tpartitionDir := filepath.Join(store.Path(), testHash[:2])\n\t\tsourceInfo, err := os.Stat(filepath.Join(partitionDir, testHash))\n\t\trequire.NoError(t, err)\n\t\ttargetInfo, err := os.Stat(targetPath)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, sourceInfo.Sys(), targetInfo.Sys())\n\t})\n\n\tt.Run(\"link to existing file\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tstore := cas.NewStore(helpers.TmpDirWOSymlinks(t))\n\n\t\tcontent := cas.NewContent(store)\n\t\ttestHash := testHashValue\n\t\ttestData := []byte(\"test content\")\n\n\t\t// Store content\n\t\terr := content.Store(l, testHash, testData)\n\t\trequire.NoError(t, err)\n\n\t\t// Create target file\n\t\ttargetDir := helpers.TmpDirWOSymlinks(t)\n\t\ttargetPath := filepath.Join(targetDir, \"test.txt\")\n\t\terr = os.WriteFile(targetPath, []byte(\"existing content\"), 0644)\n\t\trequire.NoError(t, err)\n\n\t\t// Try to create link\n\t\terr = content.Link(t.Context(), testHash, targetPath)\n\t\trequire.NoError(t, err)\n\n\t\t// Verify original content remains\n\t\texistingData, err := os.ReadFile(targetPath)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []byte(\"existing content\"), existingData)\n\t})\n}\n\nfunc TestContent_EnsureWithWait(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\tt.Run(\"content already exists\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tstore := cas.NewStore(helpers.TmpDirWOSymlinks(t))\n\t\tcontent := cas.NewContent(store)\n\t\ttestHash := testHashValue\n\t\ttestData := []byte(\"test content\")\n\n\t\t// Store content first\n\t\terr := content.Store(l, testHash, testData)\n\t\trequire.NoError(t, err)\n\n\t\t// EnsureWithWait should not need to write again\n\t\terr = content.EnsureWithWait(l, testHash, []byte(\"different content\"))\n\t\trequire.NoError(t, err)\n\n\t\t// Verify original content remains\n\t\tpartitionDir := filepath.Join(store.Path(), testHash[:2])\n\t\tstoredPath := filepath.Join(partitionDir, testHash)\n\t\tstoredData, err := os.ReadFile(storedPath)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, testData, storedData)\n\t})\n\n\tt.Run(\"content doesn't exist\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tstore := cas.NewStore(helpers.TmpDirWOSymlinks(t))\n\t\tcontent := cas.NewContent(store)\n\t\ttestHash := \"newcontent123456\"\n\t\ttestData := []byte(\"new test content\")\n\n\t\t// EnsureWithWait should store the content\n\t\terr := content.EnsureWithWait(l, testHash, testData)\n\t\trequire.NoError(t, err)\n\n\t\t// Verify content was stored\n\t\tpartitionDir := filepath.Join(store.Path(), testHash[:2])\n\t\tstoredPath := filepath.Join(partitionDir, testHash)\n\t\tstoredData, err := os.ReadFile(storedPath)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, testData, storedData)\n\t})\n\n\tt.Run(\"concurrent writes - optimization\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tstore := cas.NewStore(helpers.TmpDirWOSymlinks(t))\n\t\tcontent := cas.NewContent(store)\n\t\ttestHash := \"concurrent123456\"\n\n\t\t// Channel to coordinate the test\n\t\tprocess1Started := make(chan struct{})\n\t\tprocess1Done := make(chan struct{})\n\t\tprocess2Done := make(chan struct{})\n\n\t\t// Process 1: acquires lock first\n\t\tgo func() {\n\t\t\tdefer close(process1Done)\n\n\t\t\terr := content.EnsureWithWait(l, testHash, []byte(\"process 1 data\"))\n\t\t\tassert.NoError(t, err)\n\n\t\t\tclose(process1Started)\n\t\t}()\n\n\t\t// Process 2: should wait for process 1 and not duplicate work\n\t\tgo func() {\n\t\t\tdefer close(process2Done)\n\n\t\t\t// Wait for process 1 to start\n\t\t\t<-process1Started\n\n\t\t\terr := content.EnsureWithWait(l, testHash, []byte(\"process 2 data\"))\n\t\t\tassert.NoError(t, err)\n\t\t}()\n\n\t\t// Wait for both to complete\n\t\t<-process1Done\n\t\t<-process2Done\n\n\t\t// Verify only one content exists (from process 1)\n\t\tpartitionDir := filepath.Join(store.Path(), testHash[:2])\n\t\tstoredPath := filepath.Join(partitionDir, testHash)\n\t\tstoredData, err := os.ReadFile(storedPath)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []byte(\"process 1 data\"), storedData)\n\t})\n}\n"
  },
  {
    "path": "internal/cas/errors.go",
    "content": "package cas\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\n// Error types that can be returned by the cas package\ntype Error string\n\nfunc (e Error) Error() string {\n\treturn string(e)\n}\n\nconst (\n\t// ErrTempDir is returned when failing to create or close a temporary directory\n\tErrTempDir Error = \"failed to create or manage temporary directory\"\n\t// ErrCreateDir is returned when failing to create a directory\n\tErrCreateDir Error = \"failed to create directory\"\n\t// ErrReadFile is returned when failing to read a file\n\tErrReadFile Error = \"failed to read file\"\n\t// ErrGitClone is returned when the git clone operation fails\n\tErrGitClone Error = \"failed to complete git clone\"\n)\n\n// WrappedError provides additional context for errors\ntype WrappedError struct {\n\tOp      string // Operation that failed\n\tPath    string // File path if applicable\n\tErr     error  // Original error\n\tContext string // Additional context\n}\n\nfunc (e *WrappedError) Error() string {\n\tif e.Context != \"\" {\n\t\treturn fmt.Sprintf(\"%s: %s: %v\", e.Op, e.Context, e.Err)\n\t}\n\n\treturn fmt.Sprintf(\"%s: %v\", e.Op, e.Err)\n}\n\nfunc (e *WrappedError) Unwrap() error {\n\treturn e.Err\n}\n\n// Git operation errors\nvar (\n\tErrCommandSpawn        = errors.New(\"failed to spawn git command\")\n\tErrNoMatchingReference = errors.New(\"no matching reference\")\n\tErrReadTree            = errors.New(\"failed to read tree\")\n\tErrNoWorkDir           = errors.New(\"working directory not set\")\n)\n\nfunc wrapError(op, path string, err error) error {\n\treturn &WrappedError{\n\t\tOp:   op,\n\t\tPath: path,\n\t\tErr:  err,\n\t}\n}\n"
  },
  {
    "path": "internal/cas/errors_test.go",
    "content": "package cas_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cas\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestErrorString(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname string\n\t\terr  cas.Error\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"temp dir error\",\n\t\t\terr:  cas.ErrTempDir,\n\t\t\twant: \"failed to create or manage temporary directory\",\n\t\t},\n\t\t{\n\t\t\tname: \"git clone error\",\n\t\t\terr:  cas.ErrGitClone,\n\t\t\twant: \"failed to complete git clone\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, tt.want, tt.err.Error())\n\t\t})\n\t}\n}\n\nfunc TestWrappedError(t *testing.T) {\n\tt.Parallel()\n\n\tbaseErr := errors.New(\"base error\")\n\ttests := []struct {\n\t\tname    string\n\t\twrapped *cas.WrappedError\n\t\twant    string\n\t}{\n\t\t{\n\t\t\tname: \"with path\",\n\t\t\twrapped: &cas.WrappedError{\n\t\t\t\tOp:   \"clone\",\n\t\t\t\tPath: \"/tmp/repo\",\n\t\t\t\tErr:  baseErr,\n\t\t\t},\n\t\t\twant: \"clone: base error\",\n\t\t},\n\t\t{\n\t\t\tname: \"with context\",\n\t\t\twrapped: &cas.WrappedError{\n\t\t\t\tOp:      \"clone\",\n\t\t\t\tContext: \"repository not found\",\n\t\t\t\tErr:     baseErr,\n\t\t\t},\n\t\t\twant: \"clone: repository not found: base error\",\n\t\t},\n\t\t{\n\t\t\tname: \"basic\",\n\t\t\twrapped: &cas.WrappedError{\n\t\t\t\tOp:  \"clone\",\n\t\t\t\tErr: baseErr,\n\t\t\t},\n\t\t\twant: \"clone: base error\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, tt.want, tt.wrapped.Error())\n\t\t\tassert.Equal(t, baseErr, tt.wrapped.Unwrap())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/cas/getter.go",
    "content": "package cas\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/hashicorp/go-getter/v2\"\n)\n\n// Assert that CASGetter implements the Getter interface\nvar _ getter.Getter = &CASGetter{}\n\n// CASGetter is a go-getter Getter implementation.\ntype CASGetter struct {\n\tCAS       *CAS\n\tLogger    log.Logger\n\tOpts      *CloneOptions\n\tDetectors []getter.Detector\n}\n\nfunc NewCASGetter(l log.Logger, cas *CAS, opts *CloneOptions) *CASGetter {\n\treturn &CASGetter{\n\t\tDetectors: []getter.Detector{\n\t\t\tnew(getter.GitHubDetector),\n\t\t\tnew(getter.GitDetector),\n\t\t\tnew(getter.BitBucketDetector),\n\t\t\tnew(getter.GitLabDetector),\n\t\t\tnew(getter.FileDetector),\n\t\t},\n\t\tCAS:    cas,\n\t\tLogger: l,\n\t\tOpts:   opts,\n\t}\n}\n\nfunc (g *CASGetter) Get(ctx context.Context, req *getter.Request) error {\n\tif req.Copy {\n\t\t// Handle local directory by persisting to CAS and linking\n\t\treturn g.CAS.StoreLocalDirectory(ctx, g.Logger, req.Src, req.Dst)\n\t}\n\n\tref := \"\"\n\n\turl := req.URL()\n\n\tq := url.Query()\n\tif len(q) > 0 {\n\t\tref = q.Get(\"ref\")\n\t\tq.Del(\"ref\")\n\n\t\turl.RawQuery = q.Encode()\n\t}\n\n\topts := g.Opts\n\topts.Branch = ref\n\topts.Dir = req.Dst\n\n\turlStr := url.String()\n\turlStr = strings.TrimPrefix(urlStr, \"git::\")\n\n\t// We have to switch back to the original URL scheme to clone the repository\n\t// go-getter sets the URL like this:\n\t// git::ssh://git@github.com/gruntwork-io/terragrunt.git\n\t// We need to switch to a valid Git URL to clone the repository\n\t// Like this:\n\t// git@github.com:gruntwork-io/terragrunt.git\n\tif after, ok := strings.CutPrefix(urlStr, \"ssh://\"); ok {\n\t\turlStr = after\n\t\t// Replace the first slash with a colon\n\t\turlStr = strings.Replace(urlStr, \"/\", \":\", 1)\n\t}\n\n\treturn g.CAS.Clone(ctx, g.Logger, opts, urlStr)\n}\n\nfunc (g *CASGetter) GetFile(_ context.Context, req *getter.Request) error {\n\treturn errors.New(\"GetFile not implemented\")\n}\n\nfunc (g *CASGetter) Mode(_ context.Context, url *url.URL) (getter.Mode, error) {\n\treturn getter.ModeDir, nil\n}\n\nfunc (g *CASGetter) Detect(req *getter.Request) (bool, error) {\n\tif req.Forced == \"git\" {\n\t\treturn true, nil\n\t}\n\n\tif after, ok := strings.CutPrefix(req.Src, \"git::\"); ok {\n\t\treq.Src = after\n\t\treq.Forced = \"git\"\n\n\t\treturn true, nil\n\t}\n\n\tfor _, detector := range g.Detectors {\n\t\tsrc, ok, err := detector.Detect(req.Src, req.Pwd)\n\t\tif err != nil {\n\t\t\treturn ok, err\n\t\t}\n\n\t\tif ok {\n\t\t\t// Check if this is a FileDetector using type assertion\n\t\t\tif _, isFileDetector := detector.(*getter.FileDetector); isFileDetector {\n\t\t\t\tinfo, statErr := os.Stat(src)\n\t\t\t\tif statErr != nil {\n\t\t\t\t\treturn false, fmt.Errorf(\"%w: %s\", ErrDirectoryNotFound, src)\n\t\t\t\t}\n\n\t\t\t\tif !info.IsDir() {\n\t\t\t\t\treturn false, fmt.Errorf(\"%w: %s\", ErrNotADirectory, src)\n\t\t\t\t}\n\n\t\t\t\t// We use this as a simple way to indicate that we're working with a local directory.\n\t\t\t\treq.Copy = true\n\t\t\t}\n\n\t\t\treq.Src = src\n\n\t\t\treturn ok, nil\n\t\t}\n\t}\n\n\treturn false, nil\n}\n\nvar (\n\tErrDirectoryNotFound = errors.New(\"directory not found\")\n\tErrNotADirectory     = errors.New(\"not a directory\")\n)\n"
  },
  {
    "path": "internal/cas/getter_ssh_test.go",
    "content": "//go:build ssh\n\n// We don't want contributors to have to install SSH keys to run these tests, so we skip\n// them by default. Contributors need to opt in to run these tests by setting the\n// build flag `ssh` when running the tests. This is done by adding the `-tags ssh` flag\n// to the `go test` command. For example:\n//\n// go test -tags ssh ./...\n\npackage cas_test\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cas\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/hashicorp/go-getter/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSSHCASGetterGet(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname      string\n\t\turl       string\n\t\tqueryRef  string\n\t\texpectRef string\n\t}{\n\t\t{\n\t\t\tname:      \"Basic URL without ref\",\n\t\t\turl:       \"github.com/gruntwork-io/terragrunt\",\n\t\t\texpectRef: \"\",\n\t\t},\n\t\t{\n\t\t\tname:      \"URL as SSH\",\n\t\t\turl:       \"git@github.com:gruntwork-io/terragrunt.git\",\n\t\t\texpectRef: \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\t\t\tstorePath := filepath.Join(tmpDir, \"store\")\n\t\t\tc, err := cas.New(cas.Options{StorePath: storePath})\n\t\t\trequire.NoError(t, err)\n\n\t\t\topts := &cas.CloneOptions{\n\t\t\t\tBranch: \"main\",\n\t\t\t}\n\t\t\tl := logger.CreateLogger()\n\t\t\tg := cas.NewCASGetter(l, c, opts)\n\t\t\tclient := getter.Client{\n\t\t\t\tGetters: []getter.Getter{g},\n\t\t\t}\n\n\t\t\tres, err := client.Get(\n\t\t\t\tt.Context(),\n\t\t\t\t&getter.Request{\n\t\t\t\t\tSrc: tt.url,\n\t\t\t\t\tDst: tmpDir,\n\t\t\t\t},\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tmpDir, res.Dst)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/cas/getter_test.go",
    "content": "package cas_test\n\nimport (\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cas\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/hashicorp/go-getter/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCASGetterMode(t *testing.T) {\n\tt.Parallel()\n\n\tg := cas.NewCASGetter(nil, nil, &cas.CloneOptions{})\n\ttestURL, err := url.Parse(\"https://github.com/gruntwork-io/terragrunt\")\n\trequire.NoError(t, err)\n\n\tmode, err := g.Mode(t.Context(), testURL)\n\trequire.NoError(t, err)\n\tassert.Equal(t, getter.ModeDir, mode)\n}\n\nfunc TestCASGetterGetFile(t *testing.T) {\n\tt.Parallel()\n\n\tg := cas.NewCASGetter(nil, nil, &cas.CloneOptions{})\n\terr := g.GetFile(t.Context(), &getter.Request{})\n\trequire.Error(t, err)\n\tassert.Equal(t, \"GetFile not implemented\", err.Error())\n}\n\nfunc TestCASGetterDetect(t *testing.T) {\n\tt.Parallel()\n\n\tg := cas.NewCASGetter(nil, nil, &cas.CloneOptions{})\n\n\ttmp := helpers.TmpDirWOSymlinks(t)\n\n\tos.MkdirAll(filepath.Join(tmp, \"fake-module\"), 0755)\n\tos.WriteFile(filepath.Join(tmp, \"fake-module\", \"main.tf\"), []byte(\"\"), 0644)\n\n\ttests := []struct {\n\t\texpectedErr error\n\t\tname        string\n\t\tsrc         string\n\t\tpwd         string\n\t}{\n\t\t{\n\t\t\tname: \"GitHub repository\",\n\t\t\tsrc:  \"github.com/gruntwork-io/terragrunt\",\n\t\t\tpwd:  tmp,\n\t\t},\n\t\t{\n\t\t\tname: \"HTTPS URL repository\",\n\t\t\tsrc:  \"git::https://github.com/gruntwork-io/terragrunt\",\n\t\t\tpwd:  tmp,\n\t\t},\n\t\t{\n\t\t\tname:        \"Invalid URL\",\n\t\t\tsrc:         \"not-a-valid-url\",\n\t\t\tpwd:         tmp,\n\t\t\texpectedErr: cas.ErrDirectoryNotFound,\n\t\t},\n\t\t{\n\t\t\tname: \"Local directory\",\n\t\t\tsrc:  \"./fake-module\",\n\t\t\tpwd:  tmp,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\treq := &getter.Request{\n\t\t\t\tSrc: tt.src,\n\t\t\t\tPwd: tt.pwd,\n\t\t\t}\n\n\t\t\tok, err := g.Detect(req)\n\t\t\tif tt.expectedErr != nil {\n\t\t\t\trequire.ErrorIs(t, err, tt.expectedErr)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.True(t, ok)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCASGetterGet(t *testing.T) {\n\tt.Parallel()\n\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\tstorePath := filepath.Join(tempDir, \"store\")\n\n\tc, err := cas.New(cas.Options{\n\t\tStorePath: storePath,\n\t})\n\trequire.NoError(t, err)\n\n\topts := &cas.CloneOptions{\n\t\tBranch: \"main\",\n\t}\n\n\tl := logger.CreateLogger()\n\n\tg := cas.NewCASGetter(l, c, opts)\n\tclient := getter.Client{\n\t\tGetters: []getter.Getter{g},\n\t}\n\n\ttests := []struct {\n\t\tname      string\n\t\turl       string\n\t\tqueryRef  string\n\t\texpectRef string\n\t}{\n\t\t{\n\t\t\tname:      \"URL with ref parameter\",\n\t\t\turl:       \"github.com/gruntwork-io/terragrunt?ref=v0.75.0\",\n\t\t\texpectRef: \"v0.75.0\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\tres, err := client.Get(\n\t\t\t\tt.Context(),\n\t\t\t\t&getter.Request{\n\t\t\t\t\tSrc: tt.url,\n\t\t\t\t\tDst: tmpDir,\n\t\t\t\t},\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tmpDir, res.Dst)\n\t\t})\n\t}\n}\n\nfunc TestCASGetterLocalDir(t *testing.T) {\n\tt.Parallel()\n\n\ttmp := helpers.TmpDirWOSymlinks(t)\n\tstorePath := filepath.Join(tmp, \"store\")\n\n\tc, err := cas.New(cas.Options{\n\t\tStorePath: storePath,\n\t})\n\trequire.NoError(t, err)\n\n\topts := &cas.CloneOptions{\n\t\tBranch: \"main\",\n\t}\n\n\tl := logger.CreateLogger()\n\n\tg := cas.NewCASGetter(l, c, opts)\n\n\tfakeModule := filepath.Join(tmp, \"fake-module\")\n\tos.MkdirAll(fakeModule, 0755)\n\n\tfakeModuleSubdir := filepath.Join(fakeModule, \"subdir\")\n\tos.MkdirAll(fakeModuleSubdir, 0755)\n\n\tos.WriteFile(filepath.Join(fakeModule, \"main.tf\"), []byte(\"\"), 0644)\n\tos.WriteFile(filepath.Join(fakeModuleSubdir, \"subfile.tf\"), []byte(\"\"), 0644)\n\n\tfakeDest := filepath.Join(tmp, \"fake-dest\")\n\n\treq := &getter.Request{\n\t\tSrc: fakeModule,\n\t\tDst: fakeDest,\n\t\tPwd: tmp,\n\t}\n\n\tok, err := g.Detect(req)\n\trequire.NoError(t, err)\n\tassert.True(t, ok)\n\n\tassert.True(t, req.Copy)\n\n\terr = g.Get(t.Context(), req)\n\trequire.NoError(t, err)\n\n\tstat, err := os.Stat(filepath.Join(fakeDest, \"main.tf\"))\n\trequire.NoError(t, err)\n\tassert.Equal(t, os.FileMode(0644), stat.Mode())\n\n\tstat, err = os.Stat(filepath.Join(fakeDest, \"subdir\", \"subfile.tf\"))\n\trequire.NoError(t, err)\n\tassert.Equal(t, os.FileMode(0644), stat.Mode())\n}\n"
  },
  {
    "path": "internal/cas/integration_test.go",
    "content": "package cas_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cas\"\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestIntegration_CloneAndReuse(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\tt.Run(\"clone same repo twice uses store\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttempDir := helpers.TmpDirWOSymlinks(t)\n\t\tstorePath := filepath.Join(tempDir, \"store\")\n\n\t\t// First clone\n\t\tfirstClonePath := filepath.Join(tempDir, \"first\")\n\t\tcas1, err := cas.New(cas.Options{\n\t\t\tStorePath: storePath,\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, cas1.Clone(t.Context(), l, &cas.CloneOptions{\n\t\t\tDir: firstClonePath,\n\t\t}, \"https://github.com/gruntwork-io/terragrunt.git\"))\n\n\t\t// Get info about first clone\n\t\tfirstReadme := filepath.Join(firstClonePath, \"README.md\")\n\t\tfirstStat, err := os.Stat(firstReadme)\n\t\trequire.NoError(t, err)\n\n\t\t// Second clone\n\t\tsecondClonePath := filepath.Join(tempDir, \"second\")\n\t\tcas2, err := cas.New(cas.Options{\n\t\t\tStorePath: storePath,\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, cas2.Clone(t.Context(), l, &cas.CloneOptions{\n\t\t\tDir: secondClonePath,\n\t\t}, \"https://github.com/gruntwork-io/terragrunt.git\"))\n\n\t\t// Get info about second clone\n\t\tsecondReadme := filepath.Join(secondClonePath, \"README.md\")\n\t\tsecondStat, err := os.Stat(secondReadme)\n\t\trequire.NoError(t, err)\n\n\t\t// Verify both files exist\n\t\tassert.FileExists(t, firstReadme)\n\t\tassert.FileExists(t, secondReadme)\n\n\t\t// Verify they're hard links using os.SameFile instead of comparing entire Stat_t\n\t\tassert.True(t, os.SameFile(firstStat, secondStat))\n\t})\n\n\tt.Run(\"clone with nonexistent branch fails gracefully\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttempDir := helpers.TmpDirWOSymlinks(t)\n\n\t\tc, err := cas.New(cas.Options{\n\t\t\tStorePath: filepath.Join(tempDir, \"store\"),\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\terr = c.Clone(t.Context(), l, &cas.CloneOptions{\n\t\t\tDir:    filepath.Join(tempDir, \"repo\"),\n\t\t\tBranch: \"nonexistent-branch\",\n\t\t}, \"https://github.com/gruntwork-io/terragrunt.git\")\n\t\trequire.Error(t, err)\n\n\t\tvar wrappedErr *git.WrappedError\n\t\trequire.ErrorAs(t, err, &wrappedErr)\n\t\tassert.ErrorIs(t, wrappedErr.Err, git.ErrNoMatchingReference)\n\t})\n\n\tt.Run(\"clone with invalid repository fails gracefully\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttempDir := helpers.TmpDirWOSymlinks(t)\n\n\t\tc, err := cas.New(cas.Options{\n\t\t\tStorePath: filepath.Join(tempDir, \"store\"),\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\terr = c.Clone(t.Context(), l, &cas.CloneOptions{\n\t\t\tDir: filepath.Join(tempDir, \"repo\"),\n\t\t}, \"https://github.com/yhakbar/nonexistent-repo.git\")\n\t\trequire.Error(t, err)\n\n\t\tvar wrappedErr *git.WrappedError\n\t\trequire.ErrorAs(t, err, &wrappedErr)\n\t\tassert.ErrorIs(t, wrappedErr.Err, git.ErrCommandSpawn)\n\t})\n}\n\nfunc TestIntegration_TreeStorage(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\n\tl := logger.CreateLogger()\n\n\tt.Run(\"stores tree objects\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttempDir := helpers.TmpDirWOSymlinks(t)\n\t\tstorePath := filepath.Join(tempDir, \"store\")\n\n\t\tconst testTag = \"v0.98.0\"\n\n\t\t// First clone to populate store\n\t\tc, err := cas.New(cas.Options{\n\t\t\tStorePath: storePath,\n\t\t})\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, c.Clone(ctx, l, &cas.CloneOptions{\n\t\t\tDir:    filepath.Join(tempDir, \"repo\"),\n\t\t\tBranch: testTag,\n\t\t}, \"https://github.com/gruntwork-io/terragrunt.git\"))\n\n\t\t// Get the commit hash for the tag\n\t\tg, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\n\t\tresults, err := g.LsRemote(ctx, \"https://github.com/gruntwork-io/terragrunt.git\", testTag)\n\t\trequire.NoError(t, err)\n\t\trequire.NotEmpty(t, results)\n\t\tcommitHash := results[0].Hash\n\n\t\t// Verify the tree object is stored\n\t\tstore := cas.NewStore(storePath)\n\n\t\trequire.NoError(t, err)\n\t\tassert.False(t, store.NeedsWrite(commitHash), \"Tree object should be stored\")\n\n\t\t// Verify we can read the tree content\n\t\tcontent := cas.NewContent(store)\n\t\ttreeData, err := content.Read(commitHash)\n\t\trequire.NoError(t, err)\n\n\t\t// Parse the tree data to confirm it's valid\n\t\ttree, err := git.ParseTree(treeData, \"\")\n\t\trequire.NoError(t, err)\n\t\tassert.NotEmpty(t, tree.Entries(), \"Tree should have entries\")\n\t})\n}\n"
  },
  {
    "path": "internal/cas/local.go",
    "content": "package cas\n\nimport (\n\t\"context\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// StoreLocalDirectory persists all content from a local source directory into the CAS\n// and then links the persisted files to the target directory\nfunc (c *CAS) StoreLocalDirectory(ctx context.Context, l log.Logger, sourceDir, targetDir string) error {\n\t// Generate a synthetic hash for the local directory based on its contents\n\thash, treeData, err := c.hashDirectory(sourceDir)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to hash local directory %s: %w\", sourceDir, err)\n\t}\n\n\t// Store all files from the directory into the CAS\n\tif err = c.storeLocalContent(l, sourceDir, hash, treeData); err != nil {\n\t\treturn fmt.Errorf(\"failed to store local content: %w\", err)\n\t}\n\n\t// Parse the tree data and link to target directory\n\ttree, err := git.ParseTree(treeData, targetDir)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to parse local tree: %w\", err)\n\t}\n\n\treturn LinkTree(ctx, c.store, tree, targetDir)\n}\n\n// hashDirectory creates a synthetic hash and tree structure for a local directory\nfunc (c *CAS) hashDirectory(sourceDir string) (string, []byte, error) {\n\tvar treeData []byte\n\n\tvar allHashes []string\n\n\terr := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Implicitly handled by tracking the file hashes.\n\t\tif info.IsDir() {\n\t\t\treturn nil\n\t\t}\n\n\t\trelPath, err := filepath.Rel(sourceDir, path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Convert to forward slashes for consistency (git-style paths)\n\t\trelPath = strings.ReplaceAll(relPath, string(filepath.Separator), \"/\")\n\n\t\tfileHash, err := hashFile(path)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to hash file %s: %w\", path, err)\n\t\t}\n\n\t\t// Artificially create a tree entry for the file.\n\t\tmode := fmt.Sprintf(\"%06o\", info.Mode().Perm())\n\t\ttreeLine := fmt.Sprintf(\"%s blob %s\\t%s\\n\", mode, fileHash, relPath)\n\t\ttreeData = append(treeData, []byte(treeLine)...)\n\n\t\t// Collect all hashes for directory hash calculation\n\t\tallHashes = append(allHashes, fileHash)\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\t// Create a synthetic hash for the entire directory based on all file hashes\n\t// This ensures the same directory contents always get the same hash\n\tdirHash := hashString(strings.Join(allHashes, \"\"))\n\n\treturn dirHash, treeData, nil\n}\n\n// storeLocalContent stores all files from a local directory into the CAS\nfunc (c *CAS) storeLocalContent(l log.Logger, sourceDir, dirHash string, treeData []byte) error {\n\t// First store the tree object itself\n\tcontent := NewContent(c.store)\n\tif err := content.Ensure(l, dirHash, treeData); err != nil {\n\t\treturn fmt.Errorf(\"failed to store tree data: %w\", err)\n\t}\n\n\t// Walk the directory and store all files\n\treturn filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Skip directories and the root directory itself\n\t\tif info.IsDir() {\n\t\t\treturn nil\n\t\t}\n\n\t\t// Hash the file to get its content hash\n\t\tfileHash, err := hashFile(path)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to hash file %s: %w\", path, err)\n\t\t}\n\n\t\tif err := content.EnsureCopy(l, fileHash, path); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to store file %s: %w\", path, err)\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\nfunc hashString(s string) string {\n\th := sha1.New()\n\th.Write([]byte(s))\n\n\treturn hex.EncodeToString(h.Sum(nil))\n}\n"
  },
  {
    "path": "internal/cas/race_test.go",
    "content": "// Tests specific to race conditions are verified here\n\npackage cas_test\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cas\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/hashicorp/go-getter/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCASGetterGetWithRacing(t *testing.T) {\n\tt.Parallel()\n\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\tstorePath := filepath.Join(tempDir, \"store\")\n\n\tc, err := cas.New(cas.Options{\n\t\tStorePath: storePath,\n\t})\n\trequire.NoError(t, err)\n\n\topts := &cas.CloneOptions{\n\t\tBranch: \"main\",\n\t}\n\n\tl := logger.CreateLogger()\n\n\tg := cas.NewCASGetter(l, c, opts)\n\tclient := getter.Client{\n\t\tGetters: []getter.Getter{g},\n\t}\n\n\ttests := []struct {\n\t\tname      string\n\t\turl       string\n\t\tqueryRef  string\n\t\texpectRef string\n\t}{\n\t\t{\n\t\t\tname:      \"URL with ref parameter\",\n\t\t\turl:       \"github.com/gruntwork-io/terragrunt?ref=v0.75.0\",\n\t\t\texpectRef: \"v0.75.0\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\tres, err := client.Get(\n\t\t\t\tt.Context(),\n\t\t\t\t&getter.Request{\n\t\t\t\t\tSrc: tt.url,\n\t\t\t\t\tDst: tmpDir,\n\t\t\t\t},\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tmpDir, res.Dst)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/cas/store.go",
    "content": "package cas\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gofrs/flock\"\n)\n\n// Store manages the store directory and filesystem locks to prevent concurrent writes\ntype Store struct {\n\tpath string\n}\n\n// NewStore creates a new Store instance.\nfunc NewStore(path string) *Store {\n\treturn &Store{\n\t\tpath: path,\n\t}\n}\n\n// Path returns the current store path\nfunc (s *Store) Path() string {\n\treturn s.path\n}\n\n// NeedsWrite checks if a given hash needs to be stored\nfunc (s *Store) NeedsWrite(hash string) bool {\n\tpartitionDir := filepath.Join(s.path, hash[:2])\n\tpath := filepath.Join(partitionDir, hash)\n\n\treturn !s.hasContent(path)\n}\n\n// HasContent checks if a given hash exists in the store\nfunc (s *Store) hasContent(path string) bool {\n\t_, err := os.Stat(path)\n\n\treturn err == nil\n}\n\n// AcquireLock acquires a filesystem lock for the given hash\n// Returns the flock instance that should be unlocked when done\nfunc (s *Store) AcquireLock(hash string) (*flock.Flock, error) {\n\tpartitionDir := filepath.Join(s.path, hash[:2])\n\tlockPath := filepath.Join(partitionDir, hash+\".lock\")\n\n\t// Ensure the partition directory exists\n\tif err := os.MkdirAll(partitionDir, DefaultDirPerms); err != nil {\n\t\treturn nil, err\n\t}\n\n\tlock := flock.New(lockPath)\n\tif err := lock.Lock(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn lock, nil\n}\n\n// TryAcquireLock attempts to acquire a filesystem lock for the given hash without blocking\n// Returns the flock instance and true if successful, nil and false if the lock is already held\nfunc (s *Store) TryAcquireLock(hash string) (*flock.Flock, bool, error) {\n\tpartitionDir := filepath.Join(s.path, hash[:2])\n\tlockPath := filepath.Join(partitionDir, hash+\".lock\")\n\n\t// Ensure the partition directory exists\n\tif err := os.MkdirAll(partitionDir, DefaultDirPerms); err != nil {\n\t\treturn nil, false, err\n\t}\n\n\tlock := flock.New(lockPath)\n\n\tacquired, err := lock.TryLock()\n\tif err != nil {\n\t\treturn nil, false, err\n\t}\n\n\tif !acquired {\n\t\treturn nil, false, nil\n\t}\n\n\treturn lock, true, nil\n}\n\n// EnsureWithWait tries to acquire a lock for the given hash, and if another process\n// is writing the same content, waits for it to complete instead of doing redundant work.\n// This is an optimization for read operations that avoids duplicate writes.\n//\n// Returns:\n// - needsWrite: true if content doesn't exist and caller should write it\n// - lock: the acquired lock (nil if needsWrite is false)\n// - error: any error that occurred\nfunc (s *Store) EnsureWithWait(hash string) (needsWrite bool, lock *flock.Flock, err error) {\n\t// Fast path: check if content already exists\n\tpartitionDir := filepath.Join(s.path, hash[:2])\n\tpath := filepath.Join(partitionDir, hash)\n\n\tif s.hasContent(path) {\n\t\treturn false, nil, nil\n\t}\n\n\t// Try to acquire lock without blocking\n\tflockLock, acquired, err := s.TryAcquireLock(hash)\n\tif err != nil {\n\t\treturn false, nil, err\n\t}\n\n\tif acquired {\n\t\t// We got the lock immediately, check if we still need to write\n\t\t// (another process might have completed while we were trying)\n\t\tif !s.NeedsWrite(hash) {\n\t\t\t// Content appeared while we were acquiring lock, no write needed\n\t\t\tif err = flockLock.Unlock(); err != nil {\n\t\t\t\treturn false, nil, err\n\t\t\t}\n\n\t\t\treturn false, nil, nil\n\t\t}\n\t\t// We have the lock and content doesn't exist, caller should write\n\t\treturn true, flockLock, nil\n\t}\n\n\t// Lock is held by another process, wait for it to complete\n\twaitLock, err := s.AcquireLock(hash)\n\tif err != nil {\n\t\treturn false, nil, err\n\t}\n\n\t// Now we have the lock, check if the other process wrote the content\n\tif !s.NeedsWrite(hash) {\n\t\t// Content was written by the other process, no write needed\n\t\tif err := waitLock.Unlock(); err != nil {\n\t\t\treturn false, nil, err\n\t\t}\n\n\t\treturn false, nil, nil\n\t}\n\n\t// Content still doesn't exist, caller should write it\n\treturn true, waitLock, nil\n}\n"
  },
  {
    "path": "internal/cas/store_test.go",
    "content": "package cas_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cas\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestStore(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"custom path\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttempDir := helpers.TmpDirWOSymlinks(t)\n\t\tcustomPath := filepath.Join(tempDir, \"custom-store\")\n\n\t\tstore := cas.NewStore(customPath)\n\t\tassert.Equal(t, customPath, store.Path())\n\t})\n}\n\nfunc TestStore_NeedsWrite(t *testing.T) {\n\tt.Parallel()\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\tstore := cas.NewStore(tempDir)\n\n\t// Create a fake content file\n\ttestHash := \"abcdef123456\"\n\t// Create partition directory\n\tpartitionDir := filepath.Join(store.Path(), testHash[:2])\n\terr := os.MkdirAll(partitionDir, 0755)\n\trequire.NoError(t, err, \"Failed to create partition directory\")\n\n\ttestPath := filepath.Join(partitionDir, testHash)\n\terr = os.WriteFile(testPath, []byte(\"test\"), 0644)\n\trequire.NoError(t, err, \"Failed to create test file\")\n\n\ttests := []struct {\n\t\tname string\n\t\thash string\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"existing content\",\n\t\t\thash: testHash,\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"non-existing content\",\n\t\t\thash: \"nonexistent\",\n\t\t\twant: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, tt.want, store.NeedsWrite(tt.hash))\n\t\t})\n\t}\n}\n\nfunc TestStore_AcquireLock(t *testing.T) {\n\tt.Parallel()\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\tstore := cas.NewStore(tempDir)\n\ttestHash := \"abcdef1234567890abcdef1234567890abcdef12\"\n\n\t// Test successful lock acquisition\n\tlock, err := store.AcquireLock(testHash)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, lock)\n\n\t// Verify lock file exists\n\tlockPath := filepath.Join(tempDir, testHash[:2], testHash+\".lock\")\n\tassert.FileExists(t, lockPath)\n\n\t// Clean up\n\terr = lock.Unlock()\n\trequire.NoError(t, err)\n}\n\nfunc TestStore_TryAcquireLock(t *testing.T) {\n\tt.Parallel()\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\tstore := cas.NewStore(tempDir)\n\ttestHash := \"abcdef1234567890abcdef1234567890abcdef12\"\n\n\t// Test successful lock acquisition\n\tlock1, acquired, err := store.TryAcquireLock(testHash)\n\trequire.NoError(t, err)\n\tassert.True(t, acquired)\n\tassert.NotNil(t, lock1)\n\n\t// Test lock contention - should fail to acquire\n\tlock2, acquired, err := store.TryAcquireLock(testHash)\n\trequire.NoError(t, err)\n\tassert.False(t, acquired)\n\tassert.Nil(t, lock2)\n\n\t// Clean up first lock\n\terr = lock1.Unlock()\n\trequire.NoError(t, err)\n\n\t// Now should be able to acquire again\n\tlock3, acquired, err := store.TryAcquireLock(testHash)\n\trequire.NoError(t, err)\n\tassert.True(t, acquired)\n\tassert.NotNil(t, lock3)\n\n\t// Clean up\n\terr = lock3.Unlock()\n\tassert.NoError(t, err)\n}\n\nfunc TestStore_LockConcurrency(t *testing.T) {\n\tt.Parallel()\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\tstore := cas.NewStore(tempDir)\n\ttestHash := \"abcdef1234567890abcdef1234567890abcdef12\"\n\n\t// Test that multiple goroutines can't acquire the same lock\n\tdone := make(chan bool, 2)\n\tacquired := make(chan bool, 2)\n\n\t// First goroutine acquires lock and holds it briefly\n\tgo func() {\n\t\tlock, err := store.AcquireLock(testHash)\n\t\tassert.NoError(t, err)\n\n\t\tacquired <- true\n\n\t\ttime.Sleep(100 * time.Millisecond) // Hold lock briefly\n\n\t\terr = lock.Unlock()\n\t\tassert.NoError(t, err)\n\n\t\tdone <- true\n\t}()\n\n\t// Second goroutine tries to acquire the same lock\n\tgo func() {\n\t\t<-acquired // Wait for first goroutine to acquire lock\n\n\t\t// Should block until first lock is released\n\t\tstart := time.Now()\n\t\tlock, err := store.AcquireLock(testHash)\n\t\telapsed := time.Since(start)\n\n\t\tassert.NoError(t, err)\n\t\tassert.Greater(t, elapsed, 50*time.Millisecond, \"Second lock should have been blocked\")\n\n\t\terr = lock.Unlock()\n\t\tassert.NoError(t, err)\n\n\t\tdone <- true\n\t}()\n\n\t// Wait for both goroutines to complete\n\t<-done\n\t<-done\n}\n\nfunc TestStore_EnsureWithWait(t *testing.T) {\n\tt.Parallel()\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\tstore := cas.NewStore(tempDir)\n\ttestHash := \"abcdef1234567890abcdef1234567890abcdef12\"\n\n\tt.Run(\"content already exists\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create the content manually\n\t\tpartitionDir := filepath.Join(tempDir, testHash[:2])\n\t\terr := os.MkdirAll(partitionDir, 0755)\n\t\trequire.NoError(t, err)\n\n\t\tcontentPath := filepath.Join(partitionDir, testHash)\n\t\terr = os.WriteFile(contentPath, []byte(\"existing content\"), 0644)\n\t\trequire.NoError(t, err)\n\n\t\t// EnsureWithWait should return false (no write needed)\n\t\tneedsWrite, lock, err := store.EnsureWithWait(testHash)\n\t\trequire.NoError(t, err)\n\t\tassert.False(t, needsWrite)\n\t\tassert.Nil(t, lock)\n\t})\n\n\tt.Run(\"content doesn't exist, no contention\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttestHashNew := \"fedcba0987654321fedcba0987654321fedcba09\"\n\n\t\t// EnsureWithWait should return true (write needed) and provide lock\n\t\tneedsWrite, lock, err := store.EnsureWithWait(testHashNew)\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, needsWrite)\n\t\tassert.NotNil(t, lock)\n\n\t\t// Clean up\n\t\terr = lock.Unlock()\n\t\tassert.NoError(t, err)\n\t})\n}\n"
  },
  {
    "path": "internal/cas/tree.go",
    "content": "package cas\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// LinkTree writes the tree to a target directory\nfunc LinkTree(ctx context.Context, store *Store, t *git.Tree, targetDir string) error {\n\tcontent := NewContent(store)\n\n\tdirsToCreate := make(map[string]struct{}, len(t.Entries()))\n\n\ttype workItem struct {\n\t\titemType string\n\t\tentry    git.TreeEntry\n\t\tpath     string\n\t\tdirPath  string\n\t}\n\n\tworkItems := make([]workItem, 0, len(t.Entries()))\n\n\tfor _, entry := range t.Entries() {\n\t\tentryPath := filepath.Join(targetDir, entry.Path)\n\t\tdirPath := filepath.Dir(entryPath)\n\n\t\tdirsToCreate[dirPath] = struct{}{}\n\n\t\t// If the parent directory is in dirsToCreate,\n\t\t// we can remove it, since it will be created\n\t\t// when creating the subtree anyways.\n\t\tparentDirPath := filepath.Dir(dirPath)\n\t\tdelete(dirsToCreate, parentDirPath)\n\n\t\t// Create work items based on entry type\n\t\tswitch entry.Type {\n\t\tcase \"blob\":\n\t\t\tworkItems = append(workItems, workItem{\n\t\t\t\titemType: \"link\",\n\t\t\t\tentry:    entry,\n\t\t\t\tpath:     entryPath,\n\t\t\t\tdirPath:  dirPath,\n\t\t\t})\n\t\tcase \"tree\":\n\t\t\tworkItems = append(workItems, workItem{\n\t\t\t\titemType: \"subtree\",\n\t\t\t\tentry:    entry,\n\t\t\t\tpath:     entryPath,\n\t\t\t\tdirPath:  dirPath,\n\t\t\t})\n\t\t}\n\t}\n\n\tfor dirPath := range dirsToCreate {\n\t\tif err := os.MkdirAll(dirPath, DefaultDirPerms); err != nil {\n\t\t\treturn wrapError(\"mkdir_all\", dirPath, err)\n\t\t}\n\t}\n\n\t// Use errgroup for concurrent processing\n\tg, ctx := errgroup.WithContext(ctx)\n\n\t// Set concurrency limit\n\tscalingFactor := 2\n\tmaxWorkers := max(1, runtime.NumCPU()/scalingFactor)\n\tg.SetLimit(maxWorkers)\n\n\t// Process work items concurrently\n\tfor _, work := range workItems {\n\t\tg.Go(func() error {\n\t\t\tswitch work.itemType {\n\t\t\tcase \"link\":\n\t\t\t\terr := content.Link(ctx, work.entry.Hash, work.path)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn wrapError(\"link_blob\", work.path, err)\n\t\t\t\t}\n\t\t\tcase \"subtree\":\n\t\t\t\ttreeData, err := content.Read(work.entry.Hash)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn wrapError(\"read_tree\", work.entry.Hash, err)\n\t\t\t\t}\n\n\t\t\t\tsubTree, err := git.ParseTree(treeData, work.path)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn wrapError(\"parse_tree\", work.entry.Hash, err)\n\t\t\t\t}\n\n\t\t\t\terr = LinkTree(ctx, store, subTree, work.path)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn wrapError(\"link_subtree\", work.path, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t// Wait for all goroutines to complete and return first error if any\n\treturn g.Wait()\n}\n"
  },
  {
    "path": "internal/cas/tree_test.go",
    "content": "package cas_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cas\"\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParseTreeEntry(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname    string\n\t\tinput   string\n\t\twant    git.TreeEntry\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:  \"regular file\",\n\t\t\tinput: \"100644 blob a1b2c3d4 README.md\",\n\t\t\twant: git.TreeEntry{\n\t\t\t\tMode: \"100644\",\n\t\t\t\tType: \"blob\",\n\t\t\t\tHash: \"a1b2c3d4\",\n\t\t\t\tPath: \"README.md\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"executable file\",\n\t\t\tinput: \"100755 blob e5f6g7h8 scripts/test.sh\",\n\t\t\twant: git.TreeEntry{\n\t\t\t\tMode: \"100755\",\n\t\t\t\tType: \"blob\",\n\t\t\t\tHash: \"e5f6g7h8\",\n\t\t\t\tPath: \"scripts/test.sh\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"directory\",\n\t\t\tinput: \"040000 tree i9j0k1l2 src\",\n\t\t\twant: git.TreeEntry{\n\t\t\t\tMode: \"040000\",\n\t\t\t\tType: \"tree\",\n\t\t\t\tHash: \"i9j0k1l2\",\n\t\t\t\tPath: \"src\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"path with spaces\",\n\t\t\tinput: \"100644 blob m3n4o5p6 path with spaces.txt\",\n\t\t\twant: git.TreeEntry{\n\t\t\t\tMode: \"100644\",\n\t\t\t\tType: \"blob\",\n\t\t\t\tHash: \"m3n4o5p6\",\n\t\t\t\tPath: \"path with spaces.txt\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"invalid format\",\n\t\t\tinput:   \"invalid format\",\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tgot, err := git.ParseTreeEntry(tt.input)\n\t\t\tif tt.wantErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestParseTree(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tpath     string\n\t\twantPath string\n\t\tinput    []byte\n\t\twantLen  int\n\t\twantErr  bool\n\t}{\n\t\t{\n\t\t\tname: \"multiple entries\",\n\t\t\tinput: []byte(`100644 blob a1b2c3d4 README.md\n100755 blob e5f6g7h8 scripts/test.sh\n040000 tree i9j0k1l2 src`),\n\t\t\tpath:     \"test-repo\",\n\t\t\twantLen:  3,\n\t\t\twantPath: \"test-repo\",\n\t\t},\n\t\t{\n\t\t\tname:     \"empty input\",\n\t\t\tinput:    []byte(\"\"),\n\t\t\tpath:     \"empty-repo\",\n\t\t\twantLen:  0,\n\t\t\twantPath: \"empty-repo\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid entry\",\n\t\t\tinput: []byte(`100644 blob a1b2c3d4 README.md\ninvalid format`),\n\t\t\tpath:    \"invalid-repo\",\n\t\t\twantErr: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tgot, err := git.ParseTree(tt.input, tt.path)\n\t\t\tif tt.wantErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Len(t, got.Entries(), tt.wantLen)\n\t\t\tassert.Equal(t, tt.wantPath, got.Path())\n\t\t})\n\t}\n}\n\nfunc TestLinkTree(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname       string\n\t\tsetupStore func(t *testing.T) (*cas.Store, string)\n\t\ttreeData   []byte\n\t\twantFiles  []struct {\n\t\t\tpath    string\n\t\t\thash    string\n\t\t\tcontent []byte\n\t\t\tisDir   bool\n\t\t}\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname: \"basic tree with files and directories\",\n\t\t\tsetupStore: func(t *testing.T) (*cas.Store, string) {\n\t\t\t\tt.Helper()\n\n\t\t\t\tstoreDir := helpers.TmpDirWOSymlinks(t)\n\t\t\t\tstore := cas.NewStore(storeDir)\n\t\t\t\tcontent := cas.NewContent(store)\n\n\t\t\t\t// Create test content\n\t\t\t\ttestData := []byte(\"test content\")\n\t\t\t\ttestHash := \"a1b2c3d4\"\n\t\t\t\terr := content.Store(nil, testHash, testData)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t// Create and store the src directory tree data\n\t\t\t\tsrcTreeData := `100644 blob a1b2c3d4 README.md`\n\t\t\t\tsrcTreeHash := \"i9j0k1l2\"\n\t\t\t\terr = content.Store(nil, srcTreeHash, []byte(srcTreeData))\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\treturn store, testHash\n\t\t\t},\n\t\t\ttreeData: []byte(`100644 blob a1b2c3d4 README.md\n100755 blob a1b2c3d4 scripts/test.sh\n040000 tree i9j0k1l2 src`),\n\t\t\twantFiles: []struct {\n\t\t\t\tpath    string\n\t\t\t\thash    string\n\t\t\t\tcontent []byte\n\t\t\t\tisDir   bool\n\t\t\t}{\n\t\t\t\t{\n\t\t\t\t\tpath:    \"README.md\",\n\t\t\t\t\tcontent: []byte(\"test content\"),\n\t\t\t\t\tisDir:   false,\n\t\t\t\t\thash:    \"a1b2c3d4\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tpath:    \"scripts/test.sh\",\n\t\t\t\t\tcontent: []byte(\"test content\"),\n\t\t\t\t\tisDir:   false,\n\t\t\t\t\thash:    \"a1b2c3d4\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tpath:  \"src\",\n\t\t\t\t\tisDir: true,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tpath:    \"src/README.md\",\n\t\t\t\t\tcontent: []byte(\"test content\"),\n\t\t\t\t\tisDir:   false,\n\t\t\t\t\thash:    \"a1b2c3d4\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty tree\",\n\t\t\tsetupStore: func(t *testing.T) (*cas.Store, string) {\n\t\t\t\tt.Helper()\n\n\t\t\t\tstoreDir := helpers.TmpDirWOSymlinks(t)\n\t\t\t\tstore := cas.NewStore(storeDir)\n\n\t\t\t\treturn store, \"\"\n\t\t\t},\n\t\t\ttreeData: []byte(\"\"),\n\t\t\twantFiles: []struct {\n\t\t\t\tpath    string\n\t\t\t\thash    string\n\t\t\t\tcontent []byte\n\t\t\t\tisDir   bool\n\t\t\t}{},\n\t\t},\n\t\t{\n\t\t\tname: \"tree with missing content\",\n\t\t\tsetupStore: func(t *testing.T) (*cas.Store, string) {\n\t\t\t\tt.Helper()\n\n\t\t\t\tstoreDir := helpers.TmpDirWOSymlinks(t)\n\t\t\t\tstore := cas.NewStore(storeDir)\n\n\t\t\t\treturn store, \"\"\n\t\t\t},\n\t\t\ttreeData: []byte(`100644 blob missing123 README.md`),\n\t\t\twantErr:  true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t// Setup store\n\t\t\tstore, _ := tt.setupStore(t)\n\n\t\t\t// Parse the tree\n\t\t\ttree, err := git.ParseTree(tt.treeData, \"test-repo\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Create target directory\n\t\t\ttargetDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\t// Link the tree\n\t\t\terr = cas.LinkTree(t.Context(), store, tree, targetDir)\n\t\t\tif tt.wantErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Verify all expected files and directories\n\t\t\tfor _, want := range tt.wantFiles {\n\t\t\t\tpath := filepath.Join(targetDir, want.path)\n\n\t\t\t\t// Check if file/directory exists\n\t\t\t\tinfo, err := os.Stat(path)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, want.isDir, info.IsDir())\n\n\t\t\t\tif !want.isDir {\n\t\t\t\t\t// Check file content\n\t\t\t\t\tdata, err := os.ReadFile(path)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tassert.Equal(t, want.content, data)\n\n\t\t\t\t\tdataStat, err := os.Stat(path)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\t// Verify hard link by comparing content.\n\t\t\t\t\t// We don't compare inode numbers because the test might be running on Windows.\n\t\t\t\t\tstorePath := filepath.Join(store.Path(), want.hash[:2], want.hash)\n\t\t\t\t\tstoreStat, err := os.Stat(storePath)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tassert.True(t, os.SameFile(dataStat, storeStat))\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/cli/app.go",
    "content": "// Package cli configures the Terragrunt CLI app and its commands.\npackage cli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/engine\"\n\t\"github.com/gruntwork-io/terragrunt/internal/os/signal\"\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/global\"\n\n\t\"github.com/gruntwork-io/go-commons/version\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tAppName = \"terragrunt\"\n)\n\nfunc init() {\n\tclihelper.AppVersionTemplate = AppVersionTemplate\n\tclihelper.AppHelpTemplate = AppHelpTemplate\n\tclihelper.CommandHelpTemplate = CommandHelpTemplate\n}\n\ntype App struct {\n\t*clihelper.App\n\topts *options.TerragruntOptions\n\tl    log.Logger\n}\n\n// NewApp creates the Terragrunt CLI App.\nfunc NewApp(l log.Logger, opts *options.TerragruntOptions) *App {\n\tterragruntCommands := commands.New(l, opts)\n\n\tapp := clihelper.NewApp()\n\tapp.Name = AppName\n\tapp.Usage = \"Terragrunt is a flexible orchestration tool that allows Infrastructure as Code written in OpenTofu/Terraform to scale.\\nFor documentation, see https://docs.terragrunt.com/.\"\n\tapp.Author = \"Gruntwork <www.gruntwork.io>\"\n\tapp.Version = version.GetVersion()\n\tapp.Writer = opts.Writers.Writer\n\tapp.ErrWriter = opts.Writers.ErrWriter\n\tapp.Flags = global.NewFlags(l, opts, nil)\n\tapp.Commands = terragruntCommands.WrapAction(commands.WrapWithTelemetry(l, opts))\n\tapp.Before = beforeAction(opts)\n\tapp.OsExiter = OSExiter\n\tapp.ExitErrHandler = ExitErrHandler\n\tapp.FlagErrHandler = flags.ErrorHandler(terragruntCommands)\n\tapp.Action = clihelper.ShowAppHelp\n\n\treturn &App{app, opts, l}\n}\n\nfunc (app *App) Run(args []string) error {\n\treturn app.RunContext(context.Background(), args)\n}\n\nfunc (app *App) registerGracefullyShutdown(ctx context.Context) context.Context {\n\tctx, cancel := context.WithCancelCause(ctx)\n\n\tsignal.NotifierWithContext(ctx, func(sig os.Signal) {\n\t\t// Carriage return helps prevent \"^C\" from being printed\n\t\tfmt.Fprint(app.Writer, \"\\r\") //nolint:errcheck\n\t\tapp.l.Infof(\"%s signal received. Gracefully shutting down...\", cases.Title(language.English).String(sig.String()))\n\n\t\tcancel(signal.NewContextCanceledError(sig))\n\t}, signal.InterruptSignals...)\n\n\treturn ctx\n}\n\nfunc (app *App) RunContext(ctx context.Context, args []string) error {\n\tctx, cancel := context.WithCancel(ctx)\n\tdefer cancel()\n\n\tctx = app.registerGracefullyShutdown(ctx)\n\n\tif err := global.NewTelemetryFlags(app.opts, nil).Parse(os.Args); err != nil {\n\t\treturn err\n\t}\n\n\ttelemeter, err := telemetry.NewTelemeter(ctx, app.Name, app.Version, app.Writer, app.opts.Telemetry)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func(ctx context.Context) {\n\t\tif err := telemeter.Shutdown(ctx); err != nil {\n\t\t\t_, _ = app.ErrWriter.Write([]byte(err.Error()))\n\t\t}\n\t}(ctx)\n\n\tctx = telemetry.ContextWithTelemeter(ctx, telemeter)\n\n\tctx = config.WithConfigValues(ctx)\n\t// configure engine context\n\tctx = engine.WithEngineValues(ctx)\n\n\tctx = run.WithRunVersionCache(ctx)\n\n\tdefer func(ctx context.Context) {\n\t\tif err := engine.Shutdown(ctx, app.l, app.opts.Experiments, app.opts.EngineOptions.NoEngine); err != nil {\n\t\t\t_, _ = app.ErrWriter.Write([]byte(err.Error()))\n\t\t}\n\t}(ctx)\n\n\targs = removeNoColorFlagDuplicates(args)\n\n\tif err := app.App.RunContext(ctx, args); err != nil && !errors.IsContextCanceled(err) {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// removeNoColorFlagDuplicates removes one of the `--no-color` or `--terragrunt-no-color` arguments if both are present.\n// We have to do this because `--terragrunt-no-color` is a deprecated alias for `--no-color`,\n// therefore we end up specifying the same flag twice, which causes the `setting the flag multiple times` error.\nfunc removeNoColorFlagDuplicates(args []string) []string {\n\tvar (\n\t\tfoundNoColor bool\n\t\tfilteredArgs = make([]string, 0, len(args))\n\t)\n\n\tfor _, arg := range args {\n\t\tif strings.HasSuffix(arg, \"-\"+global.NoColorFlagName) {\n\t\t\tif foundNoColor {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfoundNoColor = true\n\t\t}\n\n\t\tfilteredArgs = append(filteredArgs, arg)\n\t}\n\n\treturn filteredArgs\n}\n\nfunc beforeAction(_ *options.TerragruntOptions) clihelper.ActionFunc {\n\treturn func(ctx context.Context, cliCtx *clihelper.Context) error {\n\t\t// setting current context to the options\n\t\t// show help if the args are not specified.\n\t\tif !cliCtx.Args().Present() {\n\t\t\terr := clihelper.ShowAppHelp(ctx, cliCtx)\n\t\t\t// exit the app\n\t\t\treturn clihelper.NewExitError(err, 0)\n\t\t}\n\n\t\t// If args are present but the first non-flag token is not a known\n\t\t// top-level command, fail fast with guidance to use `run --`.\n\t\t// This removes the legacy behavior of implicitly forwarding unknown\n\t\t// commands to OpenTofu/Terraform.\n\t\tcmdName := cliCtx.Args().CommandName()\n\t\tif cmdName != \"\" {\n\t\t\tif cliCtx.Command == nil || cliCtx.Command.Subcommand(cmdName) == nil {\n\t\t\t\t// Show a clear error pointing users to the explicit run form.\n\t\t\t\t// Example: `terragrunt workspace ls` -> suggest `terragrunt run -- workspace ls`.\n\t\t\t\treturn clihelper.NewExitError(\n\t\t\t\t\terrors.Errorf(\"unknown command: %q. Terragrunt no longer forwards unknown commands by default. Use 'terragrunt run -- %s ...' or a supported shortcut. Learn more: https://docs.terragrunt.com/migrate/cli-redesign/#use-the-new-run-command\", cmdName, cmdName),\n\t\t\t\t\tclihelper.ExitCodeGeneralError,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n}\n\n// OSExiter is an empty function that overrides the default behavior.\nfunc OSExiter(exitCode int) {\n\t// Do nothing. We just need to override this function, as the default value calls os.Exit, which\n\t// kills the app (or any automated test) dead in its tracks.\n}\n\n// ExitErrHandler is an empty function that overrides the default behavior.\nfunc ExitErrHandler(_ *clihelper.Context, err error) error {\n\treturn err\n}\n"
  },
  {
    "path": "internal/cli/app_test.go",
    "content": "package cli_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands\"\n\tawsproviderpatch \"github.com/gruntwork-io/terragrunt/internal/cli/commands/aws-provider-patch\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/hcl\"\n\thclformat \"github.com/gruntwork-io/terragrunt/internal/cli/commands/hcl/format\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/global\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar defaultLogLevel = log.DebugLevel\n\nfunc TestParseTerragruntOptionsFromArgs(t *testing.T) {\n\tt.Parallel()\n\n\tif runtime.GOOS == \"windows\" {\n\t\tt.Skip(\"Skipping test on Windows\")\n\t}\n\n\tworkingDir, err := os.Getwd()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttestCases := []struct {\n\t\texpectedErr     error\n\t\texpectedOptions *options.TerragruntOptions\n\t\targs            []string\n\t}{\n\t\t{\n\t\t\targs: []string{\"plan\"},\n\t\t\texpectedOptions: mockOptions(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(workingDir, config.DefaultTerragruntConfigPath),\n\t\t\t\tworkingDir,\n\t\t\t\t[]string{\"plan\"},\n\t\t\t\tfalse,\n\t\t\t\t\"\",\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tdefaultLogLevel,\n\t\t\t\tfalse,\n\t\t\t),\n\t\t},\n\n\t\t{\n\t\t\targs: []string{\"plan\", \"bar\"},\n\t\t\texpectedOptions: mockOptions(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(workingDir, config.DefaultTerragruntConfigPath),\n\t\t\t\tworkingDir,\n\t\t\t\t[]string{\"plan\", \"bar\"},\n\t\t\t\tfalse,\n\t\t\t\t\"\",\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tdefaultLogLevel,\n\t\t\t\tfalse,\n\t\t\t),\n\t\t},\n\n\t\t{\n\t\t\targs: []string{\"--foo\", \"--bar\"},\n\t\t\texpectedOptions: mockOptions(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(\n\t\t\t\t\tworkingDir,\n\t\t\t\t\tconfig.DefaultTerragruntConfigPath,\n\t\t\t\t), workingDir,\n\t\t\t\t[]string{\"-foo\", \"-bar\"},\n\t\t\t\tfalse,\n\t\t\t\t\"\",\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tdefaultLogLevel,\n\t\t\t\tfalse,\n\t\t\t),\n\t\t\texpectedErr: clihelper.UndefinedFlagError(\"foo\"),\n\t\t},\n\n\t\t{\n\t\t\targs: []string{\"--foo\", \"apply\", \"--bar\"},\n\t\t\texpectedOptions: mockOptions(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(\n\t\t\t\t\tworkingDir,\n\t\t\t\t\tconfig.DefaultTerragruntConfigPath,\n\t\t\t\t),\n\t\t\t\tworkingDir,\n\t\t\t\t[]string{\"apply\", \"-foo\", \"-bar\"},\n\t\t\t\tfalse,\n\t\t\t\t\"\",\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tdefaultLogLevel,\n\t\t\t\tfalse,\n\t\t\t),\n\t\t\texpectedErr: clihelper.UndefinedFlagError(\"foo\"),\n\t\t},\n\n\t\t{\n\t\t\targs:            []string{doubleDashed(global.NonInteractiveFlagName)},\n\t\t\texpectedOptions: mockOptions(t, \"\", \"\", nil, true, \"\", false, false, defaultLogLevel, false),\n\t\t},\n\n\t\t{\n\t\t\targs: []string{\"apply\", doubleDashed(shared.QueueIncludeExternalFlagName)},\n\t\t\texpectedOptions: mockOptions(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(\n\t\t\t\t\tworkingDir,\n\t\t\t\t\tconfig.DefaultTerragruntConfigPath,\n\t\t\t\t),\n\t\t\t\tworkingDir,\n\t\t\t\t[]string{\"apply\"},\n\t\t\t\tfalse,\n\t\t\t\t\"\",\n\t\t\t\tfalse,\n\t\t\t\ttrue,\n\t\t\t\tdefaultLogLevel,\n\t\t\t\tfalse,\n\t\t\t),\n\t\t},\n\n\t\t{\n\t\t\targs: []string{\n\t\t\t\t\"plan\",\n\t\t\t\tdoubleDashed(run.ConfigFlagName),\n\t\t\t\t\"/some/path/\" + config.DefaultTerragruntConfigPath,\n\t\t\t},\n\t\t\texpectedOptions: mockOptions(\n\t\t\t\tt,\n\t\t\t\t\"/some/path/\"+config.DefaultTerragruntConfigPath,\n\t\t\t\tworkingDir,\n\t\t\t\t[]string{\"plan\"},\n\t\t\t\tfalse,\n\t\t\t\t\"\",\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tdefaultLogLevel,\n\t\t\t\tfalse,\n\t\t\t),\n\t\t},\n\n\t\t{\n\t\t\targs: []string{\"plan\", doubleDashed(global.WorkingDirFlagName), \"/some/path\"},\n\t\t\texpectedOptions: mockOptions(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(\n\t\t\t\t\t\"/some/path\",\n\t\t\t\t\tconfig.DefaultTerragruntConfigPath,\n\t\t\t\t),\n\t\t\t\t\"/some/path\",\n\t\t\t\t[]string{\"plan\"},\n\t\t\t\tfalse,\n\t\t\t\t\"\",\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tdefaultLogLevel,\n\t\t\t\tfalse,\n\t\t\t),\n\t\t},\n\n\t\t{\n\t\t\targs: []string{\"plan\", doubleDashed(run.SourceFlagName), \"/some/path\"},\n\t\t\texpectedOptions: mockOptions(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(\n\t\t\t\t\tworkingDir,\n\t\t\t\t\tconfig.DefaultTerragruntConfigPath,\n\t\t\t\t),\n\t\t\t\tworkingDir,\n\t\t\t\t[]string{\"plan\"},\n\t\t\t\tfalse,\n\t\t\t\t\"/some/path\",\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tdefaultLogLevel,\n\t\t\t\tfalse,\n\t\t\t),\n\t\t},\n\n\t\t{\n\t\t\targs: []string{\n\t\t\t\t\"plan\",\n\t\t\t\tdoubleDashed(run.SourceMapFlagName),\n\t\t\t\t\"git::git@github.com:one/gw-terraform-aws-vpc.git=git::git@github.com:two/test.git?ref=FEATURE\",\n\t\t\t},\n\t\t\texpectedOptions: mockOptionsWithSourceMap(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(\n\t\t\t\t\tworkingDir,\n\t\t\t\t\tconfig.DefaultTerragruntConfigPath,\n\t\t\t\t),\n\t\t\t\tworkingDir,\n\t\t\t\t[]string{\"plan\"},\n\t\t\t\tmap[string]string{\n\t\t\t\t\t\"git::git@github.com:one/gw-terraform-aws-vpc.git\": \"git::git@github.com:two/test.git?ref=FEATURE\",\n\t\t\t\t},\n\t\t\t),\n\t\t},\n\n\t\t{\n\t\t\targs: []string{\"plan\", doubleDashed(shared.QueueIgnoreErrorsFlagName)},\n\t\t\texpectedOptions: mockOptions(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(\n\t\t\t\t\tworkingDir,\n\t\t\t\t\tconfig.DefaultTerragruntConfigPath,\n\t\t\t\t),\n\t\t\t\tworkingDir,\n\t\t\t\t[]string{\"plan\"},\n\t\t\t\tfalse,\n\t\t\t\t\"\",\n\t\t\t\ttrue,\n\t\t\t\tfalse,\n\t\t\t\tdefaultLogLevel,\n\t\t\t\tfalse,\n\t\t\t),\n\t\t},\n\n\t\t{\n\t\t\targs: []string{\"plan\", doubleDashed(shared.QueueExcludeExternalFlagName)},\n\t\t\texpectedOptions: mockOptions(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(\n\t\t\t\t\tworkingDir,\n\t\t\t\t\tconfig.DefaultTerragruntConfigPath,\n\t\t\t\t),\n\t\t\t\tworkingDir,\n\t\t\t\t[]string{\"plan\"},\n\t\t\t\tfalse,\n\t\t\t\t\"\",\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tdefaultLogLevel,\n\t\t\t\tfalse,\n\t\t\t),\n\t\t},\n\n\t\t{\n\t\t\targs: []string{\n\t\t\t\t\"plan\",\n\t\t\t\tdoubleDashed(run.IAMAssumeRoleFlagName),\n\t\t\t\t\"arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME\",\n\t\t\t},\n\t\t\texpectedOptions: mockOptionsWithIamRole(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(\n\t\t\t\t\tworkingDir,\n\t\t\t\t\tconfig.DefaultTerragruntConfigPath,\n\t\t\t\t),\n\t\t\t\tworkingDir,\n\t\t\t\t[]string{\"plan\"}, false, \"\", false, \"arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME\"),\n\t\t},\n\n\t\t{\n\t\t\targs: []string{\"plan\", doubleDashed(run.IAMAssumeRoleDurationFlagName), \"36000\"},\n\t\t\texpectedOptions: mockOptionsWithIamAssumeRoleDuration(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(\n\t\t\t\t\tworkingDir,\n\t\t\t\t\tconfig.DefaultTerragruntConfigPath,\n\t\t\t\t),\n\t\t\t\tworkingDir,\n\t\t\t\t[]string{\"plan\"},\n\t\t\t\tfalse,\n\t\t\t\t\"\",\n\t\t\t\tfalse,\n\t\t\t\t36000,\n\t\t\t),\n\t\t},\n\n\t\t{\n\t\t\targs: []string{\n\t\t\t\t\"plan\",\n\t\t\t\tdoubleDashed(run.IAMAssumeRoleSessionNameFlagName),\n\t\t\t\t\"terragrunt-iam-role-session-name\",\n\t\t\t},\n\t\t\texpectedOptions: mockOptionsWithIamAssumeRoleSessionName(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(\n\t\t\t\t\tworkingDir,\n\t\t\t\t\tconfig.DefaultTerragruntConfigPath,\n\t\t\t\t),\n\t\t\t\tworkingDir,\n\t\t\t\t[]string{\"plan\"}, false, \"\", false, \"terragrunt-iam-role-session-name\"),\n\t\t},\n\n\t\t{\n\t\t\targs: []string{\n\t\t\t\t\"plan\",\n\t\t\t\tdoubleDashed(run.IAMAssumeRoleWebIdentityTokenFlagName),\n\t\t\t\t\"web-identity-token\",\n\t\t\t},\n\t\t\texpectedOptions: mockOptionsWithIamWebIdentityToken(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(\n\t\t\t\t\tworkingDir,\n\t\t\t\t\tconfig.DefaultTerragruntConfigPath,\n\t\t\t\t),\n\t\t\t\tworkingDir,\n\t\t\t\t[]string{\"plan\"},\n\t\t\t\tfalse,\n\t\t\t\t\"\",\n\t\t\t\tfalse,\n\t\t\t\t\"web-identity-token\",\n\t\t\t),\n\t\t},\n\n\t\t{\n\t\t\targs: []string{\n\t\t\t\t\"plan\",\n\t\t\t\tdoubleDashed(run.ConfigFlagName),\n\t\t\t\t\"/some/path/\" + config.DefaultTerragruntConfigPath,\n\t\t\t\t\"-non-interactive\",\n\t\t\t},\n\n\t\t\texpectedOptions: mockOptions(\n\t\t\t\tt,\n\t\t\t\t\"/some/path/\"+config.DefaultTerragruntConfigPath,\n\t\t\t\tworkingDir,\n\t\t\t\t[]string{\"plan\"},\n\t\t\t\ttrue,\n\t\t\t\t\"\",\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tdefaultLogLevel,\n\t\t\t\tfalse,\n\t\t\t),\n\t\t},\n\n\t\t{\n\t\t\targs: []string{\n\t\t\t\t\"plan\",\n\t\t\t\tdoubleDashed(run.ConfigFlagName),\n\t\t\t\t\"/some/path/\" + config.DefaultTerragruntConfigPath,\n\t\t\t\t\"bar\",\n\t\t\t\tdoubleDashed(global.NonInteractiveFlagName),\n\t\t\t\t\"--baz\",\n\t\t\t\tdoubleDashed(global.WorkingDirFlagName),\n\t\t\t\t\"/some/path\",\n\t\t\t\tdoubleDashed(run.SourceFlagName),\n\t\t\t\t\"github.com/foo/bar//baz?ref=1.0.3\",\n\t\t\t},\n\n\t\t\texpectedOptions: mockOptions(\n\t\t\t\tt,\n\t\t\t\t\"/some/path/\"+config.DefaultTerragruntConfigPath,\n\t\t\t\t\"/some/path\",\n\t\t\t\t[]string{\"plan\",\n\t\t\t\t\t\"bar\",\n\t\t\t\t\t\"-baz\"},\n\t\t\t\ttrue,\n\t\t\t\t\"github.com/foo/bar//baz?ref=1.0.3\",\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tdefaultLogLevel,\n\t\t\t\tfalse,\n\t\t\t),\n\t\t},\n\n\t\t// Adding the --terragrunt-log-level flag should result in DebugLevel configured\n\t\t{\n\t\t\targs: []string{\"plan\", doubleDashed(global.LogLevelFlagName), \"debug\"},\n\t\t\texpectedOptions: mockOptions(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(workingDir,\n\t\t\t\t\tconfig.DefaultTerragruntConfigPath),\n\t\t\t\tworkingDir,\n\t\t\t\t[]string{\"plan\"},\n\t\t\t\tfalse,\n\t\t\t\t\"\",\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tlog.DebugLevel,\n\t\t\t\tfalse,\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\targs:        []string{\"plan\", doubleDashed(run.ConfigFlagName)},\n\t\t\texpectedErr: argMissingValueError(run.ConfigFlagName),\n\t\t},\n\n\t\t{\n\t\t\targs:        []string{\"plan\", doubleDashed(global.WorkingDirFlagName)},\n\t\t\texpectedErr: argMissingValueError(global.WorkingDirFlagName),\n\t\t},\n\n\t\t{\n\t\t\targs:        []string{\"plan\", \"--foo\", \"bar\", doubleDashed(run.ConfigFlagName)},\n\t\t\texpectedErr: argMissingValueError(run.ConfigFlagName),\n\t\t},\n\t\t{\n\t\t\targs: []string{\"plan\", doubleDashed(run.InputsDebugFlagName)},\n\t\t\texpectedOptions: mockOptions(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(workingDir,\n\t\t\t\t\tconfig.DefaultTerragruntConfigPath),\n\t\t\t\tworkingDir,\n\t\t\t\t[]string{\"plan\"},\n\t\t\t\tfalse,\n\t\t\t\t\"\",\n\t\t\t\tfalse,\n\t\t\t\tfalse,\n\t\t\t\tdefaultLogLevel,\n\t\t\t\ttrue,\n\t\t\t),\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\topts := options.NewTerragruntOptions()\n\n\t\t\tl := log.New(\n\t\t\t\tlog.WithOutput(os.Stderr),\n\t\t\t\tlog.WithLevel(defaultLogLevel),\n\t\t\t\tlog.WithFormatter(format.NewFormatter(format.NewPrettyFormatPlaceholders())),\n\t\t\t)\n\n\t\t\tactualOptions, actualErr := runAppTest(l, tc.args, opts)\n\n\t\t\tif tc.expectedErr != nil {\n\t\t\t\tassert.EqualError(t, actualErr, tc.expectedErr.Error())\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, actualErr)\n\t\t\t\tassertOptionsEqual(t, tc.expectedOptions, actualOptions, \"For args %v\", tc.args)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// We can't do a direct comparison between TerragruntOptions objects because we can't compare Logger or RunTerragrunt\n// instances. Therefore, we have to manually check everything else.\nfunc assertOptionsEqual(t *testing.T, expected *options.TerragruntOptions, actual *options.TerragruntOptions, msgAndArgs ...any) {\n\tt.Helper()\n\n\tassert.Equal(t, expected.TerragruntConfigPath, actual.TerragruntConfigPath, msgAndArgs...)\n\tassert.Equal(t, expected.NonInteractive, actual.NonInteractive, msgAndArgs...)\n\tassert.Equal(t, expected.TerraformCliArgs, actual.TerraformCliArgs, msgAndArgs...)\n\tassert.Equal(t, expected.WorkingDir, actual.WorkingDir, msgAndArgs...)\n\tassert.Equal(t, expected.Source, actual.Source, msgAndArgs...)\n\tassert.Equal(t, expected.IgnoreDependencyErrors, actual.IgnoreDependencyErrors, msgAndArgs...)\n\tassert.Equal(t, expected.IAMRoleOptions, actual.IAMRoleOptions, msgAndArgs...)\n\tassert.Equal(t, expected.OriginalIAMRoleOptions, actual.OriginalIAMRoleOptions, msgAndArgs...)\n\tassert.Equal(t, expected.Debug, actual.Debug, msgAndArgs...)\n\tassert.Equal(t, expected.SourceMap, actual.SourceMap, msgAndArgs...)\n}\n\nfunc mockOptions(t *testing.T, terragruntConfigPath string, workingDir string, terraformCliArgs []string, nonInteractive bool, terragruntSource string, ignoreDependencyErrors bool, includeExternalDependencies bool, _ log.Level, debug bool) *options.TerragruntOptions {\n\tt.Helper()\n\n\topts, err := options.NewTerragruntOptionsForTest(terragruntConfigPath)\n\tif err != nil {\n\t\tt.Fatalf(\"error: %v\\n\", errors.New(err))\n\t}\n\n\topts.WorkingDir = workingDir\n\topts.TerraformCliArgs = iacargs.New(terraformCliArgs...)\n\topts.NonInteractive = nonInteractive\n\topts.Source = terragruntSource\n\topts.IgnoreDependencyErrors = ignoreDependencyErrors\n\topts.Debug = debug\n\n\treturn opts\n}\n\nfunc mockOptionsWithIamRole(t *testing.T, terragruntConfigPath string, workingDir string, terraformCliArgs []string, nonInteractive bool, terragruntSource string, ignoreDependencyErrors bool, iamRole string) *options.TerragruntOptions {\n\tt.Helper()\n\n\topts := mockOptions(t, terragruntConfigPath, workingDir, terraformCliArgs, nonInteractive, terragruntSource, ignoreDependencyErrors, false, defaultLogLevel, false)\n\topts.OriginalIAMRoleOptions.RoleARN = iamRole\n\topts.IAMRoleOptions.RoleARN = iamRole\n\n\treturn opts\n}\n\nfunc mockOptionsWithIamAssumeRoleDuration(t *testing.T, terragruntConfigPath string, workingDir string, terraformCliArgs []string, nonInteractive bool, terragruntSource string, ignoreDependencyErrors bool, iamAssumeRoleDuration int64) *options.TerragruntOptions {\n\tt.Helper()\n\n\topts := mockOptions(t, terragruntConfigPath, workingDir, terraformCliArgs, nonInteractive, terragruntSource, ignoreDependencyErrors, false, defaultLogLevel, false)\n\topts.OriginalIAMRoleOptions.AssumeRoleDuration = iamAssumeRoleDuration\n\topts.IAMRoleOptions.AssumeRoleDuration = iamAssumeRoleDuration\n\n\treturn opts\n}\n\nfunc mockOptionsWithIamAssumeRoleSessionName(t *testing.T, terragruntConfigPath string, workingDir string, terraformCliArgs []string, nonInteractive bool, terragruntSource string, ignoreDependencyErrors bool, iamAssumeRoleSessionName string) *options.TerragruntOptions {\n\tt.Helper()\n\n\topts := mockOptions(t, terragruntConfigPath, workingDir, terraformCliArgs, nonInteractive, terragruntSource, ignoreDependencyErrors, false, defaultLogLevel, false)\n\topts.OriginalIAMRoleOptions.AssumeRoleSessionName = iamAssumeRoleSessionName\n\topts.IAMRoleOptions.AssumeRoleSessionName = iamAssumeRoleSessionName\n\n\treturn opts\n}\n\nfunc mockOptionsWithIamWebIdentityToken(t *testing.T, terragruntConfigPath string, workingDir string, terraformCliArgs []string, nonInteractive bool, terragruntSource string, ignoreDependencyErrors bool, webIdentityToken string) *options.TerragruntOptions {\n\tt.Helper()\n\n\topts := mockOptions(t, terragruntConfigPath, workingDir, terraformCliArgs, nonInteractive, terragruntSource, ignoreDependencyErrors, false, defaultLogLevel, false)\n\topts.OriginalIAMRoleOptions.WebIdentityToken = webIdentityToken\n\topts.IAMRoleOptions.WebIdentityToken = webIdentityToken\n\n\treturn opts\n}\n\nfunc mockOptionsWithSourceMap(t *testing.T, terragruntConfigPath string, workingDir string, terraformCliArgs []string, sourceMap map[string]string) *options.TerragruntOptions {\n\tt.Helper()\n\n\topts := mockOptions(t, terragruntConfigPath, workingDir, terraformCliArgs, false, \"\", false, false, defaultLogLevel, false)\n\topts.SourceMap = sourceMap\n\n\treturn opts\n}\n\nfunc TestFilterTerragruntArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\targs     []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\targs:     []string{},\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\targs:     []string{\"plan\", \"--bar\"},\n\t\t\texpected: []string{\"plan\", \"-bar\"},\n\t\t},\n\t\t{\n\t\t\targs:     []string{\"plan\", doubleDashed(run.ConfigFlagName), \"/some/path/\" + config.DefaultTerragruntConfigPath},\n\t\t\texpected: []string{\"plan\"},\n\t\t},\n\t\t{\n\t\t\targs:     []string{\"plan\", doubleDashed(global.NonInteractiveFlagName)},\n\t\t\texpected: []string{\"plan\"},\n\t\t},\n\t\t{\n\t\t\targs:     []string{\"plan\", doubleDashed(run.InputsDebugFlagName)},\n\t\t\texpected: []string{\"plan\"},\n\t\t},\n\t\t{\n\t\t\targs: []string{\n\t\t\t\t\"plan\",\n\t\t\t\tdoubleDashed(global.NonInteractiveFlagName),\n\t\t\t\t\"-bar\",\n\t\t\t\tdoubleDashed(global.WorkingDirFlagName),\n\t\t\t\t\"/some/path\",\n\t\t\t\t\"--baz\",\n\t\t\t\tdoubleDashed(run.ConfigFlagName),\n\t\t\t\t\"/some/path/\" + config.DefaultTerragruntConfigPath,\n\t\t\t},\n\t\t\texpected: []string{\"plan\", \"-bar\", \"-baz\"},\n\t\t},\n\t\t{\n\t\t\targs:     []string{\"run\", \"--all\", \"apply\", \"plan\", \"bar\"},\n\t\t\texpected: []string{tf.CommandNameApply, \"plan\", \"bar\"},\n\t\t},\n\t\t{\n\t\t\targs:     []string{\"run\", \"--all\", \"destroy\", \"--\", \"plan\", \"-foo\", \"--bar\"},\n\t\t\texpected: []string{tf.CommandNameDestroy, \"-foo\", \"-bar\", \"plan\"},\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\topts := options.NewTerragruntOptions()\n\t\t\tl := log.New(\n\t\t\t\tlog.WithOutput(os.Stderr),\n\t\t\t\tlog.WithLevel(defaultLogLevel),\n\t\t\t\tlog.WithFormatter(format.NewFormatter(format.NewPrettyFormatPlaceholders())),\n\t\t\t)\n\t\t\tactualOptions, err := runAppTest(l, tc.args, opts)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, actualOptions.TerraformCliArgs.Slice(), \"For args %v\", tc.args)\n\t\t})\n\t}\n}\n\nfunc TestParseMultiStringArg(t *testing.T) {\n\tt.Parallel()\n\n\tflagName := doubleDashed(run.ProviderCacheRegistryNamesFlagName)\n\n\ttestCases := []struct {\n\t\texpectedErr  error\n\t\targs         []string\n\t\tdefaultValue []string\n\t\texpectedVals []string\n\t}{\n\t\t{\n\t\t\targs:         []string{\"run\", \"--all\", \"apply\", flagName, \"bar\"},\n\t\t\tdefaultValue: []string{\"registry.terraform.io\", \"registry.opentofu.org\"},\n\t\t\texpectedVals: []string{\"bar\"},\n\t\t},\n\t\t{\n\t\t\targs:         []string{\"run\", \"--all\", \"apply\", \"--\", \"--test\", \"bar\"},\n\t\t\tdefaultValue: []string{\"registry.terraform.io\", \"registry.opentofu.org\"},\n\t\t\texpectedVals: []string{\"registry.terraform.io\", \"registry.opentofu.org\"},\n\t\t},\n\t\t{\n\t\t\targs:         []string{\"run\", \"--all\", \"plan\", flagName, \"bar1\", flagName, \"bar2\", \"--\", \"--test\", \"value\"},\n\t\t\tdefaultValue: []string{\"registry.terraform.io\", \"registry.opentofu.org\"},\n\t\t\texpectedVals: []string{\"bar1\", \"bar2\"},\n\t\t},\n\t\t{\n\t\t\targs:         []string{\"run\", \"--all\", \"plan\", flagName, \"bar1\", flagName, \"--\", \"--test\", \"value\"},\n\t\t\tdefaultValue: []string{\"registry.terraform.io\", \"registry.opentofu.org\"},\n\t\t\texpectedErr:  argMissingValueError(run.ProviderCacheRegistryNamesFlagName),\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\topts := options.NewTerragruntOptions()\n\t\t\tl := log.New(\n\t\t\t\tlog.WithOutput(os.Stderr),\n\t\t\t\tlog.WithLevel(defaultLogLevel),\n\t\t\t\tlog.WithFormatter(format.NewFormatter(format.NewPrettyFormatPlaceholders())),\n\t\t\t)\n\t\t\tactualOptions, actualErr := runAppTest(l, tc.args, opts)\n\n\t\t\tif tc.expectedErr != nil {\n\t\t\t\tassert.EqualError(t, actualErr, tc.expectedErr.Error())\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, actualErr)\n\t\t\t\tassert.Equal(t, tc.expectedVals, actualOptions.ProviderCacheOptions.RegistryNames, \"For args %q\", tc.args)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseMutliStringKeyValueArg(t *testing.T) {\n\tt.Parallel()\n\n\tflagName := doubleDashed(awsproviderpatch.OverrideAttrFlagName)\n\n\ttestCases := []struct {\n\t\texpectedErr  error\n\t\tdefaultValue map[string]string\n\t\texpectedVals map[string]string\n\t\targs         []string\n\t}{\n\t\t{\n\t\t\targs: []string{awsproviderpatch.CommandName},\n\t\t},\n\t\t{\n\t\t\targs:         []string{awsproviderpatch.CommandName},\n\t\t\tdefaultValue: map[string]string{\"default\": \"value\"},\n\t\t\texpectedVals: map[string]string{\"default\": \"value\"},\n\t\t},\n\t\t{\n\t\t\targs:         []string{awsproviderpatch.CommandName, \"--other\", \"arg\"},\n\t\t\tdefaultValue: map[string]string{\"default\": \"value\"},\n\t\t\texpectedVals: map[string]string{\"default\": \"value\"},\n\t\t},\n\t\t{\n\t\t\targs:         []string{awsproviderpatch.CommandName, flagName, \"key=value\"},\n\t\t\tdefaultValue: map[string]string{\"default\": \"value\"},\n\t\t\texpectedVals: map[string]string{\"key\": \"value\"},\n\t\t},\n\t\t{\n\t\t\targs:         []string{awsproviderpatch.CommandName, flagName, \"key1=value1\", flagName, \"key2=value2\", flagName, \"key3=value3\"},\n\t\t\tdefaultValue: map[string]string{\"default\": \"value\"},\n\t\t\texpectedVals: map[string]string{\"key1\": \"value1\", \"key2\": \"value2\", \"key3\": \"value3\"},\n\t\t},\n\t\t{\n\t\t\targs:         []string{awsproviderpatch.CommandName, flagName, \"invalidvalue\"},\n\t\t\tdefaultValue: map[string]string{\"default\": \"value\"},\n\t\t\texpectedErr:  clihelper.NewInvalidKeyValueError(clihelper.MapFlagKeyValSep, \"invalidvalue\"),\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\topts := options.NewTerragruntOptions()\n\t\topts.AwsProviderPatchOverrides = tc.defaultValue\n\t\tl := log.New(\n\t\t\tlog.WithOutput(os.Stderr),\n\t\t\tlog.WithLevel(defaultLogLevel),\n\t\t\tlog.WithFormatter(format.NewFormatter(format.NewPrettyFormatPlaceholders())),\n\t\t)\n\t\tactualOptions, actualErr := runAppTest(l, tc.args, opts)\n\n\t\tif tc.expectedErr != nil {\n\t\t\tassert.ErrorContains(t, actualErr, tc.expectedErr.Error())\n\t\t} else {\n\t\t\trequire.NoError(t, actualErr)\n\t\t\tassert.Equal(t, tc.expectedVals, actualOptions.AwsProviderPatchOverrides, \"For args %v\", tc.args)\n\t\t}\n\t}\n}\n\nfunc TestTerragruntVersion(t *testing.T) {\n\tt.Parallel()\n\n\tversion := \"v1.2.3\"\n\n\ttestCases := []struct {\n\t\targs []string\n\t}{\n\t\t{[]string{\"terragrunt\", \"--version\"}},\n\t\t{[]string{\"terragrunt\", \"-version\"}},\n\t\t{[]string{\"terragrunt\", \"-v\"}},\n\t}\n\n\tfor _, tc := range testCases {\n\t\toutput := &bytes.Buffer{}\n\t\topts := options.NewTerragruntOptionsWithWriters(output, os.Stderr)\n\t\tapp := cli.NewApp(logger.CreateLogger(), opts)\n\t\tapp.Version = version\n\n\t\terr := app.Run(tc.args)\n\t\trequire.NoError(t, err, tc)\n\n\t\tassert.Contains(t, output.String(), version)\n\t}\n}\n\nfunc TestTerragruntHelp(t *testing.T) {\n\tt.Parallel()\n\n\tterragruntPrefix := flags.Prefix{flags.TerragruntPrefix}\n\n\topts := options.NewTerragruntOptions()\n\tapp := cli.NewApp(logger.CreateLogger(), opts)\n\n\ttestCases := []struct {\n\t\texpected    string\n\t\tnotExpected string\n\t\targs        []string\n\t}{\n\t\t{\n\t\t\targs:        []string{\"terragrunt\", \"--help\"},\n\t\t\texpected:    app.UsageText,\n\t\t\tnotExpected: terragruntPrefix.FlagName(awsproviderpatch.OverrideAttrFlagName),\n\t\t},\n\t\t{\n\t\t\targs:        []string{\"terragrunt\", \"-help\"},\n\t\t\texpected:    app.UsageText,\n\t\t\tnotExpected: terragruntPrefix.FlagName(awsproviderpatch.OverrideAttrFlagName),\n\t\t},\n\t\t{\n\t\t\targs:        []string{\"terragrunt\", \"-h\"},\n\t\t\texpected:    app.UsageText,\n\t\t\tnotExpected: terragruntPrefix.FlagName(awsproviderpatch.OverrideAttrFlagName),\n\t\t},\n\t\t{\n\t\t\targs:        []string{\"terragrunt\", awsproviderpatch.CommandName, \"-h\"},\n\t\t\texpected:    run.ConfigFlagName,\n\t\t\tnotExpected: hcl.CommandName + \" \" + hclformat.CommandName,\n\t\t},\n\t\t{\n\t\t\targs:     []string{\"terragrunt\", run.CommandName, \"--help\"},\n\t\t\texpected: run.CommandName,\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\toutput := &bytes.Buffer{}\n\t\t\topts := options.NewTerragruntOptionsWithWriters(output, os.Stderr)\n\t\t\tapp := cli.NewApp(logger.CreateLogger(), opts)\n\t\t\terr := app.Run(tc.args)\n\t\t\trequire.NoError(t, err, tc)\n\n\t\t\tassert.Contains(t, output.String(), tc.expected)\n\n\t\t\tif tc.notExpected != \"\" {\n\t\t\t\tassert.NotContains(t, output.String(), tc.notExpected)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTerraformHelp(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpected string\n\t\targs     []string\n\t}{\n\t\t{args: []string{\"terragrunt\", tf.CommandNamePlan, \"--help\"}, expected: \"(?s)Usage: terragrunt \\\\[global options\\\\] plan.*-detailed-exitcode\"},\n\t\t{args: []string{\"terragrunt\", tf.CommandNameApply, \"-help\"}, expected: \"(?s)Usage: terragrunt \\\\[global options\\\\] apply.*-destroy\"},\n\t\t{args: []string{\"terragrunt\", tf.CommandNameApply, \"-h\"}, expected: \"(?s)Usage: terragrunt \\\\[global options\\\\] apply.*-destroy\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\toutput := &bytes.Buffer{}\n\t\topts := options.NewTerragruntOptionsWithWriters(output, os.Stderr)\n\t\tapp := cli.NewApp(logger.CreateLogger(), opts)\n\t\terr := app.Run(tc.args)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Regexp(t, tc.expected, output.String())\n\t}\n}\n\nfunc TestTerraformHelp_wrongHelpFlag(t *testing.T) {\n\tt.Parallel()\n\n\toutput := &bytes.Buffer{}\n\n\topts := options.NewTerragruntOptionsWithWriters(output, os.Stderr)\n\tapp := cli.NewApp(logger.CreateLogger(), opts)\n\n\terr := app.Run([]string{\"terragrunt\", \"plan\", \"help\"})\n\trequire.Error(t, err)\n}\n\nfunc setCommandAction(action clihelper.ActionFunc, cmds ...*clihelper.Command) {\n\tfor _, cmd := range cmds {\n\t\tcmd.Action = action\n\t\tsetCommandAction(action, cmd.Subcommands...)\n\t}\n}\n\nfunc runAppTest(l log.Logger, args []string, opts *options.TerragruntOptions) (*options.TerragruntOptions, error) {\n\temptyAction := func(ctx context.Context, cliCtx *clihelper.Context) error { return nil }\n\n\tterragruntCommands := commands.New(l, opts)\n\tsetCommandAction(emptyAction, terragruntCommands...)\n\n\tapp := clihelper.NewApp()\n\tapp.Writer = &bytes.Buffer{}\n\tapp.ErrWriter = &bytes.Buffer{}\n\n\tapp.Flags = append(global.NewFlags(l, opts, nil), run.NewFlags(l, opts, nil)...)\n\tapp.Commands = terragruntCommands.WrapAction(commands.WrapWithTelemetry(l, opts))\n\tapp.OsExiter = cli.OSExiter\n\tapp.Action = func(ctx context.Context, cliCtx *clihelper.Context) error {\n\t\tfor _, arg := range cliCtx.Args() {\n\t\t\tswitch {\n\t\t\tcase strings.HasPrefix(arg, \"-\"):\n\t\t\t\topts.TerraformCliArgs.AppendFlag(arg)\n\t\t\tcase opts.TerraformCliArgs.Command == \"\":\n\t\t\t\topts.TerraformCliArgs.SetCommand(arg)\n\t\t\tdefault:\n\t\t\t\topts.TerraformCliArgs.AppendArgument(arg)\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n\tapp.ExitErrHandler = cli.ExitErrHandler\n\n\terr := app.Run(append([]string{\"--\"}, args...))\n\n\treturn opts, err\n}\n\nfunc doubleDashed(name string) string {\n\treturn \"--\" + name\n}\n\ntype argMissingValueError string\n\nfunc (err argMissingValueError) Error() string {\n\treturn \"flag needs an argument: -\" + string(err)\n}\n\nfunc TestAutocomplete(t *testing.T) { //nolint:paralleltest\n\ttestCases := []struct {\n\t\tcompLine          string\n\t\texpectedCompletes []string\n\t}{\n\t\t{\n\t\t\t\"\",\n\t\t\t[]string{\"hcl\", \"render\", \"run\"},\n\t\t},\n\t\t{\n\t\t\t\"--versio\",\n\t\t\t[]string{\"--version\"},\n\t\t},\n\t\t{\n\t\t\t\"render -\",\n\t\t\t[]string{\"--out\", \"--with-metadata\"},\n\t\t},\n\t\t{\n\t\t\t\"run pla\",\n\t\t\t[]string{\"plan\"},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Setenv(\"COMP_LINE\", \"terragrunt \"+tc.compLine)\n\n\t\toutput := &bytes.Buffer{}\n\t\topts := options.NewTerragruntOptionsWithWriters(output, os.Stderr)\n\t\tapp := cli.NewApp(logger.CreateLogger(), opts)\n\n\t\tapp.Commands = app.Commands.FilterByNames([]string{\"hcl\", \"render\", \"run\"})\n\n\t\terr := app.Run([]string{\"terragrunt\"})\n\t\trequire.NoError(t, err)\n\n\t\tfor _, expectedComplete := range tc.expectedCompletes {\n\t\t\tassert.Contains(t, output.String(), expectedComplete)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/aws-provider-patch/aws-provider-patch.go",
    "content": "package awsproviderpatch\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/hashicorp/hcl/v2/hclwrite\"\n\tctyjson \"github.com/zclconf/go-cty/cty/json\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/prepare\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst defaultKeyParts = 2\n\nfunc Run(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\tif opts.RunAll {\n\t\treturn runAll(ctx, l, opts)\n\t}\n\n\treturn runSingle(ctx, l, opts)\n}\n\nfunc runSingle(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\tprepared, err := prepare.PrepareConfig(ctx, l, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tr := report.NewReport()\n\n\tupdatedOpts, err := prepare.PrepareSource(ctx, l, prepared.Opts, prepared.Cfg, r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trunCfg := prepared.Cfg.ToRunConfig(l)\n\n\tif err := prepare.PrepareGenerate(l, updatedOpts, runCfg); err != nil {\n\t\treturn err\n\t}\n\n\tif err := prepare.PrepareInit(ctx, l, opts, updatedOpts, runCfg, r); err != nil {\n\t\treturn err\n\t}\n\n\treturn runAwsProviderPatch(l, updatedOpts)\n}\n\nfunc runAll(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\td := discovery.NewDiscovery(opts.WorkingDir)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tunits := components.Filter(component.UnitKind).Sort()\n\n\tvar errs []error\n\n\tfor _, unit := range units {\n\t\tunitOpts := opts.Clone()\n\t\tunitOpts.WorkingDir = unit.Path()\n\n\t\tconfigFilename := config.DefaultTerragruntConfigPath\n\t\tif len(opts.TerragruntConfigPath) > 0 {\n\t\t\tconfigFilename = filepath.Base(opts.TerragruntConfigPath)\n\t\t}\n\n\t\tunitOpts.TerragruntConfigPath = filepath.Join(unit.Path(), configFilename)\n\n\t\tif err := runSingle(ctx, l, unitOpts); err != nil {\n\t\t\tif opts.FailFast {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tl.Errorf(\"aws-provider-patch failed for %s: %v\", unit.Path(), err)\n\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn errors.Join(errs...)\n\t}\n\n\treturn nil\n}\n\nfunc runAwsProviderPatch(l log.Logger, opts *options.TerragruntOptions) error {\n\tif len(opts.AwsProviderPatchOverrides) == 0 {\n\t\treturn errors.New(MissingOverrideAttrError(OverrideAttrFlagName))\n\t}\n\n\tterraformFilesInModules, err := findAllTerraformFilesInModules(opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, terraformFile := range terraformFilesInModules {\n\t\tl.Debugf(\"Looking at file %s\", terraformFile)\n\n\t\toriginalTerraformFileContents, err := util.ReadFileAsString(terraformFile)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tupdatedTerraformFileContents, codeWasUpdated, err := PatchAwsProviderInTerraformCode(originalTerraformFileContents, terraformFile, opts.AwsProviderPatchOverrides)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif codeWasUpdated {\n\t\t\tl.Debugf(\"Patching AWS provider in %s\", terraformFile)\n\n\t\t\tif err := util.WriteFileWithSamePermissions(terraformFile, terraformFile, []byte(updatedTerraformFileContents)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// TerraformModulesJSON is the format we expect in the .terraform/modules/modules.json file\ntype TerraformModulesJSON struct {\n\tModules []TerraformModule `json:\"Modules\"`\n}\n\ntype TerraformModule struct {\n\tKey    string `json:\"Key\"`\n\tSource string `json:\"Source\"`\n\tDir    string `json:\"Dir\"`\n}\n\n// findAllTerraformFiles returns all Terraform source files within the modules being used by this Terragrunt\n// configuration. To be more specific, it only returns the source files downloaded for module \"xxx\" { ... } blocks into\n// the .terraform/modules folder; it does NOT return Terraform files for the top-level (AKA \"root\") module.\n//\n// NOTE: this method supports *.tf and *.tofu files. Terraform/OpenTofu code defined in *.json files is not currently\n// supported.\nfunc findAllTerraformFilesInModules(opts *options.TerragruntOptions) ([]string, error) {\n\t// Terraform downloads modules into the .terraform/modules folder. Unfortunately, it downloads not only the module\n\t// into that folder, but the entire repo it's in, which can contain lots of other unrelated code we probably don't\n\t// want to touch. To find the paths to the actual modules, we read the modules.json file in that folder, which is\n\t// a manifest file Terraform uses to track where the modules are within each repo. Note that this is an internal\n\t// API, so the way we parse/read this modules.json file may break in future Terraform versions. Note that we\n\t// can't use the official HashiCorp code to parse this file, as it's marked internal:\n\t// https://github.com/hashicorp/terraform/blob/master/internal/modsdir/manifest.go\n\tmodulesJSONPath := filepath.Join(opts.DataDir(), \"modules\", \"modules.json\")\n\n\tif !util.FileExists(modulesJSONPath) {\n\t\treturn nil, nil\n\t}\n\n\tmodulesJSONContents, err := os.ReadFile(modulesJSONPath)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tvar terraformModulesJSON TerraformModulesJSON\n\tif err := json.Unmarshal(modulesJSONContents, &terraformModulesJSON); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tvar terraformFiles []string\n\n\tfor _, module := range terraformModulesJSON.Modules {\n\t\tif module.Key != \"\" && module.Dir != \"\" {\n\t\t\tmoduleAbsPath := module.Dir\n\t\t\tif !filepath.IsAbs(moduleAbsPath) {\n\t\t\t\tmoduleAbsPath = filepath.Join(opts.WorkingDir, moduleAbsPath)\n\t\t\t}\n\n\t\t\tmoduleFiles, err := util.FindTFFiles(moduleAbsPath)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.New(err)\n\t\t\t}\n\n\t\t\t// Filter out JSON files (.tf.json, .tofu.json, or any .json) as hclwrite cannot parse JSON\n\t\t\tfor _, file := range moduleFiles {\n\t\t\t\tif !strings.HasSuffix(file, \".json\") {\n\t\t\t\t\tterraformFiles = append(terraformFiles, file)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn terraformFiles, nil\n}\n\n// PatchAwsProviderInTerraformCode looks for provider \"aws\" { ... } blocks in the given Terraform code and overwrites\n// the attributes in those provider blocks with the given attributes. It returns the new Terraform code and a boolean\n// true if that code was updated.\n//\n// For example, if you passed in the following Terraform code:\n//\n//\tprovider \"aws\" {\n//\t   region = var.aws_region\n//\t}\n//\n// And you set attributesToOverride to map[string]string{\"region\": \"us-east-1\"}, then this method will return:\n//\n//\tprovider \"aws\" {\n//\t   region = \"us-east-1\"\n//\t}\n//\n// This is a temporary workaround for a Terraform bug (https://github.com/hashicorp/terraform/issues/13018) where\n// any dynamic values in nested provider blocks are not handled correctly when you call 'terraform import', so by\n// temporarily hard-coding them, we can allow 'import' to work.\nfunc PatchAwsProviderInTerraformCode(terraformCode string, terraformFilePath string, attributesToOverride map[string]string) (string, bool, error) {\n\tif len(attributesToOverride) == 0 {\n\t\treturn terraformCode, false, nil\n\t}\n\n\thclFile, err := hclwrite.ParseConfig([]byte(terraformCode), terraformFilePath, hcl.InitialPos)\n\tif err != nil {\n\t\treturn \"\", false, errors.New(err)\n\t}\n\n\tcodeWasUpdated := false\n\n\tfor _, block := range hclFile.Body().Blocks() {\n\t\tif block.Type() == \"provider\" && len(block.Labels()) == 1 && block.Labels()[0] == \"aws\" {\n\t\t\tfor key, value := range attributesToOverride {\n\t\t\t\tattributeOverridden, err := overrideAttributeInBlock(block, key, value)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn string(hclFile.Bytes()), codeWasUpdated, err\n\t\t\t\t}\n\n\t\t\t\tcodeWasUpdated = codeWasUpdated || attributeOverridden\n\t\t\t}\n\t\t}\n\t}\n\n\treturn string(hclFile.Bytes()), codeWasUpdated, nil\n}\n\n// Override the attribute specified in the given key to the given value in a Terraform block: that is, if the attribute\n// is already set, then update its value to the new value; if the attribute is not already set, do nothing. This method\n// returns true if an attribute was overridden and false if nothing was changed.\n//\n// Note that you can set attributes within nested blocks by using a dot syntax similar to Terraform addresses: e.g.,\n// \"<NESTED_BLOCK>.<KEY>\".\n//\n// Examples:\n//\n// Assume that block1 is:\n//\n//\tprovider \"aws\" {\n//\t  region = var.aws_region\n//\t  assume_role {\n//\t    role_arn = var.role_arn\n//\t  }\n//\t}\n//\n// If you call:\n//\n// overrideAttributeInBlock(block1, \"region\", \"eu-west-1\")\n// overrideAttributeInBlock(block1, \"assume_role.role_arn\", \"foo\")\n//\n// The result would be:\n//\n//\tprovider \"aws\" {\n//\t  region = \"eu-west-1\"\n//\t  assume_role {\n//\t    role_arn = \"foo\"\n//\t  }\n//\t}\n//\n// Assume block2 is:\n//\n// provider \"aws\" {}\n//\n// If you call:\n//\n// overrideAttributeInBlock(block2, \"region\", \"eu-west-1\")\n// overrideAttributeInBlock(block2, \"assume_role.role_arn\", \"foo\")\n//\n// The result would be:\n//\n// provider \"aws\" {}\n//\n// Returns an error if the provided value is not valid json.\nfunc overrideAttributeInBlock(block *hclwrite.Block, key string, value string) (bool, error) {\n\tbody, attr := traverseBlock(block, strings.Split(key, \".\"))\n\tif body == nil || body.GetAttribute(attr) == nil {\n\t\t// We didn't find an existing block or attribute, so there's nothing to override\n\t\treturn false, nil\n\t}\n\n\t// The cty library requires concrete types, but since the value is user provided, we don't have a way to know the\n\t// underlying type. Additionally, the provider block themselves don't give us the typing information either unless\n\t// we maintain a mapping of all possible provider configurations (which is unmaintainable). To handle this, we\n\t// assume the user provided input is json, and convert to cty that way.\n\tvalueBytes := []byte(value)\n\n\tctyType, err := ctyjson.ImpliedType(valueBytes)\n\tif err != nil {\n\t\t// Wrap error in a custom error type that has better error messaging to the user.\n\t\treturnErr := TypeInferenceError{value: value, underlyingErr: err}\n\n\t\treturn false, errors.New(returnErr)\n\t}\n\n\tctyVal, err := ctyjson.Unmarshal(valueBytes, ctyType)\n\tif err != nil {\n\t\t// Wrap error in a custom error type that has better error messaging to the user.\n\t\treturnErr := MalformedJSONValError{value: value, underlyingErr: err}\n\n\t\treturn false, errors.New(returnErr)\n\t}\n\n\tbody.SetAttributeValue(attr, ctyVal)\n\n\treturn true, nil\n}\n\n// Given a Terraform block and slice of keys, return the body of the block that is indicated by the keys, and the\n// attribute to set within that body. If the slice is of length one, this method returns the body of the current block\n// and the one entry in the slice. However, if the slice contains multiple values, those indicate nested blocks, so\n// this method will recursively descend into those blocks and return the body of the final one and the final entry in\n// the slice to set on it. If a nested block is specified that doesn't actually exist, this method returns a nil body\n// and empty string for the attribute.\n//\n// Examples:\n//\n// Assume block is:\n//\n//\tprovider \"aws\" {\n//\t  region = var.aws_region\n//\t  assume_role {\n//\t    role_arn = var.role_arn\n//\t  }\n//\t}\n//\n// traverseBlock(block, []string{\"region\"})\n//\n//\t=> returns (<body of the current block>, \"region\")\n//\n// traverseBlock(block, []string{\"assume_role\", \"role_arn\"})\n//\n//\t=> returns (<body of the nested assume_role block>, \"role_arn\")\n//\n// traverseBlock(block, []string{\"foo\"})\n//\n//\t=> returns (nil, \"\")\n//\n// traverseBlock(block, []string{\"assume_role\", \"foo\"})\n//\n//\t=> returns (nil, \"\")\nfunc traverseBlock(block *hclwrite.Block, keyParts []string) (*hclwrite.Body, string) {\n\tif block == nil {\n\t\treturn nil, \"\"\n\t}\n\n\tif len(keyParts) < defaultKeyParts {\n\t\treturn block.Body(), strings.Join(keyParts, \"\")\n\t}\n\n\tblockName := keyParts[0]\n\n\treturn traverseBlock(block.Body().FirstMatchingBlock(blockName, nil), keyParts[1:])\n}\n"
  },
  {
    "path": "internal/cli/commands/aws-provider-patch/aws-provider-patch_test.go",
    "content": "package awsproviderpatch_test\n\nimport (\n\t\"testing\"\n\n\tawsproviderpatch \"github.com/gruntwork-io/terragrunt/internal/cli/commands/aws-provider-patch\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst terraformCodeExampleOutputOnly = `\noutput \"hello\" {\n  value = \"Hello, World\"\n}\n`\n\nconst terraformCodeExampleGcpProvider = `\nprovider \"google\" {\n  credentials = file(\"account.json\")\n  project     = \"my-project-id\"\n  region      = \"us-central1\"\n}\n\noutput \"hello\" {\n  value = \"Hello, World\"\n}\n`\n\nconst terraformCodeExampleAwsProviderEmptyOriginal = `\nprovider \"aws\" {\n}\n\noutput \"hello\" {\n  value = \"Hello, World\"\n}\n`\n\nconst terraformCodeExampleAwsProviderRegionVersionOverridenExpected = `\nprovider \"aws\" {\n  region  = \"eu-west-1\"\n  version = \"0.3.0\"\n}\n\noutput \"hello\" {\n  value = \"Hello, World\"\n}\n`\n\nconst terraformCodeExampleAwsProviderRegionVersionOverridenReverseOrderExpected = `\nprovider \"aws\" {\n  version = \"0.3.0\"\n  region  = \"eu-west-1\"\n}\n\noutput \"hello\" {\n  value = \"Hello, World\"\n}\n`\n\nconst terraformCodeExampleAwsProviderNonEmptyOriginal = `\nprovider \"aws\" {\n  region  = var.aws_region\n  version = \"0.2.0\"\n}\n\noutput \"hello\" {\n  value = \"Hello, World\"\n}\n`\n\nconst terraformCodeExampleAwsProviderRegionOverridenVersionNotOverriddenExpected = `\nprovider \"aws\" {\n  region  = \"eu-west-1\"\n  version = \"0.2.0\"\n}\n\noutput \"hello\" {\n  value = \"Hello, World\"\n}\n`\n\nconst terraformCodeExampleAwsMultipleProvidersOriginal = `\nprovider \"aws\" {\n  region  = var.aws_region\n  version = \"0.2.0\"\n}\n\nprovider \"aws\" {\n  alias   = \"another\"\n  region  = var.aws_region\n  version = \"0.2.0\"\n}\n\nresource \"aws_instance\" \"example\" {\n\n}\n\nprovider \"google\" {\n  credentials = file(\"account.json\")\n  project     = \"my-project-id\"\n  region      = \"us-central1\"\n}\n\nprovider \"aws\" {\n  alias  = \"yet another\"\n  region = var.aws_region\n}\n\noutput \"hello\" {\n  value = \"Hello, World\"\n}\n`\n\nconst terraformCodeExampleAwsMultipleProvidersRegionOverridenExpected = `\nprovider \"aws\" {\n  region  = \"eu-west-1\"\n  version = \"0.2.0\"\n}\n\nprovider \"aws\" {\n  alias   = \"another\"\n  region  = \"eu-west-1\"\n  version = \"0.2.0\"\n}\n\nresource \"aws_instance\" \"example\" {\n\n}\n\nprovider \"google\" {\n  credentials = file(\"account.json\")\n  project     = \"my-project-id\"\n  region      = \"us-central1\"\n}\n\nprovider \"aws\" {\n  alias  = \"yet another\"\n  region = \"eu-west-1\"\n}\n\noutput \"hello\" {\n  value = \"Hello, World\"\n}\n`\n\nconst terraformCodeExampleAwsMultipleProvidersRegionVersionOverridenExpected = `\nprovider \"aws\" {\n  region  = \"eu-west-1\"\n  version = \"0.3.0\"\n}\n\nprovider \"aws\" {\n  alias   = \"another\"\n  region  = \"eu-west-1\"\n  version = \"0.3.0\"\n}\n\nresource \"aws_instance\" \"example\" {\n\n}\n\nprovider \"google\" {\n  credentials = file(\"account.json\")\n  project     = \"my-project-id\"\n  region      = \"us-central1\"\n}\n\nprovider \"aws\" {\n  alias  = \"yet another\"\n  region = \"eu-west-1\"\n}\n\noutput \"hello\" {\n  value = \"Hello, World\"\n}\n`\n\nconst terraformCodeExampleAwsMultipleProvidersNonEmptyWithCommentsOriginal = `\n# Make sure comments are maintained\n# And don't interfere with parsing\nprovider \"aws\" {\n  # Make sure comments are maintained\n  # And don't interfere with parsing\n  region = var.aws_region\n\n  # Make sure comments are maintained\n  # And don't interfere with parsing\n  version = \"0.2.0\"\n}\n\n# Make sure comments are maintained\n# And don't interfere with parsing\nprovider \"aws\" {\n  # Make sure comments are maintained\n  # And don't interfere with parsing\n  region = var.aws_region\n\n  # Make sure comments are maintained\n  # And don't interfere with parsing\n  version = \"0.2.0\"\n\n  # Make sure comments are maintained\n  # And don't interfere with parsing\n  alias = \"secondary\"\n}\n`\n\nconst terraformCodeExampleAwsMultipleProvidersNonEmptyWithCommentsRegionOverriddenExpected = `\n# Make sure comments are maintained\n# And don't interfere with parsing\nprovider \"aws\" {\n  # Make sure comments are maintained\n  # And don't interfere with parsing\n  region = \"eu-west-1\"\n\n  # Make sure comments are maintained\n  # And don't interfere with parsing\n  version = \"0.2.0\"\n}\n\n# Make sure comments are maintained\n# And don't interfere with parsing\nprovider \"aws\" {\n  # Make sure comments are maintained\n  # And don't interfere with parsing\n  region = \"eu-west-1\"\n\n  # Make sure comments are maintained\n  # And don't interfere with parsing\n  version = \"0.2.0\"\n\n  # Make sure comments are maintained\n  # And don't interfere with parsing\n  alias = \"secondary\"\n}\n`\n\nconst terraformCodeExampleAwsMultipleProvidersNonEmptyWithCommentsRegionVersionOverriddenExpected = `\n# Make sure comments are maintained\n# And don't interfere with parsing\nprovider \"aws\" {\n  # Make sure comments are maintained\n  # And don't interfere with parsing\n  region = \"eu-west-1\"\n\n  # Make sure comments are maintained\n  # And don't interfere with parsing\n  version = \"0.3.0\"\n}\n\n# Make sure comments are maintained\n# And don't interfere with parsing\nprovider \"aws\" {\n  # Make sure comments are maintained\n  # And don't interfere with parsing\n  region = \"eu-west-1\"\n\n  # Make sure comments are maintained\n  # And don't interfere with parsing\n  version = \"0.3.0\"\n\n  # Make sure comments are maintained\n  # And don't interfere with parsing\n  alias = \"secondary\"\n}\n`\n\nconst terraformCodeExampleAwsOneProviderNestedBlocks = `\nprovider \"aws\" {\n  region = var.aws_region\n  assume_role {\n    role_arn = var.role_arn\n  }\n}\n`\n\nconst terraformCodeExampleAwsOneProviderNestedBlocksRegionRoleArnExpected = `\nprovider \"aws\" {\n  region = \"eu-west-1\"\n  assume_role {\n    role_arn = \"nested-override\"\n  }\n}\n`\n\nfunc TestPatchAwsProviderInTerraformCodeHappyPath(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tattributesToOverride   map[string]string\n\t\ttestName               string\n\t\toriginalTerraformCode  string\n\t\texpectedTerraformCode  []string\n\t\texpectedCodeWasUpdated bool\n\t}{\n\t\t{testName: \"empty\", originalTerraformCode: \"\", attributesToOverride: nil, expectedCodeWasUpdated: false, expectedTerraformCode: []string{\"\"}},\n\t\t{testName: \"empty with attributes\", originalTerraformCode: \"\", attributesToOverride: map[string]string{\"region\": `\"eu-west-1\"`}, expectedCodeWasUpdated: false, expectedTerraformCode: []string{\"\"}},\n\t\t{testName: \"no provider\", originalTerraformCode: terraformCodeExampleOutputOnly, attributesToOverride: map[string]string{\"region\": `\"eu-west-1\"`}, expectedCodeWasUpdated: false, expectedTerraformCode: []string{terraformCodeExampleOutputOnly}},\n\t\t{testName: \"no aws provider\", originalTerraformCode: terraformCodeExampleGcpProvider, attributesToOverride: map[string]string{\"region\": `\"eu-west-1\"`}, expectedCodeWasUpdated: false, expectedTerraformCode: []string{terraformCodeExampleGcpProvider}},\n\t\t{testName: \"one empty aws provider, but no overrides\", originalTerraformCode: terraformCodeExampleAwsProviderEmptyOriginal, attributesToOverride: nil, expectedCodeWasUpdated: false, expectedTerraformCode: []string{terraformCodeExampleAwsProviderEmptyOriginal}},\n\t\t{testName: \"one empty aws provider, with region override\", originalTerraformCode: terraformCodeExampleAwsProviderEmptyOriginal, attributesToOverride: map[string]string{\"region\": `\"eu-west-1\"`}, expectedCodeWasUpdated: false, expectedTerraformCode: []string{terraformCodeExampleAwsProviderEmptyOriginal}},\n\t\t{testName: \"one empty aws provider, with region, version override\", originalTerraformCode: terraformCodeExampleAwsProviderEmptyOriginal, attributesToOverride: map[string]string{\"region\": `\"eu-west-1\"`, \"version\": `\"0.3.0\"`}, expectedCodeWasUpdated: false, expectedTerraformCode: []string{terraformCodeExampleAwsProviderEmptyOriginal}},\n\t\t{testName: \"one non-empty aws provider, but no overrides\", originalTerraformCode: terraformCodeExampleAwsProviderNonEmptyOriginal, attributesToOverride: nil, expectedCodeWasUpdated: false, expectedTerraformCode: []string{terraformCodeExampleAwsProviderNonEmptyOriginal}},\n\t\t{testName: \"one non-empty aws provider, with region override\", originalTerraformCode: terraformCodeExampleAwsProviderNonEmptyOriginal, attributesToOverride: map[string]string{\"region\": `\"eu-west-1\"`}, expectedCodeWasUpdated: true, expectedTerraformCode: []string{terraformCodeExampleAwsProviderRegionOverridenVersionNotOverriddenExpected}},\n\t\t{testName: \"one non-empty aws provider, with region, version override\", originalTerraformCode: terraformCodeExampleAwsProviderNonEmptyOriginal, attributesToOverride: map[string]string{\"region\": `\"eu-west-1\"`, \"version\": `\"0.3.0\"`}, expectedCodeWasUpdated: true, expectedTerraformCode: []string{terraformCodeExampleAwsProviderRegionVersionOverridenExpected, terraformCodeExampleAwsProviderRegionVersionOverridenReverseOrderExpected}},\n\t\t{testName: \"multiple providers, but no overrides\", originalTerraformCode: terraformCodeExampleAwsMultipleProvidersOriginal, attributesToOverride: nil, expectedCodeWasUpdated: false, expectedTerraformCode: []string{terraformCodeExampleAwsMultipleProvidersOriginal}},\n\t\t{testName: \"multiple providers, with region override\", originalTerraformCode: terraformCodeExampleAwsMultipleProvidersOriginal, attributesToOverride: map[string]string{\"region\": `\"eu-west-1\"`}, expectedCodeWasUpdated: true, expectedTerraformCode: []string{terraformCodeExampleAwsMultipleProvidersRegionOverridenExpected}},\n\t\t{testName: \"multiple providers, with region, version override\", originalTerraformCode: terraformCodeExampleAwsMultipleProvidersOriginal, attributesToOverride: map[string]string{\"region\": `\"eu-west-1\"`, \"version\": `\"0.3.0\"`}, expectedCodeWasUpdated: true, expectedTerraformCode: []string{terraformCodeExampleAwsMultipleProvidersRegionVersionOverridenExpected}},\n\t\t{testName: \"multiple providers with comments, but no overrides\", originalTerraformCode: terraformCodeExampleAwsMultipleProvidersNonEmptyWithCommentsOriginal, attributesToOverride: nil, expectedCodeWasUpdated: false, expectedTerraformCode: []string{terraformCodeExampleAwsMultipleProvidersNonEmptyWithCommentsOriginal}},\n\t\t{testName: \"multiple providers with comments, with region override\", originalTerraformCode: terraformCodeExampleAwsMultipleProvidersNonEmptyWithCommentsOriginal, attributesToOverride: map[string]string{\"region\": `\"eu-west-1\"`}, expectedCodeWasUpdated: true, expectedTerraformCode: []string{terraformCodeExampleAwsMultipleProvidersNonEmptyWithCommentsRegionOverriddenExpected}},\n\t\t{testName: \"multiple providers with comments, with region, version override\", originalTerraformCode: terraformCodeExampleAwsMultipleProvidersNonEmptyWithCommentsOriginal, attributesToOverride: map[string]string{\"region\": `\"eu-west-1\"`, \"version\": `\"0.3.0\"`}, expectedCodeWasUpdated: true, expectedTerraformCode: []string{terraformCodeExampleAwsMultipleProvidersNonEmptyWithCommentsRegionVersionOverriddenExpected}},\n\t\t{testName: \"one provider with nested blocks, with region and role_arn override\", originalTerraformCode: terraformCodeExampleAwsOneProviderNestedBlocks, attributesToOverride: map[string]string{\"region\": `\"eu-west-1\"`, \"assume_role.role_arn\": `\"nested-override\"`}, expectedCodeWasUpdated: true, expectedTerraformCode: []string{terraformCodeExampleAwsOneProviderNestedBlocksRegionRoleArnExpected}},\n\t\t{testName: \"one provider with nested blocks, with region and role_arn override, plus non-matching overrides\", originalTerraformCode: terraformCodeExampleAwsOneProviderNestedBlocks, attributesToOverride: map[string]string{\"region\": `\"eu-west-1\"`, \"assume_role.role_arn\": `\"nested-override\"`, \"should-be\": `\"ignored\"`, \"assume_role.should-be\": `\"ignored\"`}, expectedCodeWasUpdated: true, expectedTerraformCode: []string{terraformCodeExampleAwsOneProviderNestedBlocksRegionRoleArnExpected}},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.testName, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactualTerraformCode, actualCodeWasUpdated, err := awsproviderpatch.PatchAwsProviderInTerraformCode(tc.originalTerraformCode, \"test.tf\", tc.attributesToOverride)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expectedCodeWasUpdated, actualCodeWasUpdated)\n\n\t\t\t// We check an array  of possible expected code here due to possible ordering differences. That is, the\n\t\t\t// attributes within a provider block are stored in a map, and iteration order on maps is randomized, so\n\t\t\t// sometimes the provider block might come back with region first, followed by version, but other times,\n\t\t\t// the order is reversed. For those cases, we pass in multiple possible expected results and check that\n\t\t\t// one of them matches.\n\t\t\tassert.Contains(t, tc.expectedTerraformCode, actualTerraformCode)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/aws-provider-patch/cli.go",
    "content": "// Package awsproviderpatch provides the `aws-provider-patch` command.\n//\n// The `aws-provider-patch` command finds all Terraform modules nested in the current code (i.e., in the .terraform/modules\n// folder), looks for provider \"aws\" { ... } blocks in those modules, and overwrites the attributes in those provider\n// blocks with the attributes specified in terragrntOptions.\n//\n// For example, if were running Terragrunt against code that contained a module:\n//\n//\tmodule \"example\" {\n//\t  source = \"<URL>\"\n//\t}\n//\n// When you run 'init', Terraform would download the code for that module into .terraform/modules. This function would\n// scan that module code for provider blocks:\n//\n//\tprovider \"aws\" {\n//\t   region = var.aws_region\n//\t}\n//\n// And if AwsProviderPatchOverrides in opts was set to map[string]string{\"region\": \"us-east-1\"}, then this\n// method would update the module code to:\n//\n//\tprovider \"aws\" {\n//\t   region = \"us-east-1\"\n//\t}\n//\n// This is a temporary workaround for a Terraform bug (https://github.com/hashicorp/terraform/issues/13018) where\n// any dynamic values in nested provider blocks are not handled correctly when you call 'terraform import', so by\n// temporarily hard-coding them, we can allow 'import' to work.\npackage awsproviderpatch\n\nimport (\n\t\"context\"\n\n\truncmd \"github.com/gruntwork-io/terragrunt/internal/cli/commands/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tCommandName = \"aws-provider-patch\"\n\n\tOverrideAttrFlagName = \"override-attr\"\n)\n\nfunc NewFlags(l log.Logger, opts *options.TerragruntOptions, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\tterragruntPrefix := flags.Prefix{flags.TerragruntPrefix}\n\tterragruntPrefixControl := flags.StrictControlsByCommand(opts.StrictControls, CommandName)\n\n\treturn clihelper.Flags{\n\t\tflags.NewFlag(&clihelper.MapFlag[string, string]{\n\t\t\tName:        OverrideAttrFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(OverrideAttrFlagName),\n\t\t\tDestination: &opts.AwsProviderPatchOverrides,\n\t\t\tUsage:       \"A key=value attribute to override in a provider block as part of the aws-provider-patch command. May be specified multiple times.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"override-attr\"), terragruntPrefixControl)),\n\t}\n}\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\tcontrol := controls.NewDeprecatedCommand(CommandName)\n\topts.StrictControls.FilterByNames(controls.DeprecatedCommands, controls.CLIRedesign, CommandName).AddSubcontrolsToCategory(controls.CLIRedesignCommandsCategoryName, control)\n\n\tcmdFlags := append(runcmd.NewFlags(l, opts, nil), NewFlags(l, opts, nil)...)\n\tcmdFlags = append(cmdFlags, shared.NewAllFlag(opts, nil))\n\n\tcmd := &clihelper.Command{\n\t\tName:   CommandName,\n\t\tUsage:  \"Overwrite settings on nested AWS providers to work around a Terraform bug (issue #13018).\",\n\t\tHidden: true,\n\t\tFlags:  cmdFlags,\n\t\tBefore: func(ctx context.Context, _ *clihelper.Context) error {\n\t\t\tif err := control.Evaluate(ctx); err != nil {\n\t\t\t\treturn clihelper.NewExitError(err, clihelper.ExitCodeGeneralError)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t\tAction: func(ctx context.Context, _ *clihelper.Context) error {\n\t\t\treturn Run(ctx, l, opts.OptionsFromContext(ctx))\n\t\t},\n\t\tDisabledErrorOnUndefinedFlag: true,\n\t}\n\n\treturn cmd\n}\n"
  },
  {
    "path": "internal/cli/commands/aws-provider-patch/errors.go",
    "content": "package awsproviderpatch\n\nimport \"fmt\"\n\ntype MissingOverrideAttrError string\n\nfunc (flagName MissingOverrideAttrError) Error() string {\n\treturn fmt.Sprintf(\"You must specify at least one provider attribute to override via the --%s option.\", string(flagName))\n}\n\ntype TypeInferenceError struct {\n\tunderlyingErr error\n\tvalue         string\n}\n\nfunc (err TypeInferenceError) Error() string {\n\tval := err.value\n\treturn fmt.Sprintf(`Could not determine underlying type of JSON string %s. This usually happens when the JSON string is malformed, or if the value is not properly quoted (e.g., \"%s\"). Underlying error: %s`, val, val, err.underlyingErr)\n}\n\ntype MalformedJSONValError struct {\n\tunderlyingErr error\n\tvalue         string\n}\n\nfunc (err MalformedJSONValError) Error() string {\n\tval := err.value\n\treturn fmt.Sprintf(`Error unmarshaling JSON string %s. This usually happens when the JSON string is malformed, or if the value is not properly quoted (e.g., \"%s\"). Underlying error: %s`, val, val, err.underlyingErr)\n}\n"
  },
  {
    "path": "internal/cli/commands/aws-provider-patch/tofu_extensions_test.go",
    "content": "//go:build tofu\n\npackage awsproviderpatch_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\tawsproviderpatch \"github.com/gruntwork-io/terragrunt/internal/cli/commands/aws-provider-patch\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst tofuCodeExampleAwsProviderOriginal = `\nprovider \"aws\" {\n  region = var.aws_region\n}\n\nresource \"aws_instance\" \"example\" {\n  ami           = \"ami-0c55b159cbfafe1d0\"\n  instance_type = \"t2.micro\"\n}\n`\n\nconst tofuCodeExampleAwsProviderRegionOverridden = `\nprovider \"aws\" {\n  region = \"eu-west-1\"\n}\n\nresource \"aws_instance\" \"example\" {\n  ami           = \"ami-0c55b159cbfafe1d0\"\n  instance_type = \"t2.micro\"\n}\n`\n\nconst tofuCodeExampleMultipleProvidersOriginal = `\nprovider \"aws\" {\n  region = var.aws_region\n}\n\nprovider \"aws\" {\n  alias  = \"east\"\n  region = \"us-east-1\"\n}\n\nprovider \"google\" {\n  project = \"my-project\"\n  region  = \"us-central1\"\n}\n\nresource \"aws_instance\" \"example\" {\n  ami           = \"ami-0c55b159cbfafe1d0\"\n  instance_type = \"t2.micro\"\n}\n`\n\nconst tofuCodeExampleMultipleProvidersRegionOverridden = `\nprovider \"aws\" {\n  region = \"eu-west-1\"\n}\n\nprovider \"aws\" {\n  alias  = \"east\"\n  region = \"eu-west-1\"\n}\n\nprovider \"google\" {\n  project = \"my-project\"\n  region  = \"us-central1\"\n}\n\nresource \"aws_instance\" \"example\" {\n  ami           = \"ami-0c55b159cbfafe1d0\"\n  instance_type = \"t2.micro\"\n}\n`\n\nconst tofuCodeExampleNestedBlocksOriginal = `\nprovider \"aws\" {\n  region = var.aws_region\n\n  assume_role {\n    role_arn = \"arn:aws:iam::123456789012:role/example\"\n  }\n\n  default_tags {\n    tags = {\n      Environment = \"test\"\n    }\n  }\n}\n`\n\nconst tofuCodeExampleNestedBlocksRegionRoleArnOverridden = `\nprovider \"aws\" {\n  region = \"eu-west-1\"\n\n  assume_role {\n    role_arn = \"arn:aws:iam::123456789012:role/overridden\"\n  }\n\n  default_tags {\n    tags = {\n      Environment = \"test\"\n    }\n  }\n}\n`\n\nfunc TestPatchAwsProviderInTofuCode(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\ttestName               string\n\t\toriginalTofuCode       string\n\t\tattributesToOverride   map[string]string\n\t\texpectedTofuCode       []string\n\t\texpectedCodeWasUpdated bool\n\t}{\n\t\t{\n\t\t\ttestName:             \"empty tofu file\",\n\t\t\tattributesToOverride: map[string]string{\"region\": `\"eu-west-1\"`},\n\t\t\texpectedTofuCode:     []string{\"\"},\n\t\t},\n\t\t{\n\t\t\ttestName:             \"tofu file with no aws provider\",\n\t\t\toriginalTofuCode:     `resource \"null_resource\" \"example\" {}`,\n\t\t\tattributesToOverride: map[string]string{\"region\": `\"eu-west-1\"`},\n\t\t\texpectedTofuCode:     []string{`resource \"null_resource\" \"example\" {}`},\n\t\t},\n\t\t{\n\t\t\ttestName:               \"tofu file with aws provider - region override\",\n\t\t\toriginalTofuCode:       tofuCodeExampleAwsProviderOriginal,\n\t\t\tattributesToOverride:   map[string]string{\"region\": `\"eu-west-1\"`},\n\t\t\texpectedCodeWasUpdated: true,\n\t\t\texpectedTofuCode:       []string{tofuCodeExampleAwsProviderRegionOverridden},\n\t\t},\n\t\t{\n\t\t\ttestName:               \"tofu file with multiple aws providers - region override\",\n\t\t\toriginalTofuCode:       tofuCodeExampleMultipleProvidersOriginal,\n\t\t\tattributesToOverride:   map[string]string{\"region\": `\"eu-west-1\"`},\n\t\t\texpectedCodeWasUpdated: true,\n\t\t\texpectedTofuCode:       []string{tofuCodeExampleMultipleProvidersRegionOverridden},\n\t\t},\n\t\t{\n\t\t\ttestName:               \"tofu file with nested blocks - region and role_arn override\",\n\t\t\toriginalTofuCode:       tofuCodeExampleNestedBlocksOriginal,\n\t\t\tattributesToOverride:   map[string]string{\"region\": `\"eu-west-1\"`, \"assume_role.role_arn\": `\"arn:aws:iam::123456789012:role/overridden\"`},\n\t\t\texpectedCodeWasUpdated: true,\n\t\t\texpectedTofuCode:       []string{tofuCodeExampleNestedBlocksRegionRoleArnOverridden},\n\t\t},\n\t\t{\n\t\t\ttestName:         \"tofu file with aws provider - no overrides\",\n\t\t\toriginalTofuCode: tofuCodeExampleAwsProviderOriginal,\n\t\t\texpectedTofuCode: []string{tofuCodeExampleAwsProviderOriginal},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.testName, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactualTofuCode, actualCodeWasUpdated, err := awsproviderpatch.PatchAwsProviderInTerraformCode(\n\t\t\t\ttc.originalTofuCode,\n\t\t\t\t\"test.tofu\",\n\t\t\t\ttc.attributesToOverride,\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expectedCodeWasUpdated, actualCodeWasUpdated)\n\t\t\tassert.Contains(t, tc.expectedTofuCode, actualTofuCode)\n\t\t})\n\t}\n}\n\nfunc TestFindAllTerraformFilesIncludesTofuFiles(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tterraformModulesDir := filepath.Join(tmpDir, \".terraform\", \"modules\")\n\trequire.NoError(t, os.MkdirAll(terraformModulesDir, 0755))\n\n\tmodulesJSON := `{\n\t\t\"Modules\": [\n\t\t\t{\n\t\t\t\t\"Key\": \"\",\n\t\t\t\t\"Source\": \"\",\n\t\t\t\t\"Dir\": \".\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"Key\": \"vpc\",\n\t\t\t\t\"Source\": \"./modules/vpc\",\n\t\t\t\t\"Dir\": \"modules/vpc\"\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"Key\": \"security\",\n\t\t\t\t\"Source\": \"./modules/security\",\n\t\t\t\t\"Dir\": \"modules/security\"\n\t\t\t}\n\t\t]\n\t}`\n\trequire.NoError(t, os.WriteFile(filepath.Join(terraformModulesDir, \"modules.json\"), []byte(modulesJSON), 0644))\n\n\tmodules := map[string][]string{\n\t\t\"modules/vpc\":      {\"main.tf\", \"variables.tofu\", \"outputs.tf.json\"},\n\t\t\"modules/security\": {\"main.tofu\", \"variables.tf\", \"data.tofu.json\"},\n\t}\n\n\tfor moduleDir, files := range modules {\n\t\tmodulePath := filepath.Join(tmpDir, moduleDir)\n\t\trequire.NoError(t, os.MkdirAll(modulePath, 0755))\n\n\t\tfor _, file := range files {\n\t\t\tfilePath := filepath.Join(modulePath, file)\n\t\t\tcontent := \"# Test content for \" + file\n\t\t\trequire.NoError(t, os.WriteFile(filePath, []byte(content), 0644))\n\t\t}\n\t}\n\n\topts, err := options.NewTerragruntOptionsForTest(\"test.hcl\")\n\trequire.NoError(t, err)\n\n\topts.WorkingDir = tmpDir\n\n\tallFiles, err := util.FindTFFiles(tmpDir)\n\trequire.NoError(t, err)\n\n\tvar files []string\n\n\tfor _, file := range allFiles {\n\t\tif !strings.HasSuffix(file, \".json\") {\n\t\t\tfiles = append(files, file)\n\t\t}\n\t}\n\n\texpectedFiles := []string{\n\t\tfilepath.Join(tmpDir, \"modules/vpc/main.tf\"),\n\t\tfilepath.Join(tmpDir, \"modules/vpc/variables.tofu\"),\n\t\tfilepath.Join(tmpDir, \"modules/security/main.tofu\"),\n\t\tfilepath.Join(tmpDir, \"modules/security/variables.tf\"),\n\t}\n\n\tassert.Len(t, files, len(expectedFiles))\n\n\tfor _, expectedFile := range expectedFiles {\n\t\tassert.Contains(t, files, expectedFile, \"Expected file %s not found in results\", expectedFile)\n\t}\n\n\tfor _, file := range files {\n\t\tassert.NotEqual(t, \".json\", filepath.Ext(file), \"JSON file %s should be excluded\", file)\n\t}\n}\n\nfunc TestAwsProviderPatchWithMixedFileTypes(t *testing.T) {\n\tt.Parallel()\n\n\ttfContent := `terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 5.0\"\n    }\n  }\n}\n\nprovider \"aws\" {\n  region = \"us-west-2\"\n}`\n\n\tmodifiedTfContent, wasUpdated, err := awsproviderpatch.PatchAwsProviderInTerraformCode(\n\t\ttfContent,\n\t\t\"main.tf\",\n\t\tmap[string]string{\"region\": `\"eu-west-1\"`},\n\t)\n\trequire.NoError(t, err)\n\tassert.True(t, wasUpdated)\n\tassert.Contains(t, modifiedTfContent, `region = \"eu-west-1\"`)\n\n\ttofuContent := `provider \"aws\" {\n  alias  = \"secondary\"\n  region = var.secondary_region\n}\n\nresource \"aws_s3_bucket\" \"primary\" {\n  bucket = \"${var.environment}-primary-bucket\"\n}`\n\n\tmodifiedTofuContent, wasUpdated, err := awsproviderpatch.PatchAwsProviderInTerraformCode(\n\t\ttofuContent,\n\t\t\"resources.tofu\",\n\t\tmap[string]string{\"region\": `\"eu-west-1\"`},\n\t)\n\trequire.NoError(t, err)\n\tassert.True(t, wasUpdated)\n\tassert.Contains(t, modifiedTofuContent, `region = \"eu-west-1\"`)\n}\n\nfunc TestTofuFileExtensionRecognition(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tfilename         string\n\t\tdescription      string\n\t\tshouldBeIncluded bool\n\t}{\n\t\t{filename: \"main.tf\", shouldBeIncluded: true, description: \"Standard Terraform file\"},\n\t\t{filename: \"main.tofu\", shouldBeIncluded: true, description: \"OpenTofu file\"},\n\t\t{filename: \"variables.tf.json\", shouldBeIncluded: true, description: \"Terraform JSON file (recognized but filtered out during processing)\"},\n\t\t{filename: \"variables.tofu.json\", shouldBeIncluded: true, description: \"OpenTofu JSON file (recognized but filtered out during processing)\"},\n\t\t{filename: \"outputs.tf\", shouldBeIncluded: true, description: \"Terraform outputs file\"},\n\t\t{filename: \"outputs.tofu\", shouldBeIncluded: true, description: \"OpenTofu outputs file\"},\n\t\t{filename: \"providers.tf\", shouldBeIncluded: true, description: \"Terraform providers file\"},\n\t\t{filename: \"providers.tofu\", shouldBeIncluded: true, description: \"OpenTofu providers file\"},\n\t\t{filename: \"terraform.tfvars\", shouldBeIncluded: false, description: \"Terraform variables file (not a configuration file)\"},\n\t\t{filename: \"terragrunt.hcl\", shouldBeIncluded: false, description: \"Terragrunt configuration file\"},\n\t\t{filename: \"README.md\", shouldBeIncluded: false, description: \"Documentation file\"},\n\t\t{filename: \"script.sh\", shouldBeIncluded: false, description: \"Shell script\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactualResult := util.IsTFFile(tc.filename)\n\n\t\t\tif tc.shouldBeIncluded {\n\t\t\t\tassert.True(t, actualResult, \"File %s should be recognized as a TF file\", tc.filename)\n\t\t\t} else {\n\t\t\t\tassert.False(t, actualResult, \"File %s should not be recognized as a TF file\", tc.filename)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/backend/bootstrap/bootstrap.go",
    "content": "// Package bootstrap provides the ability to initialize remote state backend.\npackage bootstrap\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nfunc Run(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\tif opts.RunAll {\n\t\treturn runAll(ctx, l, opts)\n\t}\n\n\treturn runBootstrap(ctx, l, opts)\n}\n\nfunc runBootstrap(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\t_, pctx := configbridge.NewParsingContext(ctx, l, opts)\n\n\tremoteState, err := config.ParseRemoteState(ctx, l, pctx)\n\tif err != nil || remoteState == nil {\n\t\treturn err\n\t}\n\n\treturn remoteState.Bootstrap(ctx, l, configbridge.RemoteStateOptsFromOpts(opts))\n}\n\nfunc runAll(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\td := discovery.NewDiscovery(opts.WorkingDir)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tunits := components.Filter(component.UnitKind).Sort()\n\n\tvar errs []error\n\n\tfor _, unit := range units {\n\t\tunitOpts := opts.Clone()\n\t\tunitOpts.WorkingDir = unit.Path()\n\n\t\tconfigFilename := config.DefaultTerragruntConfigPath\n\t\tif len(opts.TerragruntConfigPath) > 0 {\n\t\t\tconfigFilename = filepath.Base(opts.TerragruntConfigPath)\n\t\t}\n\n\t\tunitOpts.TerragruntConfigPath = filepath.Join(unit.Path(), configFilename)\n\t\tunitOpts.OriginalTerragruntConfigPath = unitOpts.TerragruntConfigPath\n\n\t\tif err := runBootstrap(ctx, l, unitOpts); err != nil {\n\t\t\tif opts.FailFast {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\terrs = append(\n\t\t\t\terrs,\n\t\t\t\tfmt.Errorf(\n\t\t\t\t\t\"backend bootstrap for unit %s failed: %w\",\n\t\t\t\t\tunit.Path(),\n\t\t\t\t\terr,\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn errors.Join(errs...)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/cli/commands/backend/bootstrap/cli.go",
    "content": "package bootstrap\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst CommandName = \"bootstrap\"\n\nfunc NewFlags(opts *options.TerragruntOptions) clihelper.Flags {\n\tprefix := flags.Prefix{flags.TgPrefix}\n\n\tsharedFlags := clihelper.Flags{\n\t\tshared.NewConfigFlag(opts, prefix, CommandName),\n\t\tshared.NewDownloadDirFlag(opts, prefix, CommandName),\n\t}\n\tsharedFlags = append(sharedFlags, shared.NewBackendFlags(opts, prefix)...)\n\tsharedFlags = append(sharedFlags, shared.NewFeatureFlags(opts, prefix)...)\n\n\treturn sharedFlags\n}\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\tcmdFlags := NewFlags(opts)\n\tcmdFlags = append(cmdFlags, shared.NewAllFlag(opts, nil), shared.NewFailFastFlag(opts))\n\n\tcmd := &clihelper.Command{\n\t\tName:  CommandName,\n\t\tUsage: \"Bootstrap OpenTofu/Terraform backend infrastructure.\",\n\t\tFlags: cmdFlags,\n\t\tAction: func(ctx context.Context, _ *clihelper.Context) error {\n\t\t\treturn Run(ctx, l, opts.OptionsFromContext(ctx))\n\t\t},\n\t}\n\n\treturn cmd\n}\n"
  },
  {
    "path": "internal/cli/commands/backend/cli.go",
    "content": "// Package backend provides commands for interacting with remote backends.\npackage backend\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/backend/bootstrap\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/backend/delete\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/backend/migrate\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst CommandName = \"backend\"\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\treturn &clihelper.Command{\n\t\tName:  CommandName,\n\t\tUsage: \"Interact with OpenTofu/Terraform backend infrastructure.\",\n\t\tSubcommands: clihelper.Commands{\n\t\t\tbootstrap.NewCommand(l, opts),\n\t\t\tdelete.NewCommand(l, opts),\n\t\t\tmigrate.NewCommand(l, opts),\n\t\t},\n\t\tAction: clihelper.ShowCommandHelp,\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/backend/delete/cli.go",
    "content": "package delete\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tCommandName = \"delete\"\n\n\tBucketFlagName             = \"bucket\"\n\tForceBackendDeleteFlagName = \"force\"\n)\n\nfunc NewFlags(l log.Logger, opts *options.TerragruntOptions, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\n\tsharedFlags := clihelper.Flags{\n\t\tshared.NewConfigFlag(opts, prefix, CommandName),\n\t\tshared.NewDownloadDirFlag(opts, prefix, CommandName),\n\t}\n\tsharedFlags = append(sharedFlags, shared.NewBackendFlags(opts, prefix)...)\n\tsharedFlags = append(sharedFlags, shared.NewFeatureFlags(opts, prefix)...)\n\n\treturn append(sharedFlags,\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        BucketFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(BucketFlagName),\n\t\t\tUsage:       \"Delete the entire bucket.\",\n\t\t\tHidden:      true,\n\t\t\tDestination: &opts.DeleteBucket,\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        ForceBackendDeleteFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(ForceBackendDeleteFlagName),\n\t\t\tUsage:       \"Force the backend to be deleted, even if the bucket is not versioned.\",\n\t\t\tDestination: &opts.ForceBackendDelete,\n\t\t}),\n\t)\n}\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\tcmdFlags := NewFlags(l, opts, nil)\n\tcmdFlags = append(cmdFlags, shared.NewAllFlag(opts, nil), shared.NewFailFastFlag(opts))\n\n\tcmd := &clihelper.Command{\n\t\tName:  CommandName,\n\t\tUsage: \"Delete OpenTofu/Terraform state.\",\n\t\tFlags: cmdFlags,\n\t\tAction: func(ctx context.Context, _ *clihelper.Context) error {\n\t\t\treturn Run(ctx, l, opts.OptionsFromContext(ctx))\n\t\t},\n\t}\n\n\treturn cmd\n}\n"
  },
  {
    "path": "internal/cli/commands/backend/delete/delete.go",
    "content": "// Package delete provides the ability to remove remote state files/buckets.\npackage delete\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nfunc Run(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\tif opts.RunAll {\n\t\treturn runAll(ctx, l, opts)\n\t}\n\n\treturn runDelete(ctx, l, opts)\n}\n\nfunc runDelete(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\t_, pctx := configbridge.NewParsingContext(ctx, l, opts)\n\n\tremoteState, err := config.ParseRemoteState(ctx, l, pctx)\n\tif err != nil || remoteState == nil {\n\t\treturn err\n\t}\n\n\tif !opts.ForceBackendDelete {\n\t\tenabled, err := remoteState.IsVersionControlEnabled(ctx, l, configbridge.RemoteStateOptsFromOpts(opts))\n\t\tif err != nil && !errors.As(err, new(backend.BucketDoesNotExistError)) {\n\t\t\treturn err\n\t\t}\n\n\t\tif !enabled {\n\t\t\treturn errors.Errorf(\"bucket is not versioned, refusing to delete backend state. If you are sure you want to delete the backend state anyways, use the --%s flag\", ForceBackendDeleteFlagName)\n\t\t}\n\t}\n\n\tif opts.DeleteBucket {\n\t\t// TODO: Do an extra check before commenting out the code. //return remoteState.DeleteBucket(ctx, opts)\n\t\treturn errors.Errorf(\"flag -%s is not supported yet\", BucketFlagName)\n\t}\n\n\treturn remoteState.Delete(ctx, l, configbridge.RemoteStateOptsFromOpts(opts))\n}\n\nfunc runAll(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\td := discovery.NewDiscovery(opts.WorkingDir)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tunits := components.Filter(component.UnitKind).Sort()\n\n\tvar errs []error\n\n\tfor _, unit := range units {\n\t\tunitOpts := opts.Clone()\n\t\tunitOpts.WorkingDir = unit.Path()\n\n\t\tconfigFilename := config.DefaultTerragruntConfigPath\n\t\tif len(opts.TerragruntConfigPath) > 0 {\n\t\t\tconfigFilename = filepath.Base(opts.TerragruntConfigPath)\n\t\t}\n\n\t\tunitOpts.TerragruntConfigPath = filepath.Join(unit.Path(), configFilename)\n\n\t\tif err := runDelete(ctx, l, unitOpts); err != nil {\n\t\t\tif opts.FailFast {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\terrs = append(\n\t\t\t\terrs,\n\t\t\t\tfmt.Errorf(\n\t\t\t\t\t\"backend delete for unit %s failed: %w\",\n\t\t\t\t\tunit.Path(),\n\t\t\t\t\terr,\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn errors.Join(errs...)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/cli/commands/backend/migrate/cli.go",
    "content": "package migrate\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tCommandName = \"migrate\"\n\n\tForceBackendMigrateFlagName = \"force\"\n\n\tusageText = \"terragrunt backend migrate [options] <src-unit> <dst-unit>\"\n)\n\nfunc NewFlags(l log.Logger, opts *options.TerragruntOptions, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\n\tsharedFlags := clihelper.Flags{\n\t\tshared.NewConfigFlag(opts, prefix, CommandName),\n\t\tshared.NewDownloadDirFlag(opts, prefix, CommandName),\n\t}\n\tsharedFlags = append(sharedFlags, shared.NewBackendFlags(opts, prefix)...)\n\tsharedFlags = append(sharedFlags, shared.NewFeatureFlags(opts, prefix)...)\n\n\treturn append(sharedFlags,\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        ForceBackendMigrateFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(ForceBackendMigrateFlagName),\n\t\t\tUsage:       \"Force the backend to be migrated, even if the bucket is not versioned.\",\n\t\t\tDestination: &opts.ForceBackendMigrate,\n\t\t}),\n\t)\n}\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\tcmd := &clihelper.Command{\n\t\tName:      CommandName,\n\t\tUsage:     \"Migrate OpenTofu/Terraform state from one location to another.\",\n\t\tUsageText: usageText,\n\t\tFlags:     NewFlags(l, opts, nil),\n\t\tAction: func(ctx context.Context, cliCtx *clihelper.Context) error {\n\t\t\tsrcPath := cliCtx.Args().First()\n\t\t\tif srcPath == \"\" {\n\t\t\t\treturn errors.New(usageText)\n\t\t\t}\n\n\t\t\tdstPath := cliCtx.Args().Second()\n\t\t\tif dstPath == \"\" {\n\t\t\t\treturn errors.New(usageText)\n\t\t\t}\n\n\t\t\treturn Run(ctx, l, srcPath, dstPath, opts.OptionsFromContext(ctx))\n\t\t},\n\t}\n\n\treturn cmd\n}\n"
  },
  {
    "path": "internal/cli/commands/backend/migrate/migrate.go",
    "content": "// Package migrate provides the ability to bootstrap remote state backend.\npackage migrate\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nfunc Run(ctx context.Context, l log.Logger, srcPath, dstPath string, opts *options.TerragruntOptions) error {\n\tvar err error\n\n\tsrcPath, err = util.CanonicalPath(srcPath, opts.WorkingDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tl.Debugf(\"Source unit path %s\", srcPath)\n\n\tdstPath, err = util.CanonicalPath(dstPath, opts.WorkingDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tl.Debugf(\"Destination unit path %s\", dstPath)\n\n\trnr, err := runner.NewStackRunner(ctx, l, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsrcUnit := rnr.GetStack().FindUnitByPath(srcPath)\n\tif srcUnit == nil {\n\t\treturn errors.Errorf(\"src unit not found at %s\", srcPath)\n\t}\n\n\tdstUnit := rnr.GetStack().FindUnitByPath(dstPath)\n\tif dstUnit == nil {\n\t\treturn errors.Errorf(\"dst unit not found at %s\", dstPath)\n\t}\n\n\tsrcOpts, _, err := runner.BuildUnitOpts(l, opts, srcUnit)\n\tif err != nil {\n\t\treturn errors.Errorf(\"failed to build opts for src unit %s: %w\", srcPath, err)\n\t}\n\n\tdstOpts, _, err := runner.BuildUnitOpts(l, opts, dstUnit)\n\tif err != nil {\n\t\treturn errors.Errorf(\"failed to build opts for dst unit %s: %w\", dstPath, err)\n\t}\n\n\t_, srcPctx := configbridge.NewParsingContext(ctx, l, srcOpts)\n\n\tsrcRemoteState, err := config.ParseRemoteState(ctx, l, srcPctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif srcRemoteState == nil {\n\t\treturn errors.Errorf(\"missing remote state configuration for source module: %s\", srcPath)\n\t}\n\n\t// ParseRemoteState updates pctx.WorkingDir to point to the .terragrunt-cache\n\t// directory (where backend.tf and .terraform/ live) when a terraform source is\n\t// configured. Propagate that back so pullState runs in the correct directory.\n\tsrcOpts.WorkingDir = srcPctx.WorkingDir\n\n\t_, dstPctx := configbridge.NewParsingContext(ctx, l, dstOpts)\n\n\tdstRemoteState, err := config.ParseRemoteState(ctx, l, dstPctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif dstRemoteState == nil {\n\t\treturn errors.Errorf(\"missing remote state configuration for destination module: %s\", dstPath)\n\t}\n\n\t// Same for the destination: pushState needs the cache directory.\n\tdstOpts.WorkingDir = dstPctx.WorkingDir\n\n\tif !opts.ForceBackendMigrate {\n\t\tenabled, err := srcRemoteState.IsVersionControlEnabled(ctx, l, configbridge.RemoteStateOptsFromOpts(srcOpts))\n\t\tif err != nil && !errors.As(err, new(backend.BucketDoesNotExistError)) {\n\t\t\treturn err\n\t\t}\n\n\t\tif !enabled {\n\t\t\treturn errors.Errorf(\"src bucket is not versioned, refusing to migrate backend state. If you are sure you want to migrate the backend state anyways, use the --%s flag\", ForceBackendMigrateFlagName)\n\t\t}\n\t}\n\n\treturn srcRemoteState.Migrate(ctx, l, configbridge.RemoteStateOptsFromOpts(srcOpts), configbridge.RemoteStateOptsFromOpts(dstOpts), dstRemoteState)\n}\n"
  },
  {
    "path": "internal/cli/commands/catalog/TESTING.md",
    "content": "# Catalog CLI Command End-to-End Testing\n\nThis document describes the comprehensive end-to-end testing implementation for the Terragrunt Catalog CLI command using `teatest` from Charm's experimental testing library.\n\n## Overview\n\nTesting the TUI for the `catalog` command is a little tricky, as we can't conveniently have someone actually go in and test the TUI every time we make any change that could impact it. To make sure that we don't break the TUI, we take a layered approach to assuring the command works as expected.\n\n1. The core logic used for the `catalog` command is actually handled in [services/catalog](../../../internal/services/catalog).\n\n   This package can be tested in isolation with standard unit tests, and we minimize any logic done outside of it to reduce the surface area for testing of the TUI.\n\n2. The TUI itself is tested using `teatest` from Charm's experimental testing library.\n\n   This library provides a way to generate golden files that can be used to test the TUI to ensure that we don't encounter catastrophic regressions that would prevent loading of the TUI.\n\n3. The `catalog` command initialization is tested in [catalog_test.go](catalog_test.go) to make sure we can setup the CLI command correctly to start up the TUI.\n\n### Golden File Testing with teatest\n\n- Uses `teatest.RequireEqualOutput(t, out)` for consistent output testing\n- Includes `.gitattributes` file to handle golden files properly\n- Golden files capture the exact TUI output for regression testing\n\n### Waiting Patterns\n\nTests use `teatest.WaitFor()` with appropriate timeouts and check intervals:\n\n```go\nteatest.WaitFor(t, tm.Output(), func(bts []byte) bool {\n    return bytes.Contains(bts, []byte(\"List of Modules\"))\n}, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*2))\n```\n\n## Running the Tests\n\n### Update Golden Files\n\n```bash\ngo test -v ./cli/commands/catalog/tui/ -run TestTUIInitialOutput -update\n```\n"
  },
  {
    "path": "internal/cli/commands/catalog/catalog.go",
    "content": "package catalog\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/catalog/tui\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// Run is the main entry point for the catalog command.\n// It initializes the catalog service, retrieves modules, and then launches the TUI.\nfunc Run(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, repoURL string) error {\n\tsvc := catalog.NewCatalogService(opts)\n\n\tif repoURL != \"\" {\n\t\tsvc.WithRepoURL(repoURL)\n\t}\n\n\terr := svc.Load(ctx, l)\n\tif err != nil {\n\t\tl.Error(err)\n\t}\n\n\tif len(svc.Modules()) == 0 {\n\t\treturn errors.New(\"no modules found by the catalog service\")\n\t}\n\n\treturn tui.Run(ctx, l, opts, svc)\n}\n"
  },
  {
    "path": "internal/cli/commands/catalog/catalog_test.go",
    "content": "package catalog_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog\"\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog/module\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCatalogCommandInitialization(t *testing.T) {\n\tt.Parallel()\n\n\topts, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\t// Create mock repository function for testing\n\tmockNewRepo := func(ctx context.Context, logger log.Logger, repoURL, path string, walkWithSymlinks, allowCAS bool, rootWorkingDir string) (*module.Repo, error) {\n\t\t// Create a temporary directory structure for testing\n\t\tdummyRepoDir := filepath.Join(helpers.TmpDirWOSymlinks(t), strings.ReplaceAll(repoURL, \"github.com/gruntwork-io/\", \"\"))\n\t\tos.MkdirAll(filepath.Join(dummyRepoDir, \".git\"), 0755)\n\t\tos.WriteFile(filepath.Join(dummyRepoDir, \".git\", \"config\"), []byte(\"[remote \\\"origin\\\"]\\nurl = \"+repoURL), 0644)\n\t\tos.WriteFile(filepath.Join(dummyRepoDir, \".git\", \"HEAD\"), []byte(\"ref: refs/heads/main\"), 0644)\n\n\t\t// Create test modules based on repoURL\n\t\tswitch repoURL {\n\t\tcase \"github.com/gruntwork-io/test-repo-1\":\n\t\t\treadme1Path := filepath.Join(dummyRepoDir, \"README.md\")\n\t\t\tos.WriteFile(readme1Path, []byte(\"# AWS VPC Module\\nThis module creates a VPC in AWS with all the necessary components.\"), 0644)\n\t\t\tos.WriteFile(filepath.Join(dummyRepoDir, \"main.tf\"), []byte(\"# VPC terraform configuration\"), 0644)\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"unexpected repoURL in mock: %s\", repoURL)\n\t\t}\n\n\t\treturn module.NewRepo(ctx, logger, dummyRepoDir, path, walkWithSymlinks, allowCAS, \"\")\n\t}\n\n\t// Create a temporary root config file\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\trootFile := filepath.Join(tmpDir, \"root.hcl\")\n\terr = os.WriteFile(rootFile, []byte(`catalog {\n\turls = [\n\t\t\"github.com/gruntwork-io/test-repo-1\",\n\t]\n}`), 0600)\n\trequire.NoError(t, err)\n\n\tunitDir := filepath.Join(tmpDir, \"unit\")\n\tos.MkdirAll(unitDir, 0755)\n\topts.TerragruntConfigPath = filepath.Join(unitDir, \"terragrunt.hcl\")\n\topts.ScaffoldRootFileName = config.RecommendedParentConfigName\n\n\t// Test that the catalog service loads correctly\n\tsvc := catalog.NewCatalogService(opts).WithNewRepoFunc(mockNewRepo)\n\n\tctx := t.Context()\n\tl := logger.CreateLogger()\n\terr = svc.Load(ctx, l)\n\trequire.NoError(t, err)\n\n\tmodules := svc.Modules()\n\tassert.Len(t, modules, 1, \"should have 1 test module\")\n\tassert.Equal(t, \"AWS VPC Module\", modules[0].Title())\n\n\t// Test that the Run function would not return an error for no modules found\n\t// (since we have modules loaded)\n\tassert.NotEmpty(t, modules, \"catalog should have modules for TUI to display\")\n}\n"
  },
  {
    "path": "internal/cli/commands/catalog/cli.go",
    "content": "// Package catalog provides the ability to interact with a catalog of OpenTofu/Terraform modules\n// via the `terragrunt catalog` command.\npackage catalog\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/scaffold\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tCommandName = \"catalog\"\n)\n\nfunc NewFlags(opts *options.TerragruntOptions, prefix flags.Prefix) clihelper.Flags {\n\treturn shared.NewScaffoldingFlags(opts, prefix)\n}\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\treturn &clihelper.Command{\n\t\tName:  CommandName,\n\t\tUsage: \"Launch the user interface for searching and managing your module catalog.\",\n\t\tFlags: NewFlags(opts, nil),\n\t\tAction: func(ctx context.Context, cliCtx *clihelper.Context) error {\n\t\t\tvar repoPath string\n\n\t\t\tif val := cliCtx.Args().Get(0); val != \"\" {\n\t\t\t\trepoPath = val\n\t\t\t}\n\n\t\t\tif opts.ScaffoldRootFileName == \"\" {\n\t\t\t\topts.ScaffoldRootFileName = scaffold.GetDefaultRootFileName(ctx, opts)\n\t\t\t}\n\n\t\t\treturn Run(ctx, l, opts.OptionsFromContext(ctx), repoPath)\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/catalog/tui/command/scaffold.go",
    "content": "// Package command provides the implementation of the terragrunt scaffold command\n// This command is used to scaffold a new Terragrunt unit in the current directory.\npackage command\n\nimport (\n\t\"context\"\n\t\"io\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog\"\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog/module\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\ntype Scaffold struct {\n\tmodule            *module.Module\n\tterragruntOptions *options.TerragruntOptions\n\tsvc               catalog.CatalogService\n\tlogger            log.Logger\n}\n\nfunc NewScaffold(logger log.Logger, opts *options.TerragruntOptions, svc catalog.CatalogService, module *module.Module) *Scaffold {\n\treturn &Scaffold{\n\t\tmodule:            module,\n\t\tterragruntOptions: opts,\n\t\tsvc:               svc,\n\t\tlogger:            logger,\n\t}\n}\n\nfunc (cmd *Scaffold) Run() error {\n\treturn cmd.svc.Scaffold(context.Background(), cmd.logger, cmd.terragruntOptions, cmd.module)\n}\n\nfunc (cmd *Scaffold) SetStdin(io.Reader) {\n}\n\nfunc (cmd *Scaffold) SetStdout(io.Writer) {\n}\n\nfunc (cmd *Scaffold) SetStderr(io.Writer) {\n}\n"
  },
  {
    "path": "internal/cli/commands/catalog/tui/components/buttonbar/buttonbar.go",
    "content": "// Package buttonbar provides a bubbletea component that displays an inline list of buttons.\npackage buttonbar\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\n// SelectBtnMsg is a message that contains the index of the button to select.\ntype SelectBtnMsg int\n\n// ActiveBtnMsg is a message that contains the index of the current active button.\ntype ActiveBtnMsg int\n\nconst (\n\tdefaultButtonNameFmt = \"[ %s ]\"\n)\n\nvar (\n\tdefaultButtonSeparatorStyle = lipgloss.NewStyle().Padding(0, 0, 0, 1)\n\tdefaultButtonFocusedStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color(\"205\"))\n\tdefaultButtonBlurredStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\"))\n)\n\n// ButtonBar is bubbletea component that displays an inline list of buttons.\ntype ButtonBar struct {\n\tSeparatorStyle lipgloss.Style\n\tFocusedStyle   lipgloss.Style\n\tBlurredStyle   lipgloss.Style\n\tnameFmt        string\n\tbuttons        []string\n\tactiveButton   int\n}\n\n// New creates a new ButtonBar component.\nfunc New(buttons []string) *ButtonBar {\n\treturn &ButtonBar{\n\t\tbuttons:        buttons,\n\t\tactiveButton:   0,\n\t\tnameFmt:        defaultButtonNameFmt,\n\t\tSeparatorStyle: defaultButtonSeparatorStyle,\n\t\tFocusedStyle:   defaultButtonFocusedStyle,\n\t\tBlurredStyle:   defaultButtonBlurredStyle,\n\t}\n}\n\n// Init implements tea.Model.\nfunc (b *ButtonBar) Init() tea.Cmd {\n\tb.activeButton = 0\n\treturn nil\n}\n\n// Update implements tea.Model.\nfunc (b *ButtonBar) Update(msg tea.Msg) (tea.Model, tea.Cmd) {\n\tcmds := make([]tea.Cmd, 0)\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tswitch msg.String() {\n\t\tcase \"tab\":\n\t\t\tb.activeButton = (b.activeButton + 1) % len(b.buttons)\n\t\t\tcmds = append(cmds, b.activeBtnCmd)\n\t\tcase \"shift+tab\":\n\t\t\tb.activeButton = (b.activeButton - 1 + len(b.buttons)) % len(b.buttons)\n\t\t\tcmds = append(cmds, b.activeBtnCmd)\n\t\t}\n\tcase SelectBtnMsg:\n\t\tbtn := int(msg)\n\t\tif btn >= 0 && btn < len(b.buttons) {\n\t\t\tb.activeButton = int(msg)\n\t\t}\n\t}\n\n\treturn b, tea.Batch(cmds...)\n}\n\n// View implements tea.Model.\nfunc (b *ButtonBar) View() string {\n\ts := strings.Builder{}\n\n\tfor i, btn := range b.buttons {\n\t\tstyle := b.BlurredStyle\n\t\tif i == b.activeButton {\n\t\t\tstyle = b.FocusedStyle\n\t\t}\n\n\t\ts.WriteString(fmt.Sprintf(b.nameFmt, style.Render(btn)))\n\n\t\tif i != len(b.buttons)-1 {\n\t\t\ts.WriteString(b.SeparatorStyle.String())\n\t\t}\n\t}\n\n\treturn s.String()\n}\n\nfunc (b *ButtonBar) activeBtnCmd() tea.Msg {\n\treturn ActiveBtnMsg(b.activeButton)\n}\n"
  },
  {
    "path": "internal/cli/commands/catalog/tui/delegate.go",
    "content": "package tui\n\nimport (\n\t\"github.com/charmbracelet/bubbles/key\"\n\t\"github.com/charmbracelet/bubbles/list\"\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\nconst (\n\tselectedTitleForegroundColorDark       = \"#63C5DA\"\n\tselectedTitleBorderForegroundColorDark = \"#63C5DA\"\n\n\tselectedDescForegroundColorDark       = \"#59788E\"\n\tselectedDescBorderForegroundColorDark = \"#63C5DA\"\n)\n\nfunc newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate {\n\td := list.NewDefaultDelegate()\n\n\td.Styles.SelectedTitle.\n\t\tForeground(lipgloss.AdaptiveColor{Dark: selectedTitleForegroundColorDark}).\n\t\tBorderForeground(lipgloss.AdaptiveColor{Dark: selectedTitleBorderForegroundColorDark})\n\n\td.Styles.SelectedDesc = d.Styles.SelectedTitle.\n\t\tForeground(lipgloss.AdaptiveColor{Dark: selectedDescForegroundColorDark}).\n\t\tBorderForeground(lipgloss.AdaptiveColor{Dark: selectedDescBorderForegroundColorDark})\n\n\thelp := []key.Binding{keys.choose, keys.scaffold}\n\n\td.ShortHelpFunc = func() []key.Binding {\n\t\treturn help\n\t}\n\n\td.FullHelpFunc = func() [][]key.Binding {\n\t\treturn [][]key.Binding{help}\n\t}\n\n\treturn d\n}\n"
  },
  {
    "path": "internal/cli/commands/catalog/tui/keys.go",
    "content": "package tui\n\nimport (\n\t\"github.com/charmbracelet/bubbles/help\"\n\t\"github.com/charmbracelet/bubbles/key\"\n\t\"github.com/charmbracelet/bubbles/list\"\n\t\"github.com/charmbracelet/bubbles/viewport\"\n)\n\n// newListKeyMap returns a set of keybindings for the list view.\nfunc newListKeyMap() list.KeyMap {\n\treturn list.KeyMap{\n\t\t// Browsing.\n\t\tCursorUp: key.NewBinding(\n\t\t\tkey.WithKeys(\"k\", \"up\", \"ctrl+p\"),\n\t\t\tkey.WithHelp(\"k/↑/ctrl+p\", \"move up\"),\n\t\t),\n\t\tCursorDown: key.NewBinding(\n\t\t\tkey.WithKeys(\"j\", \"down\", \"ctrl+n\"),\n\t\t\tkey.WithHelp(\"j/↓/ctrl+n\", \"move down\"),\n\t\t),\n\t\tPrevPage: key.NewBinding(\n\t\t\tkey.WithKeys(\"h\", \"left\", \"pgup\", \"alt+v\"),\n\t\t\tkey.WithHelp(\"h/←/pgup/alt+v\", \"prev page\"),\n\t\t),\n\t\tNextPage: key.NewBinding(\n\t\t\tkey.WithKeys(\"l\", \"right\", \"pgdown\", \"ctrl+v\"),\n\t\t\tkey.WithHelp(\"l/→/pgdn/ctrl+v\", \"next page\"),\n\t\t),\n\t\tGoToStart: key.NewBinding(\n\t\t\tkey.WithKeys(\"home\", \"ctrl+a\"),\n\t\t\tkey.WithHelp(\"home/ctrl+a\", \"go to start\"),\n\t\t),\n\t\tGoToEnd: key.NewBinding(\n\t\t\tkey.WithKeys(\"end\", \"ctrl+e\"),\n\t\t\tkey.WithHelp(\"end/ctrl+e\", \"go to end\"),\n\t\t),\n\t\tFilter: key.NewBinding(\n\t\t\tkey.WithKeys(\"/\"),\n\t\t\tkey.WithHelp(\"/\", \"search\"),\n\t\t),\n\t\tClearFilter: key.NewBinding(\n\t\t\tkey.WithKeys(\"esc\"),\n\t\t\tkey.WithHelp(\"esc\", \"clear filter\"),\n\t\t),\n\n\t\t// Filtering.\n\t\tCancelWhileFiltering: key.NewBinding(\n\t\t\tkey.WithKeys(\"esc\"),\n\t\t\tkey.WithHelp(\"esc\", \"cancel\"),\n\t\t),\n\t\tAcceptWhileFiltering: key.NewBinding(\n\t\t\tkey.WithKeys(\"enter\", \"tab\", \"shift+tab\", \"ctrl+k\", \"up\", \"ctrl+j\", \"down\"),\n\t\t\tkey.WithHelp(\"enter\", \"apply filter\"),\n\t\t),\n\n\t\t// Toggle help.\n\t\tShowFullHelp: key.NewBinding(\n\t\t\tkey.WithKeys(\"?\"),\n\t\t\tkey.WithHelp(\"?\", \"more\"),\n\t\t),\n\t\tCloseFullHelp: key.NewBinding(\n\t\t\tkey.WithKeys(\"?\"),\n\t\t\tkey.WithHelp(\"?\", \"close help\"),\n\t\t),\n\n\t\t// Quitting.\n\t\tQuit: key.NewBinding(\n\t\t\tkey.WithKeys(\"q\", \"esc\"),\n\t\t\tkey.WithHelp(\"q\", \"quit\"),\n\t\t),\n\t\tForceQuit: key.NewBinding(key.WithKeys(\"ctrl+c\")),\n\t}\n}\n\ntype delegateKeyMap struct {\n\tchoose   key.Binding\n\tscaffold key.Binding\n}\n\n// Additional short help entries. This satisfies the help.KeyMap interface and\n// is entirely optional.\nfunc (d delegateKeyMap) ShortHelp() []key.Binding { //nolint:gocritic\n\treturn []key.Binding{\n\t\td.choose,\n\t\td.scaffold,\n\t}\n}\n\n// Additional full help entries. This satisfies the help.KeyMap interface and\n// is entirely optional.\nfunc (d delegateKeyMap) FullHelp() [][]key.Binding { //nolint:gocritic\n\treturn [][]key.Binding{\n\t\t{\n\t\t\td.choose,\n\t\t\td.scaffold,\n\t\t},\n\t}\n}\n\n// newDelegateKeyMap returns a set of keybindings.\nfunc newDelegateKeyMap() *delegateKeyMap {\n\treturn &delegateKeyMap{\n\t\tchoose: key.NewBinding(\n\t\t\tkey.WithKeys(\"enter\", \"ctrl-j\"),\n\t\t\tkey.WithHelp(\"enter/ctrl-j\", \"choose\"),\n\t\t),\n\t\tscaffold: key.NewBinding(\n\t\t\tkey.WithKeys(\"S\", \"s\"),\n\t\t\tkey.WithHelp(\"S\", \"Scaffold\"),\n\t\t),\n\t}\n}\n\n// pagerKeyMap returns a set of keybindings for the pager. It satisfies to the\n// help.KeyMap interface, which is used to render the menu.\ntype pagerKeyMap struct {\n\tviewport.KeyMap\n\n\thelp help.Model\n\n\t// Button navigation\n\tNavigation key.Binding\n\n\t// Button navigation\n\tNavigationBack key.Binding\n\n\t// Select button\n\tChoose key.Binding\n\n\t// Run Scaffold command\n\tScaffold key.Binding\n\n\t// Help toggle keybindings.\n\tHelp key.Binding\n\n\t// The quit keybinding. This won't be caught when filtering.\n\tQuit key.Binding\n\n\t// The quit-no-matter-what keybinding. This will be caught when filtering.\n\tForceQuit key.Binding\n}\n\n// ShortHelp returns keybindings to be shown in the mini help view. It's part\n// of the key.Map interface.\nfunc (keys pagerKeyMap) ShortHelp() []key.Binding { //nolint:gocritic\n\treturn []key.Binding{\n\t\tkeys.Up,\n\t\tkeys.Down,\n\t\tkeys.Navigation,\n\t\tkeys.NavigationBack,\n\t\tkeys.Choose,\n\t\tkeys.Scaffold,\n\t\tkeys.Help,\n\t\tkeys.Quit,\n\t}\n}\n\n// FullHelp returns keybindings for the expanded help view. It's part of the\n// key.Map interface.\nfunc (keys pagerKeyMap) FullHelp() [][]key.Binding { //nolint:gocritic\n\treturn [][]key.Binding{\n\t\t{keys.Up, keys.Down, keys.PageDown, keys.PageUp},                   // first column\n\t\t{keys.Navigation, keys.NavigationBack, keys.Choose, keys.Scaffold}, // second column\n\t\t{keys.Help, keys.Quit, keys.ForceQuit},                             // third column\n\t}\n}\n\n// newPagerKeyMap returns a set of keybindings for the pager view.\nfunc newPagerKeyMap() pagerKeyMap {\n\treturn pagerKeyMap{\n\t\tKeyMap: viewport.KeyMap{\n\t\t\tHalfPageUp: key.NewBinding(\n\t\t\t\tkey.WithDisabled(),\n\t\t\t),\n\t\t\tHalfPageDown: key.NewBinding(\n\t\t\t\tkey.WithDisabled(),\n\t\t\t),\n\t\t\tUp: key.NewBinding(\n\t\t\t\tkey.WithKeys(\"k\", \"up\", \"ctrl+p\"),\n\t\t\t\tkey.WithHelp(\"k/↑/ctrl+p\", \"move up\"),\n\t\t\t),\n\t\t\tDown: key.NewBinding(\n\t\t\t\tkey.WithKeys(\"j\", \"down\", \"ctrl+n\"),\n\t\t\t\tkey.WithHelp(\"j/↓/ctrl+n\", \"move down\"),\n\t\t\t),\n\t\t\tPageDown: key.NewBinding(\n\t\t\t\tkey.WithKeys(\"l\", \"right\", \"pgdown\", \"ctrl+v\"),\n\t\t\t\tkey.WithHelp(\"l/→/pgdn/ctrl+v\", \"page down\"),\n\t\t\t),\n\t\t\tPageUp: key.NewBinding(\n\t\t\t\tkey.WithKeys(\"h\", \"left\", \"pgup\", \"alt+v\"),\n\t\t\t\tkey.WithHelp(\"h/←/pgup/alt+v\", \"page up\"),\n\t\t\t),\n\t\t},\n\t\thelp: help.New(),\n\t\tNavigation: key.NewBinding(\n\t\t\tkey.WithKeys(\"tab\"),\n\t\t\tkey.WithHelp(\"tab\", \"navigation\"),\n\t\t),\n\t\tNavigationBack: key.NewBinding(\n\t\t\tkey.WithKeys(\"shift+tab\"),\n\t\t\tkey.WithHelp(\"shift+tab\", \"navigation\"),\n\t\t),\n\t\tChoose: key.NewBinding(\n\t\t\tkey.WithKeys(\"enter\", \"ctrl-j\"),\n\t\t\tkey.WithHelp(\"enter/ctrl-j\", \"choose\"),\n\t\t),\n\t\tScaffold: key.NewBinding(\n\t\t\tkey.WithKeys(\"S\", \"s\"),\n\t\t\tkey.WithHelp(\"S\", \"Scaffold\"),\n\t\t),\n\t\tHelp: key.NewBinding(\n\t\t\tkey.WithKeys(\"?\"),\n\t\t\tkey.WithHelp(\"?\", \"toggle help\"),\n\t\t),\n\t\tQuit: key.NewBinding(\n\t\t\tkey.WithKeys(\"q\", \"esc\"),\n\t\t\tkey.WithHelp(\"q\", \"back to list\"),\n\t\t),\n\t\tForceQuit: key.NewBinding(key.WithKeys(\"ctrl+c\")),\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/catalog/tui/model.go",
    "content": "package tui\n\nimport (\n\t\"github.com/charmbracelet/bubbles/list\"\n\t\"github.com/charmbracelet/bubbles/viewport\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipgloss\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/catalog/tui/components/buttonbar\"\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog\"\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog/module\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// sessionState keeps track of the view we are currently on.\ntype sessionState int\n\n// button is a button in the buttonbar component.\ntype button int\n\nconst (\n\ttitle = \"List of Modules\"\n\n\ttitleForegroundColor = \"#A8ACB1\"\n\ttitleBackgroundColor = \"#1D252F\"\n)\n\nconst (\n\tListState sessionState = iota\n\tPagerState\n\tScaffoldState\n)\n\nconst (\n\tscaffoldBtn button = iota\n\tviewSourceBtn\n)\n\nvar (\n\tavailableButtons = []button{scaffoldBtn, viewSourceBtn}\n)\n\nfunc (b button) String() string {\n\treturn []string{\n\t\t\"Scaffold\",\n\t\t\"View Source in Browser\",\n\t}[b]\n}\n\ntype Model struct {\n\tList                list.Model\n\tlogger              log.Logger\n\tterragruntOptions   *options.TerragruntOptions\n\tSVC                 catalog.CatalogService\n\tselectedModule      *module.Module\n\tdelegateKeys        *delegateKeyMap\n\tbuttonBar           *buttonbar.ButtonBar\n\tcurrentPagerButtons []button\n\tpagerKeys           pagerKeyMap\n\tlistKeys            list.KeyMap\n\tviewport            viewport.Model\n\tactiveButton        button\n\tState               sessionState\n\theight              int\n\twidth               int\n\tready               bool\n}\n\nfunc NewModel(l log.Logger, opts *options.TerragruntOptions, svc catalog.CatalogService) Model {\n\tvar (\n\t\tmodules      = svc.Modules()\n\t\titems        = make([]list.Item, 0, len(modules))\n\t\tlistKeys     = newListKeyMap()\n\t\tdelegateKeys = newDelegateKeyMap()\n\t\tpagerKeys    = newPagerKeyMap()\n\t)\n\n\t// Make the initial list of items\n\tfor _, module := range modules {\n\t\titems = append(items, module)\n\t}\n\n\t// Setup the list\n\tdelegate := newItemDelegate(delegateKeys)\n\tlist := list.New(items, delegate, 0, 0)\n\tlist.KeyMap = listKeys\n\tlist.SetFilteringEnabled(true)\n\tlist.Title = title\n\tlist.Styles.Title = lipgloss.NewStyle().\n\t\tForeground(lipgloss.Color(titleForegroundColor)).\n\t\tBackground(lipgloss.Color(titleBackgroundColor)).\n\t\tPadding(0, 1)\n\n\t// Setup the markdown viewer\n\tvp := viewport.New(0, 0)\n\n\t// Setup the button bar\n\tbs := make([]string, len(availableButtons))\n\tfor i, b := range availableButtons {\n\t\tbs[i] = b.String()\n\t}\n\n\tbb := buttonbar.New(bs)\n\n\treturn Model{\n\t\tList:              list,\n\t\tlistKeys:          listKeys,\n\t\tdelegateKeys:      delegateKeys,\n\t\tviewport:          vp,\n\t\tbuttonBar:         bb,\n\t\tpagerKeys:         pagerKeys,\n\t\tterragruntOptions: opts,\n\t\tSVC:               svc,\n\t\tlogger:            l,\n\t}\n}\n\n// Init implements bubbletea.Model.Init\nfunc (m Model) Init() tea.Cmd { //nolint:gocritic\n\treturn tea.Batch(\n\t\tm.buttonBar.Init(),\n\t)\n}\n"
  },
  {
    "path": "internal/cli/commands/catalog/tui/model_test.go",
    "content": "package tui_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/x/exp/teatest\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/catalog/tui\"\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog\"\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog/module\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Test configuration - color profiles are handled by individual test cases if needed\n\n// createMockCatalogService creates a mock catalog service with test modules for testing\nfunc createMockCatalogService(t *testing.T, opts *options.TerragruntOptions) catalog.CatalogService {\n\tt.Helper()\n\n\tmockNewRepo := func(ctx context.Context, logger log.Logger, repoURL, path string, walkWithSymlinks, allowCAS bool, rootWorkingDir string) (*module.Repo, error) {\n\t\t// Create a temporary directory structure for testing\n\t\tdummyRepoDir := filepath.Join(helpers.TmpDirWOSymlinks(t), strings.ReplaceAll(repoURL, \"github.com/gruntwork-io/\", \"\"))\n\n\t\t// Initialize as a proper git repository\n\t\tos.MkdirAll(dummyRepoDir, 0755)\n\n\t\t// Initialize git repository\n\t\tgitDir := filepath.Join(dummyRepoDir, \".git\")\n\t\tos.MkdirAll(gitDir, 0755)\n\t\tos.WriteFile(filepath.Join(gitDir, \"config\"), fmt.Appendf(nil, `[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n\tlogallrefupdates = true\n[remote \"origin\"]\n\turl = %s\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n[branch \"main\"]\n\tremote = origin\n\tmerge = refs/heads/main\n`, repoURL), 0644)\n\t\tos.WriteFile(filepath.Join(gitDir, \"HEAD\"), []byte(\"ref: refs/heads/main\\n\"), 0644)\n\n\t\t// Create refs directory structure\n\t\trefsDir := filepath.Join(gitDir, \"refs\")\n\t\theadsDir := filepath.Join(refsDir, \"heads\")\n\t\tremotesDir := filepath.Join(refsDir, \"remotes\", \"origin\")\n\n\t\tos.MkdirAll(headsDir, 0755)\n\t\tos.MkdirAll(remotesDir, 0755)\n\n\t\t// Create a fake commit hash for main branch\n\t\tfakeCommitHash := \"1234567890abcdef1234567890abcdef12345678\"\n\t\tos.WriteFile(filepath.Join(headsDir, \"main\"), []byte(fakeCommitHash+\"\\n\"), 0644)\n\t\tos.WriteFile(filepath.Join(remotesDir, \"main\"), []byte(fakeCommitHash+\"\\n\"), 0644)\n\n\t\t// Create test modules based on repoURL\n\t\tswitch repoURL {\n\t\tcase \"github.com/gruntwork-io/test-repo-1\":\n\t\t\treadme1Path := filepath.Join(dummyRepoDir, \"README.md\")\n\t\t\tos.WriteFile(readme1Path, []byte(\"# AWS VPC Module\\nThis module creates a VPC in AWS with all the necessary components.\"), 0644)\n\t\t\tos.WriteFile(filepath.Join(dummyRepoDir, \"main.tf\"), []byte(\"# VPC terraform configuration\"), 0644)\n\t\tcase \"github.com/gruntwork-io/test-repo-2\":\n\t\t\treadme2Path := filepath.Join(dummyRepoDir, \"README.md\")\n\t\t\tos.WriteFile(readme2Path, []byte(\"# AWS EKS Module\\nThis module creates an EKS cluster in AWS.\"), 0644)\n\t\t\tos.WriteFile(filepath.Join(dummyRepoDir, \"main.tf\"), []byte(\"# EKS terraform configuration\"), 0644)\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"unexpected repoURL in mock: %s\", repoURL)\n\t\t}\n\n\t\treturn module.NewRepo(ctx, logger, dummyRepoDir, path, walkWithSymlinks, allowCAS, \"\")\n\t}\n\n\t// Create a temporary root config file\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\trootFile := filepath.Join(tmpDir, \"root.hcl\")\n\terr := os.WriteFile(rootFile, []byte(`catalog {\n\turls = [\n\t\t\"github.com/gruntwork-io/test-repo-1\",\n\t\t\"github.com/gruntwork-io/test-repo-2\",\n\t]\n}`), 0600)\n\trequire.NoError(t, err)\n\n\tunitDir := filepath.Join(tmpDir, \"unit\")\n\tos.MkdirAll(unitDir, 0755)\n\topts.TerragruntConfigPath = filepath.Join(unitDir, \"terragrunt.hcl\")\n\topts.ScaffoldRootFileName = config.RecommendedParentConfigName\n\n\tsvc := catalog.NewCatalogService(opts).WithNewRepoFunc(mockNewRepo)\n\n\t// Load the modules\n\tctx := t.Context()\n\tl := logger.CreateLogger()\n\terr = svc.Load(ctx, l)\n\trequire.NoError(t, err)\n\n\treturn svc\n}\n\nfunc TestTUIFinalModel(t *testing.T) {\n\tt.Parallel()\n\n\topts, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\tsvc := createMockCatalogService(t, opts)\n\tl := logger.CreateLogger()\n\n\tm := tui.NewModel(l, opts, svc)\n\n\ttm := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(120, 40))\n\n\tteatest.WaitFor(t, tm.Output(), func(bts []byte) bool {\n\t\treturn bytes.Contains(bts, []byte(\"List of Modules\"))\n\t}, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*2))\n\n\ttm.Send(tea.KeyMsg{\n\t\tType:  tea.KeyRunes,\n\t\tRunes: []rune(\"q\"),\n\t})\n\n\ttm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*2))\n\n\tfm := tm.FinalModel(t)\n\tfinalModel, ok := fm.(tui.Model)\n\trequire.True(t, ok, \"final model should be of type tui.Model, got %T\", fm)\n\n\t// Verify the model has the expected state\n\tassert.Equal(t, tui.ListState, finalModel.State)\n\tassert.NotNil(t, finalModel.SVC)\n\tassert.NotNil(t, finalModel.List)\n\tassert.Len(t, finalModel.SVC.Modules(), 2, \"should have 2 test modules\")\n}\n\nfunc TestTUIInitialOutput(t *testing.T) {\n\tt.Parallel()\n\n\topts, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\tsvc := createMockCatalogService(t, opts)\n\tl := logger.CreateLogger()\n\n\tm := tui.NewModel(l, opts, svc)\n\n\ttm := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(120, 40))\n\n\t// Send 'q' to quit immediately for consistent output\n\ttm.Send(tea.KeyMsg{\n\t\tType:  tea.KeyRunes,\n\t\tRunes: []rune(\"q\"),\n\t})\n\n\t// Test that we get the expected output\n\tout, err := io.ReadAll(tm.FinalOutput(t))\n\trequire.NoError(t, err)\n\n\tteatest.RequireEqualOutput(t, out)\n}\n\nfunc TestTUINavigationToModuleDetails(t *testing.T) {\n\tt.Parallel()\n\n\topts, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\tsvc := createMockCatalogService(t, opts)\n\tl := logger.CreateLogger()\n\n\tm := tui.NewModel(l, opts, svc)\n\n\ttm := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(120, 40))\n\n\t// Wait for initial render\n\tteatest.WaitFor(t, tm.Output(), func(bts []byte) bool {\n\t\treturn bytes.Contains(bts, []byte(\"List of Modules\"))\n\t}, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*2))\n\n\t// Press Enter to select the first module (assuming it's pre-selected)\n\ttm.Send(tea.KeyMsg{\n\t\tType: tea.KeyEnter,\n\t})\n\n\t// Wait for the pager view to appear\n\tteatest.WaitFor(t, tm.Output(), func(bts []byte) bool {\n\t\toutput := string(bts)\n\t\t// Check for pager view elements (scroll percentage, button bar)\n\t\treturn strings.Contains(output, \"%\") && (strings.Contains(output, \"Scaffold\") || strings.Contains(output, \"View Source\"))\n\t}, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*3))\n\n\t// Send 'q' to go back to list\n\ttm.Send(tea.KeyMsg{\n\t\tType:  tea.KeyRunes,\n\t\tRunes: []rune(\"q\"),\n\t})\n\n\t// Wait for return to list view\n\tteatest.WaitFor(t, tm.Output(), func(bts []byte) bool {\n\t\treturn bytes.Contains(bts, []byte(\"List of Modules\"))\n\t}, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*2))\n\n\t// Finally quit the application\n\ttm.Send(tea.KeyMsg{\n\t\tType:  tea.KeyRunes,\n\t\tRunes: []rune(\"q\"),\n\t})\n\n\ttm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*2))\n}\n\nfunc TestTUIModuleFiltering(t *testing.T) {\n\tt.Parallel()\n\n\topts, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\tsvc := createMockCatalogService(t, opts)\n\tl := logger.CreateLogger()\n\n\tm := tui.NewModel(l, opts, svc)\n\n\ttm := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(120, 40))\n\n\t// Wait for initial render\n\tteatest.WaitFor(t, tm.Output(), func(bts []byte) bool {\n\t\treturn bytes.Contains(bts, []byte(\"List of Modules\"))\n\t}, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*2))\n\n\t// Activate filtering with '/'\n\ttm.Send(tea.KeyMsg{\n\t\tType:  tea.KeyRunes,\n\t\tRunes: []rune(\"/\"),\n\t})\n\n\t// Type filter text\n\ttm.Type(\"VPC\")\n\n\t// Wait for filtering to take effect\n\tteatest.WaitFor(t, tm.Output(), func(bts []byte) bool {\n\t\toutput := string(bts)\n\t\t// Should show filtered results containing \"VPC\"\n\t\treturn strings.Contains(strings.ToUpper(output), \"VPC\")\n\t}, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*3))\n\n\t// Press Escape to exit filtering\n\ttm.Send(tea.KeyMsg{\n\t\tType: tea.KeyEsc,\n\t})\n\n\t// Wait for return to normal list view\n\tteatest.WaitFor(t, tm.Output(), func(bts []byte) bool {\n\t\toutput := string(bts)\n\t\t// Should show both modules again\n\t\treturn strings.Contains(output, \"VPC\") && strings.Contains(output, \"EKS\")\n\t}, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*2))\n\n\t// Quit the application\n\ttm.Send(tea.KeyMsg{\n\t\tType:  tea.KeyRunes,\n\t\tRunes: []rune(\"q\"),\n\t})\n\n\ttm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*2))\n}\n\nfunc TestTUIWindowResize(t *testing.T) {\n\tt.Parallel()\n\n\topts, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\tsvc := createMockCatalogService(t, opts)\n\tl := logger.CreateLogger()\n\n\tm := tui.NewModel(l, opts, svc)\n\n\ttm := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(80, 30))\n\n\t// Wait for initial render\n\tteatest.WaitFor(t, tm.Output(), func(bts []byte) bool {\n\t\treturn bytes.Contains(bts, []byte(\"List of Modules\"))\n\t}, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*2))\n\n\t// Send window resize message\n\ttm.Send(tea.WindowSizeMsg{Width: 120, Height: 40})\n\n\t// Verify the interface handles resize gracefully\n\tteatest.WaitFor(t, tm.Output(), func(bts []byte) bool {\n\t\treturn bytes.Contains(bts, []byte(\"List of Modules\"))\n\t}, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*2))\n\n\t// Quit\n\ttm.Send(tea.KeyMsg{\n\t\tType:  tea.KeyRunes,\n\t\tRunes: []rune(\"q\"),\n\t})\n\n\ttm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*2))\n}\n\n// TestTUIScaffoldWithRealRepository tests scaffold functionality using a real git repository\n// This test requires network access and may be slower, but provides more realistic testing\nfunc TestTUIScaffoldWithRealRepository(t *testing.T) {\n\tt.Parallel()\n\n\topts, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\t// Create a temp directory for scaffold output\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\topts.WorkingDir = tempDir\n\topts.ScaffoldRootFileName = config.RecommendedParentConfigName\n\topts.ScaffoldVars = []string{\"EnableRootInclude=false\"}\n\n\t// Use real terraform-fake-modules repository\n\tsvc := catalog.NewCatalogService(opts).WithRepoURL(\"https://github.com/gruntwork-io/terraform-fake-modules.git\")\n\n\t// Load modules from the real repository\n\tctx := t.Context()\n\tl := logger.CreateLogger()\n\terr = svc.Load(ctx, l)\n\trequire.NoError(t, err)\n\n\tmodules := svc.Modules()\n\trequire.NotEmpty(t, modules, \"should have modules from real repository\")\n\n\tm := tui.NewModel(l, opts, svc)\n\n\ttm := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(120, 40))\n\n\t// Wait for initial render\n\tteatest.WaitFor(t, tm.Output(), func(bts []byte) bool {\n\t\treturn bytes.Contains(bts, []byte(\"List of Modules\"))\n\t}, teatest.WithCheckInterval(time.Millisecond*100), teatest.WithDuration(time.Second*3))\n\n\t// Press 'S' to scaffold the first module\n\ttm.Send(tea.KeyMsg{\n\t\tType:  tea.KeyRunes,\n\t\tRunes: []rune(\"S\"),\n\t})\n\n\t// Wait for scaffold to complete - the application should quit after scaffolding\n\ttm.WaitFinished(t, teatest.WithFinalTimeout(time.Second*10))\n\n\tfm := tm.FinalModel(t)\n\tfinalModel, ok := fm.(tui.Model)\n\trequire.True(t, ok, \"final model should be of type model\")\n\n\t// Verify the model transitioned to ScaffoldState\n\tassert.Equal(t, tui.ScaffoldState, finalModel.State)\n\tassert.NotNil(t, finalModel.SVC)\n\tassert.NotEmpty(t, finalModel.SVC.Modules())\n\n\t// Verify that a terragrunt.hcl file was actually created\n\tterragruntFile := filepath.Join(tempDir, \"terragrunt.hcl\")\n\tassert.FileExists(t, terragruntFile, \"scaffold should create terragrunt.hcl file\")\n}\n"
  },
  {
    "path": "internal/cli/commands/catalog/tui/testdata/TestTUIInitialOutput.golden",
    "content": "\u001b[?25l\u001b[?2004h\r   List of Modules                                                                                         \u001b[K\n                                                                                                           \u001b[K\n  2 items                                                                                                  \u001b[K\n                                                                                                           \u001b[K\n│ AWS VPC Module                                                                                           \u001b[K\n│ This module creates a VPC in AWS with all the necessary components.                                      \u001b[K\n                                                                                                           \u001b[K\n  AWS EKS Module                                                                                           \u001b[K\n  This module creates an EKS cluster in AWS.                                                               \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n                                                                                                           \u001b[K\n  k/↑/ctrl+p move up • j/↓/ctrl+n move down • enter/ctrl-j choose • S Scaffold • / search • q quit • ? more\u001b[K\u001b[120D\u001b[2K\r\u001b[?2004l\u001b[?25h\u001b[?1002l\u001b[?1003l\u001b[?1006l"
  },
  {
    "path": "internal/cli/commands/catalog/tui/tui.go",
    "content": "// Package tui provides a text-based user interface for the Terragrunt catalog command.\npackage tui\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nfunc Run(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, svc catalog.CatalogService) error {\n\tif _, err := tea.NewProgram(NewModel(l, opts, svc), tea.WithAltScreen(), tea.WithContext(ctx)).Run(); err != nil {\n\t\tif err := context.Cause(ctx); errors.Is(err, context.Canceled) {\n\t\t\treturn nil\n\t\t} else if err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/cli/commands/catalog/tui/update.go",
    "content": "package tui\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/charmbracelet/bubbles/key\"\n\t\"github.com/charmbracelet/bubbles/list\"\n\t\"github.com/charmbracelet/bubbles/viewport\"\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/glamour\"\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/pkg/browser\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/catalog/tui/command\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/catalog/tui/components/buttonbar\"\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog\"\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog/module\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nfunc updateList(msg tea.Msg, m Model) (tea.Model, tea.Cmd) { //nolint:gocritic\n\tvar (\n\t\tcmd  tea.Cmd\n\t\tcmds []tea.Cmd\n\t)\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\t// Don't match any of the keys below if we're actively filtering.\n\t\tif m.List.FilterState() == list.Filtering {\n\t\t\tbreak\n\t\t}\n\n\t\tswitch {\n\t\tcase key.Matches(msg, m.delegateKeys.choose, m.delegateKeys.scaffold):\n\t\t\tif selectedModule, ok := m.List.SelectedItem().(*module.Module); ok {\n\t\t\t\tswitch {\n\t\t\t\tcase key.Matches(msg, m.delegateKeys.choose):\n\t\t\t\t\t// prepare the viewport\n\t\t\t\t\tvar content string\n\n\t\t\t\t\tif selectedModule.IsMarkDown() {\n\t\t\t\t\t\trenderer, err := glamour.NewTermRenderer(\n\t\t\t\t\t\t\tglamour.WithAutoStyle(),\n\t\t\t\t\t\t\tglamour.WithWordWrap(m.width),\n\t\t\t\t\t\t)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn m, rendererErrCmd(err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tmd, err := renderer.Render(selectedModule.Content(false))\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn m, rendererErrCmd(err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tcontent = md\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcontent = selectedModule.Content(true)\n\t\t\t\t\t}\n\n\t\t\t\t\tm.viewport.SetContent(content)\n\n\t\t\t\t\t// Dynamically create button bar based on module URL\n\t\t\t\t\tvar pagerButtons []button\n\n\t\t\t\t\tbuttonNames := []string{}\n\n\t\t\t\t\t// Always add scaffold button\n\t\t\t\t\tpagerButtons = append(pagerButtons, scaffoldBtn)\n\t\t\t\t\tbuttonNames = append(buttonNames, scaffoldBtn.String())\n\n\t\t\t\t\tif selectedModule.URL() != \"\" {\n\t\t\t\t\t\tpagerButtons = append(pagerButtons, viewSourceBtn)\n\t\t\t\t\t\tbuttonNames = append(buttonNames, viewSourceBtn.String())\n\t\t\t\t\t}\n\n\t\t\t\t\tm.currentPagerButtons = pagerButtons\n\t\t\t\t\tm.buttonBar = buttonbar.New(buttonNames)\n\t\t\t\t\t// Ensure the button bar is initialized\n\t\t\t\t\tcmds = append(cmds, m.buttonBar.Init())\n\n\t\t\t\t\t// advance state\n\t\t\t\t\tm.selectedModule = selectedModule\n\t\t\t\t\tm.State = PagerState\n\t\t\t\tcase key.Matches(msg, m.delegateKeys.scaffold):\n\t\t\t\t\tm.State = ScaffoldState\n\t\t\t\t\treturn m, scaffoldModuleCmd(m.logger, m, m.SVC, selectedModule)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\tcase key.Matches(msg, m.listKeys.Quit):\n\t\t\t// because we're on the first screen, we simply quit at this point\n\t\t\treturn m, tea.Quit\n\t\t}\n\t}\n\n\t// Handle keyboard and mouse events for the list\n\tm.List, cmd = m.List.Update(msg)\n\n\t// Append any commands from button bar initialization\n\tif len(cmds) > 0 {\n\t\treturn m, tea.Batch(cmd, tea.Batch(cmds...))\n\t}\n\n\treturn m, cmd\n}\n\nfunc updatePager(msg tea.Msg, m Model) (tea.Model, tea.Cmd) { //nolint:gocritic\n\tvar (\n\t\tcmd  tea.Cmd\n\t\tcmds []tea.Cmd\n\t)\n\n\tswitch msg := msg.(type) {\n\tcase tea.KeyMsg:\n\t\tbbModel, barCmd := m.buttonBar.Update(msg)\n\t\tif newButtonBar, ok := bbModel.(*buttonbar.ButtonBar); ok {\n\t\t\tm.buttonBar = newButtonBar\n\t\t}\n\n\t\tif barCmd != nil {\n\t\t\tcmds = append(cmds, barCmd)\n\t\t}\n\n\t\tswitch {\n\t\tcase key.Matches(msg, m.pagerKeys.Choose):\n\t\t\t// Choose changes the action depending on the active button\n\t\t\t// m.activeButton is set by ActiveBtnMsg, which is mapped from m.currentPagerButtons\n\t\t\tcurrentAction := m.activeButton\n\n\t\t\tswitch currentAction {\n\t\t\tcase scaffoldBtn:\n\t\t\t\tm.State = ScaffoldState\n\t\t\t\treturn m, scaffoldModuleCmd(m.logger, m, m.SVC, m.selectedModule)\n\t\t\tcase viewSourceBtn:\n\t\t\t\tif m.selectedModule.URL() != \"\" {\n\t\t\t\t\tif err := browser.OpenURL(m.selectedModule.URL()); err != nil {\n\t\t\t\t\t\tm.viewport.SetContent(fmt.Sprintf(\"could not open url in browser: %s. got error: %s\", m.selectedModule.URL(), err))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tm.logger.Warnf(\"Unknown button pressed: %s\", currentAction)\n\t\t\t}\n\n\t\tcase key.Matches(msg, m.pagerKeys.Scaffold):\n\t\t\tm.State = ScaffoldState\n\t\t\treturn m, scaffoldModuleCmd(m.logger, m, m.SVC, m.selectedModule)\n\n\t\tcase key.Matches(msg, m.pagerKeys.Quit):\n\t\t\t// because we're on the second screen, we need to go back\n\t\t\tm.State = ListState\n\t\t\treturn m, nil\n\t\t}\n\tcase buttonbar.ActiveBtnMsg:\n\t\t// Map the index from buttonbar.ActiveBtnMsg to the actual button type\n\t\tif int(msg) >= 0 && int(msg) < len(m.currentPagerButtons) {\n\t\t\tm.activeButton = m.currentPagerButtons[int(msg)]\n\t\t}\n\t}\n\n\t// Handle keyboard and mouse events in the viewport\n\tm.viewport, cmd = m.viewport.Update(msg)\n\tcmds = append(cmds, cmd)\n\n\treturn m, tea.Batch(cmds...)\n}\n\n// Update handles all TUI interactions and implements bubbletea.Model.Update.\nfunc (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { //nolint:gocritic\n\tswitch msg := msg.(type) {\n\tcase tea.WindowSizeMsg:\n\t\th, v := appStyle.GetFrameSize()\n\t\tm.List.SetSize(msg.Width-h, msg.Height-v)\n\t\tm.width = msg.Width\n\t\tm.height = msg.Height\n\n\t\tif !m.ready {\n\t\t\t// Since this program is using the full size of the viewport we\n\t\t\t// need to wait until we've received the window dimensions before\n\t\t\t// we can initialize the viewport. The initial dimensions come in\n\t\t\t// quickly, though asynchronously, which is why we wait for them\n\t\t\t// here.\n\t\t\tm.viewport = viewport.New(msg.Width, msg.Height-v-lipgloss.Height(m.footerView()))\n\t\t\tm.ready = true\n\t\t} else {\n\t\t\tm.viewport.Width = msg.Width\n\t\t\tm.viewport.Height = msg.Height - v - lipgloss.Height(m.footerView())\n\t\t}\n\n\tcase scaffoldFinishedMsg:\n\t\tif msg.err != nil {\n\t\t\ttea.Printf(\"error scaffolding module: %s\", msg.err.Error())\n\t\t}\n\n\t\treturn m, tea.Quit\n\n\tcase rendererErrMsg:\n\t\tm.viewport.SetContent(\"there was an error rendering markdown: \" + msg.err.Error())\n\t\t// ensure we show the viewport\n\t\tm.State = PagerState\n\t}\n\n\t// Hand off the message and model to the appropriate update function for the\n\t// appropriate view based on the current state.\n\tswitch m.State {\n\tcase ListState:\n\t\treturn updateList(msg, m)\n\tcase PagerState:\n\t\treturn updatePager(msg, m)\n\tcase ScaffoldState:\n\t\t// if we're on the scaffold state, we do nothing and wait for the\n\t\t// scaffoldFinishedMsg message. This prevents further input.\n\t\treturn m, nil\n\t}\n\n\treturn m, nil\n}\n\ntype rendererErrMsg struct{ err error }\n\nfunc rendererErrCmd(err error) tea.Cmd {\n\treturn func() tea.Msg {\n\t\treturn rendererErrMsg{err}\n\t}\n}\n\ntype scaffoldFinishedMsg struct{ err error }\n\n// Return a tea.Cmd that will scaffold the given module.\nfunc scaffoldModuleCmd(l log.Logger, m Model, svc catalog.CatalogService, module *module.Module) tea.Cmd { //nolint:gocritic\n\treturn tea.Exec(command.NewScaffold(l, m.terragruntOptions, svc, module), func(err error) tea.Msg {\n\t\treturn scaffoldFinishedMsg{err}\n\t})\n}\n"
  },
  {
    "path": "internal/cli/commands/catalog/tui/view.go",
    "content": "package tui\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n)\n\nvar (\n\tappStyle          = lipgloss.NewStyle().Padding(1, 2) //nolint:mnd\n\tinfoPositionStyle = lipgloss.NewStyle().Padding(0, 1).BorderStyle(lipgloss.HiddenBorder())\n\tinfoLineStyle     = lipgloss.NewStyle().Foreground(lipgloss.Color(\"#1D252\"))\n\tinfoHelp          = lipgloss.NewStyle().Padding(2, 0, 0, 2) //nolint:mnd\n)\n\n// View is the main view, which just calls the appropriate sub-view and returns a string representation of the TUI\n// based on the application's state.\nfunc (m Model) View() string { //nolint:gocritic\n\tvar s string\n\n\tswitch m.State {\n\tcase ListState:\n\t\ts = m.listView()\n\tcase PagerState:\n\t\ts = m.pagerView()\n\tcase ScaffoldState:\n\tdefault:\n\t\ts = \"\"\n\t}\n\n\treturn s\n}\n\nfunc (m Model) listView() string { //nolint:gocritic\n\treturn m.List.View()\n}\n\nfunc (m Model) pagerView() string { //nolint:gocritic\n\treturn lipgloss.JoinVertical(lipgloss.Left, m.viewport.View(), m.footerView())\n}\n\nfunc (m Model) footerView() string { //nolint:gocritic\n\tvar percent float64 = 100\n\n\tinfo := infoPositionStyle.Render(fmt.Sprintf(\"%2.f%%\", m.viewport.ScrollPercent()*percent))\n\n\tline := strings.Repeat(\"─\", max(0, m.viewport.Width-lipgloss.Width(info)))\n\tline = infoLineStyle.Render(line)\n\n\tinfo = lipgloss.JoinHorizontal(lipgloss.Center, line, info)\n\n\t// button bar and key help\n\tpagerKeys := infoHelp.Render(lipgloss.JoinVertical(lipgloss.Left, m.buttonBar.View(), \"\\n\", m.pagerKeys.help.View(m.pagerKeys)))\n\n\treturn lipgloss.JoinVertical(lipgloss.Left, info, pagerKeys)\n}\n"
  },
  {
    "path": "internal/cli/commands/commands.go",
    "content": "// Package commands represents CLI commands.\npackage commands\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"golang.org/x/sync/errgroup\"\n\n\t\"github.com/gruntwork-io/go-commons/env\"\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/providercache\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tfimpl\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\n\tawsproviderpatch \"github.com/gruntwork-io/terragrunt/internal/cli/commands/aws-provider-patch\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/backend\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/catalog\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/dag\"\n\texeccmd \"github.com/gruntwork-io/terragrunt/internal/cli/commands/exec\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/find\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/hcl\"\n\thelpcmd \"github.com/gruntwork-io/terragrunt/internal/cli/commands/help\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/info\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/list\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/render\"\n\truncmd \"github.com/gruntwork-io/terragrunt/internal/cli/commands/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/scaffold\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/stack\"\n\tversioncmd \"github.com/gruntwork-io/terragrunt/internal/cli/commands/version\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\t\"github.com/gruntwork-io/terragrunt/internal/os/exec\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tips\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\"\n\t\"github.com/hashicorp/go-version\"\n)\n\n// Command category names.\nconst (\n\t// MainCommandsCategoryName represents primary Terragrunt operations like run, exec.\n\tMainCommandsCategoryName = \"Main commands\"\n\t// CatalogCommandsCategoryName represents commands for managing Terragrunt catalogs.\n\tCatalogCommandsCategoryName = \"Catalog commands\"\n\t// DiscoveryCommandsCategoryName represents commands for discovering Terragrunt configurations.\n\tDiscoveryCommandsCategoryName = \"Discovery commands\"\n\t// ConfigurationCommandsCategoryName represents commands for managing Terragrunt configurations.\n\tConfigurationCommandsCategoryName = \"Configuration commands\"\n\t// ShortcutsCommandsCategoryName represents OpenTofu-specific shortcut commands.\n\tShortcutsCommandsCategoryName = \"OpenTofu shortcuts\"\n)\n\n// New returns the set of Terragrunt commands, grouped into categories.\n// Categories are ordered in increments of 10 for easy insertion of new categories.\nfunc New(l log.Logger, opts *options.TerragruntOptions) clihelper.Commands {\n\tmainCommands := clihelper.Commands{\n\t\truncmd.NewCommand(l, opts),  // run\n\t\tstack.NewCommand(l, opts),   // stack\n\t\texeccmd.NewCommand(l, opts), // exec\n\t\tbackend.NewCommand(l, opts), // backend\n\t}.SetCategory(\n\t\t&clihelper.Category{\n\t\t\tName:  MainCommandsCategoryName,\n\t\t\tOrder: 10, //nolint: mnd\n\t\t},\n\t)\n\n\tcatalogCommands := clihelper.Commands{\n\t\tcatalog.NewCommand(l, opts),  // catalog\n\t\tscaffold.NewCommand(l, opts), // scaffold\n\t}.SetCategory(\n\t\t&clihelper.Category{\n\t\t\tName:  CatalogCommandsCategoryName,\n\t\t\tOrder: 20, //nolint: mnd\n\t\t},\n\t)\n\n\tdiscoveryCommands := clihelper.Commands{\n\t\tfind.NewCommand(l, opts), // find\n\t\tlist.NewCommand(l, opts), // list\n\t}.SetCategory(\n\t\t&clihelper.Category{\n\t\t\tName:  DiscoveryCommandsCategoryName,\n\t\t\tOrder: 30, //nolint: mnd\n\t\t},\n\t)\n\n\tconfigurationCommands := clihelper.Commands{\n\t\thcl.NewCommand(l, opts),              // hcl\n\t\tinfo.NewCommand(l, opts),             // info\n\t\tdag.NewCommand(l, opts),              // dag\n\t\trender.NewCommand(l, opts),           // render\n\t\thelpcmd.NewCommand(l, opts),          // help (hidden)\n\t\tversioncmd.NewCommand(),              // version (hidden)\n\t\tawsproviderpatch.NewCommand(l, opts), // aws-provider-patch (hidden)\n\t}.SetCategory(\n\t\t&clihelper.Category{\n\t\t\tName:  ConfigurationCommandsCategoryName,\n\t\t\tOrder: 40, //nolint: mnd\n\t\t},\n\t)\n\n\tshortcutsCommands := NewShortcutsCommands(l, opts).SetCategory(\n\t\t&clihelper.Category{\n\t\t\tName:  ShortcutsCommandsCategoryName,\n\t\t\tOrder: 50, //nolint: mnd\n\t\t},\n\t)\n\n\tallCommands := mainCommands.\n\t\tMerge(catalogCommands...).\n\t\tMerge(discoveryCommands...).\n\t\tMerge(configurationCommands...).\n\t\tMerge(shortcutsCommands...)\n\n\treturn allCommands\n}\n\n// WrapWithTelemetry wraps CLI command execution with setting of telemetry context and labels, if telemetry is disabled, just runAction the command.\nfunc WrapWithTelemetry(l log.Logger, opts *options.TerragruntOptions) func(ctx context.Context, cliCtx *clihelper.Context, action clihelper.ActionFunc) error {\n\treturn func(ctx context.Context, cliCtx *clihelper.Context, action clihelper.ActionFunc) error {\n\t\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, fmt.Sprintf(\"%s %s\", cliCtx.Command.Name, opts.TerraformCommand), map[string]any{\n\t\t\t\"terraformCommand\": opts.TerraformCommand,\n\t\t\t\"args\":             opts.TerraformCliArgs,\n\t\t\t\"dir\":              opts.WorkingDir,\n\t\t}, func(childCtx context.Context) error {\n\t\t\tif err := initialSetup(cliCtx, l, opts); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := runAction(childCtx, cliCtx, l, opts, action); err != nil {\n\t\t\t\topts.Tips.Find(tips.DebuggingDocs).Evaluate(l)\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n}\n\nfunc runAction(ctx context.Context, cliCtx *clihelper.Context, l log.Logger, opts *options.TerragruntOptions, action clihelper.ActionFunc) error {\n\tctx, cancel := context.WithCancel(ctx)\n\tdefer cancel()\n\n\terrGroup, ctx := errgroup.WithContext(ctx)\n\n\t// Set up automatic provider caching if enabled\n\tif !opts.NoAutoProviderCacheDir {\n\t\tif err := setupAutoProviderCacheDir(ctx, l, opts); err != nil {\n\t\t\tl.Debugf(\"Auto provider cache dir setup failed: %v\", err)\n\t\t}\n\t}\n\n\t// Re-enable VT processing after subprocess execution may have reset console mode.\n\t// Defense-in-depth on top of RunCommandWithOutput's own save/restore cycle.\n\tif !exec.PrepareConsole(l) {\n\t\tl.Formatter().SetDisabledColors(true)\n\t}\n\n\t// actionCtx is the context passed to the action, which may be wrapped with hooks\n\tactionCtx := ctx\n\n\t// Run provider cache server\n\tif opts.ProviderCacheOptions.Enabled {\n\t\tserver, err := providercache.InitServer(l, &opts.ProviderCacheOptions, opts.RootWorkingDir)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tln, err := server.Listen(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer ln.Close() //nolint:errcheck\n\n\t\tactionCtx = tf.ContextWithTerraformCommandHook(ctx, server.TerraformCommandHook)\n\n\t\terrGroup.Go(func() error {\n\t\t\treturn server.Run(ctx, ln)\n\t\t})\n\t}\n\n\t// Run command action\n\terrGroup.Go(func() error {\n\t\tdefer cancel()\n\n\t\tif action != nil {\n\t\t\treturn action(actionCtx, cliCtx)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\treturn errGroup.Wait()\n}\n\nconst minTofuVersionForAutoProviderCacheDir = \"1.10.0\"\n\n// setupAutoProviderCacheDir configures native provider caching by setting TF_PLUGIN_CACHE_DIR.\n//\n// Only works with OpenTofu version >= 1.10. Returns error if conditions aren't met.\nfunc setupAutoProviderCacheDir(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\t// Set TF_PLUGIN_CACHE_DIR environment variable\n\tif opts.Env[tf.EnvNameTFPluginCacheDir] != \"\" {\n\t\tl.Debugf(\n\t\t\t\"TF_PLUGIN_CACHE_DIR already set to %s, skipping auto provider cache dir\",\n\t\t\topts.Env[tf.EnvNameTFPluginCacheDir],\n\t\t)\n\n\t\treturn nil\n\t}\n\n\tif opts.TerraformVersion == nil {\n\t\t_, ver, impl, err := run.PopulateTFVersion(ctx, l, opts.WorkingDir, opts.VersionManagerFileName, configbridge.TFRunOptsFromOpts(opts))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\topts.TerraformVersion = ver\n\t\topts.TofuImplementation = impl\n\t}\n\n\tterraformVersion := opts.TerraformVersion\n\ttfImplementation := opts.TofuImplementation\n\n\t// Check if OpenTofu is being used\n\tif tfImplementation != tfimpl.OpenTofu {\n\t\treturn errors.Errorf(\"auto provider cache dir requires OpenTofu, but detected %s\", tfImplementation)\n\t}\n\n\t// Check OpenTofu version > 1.10\n\tif terraformVersion == nil {\n\t\treturn errors.New(\"cannot determine OpenTofu version\")\n\t}\n\n\trequiredVersion, err := version.NewVersion(minTofuVersionForAutoProviderCacheDir)\n\tif err != nil {\n\t\treturn errors.Errorf(\"failed to parse required version: %w\", err)\n\t}\n\n\tif terraformVersion.LessThan(requiredVersion) {\n\t\treturn errors.Errorf(\"auto provider cache dir requires OpenTofu version >= 1.10, but found %s\", terraformVersion)\n\t}\n\n\t// Set up the provider cache directory\n\tproviderCacheDir := opts.ProviderCacheOptions.Dir\n\tif providerCacheDir == \"\" {\n\t\tcacheDir, err := util.GetCacheDir()\n\t\tif err != nil {\n\t\t\treturn errors.Errorf(\"failed to get cache directory: %w\", err)\n\t\t}\n\n\t\tproviderCacheDir = filepath.Join(cacheDir, \"providers\")\n\t}\n\n\t// Make sure the cache directory is absolute\n\tif !filepath.IsAbs(providerCacheDir) {\n\t\tproviderCacheDir = filepath.Join(opts.RootWorkingDir, providerCacheDir)\n\t}\n\n\tproviderCacheDir = filepath.Clean(providerCacheDir)\n\n\tconst cacheDirMode = 0755\n\n\t// Create the cache directory if it doesn't exist\n\tif err := os.MkdirAll(providerCacheDir, cacheDirMode); err != nil {\n\t\treturn errors.Errorf(\"failed to create provider cache directory: %w\", err)\n\t}\n\n\t// Initialize environment variables map if it's nil\n\tif opts.Env == nil {\n\t\topts.Env = make(map[string]string)\n\t}\n\n\topts.Env[tf.EnvNameTFPluginCacheDir] = providerCacheDir\n\n\tl.Debugf(\"Auto provider cache dir enabled: TF_PLUGIN_CACHE_DIR=%s\", providerCacheDir)\n\n\treturn nil\n}\n\n// mostly preparing terragrunt options\nfunc initialSetup(cliCtx *clihelper.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\t// convert the rest flags (intended for terraform) to one dash, e.g. `--input=true` to `-input=true`\n\targs := cliCtx.Args().WithoutBuiltinCmdSep().Normalize(clihelper.SingleDashFlag)\n\tcmdName := cliCtx.Command.Name\n\n\tif cmdName == runcmd.CommandName {\n\t\tcmdName = args.CommandName()\n\t} else {\n\t\targs = append([]string{cmdName}, args...)\n\t}\n\n\t// `terraform apply -destroy` is an alias for `terraform destroy`.\n\t// It is important to resolve the alias because the `run --all` relies on terraform command to determine the order, for `destroy` command is used the reverse order.\n\tif cmdName == tf.CommandNameApply && slices.Contains(args, tf.FlagNameDestroy) {\n\t\tcmdName = tf.CommandNameDestroy\n\t\targs = append([]string{tf.CommandNameDestroy}, args.Tail()...)\n\t\targs = slices.DeleteFunc(args, func(arg string) bool { return arg == tf.FlagNameDestroy })\n\t}\n\n\t// Since Terragrunt and Terraform have the same `-no-color` flag,\n\t// if a user specifies `-no-color` for Terragrunt, we should propagate it to Terraform as well.\n\tif l.Formatter().DisabledColors() {\n\t\targs = append(args, tf.FlagNameNoColor)\n\t}\n\n\topts.TerraformCommand = cmdName\n\topts.TerraformCliArgs = iacargs.New(args...)\n\n\topts.Env = env.Parse(os.Environ())\n\n\t// --- Working Dir\n\tif opts.WorkingDir == \"\" {\n\t\tcurrentDir, err := os.Getwd()\n\t\tif err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\topts.WorkingDir = currentDir\n\t} else if !filepath.IsAbs(opts.WorkingDir) {\n\t\tworkingDir, err := filepath.Abs(opts.WorkingDir)\n\t\tif err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\topts.WorkingDir = workingDir\n\t}\n\n\topts.WorkingDir = filepath.Clean(opts.WorkingDir)\n\n\tl = l.WithField(placeholders.WorkDirKeyName, opts.WorkingDir)\n\n\topts.RootWorkingDir = opts.WorkingDir\n\n\tif err := l.Formatter().SetBaseDir(opts.RootWorkingDir); err != nil {\n\t\treturn err\n\t}\n\n\tif opts.Writers.LogShowAbsPaths {\n\t\tl.Formatter().DisableRelativePaths()\n\t}\n\n\t// --- Download Dir\n\tif opts.DownloadDir == \"\" {\n\t\topts.DownloadDir = filepath.Join(opts.WorkingDir, util.TerragruntCacheDir)\n\t} else if !filepath.IsAbs(opts.DownloadDir) {\n\t\topts.DownloadDir = filepath.Join(opts.RootWorkingDir, opts.DownloadDir)\n\t}\n\n\topts.DownloadDir = filepath.Clean(opts.DownloadDir)\n\n\t// --- Terragrunt ConfigPath\n\tif opts.TerragruntConfigPath == \"\" {\n\t\topts.TerragruntConfigPath = config.GetDefaultConfigPath(opts.WorkingDir)\n\t} else if !filepath.IsAbs(opts.TerragruntConfigPath) &&\n\t\t(cliCtx.Command.Name == runcmd.CommandName || slices.Contains(tf.CommandNames, cliCtx.Command.Name)) {\n\t\topts.TerragruntConfigPath = filepath.Join(opts.WorkingDir, opts.TerragruntConfigPath)\n\t}\n\n\topts.TerragruntConfigPath = filepath.Clean(opts.TerragruntConfigPath)\n\n\tif !filepath.IsAbs(opts.TFPath) && strings.Contains(opts.TFPath, string(filepath.Separator)) {\n\t\topts.TFPath = filepath.Join(opts.WorkingDir, opts.TFPath)\n\t}\n\n\tvar fileFilterStrings []string\n\n\texcludeFiltersFromFile, err := util.ExcludeFiltersFromFile(opts.WorkingDir, opts.ExcludesFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfileFilterStrings = append(fileFilterStrings, excludeFiltersFromFile...)\n\n\t// Process filters file if the filters file is not disabled\n\tif !opts.NoFiltersFile {\n\t\tfiltersFromFile, filtersFromFileErr := util.GetFiltersFromFile(opts.WorkingDir, opts.FiltersFile)\n\t\tif filtersFromFileErr != nil {\n\t\t\treturn filtersFromFileErr\n\t\t}\n\n\t\tfileFilterStrings = append(fileFilterStrings, filtersFromFile...)\n\t}\n\n\tif len(fileFilterStrings) > 0 {\n\t\tparsed, parseErr := filter.ParseFilterQueries(l, fileFilterStrings)\n\t\tif parseErr != nil {\n\t\t\treturn parseErr\n\t\t}\n\n\t\topts.Filters = append(opts.Filters, parsed...)\n\t}\n\n\t// Deduplicate filters by their string representation\n\tseen := make(map[string]struct{}, len(opts.Filters))\n\tdeduped := make(filter.Filters, 0, len(opts.Filters))\n\n\tfor _, f := range opts.Filters {\n\t\tkey := f.String()\n\t\tif _, ok := seen[key]; ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tseen[key] = struct{}{}\n\n\t\tdeduped = append(deduped, f)\n\t}\n\n\topts.Filters = deduped\n\n\t// --- Terragrunt Version\n\tterragruntVersion, err := version.NewVersion(cliCtx.Version)\n\tif err != nil {\n\t\t// Malformed Terragrunt version; set the version to 0.0\n\t\tif terragruntVersion, err = version.NewVersion(\"0.0\"); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\t}\n\n\topts.TerragruntVersion = terragruntVersion\n\t// Log the terragrunt version in debug mode. This helps with debugging issues and ensuring a specific version of terragrunt used.\n\tl.Debugf(\"Terragrunt Version: %s\", opts.TerragruntVersion)\n\n\t// --- Others\n\tif !opts.RunAllAutoApprove {\n\t\t// When running in no-auto-approve mode, set parallelism to 1 so that interactive prompts work.\n\t\topts.Parallelism = 1\n\t}\n\n\topts.OriginalTerragruntConfigPath = opts.TerragruntConfigPath\n\topts.OriginalTerraformCommand = opts.TerraformCommand\n\topts.OriginalIAMRoleOptions = opts.IAMRoleOptions\n\n\tif !exec.PrepareConsole(l) {\n\t\tl.Debugf(\"Virtual terminal processing not available, disabling colors\")\n\t\tl.Formatter().SetDisabledColors(true)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/cli/commands/dag/cli.go",
    "content": "// Package dag implements the dag command to interact with the Directed Acyclic Graph (DAG).\n// It provides functionality to visualize and analyze dependencies between Terragrunt configurations.\npackage dag\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/dag/graph\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tCommandName = \"dag\"\n)\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\treturn &clihelper.Command{\n\t\tName:  CommandName,\n\t\tUsage: \"Interact with the Directed Acyclic Graph (DAG).\",\n\t\tSubcommands: clihelper.Commands{\n\t\t\tgraph.NewCommand(l, opts),\n\t\t},\n\t\tAction: clihelper.ShowCommandHelp,\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/dag/graph/cli.go",
    "content": "// Package graph implements the terragrunt dag graph command which generates a visual\n// representation of the Terragrunt dependency graph in DOT language format.\n//\n// Alias for 'list --format=dot --dag --dependencies --external'.\npackage graph\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/list\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tCommandName = \"graph\"\n)\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\tsharedFlags := shared.NewQueueFlags(opts, nil)\n\tsharedFlags = append(sharedFlags, shared.NewBackendFlags(opts, nil)...)\n\tsharedFlags = append(sharedFlags, shared.NewFeatureFlags(opts, nil)...)\n\tsharedFlags = append(sharedFlags, shared.NewFilterFlags(l, opts)...)\n\n\treturn &clihelper.Command{\n\t\tName:      CommandName,\n\t\tUsage:     \"Graph the Directed Acyclic Graph (DAG) in DOT language. Alias for 'list --format=dot --dag --dependencies --external'.\",\n\t\tUsageText: \"terragrunt dag graph\",\n\t\tFlags:     sharedFlags,\n\t\tAction: func(ctx context.Context, _ *clihelper.Context) error {\n\t\t\treturn Run(ctx, l, opts)\n\t\t},\n\t}\n}\n\nfunc Run(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\tlistOpts := list.NewOptions(opts)\n\tlistOpts.Format = list.FormatDot\n\tlistOpts.Mode = list.ModeDAG\n\tlistOpts.Dependencies = true\n\n\treturn list.Run(ctx, l, listOpts)\n}\n"
  },
  {
    "path": "internal/cli/commands/dag/graph/cli_test.go",
    "content": "package graph_test\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/dag/graph\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Run a benchmark on runGraphDependencies for all fixtures possible.\n// This should reveal regression on execution time due to new, changed or removed features.\nfunc BenchmarkRunGraphDependencies(b *testing.B) {\n\t// Setup\n\tb.StopTimer()\n\n\ttestDir := \"../../../../../test/fixtures\"\n\n\tfixtureDirs := []struct {\n\t\tdescription          string\n\t\tworkingDir           string\n\t\tusePartialParseCache bool\n\t}{\n\t\t{\"PartialParseBenchmarkRegressionCaching\", \"regressions/benchmark-parsing/production/deployment-group-1/webserver/terragrunt.hcl\", true},\n\t\t{\"PartialParseBenchmarkRegressionNoCache\", \"regressions/benchmark-parsing/production/deployment-group-1/webserver/terragrunt.hcl\", false},\n\t\t{\"PartialParseBenchmarkRegressionIncludesCaching\", \"regressions/benchmark-parsing-includes/production/deployment-group-1/webserver/terragrunt.hcl\", true},\n\t\t{\"PartialParseBenchmarkRegressionIncludesNoCache\", \"regressions/benchmark-parsing-includes/production/deployment-group-1/webserver/terragrunt.hcl\", false},\n\t}\n\n\t// Run benchmarks\n\tfor _, fixture := range fixtureDirs {\n\t\tb.Run(fixture.description, func(b *testing.B) {\n\t\t\tworkingDir, err := filepath.Abs(filepath.Join(testDir, fixture.workingDir))\n\t\t\trequire.NoError(b, err)\n\n\t\t\tterragruntOptions, err := options.NewTerragruntOptionsForTest(workingDir)\n\t\t\tif fixture.usePartialParseCache {\n\t\t\t\tterragruntOptions.UsePartialParseConfigCache = true\n\t\t\t} else {\n\t\t\t\tterragruntOptions.UsePartialParseConfigCache = false\n\t\t\t}\n\n\t\t\trequire.NoError(b, err)\n\n\t\t\tb.ResetTimer()\n\t\t\tb.StartTimer()\n\n\t\t\terr = graph.Run(b.Context(), logger.CreateLogger(), terragruntOptions)\n\n\t\t\tb.StopTimer()\n\t\t\trequire.NoError(b, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/exec/cli.go",
    "content": "// Package exec provides the ability to execute a command using Terragrunt,\n// via the `terragrunt exec -- command_name` command.\npackage exec\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tCommandName = \"exec\"\n\n\tInDownloadDirFlagName = \"in-download-dir\"\n)\n\nfunc NewFlags(l log.Logger, opts *options.TerragruntOptions, cmdOpts *Options, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\n\tsharedFlags := append(\n\t\tclihelper.Flags{\n\t\t\tshared.NewConfigFlag(opts, prefix, CommandName),\n\t\t\tshared.NewDownloadDirFlag(opts, prefix, CommandName),\n\t\t\tshared.NewTFPathFlag(opts),\n\t\t\tshared.NewAuthProviderCmdFlag(opts, prefix, CommandName),\n\t\t\tshared.NewInputsDebugFlag(opts, prefix, CommandName),\n\t\t},\n\t\tshared.NewIAMAssumeRoleFlags(opts, prefix, CommandName)...,\n\t)\n\n\treturn append(sharedFlags,\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        InDownloadDirFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(InDownloadDirFlagName),\n\t\t\tDestination: &cmdOpts.InDownloadDir,\n\t\t\tUsage:       \"Run the provided command in the download directory.\",\n\t\t}),\n\t)\n}\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\tcmdOpts := NewOptions()\n\n\treturn &clihelper.Command{\n\t\tName:        CommandName,\n\t\tUsage:       \"Execute an arbitrary command.\",\n\t\tUsageText:   \"terragrunt exec [options] -- <command>\",\n\t\tDescription: \"Execute a command using Terragrunt.\",\n\t\tExamples: []string{\n\t\t\t\"# Utilize the AWS CLI.\\nterragrunt exec -- aws s3 ls\",\n\t\t\t\"# Inspect `main.tf` file of module for Unit\\nterragrunt exec --in-download-dir -- cat main.tf\",\n\t\t},\n\t\tFlags: NewFlags(l, opts, cmdOpts, nil),\n\t\tAction: func(ctx context.Context, cliCtx *clihelper.Context) error {\n\t\t\ttgArgs, cmdArgs := cliCtx.Args().Split(clihelper.BuiltinCmdSep)\n\n\t\t\t// Use unspecified arguments from the terragrunt command if the user\n\t\t\t// specified the target command without `--`, e.g. `terragrunt exec ls`.\n\t\t\tif len(cmdArgs) == 0 {\n\t\t\t\tcmdArgs = tgArgs\n\t\t\t}\n\n\t\t\tif len(cmdArgs) == 0 {\n\t\t\t\treturn clihelper.ShowCommandHelp(ctx, cliCtx)\n\t\t\t}\n\n\t\t\treturn Run(ctx, l, opts, cmdOpts, cmdArgs)\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/exec/exec.go",
    "content": "package exec\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/prepare\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runcfg\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nfunc Run(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, cmdOpts *Options, args clihelper.Args) error {\n\tprepared, err := prepare.PrepareConfig(ctx, l, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tr := report.NewReport()\n\n\t// Download source\n\tupdatedOpts, err := prepare.PrepareSource(ctx, l, prepared.Opts, prepared.Cfg, r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trunCfg := prepared.Cfg.ToRunConfig(l)\n\n\t// Generate config\n\tif err := prepare.PrepareGenerate(l, updatedOpts, runCfg); err != nil {\n\t\treturn err\n\t}\n\n\tif cmdOpts.InDownloadDir {\n\t\t// Run terraform init\n\t\tif err := prepare.PrepareInit(ctx, l, opts, updatedOpts, runCfg, r); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\t// Just set inputs as env vars, skip init\n\t\tupdatedOpts.AutoInit = false\n\n\t\tif err := prepare.PrepareInputsAsEnvVars(l, updatedOpts, runCfg); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn runTargetCommand(ctx, l, updatedOpts, runCfg, r, cmdOpts, args)\n}\n\nfunc runTargetCommand(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tcfg *runcfg.RunConfig,\n\tr *report.Report,\n\tcmdOpts *Options,\n\targs clihelper.Args,\n) error {\n\tvar (\n\t\tcommand = args.CommandName()\n\t\tcmdArgs = args.Tail()\n\t\tdir     = opts.WorkingDir\n\t)\n\n\tif !cmdOpts.InDownloadDir {\n\t\tdir = opts.RootWorkingDir\n\t}\n\n\trunOpts := configbridge.NewRunOptions(opts)\n\n\treturn run.RunActionWithHooks(ctx, l, command, runOpts, cfg, r, func(ctx context.Context) error {\n\t\t_, err := shell.RunCommandWithOutput(ctx, l, configbridge.ShellRunOptsFromOpts(opts), dir, false, false, command, cmdArgs...)\n\t\tif err != nil {\n\t\t\treturn errors.Errorf(\"failed to run command in directory %s: %w\", dir, err)\n\t\t}\n\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "internal/cli/commands/exec/options.go",
    "content": "package exec\n\ntype Options struct {\n\t// InDownloadDir determines whether the command should execute in the download directory\n\t// rather than the working directory.\n\tInDownloadDir bool\n}\n\nfunc NewOptions() *Options {\n\treturn &Options{}\n}\n"
  },
  {
    "path": "internal/cli/commands/find/cli.go",
    "content": "// Package find provides the ability to find Terragrunt configurations in your codebase\n// via the `terragrunt find` command.\npackage find\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tCommandName  = \"find\"\n\tCommandAlias = \"fd\"\n\n\tFormatFlagName = \"format\"\n\n\tJSONFlagName  = \"json\"\n\tJSONFlagAlias = \"j\"\n\n\tDAGFlagName = \"dag\"\n\n\tHiddenFlagName   = \"hidden\"\n\tNoHiddenFlagName = \"no-hidden\"\n\tDependencies     = \"dependencies\"\n\tExternal         = \"external\"\n\tExclude          = \"exclude\"\n\tInclude          = \"include\"\n\tReading          = \"reading\"\n\n\tQueueConstructAsFlagName  = \"queue-construct-as\"\n\tQueueConstructAsFlagAlias = \"as\"\n)\n\nfunc NewFlags(l log.Logger, opts *Options, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\n\tflags := clihelper.Flags{\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        FormatFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(FormatFlagName),\n\t\t\tDestination: &opts.Format,\n\t\t\tUsage:       \"Output format for find results. Valid values: text, json.\",\n\t\t\tDefaultText: FormatText,\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        JSONFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(JSONFlagName),\n\t\t\tAliases:     []string{JSONFlagAlias},\n\t\t\tDestination: &opts.JSON,\n\t\t\tUsage:       \"Output in JSON format (equivalent to --format=json).\",\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        DAGFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(DAGFlagName),\n\t\t\tDestination: &opts.DAG,\n\t\t\tUsage:       \"Use DAG mode to sort and group output.\",\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        NoHiddenFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(NoHiddenFlagName),\n\t\t\tDestination: &opts.NoHidden,\n\t\t\tUsage:       \"Exclude hidden directories from find results.\",\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:    HiddenFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(HiddenFlagName),\n\t\t\tUsage:   \"Include hidden directories in find results.\",\n\t\t\tHidden:  true,\n\t\t\tAction: func(ctx context.Context, _ *clihelper.Context, value bool) error {\n\t\t\t\tif value {\n\t\t\t\t\tif err := opts.StrictControls.FilterByNames(controls.DeprecatedHiddenFlag).Evaluate(ctx); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        Dependencies,\n\t\t\tEnvVars:     tgPrefix.EnvVars(Dependencies),\n\t\t\tDestination: &opts.Dependencies,\n\t\t\tUsage:       \"Include dependencies in the results (only when using --format=json).\",\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        Exclude,\n\t\t\tEnvVars:     tgPrefix.EnvVars(Exclude),\n\t\t\tDestination: &opts.Exclude,\n\t\t\tUsage:       \"Display exclude configurations in the results (only when using --format=json).\",\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        Include,\n\t\t\tEnvVars:     tgPrefix.EnvVars(Include),\n\t\t\tDestination: &opts.Include,\n\t\t\tUsage:       \"Display include configurations in the results (only when using --format=json).\",\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        Reading,\n\t\t\tEnvVars:     tgPrefix.EnvVars(Reading),\n\t\t\tDestination: &opts.Reading,\n\t\t\tUsage:       \"Include the list of files that are read by components in the results (only when using --format=json).\",\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:    External,\n\t\t\tEnvVars: tgPrefix.EnvVars(External),\n\t\t\tHidden:  true,\n\t\t\tUsage:   \"Discover external dependencies from initial results, and add them to top-level results (implies discovery of dependencies).\",\n\t\t\tAction: func(_ context.Context, _ *clihelper.Context, value bool) error {\n\t\t\t\tif !value {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tpathExpr, err := filter.NewPathFilter(\"./**\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tgraphExpr := filter.NewGraphExpression(pathExpr).WithDependencies()\n\t\t\t\topts.Filters = append(opts.Filters, filter.NewFilter(graphExpr, graphExpr.String()))\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}),\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        QueueConstructAsFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(QueueConstructAsFlagName),\n\t\t\tDestination: &opts.QueueConstructAs,\n\t\t\tUsage:       \"Construct the queue as if a specific command was run.\",\n\t\t\tAliases:     []string{QueueConstructAsFlagAlias},\n\t\t}),\n\t}\n\n\treturn append(flags, shared.NewFilterFlags(l, opts.TerragruntOptions)...)\n}\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\tcmdOpts := NewOptions(opts)\n\n\t// Base flags for find plus backend/feature flags\n\tflags := NewFlags(l, cmdOpts, nil)\n\tflags = append(flags, shared.NewBackendFlags(opts, nil)...)\n\tflags = append(flags, shared.NewFeatureFlags(opts, nil)...)\n\n\treturn &clihelper.Command{\n\t\tName:    CommandName,\n\t\tAliases: []string{CommandAlias},\n\t\tUsage:   \"Find relevant Terragrunt configurations.\",\n\t\tFlags:   flags,\n\t\tBefore: func(_ context.Context, _ *clihelper.Context) error {\n\t\t\tif cmdOpts.JSON {\n\t\t\t\tcmdOpts.Format = FormatJSON\n\t\t\t}\n\n\t\t\tif cmdOpts.DAG {\n\t\t\t\tcmdOpts.Mode = ModeDAG\n\t\t\t}\n\n\t\t\t// Requesting a specific command to be used for queue construction\n\t\t\t// implies DAG mode.\n\t\t\tif cmdOpts.QueueConstructAs != \"\" {\n\t\t\t\tcmdOpts.Mode = ModeDAG\n\t\t\t}\n\n\t\t\tif err := cmdOpts.Validate(); err != nil {\n\t\t\t\treturn clihelper.NewExitError(err, clihelper.ExitCodeGeneralError)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t\tAction: func(ctx context.Context, _ *clihelper.Context) error {\n\t\t\treturn Run(ctx, l, cmdOpts)\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/find/find.go",
    "content": "package find\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/os/stdout\"\n\t\"github.com/gruntwork-io/terragrunt/internal/queue\"\n\t\"github.com/gruntwork-io/terragrunt/internal/worktrees\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/mgutz/ansi\"\n)\n\n// Run runs the find command.\nfunc Run(ctx context.Context, l log.Logger, opts *Options) error {\n\td, err := discovery.NewForDiscoveryCommand(l, &discovery.DiscoveryCommandOptions{\n\t\tWorkingDir:        opts.WorkingDir,\n\t\tQueueConstructAs:  opts.QueueConstructAs,\n\t\tNoHidden:          opts.NoHidden,\n\t\tWithRequiresParse: opts.Dependencies || opts.Mode == ModeDAG,\n\t\tWithRelationships: opts.Dependencies || opts.Mode == ModeDAG,\n\t\tExclude:           opts.Exclude,\n\t\tInclude:           opts.Include,\n\t\tReading:           opts.Reading,\n\t\tFilters:           opts.Filters,\n\t\tExperiments:       opts.Experiments,\n\t})\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t// We do worktree generation here instead of in the discovery constructor\n\t// so that we can defer cleanup in the same context.\n\tgitFilters := opts.Filters.UniqueGitFilters()\n\n\tworktrees, worktreeErr := worktrees.NewWorktrees(ctx, l, opts.WorkingDir, gitFilters)\n\tif worktreeErr != nil {\n\t\treturn errors.Errorf(\"failed to create worktrees: %w\", worktreeErr)\n\t}\n\n\tdefer func() {\n\t\tcleanupErr := worktrees.Cleanup(ctx, l)\n\t\tif cleanupErr != nil {\n\t\t\tl.Errorf(\"failed to cleanup worktrees: %v\", cleanupErr)\n\t\t}\n\t}()\n\n\td = d.WithWorktrees(worktrees)\n\n\tvar (\n\t\tcomponents  component.Components\n\t\tdiscoverErr error\n\t)\n\n\ttelemetryErr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"find_discover\", map[string]any{\n\t\t\"working_dir\":  opts.WorkingDir,\n\t\t\"no_hidden\":    opts.NoHidden,\n\t\t\"dependencies\": opts.Dependencies,\n\t\t\"mode\":         opts.Mode,\n\t\t\"exclude\":      opts.Exclude,\n\t}, func(ctx context.Context) error {\n\t\tcomponents, discoverErr = d.Discover(ctx, l, opts.TerragruntOptions)\n\t\treturn discoverErr\n\t})\n\tif telemetryErr != nil {\n\t\tl.Debugf(\"Errors encountered while discovering components:\\n%s\", telemetryErr)\n\t}\n\n\tswitch opts.Mode {\n\tcase ModeNormal:\n\t\tcomponents = components.Sort()\n\tcase ModeDAG:\n\t\terr = telemetry.TelemeterFromContext(ctx).Collect(ctx, \"find_mode_dag\", map[string]any{\n\t\t\t\"working_dir\":  opts.WorkingDir,\n\t\t\t\"config_count\": len(components),\n\t\t}, func(ctx context.Context) error {\n\t\t\tq, queueErr := queue.NewQueue(components)\n\t\t\tif queueErr != nil {\n\t\t\t\treturn queueErr\n\t\t\t}\n\n\t\t\tcomponents = q.Components()\n\n\t\t\treturn nil\n\t\t})\n\t\tif err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\tdefault:\n\t\t// This should never happen, because of validation in the command.\n\t\t// If it happens, we want to throw so we can fix the validation.\n\t\treturn errors.New(\"invalid mode: \" + opts.Mode)\n\t}\n\n\tvar foundComponents FoundComponents\n\n\terr = telemetry.TelemeterFromContext(ctx).Collect(ctx, \"find_discovered_to_found\", map[string]any{\n\t\t\"working_dir\":  opts.WorkingDir,\n\t\t\"config_count\": len(components),\n\t}, func(ctx context.Context) error {\n\t\tvar convErr error\n\n\t\tfoundComponents, convErr = discoveredToFound(components, opts)\n\n\t\treturn convErr\n\t})\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tswitch opts.Format {\n\tcase FormatText:\n\t\treturn outputText(l, opts, foundComponents)\n\tcase FormatJSON:\n\t\treturn outputJSON(opts, foundComponents)\n\tdefault:\n\t\t// This should never happen, because of validation in the command.\n\t\t// If it happens, we want to throw so we can fix the validation.\n\t\treturn errors.New(\"invalid format: \" + opts.Format)\n\t}\n}\n\ntype FoundComponents []*FoundComponent\n\ntype FoundComponent struct {\n\tType component.Kind `json:\"type\"`\n\tPath string         `json:\"path\"`\n\n\tExclude *config.ExcludeConfig `json:\"exclude,omitempty\"`\n\tInclude map[string]string     `json:\"include,omitempty\"`\n\n\tDependencies []string `json:\"dependencies,omitempty\"`\n\tReading      []string `json:\"reading,omitempty\"`\n}\n\nfunc discoveredToFound(components component.Components, opts *Options) (FoundComponents, error) {\n\tfoundComponents := make(FoundComponents, 0, len(components))\n\terrs := []error{}\n\n\tfor _, c := range components {\n\t\tif opts.QueueConstructAs != \"\" {\n\t\t\tif unit, ok := c.(*component.Unit); ok {\n\t\t\t\tif cfg := unit.Config(); cfg != nil && cfg.Exclude != nil {\n\t\t\t\t\tif cfg.Exclude.IsActionListed(opts.QueueConstructAs) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tvar (\n\t\t\trelPath string\n\t\t\terr     error\n\t\t)\n\n\t\tif c.DiscoveryContext() != nil && c.DiscoveryContext().WorkingDir != \"\" {\n\t\t\trelPath, err = filepath.Rel(c.DiscoveryContext().WorkingDir, c.Path())\n\t\t} else {\n\t\t\trelPath, err = filepath.Rel(opts.WorkingDir, c.Path())\n\t\t}\n\n\t\tif err != nil {\n\t\t\terrs = append(errs, errors.New(err))\n\n\t\t\tcontinue\n\t\t}\n\n\t\tfoundComponent := &FoundComponent{\n\t\t\tType: c.Kind(),\n\t\t\tPath: relPath,\n\t\t}\n\n\t\tif opts.Exclude {\n\t\t\tif unit, ok := c.(*component.Unit); ok {\n\t\t\t\tif cfg := unit.Config(); cfg != nil && cfg.Exclude != nil {\n\t\t\t\t\tfoundComponent.Exclude = cfg.Exclude.Clone()\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif opts.Include {\n\t\t\tif unit, ok := c.(*component.Unit); ok {\n\t\t\t\tif cfg := unit.Config(); cfg != nil && cfg.ProcessedIncludes != nil {\n\t\t\t\t\tfoundComponent.Include = make(map[string]string, len(cfg.ProcessedIncludes))\n\t\t\t\t\tfor _, v := range cfg.ProcessedIncludes {\n\t\t\t\t\t\tfoundComponent.Include[v.Name], err = util.GetPathRelativeTo(v.Path, opts.RootWorkingDir)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\terrs = append(errs, 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}\n\n\t\tif opts.Reading && len(c.Reading()) > 0 {\n\t\t\tfoundComponent.Reading = make([]string, len(c.Reading()))\n\n\t\t\tfor i, reading := range c.Reading() {\n\t\t\t\tvar relReadingPath string\n\n\t\t\t\tif c.DiscoveryContext() != nil && c.DiscoveryContext().WorkingDir != \"\" {\n\t\t\t\t\trelReadingPath, err = filepath.Rel(c.DiscoveryContext().WorkingDir, reading)\n\t\t\t\t} else {\n\t\t\t\t\trelReadingPath, err = filepath.Rel(opts.WorkingDir, reading)\n\t\t\t\t}\n\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs = append(errs, errors.New(err))\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tfoundComponent.Reading[i] = relReadingPath\n\t\t\t}\n\t\t}\n\n\t\tif opts.Dependencies && len(c.Dependencies()) > 0 {\n\t\t\tfoundComponent.Dependencies = make([]string, len(c.Dependencies()))\n\n\t\t\tfor i, dep := range c.Dependencies() {\n\t\t\t\tvar relDepPath string\n\n\t\t\t\tif dep.DiscoveryContext() != nil && dep.DiscoveryContext().WorkingDir != \"\" {\n\t\t\t\t\trelDepPath, err = filepath.Rel(dep.DiscoveryContext().WorkingDir, dep.Path())\n\t\t\t\t} else {\n\t\t\t\t\trelDepPath, err = filepath.Rel(opts.WorkingDir, dep.Path())\n\t\t\t\t}\n\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs = append(errs, errors.New(err))\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tfoundComponent.Dependencies[i] = relDepPath\n\t\t\t}\n\t\t}\n\n\t\tfoundComponents = append(foundComponents, foundComponent)\n\t}\n\n\treturn foundComponents, errors.Join(errs...)\n}\n\n// outputJSON outputs the discovered components in JSON format.\nfunc outputJSON(opts *Options, components FoundComponents) error {\n\tjsonBytes, err := json.MarshalIndent(components, \"\", \"  \")\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t_, err = opts.Writers.Writer.Write(append(jsonBytes, []byte(\"\\n\")...))\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n\n// Colorizer is a colorizer for the discovered components.\ntype Colorizer struct {\n\tunitColorizer  func(string) string\n\tstackColorizer func(string) string\n\tpathColorizer  func(string) string\n}\n\n// NewColorizer creates a new Colorizer.\nfunc NewColorizer(shouldColor bool) *Colorizer {\n\tif !shouldColor {\n\t\treturn &Colorizer{\n\t\t\tunitColorizer:  func(s string) string { return s },\n\t\t\tstackColorizer: func(s string) string { return s },\n\t\t\tpathColorizer:  func(s string) string { return s },\n\t\t}\n\t}\n\n\treturn &Colorizer{\n\t\tunitColorizer:  ansi.ColorFunc(\"blue+bh\"),\n\t\tstackColorizer: ansi.ColorFunc(\"green+bh\"),\n\t\tpathColorizer:  ansi.ColorFunc(\"white+d\"),\n\t}\n}\n\nfunc (c *Colorizer) Colorize(foundComponent *FoundComponent) string {\n\tpath := foundComponent.Path\n\n\t// Get the directory and base name using filepath\n\tdir, base := filepath.Split(path)\n\n\tif dir == \"\" {\n\t\t// No directory part, color the whole path\n\t\tswitch foundComponent.Type {\n\t\tcase component.UnitKind:\n\t\t\treturn c.unitColorizer(path)\n\t\tcase component.StackKind:\n\t\t\treturn c.stackColorizer(path)\n\t\tdefault:\n\t\t\treturn path\n\t\t}\n\t}\n\n\t// Color the components differently\n\tcoloredPath := c.pathColorizer(dir)\n\n\tswitch foundComponent.Type {\n\tcase component.UnitKind:\n\t\treturn coloredPath + c.unitColorizer(base)\n\tcase component.StackKind:\n\t\treturn coloredPath + c.stackColorizer(base)\n\tdefault:\n\t\treturn path\n\t}\n}\n\n// outputText outputs the discovered components in text format.\nfunc outputText(l log.Logger, opts *Options, components FoundComponents) error {\n\tvar buf strings.Builder\n\n\tcolorizer := NewColorizer(shouldColor(l))\n\n\tfor _, c := range components {\n\t\tbuf.WriteString(colorizer.Colorize(c) + \"\\n\")\n\t}\n\n\t_, err := opts.Writers.Writer.Write([]byte(buf.String()))\n\n\treturn errors.New(err)\n}\n\n// shouldColor returns true if the output should be colored.\nfunc shouldColor(l log.Logger) bool {\n\treturn !l.Formatter().DisabledColors() && !stdout.IsRedirected()\n}\n"
  },
  {
    "path": "internal/cli/commands/find/find_test.go",
    "content": "package find_test\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/find\"\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRun(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tsetup         func(t *testing.T) string\n\t\tvalidate      func(t *testing.T, output string, expectedPaths []string)\n\t\tname          string\n\t\tformat        string\n\t\tmode          string\n\t\texpectedPaths []string\n\t\tnoHidden      bool\n\t\tdependencies  bool\n\t\texternal      bool\n\t\treading       bool\n\t}{\n\t\t{\n\t\t\tname: \"basic discovery\",\n\t\t\tsetup: func(t *testing.T) string {\n\t\t\t\tt.Helper()\n\n\t\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\t\t// Create test directory structure\n\t\t\t\ttestDirs := []string{\n\t\t\t\t\t\"unit1\",\n\t\t\t\t\t\"unit2\",\n\t\t\t\t\t\"stack1\",\n\t\t\t\t\t\".hidden/unit3\",\n\t\t\t\t\t\"nested/unit4\",\n\t\t\t\t}\n\n\t\t\t\tfor _, dir := range testDirs {\n\t\t\t\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\n\t\t\t\t// Create test files\n\t\t\t\ttestFiles := map[string]string{\n\t\t\t\t\t\"unit1/terragrunt.hcl\":         \"\",\n\t\t\t\t\t\"unit2/terragrunt.hcl\":         \"\",\n\t\t\t\t\t\"stack1/terragrunt.stack.hcl\":  \"\",\n\t\t\t\t\t\".hidden/unit3/terragrunt.hcl\": \"\",\n\t\t\t\t\t\"nested/unit4/terragrunt.hcl\":  \"\",\n\t\t\t\t}\n\n\t\t\t\tfor path, content := range testFiles {\n\t\t\t\t\terr := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\n\t\t\t\treturn tmpDir\n\t\t\t},\n\t\t\texpectedPaths: []string{\"unit1\", \"unit2\", \"nested/unit4\", \"stack1\"},\n\t\t\tformat:        \"text\",\n\t\t\tmode:          \"normal\",\n\t\t\tnoHidden:      true,\n\t\t\tdependencies:  false,\n\t\t\texternal:      false,\n\t\t\tvalidate: func(t *testing.T, output string, expectedPaths []string) {\n\t\t\t\tt.Helper()\n\n\t\t\t\t// Split output into lines and trim whitespace\n\t\t\t\tlines := strings.Split(strings.TrimSpace(output), \"\\n\")\n\n\t\t\t\t// Verify we have the expected number of lines\n\t\t\t\tassert.Len(t, lines, len(expectedPaths))\n\n\t\t\t\t// Convert expected paths to use OS-specific path separators\n\t\t\t\tosExpectedPaths := make([]string, 0, len(expectedPaths))\n\t\t\t\tfor _, path := range expectedPaths {\n\t\t\t\t\tosExpectedPaths = append(osExpectedPaths, filepath.FromSlash(path))\n\t\t\t\t}\n\n\t\t\t\t// Convert actual paths to use OS-specific path separators\n\t\t\t\tosPaths := make([]string, 0, len(lines))\n\t\t\t\tfor _, line := range lines {\n\t\t\t\t\tosPaths = append(osPaths, filepath.FromSlash(strings.TrimSpace(line)))\n\t\t\t\t}\n\n\t\t\t\t// Verify all expected paths are present\n\t\t\t\tassert.ElementsMatch(t, osExpectedPaths, osPaths)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"json output format\",\n\t\t\tsetup: func(t *testing.T) string {\n\t\t\t\tt.Helper()\n\n\t\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\t\t// Create test directory structure\n\t\t\t\ttestDirs := []string{\n\t\t\t\t\t\"unit1\",\n\t\t\t\t\t\"unit2\",\n\t\t\t\t\t\"stack1\",\n\t\t\t\t}\n\n\t\t\t\tfor _, dir := range testDirs {\n\t\t\t\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\n\t\t\t\t// Create test files\n\t\t\t\ttestFiles := map[string]string{\n\t\t\t\t\t\"unit1/terragrunt.hcl\":        \"\",\n\t\t\t\t\t\"unit2/terragrunt.hcl\":        \"\",\n\t\t\t\t\t\"stack1/terragrunt.stack.hcl\": \"\",\n\t\t\t\t}\n\n\t\t\t\tfor path, content := range testFiles {\n\t\t\t\t\terr := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\n\t\t\t\treturn tmpDir\n\t\t\t},\n\t\t\texpectedPaths: []string{\"unit1\", \"unit2\", \"stack1\"},\n\t\t\tformat:        \"json\",\n\t\t\tmode:          \"normal\",\n\t\t\tdependencies:  false,\n\t\t\texternal:      false,\n\t\t\tvalidate: func(t *testing.T, output string, expectedPaths []string) {\n\t\t\t\tt.Helper()\n\n\t\t\t\t// Verify the output is valid JSON\n\t\t\t\tvar configs find.FoundComponents\n\n\t\t\t\terr := json.Unmarshal([]byte(output), &configs)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t// Verify we have the expected number of configs\n\t\t\t\tassert.Len(t, configs, len(expectedPaths))\n\n\t\t\t\t// Convert expected paths to use OS-specific path separators\n\t\t\t\tosExpectedPaths := make([]string, 0, len(expectedPaths))\n\t\t\t\tfor _, path := range expectedPaths {\n\t\t\t\t\tosExpectedPaths = append(osExpectedPaths, filepath.FromSlash(path))\n\t\t\t\t}\n\n\t\t\t\t// Extract paths and convert to OS-specific separators\n\t\t\t\tpaths := make([]string, 0, len(configs))\n\t\t\t\tfor _, config := range configs {\n\t\t\t\t\tpaths = append(paths, filepath.FromSlash(config.Path))\n\t\t\t\t}\n\n\t\t\t\t// Verify all expected paths are present\n\t\t\t\tassert.ElementsMatch(t, osExpectedPaths, paths)\n\n\t\t\t\t// Verify each config has a valid type\n\t\t\t\tfor _, config := range configs {\n\t\t\t\t\tassert.NotEmpty(t, config.Type)\n\t\t\t\t\tassert.True(t, config.Type == component.UnitKind || config.Type == component.StackKind)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"hidden discovery\",\n\t\t\tsetup: func(t *testing.T) string {\n\t\t\t\tt.Helper()\n\n\t\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\t\t// Create test directory structure\n\t\t\t\ttestDirs := []string{\n\t\t\t\t\t\"unit1\",\n\t\t\t\t\t\"unit2\",\n\t\t\t\t\t\"stack1\",\n\t\t\t\t\t\".hidden/unit3\",\n\t\t\t\t\t\"nested/unit4\",\n\t\t\t\t}\n\n\t\t\t\tfor _, dir := range testDirs {\n\t\t\t\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\n\t\t\t\t// Create test files\n\t\t\t\ttestFiles := map[string]string{\n\t\t\t\t\t\"unit1/terragrunt.hcl\":         \"\",\n\t\t\t\t\t\"unit2/terragrunt.hcl\":         \"\",\n\t\t\t\t\t\"stack1/terragrunt.stack.hcl\":  \"\",\n\t\t\t\t\t\".hidden/unit3/terragrunt.hcl\": \"\",\n\t\t\t\t\t\"nested/unit4/terragrunt.hcl\":  \"\",\n\t\t\t\t}\n\n\t\t\t\tfor path, content := range testFiles {\n\t\t\t\t\terr := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\n\t\t\t\treturn tmpDir\n\t\t\t},\n\t\t\texpectedPaths: []string{\"unit1\", \"unit2\", \"nested/unit4\", \"stack1\", \".hidden/unit3\"},\n\t\t\tformat:        \"text\",\n\t\t\tmode:          \"normal\",\n\t\t\tdependencies:  false,\n\t\t\texternal:      false,\n\t\t\tvalidate: func(t *testing.T, output string, expectedPaths []string) {\n\t\t\t\tt.Helper()\n\n\t\t\t\t// Split output into lines and trim whitespace\n\t\t\t\tlines := strings.Split(strings.TrimSpace(output), \"\\n\")\n\n\t\t\t\t// Verify we have the expected number of lines\n\t\t\t\tassert.Len(t, lines, len(expectedPaths))\n\n\t\t\t\t// Convert expected paths to use OS-specific path separators\n\t\t\t\tosExpectedPaths := make([]string, 0, len(expectedPaths))\n\t\t\t\tfor _, path := range expectedPaths {\n\t\t\t\t\tosExpectedPaths = append(osExpectedPaths, filepath.FromSlash(path))\n\t\t\t\t}\n\n\t\t\t\t// Convert actual paths to use OS-specific path separators\n\t\t\t\tosPaths := make([]string, 0, len(lines))\n\t\t\t\tfor _, line := range lines {\n\t\t\t\t\tosPaths = append(osPaths, filepath.FromSlash(strings.TrimSpace(line)))\n\t\t\t\t}\n\n\t\t\t\t// Verify all expected paths are present\n\t\t\t\tassert.ElementsMatch(t, osExpectedPaths, osPaths)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"dag sorting - simple dependencies\",\n\t\t\tsetup: func(t *testing.T) string {\n\t\t\t\tt.Helper()\n\n\t\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\t\t// Create test directory structure with dependencies:\n\t\t\t\t// unit2 -> unit1\n\t\t\t\t// unit3 -> unit2\n\t\t\t\ttestDirs := []string{\n\t\t\t\t\t\"unit1\",\n\t\t\t\t\t\"unit2\",\n\t\t\t\t\t\"unit3\",\n\t\t\t\t}\n\n\t\t\t\tfor _, dir := range testDirs {\n\t\t\t\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\n\t\t\t\t// Create test files with dependencies\n\t\t\t\ttestFiles := map[string]string{\n\t\t\t\t\t\"unit1/terragrunt.hcl\": \"\",\n\t\t\t\t\t\"unit2/terragrunt.hcl\": `\ndependency \"unit1\" {\n  config_path = \"../unit1\"\n}`,\n\t\t\t\t\t\"unit3/terragrunt.hcl\": `\ndependency \"unit2\" {\n  config_path = \"../unit2\"\n}`,\n\t\t\t\t}\n\n\t\t\t\tfor path, content := range testFiles {\n\t\t\t\t\terr := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\n\t\t\t\treturn tmpDir\n\t\t\t},\n\t\t\texpectedPaths: []string{\"unit1\", \"unit2\", \"unit3\"},\n\t\t\tformat:        \"text\",\n\t\t\tmode:          \"dag\",\n\t\t\tdependencies:  true,\n\t\t\texternal:      false,\n\t\t\tvalidate: func(t *testing.T, output string, expectedPaths []string) {\n\t\t\t\tt.Helper()\n\n\t\t\t\t// Split output into lines and trim whitespace\n\t\t\t\tlines := strings.Split(strings.TrimSpace(output), \"\\n\")\n\n\t\t\t\t// Verify we have the expected number of lines\n\t\t\t\tassert.Len(t, lines, len(expectedPaths))\n\n\t\t\t\t// Convert paths to use OS-specific separators\n\t\t\t\tosPaths := make([]string, 0, len(lines))\n\t\t\t\tfor _, line := range lines {\n\t\t\t\t\tosPaths = append(osPaths, filepath.FromSlash(strings.TrimSpace(line)))\n\t\t\t\t}\n\n\t\t\t\t// Convert expected paths to use OS-specific separators\n\t\t\t\tosExpectedPaths := make([]string, 0, len(expectedPaths))\n\t\t\t\tfor _, path := range expectedPaths {\n\t\t\t\t\tosExpectedPaths = append(osExpectedPaths, filepath.FromSlash(path))\n\t\t\t\t}\n\n\t\t\t\t// For DAG sorting, order matters - verify exact order\n\t\t\t\tassert.Equal(t, osExpectedPaths, osPaths)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"dag sorting - json output with dependencies\",\n\t\t\tsetup: func(t *testing.T) string {\n\t\t\t\tt.Helper()\n\n\t\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\t\t// Create test directory structure with dependencies\n\t\t\t\ttestDirs := []string{\n\t\t\t\t\t\"A\", \"B\", \"C\",\n\t\t\t\t}\n\n\t\t\t\tfor _, dir := range testDirs {\n\t\t\t\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\n\t\t\t\t// Create test files with dependencies\n\t\t\t\ttestFiles := map[string]string{\n\t\t\t\t\t\"A/terragrunt.hcl\": \"\",\n\t\t\t\t\t\"B/terragrunt.hcl\": `\ndependency \"A\" {\n  config_path = \"../A\"\n}`,\n\t\t\t\t\t\"C/terragrunt.hcl\": `\ndependency \"B\" {\n  config_path = \"../B\"\n}`,\n\t\t\t\t}\n\n\t\t\t\tfor path, content := range testFiles {\n\t\t\t\t\terr := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\n\t\t\t\treturn tmpDir\n\t\t\t},\n\t\t\texpectedPaths: []string{\"A\", \"B\", \"C\"},\n\t\t\tformat:        \"json\",\n\t\t\tmode:          \"dag\",\n\t\t\tdependencies:  true,\n\t\t\texternal:      false,\n\t\t\tvalidate: func(t *testing.T, output string, expectedPaths []string) {\n\t\t\t\tt.Helper()\n\n\t\t\t\t// Verify the output is valid JSON\n\t\t\t\tvar configs []find.FoundComponent\n\n\t\t\t\terr := json.Unmarshal([]byte(output), &configs)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t// Verify we have the expected number of configs\n\t\t\t\tassert.Len(t, configs, len(expectedPaths))\n\n\t\t\t\t// Extract paths and verify order\n\t\t\t\tpaths := make([]string, 0, len(configs))\n\t\t\t\tfor _, config := range configs {\n\t\t\t\t\tpaths = append(paths, filepath.FromSlash(config.Path))\n\t\t\t\t}\n\n\t\t\t\t// Convert expected paths to use OS-specific separators\n\t\t\t\tosExpectedPaths := make([]string, 0, len(expectedPaths))\n\t\t\t\tfor _, path := range expectedPaths {\n\t\t\t\t\tosExpectedPaths = append(osExpectedPaths, filepath.FromSlash(path))\n\t\t\t\t}\n\n\t\t\t\tassert.Equal(t, osExpectedPaths, paths)\n\n\t\t\t\t// Verify dependencies are correctly represented in JSON\n\t\t\t\tassert.Empty(t, configs[0].Dependencies, \"A should have no dependencies\")\n\t\t\t\tassert.Equal(t, []string{\"A\"}, configs[1].Dependencies, \"B should depend on A\")\n\t\t\t\tassert.Equal(t, []string{\"B\"}, configs[2].Dependencies, \"C should depend on B\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"invalid format\",\n\t\t\tsetup: func(t *testing.T) string {\n\t\t\t\tt.Helper()\n\n\t\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\t\treturn tmpDir\n\t\t\t},\n\t\t\tformat: \"invalid\",\n\t\t\tvalidate: func(t *testing.T, output string, expectedPaths []string) {\n\t\t\t\tt.Helper()\n\t\t\t\tassert.Empty(t, output)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"invalid sort\",\n\t\t\tsetup: func(t *testing.T) string {\n\t\t\t\tt.Helper()\n\n\t\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\t\treturn tmpDir\n\t\t\t},\n\t\t\tmode: \"invalid\",\n\t\t\tvalidate: func(t *testing.T, output string, expectedPaths []string) {\n\t\t\t\tt.Helper()\n\t\t\t\tassert.Empty(t, output)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"reading flag with json output\",\n\t\t\tsetup: func(t *testing.T) string {\n\t\t\t\tt.Helper()\n\n\t\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\t\t\t\tappDir := filepath.Join(tmpDir, \"app\")\n\t\t\t\trequire.NoError(t, os.MkdirAll(appDir, 0755))\n\n\t\t\t\t// Create shared files that will be read\n\t\t\t\tsharedHCL := filepath.Join(tmpDir, \"shared.hcl\")\n\t\t\t\tsharedTFVars := filepath.Join(tmpDir, \"shared.tfvars\")\n\n\t\t\t\trequire.NoError(t, os.WriteFile(sharedHCL, []byte(`\nlocals {\n  common_value = \"test\"\n}\n`), 0644))\n\n\t\t\t\trequire.NoError(t, os.WriteFile(sharedTFVars, []byte(`\ntest_var = \"value\"\n`), 0644))\n\n\t\t\t\t// Create terragrunt config that reads both files\n\t\t\t\tterragruntConfig := filepath.Join(appDir, \"terragrunt.hcl\")\n\t\t\t\trequire.NoError(t, os.WriteFile(terragruntConfig, []byte(`\nlocals {\n  shared_config = read_terragrunt_config(\"../shared.hcl\")\n  tfvars = read_tfvars_file(\"../shared.tfvars\")\n}\n`), 0644))\n\n\t\t\t\treturn tmpDir\n\t\t\t},\n\t\t\texpectedPaths: []string{\"app\"},\n\t\t\tformat:        \"json\",\n\t\t\tmode:          \"normal\",\n\t\t\treading:       true,\n\t\t\tvalidate: func(t *testing.T, output string, expectedPaths []string) {\n\t\t\t\tt.Helper()\n\n\t\t\t\t// Verify the output is valid JSON\n\t\t\t\tvar configs find.FoundComponents\n\n\t\t\t\terr := json.Unmarshal([]byte(output), &configs)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t// Verify we have one config\n\t\t\t\trequire.Len(t, configs, 1)\n\n\t\t\t\t// Verify the component has the Reading field populated\n\t\t\t\tappConfig := configs[0]\n\t\t\t\trequire.NotNil(t, appConfig.Reading, \"Reading field should be populated\")\n\t\t\t\trequire.NotEmpty(t, appConfig.Reading, \"Reading field should contain files\")\n\n\t\t\t\t// Verify Reading field contains the shared files\n\t\t\t\treadingPaths := appConfig.Reading\n\t\t\t\tassert.Len(t, readingPaths, 2, \"should have read 2 files\")\n\n\t\t\t\t// Convert to map for easier checking\n\t\t\t\treadingMap := make(map[string]bool)\n\t\t\t\tfor _, path := range readingPaths {\n\t\t\t\t\treadingMap[filepath.FromSlash(path)] = true\n\t\t\t\t}\n\n\t\t\t\t// Check that shared files are in the reading list\n\t\t\t\tassert.True(t, readingMap[\"shared.hcl\"], \"should contain shared.hcl\")\n\t\t\t\tassert.True(t, readingMap[\"shared.tfvars\"], \"should contain shared.tfvars\")\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\tt.Parallel()\n\n\t\t\t// Setup test directory\n\t\t\ttmpDir := tt.setup(t)\n\n\t\t\ttgOpts := options.NewTerragruntOptions()\n\t\t\ttgOpts.WorkingDir = tmpDir\n\n\t\t\tl := logger.CreateLogger()\n\t\t\tl.Formatter().SetDisabledColors(true)\n\n\t\t\t// Create options\n\t\t\topts := find.NewOptions(tgOpts)\n\t\t\topts.Format = tt.format\n\t\t\topts.NoHidden = tt.noHidden\n\t\t\topts.Mode = tt.mode\n\t\t\topts.Dependencies = tt.dependencies\n\t\t\topts.Reading = tt.reading\n\n\t\t\t// Create a pipe to capture output\n\t\t\tr, w, err := os.Pipe()\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Set the writer in options\n\t\t\topts.Writers.Writer = w\n\n\t\t\terr = find.Run(t.Context(), l, opts)\n\t\t\tif tt.format == \"invalid\" || tt.mode == \"invalid\" {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Close the write end of the pipe\n\t\t\tw.Close()\n\n\t\t\t// Read all output\n\t\t\toutput, err := io.ReadAll(r)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Validate the output\n\t\t\ttt.validate(t, string(output), tt.expectedPaths)\n\t\t})\n\t}\n}\n\nfunc TestColorizer(t *testing.T) {\n\tt.Parallel()\n\n\tcolorizer := find.NewColorizer(true)\n\n\ttests := []struct {\n\t\tname   string\n\t\tconfig *find.FoundComponent\n\t\t// We can't test exact ANSI codes as they might vary by environment,\n\t\t// so we'll test that different types result in different outputs\n\t\tshouldBeDifferent []component.Kind\n\t}{\n\t\t{\n\t\t\tname: \"unit config\",\n\t\t\tconfig: &find.FoundComponent{\n\t\t\t\tType: component.UnitKind,\n\t\t\t\tPath: \"path/to/unit\",\n\t\t\t},\n\t\t\tshouldBeDifferent: []component.Kind{component.StackKind},\n\t\t},\n\t\t{\n\t\t\tname: \"stack config\",\n\t\t\tconfig: &find.FoundComponent{\n\t\t\t\tType: component.StackKind,\n\t\t\t\tPath: \"path/to/stack\",\n\t\t\t},\n\t\t\tshouldBeDifferent: []component.Kind{component.UnitKind},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresult := colorizer.Colorize(tt.config)\n\t\t\tassert.NotEmpty(t, result)\n\n\t\t\t// Test that different types produce different colorized outputs\n\t\t\tfor _, diffType := range tt.shouldBeDifferent {\n\t\t\t\tdiffConfig := &find.FoundComponent{\n\t\t\t\t\tType: diffType,\n\t\t\t\t\tPath: tt.config.Path,\n\t\t\t\t}\n\t\t\t\tdiffResult := colorizer.Colorize(diffConfig)\n\t\t\t\tassert.NotEqual(t, result, diffResult)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/find/options.go",
    "content": "package find\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\t// FormatText outputs the discovered components in text format.\n\tFormatText = \"text\"\n\n\t// FormatJSON outputs the discovered components in JSON format.\n\tFormatJSON = \"json\"\n\n\t// ModeNormal is the default mode for the find command.\n\tModeNormal = \"normal\"\n\n\t// ModeDAG is the mode for the find command that sorts and groups output in DAG order.\n\tModeDAG = \"dag\"\n)\n\ntype Options struct {\n\t*options.TerragruntOptions\n\n\t// Format determines the format of the output.\n\tFormat string\n\n\t// Mode determines the mode of the find command.\n\tMode string\n\n\t// QueueConstructAs constructs the queue as if a particular command was run.\n\tQueueConstructAs string\n\n\t// JSON determines if the output should be in JSON format.\n\t// Alias for --format=json.\n\tJSON bool\n\n\t// DAG determines if the output should be in DAG mode.\n\tDAG bool\n\n\t// NoHidden determines if hidden directories should be excluded from the output.\n\tNoHidden bool\n\n\t// Dependencies determines if dependencies should be included in the output.\n\tDependencies bool\n\n\t// Exclude determines if exclude components should be included in the output.\n\tExclude bool\n\n\t// Include determines if Include components should be included in the output.\n\tInclude bool\n\n\t// Reading determines if the list of files that are read by components should be included in the output.\n\tReading bool\n}\n\nfunc NewOptions(opts *options.TerragruntOptions) *Options {\n\treturn &Options{\n\t\tTerragruntOptions: opts,\n\t\tFormat:            FormatText,\n\t\tMode:              ModeNormal,\n\t}\n}\n\nfunc (o *Options) Validate() error {\n\terrs := []error{}\n\n\tif err := o.validateFormat(); err != nil {\n\t\terrs = append(errs, err)\n\t}\n\n\tif err := o.validateMode(); err != nil {\n\t\terrs = append(errs, err)\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn errors.New(errors.Join(errs...))\n\t}\n\n\treturn nil\n}\n\nfunc (o *Options) validateFormat() error {\n\tswitch o.Format {\n\tcase FormatText:\n\t\treturn nil\n\tcase FormatJSON:\n\t\treturn nil\n\tdefault:\n\t\treturn errors.New(\"invalid format: \" + o.Format)\n\t}\n}\n\nfunc (o *Options) validateMode() error {\n\tswitch o.Mode {\n\tcase ModeNormal:\n\t\treturn nil\n\tcase ModeDAG:\n\t\treturn nil\n\tdefault:\n\t\treturn errors.New(\"invalid mode: \" + o.Mode)\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/hcl/cli.go",
    "content": "// Package hcl provides commands for formatting and validating HCL configurations.\npackage hcl\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/hcl/format\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/hcl/validate\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst CommandName = \"hcl\"\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\treturn &clihelper.Command{\n\t\tName:        CommandName,\n\t\tUsage:       \"Interact with HCL files.\",\n\t\tDescription: \"Interact with Terragrunt files written in HashiCorp Configuration Language (HCL).\",\n\t\tSubcommands: clihelper.Commands{\n\t\t\tformat.NewCommand(l, opts),\n\t\t\tvalidate.NewCommand(l, opts),\n\t\t},\n\t\tAction: clihelper.ShowCommandHelp,\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/hcl/format/cli.go",
    "content": "package format\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tCommandName      = \"format\"\n\tCommandNameAlias = \"fmt\"\n\n\tFileFlagName       = \"file\"\n\tExcludeDirFlagName = \"exclude-dir\"\n\tCheckFlagName      = \"check\"\n\tDiffFlagName       = \"diff\"\n\tStdinFlagName      = \"stdin\"\n)\n\nfunc NewFlags(l log.Logger, opts *options.TerragruntOptions, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\tterragruntPrefix := flags.Prefix{flags.TerragruntPrefix}\n\tterragruntPrefixControl := flags.StrictControlsByCommand(opts.StrictControls, CommandName)\n\n\tflagSet := clihelper.Flags{\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        FileFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(FileFlagName),\n\t\t\tDestination: &opts.HclFile,\n\t\t\tUsage:       \"The path to a single HCL file that the command should run on.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(tgPrefix.EnvVars(\"hclfmt-file\"), terragruntPrefixControl),         // `TG_HCLFMT_FILE`\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"hclfmt-file\"), terragruntPrefixControl), // `TERRAGRUNT_HCLFMT_FILE`\n\t\t),\n\n\t\tflags.NewFlag(&clihelper.SliceFlag[string]{\n\t\t\tName:        ExcludeDirFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(ExcludeDirFlagName),\n\t\t\tDestination: &opts.HclExclude,\n\t\t\tUsage:       \"Skip HCL formatting in given directories.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(tgPrefix.EnvVars(\"hclfmt-exclude-dir\"), terragruntPrefixControl),         // `TG_HCLFMT_EXCLUDE_DIR`\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"hclfmt-exclude-dir\"), terragruntPrefixControl), // `TERRAGRUNT_EXCLUDE_DIR`\n\t\t),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        CheckFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(CheckFlagName),\n\t\t\tDestination: &opts.Check,\n\t\t\tUsage:       \"Return a status code of zero when all files are formatted correctly, and a status code of one when they aren't.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(tgPrefix.EnvVars(\"hclfmt-check\"), terragruntPrefixControl),  // `TG_HCLFMT_CHECK`\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"check\"), terragruntPrefixControl), // `TERRAGRUNT_CHECK`\n\t\t),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        DiffFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(DiffFlagName),\n\t\t\tDestination: &opts.Diff,\n\t\t\tUsage:       \"Print diff between original and modified file versions.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(tgPrefix.EnvVars(\"hclfmt-diff\"), terragruntPrefixControl),  // `TG_HCLFMT_DIFF`\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"diff\"), terragruntPrefixControl), // `TERRAGRUNT_DIFF`\n\t\t),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        StdinFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(StdinFlagName),\n\t\t\tDestination: &opts.HclFromStdin,\n\t\t\tUsage:       \"Format HCL from stdin and print result to stdout.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(tgPrefix.EnvVars(\"hclfmt-stdin\"), terragruntPrefixControl),         // `TG_HCLFMT_STDIN`\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"hclfmt-stdin\"), terragruntPrefixControl), // `TERRAGRUNT_HCLFMT_STDIN`\n\t\t),\n\t}\n\n\tflagSet = flagSet.Add(shared.NewQueueFlags(opts, nil)...)\n\tflagSet = flagSet.Add(shared.NewFilterFlags(l, opts)...)\n\tflagSet = flagSet.Add(shared.NewParallelismFlag(opts))\n\n\treturn flagSet\n}\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\tcmd := &clihelper.Command{\n\t\tName:    CommandName,\n\t\tAliases: []string{CommandNameAlias},\n\t\tUsage:   \"Recursively find HashiCorp Configuration Language (HCL) files and rewrite them into a canonical format.\",\n\t\tFlags:   NewFlags(l, opts, nil),\n\t\tAction: func(ctx context.Context, _ *clihelper.Context) error {\n\t\t\treturn Run(ctx, l, opts.OptionsFromContext(ctx))\n\t\t},\n\t}\n\n\treturn cmd\n}\n"
  },
  {
    "path": "internal/cli/commands/hcl/format/errors.go",
    "content": "package format\n\nimport \"fmt\"\n\n// FileNeedsFormattingError is an error that is returned when a file needs formatting.\ntype FileNeedsFormattingError struct {\n\tPath string\n}\n\nfunc (e FileNeedsFormattingError) Error() string {\n\treturn fmt.Sprintf(\"File '%s' needs formatting\", e.Path)\n}\n"
  },
  {
    "path": "internal/cli/commands/hcl/format/format.go",
    "content": "// Package format recursively looks for hcl files in the directory tree starting at workingDir, and formats them\n// based on the language style guides provided by Hashicorp. This is done using the official hcl2 library.\npackage format\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/os/signal\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/writer\"\n\t\"golang.org/x/exp/slices\"\n\t\"golang.org/x/sync/errgroup\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\n\t\"github.com/hashicorp/hcl/v2/hclwrite\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nvar excludePaths = []string{\n\tutil.TerragruntCacheDir,\n\tutil.DefaultBoilerplateDir,\n\tconfig.StackDir,\n}\n\nfunc Run(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\tworkingDir := opts.WorkingDir\n\ttargetFile := opts.HclFile\n\tstdIn := opts.HclFromStdin\n\n\tif stdIn {\n\t\tif targetFile != \"\" {\n\t\t\treturn errors.Errorf(\"both stdin and path flags are specified\")\n\t\t}\n\n\t\treturn formatFromStdin(l, opts)\n\t}\n\n\tif targetFile != \"\" {\n\t\tif !filepath.IsAbs(targetFile) {\n\t\t\ttargetFile = filepath.Join(workingDir, targetFile)\n\t\t}\n\n\t\tl.Debugf(\"Formatting hcl file at: %s.\", targetFile)\n\n\t\treturn formatTgHCL(ctx, l, opts, targetFile)\n\t}\n\n\tvar (\n\t\tfilters filter.Filters\n\t\terr     error\n\t)\n\n\tfilters = opts.Filters\n\n\t// We use lightweight discovery here instead of the full discovery used by\n\t// the discovery package because we want to find non-comps like includes.\n\tfiles := []string{}\n\n\terr = filepath.WalkDir(workingDir, func(path string, d fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tbasename := filepath.Base(path)\n\t\tif slices.Contains(excludePaths, basename) {\n\t\t\tl.Debugf(\"%s directory ignored by default\", path)\n\t\t\treturn filepath.SkipDir\n\t\t}\n\n\t\tif slices.Contains(opts.HclExclude, basename) {\n\t\t\tl.Debugf(\"%s directory ignored due to the %s flag\", path, ExcludeDirFlagName)\n\t\t\treturn filepath.SkipDir\n\t\t}\n\n\t\tif d.IsDir() {\n\t\t\treturn nil\n\t\t}\n\n\t\tif !strings.HasSuffix(path, \".hcl\") {\n\t\t\treturn nil\n\t\t}\n\n\t\tfiles = append(files, path)\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tvar components component.Components\n\n\tcomponents, err = filters.EvaluateOnFiles(l, files, workingDir)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tg, gctx := errgroup.WithContext(ctx)\n\n\tlimit := opts.Parallelism\n\tif limit == options.DefaultParallelism {\n\t\tlimit = runtime.NumCPU()\n\t}\n\n\tg.SetLimit(limit)\n\n\t// Pre-allocate the errs slice with max possible length\n\t// so we don't need to hold a lock to append to it.\n\terrs := make([]error, len(components))\n\n\tfor i, c := range components {\n\t\tg.Go(func() error {\n\t\t\terr := formatTgHCL(gctx, l, opts, c.Path())\n\t\t\tif err != nil {\n\t\t\t\terrs[i] = err\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t_ = g.Wait()\n\n\treturn errors.Join(errs...)\n}\n\nfunc formatFromStdin(l log.Logger, opts *options.TerragruntOptions) error {\n\tcontents, err := io.ReadAll(os.Stdin)\n\tif err != nil {\n\t\tl.Errorf(\"Error reading from stdin: %s\", err)\n\n\t\treturn fmt.Errorf(\"error reading from stdin: %w\", err)\n\t}\n\n\tif err = checkErrors(l, l.Formatter().DisabledColors(), contents, \"stdin\"); err != nil {\n\t\tl.Errorf(\"Error parsing hcl from stdin\")\n\n\t\treturn fmt.Errorf(\"error parsing hcl from stdin: %w\", err)\n\t}\n\n\tnewContents := hclwrite.Format(contents)\n\n\tbuf := bufio.NewWriter(opts.Writers.Writer)\n\n\tif _, err = buf.Write(newContents); err != nil {\n\t\tl.Errorf(\"Failed to write to stdout\")\n\n\t\treturn fmt.Errorf(\"failed to write to stdout: %w\", err)\n\t}\n\n\tif err = buf.Flush(); err != nil {\n\t\tl.Errorf(\"Failed to flush to stdout\")\n\n\t\treturn fmt.Errorf(\"failed to flush to stdout: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// formatTgHCL uses the hcl2 library to format the hcl file. This will attempt to parse the HCL file first to\n// ensure that there are no syntax errors, before attempting to format it.\nfunc formatTgHCL(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, tgHclFile string) error {\n\tl.Debugf(\"Formatting %s\", tgHclFile)\n\n\tinfo, err := os.Stat(tgHclFile)\n\tif err != nil {\n\t\tl.Errorf(\"Error retrieving file info of %s\", tgHclFile)\n\t\treturn errors.Errorf(\"failed to get file info for %s: %w\", tgHclFile, err)\n\t}\n\n\tcontents, err := os.ReadFile(tgHclFile)\n\tif err != nil {\n\t\tl.Errorf(\"Error reading %s\", tgHclFile)\n\t\treturn errors.Errorf(\"failed to read %s: %w\", tgHclFile, err)\n\t}\n\n\terr = checkErrors(l, l.Formatter().DisabledColors(), contents, tgHclFile)\n\tif err != nil {\n\t\tl.Errorf(\"Error parsing %s\", tgHclFile)\n\t\treturn err\n\t}\n\n\tnewContents := hclwrite.Format(contents)\n\n\tfileUpdated := !bytes.Equal(newContents, contents)\n\n\tif opts.Diff && fileUpdated {\n\t\tdiff, err := bytesDiff(ctx, l, contents, newContents, tgHclFile)\n\t\tif err != nil {\n\t\t\tl.Errorf(\"Failed to generate diff for %s\", tgHclFile)\n\t\t\treturn err\n\t\t}\n\n\t\t_, err = fmt.Fprintf(opts.Writers.Writer, \"%s\\n\", diff)\n\t\tif err != nil {\n\t\t\tl.Errorf(\"Failed to print diff for %s\", tgHclFile)\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif opts.Check && fileUpdated {\n\t\treturn &FileNeedsFormattingError{Path: tgHclFile}\n\t}\n\n\tif fileUpdated {\n\t\tl.Infof(\"%s was updated\", tgHclFile)\n\t\treturn os.WriteFile(tgHclFile, newContents, info.Mode())\n\t}\n\n\treturn nil\n}\n\n// checkErrors takes in the contents of a hcl file and looks for syntax errors.\nfunc checkErrors(l log.Logger, disableColor bool, contents []byte, tgHclFile string) error {\n\tparser := hclparse.NewParser()\n\t_, diags := parser.ParseHCL(contents, tgHclFile)\n\n\twriter := writer.New(writer.WithLogger(l), writer.WithDefaultLevel(log.ErrorLevel))\n\tdiagWriter := parser.GetDiagnosticsWriter(writer, disableColor)\n\n\terr := diagWriter.WriteDiagnostics(diags)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tif diags.HasErrors() {\n\t\treturn diags\n\t}\n\n\treturn nil\n}\n\n// bytesDiff uses GNU diff to display the differences between the contents of HCL file before and after formatting\nfunc bytesDiff(ctx context.Context, l log.Logger, b1, b2 []byte, path string) ([]byte, error) {\n\tf1, err := os.CreateTemp(\"\", \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer func() {\n\t\tif err = f1.Close(); err != nil {\n\t\t\tl.Warnf(\"Failed to close file %s %v\", f1.Name(), err)\n\t\t}\n\n\t\tif err = os.Remove(f1.Name()); err != nil {\n\t\t\tl.Warnf(\"Failed to remove file %s %v\", f1.Name(), err)\n\t\t}\n\t}()\n\n\tf2, err := os.CreateTemp(\"\", \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer func() {\n\t\tif err = f2.Close(); err != nil {\n\t\t\tl.Warnf(\"Failed to close file %s %v\", f2.Name(), err)\n\t\t}\n\n\t\tif err = os.Remove(f2.Name()); err != nil {\n\t\t\tl.Warnf(\"Failed to remove file %s %v\", f2.Name(), err)\n\t\t}\n\t}()\n\n\tif _, err = f1.Write(b1); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif _, err = f2.Write(b2); err != nil {\n\t\treturn nil, err\n\t}\n\n\tdiffPath, err := exec.LookPath(\"diff\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to find diff command in PATH: %w\", err)\n\t}\n\n\tcmd := exec.CommandContext(\n\t\tctx,\n\t\tdiffPath,\n\t\t\"--label=\"+filepath.Join(\"old\", path),\n\t\t\"--label=\"+filepath.Join(\"new/\", path),\n\t\t\"-u\",\n\t\tf1.Name(),\n\t\tf2.Name(),\n\t)\n\tcmd.Cancel = func() error {\n\t\tif cmd.Process == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tif sig := signal.SignalFromContext(ctx); sig != nil {\n\t\t\treturn cmd.Process.Signal(sig)\n\t\t}\n\n\t\treturn cmd.Process.Signal(os.Kill)\n\t}\n\n\tdata, err := cmd.CombinedOutput()\n\tif len(data) > 0 {\n\t\t// diff exits with a non-zero status when the files don't match.\n\t\t// Ignore that failure as long as we get output.\n\t\terr = nil\n\t}\n\n\treturn data, err\n}\n"
  },
  {
    "path": "internal/cli/commands/hcl/format/format_bench_test.go",
    "content": "package format_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/hcl/format\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\tlogformat \"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nfunc BenchmarkFormat(b *testing.B) {\n\tsourceFile := \"../../../../../test/fixtures/hcl-filter/fmt/needs-formatting/nested/api/terragrunt.hcl\"\n\n\tpristineContent, err := os.ReadFile(sourceFile)\n\tif err != nil {\n\t\tb.Fatalf(\"Failed to read source file: %v\", err)\n\t}\n\n\tfileCounts := []int{1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024}\n\n\tfor _, fileCount := range fileCounts {\n\t\tb.Run(fmt.Sprintf(\"files_%d\", fileCount), func(b *testing.B) {\n\t\t\ttmpBase := b.TempDir()\n\n\t\t\tvar excludeList []string\n\t\t\tfor i := 2; i <= fileCount; i += 2 {\n\t\t\t\texcludeList = append(excludeList, fmt.Sprintf(\"dir-%04d\", i))\n\t\t\t}\n\n\t\t\ttgOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\t\t\tif err != nil {\n\t\t\t\tb.Fatalf(\"Failed to create options: %v\", err)\n\t\t\t}\n\n\t\t\ttgOptions.WorkingDir = tmpBase\n\t\t\ttgOptions.HclExclude = excludeList\n\t\t\ttgOptions.Writers.Writer = io.Discard\n\t\t\ttgOptions.Writers.ErrWriter = io.Discard\n\n\t\t\tformatter := logformat.NewFormatter(logformat.NewKeyValueFormatPlaceholders())\n\t\t\tformatter.SetDisabledColors(true)\n\t\t\tl := log.New(log.WithOutput(io.Discard), log.WithLevel(log.ErrorLevel), log.WithFormatter(formatter))\n\t\t\tctx := context.Background()\n\n\t\t\tb.ResetTimer()\n\n\t\t\tfor b.Loop() {\n\t\t\t\tb.StopTimer()\n\n\t\t\t\tif err := createFiles(tmpBase, pristineContent, fileCount); err != nil {\n\t\t\t\t\tb.Fatalf(\"Failed to create files: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tb.StartTimer()\n\n\t\t\t\tif err := format.Run(ctx, l, tgOptions); err != nil {\n\t\t\t\t\tb.Fatalf(\"format.Run failed: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc createFiles(workingDir string, content []byte, count int) error {\n\tentries, err := os.ReadDir(workingDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, entry := range entries {\n\t\tif entry.IsDir() && strings.HasPrefix(entry.Name(), \"dir-\") {\n\t\t\tif err := os.RemoveAll(filepath.Join(workingDir, entry.Name())); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tfor i := 1; i <= count; i++ {\n\t\tdirName := fmt.Sprintf(\"dir-%04d\", i)\n\t\tdirPath := filepath.Join(workingDir, dirName)\n\n\t\tnestedPath := filepath.Join(dirPath, \"nested\", \"deep\", \"structure\")\n\t\tif err := os.MkdirAll(nestedPath, 0755); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfilePath := filepath.Join(nestedPath, \"terragrunt.hcl\")\n\t\tif err := os.WriteFile(filePath, content, 0644); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/cli/commands/hcl/format/format_test.go",
    "content": "package format_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/hcl/format\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n)\n\nfunc TestHCLFmt(t *testing.T) {\n\tt.Parallel()\n\n\ttmpPath, err := util.CopyFolderToTemp(\"./testdata/fixtures\", t.Name(), func(path string) bool { return true })\n\n\tt.Cleanup(func() {\n\t\tos.RemoveAll(tmpPath)\n\t})\n\n\trequire.NoError(t, err)\n\n\texpected, err := util.ReadFileAsString(\"./testdata/fixtures/expected.hcl\")\n\trequire.NoError(t, err)\n\n\ttgOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\ttgOptions.WorkingDir = tmpPath\n\ttgOptions.HclExclude = []string{\".history\"}\n\n\terr = format.Run(t.Context(), logger.CreateLogger(), tgOptions)\n\trequire.NoError(t, err)\n\n\tt.Run(\"group\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tdirs := []string{\n\t\t\t\"terragrunt.hcl\",\n\t\t\t\"a/terragrunt.hcl\",\n\t\t\t\"a/b/c/terragrunt.hcl\",\n\t\t\t\"a/b/c/d/services.hcl\",\n\t\t\t\"a/b/c/d/e/terragrunt.hcl\",\n\t\t}\n\t\tfor _, dir := range dirs {\n\t\t\t// Capture range variable into for block so it doesn't change while looping\n\t\t\tt.Run(dir, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\n\t\t\t\ttgHclPath := filepath.Join(tmpPath, dir)\n\t\t\t\tactual, err := util.ReadFileAsString(tgHclPath)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, expected, actual)\n\t\t\t})\n\t\t}\n\n\t\t// check to make sure the file in the `.terragrunt-cache` folder was ignored and untouched\n\t\tt.Run(\"terragrunt-cache\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\toriginalTgHclPath := \"./testdata/fixtures/ignored/.terragrunt-cache/terragrunt.hcl\"\n\t\t\toriginal, err := util.ReadFileAsString(originalTgHclPath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttgHclPath := filepath.Join(tmpPath, \"ignored/.terragrunt-cache/terragrunt.hcl\")\n\t\t\tactual, err := util.ReadFileAsString(tgHclPath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, original, actual)\n\t\t})\n\n\t\t// Finally, check to make sure the file in the `.history` folder was ignored and untouched\n\t\tt.Run(\"history\", func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\toriginalTgHclPath := \"./testdata/fixtures/ignored/.history/terragrunt.hcl\"\n\t\t\toriginal, err := util.ReadFileAsString(originalTgHclPath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttgHclPath := filepath.Join(tmpPath, \"ignored/.history/terragrunt.hcl\")\n\t\t\tactual, err := util.ReadFileAsString(tgHclPath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, original, actual)\n\t\t})\n\t})\n}\n\nfunc TestHCLFmtErrors(t *testing.T) {\n\tt.Parallel()\n\n\ttmpPath, err := util.CopyFolderToTemp(\"../../../../../test/fixtures/hclfmt-errors\", t.Name(), func(path string) bool { return true })\n\tt.Cleanup(func() {\n\t\tos.RemoveAll(tmpPath)\n\t})\n\trequire.NoError(t, err)\n\n\ttgOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\tdirs := []string{\n\t\t\"dangling-attribute\",\n\t\t\"invalid-character\",\n\t\t\"invalid-key\",\n\t}\n\tfor _, dir := range dirs {\n\t\t// Capture range variable into for block so it doesn't change while looping\n\t\tt.Run(dir, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttgHclDir := filepath.Join(tmpPath, dir)\n\t\t\tl, newTgOptions, err := tgOptions.CloneWithConfigPath(logger.CreateLogger(), tgOptions.TerragruntConfigPath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tnewTgOptions.WorkingDir = tgHclDir\n\n\t\t\terr = format.Run(t.Context(), l, newTgOptions)\n\t\t\trequire.Error(t, err)\n\t\t})\n\t}\n}\n\nfunc TestHCLFmtCheck(t *testing.T) {\n\tt.Parallel()\n\n\ttmpPath, err := util.CopyFolderToTemp(\"../../../../../test/fixtures/hclfmt-check\", t.Name(), func(path string) bool { return true })\n\n\tt.Cleanup(func() {\n\t\tos.RemoveAll(tmpPath)\n\t})\n\n\trequire.NoError(t, err)\n\n\texpected, err := os.ReadFile(\"../../../../../test/fixtures/hclfmt-check/expected.hcl\")\n\trequire.NoError(t, err)\n\n\ttgOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\ttgOptions.Check = true\n\ttgOptions.WorkingDir = tmpPath\n\n\terr = format.Run(t.Context(), logger.CreateLogger(), tgOptions)\n\trequire.NoError(t, err)\n\n\tdirs := []string{\n\t\t\"terragrunt.hcl\",\n\t\t\"a/terragrunt.hcl\",\n\t\t\"a/b/c/terragrunt.hcl\",\n\t\t\"a/b/c/d/services.hcl\",\n\t\t\"a/b/c/d/e/terragrunt.hcl\",\n\t}\n\n\tfor _, dir := range dirs {\n\t\t// Capture range variable into for block so it doesn't change while looping\n\t\tt.Run(dir, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttgHclPath := filepath.Join(tmpPath, dir)\n\t\t\tactual, err := os.ReadFile(tgHclPath)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestHCLFmtCheckErrors(t *testing.T) {\n\tt.Parallel()\n\n\ttmpPath, err := util.CopyFolderToTemp(\"../../../../../test/fixtures/hclfmt-check-errors\", t.Name(), func(path string) bool { return true })\n\n\tt.Cleanup(func() {\n\t\tos.RemoveAll(tmpPath)\n\t})\n\n\trequire.NoError(t, err)\n\n\texpected, err := os.ReadFile(\"../../../../../test/fixtures/hclfmt-check-errors/expected.hcl\")\n\trequire.NoError(t, err)\n\n\ttgOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\ttgOptions.Check = true\n\ttgOptions.WorkingDir = tmpPath\n\n\terr = format.Run(t.Context(), logger.CreateLogger(), tgOptions)\n\trequire.Error(t, err)\n\n\tdirs := []string{\n\t\t\"terragrunt.hcl\",\n\t\t\"a/terragrunt.hcl\",\n\t\t\"a/b/c/terragrunt.hcl\",\n\t\t\"a/b/c/d/services.hcl\",\n\t\t\"a/b/c/d/e/terragrunt.hcl\",\n\t}\n\n\tfor _, dir := range dirs {\n\t\tt.Run(dir, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttgHclPath := filepath.Join(tmpPath, dir)\n\t\t\tactual, err := os.ReadFile(tgHclPath)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestHCLFmtFile(t *testing.T) {\n\tt.Parallel()\n\n\ttmpPath, err := util.CopyFolderToTemp(\"./testdata/fixtures\", t.Name(), func(path string) bool { return true })\n\n\tt.Cleanup(func() {\n\t\tos.RemoveAll(tmpPath)\n\t})\n\n\trequire.NoError(t, err)\n\n\texpected, err := os.ReadFile(\"./testdata/fixtures/expected.hcl\")\n\trequire.NoError(t, err)\n\n\ttgOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\t// format only the hcl file contained within the a subdirectory of the fixture\n\ttgOptions.HclFile = \"a/terragrunt.hcl\"\n\ttgOptions.WorkingDir = tmpPath\n\terr = format.Run(t.Context(), logger.CreateLogger(), tgOptions)\n\trequire.NoError(t, err)\n\n\t// test that the formatting worked on the specified file\n\tt.Run(\"formatted\", func(t *testing.T) {\n\t\tt.Run(tgOptions.HclFile, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttgHclPath := filepath.Join(tmpPath, tgOptions.HclFile)\n\t\t\tformatted, readErr := os.ReadFile(tgHclPath)\n\t\t\trequire.NoError(t, readErr)\n\t\t\tassert.Equal(t, expected, formatted)\n\t\t})\n\t})\n\n\tdirs := []string{\n\t\t\"terragrunt.hcl\",\n\t\t\"a/b/c/terragrunt.hcl\",\n\t}\n\n\toriginal, err := os.ReadFile(\"./testdata/fixtures/terragrunt.hcl\")\n\trequire.NoError(t, err)\n\n\t// test that none of the other files were formatted\n\tfor _, dir := range dirs {\n\t\t// Capture range variable into for block so it doesn't change while looping\n\t\tt.Run(dir, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttestingPath := filepath.Join(tmpPath, dir)\n\t\t\tactual, err := os.ReadFile(testingPath)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, original, actual)\n\t\t})\n\t}\n}\n\nfunc TestHCLFmtStdin(t *testing.T) {\n\tt.Parallel()\n\n\trealStdin := os.Stdin\n\trealStdout := os.Stdout\n\n\ttempStdoutFile, err := os.CreateTemp(helpers.TmpDirWOSymlinks(t), \"stdout.hcl\")\n\n\tdefer func() {\n\t\t_ = tempStdoutFile.Close()\n\t}()\n\n\trequire.NoError(t, err)\n\n\tos.Stdout = tempStdoutFile\n\n\tdefer func() { os.Stdout = realStdout }()\n\n\tos.Stdin, err = os.Open(\"../../../../../test/fixtures/hclfmt-stdin/terragrunt.hcl\")\n\n\tdefer func() { os.Stdin = realStdin }()\n\n\trequire.NoError(t, err)\n\n\texpected, err := os.ReadFile(\"../../../../../test/fixtures/hclfmt-stdin/expected.hcl\")\n\trequire.NoError(t, err)\n\n\ttgOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\t// format hcl from stdin\n\ttgOptions.HclFromStdin = true\n\terr = format.Run(t.Context(), logger.CreateLogger(), tgOptions)\n\trequire.NoError(t, err)\n\n\tformatted, err := os.ReadFile(tempStdoutFile.Name())\n\trequire.NoError(t, err)\n\tassert.Equal(t, expected, formatted)\n}\n\nfunc TestHCLFmtHeredoc(t *testing.T) {\n\tt.Parallel()\n\n\ttmpPath, err := util.CopyFolderToTemp(\"../../../../../test/fixtures/hclfmt-heredoc\", t.Name(), func(path string) bool { return true })\n\tdefer os.RemoveAll(tmpPath)\n\n\trequire.NoError(t, err)\n\n\texpected, err := os.ReadFile(\"../../../../../test/fixtures/hclfmt-heredoc/expected.hcl\")\n\trequire.NoError(t, err)\n\n\ttgOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\ttgOptions.WorkingDir = tmpPath\n\n\terr = format.Run(t.Context(), logger.CreateLogger(), tgOptions)\n\trequire.NoError(t, err)\n\n\ttgHclPath := filepath.Join(tmpPath, \"terragrunt.hcl\")\n\tactual, err := os.ReadFile(tgHclPath)\n\trequire.NoError(t, err)\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestHCLFmtFilter(t *testing.T) {\n\tt.Parallel()\n\n\ttmpPath, err := util.CopyFolderToTemp(\"./testdata/fixtures\", t.Name(), func(path string) bool { return true })\n\n\tt.Cleanup(func() {\n\t\tos.RemoveAll(tmpPath)\n\t})\n\n\trequire.NoError(t, err)\n\n\texpected, err := util.ReadFileAsString(\"./testdata/fixtures/expected.hcl\")\n\trequire.NoError(t, err)\n\n\toriginal, err := util.ReadFileAsString(\"./testdata/fixtures/terragrunt.hcl\")\n\trequire.NoError(t, err)\n\n\ttgOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\terr = tgOptions.Experiments.EnableExperiment(\"filter-flag\")\n\trequire.NoError(t, err)\n\n\ttgOptions.WorkingDir = tmpPath\n\n\tfilters, parseErr := filter.ParseFilterQueries(logger.CreateLogger(), []string{\"./a/b/**\"})\n\trequire.NoError(t, parseErr)\n\n\ttgOptions.Filters = filters\n\n\terr = format.Run(t.Context(), logger.CreateLogger(), tgOptions)\n\trequire.NoError(t, err)\n\n\tt.Run(\"group\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tformattedDirs := []string{\n\t\t\t\"a/b/c/terragrunt.hcl\",\n\t\t\t\"a/b/c/d/services.hcl\",\n\t\t\t\"a/b/c/d/e/terragrunt.hcl\",\n\t\t}\n\t\tfor _, dir := range formattedDirs {\n\t\t\tt.Run(dir, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\n\t\t\t\ttgHclPath := filepath.Join(tmpPath, dir)\n\t\t\t\tactual, err := util.ReadFileAsString(tgHclPath)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, expected, actual, \"File %s should be formatted\", dir)\n\t\t\t})\n\t\t}\n\n\t\tunformattedDirs := []string{\n\t\t\t\"terragrunt.hcl\",\n\t\t\t\"a/terragrunt.hcl\",\n\t\t}\n\t\tfor _, dir := range unformattedDirs {\n\t\t\tt.Run(dir, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\n\t\t\t\ttgHclPath := filepath.Join(tmpPath, dir)\n\t\t\t\tactual, err := util.ReadFileAsString(tgHclPath)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, original, actual, \"File %s should NOT be formatted\", dir)\n\t\t\t})\n\t\t}\n\t})\n}\n\nfunc TestHCLFmtFilterMultiple(t *testing.T) {\n\tt.Parallel()\n\n\ttmpPath, err := util.CopyFolderToTemp(\"./testdata/fixtures\", t.Name(), func(path string) bool { return true })\n\n\tt.Cleanup(func() {\n\t\tos.RemoveAll(tmpPath)\n\t})\n\n\trequire.NoError(t, err)\n\n\texpected, err := util.ReadFileAsString(\"./testdata/fixtures/expected.hcl\")\n\trequire.NoError(t, err)\n\n\toriginal, err := util.ReadFileAsString(\"./testdata/fixtures/terragrunt.hcl\")\n\trequire.NoError(t, err)\n\n\ttgOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\terr = tgOptions.Experiments.EnableExperiment(\"filter-flag\")\n\trequire.NoError(t, err)\n\n\ttgOptions.WorkingDir = tmpPath\n\n\tfilters, parseErr := filter.ParseFilterQueries(logger.CreateLogger(), []string{\n\t\tfilepath.Join(tmpPath, \"terragrunt.hcl\"),\n\t\t\"./a/b/c/d/e/**\",\n\t})\n\trequire.NoError(t, parseErr)\n\n\ttgOptions.Filters = filters\n\n\terr = format.Run(t.Context(), logger.CreateLogger(), tgOptions)\n\trequire.NoError(t, err)\n\n\tt.Run(\"group\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tformattedDirs := []string{\n\t\t\t\"terragrunt.hcl\",\n\t\t\t\"a/b/c/d/e/terragrunt.hcl\",\n\t\t}\n\t\tfor _, dir := range formattedDirs {\n\t\t\tt.Run(dir, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\n\t\t\t\ttgHclPath := filepath.Join(tmpPath, dir)\n\t\t\t\tactual, err := util.ReadFileAsString(tgHclPath)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, expected, actual, \"File %s should be formatted\", dir)\n\t\t\t})\n\t\t}\n\n\t\tunformattedDirs := []string{\n\t\t\t\"a/terragrunt.hcl\",\n\t\t\t\"a/b/c/terragrunt.hcl\",\n\t\t\t\"a/b/c/d/services.hcl\",\n\t\t}\n\t\tfor _, dir := range unformattedDirs {\n\t\t\tt.Run(dir, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\n\t\t\t\ttgHclPath := filepath.Join(tmpPath, dir)\n\t\t\t\tactual, err := util.ReadFileAsString(tgHclPath)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, original, actual, \"File %s should NOT be formatted\", dir)\n\t\t\t})\n\t\t}\n\t})\n}\n\nfunc TestHCLFmtFilterNegation(t *testing.T) {\n\tt.Parallel()\n\n\ttmpPath, err := util.CopyFolderToTemp(\"./testdata/fixtures\", t.Name(), func(path string) bool { return true })\n\n\tt.Cleanup(func() {\n\t\tos.RemoveAll(tmpPath)\n\t})\n\n\trequire.NoError(t, err)\n\n\texpected, err := util.ReadFileAsString(\"./testdata/fixtures/expected.hcl\")\n\trequire.NoError(t, err)\n\n\toriginal, err := util.ReadFileAsString(\"./testdata/fixtures/terragrunt.hcl\")\n\trequire.NoError(t, err)\n\n\ttgOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\terr = tgOptions.Experiments.EnableExperiment(\"filter-flag\")\n\trequire.NoError(t, err)\n\n\ttgOptions.WorkingDir = tmpPath\n\n\tfilters, parseErr := filter.ParseFilterQueries(logger.CreateLogger(), []string{\n\t\t\"./a/**\",\n\t\t\"!./a/b/c/d/**\",\n\t})\n\trequire.NoError(t, parseErr)\n\n\ttgOptions.Filters = filters\n\n\terr = format.Run(t.Context(), logger.CreateLogger(), tgOptions)\n\trequire.NoError(t, err)\n\n\tt.Run(\"group\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tformattedDirs := []string{\n\t\t\t\"a/terragrunt.hcl\",\n\t\t\t\"a/b/c/terragrunt.hcl\",\n\t\t}\n\t\tfor _, dir := range formattedDirs {\n\t\t\tt.Run(dir, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\n\t\t\t\ttgHclPath := filepath.Join(tmpPath, dir)\n\t\t\t\tactual, err := util.ReadFileAsString(tgHclPath)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, expected, actual, \"File %s should be formatted\", dir)\n\t\t\t})\n\t\t}\n\n\t\tunformattedDirs := []string{\n\t\t\t\"terragrunt.hcl\",\n\t\t\t\"a/b/c/d/services.hcl\",\n\t\t\t\"a/b/c/d/e/terragrunt.hcl\",\n\t\t}\n\t\tfor _, dir := range unformattedDirs {\n\t\t\tt.Run(dir, func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\n\t\t\t\ttgHclPath := filepath.Join(tmpPath, dir)\n\t\t\t\tactual, err := util.ReadFileAsString(tgHclPath)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, original, actual, \"File %s should NOT be formatted\", dir)\n\t\t\t})\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/cli/commands/hcl/format/testdata/fixtures/a/b/c/d/e/terragrunt.hcl",
    "content": "inputs = {\n# comments\n  foo =                               \"bar\"\n  bar=\"baz\"\n\n  inputs = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n\"foo\",\n\"bar\",\n]\n}\n"
  },
  {
    "path": "internal/cli/commands/hcl/format/testdata/fixtures/a/b/c/d/services.hcl",
    "content": "inputs = {\n# comments\n  foo =                               \"bar\"\n  bar=\"baz\"\n\n  inputs = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n\"foo\",\n\"bar\",\n]\n}\n"
  },
  {
    "path": "internal/cli/commands/hcl/format/testdata/fixtures/a/b/c/terragrunt.hcl",
    "content": "inputs = {\n# comments\n  foo =                               \"bar\"\n  bar=\"baz\"\n\n  inputs = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n\"foo\",\n\"bar\",\n]\n}\n"
  },
  {
    "path": "internal/cli/commands/hcl/format/testdata/fixtures/a/terragrunt.hcl",
    "content": "inputs = {\n# comments\n  foo =                               \"bar\"\n  bar=\"baz\"\n\n  inputs = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n\"foo\",\n\"bar\",\n]\n}\n"
  },
  {
    "path": "internal/cli/commands/hcl/format/testdata/fixtures/expected.hcl",
    "content": "inputs = {\n  # comments\n  foo = \"bar\"\n  bar = \"baz\"\n\n  inputs   = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n    \"foo\",\n    \"bar\",\n  ]\n}\n"
  },
  {
    "path": "internal/cli/commands/hcl/format/testdata/fixtures/ignored/.gitignore",
    "content": "!.terragrunt-cache\n"
  },
  {
    "path": "internal/cli/commands/hcl/format/testdata/fixtures/ignored/.history/terragrunt.hcl",
    "content": "inputs = {\n# comments\n  foo =                               \"bar\"\n  bar=\"baz\"\n\n  inputs = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n\"foo\",\n\"bar\",\n]\n}\n"
  },
  {
    "path": "internal/cli/commands/hcl/format/testdata/fixtures/ignored/.terragrunt-cache/terragrunt.hcl",
    "content": "inputs = {\n# comments\n  foo =                               \"bar\"\n  bar=\"baz\"\n\n  inputs = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n\"foo\",\n\"bar\",\n]\n}\n"
  },
  {
    "path": "internal/cli/commands/hcl/format/testdata/fixtures/terragrunt.hcl",
    "content": "inputs = {\n# comments\n  foo =                               \"bar\"\n  bar=\"baz\"\n\n  inputs = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n\"foo\",\n\"bar\",\n]\n}\n"
  },
  {
    "path": "internal/cli/commands/hcl/validate/cli.go",
    "content": "package validate\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tCommandName = \"validate\"\n\n\tStrictFlagName         = \"strict\"\n\tInputsFlagName         = \"inputs\"\n\tShowConfigPathFlagName = \"show-config-path\"\n\tJSONFlagName           = \"json\"\n)\n\nfunc NewFlags(l log.Logger, opts *options.TerragruntOptions) clihelper.Flags {\n\ttgPrefix := flags.Prefix{flags.TgPrefix}\n\tterragruntPrefix := flags.Prefix{flags.TerragruntPrefix}\n\tterragruntPrefixControl := flags.StrictControlsByCommand(opts.StrictControls, CommandName)\n\n\tflagSet := clihelper.Flags{\n\t\tflags.NewFlag(\n\t\t\t&clihelper.BoolFlag{\n\t\t\t\tName:        StrictFlagName,\n\t\t\t\tEnvVars:     tgPrefix.EnvVars(StrictFlagName),\n\t\t\t\tDestination: &opts.HCLValidateStrict,\n\t\t\t\tUsage:       \"Enables strict mode. When used in combination with the `--inputs` flag, any inputs defined in Terragrunt that are _not_ used in OpenTofu/Terraform will trigger an error.\",\n\t\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(tgPrefix.EnvVars(\n\t\t\t\t\"strict-validate\",             // `TG_STRICT_VALIDATE`\n\t\t\t\t\"hclvalidate-strict-validate\", // `TG_HCLVALIDATE_STRICT_VALIDATE`\n\t\t\t), terragruntPrefixControl),\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"strict-validate\"), terragruntPrefixControl), // `TERRAGRUNT_STRICT_VALIDATE`\n\t\t),\n\n\t\tflags.NewFlag(\n\t\t\t&clihelper.BoolFlag{\n\t\t\t\tName:        InputsFlagName,\n\t\t\t\tEnvVars:     tgPrefix.EnvVars(InputsFlagName),\n\t\t\t\tDestination: &opts.HCLValidateInputs,\n\t\t\t\tUsage:       \"Checks if the Terragrunt configured inputs align with OpenTofu/Terraform defined variables.\",\n\t\t\t},\n\t\t),\n\n\t\tflags.NewFlag(\n\t\t\t&clihelper.BoolFlag{\n\t\t\t\tName:        ShowConfigPathFlagName,\n\t\t\t\tEnvVars:     tgPrefix.EnvVars(ShowConfigPathFlagName),\n\t\t\t\tUsage:       \"Emit a list of files with invalid configurations after validating all configurations.\",\n\t\t\t\tDestination: &opts.HCLValidateShowConfigPath,\n\t\t\t},\n\n\t\t\tflags.WithDeprecatedEnvVars(tgPrefix.EnvVars(\"hclvalidate-strict-validate\"), terragruntPrefixControl),          // `TG_HCLVALIDATE_STRICT_VALIDATE`\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"hclvalidate-show-config-path\"), terragruntPrefixControl), // `TERRAGRUNT_HCLVALIDATE_SHOW_CONFIG_PATH`\n\t\t),\n\n\t\tflags.NewFlag(\n\t\t\t&clihelper.BoolFlag{\n\t\t\t\tName:        JSONFlagName,\n\t\t\t\tEnvVars:     tgPrefix.EnvVars(JSONFlagName),\n\t\t\t\tDestination: &opts.HCLValidateJSONOutput,\n\t\t\t\tUsage:       \"Format results in JSON format.\",\n\t\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(tgPrefix.EnvVars(\"hclvalidate-json\"), terragruntPrefixControl),         // `TG_HCLVALIDATE_JSON`\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"hclvalidate-json\"), terragruntPrefixControl), // `TERRAGRUNT_HCLVALIDATE_JSON`\n\t\t),\n\n\t\tshared.NewTFPathFlag(opts),\n\t}\n\n\tflagSet = flagSet.Add(shared.NewQueueFlags(opts, nil)...)\n\tflagSet = flagSet.Add(shared.NewFilterFlags(l, opts)...)\n\n\treturn flagSet\n}\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\tcmd := &clihelper.Command{\n\t\tName:                         CommandName,\n\t\tUsage:                        \"Recursively find HashiCorp Configuration Language (HCL) files and validate them.\",\n\t\tFlags:                        NewFlags(l, opts),\n\t\tDisabledErrorOnUndefinedFlag: true,\n\t\tAction: func(ctx context.Context, _ *clihelper.Context) error {\n\t\t\treturn Run(ctx, l, opts.OptionsFromContext(ctx))\n\t\t},\n\t}\n\n\treturn cmd\n}\n"
  },
  {
    "path": "internal/cli/commands/hcl/validate/validate.go",
    "content": "// Package validate-inputs collects all the terraform variables defined in the target module, and the terragrunt\n// inputs that are configured, and compare the two to determine if there are any unused inputs or undefined required\n// inputs.\npackage validate\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/worktrees\"\n\n\t\"github.com/google/shlex\"\n\t\"github.com/hashicorp/hcl/v2\"\n\n\t\"maps\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/prepare\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/internal/view\"\n\t\"github.com/gruntwork-io/terragrunt/internal/view/diagnostic\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst splitCount = 2\n\nfunc Run(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\tif opts.HCLValidateInputs {\n\t\tif opts.HCLValidateShowConfigPath {\n\t\t\treturn errors.Errorf(\"specifying both -%s and -%s is invalid\", ShowConfigPathFlagName, InputsFlagName)\n\t\t}\n\n\t\tif opts.HCLValidateJSONOutput {\n\t\t\treturn errors.Errorf(\"specifying both -%s and -%s is invalid\", JSONFlagName, InputsFlagName)\n\t\t}\n\n\t\treturn RunValidateInputs(ctx, l, opts)\n\t}\n\n\tif opts.HCLValidateStrict {\n\t\treturn errors.Errorf(\"specifying -%s without -%s is invalid\", StrictFlagName, InputsFlagName)\n\t}\n\n\treturn RunValidate(ctx, l, opts)\n}\n\nfunc RunValidate(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\tvar diags diagnostic.Diagnostics\n\n\t// Diagnostics handler to collect validation errors\n\tdiagnosticsHandler := hclparse.WithDiagnosticsHandler(func(file *hcl.File, hclDiags hcl.Diagnostics) (hcl.Diagnostics, error) {\n\t\tfor _, hclDiag := range hclDiags {\n\t\t\t// Only report diagnostics that are actually in the file being parsed,\n\t\t\t// not errors from dependencies or other files\n\t\t\tif hclDiag.Subject != nil && file != nil {\n\t\t\t\tfileFilename := file.Body.MissingItemRange().Filename\n\n\t\t\t\tdiagFilename := hclDiag.Subject.Filename\n\t\t\t\tif diagFilename != fileFilename {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tnewDiag := diagnostic.NewDiagnostic(file, hclDiag)\n\t\t\tif !diags.Contains(newDiag) {\n\t\t\t\tdiags = append(diags, newDiag)\n\t\t\t}\n\t\t}\n\n\t\treturn nil, nil\n\t})\n\n\topts.SkipOutput = true\n\topts.NonInteractive = true\n\n\t// Create discovery with filter support if experiment enabled\n\td, err := discovery.NewForHCLCommand(l, discovery.HCLCommandOptions{\n\t\tWorkingDir:  opts.WorkingDir,\n\t\tFilters:     opts.Filters,\n\t\tExperiments: opts.Experiments,\n\t})\n\tif err != nil {\n\t\treturn processDiagnostics(l, opts, diags, errors.New(err))\n\t}\n\n\t// We do worktree generation here instead of in the discovery constructor\n\t// so that we can defer cleanup in the same context.\n\tgitFilters := opts.Filters.UniqueGitFilters()\n\n\tworktrees, parseErr := worktrees.NewWorktrees(ctx, l, opts.WorkingDir, gitFilters)\n\tif parseErr != nil {\n\t\treturn errors.Errorf(\"failed to create worktrees: %w\", parseErr)\n\t}\n\n\tdefer func() {\n\t\tcleanupErr := worktrees.Cleanup(ctx, l)\n\t\tif cleanupErr != nil {\n\t\t\tl.Errorf(\"failed to cleanup worktrees: %v\", cleanupErr)\n\t\t}\n\t}()\n\n\td = d.WithWorktrees(worktrees)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\tif err != nil {\n\t\treturn processDiagnostics(l, opts, diags, errors.New(err))\n\t}\n\n\tparseOptions := []hclparse.Option{diagnosticsHandler}\n\n\tparseErrs := []error{}\n\n\tfor _, c := range components {\n\t\tparseOpts := opts.Clone()\n\t\tparseOpts.WorkingDir = c.Path()\n\n\t\tif _, ok := c.(*component.Stack); ok {\n\t\t\tstackFilePath := filepath.Join(c.Path(), config.DefaultStackFile)\n\t\t\tparseOpts.TerragruntConfigPath = stackFilePath\n\n\t\t\tctx, parser := configbridge.NewParsingContext(ctx, l, parseOpts)\n\n\t\t\tvalues, err := config.ReadValues(ctx, parser, l, c.Path())\n\t\t\tif err != nil {\n\t\t\t\tparseErrs = append(parseErrs, errors.New(err))\n\t\t\t}\n\n\t\t\tparser = parser.WithParseOption(parseOptions)\n\t\t\tif values != nil {\n\t\t\t\tparser = parser.WithValues(values)\n\t\t\t}\n\n\t\t\tfile, err := hclparse.NewParser(parser.ParserOptions...).ParseFromFile(stackFilePath)\n\t\t\tif err != nil {\n\t\t\t\tparseErrs = append(parseErrs, errors.New(err))\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif _, err := config.ParseStackConfig(ctx, l, parser, file, values); err != nil {\n\t\t\t\tparseErrs = append(parseErrs, errors.New(err))\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\t// Determine which config filename to use for a full parse\n\t\tconfigFilename := config.DefaultTerragruntConfigPath\n\t\tif len(opts.TerragruntConfigPath) > 0 {\n\t\t\tconfigFilename = filepath.Base(opts.TerragruntConfigPath)\n\t\t}\n\n\t\tparseOpts.TerragruntConfigPath = filepath.Join(c.Path(), configFilename)\n\n\t\t_, pctx := configbridge.NewParsingContext(ctx, l, parseOpts)\n\t\tif _, err := config.ReadTerragruntConfig(ctx, l, pctx, parseOptions); err != nil {\n\t\t\tparseErrs = append(parseErrs, errors.New(err))\n\t\t}\n\t}\n\n\tvar combinedErr error\n\tif len(parseErrs) > 0 {\n\t\tcombinedErr = errors.Join(parseErrs...)\n\t}\n\n\treturn processDiagnostics(l, opts, diags, combinedErr)\n}\n\nfunc processDiagnostics(l log.Logger, opts *options.TerragruntOptions, diags diagnostic.Diagnostics, callErr error) error {\n\tif len(diags) == 0 {\n\t\treturn callErr\n\t}\n\n\tsort.Slice(diags, func(i, j int) bool {\n\t\tvar a, b string\n\n\t\tif diags[i].Range != nil {\n\t\t\ta = diags[i].Range.Filename\n\t\t}\n\n\t\tif diags[j].Range != nil {\n\t\t\tb = diags[j].Range.Filename\n\t\t}\n\n\t\treturn a < b\n\t})\n\n\tif err := writeDiagnostics(l, opts, diags); err != nil {\n\t\treturn err\n\t}\n\n\tdiagError := errors.Errorf(\"%d HCL validation error(s) found\", len(diags))\n\n\t// If diagnostics exist and no other error was returned,\n\t// return a synthetic error to mark validation as failed and\n\t// ensure a non-zero exit code from Terragrunt.\n\tif callErr == nil {\n\t\treturn diagError\n\t}\n\n\treturn errors.Join(callErr, diagError)\n}\n\nfunc writeDiagnostics(l log.Logger, opts *options.TerragruntOptions, diags diagnostic.Diagnostics) error {\n\trender := view.NewHumanRender(l.Formatter().DisabledColors())\n\tif opts.HCLValidateJSONOutput {\n\t\trender = view.NewJSONRender()\n\t}\n\n\twriter := view.NewWriter(opts.Writers.Writer, render)\n\n\tif opts.HCLValidateShowConfigPath {\n\t\treturn writer.ShowConfigPath(diags)\n\t}\n\n\treturn writer.Diagnostics(diags)\n}\n\nfunc RunValidateInputs(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\topts = opts.Clone()\n\n\topts.SkipOutput = true\n\topts.NonInteractive = true\n\n\td, err := discovery.NewForHCLCommand(l, discovery.HCLCommandOptions{\n\t\tWorkingDir:  opts.WorkingDir,\n\t\tFilters:     opts.Filters,\n\t\tExperiments: opts.Experiments,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif opts.Experiments.Evaluate(experiment.FilterFlag) {\n\t\tgitFilters := opts.Filters.UniqueGitFilters()\n\n\t\tworktrees, worktreeErr := worktrees.NewWorktrees(ctx, l, opts.WorkingDir, gitFilters)\n\t\tif worktreeErr != nil {\n\t\t\treturn errors.Errorf(\"failed to create worktrees: %w\", worktreeErr)\n\t\t}\n\n\t\tdefer func() {\n\t\t\tcleanupErr := worktrees.Cleanup(ctx, l)\n\t\t\tif cleanupErr != nil {\n\t\t\t\tl.Errorf(\"failed to cleanup worktrees: %v\", cleanupErr)\n\t\t\t}\n\t\t}()\n\n\t\td = d.WithWorktrees(worktrees)\n\t}\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tr := report.NewReport()\n\n\tvar errs []error\n\n\tfor _, c := range components {\n\t\t// Skip stacks, only validate inputs for units\n\t\tif _, ok := c.(*component.Stack); ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tunitOpts := opts.Clone()\n\t\tunitOpts.WorkingDir = c.Path()\n\n\t\tconfigFilename := config.DefaultTerragruntConfigPath\n\t\tif len(opts.TerragruntConfigPath) > 0 {\n\t\t\tconfigFilename = filepath.Base(opts.TerragruntConfigPath)\n\t\t}\n\n\t\tunitOpts.TerragruntConfigPath = filepath.Join(c.Path(), configFilename)\n\n\t\tprepared, err := prepare.PrepareConfig(ctx, l, unitOpts)\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Download source\n\t\tupdatedOpts, err := prepare.PrepareSource(ctx, l, prepared.Opts, prepared.Cfg, r)\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Generate config\n\t\tif err := prepare.PrepareGenerate(l, updatedOpts, prepared.Cfg.ToRunConfig(l)); err != nil {\n\t\t\terrs = append(errs, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := runValidateInputs(l, updatedOpts, prepared.Cfg); err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn errors.Join(errs...)\n\t}\n\n\treturn nil\n}\n\nfunc runValidateInputs(l log.Logger, opts *options.TerragruntOptions, cfg *config.TerragruntConfig) error {\n\trequired, optional, err := tf.ModuleVariables(opts.WorkingDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tallVars := slices.Concat(required, optional)\n\n\tallInputs, err := getDefinedTerragruntInputs(l, opts, cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Unused variables are those that are passed in by terragrunt, but are not defined in terraform.\n\tunusedVars := []string{}\n\n\tfor _, varName := range allInputs {\n\t\tif !slices.Contains(allVars, varName) {\n\t\t\tunusedVars = append(unusedVars, varName)\n\t\t}\n\t}\n\n\t// Missing variables are those that are required by the terraform config, but not defined in terragrunt.\n\tmissingVars := []string{}\n\n\tfor _, varName := range required {\n\t\tif !slices.Contains(allInputs, varName) {\n\t\t\tmissingVars = append(missingVars, varName)\n\t\t}\n\t}\n\n\t// Now print out all the information\n\tif len(unusedVars) > 0 {\n\t\tl.Warn(\"The following inputs passed in by terragrunt are unused:\\n\")\n\n\t\tfor _, varName := range unusedVars {\n\t\t\tl.Warnf(\"\\t- %s\", varName)\n\t\t}\n\n\t\tl.Warn(\"\")\n\t} else {\n\t\tl.Info(\"All variables passed in by terragrunt are in use.\")\n\t\tl.Debug(fmt.Sprintf(\"Strict mode enabled: %t\", opts.HCLValidateStrict))\n\t}\n\n\tif len(missingVars) > 0 {\n\t\tl.Error(\"The following required inputs are missing:\\n\")\n\n\t\tfor _, varName := range missingVars {\n\t\t\tl.Errorf(\"\\t- %s\", varName)\n\t\t}\n\n\t\tl.Error(\"\")\n\t} else {\n\t\tl.Info(\"All required inputs are passed in by terragrunt\")\n\t\tl.Debug(fmt.Sprintf(\"Strict mode enabled: %t\", opts.HCLValidateStrict))\n\t}\n\n\t// Return an error when there are misaligned inputs. Terragrunt strict mode defaults to false. When it is false,\n\t// an error will only be returned if required inputs are missing. When strict mode is true, an error will be\n\t// returned if required inputs are missing OR if any unused variables are passed\n\tif len(missingVars) > 0 || len(unusedVars) > 0 && opts.HCLValidateStrict {\n\t\treturn errors.New(\"terragrunt configuration has inputs that are not defined in the OpenTofu/Terraform module. This is not allowed when strict mode is enabled\")\n\t} else if len(unusedVars) > 0 {\n\t\tl.Warn(\"Terragrunt configuration has misaligned inputs, but running in relaxed mode so ignoring.\")\n\t}\n\n\treturn nil\n}\n\n// getDefinedTerragruntInputs will return a list of names of all variables that are configured by terragrunt to be\n// passed into terraform. Terragrunt can pass in inputs from:\n// - var files defined on terraform.extra_arguments blocks.\n// - -var and -var-file args passed in on extra_arguments CLI args.\n// - env vars defined on terraform.extra_arguments blocks.\n// - env vars from the external runtime calling terragrunt.\n// - inputs blocks.\n// - automatically injected terraform vars (terraform.tfvars, terraform.tfvars.json, *.auto.tfvars, *.auto.tfvars.json)\nfunc getDefinedTerragruntInputs(l log.Logger, opts *options.TerragruntOptions, cfg *config.TerragruntConfig) ([]string, error) {\n\tenvVarTFVars := getTerraformInputNamesFromEnvVar(opts, cfg)\n\tinputsTFVars := getTerraformInputNamesFromConfig(cfg)\n\n\tvarFileTFVars, err := getTerraformInputNamesFromVarFiles(l, cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcliArgsTFVars, err := getTerraformInputNamesFromCLIArgs(l, opts, cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tautoVarFileTFVars, err := getTerraformInputNamesFromAutomaticVarFiles(l, opts)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Dedupe the input vars. We use a map as a set to accomplish this.\n\ttmpOut := map[string]bool{}\n\tfor _, varName := range envVarTFVars {\n\t\ttmpOut[varName] = true\n\t}\n\n\tfor _, varName := range inputsTFVars {\n\t\ttmpOut[varName] = true\n\t}\n\n\tfor _, varName := range varFileTFVars {\n\t\ttmpOut[varName] = true\n\t}\n\n\tfor _, varName := range cliArgsTFVars {\n\t\ttmpOut[varName] = true\n\t}\n\n\tfor _, varName := range autoVarFileTFVars {\n\t\ttmpOut[varName] = true\n\t}\n\n\tout := []string{}\n\tfor varName := range tmpOut {\n\t\tout = append(out, varName)\n\t}\n\n\treturn out, nil\n}\n\n// getTerraformInputNamesFromEnvVar will check the runtime environment variables and the configured environment\n// variables from extra_arguments blocks to see if there are any TF_VAR environment variables that set terraform\n// variables. This will return the list of names of variables that are set in this way by the given terragrunt\n// configuration.\nfunc getTerraformInputNamesFromEnvVar(opts *options.TerragruntOptions, terragruntConfig *config.TerragruntConfig) []string {\n\tenvVars := opts.Env\n\n\t// Make sure to check if there are configured env vars in the parsed terragrunt config.\n\tif terragruntConfig.Terraform != nil {\n\t\tfor _, arg := range terragruntConfig.Terraform.ExtraArgs {\n\t\t\tif arg.EnvVars != nil {\n\t\t\t\tmaps.Copy(envVars, *arg.EnvVars)\n\t\t\t}\n\t\t}\n\t}\n\n\tvar (\n\t\tout         = []string{}\n\t\ttfVarPrefix = fmt.Sprintf(tf.EnvNameTFVarFmt, \"\")\n\t)\n\n\tfor envName := range envVars {\n\t\tif after, ok := strings.CutPrefix(envName, tfVarPrefix); ok {\n\t\t\tinputName := after\n\t\t\tout = append(out, inputName)\n\t\t}\n\t}\n\n\treturn out\n}\n\n// getTerraformInputNamesFromConfig will return the list of names of variables configured by the inputs block in the\n// terragrunt config.\nfunc getTerraformInputNamesFromConfig(terragruntConfig *config.TerragruntConfig) []string {\n\tout := make([]string, 0, len(terragruntConfig.Inputs))\n\tfor inputName := range terragruntConfig.Inputs {\n\t\tout = append(out, inputName)\n\t}\n\n\treturn out\n}\n\n// getTerraformInputNamesFromVarFiles will return the list of names of variables configured by var files set in the\n// extra_arguments block required_var_files and optional_var_files settings of the given terragrunt config.\nfunc getTerraformInputNamesFromVarFiles(l log.Logger, terragruntConfig *config.TerragruntConfig) ([]string, error) {\n\tif terragruntConfig.Terraform == nil {\n\t\treturn nil, nil\n\t}\n\n\tvarFiles := []string{}\n\tfor _, arg := range terragruntConfig.Terraform.ExtraArgs {\n\t\tvarFiles = append(varFiles, arg.GetVarFiles(l)...)\n\t}\n\n\treturn getVarNamesFromVarFiles(l, varFiles)\n}\n\n// getTerraformInputNamesFromCLIArgs will return the list of names of variables configured by -var and -var-file CLI\n// args that are passed in via the configured arguments attribute in the extra_arguments block of the given terragrunt\n// config and those that are directly passed in via the CLI.\nfunc getTerraformInputNamesFromCLIArgs(l log.Logger, opts *options.TerragruntOptions, terragruntConfig *config.TerragruntConfig) ([]string, error) {\n\tinputNames, varFiles, err := GetVarFlagsFromArgList(opts.TerraformCliArgs.Slice())\n\tif err != nil {\n\t\treturn inputNames, err\n\t}\n\n\tif terragruntConfig.Terraform != nil {\n\t\tfor _, arg := range terragruntConfig.Terraform.ExtraArgs {\n\t\t\tif arg.Arguments != nil {\n\t\t\t\tvars, rawVarFiles, getArgsErr := GetVarFlagsFromArgList(*arg.Arguments)\n\t\t\t\tif getArgsErr != nil {\n\t\t\t\t\treturn inputNames, getArgsErr\n\t\t\t\t}\n\n\t\t\t\tinputNames = append(inputNames, vars...)\n\t\t\t\tvarFiles = append(varFiles, rawVarFiles...)\n\t\t\t}\n\t\t}\n\t}\n\n\tfileVars, err := getVarNamesFromVarFiles(l, varFiles)\n\tif err != nil {\n\t\treturn inputNames, err\n\t}\n\n\tinputNames = append(inputNames, fileVars...)\n\n\treturn inputNames, nil\n}\n\n// getTerraformInputNamesFromAutomaticVarFiles returns all the variables names\nfunc getTerraformInputNamesFromAutomaticVarFiles(l log.Logger, opts *options.TerragruntOptions) ([]string, error) {\n\tbase := opts.WorkingDir\n\tautomaticVarFiles := []string{}\n\n\ttfTFVarsFile := filepath.Join(base, \"terraform.tfvars\")\n\tif util.FileExists(tfTFVarsFile) {\n\t\tautomaticVarFiles = append(automaticVarFiles, tfTFVarsFile)\n\t}\n\n\ttfTFVarsJSONFile := filepath.Join(base, \"terraform.tfvars.json\")\n\tif util.FileExists(tfTFVarsJSONFile) {\n\t\tautomaticVarFiles = append(automaticVarFiles, tfTFVarsJSONFile)\n\t}\n\n\tvarFiles, err := filepath.Glob(filepath.Join(base, \"*.auto.tfvars\"))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tautomaticVarFiles = append(automaticVarFiles, varFiles...)\n\n\tjsonVarFiles, err := filepath.Glob(filepath.Join(base, \"*.auto.tfvars.json\"))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tautomaticVarFiles = append(automaticVarFiles, jsonVarFiles...)\n\n\treturn getVarNamesFromVarFiles(l, automaticVarFiles)\n}\n\n// getVarNamesFromVarFiles will parse all the given var files and returns a list of names of variables that are\n// configured in all of them combined together.\nfunc getVarNamesFromVarFiles(l log.Logger, varFiles []string) ([]string, error) {\n\tinputNames := []string{}\n\n\tfor _, varFile := range varFiles {\n\t\tfileVars, err := getVarNamesFromVarFile(l, varFile)\n\t\tif err != nil {\n\t\t\treturn inputNames, err\n\t\t}\n\n\t\tinputNames = append(inputNames, fileVars...)\n\t}\n\n\treturn inputNames, nil\n}\n\n// getVarNamesFromVarFile will parse the given terraform var file and return a list of names of variables that are\n// configured in that var file.\nfunc getVarNamesFromVarFile(l log.Logger, varFile string) ([]string, error) {\n\tfileContents, err := os.ReadFile(varFile)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar variables map[string]any\n\tif strings.HasSuffix(varFile, \"json\") {\n\t\tif err := json.Unmarshal(fileContents, &variables); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tif err := config.ParseAndDecodeVarFile(l, varFile, fileContents, &variables); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tout := []string{}\n\tfor varName := range variables {\n\t\tout = append(out, varName)\n\t}\n\n\treturn out, nil\n}\n\n// GetVarFlagsFromArgList returns the CLI flags defined on the provided arguments list that correspond to -var and -var-file.\n// Returns two slices, one for `-var` args (the first one) and one for `-var-file` args (the second one).\nfunc GetVarFlagsFromArgList(argList []string) ([]string, []string, error) {\n\tvars := []string{}\n\tvarFiles := []string{}\n\n\tfor _, arg := range argList {\n\t\t// Use shlex to handle shell style quoting rules. This will reduce quoted args to remove quoting rules. For\n\t\t// example, the string:\n\t\t// -var=\"'\"foo\"'\"='bar'\n\t\t// becomes:\n\t\t// -var='foo'=bar\n\t\tshlexedArgSlice, err := shlex.Split(arg)\n\t\tif err != nil {\n\t\t\treturn vars, varFiles, err\n\t\t}\n\t\t// Since we expect each element in extra_args.arguments to correspond to a single arg for terraform, we join\n\t\t// back the shlex split slice even if it thinks there are multiple.\n\t\tshlexedArg := strings.Join(shlexedArgSlice, \" \")\n\n\t\tif strings.HasPrefix(shlexedArg, \"-var=\") {\n\t\t\t// -var is passed in in the format -var=VARNAME=VALUE, so we split on '=' and take the middle value.\n\t\t\tsplitArg := strings.Split(shlexedArg, \"=\")\n\t\t\tif len(splitArg) < splitCount {\n\t\t\t\treturn vars, varFiles, fmt.Errorf(\"unexpected -var arg format in terraform.extra_arguments.arguments. Expected '-var=VARNAME=VALUE', got %s\", arg)\n\t\t\t}\n\n\t\t\tvars = append(vars, splitArg[1])\n\t\t}\n\n\t\tif after, ok := strings.CutPrefix(shlexedArg, \"-var-file=\"); ok {\n\t\t\tvarFiles = append(varFiles, after)\n\t\t}\n\t}\n\n\treturn vars, varFiles, nil\n}\n"
  },
  {
    "path": "internal/cli/commands/hcl/validate/validate_test.go",
    "content": "package validate_test\n\nimport (\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/hcl/validate\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetVarFlagsFromExtraArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname             string\n\t\targs             []string\n\t\texpectedVars     []string\n\t\texpectedVarFiles []string\n\t}{\n\t\t{\n\t\t\t\"VarsWithQuotes\",\n\t\t\t[]string{`-var='hello=world'`, `-var=\"foo=bar\"`, `-var=\"'\"enabled\"'\"=false`},\n\t\t\t[]string{\"'enabled'\", \"foo\", \"hello\"},\n\t\t\t[]string{},\n\t\t},\n\t\t{\n\t\t\t\"VarFilesWithQuotes\",\n\t\t\t[]string{`-var-file='terraform.tfvars'`, `-var-file=\"other_vars.tfvars\"`},\n\t\t\t[]string{},\n\t\t\t[]string{\"other_vars.tfvars\", \"terraform.tfvars\"},\n\t\t},\n\t\t{\n\t\t\t\"MixedWithOtherIrrelevantArgs\",\n\t\t\t[]string{\"-lock=true\", \"-var=enabled=true\", \"-refresh=false\"},\n\t\t\t[]string{\"enabled\"},\n\t\t\t[]string{},\n\t\t},\n\t\t{\n\t\t\t\"None\",\n\t\t\t[]string{\"-lock=true\", \"-refresh=false\"},\n\t\t\t[]string{},\n\t\t\t[]string{},\n\t\t},\n\t\t{\n\t\t\t\"SpaceInVarFileName\",\n\t\t\t[]string{\"-var-file='this is a test.tfvars'\"},\n\t\t\t[]string{},\n\t\t\t[]string{\"this is a test.tfvars\"},\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.Parallel()\n\n\t\t\tvars, varFiles, err := validate.GetVarFlagsFromArgList(tc.args)\n\t\t\trequire.NoError(t, err)\n\t\t\tsort.Strings(vars)\n\t\t\tsort.Strings(varFiles)\n\t\t\tassert.Equal(t, tc.expectedVars, vars)\n\t\t\tassert.Equal(t, tc.expectedVarFiles, varFiles)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/help/cli.go",
    "content": "// Package help represents the help CLI command that works the same as the `--help` flag.\npackage help\n\nimport (\n\t\"context\"\n\t\"os\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tCommandName = \"help\"\n)\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\treturn &clihelper.Command{\n\t\tName:                         CommandName,\n\t\tUsage:                        \"Show help.\",\n\t\tHidden:                       true,\n\t\tDisabledErrorOnUndefinedFlag: true,\n\t\tAction: func(ctx context.Context, cliCtx *clihelper.Context) error {\n\t\t\treturn Action(ctx, cliCtx, l, opts)\n\t\t},\n\t}\n}\n\nfunc Action(ctx context.Context, cliCtx *clihelper.Context, l log.Logger, _ *options.TerragruntOptions) error {\n\tvar (\n\t\targs = cliCtx.Args()\n\t\tcmds = cliCtx.Commands\n\t)\n\n\tif l.Level() >= log.DebugLevel {\n\t\t// https: //github.com/urfave/cli/blob/f035ffaa3749afda2cd26fb824aa940747297ef1/help.go#L401\n\t\tif err := os.Setenv(\"CLI_TEMPLATE_ERROR_DEBUG\", \"1\"); err != nil {\n\t\t\treturn errors.Errorf(\"failed to set CLI_TEMPLATE_ERROR_DEBUG environment variable: %w\", err)\n\t\t}\n\t}\n\n\tif cmdName := args.CommandName(); cmdName == \"\" || cmds.Get(cmdName) == nil {\n\t\treturn clihelper.ShowAppHelp(ctx, cliCtx)\n\t}\n\n\tconst maxCommandDepth = 1000 // Maximum depth of nested subcommands\n\n\tfor i := 0; i < maxCommandDepth && args.Len() > 0; i++ {\n\t\tcmdName := args.CommandName()\n\n\t\tcmd := cmds.Get(cmdName)\n\t\tif cmd == nil {\n\t\t\tbreak\n\t\t}\n\n\t\targs = args.Remove(cmdName)\n\t\tcmds = cmd.Subcommands\n\t\tcliCtx = cliCtx.NewCommandContext(cmd, args)\n\t}\n\n\tif cliCtx.Command != nil {\n\t\treturn clihelper.ShowCommandHelp(ctx, cliCtx)\n\t}\n\n\treturn clihelper.NewExitError(errors.New(clihelper.InvalidCommandNameError(args.First())), clihelper.ExitCodeGeneralError)\n}\n"
  },
  {
    "path": "internal/cli/commands/info/cli.go",
    "content": "// Package info represents list of info commands that display various Terragrunt settings.\npackage info\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/info/print\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/info/strict\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tCommandName = \"info\"\n)\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\treturn &clihelper.Command{\n\t\tName:  CommandName,\n\t\tUsage: \"List of commands to display Terragrunt settings.\",\n\t\tSubcommands: clihelper.Commands{\n\t\t\tstrict.NewCommand(l, opts),\n\t\t\tprint.NewCommand(l, opts),\n\t\t},\n\t\tAction: clihelper.ShowCommandHelp,\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/info/print/cli.go",
    "content": "package print\n\nimport (\n\t\"context\"\n\n\truncmd \"github.com/gruntwork-io/terragrunt/internal/cli/commands/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tCommandName = \"print\"\n)\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\tcmdFlags := runcmd.NewFlags(l, opts, nil)\n\tcmdFlags = append(cmdFlags, shared.NewAllFlag(opts, nil))\n\n\tcmd := &clihelper.Command{\n\t\tName:      CommandName,\n\t\tUsage:     \"Print out a short description of Terragrunt context.\",\n\t\tUsageText: \"terragrunt info print\",\n\t\tFlags:     cmdFlags,\n\t\tAction: func(ctx context.Context, _ *clihelper.Context) error {\n\t\t\treturn Run(ctx, l, opts.OptionsFromContext(ctx))\n\t\t},\n\t}\n\n\treturn cmd\n}\n"
  },
  {
    "path": "internal/cli/commands/info/print/print.go",
    "content": "// Package print implements the 'terragrunt info print' command that outputs Terragrunt context\n// information in a structured JSON format. This includes configuration paths, working directories,\n// IAM roles, and other essential Terragrunt runtime information useful for debugging and\n// automation purposes.\npackage print\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/prepare\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nfunc Run(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\t// If --all flag is set, use discovery to find all units and print info for each one\n\tif opts.RunAll {\n\t\treturn runAll(ctx, l, opts)\n\t}\n\n\treturn runPrint(ctx, l, opts)\n}\n\nfunc runPrint(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\tprepared, err := prepare.PrepareConfig(ctx, l, opts)\n\tif err != nil {\n\t\t// Even on error, try to print what info we have\n\t\tl.Debugf(\"Fetching info with error: %v\", err)\n\n\t\tif printErr := printTerragruntContext(l, opts); printErr != nil {\n\t\t\tl.Errorf(\"Error printing info: %v\", printErr)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// Download source\n\tupdatedOpts, err := prepare.PrepareSource(ctx, l, prepared.Opts, prepared.Cfg, report.NewReport())\n\tif err != nil {\n\t\t// Even on error, try to print what info we have\n\t\tl.Debugf(\"Fetching info with error: %v\", err)\n\n\t\tif printErr := printTerragruntContext(l, opts); printErr != nil {\n\t\t\tl.Errorf(\"Error printing info: %v\", printErr)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn printTerragruntContext(l, updatedOpts)\n}\n\nfunc runAll(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\td := discovery.NewDiscovery(opts.WorkingDir)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tunits := components.Filter(component.UnitKind).Sort()\n\n\tvar errs []error\n\n\tfor _, unit := range units {\n\t\tunitOpts := opts.Clone()\n\t\tunitOpts.WorkingDir = unit.Path()\n\n\t\tconfigFilename := config.DefaultTerragruntConfigPath\n\t\tif len(opts.TerragruntConfigPath) > 0 {\n\t\t\tconfigFilename = filepath.Base(opts.TerragruntConfigPath)\n\t\t}\n\n\t\tunitOpts.TerragruntConfigPath = filepath.Join(unit.Path(), configFilename)\n\n\t\tif err := runPrint(ctx, l, unitOpts); err != nil {\n\t\t\tif opts.FailFast {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tl.Errorf(\"Print failed: %v\", err)\n\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn errors.Join(errs...)\n\t}\n\n\treturn nil\n}\n\n// InfoOutput represents the structured output of the info command\ntype InfoOutput struct {\n\tConfigPath       string `json:\"config_path\"`\n\tDownloadDir      string `json:\"download_dir\"`\n\tIAMRole          string `json:\"iam_role\"`\n\tTerraformBinary  string `json:\"terraform_binary\"`\n\tTerraformCommand string `json:\"terraform_command\"`\n\tWorkingDir       string `json:\"working_dir\"`\n}\n\nfunc printTerragruntContext(l log.Logger, opts *options.TerragruntOptions) error {\n\tgroup := InfoOutput{\n\t\tConfigPath:       opts.TerragruntConfigPath,\n\t\tDownloadDir:      opts.DownloadDir,\n\t\tIAMRole:          opts.IAMRoleOptions.RoleARN,\n\t\tTerraformBinary:  opts.TFPath,\n\t\tTerraformCommand: opts.TerraformCommand,\n\t\tWorkingDir:       opts.WorkingDir,\n\t}\n\n\tb, err := json.MarshalIndent(group, \"\", \"  \")\n\tif err != nil {\n\t\tl.Errorf(\"JSON error marshalling info\")\n\t\treturn errors.New(err)\n\t}\n\n\tif _, err := fmt.Fprintf(opts.Writers.Writer, \"%s\\n\", b); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/cli/commands/info/strict/command.go",
    "content": "// Package strict represents CLI command that displays Terragrunt's strict control settings.\n// Example usage:\n//\n//\tterragrunt info strict list        # List active strict controls\n//\tterragrunt info strict list --all  # List all strict controls\npackage strict\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/view\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/view/plaintext\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tCommandName = \"strict\"\n\n\tListCommandName = \"list\"\n\n\tShowAllFlagName = \"all\"\n)\n\nfunc NewListFlags(opts *options.TerragruntOptions, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\n\treturn clihelper.Flags{\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:    ShowAllFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(ShowAllFlagName),\n\t\t\tUsage:   \"Show all controls, including completed ones.\",\n\t\t}),\n\t}\n}\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\treturn &clihelper.Command{\n\t\tName:  CommandName,\n\t\tUsage: \"Command associated with strict control settings.\",\n\t\tSubcommands: clihelper.Commands{\n\t\t\t&clihelper.Command{\n\t\t\t\tName:      ListCommandName,\n\t\t\t\tFlags:     NewListFlags(opts, nil),\n\t\t\t\tUsage:     \"List the strict control settings.\",\n\t\t\t\tUsageText: \"terragrunt info strict list [options] <name>\",\n\t\t\t\tAction:    ListAction(opts),\n\t\t\t},\n\t\t},\n\t\tAction: clihelper.ShowCommandHelp,\n\t}\n}\n\nfunc ListAction(opts *options.TerragruntOptions) func(ctx context.Context, cliCtx *clihelper.Context) error {\n\treturn func(_ context.Context, cliCtx *clihelper.Context) error {\n\t\tvar allowedStatuses = []strict.Status{\n\t\t\tstrict.ActiveStatus,\n\t\t}\n\n\t\tif val, ok := cliCtx.Flag(ShowAllFlagName).Value().Get().(bool); ok && val {\n\t\t\tallowedStatuses = append(allowedStatuses, strict.CompletedStatus)\n\t\t}\n\n\t\tcontrols := opts.StrictControls.FilterByStatus(allowedStatuses...)\n\t\trender := plaintext.NewRender()\n\t\twriter := view.NewWriter(cliCtx.Writer, render)\n\n\t\tif name := cliCtx.Args().CommandName(); name != \"\" {\n\t\t\tcontrol := controls.Find(name)\n\t\t\tif control == nil {\n\t\t\t\treturn strict.NewInvalidControlNameError(controls.Names())\n\t\t\t}\n\n\t\t\treturn writer.DetailControl(control)\n\t\t}\n\n\t\treturn writer.List(controls)\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/list/cli.go",
    "content": "// Package list provides the ability to list Terragrunt configurations in your codebase\n// via the `terragrunt list` command.\npackage list\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tCommandName  = \"list\"\n\tCommandAlias = \"ls\"\n\n\tFormatFlagName = \"format\"\n\n\tTreeFlagName  = \"tree\"\n\tTreeFlagAlias = \"T\"\n\n\tLongFlagName  = \"long\"\n\tLongFlagAlias = \"l\"\n\n\tHiddenFlagName       = \"hidden\"\n\tNoHiddenFlagName     = \"no-hidden\"\n\tDependenciesFlagName = \"dependencies\"\n\tExternalFlagName     = \"external\"\n\n\tDAGFlagName = \"dag\"\n\n\tQueueConstructAsFlagName  = \"queue-construct-as\"\n\tQueueConstructAsFlagAlias = \"as\"\n)\n\nfunc NewFlags(l log.Logger, opts *Options, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\n\tflags := clihelper.Flags{\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        FormatFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(FormatFlagName),\n\t\t\tDestination: &opts.Format,\n\t\t\tUsage:       \"Output format for list results. Valid values: text, tree, long, dot.\",\n\t\t\tDefaultText: FormatText,\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        NoHiddenFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(NoHiddenFlagName),\n\t\t\tDestination: &opts.NoHidden,\n\t\t\tUsage:       \"Exclude hidden directories from list results.\",\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:    HiddenFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(HiddenFlagName),\n\t\t\tUsage:   \"Include hidden directories in list results.\",\n\t\t\tHidden:  true,\n\t\t\tAction: func(ctx context.Context, _ *clihelper.Context, value bool) error {\n\t\t\t\tif value {\n\t\t\t\t\tif err := opts.StrictControls.FilterByNames(controls.DeprecatedHiddenFlag).Evaluate(ctx); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        DependenciesFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(DependenciesFlagName),\n\t\t\tDestination: &opts.Dependencies,\n\t\t\tUsage:       \"Include dependencies in list results (only when using --long).\",\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:    ExternalFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(ExternalFlagName),\n\t\t\tUsage:   \"Discover external dependencies from initial results, and add them to top-level results (implies discovery of dependencies).\",\n\t\t\tHidden:  true,\n\t\t\tAction: func(_ context.Context, _ *clihelper.Context, value bool) error {\n\t\t\t\tif !value {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tpathExpr, err := filter.NewPathFilter(\"./**\")\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tgraphExpr := filter.NewGraphExpression(pathExpr).WithDependencies()\n\t\t\t\topts.Filters = append(opts.Filters, filter.NewFilter(graphExpr, graphExpr.String()))\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        TreeFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(TreeFlagName),\n\t\t\tDestination: &opts.Tree,\n\t\t\tUsage:       \"Output in tree format (equivalent to --format=tree).\",\n\t\t\tAliases:     []string{TreeFlagAlias},\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        LongFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(LongFlagName),\n\t\t\tDestination: &opts.Long,\n\t\t\tUsage:       \"Output in long format (equivalent to --format=long).\",\n\t\t\tAliases:     []string{LongFlagAlias},\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        DAGFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(DAGFlagName),\n\t\t\tDestination: &opts.DAG,\n\t\t\tUsage:       \"Use DAG mode to sort and group output.\",\n\t\t}),\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        QueueConstructAsFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(QueueConstructAsFlagName),\n\t\t\tDestination: &opts.QueueConstructAs,\n\t\t\tUsage:       \"Construct the queue as if a specific command was run.\",\n\t\t\tAliases:     []string{QueueConstructAsFlagAlias},\n\t\t}),\n\t}\n\n\treturn append(flags, shared.NewFilterFlags(l, opts.TerragruntOptions)...)\n}\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\tcmdOpts := NewOptions(opts)\n\tprefix := flags.Prefix{CommandName}\n\n\t// Base flags for list plus backend/feature flags\n\tflags := NewFlags(l, cmdOpts, prefix)\n\tflags = append(flags, shared.NewBackendFlags(opts, prefix)...)\n\tflags = append(flags, shared.NewFeatureFlags(opts, prefix)...)\n\n\treturn &clihelper.Command{\n\t\tName:    CommandName,\n\t\tAliases: []string{CommandAlias},\n\t\tUsage:   \"List relevant Terragrunt configurations.\",\n\t\tFlags:   flags,\n\t\tBefore: func(_ context.Context, _ *clihelper.Context) error {\n\t\t\tif cmdOpts.Tree {\n\t\t\t\tcmdOpts.Format = FormatTree\n\t\t\t}\n\n\t\t\tif cmdOpts.Long {\n\t\t\t\tcmdOpts.Format = FormatLong\n\t\t\t}\n\n\t\t\tif cmdOpts.DAG {\n\t\t\t\tcmdOpts.Mode = ModeDAG\n\t\t\t}\n\n\t\t\t// Requesting a specific command to be used for queue construction\n\t\t\t// implies DAG mode.\n\t\t\tif cmdOpts.QueueConstructAs != \"\" {\n\t\t\t\tcmdOpts.Mode = ModeDAG\n\t\t\t}\n\n\t\t\tif err := cmdOpts.Validate(); err != nil {\n\t\t\t\treturn clihelper.NewExitError(err, clihelper.ExitCodeGeneralError)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t\tAction: func(ctx context.Context, _ *clihelper.Context) error {\n\t\t\treturn Run(ctx, l, cmdOpts)\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/list/list.go",
    "content": "package list\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\n\t\"github.com/charmbracelet/lipgloss\"\n\t\"github.com/charmbracelet/lipgloss/tree\"\n\t\"github.com/charmbracelet/x/term\"\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/os/stdout\"\n\t\"github.com/gruntwork-io/terragrunt/internal/queue\"\n\t\"github.com/gruntwork-io/terragrunt/internal/worktrees\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/mgutz/ansi\"\n)\n\n// Run runs the list command.\nfunc Run(ctx context.Context, l log.Logger, opts *Options) error {\n\td, err := discovery.NewForDiscoveryCommand(l, &discovery.DiscoveryCommandOptions{\n\t\tWorkingDir:        opts.WorkingDir,\n\t\tQueueConstructAs:  opts.QueueConstructAs,\n\t\tNoHidden:          opts.NoHidden,\n\t\tWithRequiresParse: opts.Dependencies || opts.Mode == ModeDAG,\n\t\tWithRelationships: opts.Dependencies || opts.Mode == ModeDAG,\n\t\tFilters:           opts.Filters,\n\t\tExperiments:       opts.Experiments,\n\t})\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t// We do worktree generation here instead of in the discovery constructor\n\t// so that we can defer cleanup in the same context.\n\tgitFilters := opts.Filters.UniqueGitFilters()\n\n\tworktrees, worktreeErr := worktrees.NewWorktrees(ctx, l, opts.WorkingDir, gitFilters)\n\tif worktreeErr != nil {\n\t\treturn errors.Errorf(\"failed to create worktrees: %w\", worktreeErr)\n\t}\n\n\tdefer func() {\n\t\tcleanupErr := worktrees.Cleanup(ctx, l)\n\t\tif cleanupErr != nil {\n\t\t\tl.Errorf(\"failed to cleanup worktrees: %v\", cleanupErr)\n\t\t}\n\t}()\n\n\td = d.WithWorktrees(worktrees)\n\n\tvar (\n\t\tcomponents  component.Components\n\t\tdiscoverErr error\n\t)\n\n\t// Wrap discovery with telemetry\n\terr = telemetry.TelemeterFromContext(ctx).Collect(ctx, \"list_discover\", map[string]any{\n\t\t\"working_dir\":  opts.WorkingDir,\n\t\t\"no_hidden\":    opts.NoHidden,\n\t\t\"dependencies\": opts.Dependencies || opts.Mode == ModeDAG,\n\t}, func(ctx context.Context) error {\n\t\tcomponents, discoverErr = d.Discover(ctx, l, opts.TerragruntOptions)\n\t\treturn discoverErr\n\t})\n\tif err != nil {\n\t\tl.Debugf(\"Errors encountered while discovering components:\\n%s\", err)\n\t}\n\n\tswitch opts.Mode {\n\tcase ModeNormal:\n\t\tcomponents = components.Sort()\n\tcase ModeDAG:\n\t\terr = telemetry.TelemeterFromContext(ctx).Collect(ctx, \"list_mode_dag\", map[string]any{\n\t\t\t\"working_dir\":  opts.WorkingDir,\n\t\t\t\"config_count\": len(components),\n\t\t}, func(ctx context.Context) error {\n\t\t\tq, queueErr := queue.NewQueue(components)\n\t\t\tif queueErr != nil {\n\t\t\t\treturn queueErr\n\t\t\t}\n\n\t\t\tcomponents = q.Components()\n\n\t\t\treturn nil\n\t\t})\n\t\tif err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\tdefault:\n\t\t// This should never happen, because of validation in the command.\n\t\t// If it happens, we want to throw so we can fix the validation.\n\t\treturn errors.New(\"invalid mode: \" + opts.Mode)\n\t}\n\n\tvar listedComponents ListedComponents\n\n\terr = telemetry.TelemeterFromContext(ctx).Collect(ctx, \"list_discovered_to_listed\", map[string]any{\n\t\t\"working_dir\":  opts.WorkingDir,\n\t\t\"config_count\": len(components),\n\t}, func(ctx context.Context) error {\n\t\tvar convErr error\n\n\t\tlistedComponents, convErr = discoveredToListed(components, opts)\n\n\t\treturn convErr\n\t})\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tswitch opts.Format {\n\tcase FormatText:\n\t\treturn outputText(l, opts, listedComponents)\n\tcase FormatTree:\n\t\treturn outputTree(l, opts, listedComponents, opts.Mode)\n\tcase FormatLong:\n\t\treturn outputLong(l, opts, listedComponents)\n\tcase FormatDot:\n\t\treturn outputDot(l, opts, listedComponents)\n\tdefault:\n\t\t// This should never happen, because of validation in the command.\n\t\t// If it happens, we want to throw so we can fix the validation.\n\t\treturn errors.New(\"invalid format: \" + opts.Format)\n\t}\n}\n\ntype ListedComponents []*ListedComponent\n\ntype ListedComponent struct {\n\tType         component.Kind\n\tPath         string\n\tDependencies []*ListedComponent\n\tExcluded     bool\n}\n\n// Contains checks to see if the given path is in the listed components.\nfunc (l ListedComponents) Contains(path string) bool {\n\tfor _, c := range l {\n\t\tif c.Path == path {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// Get returns the component with the given path.\nfunc (l ListedComponents) Get(path string) *ListedComponent {\n\tfor _, c := range l {\n\t\tif c.Path == path {\n\t\t\treturn c\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc discoveredToListed(components component.Components, opts *Options) (ListedComponents, error) {\n\tlistedComponents := make(ListedComponents, 0, len(components))\n\terrs := []error{}\n\n\tfor _, c := range components {\n\t\texcluded := false\n\n\t\tif opts.QueueConstructAs != \"\" {\n\t\t\tif unit, ok := c.(*component.Unit); ok {\n\t\t\t\tif cfg := unit.Config(); cfg != nil && cfg.Exclude != nil {\n\t\t\t\t\tif cfg.Exclude.IsActionListed(opts.QueueConstructAs) {\n\t\t\t\t\t\tif opts.Format != FormatDot {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\texcluded = true\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tvar (\n\t\t\trelPath string\n\t\t\terr     error\n\t\t)\n\n\t\tif c.DiscoveryContext() != nil && c.DiscoveryContext().WorkingDir != \"\" {\n\t\t\trelPath, err = filepath.Rel(c.DiscoveryContext().WorkingDir, c.Path())\n\t\t} else {\n\t\t\trelPath, err = filepath.Rel(opts.WorkingDir, c.Path())\n\t\t}\n\n\t\tif err != nil {\n\t\t\terrs = append(errs, errors.New(err))\n\n\t\t\tcontinue\n\t\t}\n\n\t\tlistedCfg := &ListedComponent{\n\t\t\tType:     c.Kind(),\n\t\t\tPath:     relPath,\n\t\t\tExcluded: excluded,\n\t\t}\n\n\t\tif len(c.Dependencies()) == 0 {\n\t\t\tlistedComponents = append(listedComponents, listedCfg)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tlistedCfg.Dependencies = make([]*ListedComponent, len(c.Dependencies()))\n\n\t\tfor i, dep := range c.Dependencies() {\n\t\t\tvar relDepPath string\n\n\t\t\tif dep.DiscoveryContext() != nil && dep.DiscoveryContext().WorkingDir != \"\" {\n\t\t\t\trelDepPath, err = filepath.Rel(dep.DiscoveryContext().WorkingDir, dep.Path())\n\t\t\t} else {\n\t\t\t\trelDepPath, err = filepath.Rel(opts.WorkingDir, dep.Path())\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\terrs = append(errs, errors.New(err))\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdepExcluded := false\n\n\t\t\tif opts.QueueConstructAs != \"\" {\n\t\t\t\tif depUnit, ok := dep.(*component.Unit); ok {\n\t\t\t\t\tif depCfg := depUnit.Config(); depCfg != nil && depCfg.Exclude != nil {\n\t\t\t\t\t\tif depCfg.Exclude.IsActionListed(opts.QueueConstructAs) {\n\t\t\t\t\t\t\tdepExcluded = true\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\tlistedCfg.Dependencies[i] = &ListedComponent{\n\t\t\t\tType:     dep.Kind(),\n\t\t\t\tPath:     relDepPath,\n\t\t\t\tExcluded: depExcluded,\n\t\t\t}\n\t\t}\n\n\t\tsort.SliceStable(listedCfg.Dependencies, func(i, j int) bool {\n\t\t\treturn listedCfg.Dependencies[i].Path < listedCfg.Dependencies[j].Path\n\t\t})\n\n\t\tlistedComponents = append(listedComponents, listedCfg)\n\t}\n\n\treturn listedComponents, errors.Join(errs...)\n}\n\n// Colorizer is a colorizer for the discovered components.\ntype Colorizer struct {\n\tunitColorizer    func(string) string\n\tstackColorizer   func(string) string\n\theadingColorizer func(string) string\n\tpathColorizer    func(string) string\n}\n\n// NewColorizer creates a new Colorizer.\nfunc NewColorizer(shouldColor bool) *Colorizer {\n\tif !shouldColor {\n\t\treturn &Colorizer{\n\t\t\tunitColorizer:    func(s string) string { return s },\n\t\t\tstackColorizer:   func(s string) string { return s },\n\t\t\theadingColorizer: func(s string) string { return s },\n\t\t\tpathColorizer:    func(s string) string { return s },\n\t\t}\n\t}\n\n\treturn &Colorizer{\n\t\tunitColorizer:    ansi.ColorFunc(\"blue+bh\"),\n\t\tstackColorizer:   ansi.ColorFunc(\"green+bh\"),\n\t\theadingColorizer: ansi.ColorFunc(\"yellow+bh\"),\n\t\tpathColorizer:    ansi.ColorFunc(\"white+d\"),\n\t}\n}\n\nfunc (c *Colorizer) Colorize(listedComponent *ListedComponent) string {\n\tpath := listedComponent.Path\n\n\t// Get the directory and base name using filepath\n\tdir, base := filepath.Split(path)\n\n\tif dir == \"\" {\n\t\t// No directory part, color the whole path\n\t\tswitch listedComponent.Type {\n\t\tcase component.UnitKind:\n\t\t\treturn c.unitColorizer(path)\n\t\tcase component.StackKind:\n\t\t\treturn c.stackColorizer(path)\n\t\tdefault:\n\t\t\treturn path\n\t\t}\n\t}\n\n\t// Color the components differently\n\tcoloredPath := c.pathColorizer(dir)\n\n\tswitch listedComponent.Type {\n\tcase component.UnitKind:\n\t\treturn coloredPath + c.unitColorizer(base)\n\tcase component.StackKind:\n\t\treturn coloredPath + c.stackColorizer(base)\n\tdefault:\n\t\treturn path\n\t}\n}\n\nfunc (c *Colorizer) ColorizeType(t component.Kind) string {\n\tswitch t {\n\tcase component.UnitKind:\n\t\t// This extra space is to keep unit and stack\n\t\t// output equally spaced.\n\t\treturn c.unitColorizer(\"unit \")\n\tcase component.StackKind:\n\t\treturn c.stackColorizer(\"stack\")\n\tdefault:\n\t\treturn string(t)\n\t}\n}\n\nfunc (c *Colorizer) ColorizeHeading(dep string) string {\n\treturn c.headingColorizer(dep)\n}\n\n// outputText outputs the discovered components in text format.\nfunc outputText(l log.Logger, opts *Options, components ListedComponents) error {\n\tcolorizer := NewColorizer(shouldColor(l))\n\n\treturn renderTabular(opts, components, colorizer)\n}\n\n// outputLong outputs the discovered components in long format.\nfunc outputLong(l log.Logger, opts *Options, components ListedComponents) error {\n\tcolorizer := NewColorizer(shouldColor(l))\n\n\treturn renderLong(opts, components, colorizer)\n}\n\n// shouldColor returns true if the output should be colored.\nfunc shouldColor(l log.Logger) bool {\n\treturn !l.Formatter().DisabledColors() && !stdout.IsRedirected()\n}\n\n// renderLong renders the components in a long format.\nfunc renderLong(opts *Options, components ListedComponents, c *Colorizer) error {\n\tvar buf strings.Builder\n\n\tlongestPathLen := getLongestPathLen(components)\n\n\tbuf.WriteString(buildLongHeadings(opts, c, longestPathLen))\n\n\tfor _, component := range components {\n\t\tbuf.WriteString(c.ColorizeType(component.Type))\n\t\tbuf.WriteString(\" \" + c.Colorize(component))\n\n\t\tif opts.Dependencies && len(component.Dependencies) > 0 {\n\t\t\tcolorizedDeps := make([]string, 0, len(component.Dependencies))\n\n\t\t\tfor _, dep := range component.Dependencies {\n\t\t\t\tcolorizedDeps = append(colorizedDeps, c.Colorize(dep))\n\t\t\t}\n\n\t\t\tconst extraDependenciesPadding = 2\n\n\t\t\tdependenciesPadding := (longestPathLen - len(component.Path)) + extraDependenciesPadding\n\t\t\tfor range dependenciesPadding {\n\t\t\t\tbuf.WriteString(\" \")\n\t\t\t}\n\n\t\t\tbuf.WriteString(strings.Join(colorizedDeps, \", \"))\n\t\t}\n\n\t\tbuf.WriteString(\"\\n\")\n\t}\n\n\t_, err := opts.Writers.Writer.Write([]byte(buf.String()))\n\n\treturn errors.New(err)\n}\n\n// buildLongHeadings renders the headings for the long format.\nfunc buildLongHeadings(opts *Options, c *Colorizer, longestPathLen int) string {\n\tvar buf strings.Builder\n\n\tbuf.WriteString(c.ColorizeHeading(\"Type  Path\"))\n\n\tif opts.Dependencies {\n\t\tconst extraDependenciesPadding = 2\n\n\t\tdependenciesPadding := (longestPathLen - len(\"Path\")) + extraDependenciesPadding\n\t\tfor range dependenciesPadding {\n\t\t\tbuf.WriteString(\" \")\n\t\t}\n\n\t\tbuf.WriteString(c.ColorizeHeading(\"Dependencies\"))\n\t}\n\n\tbuf.WriteString(\"\\n\")\n\n\treturn buf.String()\n}\n\n// renderTabular renders the components in a tabular format.\nfunc renderTabular(opts *Options, components ListedComponents, c *Colorizer) error {\n\tvar buf strings.Builder\n\n\tmaxCols, colWidth := getMaxCols(components)\n\n\tfor i, component := range components {\n\t\tif i > 0 && i%maxCols == 0 {\n\t\t\tbuf.WriteString(\"\\n\")\n\t\t}\n\n\t\tbuf.WriteString(c.Colorize(component))\n\n\t\t// Add padding until the length of maxCols\n\t\tpadding := colWidth - len(component.Path)\n\t\tfor range padding {\n\t\t\tbuf.WriteString(\" \")\n\t\t}\n\t}\n\n\tbuf.WriteString(\"\\n\")\n\n\t_, err := opts.Writers.Writer.Write([]byte(buf.String()))\n\n\treturn errors.New(err)\n}\n\n// outputTree outputs the discovered components in tree format.\nfunc outputTree(l log.Logger, opts *Options, components ListedComponents, sort string) error {\n\ts := NewTreeStyler(shouldColor(l))\n\n\treturn renderTree(opts, components, s, sort)\n}\n\n// outputDot outputs the discovered components in GraphViz DOT format.\nfunc outputDot(_ log.Logger, opts *Options, components ListedComponents) error {\n\treturn renderDot(opts, components)\n}\n\ntype TreeStyler struct {\n\tentryStyle  lipgloss.Style\n\trootStyle   lipgloss.Style\n\tcolorizer   *Colorizer\n\tshouldColor bool\n}\n\nfunc NewTreeStyler(shouldColor bool) *TreeStyler {\n\tcolorizer := NewColorizer(shouldColor)\n\n\treturn &TreeStyler{\n\t\tshouldColor: shouldColor,\n\t\tentryStyle:  lipgloss.NewStyle().Foreground(lipgloss.Color(\"240\")).MarginRight(1),\n\t\trootStyle:   lipgloss.NewStyle().Foreground(lipgloss.Color(\"35\")),\n\t\tcolorizer:   colorizer,\n\t}\n}\n\nfunc (s *TreeStyler) Style(t *tree.Tree) *tree.Tree {\n\tt = t.\n\t\tEnumerator(tree.RoundedEnumerator)\n\n\tif !s.shouldColor {\n\t\treturn t\n\t}\n\n\treturn t.\n\t\tEnumeratorStyle(s.entryStyle).\n\t\tRootStyle(s.rootStyle)\n}\n\n// generateTree creates a tree structure from ListedComponents\nfunc generateTree(components ListedComponents, s *TreeStyler) *tree.Tree {\n\troot := tree.Root(\".\")\n\tnodes := make(map[string]*tree.Tree)\n\n\tfor _, c := range components {\n\t\tparts := preProcessPath(c.Path)\n\t\tif len(parts.segments) == 0 || (len(parts.segments) == 1 && parts.segments[0] == \".\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tcurrentPath := \".\"\n\t\tcurrentNode := root\n\n\t\tfor i, segment := range parts.segments {\n\t\t\tnextPath := filepath.Join(currentPath, segment)\n\t\t\tif _, exists := nodes[nextPath]; !exists {\n\t\t\t\tcomponentType := component.StackKind\n\n\t\t\t\tif c.Type == component.UnitKind && i == len(parts.segments)-1 {\n\t\t\t\t\tcomponentType = component.UnitKind\n\t\t\t\t}\n\n\t\t\t\ttmpCfg := &ListedComponent{\n\t\t\t\t\tType: componentType,\n\t\t\t\t\tPath: segment,\n\t\t\t\t}\n\n\t\t\t\tnewNode := tree.New().Root(s.colorizer.Colorize(tmpCfg))\n\t\t\t\tnodes[nextPath] = newNode\n\t\t\t\tcurrentNode.Child(newNode)\n\t\t\t}\n\n\t\t\tcurrentNode = nodes[nextPath]\n\t\t\tcurrentPath = nextPath\n\t\t}\n\t}\n\n\treturn root\n}\n\n// generateDAGTree creates a tree structure from ListedComponents.\n// It assumes that the components are already sorted in DAG order.\n// As such, it will first construct root nodes for each component\n// without a dependency in the listed components. Then, it will\n// connect the remaining nodes to their dependencies, which\n// should be doable in a single pass through the components.\n// There may be duplicate entries for dependency nodes, as\n// a node may be a dependency for multiple components.\n// That's OK.\nfunc generateDAGTree(components ListedComponents, s *TreeStyler) *tree.Tree {\n\troot := tree.Root(\".\")\n\n\trootNodes := make(map[string]*tree.Tree)\n\tdependencyNodes := make(map[string]*tree.Tree)\n\n\t// First pass: create all root nodes\n\tfor _, c := range components {\n\t\tif len(c.Dependencies) == 0 || !components.Contains(c.Path) {\n\t\t\trootNodes[c.Path] = tree.New().Root(s.colorizer.Colorize(c))\n\t\t}\n\t}\n\n\t// Second pass: connect dependencies\n\tfor _, c := range components {\n\t\tif len(c.Dependencies) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Sort dependencies to ensure deterministic order\n\t\tsortedDeps := make([]string, len(c.Dependencies))\n\t\tfor i, dep := range c.Dependencies {\n\t\t\tsortedDeps[i] = dep.Path\n\t\t}\n\n\t\tsort.Strings(sortedDeps)\n\n\t\tfor _, dependency := range sortedDeps {\n\t\t\tif _, exists := rootNodes[dependency]; exists {\n\t\t\t\tdependencyNode := tree.New().Root(s.colorizer.Colorize(c))\n\t\t\t\trootNodes[dependency].Child(dependencyNode)\n\t\t\t\tdependencyNodes[c.Path] = dependencyNode\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif _, exists := dependencyNodes[dependency]; exists {\n\t\t\t\tnewDependencyNode := tree.New().Root(s.colorizer.Colorize(c))\n\t\t\t\tdependencyNodes[dependency].Child(newDependencyNode)\n\t\t\t\tdependencyNodes[c.Path] = newDependencyNode\n\t\t\t}\n\t\t}\n\t}\n\n\t// Sort root nodes to ensure deterministic order\n\tsortedRootPaths := make([]string, 0, len(rootNodes))\n\tfor path := range rootNodes {\n\t\tsortedRootPaths = append(sortedRootPaths, path)\n\t}\n\n\tsort.Strings(sortedRootPaths)\n\n\t// Add root nodes in sorted order\n\tfor _, path := range sortedRootPaths {\n\t\troot.Child(rootNodes[path])\n\t}\n\n\treturn root\n}\n\n// pathParts holds the pre-processed parts of a component path.\ntype pathParts struct {\n\tdir      string\n\tbase     string\n\tsegments []string\n}\n\n// preProcessPath splits a path into its components.\nfunc preProcessPath(path string) pathParts {\n\tdir := filepath.Dir(path)\n\tbase := filepath.Base(path)\n\tsegments := strings.Split(path, string(os.PathSeparator))\n\n\treturn pathParts{\n\t\tdir:      dir,\n\t\tbase:     base,\n\t\tsegments: segments,\n\t}\n}\n\n// renderTree renders the components in a tree format.\nfunc renderTree(opts *Options, components ListedComponents, s *TreeStyler, _ string) error {\n\tvar t *tree.Tree\n\n\tif opts.Mode == ModeDAG {\n\t\tt = generateDAGTree(components, s)\n\t} else {\n\t\tt = generateTree(components, s)\n\t}\n\n\tt = s.Style(t)\n\n\t_, err := opts.Writers.Writer.Write([]byte(t.String() + \"\\n\"))\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n\n// getMaxCols returns the maximum number of columns\n// that can be displayed in the terminal.\n// It also returns the width of each column.\n// The width is the longest path length + 2 for padding.\nfunc getMaxCols(components ListedComponents) (int, int) {\n\tmaxCols := 0\n\n\tterminalWidth := getTerminalWidth()\n\tlongestPathLen := getLongestPathLen(components)\n\n\tconst padding = 2\n\n\tcolWidth := longestPathLen + padding\n\n\tif longestPathLen > 0 {\n\t\tmaxCols = terminalWidth / colWidth\n\t}\n\n\tif maxCols == 0 {\n\t\tmaxCols = 1\n\t}\n\n\treturn maxCols, colWidth\n}\n\n// getTerminalWidth returns the width of the terminal.\nfunc getTerminalWidth() int {\n\t// Default to 80 if we can't get the terminal width.\n\twidth := 80\n\n\tw, _, err := term.GetSize(os.Stdout.Fd())\n\tif err == nil {\n\t\twidth = w\n\t}\n\n\treturn width\n}\n\n// getLongestPathLen returns the length of the\n// longest path in the list of components.\nfunc getLongestPathLen(components ListedComponents) int {\n\tlongest := 0\n\n\tfor _, c := range components {\n\t\tif len(c.Path) > longest {\n\t\t\tlongest = len(c.Path)\n\t\t}\n\t}\n\n\treturn longest\n}\n\n// renderDot renders the components in GraphViz DOT format.\nfunc renderDot(opts *Options, components ListedComponents) error {\n\tvar buf strings.Builder\n\n\tbuf.WriteString(\"digraph {\\n\")\n\n\tsortedComponents := make(ListedComponents, len(components))\n\tcopy(sortedComponents, components)\n\tsort.Slice(sortedComponents, func(i, j int) bool {\n\t\treturn sortedComponents[i].Path < sortedComponents[j].Path\n\t})\n\n\tfor _, component := range sortedComponents {\n\t\tif len(component.Dependencies) > 1 {\n\t\t\tsort.Slice(component.Dependencies, func(i, j int) bool {\n\t\t\t\treturn component.Dependencies[i].Path < component.Dependencies[j].Path\n\t\t\t})\n\t\t}\n\t}\n\n\tfor _, component := range sortedComponents {\n\t\tstyle := \"\"\n\t\tif component.Excluded {\n\t\t\tstyle = \"[color=red]\"\n\t\t}\n\n\t\tbuf.WriteString(fmt.Sprintf(\"\\t\\\"%s\\\" %s;\\n\", component.Path, style))\n\n\t\tfor _, dep := range component.Dependencies {\n\t\t\tbuf.WriteString(fmt.Sprintf(\"\\t\\\"%s\\\" -> \\\"%s\\\";\\n\", component.Path, dep.Path))\n\t\t}\n\t}\n\n\tbuf.WriteString(\"}\\n\")\n\n\t_, err := opts.Writers.Writer.Write([]byte(buf.String()))\n\n\treturn errors.New(err)\n}\n"
  },
  {
    "path": "internal/cli/commands/list/list_test.go",
    "content": "package list_test\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/list\"\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestBasicDiscovery(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create test directory structure\n\ttestDirs := []string{\n\t\t\"unit1\",\n\t\t\"unit2\",\n\t\t\"stack1\",\n\t\t\".hidden/unit3\",\n\t\t\"nested/unit4\",\n\t}\n\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Create test files\n\ttestFiles := map[string]string{\n\t\t\"unit1/terragrunt.hcl\":         \"\",\n\t\t\"unit2/terragrunt.hcl\":         \"\",\n\t\t\"stack1/terragrunt.stack.hcl\":  \"\",\n\t\t\".hidden/unit3/terragrunt.hcl\": \"\",\n\t\t\"nested/unit4/terragrunt.hcl\":  \"\",\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\texpectedPaths := []string{\"unit1\", \"unit2\", filepath.Join(\"nested\", \"unit4\"), \"stack1\"}\n\n\ttgOpts := options.NewTerragruntOptions()\n\ttgOpts.WorkingDir = tmpDir\n\n\t// Create options\n\topts := list.NewOptions(tgOpts)\n\topts.Format = \"text\" //nolint: goconst\n\topts.Mode = \"normal\"\n\topts.NoHidden = true\n\topts.Dependencies = false\n\n\t// Create a pipe to capture output\n\tr, w, err := os.Pipe()\n\trequire.NoError(t, err)\n\n\t// Set the writer in options\n\topts.Writers.Writer = w\n\n\tl := logger.CreateLogger()\n\n\tl.Formatter().SetDisabledColors(true)\n\n\terr = list.Run(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\n\t// Close the write end of the pipe\n\tw.Close()\n\n\t// Read all output\n\toutput, err := io.ReadAll(r)\n\trequire.NoError(t, err)\n\n\t// Split output into fields and trim whitespace\n\tfields := strings.Fields(string(output))\n\n\t// Verify we have the expected number of lines\n\tassert.Len(t, fields, len(expectedPaths))\n\n\t// Verify each line is a clean path without any formatting\n\tfor _, field := range fields {\n\t\tassert.NotEmpty(t, field)\n\t}\n\n\t// Verify all expected paths are present\n\tassert.ElementsMatch(t, expectedPaths, fields)\n}\n\nfunc TestHiddenDiscovery(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create test directory structure\n\ttestDirs := []string{\n\t\t\"unit1\",\n\t\t\"unit2\",\n\t\t\"stack1\",\n\t\t\".hidden/unit3\",\n\t\t\"nested/unit4\",\n\t}\n\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Create test files\n\ttestFiles := map[string]string{\n\t\t\"unit1/terragrunt.hcl\":         \"\",\n\t\t\"unit2/terragrunt.hcl\":         \"\",\n\t\t\"stack1/terragrunt.stack.hcl\":  \"\",\n\t\t\".hidden/unit3/terragrunt.hcl\": \"\",\n\t\t\"nested/unit4/terragrunt.hcl\":  \"\",\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\texpectedPaths := []string{\"unit1\", \"unit2\", filepath.Join(\"nested\", \"unit4\"), \"stack1\", filepath.Join(\".hidden\", \"unit3\")}\n\n\ttgOpts := options.NewTerragruntOptions()\n\ttgOpts.WorkingDir = tmpDir\n\n\tl := logger.CreateLogger()\n\tl.Formatter().SetDisabledColors(true)\n\n\t// Create options\n\topts := list.NewOptions(tgOpts)\n\topts.Format = \"text\"\n\n\t// Create a pipe to capture output\n\tr, w, err := os.Pipe()\n\trequire.NoError(t, err)\n\n\t// Set the writer in options\n\topts.Writers.Writer = w\n\n\terr = list.Run(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\n\t// Close the write end of the pipe\n\tw.Close()\n\n\t// Read all output\n\toutput, err := io.ReadAll(r)\n\trequire.NoError(t, err)\n\n\t// Split output into fields and trim whitespace\n\tfields := strings.Fields(string(output))\n\n\t// Verify we have the expected number of lines\n\tassert.Len(t, fields, len(expectedPaths))\n\n\t// Verify all expected paths are present\n\tassert.ElementsMatch(t, expectedPaths, fields)\n}\n\nfunc TestDAGSortingSimpleDependencies(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create test directory structure with dependencies:\n\t// unit2 -> unit1\n\t// unit3 -> unit2\n\ttestDirs := []string{\n\t\t\"unit1\",\n\t\t\"unit2\",\n\t\t\"unit3\",\n\t}\n\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Create test files with dependencies\n\ttestFiles := map[string]string{\n\t\t\"unit1/terragrunt.hcl\": \"\",\n\t\t\"unit2/terragrunt.hcl\": `\ndependency \"unit1\" {\n  config_path = \"../unit1\"\n}`,\n\t\t\"unit3/terragrunt.hcl\": `\ndependency \"unit2\" {\n  config_path = \"../unit2\"\n}`,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\texpectedPaths := []string{\"unit1\", \"unit2\", \"unit3\"}\n\n\ttgOpts := options.NewTerragruntOptions()\n\ttgOpts.WorkingDir = tmpDir\n\n\tl := logger.CreateLogger()\n\tl.Formatter().SetDisabledColors(true)\n\n\t// Create options\n\topts := list.NewOptions(tgOpts)\n\topts.Format = \"text\"\n\topts.Mode = \"dag\" //nolint: goconst\n\topts.Dependencies = true\n\n\t// Create a pipe to capture output\n\tr, w, err := os.Pipe()\n\trequire.NoError(t, err)\n\n\t// Set the writer in options\n\topts.Writers.Writer = w\n\n\terr = list.Run(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\n\t// Close the write end of the pipe\n\tw.Close()\n\n\t// Read all output\n\toutput, err := io.ReadAll(r)\n\trequire.NoError(t, err)\n\n\t// Split output into fields and trim whitespace\n\tfields := strings.Fields(string(output))\n\n\t// Verify we have the expected number of lines\n\tassert.Len(t, fields, len(expectedPaths))\n\n\t// For DAG sorting, order matters - verify exact order\n\tassert.Equal(t, expectedPaths, fields)\n}\n\nfunc TestDAGSortingReversedDependencies(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create test directory structure with dependencies:\n\t// unit3 -> unit2\n\t// unit2 -> unit1\n\ttestDirs := []string{\n\t\t\"unit1\",\n\t\t\"unit2\",\n\t\t\"unit3\",\n\t}\n\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Create test files with dependencies\n\ttestFiles := map[string]string{\n\t\t\"unit1/terragrunt.hcl\": `\ndependency \"unit2\" {\n  config_path = \"../unit2\"\n}`,\n\t\t\"unit2/terragrunt.hcl\": `\ndependency \"unit3\" {\n  config_path = \"../unit3\"\n}`,\n\t\t\"unit3/terragrunt.hcl\": \"\",\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\texpectedPaths := []string{\"unit3\", \"unit2\", \"unit1\"}\n\n\ttgOpts := options.NewTerragruntOptions()\n\ttgOpts.WorkingDir = tmpDir\n\n\tl := logger.CreateLogger()\n\tl.Formatter().SetDisabledColors(true)\n\n\t// Create options\n\topts := list.NewOptions(tgOpts)\n\topts.Format = \"text\"\n\topts.Mode = \"dag\" //nolint: goconst\n\topts.Dependencies = true\n\n\t// Create a pipe to capture output\n\tr, w, err := os.Pipe()\n\trequire.NoError(t, err)\n\n\t// Set the writer in options\n\topts.Writers.Writer = w\n\n\terr = list.Run(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\n\t// Close the write end of the pipe\n\tw.Close()\n\n\t// Read all output\n\toutput, err := io.ReadAll(r)\n\trequire.NoError(t, err)\n\n\t// Split output into fields and trim whitespace\n\tfields := strings.Fields(string(output))\n\n\t// Verify we have the expected number of lines\n\tassert.Len(t, fields, len(expectedPaths))\n\n\t// For DAG sorting, order matters - verify exact order\n\tassert.Equal(t, expectedPaths, fields)\n\n\t// Helper to find index of a path\n\tfindIndex := func(path string) int {\n\t\tfor i, field := range fields {\n\t\t\tif field == path {\n\t\t\t\treturn i\n\t\t\t}\n\t\t}\n\n\t\treturn -1\n\t}\n\n\t// Verify dependency ordering\n\tunit1Index := findIndex(\"unit1\")\n\tunit2Index := findIndex(\"unit2\")\n\tunit3Index := findIndex(\"unit3\")\n\n\tassert.Less(t, unit3Index, unit2Index, \"unit3 (no deps) should come before unit2 (depends on unit3)\")\n\tassert.Less(t, unit2Index, unit1Index, \"unit2 should come before unit1 (depends on unit2)\")\n}\n\nfunc TestDAGSortingComplexDependencies(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create test directory structure with complex dependencies:\n\t// A (no deps)\n\t// B (no deps)\n\t// C -> A\n\t// D -> A,B\n\t// E -> C\n\t// F -> C\n\ttestDirs := []string{\n\t\t\"A\", \"B\", \"C\", \"D\", \"E\", \"F\",\n\t}\n\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Create test files with dependencies\n\ttestFiles := map[string]string{\n\t\t\"A/terragrunt.hcl\": \"\",\n\t\t\"B/terragrunt.hcl\": \"\",\n\t\t\"C/terragrunt.hcl\": `\ndependency \"A\" {\n  config_path = \"../A\"\n}`,\n\t\t\"D/terragrunt.hcl\": `\ndependency \"A\" {\n  config_path = \"../A\"\n}\ndependency \"B\" {\n  config_path = \"../B\"\n}`,\n\t\t\"E/terragrunt.hcl\": `\ndependency \"C\" {\n  config_path = \"../C\"\n}`,\n\t\t\"F/terragrunt.hcl\": `\ndependency \"C\" {\n  config_path = \"../C\"\n}`,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\texpectedPaths := []string{\"A\", \"B\", \"C\", \"D\", \"E\", \"F\"}\n\n\ttgOpts := options.NewTerragruntOptions()\n\ttgOpts.WorkingDir = tmpDir\n\n\tl := logger.CreateLogger()\n\n\tl.Formatter().SetDisabledColors(true)\n\n\t// Create options\n\topts := list.NewOptions(tgOpts)\n\topts.Format = \"text\"\n\topts.Mode = \"dag\" //nolint: goconst\n\topts.Dependencies = true\n\n\t// Create a pipe to capture output\n\tr, w, err := os.Pipe()\n\trequire.NoError(t, err)\n\n\t// Set the writer in options\n\topts.Writers.Writer = w\n\n\terr = list.Run(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\n\t// Close the write end of the pipe\n\tw.Close()\n\n\t// Read all output\n\toutput, err := io.ReadAll(r)\n\trequire.NoError(t, err)\n\n\t// Split output into fields and trim whitespace\n\tfields := strings.Fields(string(output))\n\n\t// Verify we have the expected number of lines\n\tassert.Len(t, fields, len(expectedPaths))\n\n\t// For DAG sorting, order matters - verify exact order\n\t// and also verify relative ordering constraints\n\tassert.Equal(t, expectedPaths, fields)\n\n\t// Helper to find index of a path\n\tfindIndex := func(path string) int {\n\t\tfor i, line := range fields {\n\t\t\tif line == path {\n\t\t\t\treturn i\n\t\t\t}\n\t\t}\n\n\t\treturn -1\n\t}\n\n\t// Verify dependency ordering\n\taIndex := findIndex(\"A\")\n\tbIndex := findIndex(\"B\")\n\tcIndex := findIndex(\"C\")\n\tdIndex := findIndex(\"D\")\n\teIndex := findIndex(\"E\")\n\tfIndex := findIndex(\"F\")\n\n\t// Level 0 items should be before their dependents\n\tassert.Less(t, aIndex, cIndex, \"A should come before C\")\n\tassert.Less(t, aIndex, dIndex, \"A should come before D\")\n\tassert.Less(t, bIndex, dIndex, \"B should come before D\")\n\n\t// Level 1 items should be before their dependents\n\tassert.Less(t, cIndex, eIndex, \"C should come before E\")\n\tassert.Less(t, cIndex, fIndex, \"C should come before F\")\n}\n\nfunc TestColorizer(t *testing.T) {\n\tt.Parallel()\n\n\tcolorizer := list.NewColorizer(true)\n\n\ttests := []struct {\n\t\tname   string\n\t\tconfig *list.ListedComponent\n\t\t// We can't test exact ANSI codes as they might vary by environment,\n\t\t// so we'll test that different types result in different outputs\n\t\tshouldBeDifferent []component.Kind\n\t}{\n\t\t{\n\t\t\tname: \"unit config\",\n\t\t\tconfig: &list.ListedComponent{\n\t\t\t\tType: component.UnitKind,\n\t\t\t\tPath: \"path/to/unit\",\n\t\t\t},\n\t\t\tshouldBeDifferent: []component.Kind{component.StackKind},\n\t\t},\n\t\t{\n\t\t\tname: \"stack config\",\n\t\t\tconfig: &list.ListedComponent{\n\t\t\t\tType: component.StackKind,\n\t\t\t\tPath: \"path/to/stack\",\n\t\t\t},\n\t\t\tshouldBeDifferent: []component.Kind{component.UnitKind},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresult := colorizer.Colorize(tt.config)\n\t\t\tassert.NotEmpty(t, result)\n\n\t\t\t// Test that different types produce different colorized outputs\n\t\t\tfor _, diffType := range tt.shouldBeDifferent {\n\t\t\t\tdiffConfig := &list.ListedComponent{\n\t\t\t\t\tType: diffType,\n\t\t\t\t\tPath: tt.config.Path,\n\t\t\t\t}\n\t\t\t\tdiffResult := colorizer.Colorize(diffConfig)\n\t\t\t\tassert.NotEqual(t, result, diffResult)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDotFormat(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\ttestDirs := []string{\n\t\t\"unit1\",\n\t\t\"unit2\",\n\t}\n\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\t\"unit1/terragrunt.hcl\": \"\",\n\t\t\"unit2/terragrunt.hcl\": `\ndependency \"unit1\" {\n  config_path = \"../unit1\"\n}\n`,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\ttgOptions, err := options.NewTerragruntOptionsForTest(tmpDir)\n\trequire.NoError(t, err)\n\n\topts := list.NewOptions(tgOptions)\n\topts.Format = list.FormatDot\n\topts.Mode = list.ModeDAG\n\topts.Dependencies = true\n\n\tr, w, err := os.Pipe()\n\trequire.NoError(t, err)\n\n\topts.Writers.Writer = w\n\n\terr = list.Run(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\n\tw.Close()\n\n\toutput, err := io.ReadAll(r)\n\trequire.NoError(t, err)\n\n\toutputStr := string(output)\n\n\tassert.Equal(\n\t\tt,\n\t\t`digraph {\n\t\"001/unit1\" ;\n\t\"001/unit2\" ;\n\t\"001/unit2\" -> \"001/unit1\";\n}\n`,\n\t\toutputStr,\n\t)\n}\n\nfunc TestDotFormatWithoutDependencies(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\ttestDirs := []string{\n\t\t\"unit1\",\n\t\t\"unit2\",\n\t\t\"unit3\",\n\t}\n\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\t\"unit1/terragrunt.hcl\": \"\",\n\t\t\"unit2/terragrunt.hcl\": \"\",\n\t\t\"unit3/terragrunt.hcl\": \"\",\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\ttgOptions, err := options.NewTerragruntOptionsForTest(tmpDir)\n\trequire.NoError(t, err)\n\n\topts := list.NewOptions(tgOptions)\n\topts.Format = list.FormatDot\n\topts.Dependencies = false\n\n\tr, w, err := os.Pipe()\n\trequire.NoError(t, err)\n\n\topts.Writers.Writer = w\n\n\terr = list.Run(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\n\tw.Close()\n\n\toutput, err := io.ReadAll(r)\n\trequire.NoError(t, err)\n\n\toutputStr := string(output)\n\n\tassert.Equal(\n\t\tt,\n\t\t`digraph {\n\t\"001/unit1\" ;\n\t\"001/unit2\" ;\n\t\"001/unit3\" ;\n}\n`,\n\t\toutputStr,\n\t)\n}\n\nfunc TestDotFormatWithComplexDependencies(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\ttestDirs := []string{\n\t\t\"unit1\",\n\t\t\"unit2\",\n\t\t\"unit3\",\n\t}\n\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\t\"unit1/terragrunt.hcl\": \"\",\n\t\t\"unit2/terragrunt.hcl\": `\ndependency \"unit1\" {\n  config_path = \"../unit1\"\n}\n`,\n\t\t\"unit3/terragrunt.hcl\": `\ndependency \"unit1\" {\n  config_path = \"../unit1\"\n}\n\ndependency \"unit2\" {\n  config_path = \"../unit2\"\n}\n`,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\ttgOptions, err := options.NewTerragruntOptionsForTest(tmpDir)\n\trequire.NoError(t, err)\n\n\topts := list.NewOptions(tgOptions)\n\topts.Format = list.FormatDot\n\topts.Mode = list.ModeDAG\n\topts.Dependencies = true\n\n\tr, w, err := os.Pipe()\n\trequire.NoError(t, err)\n\n\topts.Writers.Writer = w\n\n\terr = list.Run(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\n\tw.Close()\n\n\toutput, err := io.ReadAll(r)\n\trequire.NoError(t, err)\n\n\toutputStr := string(output)\n\n\tassert.Equal(\n\t\tt,\n\t\t`digraph {\n\t\"001/unit1\" ;\n\t\"001/unit2\" ;\n\t\"001/unit2\" -> \"001/unit1\";\n\t\"001/unit3\" ;\n\t\"001/unit3\" -> \"001/unit1\";\n\t\"001/unit3\" -> \"001/unit2\";\n}\n`,\n\t\toutputStr,\n\t)\n}\n\nfunc TestDotFormatWithExcludedComponents(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\ttestDirs := []string{\n\t\t\"unit1\",\n\t\t\"unit2\",\n\t\t\"unit3\",\n\t}\n\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\t\"unit1/terragrunt.hcl\": \"\",\n\t\t\"unit2/terragrunt.hcl\": `\ndependency \"unit1\" {\n  config_path = \"../unit1\"\n}\n\nexclude {\n  if      = true\n  actions = [\"apply\"]\n}\n`,\n\t\t\"unit3/terragrunt.hcl\": `\ndependency \"unit2\" {\n  config_path = \"../unit2\"\n}\n`,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\ttgOptions, err := options.NewTerragruntOptionsForTest(tmpDir)\n\trequire.NoError(t, err)\n\n\topts := list.NewOptions(tgOptions)\n\topts.Format = list.FormatDot\n\topts.Mode = list.ModeDAG\n\topts.Dependencies = true\n\topts.QueueConstructAs = \"apply\"\n\n\tr, w, err := os.Pipe()\n\trequire.NoError(t, err)\n\n\topts.Writers.Writer = w\n\n\terr = list.Run(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\n\tw.Close()\n\n\toutput, err := io.ReadAll(r)\n\trequire.NoError(t, err)\n\n\toutputStr := string(output)\n\n\tassert.Equal(\n\t\tt,\n\t\t`digraph {\n\t\"001/unit1\" ;\n\t\"001/unit2\" [color=red];\n\t\"001/unit2\" -> \"001/unit1\";\n\t\"001/unit3\" ;\n\t\"001/unit3\" -> \"001/unit2\";\n}\n`,\n\t\toutputStr,\n\t)\n}\n\nfunc TestDotFormatWithExcludedDependency(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\ttestDirs := []string{\n\t\t\"unit1\",\n\t\t\"unit2\",\n\t}\n\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\t\"unit1/terragrunt.hcl\": `\nexclude {\n  if      = true\n  actions = [\"plan\"]\n}\n`,\n\t\t\"unit2/terragrunt.hcl\": `\ndependency \"unit1\" {\n  config_path = \"../unit1\"\n}\n`,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\ttgOptions, err := options.NewTerragruntOptionsForTest(tmpDir)\n\trequire.NoError(t, err)\n\n\topts := list.NewOptions(tgOptions)\n\topts.Format = list.FormatDot\n\topts.Mode = list.ModeDAG\n\topts.Dependencies = true\n\topts.QueueConstructAs = \"plan\"\n\n\tr, w, err := os.Pipe()\n\trequire.NoError(t, err)\n\n\topts.Writers.Writer = w\n\n\terr = list.Run(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\n\tw.Close()\n\n\toutput, err := io.ReadAll(r)\n\trequire.NoError(t, err)\n\n\toutputStr := string(output)\n\n\tassert.Equal(\n\t\tt,\n\t\t`digraph {\n\t\"001/unit1\" [color=red];\n\t\"001/unit2\" ;\n\t\"001/unit2\" -> \"001/unit1\";\n}\n`,\n\t\toutputStr,\n\t)\n}\n\nfunc TestTextFormatExcludesExcludedComponents(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\ttestDirs := []string{\n\t\t\"unit1\",\n\t\t\"unit2\",\n\t\t\"unit3\",\n\t}\n\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\t\"unit1/terragrunt.hcl\": \"\",\n\t\t\"unit2/terragrunt.hcl\": `\nexclude {\n  if      = true\n  actions = [\"destroy\"]\n}\n`,\n\t\t\"unit3/terragrunt.hcl\": \"\",\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\tl.Formatter().SetDisabledColors(true)\n\n\ttgOptions, err := options.NewTerragruntOptionsForTest(tmpDir)\n\trequire.NoError(t, err)\n\n\topts := list.NewOptions(tgOptions)\n\topts.Format = list.FormatText\n\topts.QueueConstructAs = \"destroy\"\n\n\tr, w, err := os.Pipe()\n\trequire.NoError(t, err)\n\n\topts.Writers.Writer = w\n\n\terr = list.Run(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\n\tw.Close()\n\n\toutput, err := io.ReadAll(r)\n\trequire.NoError(t, err)\n\n\toutputStr := string(output)\n\n\texpectedPaths := []string{filepath.Join(\"001\", \"unit1\"), filepath.Join(\"001\", \"unit3\")}\n\n\tfields := strings.Fields(outputStr)\n\n\tassert.Len(t, fields, len(expectedPaths))\n\tassert.ElementsMatch(t, expectedPaths, fields)\n}\n\nfunc TestDotFormatWithMultipleExcludedComponents(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\ttestDirs := []string{\n\t\t\"unit1\",\n\t\t\"unit2\",\n\t\t\"unit3\",\n\t\t\"unit4\",\n\t}\n\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\t\"unit1/terragrunt.hcl\": `\nexclude {\n  if      = true\n  actions = [\"all\"]\n}\n`,\n\t\t\"unit2/terragrunt.hcl\": `\ndependency \"unit1\" {\n  config_path = \"../unit1\"\n}\n`,\n\t\t\"unit3/terragrunt.hcl\": `\ndependency \"unit2\" {\n  config_path = \"../unit2\"\n}\n\nexclude {\n  if      = true\n  actions = [\"all\"]\n}\n`,\n\t\t\"unit4/terragrunt.hcl\": `\ndependency \"unit3\" {\n  config_path = \"../unit3\"\n}\n`,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\ttgOptions, err := options.NewTerragruntOptionsForTest(tmpDir)\n\trequire.NoError(t, err)\n\n\topts := list.NewOptions(tgOptions)\n\topts.Format = list.FormatDot\n\topts.Mode = list.ModeDAG\n\topts.Dependencies = true\n\topts.QueueConstructAs = \"apply\"\n\n\tr, w, err := os.Pipe()\n\trequire.NoError(t, err)\n\n\topts.Writers.Writer = w\n\n\terr = list.Run(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\n\tw.Close()\n\n\toutput, err := io.ReadAll(r)\n\trequire.NoError(t, err)\n\n\toutputStr := string(output)\n\n\tassert.Equal(\n\t\tt,\n\t\t`digraph {\n\t\"001/unit1\" [color=red];\n\t\"001/unit2\" ;\n\t\"001/unit2\" -> \"001/unit1\";\n\t\"001/unit3\" [color=red];\n\t\"001/unit3\" -> \"001/unit2\";\n\t\"001/unit4\" ;\n\t\"001/unit4\" -> \"001/unit3\";\n}\n`,\n\t\toutputStr,\n\t)\n}\n"
  },
  {
    "path": "internal/cli/commands/list/options.go",
    "content": "package list\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\t// FormatText outputs the discovered configurations in text format.\n\tFormatText = \"text\"\n\n\t// FormatTree outputs the discovered configurations in tree format.\n\tFormatTree = \"tree\"\n\n\t// FormatLong outputs the discovered configurations in long format.\n\tFormatLong = \"long\"\n\n\t// FormatDot outputs the discovered configurations in GraphViz DOT format.\n\tFormatDot = \"dot\"\n\n\t// SortDAG sorts the discovered configurations in a topological sort order.\n\tSortDAG = \"dag\"\n\n\t// ModeNormal is the default mode for the list command.\n\tModeNormal = \"normal\"\n\n\t// ModeDAG is the mode for the list command that sorts and groups output in DAG order.\n\tModeDAG = \"dag\"\n)\n\ntype Options struct {\n\t*options.TerragruntOptions\n\n\t// Format determines the format of the output.\n\tFormat string\n\n\t// Mode determines the mode of the list command.\n\tMode string\n\n\t// QueueConstructAs constructs the queue as if a particular command was run.\n\tQueueConstructAs string\n\n\t// NoHidden determines if hidden directories should be excluded from the output.\n\tNoHidden bool\n\n\t// Dependencies determines whether to include dependencies in the output.\n\tDependencies bool\n\n\t// Tree determines whether to output in tree format.\n\tTree bool\n\n\t// Long determines whether the output should be in long format.\n\tLong bool\n\n\t// DAG determines whether to output in DAG format.\n\tDAG bool\n}\n\nfunc NewOptions(opts *options.TerragruntOptions) *Options {\n\treturn &Options{\n\t\tTerragruntOptions: opts,\n\t\tFormat:            FormatText,\n\t\tMode:              ModeNormal,\n\t}\n}\n\nfunc (o *Options) Validate() error {\n\terrs := []error{}\n\n\tif err := o.validateFormat(); err != nil {\n\t\terrs = append(errs, err)\n\t}\n\n\tif err := o.validateMode(); err != nil {\n\t\terrs = append(errs, err)\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn errors.New(errors.Join(errs...))\n\t}\n\n\treturn nil\n}\n\nfunc (o *Options) validateFormat() error {\n\tswitch o.Format {\n\tcase FormatText:\n\t\treturn nil\n\tcase FormatTree:\n\t\treturn nil\n\tcase FormatLong:\n\t\treturn nil\n\tcase FormatDot:\n\t\treturn nil\n\tdefault:\n\t\treturn errors.New(\"invalid format: \" + o.Format)\n\t}\n}\n\nfunc (o *Options) validateMode() error {\n\tswitch o.Mode {\n\tcase ModeNormal:\n\t\treturn nil\n\tcase SortDAG:\n\t\treturn nil\n\tdefault:\n\t\treturn errors.New(\"invalid mode: \" + o.Mode)\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/render/cli.go",
    "content": "// Package render provides the command to render the final terragrunt config in various formats.\npackage render\n\nimport (\n\t\"context\"\n\n\truncmd \"github.com/gruntwork-io/terragrunt/internal/cli/commands/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tCommandName = \"render\"\n\n\tFormatFlagName                  = \"format\"\n\tJSONFlagName                    = \"json\"\n\tWriteFlagName                   = \"write\"\n\tWriteAliasFlagName              = \"w\"\n\tOutFlagName                     = \"out\"\n\tWithMetadataFlagName            = \"with-metadata\"\n\tDisableDependentModulesFlagName = \"disable-dependent-modules\"\n)\n\nfunc NewFlags(opts *Options, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\tterragruntPrefix := flags.Prefix{flags.TerragruntPrefix}\n\tterragruntPrefixControl := flags.StrictControlsByCommand(opts.StrictControls, CommandName)\n\n\treturn clihelper.Flags{\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        FormatFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(FormatFlagName),\n\t\t\tDestination: &opts.Format,\n\t\t\tUsage:       \"The output format to render the config in. Currently supports: json\",\n\t\t\tAction: func(_ context.Context, _ *clihelper.Context, value string) error {\n\t\t\t\t// Set the default output path based on the format.\n\t\t\t\tswitch value {\n\t\t\t\tcase FormatJSON:\n\t\t\t\t\tif opts.OutputPath == \"\" {\n\t\t\t\t\t\topts.OutputPath = \"terragrunt.rendered.json\"\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\tcase FormatHCL:\n\t\t\t\t\tif opts.OutputPath == \"\" {\n\t\t\t\t\t\topts.OutputPath = \"terragrunt.rendered.hcl\"\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\tdefault:\n\t\t\t\t\treturn errors.New(\"invalid format: \" + value)\n\t\t\t\t}\n\t\t\t},\n\t\t}),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:    JSONFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(JSONFlagName),\n\t\t\tUsage:   \"Render the config in JSON format. Equivalent to --format=json.\",\n\t\t\tAction: func(_ context.Context, _ *clihelper.Context, value bool) error {\n\t\t\t\topts.Format = FormatJSON\n\n\t\t\t\tif opts.OutputPath == \"\" {\n\t\t\t\t\topts.OutputPath = \"terragrunt.rendered.json\"\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        WriteFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(WriteFlagName),\n\t\t\tAliases:     []string{WriteAliasFlagName},\n\t\t\tDestination: &opts.Write,\n\t\t\tUsage:       \"Write the rendered config to a file.\",\n\t\t}),\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        OutFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(OutFlagName),\n\t\t\tDestination: &opts.OutputPath,\n\t\t\tUsage:       \"The file name that terragrunt should use when rendering the terragrunt.hcl config (next to the unit configuration).\",\n\t\t},\n\t\t\tflags.WithDeprecatedFlagName(\"json-out\", terragruntPrefixControl),                          // `--json-out` (deprecated: use `--out` instead)\n\t\t\tflags.WithDeprecatedEnvVars(tgPrefix.EnvVars(\"render-json-out\"), terragruntPrefixControl),  // `TG_RENDER_JSON_OUT`\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"json-out\"), terragruntPrefixControl), // `TERRAGRUNT_JSON_OUT`\n\t\t),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        WithMetadataFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(WithMetadataFlagName),\n\t\t\tDestination: &opts.RenderMetadata,\n\t\t\tUsage:       \"Add metadata to the rendered output file.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(tgPrefix.EnvVars(\"render-json-with-metadata\"), terragruntPrefixControl), // `TG_RENDER_JSON_WITH_METADATA`\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"with-metadata\"), terragruntPrefixControl),     // `TERRAGRUNT_WITH_METADATA`\n\t\t),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:    DisableDependentModulesFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(DisableDependentModulesFlagName),\n\t\t\tHidden:  true,\n\t\t\tUsage:   \"Deprecated: Disable identification of dependent modules when rendering config. This flag has no effect as dependent modules discovery has been removed.\",\n\t\t\tAction: func(ctx context.Context, _ *clihelper.Context, value bool) error {\n\t\t\t\tif value {\n\t\t\t\t\treturn opts.StrictControls.FilterByNames(controls.DisableDependentModules).Evaluate(ctx)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(tgPrefix.EnvVars(\"render-json-disable-dependent-modules\"), terragruntPrefixControl),  // `TG_RENDER_JSON_DISABLE_DEPENDENT_MODULES`\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"json-disable-dependent-modules\"), terragruntPrefixControl), // `TERRAGRUNT_JSON_DISABLE_DEPENDENT_MODULES`\n\t\t),\n\t}\n}\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\tprefix := flags.Prefix{CommandName}\n\trenderOpts := NewOptions(opts)\n\n\tcmdFlags := append(runcmd.NewFlags(l, opts, nil), NewFlags(renderOpts, prefix)...)\n\tcmdFlags = append(cmdFlags, shared.NewAllFlag(opts, prefix))\n\n\tcmd := &clihelper.Command{\n\t\tName:        CommandName,\n\t\tUsage:       \"Render the final terragrunt config, with all variables, includes, and functions resolved, in the specified format.\",\n\t\tDescription: \"This is useful for enforcing policies using static analysis tools like Open Policy Agent, or for debugging your terragrunt config.\",\n\t\tFlags:       cmdFlags,\n\t\tAction: func(ctx context.Context, _ *clihelper.Context) error {\n\t\t\ttgOpts := opts.OptionsFromContext(ctx)\n\n\t\t\tclonedOpts := renderOpts.Clone()\n\t\t\tclonedOpts.TerragruntOptions = tgOpts\n\n\t\t\treturn Run(ctx, l, clonedOpts)\n\t\t},\n\t}\n\n\treturn cmd\n}\n"
  },
  {
    "path": "internal/cli/commands/render/options.go",
    "content": "package render\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\t// FormatHCL outputs the config in HCL format.\n\tFormatHCL = \"hcl\"\n\n\t// FormatJSON outputs the config in JSON format.\n\tFormatJSON = \"json\"\n)\n\ntype Options struct {\n\t*options.TerragruntOptions\n\n\t// Format determines the format of the output.\n\tFormat string\n\n\t// OutputPath is the path to the file to write the rendered config to.\n\t// This configuration is relative to the Terragrunt config path.\n\tOutputPath string\n\n\t// Write the rendered config to a file.\n\tWrite bool\n\n\t// RenderMetadata adds metadata to the rendered config.\n\tRenderMetadata bool\n}\n\nfunc NewOptions(opts *options.TerragruntOptions) *Options {\n\treturn &Options{\n\t\tTerragruntOptions: opts,\n\t\tFormat:            FormatHCL,\n\t\tWrite:             false,\n\t\tRenderMetadata:    false,\n\t}\n}\n\nfunc (o *Options) Clone() *Options {\n\treturn &Options{\n\t\tTerragruntOptions: o.TerragruntOptions.Clone(),\n\t\tFormat:            o.Format,\n\t\tOutputPath:        o.OutputPath,\n\t\tWrite:             o.Write,\n\t\tRenderMetadata:    o.RenderMetadata,\n\t}\n}\n\nfunc (o *Options) Validate() error {\n\tif err := o.validateFormat(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (o *Options) validateFormat() error {\n\tswitch o.Format {\n\tcase FormatHCL:\n\t\treturn nil\n\tcase FormatJSON:\n\t\treturn nil\n\tdefault:\n\t\treturn errors.New(\"invalid format: \" + o.Format)\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/render/render.go",
    "content": "// Package render provides the command to render the final terragrunt config in various formats.\npackage render\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/ctyhelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/prepare\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/zclconf/go-cty/cty\"\n\tctyjson \"github.com/zclconf/go-cty/cty/json\"\n)\n\nfunc Run(ctx context.Context, l log.Logger, opts *Options) error {\n\tif err := opts.Validate(); err != nil {\n\t\treturn err\n\t}\n\n\tif opts.RunAll {\n\t\treturn runAll(ctx, l, opts)\n\t}\n\n\tprepared, err := prepare.PrepareConfig(ctx, l, opts.TerragruntOptions)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn runRender(l, opts, prepared.Cfg)\n}\n\nfunc runAll(ctx context.Context, l log.Logger, opts *Options) error {\n\td := discovery.NewDiscovery(opts.WorkingDir)\n\n\tcomponents, err := d.Discover(ctx, l, opts.TerragruntOptions)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tunits := components.Filter(component.UnitKind).Sort()\n\n\tvar errs []error\n\n\tfor _, unit := range units {\n\t\tunitOpts := opts.Clone()\n\t\tunitOpts.WorkingDir = unit.Path()\n\n\t\tconfigFilename := config.DefaultTerragruntConfigPath\n\t\tif len(opts.TerragruntConfigPath) > 0 {\n\t\t\tconfigFilename = filepath.Base(opts.TerragruntConfigPath)\n\t\t}\n\n\t\tunitOpts.TerragruntConfigPath = filepath.Join(unit.Path(), configFilename)\n\n\t\tprepared, err := prepare.PrepareConfig(ctx, l, unitOpts.TerragruntOptions)\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := runRender(l, unitOpts, prepared.Cfg); err != nil {\n\t\t\tif opts.FailFast {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\terrs = append(\n\t\t\t\terrs,\n\t\t\t\tfmt.Errorf(\n\t\t\t\t\t\"render of unit %s failed: %w\",\n\t\t\t\t\tunit.Path(),\n\t\t\t\t\terr,\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn errors.Join(errs...)\n\t}\n\n\treturn nil\n}\n\nfunc runRender(l log.Logger, opts *Options, cfg *config.TerragruntConfig) error {\n\tif cfg == nil {\n\t\treturn errors.New(\"terragrunt was not able to render the config because it received no config. This is almost certainly a bug in Terragrunt. Please open an issue on github.com/gruntwork-io/terragrunt with this message and the contents of your terragrunt.hcl\")\n\t}\n\n\tswitch opts.Format {\n\tcase FormatJSON:\n\t\treturn renderJSON(l, opts, cfg)\n\tcase FormatHCL:\n\t\treturn renderHCL(l, opts, cfg)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported render format: %s\", opts.Format)\n\t}\n}\n\nfunc renderHCL(l log.Logger, opts *Options, cfg *config.TerragruntConfig) error {\n\tif opts.Write {\n\t\tbuf := new(bytes.Buffer)\n\n\t\t_, err := cfg.WriteTo(buf)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn writeRendered(l, opts, buf.Bytes())\n\t}\n\n\tl.Infof(\"Rendering config %s\", opts.TerragruntConfigPath)\n\n\t_, err := cfg.WriteTo(opts.Writers.Writer)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc renderJSON(l log.Logger, opts *Options, cfg *config.TerragruntConfig) error {\n\tvar terragruntConfigCty cty.Value\n\n\tif opts.RenderMetadata {\n\t\tcty, err := config.TerragruntConfigAsCtyWithMetadata(cfg)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tterragruntConfigCty = cty\n\t} else {\n\t\tcty, err := config.TerragruntConfigAsCty(cfg)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tterragruntConfigCty = cty\n\t}\n\n\tjsonBytes, err := marshalCtyValueJSONWithoutType(terragruntConfigCty)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif opts.Write {\n\t\treturn writeRendered(l, opts, jsonBytes)\n\t}\n\n\tl.Infof(\"Rendering config %s\", opts.TerragruntConfigPath)\n\n\t_, err = opts.Writers.Writer.Write(jsonBytes)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n\nfunc writeRendered(l log.Logger, opts *Options, data []byte) error {\n\toutPath := opts.OutputPath\n\tif !filepath.IsAbs(outPath) {\n\t\tterragruntConfigDir := filepath.Dir(opts.TerragruntConfigPath)\n\t\toutPath = filepath.Join(terragruntConfigDir, outPath)\n\t}\n\n\tif err := util.EnsureDirectory(filepath.Dir(outPath)); err != nil {\n\t\treturn err\n\t}\n\n\tl.Debugf(\"Rendering config %s to %s\", opts.TerragruntConfigPath, outPath)\n\n\tconst ownerWriteGlobalReadPerms = 0644\n\tif err := os.WriteFile(outPath, data, ownerWriteGlobalReadPerms); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n\n// marshalCtyValueJSONWithoutType marshals the given cty.Value object into a JSON object that does not have the type.\n// Using ctyjson directly would render a json object with two attributes, \"value\" and \"type\", and this function returns\n// just the \"value\".\n// NOTE: We have to do two marshalling passes so that we can extract just the value.\nfunc marshalCtyValueJSONWithoutType(ctyVal cty.Value) ([]byte, error) {\n\tjsonBytesIntermediate, err := ctyjson.Marshal(ctyVal, cty.DynamicPseudoType)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tvar ctyJSONOutput ctyhelper.CtyJSONOutput\n\tif err = json.Unmarshal(jsonBytesIntermediate, &ctyJSONOutput); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tjsonBytes, err := json.Marshal(ctyJSONOutput.Value)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tjsonBytes = append(jsonBytes, '\\n')\n\n\treturn jsonBytes, nil\n}\n"
  },
  {
    "path": "internal/cli/commands/render/render_test.go",
    "content": "package render_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/render\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRenderJSON_Basic(t *testing.T) {\n\tt.Parallel()\n\n\topts, _ := setupTest(t)\n\n\tvar outputBuffer bytes.Buffer\n\n\topts.Writers.Writer = &outputBuffer\n\topts.Format = render.FormatJSON\n\topts.RenderMetadata = false\n\topts.Write = false\n\n\terr := render.Run(t.Context(), logger.CreateLogger(), opts)\n\trequire.NoError(t, err)\n\n\tvar result map[string]any\n\n\terr = json.Unmarshal(outputBuffer.Bytes(), &result)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, result)\n\n\tvalidateRenderedJSON(t, result, false)\n}\n\nfunc TestRenderJSON_WithMetadata(t *testing.T) {\n\tt.Parallel()\n\n\topts, _ := setupTest(t)\n\n\tvar outputBuffer bytes.Buffer\n\n\topts.Writers.Writer = &outputBuffer\n\topts.Format = render.FormatJSON\n\topts.RenderMetadata = true\n\topts.Write = false\n\n\terr := render.Run(t.Context(), logger.CreateLogger(), opts)\n\trequire.NoError(t, err)\n\n\tvar result map[string]any\n\n\terr = json.Unmarshal(outputBuffer.Bytes(), &result)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, result)\n\n\tvalidateRenderedJSON(t, result, true)\n}\n\nfunc TestRenderJSON_WriteToFile(t *testing.T) {\n\tt.Parallel()\n\n\topts, _ := setupTest(t)\n\toutputPath := filepath.Join(helpers.TmpDirWOSymlinks(t), \"output.json\")\n\topts.Format = render.FormatJSON\n\topts.RenderMetadata = false\n\topts.Write = true\n\topts.OutputPath = outputPath\n\n\terr := render.Run(t.Context(), logger.CreateLogger(), opts)\n\trequire.NoError(t, err)\n\n\t// Verify the file was created and contains valid JSON\n\tcontent, err := os.ReadFile(outputPath)\n\trequire.NoError(t, err)\n\n\tvar result map[string]any\n\n\terr = json.Unmarshal(content, &result)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, result)\n\n\tvalidateRenderedJSON(t, result, false)\n}\n\nfunc TestRenderJSON_InvalidFormat(t *testing.T) {\n\tt.Parallel()\n\n\topts, _ := setupTest(t)\n\topts.Format = \"invalid\"\n\n\terr := render.Run(t.Context(), logger.CreateLogger(), opts)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"invalid format\")\n}\n\nfunc TestRenderJSON_HCLFormat(t *testing.T) {\n\tt.Parallel()\n\n\topts, _ := setupTest(t)\n\topts.Format = render.FormatHCL\n\n\tvar renderedBuffer bytes.Buffer\n\n\topts.Writers.Writer = &renderedBuffer\n\n\terr := render.Run(t.Context(), logger.CreateLogger(), opts)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, testTerragruntConfigFixture, renderedBuffer.String())\n}\n\n// setupTest creates a temporary directory with a terragrunt config file and returns the necessary test setup\nfunc setupTest(t *testing.T) (*render.Options, string) {\n\tt.Helper()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\tconfigPath := filepath.Join(tmpDir, \"terragrunt.hcl\")\n\terr := os.WriteFile(configPath, []byte(testTerragruntConfigFixture), 0644)\n\trequire.NoError(t, err)\n\n\ttgOptions, err := options.NewTerragruntOptionsForTest(configPath)\n\trequire.NoError(t, err)\n\n\treturn render.NewOptions(tgOptions), configPath\n}\n\n// validateRenderedJSON validates the common JSON structure and values\nfunc validateRenderedJSON(t *testing.T, result map[string]any, withMetadata bool) {\n\tt.Helper()\n\n\tinputs, ok := result[\"inputs\"].(map[string]any)\n\trequire.True(t, ok)\n\n\tstringInput := inputs[\"string_input\"]\n\n\tif withMetadata {\n\t\tdata, ok := stringInput.(map[string]any)\n\t\trequire.True(t, ok)\n\t\tassert.NotNil(t, data)\n\n\t\tmetadata, ok := data[\"metadata\"].(map[string]any)\n\t\trequire.True(t, ok)\n\t\tassert.NotNil(t, metadata)\n\n\t\tvalue, ok := data[\"value\"].(string)\n\t\trequire.True(t, ok)\n\t\tassert.Equal(t, \"test\", value)\n\t} else {\n\t\tassert.Equal(t, \"test\", stringInput)\n\t}\n\n\tnumberInput := inputs[\"number_input\"]\n\n\tif withMetadata {\n\t\tdata, ok := numberInput.(map[string]any)\n\t\trequire.True(t, ok)\n\t\tassert.NotNil(t, data)\n\t} else {\n\t\tassert.InEpsilon(t, float64(42), numberInput, 0.1)\n\t}\n\n\tboolInput := inputs[\"bool_input\"]\n\n\tif withMetadata {\n\t\tdata, ok := boolInput.(map[string]any)\n\t\trequire.True(t, ok)\n\t\tassert.NotNil(t, data)\n\t} else {\n\t\tassert.Equal(t, true, boolInput)\n\t}\n\n\tlistInput := inputs[\"list_input\"]\n\n\tif withMetadata {\n\t\tdata, ok := listInput.(map[string]any)\n\t\trequire.True(t, ok)\n\t\tassert.NotNil(t, data)\n\t} else {\n\t\tassert.Equal(t, []any{\"item1\", \"item2\"}, listInput)\n\t}\n\n\tmapInput := inputs[\"map_input\"]\n\n\tif withMetadata {\n\t\tdata, ok := mapInput.(map[string]any)\n\t\trequire.True(t, ok)\n\t\tassert.NotNil(t, data)\n\t} else {\n\t\tassert.Equal(t, map[string]any{\"key\": \"value\"}, mapInput)\n\t}\n}\n\nconst testTerragruntConfigFixture = `terraform {\n  source = \"test\"\n}\ninputs = {\n  bool_input = true\n  list_input = [\"item1\", \"item2\"]\n  map_input = {\n    key = \"value\"\n  }\n  number_input = 42\n  string_input = \"test\"\n}\n`\n"
  },
  {
    "path": "internal/cli/commands/run/cli.go",
    "content": "// Package run contains the CLI command definition for interacting with OpenTofu/Terraform.\npackage run\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/graph\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runall\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tCommandName = \"run\"\n)\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\tcmdFlags := NewFlags(l, opts, nil)\n\tcmdFlags = append(cmdFlags, shared.NewAllFlag(opts, nil), shared.NewGraphFlag(opts, nil))\n\n\tcmd := &clihelper.Command{\n\t\tName:        CommandName,\n\t\tUsage:       \"Run an OpenTofu/Terraform command.\",\n\t\tUsageText:   \"terragrunt run [options] -- <tofu/terraform command>\",\n\t\tDescription: \"Run a command, passing arguments to an orchestrated tofu/terraform binary.\\n\\nThis is the explicit, and most flexible form of running an IaC command with Terragrunt. Shortcuts can be found in \\\"terragrunt --help\\\" for common use-cases.\",\n\t\tExamples: []string{\n\t\t\t\"# Run a plan\\nterragrunt run -- plan\\n# Shortcut:\\n# terragrunt plan\",\n\t\t\t\"# Run output with -json flag\\nterragrunt run -- output -json\\n# Shortcut:\\n# terragrunt output -json\",\n\t\t},\n\t\tFlags:       cmdFlags,\n\t\tSubcommands: NewSubcommands(l, opts),\n\t\tAction: func(ctx context.Context, cliCtx *clihelper.Context) error {\n\t\t\ttgOpts := opts.OptionsFromContext(ctx)\n\n\t\t\tif tgOpts.RunAll {\n\t\t\t\treturn runall.Run(ctx, l, tgOpts)\n\t\t\t}\n\n\t\t\tif tgOpts.Graph {\n\t\t\t\treturn graph.Run(ctx, l, tgOpts)\n\t\t\t}\n\n\t\t\tif len(cliCtx.Args()) == 0 {\n\t\t\t\treturn clihelper.ShowCommandHelp(ctx, cliCtx)\n\t\t\t}\n\n\t\t\treturn Action(l, opts)(ctx, cliCtx)\n\t\t},\n\t}\n\n\treturn cmd\n}\n\nfunc NewSubcommands(l log.Logger, opts *options.TerragruntOptions) clihelper.Commands {\n\tvar subcommands = make(clihelper.Commands, len(tf.CommandNames))\n\n\tfor i, name := range tf.CommandNames {\n\t\tusage, visible := tf.CommandUsages[name]\n\n\t\tsubcommand := &clihelper.Command{\n\t\t\tName:       name,\n\t\t\tUsage:      usage,\n\t\t\tHidden:     !visible,\n\t\t\tCustomHelp: ShowTFHelp(l, opts),\n\t\t\tAction: func(ctx context.Context, cliCtx *clihelper.Context) error {\n\t\t\t\treturn Action(l, opts)(ctx, cliCtx)\n\t\t\t},\n\t\t}\n\t\tsubcommands[i] = subcommand\n\t}\n\n\treturn subcommands\n}\n\nfunc Action(l log.Logger, opts *options.TerragruntOptions) clihelper.ActionFunc {\n\treturn func(ctx context.Context, _ *clihelper.Context) error {\n\t\treturn Run(ctx, l, opts)\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/run/flags.go",
    "content": "// Package run provides Terragrunt command flags.\npackage run\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tNoAutoInitFlagName                       = \"no-auto-init\"\n\tNoAutoRetryFlagName                      = \"no-auto-retry\"\n\tNoAutoApproveFlagName                    = \"no-auto-approve\"\n\tNoAutoProviderCacheDirFlagName           = \"no-auto-provider-cache-dir\"\n\tNoEngineFlagName                         = \"no-engine\"\n\tNoDependencyFetchOutputFromStateFlagName = \"no-dependency-fetch-output-from-state\"\n\tTFForwardStdoutFlagName                  = \"tf-forward-stdout\"\n\tUnitsThatIncludeFlagName                 = \"units-that-include\"\n\tDependencyFetchOutputFromStateFlagName   = \"dependency-fetch-output-from-state\"\n\tUsePartialParseConfigCacheFlagName       = \"use-partial-parse-config-cache\"\n\tSummaryPerUnitFlagName                   = \"summary-per-unit\"\n\tVersionManagerFileNameFlagName           = \"version-manager-file-name\"\n\n\tDisableCommandValidationFlagName   = \"disable-command-validation\"\n\tNoDestroyDependenciesCheckFlagName = \"no-destroy-dependencies-check\"\n\tDestroyDependenciesCheckFlagName   = \"destroy-dependencies-check\"\n\n\tSourceFlagName       = \"source\"\n\tSourceMapFlagName    = \"source-map\"\n\tSourceUpdateFlagName = \"source-update\"\n\n\tNoStackGenerate = \"no-stack-generate\"\n\n\t// Terragrunt Provider Cache related flags.\n\n\tProviderCacheFlagName              = \"provider-cache\"\n\tProviderCacheDirFlagName           = \"provider-cache-dir\"\n\tProviderCacheHostnameFlagName      = \"provider-cache-hostname\"\n\tProviderCachePortFlagName          = \"provider-cache-port\"\n\tProviderCacheTokenFlagName         = \"provider-cache-token\"\n\tProviderCacheRegistryNamesFlagName = \"provider-cache-registry-names\"\n\n\t// Engine related environment variables.\n\n\tEngineEnableFlagName    = \"experimental-engine\"\n\tEngineCachePathFlagName = \"engine-cache-path\"\n\tEngineSkipCheckFlagName = \"engine-skip-check\"\n\tEngineLogLevelFlagName  = \"engine-log-level\"\n\n\t// Report related flags.\n\n\tSummaryDisableFlagName = \"summary-disable\"\n\tReportFileFlagName     = \"report-file\"\n\tReportFormatFlagName   = \"report-format\"\n\tReportSchemaFlagName   = \"report-schema-file\"\n\n\t// `--all` related flags.\n\n\tOutDirFlagName     = \"out-dir\"\n\tJSONOutDirFlagName = \"json-out-dir\"\n\n\t// `--graph` related flags.\n\tGraphRootFlagName = \"graph-root\"\n\n\t// Config and download flags - use shared package constants\n\tConfigFlagName = shared.ConfigFlagName\n\n\t// Auth and IAM flags - use shared package constants\n\tInputsDebugFlagName                   = shared.InputsDebugFlagName\n\tIAMAssumeRoleFlagName                 = shared.IAMAssumeRoleFlagName\n\tIAMAssumeRoleDurationFlagName         = shared.IAMAssumeRoleDurationFlagName\n\tIAMAssumeRoleSessionNameFlagName      = shared.IAMAssumeRoleSessionNameFlagName\n\tIAMAssumeRoleWebIdentityTokenFlagName = shared.IAMAssumeRoleWebIdentityTokenFlagName\n)\n\n// NewFlags creates and returns global flags.\nfunc NewFlags(l log.Logger, opts *options.TerragruntOptions, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := flags.Prefix{flags.TgPrefix}\n\tterragruntPrefix := flags.Prefix{flags.TerragruntPrefix}\n\tterragruntPrefixControl := flags.StrictControlsByCommand(opts.StrictControls, CommandName)\n\tlegacyLogsControl := flags.StrictControlsByCommand(opts.StrictControls, CommandName, controls.LegacyLogs)\n\n\tcmdFlags := clihelper.Flags{\n\t\t// `--all` related flags.\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        OutDirFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(OutDirFlagName),\n\t\t\tDestination: &opts.OutputFolder,\n\t\t\tUsage:       \"Directory to store plan files.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"out-dir\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        JSONOutDirFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(JSONOutDirFlagName),\n\t\t\tDestination: &opts.JSONOutputFolder,\n\t\t\tUsage:       \"Directory to store json plan files.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"json-out-dir\"), terragruntPrefixControl)),\n\n\t\t// `graph/-graph` related flags.\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        GraphRootFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(GraphRootFlagName),\n\t\t\tDestination: &opts.GraphRoot,\n\t\t\tUsage:       \"Root directory from where to build graph dependencies.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"graph-root\"), terragruntPrefixControl)),\n\n\t\t// `--all` and `--graph` related flags.\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        NoStackGenerate,\n\t\t\tEnvVars:     tgPrefix.EnvVars(NoStackGenerate),\n\t\t\tDestination: &opts.NoStackGenerate,\n\t\t\tUsage:       \"Disable automatic stack regeneration before running the command.\",\n\t\t}),\n\n\t\t//  Backward compatibility with `terragrunt-` prefix flags.\n\n\t\tshared.NewConfigFlag(opts, prefix, CommandName),\n\n\t\tshared.NewTFPathFlag(opts),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        NoAutoInitFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(NoAutoInitFlagName),\n\t\t\tUsage:       \"Don't automatically run 'terraform/tofu init' during other terragrunt commands. You must run 'terragrunt init' manually.\",\n\t\t\tNegative:    true,\n\t\t\tDestination: &opts.AutoInit,\n\t\t},\n\t\t\tflags.WithDeprecatedFlag(&clihelper.BoolFlag{\n\t\t\t\tEnvVars: terragruntPrefix.EnvVars(\"auto-init\"),\n\t\t\t}, nil, terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        NoAutoRetryFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(NoAutoRetryFlagName),\n\t\t\tDestination: &opts.AutoRetry,\n\t\t\tUsage:       \"Don't automatically re-run command in case of transient errors.\",\n\t\t\tNegative:    true,\n\t\t},\n\t\t\tflags.WithDeprecatedFlag(&clihelper.BoolFlag{\n\t\t\t\tEnvVars: terragruntPrefix.EnvVars(\"auto-retry\"),\n\t\t\t}, nil, terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        NoAutoApproveFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(NoAutoApproveFlagName),\n\t\t\tDestination: &opts.RunAllAutoApprove,\n\t\t\tUsage:       \"Don't automatically append '-auto-approve' to the underlying OpenTofu/Terraform commands run with 'run --all'.\",\n\t\t\tNegative:    true,\n\t\t},\n\t\t\tflags.WithDeprecatedFlag(&clihelper.BoolFlag{\n\t\t\t\tEnvVars: terragruntPrefix.EnvVars(\"auto-approve\"),\n\t\t\t}, nil, terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        NoAutoProviderCacheDirFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(NoAutoProviderCacheDirFlagName),\n\t\t\tDestination: &opts.NoAutoProviderCacheDir,\n\t\t\tUsage:       \"Disable the auto-provider-cache-dir feature even when the experiment is enabled.\",\n\t\t}),\n\n\t\tshared.NewDownloadDirFlag(opts, prefix, CommandName),\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        SourceFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(SourceFlagName),\n\t\t\tDestination: &opts.Source,\n\t\t\tUsage:       \"Download OpenTofu/Terraform configurations from the specified source into a temporary folder, and run Terraform in that temporary folder.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"source\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        SourceUpdateFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(SourceUpdateFlagName),\n\t\t\tDestination: &opts.SourceUpdate,\n\t\t\tUsage:       \"Delete the contents of the temporary folder to clear out any old, cached source code before downloading new source code into it.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"source-update\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.MapFlag[string, string]{\n\t\t\tName:        SourceMapFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(SourceMapFlagName),\n\t\t\tDestination: &opts.SourceMap,\n\t\t\tUsage:       \"Replace any source URL (including the source URL of a config pulled in with dependency blocks) that has root source with dest.\",\n\t\t\tSplitter:    util.SplitUrls,\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"source-map\"), terragruntPrefixControl)),\n\n\t\t// Assume IAM Role flags.\n\t\tshared.NewInputsDebugFlag(opts, prefix, CommandName),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        UsePartialParseConfigCacheFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(UsePartialParseConfigCacheFlagName),\n\t\t\tDestination: &opts.UsePartialParseConfigCache,\n\t\t\tUsage:       \"Enables caching of includes during partial parsing operations. Will also be used for the --iam-role option if provided.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"use-partial-parse-config-cache\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.SliceFlag[string]{\n\t\t\tName:        VersionManagerFileNameFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(VersionManagerFileNameFlagName),\n\t\t\tDestination: &opts.VersionManagerFileName,\n\t\t\tUsage:       \"File names used during the computation of the cache key for the version manager files.\",\n\t\t}),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:    DependencyFetchOutputFromStateFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(DependencyFetchOutputFromStateFlagName),\n\t\t\tUsage:   \"Enable the dependency-fetch-output-from-state experiment to fetch dependency output directly from the state file instead of using tofu/terraform output.\",\n\t\t\tAction: func(_ context.Context, _ *clihelper.Context, val bool) error {\n\t\t\t\tif val {\n\t\t\t\t\treturn opts.Experiments.EnableExperiment(experiment.DependencyFetchOutputFromState)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"fetch-dependency-output-from-state\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        NoDependencyFetchOutputFromStateFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(NoDependencyFetchOutputFromStateFlagName),\n\t\t\tDestination: &opts.NoDependencyFetchOutputFromState,\n\t\t\tUsage:       \"Disable the dependency-fetch-output-from-state feature even when the experiment is enabled.\",\n\t\t\tHidden:      true,\n\t\t}),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        TFForwardStdoutFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(TFForwardStdoutFlagName),\n\t\t\tDestination: &opts.ForwardTFStdout,\n\t\t\tUsage:       \"If specified, the output of OpenTofu/Terraform commands will be printed as is, without being integrated into the Terragrunt log.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"forward-tf-stdout\"), terragruntPrefixControl),\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"include-module-prefix\"), legacyLogsControl)),\n\n\t\tflags.NewFlag(&clihelper.SliceFlag[string]{\n\t\t\tName:    UnitsThatIncludeFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(UnitsThatIncludeFlagName),\n\t\t\tUsage:   \"If flag is set, 'run --all' will only run the command against Terragrunt modules that include the specified file.\",\n\t\t\tHidden:  true,\n\t\t\tAction: func(ctx context.Context, _ *clihelper.Context, value []string) error {\n\t\t\t\tif len(value) != 0 {\n\t\t\t\t\tif err := opts.StrictControls.FilterByNames(controls.UnitsThatInclude).Evaluate(ctx); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tfor _, v := range value {\n\t\t\t\t\t\tattrExpr, err := filter.NewAttributeExpression(filter.AttributeReading, v)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\topts.Filters = append(opts.Filters, filter.NewFilter(attrExpr, attrExpr.String()))\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"modules-that-include\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        DisableCommandValidationFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(DisableCommandValidationFlagName),\n\t\t\tDestination: &opts.DisableCommandValidation,\n\t\t\tUsage:       \"When this flag is set, Terragrunt will not validate the tofu/terraform command.\",\n\t\t\tHidden:      true,\n\t\t\tAction: func(ctx context.Context, _ *clihelper.Context, value bool) error {\n\t\t\t\tif value {\n\t\t\t\t\treturn opts.StrictControls.FilterByNames(controls.DisableCommandValidation).Evaluate(ctx)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"disable-command-validation\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:    NoDestroyDependenciesCheckFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(NoDestroyDependenciesCheckFlagName),\n\t\t\tUsage:   \"When this flag is set, Terragrunt will not check for dependent units when destroying.\",\n\t\t\tHidden:  true,\n\t\t\tAction: func(ctx context.Context, _ *clihelper.Context, value bool) error {\n\t\t\t\tif value {\n\t\t\t\t\treturn opts.StrictControls.FilterByNames(controls.NoDestroyDependenciesCheck).Evaluate(ctx)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        DestroyDependenciesCheckFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(DestroyDependenciesCheckFlagName),\n\t\t\tDestination: &opts.DestroyDependenciesCheck,\n\t\t\tUsage:       \"When this flag is set, Terragrunt will check for dependent units when destroying.\",\n\t\t}),\n\n\t\t// Terragrunt Provider Cache flags.\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        ProviderCacheFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(ProviderCacheFlagName),\n\t\t\tDestination: &opts.ProviderCacheOptions.Enabled,\n\t\t\tUsage:       \"Enables Terragrunt's provider caching.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"provider-cache\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        ProviderCacheDirFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(ProviderCacheDirFlagName),\n\t\t\tDestination: &opts.ProviderCacheOptions.Dir,\n\t\t\tUsage:       \"The path to the Terragrunt provider cache directory. By default, 'terragrunt/providers' folder in the user cache directory.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"provider-cache-dir\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        ProviderCacheTokenFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(ProviderCacheTokenFlagName),\n\t\t\tDestination: &opts.ProviderCacheOptions.Token,\n\t\t\tUsage:       \"The token for authentication to the Terragrunt Provider Cache server. By default, assigned automatically.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"provider-cache-token\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        ProviderCacheHostnameFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(ProviderCacheHostnameFlagName),\n\t\t\tDestination: &opts.ProviderCacheOptions.Hostname,\n\t\t\tUsage:       \"The hostname of the Terragrunt Provider Cache server. By default, 'localhost'.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"provider-cache-hostname\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[int]{\n\t\t\tName:        ProviderCachePortFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(ProviderCachePortFlagName),\n\t\t\tDestination: &opts.ProviderCacheOptions.Port,\n\t\t\tUsage:       \"The port of the Terragrunt Provider Cache server. By default, assigned automatically.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"provider-cache-port\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.SliceFlag[string]{\n\t\t\tName:        ProviderCacheRegistryNamesFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(ProviderCacheRegistryNamesFlagName),\n\t\t\tDestination: &opts.ProviderCacheOptions.RegistryNames,\n\t\t\tUsage:       \"The list of remote registries to cached by Terragrunt Provider Cache server. By default, 'registry.terraform.io', 'registry.opentofu.org'.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"provider-cache-registry-names\"), terragruntPrefixControl)),\n\n\t\tshared.NewAuthProviderCmdFlag(opts, prefix, CommandName),\n\n\t\t// Terragrunt engine flags.\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:    EngineEnableFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(EngineEnableFlagName),\n\t\t\tUsage:   \"Enable the iac-engine experiment to use IaC engines.\",\n\t\t\tHidden:  true,\n\t\t\tAction: func(_ context.Context, _ *clihelper.Context, val bool) error {\n\t\t\t\tif val {\n\t\t\t\t\treturn opts.Experiments.EnableExperiment(experiment.IacEngine)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"experimental-engine\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        EngineCachePathFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(EngineCachePathFlagName),\n\t\t\tDestination: &opts.EngineOptions.CachePath,\n\t\t\tUsage:       \"Cache path for Terragrunt engine files.\",\n\t\t\tHidden:      true,\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"engine-cache-path\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        EngineSkipCheckFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(EngineSkipCheckFlagName),\n\t\t\tDestination: &opts.EngineOptions.SkipChecksumCheck,\n\t\t\tUsage:       \"Skip checksum check for Terragrunt engine files.\",\n\t\t\tHidden:      true,\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"engine-skip-check\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        EngineLogLevelFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(EngineLogLevelFlagName),\n\t\t\tDestination: &opts.EngineOptions.LogLevel,\n\t\t\tUsage:       \"Terragrunt engine log level.\",\n\t\t\tHidden:      true,\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"engine-log-level\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        NoEngineFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(NoEngineFlagName),\n\t\t\tDestination: &opts.EngineOptions.NoEngine,\n\t\t\tUsage:       \"Disable IaC engines even when the iac-engine experiment is enabled.\",\n\t\t\tHidden:      true,\n\t\t}),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        SummaryDisableFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(SummaryDisableFlagName),\n\t\t\tDestination: &opts.SummaryDisable,\n\t\t\tUsage:       `Disable the summary output at the end of a run.`,\n\t\t}),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        SummaryPerUnitFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(SummaryPerUnitFlagName),\n\t\t\tDestination: &opts.SummaryPerUnit,\n\t\t\tUsage:       `Show duration information for each unit in the summary output.`,\n\t\t}),\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:    ReportFileFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(ReportFileFlagName),\n\t\t\tUsage:   `Path to generate report file in.`,\n\t\t\tSetter: func(value string) error {\n\t\t\t\tif value == \"\" {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\topts.ReportFile = value\n\n\t\t\t\text := filepath.Ext(value)\n\t\t\t\tif ext == \"\" {\n\t\t\t\t\text = \".csv\"\n\t\t\t\t}\n\n\t\t\t\tif ext != \".csv\" && ext != \".json\" {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tif opts.ReportFormat == \"\" {\n\t\t\t\t\topts.ReportFormat = report.Format(ext[1:])\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}),\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:    ReportFormatFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(ReportFormatFlagName),\n\t\t\tUsage:   `Format of the report file.`,\n\t\t\tSetter: func(value string) error {\n\t\t\t\tif value == \"\" && opts.ReportFormat == \"\" {\n\t\t\t\t\topts.ReportFormat = report.FormatCSV\n\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\topts.ReportFormat = report.Format(value)\n\n\t\t\t\tswitch opts.ReportFormat {\n\t\t\t\tcase report.FormatCSV:\n\t\t\t\tcase report.FormatJSON:\n\t\t\t\tdefault:\n\t\t\t\t\treturn fmt.Errorf(\"unsupported report format: %s\", value)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}),\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        ReportSchemaFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(ReportSchemaFlagName),\n\t\t\tUsage:       `Path to generate report schema file in.`,\n\t\t\tDestination: &opts.ReportSchemaFile,\n\t\t}),\n\t}\n\n\t// Add shared flags\n\tcmdFlags = cmdFlags.Add(shared.NewBackendFlags(opts, prefix)...)\n\tcmdFlags = cmdFlags.Add(shared.NewFeatureFlags(opts, prefix)...)\n\tcmdFlags = cmdFlags.Add(shared.NewFailFastFlag(opts))\n\tcmdFlags = cmdFlags.Add(shared.NewIAMAssumeRoleFlags(opts, prefix, CommandName)...)\n\tcmdFlags = cmdFlags.Add(shared.NewQueueFlags(opts, prefix)...)\n\tcmdFlags = cmdFlags.Add(shared.NewFilterFlags(l, opts)...)\n\tcmdFlags = cmdFlags.Add(shared.NewParallelismFlag(opts))\n\n\treturn cmdFlags.Sort()\n}\n"
  },
  {
    "path": "internal/cli/commands/run/help.go",
    "content": "package run\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// TFCommandHelpTemplate is the TF command CLI help template.\nconst TFCommandHelpTemplate = `Usage: {{ if .Command.UsageText }}{{ wrap .Command.UsageText 3 }}{{ else }}{{ range $parent := parentCommands . }}{{ $parent.HelpName }} {{ end }}[global options] {{ .Command.HelpName }} [options]{{ if eq .Command.Name \"` + tf.CommandNameApply + `\" }} [PLAN]{{ end }}{{ end }}{{ $description := .Command.Usage }}{{ if .Command.Description }}{{ $description = .Command.Description }}{{ end }}{{ if $description }}\n\n   {{ wrap $description 3 }}{{ end }}{{ if ne .Parent.Command.Name \"` + CommandName + `\" }}\n\n   This is a shortcut for the command ` + \"`terragrunt \" + CommandName + \"`\" + `.{{ end }}\n\n   It wraps the ` + \"`{{ tfCommand }}`\" + ` command of the binary defined by ` + \"`tf-path`\" + `.\n\n{{ if isTerraformPath }}Terraform{{ else }}OpenTofu{{ end }} ` + \"`{{ tfCommand }}`\" + ` help:{{ $tfHelp := runTFHelp }}{{ if $tfHelp }}\n\n{{ $tfHelp }}{{ end }}\n`\n\n// ShowTFHelp prints TF help for the given `cliCtx.Command` command.\nfunc ShowTFHelp(l log.Logger, opts *options.TerragruntOptions) clihelper.HelpFunc {\n\treturn func(ctx context.Context, cliCtx *clihelper.Context) error {\n\t\tif err := shared.NewTFPathFlag(opts).Parse(cliCtx.Args()); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tclihelper.HelpPrinterCustom(cliCtx, TFCommandHelpTemplate, map[string]any{\n\t\t\t\"isTerraformPath\": func() bool {\n\t\t\t\treturn isTerraformPath(opts)\n\t\t\t},\n\t\t\t\"runTFHelp\": func() string {\n\t\t\t\treturn runTFHelp(ctx, cliCtx, l, opts)\n\t\t\t},\n\t\t\t\"tfCommand\": func() string {\n\t\t\t\treturn cliCtx.Command.Name\n\t\t\t},\n\t\t})\n\n\t\treturn nil\n\t}\n}\n\nfunc runTFHelp(ctx context.Context, cliCtx *clihelper.Context, l log.Logger, opts *options.TerragruntOptions) string {\n\topts = opts.Clone()\n\topts.Writers.Writer = io.Discard\n\n\tterraformHelpCmd := []string{tf.FlagNameHelpLong, cliCtx.Command.Name}\n\n\tout, err := tf.RunCommandWithOutput(ctx, l, configbridge.TFRunOptsFromOpts(opts), terraformHelpCmd...)\n\tif err != nil {\n\t\tvar processError util.ProcessExecutionError\n\t\tif ok := errors.As(err, &processError); ok {\n\t\t\terr = processError.Err\n\t\t}\n\n\t\treturn fmt.Sprintf(\"Failed to execute \\\"%s %s\\\": %s\", opts.TFPath, strings.Join(terraformHelpCmd, \" \"), err.Error())\n\t}\n\n\tresult := out.Stdout.String()\n\tlines := strings.Split(result, \"\\n\")\n\n\t// Trim first empty lines or that has prefix \"Usage:\".\n\tfor i := range lines {\n\t\tif strings.TrimSpace(lines[i]) == \"\" || strings.HasPrefix(lines[i], \"Usage:\") {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn strings.Join(lines[i:], \"\\n\")\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "internal/cli/commands/run/run.go",
    "content": "package run\n\nimport (\n\t\"context\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/os/stdout\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/graph\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds/providers/externalcmd\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runall\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// Run runs the run command.\nfunc Run(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\tif opts.TerraformCommand == tf.CommandNameDestroy {\n\t\topts.CheckDependentUnits = opts.DestroyDependenciesCheck\n\t}\n\n\tr := report.NewReport().WithWorkingDir(opts.WorkingDir)\n\n\t// Configure report colors.\n\t//\n\t// This doesn't actually do anything for single-unit runs, but it's\n\t// helpful to leave it in here for consistency, if we ever add\n\t// support for run summaries in single-unit runs.\n\tif l.Formatter().DisabledColors() || stdout.IsRedirected() {\n\t\tr.WithDisableColor()\n\t}\n\n\tif opts.ReportFormat != \"\" {\n\t\tr.WithFormat(opts.ReportFormat)\n\t}\n\n\ttgOpts := opts.OptionsFromContext(ctx)\n\n\tif tgOpts.RunAll {\n\t\treturn runall.Run(ctx, l, tgOpts)\n\t}\n\n\tif tgOpts.Graph {\n\t\treturn graph.Run(ctx, l, tgOpts)\n\t}\n\n\tif opts.ReportSchemaFile != \"\" {\n\t\tdefer r.WriteSchemaToFile(opts.ReportSchemaFile) //nolint:errcheck\n\t}\n\n\tif opts.ReportFile != \"\" {\n\t\tdefer r.WriteToFile(opts.ReportFile) //nolint:errcheck\n\t}\n\n\tif opts.TerraformCommand == \"\" {\n\t\treturn errors.New(run.MissingCommand{})\n\t}\n\n\t// Early exit for version command to avoid expensive setup\n\tif opts.TerraformCommand == tf.CommandNameVersion {\n\t\treturn runVersionCommand(ctx, l, opts)\n\t}\n\n\t// We need to get the credentials from auth-provider-cmd at the very beginning,\n\t// since the locals block may contain `get_aws_account_id()` func.\n\tcredsGetter := creds.NewGetter()\n\tif err := credsGetter.ObtainAndUpdateEnvIfNecessary(\n\t\tctx,\n\t\tl,\n\t\topts.Env,\n\t\texternalcmd.NewProvider(l, opts.AuthProviderCmd, configbridge.ShellRunOptsFromOpts(opts)),\n\t); err != nil {\n\t\treturn err\n\t}\n\n\tl, err := checkVersionConstraints(ctx, l, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparseCtx, pctx := configbridge.NewParsingContext(ctx, l, opts)\n\n\tcfg, err := config.ReadTerragruntConfig(parseCtx, l, pctx, pctx.ParserOptions)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif opts.CheckDependentUnits {\n\t\tallowDestroy := confirmActionWithDependentUnits(ctx, l, opts, cfg)\n\t\tif !allowDestroy {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\trunCfg := cfg.ToRunConfig(l)\n\n\tunitPath := filepath.Clean(opts.RootWorkingDir)\n\n\tif _, err := r.EnsureRun(l, unitPath); err != nil {\n\t\treturn err\n\t}\n\n\tvar runErr error\n\n\tdefer func() {\n\t\tif runErr != nil {\n\t\t\tif endErr := r.EndRun(\n\t\t\t\tl,\n\t\t\t\tunitPath,\n\t\t\t\treport.WithResult(report.ResultFailed),\n\t\t\t\treport.WithReason(report.ReasonRunError),\n\t\t\t\treport.WithCauseRunError(runErr.Error()),\n\t\t\t); endErr != nil {\n\t\t\t\tl.Errorf(\"Error ending run for unit %s: %v\", unitPath, endErr)\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\n\t\tif endErr := r.EndRun(\n\t\t\tl,\n\t\t\tunitPath,\n\t\t\treport.WithResult(report.ResultSucceeded),\n\t\t); endErr != nil {\n\t\t\tl.Errorf(\"Error ending run for unit %s: %v\", unitPath, endErr)\n\t\t}\n\t}()\n\n\trunErr = run.Run(ctx, l, configbridge.NewRunOptions(tgOpts), r, runCfg, credsGetter)\n\n\treturn runErr\n}\n\n// isTerraformPath returns true if the TFPath ends with the default Terraform path.\n// This is used by help.go to determine whether to show \"Terraform\" or \"OpenTofu\" in help text.\nfunc isTerraformPath(opts *options.TerragruntOptions) bool {\n\treturn strings.HasSuffix(opts.TFPath, options.TerraformDefaultPath)\n}\n\n// runVersionCommand runs the version command. We do this instead of going through the normal run flow because\n// we can resolve `version` a lot more cheaply.\nfunc runVersionCommand(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\tif !opts.TFPathExplicitlySet {\n\t\tif tfPath, err := getTFPathFromConfig(ctx, l, opts); err != nil {\n\t\t\treturn err\n\t\t} else if tfPath != \"\" {\n\t\t\topts.TFPath = tfPath\n\t\t}\n\t}\n\n\treturn tf.RunCommand(ctx, l, configbridge.TFRunOptsFromOpts(opts), opts.TerraformCliArgs.Slice()...)\n}\n\nfunc getTFPathFromConfig(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) (string, error) {\n\tif !util.FileExists(opts.TerragruntConfigPath) {\n\t\tl.Debugf(\"Did not find the config file %s\", opts.TerragruntConfigPath)\n\n\t\treturn \"\", nil\n\t}\n\n\tcfg, err := getTerragruntConfig(ctx, l, opts)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn cfg.TerraformBinary, nil\n}\n\n// CheckVersionConstraints checks the version constraints of both terragrunt and terraform.\n// Note that as a side effect this will set the following settings on terragruntOptions:\n// - TerraformPath\n// - TerraformVersion\n// - FeatureFlags\n// TODO: Look into a way to refactor this function to avoid the side effect.\nfunc checkVersionConstraints(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) (log.Logger, error) {\n\tpartialTerragruntConfig, err := getTerragruntConfig(ctx, l, opts)\n\tif err != nil {\n\t\treturn l, err\n\t}\n\n\t// If the TFPath is not explicitly set, use the TFPath from the config if it is set.\n\tif !opts.TFPathExplicitlySet && partialTerragruntConfig.TerraformBinary != \"\" {\n\t\topts.TFPath = partialTerragruntConfig.TerraformBinary\n\t}\n\n\tl, ver, impl, err := run.PopulateTFVersion(ctx, l, opts.WorkingDir, opts.VersionManagerFileName, configbridge.TFRunOptsFromOpts(opts))\n\tif err != nil {\n\t\treturn l, err\n\t}\n\n\topts.TerraformVersion = ver\n\topts.TofuImplementation = impl\n\n\tterraformVersionConstraint := run.DefaultTerraformVersionConstraint\n\tif partialTerragruntConfig.TerraformVersionConstraint != \"\" {\n\t\tterraformVersionConstraint = partialTerragruntConfig.TerraformVersionConstraint\n\t}\n\n\tif err := run.CheckTerraformVersionMeetsConstraint(opts.TerraformVersion, terraformVersionConstraint); err != nil {\n\t\treturn l, err\n\t}\n\n\tif partialTerragruntConfig.TerragruntVersionConstraint != \"\" {\n\t\tif err := run.CheckTerragruntVersionMeetsConstraint(opts.TerragruntVersion, partialTerragruntConfig.TerragruntVersionConstraint); err != nil {\n\t\t\treturn l, err\n\t\t}\n\t}\n\n\tif partialTerragruntConfig.FeatureFlags != nil {\n\t\t// update feature flags for evaluation\n\t\tfor _, flag := range partialTerragruntConfig.FeatureFlags {\n\t\t\tflagName := flag.Name\n\n\t\t\tdefaultValue, err := flag.DefaultAsString()\n\t\t\tif err != nil {\n\t\t\t\treturn l, err\n\t\t\t}\n\n\t\t\tif _, exists := opts.FeatureFlags.Load(flagName); !exists {\n\t\t\t\topts.FeatureFlags.Store(flagName, defaultValue)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn l, nil\n}\n\nfunc getTerragruntConfig(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) (*config.TerragruntConfig, error) {\n\tctx, configCtx := configbridge.NewParsingContext(ctx, l, opts)\n\tconfigCtx = configCtx.WithDecodeList(\n\t\tconfig.TerragruntVersionConstraints,\n\t\tconfig.FeatureFlagsBlock,\n\t)\n\n\treturn config.PartialParseConfigFile(\n\t\tctx,\n\t\tconfigCtx,\n\t\tl,\n\t\topts.TerragruntConfigPath,\n\t\tnil,\n\t)\n}\n\n// confirmActionWithDependentUnits - Show warning with list of dependent modules from current module before destroy\nfunc confirmActionWithDependentUnits(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tcfg *config.TerragruntConfig,\n) bool {\n\tunits := findDependentUnits(ctx, l, opts, cfg)\n\tif len(units) != 0 {\n\t\tif _, err := opts.Writers.ErrWriter.Write([]byte(\"Detected dependent units:\\n\")); err != nil {\n\t\t\tl.Error(err)\n\t\t\treturn false\n\t\t}\n\n\t\tfor _, unit := range units {\n\t\t\tif _, err := opts.Writers.ErrWriter.Write([]byte(unit + \"\\n\")); err != nil {\n\t\t\t\tl.Error(err)\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\n\t\tprompt := \"WARNING: Are you sure you want to continue?\"\n\n\t\tshouldRun, err := shell.PromptUserForYesNo(ctx, l, prompt, opts.NonInteractive, opts.Writers.ErrWriter)\n\t\tif err != nil {\n\t\t\tl.Error(err)\n\t\t\treturn false\n\t\t}\n\n\t\treturn shouldRun\n\t}\n\n\treturn true\n}\n\n// findDependentUnits finds dependent units for the given unit, and returns their paths.\nfunc findDependentUnits(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tcfg *config.TerragruntConfig,\n) []string {\n\tunits := runner.FindDependentUnits(ctx, l, opts, cfg)\n\n\tpaths := make([]string, len(units))\n\tfor i, unit := range units {\n\t\tpaths[i] = unit.Path()\n\t}\n\n\treturn paths\n}\n"
  },
  {
    "path": "internal/cli/commands/scaffold/cli.go",
    "content": "// Package scaffold provides the command to scaffold a new Terragrunt module.\npackage scaffold\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tCommandName = \"scaffold\"\n\n\tOutputFolderFlagName = \"output-folder\"\n\tVarFlagName          = \"var\"\n\tVarFileFlagName      = \"var-file\"\n\tNoDependencyPrompt   = \"no-dependency-prompt\"\n)\n\nfunc NewFlags(opts *options.TerragruntOptions, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\n\t// Start with shared scaffolding flags\n\tscaffoldFlags := shared.NewScaffoldingFlags(opts, prefix)\n\n\t// Add scaffold-specific flags\n\tscaffoldFlags = append(scaffoldFlags,\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        OutputFolderFlagName,\n\t\t\tDestination: &opts.ScaffoldOutputFolder,\n\t\t\tUsage:       \"Output folder for scaffold output.\",\n\t\t}),\n\n\t\tflags.NewFlag(&clihelper.SliceFlag[string]{\n\t\t\tName:        VarFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(VarFlagName),\n\t\t\tDestination: &opts.ScaffoldVars,\n\t\t\tUsage:       \"Variables for usage in scaffolding.\",\n\t\t}),\n\n\t\tflags.NewFlag(&clihelper.SliceFlag[string]{\n\t\t\tName:        VarFileFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(VarFileFlagName),\n\t\t\tDestination: &opts.ScaffoldVarFiles,\n\t\t\tUsage:       \"Files with variables to be used in unit scaffolding.\",\n\t\t}),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        NoDependencyPrompt,\n\t\t\tEnvVars:     tgPrefix.EnvVars(NoDependencyPrompt),\n\t\t\tDestination: &opts.NoDependencyPrompt,\n\t\t\tUsage:       \"Do not prompt for confirmation to include dependencies.\",\n\t\t}),\n\t)\n\n\treturn scaffoldFlags\n}\n\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\tflags := NewFlags(opts, nil)\n\t// Accept backend and feature flags for scaffold as well\n\tflags = append(flags, shared.NewBackendFlags(opts, nil)...)\n\tflags = append(flags, shared.NewFeatureFlags(opts, nil)...)\n\n\treturn &clihelper.Command{\n\t\tName:  CommandName,\n\t\tUsage: \"Scaffold a new Terragrunt module.\",\n\t\tFlags: flags,\n\t\tAction: func(ctx context.Context, cliCtx *clihelper.Context) error {\n\t\t\tvar moduleURL, templateURL string\n\n\t\t\tif val := cliCtx.Args().Get(0); val != \"\" {\n\t\t\t\tmoduleURL = val\n\t\t\t}\n\n\t\t\tif val := cliCtx.Args().Get(1); val != \"\" {\n\t\t\t\ttemplateURL = val\n\t\t\t}\n\n\t\t\tif opts.ScaffoldRootFileName == \"\" {\n\t\t\t\topts.ScaffoldRootFileName = GetDefaultRootFileName(ctx, opts)\n\t\t\t}\n\n\t\t\treturn Run(ctx, l, opts.OptionsFromContext(ctx), moduleURL, templateURL)\n\t\t},\n\t}\n}\n\nfunc GetDefaultRootFileName(ctx context.Context, opts *options.TerragruntOptions) string {\n\tif err := opts.StrictControls.FilterByNames(controls.RootTerragruntHCL).SuppressWarning().Evaluate(ctx); err != nil {\n\t\treturn config.RecommendedParentConfigName\n\t}\n\n\t// Check to see if you can find the recommended parent config name first,\n\t// if a user has it defined, go ahead and use it.\n\tdir := opts.WorkingDir\n\n\tprevDir := \"\"\n\tfor foldersToCheck := opts.MaxFoldersToCheck; dir != prevDir && dir != \"\" && foldersToCheck > 0; foldersToCheck-- {\n\t\tprevDir = dir\n\n\t\t_, err := os.Stat(filepath.Join(dir, config.RecommendedParentConfigName))\n\t\tif err == nil {\n\t\t\treturn config.RecommendedParentConfigName\n\t\t}\n\n\t\tdir = filepath.Dir(dir)\n\t}\n\n\treturn config.DefaultTerragruntConfigPath\n}\n"
  },
  {
    "path": "internal/cli/commands/scaffold/scaffold.go",
    "content": "package scaffold\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/hcl/format\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\n\tboilerplate_options \"github.com/gruntwork-io/boilerplate/options\"\n\t\"github.com/gruntwork-io/boilerplate/templates\"\n\t\"github.com/gruntwork-io/boilerplate/variables\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/hashicorp/go-getter/v2\"\n)\n\nconst (\n\tsourceURLTypeHTTPS = \"git-https\"\n\tsourceURLTypeGit   = \"git-ssh\"\n\tsourceGitSSHUser   = \"git\"\n\n\tsourceURLTypeVar    = \"SourceUrlType\"\n\tsourceGitSSHUserVar = \"SourceGitSshUser\"\n\trefVar              = \"Ref\"\n\t// refParam - ?ref param from url\n\trefParam = \"ref\"\n\n\tmoduleURLPattern = `(?:git|hg|s3|gcs)::([^:]+)://([^/]+)(/.*)`\n\tmoduleURLParts   = 4\n\n\t// TODO: Make the root configuration file name configurable\n\tDefaultBoilerplateConfig = `\nvariables:\n  - name: EnableRootInclude\n    description: Should include root module\n    type: bool\n    default: true\n  - name: RootFileName\n    description: Name of the root Terragrunt configuration file\n    type: string\n`\n\tDefaultTerragruntTemplate = `\n# This is a Terragrunt unit generated by Gruntwork Boilerplate (https://github.com/gruntwork-io/boilerplate).\nterraform {\n  source = \"{{ .sourceUrl }}\"\n}\n{{ if .EnableRootInclude }}\ninclude \"root\" {\n  path = find_in_parent_folders(\"{{ .RootFileName }}\")\n}\n{{ end }}\ninputs = {\n  # --------------------------------------------------------------------------------------------------------------------\n  # Required input variables\n  # --------------------------------------------------------------------------------------------------------------------\n  {{ range .requiredVariables }}\n  {{- if eq 1 (regexSplit \"\\n\" .Description -1 | len ) }}\n  # Description: {{ .Description }}\n  {{- else }}\n  # Description:\n    {{- range $line := regexSplit \"\\n\" .Description -1 }}\n    # {{ $line | indent 2 }}\n    {{- end }}\n  {{- end }}\n  # Type: {{ .Type }}\n  {{ .Name }} = {{ .DefaultValuePlaceholder }}  # TODO: fill in value\n  {{ end }}\n\n  # --------------------------------------------------------------------------------------------------------------------\n  # Optional input variables\n  # Uncomment the ones you wish to set\n  # --------------------------------------------------------------------------------------------------------------------\n  {{ range .optionalVariables }}\n  {{- if eq 1 (regexSplit \"\\n\" .Description -1 | len ) }}\n  # Description: {{ .Description }}\n  {{- else }}\n  # Description:\n    {{- range $line := regexSplit \"\\n\" .Description -1 }}\n    # {{ $line | indent 2 }}\n    {{- end }}\n  {{- end }}\n  # Type: {{ .Type }}\n  # {{ .Name }} = {{ .DefaultValue }}\n  {{ end }}\n}\n`\n)\n\nvar moduleURLRegex = regexp.MustCompile(moduleURLPattern)\n\nconst (\n\tenableRootInclude = \"EnableRootInclude\"\n\trootFileName      = \"RootFileName\"\n)\n\n// NewBoilerplateOptions creates a new BoilerplateOptions struct\nfunc NewBoilerplateOptions(\n\ttemplateFolder,\n\toutputFolder string,\n\tvars map[string]any,\n\tterragruntOpts *options.TerragruntOptions,\n) *boilerplate_options.BoilerplateOptions {\n\treturn &boilerplate_options.BoilerplateOptions{\n\t\tTemplateFolder:          templateFolder,\n\t\tOutputFolder:            outputFolder,\n\t\tOnMissingKey:            boilerplate_options.DefaultMissingKeyAction,\n\t\tOnMissingConfig:         boilerplate_options.DefaultMissingConfigAction,\n\t\tVars:                    vars,\n\t\tShellCommandAnswers:     map[string]bool{},\n\t\tNoShell:                 terragruntOpts.NoShell,\n\t\tNoHooks:                 terragruntOpts.NoHooks,\n\t\tNonInteractive:          terragruntOpts.NonInteractive,\n\t\tDisableDependencyPrompt: terragruntOpts.NoDependencyPrompt,\n\t}\n}\n\nfunc Run(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, moduleURL, templateURL string) error {\n\t// Apply catalog configuration settings, with CLI flags taking precedence\n\tapplyCatalogConfigToScaffold(ctx, l, opts)\n\n\t// download remote repo to local\n\tdirsToClean := make([]string, 0, 1)\n\t// clean all temp dirs\n\tdefer func() {\n\t\tfor _, dir := range dirsToClean {\n\t\t\tif err := os.RemoveAll(dir); err != nil {\n\t\t\t\tl.Warnf(\"Failed to clean up dir %s: %v\", dir, err)\n\t\t\t}\n\t\t}\n\t}()\n\n\toutputDir := opts.ScaffoldOutputFolder\n\tif outputDir == \"\" {\n\t\toutputDir = opts.WorkingDir\n\t}\n\n\t// scaffold only in empty directories\n\tif empty, err := util.IsDirectoryEmpty(opts.WorkingDir); !empty || err != nil {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tl.Warnf(\"The working directory %s is not empty.\", opts.WorkingDir)\n\t}\n\n\tif moduleURL == \"\" {\n\t\treturn errors.New(NoModuleURLPassed{})\n\t}\n\n\t// create temporary directory where to download module\n\ttempDir, err := os.MkdirTemp(\"\", \"scaffold\")\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tdirsToClean = append(dirsToClean, tempDir)\n\n\t// prepare variables\n\tvars, err := variables.ParseVars(opts.ScaffoldVars, opts.ScaffoldVarFiles)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t// parse module url\n\tmoduleURL, err = parseModuleURL(ctx, l, opts, vars, moduleURL)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tl.Infof(\"Scaffolding a new Terragrunt module %s to %s\", moduleURL, outputDir)\n\n\tif _, err := getter.GetAny(ctx, tempDir, moduleURL); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t// extract variables from downloaded module\n\trequiredVariables, optionalVariables, err := parseVariables(l, opts, tempDir)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tl.Debugf(\"Parsed %d required variables and %d optional variables\", len(requiredVariables), len(optionalVariables))\n\n\t// prepare boilerplate files to render Terragrunt files\n\tboilerplateDir, err := prepareBoilerplateFiles(ctx, l, opts, templateURL, tempDir)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t// add additional variables\n\tvars[\"requiredVariables\"] = requiredVariables\n\tvars[\"optionalVariables\"] = optionalVariables\n\n\tvars[\"sourceUrl\"] = moduleURL\n\n\t// Only set these if the `vars` map doesn't already have them set\n\tif _, found := vars[enableRootInclude]; !found {\n\t\tvars[enableRootInclude] = !opts.ScaffoldNoIncludeRoot\n\t} else {\n\t\tl.Warnf(\n\t\t\t\"The %s variable is already set in the var flag(s). The --%s flag will be ignored.\",\n\t\t\tenableRootInclude,\n\t\t\tshared.NoIncludeRootFlagName,\n\t\t)\n\t}\n\n\tif _, found := vars[rootFileName]; !found {\n\t\tvars[rootFileName] = opts.ScaffoldRootFileName\n\t} else {\n\t\tl.Warnf(\n\t\t\t\"The %s variable is already set in the var flag(s). The --%s flag will be ignored.\",\n\t\t\trootFileName,\n\t\t\tshared.NoIncludeRootFlagName,\n\t\t)\n\t}\n\n\tl.Infof(\"Running boilerplate generation to %s\", outputDir)\n\tboilerplateOpts := NewBoilerplateOptions(boilerplateDir, outputDir, vars, opts)\n\n\temptyDep := variables.Dependency{}\n\tif err := templates.ProcessTemplate(boilerplateOpts, boilerplateOpts, emptyDep); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tl.Infof(\"Running fmt on generated code %s\", outputDir)\n\n\tif err := format.Run(ctx, l, opts); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tl.Info(\"Scaffolding completed\")\n\n\treturn nil\n}\n\n// applyCatalogConfigToScaffold applies catalog configuration settings to scaffold options.\n// CLI flags take precedence over config file settings.\nfunc applyCatalogConfigToScaffold(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) {\n\t_, pctx := configbridge.NewParsingContext(ctx, l, opts)\n\n\tcatalogCfg, err := config.ReadCatalogConfig(ctx, l, pctx)\n\tif err != nil {\n\t\t// Don't fail if catalog config can't be read - it's optional\n\t\tl.Debugf(\"Could not read catalog config for scaffold: %v\", err)\n\t\treturn\n\t}\n\n\tif catalogCfg == nil {\n\t\treturn\n\t}\n\n\t// Apply config settings only if CLI flags weren't explicitly set\n\t// Since both NoShell and NoHooks default to false, we apply the config value\n\t// only if it's true (enabling the restriction)\n\tif catalogCfg.NoShell != nil && *catalogCfg.NoShell && !opts.NoShell {\n\t\tl.Debugf(\"Applying catalog config: no_shell = true\")\n\n\t\topts.NoShell = true\n\t}\n\n\tif catalogCfg.NoHooks != nil && *catalogCfg.NoHooks && !opts.NoHooks {\n\t\tl.Debugf(\"Applying catalog config: no_hooks = true\")\n\n\t\topts.NoHooks = true\n\t}\n}\n\n// generateDefaultTemplate - write default template to provided dir\nfunc generateDefaultTemplate(boilerplateDir string) (string, error) {\n\tconst ownerWriteGlobalReadPerms = 0644\n\tif err := os.WriteFile(\n\t\tfilepath.Join(\n\t\t\tboilerplateDir,\n\t\t\tconfig.DefaultTerragruntConfigPath,\n\t\t),\n\t\t[]byte(DefaultTerragruntTemplate),\n\t\townerWriteGlobalReadPerms,\n\t); err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\tif err := os.WriteFile(\n\t\tfilepath.Join(\n\t\t\tboilerplateDir,\n\t\t\t\"boilerplate.yml\",\n\t\t),\n\t\t[]byte(DefaultBoilerplateConfig),\n\t\townerWriteGlobalReadPerms,\n\t); err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\treturn boilerplateDir, nil\n}\n\n// downloadTemplate - parse URL, download files, and handle subfolders\nfunc downloadTemplate(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\ttemplateURL,\n\ttempDir string,\n) (string, error) {\n\tparsedTemplateURL, err := tf.ToSourceURL(templateURL, tempDir)\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\t// Split the processed URL to get the base URL and subfolder\n\tbaseURL, subFolder, err := tf.SplitSourceURL(l, parsedTemplateURL)\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\t// Go-getter expects a pathspec or . for file paths\n\tif baseURL.Scheme == \"\" || baseURL.Scheme == \"file\" {\n\t\tbaseURL.Path = filepath.ToSlash(strings.TrimSuffix(baseURL.Path, \"/\")) + \"//.\"\n\t}\n\n\tbaseURL, err = rewriteTemplateURL(ctx, l, opts, baseURL)\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\ttemplateDir, err := os.MkdirTemp(tempDir, \"template\")\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\tl.Infof(\"Downloading template from %s into %s\", baseURL.String(), templateDir)\n\t// Downloading baseURL to support boilerplate dependencies and partials. Go-getter discards all but specified folder if one is provided.\n\tif _, err := getter.GetAny(ctx, templateDir, baseURL.String()); err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\t// Add subfolder to templateDir if provided, as scaffold needs path to boilerplate.yml file\n\tif subFolder != \"\" {\n\t\tsubFolder = strings.TrimPrefix(subFolder, \"/\")\n\t\ttemplateDir = filepath.Join(templateDir, subFolder)\n\t\t// Verify that subfolder exists\n\t\tif _, err := os.Stat(templateDir); os.IsNotExist(err) {\n\t\t\treturn \"\", errors.Errorf(\n\t\t\t\t\"subfolder \\\"//%s\\\" not found in downloaded template from %s\",\n\t\t\t\tsubFolder,\n\t\t\t\ttemplateURL,\n\t\t\t)\n\t\t}\n\t}\n\n\treturn templateDir, nil\n}\n\n// prepareBoilerplateFiles - prepare boilerplate files from provided template, tf module, or (custom) default template\nfunc prepareBoilerplateFiles(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\ttemplateURL,\n\ttempDir string,\n) (string, error) {\n\tboilerplateDir := filepath.Join(tempDir, util.DefaultBoilerplateDir)\n\n\t// process template url if it was passed. This overrides the .boilerplate folder in the OpenTofu/Terraform module\n\tif templateURL != \"\" {\n\t\t// process template url if it was passed\n\t\ttempTemplateDir, err := downloadTemplate(ctx, l, opts, templateURL, tempDir)\n\t\tif err != nil {\n\t\t\treturn \"\", errors.New(err)\n\t\t}\n\n\t\tboilerplateDir = tempTemplateDir\n\t}\n\n\t// if boilerplate dir is not found, create one with default template\n\tif !util.IsDir(boilerplateDir) {\n\t\t_, pctx := configbridge.NewParsingContext(ctx, l, opts)\n\n\t\tconfig, err := config.ReadCatalogConfig(ctx, l, pctx)\n\t\tif err != nil {\n\t\t\treturn \"\", errors.New(err)\n\t\t}\n\n\t\t// use defaultTemplateURL if defined in config, otherwise use basic default template\n\t\tif config != nil && config.DefaultTemplate != \"\" {\n\t\t\t// process template url if available\n\t\t\ttempTemplateDir, err := downloadTemplate(ctx, l, opts, config.DefaultTemplate, tempDir)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", errors.New(err)\n\t\t\t}\n\n\t\t\tboilerplateDir = tempTemplateDir\n\t\t} else {\n\t\t\tdefaultTempDir, err := os.MkdirTemp(tempDir, \"boilerplate\")\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", errors.New(err)\n\t\t\t}\n\n\t\t\tboilerplateDir = defaultTempDir\n\n\t\t\tboilerplateDir, err = generateDefaultTemplate(boilerplateDir)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", errors.New(err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn boilerplateDir, nil\n}\n\n// parseVariables - parse variables from tf files.\nfunc parseVariables(\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tmoduleDir string,\n) ([]*config.ParsedVariable, []*config.ParsedVariable, error) {\n\tinputs, err := config.ParseVariables(l, opts.Experiments, opts.StrictControls, moduleDir)\n\tif err != nil {\n\t\treturn nil, nil, errors.New(err)\n\t}\n\n\t// separate variables that require value and with default value\n\tvar (\n\t\trequiredVariables []*config.ParsedVariable\n\t\toptionalVariables []*config.ParsedVariable\n\t)\n\n\tfor _, value := range inputs {\n\t\tif value.DefaultValue == \"\" {\n\t\t\trequiredVariables = append(requiredVariables, value)\n\t\t} else {\n\t\t\toptionalVariables = append(optionalVariables, value)\n\t\t}\n\t}\n\n\treturn requiredVariables, optionalVariables, nil\n}\n\n// parseModuleURL - parse module url and rewrite it if required\nfunc parseModuleURL(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tvars map[string]any,\n\tmoduleURL string,\n) (string, error) {\n\tparsedModuleURL, err := tf.ToSourceURL(moduleURL, opts.WorkingDir)\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\tmoduleURL = parsedModuleURL.String()\n\n\t// rewrite module url, if required\n\tparsedModuleURL, err = rewriteModuleURL(l, opts, vars, moduleURL)\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\t// add ref to module url, if required\n\tparsedModuleURL, err = addRefToModuleURL(ctx, l, opts, parsedModuleURL, vars)\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\t// regenerate module url with all changes\n\treturn parsedModuleURL.String(), nil\n}\n\n// rewriteModuleURL rewrites module url to git ssh if required\n// github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs => git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs\nfunc rewriteModuleURL(\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tvars map[string]any,\n\tmoduleURL string,\n) (*url.URL, error) {\n\tvar updatedModuleURL = moduleURL\n\n\tsourceURLType := sourceURLTypeHTTPS\n\tif value, found := vars[sourceURLTypeVar]; found {\n\t\tsourceURLType = fmt.Sprintf(\"%s\", value)\n\t}\n\n\t// expand module url\n\tparsedValue, err := parseURL(l, moduleURL)\n\tif err != nil {\n\t\tl.Warnf(\"Failed to parse module url %s\", moduleURL)\n\n\t\tparsedModuleURL, err := tf.ToSourceURL(updatedModuleURL, opts.WorkingDir)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(err)\n\t\t}\n\n\t\treturn parsedModuleURL, nil\n\t}\n\t// try to rewrite module url if is https and is requested to be git\n\t// git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs => git::ssh://git@github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs\n\tif parsedValue.scheme == \"https\" && sourceURLType == sourceURLTypeGit {\n\t\tgitUser := sourceGitSSHUser\n\t\tif value, found := vars[sourceGitSSHUserVar]; found {\n\t\t\tgitUser = fmt.Sprintf(\"%s\", value)\n\t\t}\n\n\t\tpath := strings.TrimPrefix(parsedValue.path, \"/\")\n\t\tupdatedModuleURL = fmt.Sprintf(\"%s@%s:%s\", gitUser, parsedValue.host, path)\n\t}\n\n\t// persist changes in url.URL\n\tparsedModuleURL, err := tf.ToSourceURL(updatedModuleURL, opts.WorkingDir)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\treturn parsedModuleURL, nil\n}\n\n// rewriteTemplateURL rewrites template url with reference to tag\n// github.com/denis256/terragrunt-tests.git//scaffold/base-template => github.com/denis256/terragrunt-tests.git//scaffold/base-template?ref=v0.53.8\nfunc rewriteTemplateURL(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tparsedTemplateURL *url.URL,\n) (*url.URL, error) {\n\tvar (\n\t\tupdatedTemplateURL = parsedTemplateURL\n\t\ttemplateParams     = updatedTemplateURL.Query()\n\t)\n\n\tref := templateParams.Get(refParam)\n\tif ref == \"\" {\n\t\trootSourceURL, _, err := tf.SplitSourceURL(l, updatedTemplateURL)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(err)\n\t\t}\n\n\t\tif rootSourceURL.Scheme == \"\" || rootSourceURL.Scheme == \"file\" {\n\t\t\tl.Debugf(\"Skipping git tag lookup for local template path: %s\", rootSourceURL)\n\t\t\treturn updatedTemplateURL, nil\n\t\t}\n\n\t\ttag, err := shell.GitLastReleaseTag(ctx, l, opts.Env, opts.WorkingDir, rootSourceURL)\n\t\tif err != nil || tag == \"\" {\n\t\t\tl.Warnf(\"Failed to find last release tag for URL %s, so will not add a ref param to the URL\", rootSourceURL)\n\t\t} else {\n\t\t\ttemplateParams.Add(refParam, tag)\n\t\t\tupdatedTemplateURL.RawQuery = templateParams.Encode()\n\t\t}\n\t}\n\n\treturn updatedTemplateURL, nil\n}\n\n// addRefToModuleURL adds ref to module url if is passed through variables or find it from git tags\nfunc addRefToModuleURL(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tparsedModuleURL *url.URL,\n\tvars map[string]any,\n) (*url.URL, error) {\n\tvar moduleURL = parsedModuleURL\n\t// append ref to source url, if is passed through variables or find it from git tags\n\tparams := moduleURL.Query()\n\n\trefReplacement, refVarPassed := vars[refVar]\n\tif refVarPassed {\n\t\tparams.Set(refParam, fmt.Sprintf(\"%s\", refReplacement))\n\t\tmoduleURL.RawQuery = params.Encode()\n\t}\n\n\tref := params.Get(refParam)\n\tif ref == \"\" {\n\t\t// if ref is not passed, find last release tag\n\t\t// git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs => git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=v0.53.8\n\t\trootSourceURL, _, err := tf.SplitSourceURL(l, moduleURL)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(err)\n\t\t}\n\n\t\ttag, err := shell.GitLastReleaseTag(ctx, l, opts.Env, opts.WorkingDir, rootSourceURL)\n\t\tif err != nil || tag == \"\" {\n\t\t\tl.Warnf(\"Failed to find last release tag for %s\", rootSourceURL)\n\t\t} else {\n\t\t\tparams.Add(refParam, tag)\n\t\t\tmoduleURL.RawQuery = params.Encode()\n\t\t}\n\t}\n\n\treturn moduleURL, nil\n}\n\n// parseURL parses module url to scheme, host and path\nfunc parseURL(l log.Logger, moduleURL string) (*parsedURL, error) {\n\tmatches := moduleURLRegex.FindStringSubmatch(moduleURL)\n\tif len(matches) != moduleURLParts {\n\t\tl.Warnf(\"Failed to parse url %s\", moduleURL)\n\t\treturn nil, failedToParseURLError{}\n\t}\n\n\treturn &parsedURL{\n\t\tscheme: matches[1],\n\t\thost:   matches[2],\n\t\tpath:   matches[3],\n\t}, nil\n}\n\ntype parsedURL struct {\n\tscheme string\n\thost   string\n\tpath   string\n}\n\ntype failedToParseURLError struct {\n}\n\nfunc (err failedToParseURLError) Error() string {\n\treturn \"Failed to parse Url.\"\n}\n\ntype NoModuleURLPassed struct {\n}\n\nfunc (err NoModuleURLPassed) Error() string {\n\treturn \"No module URL passed.\"\n}\n"
  },
  {
    "path": "internal/cli/commands/scaffold/scaffold_test.go",
    "content": "package scaffold_test\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\tboilerplateoptions \"github.com/gruntwork-io/boilerplate/options\"\n\t\"github.com/gruntwork-io/boilerplate/templates\"\n\t\"github.com/gruntwork-io/boilerplate/variables\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/scaffold\"\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// newTestBoilerplateOptions creates a BoilerplateOptions for testing\nfunc newTestBoilerplateOptions(templateFolder, outputFolder string, vars map[string]any, noShell, noHooks bool) *boilerplateoptions.BoilerplateOptions {\n\treturn &boilerplateoptions.BoilerplateOptions{\n\t\tTemplateFolder:          templateFolder,\n\t\tOutputFolder:            outputFolder,\n\t\tOnMissingKey:            boilerplateoptions.DefaultMissingKeyAction,\n\t\tOnMissingConfig:         boilerplateoptions.DefaultMissingConfigAction,\n\t\tVars:                    vars,\n\t\tShellCommandAnswers:     map[string]bool{},\n\t\tNoShell:                 noShell,\n\t\tNoHooks:                 noHooks,\n\t\tNonInteractive:          true,\n\t\tDisableDependencyPrompt: false,\n\t}\n}\n\nfunc TestDefaultTemplateVariables(t *testing.T) {\n\tt.Parallel()\n\n\t// set pre-defined variables\n\tvars := map[string]any{}\n\n\trequiredVariables := make([]*config.ParsedVariable, 0, 1)\n\toptionalVariables := make([]*config.ParsedVariable, 0, 1)\n\n\trequiredVariables = append(requiredVariables, &config.ParsedVariable{\n\t\tName:                    \"required_var_1\",\n\t\tDescription:             \"required_var_1 description\",\n\t\tType:                    \"string\",\n\t\tDefaultValuePlaceholder: \"\\\"\\\"\",\n\t})\n\n\toptionalVariables = append(optionalVariables, &config.ParsedVariable{\n\t\tName:         \"optional_var_2\",\n\t\tDescription:  \"optional_ver_2 description\",\n\t\tType:         \"number\",\n\t\tDefaultValue: \"42\",\n\t})\n\n\tvars[\"requiredVariables\"] = requiredVariables\n\tvars[\"optionalVariables\"] = optionalVariables\n\n\tvars[\"sourceUrl\"] = \"git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=v0.53.8\"\n\n\tvars[\"EnableRootInclude\"] = false\n\tvars[\"RootFileName\"] = \"root.hcl\"\n\n\tworkDir := helpers.TmpDirWOSymlinks(t)\n\ttemplateDir := filepath.Join(workDir, \"template\")\n\terr := os.Mkdir(templateDir, 0755)\n\trequire.NoError(t, err)\n\n\toutputDir := filepath.Join(workDir, \"output\")\n\terr = os.Mkdir(outputDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(templateDir, \"terragrunt.hcl\"), []byte(scaffold.DefaultTerragruntTemplate), 0644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(templateDir, \"boilerplate.yml\"), []byte(scaffold.DefaultBoilerplateConfig), 0644)\n\trequire.NoError(t, err)\n\n\tboilerplateOpts := newTestBoilerplateOptions(templateDir, outputDir, vars, true, true)\n\n\temptyDep := variables.Dependency{}\n\terr = templates.ProcessTemplate(boilerplateOpts, boilerplateOpts, emptyDep)\n\trequire.NoError(t, err)\n\n\tcontent, err := util.ReadFileAsString(filepath.Join(outputDir, \"terragrunt.hcl\"))\n\trequire.NoError(t, err)\n\trequire.Contains(t, content, \"required_var_1\")\n\trequire.Contains(t, content, \"optional_var_2\")\n\n\t// read generated HCL file and check if it is parsed correctly\n\topts, err := options.NewTerragruntOptionsForTest(filepath.Join(outputDir, \"terragrunt.hcl\"))\n\trequire.NoError(t, err)\n\n\tl := logger.CreateLogger()\n\n\t_, pctx := configbridge.NewParsingContext(t.Context(), l, opts)\n\tcfg, err := config.ReadTerragruntConfig(t.Context(), l, pctx, config.DefaultParserOptions(l, opts.StrictControls))\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, cfg.Inputs)\n\tassert.Len(t, cfg.Inputs, 1)\n\t_, found := cfg.Inputs[\"required_var_1\"]\n\trequire.True(t, found)\n\trequire.Equal(t, \"git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=v0.53.8\", *cfg.Terraform.Source)\n}\n\nfunc TestCatalogConfigApplication(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tcliNoShell       *bool\n\t\tcliNoHooks       *bool\n\t\tname             string\n\t\tterragruntConfig string\n\t\tdescription      string\n\t\texpectedNoShell  bool\n\t\texpectedNoHooks  bool\n\t}{\n\t\t{\n\t\t\tname: \"config_both_flags_true\",\n\t\t\tterragruntConfig: `\ncatalog {\n  urls = [\"test-url\"]\n  no_shell = true\n  no_hooks = true\n}`,\n\t\t\texpectedNoShell: true,\n\t\t\texpectedNoHooks: true,\n\t\t\tdescription:     \"Catalog config sets both flags to true\",\n\t\t},\n\t\t{\n\t\t\tname: \"config_both_flags_false\",\n\t\t\tterragruntConfig: `\ncatalog {\n  urls = [\"test-url\"]\n  no_shell = false\n  no_hooks = false\n}`,\n\t\t\tdescription: \"Catalog config sets both flags to false\",\n\t\t},\n\t\t{\n\t\t\tname: \"config_shell_true_hooks_false\",\n\t\t\tterragruntConfig: `\ncatalog {\n  urls = [\"test-url\"]\n  no_shell = true\n  no_hooks = false\n}`,\n\t\t\texpectedNoShell: true,\n\t\t\tdescription:     \"Catalog config sets no_shell=true, no_hooks=false\",\n\t\t},\n\t\t{\n\t\t\tname: \"config_shell_false_hooks_true\",\n\t\t\tterragruntConfig: `\ncatalog {\n  urls = [\"test-url\"]\n  no_shell = false\n  no_hooks = true\n}`,\n\t\t\texpectedNoHooks: true,\n\t\t\tdescription:     \"Catalog config sets no_shell=false, no_hooks=true\",\n\t\t},\n\t\t// Test CLI flags overriding catalog config\n\t\t{\n\t\t\tname: \"cli_override_config_true_with_false\",\n\t\t\tterragruntConfig: `\ncatalog {\n  urls = [\"test-url\"]\n  no_shell = true\n  no_hooks = true\n}`,\n\t\t\tcliNoShell:  boolPtr(false),\n\t\t\tcliNoHooks:  boolPtr(false),\n\t\t\tdescription: \"CLI flags override catalog config (CLI false > config true)\",\n\t\t},\n\t\t{\n\t\t\tname: \"cli_override_config_false_with_true\",\n\t\t\tterragruntConfig: `\ncatalog {\n  urls = [\"test-url\"]\n  no_shell = false\n  no_hooks = false\n}`,\n\t\t\tcliNoShell:      boolPtr(true),\n\t\t\tcliNoHooks:      boolPtr(true),\n\t\t\texpectedNoShell: true,\n\t\t\texpectedNoHooks: true,\n\t\t\tdescription:     \"CLI flags override catalog config (CLI true > config false)\",\n\t\t},\n\t\t{\n\t\t\tname: \"cli_partial_override_shell_only\",\n\t\t\tterragruntConfig: `\ncatalog {\n  urls = [\"test-url\"]\n  no_shell = false\n  no_hooks = true\n}`,\n\t\t\tcliNoShell:      boolPtr(true),\n\t\t\texpectedNoShell: true,\n\t\t\texpectedNoHooks: true,\n\t\t\tdescription:     \"CLI --no-shell overrides config, no_hooks from config\",\n\t\t},\n\t\t{\n\t\t\tname: \"cli_partial_override_hooks_only\",\n\t\t\tterragruntConfig: `\ncatalog {\n  urls = [\"test-url\"]\n  no_shell = true\n  no_hooks = false\n}`,\n\t\t\tcliNoHooks:      boolPtr(true),\n\t\t\texpectedNoShell: true,\n\t\t\texpectedNoHooks: true,\n\t\t\tdescription:     \"CLI --no-hooks overrides config, no_shell from config\",\n\t\t},\n\t\t// Test behavior when attributes are omitted from config\n\t\t{\n\t\t\tname: \"config_omitted_attributes_no_cli\",\n\t\t\tterragruntConfig: `\ncatalog {\n  urls = [\"test-url\"]\n}`,\n\t\t\tdescription: \"Config omits no_shell/no_hooks, no CLI flags - should default to false\",\n\t\t},\n\t\t{\n\t\t\tname: \"config_omitted_attributes_cli_true\",\n\t\t\tterragruntConfig: `\ncatalog {\n  urls = [\"test-url\"]\n}`,\n\t\t\tcliNoShell:      boolPtr(true),\n\t\t\tcliNoHooks:      boolPtr(true),\n\t\t\texpectedNoShell: true,\n\t\t\texpectedNoHooks: true,\n\t\t\tdescription:     \"Config omits no_shell/no_hooks, CLI sets both true - CLI should take effect\",\n\t\t},\n\t\t{\n\t\t\tname: \"config_omitted_attributes_cli_false\",\n\t\t\tterragruntConfig: `\ncatalog {\n  urls = [\"test-url\"]\n}`,\n\t\t\tcliNoShell:  boolPtr(false),\n\t\t\tcliNoHooks:  boolPtr(false),\n\t\t\tdescription: \"Config omits no_shell/no_hooks, CLI sets both false - should remain false\",\n\t\t},\n\t\t{\n\t\t\tname: \"config_omitted_attributes_cli_partial\",\n\t\t\tterragruntConfig: `\ncatalog {\n  urls = [\"test-url\"]\n}`,\n\t\t\tcliNoShell:      boolPtr(true),\n\t\t\texpectedNoShell: true,\n\t\t\tdescription:     \"Config omits attributes, only CLI --no-shell set - only no_shell should be true\",\n\t\t},\n\t\t// Test mixed scenarios with some attributes omitted\n\t\t{\n\t\t\tname: \"config_partial_shell_only_no_cli\",\n\t\t\tterragruntConfig: `\ncatalog {\n  urls = [\"test-url\"]\n  no_shell = true\n}`,\n\t\t\texpectedNoShell: true,\n\t\t\tdescription:     \"Config sets only no_shell=true, no_hooks omitted - should be true/false\",\n\t\t},\n\t\t{\n\t\t\tname: \"config_partial_hooks_only_no_cli\",\n\t\t\tterragruntConfig: `\ncatalog {\n  urls = [\"test-url\"]\n  no_hooks = true\n}`,\n\t\t\texpectedNoHooks: true,\n\t\t\tdescription:     \"Config sets only no_hooks=true, no_shell omitted - should be false/true\",\n\t\t},\n\t\t{\n\t\t\tname: \"config_partial_shell_only_cli_override_hooks\",\n\t\t\tterragruntConfig: `\ncatalog {\n  urls = [\"test-url\"]\n  no_shell = false\n}`,\n\t\t\tcliNoHooks:      boolPtr(true),\n\t\t\texpectedNoHooks: true,\n\t\t\tdescription:     \"Config sets no_shell=false, no_hooks omitted, CLI --no-hooks - should be false/true\",\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.Parallel()\n\n\t\t\tworkDir := helpers.TmpDirWOSymlinks(t)\n\t\t\tconfigDir := filepath.Join(workDir, \"config\")\n\n\t\t\terr := os.MkdirAll(configDir, 0755)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tterragruntConfigPath := filepath.Join(configDir, \"terragrunt.hcl\")\n\t\t\terr = os.WriteFile(terragruntConfigPath, []byte(tc.terragruntConfig), 0644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\topts := options.NewTerragruntOptions()\n\t\t\t// Set CLI flags if specified in test case\n\t\t\tif tc.cliNoShell != nil {\n\t\t\t\topts.NoShell = *tc.cliNoShell\n\t\t\t} else {\n\t\t\t\topts.NoShell = false\n\t\t\t}\n\n\t\t\tif tc.cliNoHooks != nil {\n\t\t\t\topts.NoHooks = *tc.cliNoHooks\n\t\t\t} else {\n\t\t\t\topts.NoHooks = false\n\t\t\t}\n\n\t\t\topts.TerragruntConfigPath = terragruntConfigPath\n\t\t\topts.WorkingDir = configDir\n\t\t\topts.ScaffoldRootFileName = \"terragrunt.hcl\"\n\n\t\t\tl := logger.CreateLogger()\n\n\t\t\t// First, verify catalog config parsing\n\t\t\t_, catalogPctx := configbridge.NewParsingContext(context.Background(), l, opts)\n\t\t\tcatalogCfg, err := config.ReadCatalogConfig(context.Background(), l, catalogPctx)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, catalogCfg, tc.description)\n\n\t\t\t// Verify config parsing based on whether attributes are present in the config\n\t\t\tif strings.Contains(tc.terragruntConfig, \"no_shell\") {\n\t\t\t\tassert.NotNil(t, catalogCfg.NoShell, \"NoShell should not be nil when specified in config: %s\", tc.description)\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, catalogCfg.NoShell, \"NoShell should be nil when omitted from config: %s\", tc.description)\n\t\t\t}\n\n\t\t\tif strings.Contains(tc.terragruntConfig, \"no_hooks\") {\n\t\t\t\tassert.NotNil(t, catalogCfg.NoHooks, \"NoHooks should not be nil when specified in config: %s\", tc.description)\n\t\t\t} else {\n\t\t\t\tassert.Nil(t, catalogCfg.NoHooks, \"NoHooks should be nil when omitted from config: %s\", tc.description)\n\t\t\t}\n\n\t\t\t// Apply catalog config settings to options (simulating scaffold.Run behavior)\n\t\t\t// Only apply config values if CLI flags weren't explicitly set\n\t\t\tif tc.cliNoShell == nil && catalogCfg.NoShell != nil && *catalogCfg.NoShell {\n\t\t\t\topts.NoShell = true\n\t\t\t}\n\n\t\t\tif tc.cliNoHooks == nil && catalogCfg.NoHooks != nil && *catalogCfg.NoHooks {\n\t\t\t\topts.NoHooks = true\n\t\t\t}\n\n\t\t\t// Verify final option values match expected (after config application + CLI override)\n\t\t\tassert.Equal(t, tc.expectedNoShell, opts.NoShell, \"Final NoShell value should match expected: %s\", tc.description)\n\t\t\tassert.Equal(t, tc.expectedNoHooks, opts.NoHooks, \"Final NoHooks value should match expected: %s\", tc.description)\n\t\t})\n\t}\n}\n\n// Helper function to create bool pointers\nfunc boolPtr(b bool) *bool {\n\treturn &b\n}\n\n// TestCatalogConfigParsing tests that catalog config is properly parsed with new attributes\nfunc TestCatalogConfigParsing(t *testing.T) {\n\tt.Parallel()\n\n\tworkDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Test with no_shell and no_hooks attributes\n\tterragruntConfig := `\ncatalog {\n  default_template = \"test-template\"\n  urls = [\"url1\", \"url2\"]\n  no_shell = true\n  no_hooks = false\n}\n`\n\tterragruntConfigPath := filepath.Join(workDir, \"terragrunt.hcl\")\n\terr := os.WriteFile(terragruntConfigPath, []byte(terragruntConfig), 0644)\n\trequire.NoError(t, err)\n\n\topts := options.NewTerragruntOptions()\n\topts.TerragruntConfigPath = terragruntConfigPath\n\topts.WorkingDir = workDir\n\topts.ScaffoldRootFileName = \"terragrunt.hcl\"\n\n\tl := logger.CreateLogger()\n\n\t// Parse the configuration\n\t_, catalogPctx := configbridge.NewParsingContext(context.Background(), l, opts)\n\tcatalogCfg, err := config.ReadCatalogConfig(context.Background(), l, catalogPctx)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, catalogCfg)\n\n\t// Verify all fields are correctly parsed\n\tassert.Equal(t, \"test-template\", catalogCfg.DefaultTemplate)\n\tassert.Equal(t, []string{\"url1\", \"url2\"}, catalogCfg.URLs)\n\tassert.NotNil(t, catalogCfg.NoShell)\n\tassert.True(t, *catalogCfg.NoShell)\n\tassert.NotNil(t, catalogCfg.NoHooks)\n\tassert.False(t, *catalogCfg.NoHooks)\n}\n\n// TestCatalogConfigOptional tests that no_shell and no_hooks are optional attributes\nfunc TestCatalogConfigOptional(t *testing.T) {\n\tt.Parallel()\n\n\tworkDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Test without no_shell and no_hooks attributes\n\tterragruntConfig := `\ncatalog {\n  default_template = \"test-template\"\n  urls = [\"url1\"]\n}\n`\n\tterragruntConfigPath := filepath.Join(workDir, \"terragrunt.hcl\")\n\terr := os.WriteFile(terragruntConfigPath, []byte(terragruntConfig), 0644)\n\trequire.NoError(t, err)\n\n\topts := options.NewTerragruntOptions()\n\topts.TerragruntConfigPath = terragruntConfigPath\n\topts.WorkingDir = workDir\n\topts.ScaffoldRootFileName = \"terragrunt.hcl\"\n\n\tl := logger.CreateLogger()\n\n\t// Parse the configuration\n\t_, catalogPctx := configbridge.NewParsingContext(context.Background(), l, opts)\n\tcatalogCfg, err := config.ReadCatalogConfig(context.Background(), l, catalogPctx)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, catalogCfg)\n\n\t// Verify optional fields are nil when not specified\n\tassert.Equal(t, \"test-template\", catalogCfg.DefaultTemplate)\n\tassert.Equal(t, []string{\"url1\"}, catalogCfg.URLs)\n\tassert.Nil(t, catalogCfg.NoShell, \"NoShell should be nil when not specified\")\n\tassert.Nil(t, catalogCfg.NoHooks, \"NoHooks should be nil when not specified\")\n}\n\n// TestBoilerplateShellTemplateFunctionDisabled tests that NoShell=true disables shell template functions\nfunc TestBoilerplateShellTemplateFunctionDisabled(t *testing.T) {\n\tt.Parallel()\n\n\tworkDir := helpers.TmpDirWOSymlinks(t)\n\ttemplateDir := filepath.Join(workDir, \"template\")\n\toutputDir := filepath.Join(workDir, \"output\")\n\n\t// Create template and output directories\n\terr := os.MkdirAll(templateDir, 0755)\n\trequire.NoError(t, err)\n\terr = os.MkdirAll(outputDir, 0755)\n\trequire.NoError(t, err)\n\n\t// Create boilerplate.yml\n\tboilerplateConfig := `\nvariables:\n  - name: TestVar\n    description: A test variable\n    type: string\n    default: \"test-value\"\n`\n\terr = os.WriteFile(filepath.Join(templateDir, \"boilerplate.yml\"), []byte(boilerplateConfig), 0644)\n\trequire.NoError(t, err)\n\n\t// Create template file with shell template function\n\ttemplateContent := `# Test template with shell function\ntest_var = \"{{ .TestVar }}\"\n# This shell function should NOT execute when NoShell=true\nshell_output = \"{{ shell \"echo SHELL_EXECUTED\" }}\"\n`\n\terr = os.WriteFile(filepath.Join(templateDir, \"test.txt\"), []byte(templateContent), 0644)\n\trequire.NoError(t, err)\n\n\t// Create BoilerplateOptions with NoShell=true\n\tboilerplateOpts := newTestBoilerplateOptions(templateDir, outputDir, map[string]any{}, true, false)\n\n\t// Process the template\n\temptyDep := variables.Dependency{}\n\terr = templates.ProcessTemplate(boilerplateOpts, boilerplateOpts, emptyDep)\n\trequire.NoError(t, err)\n\n\t// Verify the file was generated\n\tgeneratedFile := filepath.Join(outputDir, \"test.txt\")\n\trequire.FileExists(t, generatedFile)\n\n\tcontent, err := util.ReadFileAsString(generatedFile)\n\trequire.NoError(t, err)\n\n\t// Verify that template variables were processed\n\tassert.Contains(t, content, \"test-value\", \"Template variable should be processed\")\n\n\t// When shell is disabled, the shell function should remain unprocessed\n\t// Note: The exact behavior depends on how boilerplate handles disabled shell functions\n\t// It might either leave the template as-is or throw an error\n\tassert.NotContains(t, content, \"SHELL_EXECUTED\", \"Shell function should not execute when NoShell=true\")\n}\n\n// TestBoilerplateShellTemplateFunctionEnabled tests that NoShell=false allows shell template functions\nfunc TestBoilerplateShellTemplateFunctionEnabled(t *testing.T) {\n\tt.Parallel()\n\n\tworkDir := helpers.TmpDirWOSymlinks(t)\n\ttemplateDir := filepath.Join(workDir, \"template\")\n\toutputDir := filepath.Join(workDir, \"output\")\n\n\t// Create template and output directories\n\terr := os.MkdirAll(templateDir, 0755)\n\trequire.NoError(t, err)\n\terr = os.MkdirAll(outputDir, 0755)\n\trequire.NoError(t, err)\n\n\t// Create boilerplate.yml\n\tboilerplateConfig := `\nvariables:\n  - name: TestVar\n    description: A test variable\n    type: string\n    default: \"test-value\"\n`\n\terr = os.WriteFile(filepath.Join(templateDir, \"boilerplate.yml\"), []byte(boilerplateConfig), 0644)\n\trequire.NoError(t, err)\n\n\t// Create template file with shell template function\n\ttemplateContent := `# Test template with shell function\ntest_var = \"{{ .TestVar }}\"\n# This shell function SHOULD execute when NoShell=false\nshell_output = \"{{ shell \"echo\" \"SHELL_EXECUTED\" }}\"\n`\n\terr = os.WriteFile(filepath.Join(templateDir, \"test.txt\"), []byte(templateContent), 0644)\n\trequire.NoError(t, err)\n\n\t// Create BoilerplateOptions with NoShell=false\n\tboilerplateOpts := newTestBoilerplateOptions(templateDir, outputDir, map[string]any{}, false, false)\n\n\t// Process the template\n\temptyDep := variables.Dependency{}\n\terr = templates.ProcessTemplate(boilerplateOpts, boilerplateOpts, emptyDep)\n\trequire.NoError(t, err)\n\n\t// Verify the file was generated\n\tgeneratedFile := filepath.Join(outputDir, \"test.txt\")\n\trequire.FileExists(t, generatedFile)\n\n\tcontent, err := util.ReadFileAsString(generatedFile)\n\trequire.NoError(t, err)\n\n\t// Verify that template variables were processed\n\tassert.Contains(t, content, \"test-value\", \"Template variable should be processed\")\n\n\t// When shell is enabled, the shell function should execute and output should be present\n\tassert.Contains(t, content, \"SHELL_EXECUTED\", \"Shell function should execute when NoShell=false\")\n}\n\n// TestBoilerplateHooksDisabled tests that NoHooks=true disables hooks\nfunc TestBoilerplateHooksDisabled(t *testing.T) {\n\tt.Parallel()\n\n\tworkDir := helpers.TmpDirWOSymlinks(t)\n\ttemplateDir := filepath.Join(workDir, \"template\")\n\toutputDir := filepath.Join(workDir, \"output\")\n\n\t// Create template and output directories\n\terr := os.MkdirAll(templateDir, 0755)\n\trequire.NoError(t, err)\n\terr = os.MkdirAll(outputDir, 0755)\n\trequire.NoError(t, err)\n\n\t// Create boilerplate.yml with hooks\n\tboilerplateConfig := `\nvariables:\n  - name: TestVar\n    description: A test variable\n    type: string\n    default: \"test-value\"\n\nhooks:\n  before:\n    - command: touch\n      args:\n        - ` + outputDir + `/before_hook_not_executed.txt\n      description: \"Test hook that should NOT execute\"\n  after:\n    - command: touch\n      args:\n        - ` + outputDir + `/after_hook_not_executed.txt\n      description: \"Test hook that should NOT execute\"\n`\n\terr = os.WriteFile(filepath.Join(templateDir, \"boilerplate.yml\"), []byte(boilerplateConfig), 0644)\n\trequire.NoError(t, err)\n\n\t// Create simple template file\n\ttemplateContent := `# Test template\ntest_var = \"{{ .TestVar }}\"\n`\n\terr = os.WriteFile(filepath.Join(templateDir, \"test.txt\"), []byte(templateContent), 0644)\n\trequire.NoError(t, err)\n\n\t// Create BoilerplateOptions with NoHooks=true\n\tboilerplateOpts := newTestBoilerplateOptions(templateDir, outputDir, map[string]any{}, false, true)\n\n\t// Process the template\n\temptyDep := variables.Dependency{}\n\terr = templates.ProcessTemplate(boilerplateOpts, boilerplateOpts, emptyDep)\n\trequire.NoError(t, err)\n\n\t// Verify the template file was generated\n\tgeneratedFile := filepath.Join(outputDir, \"test.txt\")\n\trequire.FileExists(t, generatedFile)\n\n\tcontent, err := util.ReadFileAsString(generatedFile)\n\trequire.NoError(t, err)\n\tassert.Contains(t, content, \"test-value\", \"Template variable should be processed\")\n\n\t// Verify that hooks did NOT execute (hook files should not exist)\n\tbeforeHookFile := filepath.Join(outputDir, \"before_hook_not_executed.txt\")\n\tafterHookFile := filepath.Join(outputDir, \"after_hook_not_executed.txt\")\n\n\tassert.NoFileExists(t, beforeHookFile, \"Before hook file should not exist when NoHooks=true\")\n\tassert.NoFileExists(t, afterHookFile, \"After hook file should not exist when NoHooks=true\")\n}\n\n// TestBoilerplateHooksEnabled tests that NoHooks=false allows hooks to execute\nfunc TestBoilerplateHooksEnabled(t *testing.T) {\n\tt.Parallel()\n\n\tworkDir := helpers.TmpDirWOSymlinks(t)\n\ttemplateDir := filepath.Join(workDir, \"template\")\n\toutputDir := filepath.Join(workDir, \"output\")\n\n\t// Create template and output directories\n\terr := os.MkdirAll(templateDir, 0755)\n\trequire.NoError(t, err)\n\terr = os.MkdirAll(outputDir, 0755)\n\trequire.NoError(t, err)\n\n\t// Create boilerplate.yml with hooks\n\tboilerplateConfig := `\nvariables:\n  - name: TestVar\n    description: A test variable\n    type: string\n    default: \"test-value\"\n\nhooks:\n  before:\n    - command: touch\n      args:\n        - ` + outputDir + `/before_hook_executed.txt\n      description: \"Test hook that SHOULD execute\"\n  after:\n    - command: touch\n      args:\n        - ` + outputDir + `/after_hook_executed.txt\n      description: \"Test hook that SHOULD execute\"\n`\n\terr = os.WriteFile(filepath.Join(templateDir, \"boilerplate.yml\"), []byte(boilerplateConfig), 0644)\n\trequire.NoError(t, err)\n\n\t// Create simple template file\n\ttemplateContent := `# Test template\ntest_var = \"{{ .TestVar }}\"\n`\n\terr = os.WriteFile(filepath.Join(templateDir, \"test.txt\"), []byte(templateContent), 0644)\n\trequire.NoError(t, err)\n\n\t// Create BoilerplateOptions with NoHooks=false\n\tboilerplateOpts := newTestBoilerplateOptions(templateDir, outputDir, map[string]any{}, false, false)\n\n\t// Process the template\n\temptyDep := variables.Dependency{}\n\terr = templates.ProcessTemplate(boilerplateOpts, boilerplateOpts, emptyDep)\n\trequire.NoError(t, err)\n\n\t// Verify the template file was generated\n\tgeneratedFile := filepath.Join(outputDir, \"test.txt\")\n\trequire.FileExists(t, generatedFile)\n\n\tcontent, err := util.ReadFileAsString(generatedFile)\n\trequire.NoError(t, err)\n\tassert.Contains(t, content, \"test-value\", \"Template variable should be processed\")\n\n\t// Verify that hooks DID execute (before and after hook files should exist)\n\tbeforeHookFile := filepath.Join(outputDir, \"before_hook_executed.txt\")\n\tafterHookFile := filepath.Join(outputDir, \"after_hook_executed.txt\")\n\n\trequire.FileExists(t, beforeHookFile, \"Before hook file should exist when NoHooks=false\")\n\trequire.FileExists(t, afterHookFile, \"After hook file should exist when NoHooks=false\")\n}\n\n// TestBoilerplateBothFlagsDisabled tests that both NoShell=true and NoHooks=true work together\nfunc TestBoilerplateBothFlagsDisabled(t *testing.T) {\n\tt.Parallel()\n\n\tworkDir := helpers.TmpDirWOSymlinks(t)\n\ttemplateDir := filepath.Join(workDir, \"template\")\n\toutputDir := filepath.Join(workDir, \"output\")\n\n\t// Create template and output directories\n\terr := os.MkdirAll(templateDir, 0755)\n\trequire.NoError(t, err)\n\terr = os.MkdirAll(outputDir, 0755)\n\trequire.NoError(t, err)\n\n\t// Create boilerplate.yml with both hooks and variables\n\tboilerplateConfig := `\nvariables:\n  - name: TestVar\n    description: A test variable\n    type: string\n    default: \"test-value\"\n\nhooks:\n  before:\n    - command: echo \"HOOK_EXECUTED\" > ` + outputDir + `/hook_output.txt\n      description: \"Test hook that should NOT execute\"\n`\n\terr = os.WriteFile(filepath.Join(templateDir, \"boilerplate.yml\"), []byte(boilerplateConfig), 0644)\n\trequire.NoError(t, err)\n\n\t// Create template file with shell template function\n\ttemplateContent := `# Test template\ntest_var = \"{{ .TestVar }}\"\nshell_result = \"{{ shell \"echo SHELL_EXECUTED\" }}\"\n`\n\terr = os.WriteFile(filepath.Join(templateDir, \"test.txt\"), []byte(templateContent), 0644)\n\trequire.NoError(t, err)\n\n\t// Create BoilerplateOptions with both NoShell=true and NoHooks=true\n\tboilerplateOpts := newTestBoilerplateOptions(templateDir, outputDir, map[string]any{}, true, true)\n\n\t// Process the template\n\temptyDep := variables.Dependency{}\n\terr = templates.ProcessTemplate(boilerplateOpts, boilerplateOpts, emptyDep)\n\trequire.NoError(t, err)\n\n\t// Verify the template file was generated\n\tgeneratedFile := filepath.Join(outputDir, \"test.txt\")\n\trequire.FileExists(t, generatedFile)\n\n\tcontent, err := util.ReadFileAsString(generatedFile)\n\trequire.NoError(t, err)\n\n\t// Verify that template variables were processed\n\tassert.Contains(t, content, \"test-value\", \"Template variable should be processed\")\n\n\t// Verify that shell function did NOT execute\n\tassert.NotContains(t, content, \"SHELL_EXECUTED\", \"Shell function should not execute when NoShell=true\")\n\n\t// Verify that hooks did NOT execute\n\thookOutputFile := filepath.Join(outputDir, \"hook_output.txt\")\n\tassert.NoFileExists(t, hookOutputFile, \"Hook should not execute when NoHooks=true\")\n}\n"
  },
  {
    "path": "internal/cli/commands/shortcuts.go",
    "content": "package commands\n\nimport (\n\t\"slices\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/run\"\n)\n\nvar (\n\tshortcutCommandNames = []string{\n\t\ttf.CommandNameInit,\n\t\ttf.CommandNameValidate,\n\t\ttf.CommandNamePlan,\n\t\ttf.CommandNameApply,\n\t\ttf.CommandNameDestroy,\n\t\ttf.CommandNameForceUnlock,\n\t\ttf.CommandNameImport,\n\t\ttf.CommandNameOutput,\n\t\ttf.CommandNameRefresh,\n\t\ttf.CommandNameShow,\n\t\ttf.CommandNameState,\n\t\ttf.CommandNameTest,\n\t}\n)\n\nfunc NewShortcutsCommands(l log.Logger, opts *options.TerragruntOptions) clihelper.Commands {\n\tvar (\n\t\trunCmd = run.NewCommand(l, opts)\n\t\tcmds   = make(clihelper.Commands, 0, len(runCmd.Subcommands))\n\t)\n\n\tfor _, runSubCmd := range runCmd.Subcommands {\n\t\tif isNotShortcutCmd := !slices.Contains(shortcutCommandNames, runSubCmd.Name); isNotShortcutCmd {\n\t\t\tcontinue\n\t\t}\n\n\t\tcmd := &clihelper.Command{\n\t\t\tName:                         runSubCmd.Name,\n\t\t\tUsage:                        runSubCmd.Usage,\n\t\t\tFlags:                        runCmd.Flags,\n\t\t\tCustomHelp:                   runSubCmd.CustomHelp,\n\t\t\tAction:                       runSubCmd.Action,\n\t\t\tDisabledErrorOnUndefinedFlag: true,\n\t\t}\n\n\t\tcmds = append(cmds, cmd)\n\t}\n\n\treturn cmds\n}\n"
  },
  {
    "path": "internal/cli/commands/stack/cli.go",
    "content": "// Package stack provides the command to stack.\npackage stack\n\nimport (\n\t\"context\"\n\n\truncmd \"github.com/gruntwork-io/terragrunt/internal/cli/commands/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\t// CommandName stack command name.\n\tCommandName          = \"stack\"\n\tOutputFormatFlagName = \"format\"\n\tJSONFormatFlagName   = \"json\"\n\tRawFormatFlagName    = \"raw\"\n\tNoStackValidate      = \"no-stack-validate\"\n\n\tgenerateCommandName = \"generate\"\n\trunCommandName      = \"run\"\n\toutputCommandName   = \"output\"\n\tcleanCommandName    = \"clean\"\n\n\trawOutputFormat  = \"raw\"\n\tjsonOutputFormat = \"json\"\n)\n\n// NewCommand builds the command for stack.\nfunc NewCommand(l log.Logger, opts *options.TerragruntOptions) *clihelper.Command {\n\treturn &clihelper.Command{\n\t\tName:  CommandName,\n\t\tUsage: \"Terragrunt stack commands.\",\n\t\tSubcommands: clihelper.Commands{\n\t\t\t&clihelper.Command{\n\t\t\t\tName:  generateCommandName,\n\t\t\t\tUsage: \"Generate a stack from a terragrunt.stack.hcl file\",\n\t\t\t\tAction: func(ctx context.Context, _ *clihelper.Context) error {\n\t\t\t\t\treturn RunGenerate(ctx, l, opts.OptionsFromContext(ctx))\n\t\t\t\t},\n\t\t\t\tFlags: defaultFlags(l, opts, nil),\n\t\t\t},\n\t\t\t&clihelper.Command{\n\t\t\t\tName:  runCommandName,\n\t\t\t\tUsage: \"Run a command on the stack generated from the current directory\",\n\t\t\t\tAction: func(ctx context.Context, _ *clihelper.Context) error {\n\t\t\t\t\treturn Run(ctx, l, opts.OptionsFromContext(ctx))\n\t\t\t\t},\n\t\t\t\tFlags: defaultFlags(l, opts, nil),\n\t\t\t},\n\t\t\t&clihelper.Command{\n\t\t\t\tName:  outputCommandName,\n\t\t\t\tUsage: \"Run fetch stack output\",\n\t\t\t\tAction: func(ctx context.Context, cliCtx *clihelper.Context) error {\n\t\t\t\t\tindex := \"\"\n\t\t\t\t\tif val := cliCtx.Args().Get(0); val != \"\" {\n\t\t\t\t\t\tindex = val\n\t\t\t\t\t}\n\n\t\t\t\t\treturn RunOutput(ctx, l, opts.OptionsFromContext(ctx), index)\n\t\t\t\t},\n\t\t\t\tFlags: outputFlags(l, opts, nil),\n\t\t\t},\n\t\t\t&clihelper.Command{\n\t\t\t\tName:  cleanCommandName,\n\t\t\t\tUsage: \"Clean the stack generated from the current directory\",\n\t\t\t\tAction: func(ctx context.Context, _ *clihelper.Context) error {\n\t\t\t\t\treturn RunClean(ctx, l, opts.OptionsFromContext(ctx))\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tAction: clihelper.ShowCommandHelp,\n\t}\n}\n\nfunc defaultFlags(l log.Logger, opts *options.TerragruntOptions, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\n\tflags := clihelper.Flags{\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        NoStackValidate,\n\t\t\tEnvVars:     tgPrefix.EnvVars(NoStackValidate),\n\t\t\tDestination: &opts.NoStackValidate,\n\t\t\tHidden:      true,\n\t\t\tUsage:       \"Disable automatic stack validation after generation.\",\n\t\t}),\n\t}\n\n\treturn append(runcmd.NewFlags(l, opts, nil), flags...)\n}\n\nfunc outputFlags(l log.Logger, opts *options.TerragruntOptions, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\n\tflags := clihelper.Flags{\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        OutputFormatFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(OutputFormatFlagName),\n\t\t\tDestination: &opts.StackOutputFormat,\n\t\t\tUsage:       \"Stack output format. Valid values are: json, raw\",\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:  RawFormatFlagName,\n\t\t\tUsage: \"Stack output in raw format\",\n\t\t\tAction: func(_ context.Context, _ *clihelper.Context, value bool) error {\n\t\t\t\topts.StackOutputFormat = rawOutputFormat\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:  JSONFormatFlagName,\n\t\t\tUsage: \"Stack output in json format\",\n\t\t\tAction: func(_ context.Context, _ *clihelper.Context, value bool) error {\n\t\t\t\topts.StackOutputFormat = jsonOutputFormat\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}),\n\t}\n\n\treturn append(defaultFlags(l, opts, prefix), flags...)\n}\n"
  },
  {
    "path": "internal/cli/commands/stack/output.go",
    "content": "package stack\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io\"\n\n\t\"github.com/hashicorp/hcl/v2/hclwrite\"\n\n\tctyjson \"github.com/zclconf/go-cty/cty/json\"\n\n\t\"github.com/zclconf/go-cty/cty\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// PrintRawOutputs formats  outputs for raw output format, similar to Tofu's output -raw.\n// When the output is a raw output for a specific path, it will extract the raw value without quotes\n// or formatting and write it directly to the provided writer.\n// It only supports primitive values (strings, numbers, and booleans) and will return an error for complex types.\nfunc PrintRawOutputs(_ *options.TerragruntOptions, writer io.Writer, outputs cty.Value) error {\n\tif outputs == cty.NilVal {\n\t\treturn nil\n\t}\n\n\t// Extract the value from the nested structure, if any\n\tvalueMap := outputs.AsValueMap()\n\n\tlength := len(valueMap)\n\n\tif length == 0 {\n\t\treturn nil\n\t}\n\n\tif length == 1 {\n\t\t// Single output, try to extract the final value\n\t\tfinalValue, err := extractSingleValue(valueMap)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn writePrimitiveValue(writer, finalValue, config.GetFirstKey(valueMap))\n\t}\n\n\t// Multiple top-level keys, can't provide a single raw output\n\treturn errors.New(\"The -raw option requires a single output value. There are multiple outputs \" +\n\t\t\"available in the current stack. Please specify which output you want to display by using \" +\n\t\t\"the full output key as an argument to the command.\")\n}\n\n// extractSingleValue extracts a single primitive value from a map with only one element,\n// potentially traversing through a nested object structure.\nfunc extractSingleValue(valueMap map[string]cty.Value) (cty.Value, error) {\n\ttopKey := config.GetFirstKey(valueMap)\n\ttopValue := valueMap[topKey]\n\n\t// If the value is not an object type, return it directly\n\tif !topValue.Type().IsObjectType() {\n\t\treturn topValue, nil\n\t}\n\n\t// Try to navigate to the leaf value through nested objects\n\treturn traverseNestedObject(topKey, topValue)\n}\n\n// traverseNestedObject follows a chain of nested objects to find a primitive value at the leaf.\n// Returns an error if a complex value is found at the leaf or if multiple paths are present.\nfunc traverseNestedObject(topKey string, topValue cty.Value) (cty.Value, error) {\n\tcurrentValue := topValue\n\tcurrentKey := topKey\n\n\tvar finalValue cty.Value\n\n\t// Traverse down the nested objects\n\tfor currentValue.Type().IsObjectType() {\n\t\tnestedMap := currentValue.AsValueMap()\n\t\tif len(nestedMap) != 1 {\n\t\t\t// If we have more than one key at any level, we can't get a single raw value\n\t\t\treturn cty.NilVal, createUnsupportedValueError(currentKey, currentValue)\n\t\t}\n\n\t\t// Get the only key-value pair in the nested object\n\t\tnextKey := config.GetFirstKey(nestedMap)\n\t\tnextValue := nestedMap[nextKey]\n\n\t\tcurrentKey = nextKey\n\t\tcurrentValue = nextValue\n\n\t\t// If we've reached a primitive value, we're done\n\t\tif !currentValue.Type().IsObjectType() && !currentValue.Type().IsMapType() {\n\t\t\tfinalValue = currentValue\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// If we didn't set finalValue, the nested structure didn't lead to a primitive\n\tif finalValue == cty.NilVal {\n\t\treturn cty.NilVal, createUnsupportedValueError(topKey, topValue)\n\t}\n\n\treturn finalValue, nil\n}\n\n// writePrimitiveValue writes a primitive value to the writer.\n// Returns an error if the value is null or a complex type.\nfunc writePrimitiveValue(writer io.Writer, value cty.Value, path string) error {\n\t// Check if the value is null\n\tif value.IsNull() {\n\t\treturn errors.New(\"Error: Unsupported value for raw output\\n\\n\" +\n\t\t\t\"The -raw option only supports strings, numbers, and boolean values, but the output value is null.\\n\\n\" +\n\t\t\t\"Use the -json option for machine-readable representations of output values that have complex types.\")\n\t}\n\n\t// Check if the value is a complex type\n\tif config.IsComplexType(value) {\n\t\treturn createUnsupportedValueError(path, value)\n\t}\n\n\t// Unmark the value if it's marked (like with \"sensitive\")\n\tif value.IsMarked() {\n\t\tvalue, _ = value.Unmark()\n\t}\n\n\tvalueStr, err := config.FormatValue(value)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t// Write the raw value without any formatting\n\tif _, err := writer.Write([]byte(valueStr)); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n\n// createUnsupportedValueError creates a formatted error for unsupported value types.\nfunc createUnsupportedValueError(path string, value cty.Value) error {\n\treturn errors.New(\"Error: Unsupported value for raw output\\n\\n\" +\n\t\t\"The -raw option only supports strings, numbers, and boolean values, but output value \\\"\" + path + \"\\\" is \" +\n\t\tvalue.Type().FriendlyName() + \".\\n\\n\" +\n\t\t\"Use the -json option for machine-readable representations of output values that have complex types.\")\n}\n\n// PrintOutputs formats outputs as HCL and writes them to the provided writer.\n// It creates a new HCL file with each top-level output as an attribute, preserving the\n// original structure of complex types like maps and objects.\nfunc PrintOutputs(writer io.Writer, outputs cty.Value) error {\n\tif outputs == cty.NilVal {\n\t\treturn nil\n\t}\n\n\tf := hclwrite.NewEmptyFile()\n\trootBody := f.Body()\n\n\tfor key, val := range outputs.AsValueMap() {\n\t\trootBody.SetAttributeRaw(key, hclwrite.TokensForValue(val))\n\t}\n\n\tif _, err := writer.Write(f.Bytes()); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n\n// PrintJSONOutput formats outputs as pretty-printed JSON with 2-space indentation.\n// It marshals the cty.Value data to JSON using the go-cty library and writes the formatted\n// result to the provided writer.\nfunc PrintJSONOutput(writer io.Writer, outputs cty.Value) error {\n\tif outputs == cty.NilVal {\n\t\treturn nil\n\t}\n\n\trawJSON, err := ctyjson.Marshal(outputs, outputs.Type())\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tvar pretty bytes.Buffer\n\tif err := json.Indent(&pretty, rawJSON, \"\", \"  \"); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tif _, err := writer.Write(pretty.Bytes()); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/cli/commands/stack/output_test.go",
    "content": "package stack_test\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/zclconf/go-cty/cty\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/stack\"\n)\n\nfunc TestPrintRawOutputsBasicTypes(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tvalue    cty.Value\n\t\texpected string\n\t\tmessage  string\n\t}{\n\t\t{\n\t\t\tname:     \"String Value\",\n\t\t\tvalue:    cty.StringVal(\"value1\"),\n\t\t\texpected: \"value1\",\n\t\t\tmessage:  \"String values should be printed without quotes\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Number Value\",\n\t\t\tvalue:    cty.NumberIntVal(42),\n\t\t\texpected: \"42\",\n\t\t\tmessage:  \"Number values should be printed as is\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Boolean Value\",\n\t\t\tvalue:    cty.BoolVal(true),\n\t\t\texpected: \"true\",\n\t\t\tmessage:  \"Boolean values should be printed as is\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar buffer bytes.Buffer\n\n\t\t\toutputs := cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\"key1\": tt.value,\n\t\t\t})\n\n\t\t\terr := stack.PrintRawOutputs(nil, &buffer, outputs)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, buffer.String(), tt.message)\n\t\t})\n\t}\n}\n\nfunc TestPrintRawOutputsComplexObject(t *testing.T) {\n\tt.Parallel()\n\n\tvar buffer bytes.Buffer\n\n\toutputs := cty.ObjectVal(map[string]cty.Value{\n\t\t\"key1\": cty.MapVal(map[string]cty.Value{\n\t\t\t\"nested\": cty.StringVal(\"value\"),\n\t\t}),\n\t})\n\n\terr := stack.PrintRawOutputs(nil, &buffer, outputs)\n\trequire.Error(t, err, \"Complex objects should return an error\")\n\tassert.Contains(t, err.Error(), \"Unsupported value for raw output\")\n\tassert.Contains(t, err.Error(), \"key1\")\n}\n\nfunc TestPrintRawOutputsMultipleKeys(t *testing.T) {\n\tt.Parallel()\n\n\tvar buffer bytes.Buffer\n\n\toutputs := cty.ObjectVal(map[string]cty.Value{\n\t\t\"key1\": cty.StringVal(\"value1\"),\n\t\t\"key2\": cty.NumberIntVal(2),\n\t})\n\n\terr := stack.PrintRawOutputs(nil, &buffer, outputs)\n\trequire.Error(t, err, \"Multiple keys should return an error\")\n\tassert.Contains(t, err.Error(), \"requires a single output value\")\n}\n\nfunc TestPrintRawOutputsList(t *testing.T) {\n\tt.Parallel()\n\n\tvar buffer bytes.Buffer\n\n\toutputs := cty.ObjectVal(map[string]cty.Value{\n\t\t\"key1\": cty.ListVal([]cty.Value{cty.StringVal(\"a\"), cty.StringVal(\"b\")}),\n\t})\n\n\terr := stack.PrintRawOutputs(nil, &buffer, outputs)\n\trequire.Error(t, err, \"List values should return an error\")\n\tassert.Contains(t, err.Error(), \"Unsupported value for raw output\")\n\tassert.Contains(t, err.Error(), \"key1\")\n\tassert.Contains(t, err.Error(), \"list\")\n}\n\nfunc TestPrintRawOutputsNil(t *testing.T) {\n\tt.Parallel()\n\n\tvar buffer bytes.Buffer\n\n\terr := stack.PrintRawOutputs(nil, &buffer, cty.NilVal)\n\trequire.NoError(t, err)\n\tassert.Empty(t, buffer.String())\n}\n\nfunc TestPrintOutputs(t *testing.T) {\n\tt.Parallel()\n\n\tvar buffer bytes.Buffer\n\n\toutputs := cty.ObjectVal(map[string]cty.Value{\n\t\t\"key1\": cty.StringVal(\"value1\"),\n\t\t\"key2\": cty.NumberIntVal(2),\n\t})\n\n\terr := stack.PrintOutputs(&buffer, outputs)\n\trequire.NoError(t, err)\n\tassert.Contains(t, buffer.String(), \"key1 = \\\"value1\\\"\")\n\tassert.Contains(t, buffer.String(), \"key2 = 2\")\n}\n\nfunc TestPrintJSONOutput(t *testing.T) {\n\tt.Parallel()\n\n\tvar buffer bytes.Buffer\n\n\toutputs := cty.ObjectVal(map[string]cty.Value{\n\t\t\"key1\": cty.StringVal(\"value1\"),\n\t\t\"key2\": cty.NumberIntVal(2),\n\t})\n\n\terr := stack.PrintJSONOutput(&buffer, outputs)\n\trequire.NoError(t, err)\n\tassert.JSONEq(t, `{\"key1\":\"value1\",\"key2\":2}`, buffer.String())\n}\n\nfunc TestPrintRawOutputsEdgeCases(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\toutputs     cty.Value\n\t\tname        string\n\t\texpected    string\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname:        \"Empty Outputs\",\n\t\t\toutputs:     cty.ObjectVal(map[string]cty.Value{}),\n\t\t\texpectError: false,\n\t\t\texpected:    \"\",\n\t\t},\n\t\t{\n\t\t\tname:        \"Nil Outputs\",\n\t\t\toutputs:     cty.NilVal,\n\t\t\texpectError: false,\n\t\t\texpected:    \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"Single Nested Structure with Single Value\",\n\t\t\toutputs: cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\"parent\": cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\t\"child\": cty.StringVal(\"value\"),\n\t\t\t\t}),\n\t\t\t}),\n\t\t\texpectError: false,\n\t\t\texpected:    \"value\",\n\t\t},\n\t\t{\n\t\t\tname: \"Multi-level Nested Structure\",\n\t\t\toutputs: cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\"level1\": cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\t\"level2\": cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\t\t\"level3\": cty.StringVal(\"deep_value\"),\n\t\t\t\t\t}),\n\t\t\t\t}),\n\t\t\t}),\n\t\t\texpectError: false,\n\t\t\texpected:    \"deep_value\",\n\t\t},\n\t\t{\n\t\t\tname: \"Multiple Top-level Keys\",\n\t\t\toutputs: cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\"string\": cty.StringVal(\"text\"),\n\t\t\t\t\"number\": cty.NumberIntVal(42),\n\t\t\t}),\n\t\t\texpectError: true,\n\t\t\texpected:    \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"List Output (Complex Type)\",\n\t\t\toutputs: cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\"list\": cty.ListVal([]cty.Value{cty.StringVal(\"a\"), cty.StringVal(\"b\")}),\n\t\t\t}),\n\t\t\texpectError: true,\n\t\t\texpected:    \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"Map Output (Complex Type)\",\n\t\t\toutputs: cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\"map\": cty.MapVal(map[string]cty.Value{\n\t\t\t\t\t\"a\": cty.StringVal(\"value\"),\n\t\t\t\t}),\n\t\t\t}),\n\t\t\texpectError: true,\n\t\t\texpected:    \"\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar buffer bytes.Buffer\n\n\t\t\terr := stack.PrintRawOutputs(nil, &buffer, tt.outputs)\n\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tt.expected, buffer.String())\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Additional test case for deeper nested structures\nfunc TestPrintRawOutputsDeepNesting(t *testing.T) {\n\tt.Parallel()\n\n\tvar buffer bytes.Buffer\n\t// Create a more deeply nested structure\n\toutputs := cty.ObjectVal(map[string]cty.Value{\n\t\t\"a\": cty.ObjectVal(map[string]cty.Value{\n\t\t\t\"b\": cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\"c\": cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\t\"d\": cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\t\t\"e\": cty.StringVal(\"very_nested_value\"),\n\t\t\t\t\t}),\n\t\t\t\t}),\n\t\t\t}),\n\t\t}),\n\t})\n\n\terr := stack.PrintRawOutputs(nil, &buffer, outputs)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"very_nested_value\", buffer.String(), \"Should extract deeply nested values\")\n}\n\n// Test partial nested pattern\nfunc TestPrintRawOutputsPartialNesting(t *testing.T) {\n\tt.Parallel()\n\n\tvar buffer bytes.Buffer\n\t// Create a structure where a nested value terminates with a complex value\n\toutputs := cty.ObjectVal(map[string]cty.Value{\n\t\t\"parent\": cty.ObjectVal(map[string]cty.Value{\n\t\t\t\"child\": cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\"complex\": cty.ListVal([]cty.Value{cty.StringVal(\"a\"), cty.StringVal(\"b\")}),\n\t\t\t}),\n\t\t}),\n\t})\n\n\terr := stack.PrintRawOutputs(nil, &buffer, outputs)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"Unsupported value for raw output\")\n}\n\n// Test the boundary case where there's exactly one leaf node value\nfunc TestPrintRawOutputsExactlyOneLeafNode(t *testing.T) {\n\tt.Parallel()\n\n\tvar buffer bytes.Buffer\n\t// Create a structure with one leaf node that is a string\n\toutputs := cty.ObjectVal(map[string]cty.Value{\n\t\t\"a\": cty.ObjectVal(map[string]cty.Value{\n\t\t\t\"b\": cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\"c\": cty.StringVal(\"leaf_value\"),\n\t\t\t}),\n\t\t}),\n\t})\n\n\terr := stack.PrintRawOutputs(nil, &buffer, outputs)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"leaf_value\", buffer.String(), \"Should extract the single leaf value\")\n}\n\n// Test with special characters in the string\nfunc TestPrintRawOutputsSpecialCharacters(t *testing.T) {\n\tt.Parallel()\n\n\tvar buffer bytes.Buffer\n\n\toutputs := cty.ObjectVal(map[string]cty.Value{\n\t\t\"special\": cty.StringVal(\"value with spaces, quotes \\\" and special chars @#$%^&*()\"),\n\t})\n\n\terr := stack.PrintRawOutputs(nil, &buffer, outputs)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"value with spaces, quotes \\\" and special chars @#$%^&*()\", buffer.String(),\n\t\t\"Should preserve special characters in the output\")\n}\n\n// Test with null value\nfunc TestPrintRawOutputsNullValue(t *testing.T) {\n\tt.Parallel()\n\n\tvar buffer bytes.Buffer\n\n\toutputs := cty.ObjectVal(map[string]cty.Value{\n\t\t\"null_val\": cty.NullVal(cty.String),\n\t})\n\n\terr := stack.PrintRawOutputs(nil, &buffer, outputs)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"Unsupported value for raw output\")\n}\n\nfunc TestPrintOutputsEdgeCases(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\toutputs  cty.Value\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"Empty Outputs\",\n\t\t\toutputs:  cty.ObjectVal(map[string]cty.Value{}),\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname:     \"Nil Outputs\",\n\t\t\toutputs:  cty.NilVal,\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"Nested Structures\",\n\t\t\toutputs: cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\"parent\": cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\t\"child\": cty.StringVal(\"value\"),\n\t\t\t\t}),\n\t\t\t}),\n\t\t\texpected: []string{\"parent = {\", \"child = \\\"value\\\"\"},\n\t\t},\n\t\t{\n\t\t\tname: \"Different Data Types\",\n\t\t\toutputs: cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\"string\": cty.StringVal(\"text\"),\n\t\t\t\t\"number\": cty.NumberIntVal(42),\n\t\t\t\t\"bool\":   cty.BoolVal(true),\n\t\t\t\t\"list\":   cty.ListVal([]cty.Value{cty.StringVal(\"a\"), cty.StringVal(\"b\")}),\n\t\t\t}),\n\t\t\texpected: []string{\n\t\t\t\t\"string = \\\"text\\\"\",\n\t\t\t\t\"number = 42\",\n\t\t\t\t\"bool   = true\",\n\t\t\t\t\"list   = [\",\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\tt.Parallel()\n\n\t\t\tvar buffer bytes.Buffer\n\n\t\t\terr := stack.PrintOutputs(&buffer, tt.outputs)\n\t\t\trequire.NoError(t, err)\n\n\t\t\toutput := buffer.String()\n\t\t\tfor _, expectedLine := range tt.expected {\n\t\t\t\tassert.Contains(t, output, expectedLine)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPrintJSONOutputEdgeCases(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\toutputs  cty.Value\n\t\texpected string\n\t\tisNil    bool\n\t}{\n\t\t{\n\t\t\tname:     \"Empty Outputs\",\n\t\t\toutputs:  cty.ObjectVal(map[string]cty.Value{}),\n\t\t\texpected: \"{}\",\n\t\t\tisNil:    false,\n\t\t},\n\t\t{\n\t\t\tname:     \"Nil Outputs\",\n\t\t\toutputs:  cty.NilVal,\n\t\t\texpected: \"\",\n\t\t\tisNil:    true,\n\t\t},\n\t\t{\n\t\t\tname: \"Nested Structures\",\n\t\t\toutputs: cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\"parent\": cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\t\"child\": cty.StringVal(\"value\"),\n\t\t\t\t}),\n\t\t\t}),\n\t\t\texpected: `{\"parent\":{\"child\":\"value\"}}`,\n\t\t\tisNil:    false,\n\t\t},\n\t\t{\n\t\t\tname: \"Different Data Types\",\n\t\t\toutputs: cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\"string\": cty.StringVal(\"text\"),\n\t\t\t\t\"number\": cty.NumberIntVal(42),\n\t\t\t\t\"bool\":   cty.BoolVal(true),\n\t\t\t\t\"list\":   cty.ListVal([]cty.Value{cty.StringVal(\"a\"), cty.StringVal(\"b\")}),\n\t\t\t}),\n\t\t\texpected: `{\"string\":\"text\",\"number\":42,\"bool\":true,\"list\":[\"a\",\"b\"]}`,\n\t\t\tisNil:    false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar buffer bytes.Buffer\n\n\t\t\terr := stack.PrintJSONOutput(&buffer, tt.outputs)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif tt.isNil {\n\t\t\t\tassert.Equal(t, tt.expected, buffer.String())\n\t\t\t} else {\n\t\t\t\tassert.JSONEq(t, tt.expected, buffer.String())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPrintRawOutputsNestedValues(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tvalue    cty.Value\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"String Value\",\n\t\t\tvalue:    cty.StringVal(\"nested_text\"),\n\t\t\texpected: \"nested_text\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Number Value\",\n\t\t\tvalue:    cty.NumberIntVal(42),\n\t\t\texpected: \"42\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Boolean Value\",\n\t\t\tvalue:    cty.BoolVal(true),\n\t\t\texpected: \"true\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar buffer bytes.Buffer\n\n\t\t\toutputs := cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\"parent\": cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\t\"child\": cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\t\t\"value\": tt.value,\n\t\t\t\t\t}),\n\t\t\t\t}),\n\t\t\t})\n\n\t\t\terr := stack.PrintRawOutputs(nil, &buffer, outputs)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, buffer.String(),\n\t\t\t\t\"Should extract the nested %s value\", tt.name)\n\t\t})\n\t}\n}\n\nfunc TestPrintRawOutputsSpecialCases(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\toutputs     cty.Value\n\t\tname        string\n\t\terrorMsg    string\n\t\texpected    string\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname: \"Nested Multiple Keys\",\n\t\t\toutputs: cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\"parent\": cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\t\"child1\": cty.StringVal(\"value1\"),\n\t\t\t\t\t\"child2\": cty.StringVal(\"value2\"),\n\t\t\t\t}),\n\t\t\t}),\n\t\t\texpectError: true,\n\t\t\terrorMsg:    \"Unsupported value for raw output\",\n\t\t\texpected:    \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"Marked String\",\n\t\t\toutputs: cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\"marked_string\": cty.StringVal(\"marked_value\").Mark(\"sensitive\"),\n\t\t\t}),\n\t\t\texpectError: false,\n\t\t\terrorMsg:    \"\",\n\t\t\texpected:    \"marked_value\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar buffer bytes.Buffer\n\n\t\t\terr := stack.PrintRawOutputs(nil, &buffer, tt.outputs)\n\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), tt.errorMsg)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tt.expected, buffer.String(), \"Should handle %s correctly\", tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/cli/commands/stack/stack.go",
    "content": "package stack\n\nimport (\n\t\"context\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/zclconf/go-cty/cty\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runall\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/stacks/clean\"\n\t\"github.com/gruntwork-io/terragrunt/internal/stacks/generate\"\n\t\"github.com/gruntwork-io/terragrunt/internal/stacks/output\"\n\t\"github.com/gruntwork-io/terragrunt/internal/worktrees\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// RunGenerate runs the stack command.\nfunc RunGenerate(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\topts.TerragruntStackConfigPath = filepath.Join(opts.WorkingDir, config.DefaultStackFile)\n\n\tif opts.NoStackGenerate {\n\t\tl.Debugf(\"Skipping stack generation for %s\", opts.TerragruntStackConfigPath)\n\t\treturn nil\n\t}\n\n\topts.StackAction = \"generate\"\n\n\t// Clean stack folders before calling `generate` when the `--source-update` flag is passed\n\tif opts.SourceUpdate {\n\t\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"stack_clean\", map[string]any{\n\t\t\t\"stack_config_path\": opts.TerragruntStackConfigPath,\n\t\t\t\"working_dir\":       opts.WorkingDir,\n\t\t}, func(ctx context.Context) error {\n\t\t\tl.Debugf(\"Running stack clean for %s, as part of generate command\", opts.WorkingDir)\n\t\t\treturn clean.CleanStacks(l, opts)\n\t\t})\n\t\tif err != nil {\n\t\t\treturn errors.Errorf(\"failed to clean stack directories under %q: %w\", opts.WorkingDir, err)\n\t\t}\n\t}\n\n\tfilters := opts.Filters\n\n\tgitFilters := filters.UniqueGitFilters()\n\n\t// Only create worktrees when git filter expressions are present\n\tvar wts *worktrees.Worktrees\n\n\tif len(gitFilters) > 0 {\n\t\tvar err error\n\n\t\twts, err = worktrees.NewWorktrees(ctx, l, opts.WorkingDir, gitFilters)\n\t\tif err != nil {\n\t\t\treturn errors.Errorf(\"failed to create worktrees: %w\", err)\n\t\t}\n\n\t\tdefer func() {\n\t\t\tcleanupErr := wts.Cleanup(ctx, l)\n\t\t\tif cleanupErr != nil {\n\t\t\t\tl.Errorf(\"failed to cleanup worktrees: %v\", cleanupErr)\n\t\t\t}\n\t\t}()\n\t}\n\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, \"stack_generate\", map[string]any{\n\t\t\"stack_config_path\": opts.TerragruntStackConfigPath,\n\t\t\"working_dir\":       opts.WorkingDir,\n\t}, func(ctx context.Context) error {\n\t\treturn generate.GenerateStacks(ctx, l, opts, wts)\n\t})\n}\n\n// Run execute stack command.\nfunc Run(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\topts.StackAction = \"run\"\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"stack_run\", map[string]any{\n\t\t\"stack_config_path\": opts.TerragruntStackConfigPath,\n\t\t\"working_dir\":       opts.WorkingDir,\n\t}, func(ctx context.Context) error {\n\t\treturn RunGenerate(ctx, l, opts)\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn runall.Run(ctx, l, opts)\n}\n\n// RunOutput stack output.\nfunc RunOutput(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, index string) error {\n\topts.StackAction = \"output\"\n\n\tvar outputs cty.Value\n\n\t// collect outputs\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"stack_output\", map[string]any{\n\t\t\"stack_config_path\": opts.TerragruntStackConfigPath,\n\t\t\"working_dir\":       opts.WorkingDir,\n\t}, func(ctx context.Context) error {\n\t\tstackOutputs, err := output.StackOutput(ctx, l, opts)\n\t\toutputs = stackOutputs\n\n\t\treturn err\n\t})\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t// Filter outputs based on index key\n\tfilteredOutputs := FilterOutputs(outputs, index)\n\n\t// render outputs\n\n\tswitch opts.StackOutputFormat {\n\tdefault:\n\t\tif err := PrintOutputs(opts.Writers.Writer, filteredOutputs); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\tcase rawOutputFormat:\n\t\tif err := PrintRawOutputs(opts, opts.Writers.Writer, filteredOutputs); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\tcase jsonOutputFormat:\n\t\tif err := PrintJSONOutput(opts.Writers.Writer, filteredOutputs); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// FilterOutputs filters the outputs based on the provided index key.\nfunc FilterOutputs(outputs cty.Value, index string) cty.Value {\n\tif !outputs.IsKnown() || outputs.IsNull() || len(index) == 0 {\n\t\treturn outputs\n\t}\n\n\t// Split the index into parts\n\tindexParts := strings.Split(index, \".\")\n\t// Traverse the map using the index parts\n\tcurrentValue := outputs\n\tfor _, part := range indexParts {\n\t\t// Check if the current value is a map or object\n\t\tif currentValue.Type().IsObjectType() || currentValue.Type().IsMapType() {\n\t\t\tvalueMap := currentValue.AsValueMap()\n\t\t\tif nextValue, exists := valueMap[part]; exists {\n\t\t\t\tcurrentValue = nextValue\n\t\t\t} else {\n\t\t\t\t// If any part of the index path is not found, return NilVal\n\t\t\t\treturn cty.NilVal\n\t\t\t}\n\t\t} else {\n\t\t\t// If the current value is not a map or object, return NilVal\n\t\t\treturn cty.NilVal\n\t\t}\n\t}\n\n\t// Reconstruct the nested map structure\n\tnested := currentValue\n\tfor i := len(indexParts) - 1; i >= 0; i-- {\n\t\tnested = cty.ObjectVal(map[string]cty.Value{\n\t\t\tindexParts[i]: nested,\n\t\t})\n\t}\n\n\treturn nested\n}\n\n// RunClean recursively removes all stack directories under the specified WorkingDir.\nfunc RunClean(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\ttelemeter := telemetry.TelemeterFromContext(ctx)\n\n\terr := telemeter.Collect(ctx, \"stack_clean\", map[string]any{\n\t\t\"stack_config_path\": opts.TerragruntStackConfigPath,\n\t\t\"working_dir\":       opts.WorkingDir,\n\t}, func(ctx context.Context) error {\n\t\treturn clean.CleanStacks(l, opts)\n\t})\n\tif err != nil {\n\t\treturn errors.Errorf(\"failed to clean stack directories under %q: %w\", opts.WorkingDir, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/cli/commands/version/cli.go",
    "content": "// Package version represents the version CLI command that works the same as the `--version` flag.\npackage version\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n)\n\nconst (\n\tCommandName = \"version\"\n)\n\nfunc NewCommand() *clihelper.Command {\n\treturn &clihelper.Command{\n\t\tName:                         CommandName,\n\t\tUsage:                        \"Show terragrunt version.\",\n\t\tHidden:                       true,\n\t\tDisabledErrorOnUndefinedFlag: true,\n\t\tAction: func(ctx context.Context, cliCtx *clihelper.Context) error {\n\t\t\treturn clihelper.NewExitError(Action(ctx, cliCtx), 0)\n\t\t},\n\t}\n}\n\nfunc Action(ctx context.Context, cliCtx *clihelper.Context) error {\n\treturn clihelper.ShowVersion(ctx, cliCtx)\n}\n"
  },
  {
    "path": "internal/cli/flags/deprecated_flag.go",
    "content": "package flags\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n)\n\nvar _ = clihelper.Flag(new(DeprecatedFlag))\n\n// DeprecatedFlags are multiple of DeprecatedFlag flags.\ntype DeprecatedFlags []*DeprecatedFlag\n\n// DeprecatedFlag represents a deprecated flag that is not shown in the CLI help, but its names, envVars, are registered.\ntype DeprecatedFlag struct {\n\tclihelper.Flag\n\tnewValueFn             NewValueFunc\n\tcontrols               strict.Controls\n\tnames                  []string\n\tenvVars                []string\n\tallowedSubcommandScope bool\n}\n\n// GetHidden implements `clihelper.Flag` interface.\nfunc (flag *DeprecatedFlag) GetHidden() bool {\n\treturn true\n}\n\n// AllowedSubcommandScope implements `clihelper.Flag` interface.\nfunc (flag *DeprecatedFlag) AllowedSubcommandScope() bool {\n\treturn flag.allowedSubcommandScope\n}\n\n// GetEnvVars implements `clihelper.Flag` interface.\nfunc (flag *DeprecatedFlag) GetEnvVars() []string {\n\treturn flag.envVars\n}\n\n// Names implements `clihelper.Flag` interface.\nfunc (flag *DeprecatedFlag) Names() []string {\n\treturn flag.names\n}\n\n// Evaluate returns an error if the one of the controls is enabled otherwise logs warning messages and returns nil.\nfunc (flag *DeprecatedFlag) Evaluate(ctx context.Context) error {\n\treturn flag.controls.Evaluate(ctx)\n}\n\n// SetStrictControls creates a strict control for the flag and registers it.\nfunc (flag *DeprecatedFlag) SetStrictControls(mainFlag *Flag, regControlsFn RegisterStrictControlsFunc) {\n\tif regControlsFn == nil {\n\t\treturn\n\t}\n\n\tvar newValue string\n\n\tif flag.newValueFn != nil {\n\t\tnewValue = flag.newValueFn(nil)\n\t}\n\n\tflagNameControl := controls.NewDeprecatedFlagName(flag, mainFlag, newValue)\n\tenvVarControl := controls.NewDeprecatedEnvVar(flag, mainFlag, newValue)\n\n\tif ok := regControlsFn(flagNameControl, envVarControl); ok {\n\t\tflag.controls = strict.Controls{flagNameControl, envVarControl}\n\t}\n}\n\n// NewValueFunc represents a function that returns a new value for the current flag if a deprecated flag is called.\n// Used when the current flag and the deprecated flag are of different types. For example, the string `log-format` flag\n// must be set to `json` when deprecated bool `terragrunt-json-log` flag is used. More examples:\n//\n// terragrunt-disable-log-formatting  replaced with: log-format=key-value\n// terragrunt-json-log                replaced with: log-format=json\n// terragrunt-tf-logs-to-json         replaced with: log-format=json\ntype NewValueFunc func(flagValue clihelper.FlagValue) string\n\n// NewValue returns a callback function that is used to get a new value for the current flag.\nfunc NewValue(val string) NewValueFunc {\n\treturn func(_ clihelper.FlagValue) string {\n\t\treturn val\n\t}\n}\n\n// RegisterStrictControlsFunc represents a callback func that registers the given controls in the `opts.StrictControls` stict control tree .\ntype RegisterStrictControlsFunc func(flagNameControl, envVarControl strict.Control) bool\n\n// StrictControlsByCommand returns a callback function that adds the taken controls as subcontrols for the given `controlNames`.\n// Using the given `commandName` as categories.\nfunc StrictControlsByCommand(strictControls strict.Controls, commandName string, controlNames ...string) RegisterStrictControlsFunc {\n\treturn func(flagNameControl, envVarControl strict.Control) bool {\n\t\tflagNamesCategory := fmt.Sprintf(controls.CommandFlagsCategoryNameFmt, commandName)\n\t\tenvVarsCategory := fmt.Sprintf(controls.CommandEnvVarsCategoryNameFmt, commandName)\n\n\t\treturn registerStrictControls(strictControls, flagNameControl, envVarControl, flagNamesCategory, envVarsCategory, controlNames...)\n\t}\n}\n\n// StrictControlsByGlobalFlags returns a callback function that adds the taken controls as subcontrols for the given `controlNames`.\n// And assigns the \"Global Flag\" category to these controls.\nfunc StrictControlsByGlobalFlags(strictControls strict.Controls, controlNames ...string) RegisterStrictControlsFunc {\n\treturn func(flagNameControl, envVarControl strict.Control) bool {\n\t\treturn registerStrictControls(strictControls, flagNameControl, envVarControl, controls.GlobalFlagsCategoryName, controls.GlobalEnvVarsCategoryName, controlNames...)\n\t}\n}\n\nfunc registerStrictControls(strictControls strict.Controls,\n\tflagNameControl, envVarControl strict.Control,\n\tflagNamesCategory, envVarsCategory string,\n\tcontrolNames ...string) bool {\n\tif strictControls == nil {\n\t\treturn false\n\t}\n\n\tif flagNameControl != nil {\n\t\tstrictControls.FilterByNames(append(\n\t\t\tcontrolNames,\n\t\t\tcontrols.TerragruntPrefixFlags,\n\t\t\tcontrols.DeprecatedFlags,\n\t\t)...).AddSubcontrolsToCategory(flagNamesCategory, flagNameControl)\n\t}\n\n\tif envVarControl != nil {\n\t\tstrictControls.FilterByNames(append(\n\t\t\tcontrolNames,\n\t\t\tcontrols.TerragruntPrefixEnvVars,\n\t\t\tcontrols.DeprecatedEnvVars,\n\t\t)...).AddSubcontrolsToCategory(envVarsCategory, envVarControl)\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "internal/cli/flags/error_handler.go",
    "content": "package flags\n\nimport (\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n)\n\n// ErrorHandler returns `FlagErrHandlerFunc` which takes a flag parsing error\n// and tries to suggest the correct command to use with this flag. Otherwise returns the error as is.\nfunc ErrorHandler(commands clihelper.Commands) clihelper.FlagErrHandlerFunc {\n\treturn func(ctx *clihelper.Context, err error) error {\n\t\tvar undefinedFlagErr clihelper.UndefinedFlagError\n\t\tif !errors.As(err, &undefinedFlagErr) {\n\t\t\treturn err\n\t\t}\n\n\t\tundefFlag := string(undefinedFlagErr)\n\n\t\tif cmds, flag := findFlagInCommands(commands, undefFlag); cmds != nil {\n\t\t\tvar (\n\t\t\t\tflagHint = util.FirstNonEmpty(flag.Names())\n\t\t\t\tcmdHint  = strings.Join(cmds.Names(), \" \")\n\t\t\t)\n\n\t\t\tif ctx.Parent().Command != nil {\n\t\t\t\treturn NewCommandFlagHintError(ctx.Command.Name, undefFlag, cmdHint, flagHint)\n\t\t\t}\n\n\t\t\treturn NewGlobalFlagHintError(undefFlag, cmdHint, flagHint)\n\t\t}\n\n\t\tif isRunContext(ctx) {\n\t\t\treturn NewPassthroughFlagHintError(undefFlag)\n\t\t}\n\n\t\treturn err\n\t}\n}\n\n// maxContextDepth is the upper bound on parent traversal in isRunContext\n// to guard against unexpectedly deep or circular context chains.\nconst maxContextDepth = 10\n\n// isRunContext returns true if the current command or any ancestor is the \"run\" command.\nfunc isRunContext(ctx *clihelper.Context) bool {\n\tfor range maxContextDepth {\n\t\tif ctx == nil {\n\t\t\treturn false\n\t\t}\n\n\t\tif ctx.Command != nil && ctx.Command.Name == \"run\" {\n\t\t\treturn true\n\t\t}\n\n\t\tctx = ctx.Parent()\n\t}\n\n\treturn false\n}\n\nfunc findFlagInCommands(commands clihelper.Commands, undefFlag string) (clihelper.Commands, clihelper.Flag) {\n\tif len(commands) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tfor _, cmd := range commands {\n\t\tfor _, flag := range cmd.Flags {\n\t\t\tflagNames := flag.Names()\n\n\t\t\tif flag, ok := flag.(interface{ DeprecatedNames() []string }); ok {\n\t\t\t\tflagNames = append(flagNames, flag.DeprecatedNames()...)\n\t\t\t}\n\n\t\t\tif slices.Contains(flagNames, undefFlag) {\n\t\t\t\treturn clihelper.Commands{cmd}, flag\n\t\t\t}\n\t\t}\n\n\t\tif cmds, flag := findFlagInCommands(cmd.Subcommands, undefFlag); cmds != nil {\n\t\t\treturn append(clihelper.Commands{cmd}, cmds...), flag\n\t\t}\n\t}\n\n\treturn nil, nil\n}\n"
  },
  {
    "path": "internal/cli/flags/error_handler_test.go",
    "content": "package flags_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestErrorHandler(t *testing.T) {\n\tt.Parallel()\n\n\t// Setup commands the error handler will search for flag hints.\n\tcommands := clihelper.Commands{\n\t\t{\n\t\t\tName: \"catalog\",\n\t\t\tFlags: clihelper.Flags{\n\t\t\t\t&clihelper.BoolFlag{Name: \"no-include-root\", Destination: new(bool)},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tName: \"stack\",\n\t\t\tSubcommands: clihelper.Commands{\n\t\t\t\t{\n\t\t\t\t\tName: \"output\",\n\t\t\t\t\tFlags: clihelper.Flags{\n\t\t\t\t\t\t&clihelper.BoolFlag{Name: \"raw\", Destination: new(bool)},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\thandler := flags.ErrorHandler(commands)\n\n\t// newRootCtx creates a context at the root (global) level,\n\t// where ctx.Parent().Command is nil.\n\tnewRootCtx := func() *clihelper.Context {\n\t\tapp := clihelper.NewApp()\n\t\tappCtx := clihelper.NewAppContext(app, nil)\n\t\trootCmd := &clihelper.Command{Name: \"terragrunt\", IsRoot: true}\n\n\t\treturn appCtx.NewCommandContext(rootCmd, nil)\n\t}\n\n\t// newCommandCtx creates a context for a named subcommand,\n\t// where ctx.Parent().Command is the root command (non-nil).\n\tnewCommandCtx := func(name string) *clihelper.Context {\n\t\tapp := clihelper.NewApp()\n\t\tappCtx := clihelper.NewAppContext(app, nil)\n\t\trootCmd := &clihelper.Command{Name: \"terragrunt\", IsRoot: true}\n\t\trootCtx := appCtx.NewCommandContext(rootCmd, nil)\n\t\tcmd := &clihelper.Command{Name: name}\n\n\t\treturn rootCtx.NewCommandContext(cmd, nil)\n\t}\n\n\t// newRunSubcommandCtx creates a context for a subcommand of \"run\"\n\t// (e.g., \"providers\" in \"terragrunt run providers lock -platform ...\").\n\tnewRunSubcommandCtx := func(name string) *clihelper.Context {\n\t\tapp := clihelper.NewApp()\n\t\tappCtx := clihelper.NewAppContext(app, nil)\n\t\trootCmd := &clihelper.Command{Name: \"terragrunt\", IsRoot: true}\n\t\trootCtx := appCtx.NewCommandContext(rootCmd, nil)\n\t\trunCmd := &clihelper.Command{Name: \"run\"}\n\t\trunCtx := rootCtx.NewCommandContext(runCmd, nil)\n\t\tcmd := &clihelper.Command{Name: name}\n\n\t\treturn runCtx.NewCommandContext(cmd, nil)\n\t}\n\n\ttestCases := []struct {\n\t\tctx           *clihelper.Context\n\t\terr           error\n\t\texpectedError error\n\t\tname          string\n\t}{\n\t\t{\n\t\t\tname:          \"non-undefined-flag error passes through unchanged\",\n\t\t\tctx:           newRootCtx(),\n\t\t\terr:           errors.New(\"some other error\"),\n\t\t\texpectedError: errors.New(\"some other error\"),\n\t\t},\n\t\t{\n\t\t\tname:          \"known flag at global level returns GlobalFlagHintError\",\n\t\t\tctx:           newRootCtx(),\n\t\t\terr:           clihelper.UndefinedFlagError(\"raw\"),\n\t\t\texpectedError: flags.NewGlobalFlagHintError(\"raw\", \"stack output\", \"raw\"),\n\t\t},\n\t\t{\n\t\t\tname:          \"known flag at command level returns CommandFlagHintError\",\n\t\t\tctx:           newCommandCtx(\"run\"),\n\t\t\terr:           clihelper.UndefinedFlagError(\"no-include-root\"),\n\t\t\texpectedError: flags.NewCommandFlagHintError(\"run\", \"no-include-root\", \"catalog\", \"no-include-root\"),\n\t\t},\n\t\t{\n\t\t\tname:          \"unknown flag on run command returns PassthroughFlagHintError\",\n\t\t\tctx:           newCommandCtx(\"run\"),\n\t\t\terr:           clihelper.UndefinedFlagError(\"platform\"),\n\t\t\texpectedError: flags.NewPassthroughFlagHintError(\"platform\"),\n\t\t},\n\t\t{\n\t\t\tname:          \"unknown flag on run subcommand returns PassthroughFlagHintError\",\n\t\t\tctx:           newRunSubcommandCtx(\"providers\"),\n\t\t\terr:           clihelper.UndefinedFlagError(\"platform\"),\n\t\t\texpectedError: flags.NewPassthroughFlagHintError(\"platform\"),\n\t\t},\n\t\t{\n\t\t\tname:          \"unknown flag on non-run command returns original error\",\n\t\t\tctx:           newCommandCtx(\"catalog\"),\n\t\t\terr:           clihelper.UndefinedFlagError(\"platform\"),\n\t\t\texpectedError: clihelper.UndefinedFlagError(\"platform\"),\n\t\t},\n\t\t{\n\t\t\tname:          \"unknown flag at global level returns original error\",\n\t\t\tctx:           newRootCtx(),\n\t\t\terr:           clihelper.UndefinedFlagError(\"platform\"),\n\t\t\texpectedError: clihelper.UndefinedFlagError(\"platform\"),\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.Parallel()\n\n\t\t\tresult := handler(tc.ctx, tc.err)\n\t\t\tassert.EqualError(t, result, tc.expectedError.Error())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/cli/flags/errors.go",
    "content": "package flags\n\nimport \"fmt\"\n\nvar _ error = new(GlobalFlagHintError)\n\ntype GlobalFlagHintError struct {\n\tundefFlag string\n\tcmdHint   string\n\tflagHint  string\n}\n\nfunc NewGlobalFlagHintError(undefFlag, cmdHint, flagHint string) *GlobalFlagHintError {\n\treturn &GlobalFlagHintError{\n\t\tundefFlag: undefFlag,\n\t\tcmdHint:   cmdHint,\n\t\tflagHint:  flagHint,\n\t}\n}\n\nfunc (err GlobalFlagHintError) Error() string {\n\treturn fmt.Sprintf(\"flag `--%s` is not a valid global flag. Did you mean to use `%s --%s`?\", err.undefFlag, err.cmdHint, err.flagHint)\n}\n\nvar _ error = new(CommandFlagHintError)\n\ntype CommandFlagHintError struct {\n\tundefFlag string\n\twrongCmd  string\n\tcmdHint   string\n\tflagHint  string\n}\n\nfunc NewCommandFlagHintError(wrongCmd, undefFlag, cmdHint, flagHint string) *CommandFlagHintError {\n\treturn &CommandFlagHintError{\n\t\tundefFlag: undefFlag,\n\t\twrongCmd:  wrongCmd,\n\t\tcmdHint:   cmdHint,\n\t\tflagHint:  flagHint,\n\t}\n}\n\nfunc (err CommandFlagHintError) Error() string {\n\treturn fmt.Sprintf(\"flag `--%s` is not a valid flag for `%s`. Did you mean to use `%s --%s`?\", err.undefFlag, err.wrongCmd, err.cmdHint, err.flagHint)\n}\n\nvar _ error = new(PassthroughFlagHintError)\n\ntype PassthroughFlagHintError struct {\n\tundefFlag string\n}\n\nfunc NewPassthroughFlagHintError(undefFlag string) *PassthroughFlagHintError {\n\treturn &PassthroughFlagHintError{undefFlag: undefFlag}\n}\n\nfunc (err PassthroughFlagHintError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"flag `-%s` is not a Terragrunt flag. If this is an OpenTofu/Terraform flag, use `--` to forward it (e.g., `terragrunt run -- <command> -%s`).\",\n\t\terr.undefFlag, err.undefFlag,\n\t)\n}\n"
  },
  {
    "path": "internal/cli/flags/flag.go",
    "content": "// Package flags provides tools that are used by all commands to create deprecation flags with strict controls.\npackage flags\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n)\n\nvar _ = clihelper.Flag(new(Flag))\n\n// EvaluateWrapperFunc represents a function that is used to wrap the `Evaluate(ctx context.Context) error` strict control method.\n// Which can be passed as an option `WithEvaluateWrapper` to `NewFlag(...)` to control the behavior of strict control evaluation.\ntype EvaluateWrapperFunc func(ctx context.Context, evalFn func(ctx context.Context) error) error\n\n// Flag is a wrapper for `clihelper.Flag` that avoids displaying deprecated flags in help, but registers their flag names and environment variables.\ntype Flag struct {\n\tclihelper.Flag\n\tevaluateWrapper EvaluateWrapperFunc\n\tdeprecatedFlags DeprecatedFlags\n}\n\n// NewFlag returns a new Flag instance.\nfunc NewFlag(new clihelper.Flag, opts ...Option) *Flag {\n\tflag := &Flag{\n\t\tFlag: new,\n\t\tevaluateWrapper: func(ctx context.Context, evalFn func(ctx context.Context) error) error {\n\t\t\treturn evalFn(ctx)\n\t\t},\n\t}\n\n\tfor _, opt := range opts {\n\t\topt(flag)\n\t}\n\n\treturn flag\n}\n\n// TakesValue implements `github.com/urfave/clihelper.DocGenerationFlag` required to generate help.\n// TakesValue returns `true` for all flags except boolean ones that are `false` or `true` inverted.\nfunc (newFlag *Flag) TakesValue() bool {\n\tif newFlag.Flag.Value() == nil {\n\t\treturn false\n\t}\n\n\tval, ok := newFlag.Flag.Value().Get().(bool)\n\n\tif newFlag.Flag.Value().IsNegativeBoolFlag() {\n\t\tval = !val\n\t}\n\n\treturn !ok || !val\n}\n\n// DeprecatedNames returns all deprecated names for this flag.\nfunc (newFlag *Flag) DeprecatedNames() []string {\n\tvar names []string\n\n\tif flag, ok := newFlag.Flag.(interface{ DeprecatedNames() []string }); ok {\n\t\tnames = flag.DeprecatedNames()\n\t}\n\n\tfor _, deprecated := range newFlag.deprecatedFlags {\n\t\tnames = append(names, deprecated.Names()...)\n\t}\n\n\treturn names\n}\n\n// Value implements `clihelper.Flag` interface.\nfunc (newFlag *Flag) Value() clihelper.FlagValue {\n\tfor _, deprecatedFlag := range newFlag.deprecatedFlags {\n\t\tif deprecatedFlag.Flag == newFlag.Flag {\n\t\t\tcontinue\n\t\t}\n\n\t\tif deprecatedFlagValue := deprecatedFlag.Value(); deprecatedFlagValue != nil && deprecatedFlagValue.IsSet() {\n\t\t\tnewValue := deprecatedFlagValue.String()\n\n\t\t\tif newFlag.Flag.Value().IsNegativeBoolFlag() && deprecatedFlagValue.IsBoolFlag() {\n\t\t\t\tif v, ok := deprecatedFlagValue.Get().(bool); ok {\n\t\t\t\t\tnewValue = strconv.FormatBool(!v)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif deprecatedFlag.newValueFn != nil {\n\t\t\t\tnewValue = deprecatedFlag.newValueFn(deprecatedFlagValue)\n\t\t\t}\n\n\t\t\tnewFlag.Flag.Value().Getter(deprecatedFlagValue.GetName()).Set(newValue) //nolint:errcheck\n\t\t}\n\t}\n\n\treturn newFlag.Flag.Value()\n}\n\n// Apply implements `clihelper.Flag` interface.\nfunc (newFlag *Flag) Apply(set *flag.FlagSet) error {\n\tif err := newFlag.Flag.Apply(set); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, deprecated := range newFlag.deprecatedFlags {\n\t\tif deprecated.Flag == newFlag.Flag {\n\t\t\tif err := clihelper.ApplyFlag(deprecated, set); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := deprecated.Apply(set); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// RunAction implements `clihelper.Flag` interface.\nfunc (newFlag *Flag) RunAction(ctx context.Context, cliCtx *clihelper.Context) error {\n\tfor _, deprecated := range newFlag.deprecatedFlags {\n\t\tif err := newFlag.evaluateWrapper(ctx, deprecated.Evaluate); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif deprecated.Flag == nil || deprecated.Flag == newFlag.Flag || !deprecated.Value().IsSet() {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := deprecated.RunAction(ctx, cliCtx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif deprecated, ok := newFlag.Flag.(interface {\n\t\tEvaluate(ctx context.Context) error\n\t}); ok {\n\t\tif err := newFlag.evaluateWrapper(ctx, deprecated.Evaluate); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn newFlag.Flag.RunAction(ctx, cliCtx)\n}\n\n// Parse parses the given `args` for the flag value and env vars values specified in the flag.\n// The value will be assigned to the `Destination` field.\n// The value can also be retrieved using `flag.Value().Get()`.\nfunc (newFlag *Flag) Parse(args clihelper.Args) error {\n\tflagSet := flag.NewFlagSet(\"\", flag.ContinueOnError)\n\tflagSet.SetOutput(io.Discard)\n\n\tif err := newFlag.Apply(flagSet); err != nil {\n\t\treturn err\n\t}\n\n\tconst maxFlagsParse = 1000 // Maximum flags parse\n\n\tfor range maxFlagsParse {\n\t\terr := flagSet.Parse(args)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tif errStr := err.Error(); !strings.HasPrefix(errStr, clihelper.ErrMsgFlagUndefined) {\n\t\t\tbreak\n\t\t}\n\n\t\targs = flagSet.Args()\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/cli/flags/flag_opts.go",
    "content": "package flags\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n)\n\n// Option is used to set options to the `Flag`.\ntype Option func(*Flag)\n\n// WithDeprecatedFlag returns an `Option` that will register the given `deprecatedFlag` as a deprecated flag.\n// `newValueFn` is called to get a value for the new flag when this deprecated flag triggers. For example:\n//\n//\tNewFlag(&clihelper.GenericFlag[string]{\n//\t  Name:    \"log-format\",\n//\t}, WithDeprecatedFlag(&clihelper.BoolFlag{\n//\t  Name:    \"terragrunt-json-log\",\n//\t}, flags.NewValue(\"json\"), nil))\nfunc WithDeprecatedFlag(deprecatedFlag clihelper.Flag, newValueFn NewValueFunc, regControlsFn RegisterStrictControlsFunc) Option {\n\treturn func(newFlag *Flag) {\n\t\tdeprecatedFlag := &DeprecatedFlag{\n\t\t\tFlag:                   deprecatedFlag,\n\t\t\tnewValueFn:             newValueFn,\n\t\t\tallowedSubcommandScope: true,\n\t\t}\n\t\tdeprecatedFlag.SetStrictControls(newFlag, regControlsFn)\n\n\t\tnewFlag.deprecatedFlags = append(newFlag.deprecatedFlags, deprecatedFlag)\n\t}\n}\n\n// WithDeprecatedPrefix returns an `Option` that will create a deprecated flag with the same name as the new flag,\n// but with the specified `prefix` prepended to the names and environment variables.\n// Should be used with caution, as changing the name of the new flag will change the name of the deprecated flag.\n// For example:\n//\n//\tNewFlag(&clihelper.GenericFlag[string]{\n//\t  Name:    \"no-color\",\n//\t  Aliases: []string{\"disable-color\"},\n//\t  EnvVars: []string{\"NO_COLOR\",\"DISABLE_COLOR\"},\n//\t}, WithDeprecatedPrefix(Prefix{\"terragrunt\"}, nil))\n//\n// The deprecated flag will have \"terragrunt-no-color\",\"terragrunt-disable-color\" names and \"TERRAGRUNT_NO_COLOR\",\"TERRAGRUNT_DISABLE_COLOR\" env vars.\n// NOTE: This function is currently unused but retained for future flag deprecation needs.\nfunc WithDeprecatedPrefix(prefix Prefix, regControlsFn RegisterStrictControlsFunc) Option {\n\treturn func(newFlag *Flag) {\n\t\tdeprecatedFlag := &DeprecatedFlag{\n\t\t\tFlag:                   newFlag.Flag,\n\t\t\tnames:                  prefix.FlagNames(newFlag.Names()...),\n\t\t\tenvVars:                prefix.EnvVars(newFlag.Names()...),\n\t\t\tallowedSubcommandScope: true,\n\t\t}\n\t\tdeprecatedFlag.SetStrictControls(newFlag, regControlsFn)\n\n\t\tnewFlag.deprecatedFlags = append(newFlag.deprecatedFlags, deprecatedFlag)\n\t}\n}\n\n// WithDeprecatedNames returns an `Option` that will create a deprecated flag.\n// The given `flagNames` names will assign both names (converting to lowercase,dash)\n// and env vars (converting to uppercase,underscore). For example:\n//\n// WithDeprecatedNames([]string{\"NO_COLOR\", \"working-dir\"}, nil)\n//\n// The deprecated flag will have \"no-color\",\"working-dir\" names and \"NO_COLOR\",\"WORKING_DIR\" env vars.\nfunc WithDeprecatedNames(flagNames []string, regControlsFn RegisterStrictControlsFunc) Option {\n\treturn func(newFlag *Flag) {\n\t\tdeprecatedFlag := &DeprecatedFlag{\n\t\t\tFlag:                   newFlag.Flag,\n\t\t\tnames:                  Prefix{}.FlagNames(flagNames...),\n\t\t\tenvVars:                Prefix{}.EnvVars(flagNames...),\n\t\t\tallowedSubcommandScope: true,\n\t\t}\n\t\tdeprecatedFlag.SetStrictControls(newFlag, regControlsFn)\n\n\t\tnewFlag.deprecatedFlags = append(newFlag.deprecatedFlags, deprecatedFlag)\n\t}\n}\n\n// WithDeprecatedName does the same as `WithDeprecatedNames`, but with a single name.\nfunc WithDeprecatedName(flagName string, regControlsFn RegisterStrictControlsFunc) Option {\n\treturn func(newFlag *Flag) {\n\t\tWithDeprecatedNames([]string{flagName}, regControlsFn)(newFlag)\n\t}\n}\n\n// WithDeprecatedNamesEnvVars returns an `Option` that will create a deprecated flag,\n// with the given `flagNames`, `envVars` assigned to the flag names and environment variables as is.\nfunc WithDeprecatedNamesEnvVars(flagNames, envVars []string, regControlsFn RegisterStrictControlsFunc) Option {\n\treturn func(newFlag *Flag) {\n\t\tdeprecatedFlag := &DeprecatedFlag{\n\t\t\tFlag:                   newFlag.Flag,\n\t\t\tnames:                  flagNames,\n\t\t\tenvVars:                envVars,\n\t\t\tallowedSubcommandScope: true,\n\t\t}\n\t\tdeprecatedFlag.SetStrictControls(newFlag, regControlsFn)\n\n\t\tnewFlag.deprecatedFlags = append(newFlag.deprecatedFlags, deprecatedFlag)\n\t}\n}\n\n// WithDeprecatedEnvVars returns an `Option` that will create a flag with the given deprecated env vars.\nfunc WithDeprecatedEnvVars(envVars []string, regControlsFn RegisterStrictControlsFunc) Option {\n\treturn func(newFlag *Flag) {\n\t\tdeprecatedFlag := &DeprecatedFlag{\n\t\t\tFlag:                   newFlag.Flag,\n\t\t\tenvVars:                envVars,\n\t\t\tallowedSubcommandScope: true,\n\t\t}\n\t\tdeprecatedFlag.SetStrictControls(newFlag, regControlsFn)\n\n\t\tnewFlag.deprecatedFlags = append(newFlag.deprecatedFlags, deprecatedFlag)\n\t}\n}\n\n// WithDeprecatedFlagNames returns an `Option` that will create a flag with the given deprecated flag names.\nfunc WithDeprecatedFlagNames(flagNames []string, regControlsFn RegisterStrictControlsFunc) Option {\n\treturn func(newFlag *Flag) {\n\t\tdeprecatedFlag := &DeprecatedFlag{\n\t\t\tFlag:                   newFlag.Flag,\n\t\t\tnames:                  flagNames,\n\t\t\tallowedSubcommandScope: true,\n\t\t}\n\t\tdeprecatedFlag.SetStrictControls(newFlag, regControlsFn)\n\n\t\tnewFlag.deprecatedFlags = append(newFlag.deprecatedFlags, deprecatedFlag)\n\t}\n}\n\n// WithDeprecatedFlagName does the same as `WithDeprecatedFlagNames`, but with a single name.\nfunc WithDeprecatedFlagName(flagName string, regControlsFn RegisterStrictControlsFunc) Option {\n\treturn func(newFlag *Flag) {\n\t\tWithDeprecatedFlagNames([]string{flagName}, regControlsFn)(newFlag)\n\t}\n}\n\n// WithEvaluateWrapper returns an Option that wraps the strict control `Evaluate(ctx context.Context)` function.\nfunc WithEvaluateWrapper(fn EvaluateWrapperFunc) Option {\n\treturn func(newFlag *Flag) {\n\t\tnewFlag.evaluateWrapper = fn\n\t}\n}\n"
  },
  {
    "path": "internal/cli/flags/flag_test.go",
    "content": "package flags_test\n\nimport (\n\t\"bytes\"\n\t\"flag\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc mockDestValue[T any](val T) *T {\n\treturn &val\n}\n\nfunc newLogger() (log.Logger, *bytes.Buffer) {\n\tformatter := format.NewFormatter(placeholders.Placeholders{placeholders.Message()})\n\toutput := new(bytes.Buffer)\n\tlogger := log.New(log.WithOutput(output), log.WithLevel(log.InfoLevel), log.WithFormatter(formatter))\n\n\treturn logger, output\n}\n\nfunc TestFlag_TakesValue(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tflag     clihelper.Flag\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\t&clihelper.BoolFlag{Name: \"name\", Destination: mockDestValue(false)},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t&clihelper.BoolFlag{Name: \"name\", Destination: mockDestValue(true)},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t&clihelper.BoolFlag{Name: \"name\", Negative: true, Destination: mockDestValue(true)},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t&clihelper.BoolFlag{Name: \"name\", Negative: true, Destination: mockDestValue(false)},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t&clihelper.GenericFlag[string]{Name: \"name\", Destination: mockDestValue(\"value\")},\n\t\t\ttrue,\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttestFlag := flags.NewFlag(tc.flag)\n\n\t\t\terr := testFlag.Apply(new(flag.FlagSet))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.expected, testFlag.TakesValue())\n\t\t})\n\t}\n}\n\nfunc TestFlag_Evaluate(t *testing.T) {\n\tt.Parallel()\n\n\tmockRegControls := func(flagNameControl, envVarControl strict.Control) bool {\n\t\treturn true\n\t}\n\n\tdeprecatedFlagWarning := func() string {\n\t\treturn controls.NewDeprecatedFlagName(&clihelper.BoolFlag{}, &clihelper.BoolFlag{}, \"\").WarningFmt\n\t}\n\n\tdeprecatedEnvVarWarning := func() string {\n\t\treturn controls.NewDeprecatedEnvVar(&clihelper.BoolFlag{}, &clihelper.BoolFlag{}, \"\").WarningFmt\n\t}\n\n\ttype testCaseFlag struct {\n\t\tflag   *flags.Flag\n\t\targ    string\n\t\tenvVar string\n\t}\n\n\ttestCases := []struct {\n\t\tflags          []testCaseFlag\n\t\texpectedOutput []string\n\t}{\n\n\t\t{\n\t\t\t[]testCaseFlag{\n\t\t\t\t{\n\t\t\t\t\tflags.NewFlag(\n\t\t\t\t\t\t&clihelper.BoolFlag{Name: \"new-flag-name\"},\n\t\t\t\t\t\tflags.WithDeprecatedName(\"old-flag-name\", mockRegControls),\n\t\t\t\t\t),\n\t\t\t\t\t\"old-flag-name\",\n\t\t\t\t\t\"\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tflags.NewFlag(\n\t\t\t\t\t\t&clihelper.BoolFlag{Name: \"new-env-var-name\", EnvVars: []string{\"NEW_ENV_VAR_NAME\"}},\n\t\t\t\t\t\tflags.WithDeprecatedName(\"old-env-var-name\", mockRegControls),\n\t\t\t\t\t),\n\t\t\t\t\t\"\",\n\t\t\t\t\t\"OLD_ENV_VAR_NAME\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\tfmt.Sprintf(deprecatedFlagWarning(), \"old-flag-name\", \"new-flag-name\"),\n\t\t\t\tfmt.Sprintf(deprecatedEnvVarWarning(), \"OLD_ENV_VAR_NAME\", \"NEW_ENV_VAR_NAME=true\"),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tlogger, output := newLogger()\n\t\t\tctx := t.Context()\n\t\t\tctx = log.ContextWithLogger(ctx, logger)\n\n\t\t\tfor _, testFlag := range tc.flags {\n\t\t\t\terr := testFlag.flag.Apply(new(flag.FlagSet))\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tif testFlag.arg != \"\" {\n\t\t\t\t\terr := testFlag.flag.Value().Getter(testFlag.arg).Set(\"1\")\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\n\t\t\t\tif testFlag.envVar != \"\" {\n\t\t\t\t\terr := testFlag.flag.Value().Getter(testFlag.envVar).EnvSet(\"1\")\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\n\t\t\t\terr = testFlag.flag.RunAction(ctx, clihelper.NewAppContext(nil, nil))\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\toutputLines := strings.Split(strings.TrimSpace(output.String()), \"\\n\")\n\t\t\tassert.Equal(t, tc.expectedOutput, outputLines)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/cli/flags/global/flags.go",
    "content": "// Package global provides CLI global flags.\npackage global\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gruntwork-io/go-commons/collections\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/help\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/version\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\t// Logs related flags.\n\n\tLogLevelFlagName        = \"log-level\"\n\tLogDisableFlagName      = \"log-disable\"\n\tShowLogAbsPathsFlagName = \"log-show-abs-paths\"\n\tLogFormatFlagName       = \"log-format\"\n\tLogCustomFormatFlagName = \"log-custom-format\"\n\tNoColorFlagName         = \"no-color\"\n\n\tNonInteractiveFlagName = \"non-interactive\"\n\tWorkingDirFlagName     = \"working-dir\"\n\n\t// Strict Mode related flags.\n\n\tStrictModeFlagName    = \"strict-mode\"\n\tStrictControlFlagName = \"strict-control\"\n\n\t// Experiment Mode related flags/envs.\n\n\tExperimentModeFlagName = \"experiment-mode\"\n\tExperimentFlagName     = \"experiment\"\n\n\t// Tips related flags.\n\n\tNoTipsFlagName = \"no-tips\"\n\tNoTipFlagName  = \"no-tip\"\n\n\t// App flags.\n\n\tHelpFlagName    = \"help\"\n\tVersionFlagName = \"version\"\n\n\t// Telemetry flags.\n\n\tTelemetryTraceExporterFlagName                  = \"telemetry-trace-exporter\"\n\tTelemetryTraceExporterInsecureEndpointFlagName  = \"telemetry-trace-exporter-insecure-endpoint\"\n\tTelemetryTraceExporterHTTPEndpointFlagName      = \"telemetry-trace-exporter-http-endpoint\"\n\tTraceparentFlagName                             = \"traceparent\"\n\tTelemetryMetricExporterFlagName                 = \"telemetry-metric-exporter\"\n\tTelemetryMetricExporterInsecureEndpointFlagName = \"telemetry-metric-exporter-insecure-endpoint\"\n\n\t// Renamed flags.\n\n\tDeprecatedLogLevelFlagName        = \"log-level\"\n\tDeprecatedLogDisableFlagName      = \"log-disable\"\n\tDeprecatedShowLogAbsPathsFlagName = \"log-show-abs-paths\"\n\tDeprecatedLogFormatFlagName       = \"log-format\"\n\tDeprecatedLogCustomFormatFlagName = \"log-custom-format\"\n\tDeprecatedNoColorFlagName         = \"no-color\"\n\tDeprecatedNonInteractiveFlagName  = \"non-interactive\"\n\tDeprecatedTFInputFlagName         = \"tf-input\"\n\tDeprecatedWorkingDirFlagName      = \"working-dir\"\n\tDeprecatedStrictModeFlagName      = \"strict-mode\"\n\tDeprecatedStrictControlFlagName   = \"strict-control\"\n\tDeprecatedExperimentModeFlagName  = \"experiment-mode\"\n\tDeprecatedExperimentFlagName      = \"experiment\"\n\n\t// Deprecated flags.\n\n\tDeprecatedDisableLogFormattingFlagName = \"disable-log-formatting\"\n\tDeprecatedJSONLogFlagName              = \"json-log\"\n\tDeprecatedTfLogJSONFlagName            = \"tf-logs-to-json\"\n)\n\n// NewFlags creates and returns global flags common for all commands.\nfunc NewFlags(l log.Logger, opts *options.TerragruntOptions, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\tterragruntPrefix := prefix.Prepend(flags.TerragruntPrefix)\n\tterragruntPrefixControl := flags.StrictControlsByGlobalFlags(opts.StrictControls)\n\tlegacyLogsControl := flags.StrictControlsByGlobalFlags(opts.StrictControls, controls.LegacyLogs)\n\n\tflags := clihelper.Flags{\n\t\tNewLogLevelFlag(l, opts, prefix),\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        WorkingDirFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(WorkingDirFlagName),\n\t\t\tDestination: &opts.WorkingDir,\n\t\t\tUsage:       \"The path to the directory of Terragrunt configurations. Default is current directory.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(DeprecatedWorkingDirFlagName), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:    LogDisableFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(LogDisableFlagName),\n\t\t\tUsage:   \"Disable logging.\",\n\t\t\tSetter: func(val bool) error {\n\t\t\t\tl.Formatter().SetDisabledOutput(val)\n\n\t\t\t\tif val {\n\t\t\t\t\topts.ForwardTFStdout = true\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(DeprecatedLogDisableFlagName), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        ShowLogAbsPathsFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(ShowLogAbsPathsFlagName),\n\t\t\tDestination: &opts.Writers.LogShowAbsPaths,\n\t\t\tUsage:       \"Show absolute paths in logs.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(DeprecatedShowLogAbsPathsFlagName), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:    NoColorFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(NoColorFlagName),\n\t\t\tUsage:   \"Disable color output.\",\n\t\t\tSetter: func(val bool) error {\n\t\t\t\tl.Formatter().SetDisabledColors(val)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(DeprecatedNoColorFlagName), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:    LogFormatFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(LogFormatFlagName),\n\t\t\tUsage:   \"Set the log format.\",\n\t\t\tSetter:  l.Formatter().SetFormat,\n\t\t\tAction: func(_ context.Context, _ *clihelper.Context, val string) error {\n\t\t\t\tswitch val {\n\t\t\t\tcase format.BareFormatName:\n\t\t\t\t\topts.ForwardTFStdout = true\n\t\t\t\tcase format.JSONFormatName:\n\t\t\t\t\topts.JSONLogFormat = true\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(DeprecatedLogFormatFlagName), terragruntPrefixControl),\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(DeprecatedDisableLogFormattingFlagName), legacyLogsControl),\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(DeprecatedJSONLogFlagName), legacyLogsControl),\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(DeprecatedTfLogJSONFlagName), legacyLogsControl)),\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:    LogCustomFormatFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(LogCustomFormatFlagName),\n\t\t\tUsage:   \"Set the custom log formatting.\",\n\t\t\tSetter:  l.Formatter().SetCustomFormat,\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(DeprecatedLogCustomFormatFlagName), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        NonInteractiveFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(NonInteractiveFlagName),\n\t\t\tDestination: &opts.NonInteractive,\n\t\t\tUsage:       `Assume \"yes\" for all prompts.`,\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(DeprecatedNonInteractiveFlagName), terragruntPrefixControl),\n\t\t\tflags.WithDeprecatedFlag(&clihelper.BoolFlag{\n\t\t\t\tNegative: true,\n\t\t\t\tEnvVars:  flags.Prefix{}.EnvVars(DeprecatedTFInputFlagName),\n\t\t\t}, nil, terragruntPrefixControl)),\n\n\t\t// Experiment Mode flags.\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:    ExperimentModeFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(ExperimentModeFlagName),\n\t\t\tUsage:   \"Enables experiment mode for Terragrunt. For more information, see https://docs.terragrunt.com/reference/experiment-mode .\",\n\t\t\tSetter: func(_ bool) error {\n\t\t\t\topts.Experiments.ExperimentMode()\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(DeprecatedExperimentModeFlagName), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.SliceFlag[string]{\n\t\t\tName:    ExperimentFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(ExperimentFlagName),\n\t\t\tUsage:   \"Enables specific experiments. For a list of available experiments, see https://docs.terragrunt.com/reference/experiment-mode .\",\n\t\t\tSetter:  opts.Experiments.EnableExperiment,\n\t\t\tAction: func(_ context.Context, _ *clihelper.Context, _ []string) error {\n\t\t\t\topts.Experiments.NotifyCompletedExperiments(l)\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(DeprecatedExperimentFlagName), terragruntPrefixControl)),\n\n\t\t// Tips Mode flags.\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:    NoTipsFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(NoTipsFlagName),\n\t\t\tUsage:   \"Disable all tips from being displayed.\",\n\t\t\tSetter: func(v bool) error {\n\t\t\t\tif v {\n\t\t\t\t\topts.Tips.DisableAll()\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}),\n\n\t\tflags.NewFlag(&clihelper.SliceFlag[string]{\n\t\t\tName:    NoTipFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(NoTipFlagName),\n\t\t\tUsage:   \"Disable specific tips from being displayed.\",\n\t\t\tSetter:  opts.Tips.DisableTip,\n\t\t}),\n\n\t\t// Strict Mode flags.\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:    StrictModeFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(StrictModeFlagName),\n\t\t\tUsage:   \"Enables strict mode for Terragrunt. For more information, run 'terragrunt info strict'.\",\n\t\t\tSetter: func(_ bool) error {\n\t\t\t\topts.StrictControls.FilterByStatus(strict.ActiveStatus).Enable()\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tAction: func(_ context.Context, _ *clihelper.Context, _ bool) error {\n\t\t\t\topts.StrictControls.LogEnabled(l)\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(DeprecatedStrictModeFlagName), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.SliceFlag[string]{\n\t\t\tName:    StrictControlFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(StrictControlFlagName),\n\t\t\tUsage:   \"Enables specific strict controls. For a list of available controls, run 'terragrunt info strict'.\",\n\t\t\tSetter: func(val string) error {\n\t\t\t\treturn opts.StrictControls.EnableControl(val)\n\t\t\t},\n\t\t\tAction: func(_ context.Context, _ *clihelper.Context, vals []string) error {\n\t\t\t\topts.StrictControls.LogEnabled(l)\n\t\t\t\topts.StrictControls.LogCompletedControls(l, vals)\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(DeprecatedStrictControlFlagName), terragruntPrefixControl)),\n\t}\n\n\tflags = flags.Add(NewTelemetryFlags(opts, nil)...)\n\tflags = flags.Sort()\n\tflags = flags.Add(NewHelpVersionFlags(l, opts)...)\n\n\treturn flags\n}\n\n// NewTelemetryFlags creates telemetry related flags.\nfunc NewTelemetryFlags(opts *options.TerragruntOptions, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\tterragruntPrefix := prefix.Prepend(flags.TerragruntPrefix)\n\tterragruntPrefixControl := flags.StrictControlsByGlobalFlags(opts.StrictControls)\n\n\treturn clihelper.Flags{\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tEnvVars:     tgPrefix.EnvVars(TelemetryTraceExporterFlagName),\n\t\t\tDestination: &opts.Telemetry.TraceExporter,\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"telemetry-trace-exporter\"), terragruntPrefixControl),\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"telemerty-trace-exporter\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tEnvVars:     tgPrefix.EnvVars(TelemetryTraceExporterInsecureEndpointFlagName),\n\t\t\tDestination: &opts.Telemetry.TraceExporterInsecureEndpoint,\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"telemetry-trace-exporter-insecure-endpoint\"), terragruntPrefixControl),\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"telemerty-trace-exporter-insecure-endpoint\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tEnvVars:     tgPrefix.EnvVars(TelemetryTraceExporterHTTPEndpointFlagName),\n\t\t\tDestination: &opts.Telemetry.TraceExporterHTTPEndpoint,\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"telemetry-trace-exporter-http-endpoint\"), terragruntPrefixControl),\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"telemerty-trace-exporter-http-endpoint\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tEnvVars:     flags.Prefix{}.EnvVars(TraceparentFlagName),\n\t\t\tDestination: &opts.Telemetry.TraceParent,\n\t\t}),\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tEnvVars:     tgPrefix.EnvVars(TelemetryMetricExporterFlagName),\n\t\t\tDestination: &opts.Telemetry.MetricExporter,\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"telemetry-metric-exporter\"), terragruntPrefixControl),\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"telemerty-metric-exporter\"), terragruntPrefixControl)),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tEnvVars:     tgPrefix.EnvVars(TelemetryMetricExporterInsecureEndpointFlagName),\n\t\t\tDestination: &opts.Telemetry.MetricExporterInsecureEndpoint,\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"telemetry-metric-exporter-insecure-endpoint\"), terragruntPrefixControl),\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"telemerty-metric-exporter-insecure-endpoint\"), terragruntPrefixControl)),\n\t}\n}\n\nfunc NewLogLevelFlag(l log.Logger, opts *options.TerragruntOptions, prefix flags.Prefix) *flags.Flag {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\tterragruntPrefix := prefix.Prepend(flags.TerragruntPrefix)\n\tterragruntPrefixControl := flags.StrictControlsByGlobalFlags(opts.StrictControls)\n\n\treturn flags.NewFlag(&clihelper.GenericFlag[string]{\n\t\tName:        LogLevelFlagName,\n\t\tEnvVars:     tgPrefix.EnvVars(LogLevelFlagName),\n\t\tDefaultText: l.Level().String(),\n\t\tSetter:      l.SetLevel,\n\t\tUsage:       fmt.Sprintf(\"Sets the logging level for Terragrunt. Supported levels: %s.\", log.AllLevels),\n\t\tAction: func(_ context.Context, _ *clihelper.Context, val string) error {\n\t\t\t// Before the release of v0.67.0, these levels actually disabled logs, since we do not use these levels for logging.\n\t\t\t// For backward compatibility we simulate the same behavior.\n\t\t\tremovedLevels := []string{\n\t\t\t\t\"panic\",\n\t\t\t\t\"fatal\",\n\t\t\t}\n\n\t\t\tif collections.ListContainsElement(removedLevels, val) {\n\t\t\t\topts.ForwardTFStdout = true\n\n\t\t\t\tl.Formatter().SetDisabledOutput(true)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t}, flags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(DeprecatedLogLevelFlagName), terragruntPrefixControl))\n}\n\nfunc NewHelpVersionFlags(l log.Logger, opts *options.TerragruntOptions) clihelper.Flags {\n\treturn clihelper.Flags{\n\t\t&clihelper.BoolFlag{\n\t\t\tName:    HelpFlagName,  // --help, -help\n\t\t\tAliases: []string{\"h\"}, //  -h\n\t\t\tUsage:   \"Show help.\",\n\t\t\tAction: func(ctx context.Context, cliCtx *clihelper.Context, _ bool) error {\n\t\t\t\treturn help.Action(ctx, cliCtx, l, opts)\n\t\t\t},\n\t\t},\n\t\t&clihelper.BoolFlag{\n\t\t\tName:    VersionFlagName, // --version, -version\n\t\t\tAliases: []string{\"v\"},   //  -v\n\t\t\tUsage:   \"Show terragrunt version.\",\n\t\t\tAction: func(ctx context.Context, cliCtx *clihelper.Context, _ bool) (err error) {\n\t\t\t\treturn version.Action(ctx, cliCtx)\n\t\t\t},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/cli/flags/prefix.go",
    "content": "package flags\n\nimport (\n\t\"strings\"\n)\n\nconst (\n\t// TgPrefix is an environment variable prefix.\n\tTgPrefix = \"TG\"\n\n\t// TerragruntPrefix is an environment variable deprecated prefix.\n\tTerragruntPrefix = \"TERRAGRUNT\"\n)\n\n// Prefix helps to combine strings into flag names or environment variables in a convenient way.\n// Can be passed to subcommands and contain the names of parent commands,\n// thus creating env vars as a chain of \"TG prefix, parent command names, command name, flag name\". For example:\n// `TG_HLC_FMT_FILE`, where `hcl` is the parent command, `fmt` is the command and `file` is a flag. Example of use:\n//\n//\tfunc main () {\n//\t\tParentCommand(Prefix{TgPrefix})\n//\t}\n//\n//\tfunc ParentCommand(prefix Prefix) {\n//\t\tCommand(prefix.Append(\"hcl\"))\n//\t}\n//\n//\tfunc Command(prefix Prefix) {\n//\t\tFlag(prefix.Append(\"fmt\"))\n//\t}\n//\n//\tfunc Flag(prefix Prefix) {\n//\t\tenvName := prefix.EnvVar(\"file\") // TG_HCL_FMT_FILE\n//\t}\ntype Prefix []string\n\n// Prepend adds a value to the beginning of the slice.\nfunc (prefix Prefix) Prepend(val string) Prefix {\n\treturn append([]string{val}, prefix...)\n}\n\n// Append adds a value to the end of the slice.\nfunc (prefix Prefix) Append(val string) Prefix {\n\treturn append(prefix, val)\n}\n\n// EnvVar returns a string that is the concatenation of the slice values with the given `name`,\n// using underscores as separators, replacing dashes with underscores, converting to uppercase.\nfunc (prefix Prefix) EnvVar(name string) string {\n\tif name == \"\" {\n\t\treturn \"\"\n\t}\n\n\tname = strings.Join(append(prefix, name), \"_\")\n\n\treturn strings.ToUpper(strings.ReplaceAll(name, \"-\", \"_\"))\n}\n\n// EnvVars does the same `EnvVar`, except it takes and returns the slice.\nfunc (prefix Prefix) EnvVars(names ...string) []string {\n\tvar envVars = make([]string, len(names))\n\n\tfor i := range names {\n\t\tenvVars[i] = prefix.EnvVar(names[i])\n\t}\n\n\treturn envVars\n}\n\n// FlagName returns a string that is the concatenation of the slice values with the given `name`,\n// using dashes as separators, replacing dashes with underscores, converting to lowercase.\nfunc (prefix Prefix) FlagName(name string) string {\n\tif name == \"\" {\n\t\treturn \"\"\n\t}\n\n\tname = strings.Join(append(prefix, name), \"-\")\n\n\treturn strings.ToLower(strings.ReplaceAll(name, \"_\", \"-\"))\n}\n\n// FlagNames does the same `FlagName`, except it takes and returns the slice.\nfunc (prefix Prefix) FlagNames(names ...string) []string {\n\tvar flagNames = make([]string, len(names))\n\n\tfor i := range names {\n\t\tflagNames[i] = prefix.FlagName(names[i])\n\t}\n\n\treturn flagNames\n}\n"
  },
  {
    "path": "internal/cli/flags/shared/all.go",
    "content": "package shared\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tAllFlagName  = \"all\"\n\tAllFlagAlias = \"a\"\n)\n\n// NewAllFlag creates the --all flag for running commands across all units in a stack.\nfunc NewAllFlag(opts *options.TerragruntOptions, prefix flags.Prefix) *flags.Flag {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\n\treturn flags.NewFlag(&clihelper.BoolFlag{\n\t\tName:        AllFlagName,\n\t\tAliases:     []string{AllFlagAlias},\n\t\tEnvVars:     tgPrefix.EnvVars(AllFlagName),\n\t\tDestination: &opts.RunAll,\n\t\tUsage:       `Run the specified command on the stack of units in the current directory.`,\n\t\tAction: func(_ context.Context, _ *clihelper.Context, _ bool) error {\n\t\t\tif opts.Graph {\n\t\t\t\treturn errors.New(new(AllGraphFlagsError))\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "internal/cli/flags/shared/auth.go",
    "content": "package shared\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tAuthProviderCmdFlagName = \"auth-provider-cmd\"\n)\n\n// NewAuthProviderCmdFlag creates a flag for specifying the auth provider command.\nfunc NewAuthProviderCmdFlag(opts *options.TerragruntOptions, prefix flags.Prefix, commandName string) *flags.Flag {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\tterragruntPrefix := prefix.Prepend(flags.TerragruntPrefix)\n\n\tvar terragruntPrefixControl flags.RegisterStrictControlsFunc\n\tif commandName != \"\" {\n\t\tterragruntPrefixControl = flags.StrictControlsByCommand(opts.StrictControls, commandName)\n\t} else {\n\t\tterragruntPrefixControl = flags.StrictControlsByGlobalFlags(opts.StrictControls)\n\t}\n\n\treturn flags.NewFlag(\n\t\t&clihelper.GenericFlag[string]{\n\t\t\tName:        AuthProviderCmdFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(AuthProviderCmdFlagName),\n\t\t\tDestination: &opts.AuthProviderCmd,\n\t\t\tUsage:       \"Run the provided command and arguments to authenticate Terragrunt dynamically when necessary.\",\n\t\t},\n\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"auth-provider-cmd\"), terragruntPrefixControl),\n\t)\n}\n"
  },
  {
    "path": "internal/cli/flags/shared/backend.go",
    "content": "package shared\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tBackendBootstrapFlagName        = \"backend-bootstrap\"\n\tBackendRequireBootstrapFlagName = \"backend-require-bootstrap\"\n\tDisableBucketUpdateFlagName     = \"disable-bucket-update\"\n)\n\n// NewBackendFlags defines backend-related flags that should be available to both `run` and `backend` commands.\nfunc NewBackendFlags(opts *options.TerragruntOptions, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\tterragruntPrefix := prefix.Prepend(flags.TerragruntPrefix)\n\tterragruntPrefixControl := flags.StrictControlsByGlobalFlags(opts.StrictControls)\n\n\treturn clihelper.Flags{\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        BackendBootstrapFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(BackendBootstrapFlagName),\n\t\t\tDestination: &opts.BackendBootstrap,\n\t\t\tUsage:       \"Automatically bootstrap backend infrastructure before attempting to use it.\",\n\t\t}),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        BackendRequireBootstrapFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(BackendRequireBootstrapFlagName),\n\t\t\tDestination: &opts.FailIfBucketCreationRequired,\n\t\t\tUsage:       \"When this flag is set Terragrunt will fail if the remote state bucket needs to be created.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"fail-on-state-bucket-creation\"), terragruntPrefixControl),\n\t\t),\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        DisableBucketUpdateFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(DisableBucketUpdateFlagName),\n\t\t\tDestination: &opts.DisableBucketUpdate,\n\t\t\tUsage:       \"When this flag is set Terragrunt will not update the remote state bucket.\",\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"disable-bucket-update\"), terragruntPrefixControl),\n\t\t),\n\t}\n}\n"
  },
  {
    "path": "internal/cli/flags/shared/config.go",
    "content": "package shared\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tConfigFlagName = \"config\"\n)\n\n// NewConfigFlag creates a flag for specifying the Terragrunt config file path.\nfunc NewConfigFlag(opts *options.TerragruntOptions, prefix flags.Prefix, commandName string) *flags.Flag {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\tterragruntPrefix := prefix.Prepend(flags.TerragruntPrefix)\n\n\tvar terragruntPrefixControl flags.RegisterStrictControlsFunc\n\tif commandName != \"\" {\n\t\tterragruntPrefixControl = flags.StrictControlsByCommand(opts.StrictControls, commandName)\n\t} else {\n\t\tterragruntPrefixControl = flags.StrictControlsByGlobalFlags(opts.StrictControls)\n\t}\n\n\treturn flags.NewFlag(\n\t\t&clihelper.GenericFlag[string]{\n\t\t\tName:        ConfigFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(ConfigFlagName),\n\t\t\tDestination: &opts.TerragruntConfigPath,\n\t\t\tUsage:       \"The path to the Terragrunt config file. Default is terragrunt.hcl.\",\n\t\t},\n\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"config\"), terragruntPrefixControl),\n\t)\n}\n"
  },
  {
    "path": "internal/cli/flags/shared/doc.go",
    "content": "// Package shared provides flags that are shared by multiple commands.\n//\n// This package is underutilized right now, as some more serious refactoring is needed to make sure all\n// shared flags use this package instead of reusing flags from other commands.\npackage shared\n"
  },
  {
    "path": "internal/cli/flags/shared/download.go",
    "content": "package shared\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tDownloadDirFlagName = \"download-dir\"\n)\n\n// NewDownloadDirFlag creates a flag for specifying the download directory path.\nfunc NewDownloadDirFlag(opts *options.TerragruntOptions, prefix flags.Prefix, commandName string) *flags.Flag {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\tterragruntPrefix := prefix.Prepend(flags.TerragruntPrefix)\n\n\tvar terragruntPrefixControl flags.RegisterStrictControlsFunc\n\tif commandName != \"\" {\n\t\tterragruntPrefixControl = flags.StrictControlsByCommand(opts.StrictControls, commandName)\n\t} else {\n\t\tterragruntPrefixControl = flags.StrictControlsByGlobalFlags(opts.StrictControls)\n\t}\n\n\treturn flags.NewFlag(\n\t\t&clihelper.GenericFlag[string]{\n\t\t\tName:        DownloadDirFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(DownloadDirFlagName),\n\t\t\tDestination: &opts.DownloadDir,\n\t\t\tUsage:       \"The path to download OpenTofu/Terraform modules into. Default is .terragrunt-cache in the working directory.\",\n\t\t},\n\t\tflags.WithDeprecatedEnvVars(\n\t\t\tappend(\n\t\t\t\tterragruntPrefix.EnvVars(\"download\"),\n\t\t\t\tterragruntPrefix.EnvVars(\"download-dir\")...,\n\t\t\t),\n\t\t\tterragruntPrefixControl,\n\t\t),\n\t)\n}\n"
  },
  {
    "path": "internal/cli/flags/shared/errors.go",
    "content": "package shared\n\n// AllGraphFlagsError is returned when both --all and --graph flags are used simultaneously.\ntype AllGraphFlagsError byte\n\nfunc (err *AllGraphFlagsError) Error() string {\n\treturn \"Using the `--all` and `--graph` flags simultaneously is not supported.\"\n}\n"
  },
  {
    "path": "internal/cli/flags/shared/failfast.go",
    "content": "package shared\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tFailFastFlagName = \"fail-fast\"\n)\n\n// NewFailFastFlag creates the --fail-fast flag for stopping execution on the first error.\nfunc NewFailFastFlag(opts *options.TerragruntOptions) *flags.Flag {\n\ttgPrefix := flags.Prefix{flags.TgPrefix}\n\n\treturn flags.NewFlag(&clihelper.BoolFlag{\n\t\tName:        FailFastFlagName,\n\t\tEnvVars:     tgPrefix.EnvVars(FailFastFlagName),\n\t\tDestination: &opts.FailFast,\n\t\tUsage:       \"Fail immediately if any unit fails, rather than continuing to process remaining units.\",\n\t})\n}\n"
  },
  {
    "path": "internal/cli/flags/shared/feature.go",
    "content": "package shared\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tFeatureFlagName = \"feature\"\n)\n\n// NewFeatureFlags defines the feature flag map that should be available to both `run` and `backend` commands.\nfunc NewFeatureFlags(opts *options.TerragruntOptions, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\tterragruntPrefix := prefix.Prepend(flags.TerragruntPrefix)\n\tterragruntPrefixControl := flags.StrictControlsByGlobalFlags(opts.StrictControls)\n\n\treturn clihelper.Flags{\n\t\tflags.NewFlag(&clihelper.MapFlag[string, string]{\n\t\t\tName:    FeatureFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(FeatureFlagName),\n\t\t\tUsage:   \"Set feature flags for the HCL code.\",\n\t\t\t// Use default splitting behavior with comma separators via MapFlag defaults\n\t\t\tAction: func(_ context.Context, _ *clihelper.Context, value map[string]string) error {\n\t\t\t\tfor key, val := range value {\n\t\t\t\t\topts.FeatureFlags.Store(key, val)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"feature\"), terragruntPrefixControl),\n\t\t),\n\t}\n}\n"
  },
  {
    "path": "internal/cli/flags/shared/filter.go",
    "content": "package shared\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tFilterFlagName             = \"filter\"\n\tFilterAffectedFlagName     = \"filter-affected\"\n\tFilterAllowDestroyFlagName = \"filter-allow-destroy\"\n\tFilterFileFlagName         = \"filters-file\"\n\tNoFilterFileFlagName       = \"no-filters-file\"\n)\n\n// NewFilterFlags creates flags for specifying filter queries.\nfunc NewFilterFlags(l log.Logger, opts *options.TerragruntOptions) clihelper.Flags {\n\ttgPrefix := flags.Prefix{flags.TgPrefix}\n\n\treturn clihelper.Flags{\n\t\tflags.NewFlag(\n\t\t\t&clihelper.SliceFlag[string]{\n\t\t\t\tName:    FilterFlagName,\n\t\t\t\tEnvVars: tgPrefix.EnvVars(FilterFlagName),\n\t\t\t\tUsage:   \"Filter components using filter syntax. Can be specified multiple times for union (OR) semantics.\",\n\t\t\t\tAction: func(_ context.Context, _ *clihelper.Context, val []string) error {\n\t\t\t\t\tif len(val) == 0 {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\tparsed, err := filter.ParseFilterQueries(l, val)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\topts.Filters = append(opts.Filters, parsed...)\n\t\t\t\t\topts.RunAll = true\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tflags.NewFlag(\n\t\t\t&clihelper.BoolFlag{\n\t\t\t\tName:    FilterAffectedFlagName,\n\t\t\t\tEnvVars: tgPrefix.EnvVars(FilterAffectedFlagName),\n\t\t\t\tUsage:   \"Filter components affected by changes between main and HEAD. Equivalent to --filter=[main...HEAD].\",\n\t\t\t\tAction: func(ctx context.Context, _ *clihelper.Context, val bool) error {\n\t\t\t\t\tif !val {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\t// Get working directory\n\t\t\t\t\tworkDir := opts.WorkingDir\n\t\t\t\t\tif workDir == \"\" {\n\t\t\t\t\t\tworkDir = opts.RootWorkingDir\n\t\t\t\t\t}\n\n\t\t\t\t\tif workDir == \"\" {\n\t\t\t\t\t\t// Fallback to current directory if neither is set\n\t\t\t\t\t\tworkDir = \".\"\n\t\t\t\t\t}\n\n\t\t\t\t\t// Check for uncommitted changes\n\t\t\t\t\tgitRunner, err := git.NewGitRunner()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn clihelper.NewExitError(err, clihelper.ExitCodeGeneralError)\n\t\t\t\t\t}\n\n\t\t\t\t\tgitRunner = gitRunner.WithWorkDir(workDir)\n\n\t\t\t\t\tif gitRunner.HasUncommittedChanges(ctx) {\n\t\t\t\t\t\tl.Warnf(\"Warning: You have uncommitted changes. The --filter-affected flag may not include all your local modifications.\")\n\t\t\t\t\t}\n\n\t\t\t\t\tdefaultBranch := gitRunner.GetDefaultBranch(ctx, l)\n\n\t\t\t\t\tgitExpr := filter.NewGitExpression(defaultBranch, \"HEAD\")\n\t\t\t\t\topts.Filters = append(opts.Filters, filter.NewFilter(gitExpr, gitExpr.String()))\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tflags.NewFlag(\n\t\t\t&clihelper.BoolFlag{\n\t\t\t\tName:        FilterAllowDestroyFlagName,\n\t\t\t\tEnvVars:     tgPrefix.EnvVars(FilterAllowDestroyFlagName),\n\t\t\t\tDestination: &opts.FilterAllowDestroy,\n\t\t\t\tUsage:       \"Allow destroy runs when using Git-based filters.\",\n\t\t\t},\n\t\t),\n\t\tflags.NewFlag(\n\t\t\t&clihelper.GenericFlag[string]{\n\t\t\t\tName:        FilterFileFlagName,\n\t\t\t\tEnvVars:     tgPrefix.EnvVars(FilterFileFlagName),\n\t\t\t\tDestination: &opts.FiltersFile,\n\t\t\t\tUsage:       \"Path to a file containing filter queries, one per line. Default is .terragrunt-filters.\",\n\t\t\t},\n\t\t),\n\t\tflags.NewFlag(\n\t\t\t&clihelper.BoolFlag{\n\t\t\t\tName:        NoFilterFileFlagName,\n\t\t\t\tEnvVars:     tgPrefix.EnvVars(NoFilterFileFlagName),\n\t\t\t\tDestination: &opts.NoFiltersFile,\n\t\t\t\tUsage:       \"Disable automatic reading of .terragrunt-filters file.\",\n\t\t\t},\n\t\t),\n\t}\n}\n"
  },
  {
    "path": "internal/cli/flags/shared/graph.go",
    "content": "package shared\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tGraphFlagName = \"graph\"\n)\n\n// NewGraphFlag creates the --graph flag for running commands following the DAG.\nfunc NewGraphFlag(opts *options.TerragruntOptions, prefix flags.Prefix) *flags.Flag {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\n\treturn flags.NewFlag(&clihelper.BoolFlag{\n\t\tName:        GraphFlagName,\n\t\tEnvVars:     tgPrefix.EnvVars(GraphFlagName),\n\t\tDestination: &opts.Graph,\n\t\tUsage:       \"Run the specified OpenTofu/Terraform command following the Directed Acyclic Graph (DAG) of dependencies.\",\n\t\tAction: func(_ context.Context, _ *clihelper.Context, _ bool) error {\n\t\t\tif opts.RunAll {\n\t\t\t\treturn errors.New(new(AllGraphFlagsError))\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "internal/cli/flags/shared/iamassumerole.go",
    "content": "package shared\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tIAMAssumeRoleFlagName                 = \"iam-assume-role\"\n\tIAMAssumeRoleDurationFlagName         = \"iam-assume-role-duration\"\n\tIAMAssumeRoleSessionNameFlagName      = \"iam-assume-role-session-name\"\n\tIAMAssumeRoleWebIdentityTokenFlagName = \"iam-assume-role-web-identity-token\"\n)\n\n// NewIAMAssumeRoleFlags creates flags for IAM assume role configuration.\nfunc NewIAMAssumeRoleFlags(opts *options.TerragruntOptions, prefix flags.Prefix, commandName string) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\tterragruntPrefix := prefix.Prepend(flags.TerragruntPrefix)\n\n\tvar terragruntPrefixControl flags.RegisterStrictControlsFunc\n\tif commandName != \"\" {\n\t\tterragruntPrefixControl = flags.StrictControlsByCommand(opts.StrictControls, commandName)\n\t} else {\n\t\tterragruntPrefixControl = flags.StrictControlsByGlobalFlags(opts.StrictControls)\n\t}\n\n\treturn clihelper.Flags{\n\t\tflags.NewFlag(\n\t\t\t&clihelper.GenericFlag[string]{\n\t\t\t\tName:        IAMAssumeRoleFlagName,\n\t\t\t\tEnvVars:     tgPrefix.EnvVars(IAMAssumeRoleFlagName),\n\t\t\t\tDestination: &opts.IAMRoleOptions.RoleARN,\n\t\t\t\tUsage:       \"Assume the specified IAM role before executing OpenTofu/Terraform.\",\n\t\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"iam-role\"), terragruntPrefixControl),\n\t\t),\n\n\t\tflags.NewFlag(\n\t\t\t&clihelper.GenericFlag[int64]{\n\t\t\t\tName:        IAMAssumeRoleDurationFlagName,\n\t\t\t\tEnvVars:     tgPrefix.EnvVars(IAMAssumeRoleDurationFlagName),\n\t\t\t\tDestination: &opts.IAMRoleOptions.AssumeRoleDuration,\n\t\t\t\tUsage:       \"Session duration for IAM Assume Role session.\",\n\t\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"iam-assume-role-duration\"), terragruntPrefixControl),\n\t\t),\n\n\t\tflags.NewFlag(\n\t\t\t&clihelper.GenericFlag[string]{\n\t\t\t\tName:        IAMAssumeRoleSessionNameFlagName,\n\t\t\t\tEnvVars:     tgPrefix.EnvVars(IAMAssumeRoleSessionNameFlagName),\n\t\t\t\tDestination: &opts.IAMRoleOptions.AssumeRoleSessionName,\n\t\t\t\tUsage:       \"Name for the IAM Assumed Role session.\",\n\t\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"iam-assume-role-session-name\"), terragruntPrefixControl),\n\t\t),\n\n\t\tflags.NewFlag(\n\t\t\t&clihelper.GenericFlag[string]{\n\t\t\t\tName:        IAMAssumeRoleWebIdentityTokenFlagName,\n\t\t\t\tEnvVars:     tgPrefix.EnvVars(IAMAssumeRoleWebIdentityTokenFlagName),\n\t\t\t\tDestination: &opts.IAMRoleOptions.WebIdentityToken,\n\t\t\t\tUsage:       \"For AssumeRoleWithWebIdentity, the WebIdentity token.\",\n\t\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(\n\t\t\t\tappend(\n\t\t\t\t\tterragruntPrefix.EnvVars(\"iam-web-identity-token\"),\n\t\t\t\t\tterragruntPrefix.EnvVars(\"iam-assume-role-web-identity-token\")...,\n\t\t\t\t),\n\t\t\t\tterragruntPrefixControl,\n\t\t\t),\n\t\t),\n\t}\n}\n"
  },
  {
    "path": "internal/cli/flags/shared/inputsdebug.go",
    "content": "package shared\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tInputsDebugFlagName = \"inputs-debug\"\n)\n\n// NewInputsDebugFlag creates a flag for enabling inputs debug output.\nfunc NewInputsDebugFlag(opts *options.TerragruntOptions, prefix flags.Prefix, commandName string) *flags.Flag {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\tterragruntPrefix := prefix.Prepend(flags.TerragruntPrefix)\n\n\tvar terragruntPrefixControl flags.RegisterStrictControlsFunc\n\tif commandName != \"\" {\n\t\tterragruntPrefixControl = flags.StrictControlsByCommand(opts.StrictControls, commandName)\n\t} else {\n\t\tterragruntPrefixControl = flags.StrictControlsByGlobalFlags(opts.StrictControls)\n\t}\n\n\treturn flags.NewFlag(\n\t\t&clihelper.BoolFlag{\n\t\t\tName:        InputsDebugFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(InputsDebugFlagName),\n\t\t\tDestination: &opts.Debug,\n\t\t\tUsage:       \"Write debug.tfvars to working folder to help root-cause issues.\",\n\t\t},\n\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"debug\"), terragruntPrefixControl),\n\t)\n}\n"
  },
  {
    "path": "internal/cli/flags/shared/parallelism.go",
    "content": "package shared\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tParallelismFlagName = \"parallelism\"\n)\n\n// NewParallelismFlag creates a flag for specifying parallelism level.\nfunc NewParallelismFlag(opts *options.TerragruntOptions) *flags.Flag {\n\ttgPrefix := flags.Prefix{flags.TgPrefix}\n\tterragruntPrefix := flags.Prefix{flags.TerragruntPrefix}\n\tterragruntPrefixControl := flags.StrictControlsByGlobalFlags(opts.StrictControls)\n\n\treturn flags.NewFlag(\n\t\t&clihelper.GenericFlag[int]{\n\t\t\tName:        ParallelismFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(ParallelismFlagName),\n\t\t\tDestination: &opts.Parallelism,\n\t\t\tUsage:       \"Parallelism for --all commands.\",\n\t\t},\n\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"parallelism\"), terragruntPrefixControl),\n\t)\n}\n"
  },
  {
    "path": "internal/cli/flags/shared/queue.go",
    "content": "package shared\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tQueueIgnoreErrorsFlagName        = \"queue-ignore-errors\"\n\tQueueIgnoreDAGOrderFlagName      = \"queue-ignore-dag-order\"\n\tQueueExcludeExternalFlagName     = \"queue-exclude-external\"\n\tQueueExcludeDirFlagName          = \"queue-exclude-dir\"\n\tQueueExcludesFileFlagName        = \"queue-excludes-file\"\n\tQueueIncludeDirFlagName          = \"queue-include-dir\"\n\tQueueIncludeExternalFlagName     = \"queue-include-external\"\n\tQueueStrictIncludeFlagName       = \"queue-strict-include\"\n\tQueueIncludeUnitsReadingFlagName = \"queue-include-units-reading\"\n)\n\n// NewQueueFlags creates the flags used for queue control\nfunc NewQueueFlags(opts *options.TerragruntOptions, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\tterragruntPrefix := flags.Prefix{flags.TerragruntPrefix}\n\tterragruntPrefixControl := flags.StrictControlsByGlobalFlags(opts.StrictControls)\n\n\treturn clihelper.Flags{\n\t\tflags.NewFlag(\n\t\t\t&clihelper.BoolFlag{\n\t\t\t\tName:        QueueIgnoreErrorsFlagName,\n\t\t\t\tEnvVars:     tgPrefix.EnvVars(QueueIgnoreErrorsFlagName),\n\t\t\t\tDestination: &opts.IgnoreDependencyErrors,\n\t\t\t\tUsage:       \"Continue processing Units even if a dependency fails.\",\n\t\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"ignore-dependency-errors\"), terragruntPrefixControl),\n\t\t),\n\n\t\tflags.NewFlag(\n\t\t\t&clihelper.BoolFlag{\n\t\t\t\tName:        QueueIgnoreDAGOrderFlagName,\n\t\t\t\tEnvVars:     tgPrefix.EnvVars(QueueIgnoreDAGOrderFlagName),\n\t\t\t\tDestination: &opts.IgnoreDependencyOrder,\n\t\t\t\tUsage:       \"Ignore DAG order for --all commands.\",\n\t\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"ignore-dependency-order\"), terragruntPrefixControl),\n\t\t),\n\n\t\tflags.NewFlag(\n\t\t\t&clihelper.BoolFlag{\n\t\t\t\tName:    QueueExcludeExternalFlagName,\n\t\t\t\tEnvVars: tgPrefix.EnvVars(QueueExcludeExternalFlagName),\n\t\t\t\tUsage:   \"Ignore external dependencies for --all commands.\",\n\t\t\t\tHidden:  true,\n\t\t\t\tAction: func(ctx context.Context, _ *clihelper.Context, value bool) error {\n\t\t\t\t\tif value {\n\t\t\t\t\t\treturn opts.StrictControls.FilterByNames(controls.QueueExcludeExternal).Evaluate(ctx)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"ignore-external-dependencies\"), terragruntPrefixControl),\n\t\t),\n\n\t\tflags.NewFlag(\n\t\t\t&clihelper.BoolFlag{\n\t\t\t\tName:    QueueIncludeExternalFlagName,\n\t\t\t\tEnvVars: tgPrefix.EnvVars(QueueIncludeExternalFlagName),\n\t\t\t\tUsage:   \"Include external dependencies for --all commands.\",\n\t\t\t\tHidden:  true,\n\t\t\t\tAction: func(_ context.Context, _ *clihelper.Context, value bool) error {\n\t\t\t\t\tif !value {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\tpathExpr, err := filter.NewPathFilter(\"./**\")\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tgraphExpr := filter.NewGraphExpression(pathExpr).WithDependencies()\n\t\t\t\t\topts.Filters = append(opts.Filters, filter.NewFilter(graphExpr, graphExpr.String()))\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"include-external-dependencies\"), terragruntPrefixControl),\n\t\t),\n\n\t\tflags.NewFlag(\n\t\t\t&clihelper.GenericFlag[string]{\n\t\t\t\tName:        QueueExcludesFileFlagName,\n\t\t\t\tEnvVars:     tgPrefix.EnvVars(QueueExcludesFileFlagName),\n\t\t\t\tDestination: &opts.ExcludesFile,\n\t\t\t\tHidden:      true,\n\t\t\t\tUsage:       \"Path to a file with a list of directories that need to be excluded when running *-all commands.\",\n\t\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"excludes-file\"), terragruntPrefixControl),\n\t\t),\n\n\t\tflags.NewFlag(\n\t\t\t&clihelper.SliceFlag[string]{\n\t\t\t\tName:    QueueExcludeDirFlagName,\n\t\t\t\tEnvVars: tgPrefix.EnvVars(QueueExcludeDirFlagName),\n\t\t\t\tHidden:  true,\n\t\t\t\tUsage:   \"Unix-style glob of directories to exclude from the queue of Units to run.\",\n\t\t\t\tAction: func(_ context.Context, _ *clihelper.Context, value []string) error {\n\t\t\t\t\tif len(value) == 0 {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\tfor _, v := range value {\n\t\t\t\t\t\tpathExpr, err := filter.NewPathFilter(v)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tprefixExpr := filter.NewPrefixExpression(\"!\", pathExpr)\n\t\t\t\t\t\topts.Filters = append(opts.Filters, filter.NewFilter(prefixExpr, prefixExpr.String()))\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"exclude-dir\"), terragruntPrefixControl),\n\t\t),\n\n\t\tflags.NewFlag(\n\t\t\t&clihelper.SliceFlag[string]{\n\t\t\t\tName:    QueueIncludeDirFlagName,\n\t\t\t\tEnvVars: tgPrefix.EnvVars(QueueIncludeDirFlagName),\n\t\t\t\tHidden:  true,\n\t\t\t\tUsage:   \"Unix-style glob of directories to include from the queue of Units to run.\",\n\t\t\t\tAction: func(_ context.Context, _ *clihelper.Context, value []string) error {\n\t\t\t\t\tif len(value) == 0 {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\tfor _, v := range value {\n\t\t\t\t\t\tpathExpr, err := filter.NewPathFilter(v)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\topts.Filters = append(opts.Filters, filter.NewFilter(pathExpr, pathExpr.String()))\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"include-dir\"), terragruntPrefixControl),\n\t\t),\n\n\t\tflags.NewFlag(\n\t\t\t&clihelper.BoolFlag{\n\t\t\t\tName:    QueueStrictIncludeFlagName,\n\t\t\t\tEnvVars: tgPrefix.EnvVars(QueueStrictIncludeFlagName),\n\t\t\t\tUsage:   \"If flag is set, only modules under the directories passed in with '--queue-include-dir' will be included.\",\n\t\t\t\tHidden:  true,\n\t\t\t\tAction: func(ctx context.Context, _ *clihelper.Context, value bool) error {\n\t\t\t\t\tif value {\n\t\t\t\t\t\treturn opts.StrictControls.FilterByNames(controls.QueueStrictInclude).Evaluate(ctx)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"strict-include\"), terragruntPrefixControl),\n\t\t),\n\n\t\tflags.NewFlag(\n\t\t\t&clihelper.SliceFlag[string]{\n\t\t\t\tName:    QueueIncludeUnitsReadingFlagName,\n\t\t\t\tEnvVars: tgPrefix.EnvVars(QueueIncludeUnitsReadingFlagName),\n\t\t\t\tUsage:   \"If flag is set, 'run --all' will only run the command against units that read the specified file via a Terragrunt HCL function or include.\",\n\t\t\t\tHidden:  true,\n\t\t\t\tAction: func(_ context.Context, _ *clihelper.Context, value []string) error {\n\t\t\t\t\tif len(value) == 0 {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\tfor _, v := range value {\n\t\t\t\t\t\tattrExpr, err := filter.NewAttributeExpression(filter.AttributeReading, v)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\topts.Filters = append(opts.Filters, filter.NewFilter(attrExpr, attrExpr.String()))\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t},\n\t\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"queue-include-units-reading\"), terragruntPrefixControl),\n\t\t),\n\t}\n}\n"
  },
  {
    "path": "internal/cli/flags/shared/scaffold.go",
    "content": "package shared\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tRootFileNameFlagName  = \"root-file-name\"\n\tNoIncludeRootFlagName = \"no-include-root\"\n\tNoShellFlagName       = \"no-shell\"\n\tNoHooksFlagName       = \"no-hooks\"\n)\n\n// NewScaffoldingFlags creates the flags shared between catalog and scaffold commands.\nfunc NewScaffoldingFlags(opts *options.TerragruntOptions, prefix flags.Prefix) clihelper.Flags {\n\ttgPrefix := prefix.Prepend(flags.TgPrefix)\n\n\treturn clihelper.Flags{\n\t\tflags.NewFlag(&clihelper.GenericFlag[string]{\n\t\t\tName:        RootFileNameFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(RootFileNameFlagName),\n\t\t\tDestination: &opts.ScaffoldRootFileName,\n\t\t\tUsage:       \"Name of the root Terragrunt configuration file, if used.\",\n\t\t\tAction: func(ctx context.Context, _ *clihelper.Context, value string) error {\n\t\t\t\tif value == \"\" {\n\t\t\t\t\treturn clihelper.NewExitError(\"root-file-name flag cannot be empty\", clihelper.ExitCodeGeneralError)\n\t\t\t\t}\n\n\t\t\t\tif value != opts.TerragruntConfigPath {\n\t\t\t\t\topts.ScaffoldRootFileName = value\n\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tif err := opts.StrictControls.FilterByNames(\"RootTerragruntHCL\").Evaluate(ctx); err != nil {\n\t\t\t\t\treturn clihelper.NewExitError(err, clihelper.ExitCodeGeneralError)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t}),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        NoIncludeRootFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(NoIncludeRootFlagName),\n\t\t\tDestination: &opts.ScaffoldNoIncludeRoot,\n\t\t\tUsage:       \"Do not include root unit in scaffolding done by catalog.\",\n\t\t}),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        NoShellFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(NoShellFlagName),\n\t\t\tDestination: &opts.NoShell,\n\t\t\tUsage:       \"Disable shell commands when using boilerplate templates.\",\n\t\t}),\n\n\t\tflags.NewFlag(&clihelper.BoolFlag{\n\t\t\tName:        NoHooksFlagName,\n\t\t\tEnvVars:     tgPrefix.EnvVars(NoHooksFlagName),\n\t\t\tDestination: &opts.NoHooks,\n\t\t\tUsage:       \"Disable hooks when using boilerplate templates.\",\n\t\t}),\n\t}\n}\n"
  },
  {
    "path": "internal/cli/flags/shared/tfpath.go",
    "content": "package shared\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nconst (\n\tTFPathFlagName = \"tf-path\"\n)\n\n// NewTFPathFlag creates a flag for specifying the OpenTofu/Terraform binary path.\nfunc NewTFPathFlag(opts *options.TerragruntOptions) *flags.Flag {\n\ttgPrefix := flags.Prefix{flags.TgPrefix}\n\tterragruntPrefix := flags.Prefix{flags.TerragruntPrefix}\n\tterragruntPrefixControl := flags.StrictControlsByGlobalFlags(opts.StrictControls)\n\n\treturn flags.NewFlag(\n\t\t&clihelper.GenericFlag[string]{\n\t\t\tName:    TFPathFlagName,\n\t\t\tEnvVars: tgPrefix.EnvVars(TFPathFlagName),\n\t\t\tUsage:   \"Path to the OpenTofu/Terraform binary. Default is tofu (on PATH).\",\n\t\t\tSetter: func(value string) error {\n\t\t\t\topts.TFPath = value\n\t\t\t\topts.TFPathExplicitlySet = true\n\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\tflags.WithDeprecatedEnvVars(terragruntPrefix.EnvVars(\"tfpath\"), terragruntPrefixControl),\n\t)\n}\n"
  },
  {
    "path": "internal/cli/help.go",
    "content": "package cli\n\n// AppHelpTemplate is the main CLI help template.\nconst AppHelpTemplate = `Usage: {{ if .App.UsageText }}{{ wrap .App.UsageText 3 }}{{ else }}{{ .App.HelpName }} [global options] <command> [options]{{ end }}{{ $description := .App.Usage }}{{ if .App.Description }}{{ $description = .App.Description }}{{ end }}{{ if $description }}\n\n   {{ wrap $description 3 }}{{ end }}{{ $commands := .App.VisibleCommands }}{{ if $commands }}{{ $cv := offsetCommands $commands 5 }}\n{{ $categories := $commands.GetCategories.Sort }}{{ range $index, $category := $categories }}{{ $categoryCommands := $commands.FilterByCategory $category }}{{ if $index }}\n{{ end }}\n{{ $category.Name }}:{{ range $categoryCommands }}\n   {{ $s := .HelpName }}{{ $s }}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp \"\"}} {{ wrap .Usage $cv }}{{ end }}{{ end }}{{ end }}{{ if .App.VisibleFlags }}\n\nGlobal Options:\n   {{ range $index, $option := .App.VisibleFlags }}{{ if $index }}\n   {{ end }}{{ wrap $option.String 6 }}{{ end }}{{ end }}{{ if not .App.HideVersion }}\n\nVersion: {{ .App.Version }}{{ end }}{{ if len .App.Authors }}\n\nAuthor: {{ range .App.Authors }}{{ . }}{{ end }} {{ end }}\n`\n\n// CommandHelpTemplate is the command CLI help template.\nconst CommandHelpTemplate = `Usage: {{ if .Command.UsageText }}{{ wrap .Command.UsageText 3 }}{{ else }}{{ range $index, $parent := parentCommands . }}{{ $parent.HelpName }} {{ end }}{{ .Command.HelpName }}{{ if .Command.VisibleSubcommands }} <command>{{ end }}{{ if .Command.VisibleFlags }} [options]{{ end }}{{ end }}{{ $description := .Command.Usage }}{{ if .Command.Description }}{{ $description = .Command.Description }}{{ end }}{{ if $description }}\n\n   {{ wrap $description 3 }}{{ end }}{{ if .Command.Examples }}\n\nExamples:\n   {{ $s := join .Command.Examples \"\\n\\n\" }}{{ wrap $s 3 }}{{ end }}{{ if .Command.VisibleSubcommands }}\n\nCommands:{{ $cv := offsetCommands .Command.VisibleSubcommands 5 }}{{ range .Command.VisibleSubcommands }}\n   {{ $s := .HelpName }}{{ $s }}{{ $sp := subtract $cv (offset $s 3) }}{{ indent $sp \"\"}} {{ wrap .Usage $cv }}{{ end }}{{ end }}{{ if .Command.VisibleFlags }}\n\nOptions:\n   {{ range $index, $option := .Command.VisibleFlags.Sort }}{{ if $index }}\n   {{ end }}{{ wrap $option.String 6 }}{{ end }}{{ end }}{{ if .App.VisibleFlags }}\n\nGlobal Options:\n   {{ range $index, $option := .App.VisibleFlags }}{{ if $index }}\n   {{ end }}{{ wrap $option.String 6 }}{{ end }}{{ end }}\n\n`\n\nconst AppVersionTemplate = `{{ .App.Name }} version {{ .App.Version }}\n`\n"
  },
  {
    "path": "internal/cli/help_test.go",
    "content": "package cli_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCommandHelpTemplate(t *testing.T) {\n\tt.Parallel()\n\n\t// Set environment variable format based on OS\n\tenvVarChar := \"$\"\n\tcloseEnvVarChar := \"\"\n\n\tif runtime.GOOS == \"windows\" {\n\t\tenvVarChar = \"%\"\n\t\tcloseEnvVarChar = \"%\"\n\t}\n\n\ttgPrefix := flags.Prefix{flags.TgPrefix}\n\n\tapp := clihelper.NewApp()\n\tapp.Flags = clihelper.Flags{\n\t\t&clihelper.GenericFlag[string]{\n\t\t\tName:    \"working-dir\",\n\t\t\tEnvVars: tgPrefix.EnvVars(\"working-dir\"),\n\t\t\tUsage:   \"The path to the directory of Terragrunt configurations. Default is current directory.\",\n\t\t},\n\t\t&clihelper.BoolFlag{\n\t\t\tName:    \"log-disable\",\n\t\t\tEnvVars: tgPrefix.EnvVars(\"log-disable\"),\n\t\t\tUsage:   \"Disable logging.\",\n\t\t},\n\t}.Sort()\n\n\tcmd := &clihelper.Command{\n\t\tName:        \"run\",\n\t\tUsage:       \"Run an OpenTofu/Terraform command.\",\n\t\tUsageText:   \"terragrunt run [options] -- <tofu/terraform command>\",\n\t\tDescription: \"Run a command, passing arguments to an orchestrated tofu/terraform binary.\\n\\nThis is the explicit, and most flexible form of running an IaC command with Terragrunt. Shortcuts can be found in \\\"terragrunt --help\\\" for common use-cases.\",\n\t\tExamples: []string{\n\t\t\t\"# Run a plan\\nterragrunt run -- plan\\n# Shortcut:\\n# terragrunt plan\",\n\t\t\t\"# Run output with -json flag\\nterragrunt run -- output -json\\n# Shortcut:\\n# terragrunt output -json\",\n\t\t\t\"# Run a plan against a Stack of configurations in the current directory\\nterragrunt run --all -- plan\",\n\t\t},\n\t\tSubcommands: clihelper.Commands{\n\t\t\t&clihelper.Command{\n\t\t\t\tName:  \"fmt\",\n\t\t\t\tUsage: \"Recursively find hcl files and rewrite them into a canonical format.\",\n\t\t\t},\n\t\t\t&clihelper.Command{\n\t\t\t\tName:  \"validate\",\n\t\t\t\tUsage: \"Find all hcl files from the config stack and validate them.\",\n\t\t\t},\n\t\t},\n\t\tFlags: clihelper.Flags{\n\t\t\t&clihelper.BoolFlag{\n\t\t\t\tName:    \"all\",\n\t\t\t\tAliases: []string{\"a\"},\n\t\t\t\tEnvVars: tgPrefix.EnvVars(\"all\"),\n\t\t\t\tUsage:   `Run the specified OpenTofu/Terraform command on the \"Stack\" of Units in the current directory.`,\n\t\t\t},\n\t\t\t&clihelper.BoolFlag{\n\t\t\t\tName:    \"graph\",\n\t\t\t\tEnvVars: tgPrefix.EnvVars(\"graph\"),\n\t\t\t\tUsage:   \"Run the specified OpenTofu/Terraform command following the Directed Acyclic Graph (DAG) of dependencies.\",\n\t\t\t},\n\t\t},\n\t}\n\n\tvar out bytes.Buffer\n\n\tapp.Writer = &out\n\n\tcliCtx := clihelper.NewAppContext(app, nil).NewCommandContext(cmd, nil)\n\trequire.Error(t, clihelper.ShowCommandHelp(t.Context(), cliCtx))\n\n\texpectedOutput := fmt.Sprintf(`Usage: terragrunt run [options] -- <tofu/terraform command>\n\n   Run a command, passing arguments to an orchestrated tofu/terraform binary.\n\n   This is the explicit, and most flexible form of running an IaC command with Terragrunt. Shortcuts can be found in \"terragrunt --help\" for common use-cases.\n\nExamples:\n   # Run a plan\n   terragrunt run -- plan\n   # Shortcut:\n   # terragrunt plan\n\n   # Run output with -json flag\n   terragrunt run -- output -json\n   # Shortcut:\n   # terragrunt output -json\n\n   # Run a plan against a Stack of configurations in the current directory\n   terragrunt run --all -- plan\n\nCommands:\n   fmt        Recursively find hcl files and rewrite them into a canonical format.\n   validate   Find all hcl files from the config stack and validate them.\n\nOptions:\n   --all, -a  Run the specified OpenTofu/Terraform command on the \"Stack\" of Units in the current directory. [%sTG_ALL%s]\n   --graph    Run the specified OpenTofu/Terraform command following the Directed Acyclic Graph (DAG) of dependencies. [%sTG_GRAPH%s]\n\nGlobal Options:\n   --log-disable        Disable logging. [%sTG_LOG_DISABLE%s]\n   --working-dir value  The path to the directory of Terragrunt configurations. Default is current directory. [%sTG_WORKING_DIR%s]\n\n`, envVarChar, closeEnvVarChar, envVarChar, closeEnvVarChar, envVarChar, closeEnvVarChar, envVarChar, closeEnvVarChar)\n\n\tassert.Equal(t, expectedOutput, out.String())\n}\n"
  },
  {
    "path": "internal/clihelper/app.go",
    "content": "// Package clihelper provides the CLI framework for Terragrunt.\npackage clihelper\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// App is a wrapper for `urfave`'s `cli.App` struct. It should be created with the cli.NewApp() function.\n// The main purpose of this wrapper is to parse commands and flags in the way we need, namely,\n// if during parsing we find undefined commands or flags, instead of returning an error, we consider them as arguments,\n// regardless of their position among the others registered commands and flags.\n//\n// For example, CLI command:\n// `terragrunt run --all apply --auto-approve --non-interactive`\n// The `App` will runs the registered command `run --all`, define the registered flags `--log-level`,\n// `--non-interactive`, and define args `apply --auto-approve` which can be obtained from the App context,\n// ctx.Args().Slice()\ntype App struct {\n\t// AutocompleteInstaller supports autocompletion via the github.com/posener/complete\n\t// library. This library supports bash, zsh and fish. To add support\n\t// for other shells, please see that library.\n\tAutocompleteInstaller AutocompleteInstaller\n\n\t// FlagErrHandler processes any error encountered while parsing flags.\n\tFlagErrHandler FlagErrHandlerFunc\n\n\t// ExitErrHandler processes any error encountered while running an App before\n\t// it is returned to the caller. If no function is provided, HandleExitCoder\n\t// is used as the default behavior.\n\tExitErrHandler ExitErrHandlerFunc\n\n\t*cli.App\n\n\t// Before is an action to execute before any subcommands are run, but after the context is ready.\n\tBefore ActionFunc\n\n\t// After is an action to execute after\n\t// any subcommands are run, but after the subcommand has finished.\n\tAfter ActionFunc\n\n\t// Complete is the function to call when checking for command completions.\n\tComplete CompleteFunc\n\n\t// Action is the action to execute when no subcommands are specified.\n\tAction ActionFunc\n\n\t// OsExiter is the function used when the app exits. If not set defaults to os.Exit.\n\tOsExiter func(code int)\n\n\t// Author is the author of the app.\n\tAuthor string\n\n\t// CustomAppVersionTemplate is a text template for app version topic.\n\tCustomAppVersionTemplate string\n\n\t// AutocompleteInstallFlag is the global flag name for installing the autocompletion handlers for the user's shell.\n\tAutocompleteInstallFlag string\n\n\t// AutocompleteUninstallFlag is the global flag name for uninstalling the autocompletion handlers for the user's shell.\n\tAutocompleteUninstallFlag string\n\n\t// Commands is a list of commands to execute.\n\tCommands Commands\n\n\t// Flags is a list of flags to parse.\n\tFlags Flags\n\n\t// Examples is a list of examples of using the App in the help.\n\tExamples []string\n\n\t// Autocomplete enables or disables subcommand auto-completion support.\n\tAutocomplete bool\n\n\t// DisabledErrorOnUndefinedFlag prevents the application to exit and return an error on any undefined flag.\n\tDisabledErrorOnUndefinedFlag bool\n\n\t// DisabledErrorOnMultipleSetFlag prevents the application to exit and return an error if any flag is set multiple times.\n\tDisabledErrorOnMultipleSetFlag bool\n}\n\n// NewApp returns app new App instance.\nfunc NewApp() *App {\n\tcliApp := cli.NewApp()\n\tcliApp.ExitErrHandler = func(_ *cli.Context, _ error) {}\n\tcliApp.HideHelp = true\n\tcliApp.HideHelpCommand = true\n\n\treturn &App{\n\t\tApp:          cliApp,\n\t\tOsExiter:     os.Exit,\n\t\tAutocomplete: true,\n\t}\n}\n\n// AddFlags adds new flags.\nfunc (app *App) AddFlags(flags ...Flag) {\n\tapp.Flags = append(app.Flags, flags...)\n}\n\n// AddCommands adds new commands.\nfunc (app *App) AddCommands(cmds ...*Command) {\n\tapp.Commands = append(app.Commands, cmds...)\n}\n\n// Run is the entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination.\nfunc (app *App) Run(arguments []string) error {\n\treturn app.RunContext(context.Background(), arguments)\n}\n\n// RunContext is like Run except it takes a Context that will be\n// passed to its commands and sub-commands. Through this, you can\n// propagate timeouts and cancellation requests\nfunc (app *App) RunContext(ctx context.Context, arguments []string) (err error) {\n\t// remove empty args\n\tfilteredArguments := []string{}\n\n\tfor _, arg := range arguments {\n\t\tif trimmedArg := strings.TrimSpace(arg); len(trimmedArg) > 0 {\n\t\t\tfilteredArguments = append(filteredArguments, trimmedArg)\n\t\t}\n\t}\n\n\targuments = filteredArguments\n\n\tapp.SkipFlagParsing = true\n\tapp.Authors = []*cli.Author{{Name: app.Author}}\n\tapp.App.Action = func(parentCtx *cli.Context) error {\n\t\tcmd := app.NewRootCommand()\n\n\t\targs := Args(parentCtx.Args().Slice())\n\t\tcliCtx := NewAppContext(app, args)\n\n\t\tif app.Autocomplete {\n\t\t\tif err := app.setupAutocomplete(args); err != nil {\n\t\t\t\treturn app.handleExitCoder(cliCtx, err)\n\t\t\t}\n\n\t\t\tif compLine := os.Getenv(envCompleteLine); compLine != \"\" {\n\t\t\t\targs = strings.Fields(compLine)\n\t\t\t\tif args[0] == app.Name {\n\t\t\t\t\targs = args[1:]\n\t\t\t\t}\n\n\t\t\t\tcliCtx.shellComplete = true\n\t\t\t}\n\t\t}\n\n\t\treturn cmd.Run(parentCtx.Context, cliCtx, args)\n\t}\n\n\treturn app.App.RunContext(ctx, arguments)\n}\n\n// VisibleFlags returns a slice of the Flags used for help.\nfunc (app *App) VisibleFlags() Flags {\n\treturn app.Flags.VisibleFlags()\n}\n\n// VisibleCommands returns a slice of the Commands used for help.\nfunc (app *App) VisibleCommands() Commands {\n\tif app.Commands == nil {\n\t\treturn nil\n\t}\n\n\treturn app.Commands.Sort().VisibleCommands()\n}\n\nfunc (app *App) NewRootCommand() *Command {\n\treturn &Command{\n\t\tName:                           app.Name,\n\t\tBefore:                         app.Before,\n\t\tAfter:                          app.After,\n\t\tAction:                         app.Action,\n\t\tUsage:                          app.Usage,\n\t\tUsageText:                      app.UsageText,\n\t\tDescription:                    app.Description,\n\t\tExamples:                       app.Examples,\n\t\tFlags:                          app.Flags,\n\t\tSubcommands:                    app.Commands,\n\t\tComplete:                       app.Complete,\n\t\tIsRoot:                         true,\n\t\tDisabledErrorOnUndefinedFlag:   app.DisabledErrorOnUndefinedFlag,\n\t\tDisabledErrorOnMultipleSetFlag: app.DisabledErrorOnMultipleSetFlag,\n\t}\n}\n\nfunc (app *App) setupAutocomplete(arguments []string) error {\n\tvar (\n\t\tisAutocompleteInstall   bool\n\t\tisAutocompleteUninstall bool\n\t)\n\n\tif app.AutocompleteInstallFlag == \"\" {\n\t\tapp.AutocompleteInstallFlag = defaultAutocompleteInstallFlag\n\t}\n\n\tif app.AutocompleteUninstallFlag == \"\" {\n\t\tapp.AutocompleteUninstallFlag = defaultAutocompleteUninstallFlag\n\t}\n\n\tif app.AutocompleteInstaller == nil {\n\t\tapp.AutocompleteInstaller = &autocompleteInstaller{}\n\t}\n\n\tfor _, arg := range arguments {\n\t\tswitch arg {\n\t\tcase \"-\" + app.AutocompleteInstallFlag, \"--\" + app.AutocompleteInstallFlag:\n\t\t\tisAutocompleteInstall = true\n\t\tcase \"-\" + app.AutocompleteUninstallFlag, \"--\" + app.AutocompleteUninstallFlag:\n\t\t\tisAutocompleteUninstall = true\n\t\t}\n\t}\n\n\t// Autocomplete requires the \"Name\" to be set so that we know what command to setup the autocomplete on.\n\tif app.Name == \"\" {\n\t\treturn errors.Errorf(\"internal error: App.Name must be specified for autocomplete to work\")\n\t}\n\n\t// If both install and uninstall flags are specified, then error\n\tif isAutocompleteInstall && isAutocompleteUninstall {\n\t\treturn errors.Errorf(\"either the autocomplete install or uninstall flag may be specified, but not both\")\n\t}\n\n\t// If the install flag is specified, perform the install or uninstall and exit\n\tif isAutocompleteInstall {\n\t\terr := app.AutocompleteInstaller.Install(app.Name)\n\t\treturn NewExitError(err, 0)\n\t}\n\n\tif isAutocompleteUninstall {\n\t\terr := app.AutocompleteInstaller.Uninstall(app.Name)\n\t\treturn NewExitError(err, 0)\n\t}\n\n\treturn nil\n}\n\nfunc (app *App) handleExitCoder(ctx *Context, err error) error {\n\tif err == nil || err.Error() == \"\" {\n\t\treturn nil\n\t}\n\n\tif app.ExitErrHandler != nil {\n\t\treturn app.ExitErrHandler(ctx, err)\n\t}\n\n\treturn handleExitCoder(ctx, err, app.OsExiter)\n}\n"
  },
  {
    "path": "internal/clihelper/args.go",
    "content": "package clihelper\n\nimport (\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n)\n\nconst (\n\ttailMinArgsLen = 2\n\tBuiltinCmdSep  = \"--\"\n)\n\nconst (\n\tSingleDashFlag NormalizeActsType = iota\n\tDoubleDashFlag\n)\n\nvar (\n\tsingleDashRegexp = regexp.MustCompile(`^-([^-]|$)`)\n\tdoubleDashRegexp = regexp.MustCompile(`^--([^-]|$)`)\n)\n\ntype NormalizeActsType byte\n\n// Args provides convenient access to CLI arguments.\ntype Args []string\n\n// String implements `fmt.Stringer` interface.\nfunc (args Args) String() string {\n\treturn strings.Join(args, \" \")\n}\n\n// Split splits `args` into two slices separated by `sep`.\nfunc (args Args) Split(sep string) (Args, Args) {\n\tif i := slices.Index(args, sep); i >= 0 {\n\t\treturn args[:i], args[i+1:]\n\t}\n\n\treturn args, nil\n}\n\nfunc (args Args) WithoutBuiltinCmdSep() Args {\n\tflags, nonFlags := args.Split(BuiltinCmdSep)\n\n\treturn append(slices.Clone(flags), nonFlags...)\n}\n\n// Get returns the nth argument, or else a blank string\nfunc (args Args) Get(n int) string {\n\tif len(args) > 0 && len(args) > n {\n\t\treturn (args)[n]\n\t}\n\n\treturn \"\"\n}\n\n// First returns the first argument or a blank string\nfunc (args Args) First() string {\n\treturn args.Get(0)\n}\n\n// Second returns the second argument or a blank string.\nfunc (args Args) Second() string {\n\treturn args.Get(1)\n}\n\n// Last returns the last argument or a blank string.\nfunc (args Args) Last() string {\n\treturn args.Get(len(args) - 1)\n}\n\n// Tail returns the rest of the arguments (not the first one)\n// or else an empty string slice.\nfunc (args Args) Tail() Args {\n\tif args.Len() < tailMinArgsLen {\n\t\treturn []string{}\n\t}\n\n\treturn slices.Clone(args[1:])\n}\n\n// Remove returns `args` with the `name` element removed.\nfunc (args Args) Remove(name string) Args {\n\treturn slices.DeleteFunc(slices.Clone(args), func(arg string) bool {\n\t\treturn arg == name\n\t})\n}\n\n// Len returns the length of the wrapped slice\nfunc (args Args) Len() int {\n\treturn len(args)\n}\n\n// Present checks if there are any arguments present\nfunc (args Args) Present() bool {\n\treturn args.Len() != 0\n}\n\n// Slice returns a copy of the internal slice\nfunc (args Args) Slice() []string {\n\treturn slices.Clone(args)\n}\n\n// Normalize formats the arguments according to the given actions.\n// if the given act is:\n//\n//\t`SingleDashFlag` - converts all arguments containing double dashes to single dashes\n//\t`DoubleDashFlag` - converts all arguments containing single dashes to double dashes\nfunc (args Args) Normalize(acts ...NormalizeActsType) Args {\n\tresult := make(Args, 0, len(args))\n\n\tfor _, arg := range args {\n\t\tfor _, act := range acts {\n\t\t\tswitch act {\n\t\t\tcase SingleDashFlag:\n\t\t\t\tif doubleDashRegexp.MatchString(arg) {\n\t\t\t\t\targ = arg[1:]\n\t\t\t\t}\n\t\t\tcase DoubleDashFlag:\n\t\t\t\tif singleDashRegexp.MatchString(arg) {\n\t\t\t\t\targ = \"-\" + arg\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tresult = append(result, arg)\n\t}\n\n\treturn result\n}\n\n// CommandNameN returns the nth argument from `args` that starts without a dash `-`.\nfunc (args Args) CommandNameN(n int) string {\n\tvar found int\n\n\tfor _, arg := range args {\n\t\tif !strings.HasPrefix(arg, \"-\") {\n\t\t\tif found == n {\n\t\t\t\treturn arg\n\t\t\t}\n\n\t\t\tfound++\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\n// CommandName returns the first arg that starts without a dash `-`,\n// otherwise that means the args do not consist any command and an empty string is returned.\nfunc (args Args) CommandName() string {\n\treturn args.CommandNameN(0)\n}\n\n// SubCommandName returns the second arg that starts without a dash `-`,\n// otherwise that means the args do not consist a subcommand and an empty string is returned.\nfunc (args Args) SubCommandName() string {\n\treturn args.CommandNameN(1)\n}\n\n// Contains returns true if args contains the given `target` arg.\nfunc (args Args) Contains(target string) bool {\n\treturn slices.Contains(args, target)\n}\n"
  },
  {
    "path": "internal/clihelper/args_test.go",
    "content": "package clihelper_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar mockArgs = func() clihelper.Args { return clihelper.Args{\"one\", \"-foo\", \"two\", \"--bar\", \"value\"} }\n\nfunc TestArgsSlice(t *testing.T) {\n\tt.Parallel()\n\n\tactual := mockArgs().Slice()\n\texpected := []string(mockArgs())\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestArgsTail(t *testing.T) {\n\tt.Parallel()\n\n\tactual := mockArgs().Tail()\n\texpected := mockArgs()[1:]\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestArgsFirst(t *testing.T) {\n\tt.Parallel()\n\n\tactual := mockArgs().First()\n\texpected := mockArgs()[0]\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestArgsGet(t *testing.T) {\n\tt.Parallel()\n\n\tactual := mockArgs().Get(2)\n\texpected := \"two\"\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestArgsLen(t *testing.T) {\n\tt.Parallel()\n\n\tactual := mockArgs().Len()\n\texpected := 5\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestArgsPresent(t *testing.T) {\n\tt.Parallel()\n\n\tactual := mockArgs().Present()\n\texpected := true\n\tassert.Equal(t, expected, actual)\n\n\targs := clihelper.Args([]string{})\n\tactual = args.Present()\n\texpected = false\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestArgsCommandName(t *testing.T) {\n\tt.Parallel()\n\n\tactual := mockArgs().CommandName()\n\texpected := \"one\"\n\tassert.Equal(t, expected, actual)\n\n\targs := mockArgs()[1:]\n\tactual = args.CommandName()\n\texpected = \"two\"\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestArgsNormalize(t *testing.T) {\n\tt.Parallel()\n\n\tactual := mockArgs().Normalize(clihelper.SingleDashFlag).Slice()\n\texpected := []string{\"one\", \"-foo\", \"two\", \"-bar\", \"value\"}\n\tassert.Equal(t, expected, actual)\n\n\tactual = mockArgs().Normalize(clihelper.DoubleDashFlag).Slice()\n\texpected = []string{\"one\", \"--foo\", \"two\", \"--bar\", \"value\"}\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestArgsRemove(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\targs           clihelper.Args\n\t\texpectedArgs   clihelper.Args\n\t\tremoveName     string\n\t\texpectedResult clihelper.Args\n\t}{\n\t\t{\n\t\t\tmockArgs(),\n\t\t\tmockArgs(),\n\t\t\t\"two\",\n\t\t\tclihelper.Args{\"one\", \"-foo\", \"--bar\", \"value\"},\n\t\t},\n\t\t{\n\t\t\tmockArgs(),\n\t\t\tmockArgs(),\n\t\t\t\"one\",\n\t\t\tclihelper.Args{\"-foo\", \"two\", \"--bar\", \"value\"},\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := tc.args.Remove(tc.removeName)\n\t\t\tassert.Equal(t, tc.expectedResult, actual)\n\t\t\tassert.Equal(t, tc.expectedArgs, tc.args)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/clihelper/autocomplete.go",
    "content": "package clihelper\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\t\"slices\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/posener/complete/cmd/install\"\n)\n\n// defaultAutocompleteInstallFlag and defaultAutocompleteUninstallFlag are the\n// default values for the autocomplete install and uninstall flags.\nconst (\n\tdefaultAutocompleteInstallFlag   = \"install-autocomplete\"\n\tdefaultAutocompleteUninstallFlag = \"uninstall-autocomplete\"\n\n\tenvCompleteLine = \"COMP_LINE\"\n\n\tmaxDashesInFlag = 2\n)\n\nvar DefaultComplete = defaultComplete //nolint:gochecknoglobals\n\n// AutocompleteInstaller is an interface to be implemented to perform the\n// autocomplete installation and uninstallation with a CLI.\n//\n// This interface is not exported because it only exists for unit tests\n// to be able to test that the installation is called properly.\ntype AutocompleteInstaller interface {\n\tInstall(string) error\n\tUninstall(string) error\n}\n\n// autocompleteInstaller uses the install package to do the\n// install/uninstall.\ntype autocompleteInstaller struct{}\n\nfunc (i *autocompleteInstaller) Install(cmd string) error {\n\tif err := install.Install(cmd); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n\nfunc (i *autocompleteInstaller) Uninstall(cmd string) error {\n\tif err := install.Uninstall(cmd); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n\n// ShowCompletions prints the lists of commands within a given context\nfunc ShowCompletions(ctx context.Context, cliCtx *Context) error {\n\tif cmd := cliCtx.Command; cmd != nil && cmd.Complete != nil {\n\t\treturn cmd.Complete(ctx, cliCtx)\n\t}\n\n\treturn DefaultComplete(cliCtx)\n}\n\nfunc defaultComplete(cliCtx *Context) error {\n\targ := cliCtx.Args().Last()\n\n\tif strings.HasPrefix(arg, \"-\") {\n\t\tif cmd := cliCtx.Command; cmd != nil {\n\t\t\treturn printFlagSuggestions(arg, cmd.Flags, cliCtx.Writer)\n\t\t}\n\n\t\treturn printFlagSuggestions(arg, cliCtx.Flags, cliCtx.Writer)\n\t}\n\n\tif cmd := cliCtx.Command; cmd != nil {\n\t\treturn printCommandSuggestions(arg, cmd.Subcommands, cliCtx.Writer)\n\t}\n\n\treturn printCommandSuggestions(arg, cliCtx.Commands, cliCtx.Writer)\n}\n\nfunc printCommandSuggestions(arg string, commands []*Command, writer io.Writer) error {\n\terrs := []error{}\n\n\tfor _, command := range commands {\n\t\tif command.Hidden {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, name := range command.Names() {\n\t\t\tif name != \"\" && (arg == \"\" || strings.HasPrefix(name, arg)) {\n\t\t\t\t_, err := fmt.Fprintln(writer, name)\n\t\t\t\terrs = append(errs, err)\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn errors.Join(errs...)\n\t}\n\n\treturn nil\n}\n\nfunc printFlagSuggestions(arg string, flags []Flag, writer io.Writer) error {\n\tcur := strings.TrimLeft(arg, \"-\")\n\n\terrs := []error{}\n\n\tfor _, flag := range flags {\n\t\tfor _, name := range flag.Names() {\n\t\t\tname = strings.TrimSpace(name)\n\t\t\t// this will get total count utf8 letters in flag name\n\t\t\tcount := min(utf8.RuneCountInString(name), maxDashesInFlag)\n\t\t\t// if flag name has more than one utf8 letter and last argument in cli has -- prefix then\n\t\t\t// skip flag completion for short flags example -v or -x\n\t\t\tif strings.HasPrefix(arg, \"--\") && count == 1 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// match if last argument matches this flag and it is not repeated\n\t\t\tif strings.HasPrefix(name, cur) && cur != name && !cliArgContains(name) {\n\t\t\t\tflagCompletion := fmt.Sprintf(\"%s%s\", strings.Repeat(\"-\", count), name)\n\n\t\t\t\t_, err := fmt.Fprintln(writer, flagCompletion)\n\t\t\t\terrs = append(errs, err)\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn errors.Join(errs...)\n\t}\n\n\treturn nil\n}\n\nfunc cliArgContains(flagName string) bool {\n\tfor name := range strings.SplitSeq(flagName, \",\") {\n\t\tname = strings.TrimSpace(name)\n\n\t\tcount := min(utf8.RuneCountInString(name), maxDashesInFlag)\n\n\t\tflag := fmt.Sprintf(\"%s%s\", strings.Repeat(\"-\", count), name)\n\t\tif slices.Contains(os.Args, flag) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "internal/clihelper/bool_flag.go",
    "content": "package clihelper\n\nimport (\n\t\"context\"\n\tlibflag \"flag\"\n\t\"fmt\"\n\n\t\"github.com/urfave/cli/v2\"\n)\n\n// BoolFlag implements Flag\nvar _ Flag = new(BoolFlag)\n\ntype BoolFlag struct {\n\tflag\n\n\t// Action is a function that is called when the flag is specified. It is executed only after all command flags have been parsed.\n\tAction FlagActionFunc[bool]\n\n\t// Setter represents the function that is called when the flag is specified.\n\t// Executed during value parsing, in case of an error the returned error is wrapped with the flag or environment variable name.\n\tSetter FlagSetterFunc[bool]\n\n\t// Destination ia a pointer to which the value of the flag or env var is assigned.\n\t// It also uses as the default value displayed in the help.\n\tDestination *bool\n\n\t// Name is the name of the flag.\n\tName string\n\n\t// DefaultText is the default value of the flag to display in the help, if it is empty, the value is taken from `Destination`.\n\tDefaultText string\n\n\t// Usage is a short usage description to display in help.\n\tUsage string\n\n\t// Aliases are usually used for the short flag name, like `-h`.\n\tAliases []string\n\n\t// EnvVars are the names of the env variables that are parsed and assigned to `Destination` before the flag value.\n\tEnvVars []string\n\n\t// Negative inverts the value of the flag.\n\t// If set to true, then the assigned flag value will be inverted.\n\t// Example: With `Negative: true`, `--boolean-flag` sets the value to `false`, and `--boolean-flag=false` sets the value to `true`.\n\tNegative bool\n\n\t// Hidden hides the flag from the help.\n\tHidden bool\n}\n\n// Apply applies Flag settings to the given flag set.\nfunc (flag *BoolFlag) Apply(set *libflag.FlagSet) error {\n\tif flag.FlagValue != nil {\n\t\treturn ApplyFlag(flag, set)\n\t}\n\n\tif flag.Destination == nil {\n\t\tflag.Destination = new(bool)\n\t}\n\n\tvalueType := newBoolVar(flag.Destination, flag.Negative)\n\tvalue := newGenericValue(valueType, flag.Setter)\n\n\tflag.FlagValue = &flagValue{\n\t\tvalue:            value,\n\t\tinitialTextValue: value.String(),\n\t\tnegative:         flag.Negative,\n\t}\n\n\treturn ApplyFlag(flag, set)\n}\n\n// GetHidden returns true if the flag should be hidden from the help.\nfunc (flag *BoolFlag) GetHidden() bool {\n\treturn flag.Hidden\n}\n\n// GetUsage returns the usage string for the flag.\nfunc (flag *BoolFlag) GetUsage() string {\n\treturn flag.Usage\n}\n\n// GetEnvVars implements `cli.Flag` interface.\nfunc (flag *BoolFlag) GetEnvVars() []string {\n\treturn flag.EnvVars\n}\n\n// TakesValue returns true of the flag takes a value, otherwise false.\n// Implements `cli.DocGenerationFlag.TakesValue` required to generate help.\nfunc (flag *BoolFlag) TakesValue() bool {\n\treturn false\n}\n\n// GetDefaultText returns the flags value as string representation and an empty string if the flag takes no value at all.\nfunc (flag *BoolFlag) GetDefaultText() string {\n\tif flag.DefaultText == \"\" && flag.FlagValue != nil {\n\t\treturn flag.GetInitialTextValue()\n\t}\n\n\treturn flag.DefaultText\n}\n\n// String returns a readable representation of this value (for usage defaults).\nfunc (flag *BoolFlag) String() string {\n\treturn cli.FlagStringer(flag)\n}\n\n// Names returns the names of the flag.\nfunc (flag *BoolFlag) Names() []string {\n\tif flag.Name == \"\" {\n\t\treturn flag.Aliases\n\t}\n\n\treturn append([]string{flag.Name}, flag.Aliases...)\n}\n\n// RunAction implements ActionableFlag.RunAction\nfunc (flag *BoolFlag) RunAction(ctx context.Context, cliCtx *Context) error {\n\tdest := flag.Destination\n\tif dest == nil {\n\t\tdest = new(bool)\n\t}\n\n\tif flag.Action != nil {\n\t\treturn flag.Action(ctx, cliCtx, *dest)\n\t}\n\n\treturn nil\n}\n\nvar _ = FlagVariable[bool](new(boolVar))\n\n// -- bool Type\ntype boolVar struct {\n\t*genericVar[bool]\n\tnegative bool\n}\n\nfunc newBoolVar(dest *bool, negative bool) *boolVar {\n\treturn &boolVar{\n\t\tgenericVar: &genericVar[bool]{dest: dest},\n\t\tnegative:   negative,\n\t}\n}\n\nfunc (val *boolVar) Clone(dest *bool) FlagVariable[bool] {\n\treturn &boolVar{\n\t\tgenericVar: &genericVar[bool]{dest: dest},\n\t\tnegative:   val.negative,\n\t}\n}\n\nfunc (val *boolVar) Set(str string) error {\n\tif err := val.genericVar.Set(str); err != nil {\n\t\treturn err\n\t}\n\n\tif val.negative {\n\t\t*val.dest = !*val.dest\n\t}\n\n\treturn nil\n}\n\n// String returns a readable representation of this value\nfunc (val *boolVar) String() string {\n\tif val.dest == nil {\n\t\treturn \"\"\n\t}\n\n\tformat := \"%v\"\n\tif _, ok := val.Get().(bool); ok {\n\t\tformat = \"%t\"\n\t}\n\n\treturn fmt.Sprintf(format, *val.dest)\n}\n"
  },
  {
    "path": "internal/clihelper/bool_flag_test.go",
    "content": "package clihelper_test\n\nimport (\n\tlibflag \"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestBoolFlagApply(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedErr   error\n\t\tenvs          map[string]string\n\t\targs          []string\n\t\tflag          clihelper.BoolFlag\n\t\texpectedValue bool\n\t}{\n\t\t{\n\t\t\tflag:          clihelper.BoolFlag{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:          []string{\"--foo\"},\n\t\t\tenvs:          map[string]string{\"FOO\": \"false\"},\n\t\t\texpectedValue: true,\n\t\t\texpectedErr:   nil,\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.BoolFlag{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:          nil,\n\t\t\tenvs:          map[string]string{\"FOO\": \"true\"},\n\t\t\texpectedValue: true,\n\t\t\texpectedErr:   nil,\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.BoolFlag{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:          nil,\n\t\t\tenvs:          nil,\n\t\t\texpectedValue: false,\n\t\t\texpectedErr:   nil,\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.BoolFlag{Name: \"foo\", EnvVars: []string{\"FOO\"}, Destination: mockDestValue(false)},\n\t\t\targs:          []string{\"--foo\"},\n\t\t\tenvs:          map[string]string{\"FOO\": \"false\"},\n\t\t\texpectedValue: true,\n\t\t\texpectedErr:   nil,\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.BoolFlag{Name: \"foo\", Destination: mockDestValue(true)},\n\t\t\targs:          nil,\n\t\t\tenvs:          nil,\n\t\t\texpectedValue: true,\n\t\t\texpectedErr:   nil,\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.BoolFlag{Name: \"foo\", Destination: mockDestValue(true), Negative: true},\n\t\t\targs:          []string{\"--foo\"},\n\t\t\tenvs:          nil,\n\t\t\texpectedValue: false,\n\t\t\texpectedErr:   nil,\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.BoolFlag{Name: \"foo\", EnvVars: []string{\"FOO\"}, Destination: mockDestValue(true), Negative: true},\n\t\t\targs:          nil,\n\t\t\tenvs:          map[string]string{\"FOO\": \"true\"},\n\t\t\texpectedValue: false,\n\t\t\texpectedErr:   nil,\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.BoolFlag{Name: \"foo\", EnvVars: []string{\"FOO\"}, Destination: mockDestValue(false), Negative: true},\n\t\t\targs:          nil,\n\t\t\tenvs:          map[string]string{\"FOO\": \"false\"},\n\t\t\texpectedValue: true,\n\t\t\texpectedErr:   nil,\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.BoolFlag{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:          []string{\"--foo\", \"--foo\"},\n\t\t\tenvs:          nil,\n\t\t\texpectedValue: false,\n\t\t\texpectedErr:   errors.New(`invalid boolean flag foo: setting the flag multiple times`),\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.BoolFlag{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:          nil,\n\t\t\tenvs:          map[string]string{\"FOO\": \"\"},\n\t\t\texpectedValue: false,\n\t\t\texpectedErr:   nil,\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.BoolFlag{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:          nil,\n\t\t\tenvs:          map[string]string{\"FOO\": \"monkey\"},\n\t\t\texpectedValue: false,\n\t\t\texpectedErr:   errors.New(`invalid value \"monkey\" for env var FOO: must be one of: \"0\", \"1\", \"f\", \"t\", \"false\", \"true\"`),\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttestBoolFlagApply(t, &tc.flag, tc.args, tc.envs, tc.expectedValue, tc.expectedErr)\n\t\t})\n\t}\n}\n\nfunc testBoolFlagApply(t *testing.T, flag *clihelper.BoolFlag, args []string, envs map[string]string, expectedValue bool, expectedErr error) {\n\tt.Helper()\n\n\tvar (\n\t\tactualValue          bool\n\t\texpectedDefaultValue string\n\t)\n\n\tif flag.Destination == nil {\n\t\tflag.Destination = new(bool)\n\t}\n\n\texpectedDefaultValue = strconv.FormatBool(*flag.Destination)\n\n\tflag.LookupEnvFunc = func(key string) []string {\n\t\tif envs == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tif val, ok := envs[key]; ok {\n\t\t\treturn []string{val}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tflagSet := libflag.NewFlagSet(\"test-cmd\", libflag.ContinueOnError)\n\tflagSet.SetOutput(io.Discard)\n\n\terr := flag.Apply(flagSet)\n\tif err == nil {\n\t\terr = flagSet.Parse(args)\n\t}\n\n\tif expectedErr != nil {\n\t\trequire.Error(t, err)\n\t\trequire.ErrorContains(t, expectedErr, err.Error())\n\n\t\treturn\n\t}\n\n\trequire.NoError(t, err)\n\n\tactualValue = (flag.Value().Get()).(bool)\n\n\tassert.Equal(t, expectedValue, actualValue)\n\n\tif actualValue {\n\t\tassert.Equal(t, strconv.FormatBool(expectedValue), flag.GetValue(), \"GetValue()\")\n\t}\n\n\tmaps.DeleteFunc(envs, func(k, v string) bool { return v == \"\" })\n\n\tassert.Equal(t, len(args) > 0 || len(envs) > 0, flag.Value().IsSet(), \"IsSet()\")\n\tassert.Equal(t, expectedDefaultValue, flag.GetDefaultText(), \"GetDefaultText()\")\n\n\tassert.True(t, flag.Value().IsBoolFlag(), \"IsBoolFlag()\")\n\tassert.False(t, flag.TakesValue(), \"TakesValue()\")\n}\n"
  },
  {
    "path": "internal/clihelper/category.go",
    "content": "package clihelper\n\nimport \"sort\"\n\n// Category represents a command category used to group commands when displaying them.\ntype Category struct {\n\t// Name is the name of the category.\n\tName string\n\t// Order is a number indicating the order in the category list.\n\tOrder uint\n}\n\n// String implements `fmt.Stringer` interface.\nfunc (category *Category) String() string {\n\treturn category.Name\n}\n\n// Categories is a slice of `Category`.\ntype Categories []*Category\n\n// Len implements `sort.Interface` interface.\nfunc (categories Categories) Len() int {\n\treturn len(categories)\n}\n\n// Less implements `sort.Interface` interface.\nfunc (categories Categories) Less(i, j int) bool {\n\tif categories[i].Order == categories[j].Order {\n\t\treturn categories[i].Name < categories[j].Name\n\t}\n\n\treturn categories[i].Order < categories[j].Order\n}\n\n// Swap implements `sort.Interface` interface.\nfunc (categories Categories) Swap(i, j int) {\n\tcategories[i], categories[j] = categories[j], categories[i]\n}\n\n// Sort returns `categories` in sorted order.\nfunc (categories Categories) Sort() Categories {\n\tsort.Sort(categories)\n\n\treturn categories\n}\n"
  },
  {
    "path": "internal/clihelper/command.go",
    "content": "package clihelper\n\nimport (\n\t\"context\"\n\t\"errors\"\n\tlibflag \"flag\"\n\t\"strings\"\n)\n\ntype Command struct {\n\t// Category is the category the command belongs to.\n\tCategory *Category\n\n\t// Before is an action to execute before the command is invoked.\n\t// If a non-nil error is returned, no further processing is done.\n\tBefore ActionFunc\n\n\t// CustomHelp is a custom function to display help text.\n\tCustomHelp HelpFunc\n\n\t// After is the function to call after the command is invoked.\n\tAfter ActionFunc\n\n\t// Complete is the function to call for shell completion.\n\tComplete CompleteFunc\n\n\t// Action is the function to execute when the command is invoked.\n\t// Runs after subcommands are finished.\n\tAction ActionFunc\n\n\t// Description is a longer explanation of how the command works.\n\tDescription string\n\n\t// HelpName is the full name of the command for help.\n\t// Defaults to the full command name, including parent commands.\n\tHelpName string\n\n\t// Name is the command name.\n\tName string\n\n\t// UsageText is custom text to show on the `Usage` section of the help.\n\tUsageText string\n\n\t// CustomHelpTemplate is a custom text template for the help topic.\n\tCustomHelpTemplate string\n\n\t// Usage is a short description of the usage for the command.\n\tUsage string\n\n\t// Flags is a list of flags to parse.\n\tFlags Flags\n\n\t// Examples is a list of examples for using the command in help.\n\tExamples []string\n\n\t// Subcommands is a list of subcommands.\n\tSubcommands Commands\n\n\t// Aliases is a list of aliases for the command.\n\tAliases []string\n\n\t// IsRoot is true if this is a root \"special\" command.\n\t// NOTE: The author of this comment doesn't know what this means.\n\tIsRoot bool\n\n\t// SkipRunning disables the parsing command, but it will\n\t// still be shown in help.\n\tSkipRunning bool\n\n\t// SkipFlagParsing treats all flags as normal arguments.\n\tSkipFlagParsing bool\n\n\t// Hidden hides the command from help.\n\tHidden bool\n\n\t// DisabledErrorOnUndefinedFlag prevents the application to exit and return an error on any undefined flag.\n\tDisabledErrorOnUndefinedFlag bool\n\n\t// DisabledErrorOnMultipleSetFlag prevents the application to exit and return an error if any flag is set multiple times.\n\tDisabledErrorOnMultipleSetFlag bool\n}\n\n// Names returns the names including short names and aliases.\nfunc (cmd *Command) Names() []string {\n\treturn append([]string{cmd.Name}, cmd.Aliases...)\n}\n\n// HasName returns true if Command.Name matches given name\nfunc (cmd *Command) HasName(name string) bool {\n\tfor _, n := range cmd.Names() {\n\t\tif n == name && name != \"\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// Subcommand returns a subcommand that matches the given name.\nfunc (cmd *Command) Subcommand(name string) *Command {\n\tfor _, c := range cmd.Subcommands {\n\t\tif c.HasName(name) {\n\t\t\treturn c\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// VisibleFlags returns a slice of the Flags, used by `urfave/cli` package to generate help.\nfunc (cmd *Command) VisibleFlags() Flags {\n\treturn cmd.Flags.VisibleFlags()\n}\n\n// VisibleSubcommands returns a slice of the Commands with Hidden=false.\n// Used by `urfave/cli` package to generate help.\nfunc (cmd *Command) VisibleSubcommands() Commands {\n\tif cmd.Subcommands == nil {\n\t\treturn nil\n\t}\n\n\treturn cmd.Subcommands.VisibleCommands()\n}\n\n// Run parses the given args for the presence of flags as well as subcommands.\n// If this is the final command, starts its execution.\nfunc (cmd *Command) Run(ctx context.Context, cliCtx *Context, args Args) (err error) {\n\targs, err = cmd.parseFlags(cliCtx, args.Slice())\n\tif err != nil {\n\t\treturn NewExitError(err, ExitCodeGeneralError)\n\t}\n\n\tcliCtx = cliCtx.NewCommandContext(cmd, args)\n\n\tsubCmdName := cliCtx.Args().CommandName()\n\tsubCmdArgs := cliCtx.Args().Remove(subCmdName)\n\tsubCmd := cmd.Subcommand(subCmdName)\n\n\tif cliCtx.shellComplete {\n\t\tif cmd := cliCtx.Command.Subcommand(args.CommandName()); cmd == nil {\n\t\t\treturn ShowCompletions(ctx, cliCtx)\n\t\t}\n\n\t\tif subCmd != nil {\n\t\t\treturn subCmd.Run(ctx, cliCtx, subCmdArgs)\n\t\t}\n\t}\n\n\tif err := cmd.Flags.RunActions(ctx, cliCtx); err != nil {\n\t\treturn cliCtx.handleExitCoder(cliCtx, err)\n\t}\n\n\tdefer func() {\n\t\tif cmd.After != nil && err == nil {\n\t\t\terr = cmd.After(ctx, cliCtx)\n\t\t\terr = cliCtx.handleExitCoder(cliCtx, err)\n\t\t}\n\t}()\n\n\tif cmd.Before != nil {\n\t\tif err := cmd.Before(ctx, cliCtx); err != nil {\n\t\t\treturn cliCtx.handleExitCoder(cliCtx, err)\n\t\t}\n\t}\n\n\tif subCmd != nil && !subCmd.SkipRunning {\n\t\treturn subCmd.Run(ctx, cliCtx, subCmdArgs)\n\t}\n\n\tif cmd.Action != nil {\n\t\tif err = cmd.Action(ctx, cliCtx); err != nil {\n\t\t\treturn cliCtx.handleExitCoder(cliCtx, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cmd *Command) parseFlags(ctx *Context, args Args) ([]string, error) {\n\tvar undefArgs Args\n\n\terrHandler := func(err error) error {\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tif cmd.DisabledErrorOnMultipleSetFlag && IsMultipleTimesSettingError(err) {\n\t\t\treturn nil\n\t\t}\n\n\t\tif flagErrHandler := ctx.FlagErrHandler; flagErrHandler != nil {\n\t\t\terr = flagErrHandler(ctx.NewCommandContext(cmd, args), err)\n\t\t}\n\n\t\treturn err\n\t}\n\n\tflagSet, err := cmd.Flags.NewFlagSet(cmd.Name, errHandler)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tflagSetWithSubcommandScope, err := cmd.Flags.WithSubcommandScope().NewFlagSet(cmd.Name, errHandler)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif cmd.SkipFlagParsing {\n\t\treturn args, nil\n\t}\n\n\targs, builtinCmd := args.Split(BuiltinCmdSep)\n\n\tfor i := 0; len(args) > 0; i++ {\n\t\tif i == 0 {\n\t\t\targs, err = cmd.flagSetParse(ctx, flagSet, args)\n\t\t} else {\n\t\t\targs, err = cmd.flagSetParse(ctx, flagSetWithSubcommandScope, args)\n\t\t}\n\n\t\tif len(args) != 0 {\n\t\t\tundefArgs = append(undefArgs, args[0])\n\t\t\targs = args[1:]\n\t\t}\n\n\t\tif err != nil {\n\t\t\tif !errors.As(err, new(UndefinedFlagError)) ||\n\t\t\t\t(cmd.Subcommands.Get(undefArgs.Get(0)) == nil) {\n\t\t\t\tif err = errHandler(err); err != nil {\n\t\t\t\t\treturn undefArgs, err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(builtinCmd) > 0 {\n\t\tundefArgs = append(undefArgs, BuiltinCmdSep)\n\t\tundefArgs = append(undefArgs, builtinCmd...)\n\t}\n\n\treturn undefArgs, nil\n}\n\nfunc (cmd *Command) flagSetParse(ctx *Context, flagSet *libflag.FlagSet, args Args) ([]string, error) {\n\tvar (\n\t\tundefArgs []string\n\t\terr       error\n\t)\n\n\tif len(args) == 0 {\n\t\treturn undefArgs, nil\n\t}\n\n\tconst maxFlagsParse = 1000 // Maximum flags parse\n\n\tfor range maxFlagsParse {\n\t\t// check if the error is due to an undefArgs flag\n\t\tvar undefArg string\n\n\t\tif err = flagSet.Parse(args); err == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tif errStr := err.Error(); strings.HasPrefix(errStr, ErrMsgFlagUndefined) {\n\t\t\tundefArg = strings.Trim(strings.TrimPrefix(errStr, ErrMsgFlagUndefined), \" -\")\n\t\t\terr = UndefinedFlagError(undefArg)\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\n\t\t// cut off the args\n\t\tvar notFoundMatch bool\n\n\t\tfor i, arg := range args {\n\t\t\t// `--var=input=from_env` trims to `var`\n\t\t\ttrimmed := strings.SplitN(strings.Trim(arg, \"-\"), \"=\", 2)[0] //nolint:mnd\n\t\t\tif trimmed == undefArg {\n\t\t\t\tundefArgs = append(undefArgs, arg)\n\t\t\t\tnotFoundMatch = true\n\t\t\t\targs = args[i+1:]\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif !cmd.DisabledErrorOnUndefinedFlag && !ctx.shellComplete {\n\t\t\tbreak\n\t\t}\n\n\t\t// This should be an impossible to reach code path, but in case the arg\n\t\t// splitting failed to happen, this will prevent infinite loops\n\t\tif !notFoundMatch {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tundefArgs = append(undefArgs, flagSet.Args()...)\n\n\treturn undefArgs, err\n}\n\nfunc (cmd *Command) WrapAction(fn func(ctx context.Context, cliCtx *Context, action ActionFunc) error) *Command {\n\tclone := *cmd\n\n\taction := clone.Action\n\tclone.Action = func(ctx context.Context, cliCtx *Context) error {\n\t\treturn fn(ctx, cliCtx, action)\n\t}\n\tclone.Subcommands = clone.Subcommands.WrapAction(fn)\n\n\treturn &clone\n}\n\n// DisableErrorOnMultipleSetFlag returns cloned commands with disabled the check for multiple values set for the same flag.\nfunc (cmd *Command) DisableErrorOnMultipleSetFlag() *Command {\n\tnewCmd := *cmd\n\tnewCmd.DisabledErrorOnMultipleSetFlag = true\n\tnewCmd.Subcommands = newCmd.Subcommands.DisableErrorOnMultipleSetFlag()\n\n\treturn &newCmd\n}\n"
  },
  {
    "path": "internal/clihelper/command_test.go",
    "content": "package clihelper_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\turfaveCli \"github.com/urfave/cli/v2\"\n)\n\nfunc TestCommandRun(t *testing.T) {\n\tt.Parallel()\n\n\ttype TestActionFunc func(expectedOrder int, expectedArgs []string) clihelper.ActionFunc\n\n\ttype TestCase struct {\n\t\texpectedErr error\n\t\targs        []string\n\t\tcommand     clihelper.Command\n\t}\n\n\ttestCaseFuncs := []func(action TestActionFunc, skip clihelper.ActionFunc) TestCase{\n\t\tfunc(action TestActionFunc, skip clihelper.ActionFunc) TestCase {\n\t\t\treturn TestCase{\n\t\t\t\targs: []string{\"--foo\", \"--foo\", \"cmd-bar\", \"--bar\", \"one\", \"-two\"},\n\t\t\t\tcommand: clihelper.Command{\n\t\t\t\t\tFlags:  clihelper.Flags{&clihelper.BoolFlag{Name: \"foo\"}},\n\t\t\t\t\tBefore: skip,\n\t\t\t\t\tAction: skip,\n\t\t\t\t\tAfter:  skip,\n\t\t\t\t},\n\t\t\t\texpectedErr: errors.New(\"invalid boolean flag foo: setting the flag multiple times\"),\n\t\t\t}\n\t\t},\n\n\t\tfunc(action TestActionFunc, skip clihelper.ActionFunc) TestCase {\n\t\t\treturn TestCase{\n\t\t\t\targs: []string{\"--foo\", \"cmd-bar\", \"--bar\", \"one\", \"-two\"},\n\t\t\t\tcommand: clihelper.Command{\n\t\t\t\t\tFlags:  clihelper.Flags{&clihelper.BoolFlag{Name: \"foo\"}},\n\t\t\t\t\tBefore: action(1, nil),\n\t\t\t\t\tAction: skip,\n\t\t\t\t\tAfter:  action(5, nil),\n\t\t\t\t\tSubcommands: clihelper.Commands{\n\t\t\t\t\t\t&clihelper.Command{\n\t\t\t\t\t\t\tName:   \"cmd-cux\",\n\t\t\t\t\t\t\tFlags:  clihelper.Flags{&clihelper.BoolFlag{Name: \"bar\"}},\n\t\t\t\t\t\t\tBefore: skip,\n\t\t\t\t\t\t\tAction: skip,\n\t\t\t\t\t\t\tAfter:  skip,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t&clihelper.Command{\n\t\t\t\t\t\t\tName:                         \"cmd-bar\",\n\t\t\t\t\t\t\tFlags:                        clihelper.Flags{&clihelper.BoolFlag{Name: \"bar\"}},\n\t\t\t\t\t\t\tBefore:                       action(2, nil),\n\t\t\t\t\t\t\tAction:                       action(3, []string{\"one\", \"-two\"}),\n\t\t\t\t\t\t\tAfter:                        action(4, nil),\n\t\t\t\t\t\t\tDisabledErrorOnUndefinedFlag: 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\tfunc(action TestActionFunc, skip clihelper.ActionFunc) TestCase {\n\t\t\treturn TestCase{\n\t\t\t\targs: []string{\"--foo\", \"cmd-bar\", \"--bar\", \"one\", \"-two\"},\n\t\t\t\tcommand: clihelper.Command{\n\t\t\t\t\tFlags:  clihelper.Flags{&clihelper.BoolFlag{Name: \"foo\"}},\n\t\t\t\t\tBefore: action(1, nil),\n\t\t\t\t\tAction: skip,\n\t\t\t\t\tAfter:  action(4, nil),\n\t\t\t\t\tSubcommands: clihelper.Commands{\n\t\t\t\t\t\t&clihelper.Command{\n\t\t\t\t\t\t\tName:                         \"cmd-bar\",\n\t\t\t\t\t\t\tFlags:                        clihelper.Flags{&clihelper.BoolFlag{Name: \"bar\"}},\n\t\t\t\t\t\t\tBefore:                       action(2, nil),\n\t\t\t\t\t\t\tAfter:                        action(3, nil),\n\t\t\t\t\t\t\tDisabledErrorOnUndefinedFlag: 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\tfunc(action TestActionFunc, skip clihelper.ActionFunc) TestCase {\n\t\t\treturn TestCase{\n\t\t\t\targs: []string{\"--foo\", \"--bar\", \"cmd-bar\", \"one\", \"-two\"},\n\t\t\t\tcommand: clihelper.Command{\n\t\t\t\t\tFlags:  clihelper.Flags{&clihelper.BoolFlag{Name: \"foo\"}},\n\t\t\t\t\tBefore: action(1, nil),\n\t\t\t\t\tAction: skip,\n\t\t\t\t\tAfter:  action(5, nil),\n\t\t\t\t\tSubcommands: clihelper.Commands{\n\t\t\t\t\t\t&clihelper.Command{\n\t\t\t\t\t\t\tName:                         \"cmd-bar\",\n\t\t\t\t\t\t\tFlags:                        clihelper.Flags{&clihelper.BoolFlag{Name: \"bar\"}},\n\t\t\t\t\t\t\tBefore:                       action(2, nil),\n\t\t\t\t\t\t\tAction:                       action(3, []string{\"one\", \"-two\"}),\n\t\t\t\t\t\t\tAfter:                        action(4, nil),\n\t\t\t\t\t\t\tDisabledErrorOnUndefinedFlag: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tDisabledErrorOnUndefinedFlag: true,\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t\tfunc(action TestActionFunc, skip clihelper.ActionFunc) TestCase {\n\t\t\treturn TestCase{\n\t\t\t\targs: []string{\"--foo\", \"cmd-bar\", \"--bar\", \"value\", \"one\", \"-two\"},\n\t\t\t\tcommand: clihelper.Command{\n\t\t\t\t\tFlags:  clihelper.Flags{&clihelper.BoolFlag{Name: \"foo\"}},\n\t\t\t\t\tBefore: action(1, nil),\n\t\t\t\t\tAction: skip,\n\t\t\t\t\tAfter:  action(5, nil),\n\t\t\t\t\tSubcommands: clihelper.Commands{\n\t\t\t\t\t\t&clihelper.Command{\n\t\t\t\t\t\t\tName:                         \"cmd-bar\",\n\t\t\t\t\t\t\tFlags:                        clihelper.Flags{&clihelper.GenericFlag[string]{Name: \"bar\"}},\n\t\t\t\t\t\t\tBefore:                       action(2, nil),\n\t\t\t\t\t\t\tAction:                       action(3, []string{\"one\", \"-two\"}),\n\t\t\t\t\t\t\tAfter:                        action(4, nil),\n\t\t\t\t\t\t\tDisabledErrorOnUndefinedFlag: 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\tfunc(action TestActionFunc, skip clihelper.ActionFunc) TestCase {\n\t\t\treturn TestCase{\n\t\t\t\targs: []string{\"--foo\", \"cmd-bar\", \"--bar\", \"value\", \"one\", \"-two\"},\n\t\t\t\tcommand: clihelper.Command{\n\t\t\t\t\tFlags:  clihelper.Flags{&clihelper.BoolFlag{Name: \"foo\"}},\n\t\t\t\t\tBefore: action(1, nil),\n\t\t\t\t\tAction: action(2, []string{\"cmd-bar\", \"--bar\", \"value\", \"one\", \"-two\"}),\n\t\t\t\t\tAfter:  action(3, nil),\n\t\t\t\t\tSubcommands: clihelper.Commands{\n\t\t\t\t\t\t&clihelper.Command{\n\t\t\t\t\t\t\tName:        \"cmd-bar\",\n\t\t\t\t\t\t\tFlags:       clihelper.Flags{&clihelper.GenericFlag[string]{Name: \"bar\"}},\n\t\t\t\t\t\t\tSkipRunning: true,\n\t\t\t\t\t\t\tBefore:      skip,\n\t\t\t\t\t\t\tAction:      skip,\n\t\t\t\t\t\t\tAfter:       skip,\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\tfor i, tcFn := range testCaseFuncs {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar actualOrder = new(int)\n\n\t\t\taction := func(expectedOrder int, expectedArgs []string) clihelper.ActionFunc {\n\t\t\t\treturn func(ctx context.Context, cliCtx *clihelper.Context) error {\n\t\t\t\t\t(*actualOrder)++\n\t\t\t\t\tassert.Equal(t, expectedOrder, *actualOrder)\n\n\t\t\t\t\tif expectedArgs != nil {\n\t\t\t\t\t\tactualArgs := cliCtx.Args().Slice()\n\t\t\t\t\t\tassert.Equal(t, expectedArgs, actualArgs)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tskip := func(ctx context.Context, cliCtx *clihelper.Context) error {\n\t\t\t\tassert.Fail(t, \"this action must be skipped\")\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\ttc := tcFn(action, skip)\n\n\t\t\tapp := &clihelper.App{App: &urfaveCli.App{Writer: io.Discard}}\n\t\t\tcliCtx := clihelper.NewAppContext(app, tc.args)\n\n\t\t\terr := tc.command.Run(t.Context(), cliCtx, tc.args)\n\t\t\tif tc.expectedErr != nil {\n\t\t\t\trequire.EqualError(t, err, tc.expectedErr.Error(), tc)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err, tc)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCommandHasName(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\thasName  string\n\t\tcommand  clihelper.Command\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tcommand: clihelper.Command{Name: \"foo\"},\n\t\t\thasName: \"bar\",\n\t\t},\n\t\t{\n\t\t\tcommand:  clihelper.Command{Name: \"foo\", Aliases: []string{\"bar\"}},\n\t\t\thasName:  \"bar\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tcommand:  clihelper.Command{Name: \"bar\"},\n\t\t\thasName:  \"bar\",\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := tc.command.HasName(tc.hasName)\n\t\t\tassert.Equal(t, tc.expected, actual, tc)\n\t\t})\n\t}\n}\n\nfunc TestCommandNames(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpected []string\n\t\tcommand  clihelper.Command\n\t}{\n\t\t{\n\t\t\tcommand:  clihelper.Command{Name: \"foo\"},\n\t\t\texpected: []string{\"foo\"},\n\t\t},\n\t\t{\n\t\t\tcommand:  clihelper.Command{Name: \"foo\", Aliases: []string{\"bar\", \"baz\"}},\n\t\t\texpected: []string{\"foo\", \"bar\", \"baz\"},\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := tc.command.Names()\n\t\t\tassert.Equal(t, tc.expected, actual, tc)\n\t\t})\n\t}\n}\n\nfunc TestCommandSubcommand(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpected      *clihelper.Command\n\t\tsearchCmdName string\n\t\tcommand       clihelper.Command\n\t}{\n\t\t{\n\t\t\tcommand:       clihelper.Command{Name: \"foo\", Subcommands: clihelper.Commands{&clihelper.Command{Name: \"bar\"}, &clihelper.Command{Name: \"baz\"}}},\n\t\t\tsearchCmdName: \"baz\",\n\t\t\texpected:      &clihelper.Command{Name: \"baz\"},\n\t\t},\n\t\t{\n\t\t\tcommand:       clihelper.Command{Name: \"foo\", Subcommands: clihelper.Commands{&clihelper.Command{Name: \"bar\"}, &clihelper.Command{Name: \"baz\"}}},\n\t\t\tsearchCmdName: \"qux\",\n\t\t\texpected:      nil,\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := tc.command.Subcommand(tc.searchCmdName)\n\t\t\tassert.Equal(t, tc.expected, actual, tc)\n\t\t})\n\t}\n}\n\nfunc TestCommandVisibleSubcommand(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpected clihelper.Commands\n\t\tcommand  clihelper.Command\n\t}{\n\t\t{\n\t\t\tcommand:  clihelper.Command{Name: \"foo\", Subcommands: clihelper.Commands{&clihelper.Command{Name: \"bar\"}, &clihelper.Command{Name: \"baz\", HelpName: \"helpBaz\"}}},\n\t\t\texpected: clihelper.Commands{{Name: \"bar\", HelpName: \"bar\"}, {Name: \"baz\", HelpName: \"helpBaz\"}},\n\t\t},\n\t\t{\n\t\t\tcommand:  clihelper.Command{Name: \"foo\", Subcommands: clihelper.Commands{&clihelper.Command{Name: \"bar\", Hidden: true}, &clihelper.Command{Name: \"baz\"}}},\n\t\t\texpected: clihelper.Commands{{Name: \"baz\", HelpName: \"baz\"}},\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := tc.command.VisibleSubcommands()\n\t\t\tassert.Equal(t, tc.expected, actual, tc)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/clihelper/commands.go",
    "content": "package clihelper\n\nimport (\n\t\"context\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"slices\"\n)\n\ntype Commands []*Command\n\n// Get returns a Command by the given name.\nfunc (commands Commands) Get(name string) *Command {\n\tfor _, cmd := range commands {\n\t\tif cmd.HasName(name) {\n\t\t\treturn cmd\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Names returns names of the commands.\nfunc (commands Commands) Names() []string {\n\tvar names = make([]string, len(commands))\n\n\tfor i, cmd := range commands {\n\t\tnames[i] = cmd.Name\n\t}\n\n\treturn names\n}\n\n// Add adds a new cmd to the list.\nfunc (commands *Commands) Add(cmd *Command) {\n\t*commands = append(*commands, cmd)\n}\n\n// FilterByNames returns a list of commands filtered by the given names.\nfunc (commands Commands) FilterByNames(names []string) Commands {\n\tvar filtered Commands\n\n\tfor _, cmd := range commands {\n\t\tfor _, name := range names {\n\t\t\tif cmd.HasName(name) {\n\t\t\t\tfiltered = append(filtered, cmd)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn filtered\n}\n\n// FilterByCategory returns a list of commands filtered by the given `categories`.\nfunc (commands Commands) FilterByCategory(categories ...*Category) Commands {\n\tvar filtered Commands\n\n\tfor _, cmd := range commands {\n\t\tif category := cmd.Category; category != nil && slices.Contains(categories, category) {\n\t\t\tfiltered = append(filtered, cmd)\n\t\t}\n\t}\n\n\treturn filtered\n}\n\n// SkipRunning prevents running commands as the final commands, but keep showing them in help.\nfunc (commands Commands) SkipRunning() Commands {\n\tfor _, cmd := range commands {\n\t\tcmd.SkipRunning = true\n\t}\n\n\treturn commands\n}\n\n// VisibleCommands returns a slice of the Commands with Hidden=false.\n// Used by `urfave/cli` package to generate help.\nfunc (commands Commands) VisibleCommands() Commands {\n\tvar visible = make(Commands, 0, len(commands))\n\n\tfor _, cmd := range commands {\n\t\tif cmd.Hidden {\n\t\t\tcontinue\n\t\t}\n\n\t\tif cmd.HelpName == \"\" {\n\t\t\tnames := append([]string{cmd.Name}, cmd.Aliases...)\n\n\t\t\tcmd.HelpName = strings.Join(names, \", \")\n\t\t}\n\n\t\tvisible = append(visible, cmd)\n\t}\n\n\treturn visible\n}\n\nfunc (commands Commands) Len() int {\n\treturn len(commands)\n}\n\nfunc (commands Commands) Less(i, j int) bool {\n\treturn LexicographicLess(commands[i].Name, commands[j].Name)\n}\n\nfunc (commands Commands) Swap(i, j int) {\n\tcommands[i], commands[j] = commands[j], commands[i]\n}\n\nfunc (commands Commands) WrapAction(fn func(ctx context.Context, cliCtx *Context, action ActionFunc) error) Commands {\n\twrapped := make(Commands, len(commands))\n\n\tfor i := range commands {\n\t\twrapped[i] = commands[i].WrapAction(fn)\n\t}\n\n\treturn wrapped\n}\n\nfunc (commands Commands) Sort() Commands {\n\tsort.Sort(commands)\n\n\treturn commands\n}\n\n// SetCategory sets the given `category` for the `commands`.\nfunc (commands Commands) SetCategory(category *Category) Commands {\n\tfor _, cmd := range commands {\n\t\tcmd.Category = category\n\t}\n\n\treturn commands\n}\n\n// GetCategories returns unique categories commands.\nfunc (commands Commands) GetCategories() Categories {\n\tvar categories Categories\n\n\tfor _, cmd := range commands {\n\t\tif category := cmd.Category; category != nil && !slices.Contains(categories, category) {\n\t\t\tcategories = append(categories, category)\n\t\t}\n\t}\n\n\treturn categories\n}\n\n// Merge merges the given `cmds` with `commands` and returns the result.\nfunc (commands Commands) Merge(cmds ...*Command) Commands {\n\treturn append(commands, cmds...)\n}\n\n// DisableErrorOnMultipleSetFlag returns a cloned command with disabled the check for multiple values set for the same flag.\nfunc (commands Commands) DisableErrorOnMultipleSetFlag() Commands {\n\tvar newCommands = make(Commands, len(commands))\n\n\tfor i := range commands {\n\t\tnewCommands[i] = commands[i].DisableErrorOnMultipleSetFlag()\n\t}\n\n\treturn newCommands\n}\n"
  },
  {
    "path": "internal/clihelper/context.go",
    "content": "package clihelper\n\n// Context can be used to retrieve context-specific args and parsed command-line options.\ntype Context struct {\n\t*App\n\tCommand       *Command\n\tparent        *Context\n\targs          Args\n\tshellComplete bool\n}\n\nfunc NewAppContext(app *App, args Args) *Context {\n\treturn &Context{\n\t\tApp:  app,\n\t\targs: args,\n\t}\n}\n\nfunc (ctx *Context) NewCommandContext(command *Command, args Args) *Context {\n\treturn &Context{\n\t\tApp:           ctx.App,\n\t\tCommand:       command,\n\t\tparent:        ctx,\n\t\targs:          args,\n\t\tshellComplete: ctx.shellComplete,\n\t}\n}\n\nfunc (ctx *Context) Parent() *Context {\n\treturn ctx.parent\n}\n\n// Args returns the command line arguments associated with the context.\nfunc (ctx *Context) Args() Args {\n\treturn ctx.args\n}\n\n// Flag retrieves a command flag by name. Returns nil if the command is not set\n// or if the flag doesn't exist.\nfunc (ctx *Context) Flag(name string) Flag {\n\tif ctx.Command != nil {\n\t\treturn ctx.Command.Flags.Get(name)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/clihelper/errors.go",
    "content": "package clihelper\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"errors\"\n\n\t\"github.com/urfave/cli/v2\"\n)\n\ntype InvalidCommandNameError string\n\nfunc (cmdName InvalidCommandNameError) Error() string {\n\treturn fmt.Sprintf(\"invalid command name %q\", string(cmdName))\n}\n\ntype InvalidKeyValueError struct {\n\tvalue string\n\tsep   string\n}\n\nfunc NewInvalidKeyValueError(sep, value string) *InvalidKeyValueError {\n\treturn &InvalidKeyValueError{value, sep}\n}\n\nfunc (err InvalidKeyValueError) Error() string {\n\treturn fmt.Sprintf(\"invalid key-value pair, expected format KEY%sVALUE, got %s.\", err.sep, err.value)\n}\n\ntype exitError struct {\n\terr      error\n\texitCode ExitCode\n}\n\nfunc (ee *exitError) Unwrap() error {\n\treturn ee.err\n}\n\nfunc (ee *exitError) Error() string {\n\tif ee.err == nil {\n\t\treturn \"\"\n\t}\n\n\treturn ee.err.Error()\n}\n\nfunc (ee *exitError) ExitCode() int {\n\treturn int(ee.exitCode)\n}\n\n// NewExitError calls Exit to create a new ExitCoder.\nfunc NewExitError(message any, exitCode ExitCode) ExitCoder {\n\tvar err error\n\n\tif message != nil {\n\t\tswitch e := message.(type) {\n\t\tcase error:\n\t\t\terr = e\n\t\tdefault:\n\t\t\terr = fmt.Errorf(\"%+v\", message)\n\t\t}\n\t}\n\n\treturn &exitError{\n\t\terr:      err,\n\t\texitCode: exitCode,\n\t}\n}\n\n// handleExitCoder handles errors implementing ExitCoder by printing their\n// message and calling osExiter with the given exit code.\n//\n// If the given error instead implements MultiError, each error will be checked\n// for the ExitCoder interface, and osExiter will be called with the last exit\n// code found, or exit code 1 if no ExitCoder is found.\n//\n// This function is the default error-handling behavior for an App.\nfunc handleExitCoder(_ *Context, err error, osExiter func(code int)) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\tvar exitErr cli.ExitCoder\n\tif ok := errors.As(err, &exitErr); ok {\n\t\tif err.Error() != \"\" {\n\t\t\t_, _ = fmt.Fprintln(cli.ErrWriter, err)\n\t\t}\n\n\t\tosExiter(exitErr.ExitCode())\n\n\t\treturn nil\n\t}\n\n\treturn err\n}\n\n// InvalidValueError is used to wrap errors from `strconv` to make the error message more user friendly.\ntype InvalidValueError struct {\n\tunderlyingError error\n\tmsg             string\n}\n\nfunc (err InvalidValueError) Error() string {\n\treturn err.msg\n}\n\nfunc (err InvalidValueError) Unwrap() error {\n\treturn err.underlyingError\n}\n\nconst ErrMsgFlagUndefined = \"flag provided but not defined:\"\n\ntype UndefinedFlagError string\n\nfunc (flag UndefinedFlagError) Error() string {\n\treturn ErrMsgFlagUndefined + \" -\" + string(flag)\n}\n\nvar (\n\tErrMultipleTimesSettingFlag   = errors.New(\"setting the flag multiple times\")\n\tErrMultipleTimesSettingEnvVar = errors.New(\"setting the env var multiple times\")\n)\n\nfunc IsMultipleTimesSettingError(err error) bool {\n\treturn strings.Contains(err.Error(), ErrMultipleTimesSettingFlag.Error()) || strings.Contains(err.Error(), ErrMultipleTimesSettingEnvVar.Error())\n}\n"
  },
  {
    "path": "internal/clihelper/exit_code.go",
    "content": "package clihelper\n\n// Constants for exit codes.\nconst (\n\tExitCodeSuccess ExitCode = iota\n\tExitCodeGeneralError\n)\n\n// ExitCode is a number between 0 and 255, which is returned by any Unix command when it returns control to its parent process.\ntype ExitCode byte\n\n// ExitCoder is the interface checked by `App` and `Command` for a custom exit code.\ntype ExitCoder interface {\n\terror\n\tExitCode() int\n\tUnwrap() error\n}\n"
  },
  {
    "path": "internal/clihelper/flag.go",
    "content": "package clihelper\n\nimport (\n\t\"context\"\n\tlibflag \"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nvar (\n\t// FlagSplitter uses to separate arguments and env vars with multiple values.\n\tFlagSplitter = strings.Split\n)\n\n// FlagSetterFunc represents function type that is called when the flag is specified.\n// Unlike `FlagActionFunc` where the function is called after the value has been parsed and assigned to the `Destination` field,\n// `FlagSetterFunc` is called earlier, during the variable parsing.\n// if `FlagSetterFunc` returns the error, it will be wrapped with the flag or environment variable name.\n// Example:\n// `fmt.Errorf(\"invalid value \\\"invalid-value\\\" for env var TG_ENV_VAR: %w\", err)`\n// Therefore, using `FlagSetterFunc` is preferable to `FlagActionFunc` when you need to indicate in the error from where the value came from.\n// If the flag has multiple values, `FlagSetterFunc` will be called for each value.\ntype FlagSetterFunc[T any] func(value T) error\n\ntype MapFlagSetterFunc[K any, V any] func(key K, value V) error\n\n// FlagActionFunc represents function type that is called when the flag is specified.\n// Executed after flag have been parsed  and assigned to the `Destination` field.\ntype FlagActionFunc[T any] func(ctx context.Context, cliCtx *Context, value T) error\n\ntype FlagVariable[T any] interface {\n\tlibflag.Getter\n\tClone(dest *T) FlagVariable[T]\n}\n\ntype FlagValue interface {\n\tfmt.Stringer\n\n\tGet() any\n\n\tSet(str string) error\n\n\tGetter(name string) FlagValueGetter\n\n\tGetName() string\n\n\tGetInitialTextValue() string\n\n\t// IsSet returns true if the flag was set either by env var or CLI arg.\n\tIsSet() bool\n\n\t// IsArgSet returns true if the flag was set by CLI arg.\n\tIsArgSet() bool\n\n\t// IsEnvSet returns true if the flag was set by env var.\n\tIsEnvSet() bool\n\n\t// IsBoolFlag returns true if the flag is of type bool.\n\tIsBoolFlag() bool\n\n\t// IsNegativeBoolFlag returns true if the boolean flag's value should be inverted.\n\t// Example: For a flag with Negative=true, when set to true it returns false, and vice versa.\n\tIsNegativeBoolFlag() bool\n\n\t// MultipleSet returns true if the flag allows multiple assignments, such as slice/map.\n\tMultipleSet() bool\n}\n\ntype Flag interface {\n\t// `urfave/cli/v2` uses to generate help\n\tcli.DocGenerationFlag\n\n\t// Value returns the `FlagValue` interface for interacting with the flag value.\n\tValue() FlagValue\n\n\t// GetHidden returns true if the flag is hidden.\n\tGetHidden() bool\n\n\t// RunAction runs the flag action.\n\tRunAction(ctx context.Context, cliCtx *Context) error\n\n\t// LookupEnv gets and splits the environment variable depending on the flag type: common, map, slice.\n\tLookupEnv(envVar string) []string\n\n\t// AllowedSubcommandScope returns true if the flag is allowed to be specified in subcommands,\n\t// and not only after the command it belongs to.\n\tAllowedSubcommandScope() bool\n\n\t// Parse parses the given args and environment variables to set the flag value.\n\tParse(args Args) error\n}\n\ntype LookupEnvFuncType func(key string) []string\n\ntype FlagValueGetter interface {\n\tlibflag.Getter\n\n\tEnvSet(str string) error\n}\n\ntype flagValueGetter struct {\n\t*flagValue\n\tvalueName string\n}\n\nfunc (flag *flagValueGetter) EnvSet(val string) error {\n\tvar err error\n\n\tif !flag.envHasBeenSet {\n\t\t// may contain a default value or an env var, so it needs to be cleared before the first setting.\n\t\tflag.value.Reset()\n\t\tflag.envHasBeenSet = true\n\t} else if !flag.multipleSet {\n\t\terr = errors.New(ErrMultipleTimesSettingEnvVar)\n\t}\n\n\tflag.name = flag.valueName\n\n\tif err := flag.value.Set(val); err != nil {\n\t\treturn err\n\t}\n\n\treturn err\n}\n\nfunc (flag *flagValueGetter) Set(val string) error {\n\tvar err error\n\n\tif !flag.hasBeenSet {\n\t\t// may contain a default value or an env var, so it needs to be cleared before the first setting.\n\t\tflag.value.Reset()\n\t\tflag.hasBeenSet = true\n\t} else if !flag.multipleSet {\n\t\terr = errors.New(ErrMultipleTimesSettingFlag)\n\t}\n\n\tflag.name = flag.valueName\n\n\tif err := flag.value.Set(val); err != nil {\n\t\treturn err\n\t}\n\n\treturn err\n}\n\ntype Value interface {\n\tlibflag.Getter\n\tReset()\n}\n\n// flag is a common flag related to parsing flags in cli.\ntype flagValue struct {\n\tvalue            Value\n\tname             string\n\tinitialTextValue string\n\tmultipleSet      bool\n\thasBeenSet       bool\n\tenvHasBeenSet    bool\n\tnegative         bool\n}\n\nfunc (flag *flagValue) MultipleSet() bool {\n\treturn flag.multipleSet\n}\n\n// IsBoolFlag implements `cli.FlagValue` interface.\nfunc (flag *flagValue) IsBoolFlag() bool {\n\t_, ok := flag.value.Get().(bool)\n\treturn ok\n}\n\n// IsNegativeBoolFlag implements `cli.FlagValue` interface.\nfunc (flag *flagValue) IsNegativeBoolFlag() bool {\n\treturn flag.negative\n}\n\nfunc (flag *flagValue) Get() any {\n\treturn flag.value.Get()\n}\n\nfunc (flag *flagValue) Set(str string) error {\n\treturn (&flagValueGetter{flagValue: flag}).Set(str)\n}\n\nfunc (flag *flagValue) String() string {\n\tif val := flag.value.Get(); val == nil {\n\t\treturn \"\"\n\t}\n\n\treturn flag.value.String()\n}\n\nfunc (flag *flagValue) GetInitialTextValue() string {\n\treturn flag.initialTextValue\n}\n\nfunc (flag *flagValue) IsSet() bool {\n\treturn flag.hasBeenSet || flag.envHasBeenSet\n}\n\nfunc (flag *flagValue) IsArgSet() bool {\n\treturn flag.hasBeenSet\n}\n\nfunc (flag *flagValue) IsEnvSet() bool {\n\treturn flag.envHasBeenSet\n}\n\nfunc (flag *flagValue) GetName() string {\n\treturn flag.name\n}\n\nfunc (flag *flagValue) Getter(name string) FlagValueGetter {\n\treturn &flagValueGetter{flagValue: flag, valueName: name}\n}\n\n// flag is a common flag related to parsing flags in cli.\ntype flag struct {\n\tFlagValue\n\tLookupEnvFunc LookupEnvFuncType\n}\n\n// Parse implements `Flag` interface.\nfunc (flag *flag) Parse(args Args) error {\n\treturn nil\n}\n\nfunc (flag *flag) LookupEnv(envVar string) []string {\n\tif flag.LookupEnvFunc == nil {\n\t\tflag.LookupEnvFunc = func(key string) []string {\n\t\t\tif val, ok := os.LookupEnv(key); ok {\n\t\t\t\treturn []string{val}\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn flag.LookupEnvFunc(envVar)\n}\n\nfunc (flag *flag) Value() FlagValue {\n\treturn flag.FlagValue\n}\n\n// TakesValue returns true if the flag needs to be given a value.\n// Implements `cli.DocGenerationFlag.TakesValue` required to generate help.\nfunc (flag *flag) TakesValue() bool {\n\treturn true\n}\n\n// GetValue returns the flags value as string representation and an empty\n// string if the flag takes no value at all.\n// Implements `cli.DocGenerationFlag.GetValue` required to generate help.\nfunc (flag *flag) GetValue() string {\n\treturn flag.String()\n}\n\n// GetCategory returns the category for the flag.\n// Implements `cli.DocGenerationFlag.GetCategory` required to generate help.\nfunc (flag *flag) GetCategory() string {\n\treturn \"\"\n}\n\n// AllowedSubcommandScope implements `cli.Flag` interface.\nfunc (flag *flag) AllowedSubcommandScope() bool {\n\treturn true\n}\n\nfunc ApplyFlag(flag Flag, set *libflag.FlagSet) error {\n\tfor _, name := range flag.GetEnvVars() {\n\t\tfor _, val := range flag.LookupEnv(name) {\n\t\t\tif val == \"\" || (flag.Value().IsEnvSet() && !flag.Value().MultipleSet()) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif err := flag.Value().Getter(name).EnvSet(val); err != nil {\n\t\t\t\treturn errors.Errorf(\"invalid value %q for env var %s: %w\", val, name, err)\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, name := range flag.Names() {\n\t\tif name != \"\" {\n\t\t\tset.Var(flag.Value().Getter(name), name, flag.GetUsage())\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/clihelper/flag_test.go",
    "content": "package clihelper_test\n\nfunc mockDestValue[T any](val T) *T {\n\treturn &val\n}\n"
  },
  {
    "path": "internal/clihelper/flags.go",
    "content": "package clihelper\n\nimport (\n\t\"context\"\n\tlibflag \"flag\"\n\t\"io\"\n\t\"sort\"\n\n\t\"github.com/gruntwork-io/go-commons/collections\"\n)\n\ntype Flags []Flag\n\nfunc (flags Flags) Parse(args Args) error {\n\tfor _, flag := range flags {\n\t\tif err := flag.Parse(args); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (flags Flags) NewFlagSet(cmdName string, errHandler func(err error) error) (*libflag.FlagSet, error) {\n\tflagSet := libflag.NewFlagSet(cmdName, libflag.ContinueOnError)\n\tflagSet.SetOutput(io.Discard)\n\n\terr := flags.Apply(flagSet, errHandler)\n\n\treturn flagSet, err\n}\n\nfunc (flags Flags) Apply(flagSet *libflag.FlagSet, errHandler func(err error) error) error {\n\tfor _, flag := range flags {\n\t\tif err := flag.Apply(flagSet); err != nil {\n\t\t\tif err = errHandler(err); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Get returns a Flag by the given name.\nfunc (flags Flags) Get(name string) Flag {\n\tfor _, flag := range flags {\n\t\tif collections.ListContainsElement(flag.Names(), name) {\n\t\t\treturn flag\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Filter returns a list of flags filtered by the given names.\nfunc (flags Flags) Filter(names ...string) Flags {\n\tvar filtered = make(Flags, 0, len(names))\n\n\tfor _, flag := range flags {\n\t\tfor _, name := range names {\n\t\t\tif collections.ListContainsElement(flag.Names(), name) {\n\t\t\t\tfiltered = append(filtered, flag)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn filtered\n}\n\n// Add adds a new flag to the list.\nfunc (flags Flags) Add(newFlags ...Flag) Flags {\n\treturn append(flags, newFlags...)\n}\n\n// VisibleFlags returns a slice of the Flags.\n// Used by `urfave/cli` package to generate help.\nfunc (flags Flags) VisibleFlags() Flags {\n\tvar visibleFlags = make(Flags, 0, len(flags))\n\n\tfor _, flag := range flags {\n\t\tif !flag.GetHidden() && len(flag.Names()) > 0 {\n\t\t\tvisibleFlags = append(visibleFlags, flag)\n\t\t}\n\t}\n\n\treturn visibleFlags\n}\n\nfunc (flags Flags) Len() int {\n\treturn len(flags)\n}\n\nfunc (flags Flags) Less(i, j int) bool {\n\tif len(flags[j].Names()) == 0 {\n\t\treturn false\n\t} else if len(flags[i].Names()) == 0 {\n\t\treturn true\n\t}\n\n\treturn LexicographicLess(flags[i].Names()[0], flags[j].Names()[0])\n}\n\nfunc (flags Flags) Swap(i, j int) {\n\tflags[i], flags[j] = flags[j], flags[i]\n}\n\nfunc (flags Flags) RunActions(ctx context.Context, cliCtx *Context) error {\n\tfor _, flag := range flags {\n\t\tif flag.Value().IsSet() {\n\t\t\tif err := flag.RunAction(ctx, cliCtx); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (flags Flags) Sort() Flags {\n\tsort.Sort(flags)\n\n\treturn flags\n}\n\nfunc (flags Flags) WithSubcommandScope() Flags {\n\tvar filtered Flags\n\n\tfor _, flag := range flags {\n\t\tif flag.AllowedSubcommandScope() {\n\t\t\tfiltered = append(filtered, flag)\n\t\t}\n\t}\n\n\treturn filtered\n}\n\nfunc (flags Flags) Names() []string {\n\tnames := make([]string, 0, len(flags))\n\n\tfor _, flag := range flags {\n\t\tnames = append(names, flag.Names()...)\n\t}\n\n\treturn names\n}\n"
  },
  {
    "path": "internal/clihelper/flags_test.go",
    "content": "package clihelper_test\n\nimport (\n\t\"context\"\n\tlibflag \"flag\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar (\n\tmockFlagFoo = &clihelper.GenericFlag[string]{Name: \"foo\"}\n\tmockFlagBar = &clihelper.SliceFlag[string]{Name: \"bar\"}\n\tmockFlagBaz = &clihelper.MapFlag[string, string]{Name: \"baz\"}\n\n\tnewMockFlags = func() clihelper.Flags {\n\t\treturn clihelper.Flags{\n\t\t\tmockFlagFoo,\n\t\t\tmockFlagBar,\n\t\t\tmockFlagBaz,\n\t\t}\n\t}\n)\n\nfunc TestFalgsGet(t *testing.T) {\n\tt.Parallel()\n\n\tactual := newMockFlags().Get(\"bar\")\n\texpected := clihelper.Flag(mockFlagBar)\n\tassert.Equal(t, expected, actual)\n\n\tactual = newMockFlags().Get(\"break\")\n\texpected = nil\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestFalgsAdd(t *testing.T) {\n\tt.Parallel()\n\n\ttestNewFlag := &clihelper.GenericFlag[string]{Name: \"qux\"}\n\n\tactual := newMockFlags()\n\tactual = actual.Add(testNewFlag)\n\n\texpected := append(newMockFlags(), testNewFlag)\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestFalgsFilter(t *testing.T) {\n\tt.Parallel()\n\n\tactual := newMockFlags().Filter(([]string{\"bar\", \"baz\"})...)\n\texpected := clihelper.Flags{mockFlagBar, mockFlagBaz}\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestFalgsRunActions(t *testing.T) {\n\tt.Parallel()\n\n\tvar actionHasBeenRun bool\n\n\tmockFlags := clihelper.Flags{\n\t\t&clihelper.SliceFlag[string]{Name: \"bar\"},\n\t\t&clihelper.GenericFlag[string]{Name: \"foo\", Action: func(ctx context.Context, cliCtx *clihelper.Context, val string) error {\n\t\t\tactionHasBeenRun = true\n\t\t\treturn nil\n\t\t}},\n\t}\n\n\tflagSet := libflag.NewFlagSet(\"test-cmd\", libflag.ContinueOnError)\n\tflagSet.SetOutput(io.Discard)\n\n\tfor _, flag := range mockFlags {\n\t\terr := flag.Apply(flagSet)\n\t\trequire.NoError(t, err)\n\n\t\terr = flag.Value().Set(\"value\")\n\t\trequire.NoError(t, err)\n\t}\n\n\tassert.False(t, actionHasBeenRun)\n\n\terr := mockFlags.RunActions(t.Context(), nil)\n\trequire.NoError(t, err)\n\n\tassert.True(t, actionHasBeenRun)\n}\n"
  },
  {
    "path": "internal/clihelper/funcs.go",
    "content": "package clihelper\n\nimport \"context\"\n\n// CompleteFunc is an action to execute when the shell completion flag is set\ntype CompleteFunc func(ctx context.Context, cliCtx *Context) error\n\n// ActionFunc is the action to execute when no commands/subcommands are specified.\ntype ActionFunc func(ctx context.Context, cliCtx *Context) error\n\n// HelpFunc is the action to execute when help needs to be displayed.\n// Example:\n//\n//\tfunc showHelp(ctx context.Context, cliCtx *Context) error {\n//\t  fmt.Println(\"Usage: ...\")\n//\t  return nil\n//\t}\ntype HelpFunc func(ctx context.Context, cliCtx *Context) error\n\n// SplitterFunc is used to parse flags containing multiple values.\ntype SplitterFunc func(s, sep string) []string\n\n// ExitErrHandlerFunc is executed if provided in order to handle exitError values\n// returned by Actions and Before/After functions.\ntype ExitErrHandlerFunc func(ctx *Context, err error) error\n\n// FlagErrHandlerFunc is executed if an error occurs while parsing flags.\ntype FlagErrHandlerFunc func(ctx *Context, err error) error\n"
  },
  {
    "path": "internal/clihelper/generic_flag.go",
    "content": "package clihelper\n\nimport (\n\t\"context\"\n\tlibflag \"flag\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// GenericFlag implements Flag\nvar _ Flag = new(GenericFlag[string])\n\ntype GenericType interface {\n\tstring | int | int64 | uint\n}\n\ntype GenericFlag[T GenericType] struct {\n\tflag\n\n\t// Action is a function that is called when the flag is specified. It is executed only after all command flags have been parsed.\n\tAction FlagActionFunc[T]\n\n\t// Setter allows to set a value to any type by calling its `func(bool) error` function.\n\tSetter FlagSetterFunc[T]\n\n\t// Destination is a pointer to which the value of the flag or env var is assigned.\n\tDestination *T\n\n\t// Name is the name of the flag.\n\tName string\n\n\t// DefaultText is the default value of the flag to display in the help, if it is empty, the value is taken from `Destination`.\n\tDefaultText string\n\n\t// Usage is a short usage description to display in help.\n\tUsage string\n\n\t// Aliases are usually used for the short flag name, like `-h`.\n\tAliases []string\n\n\t// EnvVars are the names of the env variables that are parsed and assigned to `Destination` before the flag value.\n\tEnvVars []string\n\n\t// Hidden hides the flag from the help.\n\tHidden bool\n}\n\n// Apply applies Flag settings to the given flag set.\nfunc (flag *GenericFlag[T]) Apply(set *libflag.FlagSet) error {\n\tif flag.FlagValue != nil {\n\t\treturn ApplyFlag(flag, set)\n\t}\n\n\tif flag.Destination == nil {\n\t\tflag.Destination = new(T)\n\t}\n\n\tvalueType := &genericVar[T]{dest: flag.Destination}\n\tvalue := newGenericValue(valueType, flag.Setter)\n\n\tflag.FlagValue = &flagValue{\n\t\tvalue:            value,\n\t\tinitialTextValue: value.String(),\n\t}\n\n\treturn ApplyFlag(flag, set)\n}\n\n// GetHidden returns true if the flag should be hidden from the help.\nfunc (flag *GenericFlag[T]) GetHidden() bool {\n\treturn flag.Hidden\n}\n\n// GetUsage returns the usage string for the flag.\nfunc (flag *GenericFlag[T]) GetUsage() string {\n\treturn flag.Usage\n}\n\n// GetEnvVars implements `cli.Flag` interface.\nfunc (flag *GenericFlag[T]) GetEnvVars() []string {\n\treturn flag.EnvVars\n}\n\n// GetDefaultText returns the flags value as string representation and an empty string if the flag takes no value at all.\nfunc (flag *GenericFlag[T]) GetDefaultText() string {\n\tif flag.DefaultText == \"\" && flag.FlagValue != nil {\n\t\treturn flag.GetInitialTextValue()\n\t}\n\n\treturn flag.DefaultText\n}\n\n// String returns a readable representation of this value (for usage defaults).\nfunc (flag *GenericFlag[T]) String() string {\n\treturn cli.FlagStringer(flag)\n}\n\n// Names returns the names of the flag.\nfunc (flag *GenericFlag[T]) Names() []string {\n\tif flag.Name == \"\" {\n\t\treturn flag.Aliases\n\t}\n\n\treturn append([]string{flag.Name}, flag.Aliases...)\n}\n\n// RunAction implements ActionableFlag.RunAction\nfunc (flag *GenericFlag[T]) RunAction(ctx context.Context, cliCtx *Context) error {\n\tdest := flag.Destination\n\tif dest == nil {\n\t\tdest = new(T)\n\t}\n\n\tif flag.Action != nil {\n\t\treturn flag.Action(ctx, cliCtx, *dest)\n\t}\n\n\treturn nil\n}\n\nvar _ = Value(new(genericValue[string]))\n\n// -- generic Value\ntype genericValue[T comparable] struct {\n\tsetter FlagSetterFunc[T]\n\tvalue  FlagVariable[T]\n}\n\nfunc newGenericValue[T comparable](value FlagVariable[T], setter FlagSetterFunc[T]) *genericValue[T] {\n\treturn &genericValue[T]{\n\t\tsetter: setter,\n\t\tvalue:  value,\n\t}\n}\n\nfunc (flag *genericValue[T]) Reset() {}\n\nfunc (flag *genericValue[T]) Set(str string) error {\n\tif err := flag.value.Set(str); err != nil {\n\t\treturn err\n\t}\n\n\tif flag.setter != nil {\n\t\treturn flag.setter(flag.Get().(T))\n\t}\n\n\treturn nil\n}\n\nfunc (flag *genericValue[T]) Get() any {\n\treturn flag.value.Get()\n}\n\nfunc (flag *genericValue[T]) String() string {\n\treturn flag.value.String()\n}\n\nvar _ = FlagVariable[string](new(genericVar[string]))\n\n// -- generic Type\ntype genericVar[T comparable] struct {\n\tdest *T\n}\n\nfunc (val *genericVar[T]) Clone(dest *T) FlagVariable[T] {\n\tif dest == nil {\n\t\tdest = new(T)\n\t}\n\n\treturn &genericVar[T]{dest: dest}\n}\n\nfunc (val *genericVar[T]) Set(str string) error {\n\tif val.dest == nil {\n\t\tval.dest = new(T)\n\t}\n\n\tswitch dest := (any)(val.dest).(type) {\n\tcase *string:\n\t\t*dest = str\n\n\tcase *bool:\n\t\tv, err := strconv.ParseBool(str)\n\t\tif err != nil {\n\t\t\treturn errors.New(InvalidValueError{underlyingError: err, msg: `must be one of: \"0\", \"1\", \"f\", \"t\", \"false\", \"true\"`})\n\t\t}\n\n\t\t*dest = v\n\n\tcase *int:\n\t\tv, err := strconv.ParseInt(str, 0, strconv.IntSize)\n\t\tif err != nil {\n\t\t\treturn errors.New(InvalidValueError{underlyingError: err, msg: \"must be 32-bit integer\"})\n\t\t}\n\n\t\t*dest = int(v)\n\n\tcase *uint:\n\t\tv, err := strconv.ParseUint(str, 10, 64)\n\t\tif err != nil {\n\t\t\treturn errors.New(InvalidValueError{underlyingError: err, msg: \"must be 32-bit unsigned integer\"})\n\t\t}\n\n\t\t*dest = uint(v)\n\n\tcase *int64:\n\t\tv, err := strconv.ParseInt(str, 0, 64)\n\t\tif err != nil {\n\t\t\treturn errors.New(InvalidValueError{underlyingError: err, msg: \"must be 64-bit integer\"})\n\t\t}\n\n\t\t*dest = v\n\n\tdefault:\n\t\treturn errors.Errorf(\"flag type %T is undefined\", dest)\n\t}\n\n\treturn nil\n}\n\nfunc (val *genericVar[T]) Get() any {\n\tif val.dest == nil {\n\t\treturn *new(T)\n\t}\n\n\treturn *val.dest\n}\n\n// String returns a readable representation of this value\nfunc (val *genericVar[T]) String() string {\n\tif val.dest == nil {\n\t\treturn \"\"\n\t}\n\n\tformat := \"%v\"\n\tif _, ok := val.Get().(bool); ok {\n\t\tformat = \"%t\"\n\t}\n\n\treturn fmt.Sprintf(format, *val.dest)\n}\n"
  },
  {
    "path": "internal/clihelper/generic_flag_test.go",
    "content": "package clihelper_test\n\nimport (\n\t\"errors\"\n\tlibflag \"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGenericFlagStringApply(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedErr   error\n\t\tenvs          map[string]string\n\t\texpectedValue string\n\t\targs          []string\n\t\tflag          clihelper.GenericFlag[string]\n\t}{\n\t\t{\n\t\t\tflag:          clihelper.GenericFlag[string]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:          []string{\"--foo\", \"arg-value\"},\n\t\t\tenvs:          map[string]string{\"FOO\": \"env-value\"},\n\t\t\texpectedValue: \"arg-value\",\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.GenericFlag[string]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\tenvs:          map[string]string{\"FOO\": \"env-value\"},\n\t\t\texpectedValue: \"env-value\",\n\t\t},\n\t\t{\n\t\t\tflag: clihelper.GenericFlag[string]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.GenericFlag[string]{Name: \"foo\", EnvVars: []string{\"FOO\"}, Destination: mockDestValue(\"default-value\")},\n\t\t\targs:          []string{\"--foo\", \"arg-value\"},\n\t\t\tenvs:          map[string]string{\"FOO\": \"env-value\"},\n\t\t\texpectedValue: \"arg-value\",\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.GenericFlag[string]{Name: \"foo\", Destination: mockDestValue(\"default-value\")},\n\t\t\texpectedValue: \"default-value\",\n\t\t},\n\t\t{\n\t\t\tflag:        clihelper.GenericFlag[string]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:        []string{\"--foo\", \"arg-value1\", \"--foo\", \"arg-value2\"},\n\t\t\texpectedErr: errors.New(`invalid value \"arg-value2\" for flag -foo: setting the flag multiple times`),\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttestGenericFlagApply(t, &tc.flag, tc.args, tc.envs, tc.expectedValue, tc.expectedErr)\n\t\t})\n\t}\n}\n\nfunc TestGenericFlagIntApply(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedErr   error\n\t\tenvs          map[string]string\n\t\targs          []string\n\t\tflag          clihelper.GenericFlag[int]\n\t\texpectedValue int\n\t}{\n\t\t{\n\t\t\tflag:          clihelper.GenericFlag[int]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:          []string{\"--foo\", \"10\"},\n\t\t\tenvs:          map[string]string{\"FOO\": \"20\"},\n\t\t\texpectedValue: 10,\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.GenericFlag[int]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:          []string{},\n\t\t\tenvs:          map[string]string{\"FOO\": \"20\"},\n\t\t\texpectedValue: 20,\n\t\t},\n\t\t{\n\t\t\tflag:        clihelper.GenericFlag[int]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:        []string{},\n\t\t\tenvs:        map[string]string{\"FOO\": \"monkey\"},\n\t\t\texpectedErr: errors.New(`invalid value \"monkey\" for env var FOO: must be 32-bit integer`),\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.GenericFlag[int]{Name: \"foo\", Destination: mockDestValue(55)},\n\t\t\texpectedValue: 55,\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttestGenericFlagApply(t, &tc.flag, tc.args, tc.envs, tc.expectedValue, tc.expectedErr)\n\t\t})\n\t}\n}\n\nfunc TestGenericFlagInt64Apply(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedErr   error\n\t\tenvs          map[string]string\n\t\targs          []string\n\t\tflag          clihelper.GenericFlag[int64]\n\t\texpectedValue int64\n\t}{\n\t\t{\n\t\t\tflag:          clihelper.GenericFlag[int64]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:          []string{\"--foo\", \"10\"},\n\t\t\tenvs:          map[string]string{\"FOO\": \"20\"},\n\t\t\texpectedValue: 10,\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.GenericFlag[int64]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:          []string{},\n\t\t\tenvs:          map[string]string{\"FOO\": \"20\"},\n\t\t\texpectedValue: 20,\n\t\t},\n\t\t{\n\t\t\tflag:        clihelper.GenericFlag[int64]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:        []string{},\n\t\t\tenvs:        map[string]string{\"FOO\": \"monkey\"},\n\t\t\texpectedErr: errors.New(`invalid value \"monkey\" for env var FOO: must be 64-bit integer`),\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.GenericFlag[int64]{Name: \"foo\", Destination: mockDestValue(int64(55))},\n\t\t\texpectedValue: 55,\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttestGenericFlagApply(t, &tc.flag, tc.args, tc.envs, tc.expectedValue, tc.expectedErr)\n\t\t})\n\t}\n}\n\nfunc testGenericFlagApply[T clihelper.GenericType](t *testing.T, flag *clihelper.GenericFlag[T], args []string, envs map[string]string, expectedValue T, expectedErr error) {\n\tt.Helper()\n\n\tvar (\n\t\tactualValue          T\n\t\texpectedDefaultValue string\n\t)\n\n\tif flag.Destination == nil {\n\t\tflag.Destination = new(T)\n\t}\n\n\texpectedDefaultValue = fmt.Sprintf(\"%v\", *flag.Destination)\n\n\tflag.LookupEnvFunc = func(key string) []string {\n\t\tif envs == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tif val, ok := envs[key]; ok {\n\t\t\treturn []string{val}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tflagSet := libflag.NewFlagSet(\"test-cmd\", libflag.ContinueOnError)\n\tflagSet.SetOutput(io.Discard)\n\n\terr := flag.Apply(flagSet)\n\tif err == nil {\n\t\terr = flagSet.Parse(args)\n\t}\n\n\tif expectedErr != nil {\n\t\trequire.Error(t, err)\n\t\trequire.ErrorContains(t, expectedErr, err.Error())\n\n\t\treturn\n\t}\n\n\trequire.NoError(t, err)\n\n\tactualValue = (flag.Value().Get()).(T)\n\n\tassert.Equal(t, expectedValue, actualValue)\n\tassert.Equal(t, fmt.Sprintf(\"%v\", expectedValue), flag.GetValue(), \"GetValue()\")\n\n\tassert.Equal(t, len(args) > 0 || len(envs) > 0, flag.Value().IsSet(), \"IsSet()\")\n\tassert.Equal(t, expectedDefaultValue, flag.GetInitialTextValue(), \"GetDefaultText()\")\n\n\tassert.False(t, flag.Value().IsBoolFlag(), \"IsBoolFlag()\")\n\tassert.True(t, flag.TakesValue(), \"TakesValue()\")\n}\n"
  },
  {
    "path": "internal/clihelper/help.go",
    "content": "package clihelper\n\nimport (\n\t\"context\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"maps\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/urfave/cli/v2\"\n)\n\nvar (\n\t// AppVersionTemplate is the text template for the Default version topic.\n\tAppVersionTemplate = \"\"\n\n\t// AppHelpTemplate is the text template for the Default help topic.\n\tAppHelpTemplate = \"\"\n\n\t// CommandHelpTemplate is the text template for the command help topic.\n\tCommandHelpTemplate = \"\"\n)\n\n// ShowAppHelp prints App help.\nfunc ShowAppHelp(_ context.Context, cliCtx *Context) error {\n\ttpl := cliCtx.CustomAppHelpTemplate\n\tif tpl == \"\" {\n\t\ttpl = AppHelpTemplate\n\t}\n\n\tif tpl == \"\" {\n\t\treturn errors.Errorf(\"app help template not defined\")\n\t}\n\n\tif cliCtx.HelpName == \"\" {\n\t\tcliCtx.HelpName = cliCtx.Name\n\t}\n\n\tcli.HelpPrinterCustom(cliCtx.Writer, tpl, cliCtx, map[string]any{\n\t\t\"parentCommands\": parentCommands,\n\t\t\"offsetCommands\": offsetCommands,\n\t})\n\n\treturn NewExitError(nil, ExitCodeSuccess)\n}\n\n// ShowCommandHelp prints command help for the given `cliCtx`.\nfunc ShowCommandHelp(ctx context.Context, cliCtx *Context) error {\n\tif cliCtx.Command.HelpName == \"\" {\n\t\tcliCtx.Command.HelpName = cliCtx.Command.Name\n\t}\n\n\tif cliCtx.Command.CustomHelp != nil {\n\t\tif err := cliCtx.Command.CustomHelp(ctx, cliCtx); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn NewExitError(nil, ExitCodeSuccess)\n\t}\n\n\ttpl := cliCtx.Command.CustomHelpTemplate\n\tif tpl == \"\" {\n\t\ttpl = CommandHelpTemplate\n\t}\n\n\tif tpl == \"\" {\n\t\treturn errors.Errorf(\"command help template not defined\")\n\t}\n\n\tHelpPrinterCustom(cliCtx, tpl, nil)\n\n\treturn NewExitError(nil, ExitCodeSuccess)\n}\n\nfunc HelpPrinterCustom(cliCtx *Context, tpl string, customFuncs map[string]any) {\n\tvar funcs = map[string]any{\n\t\t\"parentCommands\": parentCommands,\n\t\t\"offsetCommands\": offsetCommands,\n\t}\n\n\tif customFuncs != nil {\n\t\tmaps.Copy(funcs, customFuncs)\n\t}\n\n\tcli.HelpPrinterCustom(cliCtx.Writer, tpl, cliCtx, funcs)\n}\n\nfunc ShowVersion(_ context.Context, cliCtx *Context) error {\n\ttpl := cliCtx.CustomAppVersionTemplate\n\tif tpl == \"\" {\n\t\ttpl = AppVersionTemplate\n\t}\n\n\tif tpl == \"\" {\n\t\treturn errors.Errorf(\"app version template not defined\")\n\t}\n\n\tcli.HelpPrinterCustom(cliCtx.Writer, tpl, cliCtx, nil)\n\n\treturn NewExitError(nil, ExitCodeSuccess)\n}\n\nfunc parentCommands(ctx *Context) Commands {\n\tvar cmds Commands\n\n\tfor parent := ctx.Parent(); parent != nil; parent = parent.Parent() {\n\t\tif cmd := parent.Command; cmd != nil {\n\t\t\tif cmd.HelpName == \"\" {\n\t\t\t\tcmd.HelpName = cmd.Name\n\t\t\t}\n\n\t\t\tcmds = append(cmds, cmd)\n\t\t}\n\t}\n\n\tslices.Reverse(cmds)\n\n\treturn cmds\n}\n\n// offsetCommands tries to find the max width of the names column.\nfunc offsetCommands(cmds Commands, fixed int) int {\n\tvar width = 0\n\n\tfor _, cmd := range cmds {\n\t\ts := strings.Join(cmd.Names(), \", \")\n\t\tif len(s) > width {\n\t\t\twidth = len(s)\n\t\t}\n\t}\n\n\treturn width + fixed\n}\n"
  },
  {
    "path": "internal/clihelper/map_flag.go",
    "content": "package clihelper\n\nimport (\n\t\"context\"\n\tlibflag \"flag\"\n\t\"os\"\n\t\"strings\"\n\n\t\"maps\"\n\n\t\"github.com/gruntwork-io/go-commons/collections\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/urfave/cli/v2\"\n)\n\n// MapFlag implements Flag\nvar _ Flag = new(MapFlag[string, string])\n\nvar (\n\tMapFlagEnvVarSep = \",\"\n\tMapFlagKeyValSep = \"=\"\n\tflatPatsCount    = 2\n)\n\ntype MapFlagKeyType interface {\n\tGenericType\n}\n\ntype MapFlagValueType interface {\n\tGenericType | bool\n}\n\n// MapFlag is a key value flag.\ntype MapFlag[K MapFlagKeyType, V MapFlagValueType] struct {\n\tflag\n\n\t// Splitter is a function that is called when the flag is specified. It is executed only after all command flags have been parsed.\n\tSplitter SplitterFunc\n\n\t// Action is a function that is called when the flag is specified. It is executed only after all command flags have been parsed.\n\tAction FlagActionFunc[map[K]V]\n\n\t// Setter represents the function that is called when the flag is specified.\n\tSetter MapFlagSetterFunc[K, V]\n\n\t// Destination is a pointer to which the value of the flag or env var is assigned.\n\tDestination *map[K]V\n\n\t// DefaultText is the default value of the flag to display in the help, if it is empty, the value is taken from `Destination`.\n\tDefaultText string\n\n\t// Usage is a short usage description to display in help.\n\tUsage string\n\n\t// Name is the name of the flag.\n\tName string\n\n\t// EnvVarSep is the separator used to split the env var value.\n\tEnvVarSep string\n\n\t// KeyValSep is the separator used to split the key and value of the flag.\n\tKeyValSep string\n\n\t// Aliases are usually used for the short flag name, like `-h`.\n\tAliases []string\n\n\t// EnvVars are the names of the env variables that are parsed and assigned to `Destination` before the flag value.\n\tEnvVars []string\n\n\t// Hidden hides the flag from the help.\n\tHidden bool\n}\n\n// Apply applies Flag settings to the given flag set.\nfunc (flag *MapFlag[K, V]) Apply(set *libflag.FlagSet) error {\n\tif flag.FlagValue != nil {\n\t\treturn ApplyFlag(flag, set)\n\t}\n\n\tif flag.Destination == nil {\n\t\tdest := make(map[K]V)\n\t\tflag.Destination = &dest\n\t}\n\n\tif flag.Splitter == nil {\n\t\tflag.Splitter = FlagSplitter\n\t}\n\n\tif flag.EnvVarSep == \"\" {\n\t\tflag.EnvVarSep = MapFlagEnvVarSep\n\t}\n\n\tif flag.KeyValSep == \"\" {\n\t\tflag.KeyValSep = MapFlagKeyValSep\n\t}\n\n\tif flag.LookupEnvFunc == nil {\n\t\tflag.LookupEnvFunc = func(key string) []string {\n\t\t\tif val, ok := os.LookupEnv(key); ok {\n\t\t\t\treturn flag.Splitter(val, flag.EnvVarSep)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tkeyType := FlagVariable[K](new(genericVar[K]))\n\tvalType := FlagVariable[V](new(genericVar[V]))\n\n\tvalue := newMapValue(keyType, valType, flag.EnvVarSep, flag.KeyValSep, flag.Splitter, flag.Destination, flag.Setter)\n\n\tflag.FlagValue = &flagValue{\n\t\tmultipleSet:      true,\n\t\tvalue:            value,\n\t\tinitialTextValue: value.String(),\n\t}\n\n\treturn ApplyFlag(flag, set)\n}\n\n// GetHidden returns true if the flag should be hidden from the help.\nfunc (flag *MapFlag[K, V]) GetHidden() bool {\n\treturn flag.Hidden\n}\n\n// GetUsage returns the usage string for the flag.\nfunc (flag *MapFlag[K, V]) GetUsage() string {\n\treturn flag.Usage\n}\n\n// GetEnvVars implements `cli.Flag` interface.\nfunc (flag *MapFlag[K, V]) GetEnvVars() []string {\n\treturn flag.EnvVars\n}\n\n// GetDefaultText returns the flags value as string representation and an empty string if the flag takes no value at all.\nfunc (flag *MapFlag[K, V]) GetDefaultText() string {\n\tif flag.DefaultText == \"\" && flag.FlagValue != nil {\n\t\treturn flag.GetInitialTextValue()\n\t}\n\n\treturn flag.DefaultText\n}\n\n// String returns a readable representation of this value (for usage defaults).\nfunc (flag *MapFlag[K, V]) String() string {\n\treturn cli.FlagStringer(flag)\n}\n\n// Names returns the names of the flag.\nfunc (flag *MapFlag[K, V]) Names() []string {\n\tif flag.Name == \"\" {\n\t\treturn flag.Aliases\n\t}\n\n\treturn append([]string{flag.Name}, flag.Aliases...)\n}\n\n// RunAction implements ActionableFlag.RunAction\nfunc (flag *MapFlag[K, V]) RunAction(ctx context.Context, cliCtx *Context) error {\n\tif flag.Action != nil {\n\t\treturn flag.Action(ctx, cliCtx, *flag.Destination)\n\t}\n\n\treturn nil\n}\n\nvar _ = Value(new(mapValue[string, string]))\n\ntype mapValue[K, V comparable] struct {\n\tkeyType  FlagVariable[K]\n\tvalType  FlagVariable[V]\n\tvalues   *map[K]V\n\tsetter   MapFlagSetterFunc[K, V]\n\tsplitter SplitterFunc\n\targSep   string\n\tvalSep   string\n}\n\nfunc newMapValue[K, V comparable](keyType FlagVariable[K], valType FlagVariable[V], argSep, valSep string, splitter SplitterFunc, dest *map[K]V, setter MapFlagSetterFunc[K, V]) *mapValue[K, V] {\n\treturn &mapValue[K, V]{\n\t\tvalues:   dest,\n\t\tsetter:   setter,\n\t\tkeyType:  keyType,\n\t\tvalType:  valType,\n\t\targSep:   argSep,\n\t\tvalSep:   valSep,\n\t\tsplitter: splitter,\n\t}\n}\n\nfunc (flag *mapValue[K, V]) Reset() {\n\t*flag.values = map[K]V{}\n}\n\nfunc (flag *mapValue[K, V]) Set(str string) error {\n\tparts := flag.splitter(str, flag.valSep)\n\tif len(parts) != flatPatsCount {\n\t\treturn errors.New(NewInvalidKeyValueError(flag.valSep, str))\n\t}\n\n\tkey := flag.keyType.Clone(new(K))\n\tif err := key.Set(strings.TrimSpace(parts[0])); err != nil {\n\t\treturn err\n\t}\n\n\tval := flag.valType.Clone(new(V))\n\tif err := val.Set(strings.TrimSpace(parts[1])); err != nil {\n\t\treturn err\n\t}\n\n\t(*flag.values)[key.Get().(K)] = val.Get().(V)\n\n\tif flag.setter != nil {\n\t\treturn flag.setter(key.Get().(K), val.Get().(V))\n\t}\n\n\treturn nil\n}\n\nfunc (flag *mapValue[K, V]) Get() any {\n\tvar vals = map[K]V{}\n\n\tmaps.Copy(vals, *flag.values)\n\n\treturn vals\n}\n\n// String returns a readable representation of this value\nfunc (flag *mapValue[K, V]) String() string {\n\tif flag.values == nil {\n\t\treturn \"\"\n\t}\n\n\treturn collections.MapJoin(*flag.values, flag.argSep, flag.valSep)\n}\n"
  },
  {
    "path": "internal/clihelper/map_flag_test.go",
    "content": "package clihelper_test\n\nimport (\n\tlibflag \"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/go-commons/collections\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMapFlagStringStringApply(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedErr   error\n\t\tenvs          map[string]string\n\t\texpectedValue map[string]string\n\t\targs          []string\n\t\tflag          clihelper.MapFlag[string, string]\n\t}{\n\t\t{\n\t\t\tflag:          clihelper.MapFlag[string, string]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:          []string{\"--foo\", \"arg1-key=arg1-value\", \"--foo\", \"arg2-key = arg2-value\"},\n\t\t\tenvs:          map[string]string{\"FOO\": \"env1-key=env1-value,env2-key=env2-value\"},\n\t\t\texpectedValue: map[string]string{\"arg1-key\": \"arg1-value\", \"arg2-key\": \"arg2-value\"},\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.MapFlag[string, string]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\tenvs:          map[string]string{\"FOO\": \"env1-key=env1-value,env2-key = env2-value\"},\n\t\t\texpectedValue: map[string]string{\"env1-key\": \"env1-value\", \"env2-key\": \"env2-value\"},\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.MapFlag[string, string]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\texpectedValue: map[string]string{},\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.MapFlag[string, string]{Name: \"foo\", EnvVars: []string{\"FOO\"}, Destination: mockDestValue(map[string]string{\"default1-key\": \"default1-value\", \"default2-key\": \"default2-value\"})},\n\t\t\targs:          []string{\"--foo\", \"arg1-key=arg1-value\", \"--foo\", \"arg2-key=arg2-value\"},\n\t\t\tenvs:          map[string]string{\"FOO\": \"env1-key=env1-value,env2-key=env2-value\"},\n\t\t\texpectedValue: map[string]string{\"arg1-key\": \"arg1-value\", \"arg2-key\": \"arg2-value\"},\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.MapFlag[string, string]{Name: \"foo\", Destination: mockDestValue(map[string]string{\"default1-key\": \"default1-value\", \"default2-key\": \"default2-value\"})},\n\t\t\texpectedValue: map[string]string{\"default1-key\": \"default1-value\", \"default2-key\": \"default2-value\"},\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttestMapFlagApply(t, &tc.flag, tc.args, tc.envs, tc.expectedValue, tc.expectedErr)\n\t\t})\n\t}\n}\n\nfunc TestMapFlagStringIntApply(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedErr   error\n\t\tenvs          map[string]string\n\t\texpectedValue map[string]int\n\t\targs          []string\n\t\tflag          clihelper.MapFlag[string, int]\n\t}{\n\t\t{\n\t\t\tflag:          clihelper.MapFlag[string, int]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:          []string{\"--foo\", \"arg1-key=10\", \"--foo\", \"arg2-key=11\"},\n\t\t\tenvs:          map[string]string{\"FOO\": \"env1-key=20,env2-key=21\"},\n\t\t\texpectedValue: map[string]int{\"arg1-key\": 10, \"arg2-key\": 11},\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.MapFlag[string, int]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\tenvs:          map[string]string{\"FOO\": \"env1-key=20,env2-key=21\"},\n\t\t\texpectedValue: map[string]int{\"env1-key\": 20, \"env2-key\": 21},\n\t\t},\n\n\t\t{\n\t\t\tflag:          clihelper.MapFlag[string, int]{Name: \"foo\", EnvVars: []string{\"FOO\"}, Destination: mockDestValue(map[string]int{\"default1-key\": 50, \"default2-key\": 51})},\n\t\t\targs:          []string{\"--foo\", \"arg1-key=10\", \"--foo\", \"arg2-key=11\"},\n\t\t\tenvs:          map[string]string{\"FOO\": \"env1-key=20,env2-key=21\"},\n\t\t\texpectedValue: map[string]int{\"arg1-key\": 10, \"arg2-key\": 11},\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.MapFlag[string, int]{Name: \"foo\", Destination: mockDestValue(map[string]int{\"default1-key\": 50, \"default2-key\": 51})},\n\t\t\texpectedValue: map[string]int{\"default1-key\": 50, \"default2-key\": 51},\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttestMapFlagApply(t, &tc.flag, tc.args, tc.envs, tc.expectedValue, tc.expectedErr)\n\t\t})\n\t}\n}\n\nfunc testMapFlagApply[K clihelper.MapFlagKeyType, V clihelper.MapFlagValueType](t *testing.T, flag *clihelper.MapFlag[K, V], args []string, envs map[string]string, expectedValue map[K]V, expectedErr error) {\n\tt.Helper()\n\n\tvar (\n\t\tactualValue          = map[K]V{}\n\t\tdestDefined          bool\n\t\texpectedDefaultValue = map[K]V{}\n\t)\n\n\tif flag.Destination == nil {\n\t\tdestDefined = true\n\t\tflag.Destination = &actualValue\n\t} else {\n\t\texpectedDefaultValue = *flag.Destination\n\t}\n\n\tflag.LookupEnvFunc = func(key string) []string {\n\t\tif envs == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tif val, ok := envs[key]; ok {\n\t\t\treturn flag.Splitter(val, flag.EnvVarSep)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tflagSet := libflag.NewFlagSet(\"test-cmd\", libflag.ContinueOnError)\n\tflagSet.SetOutput(io.Discard)\n\n\terr := flag.Apply(flagSet)\n\trequire.NoError(t, err)\n\n\terr = flagSet.Parse(args)\n\tif expectedErr != nil {\n\t\trequire.Equal(t, expectedErr, err)\n\t\treturn\n\t}\n\n\trequire.NoError(t, err)\n\n\tif !destDefined {\n\t\tactualValue = (flag.Value().Get()).(map[K]V)\n\t}\n\n\tassert.Subset(t, expectedValue, actualValue)\n\n\tassert.Equal(t, collections.MapJoin(expectedValue, flag.EnvVarSep, flag.KeyValSep), flag.GetValue(), \"GetValue()\")\n\n\tassert.Equal(t, len(args) > 0 || len(envs) > 0, flag.Value().IsSet(), \"IsSet()\")\n\tassert.Equal(t, collections.MapJoin(expectedDefaultValue, flag.EnvVarSep, flag.KeyValSep), flag.GetDefaultText(), \"GetDefaultText()\")\n\n\tassert.False(t, flag.Value().IsBoolFlag(), \"IsBoolFlag()\")\n\tassert.True(t, flag.TakesValue(), \"TakesValue()\")\n}\n"
  },
  {
    "path": "internal/clihelper/slice_flag.go",
    "content": "package clihelper\n\nimport (\n\t\"context\"\n\tlibflag \"flag\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/urfave/cli/v2\"\n)\n\n// SliceFlag implements Flag\nvar _ Flag = new(SliceFlag[string])\n\nvar (\n\tSliceFlagEnvVarSep = \",\"\n)\n\ntype SliceFlagType interface {\n\tGenericType\n}\n\n// SliceFlag is a multiple flag.\ntype SliceFlag[T SliceFlagType] struct {\n\tflag\n\n\t// Action is a function that is called when the flag is specified. It is executed only after all command flags have been parsed.\n\tAction FlagActionFunc[[]T]\n\n\t// Setter represents the function that is called when the flag is specified.\n\tSetter FlagSetterFunc[T]\n\n\t// Destination is a pointer to which the value of the flag or env var is assigned.\n\tDestination *[]T\n\n\t// Splitter represents the function that is called when the flag is specified.\n\tSplitter SplitterFunc\n\n\t// Name is the name of the flag.\n\tName string\n\n\t// DefaultText is the default value of the flag to display in the help, if it is empty, the value is taken from `Destination`.\n\tDefaultText string\n\n\t// Usage is a short usage description to display in help.\n\tUsage string\n\n\t// EnvVarSep is the separator used to split the env var value.\n\tEnvVarSep string\n\n\t// Aliases are usually used for the short flag name, like `-h`.\n\tAliases []string\n\n\t// EnvVars are the names of the env variables that are parsed and assigned to `Destination` before the flag value.\n\tEnvVars []string\n\n\t// Hidden hides the flag from the help.\n\tHidden bool\n}\n\n// Apply applies Flag settings to the given flag set.\nfunc (flag *SliceFlag[T]) Apply(set *libflag.FlagSet) error {\n\tif flag.FlagValue != nil {\n\t\treturn ApplyFlag(flag, set)\n\t}\n\n\tif flag.Destination == nil {\n\t\tflag.Destination = new([]T)\n\t}\n\n\tif flag.Splitter == nil {\n\t\tflag.Splitter = FlagSplitter\n\t}\n\n\tif flag.EnvVarSep == \"\" {\n\t\tflag.EnvVarSep = SliceFlagEnvVarSep\n\t}\n\n\tif flag.LookupEnvFunc == nil {\n\t\tflag.LookupEnvFunc = func(key string) []string {\n\t\t\tif val, ok := os.LookupEnv(key); ok {\n\t\t\t\treturn flag.Splitter(val, flag.EnvVarSep)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tvalueType := FlagVariable[T](new(genericVar[T]))\n\tvalue := newSliceValue(valueType, flag.EnvVarSep, flag.Destination, flag.Setter)\n\n\tflag.FlagValue = &flagValue{\n\t\tmultipleSet:      true,\n\t\tvalue:            value,\n\t\tinitialTextValue: value.String(),\n\t}\n\n\treturn ApplyFlag(flag, set)\n}\n\n// GetHidden returns true if the flag should be hidden from the help.\nfunc (flag *SliceFlag[T]) GetHidden() bool {\n\treturn flag.Hidden\n}\n\n// GetUsage returns the usage string for the flag.\nfunc (flag *SliceFlag[T]) GetUsage() string {\n\treturn flag.Usage\n}\n\n// GetEnvVars implements `cli.Flag` interface.\nfunc (flag *SliceFlag[T]) GetEnvVars() []string {\n\treturn flag.EnvVars\n}\n\n// GetDefaultText returns the flags value as string representation and an empty string if the flag takes no value at all.\nfunc (flag *SliceFlag[T]) GetDefaultText() string {\n\tif flag.DefaultText == \"\" && flag.FlagValue != nil {\n\t\treturn flag.GetInitialTextValue()\n\t}\n\n\treturn flag.DefaultText\n}\n\n// String returns a readable representation of this value (for usage defaults).\nfunc (flag *SliceFlag[T]) String() string {\n\treturn cli.FlagStringer(flag)\n}\n\n// Names returns the names of the flag.\nfunc (flag *SliceFlag[T]) Names() []string {\n\tif flag.Name == \"\" {\n\t\treturn flag.Aliases\n\t}\n\n\treturn append([]string{flag.Name}, flag.Aliases...)\n}\n\n// RunAction implements ActionableFlag.RunAction\nfunc (flag *SliceFlag[T]) RunAction(ctx context.Context, cliCtx *Context) error {\n\tif flag.Action != nil {\n\t\treturn flag.Action(ctx, cliCtx, *flag.Destination)\n\t}\n\n\treturn nil\n}\n\nvar _ = Value(new(sliceValue[string]))\n\n// -- slice Value\ntype sliceValue[T comparable] struct {\n\tvalues    *[]T\n\tvalueType FlagVariable[T]\n\tsetter    FlagSetterFunc[T]\n\tvalSep    string\n}\n\nfunc newSliceValue[T comparable](valueType FlagVariable[T], valSep string, dest *[]T, setter FlagSetterFunc[T]) *sliceValue[T] {\n\treturn &sliceValue[T]{\n\t\tvalues:    dest,\n\t\tvalueType: valueType,\n\t\tvalSep:    valSep,\n\t\tsetter:    setter,\n\t}\n}\n\nfunc (flag *sliceValue[T]) Reset() {\n\t*flag.values = []T{}\n}\n\nfunc (flag *sliceValue[T]) Set(str string) error {\n\tvalue := flag.valueType.Clone(new(T))\n\tif err := value.Set(str); err != nil {\n\t\treturn err\n\t}\n\n\t*flag.values = append(*flag.values, value.Get().(T))\n\n\tif flag.setter != nil {\n\t\treturn flag.setter(value.Get().(T))\n\t}\n\n\treturn nil\n}\n\nfunc (flag *sliceValue[T]) Get() any {\n\tvals := make([]T, 0, len(*flag.values))\n\n\tvals = append(vals, *flag.values...)\n\n\treturn vals\n}\n\n// String returns a readable representation of this value\nfunc (flag *sliceValue[T]) String() string {\n\tif flag.values == nil {\n\t\treturn \"\"\n\t}\n\n\tvar vals = make([]string, 0, len(*flag.values))\n\n\tfor _, val := range *flag.values {\n\t\tvals = append(vals, flag.valueType.Clone(&val).String())\n\t}\n\n\treturn strings.Join(vals, flag.valSep)\n}\n"
  },
  {
    "path": "internal/clihelper/slice_flag_test.go",
    "content": "package clihelper_test\n\nimport (\n\tlibflag \"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSliceFlagStringApply(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedErr   error\n\t\tenvs          map[string]string\n\t\targs          []string\n\t\texpectedValue []string\n\t\tflag          clihelper.SliceFlag[string]\n\t}{\n\t\t{\n\t\t\tflag:          clihelper.SliceFlag[string]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:          []string{\"--foo\", \"arg-value1\", \"--foo\", \"arg-value2\"},\n\t\t\tenvs:          map[string]string{\"FOO\": \"env-value\"},\n\t\t\texpectedValue: []string{\"arg-value1\", \"arg-value2\"},\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.SliceFlag[string]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\tenvs:          map[string]string{\"FOO\": \"env-value1,env-value2\"},\n\t\t\texpectedValue: []string{\"env-value1\", \"env-value2\"},\n\t\t},\n\t\t{\n\t\t\tflag: clihelper.SliceFlag[string]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.SliceFlag[string]{Name: \"foo\", EnvVars: []string{\"FOO\"}, Destination: mockDestValue([]string{\"default-value1\", \"default-value2\"})},\n\t\t\targs:          []string{\"--foo\", \"arg-value1\", \"--foo\", \"arg-value2\"},\n\t\t\tenvs:          map[string]string{\"FOO\": \"env-value1,env-value2\"},\n\t\t\texpectedValue: []string{\"arg-value1\", \"arg-value2\"},\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.SliceFlag[string]{Name: \"foo\", Destination: mockDestValue([]string{\"default-value1\", \"default-value2\"})},\n\t\t\texpectedValue: []string{\"default-value1\", \"default-value2\"},\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttestSliceFlagApply(t, &tc.flag, tc.args, tc.envs, tc.expectedValue, tc.expectedErr)\n\t\t})\n\t}\n}\n\nfunc TestSliceFlagIntApply(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedErr   error\n\t\tenvs          map[string]string\n\t\targs          []string\n\t\texpectedValue []int\n\t\tflag          clihelper.SliceFlag[int]\n\t}{\n\t\t{\n\t\t\tflag:          clihelper.SliceFlag[int]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:          []string{\"--foo\", \"10\", \"--foo\", \"11\"},\n\t\t\tenvs:          map[string]string{\"FOO\": \"20,21\"},\n\t\t\texpectedValue: []int{10, 11},\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.SliceFlag[int]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\tenvs:          map[string]string{\"FOO\": \"20,21\"},\n\t\t\texpectedValue: []int{20, 21},\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.SliceFlag[int]{Name: \"foo\", Destination: mockDestValue([]int{50, 51})},\n\t\t\texpectedValue: []int{50, 51},\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttestSliceFlagApply(t, &tc.flag, tc.args, tc.envs, tc.expectedValue, tc.expectedErr)\n\t\t})\n\t}\n}\n\nfunc TestSliceFlagInt64Apply(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedErr   error\n\t\tenvs          map[string]string\n\t\targs          []string\n\t\texpectedValue []int64\n\t\tflag          clihelper.SliceFlag[int64]\n\t}{\n\t\t{\n\t\t\tflag:          clihelper.SliceFlag[int64]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\targs:          []string{\"--foo\", \"10\", \"--foo\", \"11\"},\n\t\t\tenvs:          map[string]string{\"FOO\": \"20,21\"},\n\t\t\texpectedValue: []int64{10, 11},\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.SliceFlag[int64]{Name: \"foo\", EnvVars: []string{\"FOO\"}},\n\t\t\tenvs:          map[string]string{\"FOO\": \"20,21\"},\n\t\t\texpectedValue: []int64{20, 21},\n\t\t},\n\t\t{\n\t\t\tflag:          clihelper.SliceFlag[int64]{Name: \"foo\", Destination: mockDestValue([]int64{50, 51})},\n\t\t\texpectedValue: []int64{50, 51},\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttestSliceFlagApply(t, &tc.flag, tc.args, tc.envs, tc.expectedValue, tc.expectedErr)\n\t\t})\n\t}\n}\n\nfunc testSliceFlagApply[T clihelper.SliceFlagType](t *testing.T, flag *clihelper.SliceFlag[T], args []string, envs map[string]string, expectedValue []T, expectedErr error) {\n\tt.Helper()\n\n\tvar (\n\t\tactualValue          []T\n\t\tdestDefined          bool\n\t\texpectedDefaultValue []T\n\t)\n\n\tif flag.Destination == nil {\n\t\tdestDefined = true\n\t\tflag.Destination = &actualValue\n\t} else {\n\t\texpectedDefaultValue = *flag.Destination\n\t}\n\n\tflag.LookupEnvFunc = func(key string) []string {\n\t\tif envs == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tif val, ok := envs[key]; ok {\n\t\t\treturn flag.Splitter(val, flag.EnvVarSep)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tflagSet := libflag.NewFlagSet(\"test-cmd\", libflag.ContinueOnError)\n\tflagSet.SetOutput(io.Discard)\n\n\terr := flag.Apply(flagSet)\n\trequire.NoError(t, err)\n\n\terr = flagSet.Parse(args)\n\tif expectedErr != nil {\n\t\trequire.Equal(t, expectedErr, err)\n\t\treturn\n\t}\n\n\trequire.NoError(t, err)\n\n\tif !destDefined {\n\t\tactualValue = (flag.Value().Get()).([]T)\n\t}\n\n\tassert.Equal(t, expectedValue, actualValue)\n\n\texpectedStringValueFn := func(value []T) string {\n\t\tstringValue := make([]string, 0, len(value))\n\t\tfor _, val := range value {\n\t\t\tstringValue = append(stringValue, fmt.Sprintf(\"%v\", val))\n\t\t}\n\n\t\treturn strings.Join(stringValue, flag.EnvVarSep)\n\t}\n\n\tassert.Equal(t, expectedStringValueFn(expectedValue), flag.GetValue(), \"GetValue()\")\n\n\tassert.Equal(t, len(args) > 0 || len(envs) > 0, flag.Value().IsSet(), \"IsSet()\")\n\tassert.Equal(t, expectedStringValueFn(expectedDefaultValue), flag.GetDefaultText(), \"GetDefaultText()\")\n\n\tassert.False(t, flag.Value().IsBoolFlag(), \"IsBoolFlag()\")\n\tassert.True(t, flag.TakesValue(), \"TakesValue()\")\n}\n"
  },
  {
    "path": "internal/clihelper/sort.go",
    "content": "package clihelper\n\nimport \"unicode\"\n\n// LexicographicLess compares strings alphabetically considering case.\nfunc LexicographicLess(i, j string) bool {\n\tiRunes := []rune(i)\n\tjRunes := []rune(j)\n\n\tlenShared := min(len(iRunes), len(jRunes))\n\n\tfor index := range lenShared {\n\t\tir := iRunes[index]\n\t\tjr := jRunes[index]\n\n\t\tif lir, ljr := unicode.ToLower(ir), unicode.ToLower(jr); lir != ljr {\n\t\t\treturn lir < ljr\n\t\t}\n\n\t\tif ir != jr {\n\t\t\treturn ir < jr\n\t\t}\n\t}\n\n\treturn i < j\n}\n"
  },
  {
    "path": "internal/clihelper/sort_test.go",
    "content": "package clihelper_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestLexicographicLess(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\ti, j     string\n\t\texpected bool\n\t}{\n\t\t{\"ab\", \"cb\", true},\n\t\t{\"ab\", \"ac\", true},\n\t\t{\"bf\", \"bc\", false},\n\t\t{\"bb\", \"bbbb\", true},\n\t\t{\"bbbb\", \"c\", true},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := clihelper.LexicographicLess(tc.i, tc.j)\n\t\t\tassert.Equal(t, tc.expected, actual, tc)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/cloner/clone.go",
    "content": "// Package cloner provides functions to deep clone any Go data.\npackage cloner\n\nimport \"reflect\"\n\n// Clone returns a deep cloned instance of the given `src` variable.\nfunc Clone[T any](src T, opts ...Option) T { //nolint:ireturn\n\tconf := &Config{}\n\tfor _, opt := range opts {\n\t\topt(conf)\n\t}\n\n\tcloner := Cloner[T]{Config: conf}\n\n\treturn cloner.Clone(src)\n}\n\n// WithShadowCopyTypes returns an `Option` that forces shadow copies\n// of the types that are in the given `values`.\nfunc WithShadowCopyTypes(values ...any) Option {\n\treturn func(opt *Config) {\n\t\tfor i := range values {\n\t\t\topt.shadowCopyTypes = append(opt.shadowCopyTypes, reflect.TypeOf(values[i]))\n\t\t}\n\t}\n}\n\n// WithSkippingTypes returns an `Option` that forces skipping copying types\n// that are in the given `values`.\nfunc WithSkippingTypes(values ...any) Option {\n\treturn func(opt *Config) {\n\t\tfor i := range values {\n\t\t\topt.skippingTypes = append(opt.skippingTypes, reflect.TypeOf(values[i]))\n\t\t}\n\t}\n}\n\n// WithShadowCopyInversePkgPrefixes returns an `Option` that forces shadow copies\n// of types whose pkg paths do not match the given `prefixes`.\nfunc WithShadowCopyInversePkgPrefixes(prefixes ...string) Option {\n\treturn func(opt *Config) {\n\t\topt.shadowCopyInversePkgPrefixes = append(opt.shadowCopyInversePkgPrefixes, prefixes...)\n\t}\n}\n"
  },
  {
    "path": "internal/cloner/cloner.go",
    "content": "package cloner\n\nimport (\n\t\"reflect\"\n\t\"strings\"\n)\n\nconst (\n\tfieldTagName = \"clone\"\n\n\t// fieldTagValueRequired forces to make deep copy of the field even if the field type is disallowed by the option.\n\tfieldTagValueRequired = \"required\"\n\t// fieldTagValueShadowCopy specifies that the dst field should be assigned the src field pointer instead of deep copying.\n\tfieldTagValueShadowCopy = \"shadowcopy\"\n\t// fieldTagValueSkip specifies that the dst field should have a null value, regardless of the src value.\n\tfieldTagValueSkip      = \"skip\"\n\tfieldTagValueSkipAlias = \"-\"\n)\n\n// Option represents an option to customize deep copied results.\ntype Option func(*Config)\n\ntype Config struct {\n\tshadowCopyTypes []reflect.Type\n\tskippingTypes   []reflect.Type\n\n\tshadowCopyInversePkgPrefixes []string\n\n\ttagPriorityOnce bool\n}\n\ntype Cloner[T any] struct {\n\t*Config\n}\n\nfunc (cloner *Cloner[T]) Clone(src T) T {\n\tvar dst T\n\n\tval := cloner.cloneValue(reflect.ValueOf(src))\n\n\treflect.ValueOf(&dst).Elem().Set(val)\n\n\treturn dst\n}\n\nfunc (cloner *Cloner[T]) getDstValue(src reflect.Value) (reflect.Value, bool) {\n\tvar (\n\t\tsrcType = src.Type()\n\t\tpkgPath = src.Type().PkgPath()\n\t\tdst     = src\n\t\tvalid   = false\n\t)\n\n\tif cloner.tagPriorityOnce {\n\t\tcloner.tagPriorityOnce = false\n\n\t\treturn dst, valid\n\t}\n\n\tif len(cloner.shadowCopyInversePkgPrefixes) != 0 {\n\t\tvalidInverse := false\n\n\t\tfor _, pkgPrefix := range cloner.shadowCopyInversePkgPrefixes {\n\t\t\tif pkgPath == \"\" || strings.HasPrefix(pkgPath, pkgPrefix) {\n\t\t\t\tvalidInverse = true\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tvalid = !validInverse\n\t}\n\n\tfor i := range cloner.skippingTypes {\n\t\tif srcType == cloner.skippingTypes[i] {\n\t\t\tdst = reflect.Zero(srcType).Elem()\n\t\t\tvalid = true\n\t\t}\n\t}\n\n\tfor i := range cloner.shadowCopyTypes {\n\t\tif srcType == cloner.shadowCopyTypes[i] {\n\t\t\tvalid = true\n\t\t}\n\t}\n\n\treturn dst, valid\n}\n\nfunc (cloner *Cloner[T]) cloneValue(src reflect.Value) reflect.Value {\n\tif dst, ok := cloner.getDstValue(src); ok {\n\t\treturn dst\n\t}\n\n\tif !src.IsValid() {\n\t\treturn src\n\t}\n\n\t// Look up the corresponding clone function.\n\tswitch src.Kind() {\n\tcase reflect.Bool:\n\t\treturn cloner.cloneBool(src)\n\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:\n\t\treturn cloner.cloneInt(src)\n\tcase reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:\n\t\treturn cloner.cloneUint(src)\n\tcase reflect.Float32, reflect.Float64:\n\t\treturn cloner.cloneFloat(src)\n\tcase reflect.String:\n\t\treturn cloner.cloneString(src)\n\tcase reflect.Slice:\n\t\treturn cloner.cloneSlice(src)\n\tcase reflect.Array:\n\t\treturn cloner.cloneArray(src)\n\tcase reflect.Map:\n\t\treturn cloner.cloneMap(src)\n\tcase reflect.Pointer, reflect.UnsafePointer:\n\t\treturn cloner.clonePointer(src)\n\tcase reflect.Struct:\n\t\treturn cloner.cloneStruct(src)\n\tcase reflect.Invalid, reflect.Uintptr, reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.Func, reflect.Interface:\n\t}\n\n\treturn src\n}\n\nfunc (cloner *Cloner[T]) cloneInt(src reflect.Value) reflect.Value {\n\tdst := reflect.New(src.Type()).Elem()\n\tdst.SetInt(src.Int())\n\n\treturn dst\n}\n\nfunc (cloner *Cloner[T]) cloneUint(src reflect.Value) reflect.Value {\n\tdst := reflect.New(src.Type()).Elem()\n\tdst.SetUint(src.Uint())\n\n\treturn dst\n}\n\nfunc (cloner *Cloner[T]) cloneFloat(src reflect.Value) reflect.Value {\n\tdst := reflect.New(src.Type()).Elem()\n\tdst.SetFloat(src.Float())\n\n\treturn dst\n}\n\nfunc (cloner *Cloner[T]) cloneBool(src reflect.Value) reflect.Value {\n\tdst := reflect.New(src.Type()).Elem()\n\tdst.SetBool(src.Bool())\n\n\treturn dst\n}\n\nfunc (cloner *Cloner[T]) cloneString(src reflect.Value) reflect.Value {\n\tif src, ok := src.Interface().(string); ok {\n\t\treturn reflect.ValueOf(strings.Clone(src))\n\t}\n\n\treturn src\n}\n\nfunc (cloner *Cloner[T]) cloneSlice(src reflect.Value) reflect.Value {\n\tsize := src.Len()\n\tdst := reflect.MakeSlice(src.Type(), size, size)\n\n\tfor i := range size {\n\t\tif val := cloner.cloneValue(src.Index(i)); val.IsValid() {\n\t\t\tdst.Index(i).Set(val)\n\t\t}\n\t}\n\n\treturn dst\n}\n\nfunc (cloner *Cloner[T]) cloneArray(src reflect.Value) reflect.Value {\n\tsize := src.Type().Len()\n\tdst := reflect.New(reflect.ArrayOf(size, src.Type().Elem())).Elem()\n\n\tfor i := range size {\n\t\tif val := cloner.cloneValue(src.Index(i)); val.IsValid() {\n\t\t\tdst.Index(i).Set(val)\n\t\t}\n\t}\n\n\treturn dst\n}\n\nfunc (cloner *Cloner[T]) cloneMap(src reflect.Value) reflect.Value {\n\tdst := reflect.MakeMapWithSize(src.Type(), src.Len())\n\titer := src.MapRange()\n\n\tfor iter.Next() {\n\t\titem := cloner.cloneValue(iter.Value())\n\t\tkey := cloner.cloneValue(iter.Key())\n\t\tdst.SetMapIndex(key, item)\n\t}\n\n\treturn dst\n}\n\nfunc (cloner *Cloner[T]) clonePointer(src reflect.Value) reflect.Value {\n\tif src.IsNil() {\n\t\treturn reflect.Zero(src.Type()).Elem()\n\t}\n\n\tdst := reflect.New(src.Type().Elem())\n\n\tif val := cloner.cloneValue(src.Elem()); val.IsValid() {\n\t\tdst.Elem().Set(val)\n\t}\n\n\treturn dst\n}\n\nfunc (cloner *Cloner[T]) cloneStruct(src reflect.Value) reflect.Value {\n\tt := src.Type()\n\tdst := reflect.New(t)\n\n\tfor i := range t.NumField() {\n\t\tsrcTypeField := t.Field(i)\n\t\tsrcField := src.Field(i)\n\n\t\tif !srcTypeField.IsExported() {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar val reflect.Value\n\n\t\tswitch srcTypeField.Tag.Get(fieldTagName) {\n\t\tcase fieldTagValueSkip, fieldTagValueSkipAlias:\n\t\t\tcloner.tagPriorityOnce = true\n\t\t\tval = reflect.Zero(srcField.Type()).Elem()\n\t\tcase fieldTagValueShadowCopy:\n\t\t\tcloner.tagPriorityOnce = true\n\t\t\tval = srcField\n\t\tcase fieldTagValueRequired:\n\t\t\tcloner.tagPriorityOnce = true\n\t\t\tfallthrough\n\t\tdefault:\n\t\t\tval = cloner.cloneValue(srcField)\n\t\t}\n\n\t\tif val.IsValid() {\n\t\t\tdst.Elem().Field(i).Set(val)\n\t\t}\n\t}\n\n\treturn dst.Elem()\n}\n"
  },
  {
    "path": "internal/codegen/codegen.go",
    "content": "// Package codegen contains routines for generating terraform code\npackage codegen\n"
  },
  {
    "path": "internal/codegen/errors.go",
    "content": "package codegen\n\nimport \"fmt\"\n\n// Custom error types\n\ntype UnknownGenerateIfExistsVal struct {\n\tval string\n}\n\nfunc (err UnknownGenerateIfExistsVal) Error() string {\n\tif err.val != \"\" {\n\t\treturn err.val + \" is not a valid value for generate if_exists\"\n\t}\n\n\treturn \"Received unknown value for if_exists\"\n}\n\ntype UnknownGenerateIfDisabledVal struct {\n\tval string\n}\n\nfunc (err UnknownGenerateIfDisabledVal) Error() string {\n\tif err.val != \"\" {\n\t\treturn err.val + \" is not a valid value for generate if_disabled\"\n\t}\n\n\treturn \"Received unknown value for if_disabled\"\n}\n\ntype GenerateFileExistsError struct {\n\tpath string\n}\n\nfunc (err GenerateFileExistsError) Error() string {\n\treturn fmt.Sprintf(\"Can not generate terraform file: %s already exists\", err.path)\n}\n\ntype GenerateFileRemoveError struct {\n\tpath string\n}\n\nfunc (err GenerateFileRemoveError) Error() string {\n\treturn \"Can not remove terraform file: \" + err.path\n}\n"
  },
  {
    "path": "internal/codegen/generate.go",
    "content": "package codegen\n\nimport (\n\t\"bufio\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/hashicorp/hcl/v2/hclsimple\"\n\t\"github.com/hashicorp/hcl/v2/hclwrite\"\n\tctyjson \"github.com/zclconf/go-cty/cty/json\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst (\n\t// A comment that is added to the top of the generated file to indicate that this file was generated by Terragrunt.\n\t// We use a hardcoded random string at the end to make the string further unique.\n\tTerragruntGeneratedSignature = \"Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa\"\n\n\t// The default prefix to use for comments in the generated file\n\tDefaultCommentPrefix = \"# \"\n)\n\n// GenerateConfigExists is an enum to represent valid values for if_exists.\ntype GenerateConfigExists int\n\nconst (\n\tExistsError GenerateConfigExists = iota\n\tExistsSkip\n\tExistsOverwrite\n\tExistsOverwriteTerragrunt\n\tExistsUnknown\n)\n\n// GenerateConfigDisabled is an enum to represent valid values for if_disabled.\ntype GenerateConfigDisabled int\n\nconst (\n\tDisabledSkip GenerateConfigDisabled = iota\n\tDisabledRemove\n\tDisabledRemoveTerragrunt\n\tDisabledUnknown\n)\n\nconst (\n\tExistsErrorStr               = \"error\"\n\tExistsSkipStr                = \"skip\"\n\tExistsOverwriteStr           = \"overwrite\"\n\tExistsOverwriteTerragruntStr = \"overwrite_terragrunt\"\n\n\tDisabledSkipStr             = \"skip\"\n\tDisabledRemoveStr           = \"remove\"\n\tDisabledRemoveTerragruntStr = \"remove_terragrunt\"\n\n\tassumeRoleConfigKey                = \"assume_role\"\n\tassumeRoleWithWebIdentityConfigKey = \"assume_role_with_web_identity\"\n\n\tencryptionBlockName = \"encryption\"\n\n\tEncryptionKeyProviderKey = \"key_provider\"\n\tencryptionResourceName   = \"default\"\n\n\tencryptionMethodKey     = \"method\"\n\tencryptionDefaultMethod = \"aes_gcm\"\n\n\tencryptionKeysAttributeName = \"keys\"\n\n\tencryptionStateBlockName = \"state\"\n\tencryptionPlanBlockName  = \"plan\"\n)\n\n// GenerateConfig is configuration for generating code\ntype GenerateConfig struct {\n\tHclFmt           *bool  `cty:\"hcl_fmt\"`\n\tPath             string `cty:\"path\"`\n\tIfExistsStr      string `cty:\"if_exists\"`\n\tIfDisabledStr    string `cty:\"if_disabled\"`\n\tCommentPrefix    string `cty:\"comment_prefix\"`\n\tContents         string `cty:\"contents\"`\n\tIfExists         GenerateConfigExists\n\tIfDisabled       GenerateConfigDisabled\n\tDisableSignature bool `cty:\"disable_signature\"`\n\tDisable          bool `cty:\"disable\"`\n}\n\n// WriteToFile will generate a new file at the given target path with the given contents. If a file already exists at\n// the target path, the behavior depends on the value of IfExists:\n// - if ExistsError, return an error.\n// - if ExistsSkip, do nothing and return\n// - if ExistsOverwrite, overwrite the existing file\nfunc WriteToFile(l log.Logger, basePath string, config *GenerateConfig) error {\n\t// Figure out the target path to generate the code in. If relative, merge with basePath.\n\tvar targetPath string\n\tif filepath.IsAbs(config.Path) {\n\t\ttargetPath = config.Path\n\t} else {\n\t\ttargetPath = filepath.Join(basePath, config.Path)\n\t}\n\n\ttargetFileExists := util.FileExists(targetPath)\n\n\t// If this GenerateConfig is disabled then skip further processing.\n\tif config.Disable {\n\t\tl.Debugf(\"Skipping generating file at %s because it is disabled\", config.Path)\n\n\t\tif targetFileExists {\n\t\t\tif shouldRemove, err := shouldRemoveWithFileExists(l, targetPath, config.IfDisabled); err != nil {\n\t\t\t\treturn err\n\t\t\t} else if shouldRemove {\n\t\t\t\tif err := os.Remove(targetPath); err != nil {\n\t\t\t\t\treturn errors.New(err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif targetFileExists {\n\t\tshouldContinue, err := shouldContinueWithFileExists(l, targetPath, config.IfExists)\n\t\tif err != nil || !shouldContinue {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Add the signature as a prefix to the file, unless it is disabled.\n\tprefix := \"\"\n\tif !config.DisableSignature {\n\t\tprefix = fmt.Sprintf(\"%s%s\\n\", config.CommentPrefix, TerragruntGeneratedSignature)\n\t}\n\n\tfmtGeneratedCode := false\n\n\tif config.HclFmt == nil {\n\t\tvar fmtExt = map[string]struct{}{\n\t\t\t\".hcl\":  {},\n\t\t\t\".tf\":   {},\n\t\t\t\".tofu\": {},\n\t\t}\n\n\t\text := filepath.Ext(config.Path)\n\t\tif _, ok := fmtExt[ext]; ok {\n\t\t\tfmtGeneratedCode = true\n\t\t}\n\t} else {\n\t\tfmtGeneratedCode = *config.HclFmt\n\t}\n\n\tcontentsToWrite := fmt.Appendf(nil, \"%s%s\", prefix, config.Contents)\n\tif fmtGeneratedCode {\n\t\tcontentsToWrite = hclwrite.Format(contentsToWrite)\n\t}\n\n\tconst ownerWriteGlobalReadPerms = 0644\n\tif err := os.WriteFile(targetPath, contentsToWrite, ownerWriteGlobalReadPerms); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tl.Debugf(\"Generated file %s.\", targetPath)\n\n\treturn nil\n}\n\n// Whether or not file generation should continue if the file path already exists. The answer depends on the\n// ifExists configuration.\nfunc shouldContinueWithFileExists(l log.Logger, path string, ifExists GenerateConfigExists) (bool, error) {\n\t// TODO: Make exhaustive\n\tswitch ifExists { //nolint:exhaustive\n\tcase ExistsError:\n\t\treturn false, errors.New(GenerateFileExistsError{path: path})\n\tcase ExistsSkip:\n\t\t// Do nothing since file exists and skip was configured\n\t\tl.Debugf(\"The file path %s already exists and if_exists for code generation set to \\\"skip\\\". Will not regenerate file.\", path)\n\n\t\treturn false, nil\n\tcase ExistsOverwrite:\n\t\t// We will continue to proceed to generate file, but log a message to indicate that we detected the file\n\t\t// exists.\n\t\tl.Debugf(\"The file path %s already exists and if_exists for code generation set to \\\"overwrite\\\". Regenerating file.\", path)\n\n\t\treturn true, nil\n\tcase ExistsOverwriteTerragrunt:\n\t\t// If file was not generated, error out because overwrite_terragrunt if_exists setting only handles if the\n\t\t// existing file was generated by terragrunt.\n\t\twasGenerated, err := fileWasGeneratedByTerragrunt(path)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tif !wasGenerated {\n\t\t\tl.Errorf(\"ERROR: The file path %s already exists and was not generated by terragrunt.\", path)\n\n\t\t\treturn false, errors.New(GenerateFileExistsError{path: path})\n\t\t}\n\n\t\t// Since file was generated by terragrunt, continue.\n\t\tl.Debugf(\"The file path %s already exists, but was a previously generated file by terragrunt. Since if_exists for code generation is set to \\\"overwrite_terragrunt\\\", regenerating file.\", path)\n\n\t\treturn true, nil\n\tdefault:\n\t\t// This shouldn't happen, but we add this case anyway for defensive coding.\n\t\treturn false, errors.New(UnknownGenerateIfExistsVal{\"\"})\n\t}\n}\n\n// shouldRemoveWithFileExists returns true if the already existing file should be removed.\nfunc shouldRemoveWithFileExists(l log.Logger, path string, ifDisable GenerateConfigDisabled) (bool, error) {\n\t// TODO: Make exhaustive\n\tswitch ifDisable { //nolint:exhaustive\n\tcase DisabledSkip:\n\t\t// Do nothing since skip was configured.\n\t\tl.Debugf(\"The file path %s already exists and if_disabled for code generation set to \\\"skip\\\", will not remove file.\", path)\n\t\treturn false, nil\n\tcase DisabledRemove:\n\t\t// The file exists and will be removed.\n\t\tl.Debugf(\"The file path %s already exists and if_disabled for code generation set to \\\"remove\\\", removing file.\", path)\n\t\treturn true, nil\n\tcase DisabledRemoveTerragrunt:\n\t\t// If file was not generated, error out because remove_terragrunt if_disabled setting only handles if the existing file was generated by terragrunt.\n\t\twasGenerated, err := fileWasGeneratedByTerragrunt(path)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tif !wasGenerated {\n\t\t\tl.Errorf(\"ERROR: The file path %s already exists and was not generated by terragrunt.\", path)\n\n\t\t\treturn false, errors.New(GenerateFileRemoveError{path: path})\n\t\t}\n\n\t\t// Since file was generated by terragrunt, removing.\n\t\tl.Debugf(\"The file path %s already exists, but was a previously generated file by terragrunt. Since if_disabled for code generation is set to \\\"remove_terragrunt\\\", removing file.\", path)\n\n\t\treturn true, nil\n\tdefault:\n\t\t// This shouldn't happen, but we add this case anyway for defensive coding.\n\t\treturn false, errors.New(UnknownGenerateIfDisabledVal{\"\"})\n\t}\n}\n\n// Check if the file was generated by terragrunt by checking if the first line of the file has the signature. Since the\n// generated string will be prefixed with the configured comment prefix, the check needs to see if the first line ends\n// with the signature string.\nfunc fileWasGeneratedByTerragrunt(path string) (bool, error) {\n\tfile, err := os.Open(path)\n\tif err != nil {\n\t\treturn false, errors.New(err)\n\t}\n\tdefer file.Close()\n\n\treader := bufio.NewReader(file)\n\n\tfirstLine, err := reader.ReadString('\\n')\n\tif err != nil {\n\t\treturn false, errors.New(err)\n\t}\n\n\treturn strings.HasSuffix(strings.TrimSpace(firstLine), TerragruntGeneratedSignature), nil\n}\n\nconst (\n\tterraformBlock = \"terraform\"\n\tbackendBlock   = \"backend\"\n)\n\n// RemoteStateConfigToTerraformCode converts the arbitrary map that represents a remote state config into HCL code to configure that remote state.\nfunc RemoteStateConfigToTerraformCode(backend string, config map[string]any, encryption map[string]any) ([]byte, error) {\n\tf := hclwrite.NewEmptyFile()\n\tterraformBlock := f.Body().AppendNewBlock(terraformBlock, nil).Body()\n\tbackendBlock := terraformBlock.AppendNewBlock(backendBlock, []string{backend})\n\tbackendBlockBody := backendBlock.Body()\n\n\tvar backendKeys = make([]string, 0, len(config))\n\n\tfor key := range config {\n\t\tbackendKeys = append(backendKeys, key)\n\t}\n\n\tsort.Strings(backendKeys)\n\n\tfor _, key := range backendKeys {\n\t\t// Since we don't have the cty type information for the config and since config can be arbitrary, we cheat by using\n\t\t// json as an intermediate representation.\n\t\t//\n\t\t// handle assume role config key in a different way since it is a single line HCL object\n\t\tif key == assumeRoleConfigKey {\n\t\t\tassumeRoleValue, isAssumeRole := config[assumeRoleConfigKey].(string)\n\t\t\tif !isAssumeRole {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Extracting the values requires two steps.\n\t\t\t// Parsing into a struct first, enabling hclsimple.Decode() to deal with complex types.\n\t\t\t// Then copying values into the assumeRoleMap for rendering to HCL.\n\t\t\tassumeRoleMap := make(map[string]any)\n\n\t\t\ttype assumeRoleConfig struct {\n\t\t\t\tRoleArn           string            `hcl:\"role_arn\"`\n\t\t\t\tDuration          string            `hcl:\"duration,optional\"`\n\t\t\t\tExternalID        string            `hcl:\"external_id,optional\"`\n\t\t\t\tPolicy            string            `hcl:\"policy,optional\"`\n\t\t\t\tPolicyArns        []string          `hcl:\"policy_arns,optional\"`\n\t\t\t\tSessionName       string            `hcl:\"session_name,optional\"`\n\t\t\t\tSourceIdentity    string            `hcl:\"source_identity,optional\"`\n\t\t\t\tTags              map[string]string `hcl:\"tags,optional\"`\n\t\t\t\tTransitiveTagKeys []string          `hcl:\"transitive_tag_keys,optional\"`\n\t\t\t}\n\n\t\t\tvar parsedConfig assumeRoleConfig\n\t\t\t// split single line hcl to default multiline file\n\t\t\thclValue := strings.TrimSuffix(assumeRoleValue, \"}\")\n\t\t\thclValue = strings.TrimPrefix(hclValue, \"{\")\n\t\t\thclValue = ReplaceAllCommasOutsideQuotesWithNewLines(hclValue)\n\n\t\t\terr := hclsimple.Decode(\"s3_assume_role.hcl\", []byte(hclValue), nil, &parsedConfig)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.New(err)\n\t\t\t}\n\n\t\t\t// Copy filled values to the map, could be made shorter but keeping it simple for now\n\t\t\tif parsedConfig.RoleArn != \"\" {\n\t\t\t\tassumeRoleMap[\"role_arn\"] = parsedConfig.RoleArn\n\t\t\t}\n\n\t\t\tif parsedConfig.Duration != \"\" {\n\t\t\t\tassumeRoleMap[\"duration\"] = parsedConfig.Duration\n\t\t\t}\n\n\t\t\tif parsedConfig.ExternalID != \"\" {\n\t\t\t\tassumeRoleMap[\"external_id\"] = parsedConfig.ExternalID\n\t\t\t}\n\n\t\t\tif parsedConfig.Policy != \"\" {\n\t\t\t\tassumeRoleMap[\"policy\"] = parsedConfig.Policy\n\t\t\t}\n\n\t\t\tif len(parsedConfig.PolicyArns) > 0 {\n\t\t\t\tassumeRoleMap[\"policy_arns\"] = parsedConfig.PolicyArns\n\t\t\t}\n\n\t\t\tif parsedConfig.SessionName != \"\" {\n\t\t\t\tassumeRoleMap[\"session_name\"] = parsedConfig.SessionName\n\t\t\t}\n\n\t\t\tif parsedConfig.SourceIdentity != \"\" {\n\t\t\t\tassumeRoleMap[\"source_identity\"] = parsedConfig.SourceIdentity\n\t\t\t}\n\n\t\t\tif len(parsedConfig.Tags) > 0 {\n\t\t\t\tassumeRoleMap[\"tags\"] = parsedConfig.Tags\n\t\t\t}\n\n\t\t\tif len(parsedConfig.TransitiveTagKeys) > 0 {\n\t\t\t\tassumeRoleMap[\"transitive_tag_keys\"] = parsedConfig.TransitiveTagKeys\n\t\t\t}\n\n\t\t\t// write assume role map as HCL object\n\t\t\tctyVal, err := convertValue(assumeRoleMap)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.New(err)\n\t\t\t}\n\n\t\t\tbackendBlockBody.SetAttributeValue(key, ctyVal.Value)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif key == assumeRoleWithWebIdentityConfigKey {\n\t\t\tassumeRoleWithWebIdentityValue, isAssumeRoleWithWebIdentity := config[assumeRoleWithWebIdentityConfigKey].(string)\n\t\t\tif !isAssumeRoleWithWebIdentity {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Extracting the values requires two steps.\n\t\t\t// Parsing into a struct first, enabling hclsimple.Decode() to deal with complex types.\n\t\t\t// Then copying values into the assumeRoleMap for rendering to HCL.\n\t\t\tassumeRoleMap := make(map[string]any)\n\n\t\t\ttype assumeRoleWithWebIdentityConfig struct {\n\t\t\t\tRoleArn              string   `hcl:\"role_arn\"`\n\t\t\t\tDuration             string   `hcl:\"duration,optional\"`\n\t\t\t\tPolicy               string   `hcl:\"policy,optional\"`\n\t\t\t\tSessionName          string   `hcl:\"session_name,optional\"`\n\t\t\t\tWebIdentityToken     string   `hcl:\"web_identity_token,optional\"`\n\t\t\t\tWebIdentityTokenFile string   `hcl:\"web_identity_token_file,optional\"`\n\t\t\t\tPolicyArns           []string `hcl:\"policy_arns,optional\"`\n\t\t\t}\n\n\t\t\tvar parsedConfig assumeRoleWithWebIdentityConfig\n\t\t\t// split single line hcl to default multiline file\n\t\t\thclValue := strings.TrimSuffix(assumeRoleWithWebIdentityValue, \"}\")\n\t\t\thclValue = strings.TrimPrefix(hclValue, \"{\")\n\t\t\thclValue = ReplaceAllCommasOutsideQuotesWithNewLines(hclValue)\n\n\t\t\terr := hclsimple.Decode(\"s3_assume_role_with_web_identity.hcl\", []byte(hclValue), nil, &parsedConfig)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.New(err)\n\t\t\t}\n\n\t\t\tif parsedConfig.RoleArn != \"\" {\n\t\t\t\tassumeRoleMap[\"role_arn\"] = parsedConfig.RoleArn\n\t\t\t}\n\n\t\t\tif parsedConfig.Duration != \"\" {\n\t\t\t\tassumeRoleMap[\"duration\"] = parsedConfig.Duration\n\t\t\t}\n\n\t\t\tif parsedConfig.Policy != \"\" {\n\t\t\t\tassumeRoleMap[\"policy\"] = parsedConfig.Policy\n\t\t\t}\n\n\t\t\tif len(parsedConfig.PolicyArns) > 0 {\n\t\t\t\tassumeRoleMap[\"policy_arns\"] = parsedConfig.PolicyArns\n\t\t\t}\n\n\t\t\tif parsedConfig.SessionName != \"\" {\n\t\t\t\tassumeRoleMap[\"session_name\"] = parsedConfig.SessionName\n\t\t\t}\n\n\t\t\tif parsedConfig.WebIdentityToken != \"\" {\n\t\t\t\tassumeRoleMap[\"web_identity_token\"] = parsedConfig.WebIdentityToken\n\t\t\t}\n\n\t\t\tif parsedConfig.WebIdentityTokenFile != \"\" {\n\t\t\t\tassumeRoleMap[\"web_identity_token_file\"] = parsedConfig.WebIdentityTokenFile\n\t\t\t}\n\n\t\t\t// write assume role map as HCL object\n\t\t\tctyVal, err := convertValue(assumeRoleMap)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.New(err)\n\t\t\t}\n\n\t\t\tbackendBlockBody.SetAttributeValue(key, ctyVal.Value)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tctyVal, err := convertValue(config[key])\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(err)\n\t\t}\n\n\t\tbackendBlockBody.SetAttributeValue(key, ctyVal.Value)\n\t}\n\n\t// encryption can be empty\n\tif len(encryption) == 0 {\n\t\treturn f.Bytes(), nil\n\t}\n\n\t// extract key_provider first to create key_provider block\n\tkeyProvider, found := encryption[EncryptionKeyProviderKey].(string)\n\tif !found {\n\t\treturn nil, errors.New(EncryptionKeyProviderKey + \" is mandatory but not found in the encryption map\")\n\t}\n\n\tkeyProviderTraversal := hcl.Traversal{\n\t\thcl.TraverseRoot{Name: EncryptionKeyProviderKey},\n\t\thcl.TraverseAttr{Name: keyProvider},\n\t\thcl.TraverseAttr{Name: encryptionResourceName},\n\t}\n\n\tmethodTraversal := hcl.Traversal{\n\t\thcl.TraverseRoot{Name: encryptionMethodKey},\n\t\thcl.TraverseAttr{Name: encryptionDefaultMethod},\n\t\thcl.TraverseAttr{Name: encryptionResourceName},\n\t}\n\n\t// encryption block\n\tencryptionBlock := terraformBlock.AppendNewBlock(encryptionBlockName, nil)\n\tencryptionBlockBody := encryptionBlock.Body()\n\n\t// Append key_provider block\n\tkeyProviderBlockBody := encryptionBlockBody.AppendNewBlock(EncryptionKeyProviderKey, []string{keyProvider, encryptionResourceName}).Body()\n\n\t// Append method block\n\tmethodBlock := encryptionBlockBody.AppendNewBlock(encryptionMethodKey, []string{encryptionDefaultMethod, encryptionResourceName}).Body()\n\tmethodBlock.SetAttributeTraversal(encryptionKeysAttributeName, keyProviderTraversal)\n\n\t// Append state block\n\tstateBlock := encryptionBlockBody.AppendNewBlock(encryptionStateBlockName, nil).Body()\n\tstateBlock.SetAttributeTraversal(encryptionMethodKey, methodTraversal)\n\n\t// Append plan block\n\tplanBlock := encryptionBlockBody.AppendNewBlock(encryptionPlanBlockName, nil).Body()\n\tplanBlock.SetAttributeTraversal(encryptionMethodKey, methodTraversal)\n\n\tvar encryptionKeys = make([]string, 0, len(encryption))\n\n\tfor key := range encryption {\n\t\tencryptionKeys = append(encryptionKeys, key)\n\t}\n\n\tsort.Strings(encryptionKeys)\n\n\t// Fill key_provider block with ordered attributes\n\tfor _, key := range encryptionKeys {\n\t\tif key == EncryptionKeyProviderKey {\n\t\t\tcontinue\n\t\t}\n\n\t\tvalue, ok := encryption[key]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Skip basic types with zero values\n\t\tif value == \"\" || value == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tctyVal, err := convertValue(value)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(err)\n\t\t}\n\n\t\tif keyProviderBlockBody != nil {\n\t\t\tkeyProviderBlockBody.SetAttributeValue(key, ctyVal.Value)\n\t\t}\n\t}\n\n\treturn f.Bytes(), nil\n}\n\nfunc convertValue(v any) (ctyjson.SimpleJSONValue, error) {\n\tjsonBytes, err := json.Marshal(v)\n\tif err != nil {\n\t\treturn ctyjson.SimpleJSONValue{}, errors.New(err)\n\t}\n\n\tvar ctyVal ctyjson.SimpleJSONValue\n\tif err := ctyVal.UnmarshalJSON(jsonBytes); err != nil {\n\t\treturn ctyjson.SimpleJSONValue{}, errors.New(err)\n\t}\n\n\treturn ctyVal, nil\n}\n\nvar (\n\t// Regex Explanation:\n\t// (          # Start group 1: Match quoted strings\n\t//  \"         # Match the opening quote\n\t//  [^\"\\\\]* # Match zero or more characters that are NOT a quote or backslash\n\t//  (?:       # Start non-capturing group (for handling escaped quotes)\n\t//    \\\\.     # Match a backslash followed by ANY character (escaped char)\n\t//    [^\"\\\\]* # Match zero or more non-quote/non-backslash chars\n\t//  )*        # End non-capturing group, repeat zero or more times\n\t//  \"         # Match the closing quote\n\t// )          # End group 1\n\t// |          # OR\n\t// (,)        # Start group 2: Match and capture a comma\n\t//\n\tre = regexp.MustCompile(`(\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\")|(,)`)\n)\n\n// ReplaceAllCommasOutsideQuotesWithNewLines replaces all commas outside quotes with new lines.\n// This is useful for instances where a single line of HCL content might contain a comma, and we don't\n// want to split the line into multiple lines.\nfunc ReplaceAllCommasOutsideQuotesWithNewLines(s string) string {\n\toutput := re.ReplaceAllStringFunc(s, func(match string) string {\n\t\t// Check if the match starts with a quote.\n\t\t// If it does, it's a quoted string (group 1 matched). Return it unchanged.\n\t\tif strings.HasPrefix(match, `\"`) {\n\t\t\treturn match\n\t\t}\n\n\t\t// Otherwise, it must be the comma (group 2 matched). Replace it with a newline.\n\t\treturn \"\\n\"\n\t})\n\n\treturn output\n}\n\n// GenerateConfigExistsFromString converts a string representation of if_exists into the enum, returning an error if it\n// is not set to one of the known values.\nfunc GenerateConfigExistsFromString(val string) (GenerateConfigExists, error) {\n\tswitch val {\n\tcase ExistsErrorStr:\n\t\treturn ExistsError, nil\n\tcase ExistsSkipStr:\n\t\treturn ExistsSkip, nil\n\tcase ExistsOverwriteStr:\n\t\treturn ExistsOverwrite, nil\n\tcase ExistsOverwriteTerragruntStr:\n\t\treturn ExistsOverwriteTerragrunt, nil\n\t}\n\n\treturn ExistsUnknown, errors.New(UnknownGenerateIfExistsVal{val: val})\n}\n\n// GenerateConfigDisabledFromString converts a string representation of if_disabled into the enum, returning an error if it is not set to one of the known values.\nfunc GenerateConfigDisabledFromString(val string) (GenerateConfigDisabled, error) {\n\tswitch val {\n\tcase DisabledSkipStr:\n\t\treturn DisabledSkip, nil\n\tcase DisabledRemoveStr:\n\t\treturn DisabledRemove, nil\n\tcase DisabledRemoveTerragruntStr:\n\t\treturn DisabledRemoveTerragrunt, nil\n\t}\n\n\treturn DisabledUnknown, errors.New(UnknownGenerateIfDisabledVal{val: val})\n}\n"
  },
  {
    "path": "internal/codegen/generate_test.go",
    "content": "package codegen_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/codegen\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRemoteStateConfigToTerraformCode(t *testing.T) {\n\tt.Parallel()\n\n\texpectedOrdered := []byte(`terraform {\n  backend \"ordered\" {\n    a = 1\n    b = 2\n    c = 3\n  }\n  encryption {\n    key_provider \"test\" \"default\" {\n      a = 1\n      b = 2\n      c = 3\n    }\n    method \"aes_gcm\" \"default\" {\n      keys = key_provider.test.default\n    }\n    state {\n      method = method.aes_gcm.default\n    }\n    plan {\n      method = method.aes_gcm.default\n    }\n  }\n}\n`)\n\texpectedEmptyConfig := []byte(`terraform {\n  backend \"empty\" {\n  }\n  encryption {\n    key_provider \"test\" \"default\" {\n    }\n    method \"aes_gcm\" \"default\" {\n      keys = key_provider.test.default\n    }\n    state {\n      method = method.aes_gcm.default\n    }\n    plan {\n      method = method.aes_gcm.default\n    }\n  }\n}\n`)\n\texpectedEmptyEncryption := []byte(`terraform {\n  backend \"empty\" {\n  }\n}\n`)\n\texpectedS3WithAssumeRole := []byte(`terraform {\n  backend \"s3\" {\n    assume_role = {\n      duration        = \"1h30m\"\n      external_id     = \"123456789012\"\n      policy          = \"{}\"\n      policy_arns     = [\"arn:aws:iam::123456789012:policy/MyPolicy\"]\n      role_arn        = \"arn:aws:iam::123456789012:role/MyRole\"\n      session_name    = \"MySession\"\n      source_identity = \"123456789012\"\n      tags = {\n        key = \"value\"\n      }\n      transitive_tag_keys = [\"key\"]\n    }\n    bucket = \"mybucket\"\n  }\n}\n`)\n\texpectedS3WithAssumeRoleWithWebIdentity := []byte(`terraform {\n  backend \"s3\" {\n    assume_role_with_web_identity = {\n      duration                = \"1h30m\"\n      policy                  = \"{}\"\n      policy_arns             = [\"arn:aws:iam::123456789012:policy/MyPolicy\"]\n      role_arn                = \"arn:aws:iam::123456789012:role/MyRole\"\n      session_name            = \"MySession\"\n      web_identity_token      = \"123456789012\"\n      web_identity_token_file = \"/path/to/web_identity_token_file\"\n    }\n    bucket = \"mybucket\"\n  }\n}\n`)\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tbackend    string\n\t\tconfig     map[string]any\n\t\tencryption map[string]any\n\t\texpected   []byte\n\t\texpectErr  bool\n\t}{\n\t\t{\n\t\t\t\"remote-state-config-unsorted-keys\",\n\t\t\t\"ordered\",\n\t\t\tmap[string]any{\n\t\t\t\t\"b\": 2,\n\t\t\t\t\"a\": 1,\n\t\t\t\t\"c\": 3,\n\t\t\t},\n\t\t\tmap[string]any{\n\t\t\t\t\"key_provider\": \"test\",\n\t\t\t\t\"b\":            2,\n\t\t\t\t\"a\":            1,\n\t\t\t\t\"c\":            3,\n\t\t\t},\n\t\t\texpectedOrdered,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"remote-state-config-empty\",\n\t\t\t\"empty\",\n\t\t\tmap[string]any{},\n\t\t\tmap[string]any{\n\t\t\t\t\"key_provider\": \"test\",\n\t\t\t},\n\t\t\texpectedEmptyConfig,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"remote-state-encryption-empty\",\n\t\t\t\"empty\",\n\t\t\tmap[string]any{},\n\t\t\tmap[string]any{},\n\t\t\texpectedEmptyEncryption,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"remote-state-encryption-missing-key-provider\",\n\t\t\t\"empty\",\n\t\t\tmap[string]any{},\n\t\t\tmap[string]any{\n\t\t\t\t\"a\": 1,\n\t\t\t},\n\t\t\t[]byte(\"\"),\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"s3-backend-with-assume-role\",\n\t\t\t\"s3\",\n\t\t\tmap[string]any{\n\t\t\t\t\"bucket\":      \"mybucket\",\n\t\t\t\t\"assume_role\": \"{role_arn=\\\"arn:aws:iam::123456789012:role/MyRole\\\",tags={key=\\\"value\\\"}, duration=\\\"1h30m\\\", external_id=\\\"123456789012\\\", policy=\\\"{}\\\", policy_arns=[\\\"arn:aws:iam::123456789012:policy/MyPolicy\\\"], session_name=\\\"MySession\\\", source_identity=\\\"123456789012\\\", transitive_tag_keys=[\\\"key\\\"]}\",\n\t\t\t},\n\t\t\tmap[string]any{},\n\t\t\texpectedS3WithAssumeRole,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"s3-backend-with-assume-role-with-web-identity\",\n\t\t\t\"s3\",\n\t\t\tmap[string]any{\n\t\t\t\t\"bucket\":                        \"mybucket\",\n\t\t\t\t\"assume_role_with_web_identity\": \"{role_arn=\\\"arn:aws:iam::123456789012:role/MyRole\\\",duration=\\\"1h30m\\\", policy=\\\"{}\\\", policy_arns=[\\\"arn:aws:iam::123456789012:policy/MyPolicy\\\"], session_name=\\\"MySession\\\", web_identity_token=\\\"123456789012\\\", web_identity_token_file=\\\"/path/to/web_identity_token_file\\\"}\",\n\t\t\t},\n\t\t\tmap[string]any{},\n\t\t\texpectedS3WithAssumeRoleWithWebIdentity,\n\t\t\tfalse,\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.Parallel()\n\n\t\t\toutput, err := codegen.RemoteStateConfigToTerraformCode(tc.backend, tc.config, tc.encryption)\n\t\t\t// validates the first output.\n\t\t\tif tc.expectErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.True(t, bytes.Contains(output, []byte(tc.backend)))\n\t\t\t\t// Comparing as string produces a nicer diff\n\t\t\t\tassert.Equal(t, string(tc.expected), string(output))\n\t\t\t}\n\n\t\t\t// runs the function a few of times again. All the outputs must be\n\t\t\t// equal to the first output.\n\t\t\tfor range 20 {\n\t\t\t\tactual, _ := codegen.RemoteStateConfigToTerraformCode(tc.backend, tc.config, tc.encryption)\n\t\t\t\tassert.Equal(t, output, actual)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestRemoteStateConfigToTerraformCode_BoolValues verifies that native bool\n// values in the config map produce unquoted true/false in the generated HCL.\n// This is the expected output when string booleans from HCL ternary type\n// unification are normalized back to Go bools before reaching codegen.\nfunc TestRemoteStateConfigToTerraformCode_BoolValues(t *testing.T) {\n\tt.Parallel()\n\n\texpected := []byte(`terraform {\n  backend \"s3\" {\n    bucket       = \"my-bucket\"\n    encrypt      = true\n    key          = \"terraform.tfstate\"\n    region       = \"us-east-1\"\n    use_lockfile = true\n  }\n}\n`)\n\n\tconfig := map[string]any{\n\t\t\"bucket\":       \"my-bucket\",\n\t\t\"key\":          \"terraform.tfstate\",\n\t\t\"region\":       \"us-east-1\",\n\t\t\"encrypt\":      true,\n\t\t\"use_lockfile\": true,\n\t}\n\n\toutput, err := codegen.RemoteStateConfigToTerraformCode(\"s3\", config, map[string]any{})\n\trequire.NoError(t, err)\n\tassert.Equal(t, string(expected), string(output))\n}\n\n// TestRemoteStateConfigToTerraformCode_StringBoolProducesQuotedValue demonstrates\n// that string \"true\"/\"false\" values produce quoted string literals in generated HCL.\n// The fix for #5646 normalizes these in S3 GetTFInitArgs before they reach codegen.\nfunc TestRemoteStateConfigToTerraformCode_StringBoolProducesQuotedValue(t *testing.T) {\n\tt.Parallel()\n\n\tconfig := map[string]any{\n\t\t\"bucket\":       \"my-bucket\",\n\t\t\"key\":          \"terraform.tfstate\",\n\t\t\"region\":       \"us-east-1\",\n\t\t\"use_lockfile\": \"true\",\n\t}\n\n\toutput, err := codegen.RemoteStateConfigToTerraformCode(\"s3\", config, map[string]any{})\n\trequire.NoError(t, err)\n\n\t// String \"true\" produces a quoted string literal in HCL, which Terraform rejects\n\tassert.Contains(t, string(output), `use_lockfile = \"true\"`)\n}\n\nfunc TestFmtGeneratedFile(t *testing.T) {\n\tt.Parallel()\n\n\ttestDir := helpers.TmpDirWOSymlinks(t)\n\n\tbTrue := true\n\tbFalse := false\n\n\ttestCases := []struct {\n\t\tfmt      *bool\n\t\tname     string\n\t\tpath     string\n\t\tcontents string\n\t\texpected string\n\t\tifExists codegen.GenerateConfigExists\n\t\tdisabled bool\n\t}{\n\t\t{\n\t\t\tname:     \"fmt-simple-hcl-file\",\n\t\t\tfmt:      &bTrue,\n\t\t\tpath:     fmt.Sprintf(\"%s/%s\", testDir, \"fmt_simple.hcl\"),\n\t\t\tcontents: \"variable \\\"msg\\\"{\\ntype=string\\n  default=\\\"hello\\\"\\n}\\n\",\n\t\t\texpected: \"variable \\\"msg\\\" {\\n  type    = string\\n  default = \\\"hello\\\"\\n}\\n\",\n\t\t\tifExists: codegen.ExistsError,\n\t\t},\n\t\t{\n\t\t\tname:     \"fmt-hcl-file-by-default\",\n\t\t\tpath:     fmt.Sprintf(\"%s/%s\", testDir, \"fmt_hcl_file_by_default.hcl\"),\n\t\t\tcontents: \"variable \\\"msg\\\"{\\ntype=string\\n  default=\\\"hello\\\"\\n}\\n\",\n\t\t\texpected: \"variable \\\"msg\\\" {\\n  type    = string\\n  default = \\\"hello\\\"\\n}\\n\",\n\t\t\tifExists: codegen.ExistsError,\n\t\t},\n\t\t{\n\t\t\tname:     \"ignore-hcl-fmt\",\n\t\t\tfmt:      &bFalse,\n\t\t\tpath:     fmt.Sprintf(\"%s/%s\", testDir, \"ignore_hcl_fmt.hcl\"),\n\t\t\tcontents: \"variable \\\"msg\\\"{\\ntype=string\\n  default=\\\"hello\\\"\\n}\\n\",\n\t\t\texpected: \"variable \\\"msg\\\"{\\ntype=string\\n  default=\\\"hello\\\"\\n}\\n\",\n\t\t\tifExists: codegen.ExistsError,\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.Parallel()\n\n\t\t\tconfig := codegen.GenerateConfig{\n\t\t\t\tPath:             tc.path,\n\t\t\t\tIfExists:         tc.ifExists,\n\t\t\t\tCommentPrefix:    \"\",\n\t\t\t\tDisableSignature: true,\n\t\t\t\tContents:         tc.contents,\n\t\t\t\tDisable:          tc.disabled,\n\t\t\t\tHclFmt:           tc.fmt,\n\t\t\t}\n\n\t\t\tl := logger.CreateLogger()\n\t\t\terr := codegen.WriteToFile(l, \"\", &config)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.True(t, util.FileExists(tc.path))\n\n\t\t\tfileContent, err := os.ReadFile(tc.path)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.expected, string(fileContent))\n\t\t})\n\t}\n}\n\nfunc TestGenerateDisabling(t *testing.T) {\n\tt.Parallel()\n\n\ttestDir := helpers.TmpDirWOSymlinks(t)\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tpath     string\n\t\tcontents string\n\t\tifExists codegen.GenerateConfigExists\n\t\tdisabled bool\n\t}{\n\t\t{\n\t\t\tname:     \"generate-disabled-true\",\n\t\t\tpath:     fmt.Sprintf(\"%s/%s\", testDir, \"disabled_true\"),\n\t\t\tcontents: \"this file should not be generated\",\n\t\t\tifExists: codegen.ExistsError,\n\t\t\tdisabled: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"generate-disabled-false\",\n\t\t\tpath:     fmt.Sprintf(\"%s/%s\", testDir, \"disabled_false\"),\n\t\t\tcontents: \"this file should be generated\",\n\t\t\tifExists: codegen.ExistsError,\n\t\t\tdisabled: false,\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.Parallel()\n\n\t\t\tconfig := codegen.GenerateConfig{\n\t\t\t\tPath:             tc.path,\n\t\t\t\tIfExists:         tc.ifExists,\n\t\t\t\tCommentPrefix:    \"\",\n\t\t\t\tDisableSignature: false,\n\t\t\t\tContents:         tc.contents,\n\t\t\t\tDisable:          tc.disabled,\n\t\t\t}\n\n\t\t\tl := logger.CreateLogger()\n\t\t\terr := codegen.WriteToFile(l, \"\", &config)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif tc.disabled {\n\t\t\t\tassert.True(t, util.FileNotExists(tc.path))\n\t\t\t} else {\n\t\t\t\tassert.True(t, util.FileExists(tc.path))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestReplaceAllCommasOutsideQuotesWithNewLines(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:  \"happy-path-basic-replacement\",\n\t\t\tinput: `key=value,another=value,third=value`,\n\t\t\texpected: `key=value\nanother=value\nthird=value`,\n\t\t},\n\t\t{\n\t\t\tname:  \"comma-inside-quotes\",\n\t\t\tinput: `key=\"value,with,commas\",another=value`,\n\t\t\texpected: `key=\"value,with,commas\"\nanother=value`,\n\t\t},\n\t\t{\n\t\t\tname:  \"mixed-quotes-and-commas\",\n\t\t\tinput: `key=\"value,with,commas\",simple=value,quoted=\"hello,world\"`,\n\t\t\texpected: `key=\"value,with,commas\"\nsimple=value\nquoted=\"hello,world\"`,\n\t\t},\n\t\t{\n\t\t\tname:     \"empty-string\",\n\t\t\tinput:    ``,\n\t\t\texpected: ``,\n\t\t},\n\t\t{\n\t\t\tname:     \"no-commas\",\n\t\t\tinput:    `key=value`,\n\t\t\texpected: `key=value`,\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.Parallel()\n\n\t\t\tactual := codegen.ReplaceAllCommasOutsideQuotesWithNewLines(tc.input)\n\t\t\tassert.Equal(t, tc.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/component/component.go",
    "content": "// Package component provides types for representing discovered Terragrunt components.\n//\n// These include units and stacks.\n//\n// This package contains only data types and their associated methods, with no discovery logic.\n// It exists separately from the discovery package to allow other packages (like filter) to\n// depend on these types without creating circular dependencies.\npackage component\n\nimport (\n\t\"slices\"\n\t\"sort\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n)\n\n// Kind is the type of Terragrunt component.\ntype Kind string\n\n// Component represents a discovered Terragrunt configuration.\n// This interface is implemented by Unit and Stack.\ntype Component interface {\n\tKind() Kind\n\tPath() string\n\tSetPath(string)\n\tDisplayPath() string\n\tExternal() bool\n\tSetExternal()\n\tReading() []string\n\tSetReading(...string)\n\tSources() []string\n\tConfigFile() string\n\tDiscoveryContext() *DiscoveryContext\n\tSetDiscoveryContext(*DiscoveryContext)\n\tOrigin() Origin\n\tAddDependency(Component)\n\tAddDependent(Component)\n\tDependencies() Components\n\tDependents() Components\n\n\tlock()\n\tunlock()\n\trLock()\n\trUnlock()\n\n\tensureDependency(Component)\n\tensureDependent(Component)\n}\n\n// Origin determines the discovery origin of a component.\n// This is important if there are multiple different reasons that a component might have been discovered.\n//\n// e.g. A component might be discovered in a Git worktree due to graph discovery from the results of a Git-based filter.\ntype Origin string\n\nconst (\n\tOriginUnknown               Origin = \"unknown\"\n\tOriginWorktreeDiscovery     Origin = \"worktree-discovery\"\n\tOriginGraphDiscovery        Origin = \"graph-discovery\"\n\tOriginPathDiscovery         Origin = \"path-discovery\"\n\tOriginRelationshipDiscovery Origin = \"relationship-discovery\"\n)\n\n// DiscoveryContext is the context in which\n// a Component was discovered.\n//\n// It's useful to know this information,\n// because it can help us determine how the\n// Component should be run or enqueued later.\ntype DiscoveryContext struct {\n\tWorkingDir string\n\tRef        string\n\n\torigin Origin\n\n\tCmd  string\n\tArgs []string\n}\n\n// Copy returns a copy of the DiscoveryContext.\nfunc (dc *DiscoveryContext) Copy() *DiscoveryContext {\n\tc := *dc\n\n\treturn &c\n}\n\n// CopyWithNewOrigin returns a copy of the DiscoveryContext with the origin set to the given origin.\n//\n// Discovered components should never have their origin overridden by subsequent phases of discovery. Only use this\n// method if you are discovering a new component that was originally discovered by a different discovery phase.\n//\n// e.g. A component discovered as a dependency/dependent of a component discovered via Git discovery should be\n// considered discovered via graph discovery, not Git discovery.\nfunc (dc *DiscoveryContext) CopyWithNewOrigin(origin Origin) *DiscoveryContext {\n\tc := dc.Copy()\n\tc.origin = origin\n\n\treturn c\n}\n\n// Origin returns the origin of the DiscoveryContext.\nfunc (dc *DiscoveryContext) Origin() Origin {\n\tif dc.origin == \"\" {\n\t\treturn OriginUnknown\n\t}\n\n\treturn dc.origin\n}\n\n// SuggestOrigin suggests an origin for the DiscoveryContext.\n//\n// Only actually updates the origin if it is empty. This is to ensure that the origin of a component is always\n// considered the first origin discovered for that component, and that it can't be overridden by subsequent phases\n// of discovery that might re-discover the same component.\nfunc (dc *DiscoveryContext) SuggestOrigin(origin Origin) {\n\tif dc.origin == \"\" {\n\t\tdc.origin = origin\n\t}\n}\n\n// Components is a list of discovered Terragrunt components.\ntype Components []Component\n\n// Sort sorts the Components by path.\nfunc (c Components) Sort() Components {\n\tsort.Slice(c, func(i, j int) bool {\n\t\treturn c[i].Path() < c[j].Path()\n\t})\n\n\treturn c\n}\n\n// Filter filters the Components by config type.\nfunc (c Components) Filter(kind Kind) Components {\n\tif len(c) == 0 {\n\t\treturn c\n\t}\n\n\tfiltered := make(Components, 0, len(c))\n\n\tfor _, component := range c {\n\t\tif component.Kind() == kind {\n\t\t\tfiltered = append(filtered, component)\n\t\t}\n\t}\n\n\treturn filtered\n}\n\n// FilterByPath filters the Components by path.\nfunc (c Components) FilterByPath(path string) Components {\n\tfiltered := make(Components, 0, 1)\n\n\tfor _, component := range c {\n\t\tif component.Path() == path {\n\t\t\tfiltered = append(filtered, component)\n\t\t}\n\t}\n\n\treturn filtered\n}\n\n// RemoveByPath removes the Component with the given path from the Components.\nfunc (c Components) RemoveByPath(path string) Components {\n\tif len(c) == 0 {\n\t\treturn c\n\t}\n\n\tfiltered := make(Components, 0, len(c)-1)\n\n\tfor _, component := range c {\n\t\tif component.Path() != path {\n\t\t\tfiltered = append(filtered, component)\n\t\t}\n\t}\n\n\treturn filtered\n}\n\n// Paths returns the paths of the Components.\nfunc (c Components) Paths() []string {\n\tpaths := make([]string, 0, len(c))\n\tfor _, component := range c {\n\t\t// Skip units explicitly marked as excluded.\n\t\tif unit, ok := component.(*Unit); ok && unit.Excluded() {\n\t\t\tcontinue\n\t\t}\n\n\t\tpaths = append(paths, component.Path())\n\t}\n\n\treturn paths\n}\n\n// CycleCheck checks for cycles in the dependency graph.\n// If a cycle is detected, it returns the first Component that is part of the cycle, and an error.\n// If no cycle is detected, it returns nil and nil.\nfunc (c Components) CycleCheck() (Component, error) {\n\tvisited := make(map[string]bool)\n\tinPath := make(map[string]bool)\n\n\tvar checkCycle func(component Component) error\n\n\tcheckCycle = func(component Component) error {\n\t\tif inPath[component.Path()] {\n\t\t\treturn errors.New(\"cycle detected in dependency graph at path: \" + component.Path())\n\t\t}\n\n\t\tif visited[component.Path()] {\n\t\t\treturn nil\n\t\t}\n\n\t\tvisited[component.Path()] = true\n\t\tinPath[component.Path()] = true\n\n\t\tfor _, dep := range component.Dependencies() {\n\t\t\tif err := checkCycle(dep); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tinPath[component.Path()] = false\n\n\t\treturn nil\n\t}\n\n\tfor _, component := range c {\n\t\tif !visited[component.Path()] {\n\t\t\tif err := checkCycle(component); err != nil {\n\t\t\t\treturn component, err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, nil\n}\n\n// ThreadSafeComponents provides thread-safe access to a Components slice.\n// It uses an RWMutex to allow concurrent reads and serialized writes.\n// Resolved paths are cached to avoid repeated filepath.EvalSymlinks syscalls\n// and ensure consistent symlink-aware comparisons across all methods.\ntype ThreadSafeComponents struct {\n\tresolvedPaths map[string]string\n\tcomponents    Components\n\tmu            sync.RWMutex\n}\n\n// NewThreadSafeComponents creates a new ThreadSafeComponents instance with the given components.\nfunc NewThreadSafeComponents(components Components) *ThreadSafeComponents {\n\ttsc := &ThreadSafeComponents{\n\t\tcomponents:    components,\n\t\tresolvedPaths: make(map[string]string, len(components)),\n\t}\n\n\t// Pre-populate resolved paths cache for initial components\n\tfor _, c := range components {\n\t\ttsc.resolvedPaths[c.Path()] = util.ResolvePath(c.Path())\n\t}\n\n\treturn tsc\n}\n\n// resolvedPathFor returns the cached resolved path for a component path if present,\n// otherwise resolves the path on the fly without mutating the cache.\n// Caller must hold at least a read lock.\nfunc (tsc *ThreadSafeComponents) resolvedPathFor(path string) string {\n\tif resolved, ok := tsc.resolvedPaths[path]; ok {\n\t\treturn resolved\n\t}\n\n\treturn util.ResolvePath(path)\n}\n\n// EnsureComponent adds a component to the components list if it's not already present.\n// This method is TOCTOU-safe (Time-Of-Check-Time-Of-Use) by using a double-check pattern.\n// Path comparison uses resolved symlink paths for consistency.\n//\n// It returns the component if it was added, and a boolean indicating if it was added.\nfunc (tsc *ThreadSafeComponents) EnsureComponent(c Component) (Component, bool) {\n\tfound, ok := tsc.findComponent(c)\n\tif !ok {\n\t\treturn tsc.addComponent(c)\n\t}\n\n\treturn found, false\n}\n\n// findComponent checks if a component is in the components slice using resolved paths.\n// If it is, it returns the component and true.\n// If it is not, it returns nil and false.\nfunc (tsc *ThreadSafeComponents) findComponent(c Component) (Component, bool) {\n\ttsc.mu.RLock()\n\tdefer tsc.mu.RUnlock()\n\n\tsearchResolved := util.ResolvePath(c.Path())\n\n\tidx := slices.IndexFunc(tsc.components, func(cc Component) bool {\n\t\treturn tsc.resolvedPathFor(cc.Path()) == searchResolved\n\t})\n\n\tif idx == -1 {\n\t\treturn nil, false\n\t}\n\n\treturn tsc.components[idx], true\n}\n\n// addComponent adds a component to the components list, acquiring a write lock.\n// Uses a double-check pattern to avoid TOCTOU race conditions.\n// Caches the resolved path for the new component.\nfunc (tsc *ThreadSafeComponents) addComponent(c Component) (Component, bool) {\n\ttsc.mu.Lock()\n\tdefer tsc.mu.Unlock()\n\n\tsearchResolved := util.ResolvePath(c.Path())\n\n\t// Do one last check to see if the component is already in the components list\n\t// to avoid a TOCTOU race condition. Uses resolved paths for comparison.\n\tidx := slices.IndexFunc(tsc.components, func(cc Component) bool {\n\t\treturn tsc.resolvedPathFor(cc.Path()) == searchResolved\n\t})\n\n\tif idx != -1 {\n\t\treturn tsc.components[idx], false\n\t}\n\n\t// Cache resolved path and add component\n\ttsc.resolvedPaths[c.Path()] = searchResolved\n\ttsc.components = append(tsc.components, c)\n\n\treturn c, true\n}\n\n// FindByPath searches for a component by its path and returns it if found, otherwise returns nil.\n// Paths are resolved to handle symlinks consistently across platforms (e.g., macOS /var -> /private/var).\n// Uses cached resolved paths to avoid repeated syscalls.\nfunc (tsc *ThreadSafeComponents) FindByPath(path string) Component {\n\ttsc.mu.RLock()\n\tdefer tsc.mu.RUnlock()\n\n\tresolvedSearchPath := util.ResolvePath(path)\n\n\tfor _, c := range tsc.components {\n\t\tif tsc.resolvedPathFor(c.Path()) == resolvedSearchPath {\n\t\t\treturn c\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// ToComponents returns a copy of the components slice.\nfunc (tsc *ThreadSafeComponents) ToComponents() Components {\n\ttsc.mu.RLock()\n\tdefer tsc.mu.RUnlock()\n\n\t// Return a copy to prevent external modification\n\tresult := make(Components, len(tsc.components))\n\tcopy(result, tsc.components)\n\n\treturn result\n}\n\n// Len returns the number of components in the components slice.\nfunc (tsc *ThreadSafeComponents) Len() int {\n\ttsc.mu.RLock()\n\tdefer tsc.mu.RUnlock()\n\n\treturn len(tsc.components)\n}\n"
  },
  {
    "path": "internal/component/component_test.go",
    "content": "package component_test\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestComponentsSort(t *testing.T) {\n\tt.Parallel()\n\n\t// Setup\n\tconfigs := component.Components{\n\t\tcomponent.NewUnit(\"c\"),\n\t\tcomponent.NewUnit(\"a\"),\n\t\tcomponent.NewStack(\"b\"),\n\t}\n\n\t// Act\n\tsorted := configs.Sort()\n\n\t// Assert\n\trequire.Len(t, sorted, 3)\n\tassert.Equal(t, \"a\", sorted[0].Path())\n\tassert.Equal(t, \"b\", sorted[1].Path())\n\tassert.Equal(t, \"c\", sorted[2].Path())\n}\n\nfunc TestComponentsFilter(t *testing.T) {\n\tt.Parallel()\n\n\t// Setup\n\tconfigs := component.Components{\n\t\tcomponent.NewUnit(\"unit1\"),\n\t\tcomponent.NewStack(\"stack1\"),\n\t\tcomponent.NewUnit(\"unit2\"),\n\t}\n\n\t// Test unit filtering\n\tt.Run(\"filter units\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tunits := configs.Filter(component.UnitKind)\n\t\trequire.Len(t, units, 2)\n\t\tassert.Equal(t, component.UnitKind, units[0].Kind())\n\t\tassert.Equal(t, component.UnitKind, units[1].Kind())\n\t\tassert.ElementsMatch(t, []string{\"unit1\", \"unit2\"}, units.Paths())\n\t})\n\n\t// Test stack filtering\n\tt.Run(\"filter stacks\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tstacks := configs.Filter(component.StackKind)\n\t\trequire.Len(t, stacks, 1)\n\t\tassert.Equal(t, component.StackKind, stacks[0].Kind())\n\t\tassert.Equal(t, \"stack1\", stacks[0].Path())\n\t})\n}\n\nfunc TestComponentsCycleCheck(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tsetupFunc     func() component.Components\n\t\tname          string\n\t\terrorExpected bool\n\t}{\n\t\t{\n\t\t\tname: \"no cycles\",\n\t\t\tsetupFunc: func() component.Components {\n\t\t\t\ta := component.NewUnit(\"a\")\n\t\t\t\tb := component.NewUnit(\"b\")\n\t\t\t\ta.AddDependency(b)\n\n\t\t\t\treturn component.Components{a, b}\n\t\t\t},\n\t\t\terrorExpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"direct cycle\",\n\t\t\tsetupFunc: func() component.Components {\n\t\t\t\ta := component.NewUnit(\"a\")\n\t\t\t\tb := component.NewUnit(\"b\")\n\t\t\t\ta.AddDependency(b)\n\t\t\t\tb.AddDependency(a)\n\n\t\t\t\treturn component.Components{a, b}\n\t\t\t},\n\t\t\terrorExpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"indirect cycle\",\n\t\t\tsetupFunc: func() component.Components {\n\t\t\t\ta := component.NewUnit(\"a\")\n\t\t\t\tb := component.NewUnit(\"b\")\n\t\t\t\tc := component.NewUnit(\"c\")\n\n\t\t\t\ta.AddDependency(b)\n\t\t\t\tb.AddDependency(c)\n\t\t\t\tc.AddDependency(a)\n\n\t\t\t\treturn component.Components{a, b, c}\n\t\t\t},\n\t\t\terrorExpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"diamond dependency - no cycle\",\n\t\t\tsetupFunc: func() component.Components {\n\t\t\t\ta := component.NewUnit(\"a\")\n\t\t\t\tb := component.NewUnit(\"b\")\n\t\t\t\tc := component.NewUnit(\"c\")\n\t\t\t\td := component.NewUnit(\"d\")\n\n\t\t\t\ta.AddDependency(b)\n\t\t\t\ta.AddDependency(c)\n\t\t\t\tb.AddDependency(d)\n\t\t\t\tc.AddDependency(d)\n\n\t\t\t\treturn component.Components{a, b, c, d}\n\t\t\t},\n\t\t\terrorExpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tconfigs := tt.setupFunc()\n\n\t\t\tcfg, err := configs.CycleCheck()\n\t\t\tif tt.errorExpected {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), \"cycle detected\")\n\t\t\t\tassert.NotNil(t, cfg)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Nil(t, cfg)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestUnitStringConcurrent(t *testing.T) {\n\tt.Parallel()\n\n\tunit := component.NewUnit(\"/test/path\")\n\tdep := component.NewUnit(\"/test/dep\")\n\tunit.AddDependency(dep)\n\n\tvar wg sync.WaitGroup\n\n\tconst goroutines = 10\n\n\tfor range goroutines {\n\t\twg.Add(1)\n\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\tfor range 100 {\n\t\t\t\ts := unit.String()\n\t\t\t\tassert.Contains(t, s, \"/test/path\")\n\t\t\t}\n\t\t}()\n\t}\n\n\twg.Wait()\n}\n\nfunc TestThreadSafeComponentsEnsureNoDuplicates(t *testing.T) {\n\tt.Parallel()\n\n\ttsc := component.NewThreadSafeComponents(component.Components{})\n\n\t// Add same path twice - should not duplicate\n\tunit1 := component.NewUnit(\"/test/path\")\n\tunit2 := component.NewUnit(\"/test/path\")\n\n\tadded1, wasAdded1 := tsc.EnsureComponent(unit1)\n\tadded2, wasAdded2 := tsc.EnsureComponent(unit2)\n\n\tassert.True(t, wasAdded1, \"first component should be added\")\n\tassert.False(t, wasAdded2, \"second component should not be added (duplicate)\")\n\tassert.Same(t, added1, added2, \"should return same component instance\")\n\tassert.Equal(t, 1, tsc.Len(), \"should have exactly one component\")\n}\n\nfunc TestThreadSafeComponentsFindByPath(t *testing.T) {\n\tt.Parallel()\n\n\tunit := component.NewUnit(\"/test/path\")\n\ttsc := component.NewThreadSafeComponents(component.Components{unit})\n\n\t// Find by exact path\n\tfound := tsc.FindByPath(\"/test/path\")\n\tassert.NotNil(t, found, \"should find component by exact path\")\n\tassert.Equal(t, \"/test/path\", found.Path())\n\n\t// Find non-existent path\n\tnotFound := tsc.FindByPath(\"/nonexistent\")\n\tassert.Nil(t, notFound, \"should not find non-existent path\")\n}\n\nfunc TestThreadSafeComponentsConcurrentAccess(t *testing.T) {\n\tt.Parallel()\n\n\ttsc := component.NewThreadSafeComponents(component.Components{})\n\n\tvar wg sync.WaitGroup\n\n\tconst goroutines = 10\n\n\t// Concurrent writes tests\n\tfor range goroutines {\n\t\twg.Go(func() {\n\t\t\tunit := component.NewUnit(\"/test/path\")\n\t\t\ttsc.EnsureComponent(unit)\n\t\t})\n\t}\n\n\t// Concurrent reads\n\tfor range goroutines {\n\t\twg.Go(func() {\n\t\t\tfor range 100 {\n\t\t\t\t_ = tsc.FindByPath(\"/test/path\")\n\t\t\t\t_ = tsc.Len()\n\t\t\t\t_ = tsc.ToComponents()\n\t\t\t}\n\t\t})\n\t}\n\n\twg.Wait()\n\n\t// Should have exactly one component despite concurrent adds\n\tassert.Equal(t, 1, tsc.Len(), \"should have exactly one component after concurrent adds\")\n}\n"
  },
  {
    "path": "internal/component/stack.go",
    "content": "package component\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n)\n\nconst (\n\tStackKind Kind = \"stack\"\n)\n\n// Stack represents a discovered Terragrunt stack configuration.\ntype Stack struct {\n\tcfg              *config.StackConfig\n\tdiscoveryContext *DiscoveryContext\n\tpath             string\n\treading          []string\n\tdependencies     Components\n\tdependents       Components\n\tUnits            []*Unit\n\tmu               sync.RWMutex\n\texternal         bool\n}\n\n// NewStack creates a new Stack component with the given path.\nfunc NewStack(path string) *Stack {\n\treturn &Stack{\n\t\tpath:             path,\n\t\tdiscoveryContext: &DiscoveryContext{},\n\t\tdependencies:     make(Components, 0),\n\t\tdependents:       make(Components, 0),\n\t}\n}\n\n// WithDiscoveryContext sets the discovery context for this stack.\nfunc (s *Stack) WithDiscoveryContext(ctx *DiscoveryContext) *Stack {\n\ts.discoveryContext = ctx\n\n\treturn s\n}\n\n// Config returns the parsed Stack configuration for this stack.\nfunc (s *Stack) Config() *config.StackConfig {\n\treturn s.cfg\n}\n\n// StoreConfig stores the parsed Stack configuration for this stack.\nfunc (s *Stack) StoreConfig(cfg *config.StackConfig) {\n\ts.cfg = cfg\n}\n\n// Kind returns the kind of component (always Stack for Stack).\nfunc (s *Stack) Kind() Kind {\n\treturn StackKind\n}\n\n// Path returns the path to the component.\nfunc (s *Stack) Path() string {\n\treturn s.path\n}\n\n// SetPath sets the path to the component.\nfunc (s *Stack) SetPath(path string) {\n\ts.path = path\n}\n\n// DisplayPath returns the path relative to DiscoveryContext.WorkingDir for display purposes.\n// Falls back to the original path if relative path calculation fails or WorkingDir is empty.\nfunc (s *Stack) DisplayPath() string {\n\tif s.discoveryContext == nil || s.discoveryContext.WorkingDir == \"\" {\n\t\treturn s.path\n\t}\n\n\tif rel, err := filepath.Rel(s.discoveryContext.WorkingDir, s.path); err == nil {\n\t\treturn rel\n\t}\n\n\treturn s.path\n}\n\n// External returns whether the component is external.\nfunc (s *Stack) External() bool {\n\treturn s.external\n}\n\n// SetExternal marks the component as external.\nfunc (s *Stack) SetExternal() {\n\ts.external = true\n}\n\n// Reading returns the list of files being read by this component.\nfunc (s *Stack) Reading() []string {\n\treturn s.reading\n}\n\n// SetReading sets the list of files being read by this component.\nfunc (s *Stack) SetReading(files ...string) {\n\ts.reading = files\n}\n\n// Sources returns the list of sources for this component.\n//\n// Stacks don't support leveraging sources right now, so we just return an empty list.\nfunc (s *Stack) Sources() []string {\n\treturn []string{}\n}\n\n// ConfigFile returns the config filename for this stack.\nfunc (s *Stack) ConfigFile() string {\n\treturn config.DefaultStackFile\n}\n\n// DiscoveryContext returns the discovery context for this component.\nfunc (s *Stack) DiscoveryContext() *DiscoveryContext {\n\treturn s.discoveryContext\n}\n\n// SetDiscoveryContext sets the discovery context for this component.\nfunc (s *Stack) SetDiscoveryContext(ctx *DiscoveryContext) {\n\ts.discoveryContext = ctx\n}\n\n// Origin returns the origin of the discovery context for this component.\nfunc (s *Stack) Origin() Origin {\n\tif s.discoveryContext == nil {\n\t\treturn OriginUnknown\n\t}\n\n\treturn s.discoveryContext.Origin()\n}\n\n// lock locks the Stack.\nfunc (s *Stack) lock() {\n\ts.mu.Lock()\n}\n\n// unlock unlocks the Stack.\nfunc (s *Stack) unlock() {\n\ts.mu.Unlock()\n}\n\n// rLock locks the Stack for reading.\nfunc (s *Stack) rLock() {\n\ts.mu.RLock()\n}\n\n// rUnlock unlocks the Stack for reading.\nfunc (s *Stack) rUnlock() {\n\ts.mu.RUnlock()\n}\n\n// AddDependency adds a dependency to the Stack and vice versa.\n//\n// Using this method ensure that the dependency graph is properly maintained,\n// making it easier to look up dependents and dependencies on a given component\n// without the entire graph available.\nfunc (s *Stack) AddDependency(dependency Component) {\n\ts.ensureDependency(dependency)\n\n\tdependency.ensureDependent(s)\n}\n\n// ensureDependency adds a dependency to a stack if it's not already present.\nfunc (s *Stack) ensureDependency(dependency Component) {\n\ts.lock()\n\tdefer s.unlock()\n\n\tif !slices.Contains(s.dependencies, dependency) {\n\t\ts.dependencies = append(s.dependencies, dependency)\n\t}\n}\n\n// ensureDependent adds a dependent to a stack if it's not already present.\nfunc (s *Stack) ensureDependent(dependent Component) {\n\ts.lock()\n\tdefer s.unlock()\n\n\tif !slices.Contains(s.dependents, dependent) {\n\t\ts.dependents = append(s.dependents, dependent)\n\t}\n}\n\n// AddDependent adds a dependent to the Stack and vice versa.\n//\n// Using this method ensure that the dependency graph is properly maintained,\n// making it easier to look up dependents and dependencies on a given component\n// without the entire graph available.\nfunc (s *Stack) AddDependent(dependent Component) {\n\ts.ensureDependent(dependent)\n\n\tdependent.ensureDependency(s)\n}\n\n// Dependencies returns the dependencies of the Stack.\nfunc (s *Stack) Dependencies() Components {\n\ts.rLock()\n\tdefer s.rUnlock()\n\n\treturn s.dependencies\n}\n\n// Dependents returns the dependents of the Stack.\nfunc (s *Stack) Dependents() Components {\n\ts.rLock()\n\tdefer s.rUnlock()\n\n\treturn s.dependents\n}\n\n// String renders this stack as a human-readable string.\n//\n// Example output:\n//\n//\tStack at /path/to/stack:\n//\t  => Unit /path/to/unit1 (excluded: false, assume applied: false, dependencies: [/dep1])\n//\t  => Unit /path/to/unit2 (excluded: true, assume applied: false, dependencies: [])\nfunc (s *Stack) String() string {\n\tunits := make([]string, 0, len(s.Units))\n\tfor _, unit := range s.Units {\n\t\tunits = append(units, \"  => \"+unit.String())\n\t}\n\n\tsort.Strings(units)\n\n\tworkingDir := s.path\n\tif s.discoveryContext != nil && s.discoveryContext.WorkingDir != \"\" {\n\t\tworkingDir = s.discoveryContext.WorkingDir\n\t}\n\n\treturn fmt.Sprintf(\"Stack at %s:\\n%s\", workingDir, strings.Join(units, \"\\n\"))\n}\n\n// FindUnitByPath finds a unit in the stack by its path.\nfunc (s *Stack) FindUnitByPath(path string) *Unit {\n\tfor _, unit := range s.Units {\n\t\tif unit.Path() == path {\n\t\t\treturn unit\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/component/unit.go",
    "content": "package component\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n)\n\nconst (\n\tUnitKind Kind = \"unit\"\n)\n\n// Unit represents a discovered Terragrunt unit configuration.\ntype Unit struct {\n\tcfg              *config.TerragruntConfig\n\tdiscoveryContext *DiscoveryContext\n\tpath             string\n\tconfigFile       string\n\treading          []string\n\tdependencies     Components\n\tdependents       Components\n\tmu               sync.RWMutex\n\texternal         bool\n\texcluded         bool\n}\n\n// NewUnit creates a new Unit component with the given path.\nfunc NewUnit(path string) *Unit {\n\treturn &Unit{\n\t\tpath:             path,\n\t\tconfigFile:       config.DefaultTerragruntConfigPath,\n\t\tdiscoveryContext: &DiscoveryContext{},\n\t\tdependencies:     make(Components, 0),\n\t\tdependents:       make(Components, 0),\n\t}\n}\n\n// WithReading appends a file to the list of files being read by this component.\n// Useful for constructing components with all files read at once.\nfunc (u *Unit) WithReading(files ...string) *Unit {\n\tu.SetReading(files...)\n\n\treturn u\n}\n\n// WithConfig adds configuration to a Unit component.\nfunc (u *Unit) WithConfig(cfg *config.TerragruntConfig) *Unit {\n\tu.cfg = cfg\n\n\treturn u\n}\n\n// WithDiscoveryContext sets the discovery context for this unit.\nfunc (u *Unit) WithDiscoveryContext(ctx *DiscoveryContext) *Unit {\n\tu.discoveryContext = ctx\n\n\treturn u\n}\n\n// Config returns the parsed Terragrunt configuration for this unit.\nfunc (u *Unit) Config() *config.TerragruntConfig {\n\treturn u.cfg\n}\n\n// StoreConfig stores the parsed Terragrunt configuration for this unit.\nfunc (u *Unit) StoreConfig(cfg *config.TerragruntConfig) {\n\tu.cfg = cfg\n}\n\n// ConfigFile returns the discovered config filename for this unit.\nfunc (u *Unit) ConfigFile() string {\n\treturn u.configFile\n}\n\n// SetConfigFile sets the discovered config filename for this unit.\nfunc (u *Unit) SetConfigFile(filename string) {\n\tu.configFile = filename\n}\n\n// Kind returns the kind of component (always Unit for Unit).\nfunc (u *Unit) Kind() Kind {\n\treturn UnitKind\n}\n\n// Path returns the path to the component.\nfunc (u *Unit) Path() string {\n\treturn u.path\n}\n\n// SetPath sets the path to the component.\nfunc (u *Unit) SetPath(path string) {\n\tu.path = path\n}\n\n// External returns whether the component is external.\nfunc (u *Unit) External() bool {\n\treturn u.external\n}\n\n// SetExternal marks the component as external.\nfunc (u *Unit) SetExternal() {\n\tu.external = true\n}\n\n// Excluded returns whether the unit was excluded during discovery/filtering.\nfunc (u *Unit) Excluded() bool {\n\treturn u.excluded\n}\n\n// SetExcluded marks the unit as excluded during discovery/filtering.\nfunc (u *Unit) SetExcluded(excluded bool) {\n\tu.excluded = excluded\n}\n\n// Reading returns the list of files being read by this component.\nfunc (u *Unit) Reading() []string {\n\treturn u.reading\n}\n\n// SetReading sets the list of files being read by this component.\nfunc (u *Unit) SetReading(files ...string) {\n\tu.reading = files\n}\n\n// Sources returns the list of sources for this component.\nfunc (u *Unit) Sources() []string {\n\tif u.cfg == nil || u.cfg.Terraform == nil || u.cfg.Terraform.Source == nil {\n\t\treturn []string{}\n\t}\n\n\treturn []string{*u.cfg.Terraform.Source}\n}\n\n// DiscoveryContext returns the discovery context for this component.\nfunc (u *Unit) DiscoveryContext() *DiscoveryContext {\n\treturn u.discoveryContext\n}\n\n// SetDiscoveryContext sets the discovery context for this component.\nfunc (u *Unit) SetDiscoveryContext(ctx *DiscoveryContext) {\n\tu.discoveryContext = ctx\n}\n\n// Origin returns the origin of the discovery context for this component.\nfunc (u *Unit) Origin() Origin {\n\tif u.discoveryContext == nil {\n\t\treturn OriginUnknown\n\t}\n\n\treturn u.discoveryContext.Origin()\n}\n\n// lock locks the Unit.\nfunc (u *Unit) lock() {\n\tu.mu.Lock()\n}\n\n// unlock unlocks the Unit.\nfunc (u *Unit) unlock() {\n\tu.mu.Unlock()\n}\n\n// rLock locks the Unit for reading.\nfunc (u *Unit) rLock() {\n\tu.mu.RLock()\n}\n\n// rUnlock unlocks the Unit for reading.\nfunc (u *Unit) rUnlock() {\n\tu.mu.RUnlock()\n}\n\n// AddDependency adds a dependency to the Unit and vice versa.\n//\n// Using this method ensure that the dependency graph is properly maintained,\n// making it easier to look up dependents and dependencies on a given component\n// without the entire graph available.\nfunc (u *Unit) AddDependency(dependency Component) {\n\tu.ensureDependency(dependency)\n\n\tdependency.ensureDependent(u)\n}\n\n// ensureDependency adds a dependency to a unit if it's not already present.\nfunc (u *Unit) ensureDependency(dependency Component) {\n\tu.lock()\n\tdefer u.unlock()\n\n\tif !slices.Contains(u.dependencies, dependency) {\n\t\tu.dependencies = append(u.dependencies, dependency)\n\t}\n}\n\n// ensureDependent adds a dependent to a unit if it's not already present.\nfunc (u *Unit) ensureDependent(dependent Component) {\n\tu.lock()\n\tdefer u.unlock()\n\n\tif !slices.Contains(u.dependents, dependent) {\n\t\tu.dependents = append(u.dependents, dependent)\n\t}\n}\n\n// AddDependent adds a dependent to the Unit and vice versa.\n//\n// Using this method ensure that the dependency graph is properly maintained,\n// making it easier to look up dependents and dependencies on a given component\n// without the entire graph available.\nfunc (u *Unit) AddDependent(dependent Component) {\n\tu.ensureDependent(dependent)\n\n\tdependent.ensureDependency(u)\n}\n\n// Dependencies returns the dependencies of the Unit.\nfunc (u *Unit) Dependencies() Components {\n\tu.rLock()\n\tdefer u.rUnlock()\n\n\treturn u.dependencies\n}\n\n// Dependents returns the dependents of the Unit.\nfunc (u *Unit) Dependents() Components {\n\tu.rLock()\n\tdefer u.rUnlock()\n\n\treturn u.dependents\n}\n\n// String renders this unit as a human-readable string for debugging.\n//\n// Example output:\n//\n//\tUnit /path/to/unit (excluded: false, assume applied: false, dependencies: [/dep1, /dep2])\nfunc (u *Unit) String() string {\n\t// Snapshot values under read lock to avoid data races\n\tu.rLock()\n\tdefer u.rUnlock()\n\n\tpath := u.DisplayPath()\n\tdeps := make([]string, 0, len(u.dependencies))\n\n\tfor _, dep := range u.dependencies {\n\t\tdeps = append(deps, dep.DisplayPath())\n\t}\n\n\treturn fmt.Sprintf(\n\t\t\"Unit %s (excluded: %v, dependencies: [%s])\",\n\t\tpath, u.excluded, strings.Join(deps, \", \"),\n\t)\n}\n\n// DisplayPath returns the path relative to DiscoveryContext.WorkingDir for display purposes.\n// Falls back to the original path if relative path calculation fails or WorkingDir is empty.\nfunc (u *Unit) DisplayPath() string {\n\tif u.discoveryContext == nil || u.discoveryContext.WorkingDir == \"\" {\n\t\treturn u.path\n\t}\n\n\tif rel, err := filepath.Rel(u.discoveryContext.WorkingDir, u.path); err == nil {\n\t\treturn rel\n\t}\n\n\treturn u.path\n}\n\n// FindInPaths returns true if the unit is located in one of the target directories.\n// Paths are normalized before comparison to handle absolute/relative path mismatches.\nfunc (u *Unit) FindInPaths(targetDirs []string) bool {\n\tcleanUnitPath := filepath.Clean(u.path)\n\n\tfor _, dir := range targetDirs {\n\t\tcleanDir := filepath.Clean(dir)\n\t\tif util.HasPathPrefix(cleanUnitPath, cleanDir) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// PlanFile returns plan file location if output folder is set.\nfunc (u *Unit) PlanFile(rootWorkingDir, outputFolder, jsonOutputFolder, tofuCommand string) string {\n\tplanFile := u.OutputFile(rootWorkingDir, outputFolder)\n\n\tplanCommand := tofuCommand == tf.CommandNamePlan ||\n\t\ttofuCommand == tf.CommandNameShow\n\n\t// if JSON output enabled and no PlanFile specified, save plan in working dir\n\tif planCommand && planFile == \"\" && jsonOutputFolder != \"\" {\n\t\tplanFile = tf.TerraformPlanFile\n\t}\n\n\treturn planFile\n}\n\n// OutputFile returns plan file location if output folder is set.\nfunc (u *Unit) OutputFile(rootWorkingDir, outputFolder string) string {\n\treturn u.planFilePath(rootWorkingDir, outputFolder, tf.TerraformPlanFile)\n}\n\n// OutputJSONFile returns plan JSON file location if JSON output folder is set.\nfunc (u *Unit) OutputJSONFile(rootWorkingDir, jsonOutputFolder string) string {\n\treturn u.planFilePath(rootWorkingDir, jsonOutputFolder, tf.TerraformPlanJSONFile)\n}\n\n// planFilePath computes the path for plan output files.\nfunc (u *Unit) planFilePath(rootWorkingDir, outputFolder, fileName string) string {\n\tif outputFolder == \"\" {\n\t\treturn \"\"\n\t}\n\n\t// Use discoveryContext.WorkingDir as base (always populated).\n\t// This is critical for git-based filters where units are discovered in temporary worktrees.\n\t// Using rootWorkingDir would cause relative paths to escape the outputFolder.\n\trelPath, err := filepath.Rel(u.discoveryContext.WorkingDir, u.path)\n\tif err != nil {\n\t\trelPath = u.path\n\t}\n\n\tdir := filepath.Join(outputFolder, relPath)\n\n\tif !filepath.IsAbs(dir) {\n\t\tdir = filepath.Join(rootWorkingDir, dir)\n\t}\n\n\tdir = filepath.Clean(dir)\n\n\treturn filepath.Join(dir, fileName)\n}\n"
  },
  {
    "path": "internal/component/unit_output.go",
    "content": "package component\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"sync\"\n)\n\n// flusher is any writer that supports Flush() error.\ntype flusher interface {\n\tFlush() error\n}\n\n// writerUnwrapper is any writer that can provide its underlying parent writer.\n// This is used to create writer-based locks that serialize flushes to the same parent.\ntype writerUnwrapper interface {\n\tUnwrap() io.Writer\n}\n\n// unitOutputLocks provides locks for serializing flushes to the same parent writer.\n// The key is the parent writer's address (via fmt.Sprintf(\"%p\", writer)).\nvar unitOutputLocks sync.Map // map[string]*sync.Mutex\n\nfunc unitOutputLock(key string) *sync.Mutex {\n\tif mu, ok := unitOutputLocks.Load(key); ok {\n\t\treturn mu.(*sync.Mutex)\n\t}\n\n\tnewMu := &sync.Mutex{}\n\n\tactual, loaded := unitOutputLocks.LoadOrStore(key, newMu)\n\tif loaded {\n\t\treturn actual.(*sync.Mutex)\n\t}\n\n\treturn newMu\n}\n\n// FlushOutput flushes buffer data to the given writer for this unit, if the writer supports it.\n// This is safe to call even if u or w is nil.\nfunc FlushOutput(u *Unit, w io.Writer) error {\n\tif u == nil || w == nil {\n\t\treturn nil\n\t}\n\n\twriter, ok := w.(flusher)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\t// Use parent writer's address as lock key to serialize flushes to same parent.\n\t// Falls back to unit path for writers without writerUnwrapper.\n\tkey := u.Path()\n\tif u, ok := w.(writerUnwrapper); ok {\n\t\tkey = fmt.Sprintf(\"%p\", u.Unwrap())\n\t}\n\n\tmu := unitOutputLock(key)\n\n\tmu.Lock()\n\tdefer mu.Unlock()\n\n\treturn writer.Flush()\n}\n"
  },
  {
    "path": "internal/configbridge/bridge.go",
    "content": "// Package configbridge provides an adapter between *options.TerragruntOptions\n// and *config.ParsingContext, allowing callers that have TerragruntOptions to\n// invoke pkg/config functions without config needing to import pkg/options.\npackage configbridge\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// NewParsingContext creates a config.ParsingContext populated from TerragruntOptions.\nfunc NewParsingContext(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) (context.Context, *config.ParsingContext) {\n\tctx, pctx := config.NewParsingContext(ctx, l, config.WithStrictControls(opts.StrictControls))\n\tpopulateFromOpts(pctx, opts)\n\n\treturn ctx, pctx\n}\n\n// populateFromOpts copies fields from TerragruntOptions into ParsingContext flat fields.\nfunc populateFromOpts(pctx *config.ParsingContext, opts *options.TerragruntOptions) {\n\tpctx.TerragruntConfigPath = opts.TerragruntConfigPath\n\tpctx.OriginalTerragruntConfigPath = opts.OriginalTerragruntConfigPath\n\tpctx.WorkingDir = opts.WorkingDir\n\tpctx.RootWorkingDir = opts.RootWorkingDir\n\tpctx.DownloadDir = opts.DownloadDir\n\tpctx.TerraformCommand = opts.TerraformCommand\n\tpctx.OriginalTerraformCommand = opts.OriginalTerraformCommand\n\tpctx.TerraformCliArgs = opts.TerraformCliArgs\n\tpctx.Source = opts.Source\n\tpctx.SourceMap = opts.SourceMap\n\tpctx.Experiments = opts.Experiments\n\tpctx.StrictControls = opts.StrictControls\n\tpctx.FeatureFlags = opts.FeatureFlags\n\tpctx.Writers = opts.Writers\n\tpctx.Env = opts.Env\n\tpctx.IAMRoleOptions = opts.IAMRoleOptions\n\tpctx.OriginalIAMRoleOptions = opts.OriginalIAMRoleOptions\n\tpctx.UsePartialParseConfigCache = opts.UsePartialParseConfigCache\n\tpctx.MaxFoldersToCheck = opts.MaxFoldersToCheck\n\tpctx.NoDependencyFetchOutputFromState = opts.NoDependencyFetchOutputFromState\n\tpctx.SkipOutput = opts.SkipOutput\n\tpctx.TFPathExplicitlySet = opts.TFPathExplicitlySet\n\tpctx.AuthProviderCmd = opts.AuthProviderCmd\n\tpctx.EngineConfig = opts.EngineConfig\n\tpctx.EngineOptions = opts.EngineOptions\n\tpctx.TFPath = opts.TFPath\n\tpctx.TofuImplementation = opts.TofuImplementation\n\tpctx.ForwardTFStdout = opts.ForwardTFStdout\n\tpctx.JSONLogFormat = opts.JSONLogFormat\n\tpctx.Debug = opts.Debug\n\tpctx.AutoInit = opts.AutoInit\n\tpctx.Headless = opts.Headless\n\tpctx.BackendBootstrap = opts.BackendBootstrap\n\tpctx.CheckDependentUnits = opts.CheckDependentUnits\n\tpctx.Telemetry = opts.Telemetry\n\tpctx.NoStackValidate = opts.NoStackValidate\n\tpctx.ScaffoldRootFileName = opts.ScaffoldRootFileName\n\tpctx.TerragruntStackConfigPath = opts.TerragruntStackConfigPath\n\tpctx.ProviderCacheOptions = opts.ProviderCacheOptions\n}\n\n// ShellRunOptsFromOpts constructs shell.ShellOptions from TerragruntOptions.\nfunc ShellRunOptsFromOpts(opts *options.TerragruntOptions) *shell.ShellOptions {\n\treturn &shell.ShellOptions{\n\t\tWriters:         opts.Writers,\n\t\tEngineOptions:   opts.EngineOptions,\n\t\tWorkingDir:      opts.WorkingDir,\n\t\tEnv:             opts.Env,\n\t\tTFPath:          opts.TFPath,\n\t\tEngineConfig:    opts.EngineConfig,\n\t\tExperiments:     opts.Experiments,\n\t\tTelemetry:       opts.Telemetry,\n\t\tRootWorkingDir:  opts.RootWorkingDir,\n\t\tHeadless:        opts.Headless,\n\t\tForwardTFStdout: opts.ForwardTFStdout,\n\t}\n}\n\n// BackendOptsFromOpts constructs backend.Options from TerragruntOptions.\nfunc BackendOptsFromOpts(opts *options.TerragruntOptions) *backend.Options {\n\treturn &backend.Options{\n\t\tWriters:                      opts.Writers,\n\t\tEnv:                          opts.Env,\n\t\tIAMRoleOptions:               opts.IAMRoleOptions,\n\t\tNonInteractive:               opts.NonInteractive,\n\t\tFailIfBucketCreationRequired: opts.FailIfBucketCreationRequired,\n\t}\n}\n\n// RemoteStateOptsFromOpts constructs remotestate.Options from TerragruntOptions.\nfunc RemoteStateOptsFromOpts(opts *options.TerragruntOptions) *remotestate.Options {\n\treturn &remotestate.Options{\n\t\tOptions:             *BackendOptsFromOpts(opts),\n\t\tDisableBucketUpdate: opts.DisableBucketUpdate,\n\t\tTFRunOpts:           TFRunOptsFromOpts(opts),\n\t}\n}\n\n// TFRunOptsFromOpts constructs tf.TFOptions from TerragruntOptions.\nfunc TFRunOptsFromOpts(opts *options.TerragruntOptions) *tf.TFOptions {\n\treturn &tf.TFOptions{\n\t\tJSONLogFormat:                opts.JSONLogFormat,\n\t\tOriginalTerragruntConfigPath: opts.OriginalTerragruntConfigPath,\n\t\tTerragruntConfigPath:         opts.TerragruntConfigPath,\n\t\tTofuImplementation:           opts.TofuImplementation,\n\t\tTerraformCliArgs:             opts.TerraformCliArgs,\n\t\tShellOptions:                 ShellRunOptsFromOpts(opts),\n\t}\n}\n\n// NewRunOptions creates a run.Options from TerragruntOptions.\n// This replaces the former run.NewOptions(opts) function.\nfunc NewRunOptions(opts *options.TerragruntOptions) *run.Options {\n\treturn &run.Options{\n\t\tWriters:                      opts.Writers,\n\t\tTerragruntConfigPath:         opts.TerragruntConfigPath,\n\t\tOriginalTerragruntConfigPath: opts.OriginalTerragruntConfigPath,\n\t\tWorkingDir:                   opts.WorkingDir,\n\t\tRootWorkingDir:               opts.RootWorkingDir,\n\t\tDownloadDir:                  opts.DownloadDir,\n\t\tTerraformCommand:             opts.TerraformCommand,\n\t\tOriginalTerraformCommand:     opts.OriginalTerraformCommand,\n\t\tTerraformCliArgs:             opts.TerraformCliArgs,\n\t\tSource:                       opts.Source,\n\t\tSourceMap:                    opts.SourceMap,\n\t\tEnv:                          opts.Env,\n\t\tIAMRoleOptions:               opts.IAMRoleOptions,\n\t\tOriginalIAMRoleOptions:       opts.OriginalIAMRoleOptions,\n\t\tEngineConfig:                 opts.EngineConfig,\n\t\tEngineOptions:                opts.EngineOptions,\n\t\tErrors:                       opts.Errors,\n\t\tExperiments:                  opts.Experiments,\n\t\tStrictControls:               opts.StrictControls,\n\t\tFeatureFlags:                 opts.FeatureFlags,\n\t\tTFPath:                       opts.TFPath,\n\t\tTofuImplementation:           opts.TofuImplementation,\n\t\tForwardTFStdout:              opts.ForwardTFStdout,\n\t\tJSONLogFormat:                opts.JSONLogFormat,\n\t\tHeadless:                     opts.Headless,\n\t\tNonInteractive:               opts.NonInteractive,\n\t\tDebug:                        opts.Debug,\n\t\tAutoInit:                     opts.AutoInit,\n\t\tAutoRetry:                    opts.AutoRetry,\n\t\tBackendBootstrap:             opts.BackendBootstrap,\n\t\tTelemetry:                    opts.Telemetry,\n\t\tAuthProviderCmd:              opts.AuthProviderCmd,\n\t\tMaxFoldersToCheck:            opts.MaxFoldersToCheck,\n\t\tFailIfBucketCreationRequired: opts.FailIfBucketCreationRequired,\n\t\tDisableBucketUpdate:          opts.DisableBucketUpdate,\n\t\tSourceUpdate:                 opts.SourceUpdate,\n\t}\n}\n"
  },
  {
    "path": "internal/ctyhelper/helper.go",
    "content": "// Package ctyhelper providers helpful tools for working with cty values.\n//\n//nolint:dupl\npackage ctyhelper\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/zclconf/go-cty/cty\"\n\t\"github.com/zclconf/go-cty/cty/gocty\"\n\tctyjson \"github.com/zclconf/go-cty/cty/json\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\n// ParseCtyValueToMap converts a cty.Value to a map[string]any.\n//\n// This is a hacky workaround to convert a cty Value to a Go map[string]any. cty does not support this directly\n// (https://github.com/hashicorp/hcl2/issues/108) and doing it with gocty.FromCtyValue is nearly impossible, as cty\n// requires you to specify all the output types and will error out when it hits interface{}. So, as an ugly workaround,\n// we convert the given value to JSON using cty's JSON library and then convert the JSON back to a\n// map[string]any using the Go json library.\n//\n// Note: This function will strip any marks (such as sensitive marks) from the values because JSON serialization does\n// not support cty marks. If you need to preserve marks, consider working with cty.Value directly instead of converting\n// to map[string]any.\nfunc ParseCtyValueToMap(value cty.Value) (map[string]any, error) {\n\tif value.IsNull() {\n\t\treturn map[string]any{}, nil\n\t}\n\n\tupdatedValue, err := UpdateUnknownCtyValValues(value)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvalue = updatedValue\n\n\t// Unmark the value (including nested values) before JSON serialization as JSON doesn't support marks.\n\tunmarkedValue, _ := value.UnmarkDeep()\n\n\tjsonBytes, err := ctyjson.Marshal(unmarkedValue, cty.DynamicPseudoType)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tvar ctyJSONOutput CtyJSONOutput\n\tif err := json.Unmarshal(jsonBytes, &ctyJSONOutput); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\treturn ctyJSONOutput.Value, nil\n}\n\n// CtyJSONOutput is a struct that captures the output of cty's JSON marshalling.\n//\n// When you convert a cty value to JSON, if any of that types are not yet known (i.e., are labeled as\n// DynamicPseudoType), cty's Marshall method will write the type information to a type field and the actual value to\n// a value field. This struct is used to capture that information so when we parse the JSON back into a Go struct, we\n// can pull out just the Value field we need.\ntype CtyJSONOutput struct {\n\tValue map[string]any `json:\"Value\"`\n\tType  any            `json:\"Type\"`\n}\n\n// UpdateUnknownCtyValValues deeply updates unknown values with default value\nfunc UpdateUnknownCtyValValues(value cty.Value) (cty.Value, error) {\n\tvar updatedValue any\n\n\tswitch {\n\tcase !value.IsKnown():\n\t\treturn cty.StringVal(\"\"), nil\n\tcase value.IsNull():\n\t\treturn value, nil\n\tcase value.Type().IsMapType(), value.Type().IsObjectType():\n\t\tmapVals := value.AsValueMap()\n\t\tfor key, val := range mapVals {\n\t\t\tval, err := UpdateUnknownCtyValValues(val)\n\t\t\tif err != nil {\n\t\t\t\treturn cty.NilVal, err\n\t\t\t}\n\n\t\t\tmapVals[key] = val\n\t\t}\n\n\t\tif len(mapVals) > 0 {\n\t\t\tupdatedValue = mapVals\n\t\t}\n\n\tcase value.Type().IsTupleType(), value.Type().IsListType():\n\t\tsliceVals := value.AsValueSlice()\n\t\tfor key, val := range sliceVals {\n\t\t\tval, err := UpdateUnknownCtyValValues(val)\n\t\t\tif err != nil {\n\t\t\t\treturn cty.NilVal, err\n\t\t\t}\n\n\t\t\tsliceVals[key] = val\n\t\t}\n\n\t\tif len(sliceVals) > 0 {\n\t\t\tupdatedValue = sliceVals\n\t\t}\n\t}\n\n\tif updatedValue == nil {\n\t\treturn value, nil\n\t}\n\n\tvalue, err := gocty.ToCtyValue(updatedValue, value.Type())\n\tif err != nil {\n\t\treturn cty.NilVal, errors.New(err)\n\t}\n\n\treturn value, nil\n}\n"
  },
  {
    "path": "internal/ctyhelper/helper_test.go",
    "content": "package ctyhelper_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/ctyhelper\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/zclconf/go-cty/cty\"\n)\n\nfunc TestUpdateUnknownCtyValValues(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tvalue         cty.Value\n\t\texpectedValue cty.Value\n\t}{\n\t\t{\n\t\t\tcty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\"items\": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\t\"firstname\": cty.StringVal(\"foo\"),\n\t\t\t\t\t\"lastname\":  cty.UnknownVal(cty.String),\n\t\t\t\t})}),\n\t\t\t})}),\n\t\t\tcty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\"items\": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{\n\t\t\t\t\t\"firstname\": cty.StringVal(\"foo\"),\n\t\t\t\t\t\"lastname\":  cty.StringVal(\"\"),\n\t\t\t\t})}),\n\t\t\t})}),\n\t\t},\n\t\t{\n\t\t\tcty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{})}),\n\t\t\tcty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{})}),\n\t\t},\n\t\t{\n\t\t\tcty.ObjectVal(map[string]cty.Value{}),\n\t\t\tcty.ObjectVal(map[string]cty.Value{}),\n\t\t},\n\t\t{\n\t\t\tcty.ObjectVal(map[string]cty.Value{\"key\": cty.UnknownVal(cty.String)}),\n\t\t\tcty.ObjectVal(map[string]cty.Value{\"key\": cty.StringVal(\"\")}),\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactualValue, err := ctyhelper.UpdateUnknownCtyValValues(tc.value)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.expectedValue, actualValue)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/discovery/benchmark_test.go",
    "content": "package discovery_test\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// unitCounts defines geometric scaling for benchmark fixture sizes.\nvar unitCounts = []int{64, 128, 256, 512, 1024}\n\nfunc BenchmarkDiscovery(b *testing.B) {\n\tb.Run(\"path_expression\", func(b *testing.B) {\n\t\tfor _, n := range unitCounts {\n\t\t\tb.Run(fmt.Sprintf(\"units_%d\", n), func(b *testing.B) {\n\t\t\t\tbenchmarkPathExpression(b, n)\n\t\t\t})\n\t\t}\n\t})\n\n\tb.Run(\"graph_expression\", func(b *testing.B) {\n\t\tfor _, n := range unitCounts {\n\t\t\tb.Run(fmt.Sprintf(\"units_%d\", n), func(b *testing.B) {\n\t\t\t\tbenchmarkGraphExpression(b, n)\n\t\t\t})\n\t\t}\n\t})\n\n\tb.Run(\"path_and_graph_expression\", func(b *testing.B) {\n\t\tfor _, n := range unitCounts {\n\t\t\tb.Run(fmt.Sprintf(\"units_%d\", n), func(b *testing.B) {\n\t\t\t\tbenchmarkPathAndGraphExpression(b, n)\n\t\t\t})\n\t\t}\n\t})\n}\n\n// benchmarkPathExpression benchmarks discovery with a path-only filter.\n// Targets 2 app units; only filesystem classification runs, no parsing occurs.\nfunc benchmarkPathExpression(b *testing.B, n int) {\n\tb.Helper()\n\n\ttmpDir := b.TempDir()\n\tcreateFixtures(b, tmpDir, n)\n\n\tl := newDiscardLogger()\n\topts := &options.TerragruntOptions{WorkingDir: tmpDir, RootWorkingDir: tmpDir}\n\n\tfilterQueries, err := filter.ParseFilterQueries(l, []string{\"./apps/app-0000\", \"./apps/app-0001\"})\n\trequire.NoError(b, err)\n\n\tb.ResetTimer()\n\n\tfor b.Loop() {\n\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\tWithFilters(filterQueries).\n\t\t\tWithSuppressParseErrors()\n\n\t\tcomponents, err := d.Discover(b.Context(), l, opts)\n\t\trequire.NoError(b, err)\n\t\trequire.Len(b, components, 2)\n\t}\n}\n\n// benchmarkGraphExpression benchmarks discovery with a graph-only filter.\n// Targets a shallow 2-unit dependency pair (infra-0001 → infra-0000).\nfunc benchmarkGraphExpression(b *testing.B, n int) {\n\tb.Helper()\n\n\ttmpDir := b.TempDir()\n\tcreateFixtures(b, tmpDir, n)\n\n\tl := newDiscardLogger()\n\topts := &options.TerragruntOptions{WorkingDir: tmpDir, RootWorkingDir: tmpDir}\n\n\tfilterQueries, err := filter.ParseFilterQueries(l, []string{\"infra-0001...\"})\n\trequire.NoError(b, err)\n\n\tb.ResetTimer()\n\n\tfor b.Loop() {\n\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\tWithFilters(filterQueries).\n\t\t\tWithSuppressParseErrors()\n\n\t\tcomponents, err := d.Discover(b.Context(), l, opts)\n\t\trequire.NoError(b, err)\n\t\trequire.Len(b, components, 2)\n\t}\n}\n\n// benchmarkPathAndGraphExpression benchmarks discovery with combined path + graph filters.\n// Targets 2 path-matched apps + 2 graph-traversed infra units (infra-0001 → infra-0000).\nfunc benchmarkPathAndGraphExpression(b *testing.B, n int) {\n\tb.Helper()\n\n\ttmpDir := b.TempDir()\n\tcreateFixtures(b, tmpDir, n)\n\n\tl := newDiscardLogger()\n\topts := &options.TerragruntOptions{WorkingDir: tmpDir, RootWorkingDir: tmpDir}\n\n\tfilterQueries, err := filter.ParseFilterQueries(l, []string{\"./apps/app-0000\", \"./apps/app-0001\", \"infra-0001...\"})\n\trequire.NoError(b, err)\n\n\tb.ResetTimer()\n\n\tfor b.Loop() {\n\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\tWithFilters(filterQueries).\n\t\t\tWithSuppressParseErrors()\n\n\t\tcomponents, err := d.Discover(b.Context(), l, opts)\n\t\trequire.NoError(b, err)\n\t\trequire.Len(b, components, 4)\n\t}\n}\n\nfunc newDiscardLogger() log.Logger {\n\tformatter := format.NewFormatter(format.NewKeyValueFormatPlaceholders())\n\tformatter.SetDisabledColors(true)\n\n\treturn log.New(log.WithOutput(io.Discard), log.WithFormatter(formatter))\n}\n\n// createFixtures creates a fixture layout with n total units:\n//   - n/2 \"app\" units in apps/app-NNNN/terragrunt.hcl (minimal, no dependencies)\n//   - n/2 \"infra\" units in infra/infra-NNNN/terragrunt.hcl (paired dependency chains:\n//     odd-numbered units depend on the preceding even unit, e.g. infra-0001 → infra-0000)\nfunc createFixtures(b *testing.B, tmpDir string, n int) {\n\tb.Helper()\n\n\thalf := n / 2\n\n\tappsDir := filepath.Join(tmpDir, \"apps\")\n\n\tfor i := range half {\n\t\tdir := filepath.Join(appsDir, fmt.Sprintf(\"app-%04d\", i))\n\t\trequire.NoError(b, os.MkdirAll(dir, 0755))\n\t\trequire.NoError(b, os.WriteFile(\n\t\t\tfilepath.Join(dir, \"terragrunt.hcl\"),\n\t\t\t[]byte(\"# Minimal config\\n\"),\n\t\t\t0644,\n\t\t))\n\t}\n\n\tinfraDir := filepath.Join(tmpDir, \"infra\")\n\n\tfor i := range half {\n\t\tdir := filepath.Join(infraDir, fmt.Sprintf(\"infra-%04d\", i))\n\t\trequire.NoError(b, os.MkdirAll(dir, 0755))\n\n\t\tvar content string\n\n\t\tif i%2 == 1 {\n\t\t\tprev := fmt.Sprintf(\"infra-%04d\", i-1)\n\t\t\tcontent = fmt.Sprintf(\"dependency \\\"prev\\\" {\\n  config_path = \\\"../%s\\\"\\n}\\n\", prev)\n\t\t} else {\n\t\t\tcontent = \"# Leaf unit\\n\"\n\t\t}\n\n\t\trequire.NoError(b, os.WriteFile(\n\t\t\tfilepath.Join(dir, \"terragrunt.hcl\"),\n\t\t\t[]byte(content),\n\t\t\t0644,\n\t\t))\n\t}\n}\n"
  },
  {
    "path": "internal/discovery/constructor.go",
    "content": "package discovery\n\nimport (\n\t\"path/filepath\"\n\t\"runtime\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/mattn/go-shellwords\"\n)\n\n// DiscoveryCommandOptions contains options for discovery commands like find and list.\ntype DiscoveryCommandOptions struct {\n\tWorkingDir        string\n\tQueueConstructAs  string\n\tFilters           filter.Filters\n\tExperiments       experiment.Experiments\n\tNoHidden          bool\n\tExclude           bool\n\tInclude           bool\n\tReading           bool\n\tWithRequiresParse bool\n\tWithRelationships bool\n}\n\n// HCLCommandOptions contains options for HCL commands like hcl validate & format.\ntype HCLCommandOptions struct {\n\tWorkingDir  string\n\tFilters     filter.Filters\n\tExperiments experiment.Experiments\n}\n\n// StackGenerateOptions contains options for stack generate commands.\ntype StackGenerateOptions struct {\n\tWorkingDir  string\n\tFilters     filter.Filters\n\tExperiments experiment.Experiments\n}\n\n// NewForDiscoveryCommand creates a Discovery configured for discovery commands (find/list).\nfunc NewForDiscoveryCommand(l log.Logger, opts *DiscoveryCommandOptions) (*Discovery, error) {\n\td := NewDiscovery(opts.WorkingDir).\n\t\tWithSuppressParseErrors().\n\t\tWithBreakCycles()\n\n\tif opts.NoHidden {\n\t\td = d.WithNoHidden()\n\t}\n\n\tif opts.WithRequiresParse {\n\t\td = d.WithRequiresParse()\n\t}\n\n\tif opts.WithRelationships {\n\t\td = d.WithRelationships()\n\t}\n\n\tif opts.Exclude {\n\t\td = d.WithParseExclude()\n\t}\n\n\tif opts.Include {\n\t\td = d.WithParseIncludes()\n\t}\n\n\tif opts.Reading {\n\t\td = d.WithReadFiles()\n\t}\n\n\tif opts.QueueConstructAs != \"\" {\n\t\td = d.WithParseExclude()\n\n\t\tparser := shellwords.NewParser()\n\n\t\t// Normalize Windows paths before parsing - shellwords treats backslashes as escape characters\n\t\targs, err := parser.Parse(filepath.ToSlash(opts.QueueConstructAs))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tcmd := args[0]\n\t\tif len(args) > 1 {\n\t\t\targs = args[1:]\n\t\t} else {\n\t\t\targs = nil\n\t\t}\n\n\t\td = d.WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: opts.WorkingDir,\n\t\t\tCmd:        cmd,\n\t\t\tArgs:       args,\n\t\t})\n\t}\n\n\tif len(opts.Filters) > 0 {\n\t\td = d.WithFilters(opts.Filters)\n\t}\n\n\treturn d, nil\n}\n\n// NewForHCLCommand creates a Discovery configured for HCL commands (hcl validate/format).\nfunc NewForHCLCommand(l log.Logger, opts HCLCommandOptions) (*Discovery, error) {\n\td := NewDiscovery(opts.WorkingDir)\n\n\tif len(opts.Filters) > 0 {\n\t\td = d.WithFilters(opts.Filters)\n\t}\n\n\treturn d, nil\n}\n\n// NewForStackGenerate creates a Discovery configured for `stack generate`.\nfunc NewForStackGenerate(l log.Logger, opts StackGenerateOptions) (*Discovery, error) {\n\td := NewDiscovery(opts.WorkingDir)\n\n\tif len(opts.Filters) > 0 {\n\t\td = d.WithFilters(opts.Filters.RestrictToStacks())\n\t}\n\n\treturn d, nil\n}\n\n// NewDiscovery creates a new Discovery with sensible defaults.\nfunc NewDiscovery(dir string) *Discovery {\n\tnumWorkers := max(min(runtime.NumCPU(), maxDiscoveryWorkers), defaultDiscoveryWorkers)\n\n\treturn &Discovery{\n\t\tnumWorkers:         numWorkers,\n\t\tmaxDependencyDepth: defaultMaxDependencyDepth,\n\t\tworkingDir:         dir,\n\t\tconfigFilenames:    DefaultConfigFilenames,\n\t\tdiscoveryContext: &component.DiscoveryContext{\n\t\t\tWorkingDir: dir,\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/discovery/discovery.go",
    "content": "package discovery\n\nimport (\n\t\"context\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// Discover performs the full discovery process.\nfunc (d *Discovery) Discover(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n) (component.Components, error) {\n\td.classifier = filter.NewClassifier(d.filters)\n\n\tresults, err := d.runFilesystemPhase(ctx, l, opts)\n\tif err != nil && (!d.suppressParseErrors || errors.As(err, new(CoexistenceError))) {\n\t\treturn nil, err\n\t}\n\n\tdiscovered, candidates := results.Discovered, results.Candidates\n\n\tif d.requiresParse || d.classifier.HasParseRequiredFilters() {\n\t\tresults, err = d.runParsePhase(ctx, l, opts, discovered, candidates)\n\t\tif err != nil && !d.suppressParseErrors {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tdiscovered, candidates = results.Discovered, results.Candidates\n\t}\n\n\tif d.classifier.HasGraphFilters() {\n\t\tif d.classifier.HasDependentFilters() && d.gitRoot == \"\" {\n\t\t\tif gitRootPath, gitErr := shell.GitTopLevelDir(ctx, l, opts.Env, d.workingDir); gitErr == nil {\n\t\t\t\td.gitRoot = gitRootPath\n\t\t\t\tl.Debugf(\"Set gitRoot for dependent discovery: %s\", d.gitRoot)\n\t\t\t}\n\t\t}\n\n\t\tresults, err = d.runGraphPhase(ctx, l, opts, discovered, candidates)\n\t\tif err != nil && !d.suppressParseErrors {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tdiscovered = results.Discovered\n\t}\n\n\tcomponents := resultsToComponents(discovered)\n\n\tif d.discoverRelationships {\n\t\tcomponents, err = d.runRelationshipPhase(ctx, l, opts, components)\n\t\tif err != nil && !d.suppressParseErrors {\n\t\t\treturn components, err\n\t\t}\n\t}\n\n\tif len(d.filters) > 0 {\n\t\tfiltered, err := d.filters.Evaluate(l, components)\n\t\tif err != nil {\n\t\t\treturn components, err\n\t\t}\n\n\t\tcomponents = filtered\n\t}\n\n\tcycleCheckErr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"discovery_cycle_check\", map[string]any{}, func(childCtx context.Context) error {\n\t\tif _, cycleErr := components.CycleCheck(); cycleErr != nil {\n\t\t\tl.Debugf(\"Cycle: %v\", cycleErr)\n\n\t\t\tif d.breakCycles {\n\t\t\t\tl.Warnf(\"Cycle detected in dependency graph, attempting removal of cycles.\")\n\n\t\t\t\tvar removeErr error\n\n\t\t\t\tcomponents, removeErr = removeCycles(components)\n\t\t\t\tif removeErr != nil {\n\t\t\t\t\treturn removeErr\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif cycleCheckErr != nil && !d.suppressParseErrors {\n\t\treturn components, cycleCheckErr\n\t}\n\n\tif d.graphTarget != \"\" {\n\t\tcomponents = d.filterGraphTarget(components)\n\t}\n\n\tcomponents = d.applyQueueFilters(opts, components)\n\n\treturn components, nil\n}\n\n// runFilesystemPhase runs the filesystem and worktree phases concurrently.\nfunc (d *Discovery) runFilesystemPhase(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n) (*PhaseResults, error) {\n\tvar (\n\t\tallDiscovered []DiscoveryResult\n\t\tallCandidates []DiscoveryResult\n\t\tallErrors     []error\n\t\tmu            sync.Mutex\n\t)\n\n\t// maxPhases is the maximum number of phases to run concurrently\n\t// for filesystem and worktree phases.\n\tconst maxPhases = 2\n\n\tg, ctx := errgroup.WithContext(ctx)\n\tg.SetLimit(maxPhases)\n\n\tg.Go(func() error {\n\t\tphase := NewFilesystemPhase(d.numWorkers)\n\t\tresult, err := phase.Run(ctx, l, &PhaseInput{\n\t\t\tOpts:       opts,\n\t\t\tClassifier: d.classifier,\n\t\t\tDiscovery:  d,\n\t\t})\n\n\t\tmu.Lock()\n\n\t\tif result != nil {\n\t\t\tallDiscovered = append(allDiscovered, result.Discovered...)\n\t\t\tallCandidates = append(allCandidates, result.Candidates...)\n\t\t}\n\n\t\tif err != nil {\n\t\t\tallErrors = append(allErrors, err)\n\t\t}\n\n\t\tmu.Unlock()\n\n\t\treturn nil\n\t})\n\n\tif len(d.gitExpressions) > 0 && d.worktrees != nil {\n\t\tg.Go(func() error {\n\t\t\tphase := NewWorktreePhase(d.gitExpressions, d.numWorkers)\n\t\t\tresult, err := phase.Run(ctx, l, &PhaseInput{\n\t\t\t\tOpts:       opts,\n\t\t\t\tClassifier: d.classifier,\n\t\t\t\tDiscovery:  d,\n\t\t\t})\n\n\t\t\tmu.Lock()\n\n\t\t\tif result != nil {\n\t\t\t\tallDiscovered = append(allDiscovered, result.Discovered...)\n\t\t\t\tallCandidates = append(allCandidates, result.Candidates...)\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tallErrors = append(allErrors, err)\n\t\t\t}\n\n\t\t\tmu.Unlock()\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tif err := g.Wait(); err != nil {\n\t\tallErrors = append(allErrors, err)\n\t}\n\n\tif err := validateNoCoexistence(allDiscovered); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := validateNoCoexistence(allCandidates); err != nil {\n\t\treturn nil, err\n\t}\n\n\tallDiscovered = deduplicateResults(allDiscovered)\n\tallCandidates = deduplicateResults(allCandidates)\n\n\treturn &PhaseResults{\n\t\tDiscovered: allDiscovered,\n\t\tCandidates: allCandidates,\n\t}, errors.Join(allErrors...)\n}\n\n// runParsePhase runs the parse phase for candidates that require parsing.\nfunc (d *Discovery) runParsePhase(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tdiscovered []DiscoveryResult,\n\tcandidates []DiscoveryResult,\n) (*PhaseResults, error) {\n\tphase := NewParsePhase(d.numWorkers)\n\tresult, err := phase.Run(ctx, l, &PhaseInput{\n\t\tOpts:       opts,\n\t\tComponents: resultsToComponents(discovered),\n\t\tCandidates: candidates,\n\t\tClassifier: d.classifier,\n\t\tDiscovery:  d,\n\t})\n\n\tallDiscovered := discovered\n\tif result != nil {\n\t\tallDiscovered = append(allDiscovered, result.Discovered...)\n\t}\n\n\tallDiscovered = deduplicateResults(allDiscovered)\n\n\tvar resultCandidates []DiscoveryResult\n\tif result != nil {\n\t\tresultCandidates = result.Candidates\n\t}\n\n\treturn &PhaseResults{\n\t\tDiscovered: allDiscovered,\n\t\tCandidates: resultCandidates,\n\t}, err\n}\n\n// runGraphPhase runs the graph traversal phase.\nfunc (d *Discovery) runGraphPhase(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tdiscovered []DiscoveryResult,\n\tcandidates []DiscoveryResult,\n) (*PhaseResults, error) {\n\tif d.classifier.HasDependentFilters() {\n\t\tallComponents := resultsToComponents(discovered)\n\t\tallComponents = append(allComponents, resultsToComponents(candidates)...)\n\n\t\tvar buildErrs []error\n\n\t\ttelemetry.TelemeterFromContext(ctx).Collect(ctx, \"discover_dependents\", map[string]any{}, func(childCtx context.Context) error { //nolint:errcheck\n\t\t\tbuildErrs = d.buildDependencyGraph(childCtx, l, opts, allComponents)\n\t\t\treturn errors.Join(buildErrs...)\n\t\t})\n\n\t\tif len(buildErrs) > 0 && !d.suppressParseErrors {\n\t\t\treturn &PhaseResults{\n\t\t\t\tDiscovered: discovered,\n\t\t\t\tCandidates: candidates,\n\t\t\t}, errors.Join(buildErrs...)\n\t\t}\n\t}\n\n\tphase := NewGraphPhase(d.numWorkers, d.maxDependencyDepth)\n\n\tvar (\n\t\tresult *PhaseResults\n\t\terr    error\n\t)\n\n\ttelemetry.TelemeterFromContext(ctx).Collect(ctx, \"discover_dependencies\", map[string]any{}, func(childCtx context.Context) error { //nolint:errcheck\n\t\tresult, err = phase.Run(childCtx, l, &PhaseInput{\n\t\t\tOpts:       opts,\n\t\t\tComponents: resultsToComponents(discovered),\n\t\t\tCandidates: candidates,\n\t\t\tClassifier: d.classifier,\n\t\t\tDiscovery:  d,\n\t\t})\n\n\t\treturn err\n\t})\n\n\tallDiscovered := discovered\n\tif result != nil {\n\t\tallDiscovered = append(allDiscovered, result.Discovered...)\n\t}\n\n\tallDiscovered = deduplicateResults(allDiscovered)\n\n\tvar resultCandidates []DiscoveryResult\n\tif result != nil {\n\t\tresultCandidates = result.Candidates\n\t}\n\n\treturn &PhaseResults{\n\t\tDiscovered: allDiscovered,\n\t\tCandidates: resultCandidates,\n\t}, err\n}\n\n// runRelationshipPhase runs the relationship discovery phase.\nfunc (d *Discovery) runRelationshipPhase(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tcomponents component.Components,\n) (component.Components, error) {\n\tphase := NewRelationshipPhase(d.numWorkers, d.maxDependencyDepth)\n\t_, err := phase.Run(ctx, l, &PhaseInput{\n\t\tOpts:       opts,\n\t\tComponents: components,\n\t\tDiscovery:  d,\n\t})\n\n\treturn components, err\n}\n\n// buildDependencyGraph parses all components and builds bidirectional dependency links.\n// This is called before the graph phase when dependent filters exist, to populate\n// the reverse links (dependents) that the graph phase needs for dependent traversal.\nfunc (d *Discovery) buildDependencyGraph(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tallComponents component.Components,\n) []error {\n\tthreadSafeComponents := component.NewThreadSafeComponents(allComponents)\n\n\tvar (\n\t\terrs []error\n\t\tmu   sync.Mutex\n\t)\n\n\tg, ctx := errgroup.WithContext(ctx)\n\tg.SetLimit(d.numWorkers)\n\n\tfor _, c := range allComponents {\n\t\tg.Go(func() error {\n\t\t\terr := d.buildComponentDependencies(ctx, l, opts, c, threadSafeComponents)\n\t\t\tif err != nil {\n\t\t\t\tmu.Lock()\n\n\t\t\t\terrs = append(errs, err)\n\n\t\t\t\tmu.Unlock()\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\terr := g.Wait()\n\tif err != nil {\n\t\tl.Debugf(\"Error building dependency graph: %v\", err)\n\t}\n\n\treturn errs\n}\n\n// buildComponentDependencies parses a single component and builds its dependency links.\nfunc (d *Discovery) buildComponentDependencies(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tc component.Component,\n\tthreadSafeComponents *component.ThreadSafeComponents,\n) error {\n\tunit, ok := c.(*component.Unit)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\tcfg := unit.Config()\n\tif cfg == nil {\n\t\terr := parseComponent(ctx, l, c, opts, d)\n\t\tif err != nil {\n\t\t\tif d.suppressParseErrors {\n\t\t\t\tl.Debugf(\"Suppressed parse error for %s: %v\", c.Path(), err)\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\treturn err\n\t\t}\n\n\t\tcfg = unit.Config()\n\t}\n\n\tdepPaths, err := extractDependencyPaths(cfg, c)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(depPaths) == 0 {\n\t\treturn nil\n\t}\n\n\tparentCtx := c.DiscoveryContext()\n\tif parentCtx == nil {\n\t\treturn nil\n\t}\n\n\tfor _, depPath := range depPaths {\n\t\tdepComponent := componentFromDependencyPath(depPath, threadSafeComponents)\n\n\t\tif isExternal(parentCtx.WorkingDir, depPath) {\n\t\t\tif ext, ok := depComponent.(*component.Unit); ok {\n\t\t\t\text.SetExternal()\n\t\t\t}\n\t\t}\n\n\t\taddedComponent, created := threadSafeComponents.EnsureComponent(depComponent)\n\t\tif created {\n\t\t\tcopiedCtx := parentCtx.CopyWithNewOrigin(component.OriginGraphDiscovery)\n\t\t\tdepComponent.SetDiscoveryContext(copiedCtx)\n\t\t}\n\n\t\tc.AddDependency(addedComponent)\n\t}\n\n\treturn nil\n}\n\n// removeCycles removes cycles from the dependency graph.\nfunc removeCycles(components component.Components) (component.Components, error) {\n\tvar (\n\t\tc   component.Component\n\t\terr error\n\t)\n\n\tfor range maxCycleRemovalAttempts {\n\t\tc, err = components.CycleCheck()\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tif c == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tcomponents = components.RemoveByPath(c.Path())\n\t}\n\n\treturn components, err\n}\n\n// filterGraphTarget prunes components to the target path and its dependents.\nfunc (d *Discovery) filterGraphTarget(components component.Components) component.Components {\n\tif d.graphTarget == \"\" {\n\t\treturn components\n\t}\n\n\ttargetPath := canonicalizeGraphTarget(d.workingDir, d.graphTarget)\n\n\tdependentUnits := buildDependentsIndex(components)\n\tpropagateTransitiveDependents(dependentUnits)\n\n\tallowed := buildAllowSet(targetPath, dependentUnits)\n\n\treturn filterByAllowSet(components, allowed)\n}\n\n// canonicalizeGraphTarget resolves the graph target to an absolute, cleaned path with symlinks resolved.\n// Returns an error if the path cannot be made absolute.\nfunc canonicalizeGraphTarget(baseDir, target string) string {\n\tvar abs string\n\n\t// If already absolute, just clean it\n\tif filepath.IsAbs(target) {\n\t\tabs = filepath.Clean(target)\n\t} else if canonicalAbs, err := util.CanonicalPath(target, baseDir); err == nil {\n\t\t// Try canonical path first\n\t\tabs = canonicalAbs\n\t} else {\n\t\t// Fallback: join with baseDir and clean\n\t\tabs = filepath.Clean(filepath.Join(baseDir, target))\n\t}\n\n\t// Resolve symlinks for consistent path comparison (important on macOS where /var -> /private/var)\n\t// EvalSymlinks can fail for: non-existent paths (expected during discovery),\n\t// broken symlinks, or permission issues. In all cases, falling back to the\n\t// absolute path is acceptable - the path will be validated later when used.\n\tresolved, evalErr := filepath.EvalSymlinks(abs)\n\tif evalErr != nil {\n\t\treturn abs\n\t}\n\n\treturn resolved\n}\n\n// buildDependentsIndex builds an index mapping each unit path to the list of units\n// that directly depend on it. Duplicate entries are removed.\n// Paths are resolved to handle symlinks consistently across platforms.\nfunc buildDependentsIndex(components component.Components) map[string][]string {\n\tdependentUnits := make(map[string][]string)\n\n\tfor _, c := range components {\n\t\tcPath := util.ResolvePath(c.Path())\n\n\t\tfor _, dep := range c.Dependencies() {\n\t\t\tdepPath := util.ResolvePath(dep.Path())\n\t\t\tdependentUnits[depPath] = util.RemoveDuplicates(append(dependentUnits[depPath], cPath))\n\t\t}\n\t}\n\n\treturn dependentUnits\n}\n\n// propagateTransitiveDependents expands the dependents index to include transitive dependents.\n// Iteratively propagates dependents until a fixed point is reached or the iteration cap is met.\nfunc propagateTransitiveDependents(dependentUnits map[string][]string) {\n\t// Determine an upper bound on iterations based on unique nodes in the graph (keys + values).\n\tnodes := make(map[string]struct{})\n\tfor unit, dependents := range dependentUnits {\n\t\tnodes[unit] = struct{}{}\n\t\tfor _, dep := range dependents {\n\t\t\tnodes[dep] = struct{}{}\n\t\t}\n\t}\n\n\tmaxIterations := len(nodes)\n\n\tfor range maxIterations {\n\t\tupdated := false\n\n\t\tfor unit, dependents := range dependentUnits {\n\t\t\tfor _, dep := range dependents {\n\t\t\t\told := dependentUnits[unit]\n\t\t\t\tnewList := util.RemoveDuplicates(append(old, dependentUnits[dep]...))\n\t\t\t\tnewList = slices.DeleteFunc(newList, func(path string) bool { return path == unit })\n\n\t\t\t\tif len(newList) != len(old) {\n\t\t\t\t\tdependentUnits[unit] = newList\n\t\t\t\t\tupdated = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif !updated {\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// buildAllowSet creates the allowlist containing the target and all of its dependents.\nfunc buildAllowSet(targetPath string, dependentUnits map[string][]string) map[string]struct{} {\n\tallowed := make(map[string]struct{})\n\n\tallowed[targetPath] = struct{}{}\n\tfor _, dep := range dependentUnits[targetPath] {\n\t\tallowed[dep] = struct{}{}\n\t}\n\n\treturn allowed\n}\n\n// filterByAllowSet returns only the components whose path exists in the allow set.\n// Paths are resolved to handle symlinks consistently across platforms.\n// The output order matches the input order (no sorting is performed here).\nfunc filterByAllowSet(components component.Components, allowed map[string]struct{}) component.Components {\n\tfiltered := make(component.Components, 0, len(components))\n\n\tfor _, c := range components {\n\t\tresolvedPath := util.ResolvePath(c.Path())\n\t\tif _, ok := allowed[resolvedPath]; ok {\n\t\t\tfiltered = append(filtered, c)\n\t\t}\n\t}\n\n\treturn filtered\n}\n\n// applyQueueFilters marks discovered units as excluded or included based on queue-related CLI flags and config.\n// The runner consumes the exclusion markers instead of re-evaluating the filters.\nfunc (d *Discovery) applyQueueFilters(opts *options.TerragruntOptions, components component.Components) component.Components {\n\tcomponents = d.applyExcludeModules(opts, components)\n\n\treturn components\n}\n\n// applyExcludeModules marks units (and optionally their dependencies) excluded via terragrunt exclude blocks.\nfunc (d *Discovery) applyExcludeModules(opts *options.TerragruntOptions, components component.Components) component.Components {\n\tfor _, c := range components {\n\t\tunit, ok := c.(*component.Unit)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tcfg := unit.Config()\n\t\tif cfg == nil || cfg.Exclude == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !cfg.Exclude.IsActionListed(opts.TerraformCommand) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif cfg.Exclude.If {\n\t\t\tunit.SetExcluded(true)\n\n\t\t\tif cfg.Exclude.ExcludeDependencies != nil && *cfg.Exclude.ExcludeDependencies {\n\t\t\t\tfor _, dep := range unit.Dependencies() {\n\t\t\t\t\tdepUnit, ok := dep.(*component.Unit)\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tdepUnit.SetExcluded(true)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn components\n}\n"
  },
  {
    "path": "internal/discovery/discovery_integration_test.go",
    "content": "package discovery_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestDiscovery_BasicWithHiddenDirectories tests discovery with and without hidden directories.\nfunc TestDiscovery_BasicWithHiddenDirectories(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create test directory structure\n\tunit1Dir := filepath.Join(tmpDir, \"unit1\")\n\tunit2Dir := filepath.Join(tmpDir, \"unit2\")\n\tstack1Dir := filepath.Join(tmpDir, \"stack1\")\n\thiddenUnitDir := filepath.Join(tmpDir, \".hidden\", \"hidden-unit\")\n\tnestedUnit4Dir := filepath.Join(tmpDir, \"nested\", \"unit4\")\n\n\ttestDirs := []string{\n\t\tunit1Dir,\n\t\tunit2Dir,\n\t\tstack1Dir,\n\t\thiddenUnitDir,\n\t\tnestedUnit4Dir,\n\t}\n\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Create test files\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(unit1Dir, \"terragrunt.hcl\"):        \"\",\n\t\tfilepath.Join(unit2Dir, \"terragrunt.hcl\"):        \"\",\n\t\tfilepath.Join(stack1Dir, \"terragrunt.stack.hcl\"): \"\",\n\t\tfilepath.Join(hiddenUnitDir, \"terragrunt.hcl\"):   \"\",\n\t\tfilepath.Join(nestedUnit4Dir, \"terragrunt.hcl\"):  \"\",\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttests := []struct {\n\t\tname       string\n\t\twantUnits  []string\n\t\twantStacks []string\n\t\tnoHidden   bool\n\t}{\n\t\t{\n\t\t\tname:       \"discovery without hidden\",\n\t\t\tnoHidden:   true,\n\t\t\twantUnits:  []string{unit1Dir, unit2Dir, nestedUnit4Dir},\n\t\t\twantStacks: []string{stack1Dir},\n\t\t},\n\t\t{\n\t\t\tname:       \"discovery with hidden\",\n\t\t\tnoHidden:   false,\n\t\t\twantUnits:  []string{unit1Dir, unit2Dir, hiddenUnitDir, nestedUnit4Dir},\n\t\t\twantStacks: []string{stack1Dir},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tl := logger.CreateLogger()\n\t\t\topts := &options.TerragruntOptions{\n\t\t\t\tWorkingDir: tmpDir,\n\t\t\t}\n\n\t\t\tctx := t.Context()\n\n\t\t\td := discovery.NewDiscovery(tmpDir).WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\tWorkingDir: tmpDir,\n\t\t\t})\n\n\t\t\tif tt.noHidden {\n\t\t\t\td = d.WithNoHidden()\n\t\t\t}\n\n\t\t\tcomponents, err := d.Discover(ctx, l, opts)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tunits := components.Filter(component.UnitKind).Paths()\n\t\t\tstacks := components.Filter(component.StackKind).Paths()\n\n\t\t\tassert.ElementsMatch(t, tt.wantUnits, units)\n\t\t\tassert.ElementsMatch(t, tt.wantStacks, stacks)\n\t\t})\n\t}\n}\n\n// TestDiscovery_StackHiddenDiscovered tests that .terragrunt-stack directories are discovered by default.\nfunc TestDiscovery_StackHiddenDiscovered(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\tstackHiddenDir := filepath.Join(tmpDir, \".terragrunt-stack\", \"u\")\n\trequire.NoError(t, os.MkdirAll(stackHiddenDir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(stackHiddenDir, \"terragrunt.hcl\"), []byte(\"\"), 0644))\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// By default, .terragrunt-stack contents should be discovered\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir})\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\tassert.Contains(t, components.Filter(component.UnitKind).Paths(), stackHiddenDir)\n}\n\n// TestDiscovery_WithDependencies tests dependency discovery and relationship building.\nfunc TestDiscovery_WithDependencies(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tinternalDir := filepath.Join(tmpDir, \"internal\")\n\tappDir := filepath.Join(internalDir, \"app\")\n\tdbDir := filepath.Join(internalDir, \"db\")\n\tvpcDir := filepath.Join(internalDir, \"vpc\")\n\n\texternalDir := filepath.Join(tmpDir, \"external\")\n\texternalAppDir := filepath.Join(externalDir, \"app\")\n\n\ttestDirs := []string{\n\t\tappDir,\n\t\tdbDir,\n\t\tvpcDir,\n\t\texternalAppDir,\n\t}\n\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Create test files with dependencies\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(appDir, \"terragrunt.hcl\"): `\n\t\tdependency \"db\" {\n\t\t\tconfig_path = \"../db\"\n\t\t}\n\n\t\tdependency \"external\" {\n\t\t\tconfig_path = \"../../external/app\"\n\t\t}\n\t\t`,\n\t\tfilepath.Join(dbDir, \"terragrunt.hcl\"): `\n\t\tdependency \"vpc\" {\n\t\t\tconfig_path = \"../vpc\"\n\t\t}\n\t\t`,\n\t\tfilepath.Join(vpcDir, \"terragrunt.hcl\"):         ``,\n\t\tfilepath.Join(externalAppDir, \"terragrunt.hcl\"): ``,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     internalDir,\n\t\tRootWorkingDir: internalDir,\n\t}\n\n\tctx := t.Context()\n\n\tt.Run(\"discovery with relationships\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\td := discovery.NewDiscovery(internalDir).\n\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: internalDir}).\n\t\t\tWithRelationships()\n\n\t\tcomponents, err := d.Discover(ctx, l, opts)\n\t\trequire.NoError(t, err)\n\n\t\t// Should discover all internal components\n\t\tpaths := components.Paths()\n\t\tassert.Contains(t, paths, appDir)\n\t\tassert.Contains(t, paths, dbDir)\n\t\tassert.Contains(t, paths, vpcDir)\n\n\t\t// Find app component and verify dependencies\n\t\tvar appComponent component.Component\n\n\t\tfor _, c := range components {\n\t\t\tif c.Path() == appDir {\n\t\t\t\tappComponent = c\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\trequire.NotNil(t, appComponent, \"app component should be discovered\")\n\t\tdepPaths := appComponent.Dependencies().Paths()\n\t\tassert.Contains(t, depPaths, dbDir, \"app should depend on db\")\n\t\tassert.Contains(t, depPaths, externalAppDir, \"app should depend on external app\")\n\n\t\t// Verify db's dependencies\n\t\tvar dbComponent component.Component\n\n\t\tfor _, c := range components {\n\t\t\tif c.Path() == dbDir {\n\t\t\t\tdbComponent = c\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\trequire.NotNil(t, dbComponent)\n\t\tassert.Contains(t, dbComponent.Dependencies().Paths(), vpcDir, \"db should depend on vpc\")\n\t})\n\n\tt.Run(\"discovery with dependency graph filter\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(l, []string{\"{./**}...\"})\n\t\trequire.NoError(t, err)\n\n\t\td := discovery.NewDiscovery(internalDir).\n\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: internalDir}).\n\t\t\tWithFilters(filters)\n\n\t\tcomponents, err := d.Discover(ctx, l, opts)\n\t\trequire.NoError(t, err)\n\n\t\t// Should discover all components including external dependency\n\t\tpaths := components.Paths()\n\t\tassert.Contains(t, paths, appDir)\n\t\tassert.Contains(t, paths, dbDir)\n\t\tassert.Contains(t, paths, vpcDir)\n\t\tassert.Contains(t, paths, externalAppDir)\n\n\t\t// Find external app and verify it's marked as external\n\t\tfor _, c := range components {\n\t\t\tif c.Path() == externalAppDir {\n\t\t\t\tassert.True(t, c.External(), \"external app should be marked as external\")\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t})\n}\n\n// TestDiscovery_CycleDetection tests that cycles in dependency graphs are detected.\nfunc TestDiscovery_CycleDetection(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tfooDir := filepath.Join(tmpDir, \"foo\")\n\tbarDir := filepath.Join(tmpDir, \"bar\")\n\n\ttestDirs := []string{fooDir, barDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Create terragrunt.hcl files with mutual dependencies (cycle)\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(fooDir, \"terragrunt.hcl\"): `\ndependency \"bar\" {\n\tconfig_path = \"../bar\"\n}\n`,\n\t\tfilepath.Join(barDir, \"terragrunt.hcl\"): `\ndependency \"foo\" {\n\tconfig_path = \"../foo\"\n}\n`,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"{./**}...\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err, \"Discovery should complete even with cycles\")\n\n\t// Verify that a cycle is detected\n\tcycleComponent, cycleErr := components.CycleCheck()\n\trequire.Error(t, cycleErr, \"Cycle check should detect a cycle between foo and bar\")\n\tassert.Contains(t, cycleErr.Error(), \"cycle detected\", \"Error message should mention cycle\")\n\tassert.NotNil(t, cycleComponent, \"Cycle check should return the component that is part of the cycle\")\n\n\t// Verify both foo and bar are in the discovered components\n\tcomponentPaths := components.Paths()\n\tassert.Contains(t, componentPaths, fooDir, \"Foo should be discovered\")\n\tassert.Contains(t, componentPaths, barDir, \"Bar should be discovered\")\n}\n\n// TestDiscovery_CycleDetectionWithDisabledDependency tests that disabled dependencies don't create cycles.\nfunc TestDiscovery_CycleDetectionWithDisabledDependency(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tfooDir := filepath.Join(tmpDir, \"foo\")\n\tbarDir := filepath.Join(tmpDir, \"bar\")\n\n\ttestDirs := []string{fooDir, barDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Create terragrunt.hcl files where one dependency is disabled\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(fooDir, \"terragrunt.hcl\"): `\ndependency \"bar\" {\n\tconfig_path = \"../bar\"\n\tenabled = false\n}\n`,\n\t\tfilepath.Join(barDir, \"terragrunt.hcl\"): `\ndependency \"foo\" {\n\tconfig_path = \"../foo\"\n}\n`,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"{./**}...\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err, \"Discovery should complete\")\n\n\t// Verify that a cycle is NOT detected because one dependency is disabled\n\t_, cycleErr := components.CycleCheck()\n\trequire.NoError(t, cycleErr, \"Cycle check should not detect a cycle when dependency is disabled\")\n\n\t// Verify both foo and bar are in the discovered components\n\tcomponentPaths := components.Paths()\n\tassert.Contains(t, componentPaths, fooDir, \"Foo should be discovered\")\n\tassert.Contains(t, componentPaths, barDir, \"Bar should be discovered\")\n}\n\n// TestDiscovery_WithParseExclude tests that WithParseExclude enables parsing of exclude blocks\n// and that the exclude configurations are accessible on the discovered units.\nfunc TestDiscovery_WithParseExclude(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create test directory structure\n\ttestDirs := []string{\n\t\t\"unit1\",\n\t\t\"unit2\",\n\t\t\"unit3\",\n\t}\n\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(filepath.Join(tmpDir, dir), 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Create test files with exclude configurations\n\ttestFiles := map[string]string{\n\t\t\"unit1/terragrunt.hcl\": `\nexclude {\n  if      = true\n  actions = [\"plan\"]\n}`,\n\t\t\"unit2/terragrunt.hcl\": `\nexclude {\n  if      = true\n  actions = [\"apply\"]\n}`,\n\t\t\"unit3/terragrunt.hcl\": \"\",\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(filepath.Join(tmpDir, path), []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// WithParseExclude sets requiresParse=true which triggers the parse phase,\n\t// allowing exclude blocks to be parsed and accessible on the units.\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithParseExclude()\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// Verify we found all configurations\n\tassert.Len(t, components, 3)\n\n\t// Helper to find unit by path\n\tfindUnit := func(path string) *component.Unit {\n\t\tfor _, c := range components {\n\t\t\tif filepath.Base(c.Path()) == path {\n\t\t\t\tif unit, ok := c.(*component.Unit); ok {\n\t\t\t\t\treturn unit\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// Verify exclude configurations were parsed correctly\n\tunit1 := findUnit(\"unit1\")\n\trequire.NotNil(t, unit1)\n\trequire.NotNil(t, unit1.Config(), \"unit1 should have a parsed config\")\n\trequire.NotNil(t, unit1.Config().Exclude, \"unit1 should have an exclude block\")\n\tassert.Contains(t, unit1.Config().Exclude.Actions, \"plan\", \"unit1 exclude should contain 'plan' action\")\n\n\tunit2 := findUnit(\"unit2\")\n\trequire.NotNil(t, unit2)\n\trequire.NotNil(t, unit2.Config(), \"unit2 should have a parsed config\")\n\trequire.NotNil(t, unit2.Config().Exclude, \"unit2 should have an exclude block\")\n\tassert.Contains(t, unit2.Config().Exclude.Actions, \"apply\", \"unit2 exclude should contain 'apply' action\")\n\n\tunit3 := findUnit(\"unit3\")\n\trequire.NotNil(t, unit3)\n\t// unit3 has an empty config, so Config() may be nil or Exclude may be nil\n\tif unit3.Config() != nil {\n\t\tassert.Nil(t, unit3.Config().Exclude, \"unit3 should not have an exclude block\")\n\t}\n}\n\n// TestDiscovery_WithCustomConfigFilenames tests discovery with custom config filenames.\nfunc TestDiscovery_WithCustomConfigFilenames(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create units with custom config filenames\n\tunit1Dir := filepath.Join(tmpDir, \"unit1\")\n\tunit2Dir := filepath.Join(tmpDir, \"unit2\")\n\n\trequire.NoError(t, os.MkdirAll(unit1Dir, 0755))\n\trequire.NoError(t, os.MkdirAll(unit2Dir, 0755))\n\n\t// Standard terragrunt.hcl in unit1\n\trequire.NoError(t, os.WriteFile(filepath.Join(unit1Dir, \"terragrunt.hcl\"), []byte(\"\"), 0644))\n\t// Custom config in unit2\n\trequire.NoError(t, os.WriteFile(filepath.Join(unit2Dir, \"custom.hcl\"), []byte(\"\"), 0644))\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\tt.Run(\"discover only custom config filename\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\tWithConfigFilenames([]string{\"custom.hcl\"})\n\n\t\tcomponents, err := d.Discover(ctx, l, opts)\n\t\trequire.NoError(t, err)\n\n\t\tunits := components.Filter(component.UnitKind).Paths()\n\t\tassert.Len(t, units, 1)\n\t\tassert.ElementsMatch(t, []string{unit2Dir}, units)\n\t})\n\n\tt.Run(\"discover both standard and custom config filenames\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\tWithConfigFilenames([]string{\"terragrunt.hcl\", \"custom.hcl\"})\n\n\t\tcomponents, err := d.Discover(ctx, l, opts)\n\t\trequire.NoError(t, err)\n\n\t\tunits := components.Filter(component.UnitKind).Paths()\n\t\tassert.Len(t, units, 2)\n\t\tassert.ElementsMatch(t, []string{unit1Dir, unit2Dir}, units)\n\t})\n}\n\n// TestDiscovery_WithReadFiles tests that reading field is populated when using reading filters.\n// The implementation requires a filter that triggers parsing to populate the reading field.\nfunc TestDiscovery_WithReadFiles(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\tappDir := filepath.Join(tmpDir, \"app\")\n\trequire.NoError(t, os.MkdirAll(appDir, 0755))\n\n\t// Create shared files that will be read\n\tsharedHCL := filepath.Join(tmpDir, \"shared.hcl\")\n\tsharedTFVars := filepath.Join(tmpDir, \"shared.tfvars\")\n\n\trequire.NoError(t, os.WriteFile(sharedHCL, []byte(`\n\t\tlocals {\n\t\t\tcommon_value = \"test\"\n\t\t}\n\t`), 0644))\n\n\trequire.NoError(t, os.WriteFile(sharedTFVars, []byte(`\n\t\ttest_var = \"value\"\n\t`), 0644))\n\n\t// Create terragrunt config that reads both files\n\tterragruntConfig := filepath.Join(appDir, \"terragrunt.hcl\")\n\trequire.NoError(t, os.WriteFile(terragruntConfig, []byte(`\n\t\tlocals {\n\t\t\tshared_config = read_terragrunt_config(\"../shared.hcl\")\n\t\t\ttfvars = read_tfvars_file(\"../shared.tfvars\")\n\t\t}\n\t`), 0644))\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Use a reading filter to trigger parsing and populate the reading field\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"reading=shared.hcl\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithFilters(filters).\n\t\tWithReadFiles()\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// Find the app component\n\tvar appComponent *component.Unit\n\n\tfor _, c := range components {\n\t\tif c.Path() == appDir {\n\t\t\tif unit, ok := c.(*component.Unit); ok {\n\t\t\t\tappComponent = unit\n\t\t\t}\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\trequire.NotNil(t, appComponent, \"app component should be discovered\")\n\trequire.NotNil(t, appComponent.Reading(), \"Reading field should be initialized\")\n\n\t// Verify Reading field contains the files that were read\n\trequire.NotEmpty(t, appComponent.Reading(), \"should have read files\")\n\tassert.Contains(t, appComponent.Reading(), sharedHCL, \"should contain shared.hcl\")\n}\n\n// TestDiscovery_WithStackConfigParsing tests that stack files are discovered but not parsed as unit configs.\nfunc TestDiscovery_WithStackConfigParsing(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tstackDir := filepath.Join(tmpDir, \"stack\")\n\tunitDir := filepath.Join(tmpDir, \"unit\")\n\n\ttestDirs := []string{stackDir, unitDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Create a stack file with unit blocks\n\tstackContent := `\nunit \"unit_a\" {\n  source = \"${get_repo_root()}/unit_a\"\n  path   = \"unit_a\"\n}\n\nunit \"unit_b\" {\n  source = \"${get_repo_root()}/unit_b\"\n  path   = \"unit_b\"\n}\n`\n\n\t// Create a unit file with valid unit configuration\n\tunitContent := `\nterraform {\n  source = \".\"\n}\n\ninputs = {\n  test = \"value\"\n}\n`\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(stackDir, \"terragrunt.stack.hcl\"): stackContent,\n\t\tfilepath.Join(unitDir, \"terragrunt.hcl\"):        unitContent,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"{./**}...\"})\n\trequire.NoError(t, err)\n\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// Verify that both stack and unit configurations are discovered\n\tunits := components.Filter(component.UnitKind)\n\tstacks := components.Filter(component.StackKind)\n\n\tassert.Len(t, units, 1)\n\tassert.Len(t, stacks, 1)\n\n\t// Verify that stack configuration is not parsed (Config should be nil)\n\tstackComp := stacks[0]\n\tstack, ok := stackComp.(*component.Stack)\n\trequire.True(t, ok, \"should be a Stack\")\n\tassert.Nil(t, stack.Config(), \"Stack configuration should not be parsed\")\n\n\t// Verify that unit configuration is parsed (Config should not be nil)\n\tunitComp := units[0]\n\tunit, ok := unitComp.(*component.Unit)\n\trequire.True(t, ok, \"should be a Unit\")\n\tassert.NotNil(t, unit.Config(), \"Unit configuration should be parsed\")\n}\n\n// TestDiscovery_IncludeExcludeFilterSemantics tests include/exclude filter behavior.\nfunc TestDiscovery_IncludeExcludeFilterSemantics(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tunit1Dir := filepath.Join(tmpDir, \"unit1\")\n\tunit2Dir := filepath.Join(tmpDir, \"unit2\")\n\tunit3Dir := filepath.Join(tmpDir, \"unit3\")\n\n\tfor _, d := range []string{unit1Dir, unit2Dir, unit3Dir} {\n\t\trequire.NoError(t, os.MkdirAll(d, 0755))\n\t}\n\n\tfor _, f := range []string{\n\t\tfilepath.Join(unit1Dir, \"terragrunt.hcl\"),\n\t\tfilepath.Join(unit2Dir, \"terragrunt.hcl\"),\n\t\tfilepath.Join(unit3Dir, \"terragrunt.hcl\"),\n\t} {\n\t\trequire.NoError(t, os.WriteFile(f, []byte(\"\"), 0644))\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\ttests := []struct {\n\t\tname    string\n\t\tfilters []string\n\t\twant    []string\n\t}{\n\t\t{\n\t\t\tname:    \"include by default (no filters)\",\n\t\t\tfilters: []string{},\n\t\t\twant:    []string{unit1Dir, unit2Dir, unit3Dir},\n\t\t},\n\t\t{\n\t\t\tname:    \"exclude by default when positive filter\",\n\t\t\tfilters: []string{\"unit1\"},\n\t\t\twant:    []string{unit1Dir},\n\t\t},\n\t\t{\n\t\t\tname:    \"include by default with only negative filter\",\n\t\t\tfilters: []string{\"!unit2\"},\n\t\t\twant:    []string{unit1Dir, unit3Dir},\n\t\t},\n\t\t{\n\t\t\tname:    \"exclude by default with positive and negative filters\",\n\t\t\tfilters: []string{\"unit1\", \"!unit2\"},\n\t\t\twant:    []string{unit1Dir},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tfilters, err := filter.ParseFilterQueries(l, tt.filters)\n\t\t\trequire.NoError(t, err)\n\n\t\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\t\tWithFilters(filters)\n\n\t\t\tcomponents, err := d.Discover(ctx, l, opts)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.ElementsMatch(t, tt.want, components.Filter(component.UnitKind).Paths())\n\t\t})\n\t}\n}\n\n// TestDiscovery_HiddenIncludedByIncludeDirs tests hidden directories are included when explicitly filtered.\nfunc TestDiscovery_HiddenIncludedByIncludeDirs(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\thiddenUnitDir := filepath.Join(tmpDir, \".hidden\", \"hunit\")\n\trequire.NoError(t, os.MkdirAll(hiddenUnitDir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(hiddenUnitDir, \"terragrunt.hcl\"), []byte(\"\"), 0644))\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"./.hidden/**\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\tassert.ElementsMatch(t, []string{hiddenUnitDir}, components.Filter(component.UnitKind).Paths())\n}\n\n// TestDiscovery_ExternalDependencies tests that external dependencies are correctly identified.\nfunc TestDiscovery_ExternalDependencies(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tinternalDir := filepath.Join(tmpDir, \"internal\")\n\texternalDir := filepath.Join(tmpDir, \"external\")\n\tappDir := filepath.Join(internalDir, \"app\")\n\tdbDir := filepath.Join(internalDir, \"db\")\n\tvpcDir := filepath.Join(internalDir, \"vpc\")\n\textApp := filepath.Join(externalDir, \"app\")\n\n\tfor _, d := range []string{appDir, dbDir, vpcDir, extApp} {\n\t\trequire.NoError(t, os.MkdirAll(d, 0755))\n\t}\n\n\trequire.NoError(t, os.WriteFile(filepath.Join(appDir, \"terragrunt.hcl\"), []byte(`\n\tdependency \"db\" { config_path = \"../db\" }\n\tdependency \"external\" { config_path = \"../../external/app\" }\n\t`), 0644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(dbDir, \"terragrunt.hcl\"), []byte(`\n\tdependency \"vpc\" { config_path = \"../vpc\" }\n\t`), 0644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(vpcDir, \"terragrunt.hcl\"), []byte(\"\"), 0644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(extApp, \"terragrunt.hcl\"), []byte(\"\"), 0644))\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     internalDir,\n\t\tRootWorkingDir: internalDir,\n\t}\n\n\tctx := t.Context()\n\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"{./**}...\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(internalDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: internalDir}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// Find app config and assert it has external dependency\n\tvar appCfg *component.Unit\n\n\tfor _, c := range components {\n\t\tif c.Path() == appDir {\n\t\t\tif unit, ok := c.(*component.Unit); ok {\n\t\t\t\tappCfg = unit\n\t\t\t}\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\trequire.NotNil(t, appCfg)\n\tdepPaths := appCfg.Dependencies().Paths()\n\tassert.Contains(t, depPaths, dbDir)\n\tassert.Contains(t, depPaths, extApp)\n\n\t// Verify external dependency is marked as external\n\tfor _, dep := range appCfg.Dependencies() {\n\t\tif dep.Path() == extApp {\n\t\t\tassert.True(t, dep.External(), \"external app should be marked as external\")\n\t\t}\n\t}\n}\n\n// TestDiscovery_BreakCycles tests that WithBreakCycles removes cyclic components.\nfunc TestDiscovery_BreakCycles(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tfooDir := filepath.Join(tmpDir, \"foo\")\n\tbarDir := filepath.Join(tmpDir, \"bar\")\n\n\ttestDirs := []string{fooDir, barDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Create terragrunt.hcl files with mutual dependencies (cycle)\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(fooDir, \"terragrunt.hcl\"): `\ndependency \"bar\" {\n\tconfig_path = \"../bar\"\n}\n`,\n\t\tfilepath.Join(barDir, \"terragrunt.hcl\"): `\ndependency \"foo\" {\n\tconfig_path = \"../foo\"\n}\n`,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"{./**}...\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithFilters(filters).\n\t\tWithBreakCycles()\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err, \"Discovery should complete with break cycles enabled\")\n\n\t// With break cycles enabled, the cycle should be resolved (one component removed)\n\t_, cycleErr := components.CycleCheck()\n\trequire.NoError(t, cycleErr, \"Cycle check should not detect a cycle after breaking\")\n}\n\n// TestDiscovery_WithNumWorkers tests that the worker count can be configured.\nfunc TestDiscovery_WithNumWorkers(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create a few test units\n\tfor i := range 5 {\n\t\tdir := filepath.Join(tmpDir, \"unit\"+string(rune('a'+i)))\n\t\trequire.NoError(t, os.MkdirAll(dir, 0755))\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(dir, \"terragrunt.hcl\"), []byte(\"\"), 0644))\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithNumWorkers(2)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\tassert.Len(t, components, 5)\n}\n\n// TestDiscovery_WithMaxDependencyDepth tests dependency depth limiting.\nfunc TestDiscovery_WithMaxDependencyDepth(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create chain: a -> b -> c -> d\n\taDir := filepath.Join(tmpDir, \"a\")\n\tbDir := filepath.Join(tmpDir, \"b\")\n\tcDir := filepath.Join(tmpDir, \"c\")\n\tdDir := filepath.Join(tmpDir, \"d\")\n\n\tfor _, dir := range []string{aDir, bDir, cDir, dDir} {\n\t\trequire.NoError(t, os.MkdirAll(dir, 0755))\n\t}\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(aDir, \"terragrunt.hcl\"): `\ndependency \"b\" {\n\tconfig_path = \"../b\"\n}\n`,\n\t\tfilepath.Join(bDir, \"terragrunt.hcl\"): `\ndependency \"c\" {\n\tconfig_path = \"../c\"\n}\n`,\n\t\tfilepath.Join(cDir, \"terragrunt.hcl\"): `\ndependency \"d\" {\n\tconfig_path = \"../d\"\n}\n`,\n\t\tfilepath.Join(dDir, \"terragrunt.hcl\"): ``,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\tt.Run(\"full depth discovers all\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(l, []string{\"a...\"})\n\t\trequire.NoError(t, err)\n\n\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\tWithFilters(filters).\n\t\t\tWithMaxDependencyDepth(100)\n\n\t\tcomponents, err := d.Discover(ctx, l, opts)\n\t\trequire.NoError(t, err)\n\n\t\tpaths := components.Paths()\n\t\tassert.Contains(t, paths, aDir)\n\t\tassert.Contains(t, paths, bDir)\n\t\tassert.Contains(t, paths, cDir)\n\t\tassert.Contains(t, paths, dDir)\n\t})\n\n\tt.Run(\"limited depth\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(l, []string{\"a...\"})\n\t\trequire.NoError(t, err)\n\n\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\tWithFilters(filters).\n\t\t\tWithMaxDependencyDepth(1)\n\n\t\tcomponents, err := d.Discover(ctx, l, opts)\n\t\trequire.NoError(t, err)\n\n\t\tpaths := components.Paths()\n\t\tassert.Contains(t, paths, aDir, \"a should always be included\")\n\t\t// With depth 1, we should get at least a and b\n\t\tassert.Contains(t, paths, bDir, \"b should be included with depth 1\")\n\t})\n}\n\n// TestDiscovery_SuppressParseErrors tests that parse errors can be suppressed.\nfunc TestDiscovery_SuppressParseErrors(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tvalidDir := filepath.Join(tmpDir, \"valid\")\n\tinvalidDir := filepath.Join(tmpDir, \"invalid\")\n\n\trequire.NoError(t, os.MkdirAll(validDir, 0755))\n\trequire.NoError(t, os.MkdirAll(invalidDir, 0755))\n\n\t// Valid config\n\trequire.NoError(t, os.WriteFile(filepath.Join(validDir, \"terragrunt.hcl\"), []byte(\"\"), 0644))\n\t// Invalid config (should cause parse error)\n\trequire.NoError(t, os.WriteFile(filepath.Join(invalidDir, \"terragrunt.hcl\"), []byte(`\nterraform {\n  source = undefined_function()\n}\n`), 0644))\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithParseExclude().\n\t\tWithSuppressParseErrors()\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err, \"Discovery should succeed with suppressed parse errors\")\n\n\t// Valid config should be discovered\n\tpaths := components.Paths()\n\tassert.Contains(t, paths, validDir)\n}\n\n// TestDiscovery_ExcludeDependencies tests that ExcludeDependencies only takes effect\n// when the dependent unit's exclude condition (If) is true.\nfunc TestDiscovery_ExcludeDependencies(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname               string\n\t\texcludeIf          string\n\t\tdependentExcluded  bool\n\t\tdependencyExcluded bool\n\t}{\n\t\t{\n\t\t\tname:               \"exclude_dependencies with if=false\",\n\t\t\texcludeIf:          \"false\",\n\t\t\tdependentExcluded:  false,\n\t\t\tdependencyExcluded: false,\n\t\t},\n\t\t{\n\t\t\tname:               \"exclude_dependencies with if=true\",\n\t\t\texcludeIf:          \"true\",\n\t\t\tdependentExcluded:  true,\n\t\t\tdependencyExcluded: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\tdependentDir := filepath.Join(tmpDir, \"dependent\")\n\t\t\tdependencyDir := filepath.Join(tmpDir, \"dependency\")\n\n\t\t\trequire.NoError(t, os.MkdirAll(dependentDir, 0755))\n\t\t\trequire.NoError(t, os.MkdirAll(dependencyDir, 0755))\n\n\t\t\tdependentHCL := `\nexclude {\n  if                   = ` + tt.excludeIf + `\n  actions              = [\"all\"]\n  exclude_dependencies = true\n}\n\ndependency \"dependency\" {\n  config_path = \"../dependency\"\n}\n`\n\t\t\trequire.NoError(t, os.WriteFile(filepath.Join(dependentDir, \"terragrunt.hcl\"), []byte(dependentHCL), 0644))\n\t\t\trequire.NoError(t, os.WriteFile(filepath.Join(dependencyDir, \"terragrunt.hcl\"), []byte(\"\"), 0644))\n\n\t\t\tl := logger.CreateLogger()\n\t\t\topts := &options.TerragruntOptions{\n\t\t\t\tWorkingDir:       tmpDir,\n\t\t\t\tRootWorkingDir:   tmpDir,\n\t\t\t\tTerraformCommand: \"plan\",\n\t\t\t}\n\n\t\t\tctx := t.Context()\n\n\t\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\t\tWithParseExclude().\n\t\t\t\tWithRelationships()\n\n\t\t\tcomponents, err := d.Discover(ctx, l, opts)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar dependentUnit, dependencyUnit *component.Unit\n\n\t\t\tfor _, c := range components {\n\t\t\t\tunit, ok := c.(*component.Unit)\n\t\t\t\tif !ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tswitch c.Path() {\n\t\t\t\tcase dependentDir:\n\t\t\t\t\tdependentUnit = unit\n\t\t\t\tcase dependencyDir:\n\t\t\t\t\tdependencyUnit = unit\n\t\t\t\t}\n\t\t\t}\n\n\t\t\trequire.NotNil(t, dependentUnit, \"dependent unit should be discovered\")\n\t\t\trequire.NotNil(t, dependencyUnit, \"dependency unit should be discovered\")\n\n\t\t\tassert.Equal(t, tt.dependentExcluded, dependentUnit.Excluded(), \"dependent excluded state\")\n\t\t\tassert.Equal(t, tt.dependencyExcluded, dependencyUnit.Excluded(), \"dependency excluded state\")\n\t\t})\n\t}\n}\n\n// TestDiscovery_OriginalTerragruntConfigPath tests that get_original_terragrunt_dir() returns the\n// correct directory during parsing. This verifies that phase_parse.go correctly sets\n// OriginalTerragruntConfigPath when parsing units.\nfunc TestDiscovery_OriginalTerragruntConfigPath(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tappDir := filepath.Join(tmpDir, \"app\")\n\tdbDir := filepath.Join(tmpDir, \"db\")\n\n\trequire.NoError(t, os.MkdirAll(appDir, 0755))\n\trequire.NoError(t, os.MkdirAll(dbDir, 0755))\n\n\t// Create a config that uses get_original_terragrunt_dir() in the terraform source\n\t// This function relies on OriginalTerragruntConfigPath being set correctly\n\trequire.NoError(t, os.WriteFile(filepath.Join(appDir, \"terragrunt.hcl\"), []byte(`\nterraform {\n  source = \"${get_original_terragrunt_dir()}/module\"\n}\n\ndependency \"db\" {\n  config_path = \"../db\"\n}\n`), 0644))\n\n\trequire.NoError(t, os.WriteFile(filepath.Join(dbDir, \"terragrunt.hcl\"), []byte(\"\"), 0644))\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t\t// Start with a different config path to simulate the scenario where opts is cloned\n\t\tTerragruntConfigPath:         tmpDir,\n\t\tOriginalTerragruntConfigPath: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Use a dependency traversal filter (app...) to trigger parsing\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"app...\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// Find the app component\n\tvar appComponent *component.Unit\n\n\tfor _, c := range components {\n\t\tif c.Path() == appDir {\n\t\t\tif unit, ok := c.(*component.Unit); ok {\n\t\t\t\tappComponent = unit\n\t\t\t}\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\trequire.NotNil(t, appComponent, \"app component should be discovered\")\n\trequire.NotNil(t, appComponent.Config(), \"app config should be parsed\")\n\trequire.NotNil(t, appComponent.Config().Terraform, \"terraform block should be parsed\")\n\trequire.NotNil(t, appComponent.Config().Terraform.Source, \"terraform source should be parsed\")\n\n\t// The key test: verify that get_original_terragrunt_dir() returned the correct directory\n\t// It should resolve to the app unit's directory, not the initial opts value (tmpDir)\n\texpectedSource := filepath.Join(appDir, \"module\")\n\tassert.Equal(t, expectedSource, *appComponent.Config().Terraform.Source,\n\t\t\"terraform source should use the correct unit directory from get_original_terragrunt_dir()\")\n}\n"
  },
  {
    "path": "internal/discovery/discovery_test.go",
    "content": "package discovery_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCandidacyClassifier_Analyze(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname                   string\n\t\tfilterStrings          []string\n\t\texpectHasPositive      bool\n\t\texpectHasParseRequired bool\n\t\texpectHasGraphFilters  bool\n\t\texpectGraphExprCount   int\n\t}{\n\t\t{\n\t\t\tname:              \"empty filters\",\n\t\t\tfilterStrings:     []string{},\n\t\t\texpectHasPositive: false,\n\t\t},\n\t\t{\n\t\t\tname:              \"simple path filter\",\n\t\t\tfilterStrings:     []string{\"./foo\"},\n\t\t\texpectHasPositive: true,\n\t\t},\n\t\t{\n\t\t\tname:              \"negated path filter only\",\n\t\t\tfilterStrings:     []string{\"!./foo\"},\n\t\t\texpectHasPositive: false,\n\t\t},\n\t\t{\n\t\t\tname:              \"path filter with negation\",\n\t\t\tfilterStrings:     []string{\"./foo\", \"!./bar\"},\n\t\t\texpectHasPositive: true,\n\t\t},\n\t\t{\n\t\t\tname:                   \"reading attribute filter\",\n\t\t\tfilterStrings:          []string{\"reading=config/*\"},\n\t\t\texpectHasPositive:      true,\n\t\t\texpectHasParseRequired: true,\n\t\t},\n\t\t{\n\t\t\tname:                  \"dependency graph filter\",\n\t\t\tfilterStrings:         []string{\"./foo...\"},\n\t\t\texpectHasPositive:     true,\n\t\t\texpectHasGraphFilters: true,\n\t\t\texpectGraphExprCount:  1,\n\t\t},\n\t\t{\n\t\t\tname:                  \"dependent graph filter\",\n\t\t\tfilterStrings:         []string{\"..../foo\"},\n\t\t\texpectHasPositive:     true,\n\t\t\texpectHasGraphFilters: true,\n\t\t\texpectGraphExprCount:  1,\n\t\t},\n\t\t{\n\t\t\tname:                  \"exclude target graph filter\",\n\t\t\tfilterStrings:         []string{\"^{./foo}...\"},\n\t\t\texpectHasPositive:     true,\n\t\t\texpectHasGraphFilters: true,\n\t\t\texpectGraphExprCount:  1,\n\t\t},\n\t\t{\n\t\t\tname:                  \"multiple graph filters\",\n\t\t\tfilterStrings:         []string{\"./foo...\", \"..../bar\"},\n\t\t\texpectHasPositive:     true,\n\t\t\texpectHasGraphFilters: true,\n\t\t\texpectGraphExprCount:  2,\n\t\t},\n\t\t{\n\t\t\tname:              \"name attribute filter\",\n\t\t\tfilterStrings:     []string{\"name=my-app\"},\n\t\t\texpectHasPositive: true,\n\t\t},\n\t\t{\n\t\t\tname:              \"type attribute filter\",\n\t\t\tfilterStrings:     []string{\"type=unit\"},\n\t\t\texpectHasPositive: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tl := logger.CreateLogger()\n\t\t\tfilters, err := filter.ParseFilterQueries(l, tt.filterStrings)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tclassifier := filter.NewClassifier(filters)\n\n\t\t\tassert.Equal(t, tt.expectHasPositive, classifier.HasPositiveFilters(), \"HasPositiveFilters mismatch\")\n\t\t\tassert.Equal(t, tt.expectHasParseRequired, classifier.HasParseRequiredFilters(), \"HasParseRequiredFilters mismatch\")\n\t\t\tassert.Equal(t, tt.expectHasGraphFilters, classifier.HasGraphFilters(), \"HasGraphFilters mismatch\")\n\n\t\t\tif tt.expectGraphExprCount > 0 {\n\t\t\t\tassert.Len(t, classifier.GraphExpressions(), tt.expectGraphExprCount, \"GraphExpressions count mismatch\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCandidacyClassifier_ClassifyComponent(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname          string\n\t\tcomponentPath string\n\t\tworkingDir    string\n\t\tfilterStrings []string\n\t\texpectStatus  filter.ClassificationStatus\n\t\texpectReason  filter.CandidacyReason\n\t\texpectIndex   int\n\t}{\n\t\t{\n\t\t\tname:          \"no filters - include by default\",\n\t\t\tfilterStrings: []string{},\n\t\t\tcomponentPath: \"/project/foo\",\n\t\t\tworkingDir:    \"/project\",\n\t\t\texpectStatus:  filter.StatusDiscovered,\n\t\t\texpectReason:  filter.CandidacyReasonNone,\n\t\t\texpectIndex:   -1,\n\t\t},\n\t\t{\n\t\t\tname:          \"matching path filter\",\n\t\t\tfilterStrings: []string{\"./foo\"},\n\t\t\tcomponentPath: \"/project/foo\",\n\t\t\tworkingDir:    \"/project\",\n\t\t\texpectStatus:  filter.StatusDiscovered,\n\t\t\texpectReason:  filter.CandidacyReasonNone,\n\t\t\texpectIndex:   -1,\n\t\t},\n\t\t{\n\t\t\tname:          \"non-matching path filter - exclude by default\",\n\t\t\tfilterStrings: []string{\"./bar\"},\n\t\t\tcomponentPath: \"/project/foo\",\n\t\t\tworkingDir:    \"/project\",\n\t\t\texpectStatus:  filter.StatusExcluded,\n\t\t\texpectReason:  filter.CandidacyReasonNone,\n\t\t\texpectIndex:   -1,\n\t\t},\n\t\t{\n\t\t\tname:          \"negated filter only - exclude component\",\n\t\t\tfilterStrings: []string{\"!./foo\"},\n\t\t\tcomponentPath: \"/project/foo\",\n\t\t\tworkingDir:    \"/project\",\n\t\t\texpectStatus:  filter.StatusExcluded,\n\t\t\texpectReason:  filter.CandidacyReasonNone,\n\t\t\texpectIndex:   -1,\n\t\t},\n\t\t{\n\t\t\tname:          \"negated filter only - include other\",\n\t\t\tfilterStrings: []string{\"!./foo\"},\n\t\t\tcomponentPath: \"/project/bar\",\n\t\t\tworkingDir:    \"/project\",\n\t\t\texpectStatus:  filter.StatusDiscovered,\n\t\t\texpectReason:  filter.CandidacyReasonNone,\n\t\t\texpectIndex:   -1,\n\t\t},\n\t\t{\n\t\t\tname:          \"graph expression target - candidate\",\n\t\t\tfilterStrings: []string{\"./foo...\"},\n\t\t\tcomponentPath: \"/project/foo\",\n\t\t\tworkingDir:    \"/project\",\n\t\t\texpectStatus:  filter.StatusCandidate,\n\t\t\texpectReason:  filter.CandidacyReasonGraphTarget,\n\t\t\texpectIndex:   0,\n\t\t},\n\t\t{\n\t\t\tname:          \"parse required filter - candidate\",\n\t\t\tfilterStrings: []string{\"reading=config/*\"},\n\t\t\tcomponentPath: \"/project/foo\",\n\t\t\tworkingDir:    \"/project\",\n\t\t\texpectStatus:  filter.StatusCandidate,\n\t\t\texpectReason:  filter.CandidacyReasonRequiresParse,\n\t\t\texpectIndex:   -1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tl := logger.CreateLogger()\n\t\t\tfilters, err := filter.ParseFilterQueries(l, tt.filterStrings)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tclassifier := filter.NewClassifier(filters)\n\n\t\t\t// Create a test component\n\t\t\tc := component.NewUnit(tt.componentPath)\n\t\t\tc.SetDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\tWorkingDir: tt.workingDir,\n\t\t\t})\n\n\t\t\tctx := filter.ClassificationContext{}\n\t\t\tstatus, reason, index := classifier.Classify(c, ctx)\n\n\t\t\tassert.Equal(t, tt.expectStatus, status, \"status mismatch\")\n\t\t\tassert.Equal(t, tt.expectReason, reason, \"reason mismatch\")\n\t\t\tassert.Equal(t, tt.expectIndex, index, \"index mismatch\")\n\t\t})\n\t}\n}\n\nfunc TestDiscovery_SimpleFilesystem(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a temporary directory structure\n\ttmpDir := t.TempDir()\n\n\t// Create some terragrunt.hcl files\n\tdirs := []string{\"foo\", \"bar\", \"baz\"}\n\tfor _, dir := range dirs {\n\t\tdirPath := filepath.Join(tmpDir, dir)\n\t\trequire.NoError(t, os.MkdirAll(dirPath, 0755))\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(dirPath, \"terragrunt.hcl\"),\n\t\t\t[]byte(\"# Test config\\n\"),\n\t\t\t0644,\n\t\t))\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Test: discover all components\n\td := discovery.NewDiscovery(tmpDir).WithDiscoveryContext(&component.DiscoveryContext{\n\t\tWorkingDir: tmpDir,\n\t})\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\tassert.Len(t, components, 3, \"should discover 3 components\")\n}\n\nfunc TestDiscovery_WithPathFilter(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a temporary directory structure\n\ttmpDir := t.TempDir()\n\n\t// Create some terragrunt.hcl files\n\tdirs := []string{\"apps/foo\", \"apps/bar\", \"infra/baz\"}\n\tfor _, dir := range dirs {\n\t\tdirPath := filepath.Join(tmpDir, dir)\n\t\trequire.NoError(t, os.MkdirAll(dirPath, 0755))\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(dirPath, \"terragrunt.hcl\"),\n\t\t\t[]byte(\"# Test config\\n\"),\n\t\t\t0644,\n\t\t))\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Test: filter to apps/* only\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"./apps/*\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: tmpDir,\n\t\t}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\tassert.Len(t, components, 2, \"should discover 2 components in apps/\")\n}\n\nfunc TestDiscovery_WithNegatedFilter(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a temporary directory structure\n\ttmpDir := t.TempDir()\n\n\t// Create some terragrunt.hcl files\n\tdirs := []string{\"foo\", \"bar\", \"baz\"}\n\tfor _, dir := range dirs {\n\t\tdirPath := filepath.Join(tmpDir, dir)\n\t\trequire.NoError(t, os.MkdirAll(dirPath, 0755))\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(dirPath, \"terragrunt.hcl\"),\n\t\t\t[]byte(\"# Test config\\n\"),\n\t\t\t0644,\n\t\t))\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Test: exclude ./bar\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"!./bar\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: tmpDir,\n\t\t}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\tassert.Len(t, components, 2, \"should discover 2 components (excluding bar)\")\n\n\t// Verify bar is not in results\n\tfor _, c := range components {\n\t\tassert.NotContains(t, c.Path(), \"bar\", \"bar should be excluded\")\n\t}\n}\n\nfunc TestDiscovery_CombinedFilters(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a temporary directory structure\n\ttmpDir := t.TempDir()\n\n\t// Create some terragrunt.hcl files\n\tdirs := []string{\"apps/foo\", \"apps/bar\", \"apps/baz\", \"infra/db\"}\n\tfor _, dir := range dirs {\n\t\tdirPath := filepath.Join(tmpDir, dir)\n\t\trequire.NoError(t, os.MkdirAll(dirPath, 0755))\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(dirPath, \"terragrunt.hcl\"),\n\t\t\t[]byte(\"# Test config\\n\"),\n\t\t\t0644,\n\t\t))\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Test: ./apps/* but not ./apps/baz\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"./apps/*\", \"!./apps/baz\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: tmpDir,\n\t\t}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\tassert.Len(t, components, 2, \"should discover 2 components (apps/* minus baz)\")\n\n\t// Verify baz is not in results\n\tfor _, c := range components {\n\t\tassert.NotContains(t, c.Path(), \"baz\", \"baz should be excluded\")\n\t}\n}\n\nfunc TestPhaseKind_String(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected string\n\t\tkind     discovery.PhaseKind\n\t}{\n\t\t{expected: \"filesystem\", kind: discovery.PhaseFilesystem},\n\t\t{expected: \"worktree\", kind: discovery.PhaseWorktree},\n\t\t{expected: \"parse\", kind: discovery.PhaseParse},\n\t\t{expected: \"graph\", kind: discovery.PhaseGraph},\n\t\t{expected: \"relationship\", kind: discovery.PhaseRelationship},\n\t\t{expected: \"final\", kind: discovery.PhaseFinal},\n\t\t{expected: \"unknown\", kind: discovery.PhaseKind(999)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.expected, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, tt.expected, tt.kind.String())\n\t\t})\n\t}\n}\n\nfunc TestDiscoveryStatus_String(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected string\n\t\tstatus   filter.ClassificationStatus\n\t}{\n\t\t{expected: \"discovered\", status: filter.StatusDiscovered},\n\t\t{expected: \"candidate\", status: filter.StatusCandidate},\n\t\t{expected: \"excluded\", status: filter.StatusExcluded},\n\t\t{expected: \"unknown\", status: filter.ClassificationStatus(999)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.expected, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, tt.expected, tt.status.String())\n\t\t})\n\t}\n}\n\nfunc TestCandidacyReason_String(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected string\n\t\treason   filter.CandidacyReason\n\t}{\n\t\t{expected: \"none\", reason: filter.CandidacyReasonNone},\n\t\t{expected: \"graph-target\", reason: filter.CandidacyReasonGraphTarget},\n\t\t{expected: \"requires-parse\", reason: filter.CandidacyReasonRequiresParse},\n\t\t{expected: \"unknown\", reason: filter.CandidacyReason(999)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.expected, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, tt.expected, tt.reason.String())\n\t\t})\n\t}\n}\n\n// TestDiscovery_PopulatesReadingField verifies that the Reading field is populated\n// with files read during parsing via read_terragrunt_config() and read_tfvars_file().\nfunc TestDiscovery_PopulatesReadingField(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\tappDir := filepath.Join(tmpDir, \"app\")\n\trequire.NoError(t, os.MkdirAll(appDir, 0755))\n\n\t// Create shared files that will be read\n\tsharedHCL := filepath.Join(tmpDir, \"shared.hcl\")\n\tsharedTFVars := filepath.Join(tmpDir, \"shared.tfvars\")\n\n\trequire.NoError(t, os.WriteFile(sharedHCL, []byte(`\n\t\tlocals {\n\t\t\tcommon_value = \"test\"\n\t\t}\n\t`), 0644))\n\n\trequire.NoError(t, os.WriteFile(sharedTFVars, []byte(`\n\t\ttest_var = \"value\"\n\t`), 0644))\n\n\t// Create terragrunt config that reads both files\n\tterragruntConfig := filepath.Join(appDir, \"terragrunt.hcl\")\n\trequire.NoError(t, os.WriteFile(terragruntConfig, []byte(`\n\t\tlocals {\n\t\t\tshared_config = read_terragrunt_config(\"../shared.hcl\")\n\t\t\ttfvars = read_tfvars_file(\"../shared.tfvars\")\n\t\t}\n\t`), 0644))\n\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tl := logger.CreateLogger()\n\tctx := t.Context()\n\n\t// Discover components with ReadFiles enabled to populate Reading field\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithReadFiles()\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// Find the app component\n\tvar appComponent *component.Unit\n\n\tfor _, c := range components {\n\t\tif c.Path() == appDir {\n\t\t\tif unit, ok := c.(*component.Unit); ok {\n\t\t\t\tappComponent = unit\n\t\t\t}\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\trequire.NotNil(t, appComponent, \"app component should be discovered\")\n\trequire.NotNil(t, appComponent.Reading(), \"Reading field should be initialized\")\n\n\t// Verify Reading field contains the files that were read\n\trequire.NotEmpty(t, appComponent.Reading(), \"should have read files\")\n\tassert.Contains(t, appComponent.Reading(), sharedHCL, \"should contain shared.hcl\")\n\tassert.Contains(t, appComponent.Reading(), sharedTFVars, \"should contain shared.tfvars\")\n}\n\nfunc TestDiscovery_BothHclAndStackFileInSameDir(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := t.TempDir()\n\tsubDir := filepath.Join(tmpDir, \"app\")\n\trequire.NoError(t, os.MkdirAll(subDir, 0755))\n\n\trequire.NoError(t, os.WriteFile(\n\t\tfilepath.Join(subDir, \"terragrunt.hcl\"),\n\t\t[]byte(\"# empty unit config\\n\"),\n\t\t0644,\n\t))\n\trequire.NoError(t, os.WriteFile(\n\t\tfilepath.Join(subDir, \"terragrunt.stack.hcl\"),\n\t\t[]byte(\"# empty stack config\\n\"),\n\t\t0644,\n\t))\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir})\n\n\t_, err := d.Discover(t.Context(), l, opts)\n\trequire.Error(t, err)\n\n\tvar coexistErr discovery.CoexistenceError\n\trequire.ErrorAs(t, err, &coexistErr)\n\tassert.Equal(t, subDir, coexistErr.ComponentPath)\n}\n\n// TestDiscovery_SingleUnitNoDuplicateError verifies that a directory with only\n// a single config file does not trigger a coexistence error.\nfunc TestDiscovery_SingleUnitNoDuplicateError(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := t.TempDir()\n\tsubDir := filepath.Join(tmpDir, \"app\")\n\trequire.NoError(t, os.MkdirAll(subDir, 0755))\n\n\trequire.NoError(t, os.WriteFile(\n\t\tfilepath.Join(subDir, \"terragrunt.hcl\"),\n\t\t[]byte(\"# config\\n\"),\n\t\t0644,\n\t))\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir})\n\n\tcomponents, err := d.Discover(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\tassert.Len(t, components, 1)\n\tassert.Equal(t, component.UnitKind, components[0].Kind())\n}\n"
  },
  {
    "path": "internal/discovery/doc.go",
    "content": "// Package discovery provides a channel-based phased discovery architecture for Terragrunt components.\n//\n// # Overview\n//\n// This package discovers Terragrunt components (units and stacks) across a directory tree\n// using a multi-phase pipeline.\n//\n// Each phase communicates via two output channels:\n//   - discovered: Components definitively included in results\n//   - candidates: Components that might be included pending further evaluation\n//\n// This dual-channel approach enables lazy evaluation. Components are only parsed or\n// graph-traversed when necessary for filter evaluation.\n//\n// # Constructors\n//\n// The package provides several constructors for different use cases:\n//\n//   - [NewDiscovery]: Creates a Discovery with sensible defaults including CPU-aware worker\n//     count (scales with runtime.NumCPU, min 4, max 8) and pre-initialized [component.DiscoveryContext].\n//     This is the recommended constructor for most use cases.\n//\n//   - [NewForDiscoveryCommand]: Creates a Discovery configured for discovery commands (find/list)\n//     with parse error suppression and cycle breaking enabled.\n//\n//   - [NewForHCLCommand]: Creates a Discovery for HCL commands (validate/format).\n//\n//   - [NewForStackGenerate]: Creates a Discovery for stack generate commands.\n//\n// # Classification Rules\n//\n// The [filter.Classifier] analyzes all filter expressions upfront and classifies\n// each component into one of three statuses:\n//\n//   - [StatusDiscovered]: Matches a positive filter (path, attribute, or git expression)\n//   - [StatusCandidate]: Needs further evaluation (graph target, requires parsing, or potential dependent)\n//   - [StatusExcluded]: Only matches negated filters, or positive filters exist but none match\n//\n// When no positive filters exist, components are included by default. When positive\n// filters exist, only matching components are included.\n//\n// # Phase Flow\n//\n// The discovery process executes in the following phases:\n//\n//  1. Filesystem + Worktree Discovery (concurrent)\n//     - [PhaseFilesystem]: Walk directories recursively, classify components via [filter.Classifier]\n//     - [PhaseWorktree]: For Git filters [ref...ref], discover components in temporary worktrees\n//     and detect added/removed/modified components via SHA256 comparison\n//\n//  2. Parse Phase (if needed)\n//     - [PhaseParse]: Parse HCL configs for candidates with [CandidacyReasonRequiresParse]\n//     - Re-classify based on parsed attributes (reading, source), promote to discovered or\n//     transition to graph candidate\n//\n//  3. Graph Phase (if needed)\n//     - Pre-graph: If dependent filters exist, parse all components and build bidirectional\n//     dependency links for reverse traversal\n//     - [PhaseGraph]: Traverse dependencies (target|N) and/or dependents (...target) based on\n//     [GraphExpressionInfo] configuration\n//     - Supports depth limits and target exclusion (^target) for flexible graph queries\n//\n//  4. Relationship Phase (optional)\n//     - [PhaseRelationship]: Build complete dependency graph for execution ordering\n//     - Creates transient components for external dependencies (not in final results)\n//\n//  5. Final Phase\n//     - [PhaseFinal]: Merge all discovered, deduplicate by path, apply final filter evaluation\n//     - Cycle detection and removal if configured via [Discovery.WithBreakCycles]\n//\n// # Filter Expressions\n//\n// The package supports several filter expression types:\n//\n//   - Path expressions: ./foo, ./foo/**, ./**/vpc (glob patterns)\n//   - Attribute expressions: name=vpc, type=unit, external=true, reading=config/*, source=*\n//   - Graph expressions: vpc (target), vpc|2 (dependencies), ...vpc (dependents), ^vpc|... (exclude target)\n//   - Git expressions: [main...develop] (changes between refs)\n//   - Negated expressions: !./internal (exclusion)\n//\n// # Configuration Methods\n//\n// Discovery uses a fluent builder pattern. Available configuration methods include:\n//\n//   - [Discovery.WithFilters]: Set filter queries for component selection\n//   - [Discovery.WithRelationships]: Enable relationship discovery for execution ordering\n//   - [Discovery.WithMaxDependencyDepth]: Set maximum dependency traversal depth (default 1000)\n//   - [Discovery.WithNumWorkers]: Set concurrent worker count (default 4, max 8)\n//   - [Discovery.WithBreakCycles]: Enable cycle detection and removal\n//   - [Discovery.WithNoHidden]: Exclude hidden directories from discovery\n//   - [Discovery.WithRequiresParse]: Force parsing of all Terragrunt configurations\n//   - [Discovery.WithSuppressParseErrors]: Continue discovery despite parse errors\n//   - [Discovery.WithParseExclude]: Parse exclude configurations\n//   - [Discovery.WithParseIncludes]: Parse include configurations\n//   - [Discovery.WithReadFiles]: Parse for file reading information\n//   - [Discovery.WithDiscoveryContext]: Set the discovery context\n//   - [Discovery.WithWorktrees]: Set worktrees for Git-based filters\n//   - [Discovery.WithConfigFilenames]: Set custom config filenames to discover\n//   - [Discovery.WithParserOptions]: Set custom HCL parser options\n//   - [Discovery.WithGitRoot]: Set git root for dependent discovery boundary\n//   - [Discovery.WithGraphTarget]: Set graph target for pruning results\n//   - [Discovery.WithOptions]: Ingest runner options for parser and graph settings\n//\n// # Example Usage\n//\n//\td := NewDiscovery(workingDir).\n//\t\tWithFilters(filters).\n//\t\tWithRelationships().\n//\t\tWithMaxDependencyDepth(10)\n//\n//\tcomponents, err := d.Discover(ctx, logger, opts)\n//\n// # Thread Safety\n//\n// All phase communication uses channels with no shared mutable state between phases.\n// [component.ThreadSafeComponents] provides concurrent component access during graph traversal.\n// A custom stringSet (RWMutex-based) tracks seen components during traversal.\n// [errgroup] with configurable worker limits (default 4, max 8) handles concurrent operations.\npackage discovery\n"
  },
  {
    "path": "internal/discovery/errors.go",
    "content": "package discovery\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\n// GitFilterCommandError represents an error that occurs when attempting to use\n// Git-based filtering with an unsupported command.\ntype GitFilterCommandError struct {\n\tCmd  string\n\tArgs []string\n}\n\nfunc (e GitFilterCommandError) Error() string {\n\tcommand := strings.TrimSpace(\n\t\tstrings.Join(\n\t\t\tappend(\n\t\t\t\t[]string{e.Cmd},\n\t\t\t\te.Args...,\n\t\t\t),\n\t\t\t\" \",\n\t\t),\n\t)\n\n\treturn fmt.Sprintf(\n\t\t\"Git-based filtering is not supported with the command '%s'. \"+\n\t\t\t\"Git-based filtering can only be used with 'plan', 'apply', \"+\n\t\t\t\"or discovery commands (like 'find' or 'list') that don't require additional arguments.\",\n\t\tcommand,\n\t)\n}\n\n// NewGitFilterCommandError creates a new GitFilterCommandError with the given command and arguments.\nfunc NewGitFilterCommandError(cmd string, args []string) error {\n\treturn errors.New(GitFilterCommandError{\n\t\tCmd:  cmd,\n\t\tArgs: args,\n\t})\n}\n\n// MissingDiscoveryContextError represents an error that occurs when a component\n// is missing its discovery context during dependency discovery. This indicates\n// a bug in Terragrunt.\ntype MissingDiscoveryContextError struct {\n\tComponentPath string\n}\n\nfunc (e MissingDiscoveryContextError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"Component at path '%s' is missing its discovery context during dependency discovery. \"+\n\t\t\t\"This is a bug in Terragrunt. \"+\n\t\t\t\"Please open a bug report at https://github.com/gruntwork-io/terragrunt/issues \"+\n\t\t\t\"with details about how you encountered this error.\",\n\t\te.ComponentPath,\n\t)\n}\n\n// NewMissingDiscoveryContextError creates a new MissingDiscoveryContextError for the given component path.\nfunc NewMissingDiscoveryContextError(componentPath string) error {\n\treturn errors.New(MissingDiscoveryContextError{\n\t\tComponentPath: componentPath,\n\t})\n}\n\n// MissingWorkingDirectoryError represents an error that occurs when a component's\n// discovery context is missing its working directory during dependency discovery.\n// This indicates a bug in Terragrunt.\ntype MissingWorkingDirectoryError struct {\n\tComponentPath string\n}\n\nfunc (e MissingWorkingDirectoryError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"Component at path '%s' has a discovery context but is missing its working directory during dependency discovery. \"+\n\t\t\t\"This is a bug in Terragrunt. \"+\n\t\t\t\"Please open a bug report at https://github.com/gruntwork-io/terragrunt/issues \"+\n\t\t\t\"with details about how you encountered this error.\",\n\t\te.ComponentPath,\n\t)\n}\n\n// NewMissingWorkingDirectoryError creates a new MissingWorkingDirectoryError for the given component path.\nfunc NewMissingWorkingDirectoryError(componentPath string) error {\n\treturn errors.New(MissingWorkingDirectoryError{\n\t\tComponentPath: componentPath,\n\t})\n}\n\n// ClassificationError represents an error during component classification.\ntype ClassificationError struct {\n\tComponentPath string\n\tReason        string\n}\n\nfunc (e ClassificationError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"Failed to classify component at '%s': %s\",\n\t\te.ComponentPath, e.Reason,\n\t)\n}\n\n// NewClassificationError creates a new ClassificationError.\nfunc NewClassificationError(componentPath, reason string) error {\n\treturn errors.New(ClassificationError{\n\t\tComponentPath: componentPath,\n\t\tReason:        reason,\n\t})\n}\n\n// CoexistenceError represents an error when a directory contains both\n// a unit configuration file and a stack configuration file.\ntype CoexistenceError struct {\n\tComponentPath   string\n\tUnitConfigFile  string\n\tStackConfigFile string\n}\n\nfunc (e CoexistenceError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"Component %q contains both configuration files %s and %s. \"+\n\t\t\t\"A component must be either a unit or a stack, not both.\",\n\t\te.ComponentPath, e.UnitConfigFile, e.StackConfigFile,\n\t)\n}\n\n// NewCoexistenceError creates a new CoexistenceError.\nfunc NewCoexistenceError(componentPath, unitConfigFile, stackConfigFile string) error {\n\treturn errors.New(CoexistenceError{\n\t\tComponentPath:   componentPath,\n\t\tUnitConfigFile:  unitConfigFile,\n\t\tStackConfigFile: stackConfigFile,\n\t})\n}\n"
  },
  {
    "path": "internal/discovery/filter_test.go",
    "content": "package discovery_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestDiscovery_GraphExpressionFilters tests graph expression filter functionality.\nfunc TestDiscovery_GraphExpressionFilters(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// To speed up this test, make the temporary directory a git repository.\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpDir)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\t// Create dependency graph: vpc -> db -> app\n\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\tdbDir := filepath.Join(tmpDir, \"db\")\n\tappDir := filepath.Join(tmpDir, \"app\")\n\n\ttestDirs := []string{vpcDir, dbDir, appDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Create test files with dependencies\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(appDir, \"terragrunt.hcl\"): `\ndependency \"db\" {\n\tconfig_path = \"../db\"\n}\n`,\n\t\tfilepath.Join(dbDir, \"terragrunt.hcl\"): `\ndependency \"vpc\" {\n\tconfig_path = \"../vpc\"\n}\n`,\n\t\tfilepath.Join(vpcDir, \"terragrunt.hcl\"): ``,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\ttests := []struct {\n\t\tname          string\n\t\tfilterQueries []string\n\t\twantUnits     []string\n\t}{\n\t\t{\n\t\t\tname:          \"dependency discovery - app...\",\n\t\t\tfilterQueries: []string{\"app...\"},\n\t\t\twantUnits:     []string{appDir, dbDir, vpcDir},\n\t\t},\n\t\t{\n\t\t\tname:          \"braced path with dependencies - {./app}...\",\n\t\t\tfilterQueries: []string{\"{./app}...\"},\n\t\t\twantUnits:     []string{appDir, dbDir, vpcDir},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tfilters, err := filter.ParseFilterQueries(l, tt.filterQueries)\n\t\t\trequire.NoError(t, err)\n\n\t\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\t\tWithFilters(filters)\n\n\t\t\tcomponents, err := d.Discover(ctx, l, opts)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tunits := components.Filter(component.UnitKind).Paths()\n\t\t\tassert.ElementsMatch(t, tt.wantUnits, units, \"Units mismatch for test: %s\", tt.name)\n\t\t})\n\t}\n}\n\n// TestDiscovery_GraphExpressionFilters_ComplexGraph tests graph expressions with a more complex graph.\nfunc TestDiscovery_GraphExpressionFilters_ComplexGraph(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// To speed up this test, make the temporary directory a git repository.\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpDir)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\t// Create complex graph: vpc -> [db, cache] -> app\n\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\tdbDir := filepath.Join(tmpDir, \"db\")\n\tcacheDir := filepath.Join(tmpDir, \"cache\")\n\tappDir := filepath.Join(tmpDir, \"app\")\n\n\ttestDirs := []string{vpcDir, dbDir, cacheDir, appDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(appDir, \"terragrunt.hcl\"): `\ndependency \"db\" {\n\tconfig_path = \"../db\"\n}\n\ndependency \"cache\" {\n\tconfig_path = \"../cache\"\n}\n`,\n\t\tfilepath.Join(dbDir, \"terragrunt.hcl\"): `\ndependency \"vpc\" {\n\tconfig_path = \"../vpc\"\n}\n`,\n\t\tfilepath.Join(cacheDir, \"terragrunt.hcl\"): `\ndependency \"vpc\" {\n\tconfig_path = \"../vpc\"\n}\n`,\n\t\tfilepath.Join(vpcDir, \"terragrunt.hcl\"): ``,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\tt.Run(\"dependency traversal from app finds all dependencies\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(l, []string{\"app...\"})\n\t\trequire.NoError(t, err)\n\n\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\tWithFilters(filters)\n\n\t\tconfigs, err := d.Discover(ctx, l, opts)\n\t\trequire.NoError(t, err)\n\n\t\tunits := configs.Filter(component.UnitKind).Paths()\n\t\tassert.ElementsMatch(t, []string{appDir, dbDir, cacheDir, vpcDir}, units)\n\t})\n}\n\n// TestDiscovery_GraphExpressionFilters_OnlyMatchingComponentsTriggerDiscovery tests selective graph discovery.\nfunc TestDiscovery_GraphExpressionFilters_OnlyMatchingComponentsTriggerDiscovery(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create components: app depends on db, but there's also an unrelated component\n\tappDir := filepath.Join(tmpDir, \"app\")\n\tdbDir := filepath.Join(tmpDir, \"db\")\n\tunrelatedDir := filepath.Join(tmpDir, \"unrelated\")\n\n\ttestDirs := []string{appDir, dbDir, unrelatedDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(appDir, \"terragrunt.hcl\"): `\ndependency \"db\" {\n\tconfig_path = \"../db\"\n}\n`,\n\t\tfilepath.Join(dbDir, \"terragrunt.hcl\"):        ``,\n\t\tfilepath.Join(unrelatedDir, \"terragrunt.hcl\"): ``,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\tt.Run(\"graph expression only discovers dependencies of matching component\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Filter for app and its dependencies - unrelated should not be included\n\t\tfilters, err := filter.ParseFilterQueries(l, []string{\"app...\"})\n\t\trequire.NoError(t, err)\n\n\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\tWithFilters(filters)\n\n\t\tconfigs, err := d.Discover(ctx, l, opts)\n\t\trequire.NoError(t, err)\n\n\t\tunits := configs.Filter(component.UnitKind).Paths()\n\t\t// Should include app and db, but NOT unrelated\n\t\tassert.ElementsMatch(t, []string{appDir, dbDir}, units)\n\t\tassert.NotContains(t, units, unrelatedDir)\n\t})\n}\n\n// TestDiscovery_GraphExpressionFilters_FiltersAppliedAfterDiscovery tests additional filters after graph discovery.\nfunc TestDiscovery_GraphExpressionFilters_FiltersAppliedAfterDiscovery(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create dependency graph: vpc -> db -> app\n\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\tdbDir := filepath.Join(tmpDir, \"db\")\n\tappDir := filepath.Join(tmpDir, \"app\")\n\n\ttestDirs := []string{vpcDir, dbDir, appDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(appDir, \"terragrunt.hcl\"): `\ndependency \"db\" {\n\tconfig_path = \"../db\"\n}\n`,\n\t\tfilepath.Join(dbDir, \"terragrunt.hcl\"): `\ndependency \"vpc\" {\n\tconfig_path = \"../vpc\"\n}\n`,\n\t\tfilepath.Join(vpcDir, \"terragrunt.hcl\"): ``,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\tt.Run(\"additional filters applied after graph discovery\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Graph expression discovers app and its dependencies, then additional filter excludes vpc\n\t\tfilters, err := filter.ParseFilterQueries(l, []string{\"app...\", \"!vpc\"})\n\t\trequire.NoError(t, err)\n\n\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\tWithFilters(filters)\n\n\t\tconfigs, err := d.Discover(ctx, l, opts)\n\t\trequire.NoError(t, err)\n\n\t\tunits := configs.Filter(component.UnitKind).Paths()\n\t\t// Should include app and db (from graph), but exclude vpc (from filter)\n\t\tassert.ElementsMatch(t, []string{appDir, dbDir}, units)\n\t\tassert.NotContains(t, units, vpcDir)\n\t})\n}\n\n// TestDiscovery_ReadingAttributeFilters tests reading attribute filter functionality.\nfunc TestDiscovery_ReadingAttributeFilters(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create shared configuration files\n\tsharedHCL := filepath.Join(tmpDir, \"shared.hcl\")\n\tsharedTFVars := filepath.Join(tmpDir, \"shared.tfvars\")\n\tcommonVars := filepath.Join(tmpDir, \"common\", \"variables.hcl\")\n\tdbConfig := filepath.Join(tmpDir, \"database.yaml\")\n\n\trequire.NoError(t, os.MkdirAll(filepath.Join(tmpDir, \"common\"), 0755))\n\n\trequire.NoError(t, os.WriteFile(sharedHCL, []byte(`\nlocals {\n\tcommon_value = \"test\"\n}\n`), 0644))\n\n\trequire.NoError(t, os.WriteFile(sharedTFVars, []byte(`\ntest_var = \"value\"\nanother_var = \"test\"\n`), 0644))\n\n\trequire.NoError(t, os.WriteFile(commonVars, []byte(`\nlocals {\n\tvpc_cidr = \"10.0.0.0/16\"\n}\n`), 0644))\n\n\trequire.NoError(t, os.WriteFile(dbConfig, []byte(`\nlocals {\n\tdb_host = \"localhost\"\n\tdb_port = 5432\n}\n`), 0644))\n\n\t// Create test components with different file reads\n\tfrontendDir := filepath.Join(tmpDir, \"apps\", \"frontend\")\n\tbackendDir := filepath.Join(tmpDir, \"apps\", \"backend\")\n\tlegacyDir := filepath.Join(tmpDir, \"apps\", \"legacy\")\n\tdbDir := filepath.Join(tmpDir, \"libs\", \"db\")\n\tcacheDir := filepath.Join(tmpDir, \"libs\", \"cache\")\n\n\ttestDirs := []string{frontendDir, backendDir, legacyDir, dbDir, cacheDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Create test files with different file reading patterns\n\t// Note: Only read_terragrunt_config and read_tfvars_file populate the Reading slice\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(frontendDir, \"terragrunt.hcl\"): `\nlocals {\n\tshared = read_terragrunt_config(\"../../shared.hcl\")\n\tvars = read_tfvars_file(\"../../shared.tfvars\")\n}\n`,\n\t\tfilepath.Join(backendDir, \"terragrunt.hcl\"): `\nlocals {\n\tshared = read_terragrunt_config(\"../../shared.hcl\")\n\tcommon = read_terragrunt_config(\"../../common/variables.hcl\")\n}\n`,\n\t\tfilepath.Join(legacyDir, \"terragrunt.hcl\"): `\nlocals {\n\t# Uses a file that will be tracked\n\tdb_config = read_terragrunt_config(\"../../database.yaml\")\n}\n`,\n\t\tfilepath.Join(dbDir, \"terragrunt.hcl\"): `\nlocals {\n\tcommon = read_terragrunt_config(\"../../common/variables.hcl\")\n\tdb_config = read_terragrunt_config(\"../../database.yaml\")\n}\n`,\n\t\tfilepath.Join(cacheDir, \"terragrunt.hcl\"): `\n# No file reads\n`,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\ttests := []struct {\n\t\tname          string\n\t\tfilterQueries []string\n\t\twantUnits     []string\n\t}{\n\t\t{\n\t\t\tname:          \"filter by exact file - shared.hcl\",\n\t\t\tfilterQueries: []string{\"reading=shared.hcl\"},\n\t\t\twantUnits:     []string{frontendDir, backendDir},\n\t\t},\n\t\t{\n\t\t\tname:          \"filter by exact file - database.yaml\",\n\t\t\tfilterQueries: []string{\"reading=database.yaml\"},\n\t\t\twantUnits:     []string{legacyDir, dbDir},\n\t\t},\n\t\t{\n\t\t\tname:          \"filter by glob - shared prefix\",\n\t\t\tfilterQueries: []string{\"reading=shared*\"},\n\t\t\twantUnits:     []string{frontendDir, backendDir},\n\t\t},\n\t\t{\n\t\t\tname:          \"filter by exact nested path\",\n\t\t\tfilterQueries: []string{\"reading=common/variables.hcl\"},\n\t\t\twantUnits:     []string{backendDir, dbDir},\n\t\t},\n\t\t{\n\t\t\tname:          \"negation - exclude components reading shared.hcl\",\n\t\t\tfilterQueries: []string{\"!reading=shared.hcl\"},\n\t\t\twantUnits:     []string{legacyDir, dbDir, cacheDir},\n\t\t},\n\t\t{\n\t\t\tname:          \"negation with glob - exclude components reading database.yaml\",\n\t\t\tfilterQueries: []string{\"!reading=database.yaml\"},\n\t\t\twantUnits:     []string{frontendDir, backendDir, cacheDir},\n\t\t},\n\t\t{\n\t\t\tname:          \"intersection - apps directory reading shared.hcl\",\n\t\t\tfilterQueries: []string{\"./apps/* | reading=shared.hcl\"},\n\t\t\twantUnits:     []string{frontendDir, backendDir},\n\t\t},\n\t\t{\n\t\t\tname:          \"intersection - libs directory with common variables\",\n\t\t\tfilterQueries: []string{\"./libs/* | reading=common/variables.hcl\"},\n\t\t\twantUnits:     []string{dbDir},\n\t\t},\n\t\t{\n\t\t\tname:          \"multiple filters - union semantics\",\n\t\t\tfilterQueries: []string{\"reading=shared.hcl\", \"reading=database.yaml\"},\n\t\t\twantUnits:     []string{frontendDir, backendDir, legacyDir, dbDir},\n\t\t},\n\t\t{\n\t\t\tname:          \"complex - apps not reading database.yaml\",\n\t\t\tfilterQueries: []string{\"./apps/* | !reading=database.yaml\"},\n\t\t\twantUnits:     []string{frontendDir, backendDir},\n\t\t},\n\t\t{\n\t\t\tname:          \"no matches - nonexistent file\",\n\t\t\tfilterQueries: []string{\"reading=nonexistent.hcl\"},\n\t\t\twantUnits:     []string{},\n\t\t},\n\t\t{\n\t\t\tname:          \"components that don't read any files\",\n\t\t\tfilterQueries: []string{\"cache\"},\n\t\t\twantUnits:     []string{cacheDir},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tfilters, err := filter.ParseFilterQueries(l, tt.filterQueries)\n\t\t\trequire.NoError(t, err)\n\n\t\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\t\tWithFilters(filters).\n\t\t\t\tWithReadFiles()\n\n\t\t\tconfigs, err := d.Discover(ctx, l, opts)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tunits := configs.Filter(component.UnitKind).Paths()\n\t\t\tassert.ElementsMatch(t, tt.wantUnits, units, \"Units mismatch for test: %s\", tt.name)\n\t\t})\n\t}\n}\n\n// TestDiscovery_ReadingAttributeFiltersAbsolutePaths tests reading attribute filter with absolute paths.\nfunc TestDiscovery_ReadingAttributeFiltersAbsolutePaths(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create a shared file with absolute path\n\tsharedFile := filepath.Join(tmpDir, \"shared.hcl\")\n\trequire.NoError(t, os.WriteFile(sharedFile, []byte(`\nlocals {\n\tvalue = \"test\"\n}\n`), 0644))\n\n\t// Create test component\n\tappDir := filepath.Join(tmpDir, \"app\")\n\trequire.NoError(t, os.MkdirAll(appDir, 0755))\n\n\tterragruntConfig := filepath.Join(appDir, \"terragrunt.hcl\")\n\trequire.NoError(t, os.WriteFile(terragruntConfig, []byte(`\nlocals {\n\tshared = read_terragrunt_config(\"../shared.hcl\")\n}\n`), 0644))\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Test with absolute path filter\n\tfilterQueries := []string{\"reading=\" + sharedFile}\n\tfilters, err := filter.ParseFilterQueries(l, filterQueries)\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithFilters(filters).\n\t\tWithReadFiles()\n\n\tconfigs, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// Should find the app component when filtering by absolute path\n\tunits := configs.Filter(component.UnitKind).Paths()\n\tassert.ElementsMatch(t, []string{appDir}, units, \"Should find component by absolute path to read file\")\n}\n\n// TestDiscovery_ReadingAttributeFiltersErrorHandling tests error handling for invalid reading filters.\nfunc TestDiscovery_ReadingAttributeFiltersErrorHandling(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tappDir := filepath.Join(tmpDir, \"app\")\n\trequire.NoError(t, os.MkdirAll(appDir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(appDir, \"terragrunt.hcl\"), []byte(\"\"), 0644))\n\n\ttests := []struct {\n\t\tname                 string\n\t\tfilterQueries        []string\n\t\terrorExpectedOnParse bool\n\t}{\n\t\t{\n\t\t\tname:                 \"invalid glob pattern in reading filter\",\n\t\t\tfilterQueries:        []string{\"reading=[invalid\"},\n\t\t\terrorExpectedOnParse: true,\n\t\t},\n\t\t{\n\t\t\tname:                 \"valid reading filter - no error\",\n\t\t\tfilterQueries:        []string{\"reading=*.hcl\"},\n\t\t\terrorExpectedOnParse: false,\n\t\t},\n\t}\n\n\tl := logger.CreateLogger()\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t// Parse filter queries\n\t\t\t_, err := filter.ParseFilterQueries(l, tt.filterQueries)\n\t\t\tif tt.errorExpectedOnParse {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter: %v\", tt.filterQueries)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestDiscovery_AttributeFilters tests path, name, type, and external attribute filters.\nfunc TestDiscovery_AttributeFilters(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create test directory structure\n\tappsDir := filepath.Join(tmpDir, \"apps\")\n\tfrontendDir := filepath.Join(appsDir, \"frontend\")\n\tbackendDir := filepath.Join(appsDir, \"backend\")\n\tlegacyDir := filepath.Join(appsDir, \"legacy\")\n\n\tlibsDir := filepath.Join(tmpDir, \"libs\")\n\tdbDir := filepath.Join(libsDir, \"db\")\n\tcacheDir := filepath.Join(libsDir, \"cache\")\n\n\tstackDir := filepath.Join(tmpDir, \"stack\")\n\n\ttestDirs := []string{\n\t\tfrontendDir,\n\t\tbackendDir,\n\t\tlegacyDir,\n\t\tdbDir,\n\t\tcacheDir,\n\t\tstackDir,\n\t}\n\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(frontendDir, \"terragrunt.hcl\"): ``,\n\t\tfilepath.Join(backendDir, \"terragrunt.hcl\"):  ``,\n\t\tfilepath.Join(legacyDir, \"terragrunt.hcl\"):   ``,\n\t\tfilepath.Join(dbDir, \"terragrunt.hcl\"):       ``,\n\t\tfilepath.Join(cacheDir, \"terragrunt.hcl\"):    ``,\n\t\tfilepath.Join(stackDir, \"terragrunt.stack.hcl\"): `\nunit \"test\" {\n  source = \".\"\n  path   = \"test\"\n}\n`,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\ttests := []struct {\n\t\tname          string\n\t\tfilterQueries []string\n\t\twantUnits     []string\n\t\twantStacks    []string\n\t}{\n\t\t{\n\t\t\tname:          \"path filter - apps directory\",\n\t\t\tfilterQueries: []string{\"./apps/*\"},\n\t\t\twantUnits:     []string{frontendDir, backendDir, legacyDir},\n\t\t\twantStacks:    []string{},\n\t\t},\n\t\t{\n\t\t\tname:          \"path filter with wildcard\",\n\t\t\tfilterQueries: []string{\"./libs/*\"},\n\t\t\twantUnits:     []string{dbDir, cacheDir},\n\t\t\twantStacks:    []string{},\n\t\t},\n\t\t{\n\t\t\tname:          \"name filter - specific component\",\n\t\t\tfilterQueries: []string{\"frontend\"},\n\t\t\twantUnits:     []string{frontendDir},\n\t\t\twantStacks:    []string{},\n\t\t},\n\t\t{\n\t\t\tname:          \"name filter with equals\",\n\t\t\tfilterQueries: []string{\"name=backend\"},\n\t\t\twantUnits:     []string{backendDir},\n\t\t\twantStacks:    []string{},\n\t\t},\n\t\t{\n\t\t\tname:          \"type filter - units only\",\n\t\t\tfilterQueries: []string{\"type=unit\"},\n\t\t\twantUnits:     []string{frontendDir, backendDir, legacyDir, dbDir, cacheDir},\n\t\t\twantStacks:    []string{},\n\t\t},\n\t\t{\n\t\t\tname:          \"type filter - stacks only\",\n\t\t\tfilterQueries: []string{\"type=stack\"},\n\t\t\twantUnits:     []string{},\n\t\t\twantStacks:    []string{stackDir},\n\t\t},\n\t\t{\n\t\t\tname:          \"negation filter - exclude legacy\",\n\t\t\tfilterQueries: []string{\"!legacy\"},\n\t\t\twantUnits:     []string{frontendDir, backendDir, dbDir, cacheDir},\n\t\t\twantStacks:    []string{stackDir},\n\t\t},\n\t\t{\n\t\t\tname:          \"negation filter - exclude apps directory\",\n\t\t\tfilterQueries: []string{\"!./apps/*\"},\n\t\t\twantUnits:     []string{dbDir, cacheDir},\n\t\t\twantStacks:    []string{stackDir},\n\t\t},\n\t\t{\n\t\t\tname:          \"intersection filter - apps and not legacy\",\n\t\t\tfilterQueries: []string{\"./apps/* | !legacy\"},\n\t\t\twantUnits:     []string{frontendDir, backendDir},\n\t\t\twantStacks:    []string{},\n\t\t},\n\t\t{\n\t\t\tname:          \"multiple filters - union semantics\",\n\t\t\tfilterQueries: []string{\"./apps/frontend\", \"./libs/db\"},\n\t\t\twantUnits:     []string{frontendDir, dbDir},\n\t\t\twantStacks:    []string{},\n\t\t},\n\t\t{\n\t\t\tname:          \"braced path filter\",\n\t\t\tfilterQueries: []string{\"{./apps/*}\"},\n\t\t\twantUnits:     []string{frontendDir, backendDir, legacyDir},\n\t\t\twantStacks:    []string{},\n\t\t},\n\t\t{\n\t\t\tname:          \"absolute path filter\",\n\t\t\tfilterQueries: []string{stackDir},\n\t\t\twantUnits:     []string{},\n\t\t\twantStacks:    []string{stackDir},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tfilters, err := filter.ParseFilterQueries(l, tt.filterQueries)\n\t\t\trequire.NoError(t, err)\n\n\t\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\t\tWithFilters(filters)\n\n\t\t\tconfigs, err := d.Discover(ctx, l, opts)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tunits := configs.Filter(component.UnitKind).Paths()\n\t\t\tstacks := configs.Filter(component.StackKind).Paths()\n\n\t\t\tassert.ElementsMatch(t, tt.wantUnits, units, \"Units mismatch for test: %s\", tt.name)\n\t\t\tassert.ElementsMatch(t, tt.wantStacks, stacks, \"Stacks mismatch for test: %s\", tt.name)\n\t\t})\n\t}\n}\n\n// TestDiscovery_FilterEdgeCases tests edge cases in filter handling.\nfunc TestDiscovery_FilterEdgeCases(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create a single component for edge case testing\n\tunitDir := filepath.Join(tmpDir, \"unit #1\")\n\terr := os.MkdirAll(unitDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(unitDir, \"terragrunt.hcl\"), []byte(\"\"), 0644)\n\trequire.NoError(t, err)\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\ttests := []struct {\n\t\tname       string\n\t\tfilters    []string\n\t\twantUnits  []string\n\t\twantStacks []string\n\t}{\n\t\t{\n\t\t\tname:       \"filter with spaces in path\",\n\t\t\tfilters:    []string{\"{unit #1}\"},\n\t\t\twantUnits:  []string{unitDir},\n\t\t\twantStacks: []string{},\n\t\t},\n\t\t{\n\t\t\tname:       \"filter with spaces in name\",\n\t\t\tfilters:    []string{\"unit #1\"},\n\t\t\twantUnits:  []string{unitDir},\n\t\t\twantStacks: []string{},\n\t\t},\n\t\t{\n\t\t\tname:       \"non-matching filter\",\n\t\t\tfilters:    []string{\"nonexistent\"},\n\t\t\twantUnits:  []string{},\n\t\t\twantStacks: []string{},\n\t\t},\n\t\t{\n\t\t\tname:       \"non-matching path filter\",\n\t\t\tfilters:    []string{\"./nonexistent/*\"},\n\t\t\twantUnits:  []string{},\n\t\t\twantStacks: []string{},\n\t\t},\n\t\t{\n\t\t\tname:       \"negation of non-matching filter\",\n\t\t\tfilters:    []string{\"!nonexistent\"},\n\t\t\twantUnits:  []string{unitDir},\n\t\t\twantStacks: []string{},\n\t\t},\n\t\t{\n\t\t\tname:       \"empty intersection\",\n\t\t\tfilters:    []string{\"unit #1 | nonexistent\"},\n\t\t\twantUnits:  []string{},\n\t\t\twantStacks: []string{},\n\t\t},\n\t\t{\n\t\t\tname:       \"double negation\",\n\t\t\tfilters:    []string{\"!!unit #1\"},\n\t\t\twantUnits:  []string{unitDir},\n\t\t\twantStacks: []string{},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tfilters, err := filter.ParseFilterQueries(l, tt.filters)\n\t\t\trequire.NoError(t, err)\n\n\t\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\t\tWithFilters(filters)\n\n\t\t\tconfigs, err := d.Discover(ctx, l, opts)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tunits := configs.Filter(component.UnitKind).Paths()\n\t\t\tstacks := configs.Filter(component.StackKind).Paths()\n\n\t\t\tassert.ElementsMatch(t, tt.wantUnits, units, \"Units mismatch for test: %s\", tt.name)\n\t\t\tassert.ElementsMatch(t, tt.wantStacks, stacks, \"Stacks mismatch for test: %s\", tt.name)\n\t\t})\n\t}\n}\n\n// TestDiscovery_FilterErrorHandling tests error handling for invalid filters.\nfunc TestDiscovery_FilterErrorHandling(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tappDir := filepath.Join(tmpDir, \"app\")\n\trequire.NoError(t, os.MkdirAll(appDir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(appDir, \"terragrunt.hcl\"), []byte(\"\"), 0644))\n\n\ttests := []struct {\n\t\tname          string\n\t\tfilterQueries []string\n\t\terrorExpected bool\n\t}{\n\t\t{\n\t\t\tname:          \"invalid filter syntax\",\n\t\t\tfilterQueries: []string{\"invalid[filter\"},\n\t\t\terrorExpected: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"empty filter query\",\n\t\t\tfilterQueries: []string{\"\"},\n\t\t\terrorExpected: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"malformed glob pattern\",\n\t\t\tfilterQueries: []string{\"./apps/[\"},\n\t\t\terrorExpected: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"invalid attribute key\",\n\t\t\tfilterQueries: []string{\"invalid=value\"},\n\t\t\terrorExpected: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"invalid type value\",\n\t\t\tfilterQueries: []string{\"type=invalid\"},\n\t\t\terrorExpected: true,\n\t\t},\n\t\t{\n\t\t\tname:          \"invalid external value\",\n\t\t\tfilterQueries: []string{\"external=maybe\"},\n\t\t\terrorExpected: true,\n\t\t},\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t// Parse filter queries\n\t\t\tfilters, err := filter.ParseFilterQueries(l, tt.filterQueries)\n\n\t\t\t// Some errors occur during parsing (like empty filter), others during evaluation\n\t\t\tif tt.errorExpected && err != nil {\n\t\t\t\t// Error occurred during parsing - this is expected for some test cases\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err) // Parsing should succeed for evaluation error test cases\n\n\t\t\t// Create discovery with filters\n\t\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\t\tWithFilters(filters)\n\n\t\t\t// Attempt discovery - errors should occur during evaluation\n\t\t\t_, err = d.Discover(ctx, l, opts)\n\t\t\tif tt.errorExpected {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter: %v\", tt.filterQueries)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestDiscovery_ExternalAttributeFilter tests external attribute filtering.\nfunc TestDiscovery_ExternalAttributeFilter(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create external component outside the working directory to make it truly external\n\tinternalDir := filepath.Join(tmpDir, \"internal\")\n\texternalDir := filepath.Join(tmpDir, \"external\")\n\n\tappDir := filepath.Join(internalDir, \"app\")\n\texternalAppDir := filepath.Join(externalDir, \"app\")\n\tdbDir := filepath.Join(internalDir, \"db\")\n\n\ttestDirs := []string{appDir, externalAppDir, dbDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(appDir, \"terragrunt.hcl\"): `\ndependency \"db\" {\n\tconfig_path = \"../db\"\n}\n\ndependency \"external\" {\n\tconfig_path = \"../../external/app\"\n}\n`,\n\t\tfilepath.Join(dbDir, \"terragrunt.hcl\"):          ``,\n\t\tfilepath.Join(externalAppDir, \"terragrunt.hcl\"): ``,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     internalDir,\n\t\tRootWorkingDir: internalDir,\n\t}\n\n\tctx := t.Context()\n\n\tt.Run(\"external=true filter\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(l, []string{\"{./**}... | external=true\"})\n\t\trequire.NoError(t, err)\n\n\t\td := discovery.NewDiscovery(internalDir).\n\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: internalDir}).\n\t\t\tWithFilters(filters)\n\n\t\tcomponents, err := d.Discover(ctx, l, opts)\n\t\trequire.NoError(t, err)\n\n\t\tunits := components.Filter(component.UnitKind).Paths()\n\t\tassert.ElementsMatch(t, []string{externalAppDir}, units)\n\t})\n\n\tt.Run(\"external=false filter\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(l, []string{\"{./**}... | external=false\"})\n\t\trequire.NoError(t, err)\n\n\t\td := discovery.NewDiscovery(internalDir).\n\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: internalDir}).\n\t\t\tWithFilters(filters)\n\n\t\tcomponents, err := d.Discover(ctx, l, opts)\n\t\trequire.NoError(t, err)\n\n\t\tunits := components.Filter(component.UnitKind).Paths()\n\t\tassert.ElementsMatch(t, []string{appDir, dbDir}, units)\n\t})\n}\n\n// TestDiscovery_DependentDiscovery_Standalone tests standalone dependent discovery (...vpc).\n// This verifies that ...vpc finds all units that depend on vpc.\nfunc TestDiscovery_DependentDiscovery_Standalone(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// To speed up this test, make the temporary directory a git repository.\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpDir)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\t// Create dependency graph: app -> db -> vpc\n\t// Dependents of vpc: db, app (db depends on vpc, app depends on db which depends on vpc)\n\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\tdbDir := filepath.Join(tmpDir, \"db\")\n\tappDir := filepath.Join(tmpDir, \"app\")\n\n\ttestDirs := []string{vpcDir, dbDir, appDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(appDir, \"terragrunt.hcl\"): `\ndependency \"db\" {\n\tconfig_path = \"../db\"\n}\n`,\n\t\tfilepath.Join(dbDir, \"terragrunt.hcl\"): `\ndependency \"vpc\" {\n\tconfig_path = \"../vpc\"\n}\n`,\n\t\tfilepath.Join(vpcDir, \"terragrunt.hcl\"): ``,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Use ...vpc to find all dependents of vpc\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"...vpc\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// Should include vpc (target) and db (direct dependent) and app (transitive dependent)\n\tunits := components.Filter(component.UnitKind).Paths()\n\tassert.ElementsMatch(t, []string{vpcDir, dbDir, appDir}, units, \"...vpc should find vpc and all its dependents (db, app)\")\n}\n\n// TestDiscovery_DependentDiscovery_ExcludeTarget tests dependent discovery with target exclusion (^...vpc).\nfunc TestDiscovery_DependentDiscovery_ExcludeTarget(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// To speed up this test, make the temporary directory a git repository.\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpDir)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\t// Create dependency graph: app -> vpc\n\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\tappDir := filepath.Join(tmpDir, \"app\")\n\n\ttestDirs := []string{vpcDir, appDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(appDir, \"terragrunt.hcl\"): `\ndependency \"vpc\" {\n\tconfig_path = \"../vpc\"\n}\n`,\n\t\tfilepath.Join(vpcDir, \"terragrunt.hcl\"): ``,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Use ...^vpc to find dependents but exclude the target (vpc)\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"...^vpc\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// Should include only app (dependent), not vpc (target is excluded)\n\tunits := components.Filter(component.UnitKind).Paths()\n\tassert.ElementsMatch(t, []string{appDir}, units, \"...^vpc should find only dependents, not the target\")\n\tassert.NotContains(t, units, vpcDir, \"vpc should be excluded as the target\")\n}\n\n// TestDiscovery_DependencyDiscovery_ExcludeTarget tests dependency discovery with target exclusion (^app...).\n// This is the inverse of TestDiscovery_DependentDiscovery_ExcludeTarget - it tests excluding the target\n// from the dependency direction rather than the dependent direction.\nfunc TestDiscovery_DependencyDiscovery_ExcludeTarget(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// To speed up this test, make the temporary directory a git repository.\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpDir)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\t// Create dependency graph: app -> db -> vpc\n\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\tdbDir := filepath.Join(tmpDir, \"db\")\n\tappDir := filepath.Join(tmpDir, \"app\")\n\n\ttestDirs := []string{vpcDir, dbDir, appDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(appDir, \"terragrunt.hcl\"): `\ndependency \"db\" {\n\tconfig_path = \"../db\"\n}\n`,\n\t\tfilepath.Join(dbDir, \"terragrunt.hcl\"): `\ndependency \"vpc\" {\n\tconfig_path = \"../vpc\"\n}\n`,\n\t\tfilepath.Join(vpcDir, \"terragrunt.hcl\"): ``,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Use ^app... to find dependencies but exclude the target (app)\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"^app...\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// Should include only db and vpc (dependencies), not app (target is excluded)\n\tunits := components.Filter(component.UnitKind).Paths()\n\tassert.ElementsMatch(t, []string{dbDir, vpcDir}, units, \"^app... should find only dependencies, not the target\")\n\tassert.NotContains(t, units, appDir, \"app should be excluded as the target\")\n}\n\n// TestDiscovery_DependentDiscovery_Bidirectional tests bidirectional discovery (...db...).\nfunc TestDiscovery_DependentDiscovery_Bidirectional(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// To speed up this test, make the temporary directory a git repository.\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpDir)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\t// Create dependency graph: app -> db -> vpc\n\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\tdbDir := filepath.Join(tmpDir, \"db\")\n\tappDir := filepath.Join(tmpDir, \"app\")\n\n\ttestDirs := []string{vpcDir, dbDir, appDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(appDir, \"terragrunt.hcl\"): `\ndependency \"db\" {\n\tconfig_path = \"../db\"\n}\n`,\n\t\tfilepath.Join(dbDir, \"terragrunt.hcl\"): `\ndependency \"vpc\" {\n\tconfig_path = \"../vpc\"\n}\n`,\n\t\tfilepath.Join(vpcDir, \"terragrunt.hcl\"): ``,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Use ...db... to find both dependencies and dependents of db\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"...db...\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// Should include: app (dependent), db (target), vpc (dependency)\n\tunits := components.Filter(component.UnitKind).Paths()\n\tassert.ElementsMatch(t, []string{appDir, dbDir, vpcDir}, units, \"...db... should find dependents, target, and dependencies\")\n}\n\n// TestDiscovery_DependentDiscovery_OutsideWorkingDir tests that dependent discovery\n// finds dependents outside the working directory but within the git root.\n// This validates the upward filesystem walking feature.\nfunc TestDiscovery_DependentDiscovery_OutsideWorkingDir(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Initialize git repository at the root\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpDir)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\t// Create structure:\n\t// /repo (git root)\n\t// ├── app/\n\t// │   └── vpc/           <- target\n\t// │       └── terragrunt.hcl\n\t// └── other-app/\n\t//     └── consumer/      <- depends on vpc (outside working dir)\n\t//         └── terragrunt.hcl\n\tappDir := filepath.Join(tmpDir, \"app\")\n\tvpcDir := filepath.Join(appDir, \"vpc\")\n\totherAppDir := filepath.Join(tmpDir, \"other-app\")\n\tconsumerDir := filepath.Join(otherAppDir, \"consumer\")\n\n\ttestDirs := []string{vpcDir, consumerDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(vpcDir, \"terragrunt.hcl\"): ``,\n\t\tfilepath.Join(consumerDir, \"terragrunt.hcl\"): `\ndependency \"vpc\" {\n\tconfig_path = \"../../app/vpc\"\n}\n`,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\n\t// Set working directory to app/ subdirectory, NOT the git root\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     appDir,\n\t\tRootWorkingDir: appDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Use ...vpc to find dependents of vpc\n\t// consumer is outside the working directory (app/) but should be found\n\t// via upward filesystem walking bounded by git root\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"...vpc\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(appDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: appDir}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// Should include vpc (target) and consumer (dependent outside working dir)\n\tunits := components.Filter(component.UnitKind).Paths()\n\tassert.Contains(t, units, vpcDir, \"vpc should be discovered as the target\")\n\tassert.Contains(t, units, consumerDir, \"consumer should be discovered even though it's outside working dir\")\n\tassert.ElementsMatch(t, []string{vpcDir, consumerDir}, units, \"...vpc should find vpc and consumer (outside working dir)\")\n}\n\n// TestDiscovery_DependentDiscovery_OutsideWorkingDir_MultipleLevels tests that dependent discovery\n// finds dependents at multiple levels outside the working directory.\nfunc TestDiscovery_DependentDiscovery_OutsideWorkingDir_MultipleLevels(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Initialize git repository at the root\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpDir)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\t// Create structure:\n\t// /repo (git root)\n\t// ├── infra/\n\t// │   └── vpc/              <- target\n\t// │       └── terragrunt.hcl\n\t// ├── services/\n\t// │   └── api/              <- depends on vpc (sibling directory)\n\t// │       └── terragrunt.hcl\n\t// └── apps/\n\t//     └── frontend/         <- depends on api (transitive dependent of vpc)\n\t//         └── terragrunt.hcl\n\tinfraDir := filepath.Join(tmpDir, \"infra\")\n\tvpcDir := filepath.Join(infraDir, \"vpc\")\n\tservicesDir := filepath.Join(tmpDir, \"services\")\n\tapiDir := filepath.Join(servicesDir, \"api\")\n\tappsDir := filepath.Join(tmpDir, \"apps\")\n\tfrontendDir := filepath.Join(appsDir, \"frontend\")\n\n\ttestDirs := []string{vpcDir, apiDir, frontendDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(vpcDir, \"terragrunt.hcl\"): ``,\n\t\tfilepath.Join(apiDir, \"terragrunt.hcl\"): `\ndependency \"vpc\" {\n\tconfig_path = \"../../infra/vpc\"\n}\n`,\n\t\tfilepath.Join(frontendDir, \"terragrunt.hcl\"): `\ndependency \"api\" {\n\tconfig_path = \"../../services/api\"\n}\n`,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\n\t// Set working directory to infra/ subdirectory\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     infraDir,\n\t\tRootWorkingDir: infraDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Use ...vpc to find dependents of vpc\n\t// api and frontend are both outside the working directory (infra/)\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"...vpc\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(infraDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: infraDir}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// Should include vpc (target), api (direct dependent), and frontend (transitive dependent)\n\tunits := components.Filter(component.UnitKind).Paths()\n\tassert.Contains(t, units, vpcDir, \"vpc should be discovered as the target\")\n\tassert.Contains(t, units, apiDir, \"api should be discovered (direct dependent outside working dir)\")\n\tassert.Contains(t, units, frontendDir, \"frontend should be discovered (transitive dependent outside working dir)\")\n\tassert.ElementsMatch(t, []string{vpcDir, apiDir, frontendDir}, units)\n}\n\n// TestDiscovery_DependentDiscovery_DirectDependentOnly tests that dependent discovery\n// finds direct dependents correctly.\nfunc TestDiscovery_DependentDiscovery_DirectDependentOnly(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// To speed up this test, make the temporary directory a git repository.\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpDir)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\t// Create dependency graph: api -> db, web -> db\n\t// Both api and web directly depend on db\n\tdbDir := filepath.Join(tmpDir, \"db\")\n\tapiDir := filepath.Join(tmpDir, \"api\")\n\twebDir := filepath.Join(tmpDir, \"web\")\n\tunrelatedDir := filepath.Join(tmpDir, \"unrelated\")\n\n\ttestDirs := []string{dbDir, apiDir, webDir, unrelatedDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(apiDir, \"terragrunt.hcl\"): `\ndependency \"db\" {\n\tconfig_path = \"../db\"\n}\n`,\n\t\tfilepath.Join(webDir, \"terragrunt.hcl\"): `\ndependency \"db\" {\n\tconfig_path = \"../db\"\n}\n`,\n\t\tfilepath.Join(dbDir, \"terragrunt.hcl\"):        ``,\n\t\tfilepath.Join(unrelatedDir, \"terragrunt.hcl\"): ``,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Use ...db to find all dependents of db\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"...db\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// Should include db (target), api (dependent), web (dependent)\n\t// Should NOT include unrelated\n\tunits := components.Filter(component.UnitKind).Paths()\n\tassert.ElementsMatch(t, []string{dbDir, apiDir, webDir}, units, \"...db should find db and all its dependents\")\n\tassert.NotContains(t, units, unrelatedDir, \"unrelated should not be included\")\n}\n\n// TestDiscovery_NegatedGraphFilters tests that negated graph expressions correctly\n// trigger the graph discovery phase and produce the expected results.\n//\n// Example with dependency chain: app -> db -> vpc (app depends on db, db depends on vpc)\n//   - `!...db` should exclude db and everything that depends on db (db and app)\n//   - `!db...` should exclude db and everything db depends on (db and vpc)\nfunc TestDiscovery_NegatedGraphFilters(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname          string\n\t\tfilters       []string\n\t\texpectedPaths []string\n\t\texcludedPaths []string\n\t}{\n\t\t{\n\t\t\tname:          \"negated dependent filter excludes target and dependents\",\n\t\t\tfilters:       []string{\"!...db\"},\n\t\t\texpectedPaths: []string{\"vpc\"},\n\t\t\texcludedPaths: []string{\"db\", \"app\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"negated dependency filter excludes target and dependencies\",\n\t\t\tfilters:       []string{\"!db...\"},\n\t\t\texpectedPaths: []string{\"app\"},\n\t\t\texcludedPaths: []string{\"db\", \"vpc\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"positive filter with negated graph filter\",\n\t\t\tfilters:       []string{\"app...\", \"!vpc\"},\n\t\t\texpectedPaths: []string{\"app\", \"db\"},\n\t\t\texcludedPaths: []string{\"vpc\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"negated bidirectional filter\",\n\t\t\tfilters:       []string{\"!...db...\"},\n\t\t\texpectedPaths: []string{},\n\t\t\texcludedPaths: []string{\"app\", \"db\", \"vpc\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\trunner, err := git.NewGitRunner()\n\t\t\trequire.NoError(t, err)\n\n\t\t\trunner = runner.WithWorkDir(tmpDir)\n\n\t\t\terr = runner.Init(t.Context())\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\t\t\tdbDir := filepath.Join(tmpDir, \"db\")\n\t\t\tappDir := filepath.Join(tmpDir, \"app\")\n\n\t\t\ttestDirs := []string{vpcDir, dbDir, appDir}\n\t\t\tfor _, dir := range testDirs {\n\t\t\t\terr := os.MkdirAll(dir, 0755)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\ttestFiles := map[string]string{\n\t\t\t\tfilepath.Join(appDir, \"terragrunt.hcl\"): `\ndependency \"db\" {\n\tconfig_path = \"../db\"\n}\n`,\n\t\t\t\tfilepath.Join(dbDir, \"terragrunt.hcl\"): `\ndependency \"vpc\" {\n\tconfig_path = \"../vpc\"\n}\n`,\n\t\t\t\tfilepath.Join(vpcDir, \"terragrunt.hcl\"): ``,\n\t\t\t}\n\n\t\t\tfor path, content := range testFiles {\n\t\t\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tl := logger.CreateLogger()\n\t\t\topts := &options.TerragruntOptions{\n\t\t\t\tWorkingDir:     tmpDir,\n\t\t\t\tRootWorkingDir: tmpDir,\n\t\t\t}\n\n\t\t\tctx := t.Context()\n\n\t\t\tfilters, err := filter.ParseFilterQueries(l, tt.filters)\n\t\t\trequire.NoError(t, err)\n\n\t\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\t\tWithFilters(filters)\n\n\t\t\tcomponents, err := d.Discover(ctx, l, opts)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tpaths := components.Filter(component.UnitKind).Paths()\n\n\t\t\tfor _, expected := range tt.expectedPaths {\n\t\t\t\texpectedPath := filepath.Join(tmpDir, expected)\n\t\t\t\tassert.Contains(\n\t\t\t\t\tt,\n\t\t\t\t\tpaths,\n\t\t\t\t\texpectedPath,\n\t\t\t\t\t\"expected %s to be included for filters %v\",\n\t\t\t\t\texpected,\n\t\t\t\t\ttt.filters,\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tfor _, excluded := range tt.excludedPaths {\n\t\t\t\texcludedPath := filepath.Join(tmpDir, excluded)\n\n\t\t\t\tassert.NotContains(\n\t\t\t\t\tt,\n\t\t\t\t\tpaths,\n\t\t\t\t\texcludedPath,\n\t\t\t\t\t\"expected %s to be excluded for filters %v\",\n\t\t\t\t\texcluded,\n\t\t\t\t\ttt.filters,\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/discovery/graph_option.go",
    "content": "package discovery\n\nimport \"github.com/gruntwork-io/terragrunt/internal/runner/common\"\n\ntype graphTargetOption struct {\n\ttarget string\n}\n\n// WithGraphTarget returns an option that, when applied to the runner stack,\n// marks a graph target that discovery will use to prune the run to the target\n// path and its dependents. Apply is a no-op; discovery picks this up via\n// Discovery.WithOptions by asserting for GraphTarget() on options.\nfunc WithGraphTarget(targetDir string) common.Option {\n\treturn graphTargetOption{target: targetDir}\n}\n\n// Apply is a no-op; discovery consumes the marker via WithOptions.\nfunc (o graphTargetOption) Apply(stack common.StackRunner) {}\n\n// GraphTarget exposes the requested graph target for discovery to consume.\nfunc (o graphTargetOption) GraphTarget() string {\n\treturn o.target\n}\n"
  },
  {
    "path": "internal/discovery/graph_target_test.go",
    "content": "package discovery_test\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n)\n\n// Test that WithGraphTarget retains the target and all dependents.\nfunc TestDiscoveryWithGraphTarget_RetainsTargetAndDependents(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Initialize a git repository in the temp directory so dependent discovery bounds traversal to the repo root.\n\tcmd := exec.CommandContext(t.Context(), \"git\", \"init\")\n\tcmd.Dir = tmpDir\n\tcmd.Env = os.Environ()\n\trequire.NoError(t, cmd.Run())\n\n\t// Create dependency chain: vpc -> db -> app\n\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\tdbDir := filepath.Join(tmpDir, \"db\")\n\tappDir := filepath.Join(tmpDir, \"app\")\n\n\trequire.NoError(t, os.MkdirAll(vpcDir, 0o755))\n\trequire.NoError(t, os.MkdirAll(dbDir, 0o755))\n\trequire.NoError(t, os.MkdirAll(appDir, 0o755))\n\n\trequire.NoError(t, os.WriteFile(filepath.Join(vpcDir, \"terragrunt.hcl\"), []byte(``), 0o644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(dbDir, \"terragrunt.hcl\"), []byte(`\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n`), 0o644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(appDir, \"terragrunt.hcl\"), []byte(`\ndependency \"db\" {\n  config_path = \"../db\"\n}\n`), 0o644))\n\n\topts := options.NewTerragruntOptions()\n\topts.WorkingDir = tmpDir\n\topts.RootWorkingDir = tmpDir\n\n\tdepsFilters, err := filter.ParseFilterQueries(logger.CreateLogger(), []string{\"{./**}...\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithFilters(depsFilters).\n\t\tWithGraphTarget(vpcDir)\n\n\tconfigs, err := d.Discover(t.Context(), logger.CreateLogger(), opts)\n\trequire.NoError(t, err)\n\n\tpaths := configs.Filter(component.UnitKind).Paths()\n\tassert.ElementsMatch(t, []string{vpcDir, dbDir, appDir}, paths)\n}\n\n// Test parity: experiment ON via filter queries vs graphTarget marker path\nfunc TestDiscoveryGraphTarget_ParityWithFilterQueries(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Initialize a git repository in the temp directory so dependent discovery bounds traversal to the repo root.\n\tcmd := exec.CommandContext(t.Context(), \"git\", \"init\")\n\tcmd.Dir = tmpDir\n\tcmd.Env = os.Environ()\n\trequire.NoError(t, cmd.Run())\n\n\t// Create dependency chain: vpc -> db -> app\n\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\tdbDir := filepath.Join(tmpDir, \"db\")\n\tappDir := filepath.Join(tmpDir, \"app\")\n\n\trequire.NoError(t, os.MkdirAll(vpcDir, 0o755))\n\trequire.NoError(t, os.MkdirAll(dbDir, 0o755))\n\trequire.NoError(t, os.MkdirAll(appDir, 0o755))\n\n\trequire.NoError(t, os.WriteFile(filepath.Join(vpcDir, \"terragrunt.hcl\"), []byte(``), 0o644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(dbDir, \"terragrunt.hcl\"), []byte(`\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n`), 0o644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(appDir, \"terragrunt.hcl\"), []byte(`\ndependency \"db\" {\n  config_path = \"../db\"\n}\n`), 0o644))\n\n\topts := options.NewTerragruntOptions()\n\topts.WorkingDir = tmpDir\n\topts.RootWorkingDir = tmpDir\n\n\t// Path A: filter queries (experiment ON equivalent)\n\tfilters, err := filter.ParseFilterQueries(logger.CreateLogger(), []string{`...{` + vpcDir + `}`})\n\trequire.NoError(t, err)\n\n\tdepsFilters, err := filter.ParseFilterQueries(logger.CreateLogger(), []string{\"{./**}...\"})\n\trequire.NoError(t, err)\n\n\tconfigsA, err := discovery.NewDiscovery(tmpDir).\n\t\tWithFilters(depsFilters).\n\t\tWithFilters(filters).\n\t\tDiscover(t.Context(), logger.CreateLogger(), opts)\n\trequire.NoError(t, err)\n\n\t// Path B: graph target marker\n\tconfigsB, err := discovery.NewDiscovery(tmpDir).\n\t\tWithFilters(depsFilters).\n\t\tWithGraphTarget(vpcDir).\n\t\tDiscover(t.Context(), logger.CreateLogger(), opts)\n\trequire.NoError(t, err)\n\n\tassert.ElementsMatch(t, configsA.Filter(component.UnitKind).Paths(), configsB.Filter(component.UnitKind).Paths())\n}\n\n// Test that graph target with no dependents returns only the target.\nfunc TestDiscoveryWithGraphTarget_NoDependents(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Initialize a git repository\n\tcmd := exec.CommandContext(t.Context(), \"git\", \"init\")\n\tcmd.Dir = tmpDir\n\tcmd.Env = os.Environ()\n\trequire.NoError(t, cmd.Run())\n\n\t// Create standalone units (no dependencies between them)\n\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\tdbDir := filepath.Join(tmpDir, \"db\")\n\tappDir := filepath.Join(tmpDir, \"app\")\n\n\trequire.NoError(t, os.MkdirAll(vpcDir, 0o755))\n\trequire.NoError(t, os.MkdirAll(dbDir, 0o755))\n\trequire.NoError(t, os.MkdirAll(appDir, 0o755))\n\n\trequire.NoError(t, os.WriteFile(filepath.Join(vpcDir, \"terragrunt.hcl\"), []byte(``), 0o644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(dbDir, \"terragrunt.hcl\"), []byte(``), 0o644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(appDir, \"terragrunt.hcl\"), []byte(``), 0o644))\n\n\topts := options.NewTerragruntOptions()\n\topts.WorkingDir = tmpDir\n\topts.RootWorkingDir = tmpDir\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithRelationships().\n\t\tWithGraphTarget(vpcDir)\n\n\tconfigs, err := d.Discover(t.Context(), logger.CreateLogger(), opts)\n\trequire.NoError(t, err)\n\n\tpaths := configs.Filter(component.UnitKind).Paths()\n\t// Should only return the target since no one depends on it\n\tassert.ElementsMatch(t, []string{vpcDir}, paths)\n}\n\n// Test that WithOptions interface assertion works for GraphTarget.\nfunc TestDiscoveryWithOptions_GraphTarget(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Initialize a git repository\n\tcmd := exec.CommandContext(t.Context(), \"git\", \"init\")\n\tcmd.Dir = tmpDir\n\tcmd.Env = os.Environ()\n\trequire.NoError(t, cmd.Run())\n\n\t// Create dependency chain: vpc -> db\n\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\tdbDir := filepath.Join(tmpDir, \"db\")\n\n\trequire.NoError(t, os.MkdirAll(vpcDir, 0o755))\n\trequire.NoError(t, os.MkdirAll(dbDir, 0o755))\n\n\trequire.NoError(t, os.WriteFile(filepath.Join(vpcDir, \"terragrunt.hcl\"), []byte(``), 0o644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(dbDir, \"terragrunt.hcl\"), []byte(`\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n`), 0o644))\n\n\topts := options.NewTerragruntOptions()\n\topts.WorkingDir = tmpDir\n\topts.RootWorkingDir = tmpDir\n\n\t// Create an option that implements GraphTarget() interface\n\tgraphTargetOpt := &mockGraphTargetOption{target: vpcDir}\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithRelationships().\n\t\tWithOptions(graphTargetOpt)\n\n\tconfigs, err := d.Discover(t.Context(), logger.CreateLogger(), opts)\n\trequire.NoError(t, err)\n\n\tpaths := configs.Filter(component.UnitKind).Paths()\n\tassert.ElementsMatch(t, []string{vpcDir, dbDir}, paths)\n}\n\n// mockGraphTargetOption implements the GraphTarget() interface for testing.\ntype mockGraphTargetOption struct {\n\ttarget string\n}\n\nfunc (m *mockGraphTargetOption) GraphTarget() string {\n\treturn m.target\n}\n"
  },
  {
    "path": "internal/discovery/helpers.go",
    "content": "package discovery\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/zclconf/go-cty/cty\"\n)\n\nconst (\n\t// defaultDiscoveryWorkers is the default number of concurrent workers for discovery operations.\n\tdefaultDiscoveryWorkers = 4\n\n\t// maxDiscoveryWorkers is the maximum number of workers (2x default to prevent excessive concurrency).\n\tmaxDiscoveryWorkers = defaultDiscoveryWorkers * 2\n\n\t// defaultMaxDependencyDepth is the default maximum dependency depth for discovery.\n\tdefaultMaxDependencyDepth = 1000\n\n\t// maxCycleRemovalAttempts is the maximum number of cycle removal attempts.\n\tmaxCycleRemovalAttempts = 100\n)\n\n// DefaultConfigFilenames are the default Terragrunt config filenames used in discovery.\nvar DefaultConfigFilenames = []string{config.DefaultTerragruntConfigPath, config.DefaultStackFile}\n\n// stringSet is a thread-safe set of strings using map and RWMutex.\n// This is more performant than sync.Map for string keys with simple bool values.\ntype stringSet struct {\n\tm  map[string]struct{}\n\tmu sync.RWMutex\n}\n\n// newStringSet creates a new stringSet.\nfunc newStringSet() *stringSet {\n\treturn &stringSet{\n\t\tm: make(map[string]struct{}),\n\t}\n}\n\n// LoadOrStore returns true if the key was already present (loaded),\n// false if the key was newly stored.\nfunc (s *stringSet) LoadOrStore(key string) (loaded bool) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tif _, ok := s.m[key]; ok {\n\t\treturn true\n\t}\n\n\ts.m[key] = struct{}{}\n\n\treturn false\n}\n\n// Load returns whether the key exists in the set.\nfunc (s *stringSet) Load(key string) bool {\n\ts.mu.RLock()\n\tdefer s.mu.RUnlock()\n\n\t_, ok := s.m[key]\n\n\treturn ok\n}\n\n// isExternal checks if a component path is outside the given working directory.\n// A path is considered external if it's not within or equal to the working directory.\n// We conservatively evaluate paths as external if we cannot determine their absolute path.\nfunc isExternal(workingDir string, componentPath string) bool {\n\tif workingDir == \"\" {\n\t\treturn true\n\t}\n\n\tworkingDirClean := filepath.Clean(workingDir)\n\tcomponentPathClean := filepath.Clean(componentPath)\n\n\tworkingDirResolved, err := filepath.EvalSymlinks(workingDirClean)\n\tif err != nil {\n\t\tworkingDirResolved = workingDirClean\n\t}\n\n\tcomponentPathResolved, err := filepath.EvalSymlinks(componentPathClean)\n\tif err != nil {\n\t\tcomponentPathResolved = componentPathClean\n\t}\n\n\trelPath, err := filepath.Rel(workingDirResolved, componentPathResolved)\n\tif err != nil {\n\t\treturn true\n\t}\n\n\treturn relPath == \"..\" || strings.HasPrefix(relPath, \"..\"+string(filepath.Separator))\n}\n\n// componentFromDependencyPath returns a component for a dependency path. If the path already\n// exists in the thread-safe components, it returns that. If the path contains a stack file,\n// it creates a stack. Otherwise, it creates a unit.\nfunc componentFromDependencyPath(path string, components *component.ThreadSafeComponents) component.Component {\n\tif existing := components.FindByPath(path); existing != nil {\n\t\treturn existing\n\t}\n\n\tif _, err := os.Stat(filepath.Join(path, config.DefaultStackFile)); err == nil {\n\t\treturn component.NewStack(path)\n\t}\n\n\treturn component.NewUnit(path)\n}\n\n// createComponentFromPath creates a component from a file path if it matches one of the config filenames.\n// Returns nil if the file doesn't match any of the provided filenames.\nfunc createComponentFromPath(\n\tpath string,\n\tfilenames []string,\n\tdiscoveryContext *component.DiscoveryContext,\n) component.Component {\n\tbase := filepath.Base(path)\n\tdir := filepath.Dir(path)\n\n\tcomponentOfBase := func(dir, base string) component.Component {\n\t\tif base == config.DefaultStackFile {\n\t\t\treturn component.NewStack(dir)\n\t\t}\n\n\t\treturn component.NewUnit(dir)\n\t}\n\n\tfor _, fname := range filenames {\n\t\tif base != fname {\n\t\t\tcontinue\n\t\t}\n\n\t\tc := componentOfBase(dir, base)\n\t\tif unit, ok := c.(*component.Unit); ok {\n\t\t\tunit.SetConfigFile(base)\n\t\t}\n\n\t\tif discoveryContext != nil {\n\t\t\tdiscoveryCtx := discoveryContext.Copy()\n\t\t\tdiscoveryCtx.SuggestOrigin(component.OriginPathDiscovery)\n\n\t\t\tc.SetDiscoveryContext(discoveryCtx)\n\t\t}\n\n\t\treturn c\n\t}\n\n\treturn nil\n}\n\n// validateNoCoexistence checks that no directory has both a unit and a stack config file.\n// Returns a CoexistenceError if a directory contains both.\nfunc validateNoCoexistence(results []DiscoveryResult) error {\n\tseen := make(map[string]DiscoveryResult, len(results))\n\n\tfor _, result := range results {\n\t\tpath := result.Component.Path()\n\n\t\tif existing, ok := seen[path]; ok && existing.Component.Kind() != result.Component.Kind() {\n\t\t\tunitFile, stackFile := existing.Component.ConfigFile(), result.Component.ConfigFile()\n\t\t\tif result.Component.Kind() == component.UnitKind {\n\t\t\t\tunitFile, stackFile = result.Component.ConfigFile(), existing.Component.ConfigFile()\n\t\t\t}\n\n\t\t\treturn NewCoexistenceError(path, unitFile, stackFile)\n\t\t}\n\n\t\tseen[path] = result\n\t}\n\n\treturn nil\n}\n\n// deduplicateResults removes duplicate components from results by path.\nfunc deduplicateResults(results []DiscoveryResult) []DiscoveryResult {\n\tseen := make(map[string]struct{}, len(results))\n\tunique := make([]DiscoveryResult, 0, len(results))\n\n\tfor _, result := range results {\n\t\tpath := result.Component.Path()\n\t\tif _, exists := seen[path]; !exists {\n\t\t\tseen[path] = struct{}{}\n\n\t\t\tunique = append(unique, result)\n\t\t}\n\t}\n\n\treturn unique\n}\n\n// resultsToComponents extracts the components from discovery results.\nfunc resultsToComponents(results []DiscoveryResult) component.Components {\n\tcomponents := make(component.Components, 0, len(results))\n\tfor _, result := range results {\n\t\tcomponents = append(components, result.Component)\n\t}\n\n\treturn components\n}\n\n// sanitizeReadFiles clones, removes empty strings, sorts, and deduplicates the file list.\nfunc sanitizeReadFiles(files []string) []string {\n\tif len(files) == 0 {\n\t\treturn []string{}\n\t}\n\n\tfiles = slices.Clone(files)\n\tfiles = slices.DeleteFunc(files, func(file string) bool {\n\t\treturn len(file) == 0\n\t})\n\tslices.Sort(files)\n\n\treturn slices.Compact(files)\n}\n\n// extractDependencyPaths extracts all dependency paths from a Terragrunt configuration.\nfunc extractDependencyPaths(cfg *config.TerragruntConfig, c component.Component) ([]string, error) {\n\tif cfg == nil {\n\t\treturn nil, nil\n\t}\n\n\tmaxDedupLen := len(cfg.TerragruntDependencies)\n\tif cfg.Dependencies != nil {\n\t\tmaxDedupLen += len(cfg.Dependencies.Paths)\n\t}\n\n\tdeduped := make(map[string]struct{}, maxDedupLen)\n\n\terrs := make([]error, 0, maxDedupLen)\n\n\tfor _, dependency := range cfg.TerragruntDependencies {\n\t\tif dependency.Enabled != nil && !*dependency.Enabled {\n\t\t\tcontinue\n\t\t}\n\n\t\tif dependency.ConfigPath.Type() != cty.String {\n\t\t\terrs = append(errs, errors.New(\"dependency config path is not a string\"))\n\t\t\tcontinue\n\t\t}\n\n\t\tdepPath := dependency.ConfigPath.AsString()\n\t\tif !filepath.IsAbs(depPath) {\n\t\t\tdepPath = filepath.Clean(filepath.Join(c.Path(), depPath))\n\t\t}\n\n\t\tdepPath = util.ResolvePath(depPath)\n\t\tdeduped[depPath] = struct{}{}\n\t}\n\n\tif cfg.Dependencies != nil {\n\t\tfor _, dependency := range cfg.Dependencies.Paths {\n\t\t\tif !filepath.IsAbs(dependency) {\n\t\t\t\tdependency = filepath.Clean(filepath.Join(c.Path(), dependency))\n\t\t\t}\n\n\t\t\tdependency = util.ResolvePath(dependency)\n\t\t\tdeduped[dependency] = struct{}{}\n\t\t}\n\t}\n\n\tdepPaths := make([]string, 0, len(deduped))\n\tfor depPath := range deduped {\n\t\tdepPaths = append(depPaths, depPath)\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn depPaths, errors.Join(errs...)\n\t}\n\n\treturn depPaths, nil\n}\n"
  },
  {
    "path": "internal/discovery/options.go",
    "content": "package discovery\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/worktrees\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n)\n\n// WithDiscoveryContext sets the discovery context.\nfunc (d *Discovery) WithDiscoveryContext(ctx *component.DiscoveryContext) *Discovery {\n\td.discoveryContext = ctx\n\treturn d\n}\n\n// WithWorktrees sets the worktrees for Git-based filters.\nfunc (d *Discovery) WithWorktrees(w *worktrees.Worktrees) *Discovery {\n\td.worktrees = w\n\treturn d\n}\n\n// WithConfigFilenames sets the config filenames to discover.\nfunc (d *Discovery) WithConfigFilenames(filenames []string) *Discovery {\n\td.configFilenames = filenames\n\treturn d\n}\n\n// WithParserOptions sets custom HCL parser options.\nfunc (d *Discovery) WithParserOptions(opts []hclparse.Option) *Discovery {\n\td.parserOptions = opts\n\treturn d\n}\n\n// WithFilters sets filter queries for component selection.\nfunc (d *Discovery) WithFilters(filters filter.Filters) *Discovery {\n\td.filters = filters\n\n\t// If there are any positive filters, exclude by default\n\tif d.filters.HasPositiveFilter() {\n\t\td.excludeByDefault = true\n\t}\n\n\t// Check if filters require parsing\n\tif _, ok := d.filters.RequiresParse(); ok {\n\t\td.requiresParse = true\n\t}\n\n\t// Collect Git expressions\n\td.gitExpressions = d.filters.UniqueGitFilters()\n\n\treturn d\n}\n\n// WithMaxDependencyDepth sets the maximum dependency depth.\nfunc (d *Discovery) WithMaxDependencyDepth(depth int) *Discovery {\n\td.maxDependencyDepth = depth\n\treturn d\n}\n\n// WithNumWorkers sets the number of concurrent workers.\nfunc (d *Discovery) WithNumWorkers(numWorkers int) *Discovery {\n\tif numWorkers > 0 && numWorkers <= maxDiscoveryWorkers {\n\t\td.numWorkers = numWorkers\n\t}\n\n\treturn d\n}\n\n// WithNoHidden excludes hidden directories from discovery.\nfunc (d *Discovery) WithNoHidden() *Discovery {\n\td.noHidden = true\n\treturn d\n}\n\n// WithRequiresParse enables parsing of Terragrunt configurations.\nfunc (d *Discovery) WithRequiresParse() *Discovery {\n\td.requiresParse = true\n\treturn d\n}\n\n// WithParseExclude enables parsing of exclude configurations.\nfunc (d *Discovery) WithParseExclude() *Discovery {\n\td.parseExclude = true\n\td.requiresParse = true\n\n\treturn d\n}\n\n// WithParseIncludes enables parsing for include configurations.\nfunc (d *Discovery) WithParseIncludes() *Discovery {\n\td.parseIncludes = true\n\td.requiresParse = true\n\n\treturn d\n}\n\n// WithReadFiles enables parsing for file reading information.\nfunc (d *Discovery) WithReadFiles() *Discovery {\n\td.readFiles = true\n\td.requiresParse = true\n\n\treturn d\n}\n\n// WithSuppressParseErrors suppresses errors during parsing.\nfunc (d *Discovery) WithSuppressParseErrors() *Discovery {\n\td.suppressParseErrors = true\n\treturn d\n}\n\n// WithBreakCycles enables breaking cycles in the dependency graph.\nfunc (d *Discovery) WithBreakCycles() *Discovery {\n\td.breakCycles = true\n\treturn d\n}\n\n// WithRelationships enables relationship discovery.\nfunc (d *Discovery) WithRelationships() *Discovery {\n\td.discoverRelationships = true\n\treturn d\n}\n\n// WithGitRoot sets the git root directory for dependent discovery boundary.\nfunc (d *Discovery) WithGitRoot(gitRoot string) *Discovery {\n\td.gitRoot = gitRoot\n\treturn d\n}\n\n// WithGraphTarget sets the graph target so discovery can prune to the target and its dependents.\nfunc (d *Discovery) WithGraphTarget(target string) *Discovery {\n\td.graphTarget = target\n\treturn d\n}\n\n// WithOptions ingests runner options and applies any discovery-relevant settings.\n// Currently, it extracts HCL parser options provided via common.ParseOptionsProvider\n// and graph target options, and forwards them to discovery's configuration.\nfunc (d *Discovery) WithOptions(opts ...any) *Discovery {\n\tvar parserOptions []hclparse.Option\n\n\tfor _, opt := range opts {\n\t\tif p, ok := opt.(interface{ GetParseOptions() []hclparse.Option }); ok {\n\t\t\tparserOptions = append(parserOptions, p.GetParseOptions()...)\n\t\t}\n\n\t\tif g, ok := opt.(interface{ GraphTarget() string }); ok {\n\t\t\tif target := g.GraphTarget(); target != \"\" {\n\t\t\t\td = d.WithGraphTarget(target)\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(parserOptions) > 0 {\n\t\td = d.WithParserOptions(parserOptions)\n\t}\n\n\treturn d\n}\n"
  },
  {
    "path": "internal/discovery/phase_filesystem.go",
    "content": "package discovery\n\nimport (\n\t\"context\"\n\t\"io/fs\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// FilesystemPhase walks directories to discover Terragrunt configurations.\ntype FilesystemPhase struct {\n\t// numWorkers is the number of concurrent workers.\n\tnumWorkers int\n}\n\n// NewFilesystemPhase creates a new FilesystemPhase.\nfunc NewFilesystemPhase(numWorkers int) *FilesystemPhase {\n\tnumWorkers = max(numWorkers, defaultDiscoveryWorkers)\n\n\treturn &FilesystemPhase{\n\t\tnumWorkers: numWorkers,\n\t}\n}\n\n// Name returns the human-readable name of the phase.\nfunc (p *FilesystemPhase) Name() string {\n\treturn \"filesystem\"\n}\n\n// Kind returns the PhaseKind identifier.\nfunc (p *FilesystemPhase) Kind() PhaseKind {\n\treturn PhaseFilesystem\n}\n\n// Run executes the filesystem discovery phase.\nfunc (p *FilesystemPhase) Run(ctx context.Context, l log.Logger, input *PhaseInput) (*PhaseResults, error) {\n\tresults := NewPhaseResults()\n\n\tdiscovery := input.Discovery\n\tif discovery == nil {\n\t\treturn nil, NewClassificationError(\"\", \"discovery configuration is nil\")\n\t}\n\n\tdiscoveryContext := discovery.discoveryContext\n\tif discoveryContext == nil || discoveryContext.WorkingDir == \"\" {\n\t\treturn nil, NewClassificationError(\"\", \"discovery context or working directory is nil\")\n\t}\n\n\tfilenames := discovery.configFilenames\n\tif len(filenames) == 0 {\n\t\tfilenames = DefaultConfigFilenames\n\t}\n\n\twalkFn := filepath.WalkDir\n\tif input.Opts != nil && input.Opts.Experiments.Evaluate(experiment.Symlinks) {\n\t\twalkFn = util.WalkDirWithSymlinks\n\t}\n\n\terr := walkFn(discoveryContext.WorkingDir, func(path string, d fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tif d.IsDir() {\n\t\t\treturn p.skipDirIfIgnorable(discovery, d.Name())\n\t\t}\n\n\t\tresult := p.processFile(input, path, filenames)\n\t\tif result == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tswitch result.Status {\n\t\tcase StatusDiscovered:\n\t\t\tresults.AddDiscovered(*result)\n\t\tcase StatusCandidate:\n\t\t\tresults.AddCandidate(*result)\n\t\tcase StatusExcluded:\n\t\t\t// Excluded components are not added\n\t\t}\n\n\t\treturn nil\n\t})\n\n\treturn results, err\n}\n\n// skipDirIfIgnorable determines if a directory should be skipped during traversal.\nfunc (p *FilesystemPhase) skipDirIfIgnorable(discovery *Discovery, dir string) error {\n\tif err := util.SkipDirIfIgnorable(dir); err != nil {\n\t\treturn err\n\t}\n\n\tif discovery.noHidden {\n\t\tif strings.HasPrefix(dir, \".\") && dir != \".\" && dir != \"..\" {\n\t\t\treturn filepath.SkipDir\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// processFile processes a single file to determine if it's a Terragrunt configuration\n// and classifies it as discovered, candidate, or excluded.\nfunc (p *FilesystemPhase) processFile(\n\tinput *PhaseInput,\n\tpath string,\n\tfilenames []string,\n) *DiscoveryResult {\n\tdiscovery := input.Discovery\n\n\tc := createComponentFromPath(path, filenames, discovery.discoveryContext)\n\tif c == nil {\n\t\treturn nil\n\t}\n\n\tif input.Classifier != nil {\n\t\tctx := filter.ClassificationContext{}\n\t\tstatus, reason, graphIdx := input.Classifier.Classify(c, ctx)\n\n\t\treturn &DiscoveryResult{\n\t\t\tComponent:            c,\n\t\t\tStatus:               status,\n\t\t\tReason:               reason,\n\t\t\tPhase:                PhaseFilesystem,\n\t\t\tGraphExpressionIndex: graphIdx,\n\t\t}\n\t}\n\n\treturn &DiscoveryResult{\n\t\tComponent: c,\n\t\tStatus:    StatusDiscovered,\n\t\tReason:    CandidacyReasonNone,\n\t\tPhase:     PhaseFilesystem,\n\t}\n}\n"
  },
  {
    "path": "internal/discovery/phase_graph.go",
    "content": "package discovery\n\nimport (\n\t\"context\"\n\t\"io/fs\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// GraphPhase traverses dependency/dependent relationships based on graph expressions.\ntype GraphPhase struct {\n\t// numWorkers is the number of concurrent workers.\n\tnumWorkers int\n\t// maxDepth is the maximum depth for dependency traversal.\n\tmaxDepth int\n}\n\n// graphTraversalState consolidates shared state used across graph traversal functions.\ntype graphTraversalState struct {\n\topts                 *options.TerragruntOptions\n\tdiscovery            *Discovery\n\tthreadSafeComponents *component.ThreadSafeComponents\n\tseenComponents       *stringSet\n\tresults              *PhaseResults\n}\n\n// NewGraphPhase creates a new GraphPhase.\nfunc NewGraphPhase(numWorkers, maxDepth int) *GraphPhase {\n\tnumWorkers = max(numWorkers, defaultDiscoveryWorkers)\n\n\tif maxDepth <= 0 {\n\t\tmaxDepth = defaultMaxDependencyDepth\n\t}\n\n\treturn &GraphPhase{\n\t\tnumWorkers: numWorkers,\n\t\tmaxDepth:   maxDepth,\n\t}\n}\n\n// Name returns the human-readable name of the phase.\nfunc (p *GraphPhase) Name() string {\n\treturn \"graph\"\n}\n\n// Kind returns the PhaseKind identifier.\nfunc (p *GraphPhase) Kind() PhaseKind {\n\treturn PhaseGraph\n}\n\n// Run executes the graph discovery phase.\nfunc (p *GraphPhase) Run(ctx context.Context, l log.Logger, input *PhaseInput) (*PhaseResults, error) {\n\tresults := NewPhaseResults()\n\n\tdiscovery := input.Discovery\n\tif discovery == nil {\n\t\treturn results, nil\n\t}\n\n\tclassifier := input.Classifier\n\tif classifier == nil || !classifier.HasGraphFilters() {\n\t\tfor _, candidate := range input.Candidates {\n\t\t\tif candidate.Reason != CandidacyReasonGraphTarget {\n\t\t\t\tresults.AddCandidate(candidate)\n\t\t\t}\n\t\t}\n\n\t\treturn results, nil\n\t}\n\n\tgraphExprs := classifier.GraphExpressions()\n\tif len(graphExprs) == 0 {\n\t\treturn results, nil\n\t}\n\n\tcandidateComponents := resultsToComponents(input.Candidates)\n\tallComponents := make([]component.Component, 0, len(input.Components)+len(candidateComponents))\n\tallComponents = append(allComponents, input.Components...)\n\tallComponents = append(allComponents, candidateComponents...)\n\tthreadSafeComponents := component.NewThreadSafeComponents(allComponents)\n\n\tgraphTargetCandidates := make([]DiscoveryResult, 0, len(input.Candidates))\n\totherCandidates := make([]DiscoveryResult, 0, len(input.Candidates))\n\n\tfor _, candidate := range input.Candidates {\n\t\tswitch candidate.Reason {\n\t\tcase CandidacyReasonGraphTarget:\n\t\t\tgraphTargetCandidates = append(graphTargetCandidates, candidate)\n\t\tcase CandidacyReasonPotentialDependent:\n\t\t\t// Potential dependents are NOT passed through - they're only used\n\t\t\t// for building the dependency graph. If they're actual dependents,\n\t\t\t// they'll be discovered during dependent traversal.\n\t\tcase CandidacyReasonNone, CandidacyReasonRequiresParse:\n\t\t\totherCandidates = append(otherCandidates, candidate)\n\t\t}\n\t}\n\n\tfor _, candidate := range otherCandidates {\n\t\tresults.AddCandidate(candidate)\n\t}\n\n\tseenComponents := newStringSet()\n\n\tstate := &graphTraversalState{\n\t\topts:                 input.Opts,\n\t\tdiscovery:            discovery,\n\t\tthreadSafeComponents: threadSafeComponents,\n\t\tseenComponents:       seenComponents,\n\t\tresults:              results,\n\t}\n\n\tvar (\n\t\terrs  []error\n\t\terrMu sync.Mutex\n\t)\n\n\tg, ctx := errgroup.WithContext(ctx)\n\tg.SetLimit(p.numWorkers)\n\n\tfor _, graphExpr := range graphExprs {\n\t\tmatchingCandidates := make([]DiscoveryResult, 0, len(graphTargetCandidates))\n\n\t\tfor _, candidate := range graphTargetCandidates {\n\t\t\tif candidate.GraphExpressionIndex == graphExpr.Index {\n\t\t\t\tmatchingCandidates = append(matchingCandidates, candidate)\n\t\t\t}\n\t\t}\n\n\t\tif len(matchingCandidates) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, candidate := range matchingCandidates {\n\t\t\tg.Go(func() error {\n\t\t\t\terr := p.processGraphTarget(ctx, l, state, candidate, graphExpr)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrMu.Lock()\n\n\t\t\t\t\terrs = append(errs, err)\n\n\t\t\t\t\terrMu.Unlock()\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}\n\t}\n\n\tif err := g.Wait(); err != nil {\n\t\terrs = append(errs, err)\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn results, errors.Join(errs...)\n\t}\n\n\treturn results, nil\n}\n\n// processGraphTarget processes a single graph expression target.\nfunc (p *GraphPhase) processGraphTarget(\n\tctx context.Context,\n\tl log.Logger,\n\tstate *graphTraversalState,\n\tcandidate DiscoveryResult,\n\tgraphExpr *GraphExpressionInfo,\n) error {\n\tc := candidate.Component\n\n\t// Always add the target to discovered, regardless of ExcludeTarget.\n\t// The final filter evaluation will handle ExcludeTarget appropriately.\n\t// We need the target in the result set for the final evaluation to work\n\t// (it uses the target as the starting point for traversing dependents).\n\tif loaded := state.seenComponents.LoadOrStore(c.Path()); !loaded {\n\t\tstate.results.AddDiscovered(DiscoveryResult{\n\t\t\tComponent: c,\n\t\t\tStatus:    StatusDiscovered,\n\t\t\tReason:    CandidacyReasonNone,\n\t\t\tPhase:     PhaseGraph,\n\t\t})\n\t}\n\n\tif graphExpr.IncludeDependencies {\n\t\tdepth := p.maxDepth\n\t\tif graphExpr.DependencyDepth > 0 {\n\t\t\tdepth = graphExpr.DependencyDepth\n\t\t}\n\n\t\terr := p.discoverDependencies(ctx, l, state, c, depth)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif graphExpr.IncludeDependents {\n\t\tdepth := p.maxDepth\n\t\tif graphExpr.DependentDepth > 0 {\n\t\t\tdepth = graphExpr.DependentDepth\n\t\t}\n\n\t\terr := p.discoverDependents(ctx, l, state, c, depth)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif state.discovery.gitRoot != \"\" {\n\t\t\t// Use the discovery's workingDir as the starting point for dependent discovery.\n\t\t\t// This is important when the target was discovered from a worktree - dependents\n\t\t\t// exist in the original working directory, not in the worktree.\n\t\t\tstartDir := state.discovery.workingDir\n\t\t\tl.Debugf(\n\t\t\t\t\"Starting upstream dependent discovery from %s to gitRoot %s\",\n\t\t\t\tstartDir,\n\t\t\t\tstate.discovery.gitRoot,\n\t\t\t)\n\n\t\t\tvisitedDirs := newStringSet()\n\n\t\t\terr := p.discoverDependentsUpstream(ctx, l, state, c, visitedDirs, startDir, depth)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// discoverDependencies recursively discovers dependencies of a component.\nfunc (p *GraphPhase) discoverDependencies(\n\tctx context.Context,\n\tl log.Logger,\n\tstate *graphTraversalState,\n\tc component.Component,\n\tdepthRemaining int,\n) error {\n\tif depthRemaining <= 0 {\n\t\treturn nil\n\t}\n\n\tif _, ok := c.(*component.Stack); ok {\n\t\treturn nil\n\t}\n\n\tunit, ok := c.(*component.Unit)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\tcfg := unit.Config()\n\tif cfg == nil {\n\t\terr := parseComponent(ctx, l, c, state.opts, state.discovery)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcfg = unit.Config()\n\t}\n\n\tdepPaths, err := extractDependencyPaths(cfg, c)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(depPaths) == 0 {\n\t\treturn nil\n\t}\n\n\tvar (\n\t\terrs  []error\n\t\terrMu sync.Mutex\n\t)\n\n\tg, ctx := errgroup.WithContext(ctx)\n\tg.SetLimit(p.numWorkers)\n\n\tfor _, depPath := range depPaths {\n\t\tg.Go(func() error {\n\t\t\tdepComponent, err := p.resolveDependency(\n\t\t\t\tc, depPath, state.threadSafeComponents,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\terrMu.Lock()\n\n\t\t\t\terrs = append(errs, err)\n\n\t\t\t\terrMu.Unlock()\n\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif depComponent == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif loaded := state.seenComponents.LoadOrStore(depComponent.Path()); !loaded {\n\t\t\t\tstate.results.AddDiscovered(DiscoveryResult{\n\t\t\t\t\tComponent: depComponent,\n\t\t\t\t\tStatus:    StatusDiscovered,\n\t\t\t\t\tReason:    CandidacyReasonNone,\n\t\t\t\t\tPhase:     PhaseGraph,\n\t\t\t\t})\n\n\t\t\t\terr = p.discoverDependencies(ctx, l, state, depComponent, depthRemaining-1)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrMu.Lock()\n\n\t\t\t\t\terrs = append(errs, err)\n\n\t\t\t\t\terrMu.Unlock()\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tif err := g.Wait(); err != nil {\n\t\treturn err\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn errors.Join(errs...)\n\t}\n\n\treturn nil\n}\n\n// discoverDependents discovers dependents of a component by traversing the existing graph.\nfunc (p *GraphPhase) discoverDependents(\n\tctx context.Context,\n\tl log.Logger,\n\tstate *graphTraversalState,\n\tc component.Component,\n\tdepthRemaining int,\n) error {\n\tif depthRemaining <= 0 {\n\t\treturn nil\n\t}\n\n\tdependents := c.Dependents()\n\tif len(dependents) == 0 {\n\t\treturn nil\n\t}\n\n\tvar (\n\t\terrs  []error\n\t\terrMu sync.Mutex\n\t)\n\n\tg, ctx := errgroup.WithContext(ctx)\n\tg.SetLimit(p.numWorkers)\n\n\tfor _, dependent := range dependents {\n\t\tg.Go(func() error {\n\t\t\tif loaded := state.seenComponents.LoadOrStore(dependent.Path()); loaded {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tstate.results.AddDiscovered(DiscoveryResult{\n\t\t\t\tComponent: dependent,\n\t\t\t\tStatus:    StatusDiscovered,\n\t\t\t\tReason:    CandidacyReasonNone,\n\t\t\t\tPhase:     PhaseGraph,\n\t\t\t})\n\n\t\t\terr := p.discoverDependents(ctx, l, state, dependent, depthRemaining-1)\n\t\t\tif err != nil {\n\t\t\t\terrMu.Lock()\n\n\t\t\t\terrs = append(errs, err)\n\n\t\t\t\terrMu.Unlock()\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tif err := g.Wait(); err != nil {\n\t\treturn err\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn errors.Join(errs...)\n\t}\n\n\treturn nil\n}\n\n// upstreamDiscoveryState holds shared state for processing upstream candidates.\n// Created once per discoverDependentsUpstream call and reused across candidates.\ntype upstreamDiscoveryState struct {\n\tgraphTraversalState         *graphTraversalState\n\ttarget                      component.Component\n\tcheckedForTarget            *stringSet\n\terrs                        *[]error\n\terrMu                       *sync.Mutex\n\tresolvedTargetPath          string\n\ttargetRelSuffix             string\n\tresolvedDiscoveryWorkingDir string\n}\n\n// discoverDependentsUpstream discovers dependents by walking up the filesystem\n// from the target component's directory to gitRoot (or filesystem root if gitRoot is empty).\n// At each directory level, it walks down to find terragrunt configs and checks if they\n// depend on the target component.\nfunc (p *GraphPhase) discoverDependentsUpstream(\n\tctx context.Context,\n\tl log.Logger,\n\tstate *graphTraversalState,\n\ttarget component.Component,\n\tvisitedDirs *stringSet,\n\tcurrentDir string,\n\tdepthRemaining int,\n) error {\n\tl.Debugf(\"discoverDependentsUpstream: target=%s currentDir=%s depth=%d\", target.Path(), currentDir, depthRemaining)\n\n\tif depthRemaining <= 0 {\n\t\tl.Debugf(\"discoverDependentsUpstream: depth limit reached\")\n\t\treturn nil\n\t}\n\n\tif currentDir == filepath.Dir(currentDir) {\n\t\tl.Debugf(\"discoverDependentsUpstream: reached filesystem root\")\n\t\treturn nil\n\t}\n\n\tgitRoot := state.discovery.gitRoot\n\tif gitRoot != \"\" && currentDir != gitRoot && !strings.HasPrefix(currentDir, gitRoot) {\n\t\tl.Debugf(\"discoverDependentsUpstream: outside git root boundary (currentDir=%s, gitRoot=%s)\", currentDir, gitRoot)\n\t\treturn nil\n\t}\n\n\tresolvedTargetPath := util.ResolvePath(target.Path())\n\n\t// When the target is from a worktree, we need to compare using relative suffixes\n\t// because the absolute paths will differ (worktree vs original directory).\n\t// We resolve paths to handle symlinks (e.g., /var -> /private/var on macOS).\n\ttargetRelSuffix := \"\"\n\n\tif targetDCtx := target.DiscoveryContext(); targetDCtx != nil && targetDCtx.WorkingDir != \"\" {\n\t\tresolvedWorkingDir := util.ResolvePath(targetDCtx.WorkingDir)\n\t\ttargetRelSuffix = strings.TrimPrefix(resolvedTargetPath, resolvedWorkingDir)\n\t}\n\n\t// Resolve discovery.workingDir for consistent path comparison.\n\tresolvedDiscoveryWorkingDir := util.ResolvePath(state.discovery.workingDir)\n\n\tvar candidates []component.Component\n\n\twalkFn := filepath.WalkDir\n\tif state.opts != nil && state.opts.Experiments.Evaluate(experiment.Symlinks) {\n\t\twalkFn = util.WalkDirWithSymlinks\n\t}\n\n\terr := walkFn(currentDir, func(path string, d fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tif d.IsDir() {\n\t\t\tif loaded := visitedDirs.LoadOrStore(path); loaded {\n\t\t\t\treturn filepath.SkipDir\n\t\t\t}\n\n\t\t\tif err := util.SkipDirIfIgnorable(d.Name()); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\n\t\tbase := filepath.Base(path)\n\t\tif !slices.Contains(state.discovery.configFilenames, base) {\n\t\t\treturn nil\n\t\t}\n\n\t\tcandidate := createComponentFromPath(path, state.discovery.configFilenames, state.discovery.discoveryContext)\n\t\tif candidate != nil {\n\t\t\tcandidates = append(candidates, candidate)\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar (\n\t\tdiscoveredDependents []component.Component\n\t\tdependentsMu         sync.Mutex\n\t\terrs                 []error\n\t\terrMu                sync.Mutex\n\t)\n\n\tupstreamState := &upstreamDiscoveryState{\n\t\tgraphTraversalState:         state,\n\t\ttarget:                      target,\n\t\tcheckedForTarget:            newStringSet(),\n\t\tresolvedTargetPath:          resolvedTargetPath,\n\t\ttargetRelSuffix:             targetRelSuffix,\n\t\tresolvedDiscoveryWorkingDir: resolvedDiscoveryWorkingDir,\n\t\terrs:                        &errs,\n\t\terrMu:                       &errMu,\n\t}\n\n\tg, gCtx := errgroup.WithContext(ctx)\n\tg.SetLimit(p.numWorkers)\n\n\tfor _, candidate := range candidates {\n\t\tg.Go(func() error {\n\t\t\tdependent := p.processUpstreamCandidate(gCtx, l, upstreamState, candidate)\n\t\t\tif dependent != nil {\n\t\t\t\tdependentsMu.Lock()\n\n\t\t\t\tdiscoveredDependents = append(discoveredDependents, dependent)\n\n\t\t\t\tdependentsMu.Unlock()\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tif err := g.Wait(); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, dependent := range discoveredDependents {\n\t\tif loaded := state.seenComponents.LoadOrStore(dependent.Path()); loaded {\n\t\t\tcontinue\n\t\t}\n\n\t\tl.Debugf(\"Found dependent during upstream walk: %s (depends on target), adding to results\", dependent.Path())\n\n\t\tstate.results.AddDiscovered(DiscoveryResult{\n\t\t\tComponent: dependent,\n\t\t\tStatus:    StatusDiscovered,\n\t\t\tReason:    CandidacyReasonNone,\n\t\t\tPhase:     PhaseGraph,\n\t\t})\n\n\t\tl.Debugf(\"Successfully added %s to results\", dependent.Path())\n\n\t\tfreshVisitedDirs := newStringSet()\n\n\t\tl.Debugf(\"Recursively discovering dependents of %s from %s\", dependent.Path(), filepath.Dir(dependent.Path()))\n\n\t\terr := p.discoverDependentsUpstream(\n\t\t\tctx, l, state, dependent, freshVisitedDirs,\n\t\t\tfilepath.Dir(dependent.Path()), depthRemaining-1,\n\t\t)\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\n\tparentDir := filepath.Dir(currentDir)\n\tif parentDir != currentDir && depthRemaining > 0 {\n\t\terr := p.discoverDependentsUpstream(\n\t\t\tctx, l, state, target, visitedDirs,\n\t\t\tparentDir, depthRemaining-1,\n\t\t)\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn errors.Join(errs...)\n\t}\n\n\treturn nil\n}\n\n// processUpstreamCandidate processes a single candidate to check if it depends on the target.\n// Returns the canonical component if it depends on the target, nil otherwise.\n// This function is designed to be called concurrently from multiple goroutines.\nfunc (p *GraphPhase) processUpstreamCandidate(\n\tctx context.Context,\n\tl log.Logger,\n\tstate *upstreamDiscoveryState,\n\tcandidate component.Component,\n) component.Component {\n\tif loaded := state.checkedForTarget.LoadOrStore(candidate.Path()); loaded {\n\t\treturn nil\n\t}\n\n\tif state.graphTraversalState.seenComponents.Load(candidate.Path()) {\n\t\treturn nil\n\t}\n\n\tif _, ok := candidate.(*component.Stack); ok {\n\t\treturn nil\n\t}\n\n\tif candidate.Path() == state.target.Path() {\n\t\treturn nil\n\t}\n\n\tunit, ok := candidate.(*component.Unit)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\tcfg := unit.Config()\n\tif cfg == nil {\n\t\terr := parseComponent(ctx, l, candidate, state.graphTraversalState.opts, state.graphTraversalState.discovery)\n\t\tif err != nil {\n\t\t\tif !state.graphTraversalState.discovery.suppressParseErrors {\n\t\t\t\tstate.errMu.Lock()\n\n\t\t\t\t*state.errs = append(*state.errs, err)\n\n\t\t\t\tstate.errMu.Unlock()\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\n\t\tcfg = unit.Config()\n\t}\n\n\tdeps, err := extractDependencyPaths(cfg, candidate)\n\tif err != nil {\n\t\tstate.errMu.Lock()\n\n\t\t*state.errs = append(*state.errs, err)\n\n\t\tstate.errMu.Unlock()\n\n\t\treturn nil\n\t}\n\n\tcanonicalCandidate, created := state.graphTraversalState.threadSafeComponents.EnsureComponent(candidate)\n\tif created {\n\t\tdCtx := state.target.DiscoveryContext()\n\t\tif dCtx != nil {\n\t\t\tcopiedCtx := dCtx.CopyWithNewOrigin(component.OriginGraphDiscovery)\n\n\t\t\t// Clear the Ref and related args for graph-discovered components.\n\t\t\t// They shouldn't inherit the git ref from the target, as this would\n\t\t\t// cause them to match git filters and become targets themselves.\n\t\t\tcopiedCtx.Ref = \"\"\n\t\t\tcopiedCtx.Args = slices.DeleteFunc(copiedCtx.Args, func(arg string) bool {\n\t\t\t\treturn arg == \"-destroy\"\n\t\t\t})\n\n\t\t\tcanonicalCandidate.SetDiscoveryContext(copiedCtx)\n\t\t}\n\t}\n\n\tdependsOnTarget := false\n\n\tfor _, dep := range deps {\n\t\tdepComponent := componentFromDependencyPath(dep, state.graphTraversalState.threadSafeComponents)\n\t\tdepComponent, _ = state.graphTraversalState.threadSafeComponents.EnsureComponent(depComponent)\n\n\t\tparentCtx := canonicalCandidate.DiscoveryContext()\n\t\tif parentCtx != nil && isExternal(parentCtx.WorkingDir, dep) {\n\t\t\tif ext, ok := depComponent.(*component.Unit); ok {\n\t\t\t\text.SetExternal()\n\t\t\t}\n\t\t}\n\n\t\t// Compare paths: first try exact match, then try relative suffix match\n\t\t// for worktree scenarios where target is in a different directory.\n\t\tresolvedDep := util.ResolvePath(dep)\n\n\t\tswitch {\n\t\tcase resolvedDep == state.resolvedTargetPath:\n\t\t\t// Direct match - link to the existing depComponent\n\t\t\tcanonicalCandidate.AddDependency(depComponent)\n\n\t\t\tdependsOnTarget = true\n\t\tcase state.targetRelSuffix != \"\":\n\t\t\t// Compare relative suffixes when target is from a worktree.\n\t\t\t// Use resolved paths to handle symlinks consistently.\n\t\t\tdepRelSuffix := strings.TrimPrefix(resolvedDep, state.resolvedDiscoveryWorkingDir)\n\t\t\tif depRelSuffix == state.targetRelSuffix {\n\t\t\t\t// The dependency path matches the target's relative suffix.\n\t\t\t\t// Link to the actual target component instead of the path-based component,\n\t\t\t\t// so that the dependent relationship is properly established.\n\t\t\t\tcanonicalCandidate.AddDependency(state.target)\n\n\t\t\t\tdependsOnTarget = true\n\t\t\t} else {\n\t\t\t\tcanonicalCandidate.AddDependency(depComponent)\n\t\t\t}\n\t\tdefault:\n\t\t\tcanonicalCandidate.AddDependency(depComponent)\n\t\t}\n\t}\n\n\tif dependsOnTarget {\n\t\treturn canonicalCandidate\n\t}\n\n\treturn nil\n}\n\n// resolveDependency resolves a dependency path to a component.\nfunc (p *GraphPhase) resolveDependency(\n\tparent component.Component,\n\tdepPath string,\n\tthreadSafeComponents *component.ThreadSafeComponents,\n) (component.Component, error) {\n\tparentCtx := parent.DiscoveryContext()\n\tif parentCtx == nil {\n\t\treturn nil, NewMissingDiscoveryContextError(parent.Path())\n\t}\n\n\tif parentCtx.WorkingDir == \"\" {\n\t\treturn nil, NewMissingWorkingDirectoryError(parent.Path())\n\t}\n\n\tdepComponent := componentFromDependencyPath(depPath, threadSafeComponents)\n\n\taddedComponent, created := threadSafeComponents.EnsureComponent(depComponent)\n\tif created {\n\t\tcopiedCtx := parentCtx.CopyWithNewOrigin(component.OriginGraphDiscovery)\n\n\t\t// Clear the Ref and related args for graph-discovered dependencies.\n\t\t// They shouldn't inherit the git ref from the parent, as this would\n\t\t// cause them to match git filters and become targets themselves.\n\t\tcopiedCtx.Ref = \"\"\n\t\tcopiedCtx.Args = slices.DeleteFunc(copiedCtx.Args, func(arg string) bool {\n\t\t\treturn arg == \"-destroy\"\n\t\t})\n\n\t\tdepComponent.SetDiscoveryContext(copiedCtx)\n\t}\n\n\tif isExternal(parentCtx.WorkingDir, depPath) {\n\t\tif ext, ok := addedComponent.(*component.Unit); ok {\n\t\t\text.SetExternal()\n\t\t}\n\t}\n\n\tparent.AddDependency(addedComponent)\n\n\treturn addedComponent, nil\n}\n"
  },
  {
    "path": "internal/discovery/phase_parse.go",
    "content": "package discovery\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"path/filepath\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// ParsePhase parses HCL configurations for filter evaluation.\ntype ParsePhase struct {\n\t// numWorkers is the number of concurrent workers.\n\tnumWorkers int\n}\n\n// NewParsePhase creates a new ParsePhase.\nfunc NewParsePhase(numWorkers int) *ParsePhase {\n\tnumWorkers = max(numWorkers, defaultDiscoveryWorkers)\n\n\treturn &ParsePhase{\n\t\tnumWorkers: numWorkers,\n\t}\n}\n\n// Name returns the human-readable name of the phase.\nfunc (p *ParsePhase) Name() string {\n\treturn \"parse\"\n}\n\n// Kind returns the PhaseKind identifier.\nfunc (p *ParsePhase) Kind() PhaseKind {\n\treturn PhaseParse\n}\n\n// Run executes the parse phase.\nfunc (p *ParsePhase) Run(ctx context.Context, l log.Logger, input *PhaseInput) (*PhaseResults, error) {\n\tresults := NewPhaseResults()\n\tdiscovery := input.Discovery\n\n\tcomponentsToParse := make([]DiscoveryResult, 0, len(input.Candidates))\n\tfor _, candidate := range input.Candidates {\n\t\tif candidate.Reason == CandidacyReasonRequiresParse {\n\t\t\tcomponentsToParse = append(componentsToParse, candidate)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tresults.AddCandidate(candidate)\n\t}\n\n\t// When readFiles, parseExclude, or parseIncludes is enabled, also parse discovered components\n\t// to populate the Reading field, Exclude configuration, or ProcessedIncludes even without filters\n\tif discovery.readFiles || discovery.parseExclude || discovery.parseIncludes {\n\t\tfor _, c := range input.Components {\n\t\t\tcomponentsToParse = append(componentsToParse, DiscoveryResult{\n\t\t\t\tComponent: c,\n\t\t\t\tStatus:    StatusDiscovered,\n\t\t\t\tReason:    CandidacyReasonNone,\n\t\t\t\tPhase:     PhaseParse,\n\t\t\t})\n\t\t}\n\t}\n\n\tif len(componentsToParse) == 0 {\n\t\treturn results, nil\n\t}\n\n\tvar (\n\t\terrs  []error\n\t\terrMu sync.Mutex\n\t)\n\n\tg, ctx := errgroup.WithContext(ctx)\n\tg.SetLimit(p.numWorkers)\n\n\tfor _, candidate := range componentsToParse {\n\t\tg.Go(func() error {\n\t\t\tresult, err := p.parseAndReclassify(ctx, l, input.Opts, discovery, candidate)\n\t\t\tif err != nil {\n\t\t\t\terrMu.Lock()\n\n\t\t\t\terrs = append(errs, err)\n\n\t\t\t\terrMu.Unlock()\n\t\t\t\t// Return nil to continue processing other components\n\t\t\t\treturn nil //nolint:nilerr\n\t\t\t}\n\n\t\t\tif result == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tswitch result.Status {\n\t\t\tcase StatusDiscovered:\n\t\t\t\tresults.AddDiscovered(*result)\n\t\t\tcase StatusCandidate:\n\t\t\t\tresults.AddCandidate(*result)\n\t\t\tcase StatusExcluded:\n\t\t\t\t// Excluded components are not added\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tif err := g.Wait(); err != nil {\n\t\terrs = append(errs, err)\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn results, errors.Join(errs...)\n\t}\n\n\treturn results, nil\n}\n\n// parseAndReclassify parses a component and reclassifies it based on filter evaluation.\nfunc (p *ParsePhase) parseAndReclassify(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tdiscovery *Discovery,\n\tcandidate DiscoveryResult,\n) (*DiscoveryResult, error) {\n\tc := candidate.Component\n\n\tif err := parseComponent(ctx, l, c, opts, discovery); err != nil {\n\t\tif discovery.suppressParseErrors {\n\t\t\tl.Debugf(\"Suppressed parse error for %s: %v\", c.Path(), err)\n\n\t\t\treturn &DiscoveryResult{\n\t\t\t\tComponent: c,\n\t\t\t\tStatus:    StatusExcluded,\n\t\t\t\tReason:    CandidacyReasonNone,\n\t\t\t\tPhase:     PhaseParse,\n\t\t\t}, nil\n\t\t}\n\n\t\treturn nil, err\n\t}\n\n\tif discovery.classifier != nil {\n\t\tfor _, expr := range discovery.classifier.ParseExpressions() {\n\t\t\tmatched, err := filter.Evaluate(l, expr, component.Components{c})\n\t\t\tif err != nil {\n\t\t\t\tl.Debugf(\"Error evaluating parse expression for %s: %v\", c.Path(), err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif len(matched) > 0 {\n\t\t\t\treturn &DiscoveryResult{\n\t\t\t\t\tComponent: c,\n\t\t\t\t\tStatus:    StatusDiscovered,\n\t\t\t\t\tReason:    CandidacyReasonNone,\n\t\t\t\t\tPhase:     PhaseParse,\n\t\t\t\t}, nil\n\t\t\t}\n\t\t}\n\n\t\tclassCtx := filter.ClassificationContext{ParseDataAvailable: true}\n\t\tstatus, reason, graphIdx := discovery.classifier.Classify(c, classCtx)\n\n\t\treturn &DiscoveryResult{\n\t\t\tComponent:            c,\n\t\t\tStatus:               status,\n\t\t\tReason:               reason,\n\t\t\tPhase:                PhaseParse,\n\t\t\tGraphExpressionIndex: graphIdx,\n\t\t}, nil\n\t}\n\n\treturn &DiscoveryResult{\n\t\tComponent: c,\n\t\tStatus:    candidate.Status,\n\t\tReason:    candidate.Reason,\n\t\tPhase:     PhaseParse,\n\t}, nil\n}\n\n// parseComponent parses a Terragrunt configuration.\nfunc parseComponent(\n\tctx context.Context,\n\tl log.Logger,\n\tc component.Component,\n\topts *options.TerragruntOptions,\n\tdiscovery *Discovery,\n) error {\n\tparseOpts := opts.Clone()\n\n\tcomponentPath := c.Path()\n\tworkingDir := componentPath\n\n\tif util.FileExists(componentPath) && !util.IsDir(componentPath) {\n\t\tworkingDir = filepath.Dir(componentPath)\n\t}\n\n\tconfigFilename := config.DefaultTerragruntConfigPath\n\n\tswitch c.(type) {\n\tcase *component.Stack:\n\t\tconfigFilename = config.DefaultStackFile\n\tdefault:\n\t\tif unit, ok := c.(*component.Unit); ok && unit.ConfigFile() != \"\" {\n\t\t\tconfigFilename = unit.ConfigFile()\n\t\t\tbreak\n\t\t}\n\n\t\tif opts.TerragruntConfigPath != \"\" && !util.IsDir(opts.TerragruntConfigPath) {\n\t\t\tconfigFilename = filepath.Base(opts.TerragruntConfigPath)\n\t\t}\n\t}\n\n\tparseOpts.WorkingDir = workingDir\n\tparseOpts.Writers.Writer = io.Discard\n\tparseOpts.Writers.ErrWriter = io.Discard\n\tparseOpts.SkipOutput = true\n\tparseOpts.TerragruntConfigPath = filepath.Join(parseOpts.WorkingDir, configFilename)\n\tparseOpts.OriginalTerragruntConfigPath = parseOpts.TerragruntConfigPath\n\n\tctx, parsingCtx := configbridge.NewParsingContext(ctx, l, parseOpts)\n\tparsingCtx = parsingCtx.WithDecodeList(\n\t\tconfig.TerraformSource,\n\t\tconfig.DependenciesBlock,\n\t\tconfig.DependencyBlock,\n\t\tconfig.TerragruntFlags,\n\t\tconfig.FeatureFlagsBlock,\n\t\tconfig.ExcludeBlock,\n\t\tconfig.ErrorsBlock,\n\t\tconfig.RemoteStateBlock,\n\t\tconfig.TerragruntVersionConstraints,\n\t).WithSkipOutputsResolution()\n\n\tif len(discovery.parserOptions) > 0 {\n\t\tparsingCtx = parsingCtx.WithParseOption(discovery.parserOptions)\n\t}\n\n\tif discovery.suppressParseErrors {\n\t\tparserOpts := parsingCtx.ParserOptions\n\t\tparserOpts = append(parserOpts, hclparse.WithDiagnosticsHandler(func(\n\t\t\tfile *hcl.File,\n\t\t\thclDiags hcl.Diagnostics,\n\t\t) (hcl.Diagnostics, error) {\n\t\t\tl.Debugf(\"Suppressed parsing errors %v\", hclDiags)\n\t\t\treturn nil, nil\n\t\t}))\n\t\tparsingCtx = parsingCtx.WithParseOption(parserOpts)\n\t}\n\n\tcfg, err := config.PartialParseConfigFile(ctx, parsingCtx, l, parseOpts.TerragruntConfigPath, nil)\n\tif err != nil {\n\t\tif discovery.suppressParseErrors {\n\t\t\tvar notFoundErr config.TerragruntConfigNotFoundError\n\t\t\tif errors.As(err, &notFoundErr) {\n\t\t\t\tl.Debugf(\"Skipping missing config during discovery: %s\", parseOpts.TerragruntConfigPath)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\tif !discovery.suppressParseErrors || cfg == nil {\n\t\t\treturn err\n\t\t}\n\n\t\tl.Debugf(\"Suppressing parse error for %s: %s\", parseOpts.TerragruntConfigPath, err)\n\t}\n\n\tif unit, ok := c.(*component.Unit); ok {\n\t\tunit.StoreConfig(cfg)\n\t}\n\n\tif parsingCtx.FilesRead != nil {\n\t\treadFiles := sanitizeReadFiles(*parsingCtx.FilesRead)\n\t\tc.SetReading(readFiles...)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/discovery/phase_relationship.go",
    "content": "package discovery\n\nimport (\n\t\"context\"\n\t\"slices\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// RelationshipPhase builds dependency relationships between discovered components.\n// It discovers dependencies of \"orphan\" components (those without known dependencies)\n// to build a complete dependency graph for execution ordering.\ntype RelationshipPhase struct {\n\t// numWorkers is the number of concurrent workers.\n\tnumWorkers int\n\t// maxDepth is the maximum depth for relationship discovery.\n\tmaxDepth int\n}\n\n// relationshipTraversalState consolidates state for relationship discovery.\ntype relationshipTraversalState struct {\n\topts                     *options.TerragruntOptions\n\tdiscovery                *Discovery\n\tallComponents            *component.Components\n\tinterTransientComponents *component.ThreadSafeComponents\n}\n\n// NewRelationshipPhase creates a new RelationshipPhase.\nfunc NewRelationshipPhase(numWorkers, maxDepth int) *RelationshipPhase {\n\tnumWorkers = max(numWorkers, defaultDiscoveryWorkers)\n\n\tif maxDepth <= 0 {\n\t\tmaxDepth = defaultMaxDependencyDepth\n\t}\n\n\treturn &RelationshipPhase{\n\t\tnumWorkers: numWorkers,\n\t\tmaxDepth:   maxDepth,\n\t}\n}\n\n// Name returns the human-readable name of the phase.\nfunc (p *RelationshipPhase) Name() string {\n\treturn \"relationship\"\n}\n\n// Kind returns the PhaseKind identifier.\nfunc (p *RelationshipPhase) Kind() PhaseKind {\n\treturn PhaseRelationship\n}\n\n// Run executes the relationship discovery phase.\nfunc (p *RelationshipPhase) Run(ctx context.Context, l log.Logger, input *PhaseInput) (*PhaseResults, error) {\n\tresults := NewPhaseResults()\n\n\terr := p.runRelationshipDiscovery(ctx, l, input, results)\n\n\treturn results, err\n}\n\n// runRelationshipDiscovery performs the actual relationship discovery.\nfunc (p *RelationshipPhase) runRelationshipDiscovery(\n\tctx context.Context,\n\tl log.Logger,\n\tinput *PhaseInput,\n\t_ *PhaseResults,\n) error {\n\tdiscovery := input.Discovery\n\tif discovery == nil || !discovery.discoverRelationships {\n\t\treturn nil\n\t}\n\n\tinterTransientComponents := component.NewThreadSafeComponents(component.Components{})\n\n\tstate := &relationshipTraversalState{\n\t\topts:                     input.Opts,\n\t\tdiscovery:                discovery,\n\t\tallComponents:            &input.Components,\n\t\tinterTransientComponents: interTransientComponents,\n\t}\n\n\tvar (\n\t\terrs  = make([]error, 0, len(input.Components))\n\t\terrMu sync.Mutex\n\t)\n\n\tg, ctx := errgroup.WithContext(ctx)\n\tg.SetLimit(p.numWorkers)\n\n\tfor _, c := range input.Components {\n\t\t// terminalTracker tracks components that, if encountered, indicate we can stop\n\t\t// traversal, as they are terminal components in the dependency graph.\n\t\tterminalTracker := newTerminalTracker(slices.Collect(func(yield func(component.Component) bool) {\n\t\t\tfor _, rc := range input.Components {\n\t\t\t\tif rc != nil && rc != c {\n\t\t\t\t\tif !yield(rc) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}))\n\n\t\tg.Go(func() error {\n\t\t\terr := p.discoverRelationships(ctx, l, state, c, terminalTracker, p.maxDepth)\n\t\t\tif err != nil {\n\t\t\t\terrMu.Lock()\n\n\t\t\t\terrs = append(errs, err)\n\n\t\t\t\terrMu.Unlock()\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tif err := g.Wait(); err != nil {\n\t\terrs = append(errs, err)\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn errors.Join(errs...)\n\t}\n\n\treturn nil\n}\n\n// discoverRelationships discovers dependencies for a single component.\nfunc (p *RelationshipPhase) discoverRelationships(\n\tctx context.Context,\n\tl log.Logger,\n\tstate *relationshipTraversalState,\n\tc component.Component,\n\ttracker *terminalTracker,\n\tdepthRemaining int,\n) error {\n\tif depthRemaining <= 0 {\n\t\treturn nil\n\t}\n\n\tif _, ok := c.(*component.Stack); ok {\n\t\treturn nil\n\t}\n\n\tunit, ok := c.(*component.Unit)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\tcfg := unit.Config()\n\tif cfg == nil {\n\t\terr := parseComponent(ctx, l, c, state.opts, state.discovery)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcfg = unit.Config()\n\t}\n\n\tpaths, err := extractDependencyPaths(cfg, c)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(paths) == 0 {\n\t\treturn nil\n\t}\n\n\tdepsToDiscover := make(component.Components, 0, len(paths))\n\n\tfor _, path := range paths {\n\t\tdep, created := p.dependencyToDiscover(c, path, state.allComponents, state.interTransientComponents, state.discovery)\n\n\t\ttracker.remove(dep.Path())\n\n\t\tif created {\n\t\t\tdepsToDiscover = append(depsToDiscover, dep)\n\t\t}\n\t}\n\n\tif len(depsToDiscover) == 0 {\n\t\treturn nil\n\t}\n\n\tif tracker.isEmpty() {\n\t\treturn nil\n\t}\n\n\tvar (\n\t\terrs  = make([]error, 0, len(depsToDiscover))\n\t\terrMu sync.Mutex\n\t)\n\n\tg, ctx := errgroup.WithContext(ctx)\n\tg.SetLimit(p.numWorkers)\n\n\tfor _, dep := range depsToDiscover {\n\t\tg.Go(func() error {\n\t\t\terr := p.discoverRelationships(\n\t\t\t\tctx,\n\t\t\t\tl,\n\t\t\t\tstate,\n\t\t\t\tdep,\n\t\t\t\ttracker,\n\t\t\t\tdepthRemaining-1,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\terrMu.Lock()\n\n\t\t\t\terrs = append(errs, err)\n\n\t\t\t\terrMu.Unlock()\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tif err := g.Wait(); err != nil {\n\t\treturn err\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn errors.Join(errs...)\n\t}\n\n\treturn nil\n}\n\n// dependencyToDiscover resolves a dependency path and links it to the component.\nfunc (p *RelationshipPhase) dependencyToDiscover(\n\tc component.Component,\n\tpath string,\n\tallComponents *component.Components,\n\tinterTransientComponents *component.ThreadSafeComponents,\n\tdiscovery *Discovery,\n) (component.Component, bool) {\n\tfor _, dep := range *allComponents {\n\t\tif dep.Path() == path {\n\t\t\tif !slices.Contains(c.Dependencies(), dep) {\n\t\t\t\tc.AddDependency(dep)\n\t\t\t}\n\n\t\t\treturn dep, false\n\t\t}\n\t}\n\n\tnewUnit := component.NewUnit(path)\n\n\tdep, created := interTransientComponents.EnsureComponent(newUnit)\n\n\tif discovery.discoveryContext != nil {\n\t\tdiscoveryCtx := discovery.discoveryContext.Copy()\n\t\tdiscoveryCtx.SuggestOrigin(component.OriginRelationshipDiscovery)\n\t\tdep.SetDiscoveryContext(discoveryCtx)\n\n\t\tif isExternal(discoveryCtx.WorkingDir, path) {\n\t\t\tdep.SetExternal()\n\t\t}\n\t}\n\n\tc.AddDependency(dep)\n\n\treturn dep, created\n}\n\n// terminalTracker provides thread-safe tracking of terminal components.\n// Components are removed as they're discovered as dependencies.\n// When empty, relationship discovery can stop early.\ntype terminalTracker struct {\n\tcomponents component.Components\n\tmu         sync.RWMutex\n}\n\nfunc newTerminalTracker(components component.Components) *terminalTracker {\n\treturn &terminalTracker{\n\t\tcomponents: components,\n\t}\n}\n\nfunc (t *terminalTracker) remove(path string) {\n\tt.mu.Lock()\n\tdefer t.mu.Unlock()\n\n\tt.components = slices.DeleteFunc(t.components, func(tc component.Component) bool {\n\t\treturn tc != nil && tc.Path() == path\n\t})\n}\n\nfunc (t *terminalTracker) isEmpty() bool {\n\tt.mu.RLock()\n\tdefer t.mu.RUnlock()\n\n\treturn len(t.components) == 0\n}\n"
  },
  {
    "path": "internal/discovery/phase_test.go",
    "content": "package discovery_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestFilesystemPhase_BasicDiscovery tests the filesystem phase directly.\nfunc TestFilesystemPhase_BasicDiscovery(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create test directory structure\n\tunit1Dir := filepath.Join(tmpDir, \"unit1\")\n\tunit2Dir := filepath.Join(tmpDir, \"unit2\")\n\tstackDir := filepath.Join(tmpDir, \"stack1\")\n\n\ttestDirs := []string{unit1Dir, unit2Dir, stackDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(unit1Dir, \"terragrunt.hcl\"):       \"\",\n\t\tfilepath.Join(unit2Dir, \"terragrunt.hcl\"):       \"\",\n\t\tfilepath.Join(stackDir, \"terragrunt.stack.hcl\"): \"\",\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Run filesystem phase via full discovery\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir})\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// Verify phase discovered all components\n\tunits := components.Filter(component.UnitKind)\n\tstacks := components.Filter(component.StackKind)\n\n\tassert.Len(t, units, 2)\n\tassert.Len(t, stacks, 1)\n}\n\n// TestFilesystemPhase_SkipsIgnorableDirs tests that .git, .terraform, .terragrunt-cache are skipped.\nfunc TestFilesystemPhase_SkipsIgnorableDirs(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create valid unit\n\tunitDir := filepath.Join(tmpDir, \"unit\")\n\trequire.NoError(t, os.MkdirAll(unitDir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(unitDir, \"terragrunt.hcl\"), []byte(\"\"), 0644))\n\n\t// Create units in ignorable directories (should be skipped)\n\tignorableDirs := []string{\".git\", \".terraform\", \".terragrunt-cache\"}\n\tfor _, dir := range ignorableDirs {\n\t\tignorableUnit := filepath.Join(tmpDir, dir, \"ignored\")\n\t\trequire.NoError(t, os.MkdirAll(ignorableUnit, 0755))\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(ignorableUnit, \"terragrunt.hcl\"), []byte(\"\"), 0644))\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir})\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// Should only find the valid unit, not the ones in ignorable directories\n\tassert.Len(t, components, 1)\n\tassert.Equal(t, unitDir, components[0].Path())\n}\n\n// TestFilesystemPhase_WithNoHidden tests hidden directory filtering.\nfunc TestFilesystemPhase_WithNoHidden(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create visible unit\n\tvisibleDir := filepath.Join(tmpDir, \"visible\")\n\trequire.NoError(t, os.MkdirAll(visibleDir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(visibleDir, \"terragrunt.hcl\"), []byte(\"\"), 0644))\n\n\t// Create hidden unit\n\thiddenDir := filepath.Join(tmpDir, \".hidden\", \"unit\")\n\trequire.NoError(t, os.MkdirAll(hiddenDir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(hiddenDir, \"terragrunt.hcl\"), []byte(\"\"), 0644))\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\tt.Run(\"without noHidden\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir})\n\n\t\tcomponents, err := d.Discover(ctx, l, opts)\n\t\trequire.NoError(t, err)\n\t\tassert.Len(t, components, 2, \"Should find both visible and hidden\")\n\t})\n\n\tt.Run(\"with noHidden\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\td := discovery.NewDiscovery(tmpDir).\n\t\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\t\tWithNoHidden()\n\n\t\tcomponents, err := d.Discover(ctx, l, opts)\n\t\trequire.NoError(t, err)\n\t\tassert.Len(t, components, 1, \"Should find only visible\")\n\t\tassert.Equal(t, visibleDir, components[0].Path())\n\t})\n}\n\n// TestParsePhase_ParsesConfigsForParseRequiredFilters tests that parse phase handles parse-required filters.\nfunc TestParsePhase_ParsesConfigsForParseRequiredFilters(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create shared file\n\tsharedFile := filepath.Join(tmpDir, \"shared.hcl\")\n\trequire.NoError(t, os.WriteFile(sharedFile, []byte(`\nlocals {\n\tvalue = \"test\"\n}\n`), 0644))\n\n\t// Create unit that reads the shared file\n\tunitDir := filepath.Join(tmpDir, \"unit\")\n\trequire.NoError(t, os.MkdirAll(unitDir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(unitDir, \"terragrunt.hcl\"), []byte(`\nlocals {\n\tshared = read_terragrunt_config(\"../shared.hcl\")\n}\n`), 0644))\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Filter with reading= attribute requires parsing\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"reading=shared.hcl\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithFilters(filters).\n\t\tWithReadFiles()\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\tassert.Len(t, components, 1)\n\tassert.Equal(t, unitDir, components[0].Path())\n}\n\n// TestGraphPhase_DependencyDiscovery tests the graph phase dependency discovery.\nfunc TestGraphPhase_DependencyDiscovery(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create dependency chain: vpc -> db -> app\n\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\tdbDir := filepath.Join(tmpDir, \"db\")\n\tappDir := filepath.Join(tmpDir, \"app\")\n\n\ttestDirs := []string{vpcDir, dbDir, appDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(appDir, \"terragrunt.hcl\"): `\ndependency \"db\" {\n\tconfig_path = \"../db\"\n}\n`,\n\t\tfilepath.Join(dbDir, \"terragrunt.hcl\"): `\ndependency \"vpc\" {\n\tconfig_path = \"../vpc\"\n}\n`,\n\t\tfilepath.Join(vpcDir, \"terragrunt.hcl\"): ``,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Use graph filter to trigger graph phase\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"app...\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// Graph phase should discover all dependencies\n\tpaths := components.Paths()\n\tassert.Contains(t, paths, appDir)\n\tassert.Contains(t, paths, dbDir)\n\tassert.Contains(t, paths, vpcDir)\n\n\t// Verify dependency relationships are built\n\tvar appComponent component.Component\n\n\tfor _, c := range components {\n\t\tif c.Path() == appDir {\n\t\t\tappComponent = c\n\t\t\tbreak\n\t\t}\n\t}\n\n\trequire.NotNil(t, appComponent)\n\tassert.Contains(t, appComponent.Dependencies().Paths(), dbDir)\n}\n\n// TestGraphPhase_DependentDiscoveryRequiresRelationships tests that dependent discovery\n// requires relationships to be built first. This is a behavioral test documenting the\n// current implementation's requirements for dependent traversal.\nfunc TestGraphPhase_DependentDiscoveryRequiresRelationships(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create dependency chain: vpc -> db -> app\n\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\tdbDir := filepath.Join(tmpDir, \"db\")\n\tappDir := filepath.Join(tmpDir, \"app\")\n\n\ttestDirs := []string{vpcDir, dbDir, appDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(appDir, \"terragrunt.hcl\"): `\ndependency \"db\" {\n\tconfig_path = \"../db\"\n}\n`,\n\t\tfilepath.Join(dbDir, \"terragrunt.hcl\"): `\ndependency \"vpc\" {\n\tconfig_path = \"../vpc\"\n}\n`,\n\t\tfilepath.Join(vpcDir, \"terragrunt.hcl\"): ``,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Using dependent filter (...vpc) without pre-built relationships\n\t// Currently, the implementation requires relationships to be built\n\t// before dependent traversal can work (unlike dependency traversal which\n\t// parses configs on-the-fly)\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"...vpc\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// The vpc component should always be discovered (it's the target)\n\tpaths := components.Paths()\n\tassert.Contains(t, paths, vpcDir, \"vpc should always be included as the target\")\n}\n\n// TestRelationshipPhase_BuildsRelationships tests the relationship phase.\nfunc TestRelationshipPhase_BuildsRelationships(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create components with dependencies\n\tappDir := filepath.Join(tmpDir, \"app\")\n\tdbDir := filepath.Join(tmpDir, \"db\")\n\n\ttestDirs := []string{appDir, dbDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(appDir, \"terragrunt.hcl\"): `\ndependency \"db\" {\n\tconfig_path = \"../db\"\n}\n`,\n\t\tfilepath.Join(dbDir, \"terragrunt.hcl\"): ``,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Use WithRelationships to enable relationship phase\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithRelationships()\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// Verify relationships are built\n\tvar appComponent component.Component\n\n\tfor _, c := range components {\n\t\tif c.Path() == appDir {\n\t\t\tappComponent = c\n\t\t\tbreak\n\t\t}\n\t}\n\n\trequire.NotNil(t, appComponent)\n\tdepPaths := appComponent.Dependencies().Paths()\n\tassert.Contains(t, depPaths, dbDir)\n}\n\n// TestCandidacyClassifier_AnalyzesFiltersCorrectly tests the candidacy classifier analysis.\nfunc TestCandidacyClassifier_AnalyzesFiltersCorrectly(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\ttests := []struct {\n\t\tname                   string\n\t\tfilterStrings          []string\n\t\texpectHasPositive      bool\n\t\texpectHasParseRequired bool\n\t\texpectHasGraphFilters  bool\n\t\texpectGraphExprCount   int\n\t}{\n\t\t{\n\t\t\tname:              \"empty filters\",\n\t\t\tfilterStrings:     []string{},\n\t\t\texpectHasPositive: false,\n\t\t},\n\t\t{\n\t\t\tname:              \"simple path filter\",\n\t\t\tfilterStrings:     []string{\"./foo\"},\n\t\t\texpectHasPositive: true,\n\t\t},\n\t\t{\n\t\t\tname:              \"negated path filter only\",\n\t\t\tfilterStrings:     []string{\"!./foo\"},\n\t\t\texpectHasPositive: false,\n\t\t},\n\t\t{\n\t\t\tname:              \"path filter with negation\",\n\t\t\tfilterStrings:     []string{\"./foo\", \"!./bar\"},\n\t\t\texpectHasPositive: true,\n\t\t},\n\t\t{\n\t\t\tname:                   \"reading attribute filter\",\n\t\t\tfilterStrings:          []string{\"reading=config/*\"},\n\t\t\texpectHasPositive:      true,\n\t\t\texpectHasParseRequired: true,\n\t\t},\n\t\t{\n\t\t\tname:                  \"dependency graph filter\",\n\t\t\tfilterStrings:         []string{\"./foo...\"},\n\t\t\texpectHasPositive:     true,\n\t\t\texpectHasGraphFilters: true,\n\t\t\texpectGraphExprCount:  1,\n\t\t},\n\t\t{\n\t\t\tname:                  \"dependent graph filter\",\n\t\t\tfilterStrings:         []string{\"..../foo\"},\n\t\t\texpectHasPositive:     true,\n\t\t\texpectHasGraphFilters: true,\n\t\t\texpectGraphExprCount:  1,\n\t\t},\n\t\t{\n\t\t\tname:                  \"exclude target graph filter\",\n\t\t\tfilterStrings:         []string{\"^{./foo}...\"},\n\t\t\texpectHasPositive:     true,\n\t\t\texpectHasGraphFilters: true,\n\t\t\texpectGraphExprCount:  1,\n\t\t},\n\t\t{\n\t\t\tname:                  \"multiple graph filters\",\n\t\t\tfilterStrings:         []string{\"./foo...\", \"..../bar\"},\n\t\t\texpectHasPositive:     true,\n\t\t\texpectHasGraphFilters: true,\n\t\t\texpectGraphExprCount:  2,\n\t\t},\n\t\t{\n\t\t\tname:              \"name attribute filter\",\n\t\t\tfilterStrings:     []string{\"name=my-app\"},\n\t\t\texpectHasPositive: true,\n\t\t},\n\t\t{\n\t\t\tname:              \"type attribute filter\",\n\t\t\tfilterStrings:     []string{\"type=unit\"},\n\t\t\texpectHasPositive: true,\n\t\t},\n\t\t{\n\t\t\tname:              \"external attribute filter\",\n\t\t\tfilterStrings:     []string{\"external=true\"},\n\t\t\texpectHasPositive: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tfilters, err := filter.ParseFilterQueries(l, tt.filterStrings)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tclassifier := filter.NewClassifier(filters)\n\n\t\t\tassert.Equal(t, tt.expectHasPositive, classifier.HasPositiveFilters(), \"HasPositiveFilters mismatch\")\n\t\t\tassert.Equal(t, tt.expectHasParseRequired, classifier.HasParseRequiredFilters(), \"HasParseRequiredFilters mismatch\")\n\t\t\tassert.Equal(t, tt.expectHasGraphFilters, classifier.HasGraphFilters(), \"HasGraphFilters mismatch\")\n\n\t\t\tif tt.expectGraphExprCount > 0 {\n\t\t\t\tassert.Len(t, classifier.GraphExpressions(), tt.expectGraphExprCount, \"GraphExpressions count mismatch\")\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestCandidacyClassifier_ClassifiesComponentsCorrectly tests component classification.\nfunc TestCandidacyClassifier_ClassifiesComponentsCorrectly(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\ttests := []struct {\n\t\tname          string\n\t\tcomponentPath string\n\t\tworkingDir    string\n\t\tfilterStrings []string\n\t\texpectStatus  filter.ClassificationStatus\n\t\texpectReason  filter.CandidacyReason\n\t}{\n\t\t{\n\t\t\tname:          \"no filters - include by default\",\n\t\t\tfilterStrings: []string{},\n\t\t\tcomponentPath: \"/project/foo\",\n\t\t\tworkingDir:    \"/project\",\n\t\t\texpectStatus:  filter.StatusDiscovered,\n\t\t\texpectReason:  filter.CandidacyReasonNone,\n\t\t},\n\t\t{\n\t\t\tname:          \"matching path filter\",\n\t\t\tfilterStrings: []string{\"./foo\"},\n\t\t\tcomponentPath: \"/project/foo\",\n\t\t\tworkingDir:    \"/project\",\n\t\t\texpectStatus:  filter.StatusDiscovered,\n\t\t\texpectReason:  filter.CandidacyReasonNone,\n\t\t},\n\t\t{\n\t\t\tname:          \"non-matching path filter - exclude by default\",\n\t\t\tfilterStrings: []string{\"./bar\"},\n\t\t\tcomponentPath: \"/project/foo\",\n\t\t\tworkingDir:    \"/project\",\n\t\t\texpectStatus:  filter.StatusExcluded,\n\t\t\texpectReason:  filter.CandidacyReasonNone,\n\t\t},\n\t\t{\n\t\t\tname:          \"negated filter only - exclude component\",\n\t\t\tfilterStrings: []string{\"!./foo\"},\n\t\t\tcomponentPath: \"/project/foo\",\n\t\t\tworkingDir:    \"/project\",\n\t\t\texpectStatus:  filter.StatusExcluded,\n\t\t\texpectReason:  filter.CandidacyReasonNone,\n\t\t},\n\t\t{\n\t\t\tname:          \"negated filter only - include other\",\n\t\t\tfilterStrings: []string{\"!./foo\"},\n\t\t\tcomponentPath: \"/project/bar\",\n\t\t\tworkingDir:    \"/project\",\n\t\t\texpectStatus:  filter.StatusDiscovered,\n\t\t\texpectReason:  filter.CandidacyReasonNone,\n\t\t},\n\t\t{\n\t\t\tname:          \"graph expression target - candidate\",\n\t\t\tfilterStrings: []string{\"./foo...\"},\n\t\t\tcomponentPath: \"/project/foo\",\n\t\t\tworkingDir:    \"/project\",\n\t\t\texpectStatus:  filter.StatusCandidate,\n\t\t\texpectReason:  filter.CandidacyReasonGraphTarget,\n\t\t},\n\t\t{\n\t\t\tname:          \"parse required filter - candidate\",\n\t\t\tfilterStrings: []string{\"reading=config/*\"},\n\t\t\tcomponentPath: \"/project/foo\",\n\t\t\tworkingDir:    \"/project\",\n\t\t\texpectStatus:  filter.StatusCandidate,\n\t\t\texpectReason:  filter.CandidacyReasonRequiresParse,\n\t\t},\n\t\t{\n\t\t\tname:          \"wildcard path filter match\",\n\t\t\tfilterStrings: []string{\"./apps/*\"},\n\t\t\tcomponentPath: \"/project/apps/frontend\",\n\t\t\tworkingDir:    \"/project\",\n\t\t\texpectStatus:  filter.StatusDiscovered,\n\t\t\texpectReason:  filter.CandidacyReasonNone,\n\t\t},\n\t\t{\n\t\t\tname:          \"name filter match\",\n\t\t\tfilterStrings: []string{\"name=foo\"},\n\t\t\tcomponentPath: \"/project/foo\",\n\t\t\tworkingDir:    \"/project\",\n\t\t\texpectStatus:  filter.StatusDiscovered,\n\t\t\texpectReason:  filter.CandidacyReasonNone,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tfilters, err := filter.ParseFilterQueries(l, tt.filterStrings)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tclassifier := filter.NewClassifier(filters)\n\n\t\t\t// Create a test component\n\t\t\tc := component.NewUnit(tt.componentPath)\n\t\t\tc.SetDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\tWorkingDir: tt.workingDir,\n\t\t\t})\n\n\t\t\tctx := filter.ClassificationContext{}\n\t\t\tstatus, reason, _ := classifier.Classify(c, ctx)\n\n\t\t\tassert.Equal(t, tt.expectStatus, status, \"status mismatch\")\n\t\t\tassert.Equal(t, tt.expectReason, reason, \"reason mismatch\")\n\t\t})\n\t}\n}\n\n// TestClassifier_ParseExpressions tests the ParseExpressions method.\nfunc TestClassifier_ParseExpressions(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"reading=config/*\", \"reading=shared.hcl\"})\n\trequire.NoError(t, err)\n\n\tclassifier := filter.NewClassifier(filters)\n\n\tparseExprs := classifier.ParseExpressions()\n\tassert.Len(t, parseExprs, 2, \"Should have 2 parse expressions\")\n}\n\n// TestClassifier_NegatedExpressions tests the NegatedExpressions method.\nfunc TestClassifier_NegatedExpressions(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"!./foo\", \"!./bar\", \"./baz\"})\n\trequire.NoError(t, err)\n\n\tclassifier := filter.NewClassifier(filters)\n\n\tnegatedExprs := classifier.NegatedExpressions()\n\tassert.Len(t, negatedExprs, 2, \"Should have 2 negated expressions\")\n}\n\n// TestClassifier_HasDependentFilters tests the HasDependentFilters method.\nfunc TestClassifier_HasDependentFilters(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\ttests := []struct {\n\t\tname          string\n\t\tfilterStrings []string\n\t\texpectResult  bool\n\t}{\n\t\t{\n\t\t\tname:          \"no graph filters\",\n\t\t\tfilterStrings: []string{\"./foo\"},\n\t\t\texpectResult:  false,\n\t\t},\n\t\t{\n\t\t\tname:          \"dependency only filter - app...\",\n\t\t\tfilterStrings: []string{\"app...\"},\n\t\t\texpectResult:  false,\n\t\t},\n\t\t{\n\t\t\tname:          \"dependent only filter - ...vpc\",\n\t\t\tfilterStrings: []string{\"...vpc\"},\n\t\t\texpectResult:  true,\n\t\t},\n\t\t{\n\t\t\tname:          \"bidirectional filter - ...db...\",\n\t\t\tfilterStrings: []string{\"...db...\"},\n\t\t\texpectResult:  true,\n\t\t},\n\t\t{\n\t\t\tname:          \"exclude target dependent - ...^vpc\",\n\t\t\tfilterStrings: []string{\"...^vpc\"},\n\t\t\texpectResult:  true,\n\t\t},\n\t\t{\n\t\t\tname:          \"multiple filters with dependent\",\n\t\t\tfilterStrings: []string{\"app...\", \"...vpc\"},\n\t\t\texpectResult:  true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tfilters, err := filter.ParseFilterQueries(l, tt.filterStrings)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tclassifier := filter.NewClassifier(filters)\n\n\t\t\tassert.Equal(t, tt.expectResult, classifier.HasDependentFilters(), \"HasDependentFilters mismatch\")\n\t\t})\n\t}\n}\n\n// TestGraphPhase_DependentDiscovery_WithPreBuiltGraph tests that dependent discovery\n// works correctly when the dependency graph is pre-built.\nfunc TestGraphPhase_DependentDiscovery_WithPreBuiltGraph(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create dependency chain: vpc -> db -> app\n\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\tdbDir := filepath.Join(tmpDir, \"db\")\n\tappDir := filepath.Join(tmpDir, \"app\")\n\n\ttestDirs := []string{vpcDir, dbDir, appDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\ttestFiles := map[string]string{\n\t\tfilepath.Join(appDir, \"terragrunt.hcl\"): `\ndependency \"db\" {\n\tconfig_path = \"../db\"\n}\n`,\n\t\tfilepath.Join(dbDir, \"terragrunt.hcl\"): `\ndependency \"vpc\" {\n\tconfig_path = \"../vpc\"\n}\n`,\n\t\tfilepath.Join(vpcDir, \"terragrunt.hcl\"): ``,\n\t}\n\n\tfor path, content := range testFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tl := logger.CreateLogger()\n\topts := &options.TerragruntOptions{\n\t\tWorkingDir:     tmpDir,\n\t\tRootWorkingDir: tmpDir,\n\t}\n\n\tctx := t.Context()\n\n\t// Using dependent filter (...vpc) should now work with pre-built graph\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"...vpc\"})\n\trequire.NoError(t, err)\n\n\td := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(&component.DiscoveryContext{WorkingDir: tmpDir}).\n\t\tWithFilters(filters)\n\n\tcomponents, err := d.Discover(ctx, l, opts)\n\trequire.NoError(t, err)\n\n\t// With pre-built dependency graph, dependent discovery should now find all dependents\n\tpaths := components.Paths()\n\tassert.Contains(t, paths, vpcDir, \"vpc should be included as the target\")\n\tassert.Contains(t, paths, dbDir, \"db should be included as direct dependent of vpc\")\n\tassert.Contains(t, paths, appDir, \"app should be included as transitive dependent of vpc\")\n}\n"
  },
  {
    "path": "internal/discovery/phase_worktree.go",
    "content": "package discovery\n\nimport (\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/worktrees\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// WorktreePhase discovers components in Git worktrees for Git-based filters.\ntype WorktreePhase struct {\n\t// gitExpressions contains Git filter expressions that require worktree discovery.\n\tgitExpressions filter.GitExpressions\n\t// numWorkers is the number of concurrent workers.\n\tnumWorkers int\n}\n\n// NewWorktreePhase creates a new WorktreePhase.\nfunc NewWorktreePhase(gitExpressions filter.GitExpressions, numWorkers int) *WorktreePhase {\n\tif numWorkers <= 0 {\n\t\tnumWorkers = runtime.NumCPU()\n\t}\n\n\treturn &WorktreePhase{\n\t\tgitExpressions: gitExpressions,\n\t\tnumWorkers:     numWorkers,\n\t}\n}\n\n// Name returns the human-readable name of the phase.\nfunc (p *WorktreePhase) Name() string {\n\treturn \"worktree\"\n}\n\n// Kind returns the PhaseKind identifier.\nfunc (p *WorktreePhase) Kind() PhaseKind {\n\treturn PhaseWorktree\n}\n\n// NumWorkers returns the number of concurrent workers.\nfunc (p *WorktreePhase) NumWorkers() int {\n\treturn p.numWorkers\n}\n\n// Run executes the worktree discovery phase.\nfunc (p *WorktreePhase) Run(ctx context.Context, l log.Logger, input *PhaseInput) (*PhaseResults, error) {\n\tresults := NewPhaseResults()\n\n\tdiscovery := input.Discovery\n\tif discovery == nil || discovery.worktrees == nil {\n\t\tl.Debug(\"No worktrees provided, skipping worktree discovery\")\n\t\treturn results, nil\n\t}\n\n\tw := discovery.worktrees\n\tif len(w.WorktreePairs) == 0 {\n\t\tl.Debug(\"No worktree pairs available, skipping worktree discovery\")\n\t\treturn results, nil\n\t}\n\n\tdiscoveredComponents := component.NewThreadSafeComponents(component.Components{})\n\n\tdiscoveryGroup, discoveryCtx := errgroup.WithContext(ctx)\n\tdiscoveryGroup.SetLimit(p.numWorkers)\n\n\tfor _, pair := range w.WorktreePairs {\n\t\tdiscoveryGroup.Go(func() error {\n\t\t\tfromFilters, toFilters, err := pair.Expand()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfromToG, fromToCtx := errgroup.WithContext(discoveryCtx)\n\n\t\t\tif len(fromFilters) > 0 {\n\t\t\t\tfromToG.Go(func() error {\n\t\t\t\t\tcomponents, err := p.discoverInWorktree(fromToCtx, l, input, pair.FromWorktree, fromFilters, FromWorktreeKind)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tfor _, c := range components {\n\t\t\t\t\t\tdiscoveredComponents.EnsureComponent(c)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif len(toFilters) > 0 {\n\t\t\t\tfromToG.Go(func() error {\n\t\t\t\t\tcomponents, err := p.discoverInWorktree(fromToCtx, l, input, pair.ToWorktree, toFilters, ToWorktreeKind)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tfor _, c := range components {\n\t\t\t\t\t\tdiscoveredComponents.EnsureComponent(c)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t\t}\n\n\t\t\treturn fromToG.Wait()\n\t\t})\n\t}\n\n\tdiscoveryGroup.Go(func() error {\n\t\tcomponents, err := p.discoverChangesInWorktreeStacks(discoveryCtx, l, input, w)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor _, c := range components {\n\t\t\tdiscoveredComponents.EnsureComponent(c)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err := discoveryGroup.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, c := range discoveredComponents.ToComponents() {\n\t\tstatus, reason, graphIdx := StatusDiscovered, CandidacyReasonNone, -1\n\n\t\tif input.Classifier != nil {\n\t\t\tclassCtx := filter.ClassificationContext{}\n\t\t\tstatus, reason, graphIdx = input.Classifier.Classify(c, classCtx)\n\t\t}\n\n\t\tresult := DiscoveryResult{\n\t\t\tComponent:            c,\n\t\t\tStatus:               status,\n\t\t\tReason:               reason,\n\t\t\tPhase:                PhaseWorktree,\n\t\t\tGraphExpressionIndex: graphIdx,\n\t\t}\n\n\t\tswitch result.Status {\n\t\tcase StatusDiscovered:\n\t\t\tresults.AddDiscovered(result)\n\t\tcase StatusCandidate:\n\t\t\tresults.AddCandidate(result)\n\t\tcase StatusExcluded:\n\t\t\t// Excluded components are not added\n\t\t}\n\t}\n\n\treturn results, nil\n}\n\n// discoverInWorktree discovers components in a single worktree.\nfunc (p *WorktreePhase) discoverInWorktree(\n\tctx context.Context,\n\tl log.Logger,\n\tinput *PhaseInput,\n\twt worktrees.Worktree,\n\tfilters filter.Filters,\n\tkind WorktreeKind,\n) (component.Components, error) {\n\tdiscovery := input.Discovery\n\n\tdiscoveryContext := discovery.discoveryContext.Copy()\n\tdiscoveryContext.Ref = wt.Ref\n\tdiscoveryContext.WorkingDir = wt.Path\n\tdiscoveryContext.SuggestOrigin(component.OriginWorktreeDiscovery)\n\n\tif discoveryContext.Args != nil {\n\t\targsCopy := make([]string, len(discoveryContext.Args))\n\t\tcopy(argsCopy, discoveryContext.Args)\n\t\tdiscoveryContext.Args = argsCopy\n\t}\n\n\tdiscoveryContext, err := TranslateDiscoveryContextArgsForWorktree(discoveryContext, kind)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsubDiscovery := NewDiscovery(wt.Path).\n\t\tWithFilters(filters).\n\t\tWithDiscoveryContext(discoveryContext).\n\t\tWithNumWorkers(p.numWorkers)\n\n\tif discovery.suppressParseErrors {\n\t\tsubDiscovery = subDiscovery.WithSuppressParseErrors()\n\t}\n\n\tif len(discovery.parserOptions) > 0 {\n\t\tsubDiscovery = subDiscovery.WithParserOptions(discovery.parserOptions)\n\t}\n\n\tcomponents, err := subDiscovery.Discover(ctx, l, input.Opts)\n\tif err != nil {\n\t\treturn components, err\n\t}\n\n\treturn components, nil\n}\n\n// discoverChangesInWorktreeStacks discovers changes in worktree stacks.\nfunc (p *WorktreePhase) discoverChangesInWorktreeStacks(\n\tctx context.Context,\n\tl log.Logger,\n\tinput *PhaseInput,\n\tw *worktrees.Worktrees,\n) (component.Components, error) {\n\tdiscoveredComponents := component.NewThreadSafeComponents(component.Components{})\n\n\tstackDiff := w.Stacks()\n\n\tg, ctx := errgroup.WithContext(ctx)\n\tg.SetLimit(max(1, min(runtime.NumCPU(), len(stackDiff.Added)+len(stackDiff.Removed)+len(stackDiff.Changed)*2)))\n\n\tvar (\n\t\tmu   sync.Mutex\n\t\terrs = make([]error, 0, len(stackDiff.Changed))\n\t)\n\n\tfor _, changed := range stackDiff.Changed {\n\t\tg.Go(func() error {\n\t\t\tcomponents, err := p.walkChangedStack(ctx, l, input, changed.FromStack, changed.ToStack)\n\t\t\tif err != nil {\n\t\t\t\tmu.Lock()\n\n\t\t\t\terrs = append(errs, err)\n\n\t\t\t\tmu.Unlock()\n\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfor _, c := range components {\n\t\t\t\tdiscoveredComponents.EnsureComponent(c)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tif err := g.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn nil, errors.Join(errs...)\n\t}\n\n\treturn discoveredComponents.ToComponents(), nil\n}\n\n// walkChangedStack walks a changed stack and discovers components within it.\nfunc (p *WorktreePhase) walkChangedStack(\n\tctx context.Context,\n\tl log.Logger,\n\tinput *PhaseInput,\n\tfromStack *component.Stack,\n\ttoStack *component.Stack,\n) (component.Components, error) {\n\tdiscovery := input.Discovery\n\n\tfromDiscoveryContext := discovery.discoveryContext.Copy()\n\tfromDiscoveryContext.WorkingDir = fromStack.Path()\n\tfromDiscoveryContext.Ref = fromStack.DiscoveryContext().Ref\n\n\tfromDiscoveryContext, err := TranslateDiscoveryContextArgsForWorktree(fromDiscoveryContext, FromWorktreeKind)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttoDiscoveryContext := discovery.discoveryContext.Copy()\n\ttoDiscoveryContext.WorkingDir = toStack.Path()\n\ttoDiscoveryContext.Ref = toStack.DiscoveryContext().Ref\n\n\ttoDiscoveryContext, err = TranslateDiscoveryContextArgsForWorktree(toDiscoveryContext, ToWorktreeKind)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar fromComponents, toComponents component.Components\n\n\tdiscoveryGroup, discoveryCtx := errgroup.WithContext(ctx)\n\tdiscoveryGroup.SetLimit(min(runtime.NumCPU(), 2)) //nolint:mnd\n\n\tvar (\n\t\tmu   sync.Mutex\n\t\terrs = make([]error, 0, 2) //nolint:mnd\n\t)\n\n\tdiscoveryGroup.Go(func() error {\n\t\tfromDiscovery := NewDiscovery(fromStack.Path()).\n\t\t\tWithDiscoveryContext(fromDiscoveryContext).\n\t\t\tWithFilters(filter.Filters{}).\n\t\t\tWithNumWorkers(p.numWorkers)\n\n\t\tvar fromDiscoveryErr error\n\n\t\tfromComponents, fromDiscoveryErr = fromDiscovery.Discover(discoveryCtx, l, input.Opts)\n\t\tif fromDiscoveryErr != nil {\n\t\t\tmu.Lock()\n\n\t\t\terrs = append(errs, fromDiscoveryErr)\n\n\t\t\tmu.Unlock()\n\n\t\t\treturn nil\n\t\t}\n\n\t\tfor _, c := range fromComponents {\n\t\t\tdc := c.DiscoveryContext().CopyWithNewOrigin(component.OriginWorktreeDiscovery)\n\t\t\tdc.WorkingDir = fromStack.DiscoveryContext().WorkingDir\n\t\t\tc.SetDiscoveryContext(dc)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tdiscoveryGroup.Go(func() error {\n\t\ttoDiscovery := NewDiscovery(toStack.Path()).\n\t\t\tWithDiscoveryContext(toDiscoveryContext).\n\t\t\tWithFilters(filter.Filters{}).\n\t\t\tWithNumWorkers(p.numWorkers)\n\n\t\tvar toDiscoveryErr error\n\n\t\ttoComponents, toDiscoveryErr = toDiscovery.Discover(discoveryCtx, l, input.Opts)\n\t\tif toDiscoveryErr != nil {\n\t\t\tmu.Lock()\n\n\t\t\terrs = append(errs, toDiscoveryErr)\n\n\t\t\tmu.Unlock()\n\n\t\t\treturn nil\n\t\t}\n\n\t\tfor _, c := range toComponents {\n\t\t\tdc := c.DiscoveryContext().CopyWithNewOrigin(component.OriginWorktreeDiscovery)\n\t\t\tdc.WorkingDir = toStack.DiscoveryContext().WorkingDir\n\t\t\tc.SetDiscoveryContext(dc)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err = discoveryGroup.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn nil, errors.Join(errs...)\n\t}\n\n\tcomponentPairs, err := MatchComponentPairs(fromComponents, toComponents)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfinalComponents := make(component.Components, 0, max(len(fromComponents), len(toComponents)))\n\n\tfor _, fromComponent := range fromComponents {\n\t\tif !slices.ContainsFunc(componentPairs, func(cp ComponentPair) bool {\n\t\t\treturn cp.FromComponent == fromComponent\n\t\t}) {\n\t\t\tfinalComponents = append(finalComponents, fromComponent)\n\t\t}\n\t}\n\n\tfor _, toComponent := range toComponents {\n\t\tif !slices.ContainsFunc(componentPairs, func(cp ComponentPair) bool {\n\t\t\treturn cp.ToComponent == toComponent\n\t\t}) {\n\t\t\tfinalComponents = append(finalComponents, toComponent)\n\t\t}\n\t}\n\n\tfor _, pair := range componentPairs {\n\t\tvar fromSHA, toSHA string\n\n\t\tshaGroup, _ := errgroup.WithContext(ctx)\n\t\tshaGroup.SetLimit(min(runtime.NumCPU(), 2)) //nolint:mnd\n\n\t\tshaGroup.Go(func() error {\n\t\t\tvar localErr error\n\n\t\t\tfromSHA, localErr = GenerateDirSHA256(pair.FromComponent.Path())\n\n\t\t\treturn localErr\n\t\t})\n\n\t\tshaGroup.Go(func() error {\n\t\t\tvar localErr error\n\n\t\t\ttoSHA, localErr = GenerateDirSHA256(pair.ToComponent.Path())\n\n\t\t\treturn localErr\n\t\t})\n\n\t\tif err := shaGroup.Wait(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif fromSHA != toSHA {\n\t\t\tdc := pair.ToComponent.DiscoveryContext().CopyWithNewOrigin(component.OriginWorktreeDiscovery)\n\t\t\tpair.ToComponent.SetDiscoveryContext(dc)\n\t\t\tfinalComponents = append(finalComponents, pair.ToComponent)\n\t\t}\n\t}\n\n\treturn finalComponents, nil\n}\n\n// ComponentPair represents a pair of matched components from different worktrees.\ntype ComponentPair struct {\n\tFromComponent component.Component\n\tToComponent   component.Component\n}\n\n// MatchComponentPairs matches components between from and to stacks by their relative paths.\nfunc MatchComponentPairs(\n\tfromComponents component.Components,\n\ttoComponents component.Components,\n) ([]ComponentPair, error) {\n\tcomponentPairs := make([]ComponentPair, 0, max(len(fromComponents), len(toComponents)))\n\n\tfor _, fromComponent := range fromComponents {\n\t\tif fromComponent.DiscoveryContext() == nil {\n\t\t\treturn nil, NewMissingDiscoveryContextError(fromComponent.Path())\n\t\t}\n\n\t\tfromComponentSuffix := strings.TrimPrefix(\n\t\t\tfromComponent.Path(),\n\t\t\tfromComponent.DiscoveryContext().WorkingDir,\n\t\t)\n\n\t\tfor _, toComponent := range toComponents {\n\t\t\tif toComponent.DiscoveryContext() == nil {\n\t\t\t\treturn nil, NewMissingDiscoveryContextError(toComponent.Path())\n\t\t\t}\n\n\t\t\ttoComponentSuffix := strings.TrimPrefix(\n\t\t\t\ttoComponent.Path(),\n\t\t\t\ttoComponent.DiscoveryContext().WorkingDir,\n\t\t\t)\n\n\t\t\tif filepath.Clean(fromComponentSuffix) == filepath.Clean(toComponentSuffix) {\n\t\t\t\tcomponentPairs = append(componentPairs, ComponentPair{\n\t\t\t\t\tFromComponent: fromComponent,\n\t\t\t\t\tToComponent:   toComponent,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\treturn componentPairs, nil\n}\n\n// WorktreeKind represents the type of worktree (from or to).\ntype WorktreeKind int\n\nconst (\n\t// FromWorktreeKind represents a \"from\" worktree (the older reference).\n\tFromWorktreeKind WorktreeKind = iota\n\t// ToWorktreeKind represents a \"to\" worktree (the newer reference).\n\tToWorktreeKind\n)\n\n// TranslateDiscoveryContextArgsForWorktree translates discovery context arguments for a worktree.\nfunc TranslateDiscoveryContextArgsForWorktree(\n\tdiscoveryContext *component.DiscoveryContext,\n\twKind WorktreeKind,\n) (*component.DiscoveryContext, error) {\n\tswitch wKind {\n\tcase FromWorktreeKind:\n\t\tswitch {\n\t\tcase (discoveryContext.Cmd == \"plan\" || discoveryContext.Cmd == \"apply\") &&\n\t\t\t!slices.Contains(discoveryContext.Args, \"-destroy\"):\n\t\t\tdiscoveryContext.Args = append(discoveryContext.Args, \"-destroy\")\n\t\tcase discoveryContext.Cmd == \"\" && len(discoveryContext.Args) == 0:\n\t\t\t// Discovery commands like find or list - no args needed\n\t\tdefault:\n\t\t\treturn discoveryContext, NewGitFilterCommandError(discoveryContext.Cmd, discoveryContext.Args)\n\t\t}\n\n\t\treturn discoveryContext, nil\n\n\tcase ToWorktreeKind:\n\t\tswitch {\n\t\tcase (discoveryContext.Cmd == \"plan\" || discoveryContext.Cmd == \"apply\") &&\n\t\t\t!slices.Contains(discoveryContext.Args, \"-destroy\"):\n\t\t\t// No -destroy flag needed for to worktrees\n\t\tcase discoveryContext.Cmd == \"\" && len(discoveryContext.Args) == 0:\n\t\t\t// Discovery commands like find or list - no args needed\n\t\tdefault:\n\t\t\treturn discoveryContext, NewGitFilterCommandError(discoveryContext.Cmd, discoveryContext.Args)\n\t\t}\n\n\t\treturn discoveryContext, nil\n\n\tdefault:\n\t\treturn discoveryContext, NewGitFilterCommandError(discoveryContext.Cmd, discoveryContext.Args)\n\t}\n}\n\n// GenerateDirSHA256 calculates a single SHA256 checksum for all files in a directory.\nfunc GenerateDirSHA256(rootDir string) (string, error) {\n\tvar filePaths []string\n\n\terr := filepath.WalkDir(rootDir, func(path string, d fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif d.IsDir() {\n\t\t\treturn nil\n\t\t}\n\n\t\t// Ignore .terragrunt-stack-manifest as it contains absolute paths\n\t\tif filepath.Base(path) == \".terragrunt-stack-manifest\" {\n\t\t\treturn nil\n\t\t}\n\n\t\tfilePaths = append(filePaths, path)\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error walking directory: %w\", err)\n\t}\n\n\tsort.Strings(filePaths)\n\n\thash := sha256.New()\n\n\tfor _, path := range filePaths {\n\t\trelPath, err := filepath.Rel(rootDir, path)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"could not compute relative path for %s: %w\", path, err)\n\t\t}\n\n\t\tnormalizedPath := filepath.ToSlash(relPath)\n\n\t\t// These writes are guaranteed to succeed. They just return errors because of the\n\t\t// Writer interface, but we don't care about those errors.\n\t\t_, _ = hash.Write([]byte(normalizedPath))\n\t\t_, _ = hash.Write([]byte{0})\n\n\t\tf, err := os.Open(path)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"could not open file %s: %w\", path, err)\n\t\t}\n\n\t\t_, err = io.Copy(hash, f)\n\t\tcloseErr := f.Close()\n\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"could not copy file %s to hash: %w\", path, err)\n\t\t}\n\n\t\tif closeErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"could not close file %s: %w\", path, closeErr)\n\t\t}\n\t}\n\n\treturn hex.EncodeToString(hash.Sum(nil)), nil\n}\n"
  },
  {
    "path": "internal/discovery/phase_worktree_integration_test.go",
    "content": "package discovery_test\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\tgogit \"github.com/go-git/go-git/v6\"\n\t\"github.com/go-git/go-git/v6/plumbing/object\"\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/internal/stacks/generate\"\n\t\"github.com/gruntwork-io/terragrunt/internal/worktrees\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestWorktreePhase_Integration_UnitLifecycle tests the full worktree discovery flow\n// for created, modified, removed, and untouched units.\nfunc TestWorktreePhase_Integration_UnitLifecycle(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir, runner := setupGitRepo(t)\n\n\t// Create initial units\n\tcreateUnit(t, tmpDir, \"unit-to-be-modified\", `# Unit to be modified`)\n\tcreateUnit(t, tmpDir, \"unit-to-be-removed\", `# Unit to be removed`)\n\tcreateUnit(t, tmpDir, \"unit-to-be-untouched\", `# Unit to be untouched`)\n\n\tcommitChanges(t, runner, \"Initial commit\")\n\n\t// Modify the unit\n\terr := os.WriteFile(filepath.Join(tmpDir, \"unit-to-be-modified\", \"terragrunt.hcl\"), []byte(`# Unit modified`), 0644)\n\trequire.NoError(t, err)\n\n\t// Remove the unit\n\terr = os.RemoveAll(filepath.Join(tmpDir, \"unit-to-be-removed\"))\n\trequire.NoError(t, err)\n\n\t// Add a new unit\n\tcreateUnit(t, tmpDir, \"unit-to-be-created\", `# Unit created`)\n\n\t// Do nothing to the untouched unit\n\n\tcommitChanges(t, runner, \"Create, modify, and remove units\")\n\n\t// Run worktree discovery\n\tgitExpressions := filter.GitExpressions{filter.NewGitExpression(\"HEAD~1\", \"HEAD\")}\n\tcomponents, w := runWorktreeDiscovery(t, tmpDir, gitExpressions, \"\", nil)\n\n\t// Verify worktrees were created\n\tassert.NotEmpty(t, w.WorktreePairs, \"Worktrees should be created\")\n\tassert.Contains(t, w.WorktreePairs, \"[HEAD~1...HEAD]\", \"Worktree should exist\")\n\n\tworktreePair := w.WorktreePairs[\"[HEAD~1...HEAD]\"]\n\tfromWorktree := worktreePair.FromWorktree.Path\n\ttoWorktree := worktreePair.ToWorktree.Path\n\n\t// Verify units were discovered\n\tunits := components.Filter(component.UnitKind)\n\tunitPaths := units.Paths()\n\n\texpectedUnitToBeCreated := filepath.Join(toWorktree, \"unit-to-be-created\")\n\texpectedUnitToBeModified := filepath.Join(toWorktree, \"unit-to-be-modified\")\n\texpectedUnitToBeRemoved := filepath.Join(fromWorktree, \"unit-to-be-removed\")\n\texpectedUnitToBeUntouched := filepath.Join(toWorktree, \"unit-to-be-untouched\")\n\n\tassert.Contains(t, unitPaths, expectedUnitToBeCreated, \"Unit should be discovered as it was created\")\n\tassert.DirExists(t, expectedUnitToBeCreated)\n\n\tassert.Contains(t, unitPaths, expectedUnitToBeModified, \"Unit should be discovered as it was modified\")\n\tassert.DirExists(t, expectedUnitToBeModified)\n\n\tassert.Contains(t, unitPaths, expectedUnitToBeRemoved, \"Unit should be discovered as it was removed\")\n\tassert.DirExists(t, expectedUnitToBeRemoved)\n\n\tassert.NotContains(t, unitPaths, expectedUnitToBeUntouched, \"Unit should not be discovered as it was untouched\")\n\tassert.DirExists(t, expectedUnitToBeUntouched)\n}\n\n// TestWorktreePhase_Integration_CommandArgs tests command argument handling for worktrees.\nfunc TestWorktreePhase_Integration_CommandArgs(t *testing.T) {\n\tt.Parallel()\n\n\tgitExpressions := filter.GitExpressions{filter.NewGitExpression(\"HEAD~1\", \"HEAD\")}\n\n\ttests := []struct {\n\t\tname             string\n\t\tcmd              string\n\t\texpectedErrorMsg string\n\t\tdescription      string\n\t\targs             []string\n\t\texpectError      bool\n\t}{\n\t\t{\n\t\t\tname:        \"plan_command_removed_unit_has_destroy_flag\",\n\t\t\tcmd:         \"plan\",\n\t\t\targs:        []string{},\n\t\t\texpectError: false,\n\t\t\tdescription: \"Plan command should add '-destroy' flag for removed units\",\n\t\t},\n\t\t{\n\t\t\tname:        \"apply_command_removed_unit_has_destroy_flag\",\n\t\t\tcmd:         \"apply\",\n\t\t\targs:        []string{},\n\t\t\texpectError: false,\n\t\t\tdescription: \"Apply command should add '-destroy' flag for removed units\",\n\t\t},\n\t\t{\n\t\t\tname:        \"plan_command_with_destroy_throws_error\",\n\t\t\tcmd:         \"plan\",\n\t\t\targs:        []string{\"-destroy\"},\n\t\t\texpectError: true,\n\t\t\tdescription: \"Plan command with '-destroy' already present should error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"empty_command_allowed\",\n\t\t\tcmd:         \"\",\n\t\t\targs:        []string{},\n\t\t\texpectError: false,\n\t\t\tdescription: \"Empty command should be allowed for discovery commands\",\n\t\t},\n\t\t{\n\t\t\tname:             \"unsupported_command_returns_error\",\n\t\t\tcmd:              \"destroy\",\n\t\t\targs:             []string{},\n\t\t\texpectError:      true,\n\t\t\texpectedErrorMsg: \"Git-based filtering is not supported with the command 'destroy'\",\n\t\t\tdescription:      \"Unsupported command should return error\",\n\t\t},\n\t\t{\n\t\t\tname:        \"plan_with_other_args_allowed\",\n\t\t\tcmd:         \"plan\",\n\t\t\targs:        []string{\"-out\", \"plan.out\"},\n\t\t\texpectError: false,\n\t\t\tdescription: \"Plan command with other args should be allowed\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t// Each subtest creates its own git repository\n\t\t\ttmpDir, runner := setupGitRepo(t)\n\n\t\t\t// Create initial units\n\t\t\tcreateUnit(t, tmpDir, \"unit-to-be-modified\", `# Unit to be modified`)\n\t\t\tcreateUnit(t, tmpDir, \"unit-to-be-removed\", `# Unit to be removed`)\n\n\t\t\tcommitChanges(t, runner, \"Initial commit\")\n\n\t\t\t// Modify the unit\n\t\t\terr := os.WriteFile(filepath.Join(tmpDir, \"unit-to-be-modified\", \"terragrunt.hcl\"), []byte(`# Modified`), 0644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Remove the unit\n\t\t\terr = os.RemoveAll(filepath.Join(tmpDir, \"unit-to-be-removed\"))\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Add a new unit\n\t\t\tcreateUnit(t, tmpDir, \"unit-to-be-created\", `# Created`)\n\n\t\t\tcommitChanges(t, runner, \"Update units\")\n\n\t\t\t// Set up discovery\n\t\t\tl := logger.CreateLogger()\n\n\t\t\tw, err := worktrees.NewWorktrees(t.Context(), l, tmpDir, gitExpressions)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tt.Cleanup(func() {\n\t\t\t\tcleanupErr := w.Cleanup(context.WithoutCancel(t.Context()), l)\n\t\t\t\trequire.NoError(t, cleanupErr)\n\t\t\t})\n\n\t\t\topts := options.NewTerragruntOptions()\n\t\t\topts.WorkingDir = tmpDir\n\t\t\topts.RootWorkingDir = tmpDir\n\n\t\t\tdiscoveryContext := &component.DiscoveryContext{\n\t\t\t\tWorkingDir: tmpDir,\n\t\t\t\tCmd:        tt.cmd,\n\t\t\t\tArgs:       tt.args,\n\t\t\t}\n\n\t\t\tdiscovery := discovery.NewDiscovery(tmpDir).\n\t\t\t\tWithDiscoveryContext(discoveryContext).\n\t\t\t\tWithWorktrees(w)\n\n\t\t\tfilters := filter.Filters{}\n\n\t\t\tfor _, gitExpr := range gitExpressions {\n\t\t\t\tf := filter.NewFilter(gitExpr, gitExpr.String())\n\t\t\t\tfilters = append(filters, f)\n\t\t\t}\n\n\t\t\tdiscovery = discovery.WithFilters(filters)\n\n\t\t\tcomponents, err := discovery.Discover(t.Context(), l, opts)\n\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for: %s\", tt.description)\n\n\t\t\t\tif tt.expectedErrorMsg != \"\" {\n\t\t\t\t\tassert.Contains(t, err.Error(), tt.expectedErrorMsg)\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err, \"Should not error for: %s\", tt.description)\n\n\t\t\t// Verify worktrees were created\n\t\t\tassert.NotEmpty(t, w.WorktreePairs, \"Worktrees should be created\")\n\n\t\t\tworktreePair := w.WorktreePairs[\"[HEAD~1...HEAD]\"]\n\t\t\tfromWorktree := worktreePair.FromWorktree.Path\n\t\t\ttoWorktree := worktreePair.ToWorktree.Path\n\n\t\t\t// Verify units were discovered\n\t\t\tunits := components.Filter(component.UnitKind)\n\n\t\t\texpectedUnitToBeCreated := filepath.Join(toWorktree, \"unit-to-be-created\")\n\t\t\texpectedUnitToBeModified := filepath.Join(toWorktree, \"unit-to-be-modified\")\n\t\t\texpectedUnitToBeRemoved := filepath.Join(fromWorktree, \"unit-to-be-removed\")\n\n\t\t\t// Verify discovery context args for each unit\n\t\t\tfor _, unit := range units {\n\t\t\t\tctx := unit.DiscoveryContext()\n\t\t\t\trequire.NotNil(t, ctx, \"Component should have discovery context\")\n\n\t\t\t\tunitPath := unit.Path()\n\n\t\t\t\t// Check removed unit (discovered in \"from\" worktree)\n\t\t\t\tif unitPath == expectedUnitToBeRemoved {\n\t\t\t\t\tif tt.cmd == \"plan\" || tt.cmd == \"apply\" {\n\t\t\t\t\t\tassert.Contains(t, ctx.Args, \"-destroy\",\n\t\t\t\t\t\t\t\"Removed unit should have '-destroy' flag for %s command\", tt.cmd)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Check added unit (discovered in \"to\" worktree)\n\t\t\t\tif unitPath == expectedUnitToBeCreated {\n\t\t\t\t\tif tt.cmd == \"plan\" || tt.cmd == \"apply\" {\n\t\t\t\t\t\tassert.NotContains(t, ctx.Args, \"-destroy\",\n\t\t\t\t\t\t\t\"Added unit should NOT have '-destroy' flag for %s command\", tt.cmd)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Check modified unit (discovered in \"to\" worktree)\n\t\t\t\tif unitPath == expectedUnitToBeModified {\n\t\t\t\t\tif tt.cmd == \"plan\" || tt.cmd == \"apply\" {\n\t\t\t\t\t\tassert.NotContains(t, ctx.Args, \"-destroy\",\n\t\t\t\t\t\t\t\"Modified unit should NOT have '-destroy' flag for %s command\", tt.cmd)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestWorktreePhase_Integration_EmptyFilters tests that discovery produces no results\n// when git diff contains no terragrunt files.\nfunc TestWorktreePhase_Integration_EmptyFilters(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir, runner := setupGitRepo(t)\n\n\t// Create initial empty commit\n\terr := runner.GoCommit(\"Initial commit\", &gogit.CommitOptions{\n\t\tAllowEmptyCommits: true,\n\t\tAuthor: &object.Signature{\n\t\t\tName:  \"Test User\",\n\t\t\tEmail: \"test@example.com\",\n\t\t\tWhen:  time.Now(),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Create a second commit with only non-terragrunt files\n\treadmePath := filepath.Join(tmpDir, \"README.md\")\n\terr = os.WriteFile(readmePath, []byte(\"# Test\"), 0644)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Update README\")\n\n\t// Run worktree discovery\n\tgitExpressions := filter.GitExpressions{filter.NewGitExpression(\"HEAD~1\", \"HEAD\")}\n\tcomponents, _ := runWorktreeDiscovery(t, tmpDir, gitExpressions, \"\", nil)\n\n\t// Verify that no components were discovered\n\tassert.Empty(t, components, \"No components should be discovered when filters are empty\")\n}\n\n// TestWorktreePhase_Integration_EmptyDiffs tests that discovery produces no results\n// when there are no changes between commits.\nfunc TestWorktreePhase_Integration_EmptyDiffs(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir, runner := setupGitRepo(t)\n\n\t// Create initial empty commit\n\terr := runner.GoCommit(\"Initial commit\", &gogit.CommitOptions{\n\t\tAllowEmptyCommits: true,\n\t\tAuthor: &object.Signature{\n\t\t\tName:  \"Test User\",\n\t\t\tEmail: \"test@example.com\",\n\t\t\tWhen:  time.Now(),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Create a second empty commit\n\terr = runner.GoCommit(\"Empty commit\", &gogit.CommitOptions{\n\t\tAllowEmptyCommits: true,\n\t\tAuthor: &object.Signature{\n\t\t\tName:  \"Test User\",\n\t\t\tEmail: \"test@example.com\",\n\t\t\tWhen:  time.Now(),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Run worktree discovery\n\tgitExpressions := filter.GitExpressions{filter.NewGitExpression(\"HEAD~1\", \"HEAD\")}\n\tcomponents, _ := runWorktreeDiscovery(t, tmpDir, gitExpressions, \"\", nil)\n\n\t// Verify that no components were discovered\n\tassert.Empty(t, components, \"No components should be discovered when there are no diffs\")\n}\n\n// TestWorktreePhase_Integration_Stacks tests stack discovery with generated units.\nfunc TestWorktreePhase_Integration_Stacks(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir, runner := setupGitRepo(t)\n\n\t// Create a catalog of units\n\tlegacyUnitDir := filepath.Join(tmpDir, \"catalog\", \"units\", \"legacy\")\n\terr := os.MkdirAll(legacyUnitDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(legacyUnitDir, \"terragrunt.hcl\"), []byte(`# Legacy unit`), 0644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(legacyUnitDir, \"main.tf\"), []byte(`# Intentionally empty`), 0644)\n\trequire.NoError(t, err)\n\n\tmodernUnitDir := filepath.Join(tmpDir, \"catalog\", \"units\", \"modern\")\n\terr = os.MkdirAll(modernUnitDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(modernUnitDir, \"terragrunt.hcl\"), []byte(`# Modern unit`), 0644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(modernUnitDir, \"main.tf\"), []byte(`# Intentionally empty`), 0644)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Create catalog units\")\n\n\t// Create stacks\n\tstackFileContents := `unit \"unit_to_be_modified\" {\n\tsource = \"${get_repo_root()}/catalog/units/legacy\"\n\tpath   = \"unit_to_be_modified\"\n}\n\nunit \"unit_to_be_removed\" {\n\tsource = \"${get_repo_root()}/catalog/units/legacy\"\n\tpath   = \"unit_to_be_removed\"\n}\n\nunit \"unit_to_be_untouched\" {\n\tsource = \"${get_repo_root()}/catalog/units/legacy\"\n\tpath   = \"unit_to_be_untouched\"\n}\n`\n\n\tstackToBeModifiedDir := filepath.Join(tmpDir, \"live\", \"stack-to-be-modified\")\n\terr = os.MkdirAll(stackToBeModifiedDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(stackToBeModifiedDir, \"terragrunt.stack.hcl\"), []byte(stackFileContents), 0644)\n\trequire.NoError(t, err)\n\n\tstackToBeRemovedDir := filepath.Join(tmpDir, \"live\", \"stack-to-be-removed\")\n\terr = os.MkdirAll(stackToBeRemovedDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(stackToBeRemovedDir, \"terragrunt.stack.hcl\"), []byte(stackFileContents), 0644)\n\trequire.NoError(t, err)\n\n\tstackToBeUntouchedDir := filepath.Join(tmpDir, \"live\", \"stack-to-be-untouched\")\n\terr = os.MkdirAll(stackToBeUntouchedDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(stackToBeUntouchedDir, \"terragrunt.stack.hcl\"), []byte(stackFileContents), 0644)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Create stacks\")\n\n\t// Add a new stack\n\tstackToBeAddedDir := filepath.Join(tmpDir, \"live\", \"stack-to-be-added\")\n\terr = os.MkdirAll(stackToBeAddedDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(stackToBeAddedDir, \"terragrunt.stack.hcl\"), []byte(stackFileContents), 0644)\n\trequire.NoError(t, err)\n\n\t// Modify the first stack\n\tmodifiedStackContents := `unit \"unit_to_be_added\" {\n\tsource = \"${get_repo_root()}/catalog/units/modern\"\n\tpath   = \"unit_to_be_added\"\n}\n\nunit \"unit_to_be_modified\" {\n\tsource = \"${get_repo_root()}/catalog/units/modern\"\n\tpath   = \"unit_to_be_modified\"\n}\n\nunit \"unit_to_be_untouched\" {\n\tsource = \"${get_repo_root()}/catalog/units/legacy\"\n\tpath   = \"unit_to_be_untouched\"\n}\n`\n\terr = os.WriteFile(filepath.Join(stackToBeModifiedDir, \"terragrunt.stack.hcl\"), []byte(modifiedStackContents), 0644)\n\trequire.NoError(t, err)\n\n\t// Remove the second stack\n\terr = os.RemoveAll(stackToBeRemovedDir)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Modify and remove stacks\")\n\n\t// Set up discovery with worktrees\n\tl := logger.CreateLogger()\n\tgitExpressions := filter.GitExpressions{filter.NewGitExpression(\"HEAD~1\", \"HEAD\")}\n\n\tw, err := worktrees.NewWorktrees(t.Context(), l, tmpDir, gitExpressions)\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\tcleanupErr := w.Cleanup(context.WithoutCancel(t.Context()), l)\n\t\trequire.NoError(t, cleanupErr)\n\t})\n\n\t// Generate stacks in worktrees\n\topts := options.NewTerragruntOptions()\n\topts.WorkingDir = tmpDir\n\topts.RootWorkingDir = tmpDir\n\tparsedFilters, parseErr := filter.ParseFilterQueries(l, []string{\"[HEAD~1...HEAD]\"})\n\trequire.NoError(t, parseErr)\n\n\topts.Filters = parsedFilters\n\topts.Experiments = experiment.NewExperiments()\n\terr = opts.Experiments.EnableExperiment(experiment.FilterFlag)\n\trequire.NoError(t, err)\n\n\terr = generate.GenerateStacks(t.Context(), l, opts, w)\n\trequire.NoError(t, err)\n\n\t// Run discovery\n\tdiscoveryContext := &component.DiscoveryContext{\n\t\tWorkingDir: tmpDir,\n\t\tCmd:        \"plan\",\n\t}\n\n\tdiscovery := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(discoveryContext).\n\t\tWithWorktrees(w)\n\n\tfilters := filter.Filters{}\n\n\tfor _, gitExpr := range gitExpressions {\n\t\tf := filter.NewFilter(gitExpr, gitExpr.String())\n\t\tfilters = append(filters, f)\n\t}\n\n\tdiscovery = discovery.WithFilters(filters)\n\n\tcomponents, err := discovery.Discover(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\n\t// Verify that components were discovered\n\tassert.NotEmpty(t, components)\n\n\t// Get worktree paths\n\tworktreePair := w.WorktreePairs[\"[HEAD~1...HEAD]\"]\n\trequire.NotEmpty(t, worktreePair)\n\n\tfromWorktree := worktreePair.FromWorktree.Path\n\ttoWorktree := worktreePair.ToWorktree.Path\n\n\t// Get relative paths\n\tstackToBeAddedRel, err := filepath.Rel(tmpDir, stackToBeAddedDir)\n\trequire.NoError(t, err)\n\tstackToBeRemovedRel, err := filepath.Rel(tmpDir, stackToBeRemovedDir)\n\trequire.NoError(t, err)\n\n\t// Verify added stack and its units are in toWorktree\n\taddedStackPath := filepath.Join(toWorktree, stackToBeAddedRel)\n\tfoundAddedStack := false\n\n\tfor _, c := range components {\n\t\tif c.Path() == addedStackPath {\n\t\t\tfoundAddedStack = true\n\t\t\tdc := c.DiscoveryContext()\n\t\t\tassert.NotNil(t, dc)\n\t\t\tassert.Equal(t, \"HEAD\", dc.Ref)\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\tassert.True(t, foundAddedStack, \"Added stack should be discovered\")\n\n\t// Verify removed stack is in fromWorktree\n\tremovedStackPath := filepath.Join(fromWorktree, stackToBeRemovedRel)\n\tfoundRemovedStack := false\n\n\tfor _, c := range components {\n\t\tif c.Path() == removedStackPath {\n\t\t\tfoundRemovedStack = true\n\t\t\tdc := c.DiscoveryContext()\n\t\t\tassert.NotNil(t, dc)\n\t\t\tassert.Equal(t, \"HEAD~1\", dc.Ref)\n\t\t\tassert.Contains(t, dc.Args, \"-destroy\", \"Removed stack should have -destroy flag\")\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\tassert.True(t, foundRemovedStack, \"Removed stack should be discovered\")\n}\n\n// TestWorktreePhase_Integration_FileRename tests that file renames are detected.\nfunc TestWorktreePhase_Integration_FileRename(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir, runner := setupGitRepo(t)\n\n\t// Create a unit with a file\n\tunitDir := createUnit(t, tmpDir, \"unit\", `# Unit config`)\n\n\terr := os.WriteFile(filepath.Join(unitDir, \"original.tf\"), []byte(`# Same content before and after rename`), 0644)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Initial commit with original.tf\")\n\n\t// Rename the file (same content, different name)\n\terr = os.Rename(\n\t\tfilepath.Join(unitDir, \"original.tf\"),\n\t\tfilepath.Join(unitDir, \"renamed.tf\"),\n\t)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Rename original.tf to renamed.tf\")\n\n\t// Run worktree discovery\n\tgitExpressions := filter.GitExpressions{filter.NewGitExpression(\"HEAD~1\", \"HEAD\")}\n\tcomponents, w := runWorktreeDiscovery(t, tmpDir, gitExpressions, \"plan\", nil)\n\n\t// The unit should be detected as changed because the file was renamed\n\tassert.NotEmpty(t, components, \"Unit with renamed file should be detected as changed\")\n\n\t// Verify we have the unit\n\ttoWorktree := w.WorktreePairs[\"[HEAD~1...HEAD]\"].ToWorktree.Path\n\texpectedUnitPath := filepath.Join(toWorktree, \"unit\")\n\n\tunitPaths := components.Paths()\n\tassert.Contains(t, unitPaths, expectedUnitPath, \"Should discover the unit with renamed file\")\n}\n\n// TestWorktreePhase_Integration_FileMove tests that file moves are detected.\nfunc TestWorktreePhase_Integration_FileMove(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir, runner := setupGitRepo(t)\n\n\t// Create a unit with a file in root\n\tunitDir := createUnit(t, tmpDir, \"unit\", `# Unit config`)\n\n\terr := os.WriteFile(filepath.Join(unitDir, \"module.tf\"), []byte(`# Module content`), 0644)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Initial commit with module.tf in root\")\n\n\t// Move file to subdirectory (same content, different path)\n\tsubDir := filepath.Join(unitDir, \"modules\")\n\terr = os.MkdirAll(subDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.Rename(\n\t\tfilepath.Join(unitDir, \"module.tf\"),\n\t\tfilepath.Join(subDir, \"module.tf\"),\n\t)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Move module.tf to modules/ subdirectory\")\n\n\t// Run worktree discovery\n\tgitExpressions := filter.GitExpressions{filter.NewGitExpression(\"HEAD~1\", \"HEAD\")}\n\tcomponents, _ := runWorktreeDiscovery(t, tmpDir, gitExpressions, \"plan\", nil)\n\n\t// The unit should be detected as changed because the file was moved\n\tassert.NotEmpty(t, components, \"Unit with moved file should be detected as changed\")\n\n\t// Verify we have the unit\n\tfoundUnit := false\n\n\tfor _, c := range components {\n\t\tif _, ok := c.(*component.Unit); ok {\n\t\t\tfoundUnit = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tassert.True(t, foundUnit, \"Should discover the unit with moved file\")\n}\n\n// TestWorktreePhase_Integration_NestedUnits tests discovery of nested units.\nfunc TestWorktreePhase_Integration_NestedUnits(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir, runner := setupGitRepo(t)\n\n\t// Create nested unit structure\n\tcreateUnit(t, tmpDir, \"apps/frontend\", `# Frontend unit`)\n\tcreateUnit(t, tmpDir, \"apps/backend\", `# Backend unit`)\n\tcreateUnit(t, tmpDir, \"apps/backend/db\", `# Database unit`)\n\n\tcommitChanges(t, runner, \"Initial commit\")\n\n\t// Modify the nested unit\n\terr := os.WriteFile(\n\t\tfilepath.Join(tmpDir, \"apps/backend/db\", \"terragrunt.hcl\"),\n\t\t[]byte(`# Modified database unit`),\n\t\t0644,\n\t)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Modify nested unit\")\n\n\t// Run worktree discovery\n\tgitExpressions := filter.GitExpressions{filter.NewGitExpression(\"HEAD~1\", \"HEAD\")}\n\tcomponents, w := runWorktreeDiscovery(t, tmpDir, gitExpressions, \"\", nil)\n\n\t// Verify the nested unit was discovered\n\tunits := components.Filter(component.UnitKind)\n\tassert.Len(t, units, 1, \"Only the modified nested unit should be discovered\")\n\n\tworktreePair := w.WorktreePairs[\"[HEAD~1...HEAD]\"]\n\ttoWorktree := worktreePair.ToWorktree.Path\n\texpectedPath := filepath.Join(toWorktree, \"apps/backend/db\")\n\n\tunitPaths := units.Paths()\n\tassert.Contains(t, unitPaths, expectedPath, \"Nested unit should be discovered\")\n}\n\n// TestWorktreePhase_Integration_MultipleGitExpressions tests discovery with multiple git expressions.\nfunc TestWorktreePhase_Integration_MultipleGitExpressions(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir, runner := setupGitRepo(t)\n\n\t// Create initial unit\n\tcreateUnit(t, tmpDir, \"unit-a\", `# Unit A`)\n\n\tcommitChanges(t, runner, \"Initial commit\")\n\n\t// Create second unit\n\tcreateUnit(t, tmpDir, \"unit-b\", `# Unit B`)\n\n\tcommitChanges(t, runner, \"Add unit B\")\n\n\t// Create third unit\n\tcreateUnit(t, tmpDir, \"unit-c\", `# Unit C`)\n\n\tcommitChanges(t, runner, \"Add unit C\")\n\n\t// Run worktree discovery with expression covering last commit\n\tgitExpressions := filter.GitExpressions{filter.NewGitExpression(\"HEAD~1\", \"HEAD\")}\n\tcomponents, w := runWorktreeDiscovery(t, tmpDir, gitExpressions, \"\", nil)\n\n\t// Should only discover unit-c (added in last commit)\n\tunits := components.Filter(component.UnitKind)\n\tassert.Len(t, units, 1, \"Only unit-c should be discovered\")\n\n\tworktreePair := w.WorktreePairs[\"[HEAD~1...HEAD]\"]\n\ttoWorktree := worktreePair.ToWorktree.Path\n\texpectedPath := filepath.Join(toWorktree, \"unit-c\")\n\n\tunitPaths := units.Paths()\n\tassert.Contains(t, unitPaths, expectedPath, \"Unit C should be discovered\")\n}\n\n// TestWorktreePhase_Integration_GitFilterCombinedWithOtherFilters tests git filters combined\n// with other filter types (path, name, type, negation).\nfunc TestWorktreePhase_Integration_GitFilterCombinedWithOtherFilters(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname            string\n\t\tfilterQueries   func(fromRef, toRef string) []string\n\t\twantUnits       func(fromDir, toDir string) []string\n\t\twantStacks      []string\n\t\texpectedChanged []string // which units are expected to be changed in the test setup\n\t}{\n\t\t{\n\t\t\tname: \"Git filter combined with path filter\",\n\t\t\tfilterQueries: func(fromRef, toRef string) []string {\n\t\t\t\treturn []string{\"[\" + fromRef + \"...\" + toRef + \"] | ./app\"}\n\t\t\t},\n\t\t\twantUnits: func(_, toDir string) []string {\n\t\t\t\treturn []string{filepath.Join(toDir, \"app\")}\n\t\t\t},\n\t\t\twantStacks:      []string{},\n\t\t\texpectedChanged: []string{\"app\", \"new\"},\n\t\t},\n\t\t{\n\t\t\tname: \"Git filter combined with name filter\",\n\t\t\tfilterQueries: func(fromRef, toRef string) []string {\n\t\t\t\treturn []string{\"[\" + fromRef + \"...\" + toRef + \"] | name=new\"}\n\t\t\t},\n\t\t\twantUnits: func(_, toDir string) []string {\n\t\t\t\treturn []string{filepath.Join(toDir, \"new\")}\n\t\t\t},\n\t\t\twantStacks:      []string{},\n\t\t\texpectedChanged: []string{\"app\", \"new\"},\n\t\t},\n\t\t{\n\t\t\tname: \"Git filter with negation\",\n\t\t\tfilterQueries: func(fromRef, toRef string) []string {\n\t\t\t\treturn []string{\"[\" + fromRef + \"...\" + toRef + \"] | !name=new\"}\n\t\t\t},\n\t\t\twantUnits: func(fromDir, toDir string) []string {\n\t\t\t\treturn []string{\n\t\t\t\t\tfilepath.Join(fromDir, \"cache\"),\n\t\t\t\t\tfilepath.Join(toDir, \"app\"),\n\t\t\t\t}\n\t\t\t},\n\t\t\twantStacks:      []string{},\n\t\t\texpectedChanged: []string{\"app\", \"new\", \"cache\"},\n\t\t},\n\t\t{\n\t\t\tname: \"Git filter - single reference (compared to HEAD)\",\n\t\t\tfilterQueries: func(fromRef, _ string) []string {\n\t\t\t\treturn []string{\"[\" + fromRef + \"]\"}\n\t\t\t},\n\t\t\twantUnits: func(fromDir, toDir string) []string {\n\t\t\t\treturn []string{\n\t\t\t\t\tfilepath.Join(fromDir, \"cache\"),\n\t\t\t\t\tfilepath.Join(toDir, \"app\"),\n\t\t\t\t\tfilepath.Join(toDir, \"new\"),\n\t\t\t\t}\n\t\t\t},\n\t\t\twantStacks:      []string{},\n\t\t\texpectedChanged: []string{\"app\", \"new\", \"cache\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpDir, runner := setupGitRepo(t)\n\n\t\t\t// Create initial components\n\t\t\tcreateUnit(t, tmpDir, \"app\", `# App unit`)\n\t\t\tcreateUnit(t, tmpDir, \"db\", `# DB unit`)\n\t\t\tcreateUnit(t, tmpDir, \"cache\", `# Cache unit`)\n\n\t\t\tcommitChanges(t, runner, \"Initial commit\")\n\n\t\t\t// Modify app component\n\t\t\terr := os.WriteFile(filepath.Join(tmpDir, \"app\", \"terragrunt.hcl\"), []byte(`\nlocals {\n\tmodified = true\n}\n`), 0644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Add new component\n\t\t\tcreateUnit(t, tmpDir, \"new\", `# New unit`)\n\n\t\t\t// Remove cache component\n\t\t\terr = os.RemoveAll(filepath.Join(tmpDir, \"cache\"))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcommitChanges(t, runner, \"Changes: modified app, added new, removed cache\")\n\n\t\t\t// Parse filter queries\n\t\t\tl := logger.CreateLogger()\n\t\t\tfilterQueries := tt.filterQueries(\"HEAD~1\", \"HEAD\")\n\t\t\tfilters, err := filter.ParseFilterQueries(l, filterQueries)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Create worktrees\n\t\t\tw, err := worktrees.NewWorktrees(t.Context(), l, tmpDir, filters.UniqueGitFilters())\n\t\t\trequire.NoError(t, err)\n\n\t\t\tt.Cleanup(func() {\n\t\t\t\tcleanupErr := w.Cleanup(context.WithoutCancel(t.Context()), l)\n\t\t\t\trequire.NoError(t, cleanupErr)\n\t\t\t})\n\n\t\t\topts := options.NewTerragruntOptions()\n\t\t\topts.WorkingDir = tmpDir\n\t\t\topts.RootWorkingDir = tmpDir\n\n\t\t\tdiscoveryContext := &component.DiscoveryContext{\n\t\t\t\tWorkingDir: tmpDir,\n\t\t\t}\n\n\t\t\tdiscovery := discovery.NewDiscovery(tmpDir).\n\t\t\t\tWithDiscoveryContext(discoveryContext).\n\t\t\t\tWithWorktrees(w).\n\t\t\t\tWithFilters(filters)\n\n\t\t\tcomponents, err := discovery.Discover(t.Context(), l, opts)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Filter results by type\n\t\t\tunits := components.Filter(component.UnitKind).Paths()\n\t\t\tstacks := components.Filter(component.StackKind).Paths()\n\n\t\t\tworktreePair := w.WorktreePairs[\"[HEAD~1...HEAD]\"]\n\t\t\trequire.NotEmpty(t, worktreePair)\n\n\t\t\twantUnits := tt.wantUnits(worktreePair.FromWorktree.Path, worktreePair.ToWorktree.Path)\n\n\t\t\t// Verify results\n\t\t\tassert.ElementsMatch(t, wantUnits, units, \"Units mismatch for test: %s\", tt.name)\n\t\t\tassert.ElementsMatch(t, tt.wantStacks, stacks, \"Stacks mismatch for test: %s\", tt.name)\n\t\t})\n\t}\n}\n\n// TestWorktreePhase_Integration_FromSubdirectory tests that git filter discovery works correctly\n// when running from a subdirectory of the git root. This is a regression test for the bug where\n// paths were incorrectly duplicated (e.g., \"basic/basic/basic-2\" instead of \"basic/basic-2\").\nfunc TestWorktreePhase_Integration_FromSubdirectory(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir, runner := setupGitRepo(t)\n\n\t// Create subdirectory structure: basic/basic-1, basic/basic-2\n\tbasicDir := filepath.Join(tmpDir, \"basic\")\n\tbasic1Dir := filepath.Join(basicDir, \"basic-1\")\n\tbasic2Dir := filepath.Join(basicDir, \"basic-2\")\n\n\t// Also create a component outside the subdirectory\n\totherDir := filepath.Join(tmpDir, \"other\")\n\n\ttestDirs := []string{basic1Dir, basic2Dir, otherDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Create initial files\n\tinitialFiles := map[string]string{\n\t\tfilepath.Join(basic1Dir, \"terragrunt.hcl\"): ``,\n\t\tfilepath.Join(basic2Dir, \"terragrunt.hcl\"): ``,\n\t\tfilepath.Join(otherDir, \"terragrunt.hcl\"):  ``,\n\t}\n\n\tfor path, content := range initialFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tcommitChanges(t, runner, \"Initial commit\")\n\n\t// Modify basic-2 component\n\terr := os.WriteFile(filepath.Join(basic2Dir, \"terragrunt.hcl\"), []byte(`\nlocals {\n\tmodified = true\n}\n`), 0644)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Modified basic-2\")\n\n\t// Now run discovery FROM THE SUBDIRECTORY (basic)\n\tl := logger.CreateLogger()\n\n\t// Parse filter with Git reference\n\tfilters, err := filter.ParseFilterQueries(l, []string{\"[HEAD~1]\"})\n\trequire.NoError(t, err)\n\n\t// Create worktrees from the subdirectory\n\tw, err := worktrees.NewWorktrees(t.Context(), l, basicDir, filters.UniqueGitFilters())\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\tcleanupErr := w.Cleanup(context.WithoutCancel(t.Context()), l)\n\t\trequire.NoError(t, cleanupErr)\n\t})\n\n\topts := options.NewTerragruntOptions()\n\topts.WorkingDir = basicDir\n\topts.RootWorkingDir = basicDir\n\n\tdiscoveryContext := &component.DiscoveryContext{\n\t\tWorkingDir: basicDir,\n\t}\n\n\tdiscovery := discovery.NewDiscovery(basicDir).\n\t\tWithDiscoveryContext(discoveryContext).\n\t\tWithWorktrees(w).\n\t\tWithFilters(filters)\n\n\tcomponents, err := discovery.Discover(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\n\t// Filter results by type\n\tunits := components.Filter(component.UnitKind).Paths()\n\n\t// With worktree-based execution, discovery runs directly in the worktree path\n\tworktreePair := w.WorktreePairs[\"[HEAD~1...HEAD]\"]\n\trequire.NotEmpty(t, worktreePair)\n\n\texpectedPath := filepath.Join(worktreePair.ToWorktree.Path, \"basic\", \"basic-2\")\n\tassert.ElementsMatch(t, []string{expectedPath}, units,\n\t\t\"Should discover basic-2 with correct path when running from subdirectory\")\n\n\t// Verify the path doesn't have duplicated directory names\n\tfor _, unitPath := range units {\n\t\tassert.NotContains(t, unitPath, \"basic\"+string(filepath.Separator)+\"basic\"+string(filepath.Separator)+\"basic-\",\n\t\t\t\"Path should not have duplicated directory names\")\n\t}\n}\n\n// setupMultiCommitTestRepo creates a git repository with 4 commits for testing\n// git filter discovery from a subdirectory. Returns the basicDir (subdirectory).\nfunc setupMultiCommitTestRepo(t *testing.T) string {\n\tt.Helper()\n\n\ttmpDir, runner := setupGitRepo(t)\n\n\t// Create subdirectory structure: basic/basic-1, basic/basic-2, basic/basic-3\n\tbasicDir := filepath.Join(tmpDir, \"basic\")\n\tbasic1Dir := filepath.Join(basicDir, \"basic-1\")\n\tbasic2Dir := filepath.Join(basicDir, \"basic-2\")\n\tbasic3Dir := filepath.Join(basicDir, \"basic-3\")\n\n\t// Also create components outside the subdirectory\n\totherDir := filepath.Join(tmpDir, \"other\")\n\tanotherDir := filepath.Join(tmpDir, \"another\")\n\n\ttestDirs := []string{basic1Dir, basic2Dir, basic3Dir, otherDir, anotherDir}\n\tfor _, dir := range testDirs {\n\t\terr := os.MkdirAll(dir, 0755)\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Commit 1: Initial state with all components\n\tinitialFiles := map[string]string{\n\t\tfilepath.Join(basic1Dir, \"terragrunt.hcl\"):  ``,\n\t\tfilepath.Join(basic2Dir, \"terragrunt.hcl\"):  ``,\n\t\tfilepath.Join(basic3Dir, \"terragrunt.hcl\"):  ``,\n\t\tfilepath.Join(otherDir, \"terragrunt.hcl\"):   ``,\n\t\tfilepath.Join(anotherDir, \"terragrunt.hcl\"): ``,\n\t}\n\n\tfor path, content := range initialFiles {\n\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\t}\n\n\tcommitChanges(t, runner, \"Initial commit\")\n\n\t// Commit 2: Modify basic-1 and other (outside subdirectory)\n\terr := os.WriteFile(filepath.Join(basic1Dir, \"terragrunt.hcl\"), []byte(`\nlocals {\n\tversion = \"v1\"\n}\n`), 0644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(otherDir, \"terragrunt.hcl\"), []byte(`\nlocals {\n\tmodified = true\n}\n`), 0644)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Commit 2: modify basic-1 and other\")\n\n\t// Commit 3: Modify basic-2 and another (outside subdirectory)\n\terr = os.WriteFile(filepath.Join(basic2Dir, \"terragrunt.hcl\"), []byte(`\nlocals {\n\tversion = \"v2\"\n}\n`), 0644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(anotherDir, \"terragrunt.hcl\"), []byte(`\nlocals {\n\tmodified = true\n}\n`), 0644)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Commit 3: modify basic-2 and another\")\n\n\t// Commit 4: Modify basic-3\n\terr = os.WriteFile(filepath.Join(basic3Dir, \"terragrunt.hcl\"), []byte(`\nlocals {\n\tversion = \"v3\"\n}\n`), 0644)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Commit 4: modify basic-3\")\n\n\treturn basicDir\n}\n\n// TestWorktreePhase_Integration_NegatedGitGraphExpressions tests that negated Git+Graph expressions\n// work correctly. These are expressions where the negation wraps the Git expression:\n// - `![HEAD~1...HEAD]...` - Exclude changed components AND their dependencies\n// - `!...[HEAD~1...HEAD]` - Exclude changed components AND their dependents\n//\n// In worktree-based discovery, only changed components are initially discovered.\n// When a negated git+graph filter is used in combination with a positive git filter,\n// the filter semantics cause components matching the negation to be excluded.\n//\n// This validates the complete pipeline: worktree → graph → final evaluation with negation.\nfunc TestWorktreePhase_Integration_NegatedGitGraphExpressions(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname            string\n\t\tfilterQueries   func(fromRef, toRef string) []string\n\t\twantUnits       func(fromDir, toDir string) []string\n\t\tdescription     string\n\t\texpectedChanged []string // which units are expected to be changed in the test setup\n\t}{\n\t\t{\n\t\t\tname: \"simple negated git expression excludes all changed\",\n\t\t\tfilterQueries: func(fromRef, toRef string) []string {\n\t\t\t\t// ![HEAD~1...HEAD] = Negated git expression excludes changed components\n\t\t\t\t// When combined with positive filter, negation takes precedence\n\t\t\t\treturn []string{\n\t\t\t\t\t\"[\" + fromRef + \"...\" + toRef + \"]\",  // Include all changed\n\t\t\t\t\t\"![\" + fromRef + \"...\" + toRef + \"]\", // Exclude changed\n\t\t\t\t}\n\t\t\t},\n\t\t\twantUnits: func(_, _ string) []string {\n\t\t\t\t// Both filters apply: positive includes, negative excludes\n\t\t\t\t// Components matching negation are excluded from final result\n\t\t\t\treturn []string{}\n\t\t\t},\n\t\t\tdescription:     \"Positive and negative git filters - negation excludes all\",\n\t\t\texpectedChanged: []string{\"app\", \"db\"},\n\t\t},\n\t\t{\n\t\t\tname: \"negated git with dependency traversal in intersection\",\n\t\t\tfilterQueries: func(fromRef, toRef string) []string {\n\t\t\t\t// Use intersection to apply negated graph filter to git results\n\t\t\t\t// [HEAD~1...HEAD] | ![HEAD~1...HEAD]... = changed AND NOT (changed with deps)\n\t\t\t\treturn []string{\"[\" + fromRef + \"...\" + toRef + \"] | ![\" + fromRef + \"...\" + toRef + \"]...\"}\n\t\t\t},\n\t\t\twantUnits: func(_, _ string) []string {\n\t\t\t\t// Intersection: component must match [changed] AND match ![changed]...\n\t\t\t\t// For any changed component, ![changed]... is false (negation of true)\n\t\t\t\t// So intersection is always empty\n\t\t\t\treturn []string{}\n\t\t\t},\n\t\t\tdescription:     \"Git filter intersected with negated git+deps - empty result\",\n\t\t\texpectedChanged: []string{\"app\"},\n\t\t},\n\t\t{\n\t\t\tname: \"negated git with dependent traversal in intersection\",\n\t\t\tfilterQueries: func(fromRef, toRef string) []string {\n\t\t\t\t// Use intersection: [HEAD~1...HEAD] | !...[HEAD~1...HEAD]\n\t\t\t\treturn []string{\"[\" + fromRef + \"...\" + toRef + \"] | !...[\" + fromRef + \"...\" + toRef + \"]\"}\n\t\t\t},\n\t\t\twantUnits: func(_, _ string) []string {\n\t\t\t\t// Intersection: component must match [changed] AND match !...[changed]\n\t\t\t\t// For any changed component, !...[changed] is false\n\t\t\t\t// So intersection is always empty\n\t\t\t\treturn []string{}\n\t\t\t},\n\t\t\tdescription:     \"Git filter intersected with negated git+dependents - empty result\",\n\t\t\texpectedChanged: []string{\"vpc\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpDir, runner := setupGitRepo(t)\n\n\t\t\t// Create dependency chain: app -> db -> vpc\n\t\t\t// Plus an unrelated component for verification\n\t\t\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\t\t\tdbDir := filepath.Join(tmpDir, \"db\")\n\t\t\tappDir := filepath.Join(tmpDir, \"app\")\n\t\t\tunrelatedDir := filepath.Join(tmpDir, \"unrelated\")\n\n\t\t\ttestDirs := []string{vpcDir, dbDir, appDir, unrelatedDir}\n\t\t\tfor _, dir := range testDirs {\n\t\t\t\terr := os.MkdirAll(dir, 0755)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\t// Create initial files with dependencies\n\t\t\ttestFiles := map[string]string{\n\t\t\t\tfilepath.Join(appDir, \"terragrunt.hcl\"): `\ndependency \"db\" {\n\tconfig_path = \"../db\"\n}\n`,\n\t\t\t\tfilepath.Join(dbDir, \"terragrunt.hcl\"): `\ndependency \"vpc\" {\n\tconfig_path = \"../vpc\"\n}\n`,\n\t\t\t\tfilepath.Join(vpcDir, \"terragrunt.hcl\"):       ``,\n\t\t\t\tfilepath.Join(unrelatedDir, \"terragrunt.hcl\"): ``,\n\t\t\t}\n\n\t\t\tfor path, content := range testFiles {\n\t\t\t\terr := os.WriteFile(path, []byte(content), 0644)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tcommitChanges(t, runner, \"Initial commit\")\n\n\t\t\t// Modify the expected changed components based on test case\n\t\t\tfor _, changed := range tt.expectedChanged {\n\t\t\t\tchangedPath := filepath.Join(tmpDir, changed, \"terragrunt.hcl\")\n\t\t\t\tcurrentContent, err := os.ReadFile(changedPath)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tnewContent := string(currentContent) + `\nlocals {\n\tmodified = true\n}\n`\n\t\t\t\terr = os.WriteFile(changedPath, []byte(newContent), 0644)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tcommitChanges(t, runner, \"Modify components: \"+tt.description)\n\n\t\t\t// Parse filter queries\n\t\t\tl := logger.CreateLogger()\n\t\t\tfilterQueries := tt.filterQueries(\"HEAD~1\", \"HEAD\")\n\t\t\tfilters, err := filter.ParseFilterQueries(l, filterQueries)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Create worktrees\n\t\t\tw, err := worktrees.NewWorktrees(t.Context(), l, tmpDir, filters.UniqueGitFilters())\n\t\t\trequire.NoError(t, err)\n\n\t\t\tt.Cleanup(func() {\n\t\t\t\tcleanupErr := w.Cleanup(context.WithoutCancel(t.Context()), l)\n\t\t\t\trequire.NoError(t, cleanupErr)\n\t\t\t})\n\n\t\t\topts := options.NewTerragruntOptions()\n\t\t\topts.WorkingDir = tmpDir\n\t\t\topts.RootWorkingDir = tmpDir\n\n\t\t\tdiscoveryContext := &component.DiscoveryContext{\n\t\t\t\tWorkingDir: tmpDir,\n\t\t\t}\n\n\t\t\tdiscovery := discovery.NewDiscovery(tmpDir).\n\t\t\t\tWithDiscoveryContext(discoveryContext).\n\t\t\t\tWithWorktrees(w).\n\t\t\t\tWithFilters(filters)\n\n\t\t\tcomponents, err := discovery.Discover(t.Context(), l, opts)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Filter results by type\n\t\t\tunits := components.Filter(component.UnitKind).Paths()\n\n\t\t\tworktreePair := w.WorktreePairs[\"[HEAD~1...HEAD]\"]\n\t\t\trequire.NotEmpty(t, worktreePair)\n\n\t\t\twantUnits := tt.wantUnits(worktreePair.FromWorktree.Path, worktreePair.ToWorktree.Path)\n\n\t\t\t// Verify results\n\t\t\tassert.ElementsMatch(t, wantUnits, units, \"Units mismatch for test: %s\\nDescription: %s\", tt.name, tt.description)\n\t\t})\n\t}\n}\n\n// TestWorktreePhase_Integration_FromSubdirectory_MultipleCommits tests git filter discovery\n// initiated from a subdirectory when comparing against multiple commits back (HEAD~2, HEAD~3).\nfunc TestWorktreePhase_Integration_FromSubdirectory_MultipleCommits(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpectedUnitsFunc func(toWorktreePath string) []string\n\t\tname              string\n\t\tgitRef            string\n\t}{\n\t\t{\n\t\t\tname:   \"HEAD~1 from subdirectory - only basic-3\",\n\t\t\tgitRef: \"HEAD~1\",\n\t\t\texpectedUnitsFunc: func(toWorktreePath string) []string {\n\t\t\t\treturn []string{filepath.Join(toWorktreePath, \"basic\", \"basic-3\")}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"HEAD~2 from subdirectory - basic-2 and basic-3, plus another\",\n\t\t\tgitRef: \"HEAD~2\",\n\t\t\texpectedUnitsFunc: func(toWorktreePath string) []string {\n\t\t\t\t// With worktree-root discovery, we find all changed units including 'another'\n\t\t\t\treturn []string{\n\t\t\t\t\tfilepath.Join(toWorktreePath, \"another\"),\n\t\t\t\t\tfilepath.Join(toWorktreePath, \"basic\", \"basic-2\"),\n\t\t\t\t\tfilepath.Join(toWorktreePath, \"basic\", \"basic-3\"),\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"HEAD~3 from subdirectory - basic-1, basic-2, basic-3, plus other and another\",\n\t\t\tgitRef: \"HEAD~3\",\n\t\t\texpectedUnitsFunc: func(toWorktreePath string) []string {\n\t\t\t\t// With worktree-root discovery, we find all changed units\n\t\t\t\treturn []string{\n\t\t\t\t\tfilepath.Join(toWorktreePath, \"other\"),\n\t\t\t\t\tfilepath.Join(toWorktreePath, \"another\"),\n\t\t\t\t\tfilepath.Join(toWorktreePath, \"basic\", \"basic-1\"),\n\t\t\t\t\tfilepath.Join(toWorktreePath, \"basic\", \"basic-2\"),\n\t\t\t\t\tfilepath.Join(toWorktreePath, \"basic\", \"basic-3\"),\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t// Each subtest creates its own git repository\n\t\t\tbasicDir := setupMultiCommitTestRepo(t)\n\n\t\t\tl := logger.CreateLogger()\n\n\t\t\t// Parse filter with Git reference\n\t\t\tfilters, err := filter.ParseFilterQueries(l, []string{\"[\" + tt.gitRef + \"]\"})\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Create worktrees from the subdirectory\n\t\t\tw, err := worktrees.NewWorktrees(t.Context(), l, basicDir, filters.UniqueGitFilters())\n\t\t\trequire.NoError(t, err)\n\n\t\t\tt.Cleanup(func() {\n\t\t\t\tcleanupErr := w.Cleanup(context.WithoutCancel(t.Context()), l)\n\t\t\t\trequire.NoError(t, cleanupErr)\n\t\t\t})\n\n\t\t\topts := options.NewTerragruntOptions()\n\t\t\topts.WorkingDir = basicDir\n\t\t\topts.RootWorkingDir = basicDir\n\n\t\t\tdiscoveryContext := &component.DiscoveryContext{\n\t\t\t\tWorkingDir: basicDir,\n\t\t\t}\n\n\t\t\tdiscovery := discovery.NewDiscovery(basicDir).\n\t\t\t\tWithDiscoveryContext(discoveryContext).\n\t\t\t\tWithWorktrees(w).\n\t\t\t\tWithFilters(filters)\n\n\t\t\tcomponents, err := discovery.Discover(t.Context(), l, opts)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Filter results by type\n\t\t\tunits := components.Filter(component.UnitKind).Paths()\n\n\t\t\t// Get worktree pair for expected path calculation\n\t\t\tworktreePair := w.WorktreePairs[\"[\"+tt.gitRef+\"...HEAD]\"]\n\t\t\trequire.NotEmpty(t, worktreePair)\n\n\t\t\t// Verify correct units are discovered\n\t\t\texpectedUnits := tt.expectedUnitsFunc(worktreePair.ToWorktree.Path)\n\t\t\tassert.ElementsMatch(t, expectedUnits, units,\n\t\t\t\t\"Should discover correct units when running from subdirectory with %s\", tt.gitRef)\n\n\t\t\t// Verify no path duplication\n\t\t\tfor _, unitPath := range units {\n\t\t\t\tassert.NotContains(t, unitPath,\n\t\t\t\t\t\"basic\"+string(filepath.Separator)+\"basic\"+string(filepath.Separator)+\"basic-\",\n\t\t\t\t\t\"Path should not have duplicated directory names\")\n\t\t\t}\n\t\t})\n\t}\n}\n\n// setupGitRepo creates a git repository with initial structure for integration tests.\nfunc setupGitRepo(t *testing.T) (string, *git.GitRunner) {\n\tt.Helper()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpDir)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\terr = runner.GoOpenRepo()\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\tif err := runner.GoCloseStorage(); err != nil {\n\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t}\n\t})\n\n\treturn tmpDir, runner\n}\n\n// commitChanges stages all changes and commits with the given message.\nfunc commitChanges(t *testing.T, runner *git.GitRunner, message string) {\n\tt.Helper()\n\n\terr := runner.GoAdd(\".\")\n\trequire.NoError(t, err)\n\n\terr = runner.GoCommit(message, &gogit.CommitOptions{\n\t\tAuthor: &object.Signature{\n\t\t\tName:  \"Test User\",\n\t\t\tEmail: \"test@example.com\",\n\t\t\tWhen:  time.Now(),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n}\n\n// createUnit creates a unit directory with terragrunt.hcl.\nfunc createUnit(t *testing.T, baseDir, unitName, content string) string {\n\tt.Helper()\n\n\tunitDir := filepath.Join(baseDir, unitName)\n\terr := os.MkdirAll(unitDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(unitDir, \"terragrunt.hcl\"), []byte(content), 0644)\n\trequire.NoError(t, err)\n\n\treturn unitDir\n}\n\n// TestWorktreePhase_Integration_StackReadingChanges tests that changes to files referenced\n// via read_terragrunt_config() in a stack file trigger stack change detection, while changes\n// to unreferenced files in the same directory do not.\nfunc TestWorktreePhase_Integration_StackReadingChanges(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir, runner := setupGitRepo(t)\n\n\t// Create a catalog unit\n\tlegacyUnitDir := filepath.Join(tmpDir, \"catalog\", \"units\", \"legacy\")\n\terr := os.MkdirAll(legacyUnitDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(legacyUnitDir, \"terragrunt.hcl\"), []byte(`# Legacy unit`), 0644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(legacyUnitDir, \"main.tf\"), []byte(`# Intentionally empty`), 0644)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Create catalog units\")\n\n\t// Create a stack that references a sidecar file via read_terragrunt_config\n\tstackWithRefDir := filepath.Join(tmpDir, \"live\", \"stack-with-ref\")\n\terr = os.MkdirAll(stackWithRefDir, 0755)\n\trequire.NoError(t, err)\n\n\t// Sidecar file referenced by the stack\n\terr = os.WriteFile(filepath.Join(stackWithRefDir, \"config.hcl\"), []byte(`inputs = { version = \"v1\" }`), 0644)\n\trequire.NoError(t, err)\n\n\tstackWithRefContent := `\nlocals {\n  config = read_terragrunt_config(\"config.hcl\")\n}\n\nunit \"app\" {\n  source = \"${get_repo_root()}/catalog/units/legacy\"\n  path   = \"app\"\n}\n`\n\terr = os.WriteFile(filepath.Join(stackWithRefDir, \"terragrunt.stack.hcl\"), []byte(stackWithRefContent), 0644)\n\trequire.NoError(t, err)\n\n\t// Create a stack WITHOUT read_terragrunt_config but with a file in same dir\n\tstackNoRefDir := filepath.Join(tmpDir, \"live\", \"stack-no-ref\")\n\terr = os.MkdirAll(stackNoRefDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(stackNoRefDir, \"unrelated.hcl\"), []byte(`# not referenced`), 0644)\n\trequire.NoError(t, err)\n\n\tstackNoRefContent := `\nunit \"app\" {\n  source = \"${get_repo_root()}/catalog/units/legacy\"\n  path   = \"app\"\n}\n`\n\terr = os.WriteFile(filepath.Join(stackNoRefDir, \"terragrunt.stack.hcl\"), []byte(stackNoRefContent), 0644)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Create stacks with and without read_terragrunt_config\")\n\n\t// Change only the sidecar files (not the stack files)\n\terr = os.WriteFile(filepath.Join(stackWithRefDir, \"config.hcl\"), []byte(`inputs = { version = \"v2\" }`), 0644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(stackNoRefDir, \"unrelated.hcl\"), []byte(`# still not referenced but modified`), 0644)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Update sidecar files only\")\n\n\t// Set up discovery with worktrees\n\tl := logger.CreateLogger()\n\tgitExpressions := filter.GitExpressions{filter.NewGitExpression(\"HEAD~1\", \"HEAD\")}\n\n\tw, err := worktrees.NewWorktrees(t.Context(), l, tmpDir, gitExpressions)\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\tcleanupErr := w.Cleanup(context.WithoutCancel(t.Context()), l)\n\t\trequire.NoError(t, cleanupErr)\n\t})\n\n\t// Generate stacks in worktrees\n\topts := options.NewTerragruntOptions()\n\topts.WorkingDir = tmpDir\n\topts.RootWorkingDir = tmpDir\n\n\tparsedFilters, parseErr := filter.ParseFilterQueries(l, []string{\"[HEAD~1...HEAD]\"})\n\trequire.NoError(t, parseErr)\n\n\topts.Filters = parsedFilters\n\topts.Experiments = experiment.NewExperiments()\n\terr = opts.Experiments.EnableExperiment(experiment.FilterFlag)\n\trequire.NoError(t, err)\n\n\terr = generate.GenerateStacks(t.Context(), l, opts, w)\n\trequire.NoError(t, err)\n\n\t// Run discovery\n\tdiscoveryContext := &component.DiscoveryContext{\n\t\tWorkingDir: tmpDir,\n\t\tCmd:        \"plan\",\n\t}\n\n\tdisc := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(discoveryContext).\n\t\tWithWorktrees(w)\n\n\tfilters := filter.Filters{}\n\n\tfor _, gitExpr := range gitExpressions {\n\t\tf := filter.NewFilter(gitExpr, gitExpr.String())\n\t\tfilters = append(filters, f)\n\t}\n\n\tdisc = disc.WithFilters(filters)\n\n\tcomponents, err := disc.Discover(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\n\t// Get worktree paths\n\trequire.Contains(t, w.WorktreePairs, \"[HEAD~1...HEAD]\", \"Worktree pair should exist\")\n\n\tworktreePair := w.WorktreePairs[\"[HEAD~1...HEAD]\"]\n\ttoWorktree := worktreePair.ToWorktree.Path\n\n\t// Collect component paths for debugging on failure\n\tcomponentPaths := make([]string, 0, len(components))\n\tfor _, c := range components {\n\t\tcomponentPaths = append(componentPaths, c.Path())\n\t}\n\n\t// Verify: stack-with-ref should be discovered (config.hcl is referenced via read_terragrunt_config)\n\tstackWithRefRel, err := filepath.Rel(tmpDir, stackWithRefDir)\n\trequire.NoError(t, err)\n\n\texpectedStackWithRef := filepath.Join(toWorktree, stackWithRefRel)\n\tfoundStackWithRef := false\n\n\tfor _, c := range components {\n\t\tif c.Path() == expectedStackWithRef {\n\t\t\tfoundStackWithRef = true\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\tassert.True(t, foundStackWithRef,\n\t\t\"Stack with read_terragrunt_config reference should be discovered when sidecar changes; got: %v\", componentPaths)\n\n\t// Verify: stack-no-ref should NOT be discovered (unrelated.hcl is not referenced)\n\tstackNoRefRel, err := filepath.Rel(tmpDir, stackNoRefDir)\n\trequire.NoError(t, err)\n\n\texpectedStackNoRef := filepath.Join(toWorktree, stackNoRefRel)\n\tfoundStackNoRef := false\n\n\tfor _, c := range components {\n\t\tif c.Path() == expectedStackNoRef {\n\t\t\tfoundStackNoRef = true\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\tassert.False(t, foundStackNoRef,\n\t\t\"Stack without read_terragrunt_config reference should NOT be discovered; got: %v\", componentPaths)\n}\n\n// TestWorktreePhase_Integration_StackReadingDedup tests that when both the stack file itself\n// and a sidecar file referenced via read_terragrunt_config() change in the same commit,\n// the stack is discovered exactly once (no duplication from buildHandledStackDirs).\nfunc TestWorktreePhase_Integration_StackReadingDedup(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir, runner := setupGitRepo(t)\n\n\t// Create a catalog unit\n\tlegacyUnitDir := filepath.Join(tmpDir, \"catalog\", \"units\", \"legacy\")\n\terr := os.MkdirAll(legacyUnitDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(legacyUnitDir, \"terragrunt.hcl\"), []byte(`# Legacy unit`), 0644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(legacyUnitDir, \"main.tf\"), []byte(`# Intentionally empty`), 0644)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Create catalog units\")\n\n\t// Create a stack with read_terragrunt_config + sidecar\n\tstackDir := filepath.Join(tmpDir, \"live\", \"dedup-stack\")\n\terr = os.MkdirAll(stackDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(stackDir, \"config.hcl\"), []byte(`inputs = { version = \"v1\" }`), 0644)\n\trequire.NoError(t, err)\n\n\tstackContent := `\nlocals {\n  config = read_terragrunt_config(\"config.hcl\")\n}\n\nunit \"app\" {\n  source = \"${get_repo_root()}/catalog/units/legacy\"\n  path   = \"app\"\n}\n`\n\terr = os.WriteFile(filepath.Join(stackDir, \"terragrunt.stack.hcl\"), []byte(stackContent), 0644)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Create stack with read_terragrunt_config\")\n\n\t// Change BOTH the stack file AND the sidecar file in the same commit\n\tupdatedStackContent := `\nlocals {\n  config = read_terragrunt_config(\"config.hcl\")\n}\n\nunit \"app\" {\n  source = \"${get_repo_root()}/catalog/units/legacy\"\n  path   = \"app-v2\"\n}\n`\n\terr = os.WriteFile(filepath.Join(stackDir, \"terragrunt.stack.hcl\"), []byte(updatedStackContent), 0644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(stackDir, \"config.hcl\"), []byte(`inputs = { version = \"v2\" }`), 0644)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Update both stack file and sidecar\")\n\n\t// Set up discovery\n\tl := logger.CreateLogger()\n\tgitExpressions := filter.GitExpressions{filter.NewGitExpression(\"HEAD~1\", \"HEAD\")}\n\n\tw, err := worktrees.NewWorktrees(t.Context(), l, tmpDir, gitExpressions)\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\tcleanupErr := w.Cleanup(context.WithoutCancel(t.Context()), l)\n\t\trequire.NoError(t, cleanupErr)\n\t})\n\n\topts := options.NewTerragruntOptions()\n\topts.WorkingDir = tmpDir\n\topts.RootWorkingDir = tmpDir\n\n\tparsedFilters, parseErr := filter.ParseFilterQueries(l, []string{\"[HEAD~1...HEAD]\"})\n\trequire.NoError(t, parseErr)\n\n\topts.Filters = parsedFilters\n\topts.Experiments = experiment.NewExperiments()\n\terr = opts.Experiments.EnableExperiment(experiment.FilterFlag)\n\trequire.NoError(t, err)\n\n\terr = generate.GenerateStacks(t.Context(), l, opts, w)\n\trequire.NoError(t, err)\n\n\t// Run discovery\n\tdiscoveryContext := &component.DiscoveryContext{\n\t\tWorkingDir: tmpDir,\n\t\tCmd:        \"plan\",\n\t}\n\n\tdisc := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(discoveryContext).\n\t\tWithWorktrees(w)\n\n\tfilters := filter.Filters{}\n\n\tfor _, gitExpr := range gitExpressions {\n\t\tf := filter.NewFilter(gitExpr, gitExpr.String())\n\t\tfilters = append(filters, f)\n\t}\n\n\tdisc = disc.WithFilters(filters)\n\n\tcomponents, err := disc.Discover(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\n\t// Get worktree pair path\n\trequire.Contains(t, w.WorktreePairs, \"[HEAD~1...HEAD]\", \"Worktree pair should exist\")\n\n\tworktreePair := w.WorktreePairs[\"[HEAD~1...HEAD]\"]\n\ttoWorktree := worktreePair.ToWorktree.Path\n\n\t// Collect component paths\n\tcomponentPaths := make([]string, 0, len(components))\n\tfor _, c := range components {\n\t\tcomponentPaths = append(componentPaths, c.Path())\n\t}\n\n\t// The stack should be discovered (stack file changed)\n\tstackRel, err := filepath.Rel(tmpDir, stackDir)\n\trequire.NoError(t, err)\n\n\texpectedStackPath := filepath.Join(toWorktree, stackRel)\n\n\t// Verify no duplicate paths — dedup via buildHandledStackDirs should prevent\n\t// findStacksAffectedByReading from adding the stack again\n\tseen := make(map[string]int, len(components))\n\n\tfor _, c := range components {\n\t\tseen[c.Path()]++\n\t}\n\n\tfor p, count := range seen {\n\t\tassert.Equal(t, 1, count,\n\t\t\t\"Component path %s appears %d times (expected 1); all: %v\", p, count, componentPaths)\n\t}\n\n\t// Verify the stack itself is discovered\n\t_, foundStack := seen[expectedStackPath]\n\tassert.True(t, foundStack,\n\t\t\"Stack %s should be discovered when both stack file and sidecar change; got: %v\", expectedStackPath, componentPaths)\n}\n\n// TestWorktreePhase_Integration_StackReadingNestedPath tests that stacks referencing sidecar\n// files at nested or sibling paths (e.g., read_terragrunt_config(\"../../env/config.hcl\"))\n// are correctly discovered when those files change.\nfunc TestWorktreePhase_Integration_StackReadingNestedPath(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir, runner := setupGitRepo(t)\n\n\t// Create a catalog unit\n\tlegacyUnitDir := filepath.Join(tmpDir, \"catalog\", \"units\", \"legacy\")\n\terr := os.MkdirAll(legacyUnitDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(legacyUnitDir, \"terragrunt.hcl\"), []byte(`# Legacy unit`), 0644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(legacyUnitDir, \"main.tf\"), []byte(`# Intentionally empty`), 0644)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Create catalog units\")\n\n\t// Create a sidecar config in a DIFFERENT directory tree than the stack\n\tenvDir := filepath.Join(tmpDir, \"env\")\n\terr = os.MkdirAll(envDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(envDir, \"config.hcl\"), []byte(`inputs = { version = \"v1\" }`), 0644)\n\trequire.NoError(t, err)\n\n\t// Create a stack that references the sidecar via a nested/sibling path\n\tstackDir := filepath.Join(tmpDir, \"live\", \"my-stack\")\n\terr = os.MkdirAll(stackDir, 0755)\n\trequire.NoError(t, err)\n\n\tstackContent := `\nlocals {\n  config = read_terragrunt_config(\"../../env/config.hcl\")\n}\n\nunit \"app\" {\n  source = \"${get_repo_root()}/catalog/units/legacy\"\n  path   = \"app\"\n}\n`\n\terr = os.WriteFile(filepath.Join(stackDir, \"terragrunt.stack.hcl\"), []byte(stackContent), 0644)\n\trequire.NoError(t, err)\n\n\t// Create a stack WITHOUT a cross-directory reference (control)\n\tstackNoRefDir := filepath.Join(tmpDir, \"live\", \"no-ref-stack\")\n\terr = os.MkdirAll(stackNoRefDir, 0755)\n\trequire.NoError(t, err)\n\n\tstackNoRefContent := `\nunit \"app\" {\n  source = \"${get_repo_root()}/catalog/units/legacy\"\n  path   = \"app\"\n}\n`\n\terr = os.WriteFile(filepath.Join(stackNoRefDir, \"terragrunt.stack.hcl\"), []byte(stackNoRefContent), 0644)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Create stacks and env config\")\n\n\t// Change ONLY the sidecar file in the separate directory\n\terr = os.WriteFile(filepath.Join(envDir, \"config.hcl\"), []byte(`inputs = { version = \"v2\" }`), 0644)\n\trequire.NoError(t, err)\n\n\tcommitChanges(t, runner, \"Update env config only\")\n\n\t// Set up discovery\n\tl := logger.CreateLogger()\n\tgitExpressions := filter.GitExpressions{filter.NewGitExpression(\"HEAD~1\", \"HEAD\")}\n\n\tw, err := worktrees.NewWorktrees(t.Context(), l, tmpDir, gitExpressions)\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\tcleanupErr := w.Cleanup(context.WithoutCancel(t.Context()), l)\n\t\trequire.NoError(t, cleanupErr)\n\t})\n\n\topts := options.NewTerragruntOptions()\n\topts.WorkingDir = tmpDir\n\topts.RootWorkingDir = tmpDir\n\n\tparsedFilters, parseErr := filter.ParseFilterQueries(l, []string{\"[HEAD~1...HEAD]\"})\n\trequire.NoError(t, parseErr)\n\n\topts.Filters = parsedFilters\n\topts.Experiments = experiment.NewExperiments()\n\terr = opts.Experiments.EnableExperiment(experiment.FilterFlag)\n\trequire.NoError(t, err)\n\n\terr = generate.GenerateStacks(t.Context(), l, opts, w)\n\trequire.NoError(t, err)\n\n\t// Run discovery\n\tdiscoveryContext := &component.DiscoveryContext{\n\t\tWorkingDir: tmpDir,\n\t\tCmd:        \"plan\",\n\t}\n\n\tdisc := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(discoveryContext).\n\t\tWithWorktrees(w)\n\n\tfilters := filter.Filters{}\n\n\tfor _, gitExpr := range gitExpressions {\n\t\tf := filter.NewFilter(gitExpr, gitExpr.String())\n\t\tfilters = append(filters, f)\n\t}\n\n\tdisc = disc.WithFilters(filters)\n\n\tcomponents, err := disc.Discover(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\n\t// Get worktree paths\n\trequire.Contains(t, w.WorktreePairs, \"[HEAD~1...HEAD]\", \"Worktree pair should exist\")\n\n\tworktreePair := w.WorktreePairs[\"[HEAD~1...HEAD]\"]\n\ttoWorktree := worktreePair.ToWorktree.Path\n\n\t// Collect component paths for debugging\n\tcomponentPaths := make([]string, 0, len(components))\n\tfor _, c := range components {\n\t\tcomponentPaths = append(componentPaths, c.Path())\n\t}\n\n\t// Verify: stack with cross-directory read_terragrunt_config reference IS discovered\n\tstackRel, err := filepath.Rel(tmpDir, stackDir)\n\trequire.NoError(t, err)\n\n\texpectedStack := filepath.Join(toWorktree, stackRel)\n\tfoundStack := false\n\n\tfor _, c := range components {\n\t\tif c.Path() == expectedStack {\n\t\t\tfoundStack = true\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\tassert.True(t, foundStack,\n\t\t\"Stack with nested read_terragrunt_config reference should be discovered when sidecar changes; got: %v\", componentPaths)\n\n\t// Verify: stack WITHOUT the reference should NOT be discovered\n\tstackNoRefRel, err := filepath.Rel(tmpDir, stackNoRefDir)\n\trequire.NoError(t, err)\n\n\texpectedNoRef := filepath.Join(toWorktree, stackNoRefRel)\n\tfoundNoRef := false\n\n\tfor _, c := range components {\n\t\tif c.Path() == expectedNoRef {\n\t\t\tfoundNoRef = true\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\tassert.False(t, foundNoRef,\n\t\t\"Stack without read_terragrunt_config reference should NOT be discovered; got: %v\", componentPaths)\n}\n\n// runWorktreeDiscovery runs discovery with worktree phase enabled.\nfunc runWorktreeDiscovery(\n\tt *testing.T,\n\ttmpDir string,\n\tgitExpressions filter.GitExpressions,\n\tcmd string,\n\targs []string,\n) (component.Components, *worktrees.Worktrees) {\n\tt.Helper()\n\n\tl := logger.CreateLogger()\n\n\tw, err := worktrees.NewWorktrees(t.Context(), l, tmpDir, gitExpressions)\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\tcleanupErr := w.Cleanup(context.WithoutCancel(t.Context()), l)\n\t\trequire.NoError(t, cleanupErr)\n\t})\n\n\topts := options.NewTerragruntOptions()\n\topts.WorkingDir = tmpDir\n\topts.RootWorkingDir = tmpDir\n\n\tdiscoveryContext := &component.DiscoveryContext{\n\t\tWorkingDir: tmpDir,\n\t\tCmd:        cmd,\n\t\tArgs:       args,\n\t}\n\n\t// Build filters from git expressions\n\tfilters := make(filter.Filters, 0, len(gitExpressions))\n\tfor _, gitExpr := range gitExpressions {\n\t\tf := filter.NewFilter(gitExpr, gitExpr.String())\n\t\tfilters = append(filters, f)\n\t}\n\n\tdiscovery := discovery.NewDiscovery(tmpDir).\n\t\tWithDiscoveryContext(discoveryContext).\n\t\tWithWorktrees(w).\n\t\tWithFilters(filters)\n\n\tcomponents, err := discovery.Discover(t.Context(), l, opts)\n\trequire.NoError(t, err)\n\n\treturn components, w\n}\n"
  },
  {
    "path": "internal/discovery/phase_worktree_test.go",
    "content": "package discovery_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestNewWorktreePhase tests the WorktreePhase constructor.\nfunc TestNewWorktreePhase(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname               string\n\t\tnumWorkers         int\n\t\texpectedNumWorkers int\n\t}{\n\t\t{\n\t\t\tname:               \"positive workers\",\n\t\t\tnumWorkers:         4,\n\t\t\texpectedNumWorkers: 4,\n\t\t},\n\t\t{\n\t\t\tname:               \"zero workers defaults to CPU count\",\n\t\t\tnumWorkers:         0,\n\t\t\texpectedNumWorkers: -1, // Will check > 0\n\t\t},\n\t\t{\n\t\t\tname:               \"negative workers defaults to CPU count\",\n\t\t\tnumWorkers:         -1,\n\t\t\texpectedNumWorkers: -1, // Will check > 0\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tphase := discovery.NewWorktreePhase(nil, tt.numWorkers)\n\n\t\t\tassert.NotNil(t, phase)\n\t\t\tassert.Equal(t, \"worktree\", phase.Name())\n\t\t\tassert.Equal(t, discovery.PhaseWorktree, phase.Kind())\n\n\t\t\tif tt.expectedNumWorkers > 0 {\n\t\t\t\tassert.Equal(t, tt.expectedNumWorkers, phase.NumWorkers())\n\t\t\t} else {\n\t\t\t\t// When workers <= 0, it should default to runtime.NumCPU()\n\t\t\t\tassert.Positive(t, phase.NumWorkers())\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestGenerateDirSHA256 tests the SHA256 hash generation for directories.\nfunc TestGenerateDirSHA256(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"empty_directory_produces_consistent_hash\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttmpDir := t.TempDir()\n\n\t\thash1, err := discovery.GenerateDirSHA256(tmpDir)\n\t\trequire.NoError(t, err)\n\n\t\thash2, err := discovery.GenerateDirSHA256(tmpDir)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, hash1, hash2, \"Same empty directory should produce same hash\")\n\t})\n\n\tt.Run(\"same_files_produce_same_hash\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttmpDir1 := t.TempDir()\n\t\ttmpDir2 := t.TempDir()\n\n\t\tcontent := []byte(\"test content\")\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(tmpDir1, \"file.txt\"), content, 0644))\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(tmpDir2, \"file.txt\"), content, 0644))\n\n\t\thash1, err := discovery.GenerateDirSHA256(tmpDir1)\n\t\trequire.NoError(t, err)\n\n\t\thash2, err := discovery.GenerateDirSHA256(tmpDir2)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, hash1, hash2, \"Directories with same files should produce same hash\")\n\t})\n\n\tt.Run(\"modified_file_produces_different_hash\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttmpDir := t.TempDir()\n\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(tmpDir, \"file.txt\"), []byte(\"content1\"), 0644))\n\n\t\thash1, err := discovery.GenerateDirSHA256(tmpDir)\n\t\trequire.NoError(t, err)\n\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(tmpDir, \"file.txt\"), []byte(\"content2\"), 0644))\n\n\t\thash2, err := discovery.GenerateDirSHA256(tmpDir)\n\t\trequire.NoError(t, err)\n\n\t\tassert.NotEqual(t, hash1, hash2, \"Modified file should produce different hash\")\n\t})\n\n\tt.Run(\"file_rename_produces_different_hash\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttmpDir1 := t.TempDir()\n\t\ttmpDir2 := t.TempDir()\n\n\t\tcontent := []byte(\"same content\")\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(tmpDir1, \"original.txt\"), content, 0644))\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(tmpDir2, \"renamed.txt\"), content, 0644))\n\n\t\thash1, err := discovery.GenerateDirSHA256(tmpDir1)\n\t\trequire.NoError(t, err)\n\n\t\thash2, err := discovery.GenerateDirSHA256(tmpDir2)\n\t\trequire.NoError(t, err)\n\n\t\tassert.NotEqual(t, hash1, hash2, \"File rename (different path) should produce different hash\")\n\t})\n\n\tt.Run(\"file_move_to_subdirectory_produces_different_hash\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttmpDir1 := t.TempDir()\n\t\ttmpDir2 := t.TempDir()\n\n\t\tcontent := []byte(\"same content\")\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(tmpDir1, \"file.txt\"), content, 0644))\n\n\t\tsubDir := filepath.Join(tmpDir2, \"subdir\")\n\t\trequire.NoError(t, os.MkdirAll(subDir, 0755))\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(subDir, \"file.txt\"), content, 0644))\n\n\t\thash1, err := discovery.GenerateDirSHA256(tmpDir1)\n\t\trequire.NoError(t, err)\n\n\t\thash2, err := discovery.GenerateDirSHA256(tmpDir2)\n\t\trequire.NoError(t, err)\n\n\t\tassert.NotEqual(t, hash1, hash2, \"File move to subdirectory should produce different hash\")\n\t})\n\n\tt.Run(\"ignores_terragrunt_stack_manifest\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttmpDir1 := t.TempDir()\n\t\ttmpDir2 := t.TempDir()\n\n\t\tcontent := []byte(\"test content\")\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(tmpDir1, \"file.txt\"), content, 0644))\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(tmpDir2, \"file.txt\"), content, 0644))\n\n\t\t// Add .terragrunt-stack-manifest only to tmpDir2\n\t\tmanifestContent := []byte(\"/path/to/something\\n/another/path\")\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(tmpDir2, \".terragrunt-stack-manifest\"), manifestContent, 0644))\n\n\t\thash1, err := discovery.GenerateDirSHA256(tmpDir1)\n\t\trequire.NoError(t, err)\n\n\t\thash2, err := discovery.GenerateDirSHA256(tmpDir2)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, hash1, hash2, \".terragrunt-stack-manifest should be ignored in hash calculation\")\n\t})\n\n\tt.Run(\"multiple_files_order_independent\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttmpDir1 := t.TempDir()\n\t\ttmpDir2 := t.TempDir()\n\n\t\t// Create files in different order but same content\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(tmpDir1, \"a.txt\"), []byte(\"a\"), 0644))\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(tmpDir1, \"b.txt\"), []byte(\"b\"), 0644))\n\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(tmpDir2, \"b.txt\"), []byte(\"b\"), 0644))\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(tmpDir2, \"a.txt\"), []byte(\"a\"), 0644))\n\n\t\thash1, err := discovery.GenerateDirSHA256(tmpDir1)\n\t\trequire.NoError(t, err)\n\n\t\thash2, err := discovery.GenerateDirSHA256(tmpDir2)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, hash1, hash2, \"File creation order should not affect hash\")\n\t})\n\n\tt.Run(\"nonexistent_directory_returns_error\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t_, err := discovery.GenerateDirSHA256(\"/nonexistent/path/to/directory\")\n\t\trequire.Error(t, err)\n\t})\n\n\tt.Run(\"nested_directories_included\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttmpDir1 := t.TempDir()\n\t\ttmpDir2 := t.TempDir()\n\n\t\t// Create nested structure in both\n\t\tsubDir1 := filepath.Join(tmpDir1, \"sub\", \"nested\")\n\t\tsubDir2 := filepath.Join(tmpDir2, \"sub\", \"nested\")\n\n\t\trequire.NoError(t, os.MkdirAll(subDir1, 0755))\n\t\trequire.NoError(t, os.MkdirAll(subDir2, 0755))\n\n\t\tcontent := []byte(\"nested content\")\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(subDir1, \"file.txt\"), content, 0644))\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(subDir2, \"file.txt\"), content, 0644))\n\n\t\thash1, err := discovery.GenerateDirSHA256(tmpDir1)\n\t\trequire.NoError(t, err)\n\n\t\thash2, err := discovery.GenerateDirSHA256(tmpDir2)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, hash1, hash2, \"Nested directories with same structure should produce same hash\")\n\t})\n}\n\n// TestMatchComponentPairs tests the component pair matching logic.\nfunc TestMatchComponentPairs(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"matches_by_relative_path\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfromComponents := component.Components{\n\t\t\tcreateTestComponent(\"/worktree-from/app\", \"/worktree-from\"),\n\t\t\tcreateTestComponent(\"/worktree-from/db\", \"/worktree-from\"),\n\t\t}\n\n\t\ttoComponents := component.Components{\n\t\t\tcreateTestComponent(\"/worktree-to/app\", \"/worktree-to\"),\n\t\t\tcreateTestComponent(\"/worktree-to/db\", \"/worktree-to\"),\n\t\t}\n\n\t\tpairs, err := discovery.MatchComponentPairs(fromComponents, toComponents)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Len(t, pairs, 2, \"Should match 2 component pairs\")\n\n\t\t// Verify the pairs are correctly matched\n\t\tpaths := make(map[string]bool)\n\n\t\tfor _, p := range pairs {\n\t\t\tfromSuffix := getRelativePath(p.FromComponent)\n\t\t\ttoSuffix := getRelativePath(p.ToComponent)\n\t\t\tassert.Equal(t, fromSuffix, toSuffix, \"Matched components should have same relative paths\")\n\t\t\tpaths[fromSuffix] = true\n\t\t}\n\n\t\tassert.True(t, paths[\"/app\"], \"Should have matched app\")\n\t\tassert.True(t, paths[\"/db\"], \"Should have matched db\")\n\t})\n\n\tt.Run(\"handles_added_only_components\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfromComponents := component.Components{}\n\n\t\ttoComponents := component.Components{\n\t\t\tcreateTestComponent(\"/worktree-to/new-unit\", \"/worktree-to\"),\n\t\t}\n\n\t\tpairs, err := discovery.MatchComponentPairs(fromComponents, toComponents)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Empty(t, pairs, \"Added-only components should not produce pairs\")\n\t})\n\n\tt.Run(\"handles_removed_only_components\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfromComponents := component.Components{\n\t\t\tcreateTestComponent(\"/worktree-from/removed-unit\", \"/worktree-from\"),\n\t\t}\n\n\t\ttoComponents := component.Components{}\n\n\t\tpairs, err := discovery.MatchComponentPairs(fromComponents, toComponents)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Empty(t, pairs, \"Removed-only components should not produce pairs\")\n\t})\n\n\tt.Run(\"handles_renamed_components_no_match\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfromComponents := component.Components{\n\t\t\tcreateTestComponent(\"/worktree-from/old-name\", \"/worktree-from\"),\n\t\t}\n\n\t\ttoComponents := component.Components{\n\t\t\tcreateTestComponent(\"/worktree-to/new-name\", \"/worktree-to\"),\n\t\t}\n\n\t\tpairs, err := discovery.MatchComponentPairs(fromComponents, toComponents)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Empty(t, pairs, \"Renamed components (different paths) should not match\")\n\t})\n\n\tt.Run(\"handles_mixed_scenario\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfromComponents := component.Components{\n\t\t\tcreateTestComponent(\"/worktree-from/shared\", \"/worktree-from\"),\n\t\t\tcreateTestComponent(\"/worktree-from/removed\", \"/worktree-from\"),\n\t\t}\n\n\t\ttoComponents := component.Components{\n\t\t\tcreateTestComponent(\"/worktree-to/shared\", \"/worktree-to\"),\n\t\t\tcreateTestComponent(\"/worktree-to/added\", \"/worktree-to\"),\n\t\t}\n\n\t\tpairs, err := discovery.MatchComponentPairs(fromComponents, toComponents)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Len(t, pairs, 1, \"Should only match the shared component\")\n\t\tassert.Equal(t, \"/shared\", getRelativePath(pairs[0].FromComponent))\n\t\tassert.Equal(t, \"/shared\", getRelativePath(pairs[0].ToComponent))\n\t})\n\n\tt.Run(\"handles_empty_inputs\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tpairs, err := discovery.MatchComponentPairs(component.Components{}, component.Components{})\n\t\trequire.NoError(t, err)\n\n\t\tassert.Empty(t, pairs, \"Empty inputs should produce empty pairs\")\n\t})\n\n\tt.Run(\"handles_nested_paths\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfromComponents := component.Components{\n\t\t\tcreateTestComponent(\"/worktree-from/apps/frontend\", \"/worktree-from\"),\n\t\t\tcreateTestComponent(\"/worktree-from/apps/backend\", \"/worktree-from\"),\n\t\t}\n\n\t\ttoComponents := component.Components{\n\t\t\tcreateTestComponent(\"/worktree-to/apps/frontend\", \"/worktree-to\"),\n\t\t\tcreateTestComponent(\"/worktree-to/apps/backend\", \"/worktree-to\"),\n\t\t}\n\n\t\tpairs, err := discovery.MatchComponentPairs(fromComponents, toComponents)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Len(t, pairs, 2, \"Should match 2 nested component pairs\")\n\t})\n\n\tt.Run(\"returns_error_for_nil_discovery_context\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tnilCtxComponent := component.NewUnit(\"/some/path\")\n\t\tnilCtxComponent.SetDiscoveryContext(nil)\n\n\t\tfromComponents := component.Components{nilCtxComponent}\n\t\ttoComponents := component.Components{}\n\n\t\t_, err := discovery.MatchComponentPairs(fromComponents, toComponents)\n\t\trequire.Error(t, err)\n\n\t\tvar missingCtxErr discovery.MissingDiscoveryContextError\n\t\trequire.ErrorAs(t, err, &missingCtxErr)\n\t\tassert.Equal(t, \"/some/path\", missingCtxErr.ComponentPath)\n\t})\n}\n\n// TestTranslateDiscoveryContextArgsForWorktree tests the command argument translation for worktrees.\nfunc TestTranslateDiscoveryContextArgsForWorktree(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname             string\n\t\tcmd              string\n\t\targs             []string\n\t\tkind             discovery.WorktreeKind\n\t\texpectError      bool\n\t\texpectDestroyArg bool\n\t}{\n\t\t// fromWorktree cases - should add -destroy for plan/apply\n\t\t{\n\t\t\tname:             \"from_worktree_plan_adds_destroy\",\n\t\t\tcmd:              \"plan\",\n\t\t\targs:             []string{},\n\t\t\tkind:             discovery.FromWorktreeKind,\n\t\t\texpectError:      false,\n\t\t\texpectDestroyArg: true,\n\t\t},\n\t\t{\n\t\t\tname:             \"from_worktree_apply_adds_destroy\",\n\t\t\tcmd:              \"apply\",\n\t\t\targs:             []string{},\n\t\t\tkind:             discovery.FromWorktreeKind,\n\t\t\texpectError:      false,\n\t\t\texpectDestroyArg: true,\n\t\t},\n\t\t{\n\t\t\tname:             \"from_worktree_plan_with_other_args_adds_destroy\",\n\t\t\tcmd:              \"plan\",\n\t\t\targs:             []string{\"-out\", \"plan.out\"},\n\t\t\tkind:             discovery.FromWorktreeKind,\n\t\t\texpectError:      false,\n\t\t\texpectDestroyArg: true,\n\t\t},\n\t\t{\n\t\t\tname:             \"from_worktree_plan_with_destroy_already_present_errors\",\n\t\t\tcmd:              \"plan\",\n\t\t\targs:             []string{\"-destroy\"},\n\t\t\tkind:             discovery.FromWorktreeKind,\n\t\t\texpectError:      true,\n\t\t\texpectDestroyArg: false,\n\t\t},\n\t\t{\n\t\t\tname:             \"from_worktree_empty_command_allowed\",\n\t\t\tcmd:              \"\",\n\t\t\targs:             []string{},\n\t\t\tkind:             discovery.FromWorktreeKind,\n\t\t\texpectError:      false,\n\t\t\texpectDestroyArg: false,\n\t\t},\n\t\t{\n\t\t\tname:             \"from_worktree_unsupported_command_errors\",\n\t\t\tcmd:              \"destroy\",\n\t\t\targs:             []string{},\n\t\t\tkind:             discovery.FromWorktreeKind,\n\t\t\texpectError:      true,\n\t\t\texpectDestroyArg: false,\n\t\t},\n\t\t{\n\t\t\tname:             \"from_worktree_output_command_errors\",\n\t\t\tcmd:              \"output\",\n\t\t\targs:             []string{},\n\t\t\tkind:             discovery.FromWorktreeKind,\n\t\t\texpectError:      true,\n\t\t\texpectDestroyArg: false,\n\t\t},\n\t\t// toWorktree cases - should NOT add -destroy for plan/apply\n\t\t{\n\t\t\tname:             \"to_worktree_plan_no_destroy\",\n\t\t\tcmd:              \"plan\",\n\t\t\targs:             []string{},\n\t\t\tkind:             discovery.ToWorktreeKind,\n\t\t\texpectError:      false,\n\t\t\texpectDestroyArg: false,\n\t\t},\n\t\t{\n\t\t\tname:             \"to_worktree_apply_no_destroy\",\n\t\t\tcmd:              \"apply\",\n\t\t\targs:             []string{},\n\t\t\tkind:             discovery.ToWorktreeKind,\n\t\t\texpectError:      false,\n\t\t\texpectDestroyArg: false,\n\t\t},\n\t\t{\n\t\t\tname:             \"to_worktree_plan_with_other_args\",\n\t\t\tcmd:              \"plan\",\n\t\t\targs:             []string{\"-out\", \"plan.out\"},\n\t\t\tkind:             discovery.ToWorktreeKind,\n\t\t\texpectError:      false,\n\t\t\texpectDestroyArg: false,\n\t\t},\n\t\t{\n\t\t\tname:             \"to_worktree_plan_with_destroy_already_present_errors\",\n\t\t\tcmd:              \"plan\",\n\t\t\targs:             []string{\"-destroy\"},\n\t\t\tkind:             discovery.ToWorktreeKind,\n\t\t\texpectError:      true,\n\t\t\texpectDestroyArg: false,\n\t\t},\n\t\t{\n\t\t\tname:             \"to_worktree_empty_command_allowed\",\n\t\t\tcmd:              \"\",\n\t\t\targs:             []string{},\n\t\t\tkind:             discovery.ToWorktreeKind,\n\t\t\texpectError:      false,\n\t\t\texpectDestroyArg: false,\n\t\t},\n\t\t{\n\t\t\tname:             \"to_worktree_unsupported_command_errors\",\n\t\t\tcmd:              \"destroy\",\n\t\t\targs:             []string{},\n\t\t\tkind:             discovery.ToWorktreeKind,\n\t\t\texpectError:      true,\n\t\t\texpectDestroyArg: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tdc := &component.DiscoveryContext{\n\t\t\t\tCmd:  tt.cmd,\n\t\t\t\tArgs: tt.args,\n\t\t\t}\n\n\t\t\tresult, err := discovery.TranslateDiscoveryContextArgsForWorktree(dc, tt.kind)\n\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), \"Git-based filtering is not supported\")\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NotNil(t, result)\n\n\t\t\tif tt.expectDestroyArg {\n\t\t\t\tassert.Contains(t, result.Args, \"-destroy\",\n\t\t\t\t\t\"Expected -destroy flag for %s command in from worktree\", tt.cmd)\n\t\t\t} else if tt.cmd == \"plan\" || tt.cmd == \"apply\" {\n\t\t\t\t// For to worktrees, verify -destroy is not added\n\t\t\t\tassert.False(t, slices.Contains(result.Args, \"-destroy\"),\n\t\t\t\t\t\"Did not expect -destroy flag for %s command in to worktree\", tt.cmd)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestWorktreeKind tests the worktreeKind constants.\nfunc TestWorktreeKind(t *testing.T) {\n\tt.Parallel()\n\n\tassert.Equal(t, discovery.FromWorktreeKind, discovery.WorktreeKind(0))\n\tassert.Equal(t, discovery.ToWorktreeKind, discovery.WorktreeKind(1))\n\tassert.NotEqual(t, discovery.FromWorktreeKind, discovery.ToWorktreeKind)\n}\n\n// Helper function to create a test component with discovery context.\nfunc createTestComponent(path, workingDir string) component.Component {\n\tc := component.NewUnit(path)\n\tc.SetDiscoveryContext(&component.DiscoveryContext{\n\t\tWorkingDir: workingDir,\n\t})\n\n\treturn c\n}\n\n// Helper function to get the relative path of a component.\nfunc getRelativePath(c component.Component) string {\n\tdc := c.DiscoveryContext()\n\tif dc == nil {\n\t\treturn c.Path()\n\t}\n\n\trel := c.Path()[len(dc.WorkingDir):]\n\tif rel == \"\" {\n\t\treturn \"/\"\n\t}\n\n\treturn filepath.Clean(rel)\n}\n"
  },
  {
    "path": "internal/discovery/types.go",
    "content": "package discovery\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/worktrees\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// Type aliases for filter package types used throughout discovery.\n// These provide backward compatibility and shorter type names within the discovery package.\ntype (\n\t// ClassificationStatus is an alias for filter.ClassificationStatus.\n\tClassificationStatus = filter.ClassificationStatus\n\t// CandidacyReason is an alias for filter.CandidacyReason.\n\tCandidacyReason = filter.CandidacyReason\n\t// GraphExpressionInfo is an alias for filter.GraphExpressionInfo.\n\tGraphExpressionInfo = filter.GraphExpressionInfo\n)\n\n// Status constants are aliases for filter package constants.\nconst (\n\tStatusDiscovered = filter.StatusDiscovered\n\tStatusCandidate  = filter.StatusCandidate\n\tStatusExcluded   = filter.StatusExcluded\n)\n\n// CandidacyReason constants are aliases for filter package constants.\nconst (\n\tCandidacyReasonNone               = filter.CandidacyReasonNone\n\tCandidacyReasonGraphTarget        = filter.CandidacyReasonGraphTarget\n\tCandidacyReasonRequiresParse      = filter.CandidacyReasonRequiresParse\n\tCandidacyReasonPotentialDependent = filter.CandidacyReasonPotentialDependent\n)\n\n// PhaseKind identifies the type of discovery phase.\ntype PhaseKind int\n\nconst (\n\t// PhaseFilesystem walks directories to find terragrunt configurations.\n\tPhaseFilesystem PhaseKind = iota\n\t// PhaseWorktree discovers components in Git worktrees (concurrent with Filesystem).\n\tPhaseWorktree\n\t// PhaseParse parses HCL configurations for filter evaluation.\n\tPhaseParse\n\t// PhaseGraph traverses dependency/dependent relationships.\n\tPhaseGraph\n\t// PhaseRelationship builds dependency graph for orphan components.\n\tPhaseRelationship\n\t// PhaseFinal applies final filter evaluation and cycle checking.\n\tPhaseFinal\n)\n\n// String returns a string representation of the PhaseKind.\nfunc (pk PhaseKind) String() string {\n\tswitch pk {\n\tcase PhaseFilesystem:\n\t\treturn \"filesystem\"\n\tcase PhaseWorktree:\n\t\treturn \"worktree\"\n\tcase PhaseParse:\n\t\treturn \"parse\"\n\tcase PhaseGraph:\n\t\treturn \"graph\"\n\tcase PhaseRelationship:\n\t\treturn \"relationship\"\n\tcase PhaseFinal:\n\t\treturn \"final\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// DiscoveryResult represents a discovered or candidate component with metadata.\ntype DiscoveryResult struct {\n\t// Component is the discovered Terragrunt component.\n\tComponent component.Component\n\t// Status indicates whether this is a definite discovery, candidate, or excluded.\n\tStatus ClassificationStatus\n\t// Reason explains why the component is a candidate (only meaningful when Status == StatusCandidate).\n\tReason CandidacyReason\n\t// Phase indicates which phase produced this result.\n\tPhase PhaseKind\n\t// GraphExpressionIndex is the index of the graph expression that matched (for candidates).\n\t// This is used during the graph phase to determine how to traverse.\n\tGraphExpressionIndex int\n}\n\n// PhaseResults contains the results from running a discovery phase.\n// It provides thread-safe methods for collecting results during concurrent processing.\ntype PhaseResults struct {\n\t// Discovered contains components definitively included in results.\n\tDiscovered []DiscoveryResult\n\t// Candidates contains components that might be included pending further evaluation.\n\tCandidates []DiscoveryResult\n\t// mu protects concurrent access to Discovered and Candidates.\n\tmu sync.Mutex\n}\n\n// NewPhaseResults creates a new PhaseResults.\nfunc NewPhaseResults() *PhaseResults {\n\treturn &PhaseResults{}\n}\n\n// AddDiscovered adds a discovered result to the results in a thread-safe manner.\nfunc (pr *PhaseResults) AddDiscovered(result DiscoveryResult) {\n\tpr.mu.Lock()\n\tdefer pr.mu.Unlock()\n\n\tpr.Discovered = append(pr.Discovered, result)\n}\n\n// AddCandidate adds a candidate result to the results in a thread-safe manner.\nfunc (pr *PhaseResults) AddCandidate(result DiscoveryResult) {\n\tpr.mu.Lock()\n\tdefer pr.mu.Unlock()\n\n\tpr.Candidates = append(pr.Candidates, result)\n}\n\n// PhaseInput provides input data to a discovery phase.\ntype PhaseInput struct {\n\tOpts       *options.TerragruntOptions\n\tClassifier *filter.Classifier\n\tDiscovery  *Discovery\n\tComponents component.Components\n\tCandidates []DiscoveryResult\n}\n\n// Phase defines the interface for a discovery phase.\ntype Phase interface {\n\t// Name returns the human-readable name of the phase.\n\tName() string\n\t// Kind returns the PhaseKind identifier.\n\tKind() PhaseKind\n\t// Run executes the phase with the given input and returns the result and any error.\n\tRun(ctx context.Context, l log.Logger, input *PhaseInput) (*PhaseResults, error)\n}\n\n// Discovery is the main configuration for discovery.\ntype Discovery struct {\n\t// discoveryContext is the context in which the discovery is happening.\n\tdiscoveryContext *component.DiscoveryContext\n\n\t// worktrees is the worktrees created for Git-based filters.\n\tworktrees *worktrees.Worktrees\n\n\t// workingDir is the directory to search for Terragrunt configurations.\n\tworkingDir string\n\n\t// gitRoot is the git repository root, used as boundary for dependent discovery.\n\tgitRoot string\n\n\t// graphTarget is the target path for graph filtering (prune to target + dependents).\n\tgraphTarget string\n\n\t// configFilenames is the list of config filenames to discover. If nil, defaults are used.\n\tconfigFilenames []string\n\n\t// parserOptions are custom HCL parser options to use when parsing during discovery.\n\tparserOptions []hclparse.Option\n\n\t// filters contains filter queries for component selection.\n\tfilters filter.Filters\n\n\t// classifier categorizes filter expressions for efficient evaluation.\n\tclassifier *filter.Classifier\n\n\t// gitExpressions contains Git filter expressions that require worktree discovery.\n\tgitExpressions filter.GitExpressions\n\n\t// maxDependencyDepth is the maximum depth of the dependency tree to discover.\n\tmaxDependencyDepth int\n\n\t// numWorkers determines the number of concurrent workers for discovery operations.\n\tnumWorkers int\n\n\t// noHidden determines whether to detect configurations in hidden directories.\n\tnoHidden bool\n\n\t// requiresParse is true when the discovery requires parsing Terragrunt configurations.\n\trequiresParse bool\n\n\t// parseExclude determines whether to parse exclude configurations.\n\tparseExclude bool\n\n\t// parseIncludes determines whether to parse for include configurations.\n\tparseIncludes bool\n\n\t// readFiles determines whether to parse for reading files.\n\treadFiles bool\n\n\t// suppressParseErrors determines whether to suppress errors when parsing Terragrunt configurations.\n\tsuppressParseErrors bool\n\n\t// breakCycles determines whether to break cycles in the dependency graph if any exist.\n\tbreakCycles bool\n\n\t// excludeByDefault determines whether to exclude configurations by default (triggered by include flags).\n\texcludeByDefault bool\n\n\t// discoverRelationships determines whether to run relationship discovery.\n\tdiscoverRelationships bool\n}\n"
  },
  {
    "path": "internal/engine/engine.go",
    "content": "// Package engine provides the pluggable IaC engine for Terragrunt.\npackage engine\n\nimport (\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\"\n\tlogwriter \"github.com/gruntwork-io/terragrunt/pkg/log/writer\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cache\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/github\"\n\t\"github.com/gruntwork-io/terragrunt/internal/os/signal\"\n\n\t\"github.com/hashicorp/go-hclog\"\n\n\t\"google.golang.org/grpc/credentials/insecure\"\n\n\t\"github.com/gruntwork-io/terragrunt-engine-go/engine\"\n\t\"github.com/gruntwork-io/terragrunt-engine-go/proto\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/internal/writer\"\n\t\"github.com/hashicorp/go-plugin\"\n\t\"google.golang.org/grpc\"\n\t\"google.golang.org/protobuf/types/known/anypb\"\n\t\"google.golang.org/protobuf/types/known/structpb\"\n)\n\nconst (\n\tengineVersion     = 1\n\tengineCookieKey   = \"engine\"\n\tengineCookieValue = \"terragrunt\"\n\n\tdefaultCacheDir                             = \".cache\"\n\tdefaultEngineCachePath                      = \"terragrunt/plugins/iac-engine\"\n\tprefixTrim                                  = \"terragrunt-\"\n\tfileNameFormat                              = \"terragrunt-iac-%s_%s_%s_%s_%s\"\n\tchecksumFileNameFormat                      = \"terragrunt-iac-%s_%s_%s_SHA256SUMS\"\n\tengineLogLevelEnv                           = \"TG_ENGINE_LOG_LEVEL\"\n\tdefaultEngineRepoRoot                       = \"github.com/\"\n\tterraformCommandContextKey engineClientsKey = iota\n\tlocksContextKey            engineLocksKey   = iota\n\tlatestVersionsContextKey   engineLocksKey   = iota\n\n\tdirPerm = 0755\n\n\terrMsgEngineClientsFetch = \"failed to fetch engine clients from context\"\n\terrMsgEngineClientsCast  = \"failed to cast engine clients from context\"\n\terrMsgVersionsCacheFetch = \"failed to fetch engine versions cache from context\"\n\terrMsgVersionsCacheCast  = \"failed to cast engine versions cache from context\"\n)\n\ntype (\n\tengineClientsKey byte\n\tengineLocksKey   byte\n)\n\ntype ExecutionOptions struct {\n\tWriters           writer.Writers\n\tEngineOptions     *EngineOptions\n\tEngineConfig      *EngineConfig\n\tEnv               map[string]string\n\tWorkingDir        string\n\tRootWorkingDir    string\n\tCommand           string\n\tArgs              []string\n\tHeadless          bool\n\tForwardTFStdout   bool\n\tSuppressStdout    bool\n\tAllocatePseudoTty bool\n}\n\ntype engineInstance struct {\n\tengineClient *proto.EngineClient\n\tclient       *plugin.Client\n\texecOptions  *ExecutionOptions\n}\n\n// Run executes the given command with the experimental engine.\nfunc Run(\n\tctx context.Context,\n\tl log.Logger,\n\texecOptions *ExecutionOptions,\n) (*util.CmdOutput, error) {\n\tengineClients, err := engineClientsFromContext(ctx)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tworkingDir := execOptions.WorkingDir\n\tinstance, found := engineClients.Load(workingDir)\n\t// initialize engine for working directory\n\tif !found {\n\t\t// download engine if not available\n\t\tif err = downloadEngine(ctx, l, execOptions); err != nil {\n\t\t\treturn nil, errors.New(err)\n\t\t}\n\n\t\tterragruntEngine, client, createEngineErr := createEngine(ctx, l, execOptions)\n\t\tif createEngineErr != nil {\n\t\t\treturn nil, errors.New(createEngineErr)\n\t\t}\n\n\t\tengineClients.Store(workingDir, &engineInstance{\n\t\t\tengineClient: terragruntEngine,\n\t\t\tclient:       client,\n\t\t\texecOptions:  execOptions,\n\t\t})\n\n\t\tinstance, _ = engineClients.Load(workingDir)\n\n\t\tif err = initialize(ctx, l, execOptions, terragruntEngine); err != nil {\n\t\t\treturn nil, errors.New(err)\n\t\t}\n\t}\n\n\tengInst, ok := instance.(*engineInstance)\n\tif !ok {\n\t\treturn nil, errors.Errorf(\"failed to fetch engine instance %s\", workingDir)\n\t}\n\n\tterragruntEngine := engInst.engineClient\n\n\toutput, err := invoke(ctx, l, execOptions, terragruntEngine)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\treturn output, nil\n}\n\n// WithEngineValues add to context default values for engine.\nfunc WithEngineValues(ctx context.Context) context.Context {\n\tctx = context.WithValue(ctx, terraformCommandContextKey, &sync.Map{})\n\tctx = context.WithValue(ctx, locksContextKey, util.NewKeyLocks())\n\tctx = context.WithValue(ctx, latestVersionsContextKey, cache.NewCache[string](\"engineVersions\"))\n\n\treturn ctx\n}\n\n// downloadEngine downloads the engine for the given options.\nfunc downloadEngine(ctx context.Context, l log.Logger, execOptions *ExecutionOptions) error {\n\te := execOptions.EngineConfig\n\tif e == nil {\n\t\treturn nil\n\t}\n\n\tif util.FileExists(e.Source) {\n\t\t// if source is a file, no need to download, exit\n\t\treturn nil\n\t}\n\n\t// If source is empty, we cannot download the engine\n\t// This indicates an engine block was configured but source was not provided\n\tif e.Source == \"\" {\n\t\treturn errors.Errorf(\n\t\t\t\"engine block is configured but source is empty. Please provide an engine source or remove the engine block\",\n\t\t)\n\t}\n\n\t// identify engine version if not specified\n\tif len(e.Version) == 0 {\n\t\tif !strings.Contains(e.Source, \"://\") {\n\t\t\ttag, err := lastReleaseVersion(ctx, execOptions)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.New(err)\n\t\t\t}\n\n\t\t\te.Version = tag\n\t\t}\n\t}\n\n\tpath, err := engineDir(execOptions)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tif ensureErr := util.EnsureDirectory(path); ensureErr != nil {\n\t\treturn errors.New(ensureErr)\n\t}\n\n\tlocalEngineFile := filepath.Join(path, engineFileName(e))\n\n\t// lock downloading process for only one instance\n\tlocks, err := downloadLocksFromContext(ctx)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\t// locking by file where engine is downloaded\n\t// however, it will not help in case of multiple parallel Terragrunt runs\n\tlocks.Lock(localEngineFile)\n\tdefer locks.Unlock(localEngineFile)\n\n\tif util.FileExists(localEngineFile) {\n\t\treturn nil\n\t}\n\n\tdownloadFile := filepath.Join(path, enginePackageName(e))\n\n\t// Prepare download assets\n\tassets := &github.ReleaseAssets{\n\t\tRepository:  e.Source,\n\t\tVersion:     e.Version,\n\t\tPackageFile: downloadFile,\n\t}\n\n\tvar checksumFile, checksumSigFile string\n\n\t// Only add checksum files for GitHub releases (not direct URLs)\n\tif !strings.Contains(e.Source, \"://\") {\n\t\tchecksumFile = filepath.Join(path, engineChecksumName(e))\n\t\tchecksumSigFile = filepath.Join(path, engineChecksumSigName(e))\n\t\tassets.ChecksumFile = checksumFile\n\t\tassets.ChecksumSigFile = checksumSigFile\n\t}\n\n\t// Create download client and download assets\n\tdownloadClient := github.NewGitHubReleasesDownloadClient(github.WithLogger(l))\n\n\tresult, err := downloadClient.DownloadReleaseAssets(ctx, assets)\n\tif err != nil {\n\t\treturn errors.Errorf(\"failed to download engine assets: %w\", err)\n\t}\n\n\t// Update file paths from result\n\tdownloadFile = result.PackageFile\n\tchecksumFile = result.ChecksumFile\n\tchecksumSigFile = result.ChecksumSigFile\n\n\tif !execOptions.EngineOptions.SkipChecksumCheck && checksumFile != \"\" && checksumSigFile != \"\" {\n\t\tl.Infof(\"Verifying checksum for %s\", downloadFile)\n\n\t\tif err := verifyFile(downloadFile, checksumFile, checksumSigFile); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\t} else {\n\t\tl.Warnf(\"Skipping verification for %s\", downloadFile)\n\t}\n\n\tif err := extractArchive(l, downloadFile, localEngineFile); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tl.Infof(\"Engine available as %s\", path)\n\n\treturn nil\n}\n\nfunc lastReleaseVersion(ctx context.Context, opts *ExecutionOptions) (string, error) {\n\trepository := strings.TrimPrefix(opts.EngineConfig.Source, defaultEngineRepoRoot)\n\n\tversionCache, err := engineVersionsCacheFromContext(ctx)\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\tcacheKey := \"github_release_\" + repository\n\tif val, found := versionCache.Get(ctx, cacheKey); found {\n\t\treturn val, nil\n\t}\n\n\tgithubClient := github.NewGitHubAPIClient(github.WithGithubComDefaultAuth())\n\n\ttag, err := githubClient.GetLatestReleaseTag(ctx, repository)\n\tif err != nil {\n\t\treturn \"\", errors.Errorf(\"failed to get latest release for repository %s: %w\", repository, err)\n\t}\n\n\tversionCache.Put(ctx, cacheKey, tag)\n\n\treturn tag, nil\n}\n\nfunc extractArchive(l log.Logger, downloadFile string, engineFile string) error {\n\tif !isArchiveByHeader(l, downloadFile) {\n\t\tl.Info(\"Downloaded file is not an archive, no extraction needed\")\n\t\t// move file directly if it is not an archive\n\t\tif err := os.Rename(downloadFile, engineFile); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\treturn nil\n\t}\n\t// extract package and process files\n\tpath := filepath.Dir(engineFile)\n\n\ttempDir, err := os.MkdirTemp(path, \"temp-\")\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tdefer func() {\n\t\tif err = os.RemoveAll(tempDir); err != nil {\n\t\t\tl.Warnf(\"Failed to clean temp dir %s: %v\", tempDir, err)\n\t\t}\n\t}()\n\t// extract archive\n\tif err = extract(l, downloadFile, tempDir); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t// process files\n\tfiles, err := os.ReadDir(tempDir)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tl.Infof(\"Engine extracted to %s\", path)\n\n\tif len(files) == 1 && !files[0].IsDir() {\n\t\t// handle case where archive contains a single file, most of the cases\n\t\tsingleFile := filepath.Join(tempDir, files[0].Name())\n\t\tif err := os.Rename(singleFile, engineFile); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// Move all files to the engine directory\n\tfor _, file := range files {\n\t\tsrcPath := filepath.Join(tempDir, file.Name())\n\n\t\tdstPath := filepath.Join(path, file.Name())\n\t\tif err := os.Rename(srcPath, dstPath); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// engineDir returns the directory path where engine files are stored.\nfunc engineDir(opts *ExecutionOptions) (string, error) {\n\tengine := opts.EngineConfig\n\tif util.FileExists(engine.Source) {\n\t\treturn filepath.Dir(engine.Source), nil\n\t}\n\n\tcacheDir := opts.EngineOptions.CachePath\n\tif len(cacheDir) == 0 {\n\t\thomeDir, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn \"\", errors.New(err)\n\t\t}\n\n\t\tcacheDir = filepath.Join(homeDir, defaultCacheDir)\n\t}\n\n\tplatform := runtime.GOOS\n\tarch := runtime.GOARCH\n\n\treturn filepath.Join(cacheDir, defaultEngineCachePath, engine.Type, engine.Version, platform, arch), nil\n}\n\n// engineFileName returns the file name for the engine.\nfunc engineFileName(e *EngineConfig) string {\n\tengineName := filepath.Base(e.Source)\n\tif util.FileExists(e.Source) {\n\t\t// return file name if source is absolute path\n\t\treturn engineName\n\t}\n\n\tplatform := runtime.GOOS\n\tarch := runtime.GOARCH\n\tengineName = strings.TrimPrefix(engineName, prefixTrim)\n\n\treturn fmt.Sprintf(fileNameFormat, engineName, e.Type, e.Version, platform, arch)\n}\n\n// engineChecksumName returns the file name of engine checksum file\nfunc engineChecksumName(e *EngineConfig) string {\n\tengineName := filepath.Base(e.Source)\n\n\tengineName = strings.TrimPrefix(engineName, prefixTrim)\n\n\treturn fmt.Sprintf(checksumFileNameFormat, engineName, e.Type, e.Version)\n}\n\n// engineChecksumSigName returns the file name of engine checksum file signature\nfunc engineChecksumSigName(e *EngineConfig) string {\n\treturn engineChecksumName(e) + \".sig\"\n}\n\n// enginePackageName returns the package name for the engine.\nfunc enginePackageName(e *EngineConfig) string {\n\treturn engineFileName(e) + \".zip\"\n}\n\n// isArchiveByHeader checks if a file is an archive by examining its header.\nfunc isArchiveByHeader(l log.Logger, filePath string) bool {\n\tarchiveType, err := detectFileType(l, filePath)\n\n\treturn err == nil && archiveType != \"\"\n}\n\n// engineClientsFromContext returns the engine clients map from the context.\nfunc engineClientsFromContext(ctx context.Context) (*sync.Map, error) {\n\tval := ctx.Value(terraformCommandContextKey)\n\tif val == nil {\n\t\treturn nil, errors.New(errMsgEngineClientsFetch)\n\t}\n\n\tresult, ok := val.(*sync.Map)\n\tif !ok {\n\t\treturn nil, errors.New(errMsgEngineClientsCast)\n\t}\n\n\treturn result, nil\n}\n\n// downloadLocksFromContext returns the locks map from the context.\nfunc downloadLocksFromContext(ctx context.Context) (*util.KeyLocks, error) {\n\tval := ctx.Value(locksContextKey)\n\tif val == nil {\n\t\treturn nil, errors.New(errMsgEngineClientsFetch)\n\t}\n\n\tresult, ok := val.(*util.KeyLocks)\n\tif !ok {\n\t\treturn nil, errors.New(errMsgEngineClientsCast)\n\t}\n\n\treturn result, nil\n}\n\nfunc engineVersionsCacheFromContext(ctx context.Context) (*cache.Cache[string], error) {\n\tval := ctx.Value(latestVersionsContextKey)\n\tif val == nil {\n\t\treturn nil, errors.New(errMsgVersionsCacheFetch)\n\t}\n\n\tresult, ok := val.(*cache.Cache[string])\n\tif !ok {\n\t\treturn nil, errors.New(errMsgVersionsCacheCast)\n\t}\n\n\treturn result, nil\n}\n\nconst (\n\tgracefulExitTimeout    = 5 * time.Second\n\tpluginExitPollInterval = 50 * time.Millisecond\n)\n\n// Shutdown shuts down the experimental engine.\nfunc Shutdown(ctx context.Context, l log.Logger, experiments experiment.Experiments, noEngine bool) error {\n\tif !experiments.Evaluate(experiment.IacEngine) || noEngine {\n\t\treturn nil\n\t}\n\n\t// iterate over all engine instances and shutdown\n\tengineClients, err := engineClientsFromContext(ctx)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tengineClients.Range(func(key, value any) bool {\n\t\tinstance := value.(*engineInstance)\n\t\tl.Debugf(\"Shutting down engine for %s\", instance.execOptions.WorkingDir)\n\n\t\t// We use without cancel here to ensure that the shutdown isn't cancelled by the main context,\n\t\t// like it is in the RunCommandWithOutput function. This ensures that we don't cancel the shutdown\n\t\t// when the command is cancelled.\n\t\tif err := shutdown(\n\t\t\tcontext.WithoutCancel(ctx),\n\t\t\tl,\n\t\t\tinstance.execOptions,\n\t\t\tinstance.engineClient,\n\t\t); err != nil {\n\t\t\tl.Errorf(\"Error shutting down engine: %v\", err)\n\t\t}\n\n\t\t// Wait for plugin to exit gracefully before force-killing.\n\t\t// The shutdown RPC has already told the plugin to exit, so it should\n\t\t// be cleaning up and exiting on its own. Give it time to finish.\n\t\tif !waitForPluginExit(instance.client, gracefulExitTimeout) {\n\t\t\tl.Debugf(\"Plugin did not exit gracefully within timeout, force killing\")\n\t\t\tinstance.client.Kill()\n\t\t}\n\n\t\treturn true\n\t})\n\n\treturn nil\n}\n\n// waitForPluginExit waits for the plugin process to exit, returning true if it exited\n// within the timeout, false otherwise.\nfunc waitForPluginExit(client *plugin.Client, timeout time.Duration) bool {\n\tdone := make(chan struct{})\n\n\tgo func() {\n\t\t// Client.Exited() returns true when the plugin process has exited\n\t\tfor !client.Exited() {\n\t\t\ttime.Sleep(pluginExitPollInterval)\n\t\t}\n\n\t\tclose(done)\n\t}()\n\n\tselect {\n\tcase <-done:\n\t\treturn true\n\tcase <-time.After(timeout):\n\t\treturn false\n\t}\n}\n\n// logEngineMessage logs a message from the engine at the appropriate log level.\nfunc logEngineMessage(l log.Logger, logLevel proto.LogLevel, content string) {\n\tswitch logLevel {\n\tcase proto.LogLevel_LOG_LEVEL_DEBUG:\n\t\tl.Debug(content)\n\tcase proto.LogLevel_LOG_LEVEL_INFO:\n\t\tl.Info(content)\n\tcase proto.LogLevel_LOG_LEVEL_WARN:\n\t\tl.Warn(content)\n\tcase proto.LogLevel_LOG_LEVEL_ERROR:\n\t\tl.Error(content)\n\tcase proto.LogLevel_LOG_LEVEL_UNSPECIFIED:\n\t\t// Treat unspecified as debug level\n\t\tl.Debug(content)\n\t}\n}\n\n// createEngine create engine for working directory\nfunc createEngine(\n\tctx context.Context,\n\tl log.Logger,\n\texecOptions *ExecutionOptions,\n) (*proto.EngineClient, *plugin.Client, error) {\n\tif execOptions.EngineConfig == nil {\n\t\treturn nil, nil, errors.Errorf(\"engine options are nil\")\n\t}\n\n\t// If source is empty, we cannot determine the engine file path\n\tif execOptions.EngineConfig.Source == \"\" {\n\t\treturn nil, nil, errors.Errorf(\"engine source is empty, cannot create engine\")\n\t}\n\n\tpath, err := engineDir(execOptions)\n\tif err != nil {\n\t\treturn nil, nil, errors.New(err)\n\t}\n\n\tlocalEnginePath := filepath.Join(path, engineFileName(execOptions.EngineConfig))\n\tlocalChecksumFile := filepath.Join(path, engineChecksumName(execOptions.EngineConfig))\n\tlocalChecksumSigFile := filepath.Join(path, engineChecksumSigName(execOptions.EngineConfig))\n\n\t// validate engine before loading if verification is not disabled\n\tskipCheck := execOptions.EngineOptions.SkipChecksumCheck\n\tif !skipCheck && util.FileExists(localEnginePath) && util.FileExists(localChecksumFile) &&\n\t\tutil.FileExists(localChecksumSigFile) {\n\t\tif err = verifyFile(localEnginePath, localChecksumFile, localChecksumSigFile); err != nil {\n\t\t\treturn nil, nil, errors.New(err)\n\t\t}\n\t} else {\n\t\tl.Warnf(\"Skipping verification for %s\", localEnginePath)\n\t}\n\n\tl.Debugf(\"Creating engine %s\", localEnginePath)\n\n\tengineLogLevel := execOptions.EngineOptions.LogLevel\n\n\tif len(engineLogLevel) == 0 {\n\t\tengineLogLevel = hclog.Warn.String()\n\t\t// update log level if it is different from info\n\t\tif l.Level() != log.InfoLevel {\n\t\t\tengineLogLevel = l.Level().String()\n\t\t}\n\t\t// turn off log formatting if disabled for Terragrunt\n\t\tif l.Formatter().DisabledOutput() {\n\t\t\tengineLogLevel = hclog.Off.String()\n\t\t}\n\t}\n\n\tlogger := hclog.NewInterceptLogger(&hclog.LoggerOptions{\n\t\tLevel:  hclog.LevelFromString(engineLogLevel),\n\t\tOutput: l.Writer(),\n\t})\n\n\t// We use without cancel here to ensure that the plugin isn't killed when the main context is cancelled,\n\t// like it is in the RunCommandWithOutput function. This ensures that we don't cancel the shutdown\n\t// when the command is cancelled.\n\tcmd := exec.CommandContext(\n\t\tcontext.WithoutCancel(ctx),\n\t\tlocalEnginePath,\n\t)\n\tcmd.Cancel = func() error {\n\t\tif cmd.Process == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tif sig := signal.SignalFromContext(ctx); sig != nil {\n\t\t\treturn cmd.Process.Signal(sig)\n\t\t}\n\n\t\treturn cmd.Process.Signal(os.Kill)\n\t}\n\t// pass log level to engine\n\tcmd.Env = append(cmd.Env, fmt.Sprintf(\"%s=%s\", engineLogLevelEnv, engineLogLevel))\n\tclient := plugin.NewClient(&plugin.ClientConfig{\n\t\tLogger: logger,\n\t\tHandshakeConfig: plugin.HandshakeConfig{\n\t\t\tProtocolVersion:  engineVersion,\n\t\t\tMagicCookieKey:   engineCookieKey,\n\t\t\tMagicCookieValue: engineCookieValue,\n\t\t},\n\t\tPlugins: map[string]plugin.Plugin{\n\t\t\t\"plugin\": &engine.TerragruntGRPCEngine{},\n\t\t},\n\t\tCmd: cmd,\n\t\tGRPCDialOptions: []grpc.DialOption{\n\t\t\tgrpc.WithTransportCredentials(insecure.NewCredentials()),\n\t\t},\n\t\tAllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},\n\t})\n\n\trpcClient, err := client.Client()\n\tif err != nil {\n\t\treturn nil, nil, errors.New(err)\n\t}\n\n\trawClient, err := rpcClient.Dispense(\"plugin\")\n\tif err != nil {\n\t\treturn nil, nil, errors.New(err)\n\t}\n\n\tterragruntEngine := rawClient.(proto.EngineClient)\n\n\treturn &terragruntEngine, client, nil\n}\n\n// invoke engine for working directory\nfunc invoke(ctx context.Context, l log.Logger, runOptions *ExecutionOptions, client *proto.EngineClient) (*util.CmdOutput, error) {\n\tl = l.WithField(placeholders.TFPathKeyName, \"engine\")\n\n\tmeta, err := ConvertMetaToProtobuf(runOptions.EngineConfig.Meta)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tresponse, err := (*client).Run(ctx, &proto.RunRequest{\n\t\tCommand:           runOptions.Command,\n\t\tArgs:              runOptions.Args,\n\t\tAllocatePseudoTty: runOptions.AllocatePseudoTty,\n\t\tWorkingDir:        runOptions.WorkingDir,\n\t\tMeta:              meta,\n\t\tEnvVars:           runOptions.Env,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\t// Determine log levels based on headless mode (similar to buildOutWriter/buildErrWriter)\n\tstdoutLogLevel := log.StdoutLevel\n\tstderrLogLevel := log.StderrLevel\n\n\tstdoutWriter := writer.ExtractOriginalWriter(runOptions.Writers.Writer)\n\tstderrWriter := writer.ExtractOriginalWriter(runOptions.Writers.ErrWriter)\n\n\tif runOptions.Headless && !runOptions.ForwardTFStdout {\n\t\tstdoutLogLevel = log.InfoLevel\n\t\tstderrLogLevel = log.ErrorLevel\n\t\tstdoutWriter = writer.ExtractOriginalWriter(runOptions.Writers.ErrWriter)\n\t}\n\n\tvar (\n\t\toutput = util.CmdOutput{}\n\n\t\t// Use the original output writers (before they were wrapped by logTFOutput)\n\t\t// and create new writers with the engine logger\n\t\tengineStdout = logwriter.New(\n\t\t\tlogwriter.WithLogger(l.WithOptions(log.WithOutput(stdoutWriter))),\n\t\t\tlogwriter.WithDefaultLevel(stdoutLogLevel),\n\t\t\tlogwriter.WithMsgSeparator(\"\\n\"),\n\t\t)\n\t\tengineStderr = logwriter.New(\n\t\t\tlogwriter.WithLogger(l.WithOptions(log.WithOutput(stderrWriter))),\n\t\t\tlogwriter.WithDefaultLevel(stderrLogLevel),\n\t\t\tlogwriter.WithMsgSeparator(\"\\n\"),\n\t\t)\n\n\t\tstdout = io.MultiWriter(engineStdout, &output.Stdout)\n\t\tstderr = io.MultiWriter(engineStderr, &output.Stderr)\n\t)\n\n\tvar (\n\t\tstdoutLineBuf, stderrLineBuf bytes.Buffer\n\t\tresultCode                   int\n\t)\n\n\tfor {\n\t\trunResp, recvErr := response.Recv()\n\t\tif recvErr != nil || runResp == nil {\n\t\t\tbreak\n\t\t}\n\n\t\tresponseType := runResp.GetResponse()\n\t\tif responseType == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch resp := responseType.(type) {\n\t\tcase *proto.RunResponse_Stdout:\n\t\t\tif resp.Stdout != nil {\n\t\t\t\tif err = processStream(resp.Stdout.GetContent(), &stdoutLineBuf, stdout); err != nil {\n\t\t\t\t\treturn nil, errors.New(err)\n\t\t\t\t}\n\t\t\t}\n\t\tcase *proto.RunResponse_Stderr:\n\t\t\tif resp.Stderr != nil {\n\t\t\t\tif err = processStream(resp.Stderr.GetContent(), &stderrLineBuf, stderr); err != nil {\n\t\t\t\t\treturn nil, errors.New(err)\n\t\t\t\t}\n\t\t\t}\n\t\tcase *proto.RunResponse_ExitResult:\n\t\t\tif resp.ExitResult != nil {\n\t\t\t\tresultCode = int(resp.ExitResult.GetCode())\n\t\t\t}\n\t\tcase *proto.RunResponse_Log:\n\t\t\tif resp.Log != nil {\n\t\t\t\tif logContent := resp.Log.GetContent(); logContent != \"\" {\n\t\t\t\t\tlogEngineMessage(l, resp.Log.GetLevel(), logContent)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif err = flushBuffer(&stdoutLineBuf, stdout); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tif err = flushBuffer(&stderrLineBuf, stderr); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tl.Debugf(\"Engine execution done in %v\", runOptions.WorkingDir)\n\n\tif resultCode != 0 {\n\t\terr = util.ProcessExecutionError{\n\t\t\tErr:             errors.Errorf(\"command failed with exit code %d\", resultCode),\n\t\t\tOutput:          output,\n\t\t\tWorkingDir:      runOptions.WorkingDir,\n\t\t\tRootWorkingDir:  runOptions.RootWorkingDir,\n\t\t\tLogShowAbsPaths: runOptions.Writers.LogShowAbsPaths,\n\t\t\tCommand:         runOptions.Command,\n\t\t\tArgs:            runOptions.Args,\n\t\t\tDisableSummary:  runOptions.Writers.LogDisableErrorSummary,\n\t\t}\n\n\t\treturn nil, errors.New(err)\n\t}\n\n\treturn &output, nil\n}\n\n// processStream handles the character buffering and line printing for a given stream\nfunc processStream(data string, lineBuf *bytes.Buffer, output io.Writer) error {\n\tfor _, ch := range data {\n\t\tlineBuf.WriteRune(ch)\n\n\t\tif ch == '\\n' {\n\t\t\tif _, err := fmt.Fprint(output, lineBuf.String()); err != nil {\n\t\t\t\treturn errors.New(err)\n\t\t\t}\n\n\t\t\tlineBuf.Reset()\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// flushBuffer prints any remaining data in the buffer\nfunc flushBuffer(lineBuf *bytes.Buffer, output io.Writer) error {\n\tif lineBuf.Len() > 0 {\n\t\tif _, err := fmt.Fprint(output, lineBuf.String()); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nvar ErrEngineInitFailed = errors.New(\"engine init failed\")\n\n// initialize engine for working directory\nfunc initialize(ctx context.Context, l log.Logger, runOptions *ExecutionOptions, client *proto.EngineClient) error {\n\tmeta, err := ConvertMetaToProtobuf(runOptions.EngineConfig.Meta)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tl.Debugf(\"Running init for engine in %s\", runOptions.WorkingDir)\n\n\trequest, err := (*client).Init(ctx, &proto.InitRequest{\n\t\tEnvVars:    runOptions.Env,\n\t\tWorkingDir: runOptions.WorkingDir,\n\t\tMeta:       meta,\n\t})\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tl.Debugf(\"Reading init output for engine in %s\", runOptions.WorkingDir)\n\n\treturn ReadEngineOutput(runOptions, true, func() (*OutputLine, error) {\n\t\toutput, err := request.Recv()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif output == nil {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\toutputLine := &OutputLine{}\n\n\t\t//nolint:dupl // Similar structure to shutdown response handling, but different protobuf types\n\t\tswitch resp := output.GetResponse().(type) {\n\t\tcase *proto.InitResponse_Stdout:\n\t\t\tif resp.Stdout != nil {\n\t\t\t\toutputLine.Stdout = resp.Stdout.GetContent()\n\t\t\t}\n\t\tcase *proto.InitResponse_Stderr:\n\t\t\tif resp.Stderr != nil {\n\t\t\t\toutputLine.Stderr = resp.Stderr.GetContent()\n\t\t\t}\n\t\tcase *proto.InitResponse_ExitResult:\n\t\t\tif resp.ExitResult != nil {\n\t\t\t\texitCode := int(resp.ExitResult.GetCode())\n\t\t\t\tif exitCode != 0 {\n\t\t\t\t\tl.Errorf(\"Engine init failed with exit code %d\", exitCode)\n\t\t\t\t\treturn nil, errors.Errorf(\"%w with exit code %d\", ErrEngineInitFailed, exitCode)\n\t\t\t\t}\n\t\t\t}\n\t\tcase *proto.InitResponse_Log:\n\t\t\tif resp.Log != nil {\n\t\t\t\tif logContent := resp.Log.GetContent(); logContent != \"\" {\n\t\t\t\t\tlogEngineMessage(l, resp.Log.GetLevel(), logContent)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn outputLine, nil\n\t})\n}\n\nvar ErrEngineShutdownFailed = errors.New(\"engine shutdown failed\")\n\n// shutdown engine for working directory\nfunc shutdown(ctx context.Context, l log.Logger, runOptions *ExecutionOptions, terragruntEngine *proto.EngineClient) error {\n\tmeta, err := ConvertMetaToProtobuf(runOptions.EngineConfig.Meta)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\trequest, err := (*terragruntEngine).Shutdown(ctx, &proto.ShutdownRequest{\n\t\tWorkingDir: runOptions.WorkingDir,\n\t\tMeta:       meta,\n\t\tEnvVars:    runOptions.Env,\n\t})\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tl.Debugf(\"Reading shutdown output for engine in %s\", runOptions.WorkingDir)\n\n\treturn ReadEngineOutput(runOptions, true, func() (*OutputLine, error) {\n\t\toutput, err := request.Recv()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif output == nil {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\toutputLine := &OutputLine{}\n\n\t\tresponseType := output.GetResponse()\n\t\tif responseType == nil {\n\t\t\treturn outputLine, nil\n\t\t}\n\n\t\t//nolint:dupl // Similar structure to init response handling, but different protobuf types\n\t\tswitch resp := responseType.(type) {\n\t\tcase *proto.ShutdownResponse_Stdout:\n\t\t\tif resp.Stdout != nil {\n\t\t\t\toutputLine.Stdout = resp.Stdout.GetContent()\n\t\t\t}\n\t\tcase *proto.ShutdownResponse_Stderr:\n\t\t\tif resp.Stderr != nil {\n\t\t\t\toutputLine.Stderr = resp.Stderr.GetContent()\n\t\t\t}\n\t\tcase *proto.ShutdownResponse_ExitResult:\n\t\t\tif resp.ExitResult != nil {\n\t\t\t\texitCode := int(resp.ExitResult.GetCode())\n\t\t\t\tif exitCode != 0 {\n\t\t\t\t\tl.Errorf(\"Engine shutdown failed with exit code %d\", exitCode)\n\t\t\t\t\treturn nil, errors.Errorf(\"%w with exit code %d\", ErrEngineShutdownFailed, exitCode)\n\t\t\t\t}\n\t\t\t}\n\t\tcase *proto.ShutdownResponse_Log:\n\t\t\tif resp.Log != nil {\n\t\t\t\tif logContent := resp.Log.GetContent(); logContent != \"\" {\n\t\t\t\t\tlogEngineMessage(l, resp.Log.GetLevel(), logContent)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn outputLine, nil\n\t})\n}\n\n// OutputLine represents the output from the engine\ntype OutputLine struct {\n\tStdout string\n\tStderr string\n}\n\ntype outputFn func() (*OutputLine, error)\n\n// ReadEngineOutput reads the output from the engine, since grpc plugins don't have common type,\n// use lambda function to read bytes from the stream\nfunc ReadEngineOutput(runOptions *ExecutionOptions, forceStdErr bool, output outputFn) error {\n\tcmdStdout := runOptions.Writers.Writer\n\tcmdStderr := runOptions.Writers.ErrWriter\n\n\tfor {\n\t\tresponse, err := output()\n\t\tif err != nil && (errors.Is(err, ErrEngineInitFailed) || errors.Is(err, ErrEngineShutdownFailed)) {\n\t\t\treturn err\n\t\t}\n\n\t\tif response == nil || err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tif response.Stdout != \"\" {\n\t\t\tif forceStdErr { // redirect stdout to stderr\n\t\t\t\tif _, err := cmdStderr.Write([]byte(response.Stdout)); err != nil {\n\t\t\t\t\treturn errors.New(err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif _, err := cmdStdout.Write([]byte(response.Stdout)); err != nil {\n\t\t\t\t\treturn errors.New(err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif response.Stderr != \"\" {\n\t\t\tif _, err := cmdStderr.Write([]byte(response.Stderr)); err != nil {\n\t\t\t\treturn errors.New(err)\n\t\t\t}\n\t\t}\n\t}\n\t// TODO: Why does this lint need to be ignored?\n\treturn nil //nolint:nilerr\n}\n\n// ConvertMetaToProtobuf converts metadata map to protobuf map\nfunc ConvertMetaToProtobuf(meta map[string]any) (map[string]*anypb.Any, error) {\n\tprotoMeta := make(map[string]*anypb.Any)\n\tif meta == nil {\n\t\treturn protoMeta, nil\n\t}\n\n\tfor key, value := range meta {\n\t\tjsonData, err := json.Marshal(value)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error marshaling value to JSON: %w\", err)\n\t\t}\n\n\t\tjsonStructValue, err := structpb.NewValue(string(jsonData))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tv, err := anypb.New(jsonStructValue)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tprotoMeta[key] = v\n\t}\n\n\treturn protoMeta, nil\n}\n\n// extract extracts a ZIP file into a specified destination directory.\nfunc extract(l log.Logger, zipFile, destDir string) error {\n\tr, err := zip.OpenReader(zipFile)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tdefer func() {\n\t\tif closeErr := r.Close(); closeErr != nil {\n\t\t\tl.Warnf(\"warning: failed to close zip reader: %v\", closeErr)\n\t\t}\n\t}()\n\n\tif err = os.MkdirAll(destDir, dirPerm); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t// Extract each file in the archive\n\tfor _, file := range r.File {\n\t\tfPath := filepath.Join(destDir, file.Name)\n\n\t\t// Check for ZipSlip vulnerability\n\t\tif !strings.HasPrefix(fPath, filepath.Clean(destDir)+string(os.PathSeparator)) {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\tif file.FileInfo().IsDir() {\n\t\t\t// Create directories\n\t\t\tif err := os.MkdirAll(fPath, file.Mode()); err != nil {\n\t\t\t\treturn errors.New(err)\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := os.MkdirAll(filepath.Dir(fPath), dirPerm); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\toutFile, err := os.OpenFile(fPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())\n\t\tif err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\tdefer func() {\n\t\t\tif closeErr := outFile.Close(); closeErr != nil {\n\t\t\t\tl.Warnf(\"warning: failed to close zip reader: %v\", closeErr)\n\t\t\t}\n\t\t}()\n\n\t\trc, err := file.Open()\n\t\tif err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\tdefer func() {\n\t\t\tif closeErr := rc.Close(); closeErr != nil {\n\t\t\t\tl.Warnf(\"warning: failed to close file reader: %v\", closeErr)\n\t\t\t}\n\t\t}()\n\n\t\t// Write file content\n\t\tif _, err := io.Copy(outFile, rc); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// detectFileType determines the type of file based on its magic bytes.\nfunc detectFileType(l log.Logger, filePath string) (string, error) {\n\tfile, err := os.Open(filePath)\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\tdefer func() {\n\t\tif closeErr := file.Close(); closeErr != nil {\n\t\t\tl.Warnf(\"warning: failed to close file : %v\", filePath)\n\t\t}\n\t}()\n\n\tconst headerSize = 4 // 4 bytes are enough for common formats\n\n\theader := make([]byte, headerSize)\n\n\tif _, err := file.Read(header); err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\tswitch {\n\tcase bytes.HasPrefix(header, []byte(\"PK\\x03\\x04\")):\n\t\treturn \"zip\", nil\n\tcase bytes.HasPrefix(header, []byte(\"\\x1F\\x8B\")):\n\t\treturn \"gzip\", nil\n\tcase bytes.HasPrefix(header, []byte(\"ustar\")):\n\t\treturn \"tar\", nil\n\tdefault:\n\t\treturn \"\", nil\n\t}\n}\n"
  },
  {
    "path": "internal/engine/engine_test.go",
    "content": "package engine_test\n\nimport (\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/engine\"\n\t\"github.com/gruntwork-io/terragrunt/internal/writer\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestConvertMetaToProtobuf(t *testing.T) {\n\tt.Parallel()\n\n\tmeta := map[string]any{\n\t\t\"key1\": \"value1\",\n\t\t\"key2\": 42,\n\t}\n\n\tprotoMeta, err := engine.ConvertMetaToProtobuf(meta)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, protoMeta)\n\tassert.Len(t, protoMeta, 2)\n}\n\nfunc TestReadEngineOutput(t *testing.T) {\n\tt.Parallel()\n\n\trunOptions := &engine.ExecutionOptions{\n\t\tWriters: writer.Writers{Writer: io.Discard, ErrWriter: io.Discard},\n\t}\n\n\toutputReturned := false\n\toutputFn := func() (*engine.OutputLine, error) {\n\t\tif outputReturned {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\toutputReturned = true\n\n\t\treturn &engine.OutputLine{\n\t\t\tStdout: \"stdout output\",\n\t\t\tStderr: \"stderr output\",\n\t\t}, nil\n\t}\n\n\terr := engine.ReadEngineOutput(runOptions, false, outputFn)\n\tassert.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/engine/public_keys.go",
    "content": "package engine\n\nconst PublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQGNBGaBbooBDADTCKKFW1uV5krG++w0u4QA7r2H6t39NfEKb8bbssM2oFIiTsEY\n6WQbddDAbzA9KFyIA47yga1nB3tOgih+4QwZF/Wctw63sfeKQ/kdT/p3lSwI1Rbq\nBuWJ0pSrZCsS8ldxNuel2Imnr3rZtB+jAWrfJio10T3paCy8HGE470ehXYpqlcUJ\nrOUxR4PTcLnWY0PrNfMgljXyFMLvqe1sG0LuPIH3ZbGZOzmdVyo/ngeJ9fluP8DC\nXZKEXqzGe58m6iJmDBUuRRV+LPVo8NrrVfF7waQrlGjaE4GZvvsmApxXv/iM9DIg\nNpZWE3vTH/pBSsc0HapVWD/DzYQpXhwcKWdF2wtpRrYOLFiXmdTHBga3xPDpXrID\nvPghYlW19j60A9o2MRzbjnPHvNwHKv5XPBIhLcoyWnNCp6WASTbiBRwDx3miM1ZT\neuQPagG68aGabkWdEH1Pa33ZEF5oDH7j9C9ALJlUhrk5zgFSRN5GKcm00K209g2M\ndlnvgWBjoUwYU0MAEQEAAbQdR3J1bnR3b3JrIDxpbmZvQGdydW50d29yay5pbz6J\nAc4EEwEKADgWIQQbc6gAIzjCuyjbMPSvWWjac5v8XAUCZoFuigIbAwULCQgHAgYV\nCgkICwIEFgIDAQIeAQIXgAAKCRCvWWjac5v8XAwWC/9IptEC3WhW7j8BdBjDVy5W\njaGb75PlL8pkQFBrfNPxiLGxuLi6xuON6zSIGtKZe4XTjwnVniyYyiyfSojrKCRT\nYCctVVvgoBaylybk8ppCysyID9xs0YqrhdCZvJyH+yLAXTdmkddzj906hRkW+xmq\n7XLA2emNxv6P4mHJr9pd4aa+aloZceRZ0OgUju+8E/ZTvW6A5YYExSFoNPBlG9nn\nWrT/D6aO9gqyMzN6w888p+jo+6s3JDQ6WEnf5s2Ha8g1k/Fg0Tk6YrbhcaYVQHEW\nWrSj9wVrXWa7RjRrZTREOMe9zLI3YHIsmBM0KNgHzvmyyhPsw1hR7MBJJfHKfpJ9\nSitdkyCFWlI/UZITEAcADZkRpvaixUvzIXAsk30aWGonCaXsqrdJwmpdLRJkC8xd\nW6D6rxDhqdVyxDRi7jas6mtOk7Ao7wFMDuedX1TB8yqkhvx96FaoG22Qoy4cma7C\nZz2jO2+ix/xztd/wq56jl0DjgKqpk06lECy/9+niyim5AY0EZoFuigEMAL7fKX6T\ne1K3K1e/WcaqGNFUGYWxlZZoGhihUAotWYeseleQB5RUmj9lwazI9zH3pteke+lV\nVwPqRD9djsQOv/B28Q6YpOd7sDbqxM6GSXED61sBAsJyDvmm0p5X7bbKJeRxhrhV\nFJjFf9F3t5gZb5Kff0vNYzCPmemT7UFaNUwDbE9wjRl5oKZfyDeUBBXB9H8aFE0J\nwLyFTnPKSpedJx7IlTbnCCzhTn0H7TKAVNYwRpSYN2GOChMiowkJrqD22G9HVZth\ng/sBJlmAFvLy8Ed8ktbZ426Xm44WRS6MFglZJJKZSEXOdSla8F4GT0Zxd6hsc6A8\nbLHQw34mFVmGZ6Q81+z2L3MV+zA3Dug3kEgRpH6g++KCX3+gpgsEugxli176rO1M\nCtZMnyR1fWBI9W8CuNm4MysImBHdOO73IUIsT2wiv4RTTGaLhU0YIfIEyojAFgEm\nS9BKCgF4BTP9FTOxxMZmINFTzDqi/b51qPBxBs6DXa/E7muOePzclQIBowARAQAB\niQG2BBgBCgAgFiEEG3OoACM4wrso2zD0r1lo2nOb/FwFAmaBbooCGwwACgkQr1lo\n2nOb/FzonAwAw1jzHGUMAIPuLZAQNhrhj05ZbuC2A7TvWiQba9W1HPHFUZJgrxKW\nKNPaWb8oCQR8JDJlWqiZG6hWTAJ66suPrLF0KNnbiZ5Us4+o7Nv5q1i4lxpJRgoY\nFuCDZbQHXPn3jzSEDQPSA62+ZyRGxXfpqgVPpT8IPzAdCRAhMuUZb62h+WX2ey91\nrnRFIXOlPTbrOMPLaMnGBjDnsWuCQmxBCXeevBh3u8q4Wa1xCZiqN8T7PSUusalu\nxita/w5ZA+Tzwxe8VqrgutCJdj5m5OxXX4v10xbbyPfhhnaahMduGL60MV/noxy6\n9TFlXIhgDj8dxV8wt8Tv/GZSSUALaBuPs9U7Q+fGiPFpC48Q75y0uS0QWXm0taxs\nRm6AMowi8dJEq1BIKvdEO2lDJ1uSw5Xcamj6Nu0JrM8tc1uCFNYOAEHw2bDHIcHK\n+uFqASiTa9QRj1SpSRkbPJB3yuPgUr1DgEoolSlMOnUiI46K3I9APD54nuShVbVq\niE6bHk4c9kBU\n=TmYc\n-----END PGP PUBLIC KEY BLOCK-----`\n"
  },
  {
    "path": "internal/engine/types.go",
    "content": "package engine\n\n// EngineOptions groups CLI-supplied engine options.\ntype EngineOptions struct {\n\t// CachePath is the path to the cache directory for engine files.\n\tCachePath string\n\t// LogLevel is the custom log level for engine.\n\tLogLevel string\n\t// SkipChecksumCheck skips checksum verification for engine packages.\n\tSkipChecksumCheck bool\n\t// NoEngine disables IaC engines even when the iac-engine experiment is enabled.\n\tNoEngine bool\n}\n\n// EngineConfig represents the configurations for a Terragrunt engine.\ntype EngineConfig struct {\n\tMeta    map[string]any\n\tSource  string\n\tVersion string\n\tType    string\n}\n"
  },
  {
    "path": "internal/engine/verification.go",
    "content": "package engine\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/ProtonMail/go-crypto/openpgp\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n)\n\n// verifyFile verifies the checksums file and the signature file of the passed file\nfunc verifyFile(checkedFile, checksumsFile, signatureFile string) error {\n\tchecksums, err := os.ReadFile(checksumsFile)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tchecksumsSignature, err := os.ReadFile(signatureFile)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t// validate first checksum file signature\n\tkeyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(PublicKey))\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t_, err = openpgp.CheckDetachedSignature(keyring, bytes.NewReader(checksums), bytes.NewReader(checksumsSignature), nil)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t// verify checksums\n\t// calculate checksum of package file\n\tpackageChecksum, err := util.FileSHA256(checkedFile)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t// match expected checksum\n\texpectedChecksum := util.MatchSha256Checksum(checksums, []byte(filepath.Base(checkedFile)))\n\tif expectedChecksum == nil {\n\t\treturn errors.Errorf(\"checksum list has no entry for %s\", checkedFile)\n\t}\n\n\tvar expectedSHA256Sum [sha256.Size]byte\n\tif _, err := hex.Decode(expectedSHA256Sum[:], expectedChecksum); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tif !bytes.Equal(expectedSHA256Sum[:], packageChecksum) {\n\t\treturn errors.Errorf(\"checksum list has unexpected SHA-256 hash %x (expected %x)\", packageChecksum, expectedSHA256Sum)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/errorconfig/types.go",
    "content": "// Package errorconfig defines types for structured error handling configuration.\npackage errorconfig\n\nimport (\n\t\"fmt\"\n\t\"maps\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// ErrorCleanPattern is used to clean error messages when looking for retry and ignore patterns.\nvar ErrorCleanPattern = regexp.MustCompile(`[^a-zA-Z0-9./'\"():=\\- ]+`)\n\n// Config is the extracted errors handling configuration.\ntype Config struct {\n\tRetry  map[string]*RetryConfig\n\tIgnore map[string]*IgnoreConfig\n}\n\n// RetryConfig represents the configuration for retrying specific errors.\ntype RetryConfig struct {\n\tName             string\n\tRetryableErrors  []*Pattern\n\tMaxAttempts      int\n\tSleepIntervalSec int\n}\n\n// IgnoreConfig represents the configuration for ignoring specific errors.\ntype IgnoreConfig struct {\n\tSignals         map[string]any\n\tName            string\n\tMessage         string\n\tIgnorableErrors []*Pattern\n}\n\n// Pattern represents a regex pattern for matching errors, with optional negation.\ntype Pattern struct {\n\tPattern  *regexp.Regexp `clone:\"shadowcopy\"`\n\tNegative bool\n}\n\n// Action represents the action to take when an error occurs.\ntype Action struct {\n\tIgnoreSignals   map[string]any\n\tIgnoreBlockName string\n\tRetryBlockName  string\n\tIgnoreMessage   string\n\tRetryAttempts   int\n\tRetrySleepSecs  int\n\tShouldIgnore    bool\n\tShouldRetry     bool\n}\n\n// MaxAttemptsReachedError is returned when the maximum number of retry attempts is reached.\ntype MaxAttemptsReachedError struct {\n\tErr        error\n\tMaxRetries int\n}\n\nfunc (e *MaxAttemptsReachedError) Error() string {\n\treturn fmt.Sprintf(\"max retry attempts (%d) reached for error: %v\", e.MaxRetries, e.Err)\n}\n\n// AttemptErrorRecovery attempts to recover from an error by checking the ignore and retry rules.\nfunc (c *Config) AttemptErrorRecovery(l log.Logger, err error, currentAttempt int) (*Action, error) {\n\tif err == nil {\n\t\treturn nil, nil\n\t}\n\n\terrStr := ExtractErrorMessage(err)\n\taction := &Action{}\n\n\tl.Debugf(\"Attempting error recovery for error: %s\", errStr)\n\n\t// First check ignore rules\n\tfor _, ignoreBlock := range c.Ignore {\n\t\tisIgnorable := MatchesAnyRegexpPattern(errStr, ignoreBlock.IgnorableErrors)\n\t\tif !isIgnorable {\n\t\t\tcontinue\n\t\t}\n\n\t\taction.IgnoreBlockName = ignoreBlock.Name\n\t\taction.ShouldIgnore = true\n\t\taction.IgnoreMessage = ignoreBlock.Message\n\t\taction.IgnoreSignals = make(map[string]any)\n\n\t\t// Convert cty.Value map to regular map\n\t\tmaps.Copy(action.IgnoreSignals, ignoreBlock.Signals)\n\n\t\treturn action, nil\n\t}\n\n\t// Then check retry rules\n\tfor _, retryBlock := range c.Retry {\n\t\tisRetryable := MatchesAnyRegexpPattern(errStr, retryBlock.RetryableErrors)\n\t\tif !isRetryable {\n\t\t\tcontinue\n\t\t}\n\n\t\tif currentAttempt >= retryBlock.MaxAttempts {\n\t\t\treturn nil, &MaxAttemptsReachedError{\n\t\t\t\tMaxRetries: retryBlock.MaxAttempts,\n\t\t\t\tErr:        err,\n\t\t\t}\n\t\t}\n\n\t\taction.RetryBlockName = retryBlock.Name\n\t\taction.ShouldRetry = true\n\t\taction.RetryAttempts = retryBlock.MaxAttempts\n\t\taction.RetrySleepSecs = retryBlock.SleepIntervalSec\n\n\t\treturn action, nil\n\t}\n\n\t// We encountered no error while attempting error recovery, even though the underlying error\n\t// is still present. Recovery did not error, the original error will be handled externally.\n\treturn nil, nil\n}\n\n// ExtractErrorMessage extracts and cleans the error message for pattern matching.\nfunc ExtractErrorMessage(err error) string {\n\tvar errText string\n\n\t// For ProcessExecutionError, match only against stderr and the underlying error,\n\t// not the full command string with flags.\n\tvar processErr util.ProcessExecutionError\n\tif errors.As(err, &processErr) {\n\t\terrText = processErr.Output.Stderr.String() + \"\\n\" + processErr.Err.Error()\n\t} else {\n\t\terrText = err.Error()\n\t}\n\n\tmultilineText := log.RemoveAllASCISeq(errText)\n\terrorText := ErrorCleanPattern.ReplaceAllString(multilineText, \" \")\n\n\treturn strings.Join(strings.Fields(errorText), \" \")\n}\n\n// MatchesAnyRegexpPattern checks if the input string matches any of the provided compiled patterns.\nfunc MatchesAnyRegexpPattern(input string, patterns []*Pattern) bool {\n\tfor _, pattern := range patterns {\n\t\tisNegative := pattern.Negative\n\t\tmatched := pattern.Pattern.MatchString(input)\n\n\t\tif matched {\n\t\t\treturn !isNegative\n\t\t}\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "internal/errorconfig/types_test.go",
    "content": "package errorconfig_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errorconfig\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestExtractErrorMessage_ExcludesCommandFlags(t *testing.T) {\n\tt.Parallel()\n\n\tvar stderr bytes.Buffer\n\tstderr.WriteString(\"flag provided but not defined: -abc\")\n\n\terr := util.ProcessExecutionError{\n\t\tErr:        errors.New(\"exit status 1\"),\n\t\tCommand:    \"tofu\",\n\t\tArgs:       []string{\"plan\", \"-lock-timeout=120m\", \"-input=false\"},\n\t\tWorkingDir: \"/some/path\",\n\t\tOutput:     util.CmdOutput{Stderr: stderr},\n\t}\n\n\tmsg := errorconfig.ExtractErrorMessage(err)\n\n\t// The extracted message should only contain stderr and the underlying error,\n\t// not the command string with flags.\n\tassert.NotContains(t, msg, \"-lock-timeout\")\n\tassert.NotContains(t, msg, \"tofu plan\")\n\t// Should contain the actual error text from stderr and the exit error\n\tassert.Contains(t, msg, \"flag provided but not defined\")\n\tassert.Contains(t, msg, \"exit status 1\")\n}\n\nfunc TestExtractErrorMessage_DoesNotFalselyMatchTimeout(t *testing.T) {\n\tt.Parallel()\n\n\t// Simulate the exact scenario from issue #5088:\n\t// Command has -lock-timeout=120m flag, but the actual error is unrelated to timeout.\n\tvar stderr bytes.Buffer\n\tstderr.WriteString(\"flag provided but not defined: -abc\")\n\n\terr := util.ProcessExecutionError{\n\t\tErr:        errors.New(\"exit status 1\"),\n\t\tCommand:    \"tofu\",\n\t\tArgs:       []string{\"plan\", \"-lock-timeout=120m\", \"-input=false\", \"-fes\"},\n\t\tWorkingDir: \"/some/path\",\n\t\tOutput:     util.CmdOutput{Stderr: stderr},\n\t}\n\n\ttimeoutPattern := regexp.MustCompile(`(?s).*timeout.*`)\n\tpatterns := []*errorconfig.Pattern{\n\t\t{Pattern: timeoutPattern},\n\t}\n\n\tmsg := errorconfig.ExtractErrorMessage(err)\n\n\t// The timeout pattern should NOT match because the extracted message only\n\t// contains stderr and exit error, not the command flags.\n\tmatched := errorconfig.MatchesAnyRegexpPattern(msg, patterns)\n\tassert.False(t, matched, \"timeout pattern should NOT match when 'timeout' only appears in command flags; cleaned message: %s\", msg)\n}\n\nfunc TestExtractErrorMessage_StillMatchesRealTimeout(t *testing.T) {\n\tt.Parallel()\n\n\t// When stderr actually contains \"timeout\", the pattern should match.\n\tvar stderr bytes.Buffer\n\tstderr.WriteString(\"Error: timeout waiting for resource to become available\")\n\n\terr := util.ProcessExecutionError{\n\t\tErr:        errors.New(\"exit status 1\"),\n\t\tCommand:    \"tofu\",\n\t\tArgs:       []string{\"apply\", \"-auto-approve\"},\n\t\tWorkingDir: \"/some/path\",\n\t\tOutput:     util.CmdOutput{Stderr: stderr},\n\t}\n\n\ttimeoutPattern := regexp.MustCompile(`(?s).*timeout.*`)\n\tpatterns := []*errorconfig.Pattern{\n\t\t{Pattern: timeoutPattern},\n\t}\n\n\tmsg := errorconfig.ExtractErrorMessage(err)\n\tmatched := errorconfig.MatchesAnyRegexpPattern(msg, patterns)\n\tassert.True(t, matched, \"timeout pattern should match when stderr actually contains 'timeout'; cleaned message: %s\", msg)\n}\n\nfunc TestExtractErrorMessage_StillMatchesTimeoutInStderrWithFlags(t *testing.T) {\n\tt.Parallel()\n\n\t// Even when the command has -lock-timeout flags, if stderr also contains \"timeout\",\n\t// the pattern should match.\n\tvar stderr bytes.Buffer\n\tstderr.WriteString(\"Error: timeout waiting for state lock\")\n\n\terr := util.ProcessExecutionError{\n\t\tErr:        errors.New(\"exit status 1\"),\n\t\tCommand:    \"tofu\",\n\t\tArgs:       []string{\"plan\", \"-lock-timeout=120m\", \"-input=false\"},\n\t\tWorkingDir: \"/some/path\",\n\t\tOutput:     util.CmdOutput{Stderr: stderr},\n\t}\n\n\ttimeoutPattern := regexp.MustCompile(`(?s).*timeout.*`)\n\tpatterns := []*errorconfig.Pattern{\n\t\t{Pattern: timeoutPattern},\n\t}\n\n\tmsg := errorconfig.ExtractErrorMessage(err)\n\tmatched := errorconfig.MatchesAnyRegexpPattern(msg, patterns)\n\tassert.True(t, matched, \"timeout pattern should match when stderr actually contains 'timeout'; cleaned message: %s\", msg)\n}\n\nfunc TestExtractErrorMessage_NonProcessError(t *testing.T) {\n\tt.Parallel()\n\n\t// For non-ProcessExecutionError errors, the full error string is used.\n\terr := errors.New(\"some generic error with timeout in it\")\n\n\tmsg := errorconfig.ExtractErrorMessage(err)\n\tassert.Contains(t, msg, \"timeout\")\n\n\ttimeoutPattern := regexp.MustCompile(`(?s).*timeout.*`)\n\tpatterns := []*errorconfig.Pattern{\n\t\t{Pattern: timeoutPattern},\n\t}\n\n\tmatched := errorconfig.MatchesAnyRegexpPattern(msg, patterns)\n\tassert.True(t, matched, \"timeout pattern should match for non-ProcessExecutionError; cleaned message: %s\", msg)\n}\n\nfunc TestErrorCleanPattern_PreservesCharacters(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"preserves hyphens in flags\",\n\t\t\tinput:    \"-lock-timeout=120m\",\n\t\t\texpected: \"-lock-timeout=120m\",\n\t\t},\n\t\t{\n\t\t\tname:     \"preserves equals signs\",\n\t\t\tinput:    \"-input=false\",\n\t\t\texpected: \"-input=false\",\n\t\t},\n\t\t{\n\t\t\tname:     \"strips control chars\",\n\t\t\tinput:    \"error\\x00here\",\n\t\t\texpected: \"error here\",\n\t\t},\n\t\t{\n\t\t\tname:     \"preserves alphanumeric and standard punctuation\",\n\t\t\tinput:    `Failed to execute \"tofu plan\" in /some/path`,\n\t\t\texpected: `Failed to execute \"tofu plan\" in /some/path`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresult := errorconfig.ErrorCleanPattern.ReplaceAllString(tt.input, \" \")\n\t\t\trequire.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestMatchesAnyRegexpPattern_NoMatch(t *testing.T) {\n\tt.Parallel()\n\n\ttimeoutPattern := regexp.MustCompile(`(?s).*timeout.*`)\n\tpatterns := []*errorconfig.Pattern{\n\t\t{Pattern: timeoutPattern},\n\t}\n\n\tmatched := errorconfig.MatchesAnyRegexpPattern(\"no match here\", patterns)\n\tassert.False(t, matched)\n}\n\nfunc TestMatchesAnyRegexpPattern_NegativePattern(t *testing.T) {\n\tt.Parallel()\n\n\ttimeoutPattern := regexp.MustCompile(`(?s).*timeout.*`)\n\tpatterns := []*errorconfig.Pattern{\n\t\t{Pattern: timeoutPattern, Negative: true},\n\t}\n\n\tmatched := errorconfig.MatchesAnyRegexpPattern(\"timeout occurred\", patterns)\n\tassert.False(t, matched, \"negative pattern should invert the match\")\n}\n"
  },
  {
    "path": "internal/errors/errors.go",
    "content": "// Package errors contains helper functions for wrapping errors with stack traces, stack output, and panic recovery.\npackage errors\n\nimport (\n\t\"fmt\"\n\n\tgoerrors \"github.com/go-errors/errors\"\n)\n\nconst (\n\tnewSkip    = 2\n\terrorfSkip = 2\n)\n\n// New creates a new instance of Error.\n// If the given value does not contain a stack trace, it will be created.\nfunc New(val any) error {\n\tif val == nil {\n\t\treturn nil\n\t}\n\n\treturn newWithSkip(newSkip, val)\n}\n\n// Errorf creates a new error with the given format and values.\n// It can be used as a drop-in replacement for fmt.Errorf() to provide descriptive errors in return values.\n// If none of the given values contains a stack trace, it will be created.\nfunc Errorf(format string, vals ...any) error {\n\treturn errorfWithSkip(errorfSkip, format, vals...)\n}\n\nfunc newWithSkip(skip int, val any) error {\n\tif err, ok := val.(error); ok && ContainsStackTrace(err) {\n\t\treturn fmt.Errorf(\"%w\", err)\n\t}\n\n\treturn goerrors.Wrap(val, skip)\n}\n\nfunc errorfWithSkip(skip int, format string, vals ...any) error {\n\terr := fmt.Errorf(format, vals...) //nolint:err113\n\n\tfor _, val := range vals {\n\t\tif val, ok := val.(error); ok && val != nil && ContainsStackTrace(val) {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn goerrors.Wrap(err, skip)\n}\n"
  },
  {
    "path": "internal/errors/export.go",
    "content": "package errors\n\nimport \"errors\"\n\n// As finds the first error in err's tree that matches target, and if one is found, sets\n// target to that error value and returns true. Otherwise, it returns false.\nfunc As(err error, target any) bool {\n\treturn errors.As(err, target)\n}\n\n// Is reports whether any error in err's tree matches target.\nfunc Is(err, target error) bool {\n\treturn errors.Is(err, target)\n}\n\n// Join returns an error that wraps the given errors.\nfunc Join(errs ...error) error {\n\treturn errors.Join(errs...)\n}\n\n// Unwrap returns the result of calling the Unwrap method on err, if err's\n// type contains an Unwrap method returning error.\n// Otherwise, Unwrap returns nil.\nfunc Unwrap(err error) error {\n\treturn errors.Unwrap(err)\n}\n"
  },
  {
    "path": "internal/errors/multierror.go",
    "content": "package errors\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/go-multierror\"\n)\n\n// MultiError is an error type to track multiple errors.\ntype MultiError struct {\n\tinner *multierror.Error\n}\n\n// WrappedErrors returns the error slice that this Error is wrapping.\nfunc (errs *MultiError) WrappedErrors() []error {\n\tif errs.inner == nil {\n\t\treturn nil\n\t}\n\n\treturn errs.inner.WrappedErrors()\n}\n\nfunc (errs *MultiError) Unwrap() []error {\n\treturn errs.WrappedErrors()\n}\n\n// ErrorOrNil returns an error interface if this Error represents\n// a list of errors, or returns nil if the list of errors is empty.\nfunc (errs *MultiError) ErrorOrNil() error {\n\tif errs == nil || errs.inner == nil {\n\t\treturn nil\n\t}\n\n\tif err := errs.inner.ErrorOrNil(); err != nil {\n\t\treturn errs\n\t}\n\n\treturn nil\n}\n\n// Append is a helper function that will append more errors\n// onto a Multierror in order to create a larger errs-error.\nfunc (errs *MultiError) Append(appendErrs ...error) *MultiError {\n\tif errs == nil {\n\t\terrs = &MultiError{inner: new(multierror.Error)}\n\t}\n\n\tif errs.inner == nil {\n\t\terrs.inner = new(multierror.Error)\n\t}\n\n\treturn &MultiError{inner: multierror.Append(errs.inner, appendErrs...)}\n}\n\n// Len implements sort.Interface function for length.\nfunc (errs *MultiError) Len() int {\n\tif errs == nil {\n\t\terrs = &MultiError{inner: new(multierror.Error)}\n\t}\n\n\tif errs.inner == nil {\n\t\terrs.inner = new(multierror.Error)\n\t}\n\n\treturn len(errs.inner.Errors)\n}\n\n// Swap implements sort.Interface function for swapping elements.\nfunc (errs *MultiError) Swap(i, j int) {\n\terrs.inner.Errors[i], errs.inner.Errors[j] = errs.inner.Errors[j], errs.inner.Errors[i]\n}\n\n// Less implements sort.Interface function for determining order.\nfunc (errs *MultiError) Less(i, j int) bool {\n\treturn errs.inner.Errors[i].Error() < errs.inner.Errors[j].Error()\n}\n\n// Error implements the error interface.\nfunc (errs *MultiError) Error() string {\n\tunwrappedErrs := UnwrapMultiErrors(errs)\n\n\tstrs := make([]string, len(unwrappedErrs))\n\n\tfor i := range unwrappedErrs {\n\t\tstrs[i] = addIndent(unwrappedErrs[i].Error())\n\t}\n\n\terrStr := strings.Join(strs, \"\\n\\n\")\n\n\tif len(strs) == 1 {\n\t\treturn fmt.Sprintf(\"error occurred:\\n\\n%s\\n\", errStr)\n\t}\n\n\treturn fmt.Sprintf(\"%d errors occurred:\\n\\n%s\\n\", len(strs), errStr)\n}\n\nfunc addIndent(str string) string {\n\t// for output on Windows OS\n\tstr = strings.ReplaceAll(str, \"\\r\\n\", \"\\n\")\n\trawLines := strings.Split(str, \"\\n\")\n\n\tvar lines []string //nolint:prealloc\n\n\tfor i, line := range rawLines {\n\t\tformat := \"  %s\"\n\t\tif i == 0 {\n\t\t\tformat = \"* %s\"\n\t\t}\n\n\t\tline = fmt.Sprintf(format, line)\n\t\tlines = append(lines, line)\n\t}\n\n\treturn strings.Join(lines, \"\\n\")\n}\n"
  },
  {
    "path": "internal/errors/util.go",
    "content": "package errors\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"slices\"\n\n\tgoerrors \"github.com/go-errors/errors\"\n)\n\n// ErrorStack returns an stack trace if available.\nfunc ErrorStack(err error) string {\n\tvar errStacks []string\n\n\tfor _, err := range UnwrapMultiErrors(err) {\n\t\tfor {\n\t\t\tif err, ok := err.(interface{ ErrorStack() string }); ok {\n\t\t\t\terrStacks = append(errStacks, err.ErrorStack())\n\t\t\t}\n\n\t\t\tif err = errors.Unwrap(err); err == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.Join(errStacks, \"\\n\")\n}\n\n// ContainsStackTrace returns true if the given error contain the stack trace.\n// Useful to avoid creating a nested stack trace.\nfunc ContainsStackTrace(err error) bool {\n\tfor _, err := range UnwrapMultiErrors(err) {\n\t\tfor {\n\t\t\tif err, ok := err.(interface{ ErrorStack() string }); ok && err != nil {\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\tif err = errors.Unwrap(err); err == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n\n// IsContextCanceled returns `true` if error has occurred by event `context.Canceled` which is not really an error.\nfunc IsContextCanceled(err error) bool {\n\treturn errors.Is(err, context.Canceled)\n}\n\n// IsError returns true if actual is the same type of error as expected. This method unwraps the given error objects (if they\n// are wrapped in objects with a stacktrace) and then does a simple equality check on them.\nfunc IsError(actual error, expected error) bool {\n\treturn goerrors.Is(actual, expected)\n}\n\n// Recover tries to recover from panics, and if it succeeds, calls the given onPanic function with an error that\n// explains the cause of the panic. This function should only be called from a defer statement.\nfunc Recover(onPanic func(cause error)) {\n\tif rec := recover(); rec != nil {\n\t\terr, isError := rec.(error)\n\t\tif !isError {\n\t\t\terr = fmt.Errorf(\"%v\", rec) //nolint:err113\n\t\t}\n\n\t\tonPanic(New(err))\n\t}\n}\n\n// UnwrapMultiErrors unwraps all nested multierrors into error slice.\nfunc UnwrapMultiErrors(err error) []error {\n\terrs := []error{err}\n\n\tfor index := 0; index < len(errs); index++ {\n\t\terr := errs[index]\n\n\t\tfor {\n\t\t\tif err, ok := err.(interface{ Unwrap() []error }); ok {\n\t\t\t\terrs = slices.Delete(errs, index, index+1)\n\t\t\t\tindex--\n\n\t\t\t\terrs = append(errs, err.Unwrap()...)\n\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif err = errors.Unwrap(err); err == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn errs\n}\n\n// UnwrapErrors unwraps all nested multierrors, and errors that were wrapped with `fmt.Errorf(\"%w\", err)`.\nfunc UnwrapErrors(err error) []error {\n\tvar errs []error\n\n\tfor _, err := range UnwrapMultiErrors(err) {\n\t\tfor {\n\t\t\terrs = append(errs, err)\n\t\t\tif err = errors.Unwrap(err); err == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn errs\n}\n"
  },
  {
    "path": "internal/experiment/errors.go",
    "content": "package experiment\n\nimport (\n\t\"strings\"\n)\n\n// InvalidExperimentNameError is an error that is returned when an invalid experiment name is requested.\ntype InvalidExperimentNameError struct {\n\tallowedNames []string\n}\n\nfunc NewInvalidExperimentNameError(allowedNames []string) *InvalidExperimentNameError {\n\treturn &InvalidExperimentNameError{\n\t\tallowedNames: allowedNames,\n\t}\n}\n\nfunc (err InvalidExperimentNameError) Error() string {\n\treturn \"allowed experiment(s): \" + strings.Join(err.allowedNames, \", \")\n}\n\nfunc (err InvalidExperimentNameError) Is(target error) bool {\n\t_, ok := target.(*InvalidExperimentNameError)\n\treturn ok\n}\n"
  },
  {
    "path": "internal/experiment/experiment.go",
    "content": "// Package experiment provides utilities used by Terragrunt to support an \"experiment\" mode.\n// By default, experiment mode is disabled, but when enabled, experimental features can be enabled.\n// These features are not yet stable and may change in the future.\n//\n// Note that any behavior outlined here should be documented in /docs/_docs/04_reference/experiments.md\n//\n// That is how users will know what to expect when they enable experiment mode, and how to customize it.\npackage experiment\n\nimport (\n\t\"slices\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst (\n\t// Symlinks is the experiment that allows symlinks to be used in Terragrunt configurations.\n\tSymlinks = \"symlinks\"\n\t// CLIRedesign is an experiment that allows users to use new commands related to the CLI redesign.\n\tCLIRedesign = \"cli-redesign\"\n\t// Stacks is the experiment that allows stacks to be used in Terragrunt.\n\tStacks = \"stacks\"\n\t// CAS is the experiment that enables using the CAS package for git operations\n\t// in the catalog command, which provides better performance through content-addressable storage.\n\tCAS = \"cas\"\n\t// Report is the experiment that enables the new run report.\n\tReport = \"report\"\n\t// RunnerPool is the experiment that allows using a pool of runners for parallel execution.\n\tRunnerPool = \"runner-pool\"\n\t// AutoProviderCacheDir is the experiment that automatically enables central\n\t// provider caching by setting TF_PLUGIN_CACHE_DIR.\n\t//\n\t// Only works with OpenTofu version >= 1.10.\n\tAutoProviderCacheDir = \"auto-provider-cache-dir\"\n\t// FilterFlag is the experiment that enables usage of the filter flag for filtering components\n\tFilterFlag = \"filter-flag\"\n\t// IacEngine is the experiment that enables usage of Terragrunt IaC engines for running IaC operations.\n\tIacEngine = \"iac-engine\"\n\t// DependencyFetchOutputFromState is the experiment that enables fetching dependency outputs\n\t// directly from state files instead of using terraform/tofu output commands.\n\tDependencyFetchOutputFromState = \"dependency-fetch-output-from-state\"\n)\n\nconst (\n\t// StatusOngoing is the status of an experiment that is ongoing.\n\tStatusOngoing byte = iota\n\t// StatusCompleted is the status of an experiment that is completed.\n\tStatusCompleted\n)\n\ntype Experiments []*Experiment\n\n// NewExperiments returns a new Experiments map with all experiments disabled.\n//\n// Bottom values for each experiment are the defaults, so only the names of experiments need to be set.\nfunc NewExperiments() Experiments {\n\treturn Experiments{\n\t\t{\n\t\t\tName: Symlinks,\n\t\t},\n\t\t{\n\t\t\tName:   CLIRedesign,\n\t\t\tStatus: StatusCompleted,\n\t\t},\n\t\t{\n\t\t\tName:   Stacks,\n\t\t\tStatus: StatusCompleted,\n\t\t},\n\t\t{\n\t\t\tName: CAS,\n\t\t},\n\t\t{\n\t\t\tName:   Report,\n\t\t\tStatus: StatusCompleted,\n\t\t},\n\t\t{\n\t\t\tName:   RunnerPool,\n\t\t\tStatus: StatusCompleted,\n\t\t},\n\t\t{\n\t\t\tName:   AutoProviderCacheDir,\n\t\t\tStatus: StatusCompleted,\n\t\t},\n\t\t{\n\t\t\tName:   FilterFlag,\n\t\t\tStatus: StatusCompleted,\n\t\t},\n\t\t{\n\t\t\tName: IacEngine,\n\t\t},\n\t\t{\n\t\t\tName: DependencyFetchOutputFromState,\n\t\t},\n\t}\n}\n\n// Names returns all experiment names.\nfunc (exps Experiments) Names() []string {\n\tnames := make([]string, 0, len(exps))\n\n\tfor _, exp := range exps {\n\t\tnames = append(names, exp.Name)\n\t}\n\n\tslices.Sort(names)\n\n\treturn names\n}\n\n// FilterByStatus returns experiments filtered by the given `status`.\nfunc (exps Experiments) FilterByStatus(status byte) Experiments {\n\tvar found Experiments\n\n\tfor _, experiment := range exps {\n\t\tif experiment.Status == status {\n\t\t\tfound = append(found, experiment)\n\t\t}\n\t}\n\n\treturn found\n}\n\n// Find searches and returns the experiment by the given `name`.\nfunc (exps Experiments) Find(name string) *Experiment {\n\tfor _, experiment := range exps {\n\t\tif experiment.Name == name {\n\t\t\treturn experiment\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// ExperimentMode enables the experiment mode.\nfunc (exps Experiments) ExperimentMode() {\n\tfor _, e := range exps {\n\t\tif e.Status == StatusOngoing {\n\t\t\te.Enabled = true\n\t\t}\n\t}\n}\n\n// EnableExperiment validates that the specified experiment name is valid and enables this experiment.\nfunc (exps Experiments) EnableExperiment(name string) error {\n\tfor _, e := range exps {\n\t\tif e.Name == name {\n\t\t\te.Enabled = true\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn NewInvalidExperimentNameError(exps.FilterByStatus(StatusOngoing).Names())\n}\n\n// NotifyCompletedExperiments logs the experiment names that are Enabled and have completed Status.\nfunc (exps Experiments) NotifyCompletedExperiments(logger log.Logger) {\n\tvar completed Experiments\n\n\tfor _, experiment := range exps.FilterByStatus(StatusCompleted) {\n\t\tif experiment.Enabled {\n\t\t\tcompleted = append(completed, experiment)\n\t\t}\n\t}\n\n\tif len(completed) == 0 {\n\t\treturn\n\t}\n\n\tlogger.Warnf(\"%s\", NewCompletedExperimentsWarning(completed.Names()).String())\n}\n\n// Evaluate returns true if the experiment is found and enabled otherwise returns false.\nfunc (exps Experiments) Evaluate(name string) bool {\n\tif experiment := exps.FilterByStatus(StatusOngoing).Find(name); experiment != nil {\n\t\treturn experiment.Evaluate()\n\t}\n\n\treturn false\n}\n\n// Experiment represents an experiment that can be enabled.\n// When the experiment is enabled, Terragrunt will behave in a way that uses some experimental functionality.\ntype Experiment struct {\n\t// Name is the name of the experiment.\n\tName string\n\t// Enabled determines if the experiment is enabled.\n\tEnabled bool\n\t// Status is the status of the experiment.\n\tStatus byte\n}\n\nfunc (exps Experiment) String() string {\n\treturn exps.Name\n}\n\n// Evaluate returns true the experiment is enabled.\n//\n// If the experiment is completed, consider it permanently enabled.\nfunc (exps Experiment) Evaluate() bool {\n\treturn exps.Enabled || exps.Status == StatusCompleted\n}\n"
  },
  {
    "path": "internal/experiment/experiment_test.go",
    "content": "package experiment_test\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestOngoingA   = \"test-ongoing-a\"\n\ttestOngoingB   = \"test-ongoing-b\"\n\ttestCompletedA = \"test-completed-a\"\n)\n\nfunc newTestLogger() (log.Logger, *bytes.Buffer) {\n\tformatter := format.NewFormatter(placeholders.Placeholders{placeholders.Message()})\n\toutput := new(bytes.Buffer)\n\tlogger := log.New(log.WithOutput(output), log.WithLevel(log.InfoLevel), log.WithFormatter(formatter))\n\n\treturn logger, output\n}\n\nfunc TestValidateExperiments(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedError   error\n\t\tname            string\n\t\texpectedWarning string\n\t\texperiments     experiment.Experiments\n\t\texperimentNames []string\n\t}{\n\t\t{\n\t\t\tname: \"no experiments\",\n\t\t\texperiments: experiment.Experiments{\n\t\t\t\t{\n\t\t\t\t\tName:   testOngoingA,\n\t\t\t\t\tStatus: experiment.StatusCompleted,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: experiment.CLIRedesign,\n\t\t\t\t},\n\t\t\t},\n\t\t\texperimentNames: []string{},\n\t\t\texpectedWarning: \"\",\n\t\t\texpectedError:   nil,\n\t\t},\n\t\t{\n\t\t\tname: \"valid experiment\",\n\t\t\texperiments: experiment.Experiments{\n\t\t\t\t{\n\t\t\t\t\tName: testOngoingA,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: testOngoingB,\n\t\t\t\t},\n\t\t\t},\n\t\t\texperimentNames: []string{testOngoingA},\n\t\t\texpectedWarning: \"\",\n\t\t\texpectedError:   nil,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid experiment\",\n\t\t\texperiments: experiment.Experiments{\n\t\t\t\t{\n\t\t\t\t\tName:   testCompletedA,\n\t\t\t\t\tStatus: experiment.StatusCompleted,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: testOngoingA,\n\t\t\t\t},\n\t\t\t},\n\t\t\texperimentNames: []string{\"invalid\"},\n\t\t\texpectedWarning: \"\",\n\t\t\texpectedError:   experiment.NewInvalidExperimentNameError([]string{testOngoingA}),\n\t\t},\n\t\t{\n\t\t\tname: \"completed experiment\",\n\t\t\texperiments: experiment.Experiments{\n\t\t\t\t{\n\t\t\t\t\tName:   testCompletedA,\n\t\t\t\t\tStatus: experiment.StatusCompleted,\n\t\t\t\t},\n\t\t\t},\n\t\t\texperimentNames: []string{testCompletedA},\n\t\t\texpectedWarning: \"The following experiment(s) are already completed: \" + testCompletedA + \". Please remove any completed experiments, as setting them no longer does anything. For a list of all ongoing experiments, and the outcomes of previous experiments, see https://docs.terragrunt.com/reference/experiments\",\n\t\t\texpectedError:   nil,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid and completed experiment\",\n\t\t\texperiments: experiment.Experiments{\n\t\t\t\t{\n\t\t\t\t\tName:   testCompletedA,\n\t\t\t\t\tStatus: experiment.StatusCompleted,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: testOngoingA,\n\t\t\t\t},\n\t\t\t},\n\t\t\texperimentNames: []string{\"invalid\", testCompletedA},\n\t\t\texpectedWarning: \"The following experiment(s) are already completed: \" + testCompletedA + \". Please remove any completed experiments, as setting them no longer does anything. For a list of all ongoing experiments, and the outcomes of previous experiments, see https://docs.terragrunt.com/reference/experiments\",\n\t\t\texpectedError:   experiment.NewInvalidExperimentNameError([]string{testOngoingA}),\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.Parallel()\n\n\t\t\tfor _, name := range tc.experimentNames {\n\t\t\t\tif err := tc.experiments.EnableExperiment(name); err != nil {\n\t\t\t\t\trequire.EqualError(t, err, tc.expectedError.Error())\n\t\t\t\t} else {\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlogger, output := newTestLogger()\n\n\t\t\ttc.experiments.NotifyCompletedExperiments(logger)\n\n\t\t\tif tc.expectedWarning == \"\" {\n\t\t\t\tassert.Empty(t, output.String())\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Contains(t, strings.TrimSpace(output.String()), tc.expectedWarning)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/experiment/warnings.go",
    "content": "package experiment\n\nimport (\n\t\"strings\"\n)\n\n// CompletedExperimentsWarning is a warning that is returned when completed experiments are requested.\ntype CompletedExperimentsWarning struct {\n\texperimentsNames []string\n}\n\nfunc NewCompletedExperimentsWarning(experimentsNames []string) *CompletedExperimentsWarning {\n\treturn &CompletedExperimentsWarning{\n\t\texperimentsNames: experimentsNames,\n\t}\n}\n\nfunc (w CompletedExperimentsWarning) String() string {\n\treturn \"The following experiment(s) are already completed: \" + strings.Join(w.experimentsNames, \", \") + \". Please remove any completed experiments, as setting them no longer does anything. For a list of all ongoing experiments, and the outcomes of previous experiments, see https://docs.terragrunt.com/reference/experiments\"\n}\n"
  },
  {
    "path": "internal/filter/ast.go",
    "content": "package filter\n\nimport (\n\t\"path/filepath\"\n\t\"strconv\"\n\n\t\"github.com/gobwas/glob\"\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n)\n\n// Expression is the interface that all AST nodes must implement.\ntype Expression interface {\n\t// expressionNode is a marker method to distinguish expression nodes.\n\texpressionNode()\n\t// String returns a string representation of the expression for debugging.\n\tString() string\n\t// RequiresDiscovery returns the first expression that requires discovery of Terragrunt components if any do.\n\t// Additionally, it returns a secondary value of true if any do.\n\tRequiresDiscovery() (Expression, bool)\n\t// RequiresParse returns the first expression that requires parsing Terragrunt HCL configurations if any do.\n\t// Additionally, it returns a secondary value of true if any do.\n\tRequiresParse() (Expression, bool)\n\t// IsRestrictedToStacks returns true if the expression is restricted to stacks.\n\tIsRestrictedToStacks() bool\n\t// Negated returns the equivalent expression with negation flipped.\n\tNegated() Expression\n}\n\n// Expressions is a slice of expressions.\ntype Expressions []Expression\n\n// PathExpression represents a path or glob filter (e.g., \"./path/**/*\" or \"/absolute/path\").\ntype PathExpression struct {\n\tcompiledGlob glob.Glob\n\tValue        string\n}\n\n// NewPathFilter creates a new PathFilter with eager glob compilation.\nfunc NewPathFilter(value string) (*PathExpression, error) {\n\tpattern := filepath.Clean(filepath.ToSlash(value))\n\n\tcompiled, err := glob.Compile(pattern, '/')\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &PathExpression{Value: value, compiledGlob: compiled}, nil\n}\n\n// Glob returns the pre-compiled glob pattern.\nfunc (p *PathExpression) Glob() glob.Glob {\n\treturn p.compiledGlob\n}\n\nfunc (p *PathExpression) expressionNode()                       {}\nfunc (p *PathExpression) String() string                        { return p.Value }\nfunc (p *PathExpression) RequiresDiscovery() (Expression, bool) { return p, false }\nfunc (p *PathExpression) RequiresParse() (Expression, bool)     { return p, false }\nfunc (p *PathExpression) IsRestrictedToStacks() bool            { return false }\nfunc (p *PathExpression) Negated() Expression                   { return NewPrefixExpression(\"!\", p) }\n\n// AttributeExpression represents a key-value attribute filter (e.g., \"name=my-app\").\ntype AttributeExpression struct {\n\tcompiledGlob glob.Glob\n\tKey          string\n\tValue        string\n}\n\n// NewAttributeExpression creates a new AttributeExpression with eager glob compilation\n// for attributes that support glob matching (name, reading, source).\nfunc NewAttributeExpression(key string, value string) (*AttributeExpression, error) {\n\texpr := &AttributeExpression{Key: key, Value: value}\n\n\tif expr.supportsGlob() {\n\t\tpattern := value\n\n\t\tif key == AttributeReading {\n\t\t\tpattern = filepath.Clean(filepath.ToSlash(pattern))\n\t\t}\n\n\t\tcompiled, err := glob.Compile(pattern, '/')\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\texpr.compiledGlob = compiled\n\t}\n\n\treturn expr, nil\n}\n\n// NewTypeExpression creates a new AttributeExpression for the \"type\" attribute.\n// Type filters do not support glob matching, so this constructor cannot fail.\nfunc NewTypeExpression(kind component.Kind) *AttributeExpression {\n\treturn &AttributeExpression{Key: AttributeType, Value: string(kind)}\n}\n\n// Glob returns the pre-compiled glob pattern.\nfunc (a *AttributeExpression) Glob() glob.Glob {\n\treturn a.compiledGlob\n}\n\n// supportsGlob returns true if the attribute filter supports glob patterns.\nfunc (a *AttributeExpression) supportsGlob() bool {\n\treturn a.Key == AttributeReading || a.Key == AttributeName || a.Key == AttributeSource\n}\n\nfunc (a *AttributeExpression) expressionNode()                       {}\nfunc (a *AttributeExpression) String() string                        { return a.Key + \"=\" + a.Value }\nfunc (a *AttributeExpression) RequiresDiscovery() (Expression, bool) { return a, true }\nfunc (a *AttributeExpression) RequiresParse() (Expression, bool) {\n\tswitch a.Key {\n\t// All of these attributes can be determined based on the component + configuration filepath.\n\tcase AttributeName, AttributeType, AttributeExternal:\n\t\treturn nil, false\n\t// We only know what a component reads if we parse it.\n\tcase AttributeReading:\n\t\treturn a, true\n\t// We default to true to be conservative in-case we forget to register\n\t// a new attribute here that does require parsing.\n\tdefault:\n\t\treturn nil, true\n\t}\n}\nfunc (a *AttributeExpression) IsRestrictedToStacks() bool {\n\treturn a.Key == \"type\" && a.Value == \"stack\"\n}\nfunc (a *AttributeExpression) Negated() Expression {\n\treturn NewPrefixExpression(\"!\", a)\n}\n\n// PrefixExpression represents a prefix operator expression (e.g., \"!name=foo\").\ntype PrefixExpression struct {\n\tRight    Expression\n\tOperator string\n}\n\n// NewPrefixExpression creates a new PrefixExpression.\nfunc NewPrefixExpression(operator string, right Expression) *PrefixExpression {\n\treturn &PrefixExpression{Operator: operator, Right: right}\n}\n\nfunc (p *PrefixExpression) expressionNode() {}\nfunc (p *PrefixExpression) String() string  { return p.Operator + p.Right.String() }\nfunc (p *PrefixExpression) RequiresDiscovery() (Expression, bool) {\n\treturn p.Right.RequiresDiscovery()\n}\nfunc (p *PrefixExpression) RequiresParse() (Expression, bool) {\n\treturn p.Right.RequiresParse()\n}\nfunc (p *PrefixExpression) IsRestrictedToStacks() bool {\n\tswitch p.Operator {\n\tcase \"!\":\n\t\tswitch a := p.Right.(type) {\n\t\tcase *AttributeExpression:\n\t\t\tswitch a.Key {\n\t\t\tcase \"type\":\n\t\t\t\treturn a.Value != \"stack\"\n\t\t\tdefault:\n\t\t\t\treturn false\n\t\t\t}\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\tdefault:\n\t\treturn false\n\t}\n}\nfunc (p *PrefixExpression) Negated() Expression {\n\tswitch p.Operator {\n\tcase \"!\":\n\t\treturn p.Right\n\tdefault:\n\t\treturn NewPrefixExpression(\"!\", p.Right)\n\t}\n}\n\n// InfixExpression represents an infix operator expression (e.g., \"./apps/* | name=bar\").\ntype InfixExpression struct {\n\tLeft     Expression\n\tRight    Expression\n\tOperator string\n}\n\n// NewInfixExpression creates a new InfixExpression.\nfunc NewInfixExpression(left Expression, operator string, right Expression) *InfixExpression {\n\treturn &InfixExpression{Left: left, Operator: operator, Right: right}\n}\n\nfunc (i *InfixExpression) expressionNode() {}\nfunc (i *InfixExpression) String() string {\n\treturn i.Left.String() + \" \" + i.Operator + \" \" + i.Right.String()\n}\nfunc (i *InfixExpression) RequiresDiscovery() (Expression, bool) {\n\tif _, ok := i.Left.RequiresDiscovery(); ok {\n\t\treturn i, true\n\t}\n\n\tif _, ok := i.Right.RequiresDiscovery(); ok {\n\t\treturn i, true\n\t}\n\n\treturn nil, false\n}\nfunc (i *InfixExpression) RequiresParse() (Expression, bool) {\n\tif _, ok := i.Left.RequiresParse(); ok {\n\t\treturn i, true\n\t}\n\n\tif _, ok := i.Right.RequiresParse(); ok {\n\t\treturn i, true\n\t}\n\n\treturn nil, false\n}\nfunc (i *InfixExpression) IsRestrictedToStacks() bool {\n\tswitch i.Operator {\n\tcase \"|\":\n\t\treturn i.Left.IsRestrictedToStacks() || i.Right.IsRestrictedToStacks()\n\tdefault:\n\t\treturn false\n\t}\n}\nfunc (i *InfixExpression) Negated() Expression {\n\tswitch i.Operator {\n\tcase \"|\":\n\t\treturn NewInfixExpression(i.Left.Negated(), i.Operator, i.Right)\n\tdefault:\n\t\treturn NewInfixExpression(i.Left.Negated(), i.Operator, i.Right)\n\t}\n}\n\n// GraphExpression represents a graph traversal expression (e.g., \"...foo\", \"foo...\", \"..1foo\", \"foo..2\").\n// Depth fields control how many levels of dependencies/dependents to traverse.\ntype GraphExpression struct {\n\tTarget              Expression\n\tIncludeDependents   bool\n\tIncludeDependencies bool\n\tExcludeTarget       bool\n\tDependentDepth      int\n\tDependencyDepth     int\n}\n\n// NewGraphExpression creates a new GraphExpression for the given target.\n// Use the builder methods WithDependents, WithDependencies, and WithExcludeTarget\n// to configure graph traversal behavior.\nfunc NewGraphExpression(target Expression) *GraphExpression {\n\treturn &GraphExpression{\n\t\tTarget: target,\n\t}\n}\n\n// WithDependents includes dependents (reverse dependencies) in the graph traversal.\nfunc (g *GraphExpression) WithDependents() *GraphExpression {\n\tg.IncludeDependents = true\n\treturn g\n}\n\n// WithDependencies includes dependencies in the graph traversal.\nfunc (g *GraphExpression) WithDependencies() *GraphExpression {\n\tg.IncludeDependencies = true\n\treturn g\n}\n\n// WithExcludeTarget excludes the target itself from the graph traversal results.\nfunc (g *GraphExpression) WithExcludeTarget() *GraphExpression {\n\tg.ExcludeTarget = true\n\treturn g\n}\n\nfunc (g *GraphExpression) expressionNode() {}\nfunc (g *GraphExpression) String() string {\n\tresult := \"\"\n\n\tif g.IncludeDependents {\n\t\tif g.DependentDepth > 0 {\n\t\t\tresult += strconv.Itoa(g.DependentDepth)\n\t\t}\n\n\t\tresult += \"...\"\n\t}\n\n\tif g.ExcludeTarget {\n\t\tresult += \"^\"\n\t}\n\n\tresult += g.Target.String()\n\n\tif g.IncludeDependencies {\n\t\tresult += \"...\"\n\n\t\tif g.DependencyDepth > 0 {\n\t\t\tresult += strconv.Itoa(g.DependencyDepth)\n\t\t}\n\t}\n\n\treturn result\n}\nfunc (g *GraphExpression) RequiresDiscovery() (Expression, bool) {\n\t// Graph expressions require dependency discovery to traverse the graph\n\treturn g, true\n}\nfunc (g *GraphExpression) RequiresParse() (Expression, bool) {\n\t// Graph expressions require parsing to traverse the graph.\n\treturn g, true\n}\nfunc (g *GraphExpression) IsRestrictedToStacks() bool { return false }\nfunc (g *GraphExpression) Negated() Expression {\n\treturn NewPrefixExpression(\"!\", g)\n}\n\n// GitExpression represents a Git-based filter expression (e.g., \"[main...HEAD]\" or \"[main]\").\n// It filters components based on changes between Git references.\ntype GitExpression struct {\n\tFromRef string\n\tToRef   string\n}\n\nfunc NewGitExpression(fromRef, toRef string) *GitExpression {\n\treturn &GitExpression{FromRef: fromRef, ToRef: toRef}\n}\n\nfunc (g *GitExpression) expressionNode() {}\nfunc (g *GitExpression) String() string {\n\treturn \"[\" + g.FromRef + \"...\" + g.ToRef + \"]\"\n}\nfunc (g *GitExpression) RequiresDiscovery() (Expression, bool) {\n\t// Git filters require discovery to check which components changed between references\n\treturn g, true\n}\nfunc (g *GitExpression) RequiresParse() (Expression, bool) {\n\t// Git filters don't require parsing - they compare file paths, not HCL content\n\treturn nil, false\n}\nfunc (g *GitExpression) IsRestrictedToStacks() bool { return false }\nfunc (g *GitExpression) Negated() Expression {\n\treturn NewPrefixExpression(\"!\", g)\n}\n\n// GitExpressions is a slice of Git expressions.\ntype GitExpressions []*GitExpression\n\n// UniqueGitRefs returns all unique Git references in a slice of expressions.\nfunc (e GitExpressions) UniqueGitRefs() []string {\n\trefSet := make(map[string]struct{}, len(e))\n\n\tfor _, expr := range e {\n\t\trefs := collectGitReferences(expr)\n\t\tfor _, ref := range refs {\n\t\t\trefSet[ref] = struct{}{}\n\t\t}\n\t}\n\n\tresult := make([]string, 0, len(refSet))\n\tfor ref := range refSet {\n\t\tresult = append(result, ref)\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "internal/filter/ast_test.go",
    "content": "package filter_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRestrictToStacks(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texprFn   func(t *testing.T) filter.Expression\n\t\tname     string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname: \"path filter\",\n\t\t\texprFn: func(t *testing.T) filter.Expression {\n\t\t\t\tt.Helper()\n\t\t\t\treturn mustPath(t, \"./apps/*\")\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"attribute filter restricted to stacks\",\n\t\t\texprFn: func(t *testing.T) filter.Expression {\n\t\t\t\tt.Helper()\n\t\t\t\treturn mustAttr(t, \"type\", \"stack\")\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"attribute filter not restricted to stacks\",\n\t\t\texprFn: func(t *testing.T) filter.Expression {\n\t\t\t\tt.Helper()\n\t\t\t\treturn mustAttr(t, \"name\", \"foo\")\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"prefix expression restricted to stacks\",\n\t\t\texprFn: func(t *testing.T) filter.Expression {\n\t\t\t\tt.Helper()\n\t\t\t\treturn filter.NewPrefixExpression(\"!\", mustAttr(t, \"type\", \"unit\"))\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"prefix expression not restricted to stacks\",\n\t\t\texprFn: func(t *testing.T) filter.Expression {\n\t\t\t\tt.Helper()\n\t\t\t\treturn filter.NewPrefixExpression(\"!\", mustAttr(t, \"name\", \"foo\"))\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"infix expression restricted to stacks\",\n\t\t\texprFn: func(t *testing.T) filter.Expression {\n\t\t\t\tt.Helper()\n\t\t\t\treturn filter.NewInfixExpression(mustAttr(t, \"type\", \"stack\"), \"|\", mustAttr(t, \"external\", \"true\"))\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"infix expression also restricted to stacks\",\n\t\t\texprFn: func(t *testing.T) filter.Expression {\n\t\t\t\tt.Helper()\n\t\t\t\treturn filter.NewInfixExpression(mustAttr(t, \"external\", \"true\"), \"|\", mustAttr(t, \"type\", \"stack\"))\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"infix expression not restricted to stacks\",\n\t\t\texprFn: func(t *testing.T) filter.Expression {\n\t\t\t\tt.Helper()\n\t\t\t\treturn filter.NewInfixExpression(mustAttr(t, \"name\", \"foo\"), \"|\", mustAttr(t, \"external\", \"true\"))\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"graph expression\",\n\t\t\texprFn: func(t *testing.T) filter.Expression {\n\t\t\t\tt.Helper()\n\t\t\t\treturn filter.NewGraphExpression(mustAttr(t, \"name\", \"foo\")).WithDependents()\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\texpr := tt.exprFn(t)\n\t\t\tassert.Equal(t, tt.expected, expr.IsRestrictedToStacks())\n\t\t})\n\t}\n}\n\nfunc mustPath(t *testing.T, value string) *filter.PathExpression {\n\tt.Helper()\n\n\texpr, err := filter.NewPathFilter(value)\n\trequire.NoError(t, err)\n\n\treturn expr\n}\n\nfunc mustAttr(t *testing.T, key, value string) *filter.AttributeExpression {\n\tt.Helper()\n\n\texpr, err := filter.NewAttributeExpression(key, value)\n\trequire.NoError(t, err)\n\n\treturn expr\n}\n"
  },
  {
    "path": "internal/filter/candidacy.go",
    "content": "package filter\n\n// GraphDirection represents the direction of graph traversal.\ntype GraphDirection int\n\nconst (\n\t// GraphDirectionNone indicates no graph traversal.\n\tGraphDirectionNone GraphDirection = iota\n\t// GraphDirectionDependencies indicates traversing dependencies (downstream).\n\tGraphDirectionDependencies\n\t// GraphDirectionDependents indicates traversing dependents (upstream).\n\tGraphDirectionDependents\n\t// GraphDirectionBoth indicates traversing both directions.\n\tGraphDirectionBoth\n)\n\n// String returns a string representation of the GraphDirection.\nfunc (d GraphDirection) String() string {\n\tswitch d {\n\tcase GraphDirectionNone:\n\t\treturn \"none\"\n\tcase GraphDirectionDependencies:\n\t\treturn \"dependencies\"\n\tcase GraphDirectionDependents:\n\t\treturn \"dependents\"\n\tcase GraphDirectionBoth:\n\t\treturn \"both\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// IsNegated returns true if the expression starts with a negation operator.\nfunc IsNegated(expr Expression) bool {\n\tswitch node := expr.(type) {\n\tcase *PrefixExpression:\n\t\treturn node.Operator == \"!\"\n\tcase *InfixExpression:\n\t\treturn IsNegated(node.Left)\n\tdefault:\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "internal/filter/candidacy_test.go",
    "content": "package filter_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestIsNegated(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texprFn   func(t *testing.T) filter.Expression\n\t\tname     string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname: \"path expression\",\n\t\t\texprFn: func(t *testing.T) filter.Expression {\n\t\t\t\tt.Helper()\n\t\t\t\treturn mustPath(t, \"./foo\")\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"negated path\",\n\t\t\texprFn: func(t *testing.T) filter.Expression {\n\t\t\t\tt.Helper()\n\t\t\t\treturn filter.NewPrefixExpression(\"!\", mustPath(t, \"./foo\"))\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"double negation\",\n\t\t\texprFn: func(t *testing.T) filter.Expression {\n\t\t\t\tt.Helper()\n\t\t\t\treturn filter.NewPrefixExpression(\"!\", filter.NewPrefixExpression(\"!\", mustPath(t, \"./foo\")))\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"infix with negated left\",\n\t\t\texprFn: func(t *testing.T) filter.Expression {\n\t\t\t\tt.Helper()\n\n\t\t\t\treturn filter.NewInfixExpression(\n\t\t\t\t\tfilter.NewPrefixExpression(\"!\", mustPath(t, \"./foo\")),\n\t\t\t\t\t\"|\",\n\t\t\t\t\tmustPath(t, \"./bar\"),\n\t\t\t\t)\n\t\t\t},\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"infix with non-negated left\",\n\t\t\texprFn: func(t *testing.T) filter.Expression {\n\t\t\t\tt.Helper()\n\n\t\t\t\treturn filter.NewInfixExpression(\n\t\t\t\t\tmustPath(t, \"./foo\"),\n\t\t\t\t\t\"|\",\n\t\t\t\t\tfilter.NewPrefixExpression(\"!\", mustPath(t, \"./bar\")),\n\t\t\t\t)\n\t\t\t},\n\t\t\texpected: false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresult := filter.IsNegated(tt.exprFn(t))\n\t\t\tassert.Equal(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestGraphDirection_String(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected  string\n\t\tdirection filter.GraphDirection\n\t}{\n\t\t{\"none\", filter.GraphDirectionNone},\n\t\t{\"dependencies\", filter.GraphDirectionDependencies},\n\t\t{\"dependents\", filter.GraphDirectionDependents},\n\t\t{\"both\", filter.GraphDirectionBoth},\n\t\t{\"unknown\", filter.GraphDirection(999)},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.expected, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, tt.expected, tt.direction.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/filter/classifier.go",
    "content": "package filter\n\nimport (\n\t\"slices\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n)\n\n// ClassificationStatus indicates whether a component is definitely included, a candidate, or excluded.\ntype ClassificationStatus int\n\nconst (\n\t// StatusDiscovered indicates the component is definitely included in results.\n\tStatusDiscovered ClassificationStatus = iota\n\t// StatusCandidate indicates the component might be included (needs further evaluation).\n\tStatusCandidate\n\t// StatusExcluded indicates the component is definitely excluded from results.\n\tStatusExcluded\n)\n\n// String returns a string representation of the ClassificationStatus.\nfunc (cs ClassificationStatus) String() string {\n\tswitch cs {\n\tcase StatusDiscovered:\n\t\treturn \"discovered\"\n\tcase StatusCandidate:\n\t\treturn \"candidate\"\n\tcase StatusExcluded:\n\t\treturn \"excluded\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// CandidacyReason explains why a component is classified as a candidate.\ntype CandidacyReason int\n\nconst (\n\t// CandidacyReasonNone indicates no candidacy reason (component is discovered or excluded).\n\tCandidacyReasonNone CandidacyReason = iota\n\t// CandidacyReasonGraphTarget indicates the component matches a graph expression target\n\t// and needs the graph phase to determine if it should be included.\n\tCandidacyReasonGraphTarget\n\t// CandidacyReasonRequiresParse indicates the component needs parsing to evaluate\n\t// attribute filters (e.g., reading=config/*).\n\tCandidacyReasonRequiresParse\n\t// CandidacyReasonPotentialDependent indicates the component is a potential dependent\n\t// when dependent filters (e.g., ...vpc) exist. These components need to be parsed\n\t// to build the dependency graph and determine if they are dependents of the target.\n\tCandidacyReasonPotentialDependent\n)\n\n// String returns a string representation of the CandidacyReason.\nfunc (cr CandidacyReason) String() string {\n\tswitch cr {\n\tcase CandidacyReasonNone:\n\t\treturn \"none\"\n\tcase CandidacyReasonGraphTarget:\n\t\treturn \"graph-target\"\n\tcase CandidacyReasonRequiresParse:\n\t\treturn \"requires-parse\"\n\tcase CandidacyReasonPotentialDependent:\n\t\treturn \"potential-dependent\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// ClassificationContext provides context for component classification.\ntype ClassificationContext struct {\n\t// ParseDataAvailable indicates whether parsed data is available for classification.\n\tParseDataAvailable bool\n}\n\n// GraphExpressionInfo contains information about a graph expression for the classifier.\ntype GraphExpressionInfo struct {\n\t// Target is the target expression within the graph expression.\n\tTarget Expression\n\t// FullExpression is the complete graph expression.\n\tFullExpression *GraphExpression\n\t// Index is the position of this expression in the original filter list.\n\tIndex int\n\t// IncludeDependencies indicates if dependencies should be traversed.\n\tIncludeDependencies bool\n\t// IncludeDependents indicates if dependents should be traversed.\n\tIncludeDependents bool\n\t// ExcludeTarget indicates if the target itself should be excluded from results (^ prefix).\n\tExcludeTarget bool\n\t// IsNegated indicates if this graph expression is within a negation (e.g., !...db).\n\tIsNegated bool\n\t// DependencyDepth is the maximum depth for dependency traversal.\n\tDependencyDepth int\n\t// DependentDepth is the maximum depth for dependent traversal.\n\tDependentDepth int\n}\n\n// Classifier analyzes filter expressions to efficiently classify components\n// as discovered, candidate, or excluded without full evaluation.\ntype Classifier struct {\n\tfilesystemExprs    []Expression\n\tparseExprs         []Expression\n\tgraphExprs         []*GraphExpressionInfo\n\tgitExprs           []*GitExpression\n\tnegatedExprs       []Expression\n\thasPositiveFilters bool\n}\n\n// NewClassifier creates a new Classifier that categorizes all filter expressions\n// for efficient component classification.\n// It separates filters into filesystem-evaluable, parse-required, and graph expressions.\nfunc NewClassifier(filters Filters) *Classifier {\n\tc := &Classifier{}\n\n\tfor i, f := range filters {\n\t\texpr := f.Expression()\n\t\tif expr == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tc.analyzeExpression(expr, i)\n\t}\n\n\treturn c\n}\n\n// Classify determines whether a component should be discovered, is a candidate,\n// or should be excluded based on the analyzed filters.\n//\n// Returns the classification status, the reason for candidacy (if applicable),\n// and the index of the matching graph expression (-1 if not a graph target match).\n//\n// Classification algorithm:\n//  1. Check if component ONLY matches negated filters -> EXCLUDED\n//  2. Check if parse expressions exist and parse data unavailable -> CANDIDATE (RequiresParse)\n//  3. Check if component matches any positive filesystem filter -> DISCOVERED\n//  4. Check if component matches any git expression -> DISCOVERED\n//  5. Check if component matches any graph expression target -> CANDIDATE (GraphTarget, returns index)\n//  6. Check if dependent filters exist and parse data unavailable -> CANDIDATE (PotentialDependent)\n//  7. If negated expressions exist and component doesn't match any -> DISCOVERED (negation acts as inclusion)\n//  8. If positive filters exist but no match -> EXCLUDED (exclude-by-default)\n//  9. If no positive filters exist -> DISCOVERED (include-by-default)\nfunc (c *Classifier) Classify(comp component.Component, classCtx ClassificationContext) (ClassificationStatus, CandidacyReason, int) {\n\thasNegativeMatch := c.matchesAnyNegated(comp)\n\thasPositiveMatch := c.matchesAnyPositive(comp, classCtx)\n\n\t// Before excluding due to negation, check if the component matches a negated graph expression target.\n\t// If so, we need to process it through the graph phase to discover dependencies/dependents\n\t// that should also be excluded. The final filter evaluation will handle the actual exclusion.\n\tif hasNegativeMatch && !hasPositiveMatch {\n\t\tif graphIdx := c.matchesNegatedGraphExpressionTarget(comp); graphIdx >= 0 {\n\t\t\treturn StatusCandidate, CandidacyReasonGraphTarget, graphIdx\n\t\t}\n\n\t\treturn StatusExcluded, CandidacyReasonNone, -1\n\t}\n\n\tmatchesFilesystem := c.matchesFilesystemExpression(comp)\n\tmatchesGit := c.matchesGitExpression(comp)\n\n\tif len(c.parseExprs) > 0 && !classCtx.ParseDataAvailable {\n\t\treturn StatusCandidate, CandidacyReasonRequiresParse, -1\n\t}\n\n\tif matchesFilesystem {\n\t\treturn StatusDiscovered, CandidacyReasonNone, -1\n\t}\n\n\tif matchesGit {\n\t\treturn StatusDiscovered, CandidacyReasonNone, -1\n\t}\n\n\tif graphIdx := c.matchesGraphExpressionTarget(comp); graphIdx >= 0 {\n\t\treturn StatusCandidate, CandidacyReasonGraphTarget, graphIdx\n\t}\n\n\tif c.HasDependentFilters() && !classCtx.ParseDataAvailable {\n\t\treturn StatusCandidate, CandidacyReasonPotentialDependent, -1\n\t}\n\n\tif len(c.negatedExprs) > 0 && !hasNegativeMatch {\n\t\treturn StatusDiscovered, CandidacyReasonNone, -1\n\t}\n\n\tif c.hasPositiveFilters {\n\t\treturn StatusExcluded, CandidacyReasonNone, -1\n\t}\n\n\treturn StatusDiscovered, CandidacyReasonNone, -1\n}\n\n// analyzeExpression recursively analyzes an expression and categorizes it.\nfunc (c *Classifier) analyzeExpression(expr Expression, filterIndex int) {\n\tswitch node := expr.(type) {\n\tcase *PathExpression:\n\t\tc.filesystemExprs = append(c.filesystemExprs, node)\n\t\tc.hasPositiveFilters = true\n\n\tcase *AttributeExpression:\n\t\tif _, requiresParse := node.RequiresParse(); requiresParse {\n\t\t\tc.parseExprs = append(c.parseExprs, node)\n\t\t} else {\n\t\t\tc.filesystemExprs = append(c.filesystemExprs, node)\n\t\t}\n\n\t\tc.hasPositiveFilters = true\n\n\tcase *GraphExpression:\n\t\tinfo := &GraphExpressionInfo{\n\t\t\tTarget:              node.Target,\n\t\t\tFullExpression:      node,\n\t\t\tIndex:               filterIndex,\n\t\t\tIncludeDependencies: node.IncludeDependencies,\n\t\t\tIncludeDependents:   node.IncludeDependents,\n\t\t\tExcludeTarget:       node.ExcludeTarget,\n\t\t\tDependencyDepth:     node.DependencyDepth,\n\t\t\tDependentDepth:      node.DependentDepth,\n\t\t}\n\t\tc.graphExprs = append(c.graphExprs, info)\n\t\tc.hasPositiveFilters = true\n\n\tcase *GitExpression:\n\t\tc.gitExprs = append(c.gitExprs, node)\n\t\tc.hasPositiveFilters = true\n\n\tcase *PrefixExpression:\n\t\t// Right now, the only prefix operator is \"!\".\n\t\t// If we encounter an unknown operator, just analyze the inner expression.\n\t\tif node.Operator != \"!\" {\n\t\t\tc.analyzeExpression(node.Right, filterIndex)\n\t\t\tbreak\n\t\t}\n\n\t\tc.negatedExprs = append(c.negatedExprs, node.Right)\n\t\tif _, requiresParse := node.Right.RequiresParse(); requiresParse {\n\t\t\tc.parseExprs = append(c.parseExprs, node.Right)\n\t\t}\n\n\t\tc.extractNegatedGraphExpressions(node.Right, filterIndex)\n\n\tcase *InfixExpression:\n\t\tc.analyzeExpression(node.Left, filterIndex)\n\t\tc.analyzeExpression(node.Right, filterIndex)\n\t}\n}\n\n// extractNegatedGraphExpressions walks through a negated expression and extracts\n// any graph expressions found within it. This ensures that filters like \"!...db\"\n// or \"!db...\" trigger the graph discovery phase.\nfunc (c *Classifier) extractNegatedGraphExpressions(expr Expression, filterIndex int) {\n\tWalkExpressions(expr, func(e Expression) bool {\n\t\tif graphExpr, ok := e.(*GraphExpression); ok {\n\t\t\tinfo := &GraphExpressionInfo{\n\t\t\t\tTarget:              graphExpr.Target,\n\t\t\t\tFullExpression:      graphExpr,\n\t\t\t\tIndex:               filterIndex,\n\t\t\t\tIncludeDependencies: graphExpr.IncludeDependencies,\n\t\t\t\tIncludeDependents:   graphExpr.IncludeDependents,\n\t\t\t\tExcludeTarget:       graphExpr.ExcludeTarget,\n\t\t\t\tDependencyDepth:     graphExpr.DependencyDepth,\n\t\t\t\tDependentDepth:      graphExpr.DependentDepth,\n\t\t\t\tIsNegated:           true,\n\t\t\t}\n\t\t\tc.graphExprs = append(c.graphExprs, info)\n\t\t}\n\n\t\treturn true\n\t})\n}\n\n// matchesAnyNegated checks if the component matches any negated expression.\nfunc (c *Classifier) matchesAnyNegated(comp component.Component) bool {\n\treturn slices.ContainsFunc(c.negatedExprs, func(expr Expression) bool {\n\t\treturn MatchComponent(comp, expr)\n\t})\n}\n\n// matchesAnyPositive checks if the component matches any positive (non-negated) expression.\nfunc (c *Classifier) matchesAnyPositive(comp component.Component, classCtx ClassificationContext) bool {\n\tif c.matchesFilesystemExpression(comp) {\n\t\treturn true\n\t}\n\n\tif c.matchesGraphExpressionTarget(comp) >= 0 {\n\t\treturn true\n\t}\n\n\tif c.matchesGitExpression(comp) {\n\t\treturn true\n\t}\n\n\tif !classCtx.ParseDataAvailable || len(c.parseExprs) == 0 {\n\t\treturn false\n\t}\n\n\treturn slices.ContainsFunc(c.parseExprs, func(expr Expression) bool {\n\t\treturn MatchComponent(comp, expr)\n\t})\n}\n\n// matchesGitExpression checks if a component matches any git expression.\n// Components discovered from worktrees have a Ref set in their discovery context.\nfunc (c *Classifier) matchesGitExpression(comp component.Component) bool {\n\tdiscoveryCtx := comp.DiscoveryContext()\n\tif discoveryCtx == nil || discoveryCtx.Ref == \"\" {\n\t\treturn false\n\t}\n\n\treturn slices.ContainsFunc(c.gitExprs, func(gitExpr *GitExpression) bool {\n\t\treturn discoveryCtx.Ref == gitExpr.FromRef || discoveryCtx.Ref == gitExpr.ToRef\n\t})\n}\n\n// matchesFilesystemExpression checks if the component matches any filesystem-evaluable expression.\nfunc (c *Classifier) matchesFilesystemExpression(comp component.Component) bool {\n\treturn slices.ContainsFunc(c.filesystemExprs, func(expr Expression) bool {\n\t\treturn MatchComponent(comp, expr)\n\t})\n}\n\n// matchesGraphExpressionTarget checks if the component matches any non-negated graph expression target.\n// Returns the index of the matching graph expression, or -1 if no match.\n// Negated graph expressions are handled separately by matchesNegatedGraphExpressionTarget.\nfunc (c *Classifier) matchesGraphExpressionTarget(comp component.Component) int {\n\tidx := slices.IndexFunc(c.graphExprs, func(info *GraphExpressionInfo) bool {\n\t\tif info.IsNegated {\n\t\t\treturn false\n\t\t}\n\n\t\treturn MatchComponent(comp, info.Target)\n\t})\n\n\tif idx >= 0 {\n\t\treturn c.graphExprs[idx].Index\n\t}\n\n\treturn -1\n}\n\n// matchesNegatedGraphExpressionTarget checks if the component matches any negated graph expression target.\n// Returns the index of the matching graph expression, or -1 if no match.\n// This is used to identify components that need graph traversal even when they would otherwise be excluded.\nfunc (c *Classifier) matchesNegatedGraphExpressionTarget(comp component.Component) int {\n\tidx := slices.IndexFunc(c.graphExprs, func(info *GraphExpressionInfo) bool {\n\t\tif !info.IsNegated {\n\t\t\treturn false\n\t\t}\n\n\t\treturn MatchComponent(comp, info.Target)\n\t})\n\n\tif idx >= 0 {\n\t\treturn c.graphExprs[idx].Index\n\t}\n\n\treturn -1\n}\n\n// GraphExpressions returns the analyzed graph expressions.\nfunc (c *Classifier) GraphExpressions() []*GraphExpressionInfo {\n\treturn c.graphExprs\n}\n\n// HasPositiveFilters returns whether any positive (non-negated) filters exist.\nfunc (c *Classifier) HasPositiveFilters() bool {\n\treturn c.hasPositiveFilters\n}\n\n// HasParseRequiredFilters returns whether any filters require HCL parsing.\nfunc (c *Classifier) HasParseRequiredFilters() bool {\n\treturn len(c.parseExprs) > 0\n}\n\n// HasGraphFilters returns whether any graph traversal filters exist.\nfunc (c *Classifier) HasGraphFilters() bool {\n\treturn len(c.graphExprs) > 0\n}\n\n// HasDependentFilters returns whether any graph expressions include dependent traversal.\n// This is used to determine if pre-graph dependency building is needed to populate\n// reverse links before dependent discovery can work.\nfunc (c *Classifier) HasDependentFilters() bool {\n\treturn slices.ContainsFunc(c.graphExprs, func(expr *GraphExpressionInfo) bool {\n\t\treturn expr.IncludeDependents\n\t})\n}\n\n// ParseExpressions returns the expressions that require parsing.\nfunc (c *Classifier) ParseExpressions() []Expression {\n\treturn c.parseExprs\n}\n\n// NegatedExpressions returns the negated expressions.\nfunc (c *Classifier) NegatedExpressions() []Expression {\n\treturn c.negatedExprs\n}\n"
  },
  {
    "path": "internal/filter/classifier_test.go",
    "content": "package filter_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestClassifier_NegatedGraphExpression_HasGraphFilters(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname              string\n\t\tfilterStr         string\n\t\texpectGraphFilter bool\n\t\texpectDependents  bool\n\t}{\n\t\t{\n\t\t\tname:              \"negated dependent filter triggers HasGraphFilters and HasDependentFilters\",\n\t\t\tfilterStr:         \"!...db\",\n\t\t\texpectGraphFilter: true,\n\t\t\texpectDependents:  true,\n\t\t},\n\t\t{\n\t\t\tname:              \"negated dependency filter triggers HasGraphFilters\",\n\t\t\tfilterStr:         \"!db...\",\n\t\t\texpectGraphFilter: true,\n\t\t\texpectDependents:  false,\n\t\t},\n\t\t{\n\t\t\tname:              \"non-negated dependent filter\",\n\t\t\tfilterStr:         \"...db\",\n\t\t\texpectGraphFilter: true,\n\t\t\texpectDependents:  true,\n\t\t},\n\t\t{\n\t\t\tname:              \"non-negated dependency filter\",\n\t\t\tfilterStr:         \"db...\",\n\t\t\texpectGraphFilter: true,\n\t\t\texpectDependents:  false,\n\t\t},\n\t\t{\n\t\t\tname:              \"simple path filter\",\n\t\t\tfilterStr:         \"./foo\",\n\t\t\texpectGraphFilter: false,\n\t\t\texpectDependents:  false,\n\t\t},\n\t\t{\n\t\t\tname:              \"negated path filter\",\n\t\t\tfilterStr:         \"!./foo\",\n\t\t\texpectGraphFilter: false,\n\t\t\texpectDependents:  false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tf, err := filter.Parse(tt.filterStr)\n\t\t\trequire.NoError(t, err, \"failed to parse filter\")\n\n\t\t\tclassifier := filter.NewClassifier(filter.Filters{f})\n\n\t\t\tassert.Equal(t, tt.expectGraphFilter, classifier.HasGraphFilters(),\n\t\t\t\t\"HasGraphFilters() mismatch for filter %q\", tt.filterStr)\n\t\t\tassert.Equal(t, tt.expectDependents, classifier.HasDependentFilters(),\n\t\t\t\t\"HasDependentFilters() mismatch for filter %q\", tt.filterStr)\n\t\t})\n\t}\n}\n\nfunc TestClassifier_NegatedGraphExpression_IsNegatedFlag(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname            string\n\t\tfilterStr       string\n\t\texpectIsNegated []bool\n\t}{\n\t\t{\n\t\t\tname:            \"single negated dependent filter\",\n\t\t\tfilterStr:       \"!...db\",\n\t\t\texpectIsNegated: []bool{true},\n\t\t},\n\t\t{\n\t\t\tname:            \"single negated dependency filter\",\n\t\t\tfilterStr:       \"!db...\",\n\t\t\texpectIsNegated: []bool{true},\n\t\t},\n\t\t{\n\t\t\tname:            \"non-negated dependent filter\",\n\t\t\tfilterStr:       \"...db\",\n\t\t\texpectIsNegated: []bool{false},\n\t\t},\n\t\t{\n\t\t\tname:            \"non-negated dependency filter\",\n\t\t\tfilterStr:       \"db...\",\n\t\t\texpectIsNegated: []bool{false},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tf, err := filter.Parse(tt.filterStr)\n\t\t\trequire.NoError(t, err, \"failed to parse filter\")\n\n\t\t\tclassifier := filter.NewClassifier(filter.Filters{f})\n\n\t\t\tgraphExprs := classifier.GraphExpressions()\n\t\t\trequire.Len(t, graphExprs, len(tt.expectIsNegated),\n\t\t\t\t\"unexpected number of graph expressions\")\n\n\t\t\tfor i, expected := range tt.expectIsNegated {\n\t\t\t\tassert.Equal(t, expected, graphExprs[i].IsNegated,\n\t\t\t\t\t\"IsNegated mismatch for graph expression %d\", i)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClassifier_MixedNegatedAndNonNegatedGraphFilters(t *testing.T) {\n\tt.Parallel()\n\n\tfooFilter, err := filter.Parse(\"...foo\")\n\trequire.NoError(t, err)\n\n\tbarFilter, err := filter.Parse(\"!...bar\")\n\trequire.NoError(t, err)\n\n\tclassifier := filter.NewClassifier(filter.Filters{fooFilter, barFilter})\n\n\tassert.True(t, classifier.HasGraphFilters(), \"should have graph filters\")\n\tassert.True(t, classifier.HasDependentFilters(), \"should have dependent filters\")\n\n\tgraphExprs := classifier.GraphExpressions()\n\trequire.Len(t, graphExprs, 2, \"should have 2 graph expressions\")\n\n\t// First one is positive (...foo)\n\tassert.False(t, graphExprs[0].IsNegated, \"first graph expression should not be negated\")\n\tassert.Equal(t, 0, graphExprs[0].Index, \"first graph expression should have index 0\")\n\tassert.True(t, graphExprs[0].IncludeDependents, \"first should include dependents\")\n\n\t// Second one is negated (!...bar)\n\tassert.True(t, graphExprs[1].IsNegated, \"second graph expression should be negated\")\n\tassert.Equal(t, 1, graphExprs[1].Index, \"second graph expression should have index 1\")\n\tassert.True(t, graphExprs[1].IncludeDependents, \"second should include dependents\")\n}\n\nfunc TestClassifier_NestedNegatedGraphExpression(t *testing.T) {\n\tt.Parallel()\n\n\ttarget, err := filter.NewPathFilter(\"./db\")\n\trequire.NoError(t, err)\n\n\tgraphExpr := filter.NewGraphExpression(target).WithDependencies()\n\tnegatedExpr := filter.NewPrefixExpression(\"!\", graphExpr)\n\n\tf := filter.NewFilter(negatedExpr, \"!./db...\")\n\n\tclassifier := filter.NewClassifier(filter.Filters{f})\n\n\tassert.True(t, classifier.HasGraphFilters(), \"should have graph filters\")\n\tassert.False(t, classifier.HasDependentFilters(), \"should not have dependent filters (db... is dependencies)\")\n\n\tgraphExprs := classifier.GraphExpressions()\n\trequire.Len(t, graphExprs, 1)\n\tassert.True(t, graphExprs[0].IsNegated)\n\tassert.False(t, graphExprs[0].IncludeDependents)\n\tassert.True(t, graphExprs[0].IncludeDependencies)\n}\n\nfunc TestClassifier_NegatedBidirectionalGraphExpression(t *testing.T) {\n\tt.Parallel()\n\n\ttarget, err := filter.NewPathFilter(\"./db\")\n\trequire.NoError(t, err)\n\n\tgraphExpr := filter.NewGraphExpression(target).WithDependents().WithDependencies()\n\tnegatedExpr := filter.NewPrefixExpression(\"!\", graphExpr)\n\n\tf := filter.NewFilter(negatedExpr, \"!...db...\")\n\n\tclassifier := filter.NewClassifier(filter.Filters{f})\n\n\tassert.True(t, classifier.HasGraphFilters(), \"should have graph filters\")\n\tassert.True(t, classifier.HasDependentFilters(), \"should have dependent filters\")\n\n\tgraphExprs := classifier.GraphExpressions()\n\trequire.Len(t, graphExprs, 1)\n\tassert.True(t, graphExprs[0].IsNegated)\n\tassert.True(t, graphExprs[0].IncludeDependencies)\n\tassert.True(t, graphExprs[0].IncludeDependents)\n}\n\nfunc TestClassifier_Classify(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname               string\n\t\tcomponentPath      string\n\t\tcomponentRef       string\n\t\tfilterStrs         []string\n\t\texpectedStatus     filter.ClassificationStatus\n\t\texpectedReason     filter.CandidacyReason\n\t\texpectedIdx        int\n\t\tparseDataAvailable bool\n\t\texpectIdxGteZero   bool\n\t}{\n\t\t{\n\t\t\tname:           \"no_filters_returns_discovered\",\n\t\t\tfilterStrs:     nil,\n\t\t\tcomponentPath:  \"./apps/app1\",\n\t\t\texpectedStatus: filter.StatusDiscovered,\n\t\t\texpectedReason: filter.CandidacyReasonNone,\n\t\t\texpectedIdx:    -1,\n\t\t},\n\t\t{\n\t\t\tname:           \"only_matches_negation_returns_excluded\",\n\t\t\tfilterStrs:     []string{\"!./apps/app1\"},\n\t\t\tcomponentPath:  \"./apps/app1\",\n\t\t\texpectedStatus: filter.StatusExcluded,\n\t\t\texpectedReason: filter.CandidacyReasonNone,\n\t\t\texpectedIdx:    -1,\n\t\t},\n\t\t{\n\t\t\tname:             \"matches_negated_graph_expression_target_returns_candidate\",\n\t\t\tfilterStrs:       []string{\"!...db\"},\n\t\t\tcomponentPath:    \"./libs/db\",\n\t\t\texpectedStatus:   filter.StatusCandidate,\n\t\t\texpectedReason:   filter.CandidacyReasonGraphTarget,\n\t\t\texpectIdxGteZero: true,\n\t\t},\n\t\t{\n\t\t\tname:               \"parse_expressions_without_parse_data_returns_candidate\",\n\t\t\tfilterStrs:         []string{\"reading=config/*.hcl\"},\n\t\t\tcomponentPath:      \"./apps/app1\",\n\t\t\tparseDataAvailable: false,\n\t\t\texpectedStatus:     filter.StatusCandidate,\n\t\t\texpectedReason:     filter.CandidacyReasonRequiresParse,\n\t\t\texpectedIdx:        -1,\n\t\t},\n\t\t{\n\t\t\tname:           \"matches_filesystem_expression_returns_discovered\",\n\t\t\tfilterStrs:     []string{\"./apps/*\"},\n\t\t\tcomponentPath:  \"./apps/app1\",\n\t\t\texpectedStatus: filter.StatusDiscovered,\n\t\t\texpectedReason: filter.CandidacyReasonNone,\n\t\t\texpectedIdx:    -1,\n\t\t},\n\t\t{\n\t\t\tname:           \"matches_git_expression_returns_discovered\",\n\t\t\tfilterStrs:     []string{\"[main...feature]\"},\n\t\t\tcomponentPath:  \"./apps/app1\",\n\t\t\tcomponentRef:   \"main\",\n\t\t\texpectedStatus: filter.StatusDiscovered,\n\t\t\texpectedReason: filter.CandidacyReasonNone,\n\t\t\texpectedIdx:    -1,\n\t\t},\n\t\t{\n\t\t\tname:           \"matches_graph_expression_target_returns_candidate\",\n\t\t\tfilterStrs:     []string{\"./libs/db...\"},\n\t\t\tcomponentPath:  \"./libs/db\",\n\t\t\texpectedStatus: filter.StatusCandidate,\n\t\t\texpectedReason: filter.CandidacyReasonGraphTarget,\n\t\t\texpectedIdx:    0,\n\t\t},\n\t\t{\n\t\t\tname:               \"dependent_filters_without_parse_data_returns_candidate\",\n\t\t\tfilterStrs:         []string{\"...vpc\"},\n\t\t\tcomponentPath:      \"./apps/app1\",\n\t\t\tparseDataAvailable: false,\n\t\t\texpectedStatus:     filter.StatusCandidate,\n\t\t\texpectedReason:     filter.CandidacyReasonPotentialDependent,\n\t\t\texpectedIdx:        -1,\n\t\t},\n\t\t{\n\t\t\tname:           \"negation_exists_component_not_matching_returns_discovered\",\n\t\t\tfilterStrs:     []string{\"!./libs/db\"},\n\t\t\tcomponentPath:  \"./apps/app1\",\n\t\t\texpectedStatus: filter.StatusDiscovered,\n\t\t\texpectedReason: filter.CandidacyReasonNone,\n\t\t\texpectedIdx:    -1,\n\t\t},\n\t\t{\n\t\t\tname:           \"positive_filters_no_match_returns_excluded\",\n\t\t\tfilterStrs:     []string{\"./libs/*\"},\n\t\t\tcomponentPath:  \"./apps/app1\",\n\t\t\texpectedStatus: filter.StatusExcluded,\n\t\t\texpectedReason: filter.CandidacyReasonNone,\n\t\t\texpectedIdx:    -1,\n\t\t},\n\t\t{\n\t\t\tname:           \"positive_match_with_negation_returns_discovered\",\n\t\t\tfilterStrs:     []string{\"!./apps/app1\", \"./apps/*\"},\n\t\t\tcomponentPath:  \"./apps/app1\",\n\t\t\texpectedStatus: filter.StatusDiscovered,\n\t\t\texpectedReason: filter.CandidacyReasonNone,\n\t\t\texpectedIdx:    -1,\n\t\t},\n\t\t{\n\t\t\tname:           \"multiple_graph_expressions_returns_correct_index\",\n\t\t\tfilterStrs:     []string{\"./libs/api...\", \"./libs/db...\"},\n\t\t\tcomponentPath:  \"./libs/db\",\n\t\t\texpectedStatus: filter.StatusCandidate,\n\t\t\texpectedReason: filter.CandidacyReasonGraphTarget,\n\t\t\texpectedIdx:    1,\n\t\t},\n\t\t{\n\t\t\tname:           \"graph_expression_index_with_preceding_non_graph_filter\",\n\t\t\tfilterStrs:     []string{\"./libs/api\", \"./libs/db...\"},\n\t\t\tcomponentPath:  \"./libs/db\",\n\t\t\texpectedStatus: filter.StatusCandidate,\n\t\t\texpectedReason: filter.CandidacyReasonGraphTarget,\n\t\t\texpectedIdx:    1,\n\t\t},\n\t\t{\n\t\t\tname:           \"graph_expression_index_with_multiple_preceding_non_graph_filters\",\n\t\t\tfilterStrs:     []string{\"./libs/api\", \"!./libs/cache\", \"./libs/db...\"},\n\t\t\tcomponentPath:  \"./libs/db\",\n\t\t\texpectedStatus: filter.StatusCandidate,\n\t\t\texpectedReason: filter.CandidacyReasonGraphTarget,\n\t\t\texpectedIdx:    2,\n\t\t},\n\t\t{\n\t\t\tname:               \"parse_expressions_with_parse_data_evaluates_normally\",\n\t\t\tfilterStrs:         []string{\"reading=config/*.hcl\"},\n\t\t\tcomponentPath:      \"./apps/app1\",\n\t\t\tparseDataAvailable: true,\n\t\t\texpectedStatus:     filter.StatusExcluded,\n\t\t\texpectedReason:     filter.CandidacyReasonNone,\n\t\t\texpectedIdx:        -1,\n\t\t},\n\t\t{\n\t\t\tname:               \"dependent_filters_with_parse_data_no_potential_dependent\",\n\t\t\tfilterStrs:         []string{\"...vpc\"},\n\t\t\tcomponentPath:      \"./apps/app1\",\n\t\t\tparseDataAvailable: true,\n\t\t\texpectedStatus:     filter.StatusExcluded,\n\t\t\texpectedReason:     filter.CandidacyReasonNone,\n\t\t\texpectedIdx:        -1,\n\t\t},\n\t\t{\n\t\t\tname:           \"git_expression_component_without_ref_no_match\",\n\t\t\tfilterStrs:     []string{\"[main...feature]\"},\n\t\t\tcomponentPath:  \"./apps/app1\",\n\t\t\texpectedStatus: filter.StatusExcluded,\n\t\t\texpectedReason: filter.CandidacyReasonNone,\n\t\t\texpectedIdx:    -1,\n\t\t},\n\t\t{\n\t\t\tname:           \"name_attribute_filter_matches\",\n\t\t\tfilterStrs:     []string{\"name=app1\"},\n\t\t\tcomponentPath:  \"./apps/app1\",\n\t\t\texpectedStatus: filter.StatusDiscovered,\n\t\t\texpectedReason: filter.CandidacyReasonNone,\n\t\t\texpectedIdx:    -1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tvar filters filter.Filters\n\n\t\t\tfor _, filterStr := range tt.filterStrs {\n\t\t\t\tf, err := filter.Parse(filterStr)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tfilters = append(filters, f)\n\t\t\t}\n\n\t\t\tclassifier := filter.NewClassifier(filters)\n\n\t\t\tvar comp component.Component\n\t\t\tif tt.componentRef != \"\" {\n\t\t\t\tcomp = newTestComponentWithRef(tt.componentPath, tt.componentRef)\n\t\t\t} else {\n\t\t\t\tcomp = newTestComponent(tt.componentPath)\n\t\t\t}\n\n\t\t\tstatus, reason, idx := classifier.Classify(comp, filter.ClassificationContext{\n\t\t\t\tParseDataAvailable: tt.parseDataAvailable,\n\t\t\t})\n\n\t\t\tassert.Equal(t, tt.expectedStatus, status)\n\t\t\tassert.Equal(t, tt.expectedReason, reason)\n\n\t\t\tif tt.expectIdxGteZero {\n\t\t\t\tassert.GreaterOrEqual(t, idx, 0)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tt.expectedIdx, idx)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClassifier_Classify_StatusString(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected string\n\t\tstatus   filter.ClassificationStatus\n\t}{\n\t\t{status: filter.StatusDiscovered, expected: \"discovered\"},\n\t\t{status: filter.StatusCandidate, expected: \"candidate\"},\n\t\t{status: filter.StatusExcluded, expected: \"excluded\"},\n\t\t{status: filter.ClassificationStatus(99), expected: \"unknown\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.expected, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, tt.expected, tt.status.String())\n\t\t})\n\t}\n}\n\nfunc TestClassifier_Classify_CandidacyReasonString(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected string\n\t\treason   filter.CandidacyReason\n\t}{\n\t\t{reason: filter.CandidacyReasonNone, expected: \"none\"},\n\t\t{reason: filter.CandidacyReasonGraphTarget, expected: \"graph-target\"},\n\t\t{reason: filter.CandidacyReasonRequiresParse, expected: \"requires-parse\"},\n\t\t{reason: filter.CandidacyReasonPotentialDependent, expected: \"potential-dependent\"},\n\t\t{reason: filter.CandidacyReason(99), expected: \"unknown\"},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.expected, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, tt.expected, tt.reason.String())\n\t\t})\n\t}\n}\n\nfunc newTestComponent(path string) component.Component {\n\treturn component.NewUnit(path).WithDiscoveryContext(&component.DiscoveryContext{\n\t\tWorkingDir: \".\",\n\t})\n}\n\nfunc newTestComponentWithRef(path, ref string) component.Component {\n\treturn component.NewUnit(path).WithDiscoveryContext(&component.DiscoveryContext{\n\t\tWorkingDir: \".\",\n\t\tRef:        ref,\n\t})\n}\n"
  },
  {
    "path": "internal/filter/complex_test.go",
    "content": "package filter_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParser_ComplexDepthExpressions(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname        string\n\t\tinput       string\n\t\texpected    string\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname:     \"depth with intersection\",\n\t\t\tinput:    \"1...foo | bar\",\n\t\t\texpected: \"1...name=foo | name=bar\",\n\t\t},\n\t\t{\n\t\t\tname:     \"both sides have depth\",\n\t\t\tinput:    \"foo...1 | bar...2\",\n\t\t\texpected: \"name=foo...1 | name=bar...2\",\n\t\t},\n\t\t{\n\t\t\tname:     \"full depth both sides of intersection\",\n\t\t\tinput:    \"1...foo...1 | 2...bar...2\",\n\t\t\texpected: \"1...name=foo...1 | 2...name=bar...2\",\n\t\t},\n\t\t{\n\t\t\tname:     \"negation with depth prefix\",\n\t\t\tinput:    \"!1...foo\",\n\t\t\texpected: \"!1...name=foo\",\n\t\t},\n\t\t{\n\t\t\tname:     \"negation with depth postfix\",\n\t\t\tinput:    \"!foo...1\",\n\t\t\texpected: \"!name=foo...1\",\n\t\t},\n\t\t{\n\t\t\tname:     \"intersection with negation and depth\",\n\t\t\tinput:    \"1...foo | !bar...2\",\n\t\t\texpected: \"1...name=foo | !name=bar...2\",\n\t\t},\n\t\t{\n\t\t\tname:     \"unlimited mixed with depth\",\n\t\t\tinput:    \"...foo | bar...1\",\n\t\t\texpected: \"...name=foo | name=bar...1\",\n\t\t},\n\t\t{\n\t\t\tname:     \"chained intersections with depth\",\n\t\t\tinput:    \"1...a | b...2 | 3...c\",\n\t\t\texpected: \"1...name=a | name=b...2 | 3...name=c\",\n\t\t},\n\t\t{\n\t\t\tname:     \"depth with path filter\",\n\t\t\tinput:    \"1..../apps/*\",\n\t\t\texpected: \"1..../apps/*\",\n\t\t},\n\t\t{\n\t\t\tname:     \"depth with braced path\",\n\t\t\tinput:    \"1...{my app}...2\",\n\t\t\texpected: \"1...my app...2\",\n\t\t},\n\t\t{\n\t\t\tname:     \"depth with attribute filter\",\n\t\t\tinput:    \"1...type=unit...2\",\n\t\t\texpected: \"1...type=unit...2\",\n\t\t},\n\t\t{\n\t\t\tname:     \"depth with caret and intersection\",\n\t\t\tinput:    \"1...^foo | bar...\",\n\t\t\texpected: \"1...^name=foo | name=bar...\",\n\t\t},\n\t\t{\n\t\t\tname:     \"parentheses treated as part of identifier\",\n\t\t\tinput:    \"1...(foo | bar)\",\n\t\t\texpected: \"1...name=(foo | name=bar)\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tlexer := filter.NewLexer(tt.input)\n\t\t\tparser := filter.NewParser(lexer)\n\t\t\texpr, err := parser.ParseExpression()\n\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, expr.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/filter/diagnostic.go",
    "content": "package filter\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// ANSI escape codes for colored output.\nconst (\n\tansiReset = \"\\033[0m\"\n\tansiBold  = \"\\033[1m\"\n\tansiRed   = \"\\033[31m\"\n\tansiBlue  = \"\\033[34m\"\n\tansiCyan  = \"\\033[36m\"\n)\n\n// FormatDiagnostic produces an error message from a ParseError.\n//\n// These diagnostics are formatted like so:\n//\n// ```\n// Filter parsing error: Missing Git reference\n//\n//\t--> --filter '[main...]'\n//\n//\t    [main...]\n//\t            ^ Expected second Git reference after '...'\n//\n//\t hint: Git filters with '...' require a reference on each side. e.g. '[main...HEAD]'\n//\n// ```\nfunc FormatDiagnostic(err *ParseError, filterIndex int, useColor bool) string {\n\tvar sb strings.Builder\n\n\tfmt.Fprintf(&sb, \"Filter parsing error: %s\\n\", err.Title)\n\n\tvar arrow string\n\tif useColor {\n\t\tarrow = fmt.Sprintf(\"%s%s --> %s\", ansiBold, ansiBlue, ansiReset)\n\t} else {\n\t\tarrow = \" --> \"\n\t}\n\n\tif filterIndex > 0 {\n\t\tfmt.Fprintf(&sb, \"%s--filter[%d] '%s'\\n\", arrow, filterIndex, err.Query)\n\t} else {\n\t\tfmt.Fprintf(&sb, \"%s--filter '%s'\\n\", arrow, err.Query)\n\t}\n\n\tsb.WriteString(\"\\n\")\n\n\tfmt.Fprintf(&sb, \"     %s\\n\", err.Query)\n\n\tindent := \"     \"\n\tspaces := strings.Repeat(\" \", err.ErrorPosition)\n\tcaret := \"^\"\n\tdetail := \" \" + err.Message\n\n\tif useColor {\n\t\tfmt.Fprintf(&sb, \"%s%s%s%s%s%s%s\\n\", indent, spaces, ansiBold, ansiRed, caret, ansiReset, detail)\n\t} else {\n\t\tfmt.Fprintf(&sb, \"%s%s%s%s\\n\", indent, spaces, caret, detail)\n\t}\n\n\thint := GetHint(err.ErrorCode, err.TokenLiteral, err.Query, err.Position)\n\tif hint != \"\" {\n\t\tsb.WriteString(\"\\n\")\n\n\t\tif useColor {\n\t\t\tfmt.Fprintf(&sb, \"  %s%shint:%s %s\\n\", ansiBold, ansiCyan, ansiReset, hint)\n\t\t} else {\n\t\t\tfmt.Fprintf(&sb, \"  hint: %s\\n\", hint)\n\t\t}\n\t}\n\n\treturn sb.String()\n}\n"
  },
  {
    "path": "internal/filter/diagnostic_test.go",
    "content": "package filter_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testLoggerForDiagnostics creates a logger for tests with colors disabled.\nfunc testLoggerForDiagnostics() log.Logger {\n\tformatter := format.NewFormatter(format.NewKeyValueFormatPlaceholders())\n\tformatter.SetDisabledColors(true)\n\n\treturn log.New(log.WithLevel(log.DebugLevel), log.WithFormatter(formatter))\n}\n\nfunc TestFormatDiagnostic_UnexpectedToken(t *testing.T) {\n\tt.Parallel()\n\n\terr := &filter.ParseError{\n\t\tTitle:         \"Unexpected token\",\n\t\tMessage:       \"unexpected '^' after expression\",\n\t\tPosition:      4,\n\t\tErrorPosition: 4,\n\t\tQuery:         \"HEAD^\",\n\t\tTokenLiteral:  \"^\",\n\t\tTokenLength:   1,\n\t\tErrorCode:     filter.ErrorCodeUnexpectedToken,\n\t}\n\n\tresult := filter.FormatDiagnostic(err, 0, false)\n\n\tassert.Contains(t, result, \"Filter parsing error: Unexpected token\")\n\n\tassert.Contains(t, result, \" --> --filter 'HEAD^'\")\n\n\tassert.Contains(t, result, \"     HEAD^\")\n\n\tassert.Contains(t, result, \"^ unexpected '^' after expression\")\n\n\tassert.Contains(t, result, \"hint:\")\n\tassert.Contains(t, result, \"Git\")\n}\n\nfunc TestFormatDiagnostic_WithFilterIndex(t *testing.T) {\n\tt.Parallel()\n\n\terr := &filter.ParseError{\n\t\tTitle:         \"Unexpected token\",\n\t\tMessage:       \"unexpected '|'\",\n\t\tPosition:      0,\n\t\tErrorPosition: 0,\n\t\tQuery:         \"| foo\",\n\t\tTokenLiteral:  \"|\",\n\t\tTokenLength:   1,\n\t\tErrorCode:     filter.ErrorCodeUnexpectedToken,\n\t}\n\n\tresult := filter.FormatDiagnostic(err, 2, false)\n\n\tassert.Contains(t, result, \" --> --filter[2] '| foo'\")\n}\n\nfunc TestFormatDiagnostic_MissingClosingBracket(t *testing.T) {\n\tt.Parallel()\n\n\terr := &filter.ParseError{\n\t\tTitle:         \"Unclosed Git filter expression\",\n\t\tMessage:       \"this Git-based expression is missing a closing ']'\",\n\t\tPosition:      12,\n\t\tErrorPosition: 0,\n\t\tQuery:         \"[main...HEAD\",\n\t\tTokenLiteral:  \"\",\n\t\tTokenLength:   1,\n\t\tErrorCode:     filter.ErrorCodeMissingClosingBracket,\n\t}\n\n\tresult := filter.FormatDiagnostic(err, 0, false)\n\n\tassert.Contains(t, result, \"Filter parsing error: Unclosed Git filter expression\")\n\n\tassert.Contains(t, result, \"     ^ this Git-based expression is missing a closing ']'\")\n\n\tassert.Contains(t, result, \"hint: Git-based expressions require surrounding references with '[]'\")\n}\n\nfunc TestFormatDiagnostic_EmptyGitFilter(t *testing.T) {\n\tt.Parallel()\n\n\terr := &filter.ParseError{\n\t\tTitle:         \"Empty Git filter\",\n\t\tMessage:       \"Git filter expression cannot be empty\",\n\t\tPosition:      1,\n\t\tErrorPosition: 1,\n\t\tQuery:         \"[]\",\n\t\tTokenLiteral:  \"]\",\n\t\tTokenLength:   1,\n\t\tErrorCode:     filter.ErrorCodeEmptyGitFilter,\n\t}\n\n\tresult := filter.FormatDiagnostic(err, 0, false)\n\n\tassert.Contains(t, result, \"Filter parsing error: Empty Git filter\")\n\n\tassert.NotContains(t, result, \"hint:\")\n}\n\nfunc TestFormatDiagnostic_WithColor(t *testing.T) {\n\tt.Parallel()\n\n\terr := &filter.ParseError{\n\t\tTitle:         \"Unexpected token\",\n\t\tMessage:       \"unexpected '^' after expression\",\n\t\tPosition:      4,\n\t\tErrorPosition: 4,\n\t\tQuery:         \"HEAD^\",\n\t\tTokenLiteral:  \"^\",\n\t\tTokenLength:   1,\n\t\tErrorCode:     filter.ErrorCodeUnexpectedToken,\n\t}\n\n\tresult := filter.FormatDiagnostic(err, 0, true)\n\n\tassert.Contains(t, result, \"\\033[\")\n}\n\nfunc TestFormatDiagnostic_NoColor(t *testing.T) {\n\tt.Parallel()\n\n\terr := &filter.ParseError{\n\t\tTitle:         \"Unexpected token\",\n\t\tMessage:       \"unexpected '^' after expression\",\n\t\tPosition:      4,\n\t\tErrorPosition: 4,\n\t\tQuery:         \"HEAD^\",\n\t\tTokenLiteral:  \"^\",\n\t\tTokenLength:   1,\n\t\tErrorCode:     filter.ErrorCodeUnexpectedToken,\n\t}\n\n\tresult := filter.FormatDiagnostic(err, 0, false)\n\n\tassert.NotContains(t, result, \"\\033[\")\n}\n\nfunc TestGetHint_CaretAfterIdentifier(t *testing.T) {\n\tt.Parallel()\n\n\thint := filter.GetHint(filter.ErrorCodeUnexpectedToken, \"^\", \"HEAD^\", 4)\n\n\trequire.NotEmpty(t, hint)\n\tassert.Contains(t, hint, \"Git\")\n\tassert.Contains(t, hint, \"[HEAD^]\")\n}\n\nfunc TestGetHint_CaretAtStart(t *testing.T) {\n\tt.Parallel()\n\n\thint := filter.GetHint(filter.ErrorCodeUnexpectedToken, \"^\", \"^foo\", 0)\n\n\trequire.NotEmpty(t, hint)\n\tassert.Contains(t, hint, \"excludes the target\")\n}\n\nfunc TestGetHint_MissingClosingBracket(t *testing.T) {\n\tt.Parallel()\n\n\thint := filter.GetHint(filter.ErrorCodeMissingClosingBracket, \"\", \"[main...HEAD\", 12)\n\n\trequire.NotEmpty(t, hint)\n\tassert.Contains(t, hint, \"[]\")\n}\n\nfunc TestGetHint_MissingClosingBrace(t *testing.T) {\n\tt.Parallel()\n\n\thint := filter.GetHint(filter.ErrorCodeMissingClosingBrace, \"\", \"{my path\", 8)\n\n\trequire.NotEmpty(t, hint)\n\tassert.Contains(t, hint, \"{}\")\n}\n\nfunc TestGetHint_EmptyGitFilter(t *testing.T) {\n\tt.Parallel()\n\n\thint := filter.GetHint(filter.ErrorCodeEmptyGitFilter, \"]\", \"[]\", 1)\n\n\tassert.Empty(t, hint)\n}\n\nfunc TestGetHint_PipeOperator(t *testing.T) {\n\tt.Parallel()\n\n\thint := filter.GetHint(filter.ErrorCodeUnexpectedToken, \"|\", \"| foo\", 0)\n\n\t// Pipe errors have specific messages that are self-explanatory, no hint needed\n\tassert.Empty(t, hint)\n}\n\nfunc TestParseFilterQueries_RichDiagnostics(t *testing.T) {\n\tt.Parallel()\n\n\t_, err := filter.ParseFilterQueries(testLoggerForDiagnostics(), []string{\"HEAD^\"})\n\n\trequire.Error(t, err)\n\n\terrMsg := err.Error()\n\n\t// Check error structure\n\tassert.Contains(t, errMsg, \"error:\")\n\tassert.Contains(t, errMsg, \" --> \")\n\tassert.Contains(t, errMsg, \"HEAD^\")\n\tassert.Contains(t, errMsg, \"^\")\n\tassert.Contains(t, errMsg, \"hint:\")\n}\n\nfunc TestParseFilterQueries_MultipleErrors(t *testing.T) {\n\tt.Parallel()\n\n\t_, err := filter.ParseFilterQueries(testLoggerForDiagnostics(), []string{\"HEAD^\", \"[unclosed\"})\n\n\trequire.Error(t, err)\n\n\terrMsg := err.Error()\n\n\t// Check both errors are present\n\t// First filter (index 0) shows as \"--filter 'HEAD^'\" without index\n\tassert.Contains(t, errMsg, \"--filter 'HEAD^'\")\n\t// Second filter (index 1) shows as \"--filter[1]\"\n\tassert.Contains(t, errMsg, \"--filter[1]\")\n\tassert.Contains(t, errMsg, \"unclosed\")\n}\n\nfunc TestParseFilterQueries_ValidFilters(t *testing.T) {\n\tt.Parallel()\n\n\tfilters, err := filter.ParseFilterQueries(testLoggerForDiagnostics(), []string{\"name=foo\", \"./apps/*\"})\n\n\trequire.NoError(t, err)\n\tassert.Len(t, filters, 2)\n}\n\nfunc TestParseFilterQueries_EmptyInput(t *testing.T) {\n\tt.Parallel()\n\n\tfilters, err := filter.ParseFilterQueries(testLoggerForDiagnostics(), []string{})\n\n\trequire.NoError(t, err)\n\tassert.Empty(t, filters)\n}\n"
  },
  {
    "path": "internal/filter/doc.go",
    "content": "// Package filter provides a parser and evaluator for filter query strings used to select Terragrunt components.\n//\n// # Overview\n//\n// The filter package implements a three-stage compiler architecture:\n//  1. Lexer: Tokenizes the input filter query string\n//  2. Parser: Builds an Abstract Syntax Tree (AST) from tokens\n//  3. Evaluator: Applies the filter logic to discovered Terragrunt components\n//\n// This design follows the classic compiler pattern and provides a clean separation of concerns\n// between syntax analysis and semantic evaluation.\n//\n// # Filter Syntax\n//\n// The filter package supports the following syntax elements:\n//\n// ## Path Filters\n//\n// Path filters match components by their file system path. They support glob patterns:\n//\n//\t./apps/frontend         # Exact path match\n//\t./apps/*                # Single-level wildcard\n//\t./apps/**/api           # Recursive wildcard\n//\t/absolute/path          # Absolute path\n//\n// ## Attribute Filters\n//\n// Attribute filters match components by their attributes:\n//\n//\tname=my-app             # Match by config name (directory basename)\n//\ttype=unit               # Match components of type \"unit\"\n//\ttype=stack              # Match components of type \"stack\"\n//\texternal=true           # Match external dependencies\n//\texternal=false          # Match internal dependencies (not external)\n//\tfoo                     # Shorthand for name=foo\n//\n// ## Negation Operator (!)\n//\n// The negation operator excludes matching components:\n//\n//\t!name=legacy            # Exclude components named \"legacy\"\n//\t!./apps/old             # Exclude components at path ./apps/old\n//\t!foo                    # Exclude components named \"foo\"\n//\t!external=true          # Exclude external dependencies\n//\n// ## Intersection Operator (|)\n//\n// The intersection operator refines/narrows results by applying filters from left to right.\n// Each filter in the chain further restricts the results from the previous filter.\n// The pipe character (|) is the only delimiter between filter expressions.\n// Whitespace is optional around operators but is NOT a delimiter itself.\n//\n//\t./apps/* | name=web         # Components in ./apps/* AND named \"web\"\n//\t./apps/*|name=web           # Same as above (spaces optional)\n//\t./foo* | !./foobar*         # Components in ./foo* AND NOT in ./foobar*\n//\ttype=unit | !external=true  # Internal components only\n//\n// Spaces within component names and paths are preserved:\n//\n//\tmy app                  # Component named \"my app\" (with space)\n//\t./my path/file          # Path with spaces\n//\n// ## Braced Path Syntax ({})\n//\n// Use braces to explicitly mark a path expression. This is useful when:\n// - The path doesn't start with ./ or /\n// - You want to be explicit that something is a path, not an identifier\n//\n//\t{./apps/*}              # Explicitly a path\n//\t{my path/file}          # Path without ./ prefix\n//\t{apps}                  # Treat \"apps\" as a path, not a name filter\n//\n// ## Graph Traversal Operators (...)\n//\n// Graph traversal operators include dependencies and/or dependents in the result:\n//\n//\tfoo...                  # foo and all its dependencies (transitive)\n//\t...foo                  # foo and all its dependents (transitive)\n//\t...foo...               # foo, all its dependencies, and all its dependents\n//\t^foo...                 # All dependencies of foo, excluding foo itself\n//\t...^foo                 # All dependents of foo, excluding foo itself\n//\n// Depth-limited traversal allows specifying how many levels to traverse.\n// Place the depth number on the outside of the ellipsis:\n//\n//\tfoo...1                 # foo and its direct dependencies only\n//\tfoo...2                 # foo and dependencies up to 2 levels deep\n//\t1...foo                 # foo and its direct dependents only\n//\t2...foo                 # foo and dependents up to 2 levels deep\n//\t1...foo...2             # Direct dependents and dependencies up to 2 levels\n//\n// When depth is not specified, traversal is unlimited (default behavior).\n//\n// ## Numeric Directory Disambiguation\n//\n// When a purely numeric token appears adjacent to \"...\", it is interpreted as a depth:\n//\n//\t1...1                   # Parsed as: dependent depth 1, target \"1\"\n//\t                        # (first 1 is depth, second 1 is target)\n//\n// To explicitly specify a numeric directory as the target, use escape hatches:\n//\n// Braced path syntax (for path filters):\n//\n//\t{1}...1                 # target path \"1\", dependency depth 1\n//\t1...{1}                 # dependent depth 1, target path \"1\"\n//\t1...{1}...1             # depth 1 dependents, path \"1\", depth 1 dependencies\n//\n// Explicit name attribute (for name filters):\n//\n//\tname=1...1              # target name=1, dependency depth 1\n//\t1...name=1              # dependent depth 1, target name=1\n//\n// Alphanumeric names are not ambiguous (only purely numeric tokens are depths):\n//\n//\t1...1foo                # dependent depth 1, target \"1foo\"\n//\tfoo1...1                # target \"foo1\", dependency depth 1\n//\n// # Operator Precedence\n//\n// Operators are evaluated with the following precedence (highest to lowest):\n//  1. Prefix operators (!)\n//  2. Infix operators (| - intersection/refinement, left-to-right)\n//\n// This means !foo | bar is evaluated as (!foo) | bar, not !(foo | bar).\n// The intersection operator applies filters left-to-right, each filter\n// refining/narrowing the results from the previous filter.\n//\n// # Usage Examples\n//\n// ## Basic Usage\n//\n//\t// Parse a filter query\n//\tfilter, err := filter.Parse(\"./apps/* | !legacy\", \".\")\n//\tif err != nil {\n//\t    log.Fatal(err)\n//\t}\n//\n//\t// Apply the filter to discovered components\n//\t// (typically obtained from discovery.Discover())\n//\tcomponents := []*component.Component{\n//\t    {Path: \"./apps/app1\", Kind: component.Unit},\n//\t    {Path: \"./apps/legacy\", Kind: component.Unit},\n//\t    {Path: \"./libs/db\", Kind: component.Unit},\n//\t}\n//\tresult, err := filter.Evaluate(components)\n//\tif err != nil {\n//\t    log.Fatal(err)\n//\t}\n//\n// ## Multiple Filters (Union)\n//\n// Multiple filter queries can be combined using the Filters type, which applies\n// union (OR) semantics. This is different from using | within a single filter,\n// which applies intersection (AND) semantics.\n//\n//\t// Parse multiple filter queries\n//\tfilters, err := filter.ParseFilterQueries([]string{\n//\t    \"./apps/*\",      // Select all apps\n//\t    \"name=db\",       // OR select db\n//\t}, \".\")\n//\tif err != nil {\n//\t    log.Fatal(err)\n//\t}\n//\n//\tresult, err := filters.Evaluate(components)\n//\t// Returns: all components in ./apps/* OR components named \"db\"\n//\n// Multiple filters are evaluated in two phases:\n//  1. Positive filters (non-negated) are evaluated and their results are unioned\n//  2. Negative filters (starting with !) are applied to remove matching components\n//\n// The ExcludeByDefault() method signals whether filters operate in exclude-by-default\n// mode. This is true if ANY filter doesn't start with a negation expression:\n//\n//\tfilters.ExcludeByDefault() // true if any filter is positive\n//\n// When true, discovery should start with an empty set and add matches.\n// When false (all filters are negated), discovery should start with all components\n// and remove matches.\n//\n// ## One-Shot Usage\n//\n//\t// Parse and evaluate in one step\n//\tresult, err := filter.Apply(\"./apps/* | name=web\", \".\", components)\n//\n// # Implementation Details\n//\n// ## Lexer\n//\n// The lexer (lexer.go) scans the input string and produces tokens:\n//   - IDENT: Identifiers (foo, name, etc.)\n//   - PATH: Paths (./apps/*, /absolute, etc.)\n//   - BANG: Negation operator (!)\n//   - PIPE: Intersection operator (|)\n//   - EQUAL: Assignment operator (=)\n//   - LBRACE: Left brace ({)\n//   - RBRACE: Right brace (})\n//   - EOF: End of input\n//\n// ## Parser\n//\n// The parser (parser.go) uses recursive descent parsing with Pratt parsing for operators.\n// It produces an AST with the following node types:\n//   - PathFilter: Path/glob filter\n//   - AttributeFilter: Key-value attribute filter\n//   - PrefixExpression: Negation operator\n//   - InfixExpression: Union operator\n//\n// ## Evaluator\n//\n// The evaluator (evaluator.go) walks the AST and applies the filter logic:\n//   - PathFilter: Uses glob matching (github.com/gobwas/glob) with eager compilation\n//     and caching via sync.Once for performance\n//   - AttributeFilter: Matches attributes by key-value pairs:\n//   - name: Matches filepath.Base(component.Path)\n//   - type: Matches component.Kind (unit or stack)\n//   - external: Matches component.External (true or false)\n//   - PrefixExpression: Returns the complement of the right side\n//   - InfixExpression: Returns the intersection by applying right filter to left results\n//\n// Path filters compile their glob pattern once on first evaluation and cache\n// the compiled result for reuse in subsequent evaluations, providing significant\n// performance improvements when filters are evaluated multiple times.\n//\n// # Related\n//\n// This package implements the filter syntax described in RFC #4060:\n// https://github.com/gruntwork-io/terragrunt/issues/4060\n//\n// The syntax is inspired by Turborepo's filter syntax:\n// https://turbo.build/repo/docs/reference/run#--filter-string\n//\n// # Future Enhancements\n//\n// Future versions will support:\n//   - Git-based filtering ([main...HEAD])\n//   - Dependency traversal (name=foo...)\n//   - Dependents traversal (...name=foo)\n//   - Read-based filtering (reads=path/to/file)\npackage filter\n"
  },
  {
    "path": "internal/filter/errors.go",
    "content": "package filter\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\n// ErrorCode categorizes parse errors for hint lookup.\ntype ErrorCode int\n\nconst (\n\tErrorCodeUnknown ErrorCode = iota\n\tErrorCodeUnexpectedToken\n\tErrorCodeUnexpectedEOF\n\tErrorCodeEmptyExpression\n\tErrorCodeMissingClosingBracket\n\tErrorCodeMissingClosingBrace\n\tErrorCodeIllegalToken\n\tErrorCodeMissingOperand\n\tErrorCodeEmptyGitFilter\n\tErrorCodeMissingGitRef\n\tErrorCodeInvalidGlob\n)\n\n// ParseError represents an error that occurred during parsing.\ntype ParseError struct {\n\t// Title is a high-level error description (e.g., \"Unclosed Git filter expression\")\n\tTitle string\n\t// Message is a detailed explanation shown at the problematic location (e.g., \"this Git-based expression is missing a closing ']'\")\n\tMessage string\n\t// Query is the original filter query\n\tQuery string\n\t// TokenLiteral is the problematic token\n\tTokenLiteral string\n\t// TokenLength is the length of the problematic token (used for underline width)\n\tTokenLength int\n\t// Position is the position of the problematic token\n\tPosition int\n\t// ErrorPosition is the position to show the caret (e.g. for unclosed brackets, it points to the opening bracket)\n\tErrorPosition int\n\t// ErrorCode is the error code, used for hint lookup\n\tErrorCode ErrorCode\n}\n\n// Error returns a string representation of the error.\n//\n// We suppress the gocritic \"hugeParam\" warning because this is a very large struct,\n// but we need it to implement the error interface, not its pointer.\n//\n//nolint:gocritic\nfunc (e ParseError) Error() string {\n\treturn fmt.Sprintf(\"Parse error at position %d: %s\", e.Position, e.Message)\n}\n\n// NewParseError creates a new ParseError with the given message and position.\nfunc NewParseError(message string, position int) error {\n\treturn errors.New(ParseError{Message: message, Position: position})\n}\n\n// NewParseErrorWithContext creates a new ParseError with full context for rich diagnostics.\nfunc NewParseErrorWithContext(title, message string, position, errorPosition int, query, tokenLiteral string, tokenLength int, code ErrorCode) error {\n\treturn errors.New(ParseError{\n\t\tTitle:         title,\n\t\tMessage:       message,\n\t\tPosition:      position,\n\t\tErrorPosition: errorPosition,\n\t\tQuery:         query,\n\t\tTokenLiteral:  tokenLiteral,\n\t\tTokenLength:   tokenLength,\n\t\tErrorCode:     code,\n\t})\n}\n\n// EvaluationError represents an error that occurred during evaluation.\ntype EvaluationError struct {\n\tCause   error\n\tMessage string\n}\n\nfunc (e EvaluationError) Error() string {\n\tif e.Cause != nil {\n\t\treturn fmt.Sprintf(\"Evaluation error: %s: %v\", e.Message, e.Cause)\n\t}\n\n\treturn \"evaluation error: \" + e.Message\n}\n\n// NewEvaluationError creates a new EvaluationError with the given message.\nfunc NewEvaluationError(message string) error {\n\treturn errors.New(EvaluationError{Message: message})\n}\n\n// NewEvaluationErrorWithCause creates a new EvaluationError with the given message and cause.\nfunc NewEvaluationErrorWithCause(message string, cause error) error {\n\treturn errors.New(EvaluationError{Message: message, Cause: cause})\n}\n\n// FilterQueryRequiresDiscoveryError is an error that is returned when a filter query requires discovery of Terragrunt configurations.\ntype FilterQueryRequiresDiscoveryError struct {\n\tQuery string\n}\n\nfunc (e FilterQueryRequiresDiscoveryError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"Filter query '%s' requires discovery of Terragrunt configurations, which is not supported when evaluating filters on generic files\",\n\t\te.Query,\n\t)\n}\n"
  },
  {
    "path": "internal/filter/evaluator.go",
    "content": "package filter\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"slices\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst (\n\tAttributeName     = \"name\"\n\tAttributeType     = \"type\"\n\tAttributeExternal = \"external\"\n\tAttributeReading  = \"reading\"\n\tAttributeSource   = \"source\"\n\n\tAttributeTypeValueUnit  = string(component.UnitKind)\n\tAttributeTypeValueStack = string(component.StackKind)\n\n\tAttributeExternalValueTrue  = \"true\"\n\tAttributeExternalValueFalse = \"false\"\n\n\t// MaxTraversalDepth is the maximum depth to traverse the graph for both dependencies and dependents.\n\tMaxTraversalDepth = 1000000\n)\n\n// graphTraversalParams consolidates parameters for filter graph traversal.\ntype graphTraversalParams struct {\n\tresultSet   map[string]component.Component\n\tvisited     map[string]int\n\tdirection   GraphDirection\n\twarnOnLimit bool\n}\n\n// EvaluationContext provides additional context for filter evaluation, such as Git worktree directories.\ntype EvaluationContext struct {\n\t// GitWorktrees maps Git references to temporary worktree directory paths.\n\t// This is used by GitFilter expressions to access different Git references.\n\tGitWorktrees map[string]string\n\t// WorkingDir is the base working directory for resolving relative paths.\n\tWorkingDir string\n}\n\n// Evaluate evaluates an expression against a list of components and returns the filtered components.\n// If logger is provided, it will be used for logging warnings during evaluation.\nfunc Evaluate(l log.Logger, expr Expression, components component.Components) (component.Components, error) {\n\tif expr == nil {\n\t\treturn nil, NewEvaluationError(\"expression is nil\")\n\t}\n\n\tswitch node := expr.(type) {\n\tcase *PathExpression:\n\t\treturn evaluatePathFilter(node, components)\n\tcase *AttributeExpression:\n\t\treturn evaluateAttributeFilter(node, components)\n\tcase *PrefixExpression:\n\t\treturn evaluatePrefixExpression(l, node, components)\n\tcase *InfixExpression:\n\t\treturn evaluateInfixExpression(l, node, components)\n\tcase *GraphExpression:\n\t\treturn evaluateGraphExpression(l, node, components)\n\tcase *GitExpression:\n\t\treturn evaluateGitFilter(node, components)\n\tdefault:\n\t\treturn nil, NewEvaluationError(\"unknown expression type\")\n\t}\n}\n\n// evaluatePathFilter evaluates a path filter using glob matching.\nfunc evaluatePathFilter(filter *PathExpression, components component.Components) (component.Components, error) {\n\tresult := make(component.Components, 0, len(components))\n\n\tfor _, c := range components {\n\t\tif matchPath(c, filter) {\n\t\t\tresult = append(result, c)\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\n// evaluateAttributeFilter evaluates an attribute filter.\nfunc evaluateAttributeFilter(filter *AttributeExpression, components []component.Component) ([]component.Component, error) {\n\tvar result []component.Component\n\n\tswitch filter.Key {\n\tcase AttributeName:\n\t\tg := filter.Glob()\n\n\t\tfor _, c := range components {\n\t\t\tif g.Match(filepath.Base(c.Path())) {\n\t\t\t\tresult = append(result, c)\n\t\t\t}\n\t\t}\n\n\tcase AttributeType:\n\t\tswitch filter.Value {\n\t\tcase AttributeTypeValueUnit:\n\t\t\tfor _, c := range components {\n\t\t\t\tif _, ok := c.(*component.Unit); ok {\n\t\t\t\t\tresult = append(result, c)\n\t\t\t\t}\n\t\t\t}\n\t\tcase AttributeTypeValueStack:\n\t\t\tfor _, c := range components {\n\t\t\t\tif _, ok := c.(*component.Stack); ok {\n\t\t\t\t\tresult = append(result, c)\n\t\t\t\t}\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, NewEvaluationError(\"invalid type value: \" + filter.Value + \" (expected 'unit' or 'stack')\")\n\t\t}\n\tcase AttributeExternal:\n\t\tswitch filter.Value {\n\t\tcase AttributeExternalValueTrue:\n\t\t\tfor _, c := range components {\n\t\t\t\tif c.External() {\n\t\t\t\t\tresult = append(result, c)\n\t\t\t\t}\n\t\t\t}\n\t\tcase AttributeExternalValueFalse:\n\t\t\tfor _, c := range components {\n\t\t\t\tif !c.External() {\n\t\t\t\t\tresult = append(result, c)\n\t\t\t\t}\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, NewEvaluationError(\"invalid external value: \" + filter.Value + \" (expected 'true' or 'false')\")\n\t\t}\n\tcase AttributeReading:\n\t\tg := filter.Glob()\n\n\t\tfor _, c := range components {\n\t\t\tif slices.ContainsFunc(c.Reading(), g.Match) {\n\t\t\t\tresult = append(result, c)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdiscoveryCtx := c.DiscoveryContext()\n\t\t\tif discoveryCtx == nil || discoveryCtx.WorkingDir == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\trelReading := make([]string, 0, len(c.Reading()))\n\t\t\tfor _, reading := range c.Reading() {\n\t\t\t\trel, err := filepath.Rel(c.DiscoveryContext().WorkingDir, reading)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, NewEvaluationErrorWithCause(fmt.Sprintf(\"failed to get relative path for component %s reading: %s\", c.Path(), reading), err)\n\t\t\t\t}\n\n\t\t\t\trelReading = append(relReading, filepath.ToSlash(rel))\n\t\t\t}\n\n\t\t\tif slices.ContainsFunc(relReading, g.Match) {\n\t\t\t\tresult = append(result, c)\n\t\t\t}\n\t\t}\n\tcase AttributeSource:\n\t\tg := filter.Glob()\n\n\t\tfor _, c := range components {\n\t\t\tif slices.ContainsFunc(c.Sources(), g.Match) {\n\t\t\t\tresult = append(result, c)\n\t\t\t}\n\t\t}\n\tdefault:\n\t\treturn nil, NewEvaluationError(\"unknown attribute key: \" + filter.Key)\n\t}\n\n\treturn result, nil\n}\n\n// evaluatePrefixExpression evaluates a prefix expression (negation).\nfunc evaluatePrefixExpression(l log.Logger, expr *PrefixExpression, components component.Components) (component.Components, error) {\n\tif expr.Operator != \"!\" {\n\t\treturn nil, NewEvaluationError(\"unknown prefix operator: \" + expr.Operator)\n\t}\n\n\ttoExclude, err := Evaluate(l, expr.Right, components)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(toExclude) == 0 {\n\t\treturn components, nil\n\t}\n\n\t// Build a set of paths to exclude for efficient lookup.\n\t// We compare by path rather than object identity because graph traversal\n\t// may return component instances from Dependencies()/Dependents() that are\n\t// different objects than those in the input list.\n\texcludePaths := make(map[string]struct{}, len(toExclude))\n\tfor _, c := range toExclude {\n\t\texcludePaths[c.Path()] = struct{}{}\n\t}\n\n\t// We don't use slices.DeleteFunc here because we don't want the members of the original components slice to be\n\t// zeroed.\n\tresults := make(component.Components, 0, len(components)-len(toExclude))\n\n\tfor _, c := range components {\n\t\tif _, excluded := excludePaths[c.Path()]; excluded {\n\t\t\tcontinue\n\t\t}\n\n\t\tresults = append(results, c)\n\t}\n\n\treturn results, nil\n}\n\n// evaluateInfixExpression evaluates an infix expression (intersection).\nfunc evaluateInfixExpression(l log.Logger, expr *InfixExpression, components component.Components) (component.Components, error) {\n\tif expr.Operator != \"|\" {\n\t\treturn nil, NewEvaluationError(\"unknown infix operator: \" + expr.Operator)\n\t}\n\n\tleftResult, err := Evaluate(l, expr.Left, components)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trightResult, err := Evaluate(l, expr.Right, leftResult)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn rightResult, nil\n}\n\n// evaluateGraphExpression evaluates a graph expression by traversing dependency/dependent graphs.\nfunc evaluateGraphExpression(l log.Logger, expr *GraphExpression, components component.Components) (component.Components, error) {\n\ttargetMatches, err := Evaluate(l, expr.Target, components)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// NOTE: We previously filtered out components with OriginGraphDiscovery here to avoid\n\t// including components that were only discovered via graph relationships. However, this\n\t// caused issues with intersection filters like \"service... | !^db...\" where db is\n\t// discovered via the first filter and then needs to be used as a target in the second.\n\t// The discovery phase already handles this logic properly, so we don't need to filter\n\t// by origin here during filter evaluation.\n\n\tif len(targetMatches) == 0 {\n\t\treturn component.Components{}, nil\n\t}\n\n\tresultSet := make(map[string]component.Component)\n\n\tif !expr.ExcludeTarget {\n\t\tfor _, c := range targetMatches {\n\t\t\tresultSet[c.Path()] = c\n\t\t}\n\t}\n\n\tif expr.IncludeDependencies {\n\t\tdepth := MaxTraversalDepth\n\t\twarnOnLimit := true\n\n\t\tif expr.DependencyDepth > 0 {\n\t\t\tdepth = expr.DependencyDepth\n\t\t\twarnOnLimit = false\n\t\t}\n\n\t\tparams := &graphTraversalParams{\n\t\t\tresultSet:   resultSet,\n\t\t\tvisited:     make(map[string]int),\n\t\t\tdirection:   GraphDirectionDependencies,\n\t\t\twarnOnLimit: warnOnLimit,\n\t\t}\n\n\t\tfor _, target := range targetMatches {\n\t\t\ttraverseGraph(l, target, params, depth)\n\t\t}\n\t}\n\n\tif expr.IncludeDependents {\n\t\tdepth := MaxTraversalDepth\n\t\twarnOnLimit := true\n\n\t\tif expr.DependentDepth > 0 {\n\t\t\tdepth = expr.DependentDepth\n\t\t\twarnOnLimit = false\n\t\t}\n\n\t\tparams := &graphTraversalParams{\n\t\t\tresultSet:   resultSet,\n\t\t\tvisited:     make(map[string]int),\n\t\t\tdirection:   GraphDirectionDependents,\n\t\t\twarnOnLimit: warnOnLimit,\n\t\t}\n\n\t\tfor _, target := range targetMatches {\n\t\t\ttraverseGraph(l, target, params, depth)\n\t\t}\n\t}\n\n\tresult := make(component.Components, 0, len(resultSet))\n\tfor _, c := range resultSet {\n\t\tresult = append(result, c)\n\t}\n\n\treturn result, nil\n}\n\n// evaluateGitFilter evaluates a Git filter expression by comparing components between Git references.\n// It returns components that were added, removed, or changed between FromRef and ToRef.\nfunc evaluateGitFilter(filter *GitExpression, components component.Components) (component.Components, error) {\n\tresults := make(component.Components, 0, len(components))\n\n\tfor _, c := range components {\n\t\tdiscoveryCtx := c.DiscoveryContext()\n\t\tif discoveryCtx == nil || discoveryCtx.Ref == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif discoveryCtx.Ref == filter.FromRef || discoveryCtx.Ref == filter.ToRef {\n\t\t\tresults = append(results, c)\n\t\t}\n\t}\n\n\treturn results, nil\n}\n\n// traverseGraph recursively traverses the graph in the specified direction (dependencies or dependents).\n// The visited map tracks the maximum remaining depth at which each node was visited, allowing re-traversal\n// when a node is reached with more remaining depth (e.g., from a closer target).\n// The warnOnLimit flag controls whether to log a warning when depth is exhausted (used for safety limits only).\nfunc traverseGraph(\n\tl log.Logger,\n\tc component.Component,\n\tparams *graphTraversalParams,\n\tremainingDepth int,\n) {\n\tif remainingDepth <= 0 {\n\t\tif l != nil && params.warnOnLimit {\n\t\t\tdirectionName := params.direction.String()\n\n\t\t\tl.Warnf(\n\t\t\t\t\"Maximum %s traversal depth (%d) reached for component %s during filtering. Some %s may have been excluded from results.\",\n\t\t\t\tdirectionName,\n\t\t\t\tMaxTraversalDepth,\n\t\t\t\tc.Path(),\n\t\t\t\tdirectionName,\n\t\t\t)\n\t\t}\n\n\t\treturn\n\t}\n\n\tpath := c.Path()\n\n\tif prevDepth, seen := params.visited[path]; seen && prevDepth >= remainingDepth {\n\t\treturn\n\t}\n\n\tparams.visited[path] = remainingDepth\n\n\tvar relatedComponents []component.Component\n\tif params.direction == GraphDirectionDependencies {\n\t\trelatedComponents = c.Dependencies()\n\t} else {\n\t\trelatedComponents = c.Dependents()\n\t}\n\n\tfor _, related := range relatedComponents {\n\t\trelatedPath := related.Path()\n\n\t\t// It's not clear why this isn't necessary. It might be in the future.\n\t\t// Tests pass without it, however, so we'll leave it out for now.\n\t\t//\n\t\t// Needs more investigation.\n\t\t//\n\t\t// relatedCtx := related.DiscoveryContext()\n\t\t// if relatedCtx != nil {\n\t\t// \torigin := relatedCtx.Origin()\n\t\t// \tif origin != component.OriginGraphDiscovery {\n\t\t// \t\tl.Debugf(\n\t\t// \t\t\t\"Skipping %s %s in graph expression traversal: component was discovered via %s, not graph discovery\",\n\t\t// \t\t\tdirection.String(),\n\t\t// \t\t\trelatedPath,\n\t\t// \t\t\torigin,\n\t\t// \t\t)\n\n\t\t// \t\tcontinue\n\t\t// \t}\n\t\t// }\n\n\t\tparams.resultSet[relatedPath] = related\n\n\t\ttraverseGraph(l, related, params, remainingDepth-1)\n\t}\n}\n"
  },
  {
    "path": "internal/filter/evaluator_test.go",
    "content": "package filter_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEvaluate_PathFilter(t *testing.T) {\n\tt.Parallel()\n\n\tcomponents := []component.Component{\n\t\tcomponent.NewUnit(\"./apps/app1\"),\n\t\tcomponent.NewUnit(\"./apps/app2\"),\n\t\tcomponent.NewUnit(\"./apps/legacy\"),\n\t\tcomponent.NewUnit(\"./libs/db\"),\n\t\tcomponent.NewUnit(\"./libs/api\"),\n\t\tcomponent.NewUnit(\"./apps/subdir/nested\"),\n\t}\n\n\tfor _, c := range components {\n\t\tc.SetDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t})\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\tfilter   *filter.PathExpression\n\t\texpected []component.Component\n\t}{\n\t\t{\n\t\t\tname:   \"exact path match\",\n\t\t\tfilter: mustPath(t, \"./apps/app1\"),\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"glob with single wildcard\",\n\t\t\tfilter: mustPath(t, \"./apps/*\"),\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./apps/app2\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./apps/legacy\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"glob with single wildcard and partial match\",\n\t\t\tfilter: mustPath(t, \"./apps/app*\"),\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./apps/app2\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"glob with recursive wildcard\",\n\t\t\tfilter: mustPath(t, \"./apps/**\"),\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./apps/app2\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./apps/legacy\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./apps/subdir/nested\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"no matches\",\n\t\t\tfilter:   mustPath(t, \"./nonexistent/*\"),\n\t\t\texpected: []component.Component{},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tl := log.New()\n\t\t\tresult, err := filter.Evaluate(l, tt.filter, components)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.ElementsMatch(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestEvaluate_AttributeFilter(t *testing.T) {\n\tt.Parallel()\n\n\tcomponents := []component.Component{\n\t\tcomponent.NewUnit(\"./apps/app\"),\n\t\tcomponent.NewUnit(\"./libs/app\"),\n\t\tcomponent.NewUnit(\"./libs/db\"),\n\t\tcomponent.NewUnit(\"./libs/api\"),\n\t\tcomponent.NewStack(\"./libs/api\"),\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\tfilter   *filter.AttributeExpression\n\t\texpected []component.Component\n\t}{\n\t\t{\n\t\t\tname:   \"name filter single match\",\n\t\t\tfilter: mustAttr(t, \"name\", \"db\"),\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./libs/db\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"name filter multiple matches\",\n\t\t\tfilter: mustAttr(t, \"name\", \"app\"),\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app\"),\n\t\t\t\tcomponent.NewUnit(\"./libs/app\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"name filter no matches\",\n\t\t\tfilter:   mustAttr(t, \"name\", \"nonexistent\"),\n\t\t\texpected: []component.Component{},\n\t\t},\n\t\t{\n\t\t\tname:   \"type filter unit\",\n\t\t\tfilter: mustAttr(t, \"type\", \"unit\"),\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app\"),\n\t\t\t\tcomponent.NewUnit(\"./libs/app\"),\n\t\t\t\tcomponent.NewUnit(\"./libs/db\"),\n\t\t\t\tcomponent.NewUnit(\"./libs/api\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"type filter stack\",\n\t\t\tfilter: mustAttr(t, \"type\", \"stack\"),\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewStack(\"./libs/api\"),\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\tt.Parallel()\n\n\t\t\tl := log.New()\n\t\t\tresult, err := filter.Evaluate(l, tt.filter, components)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.ElementsMatch(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestEvaluate_AttributeFilter_InvalidKey(t *testing.T) {\n\tt.Parallel()\n\n\tcomponents := []component.Component{\n\t\tcomponent.NewUnit(\"./apps/app\"),\n\t}\n\n\tattrFilter := mustAttr(t, \"invalid\", \"foo\")\n\tl := log.New()\n\tresult, err := filter.Evaluate(l, attrFilter, components)\n\n\trequire.Error(t, err)\n\tassert.Nil(t, result)\n\tassert.Contains(t, err.Error(), \"unknown attribute key\")\n}\n\nfunc TestEvaluate_AttributeFilter_Reading(t *testing.T) {\n\tt.Parallel()\n\n\tcomponents := []component.Component{\n\t\tcomponent.NewUnit(\"./apps/app1\").WithReading(\"shared.hcl\", \"shared.tfvars\"),\n\t\tcomponent.NewUnit(\"./apps/app2\").WithReading(\"shared.hcl\", \"common/variables.hcl\"),\n\t\tcomponent.NewUnit(\"./apps/app3\").WithReading(\"config.yaml\", \"settings.json\"),\n\t\tcomponent.NewUnit(\"./libs/db\").WithReading(\"database.hcl\"),\n\t\tcomponent.NewUnit(\"./libs/api\").WithReading(),\n\t\tcomponent.NewUnit(\"./apps/app4\").WithReading(\"shared.hcl\", \"shared.tfvars\", \"extra.hcl\"),\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\tfilter   *filter.AttributeExpression\n\t\texpected []component.Component\n\t}{\n\t\t{\n\t\t\tname:   \"exact file path match - single match\",\n\t\t\tfilter: mustAttr(t, \"reading\", \"database.hcl\"),\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./libs/db\").WithReading(\"database.hcl\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"exact file path match - multiple matches\",\n\t\t\tfilter: mustAttr(t, \"reading\", \"shared.hcl\"),\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithReading(\"shared.hcl\", \"shared.tfvars\"),\n\t\t\t\tcomponent.NewUnit(\"./apps/app2\").WithReading(\"shared.hcl\", \"common/variables.hcl\"),\n\t\t\t\tcomponent.NewUnit(\"./apps/app4\").WithReading(\"shared.hcl\", \"shared.tfvars\", \"extra.hcl\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"exact file path match - no matches\",\n\t\t\tfilter:   mustAttr(t, \"reading\", \"nonexistent.hcl\"),\n\t\t\texpected: []component.Component{},\n\t\t},\n\t\t{\n\t\t\tname:   \"glob pattern with single wildcard - *.hcl\",\n\t\t\tfilter: mustAttr(t, \"reading\", \"*.hcl\"),\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithReading(\"shared.hcl\", \"shared.tfvars\"),\n\t\t\t\tcomponent.NewUnit(\"./apps/app2\").WithReading(\"shared.hcl\", \"common/variables.hcl\"),\n\t\t\t\tcomponent.NewUnit(\"./libs/db\").WithReading(\"database.hcl\"),\n\t\t\t\tcomponent.NewUnit(\"./apps/app4\").WithReading(\"shared.hcl\", \"shared.tfvars\", \"extra.hcl\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"glob pattern with prefix - shared*\",\n\t\t\tfilter: mustAttr(t, \"reading\", \"shared*\"),\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithReading(\"shared.hcl\", \"shared.tfvars\"),\n\t\t\t\tcomponent.NewUnit(\"./apps/app2\").WithReading(\"shared.hcl\", \"common/variables.hcl\"),\n\t\t\t\tcomponent.NewUnit(\"./apps/app4\").WithReading(\"shared.hcl\", \"shared.tfvars\", \"extra.hcl\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"glob pattern with double wildcard - **/variables.hcl\",\n\t\t\tfilter: mustAttr(t, \"reading\", \"**/variables.hcl\"),\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app2\").WithReading(\"shared.hcl\", \"common/variables.hcl\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty Reading slice - no matches\",\n\t\t\tfilter:   mustAttr(t, \"reading\", \"*.hcl\"),\n\t\t\texpected: []component.Component{},\n\t\t},\n\t\t{\n\t\t\tname:   \"glob pattern with question mark - config.???l\",\n\t\t\tfilter: mustAttr(t, \"reading\", \"config.???l\"),\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app3\").WithReading(\"config.yaml\", \"settings.json\"),\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\tt.Parallel()\n\n\t\t\tvar testComponents []component.Component\n\t\t\tif tt.name == \"empty Reading slice - no matches\" {\n\t\t\t\ttestComponents = []component.Component{\n\t\t\t\t\tcomponent.NewUnit(\"./libs/api\").WithReading(),\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttestComponents = components\n\t\t\t}\n\n\t\t\tl := log.New()\n\t\t\tresult, err := filter.Evaluate(l, tt.filter, testComponents)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.ElementsMatch(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestEvaluate_AttributeFilter_Source(t *testing.T) {\n\tt.Parallel()\n\n\tcomponents := []component.Component{\n\t\tcomponent.NewUnit(\"./apps/app1\").WithConfig(\n\t\t\t&config.TerragruntConfig{\n\t\t\t\tTerraform: &config.TerraformConfig{\n\t\t\t\t\tSource: helpers.PointerTo(\"github.com/acme/foo\"),\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tcomponent.NewUnit(\"./apps/app2\").WithConfig(\n\t\t\t&config.TerragruntConfig{\n\t\t\t\tTerraform: &config.TerraformConfig{\n\t\t\t\t\tSource: helpers.PointerTo(\"git::git@github.com:acme/bar?ref=v1.0.0\"),\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\tfilter   *filter.AttributeExpression\n\t\texpected []component.Component\n\t}{\n\t\t{\n\t\t\tname:   \"glob pattern with single wildcard - github.com/acme/*\",\n\t\t\tfilter: mustAttr(t, \"source\", \"github.com/acme/*\"),\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponents[0],\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"glob pattern with double wildcard - git::git@github.com:acme/**\",\n\t\t\tfilter: mustAttr(t, \"source\", \"git::git@github.com:acme/**\"),\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponents[1],\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"glob pattern with double wildcard - **github.com**\",\n\t\t\tfilter: mustAttr(t, \"source\", \"**github.com**\"),\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponents[0],\n\t\t\t\tcomponents[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\tt.Parallel()\n\n\t\t\tl := log.New()\n\t\t\tresult, err := filter.Evaluate(l, tt.filter, components)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.ElementsMatch(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestEvaluate_AttributeFilter_Reading_ComponentAddedOnlyOnce(t *testing.T) {\n\tt.Parallel()\n\n\tcomponents := []component.Component{\n\t\tcomponent.NewUnit(\"./apps/app1\").WithReading(\"shared.hcl\", \"shared.tfvars\", \"shared.yaml\"),\n\t}\n\n\t// This glob should match multiple files in the Reading slice, but component should only be added once\n\tattrFilter := mustAttr(t, \"reading\", \"shared*\")\n\tl := log.New()\n\tresult, err := filter.Evaluate(l, attrFilter, components)\n\trequire.NoError(t, err)\n\n\t// Should only have one component even though three files matched\n\tassert.Len(t, result, 1)\n\tassert.Equal(t, \"./apps/app1\", result[0].Path())\n}\n\nfunc TestEvaluate_PrefixExpression(t *testing.T) {\n\tt.Parallel()\n\n\tcomponents := []component.Component{\n\t\tcomponent.NewUnit(\"./apps/app1\"),\n\t\tcomponent.NewUnit(\"./apps/app2\"),\n\t\tcomponent.NewUnit(\"./apps/legacy\"),\n\t\tcomponent.NewUnit(\"./libs/db\"),\n\t}\n\n\tfor _, c := range components {\n\t\tc.SetDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t})\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\texpr     *filter.PrefixExpression\n\t\texpected []component.Component\n\t}{\n\t\t{\n\t\t\tname: \"exclude by name\",\n\t\t\texpr: &filter.PrefixExpression{\n\t\t\t\tOperator: \"!\",\n\t\t\t\tRight:    mustAttr(t, \"name\", \"legacy\"),\n\t\t\t},\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./apps/app2\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./libs/db\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"exclude by path\",\n\t\t\texpr: &filter.PrefixExpression{\n\t\t\t\tOperator: \"!\",\n\t\t\t\tRight:    mustPath(t, \"./apps/legacy\"),\n\t\t\t},\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./apps/app2\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./libs/db\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"exclude by glob\",\n\t\t\texpr: &filter.PrefixExpression{\n\t\t\t\tOperator: \"!\",\n\t\t\t\tRight:    mustPath(t, \"./apps/*\"),\n\t\t\t},\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./libs/db\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"exclude all (double negation effect)\",\n\t\t\texpr: &filter.PrefixExpression{\n\t\t\t\tOperator: \"!\",\n\t\t\t\tRight:    mustAttr(t, \"type\", \"unit\"),\n\t\t\t},\n\t\t\texpected: []component.Component{},\n\t\t},\n\t\t{\n\t\t\tname: \"exclude nothing\",\n\t\t\texpr: &filter.PrefixExpression{\n\t\t\t\tOperator: \"!\",\n\t\t\t\tRight:    mustAttr(t, \"name\", \"nonexistent\"),\n\t\t\t},\n\t\t\texpected: components,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tl := log.New()\n\t\t\tresult, err := filter.Evaluate(l, tt.expr, components)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.ElementsMatch(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestEvaluate_InfixExpression(t *testing.T) {\n\tt.Parallel()\n\n\tcomponents := []component.Component{\n\t\tcomponent.NewUnit(\"./apps/app1\"),\n\t\tcomponent.NewUnit(\"./apps/app2\"),\n\t\tcomponent.NewUnit(\"./apps/legacy\"),\n\t\tcomponent.NewUnit(\"./libs/db\"),\n\t\tcomponent.NewUnit(\"./libs/api\"),\n\t}\n\n\tfor _, c := range components {\n\t\tc.SetDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t})\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\texpr     *filter.InfixExpression\n\t\texpected []component.Component\n\t}{\n\t\t{\n\t\t\tname: \"intersection of path and name\",\n\t\t\texpr: &filter.InfixExpression{\n\t\t\t\tLeft:     mustPath(t, \"./apps/*\"),\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight:    mustAttr(t, \"name\", \"app1\"),\n\t\t\t},\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"intersection with no overlap\",\n\t\t\texpr: &filter.InfixExpression{\n\t\t\t\tLeft:     mustPath(t, \"./apps/*\"),\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight:    mustAttr(t, \"name\", \"db\"),\n\t\t\t},\n\t\t\texpected: []component.Component{},\n\t\t},\n\t\t{\n\t\t\tname: \"intersection of exact path and name\",\n\t\t\texpr: &filter.InfixExpression{\n\t\t\t\tLeft:     mustPath(t, \"./apps/app1\"),\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight:    mustAttr(t, \"name\", \"app1\"),\n\t\t\t},\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"intersection of empty results\",\n\t\t\texpr: &filter.InfixExpression{\n\t\t\t\tLeft:     mustAttr(t, \"name\", \"nonexistent1\"),\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight:    mustAttr(t, \"name\", \"app1\"),\n\t\t\t},\n\t\t\texpected: []component.Component{},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tl := log.New()\n\t\t\tresult, err := filter.Evaluate(l, tt.expr, components)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.ElementsMatch(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestEvaluate_ComplexExpressions(t *testing.T) {\n\tt.Parallel()\n\n\tcomponents := []component.Component{\n\t\tcomponent.NewUnit(\"./apps/app1\"),\n\t\tcomponent.NewUnit(\"./apps/app2\"),\n\t\tcomponent.NewUnit(\"./apps/legacy\"),\n\t\tcomponent.NewUnit(\"./libs/db\"),\n\t\tcomponent.NewUnit(\"./libs/api\"),\n\t\tcomponent.NewUnit(\"./special/unit\"),\n\t}\n\n\tfor _, c := range components {\n\t\tc.SetDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t})\n\t}\n\n\ttests := []struct {\n\t\tname     string\n\t\texpr     filter.Expression\n\t\texpected []component.Component\n\t}{\n\t\t{\n\t\t\tname: \"intersection with negation (refinement)\",\n\t\t\texpr: &filter.InfixExpression{\n\t\t\t\tLeft:     mustPath(t, \"./apps/*\"),\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight: &filter.PrefixExpression{\n\t\t\t\t\tOperator: \"!\",\n\t\t\t\t\tRight:    mustAttr(t, \"name\", \"legacy\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./apps/app2\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"negated intersection\",\n\t\t\texpr: &filter.PrefixExpression{\n\t\t\t\tOperator: \"!\",\n\t\t\t\tRight: &filter.InfixExpression{\n\t\t\t\t\tLeft:     mustPath(t, \"./apps/*\"),\n\t\t\t\t\tOperator: \"|\",\n\t\t\t\t\tRight:    mustAttr(t, \"name\", \"app1\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app2\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./apps/legacy\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./libs/db\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./libs/api\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./special/unit\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"chained intersections (multiple refinements)\",\n\t\t\texpr: &filter.InfixExpression{\n\t\t\t\tLeft: &filter.InfixExpression{\n\t\t\t\t\tLeft:     mustPath(t, \"./apps/*\"),\n\t\t\t\t\tOperator: \"|\",\n\t\t\t\t\tRight:    &filter.PrefixExpression{Operator: \"!\", Right: mustAttr(t, \"name\", \"legacy\")},\n\t\t\t\t},\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight:    mustAttr(t, \"name\", \"app1\"),\n\t\t\t},\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tl := log.New()\n\t\t\tresult, err := filter.Evaluate(l, tt.expr, components)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.ElementsMatch(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestEvaluate_EdgeCases(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"nil expression\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tcomponents := []component.Component{component.NewUnit(\"./app\")}\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, nil, components)\n\n\t\trequire.Error(t, err)\n\t\tassert.Nil(t, result)\n\t\tassert.Contains(t, err.Error(), \"expression is nil\")\n\t})\n\n\tt.Run(\"empty components list\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpr := mustAttr(t, \"name\", \"foo\")\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, []component.Component{})\n\n\t\trequire.NoError(t, err)\n\t\tassert.Empty(t, result)\n\t})\n\n\tt.Run(\"invalid glob pattern\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t_, err := filter.NewPathFilter(\"[invalid-glob\")\n\t\trequire.Error(t, err)\n\t})\n}\n\nfunc TestEvaluate_GraphExpression(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a component graph: vpc -> db -> app\n\n\ttests := []struct {\n\t\texpr     *filter.GraphExpression\n\t\tsetup    func() []component.Component\n\t\tname     string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname: \"dependency traversal - app...\",\n\t\t\texpr: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"app\"),\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tIncludeDependents:   false,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t},\n\t\t\texpected: []string{\"./app\", \"./db\", \"./vpc\"},\n\t\t\tsetup: func() []component.Component {\n\t\t\t\tvpcCtx := &component.DiscoveryContext{}\n\t\t\t\tvpcCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\t\t\t\tvpc := component.NewUnit(\"./vpc\").WithDiscoveryContext(vpcCtx)\n\n\t\t\t\tdbCtx := &component.DiscoveryContext{}\n\t\t\t\tdbCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\t\t\t\tdb := component.NewUnit(\"./db\").WithDiscoveryContext(dbCtx)\n\n\t\t\t\tapp := component.NewUnit(\"./app\")\n\n\t\t\t\tapp.AddDependency(db)\n\t\t\t\tdb.AddDependency(vpc)\n\n\t\t\t\treturn []component.Component{vpc, db, app}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"dependent traversal - ...vpc\",\n\t\t\texpr: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"vpc\"),\n\t\t\t\tIncludeDependencies: false,\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t},\n\t\t\texpected: []string{\"./vpc\", \"./db\", \"./app\"},\n\t\t\tsetup: func() []component.Component {\n\t\t\t\tvpc := component.NewUnit(\"./vpc\")\n\n\t\t\t\tdbCtx := &component.DiscoveryContext{}\n\t\t\t\tdbCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\t\t\t\tdb := component.NewUnit(\"./db\").WithDiscoveryContext(dbCtx)\n\n\t\t\t\tappCtx := &component.DiscoveryContext{}\n\t\t\t\tappCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\t\t\t\tapp := component.NewUnit(\"./app\").WithDiscoveryContext(appCtx)\n\n\t\t\t\tapp.AddDependency(db)\n\t\t\t\tdb.AddDependency(vpc)\n\n\t\t\t\treturn []component.Component{vpc, db, app}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"both directions - ...db...\",\n\t\t\texpr: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"db\"),\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t},\n\t\t\texpected: []string{\"./db\", \"./vpc\", \"./app\"},\n\t\t\tsetup: func() []component.Component {\n\t\t\t\tvpcCtx := &component.DiscoveryContext{}\n\t\t\t\tvpcCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\t\t\t\tvpc := component.NewUnit(\"./vpc\").WithDiscoveryContext(vpcCtx)\n\n\t\t\t\tdb := component.NewUnit(\"./db\")\n\n\t\t\t\tappCtx := &component.DiscoveryContext{}\n\t\t\t\tappCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\t\t\t\tapp := component.NewUnit(\"./app\").WithDiscoveryContext(appCtx)\n\n\t\t\t\tapp.AddDependency(db)\n\t\t\t\tdb.AddDependency(vpc)\n\n\t\t\t\treturn []component.Component{vpc, db, app}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"exclude target - ^app...\",\n\t\t\texpr: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"app\"),\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tIncludeDependents:   false,\n\t\t\t\tExcludeTarget:       true,\n\t\t\t},\n\t\t\texpected: []string{\"./db\", \"./vpc\"},\n\t\t\tsetup: func() []component.Component {\n\t\t\t\tvpcCtx := &component.DiscoveryContext{}\n\t\t\t\tvpcCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\t\t\t\tvpc := component.NewUnit(\"./vpc\").WithDiscoveryContext(vpcCtx)\n\n\t\t\t\tdbCtx := &component.DiscoveryContext{}\n\t\t\t\tdbCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\t\t\t\tdb := component.NewUnit(\"./db\").WithDiscoveryContext(dbCtx)\n\n\t\t\t\tapp := component.NewUnit(\"./app\")\n\n\t\t\t\tapp.AddDependency(db)\n\t\t\t\tdb.AddDependency(vpc)\n\n\t\t\t\treturn []component.Component{vpc, db, app}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"exclude target with dependents - ...^db...\",\n\t\t\texpr: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"db\"),\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tExcludeTarget:       true,\n\t\t\t},\n\t\t\texpected: []string{\"./vpc\", \"./app\"},\n\t\t\tsetup: func() []component.Component {\n\t\t\t\tvpcCtx := &component.DiscoveryContext{}\n\t\t\t\tvpcCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\t\t\t\tvpc := component.NewUnit(\"./vpc\").WithDiscoveryContext(vpcCtx)\n\n\t\t\t\tdb := component.NewUnit(\"./db\")\n\n\t\t\t\tappCtx := &component.DiscoveryContext{}\n\t\t\t\tappCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\t\t\t\tapp := component.NewUnit(\"./app\").WithDiscoveryContext(appCtx)\n\n\t\t\t\tapp.AddDependency(db)\n\t\t\t\tdb.AddDependency(vpc)\n\n\t\t\t\treturn []component.Component{vpc, db, app}\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\tt.Parallel()\n\n\t\t\tcomponents := tt.setup()\n\n\t\t\texpected := make([]component.Component, 0, len(tt.expected))\n\n\t\t\texpectedMap := make(map[string]bool)\n\t\t\tfor _, path := range tt.expected {\n\t\t\t\texpectedMap[path] = true\n\t\t\t}\n\n\t\t\tfor _, c := range components {\n\t\t\t\tif expectedMap[c.Path()] {\n\t\t\t\t\texpected = append(expected, c)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tl := log.New()\n\t\t\tresult, err := filter.Evaluate(l, tt.expr, components)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.ElementsMatch(t, expected, result)\n\t\t})\n\t}\n}\n\nfunc TestEvaluate_GraphExpression_ComplexGraph(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a more complex graph:\n\t// vpc -> [db, cache] -> app\n\n\tt.Run(\"dependency traversal from app finds all dependencies\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvpcCtx := &component.DiscoveryContext{}\n\t\tvpcCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\t\tvpc := component.NewUnit(\"./vpc\").WithDiscoveryContext(vpcCtx)\n\n\t\tdbCtx := &component.DiscoveryContext{}\n\t\tdbCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\t\tdb := component.NewUnit(\"./db\").WithDiscoveryContext(dbCtx)\n\n\t\tcacheCtx := &component.DiscoveryContext{}\n\t\tcacheCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\t\tcache := component.NewUnit(\"./cache\").WithDiscoveryContext(cacheCtx)\n\n\t\tapp := component.NewUnit(\"./app\")\n\n\t\tapp.AddDependency(db)\n\t\tapp.AddDependency(cache)\n\t\tdb.AddDependency(vpc)\n\t\tcache.AddDependency(vpc)\n\n\t\tcomponents := []component.Component{vpc, db, cache, app}\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              mustAttr(t, \"name\", \"app\"),\n\t\t\tIncludeDependencies: true,\n\t\t\tIncludeDependents:   false,\n\t\t\tExcludeTarget:       false,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\t\tassert.ElementsMatch(t, []component.Component{app, db, cache, vpc}, result)\n\t})\n\n\tt.Run(\"dependent traversal from vpc finds all dependents\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvpc := component.NewUnit(\"./vpc\")\n\n\t\tdbCtx := &component.DiscoveryContext{}\n\t\tdbCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\t\tdb := component.NewUnit(\"./db\").WithDiscoveryContext(dbCtx)\n\n\t\tcacheCtx := &component.DiscoveryContext{}\n\t\tcacheCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\t\tcache := component.NewUnit(\"./cache\").WithDiscoveryContext(cacheCtx)\n\n\t\tappCtx := &component.DiscoveryContext{}\n\t\tappCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\t\tapp := component.NewUnit(\"./app\").WithDiscoveryContext(appCtx)\n\n\t\tapp.AddDependency(db)\n\t\tapp.AddDependency(cache)\n\t\tdb.AddDependency(vpc)\n\t\tcache.AddDependency(vpc)\n\n\t\tcomponents := []component.Component{vpc, db, cache, app}\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              mustAttr(t, \"name\", \"vpc\"),\n\t\t\tIncludeDependencies: false,\n\t\t\tIncludeDependents:   true,\n\t\t\tExcludeTarget:       false,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\t\tassert.ElementsMatch(t, []component.Component{vpc, db, cache, app}, result)\n\t})\n}\n\nfunc TestEvaluate_GraphExpression_EmptyResults(t *testing.T) {\n\tt.Parallel()\n\n\tcomponents := []component.Component{\n\t\tcomponent.NewUnit(\"./app\"),\n\t\tcomponent.NewUnit(\"./db\"),\n\t}\n\n\tt.Run(\"target doesn't match any component\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              mustAttr(t, \"name\", \"nonexistent\"),\n\t\t\tIncludeDependencies: true,\n\t\t\tIncludeDependents:   true,\n\t\t\tExcludeTarget:       false,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\t\tassert.Empty(t, result)\n\t})\n}\n\nfunc TestEvaluate_GraphExpression_NoDependencies(t *testing.T) {\n\tt.Parallel()\n\n\t// Components with no dependencies or dependents\n\tisolated := component.NewUnit(\"./isolated\")\n\tanother := component.NewUnit(\"./another\")\n\n\tcomponents := []component.Component{isolated, another}\n\n\tt.Run(\"component with no dependencies\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              mustAttr(t, \"name\", \"isolated\"),\n\t\t\tIncludeDependencies: true,\n\t\t\tIncludeDependents:   false,\n\t\t\tExcludeTarget:       false,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\t\tassert.ElementsMatch(t, []component.Component{isolated}, result)\n\t})\n\n\tt.Run(\"component with no dependents\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              mustAttr(t, \"name\", \"isolated\"),\n\t\t\tIncludeDependencies: false,\n\t\t\tIncludeDependents:   true,\n\t\t\tExcludeTarget:       false,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\t\tassert.ElementsMatch(t, []component.Component{isolated}, result)\n\t})\n}\n\nfunc TestEvaluate_GraphExpression_CircularDependencies(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a circular dependency: a -> b -> a\n\t// The traversal should not infinite loop\n\ta := component.NewUnit(\"./a\")\n\n\tbCtx := &component.DiscoveryContext{}\n\tbCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\tb := component.NewUnit(\"./b\").WithDiscoveryContext(bCtx)\n\n\ta.AddDependency(b)\n\tb.AddDependency(a)\n\n\tcomponents := []component.Component{a, b}\n\n\tt.Run(\"circular dependency - dependency traversal stops at cycle\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              mustAttr(t, \"name\", \"a\"),\n\t\t\tIncludeDependencies: true,\n\t\t\tIncludeDependents:   false,\n\t\t\tExcludeTarget:       false,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\t\t// Should include both a and b, but not loop infinitely\n\t\tassert.ElementsMatch(t, []component.Component{a, b}, result)\n\t\tassert.Len(t, result, 2)\n\t})\n\n\tt.Run(\"circular dependency - dependent traversal stops at cycle\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              mustAttr(t, \"name\", \"a\"),\n\t\t\tIncludeDependencies: false,\n\t\t\tIncludeDependents:   true,\n\t\t\tExcludeTarget:       false,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\t\t// Should include both a and b, but not loop infinitely\n\t\tassert.ElementsMatch(t, []component.Component{a, b}, result)\n\t\tassert.Len(t, result, 2)\n\t})\n}\n\nfunc TestEvaluate_GraphExpression_WithPathFilter(t *testing.T) {\n\tt.Parallel()\n\n\tvpcCtx := &component.DiscoveryContext{\n\t\tWorkingDir: \".\",\n\t}\n\tvpcCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\tvpc := component.NewUnit(\"./vpc\").WithDiscoveryContext(vpcCtx)\n\n\tdbCtx := &component.DiscoveryContext{\n\t\tWorkingDir: \".\",\n\t}\n\tdbCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\tdb := component.NewUnit(\"./db\").WithDiscoveryContext(dbCtx)\n\n\tapp := component.NewUnit(\"./app\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\tWorkingDir: \".\",\n\t})\n\n\tapp.AddDependency(db)\n\tdb.AddDependency(vpc)\n\n\tcomponents := []component.Component{vpc, db, app}\n\n\tt.Run(\"graph expression with path filter target\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              mustPath(t, \"./app\"),\n\t\t\tIncludeDependencies: true,\n\t\t\tIncludeDependents:   false,\n\t\t\tExcludeTarget:       false,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\t\tassert.ElementsMatch(t, []component.Component{app, db, vpc}, result)\n\t})\n}\n\nfunc TestEvaluate_GraphExpression_DepthLimited(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a component graph: a -> b -> c -> d\n\ta := component.NewUnit(\"./a\")\n\tb := component.NewUnit(\"./b\")\n\tc := component.NewUnit(\"./c\")\n\td := component.NewUnit(\"./d\")\n\n\t// Set up dependencies: d depends on c, c depends on b, b depends on a\n\td.AddDependency(c)\n\tc.AddDependency(b)\n\tb.AddDependency(a)\n\n\tcomponents := []component.Component{a, b, c, d}\n\n\tt.Run(\"depth 1 dependency traversal from d\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              mustAttr(t, \"name\", \"d\"),\n\t\t\tIncludeDependencies: true,\n\t\t\tIncludeDependents:   false,\n\t\t\tExcludeTarget:       false,\n\t\t\tDependencyDepth:     1,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\t\tassert.ElementsMatch(t, []component.Component{d, c}, result)\n\t})\n\n\tt.Run(\"depth 2 dependency traversal from d\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              mustAttr(t, \"name\", \"d\"),\n\t\t\tIncludeDependencies: true,\n\t\t\tIncludeDependents:   false,\n\t\t\tExcludeTarget:       false,\n\t\t\tDependencyDepth:     2,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\t\tassert.ElementsMatch(t, []component.Component{d, c, b}, result)\n\t})\n\n\tt.Run(\"depth 1 dependent traversal from a\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              mustAttr(t, \"name\", \"a\"),\n\t\t\tIncludeDependencies: false,\n\t\t\tIncludeDependents:   true,\n\t\t\tExcludeTarget:       false,\n\t\t\tDependentDepth:      1,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\t\tassert.ElementsMatch(t, []component.Component{a, b}, result)\n\t})\n\n\tt.Run(\"depth 2 dependent traversal from a\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              mustAttr(t, \"name\", \"a\"),\n\t\t\tIncludeDependencies: false,\n\t\t\tIncludeDependents:   true,\n\t\t\tExcludeTarget:       false,\n\t\t\tDependentDepth:      2,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\t\tassert.ElementsMatch(t, []component.Component{a, b, c}, result)\n\t})\n\n\tt.Run(\"unlimited depth (0) traverses all\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              mustAttr(t, \"name\", \"d\"),\n\t\t\tIncludeDependencies: true,\n\t\t\tIncludeDependents:   false,\n\t\t\tExcludeTarget:       false,\n\t\t\tDependencyDepth:     0,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\t\tassert.ElementsMatch(t, []component.Component{d, c, b, a}, result)\n\t})\n}\n\nfunc TestEvaluate_GraphExpression_DepthLimited_MultipleTargets(t *testing.T) {\n\tt.Parallel()\n\n\t// Graph structure:\n\t//   targetA (2 hops from shared) --> intermediate --> shared --> deep1 --> deep2\n\t//   targetB (1 hop from shared) --> shared\n\t//\n\t// With depth=2:\n\t//   - From targetA: can reach intermediate, shared (2 hops)\n\t//   - From targetB: can reach shared, deep1 (2 hops)\n\t//   - Result should include deep1 even though targetA reaches shared first with less remaining depth\n\n\tctx := &component.DiscoveryContext{WorkingDir: \".\"}\n\n\ttargetA := component.NewUnit(\"./targetA\").WithDiscoveryContext(ctx)\n\ttargetB := component.NewUnit(\"./targetB\").WithDiscoveryContext(ctx)\n\tintermediate := component.NewUnit(\"./intermediate\").WithDiscoveryContext(ctx)\n\tshared := component.NewUnit(\"./shared\").WithDiscoveryContext(ctx)\n\tdeep1 := component.NewUnit(\"./deep1\").WithDiscoveryContext(ctx)\n\tdeep2 := component.NewUnit(\"./deep2\").WithDiscoveryContext(ctx)\n\n\t// Set up dependencies\n\ttargetA.AddDependency(intermediate)\n\tintermediate.AddDependency(shared)\n\ttargetB.AddDependency(shared)\n\tshared.AddDependency(deep1)\n\tdeep1.AddDependency(deep2)\n\n\tcomponents := []component.Component{targetA, targetB, intermediate, shared, deep1, deep2}\n\n\tt.Run(\"multiple targets with shared dependency at different distances\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Match both targetA and targetB using glob\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              mustPath(t, \"./target*\"),\n\t\t\tIncludeDependencies: true,\n\t\t\tIncludeDependents:   false,\n\t\t\tExcludeTarget:       false,\n\t\t\tDependencyDepth:     2,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\n\t\t// Should include: targetA, targetB, intermediate (1 hop from A), shared (2 hops from A, 1 from B), deep1 (2 hops from B)\n\t\t// Should NOT include: deep2 (3 hops from B, too deep)\n\t\tassert.ElementsMatch(t, []component.Component{targetA, targetB, intermediate, shared, deep1}, result)\n\t})\n}\n\nfunc TestEvaluate_GitFilter(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname      string\n\t\tfromRef   string\n\t\ttoRef     string\n\t\tsetup     func() []component.Component\n\t\texpected  []component.Component\n\t\twantError bool\n\t}{\n\t\t{\n\t\t\tname:    \"components without DiscoveryContext are filtered out\",\n\t\t\tfromRef: \"main\",\n\t\t\ttoRef:   \"HEAD\",\n\t\t\tsetup: func() []component.Component {\n\t\t\t\treturn []component.Component{\n\t\t\t\t\tcomponent.NewUnit(\"./apps/app1\"),\n\t\t\t\t\tcomponent.NewUnit(\"./apps/app2\"),\n\t\t\t\t\tcomponent.NewUnit(\"./libs/db\"),\n\t\t\t\t}\n\t\t\t},\n\t\t\texpected: []component.Component{},\n\t\t},\n\t\t{\n\t\t\tname:    \"components with empty Ref are filtered out\",\n\t\t\tfromRef: \"main\",\n\t\t\ttoRef:   \"HEAD\",\n\t\t\tsetup: func() []component.Component {\n\t\t\t\tapp1 := component.NewUnit(\"./apps/app1\")\n\t\t\t\tapp1.SetDiscoveryContext(&component.DiscoveryContext{Ref: \"\"})\n\n\t\t\t\tapp2 := component.NewUnit(\"./apps/app2\")\n\t\t\t\tapp2.SetDiscoveryContext(&component.DiscoveryContext{Ref: \"\"})\n\n\t\t\t\treturn []component.Component{app1, app2}\n\t\t\t},\n\t\t\texpected: []component.Component{},\n\t\t},\n\t\t{\n\t\t\tname:    \"components with Ref matching FromRef are included\",\n\t\t\tfromRef: \"main\",\n\t\t\ttoRef:   \"HEAD\",\n\t\t\tsetup: func() []component.Component {\n\t\t\t\tapp1 := component.NewUnit(\"./apps/app1\")\n\t\t\t\tapp1.SetDiscoveryContext(&component.DiscoveryContext{Ref: \"main\"})\n\n\t\t\t\tapp2 := component.NewUnit(\"./apps/app2\")\n\t\t\t\tapp2.SetDiscoveryContext(&component.DiscoveryContext{Ref: \"feature\"})\n\n\t\t\t\tdb := component.NewUnit(\"./libs/db\")\n\t\t\t\tdb.SetDiscoveryContext(&component.DiscoveryContext{Ref: \"main\"})\n\n\t\t\t\treturn []component.Component{app1, app2, db}\n\t\t\t},\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\"),\n\t\t\t\tcomponent.NewUnit(\"./libs/db\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"components with Ref matching ToRef are included\",\n\t\t\tfromRef: \"main\",\n\t\t\ttoRef:   \"HEAD\",\n\t\t\tsetup: func() []component.Component {\n\t\t\t\tapp1 := component.NewUnit(\"./apps/app1\")\n\t\t\t\tapp1.SetDiscoveryContext(&component.DiscoveryContext{Ref: \"HEAD\"})\n\n\t\t\t\tapp2 := component.NewUnit(\"./apps/app2\")\n\t\t\t\tapp2.SetDiscoveryContext(&component.DiscoveryContext{Ref: \"feature\"})\n\n\t\t\t\tdb := component.NewUnit(\"./libs/db\")\n\t\t\t\tdb.SetDiscoveryContext(&component.DiscoveryContext{Ref: \"HEAD\"})\n\n\t\t\t\treturn []component.Component{app1, app2, db}\n\t\t\t},\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\"),\n\t\t\t\tcomponent.NewUnit(\"./libs/db\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"components with Ref matching either FromRef or ToRef are included\",\n\t\t\tfromRef: \"main\",\n\t\t\ttoRef:   \"HEAD\",\n\t\t\tsetup: func() []component.Component {\n\t\t\t\tapp1 := component.NewUnit(\"./apps/app1\")\n\t\t\t\tapp1.SetDiscoveryContext(&component.DiscoveryContext{Ref: \"main\"})\n\n\t\t\t\tapp2 := component.NewUnit(\"./apps/app2\")\n\t\t\t\tapp2.SetDiscoveryContext(&component.DiscoveryContext{Ref: \"HEAD\"})\n\n\t\t\t\tdb := component.NewUnit(\"./libs/db\")\n\t\t\t\tdb.SetDiscoveryContext(&component.DiscoveryContext{Ref: \"feature\"})\n\n\t\t\t\tapi := component.NewUnit(\"./libs/api\")\n\t\t\t\tapi.SetDiscoveryContext(&component.DiscoveryContext{Ref: \"main\"})\n\n\t\t\t\treturn []component.Component{app1, app2, db, api}\n\t\t\t},\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\"),\n\t\t\t\tcomponent.NewUnit(\"./apps/app2\"),\n\t\t\t\tcomponent.NewUnit(\"./libs/api\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"components with Ref not matching either are filtered out\",\n\t\t\tfromRef: \"main\",\n\t\t\ttoRef:   \"HEAD\",\n\t\t\tsetup: func() []component.Component {\n\t\t\t\tapp1 := component.NewUnit(\"./apps/app1\")\n\t\t\t\tapp1.SetDiscoveryContext(&component.DiscoveryContext{Ref: \"feature\"})\n\n\t\t\t\tapp2 := component.NewUnit(\"./apps/app2\")\n\t\t\t\tapp2.SetDiscoveryContext(&component.DiscoveryContext{Ref: \"develop\"})\n\n\t\t\t\tdb := component.NewUnit(\"./libs/db\")\n\t\t\t\tdb.SetDiscoveryContext(&component.DiscoveryContext{Ref: \"release\"})\n\n\t\t\t\treturn []component.Component{app1, app2, db}\n\t\t\t},\n\t\t\texpected: []component.Component{},\n\t\t},\n\t\t{\n\t\t\tname:    \"mixed components with and without DiscoveryContext\",\n\t\t\tfromRef: \"main\",\n\t\t\ttoRef:   \"HEAD\",\n\t\t\tsetup: func() []component.Component {\n\t\t\t\tapp1 := component.NewUnit(\"./apps/app1\")\n\t\t\t\tapp1.SetDiscoveryContext(&component.DiscoveryContext{Ref: \"main\"})\n\n\t\t\t\tapp2 := component.NewUnit(\"./apps/app2\")\n\t\t\t\t// No DiscoveryContext set\n\n\t\t\t\tdb := component.NewUnit(\"./libs/db\")\n\t\t\t\tdb.SetDiscoveryContext(&component.DiscoveryContext{Ref: \"HEAD\"})\n\n\t\t\t\tapi := component.NewUnit(\"./libs/api\")\n\t\t\t\tapi.SetDiscoveryContext(&component.DiscoveryContext{Ref: \"\"})\n\n\t\t\t\treturn []component.Component{app1, app2, db, api}\n\t\t\t},\n\t\t\texpected: []component.Component{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\"),\n\t\t\t\tcomponent.NewUnit(\"./libs/db\"),\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\tt.Parallel()\n\n\t\t\tgitFilter := filter.NewGitExpression(tt.fromRef, tt.toRef)\n\t\t\tcomponents := tt.setup()\n\n\t\t\tl := log.New()\n\t\t\tresult, err := filter.Evaluate(l, gitFilter, components)\n\n\t\t\tif tt.wantError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Nil(t, result)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tresultPaths := make([]string, len(result))\n\t\t\t\tfor i, c := range result {\n\t\t\t\t\tresultPaths[i] = c.Path()\n\t\t\t\t}\n\n\t\t\t\texpectedPaths := make([]string, len(tt.expected))\n\t\t\t\tfor i, c := range tt.expected {\n\t\t\t\t\texpectedPaths[i] = c.Path()\n\t\t\t\t}\n\n\t\t\t\tassert.ElementsMatch(t, expectedPaths, resultPaths)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEvaluate_GitFilterString(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tfilter   *filter.GitExpression\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"two references\",\n\t\t\tfilter:   filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\texpected: \"[main...HEAD]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"commit SHA references\",\n\t\t\tfilter:   filter.NewGitExpression(\"abc123\", \"def456\"),\n\t\t\texpected: \"[abc123...def456]\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassert.Equal(t, tt.expected, tt.filter.String())\n\t\t})\n\t}\n}\n\nfunc TestGitFilter_RequiresDiscovery(t *testing.T) {\n\tt.Parallel()\n\n\tgitFilter := filter.NewGitExpression(\"main\", \"HEAD\")\n\n\texpr, requires := gitFilter.RequiresDiscovery()\n\tassert.True(t, requires)\n\tassert.Equal(t, gitFilter, expr)\n}\n\nfunc TestGitFilter_RequiresParse(t *testing.T) {\n\tt.Parallel()\n\n\tgitFilter := filter.NewGitExpression(\"main\", \"HEAD\")\n\n\texpr, requires := gitFilter.RequiresParse()\n\tassert.False(t, requires)\n\tassert.Nil(t, expr)\n}\n\n// TestEvaluate_GraphExpressionWithGitExpressionTarget tests evaluating GraphExpressions\n// where the target is a GitExpression.\n//\n// e.g.\n// `... [main...commit] ...`\nfunc TestEvaluate_GraphExpressionWithGitExpressionTarget(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"dependencies of git-changed component\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvpc := component.NewUnit(\"./vpc\")\n\t\tdb := component.NewUnit(\"./db\")\n\t\tapp := component.NewUnit(\"./app\")\n\n\t\tapp.AddDependency(db)\n\t\tdb.AddDependency(vpc)\n\n\t\tdiscoveryCtx := &component.DiscoveryContext{Ref: \"HEAD\"}\n\t\tdiscoveryCtx.SuggestOrigin(component.OriginWorktreeDiscovery)\n\n\t\tdb.SetDiscoveryContext(discoveryCtx)\n\n\t\tgraphDiscoveryCtx := &component.DiscoveryContext{}\n\t\tgraphDiscoveryCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\n\t\tvpc.SetDiscoveryContext(graphDiscoveryCtx)\n\t\tapp.SetDiscoveryContext(graphDiscoveryCtx)\n\n\t\tcomponents := []component.Component{vpc, db, app}\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\tIncludeDependencies: true,\n\t\t\tIncludeDependents:   false,\n\t\t\tExcludeTarget:       false,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\n\t\tresultPaths := make([]string, len(result))\n\t\tfor i, c := range result {\n\t\t\tresultPaths[i] = c.Path()\n\t\t}\n\n\t\tassert.ElementsMatch(\n\t\t\tt,\n\t\t\t[]string{\"./db\", \"./vpc\"},\n\t\t\tresultPaths,\n\t\t\t\"Should include db (git-matched) and vpc (its dependency)\",\n\t\t)\n\t})\n\n\tt.Run(\"dependents of git-changed component\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvpc := component.NewUnit(\"./vpc\")\n\t\tdb := component.NewUnit(\"./db\")\n\t\tapp := component.NewUnit(\"./app\")\n\n\t\tapp.AddDependency(db)\n\t\tdb.AddDependency(vpc)\n\n\t\tdiscoveryCtx := &component.DiscoveryContext{Ref: \"HEAD\"}\n\t\tdiscoveryCtx.SuggestOrigin(component.OriginWorktreeDiscovery)\n\n\t\tdb.SetDiscoveryContext(discoveryCtx)\n\n\t\tgraphDiscoveryCtx := &component.DiscoveryContext{}\n\t\tgraphDiscoveryCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\n\t\tvpc.SetDiscoveryContext(graphDiscoveryCtx)\n\t\tapp.SetDiscoveryContext(graphDiscoveryCtx)\n\n\t\tcomponents := []component.Component{vpc, db, app}\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\tIncludeDependencies: false,\n\t\t\tIncludeDependents:   true,\n\t\t\tExcludeTarget:       false,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\n\t\tresultPaths := make([]string, len(result))\n\t\tfor i, c := range result {\n\t\t\tresultPaths[i] = c.Path()\n\t\t}\n\n\t\tassert.ElementsMatch(\n\t\t\tt,\n\t\t\t[]string{\"./db\", \"./app\"},\n\t\t\tresultPaths,\n\t\t\t\"Should include db (git-matched) and app (its dependent)\",\n\t\t)\n\t})\n\n\tt.Run(\"both directions of git-changed component\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvpc := component.NewUnit(\"./vpc\")\n\t\tdb := component.NewUnit(\"./db\")\n\t\tapp := component.NewUnit(\"./app\")\n\n\t\tapp.AddDependency(db)\n\t\tdb.AddDependency(vpc)\n\n\t\tdiscoveryCtx := &component.DiscoveryContext{Ref: \"HEAD\"}\n\t\tdiscoveryCtx.SuggestOrigin(component.OriginWorktreeDiscovery)\n\n\t\tdb.SetDiscoveryContext(discoveryCtx)\n\n\t\tgraphDiscoveryCtx := &component.DiscoveryContext{}\n\t\tgraphDiscoveryCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\n\t\tvpc.SetDiscoveryContext(graphDiscoveryCtx)\n\t\tapp.SetDiscoveryContext(graphDiscoveryCtx)\n\n\t\tcomponents := []component.Component{vpc, db, app}\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\tIncludeDependencies: true,\n\t\t\tIncludeDependents:   true,\n\t\t\tExcludeTarget:       false,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\n\t\tresultPaths := make([]string, len(result))\n\t\tfor i, c := range result {\n\t\t\tresultPaths[i] = c.Path()\n\t\t}\n\n\t\tassert.ElementsMatch(\n\t\t\tt,\n\t\t\t[]string{\"./vpc\", \"./db\", \"./app\"},\n\t\t\tresultPaths,\n\t\t\t\"Should include db (git-matched), vpc (dependency), and app (dependent)\",\n\t\t)\n\t})\n\n\tt.Run(\"no components match git filter - returns empty\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvpc := component.NewUnit(\"./vpc\")\n\t\tdb := component.NewUnit(\"./db\")\n\t\tapp := component.NewUnit(\"./app\")\n\n\t\tapp.AddDependency(db)\n\t\tdb.AddDependency(vpc)\n\n\t\tgraphDiscoveryCtx := &component.DiscoveryContext{}\n\t\tgraphDiscoveryCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\n\t\tvpc.SetDiscoveryContext(graphDiscoveryCtx)\n\t\tapp.SetDiscoveryContext(graphDiscoveryCtx)\n\n\t\tcomponents := []component.Component{vpc, db, app}\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\tIncludeDependencies: true,\n\t\t\tIncludeDependents:   true,\n\t\t\tExcludeTarget:       false,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Empty(t, result, \"Should return empty when no components match git filter\")\n\t})\n\n\tt.Run(\"multiple git-changed components with shared dependencies\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvpc := component.NewUnit(\"./vpc\")\n\t\tdb := component.NewUnit(\"./db\")\n\t\tcache := component.NewUnit(\"./cache\")\n\t\tapp := component.NewUnit(\"./app\")\n\n\t\tapp.AddDependency(db)\n\t\tapp.AddDependency(cache)\n\t\tdb.AddDependency(vpc)\n\t\tcache.AddDependency(vpc)\n\n\t\tdiscoveryCtx := &component.DiscoveryContext{Ref: \"HEAD\"}\n\t\tdiscoveryCtx.SuggestOrigin(component.OriginWorktreeDiscovery)\n\n\t\tdb.SetDiscoveryContext(discoveryCtx)\n\n\t\tdiscoveryCtx = &component.DiscoveryContext{Ref: \"HEAD\"}\n\t\tdiscoveryCtx.SuggestOrigin(component.OriginWorktreeDiscovery)\n\n\t\tcache.SetDiscoveryContext(discoveryCtx)\n\n\t\tgraphDiscoveryCtx := &component.DiscoveryContext{}\n\t\tgraphDiscoveryCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\n\t\tvpc.SetDiscoveryContext(graphDiscoveryCtx)\n\t\tapp.SetDiscoveryContext(graphDiscoveryCtx)\n\n\t\tcomponents := []component.Component{vpc, db, cache, app}\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\tIncludeDependencies: true,\n\t\t\tIncludeDependents:   true,\n\t\t\tExcludeTarget:       false,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\n\t\tresultPaths := make([]string, len(result))\n\t\tfor i, c := range result {\n\t\t\tresultPaths[i] = c.Path()\n\t\t}\n\n\t\tassert.ElementsMatch(\n\t\t\tt,\n\t\t\t[]string{\"./vpc\", \"./db\", \"./cache\", \"./app\"},\n\t\t\tresultPaths,\n\t\t\t\"Should include all components connected to git-changed components\",\n\t\t)\n\t})\n\n\tt.Run(\"exclude target with git expression\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvpc := component.NewUnit(\"./vpc\")\n\t\tdb := component.NewUnit(\"./db\")\n\t\tapp := component.NewUnit(\"./app\")\n\n\t\tapp.AddDependency(db)\n\t\tdb.AddDependency(vpc)\n\n\t\tdiscoveryCtx := &component.DiscoveryContext{Ref: \"HEAD\"}\n\t\tdiscoveryCtx.SuggestOrigin(component.OriginWorktreeDiscovery)\n\n\t\tdb.SetDiscoveryContext(discoveryCtx)\n\n\t\tgraphDiscoveryCtx := &component.DiscoveryContext{}\n\t\tgraphDiscoveryCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\n\t\tvpc.SetDiscoveryContext(graphDiscoveryCtx)\n\t\tapp.SetDiscoveryContext(graphDiscoveryCtx)\n\n\t\tcomponents := []component.Component{vpc, db, app}\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\tIncludeDependencies: true,\n\t\t\tIncludeDependents:   false,\n\t\t\tExcludeTarget:       true,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\n\t\tresultPaths := make([]string, len(result))\n\t\tfor i, c := range result {\n\t\t\tresultPaths[i] = c.Path()\n\t\t}\n\n\t\tassert.ElementsMatch(\n\t\t\tt,\n\t\t\t[]string{\"./vpc\"},\n\t\t\tresultPaths,\n\t\t\t\"Should include only vpc (dependency), excluding db (target)\",\n\t\t)\n\t})\n\n\tt.Run(\"git-changed component with Ref matching FromRef\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvpc := component.NewUnit(\"./vpc\")\n\t\tdb := component.NewUnit(\"./db\")\n\t\tapp := component.NewUnit(\"./app\")\n\n\t\tapp.AddDependency(db)\n\t\tdb.AddDependency(vpc)\n\n\t\tdiscoveryCtx := &component.DiscoveryContext{Ref: \"main\"}\n\t\tdiscoveryCtx.SuggestOrigin(component.OriginWorktreeDiscovery)\n\n\t\tdb.SetDiscoveryContext(discoveryCtx)\n\n\t\tgraphDiscoveryCtx := &component.DiscoveryContext{}\n\t\tgraphDiscoveryCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\n\t\tvpc.SetDiscoveryContext(graphDiscoveryCtx)\n\t\tapp.SetDiscoveryContext(graphDiscoveryCtx)\n\n\t\tcomponents := []component.Component{vpc, db, app}\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\tIncludeDependencies: true,\n\t\t\tIncludeDependents:   true,\n\t\t\tExcludeTarget:       false,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\n\t\tresultPaths := make([]string, len(result))\n\t\tfor i, c := range result {\n\t\t\tresultPaths[i] = c.Path()\n\t\t}\n\n\t\tassert.ElementsMatch(\n\t\t\tt,\n\t\t\t[]string{\"./vpc\", \"./db\", \"./app\"},\n\t\t\tresultPaths,\n\t\t\t\"Should include components when Ref matches FromRef\",\n\t\t)\n\t})\n}\n\n// TestEvaluate_GraphExpressionWithGitTarget_DependencyChain tests that dependency\n// traversal works correctly through a chain when the starting component is git-matched.\nfunc TestEvaluate_GraphExpressionWithGitTarget_DependencyChain(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a longer chain: a -> b -> c -> d -> e\n\ta := component.NewUnit(\"./a\")\n\tb := component.NewUnit(\"./b\")\n\tc := component.NewUnit(\"./c\")\n\td := component.NewUnit(\"./d\")\n\te := component.NewUnit(\"./e\")\n\n\ta.AddDependency(b)\n\tb.AddDependency(c)\n\tc.AddDependency(d)\n\td.AddDependency(e)\n\n\taDiscoveryCtx := &component.DiscoveryContext{}\n\taDiscoveryCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\ta.SetDiscoveryContext(aDiscoveryCtx)\n\n\tbDiscoveryCtx := &component.DiscoveryContext{}\n\tbDiscoveryCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\tb.SetDiscoveryContext(bDiscoveryCtx)\n\n\tcDiscoveryCtx := &component.DiscoveryContext{Ref: \"HEAD\"}\n\tcDiscoveryCtx.SuggestOrigin(component.OriginWorktreeDiscovery)\n\tc.SetDiscoveryContext(cDiscoveryCtx)\n\n\tdDiscoveryCtx := &component.DiscoveryContext{}\n\tdDiscoveryCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\td.SetDiscoveryContext(dDiscoveryCtx)\n\n\teDiscoveryCtx := &component.DiscoveryContext{}\n\teDiscoveryCtx.SuggestOrigin(component.OriginGraphDiscovery)\n\te.SetDiscoveryContext(eDiscoveryCtx)\n\n\tcomponents := []component.Component{a, b, c, d, e}\n\n\tt.Run(\"dependencies traverse the full chain\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\tIncludeDependencies: true,\n\t\t\tIncludeDependents:   false,\n\t\t\tExcludeTarget:       false,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\n\t\tresultPaths := make([]string, len(result))\n\t\tfor i, comp := range result {\n\t\t\tresultPaths[i] = comp.Path()\n\t\t}\n\n\t\tassert.ElementsMatch(t, []string{\"./c\", \"./d\", \"./e\"}, resultPaths)\n\t})\n\n\tt.Run(\"dependents traverse the full chain\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\tIncludeDependencies: false,\n\t\t\tIncludeDependents:   true,\n\t\t\tExcludeTarget:       false,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\n\t\tresultPaths := make([]string, len(result))\n\t\tfor i, comp := range result {\n\t\t\tresultPaths[i] = comp.Path()\n\t\t}\n\n\t\tt.Logf(\"Result paths: %v\", resultPaths)\n\n\t\tassert.ElementsMatch(t, []string{\"./a\", \"./b\", \"./c\"}, resultPaths)\n\t})\n\n\tt.Run(\"both directions traverse the full graph\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\texpr := &filter.GraphExpression{\n\t\t\tTarget:              filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\tIncludeDependencies: true,\n\t\t\tIncludeDependents:   true,\n\t\t\tExcludeTarget:       false,\n\t\t}\n\n\t\tl := log.New()\n\t\tresult, err := filter.Evaluate(l, expr, components)\n\t\trequire.NoError(t, err)\n\n\t\tresultPaths := make([]string, len(result))\n\t\tfor i, comp := range result {\n\t\t\tresultPaths[i] = comp.Path()\n\t\t}\n\n\t\tt.Logf(\"Result paths: %v\", resultPaths)\n\n\t\tassert.ElementsMatch(\n\t\t\tt,\n\t\t\t[]string{\"./a\", \"./b\", \"./c\", \"./d\", \"./e\"},\n\t\t\tresultPaths,\n\t\t)\n\t})\n}\n"
  },
  {
    "path": "internal/filter/examples_test.go",
    "content": "package filter_test\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"sort\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n)\n\n// exampleLogger creates a logger for examples with a proper formatter.\nfunc exampleLogger() log.Logger {\n\tformatter := format.NewFormatter(format.NewKeyValueFormatPlaceholders())\n\tformatter.SetDisabledColors(true)\n\n\treturn log.New(log.WithFormatter(formatter))\n}\n\n// Example_basicPathFilter demonstrates filtering components by path with a glob pattern.\nfunc Example_basicPathFilter() {\n\tcomponents := []component.Component{\n\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./apps/app2\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./libs/db\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t}\n\n\tl := log.New()\n\tresult, _ := filter.Apply(l, \"./apps/*\", components)\n\n\tfor _, c := range result {\n\t\tfmt.Println(filepath.Base(c.Path()))\n\t}\n\t// Output:\n\t// app1\n\t// app2\n}\n\n// Example_attributeFilter demonstrates filtering components by name attribute.\nfunc Example_attributeFilter() {\n\tcomponents := []component.Component{\n\t\tcomponent.NewUnit(\"./apps/frontend\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./apps/backend\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./services/api\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t}\n\n\tl := log.New()\n\tresult, _ := filter.Apply(l, \"name=api\", components)\n\n\tfor _, c := range result {\n\t\tfmt.Println(c.Path())\n\t}\n\t// Output:\n\t// ./services/api\n}\n\n// Example_exclusionFilter demonstrates excluding components using the negation operator.\nfunc Example_exclusionFilter() {\n\tcomponents := []component.Component{\n\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./apps/app2\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./apps/legacy\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t}\n\n\tl := log.New()\n\tresult, _ := filter.Apply(l, \"!legacy\", components)\n\n\tfor _, c := range result {\n\t\tfmt.Println(filepath.Base(c.Path()))\n\t}\n\t// Output:\n\t// app1\n\t// app2\n}\n\n// Example_intersectionFilter demonstrates refining results with the intersection operator.\nfunc Example_intersectionFilter() {\n\tcomponents := []component.Component{\n\t\tcomponent.NewUnit(\"./apps/frontend\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./apps/backend\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./libs/db\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./libs/api\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t}\n\n\t// Select components in ./apps/ that are named \"frontend\"\n\tl := log.New()\n\tresult, _ := filter.Apply(l, \"./apps/* | frontend\", components)\n\n\tfor _, c := range result {\n\t\tfmt.Println(filepath.Base(c.Path()))\n\t}\n\t// Output:\n\t// frontend\n}\n\n// Example_complexQuery demonstrates a complex filter combining paths and negation.\nfunc Example_complexQuery() {\n\tcomponents := []component.Component{\n\t\tcomponent.NewUnit(\"./services/web\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./services/worker\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./libs/db\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./libs/api\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./libs/cache\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t}\n\n\t// Select all services except worker\n\tl := log.New()\n\tresult, _ := filter.Apply(l, \"./services/* | !worker\", components)\n\n\tfor _, c := range result {\n\t\tfmt.Println(filepath.Base(c.Path()))\n\t}\n\t// Output:\n\t// web\n}\n\n// Example_parseAndEvaluate demonstrates the two-step process of parsing and evaluating.\nfunc Example_parseAndEvaluate() {\n\tcomponents := []component.Component{\n\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./apps/app2\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t}\n\n\t// Parse the filter once\n\tf, err := filter.Parse(\"app1\")\n\tif err != nil {\n\t\tfmt.Println(\"Parse error:\", err)\n\t\treturn\n\t}\n\n\t// Evaluate multiple times with different config sets\n\tl := log.New()\n\tresult1, _ := f.Evaluate(l, components)\n\tfmt.Printf(\"Found %d components\\n\", len(result1))\n\n\t// You can also inspect the original query\n\tfmt.Printf(\"Original query: %s\\n\", f.String())\n\n\t// Output:\n\t// Found 1 components\n\t// Original query: app1\n}\n\n// Example_recursiveWildcard demonstrates using recursive wildcards to match nested paths.\nfunc Example_recursiveWildcard() {\n\tcomponents := []component.Component{\n\t\tcomponent.NewUnit(\"./infrastructure/networking/vpc\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./infrastructure/networking/subnets\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./infrastructure/compute/app-server\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t}\n\n\t// Match all infrastructure components at any depth\n\tl := log.New()\n\tresult, _ := filter.Apply(l, \"./infrastructure/**\", components)\n\n\tfor _, c := range result {\n\t\tfmt.Println(filepath.Base(c.Path()))\n\t}\n\t// Output:\n\t// vpc\n\t// subnets\n\t// app-server\n}\n\n// Example_errorHandling demonstrates handling parsing errors.\nfunc Example_errorHandling() {\n\t// Invalid syntax - missing value after =\n\t_, err := filter.Parse(\"name=\")\n\tif err != nil {\n\t\tfmt.Println(\"Error occurred\")\n\t}\n\n\t// Valid syntax\n\t_, err = filter.Parse(\"name=foo\")\n\tif err == nil {\n\t\tfmt.Println(\"Successfully parsed\")\n\t}\n\n\t// Output:\n\t// Error occurred\n\t// Successfully parsed\n}\n\n// Example_multipleFilters demonstrates using multiple filters with union semantics.\nfunc Example_multipleFilters() {\n\tcomponents := []component.Component{\n\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./apps/app2\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./libs/db\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t\tcomponent.NewUnit(\"./libs/api\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t}),\n\t}\n\n\tl := exampleLogger()\n\n\t// Parse multiple filters - results are unioned\n\tfilters, _ := filter.ParseFilterQueries(l, []string{\n\t\t\"./apps/*\",\n\t\t\"name=db\",\n\t})\n\tresult, _ := filters.Evaluate(l, components)\n\n\t// Sort for consistent output\n\tnames := make([]string, len(result))\n\tfor i, c := range result {\n\t\tnames[i] = filepath.Base(c.Path())\n\t}\n\n\tsort.Strings(names)\n\n\tfor _, name := range names {\n\t\tfmt.Println(name)\n\t}\n\t// Output:\n\t// app1\n\t// app2\n\t// db\n}\n"
  },
  {
    "path": "internal/filter/filter.go",
    "content": "package filter\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// Filter represents a parsed filter query that can be evaluated against discovered configs.\ntype Filter struct {\n\texpr          Expression\n\toriginalQuery string\n}\n\n// Parse parses a filter query string and returns a Filter object.\n// Returns an error if the query cannot be parsed.\nfunc Parse(filterString string) (*Filter, error) {\n\tlexer := NewLexer(filterString)\n\tparser := NewParser(lexer)\n\n\texpr, err := parser.ParseExpression()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Filter{\n\t\texpr:          expr,\n\t\toriginalQuery: filterString,\n\t}, nil\n}\n\n// NewFilter creates a new Filter object.\nfunc NewFilter(expr Expression, originalQuery string) *Filter {\n\treturn &Filter{expr: expr, originalQuery: originalQuery}\n}\n\n// String returns a string representation of the filter.\nfunc (f *Filter) String() string {\n\treturn f.originalQuery\n}\n\n// Evaluate applies the filter to a list of components and returns the filtered result.\n// If logger is provided, it will be used for logging warnings during evaluation.\nfunc (f *Filter) Evaluate(l log.Logger, components component.Components) (component.Components, error) {\n\treturn Evaluate(l, f.expr, components)\n}\n\n// Expression returns the parsed AST expression.\n// This is useful for debugging or advanced use cases.\nfunc (f *Filter) Expression() Expression {\n\treturn f.expr\n}\n\n// RequiresParse returns true if the filter requires parsing of Terragrunt HCL configurations.\nfunc (f *Filter) RequiresParse() (Expression, bool) {\n\treturn f.expr.RequiresParse()\n}\n\n// Negated returns the equivalent filter with negation flipped.\n//\n// If the filter is already negated, it will return the non-negated filter.\nfunc (f *Filter) Negated() *Filter {\n\tswitch node := f.expr.(type) {\n\tcase *PrefixExpression:\n\t\treturn NewFilter(node.Right, f.originalQuery)\n\tcase *InfixExpression:\n\t\treturn NewFilter(\n\t\t\tNewInfixExpression(\n\t\t\t\tnode.Left.Negated(),\n\t\t\t\tnode.Operator,\n\t\t\t\tnode.Right,\n\t\t\t),\n\t\t\tf.originalQuery,\n\t\t)\n\tdefault:\n\t\treturn f\n\t}\n}\n\n// Apply is a convenience function that parses and evaluates a filter in one step.\n// It's equivalent to calling Parse followed by Evaluate.\nfunc Apply(l log.Logger, filterString string, components component.Components) (component.Components, error) {\n\tfilter, err := Parse(filterString)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn filter.Evaluate(l, components)\n}\n"
  },
  {
    "path": "internal/filter/filter_test.go",
    "content": "package filter_test\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar testComponents = []component.Component{\n\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\tWorkingDir: \".\",\n\t}),\n\tcomponent.NewUnit(\"./apps/app2\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\tWorkingDir: \".\",\n\t}),\n\tcomponent.NewUnit(\"./apps/legacy\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\tWorkingDir: \".\",\n\t}),\n\tcomponent.NewUnit(\"./libs/db\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\tWorkingDir: \".\",\n\t}),\n\tcomponent.NewUnit(\"./libs/api\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\tWorkingDir: \".\",\n\t}),\n\tcomponent.NewUnit(\"./services/web\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\tWorkingDir: \".\",\n\t}),\n\tcomponent.NewUnit(\"./services/worker\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\tWorkingDir: \".\",\n\t}),\n}\n\nfunc TestFilter_ParseAndEvaluate(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname         string\n\t\tfilterString string\n\t\texpected     component.Components\n\t\texpectError  bool\n\t}{\n\t\t{\n\t\t\tname:         \"simple name filter\",\n\t\t\tfilterString: \"app1\",\n\t\t\texpected: component.Components{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:         \"attribute filter\",\n\t\t\tfilterString: \"name=db\",\n\t\t\texpected: component.Components{\n\t\t\t\tcomponent.NewUnit(\"./libs/db\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:         \"path filter with wildcard\",\n\t\t\tfilterString: \"./apps/*\",\n\t\t\texpected: component.Components{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./apps/app2\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./apps/legacy\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:         \"negated filter\",\n\t\t\tfilterString: \"!legacy\",\n\t\t\texpected: component.Components{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./apps/app2\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./libs/db\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./libs/api\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./services/web\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./services/worker\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:         \"intersection of path and name\",\n\t\t\tfilterString: \"./apps/* | app1\",\n\t\t\texpected: component.Components{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:         \"intersection with negation\",\n\t\t\tfilterString: \"./apps/* | !legacy\",\n\t\t\texpected: component.Components{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./apps/app2\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:         \"chained intersections\",\n\t\t\tfilterString: \"./apps/* | !legacy | app1\",\n\t\t\texpected: component.Components{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:         \"recursive wildcard\",\n\t\t\tfilterString: \"./services/**\",\n\t\t\texpected: component.Components{\n\t\t\t\tcomponent.NewUnit(\"./services/web\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./services/worker\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:         \"parse error - empty\",\n\t\t\tfilterString: \"\",\n\t\t\texpected:     nil,\n\t\t\texpectError:  true,\n\t\t},\n\t\t{\n\t\t\tname:         \"parse error - invalid syntax\",\n\t\t\tfilterString: \"foo |\",\n\t\t\texpected:     nil,\n\t\t\texpectError:  true,\n\t\t},\n\t\t{\n\t\t\tname:         \"parse error - incomplete expression\",\n\t\t\tfilterString: \"name=\",\n\t\t\texpected:     nil,\n\t\t\texpectError:  true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tfilter, err := filter.Parse(tt.filterString)\n\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Nil(t, filter)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.NotNil(t, filter)\n\n\t\t\tlogger := log.New()\n\t\t\tresult, err := filter.Evaluate(logger, testComponents)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.ElementsMatch(t, tt.expected, result)\n\n\t\t\t// Verify String() returns original query\n\t\t\tassert.Equal(t, tt.filterString, filter.String())\n\t\t})\n\t}\n}\n\nfunc TestFilter_Apply(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname         string\n\t\tfilterString string\n\t\tcomponents   component.Components\n\t\texpected     component.Components\n\t\texpectError  bool\n\t}{\n\t\t{\n\t\t\tname:         \"apply with simple filter\",\n\t\t\tfilterString: \"app1\",\n\t\t\tcomponents:   testComponents,\n\t\t\texpected: component.Components{\n\t\t\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:         \"apply with path filter\",\n\t\t\tfilterString: \"./libs/*\",\n\t\t\tcomponents:   testComponents,\n\t\t\texpected: component.Components{\n\t\t\t\tcomponent.NewUnit(\"./libs/db\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t\tcomponent.NewUnit(\"./libs/api\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\t\tWorkingDir: \".\",\n\t\t\t\t}),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:         \"apply with empty components\",\n\t\t\tfilterString: \"anything\",\n\t\t\tcomponents:   component.Components{},\n\t\t\texpected:     component.Components{},\n\t\t},\n\t\t{\n\t\t\tname:         \"apply with parse error\",\n\t\t\tfilterString: \"!\",\n\t\t\tcomponents:   testComponents,\n\t\t\texpected:     nil,\n\t\t\texpectError:  true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tl := log.New()\n\t\t\tresult, err := filter.Apply(l, tt.filterString, tt.components)\n\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Nil(t, result)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.ElementsMatch(t, tt.expected, result)\n\t\t})\n\t}\n}\n\nfunc TestFilter_Expression(t *testing.T) {\n\tt.Parallel()\n\n\tfilterString := \"name=foo\"\n\tf, err := filter.Parse(filterString)\n\trequire.NoError(t, err)\n\n\texpr := f.Expression()\n\tassert.NotNil(t, expr)\n\n\t// Verify it's the correct type\n\tattrFilter, ok := expr.(*filter.AttributeExpression)\n\tassert.True(t, ok)\n\tassert.Equal(t, \"name\", attrFilter.Key)\n\tassert.Equal(t, \"foo\", attrFilter.Value)\n}\n\nfunc TestFilter_RealWorldScenarios(t *testing.T) {\n\tt.Parallel()\n\n\trepoComponents := []component.Component{\n\t\tcomponent.NewUnit(\"./infrastructure/networking/vpc\"),\n\t\tcomponent.NewUnit(\"./infrastructure/networking/subnets\"),\n\t\tcomponent.NewUnit(\"./infrastructure/networking/security-groups\"),\n\t\tcomponent.NewUnit(\"./infrastructure/compute/app-server\"),\n\t\tcomponent.NewUnit(\"./infrastructure/compute/db-server\"),\n\t\tcomponent.NewUnit(\"./apps/frontend\"),\n\t\tcomponent.NewUnit(\"./apps/backend\"),\n\t\tcomponent.NewUnit(\"./apps/api\"),\n\t\tcomponent.NewUnit(\"./test/test-app\"),\n\t}\n\n\tfor _, c := range repoComponents {\n\t\tc.SetDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t})\n\t}\n\n\ttests := []struct {\n\t\tname         string\n\t\tfilterString string\n\t\tdescription  string\n\t\texpected     []string\n\t}{\n\t\t{\n\t\t\tname:         \"all networking infrastructure\",\n\t\t\tfilterString: \"./infrastructure/networking/*\",\n\t\t\tdescription:  \"Select all networking-related units\",\n\t\t\texpected:     []string{\"vpc\", \"subnets\", \"security-groups\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"apps excluding test-app\",\n\t\t\tfilterString: \"./apps/* | !test-app\",\n\t\t\tdescription:  \"Select all apps except test-app\",\n\t\t\texpected:     []string{\"frontend\", \"backend\", \"api\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"compute infrastructure excluding db-server\",\n\t\t\tfilterString: \"./infrastructure/compute/* | !db-server\",\n\t\t\tdescription:  \"Select compute infrastructure except db-server\",\n\t\t\texpected:     []string{\"app-server\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"everything in infrastructure\",\n\t\t\tfilterString: \"./infrastructure/**\",\n\t\t\tdescription:  \"Select all infrastructure units recursively\",\n\t\t\texpected:     []string{\"vpc\", \"subnets\", \"security-groups\", \"app-server\", \"db-server\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"exclude specific unit\",\n\t\t\tfilterString: \"!test-app\",\n\t\t\tdescription:  \"Exclude test-app from all units\",\n\t\t\texpected:     []string{\"vpc\", \"subnets\", \"security-groups\", \"app-server\", \"db-server\", \"frontend\", \"backend\", \"api\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tl := log.New()\n\t\t\tresult, err := filter.Apply(l, tt.filterString, repoComponents)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tresultNames := make([]string, 0, len(result))\n\t\t\tfor _, c := range result {\n\t\t\t\tresultNames = append(resultNames, filepath.Base(c.Path()))\n\t\t\t}\n\n\t\t\tassert.ElementsMatch(t, tt.expected, resultNames, tt.description)\n\t\t})\n\t}\n}\n\nfunc TestFilter_EdgeCasesAndErrorHandling(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"filter with no matches\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tl := log.New()\n\t\tresult, err := filter.Apply(l, \"nonexistent\", testComponents)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Empty(t, result)\n\t})\n\n\tt.Run(\"multiple parse and evaluate calls\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilter, err := filter.Parse(\"app1\")\n\t\trequire.NoError(t, err)\n\n\t\tl := log.New()\n\n\t\tresult1, err := filter.Evaluate(l, testComponents)\n\t\trequire.NoError(t, err)\n\n\t\tresult2, err := filter.Evaluate(l, testComponents)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, result1, result2)\n\t})\n\n\tt.Run(\"whitespace handling\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttests := []struct {\n\t\t\tfilterString string\n\t\t}{\n\t\t\t{\"./apps/* |   !legacy\"},\n\t\t\t{\"  ./apps/*  |  !legacy  \"},\n\t\t\t{\"./apps/* | !legacy\"},\n\t\t}\n\n\t\texpected := component.Components{\n\t\t\tcomponent.NewUnit(\"./apps/app1\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\tWorkingDir: \".\",\n\t\t\t}),\n\t\t\tcomponent.NewUnit(\"./apps/app2\").WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\tWorkingDir: \".\",\n\t\t\t}),\n\t\t}\n\n\t\tfor _, tt := range tests {\n\t\t\tl := log.New()\n\t\t\tresult, err := filter.Apply(l, tt.filterString, testComponents)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.ElementsMatch(t, expected, result)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/filter/filters.go",
    "content": "package filter\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// Filters represents multiple filter queries that are evaluated with union (OR) semantics.\n// Multiple filters in Filters are always unioned (as opposed to multiple filters\n// within one filter string separated by |, which are intersected).\ntype Filters []*Filter\n\n// ParseFilterQueries parses multiple filter strings and returns a Filters object.\n// Collects all parse errors and returns them as a joined error if any occur.\n// Returns an empty Filters if filterStrings is empty.\n// Color output for diagnostics is determined by the logger's color settings and terminal detection.\nfunc ParseFilterQueries(l log.Logger, filterStrings []string) (Filters, error) {\n\tif len(filterStrings) == 0 {\n\t\treturn Filters{}, nil\n\t}\n\n\t// Determine if we should use color based on logger settings and terminal detection.\n\t// Error output goes to stderr, so we check if stderr is redirected.\n\tuseColor := !l.Formatter().DisabledColors()\n\n\tfilters := make([]*Filter, 0, len(filterStrings))\n\n\tvar diagnostics []string\n\n\tfor i, filterString := range filterStrings {\n\t\tfilter, err := Parse(filterString)\n\t\tif err != nil {\n\t\t\tvar parseErr ParseError\n\t\t\tif errors.As(err, &parseErr) {\n\t\t\t\tdiagnostics = append(diagnostics, FormatDiagnostic(&parseErr, i, useColor))\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdiagnostics = append(diagnostics, fmt.Sprintf(\"filter %d: %v\", i, err))\n\n\t\t\tcontinue\n\t\t}\n\n\t\tfilters = append(filters, filter)\n\t}\n\n\tresult := Filters(filters)\n\n\tif len(diagnostics) > 0 {\n\t\treturn result, fmt.Errorf(\"%s\", strings.Join(diagnostics, \"\\n\"))\n\t}\n\n\treturn result, nil\n}\n\n// HasPositiveFilter returns true if the filters have any positive filters.\nfunc (f Filters) HasPositiveFilter() bool {\n\tfor _, filter := range f {\n\t\tif !IsNegated(filter.expr) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// RequiresDiscovery returns the first expression that requires discovery of Terragrunt components if any do.\nfunc (f Filters) RequiresDiscovery() (Expression, bool) {\n\tfor _, filter := range f {\n\t\tif e, ok := filter.expr.RequiresDiscovery(); ok {\n\t\t\treturn e, true\n\t\t}\n\t}\n\n\treturn nil, false\n}\n\n// RequiresParse returns the first expression that requires parsing of Terragrunt HCL configurations if any do.\nfunc (f Filters) RequiresParse() (Expression, bool) {\n\tfor _, filter := range f {\n\t\tif e, ok := filter.RequiresParse(); ok {\n\t\t\treturn e, true\n\t\t}\n\t}\n\n\treturn nil, false\n}\n\n// DependencyGraphExpressions returns all target expressions from graph expressions that require dependency traversal.\nfunc (f Filters) DependencyGraphExpressions() []Expression {\n\ttargets := make([]Expression, 0, len(f))\n\n\tfor _, filter := range f {\n\t\ttargets = append(targets, collectGraphExpressionTargetsWithDependencies(filter.expr)...)\n\t}\n\n\treturn targets\n}\n\n// DependentGraphExpressions returns all target expressions from graph expressions that require dependent traversal.\nfunc (f Filters) DependentGraphExpressions() []Expression {\n\ttargets := make([]Expression, 0, len(f))\n\n\tfor _, filter := range f {\n\t\ttargets = append(targets, collectGraphExpressionTargetsWithDependents(filter.expr)...)\n\t}\n\n\treturn targets\n}\n\n// UniqueGitFilters returns all unique Git filters that require worktree discovery.\nfunc (f Filters) UniqueGitFilters() GitExpressions {\n\tvar targets GitExpressions\n\n\tseen := make(map[string]struct{})\n\n\tfor _, filter := range f {\n\t\tfilterWorktreeExpressions := collectWorktreeExpressions(filter.expr)\n\n\t\tfor _, filterWorktreeExpression := range filterWorktreeExpressions {\n\t\t\tif _, ok := seen[filterWorktreeExpression.String()]; ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tseen[filterWorktreeExpression.String()] = struct{}{}\n\t\t\ttargets = append(targets, filterWorktreeExpression)\n\t\t}\n\t}\n\n\treturn targets\n}\n\n// RestrictToStacks returns a new Filters object with only the filters that are restricted to stacks.\nfunc (f Filters) RestrictToStacks() Filters {\n\treturn slices.Collect(func(yield func(*Filter) bool) {\n\t\tfor _, filter := range f {\n\t\t\tif filter.expr.IsRestrictedToStacks() && !yield(filter) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t})\n}\n\n// collectGraphExpressionTargetsWithDependencies collects target expressions from GraphExpression nodes that have IncludeDependencies set.\nfunc collectGraphExpressionTargetsWithDependencies(expr Expression) []Expression {\n\tvar targets []Expression\n\n\tWalkExpressions(expr, func(e Expression) bool {\n\t\tif graphExpr, ok := e.(*GraphExpression); ok && graphExpr.IncludeDependencies {\n\t\t\ttargets = append(targets, graphExpr.Target)\n\t\t}\n\n\t\treturn true\n\t})\n\n\treturn targets\n}\n\n// collectGraphExpressionTargetsWithDependents collects target expressions from GraphExpression nodes that have IncludeDependents set.\nfunc collectGraphExpressionTargetsWithDependents(expr Expression) []Expression {\n\tvar targets []Expression\n\n\tWalkExpressions(expr, func(e Expression) bool {\n\t\tif graphExpr, ok := e.(*GraphExpression); ok && graphExpr.IncludeDependents {\n\t\t\ttargets = append(targets, graphExpr.Target)\n\t\t}\n\n\t\treturn true\n\t})\n\n\treturn targets\n}\n\n// collectWorktreeExpressions collects worktree expressions from GitExpression nodes.\nfunc collectWorktreeExpressions(expr Expression) []*GitExpression {\n\tvar targets []*GitExpression\n\n\tWalkExpressions(expr, func(e Expression) bool {\n\t\tif gitExpr, ok := e.(*GitExpression); ok {\n\t\t\ttargets = append(targets, gitExpr)\n\t\t}\n\n\t\treturn true\n\t})\n\n\treturn targets\n}\n\n// collectGitReferences collects Git references from GitExpression nodes.\nfunc collectGitReferences(expr Expression) []string {\n\tvar refs []string\n\n\tWalkExpressions(expr, func(e Expression) bool {\n\t\tif gitExpr, ok := e.(*GitExpression); ok {\n\t\t\trefs = append(refs, gitExpr.FromRef, gitExpr.ToRef)\n\t\t}\n\n\t\treturn true\n\t})\n\n\treturn refs\n}\n\n// Evaluate applies all filters with union (OR) semantics in two phases:\n//  1. Positive filters (non-negated) are evaluated and their results are unioned\n//  2. Negative filters (starting with negation) are evaluated against the combined\n//     results and remove matching components\n//\n// If logger is provided, it will be used for logging warnings during evaluation.\nfunc (f Filters) Evaluate(l log.Logger, components component.Components) (component.Components, error) {\n\tif len(f) == 0 {\n\t\treturn components, nil\n\t}\n\n\tvar (\n\t\tpositiveFilters = make([]*Filter, 0, len(f))\n\t\tnegativeFilters = make([]*Filter, 0, len(f))\n\t)\n\n\tfor _, filter := range f {\n\t\tif IsNegated(filter.expr) {\n\t\t\tnegativeFilters = append(negativeFilters, filter)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tpositiveFilters = append(positiveFilters, filter)\n\t}\n\n\t// Phase 1: Get initial set of components, which might need to be filtered further by negative filters\n\tcombined, err := initialComponents(l, positiveFilters, components)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(negativeFilters) == 0 {\n\t\treturn combined, nil\n\t}\n\n\t// Phase 2: Apply negative filters to find components to remove\n\ttoRemove := make(component.Components, 0, len(combined))\n\n\tfor _, filter := range negativeFilters {\n\t\tremoved, err := filter.Negated().Evaluate(l, combined)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor _, c := range removed {\n\t\t\tif !slices.Contains(toRemove, c) {\n\t\t\t\ttoRemove = append(toRemove, c)\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(toRemove) == 0 {\n\t\treturn combined, nil\n\t}\n\n\t// Phase 3: Remove components from the initial set\n\n\t// We don't use slices.DeleteFunc here because we don't want the members of the original components slice to be\n\t// zeroed.\n\tresults := make(component.Components, 0, len(combined)-len(toRemove))\n\tfor _, c := range combined {\n\t\tif slices.Contains(toRemove, c) {\n\t\t\tcontinue\n\t\t}\n\n\t\tresults = append(results, c)\n\t}\n\n\treturn results, nil\n}\n\n// EvaluateOnFiles evaluates the filters on a list of files and returns the filtered result.\n// This is useful for the hcl format command, where we want to evaluate filters on files\n// rather than directories, like we do with components.\nfunc (f Filters) EvaluateOnFiles(l log.Logger, files []string, workingDir string) (component.Components, error) {\n\tif e, ok := f.RequiresDiscovery(); ok {\n\t\treturn nil, FilterQueryRequiresDiscoveryError{Query: e.String()}\n\t}\n\n\tcomps := make(component.Components, 0, len(files))\n\tfor _, file := range files {\n\t\tunit := component.NewUnit(file)\n\t\tif workingDir != \"\" {\n\t\t\tunit = unit.WithDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\tWorkingDir: workingDir,\n\t\t\t})\n\t\t}\n\n\t\tcomps = append(comps, unit)\n\t}\n\n\tif len(f) == 0 {\n\t\treturn comps, nil\n\t}\n\n\treturn f.Evaluate(l, comps)\n}\n\nfunc initialComponents(l log.Logger, positiveFilters []*Filter, components component.Components) (component.Components, error) {\n\tif len(positiveFilters) == 0 {\n\t\treturn components, nil\n\t}\n\n\tseen := make(map[string]component.Component, len(components))\n\n\tfor _, filter := range positiveFilters {\n\t\tresult, err := filter.Evaluate(l, components)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor _, c := range result {\n\t\t\tseen[c.Path()] = c\n\t\t}\n\t}\n\n\tremaining := make(component.Components, 0, len(seen))\n\tfor _, c := range seen {\n\t\tremaining = append(remaining, c)\n\t}\n\n\treturn remaining, nil\n}\n\n// String returns a JSON array representation of all filter strings.\nfunc (f Filters) String() string {\n\tfilterStrings := make([]string, len(f))\n\tfor i, filter := range f {\n\t\tfilterStrings[i] = filter.String()\n\t}\n\n\tjsonBytes, err := json.Marshal(filterStrings)\n\tif err != nil {\n\t\treturn \"[]\"\n\t}\n\n\treturn string(jsonBytes)\n}\n"
  },
  {
    "path": "internal/filter/filters_test.go",
    "content": "package filter_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// testLogger creates a logger for tests with colors disabled.\nfunc testLogger() log.Logger {\n\tformatter := format.NewFormatter(format.NewKeyValueFormatPlaceholders())\n\tformatter.SetDisabledColors(true)\n\n\treturn log.New(log.WithLevel(log.DebugLevel), log.WithFormatter(formatter))\n}\n\nfunc TestFilters_ParseFilterQueries(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"empty filter list\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{})\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, filters)\n\t\tassert.Equal(t, \"[]\", filters.String())\n\t})\n\n\tt.Run(\"single valid filter\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"./apps/*\"})\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, filters)\n\t\tassert.Equal(t, `[\"./apps/*\"]`, filters.String())\n\t})\n\n\tt.Run(\"multiple valid filters\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"./apps/*\", \"name=db\", \"!legacy\"})\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, filters)\n\t\tassert.Equal(t, `[\"./apps/*\",\"name=db\",\"!legacy\"]`, filters.String())\n\t})\n\n\tt.Run(\"single invalid filter\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"invalid |\"})\n\t\trequire.Error(t, err)\n\t\tassert.NotNil(t, filters)\n\t\t// Rich diagnostic format\n\t\tassert.Contains(t, err.Error(), \"--filter\")\n\t\tassert.Contains(t, err.Error(), \"invalid |\")\n\t})\n\n\tt.Run(\"mixed valid and invalid filters\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"./apps/*\", \"name=\", \"!legacy\"})\n\t\trequire.Error(t, err)\n\t\tassert.NotNil(t, filters)\n\t\t// Should have 2 valid filters parsed\n\t\tassert.Contains(t, filters.String(), \"./apps/*\")\n\t\tassert.Contains(t, filters.String(), \"!legacy\")\n\t\t// Error should mention the invalid filter with rich diagnostic format\n\t\tassert.Contains(t, err.Error(), \"--filter[1]\")\n\t\tassert.Contains(t, err.Error(), \"name=\")\n\t})\n\n\tt.Run(\"multiple invalid filters\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"foo |\", \"bar |\", \"!baz\"})\n\t\trequire.Error(t, err)\n\t\tassert.NotNil(t, filters)\n\t\t// Should have 1 valid filter\n\t\tassert.Equal(t, `[\"!baz\"]`, filters.String())\n\t\t// Error should mention both invalid filters with rich diagnostic format\n\t\tassert.Contains(t, err.Error(), \"--filter 'foo |'\")\n\t\tassert.Contains(t, err.Error(), \"--filter[1]\")\n\t})\n\n\tt.Run(\"filter in parent directory\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"../apps/*\"})\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, filters)\n\t\tassert.Equal(t, `[\"../apps/*\"]`, filters.String())\n\t})\n\n\tt.Run(\"name filter with slash\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"app/1\"})\n\t\trequire.NoError(t, err)\n\t\tassert.NotNil(t, filters)\n\t\tassert.Equal(t, `[\"app/1\"]`, filters.String())\n\t})\n}\n\nfunc TestFilters_Evaluate(t *testing.T) {\n\tt.Parallel()\n\n\tcomponents := component.Components{\n\t\tcomponent.NewUnit(\"./apps/app1\"),\n\t\tcomponent.NewUnit(\"./apps/app2\"),\n\t\tcomponent.NewUnit(\"./apps/legacy\"),\n\t\tcomponent.NewUnit(\"./libs/db\"),\n\t\tcomponent.NewUnit(\"./libs/api\"),\n\t}\n\n\tfor _, c := range components {\n\t\tc.SetDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: \".\",\n\t\t})\n\t}\n\n\tt.Run(\"empty filters returns all components\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{})\n\t\trequire.NoError(t, err)\n\n\t\tl := log.New()\n\t\tresult, err := filters.Evaluate(l, components)\n\t\trequire.NoError(t, err)\n\t\tassert.ElementsMatch(t, components, result)\n\t})\n\n\tt.Run(\"single positive filter\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"./apps/*\"})\n\t\trequire.NoError(t, err)\n\n\t\tl := log.New()\n\t\tresult, err := filters.Evaluate(l, components)\n\t\trequire.NoError(t, err)\n\n\t\texpected := component.Components{\n\t\t\tcomponent.NewUnit(\"./apps/app1\"),\n\t\t\tcomponent.NewUnit(\"./apps/app2\"),\n\t\t\tcomponent.NewUnit(\"./apps/legacy\"),\n\t\t}\n\n\t\tfor _, c := range expected {\n\t\t\tc.SetDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\tWorkingDir: \".\",\n\t\t\t})\n\t\t}\n\n\t\tassert.ElementsMatch(t, expected, result)\n\t})\n\n\tt.Run(\"union of multiple positive filters\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"./apps/app1\", \"name=db\"})\n\t\trequire.NoError(t, err)\n\n\t\tl := log.New()\n\t\tresult, err := filters.Evaluate(l, components)\n\t\trequire.NoError(t, err)\n\n\t\texpected := component.Components{\n\t\t\tcomponent.NewUnit(\"./apps/app1\"),\n\t\t\tcomponent.NewUnit(\"./libs/db\"),\n\t\t}\n\n\t\tfor _, c := range expected {\n\t\t\tc.SetDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\tWorkingDir: \".\",\n\t\t\t})\n\t\t}\n\n\t\tassert.ElementsMatch(t, expected, result)\n\t})\n\n\tt.Run(\"union with overlapping results (deduplication)\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"./apps/*\", \"name=app1\"})\n\t\trequire.NoError(t, err)\n\n\t\tl := log.New()\n\t\tresult, err := filters.Evaluate(l, components)\n\t\trequire.NoError(t, err)\n\n\t\texpected := component.Components{\n\t\t\tcomponent.NewUnit(\"./apps/app1\"),\n\t\t\tcomponent.NewUnit(\"./apps/app2\"),\n\t\t\tcomponent.NewUnit(\"./apps/legacy\"),\n\t\t}\n\n\t\tfor _, c := range expected {\n\t\t\tc.SetDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\tWorkingDir: \".\",\n\t\t\t})\n\t\t}\n\n\t\tassert.ElementsMatch(t, expected, result)\n\t\t// Verify no duplicates - should have exactly 3 components\n\t\tassert.Len(t, result, 3)\n\t})\n\n\tt.Run(\"positive filters then negative filter removes results\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"./apps/*\", \"!legacy\"})\n\t\trequire.NoError(t, err)\n\n\t\tl := log.New()\n\t\tresult, err := filters.Evaluate(l, components)\n\t\trequire.NoError(t, err)\n\n\t\texpected := component.Components{\n\t\t\tcomponent.NewUnit(\"./apps/app1\"),\n\t\t\tcomponent.NewUnit(\"./apps/app2\"),\n\t\t}\n\n\t\tfor _, c := range expected {\n\t\t\tc.SetDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\tWorkingDir: \".\",\n\t\t\t})\n\t\t}\n\n\t\tassert.ElementsMatch(t, expected, result)\n\t})\n\n\tt.Run(\"multiple negative filters applied in sequence\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"./apps/*\", \"!legacy\", \"!app2\"})\n\t\trequire.NoError(t, err)\n\n\t\tl := log.New()\n\t\tresult, err := filters.Evaluate(l, components)\n\t\trequire.NoError(t, err)\n\n\t\texpected := component.Components{\n\t\t\tcomponent.NewUnit(\"./apps/app1\"),\n\t\t}\n\n\t\tfor _, c := range expected {\n\t\t\tc.SetDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\tWorkingDir: \".\",\n\t\t\t})\n\t\t}\n\n\t\tassert.ElementsMatch(t, expected, result)\n\t})\n\n\tt.Run(\"only negative filters\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"!legacy\", \"!db\"})\n\t\trequire.NoError(t, err)\n\n\t\tl := log.New()\n\t\tresult, err := filters.Evaluate(l, components)\n\t\trequire.NoError(t, err)\n\n\t\texpected := component.Components{\n\t\t\tcomponent.NewUnit(\"./apps/app1\"),\n\t\t\tcomponent.NewUnit(\"./apps/app2\"),\n\t\t\tcomponent.NewUnit(\"./libs/api\"),\n\t\t}\n\n\t\tfor _, c := range expected {\n\t\t\tc.SetDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\tWorkingDir: \".\",\n\t\t\t})\n\t\t}\n\n\t\tassert.ElementsMatch(t, expected, result)\n\t})\n\n\tt.Run(\"complex mix of positive and negative filters\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\n\t\t\t\"./apps/*\",\n\t\t\t\"./libs/*\",\n\t\t\t\"!legacy\",\n\t\t\t\"!api\",\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tl := log.New()\n\t\tresult, err := filters.Evaluate(l, components)\n\t\trequire.NoError(t, err)\n\n\t\texpected := component.Components{\n\t\t\tcomponent.NewUnit(\"./apps/app1\"),\n\t\t\tcomponent.NewUnit(\"./apps/app2\"),\n\t\t\tcomponent.NewUnit(\"./libs/db\"),\n\t\t}\n\n\t\tfor _, c := range expected {\n\t\t\tc.SetDiscoveryContext(&component.DiscoveryContext{\n\t\t\t\tWorkingDir: \".\",\n\t\t\t})\n\t\t}\n\n\t\tassert.ElementsMatch(t, expected, result)\n\t})\n}\n\nfunc TestFilters_HasPositiveFilter(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"empty filters - has positive filter is false\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{})\n\t\trequire.NoError(t, err)\n\t\tassert.False(t, filters.HasPositiveFilter())\n\t})\n\n\tt.Run(\"single positive filter - has positive filter is true\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"./apps/*\"})\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, filters.HasPositiveFilter())\n\t})\n\n\tt.Run(\"single negative filter - has positive filter is false\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"!legacy\"})\n\t\trequire.NoError(t, err)\n\t\tassert.False(t, filters.HasPositiveFilter())\n\t})\n\n\tt.Run(\"multiple negative filters - has positive filter is false\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"!legacy\", \"!test\"})\n\t\trequire.NoError(t, err)\n\t\tassert.False(t, filters.HasPositiveFilter())\n\t})\n\n\tt.Run(\"multiple positive filters - has positive filter is true\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"./apps/*\", \"./libs/*\"})\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, filters.HasPositiveFilter())\n\t})\n\n\tt.Run(\"mixed positive and negative - has positive filter is true\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"./apps/*\", \"!legacy\"})\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, filters.HasPositiveFilter())\n\t})\n\n\tt.Run(\"negative then positive - has positive filter is true\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"!legacy\", \"./apps/*\"})\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, filters.HasPositiveFilter())\n\t})\n}\n\nfunc TestFilters_String(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"empty filters\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{})\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"[]\", filters.String())\n\t})\n\n\tt.Run(\"single filter\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"./apps/*\"})\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, `[\"./apps/*\"]`, filters.String())\n\t})\n\n\tt.Run(\"multiple filters\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"./apps/*\", \"name=db\", \"!legacy\"})\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, `[\"./apps/*\",\"name=db\",\"!legacy\"]`, filters.String())\n\t})\n}\n\nfunc TestFilters_RequiresDependencyDiscovery(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"no graph expressions - empty result\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"./apps/*\", \"name=db\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependencyGraphExpressions()\n\t\tassert.Empty(t, targets)\n\t})\n\n\tt.Run(\"single dependency graph expression\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"app...\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependencyGraphExpressions()\n\t\trequire.Len(t, targets, 1)\n\n\t\t// Verify the target is the correct expression\n\t\tassert.Equal(t, mustAttr(t, \"name\", \"app\"), targets[0])\n\t})\n\n\tt.Run(\"multiple dependency graph expressions\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"app...\", \"db...\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependencyGraphExpressions()\n\t\trequire.Len(t, targets, 2)\n\n\t\tassert.Equal(t, mustAttr(t, \"name\", \"app\"), targets[0])\n\t\tassert.Equal(t, mustAttr(t, \"name\", \"db\"), targets[1])\n\t})\n\n\tt.Run(\"dependent-only graph expression - no dependency discovery\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"...app\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependencyGraphExpressions()\n\t\tassert.Empty(t, targets)\n\t})\n\n\tt.Run(\"both directions graph expression - includes dependency discovery\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"...app...\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependencyGraphExpressions()\n\t\trequire.Len(t, targets, 1)\n\t\tassert.Equal(t, mustAttr(t, \"name\", \"app\"), targets[0])\n\t})\n\n\tt.Run(\"nested graph expressions in infix\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"app... | db...\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependencyGraphExpressions()\n\t\trequire.Len(t, targets, 2)\n\t})\n\n\tt.Run(\"graph expression in prefix expression\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"!app...\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependencyGraphExpressions()\n\t\trequire.Len(t, targets, 1)\n\t})\n\n\tt.Run(\"mixed graph and non-graph expressions\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"app...\", \"./apps/*\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependencyGraphExpressions()\n\t\trequire.Len(t, targets, 1)\n\t\tassert.Equal(t, mustAttr(t, \"name\", \"app\"), targets[0])\n\t})\n}\n\nfunc TestFilters_RequiresDependentDiscovery(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"no graph expressions - empty result\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"./apps/*\", \"name=db\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependentGraphExpressions()\n\t\tassert.Empty(t, targets)\n\t})\n\n\tt.Run(\"single dependent graph expression\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"...app\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependentGraphExpressions()\n\t\trequire.Len(t, targets, 1)\n\n\t\tassert.Equal(t, mustAttr(t, \"name\", \"app\"), targets[0])\n\t})\n\n\tt.Run(\"multiple dependent graph expressions\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"...app\", \"...db\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependentGraphExpressions()\n\t\trequire.Len(t, targets, 2)\n\n\t\tassert.Equal(t, mustAttr(t, \"name\", \"app\"), targets[0])\n\t\tassert.Equal(t, mustAttr(t, \"name\", \"db\"), targets[1])\n\t})\n\n\tt.Run(\"dependency-only graph expression - no dependent discovery\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"app...\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependentGraphExpressions()\n\t\tassert.Empty(t, targets)\n\t})\n\n\tt.Run(\"both directions graph expression - includes dependent discovery\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"...app...\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependentGraphExpressions()\n\t\trequire.Len(t, targets, 1)\n\t\tassert.Equal(t, mustAttr(t, \"name\", \"app\"), targets[0])\n\t})\n\n\tt.Run(\"nested graph expressions in infix\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"...app | ...db\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependentGraphExpressions()\n\t\trequire.Len(t, targets, 2)\n\t})\n\n\tt.Run(\"graph expression in prefix expression\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"!...app\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependentGraphExpressions()\n\t\trequire.Len(t, targets, 1)\n\t})\n\n\tt.Run(\"mixed graph and non-graph expressions\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"...app\", \"./apps/*\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependentGraphExpressions()\n\t\trequire.Len(t, targets, 1)\n\t\tassert.Equal(t, mustAttr(t, \"name\", \"app\"), targets[0])\n\t})\n}\n\nfunc TestFilters_RestrictToStacks(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"empty filters - empty result\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{})\n\t\trequire.NoError(t, err)\n\n\t\trestricted := filters.RestrictToStacks()\n\t\tassert.Empty(t, restricted)\n\t})\n\n\tt.Run(\"single filter - restricted to stacks\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"type=stack\"})\n\t\trequire.NoError(t, err)\n\n\t\trestricted := filters.RestrictToStacks()\n\t\trequire.Len(t, restricted, 1)\n\t})\n\n\tt.Run(\"multiple filters - one of them restricted to stacks\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"type=stack\", \"name=app\"})\n\t\trequire.NoError(t, err)\n\n\t\trestricted := filters.RestrictToStacks()\n\t\trequire.Len(t, restricted, 1)\n\t})\n\n\tt.Run(\"multiple filters - none of them restricted to stacks\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"name=app\", \"type=unit\"})\n\t\trequire.NoError(t, err)\n\n\t\trestricted := filters.RestrictToStacks()\n\t\trequire.Empty(t, restricted)\n\t})\n}\n\nfunc TestFilters_RequiresGitReferences(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"no Git filters - empty result\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"./apps/*\", \"name=db\"})\n\t\trequire.NoError(t, err)\n\n\t\trefs := filters.UniqueGitFilters().UniqueGitRefs()\n\t\tassert.Empty(t, refs)\n\t})\n\n\tt.Run(\"single Git filter with one reference\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"[main]\"})\n\t\trequire.NoError(t, err)\n\n\t\trefs := filters.UniqueGitFilters().UniqueGitRefs()\n\t\trequire.Len(t, refs, 2)\n\t\tassert.ElementsMatch(t, refs, []string{\"main\", \"HEAD\"})\n\t})\n\n\tt.Run(\"single Git filter with two references\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"[main...HEAD]\"})\n\t\trequire.NoError(t, err)\n\n\t\trefs := filters.UniqueGitFilters().UniqueGitRefs()\n\t\trequire.Len(t, refs, 2)\n\t\tassert.ElementsMatch(t, refs, []string{\"main\", \"HEAD\"})\n\t})\n\n\tt.Run(\"multiple Git filters\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"[main...HEAD]\", \"[feature-branch]\"})\n\t\trequire.NoError(t, err)\n\n\t\trefs := filters.UniqueGitFilters().UniqueGitRefs()\n\t\trequire.Len(t, refs, 3)\n\t\tassert.ElementsMatch(t, refs, []string{\"main\", \"HEAD\", \"feature-branch\"})\n\t})\n\n\tt.Run(\"Git filters with deduplication\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"[main...HEAD]\", \"[HEAD...main]\"})\n\t\trequire.NoError(t, err)\n\n\t\trefs := filters.UniqueGitFilters().UniqueGitRefs()\n\t\trequire.Len(t, refs, 2) // main and HEAD, no duplicates\n\t\tassert.ElementsMatch(t, refs, []string{\"main\", \"HEAD\"})\n\t})\n\n\tt.Run(\"Git filter combined with other filters\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"[main...HEAD]\", \"./apps/*\", \"name=db\"})\n\t\trequire.NoError(t, err)\n\n\t\trefs := filters.UniqueGitFilters().UniqueGitRefs()\n\t\trequire.Len(t, refs, 2)\n\t\tassert.ElementsMatch(t, refs, []string{\"main\", \"HEAD\"})\n\t})\n\n\tt.Run(\"Git filter with negation\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"![main...HEAD]\"})\n\t\trequire.NoError(t, err)\n\n\t\trefs := filters.UniqueGitFilters().UniqueGitRefs()\n\t\trequire.Len(t, refs, 2)\n\t\tassert.ElementsMatch(t, refs, []string{\"main\", \"HEAD\"})\n\t})\n\n\tt.Run(\"Git filter with intersection\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"[main...HEAD] | ./apps/*\"})\n\t\trequire.NoError(t, err)\n\n\t\trefs := filters.UniqueGitFilters().UniqueGitRefs()\n\t\trequire.Len(t, refs, 2)\n\t\tassert.ElementsMatch(t, refs, []string{\"main\", \"HEAD\"})\n\t})\n\n\tt.Run(\"Git filter nested in graph expression\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"[main...HEAD]...\"})\n\t\trequire.NoError(t, err)\n\n\t\trefs := filters.UniqueGitFilters().UniqueGitRefs()\n\t\trequire.Len(t, refs, 2)\n\t\tassert.ElementsMatch(t, refs, []string{\"main\", \"HEAD\"})\n\t})\n}\n\n// TestFilters_GitExpressionAsGraphTarget tests that Filters correctly extracts\n// GitExpression targets from GraphExpressions for dependency/dependent discovery.\nfunc TestFilters_GitExpressionAsGraphTarget(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"DependencyGraphExpressions extracts GitExpression target - dependencies only\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"[main...HEAD]...\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependencyGraphExpressions()\n\t\trequire.Len(t, targets, 1, \"Should have one dependency target expression\")\n\n\t\tgitExpr, ok := targets[0].(*filter.GitExpression)\n\t\trequire.True(t, ok, \"Target should be a GitExpression, got %T\", targets[0])\n\t\tassert.Equal(t, \"main\", gitExpr.FromRef)\n\t\tassert.Equal(t, \"HEAD\", gitExpr.ToRef)\n\t})\n\n\tt.Run(\"DependentGraphExpressions extracts GitExpression target - dependents only\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"...[main...HEAD]\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependentGraphExpressions()\n\t\trequire.Len(t, targets, 1, \"Should have one dependent target expression\")\n\n\t\tgitExpr, ok := targets[0].(*filter.GitExpression)\n\t\trequire.True(t, ok, \"Target should be a GitExpression, got %T\", targets[0])\n\t\tassert.Equal(t, \"main\", gitExpr.FromRef)\n\t\tassert.Equal(t, \"HEAD\", gitExpr.ToRef)\n\t})\n\n\tt.Run(\"Both graph expressions extract GitExpression target - issue #5307 pattern\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"...[main...HEAD]...\"})\n\t\trequire.NoError(t, err)\n\n\t\tdepTargets := filters.DependencyGraphExpressions()\n\t\trequire.Len(t, depTargets, 1, \"Should have one dependency target expression\")\n\n\t\tdepGitExpr, ok := depTargets[0].(*filter.GitExpression)\n\t\trequire.True(t, ok, \"Dependency target should be a GitExpression\")\n\t\tassert.Equal(t, \"main\", depGitExpr.FromRef)\n\t\tassert.Equal(t, \"HEAD\", depGitExpr.ToRef)\n\n\t\tdepentTargets := filters.DependentGraphExpressions()\n\t\trequire.Len(t, depentTargets, 1, \"Should have one dependent target expression\")\n\n\t\tdepentGitExpr, ok := depentTargets[0].(*filter.GitExpression)\n\t\trequire.True(t, ok, \"Dependent target should be a GitExpression\")\n\t\tassert.Equal(t, \"main\", depentGitExpr.FromRef)\n\t\tassert.Equal(t, \"HEAD\", depentGitExpr.ToRef)\n\t})\n\n\tt.Run(\"UniqueGitFilters extracts GitExpression from all graph positions\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"...[main...HEAD]...\"})\n\t\trequire.NoError(t, err)\n\n\t\tgitFilters := filters.UniqueGitFilters()\n\t\trequire.Len(t, gitFilters, 1, \"Should have one unique git filter\")\n\t\tassert.Equal(t, \"main\", gitFilters[0].FromRef)\n\t\tassert.Equal(t, \"HEAD\", gitFilters[0].ToRef)\n\t})\n\n\tt.Run(\"Multiple git expressions in graph - unique extraction\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\n\t\t\t\"[main...HEAD]...\",\n\t\t\t\"...[feature...develop]\",\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tdepTargets := filters.DependencyGraphExpressions()\n\t\trequire.Len(t, depTargets, 1, \"First filter has dependencies\")\n\n\t\tdepentTargets := filters.DependentGraphExpressions()\n\t\trequire.Len(t, depentTargets, 1, \"Second filter has dependents\")\n\n\t\tgitFilters := filters.UniqueGitFilters()\n\t\trequire.Len(t, gitFilters, 2, \"Should have two unique git filters\")\n\t})\n\n\tt.Run(\"Exclude target with GitExpression\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"^[main...HEAD]...\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependencyGraphExpressions()\n\t\trequire.Len(t, targets, 1)\n\n\t\tgitExpr, ok := targets[0].(*filter.GitExpression)\n\t\trequire.True(t, ok)\n\t\tassert.Equal(t, \"main\", gitExpr.FromRef)\n\t})\n\n\tt.Run(\"Single git ref with graph expression\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"[main]...\"})\n\t\trequire.NoError(t, err)\n\n\t\ttargets := filters.DependencyGraphExpressions()\n\t\trequire.Len(t, targets, 1)\n\n\t\tgitExpr, ok := targets[0].(*filter.GitExpression)\n\t\trequire.True(t, ok)\n\t\tassert.Equal(t, \"main\", gitExpr.FromRef)\n\t\tassert.Equal(t, \"HEAD\", gitExpr.ToRef)\n\t})\n\n\tt.Run(\"Git expression with commit SHA in graph\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"...[abc123...def456]...\"})\n\t\trequire.NoError(t, err)\n\n\t\tdepTargets := filters.DependencyGraphExpressions()\n\t\trequire.Len(t, depTargets, 1)\n\n\t\tgitExpr, ok := depTargets[0].(*filter.GitExpression)\n\t\trequire.True(t, ok)\n\t\tassert.Equal(t, \"abc123\", gitExpr.FromRef)\n\t\tassert.Equal(t, \"def456\", gitExpr.ToRef)\n\t})\n\n\tt.Run(\"RequiresParse returns true for git-graph expression\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"...[main...HEAD]...\"})\n\t\trequire.NoError(t, err)\n\n\t\texpr, requires := filters.RequiresParse()\n\t\tassert.True(t, requires, \"Git-graph expression should require parsing\")\n\t\tassert.NotNil(t, expr)\n\t})\n\n\tt.Run(\"HasPositiveFilter returns true for git-graph expression\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfilters, err := filter.ParseFilterQueries(testLogger(), []string{\"...[main...HEAD]...\"})\n\t\trequire.NoError(t, err)\n\n\t\tassert.True(t, filters.HasPositiveFilter(), \"Git-graph expression is a positive filter\")\n\t})\n}\n"
  },
  {
    "path": "internal/filter/fuzz_test.go",
    "content": "package filter_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n)\n\n// FuzzParse tests the main Parse() function with arbitrary input.\n// It verifies that Parse never panics regardless of input.\nfunc FuzzParse(f *testing.F) {\n\tseeds := []string{\n\t\t// Simple paths\n\t\t\"foo\",\n\t\t\"foo/bar\",\n\t\t\"**/*.hcl\",\n\t\t\"./apps\",\n\t\t\"./apps/*\",\n\t\t\"./apps/**/foo\",\n\t\t\"/absolute/path\",\n\t\t\"../foo\",\n\t\t\"./my-app_v2/foo-bar\",\n\n\t\t// Attributes\n\t\t\"name=foo\",\n\t\t\"name=bar\",\n\t\t\"type=unit\",\n\t\t\"key=value\",\n\t\t\"source=github.com/acme/foo/bar\",\n\n\t\t// Operators\n\t\t\"!foo\",\n\t\t\"foo | bar\",\n\t\t\"!name=bar\",\n\t\t\"!./apps/legacy\",\n\t\t\"./apps/* | name=bar\",\n\t\t\"name=foo | !./legacy | ./apps/**\",\n\n\t\t// Graph expressions\n\t\t\"...foo\",\n\t\t\"foo...\",\n\t\t\"...foo...\",\n\t\t\"^foo\",\n\t\t\"...^foo\",\n\t\t\"^foo...\",\n\t\t\"...^foo...\",\n\t\t\"1...foo\",\n\t\t\"foo...1\",\n\t\t\"2...foo...3\",\n\t\t\"1...^foo...2\",\n\n\t\t// Braced paths\n\t\t\"{./apps/*}\",\n\t\t\"{my path/file}\",\n\t\t\"!{./apps/legacy}\",\n\t\t\"{1}\",\n\n\t\t// Git refs\n\t\t\"[HEAD]\",\n\t\t\"[main]\",\n\t\t\"[main...HEAD]\",\n\t\t\"[main...feature]\",\n\t\t\"[abc123...def456]\",\n\t\t\"[v1.0.0...v2.0.0]\",\n\t\t\"[HEAD~1...HEAD]\",\n\t\t\"[feature/name]\",\n\n\t\t// Git + graph combinations\n\t\t\"[main...HEAD]...\",\n\t\t\"...[main...HEAD]\",\n\t\t\"...[main...HEAD]...\",\n\t\t\"...^[main...HEAD]...\",\n\n\t\t// Edge cases\n\t\t\"\",\n\t\t\".\",\n\t\t\"...\",\n\t\t\"{\",\n\t\t\"[\",\n\t\t\"!\",\n\t\t\"|\",\n\t\t\"=\",\n\t\t\"^\",\n\t\t\"}\",\n\t\t\"]\",\n\t\t\"[]\",\n\t\t\"{}\",\n\t\t\".gitignore\",\n\t\t\".terragrunt-cache\",\n\t\t\"@username\",\n\t\t\"foo bar\",\n\t\t\"   \\t\\n  \",\n\t\t\"..1\",\n\t\t\"..25\",\n\t\t\"foo..bar\",\n\t\t\"foo..1\",\n\t\t\"..2foo\",\n\t}\n\n\tfor _, seed := range seeds {\n\t\tf.Add(seed)\n\t}\n\n\tf.Fuzz(func(t *testing.T, input string) {\n\t\t_, _ = filter.Parse(input)\n\t})\n}\n\n// FuzzLexer tests the lexer by tokenizing arbitrary input.\n// It verifies that the lexer never panics and always terminates.\nfunc FuzzLexer(f *testing.F) {\n\tseeds := []string{\n\t\t// Single operators\n\t\t\"!\",\n\t\t\"|\",\n\t\t\"=\",\n\t\t\"{\",\n\t\t\"}\",\n\t\t\"[\",\n\t\t\"]\",\n\t\t\"^\",\n\t\t\"...\",\n\n\t\t// Identifiers\n\t\t\"foo\",\n\t\t\"foo_bar\",\n\t\t\"foo-bar\",\n\t\t\".gitignore\",\n\t\t\".terragrunt-cache\",\n\t\t\"@username\",\n\n\t\t// Paths\n\t\t\"./apps\",\n\t\t\"/absolute/path\",\n\t\t\"./apps/*\",\n\t\t\"./apps/**/foo\",\n\t\t\"../foo\",\n\t\t\"foo/bar\",\n\n\t\t// Complex sequences\n\t\t\"name=foo\",\n\t\t\"!name=bar\",\n\t\t\"./apps/* | name=bar\",\n\t\t\"name=foo | !./legacy | ./apps/**\",\n\t\t\"...foo\",\n\t\t\"foo...\",\n\t\t\"1...foo\",\n\t\t\"foo...1\",\n\t\t\"{./apps/*}\",\n\t\t\"[main...HEAD]\",\n\n\t\t// Edge cases\n\t\t\"\",\n\t\t\"   \\t\\n  \",\n\t\t\".\",\n\t\t\"..1\",\n\t\t\"..25\",\n\t\t\"foo..bar\",\n\t\t\"1...1\",\n\t\t\"99999999999999999999999...foo\",\n\t}\n\n\tfor _, seed := range seeds {\n\t\tf.Add(seed)\n\t}\n\n\tf.Fuzz(func(t *testing.T, input string) {\n\t\tlexer := filter.NewLexer(input)\n\n\t\t// Tokenize until EOF - should never hang or panic\n\t\t// Set a reasonable limit to prevent infinite loops in case of bugs\n\t\tconst maxTokens = 10000\n\t\tfor range maxTokens {\n\t\t\ttok := lexer.NextToken()\n\t\t\tif tok.Type == filter.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t})\n}\n\n// FuzzParser tests the parser by parsing arbitrary input.\n// It verifies that the parser never panics during AST construction.\nfunc FuzzParser(f *testing.F) {\n\tseeds := []string{\n\t\t// Simple expressions\n\t\t\"foo\",\n\t\t\"name=bar\",\n\t\t\"./apps/*\",\n\t\t\"{./apps/*}\",\n\t\t\"[main]\",\n\n\t\t// Prefix expressions\n\t\t\"!foo\",\n\t\t\"!name=bar\",\n\t\t\"!./apps/legacy\",\n\t\t\"!{./apps/legacy}\",\n\t\t\"![main...HEAD]\",\n\n\t\t// Infix expressions\n\t\t\"foo | bar\",\n\t\t\"foo | bar | baz\",\n\t\t\"./apps/* | name=bar\",\n\t\t\"name=foo | !./legacy | ./apps/**\",\n\t\t\"!foo | bar\",\n\t\t\"foo | !bar\",\n\n\t\t// Graph expressions\n\t\t\"...foo\",\n\t\t\"foo...\",\n\t\t\"...foo...\",\n\t\t\"^foo\",\n\t\t\"...^foo\",\n\t\t\"^foo...\",\n\t\t\"...^foo...\",\n\t\t\"1...foo\",\n\t\t\"foo...1\",\n\t\t\"2...foo...3\",\n\t\t\"1...^foo...2\",\n\t\t\"10...foo...25\",\n\t\t\"999999999...foo\",\n\n\t\t// Git expressions\n\t\t\"[main]\",\n\t\t\"[main...HEAD]\",\n\t\t\"[abc123...def456]\",\n\t\t\"[v1.0.0...v2.0.0]\",\n\t\t\"[HEAD~1...HEAD]\",\n\t\t\"[feature/name]\",\n\n\t\t// Combined expressions\n\t\t\"[main...HEAD]...\",\n\t\t\"...[main...HEAD]\",\n\t\t\"...[main...HEAD]...\",\n\t\t\"[main...HEAD] | ./apps/*\",\n\t\t\"!...foo\",\n\t\t\"...!foo\",\n\t\t\"...foo | bar\",\n\t\t\"foo | bar...\",\n\n\t\t// Error cases (parser should handle gracefully)\n\t\t\"\",\n\t\t\"!\",\n\t\t\"name=\",\n\t\t\"foo |\",\n\t\t\"foo | bar |\",\n\t\t\"|\",\n\t\t\"| foo\",\n\t\t\"[]\",\n\t\t\"[main\",\n\t\t\"[...]\",\n\t\t\"[main...]\",\n\t\t\"]\",\n\t\t\"{}\",\n\t\t\"{\",\n\t\t\"...\",\n\t\t\"^\",\n\t\t\"... |\",\n\t\t\"^ |\",\n\t\t\"1...\",\n\t\t\"1... \",\n\t\t\"1......2\",\n\t}\n\n\tfor _, seed := range seeds {\n\t\tf.Add(seed)\n\t}\n\n\tf.Fuzz(func(t *testing.T, input string) {\n\t\tlexer := filter.NewLexer(input)\n\t\tparser := filter.NewParser(lexer)\n\n\t\t// ParseExpression should not panic on any input\n\t\t// It may return an error for invalid input, which is expected\n\t\t_, _ = parser.ParseExpression()\n\t})\n}\n"
  },
  {
    "path": "internal/filter/hints.go",
    "content": "package filter\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// GetHint returns a single consolidated hint for a parse error.\nfunc GetHint(code ErrorCode, token, query string, position int) string {\n\tswitch code {\n\tcase ErrorCodeUnexpectedToken:\n\t\treturn getUnexpectedTokenHint(token, query, position)\n\tcase ErrorCodeMissingClosingBracket:\n\t\treturn getMissingClosingBracketHint(query)\n\tcase ErrorCodeMissingClosingBrace:\n\t\treturn getMissingClosingBraceHint(query)\n\tcase ErrorCodeMissingGitRef:\n\t\treturn \"Git filters with '...' require a reference on each side. e.g. '[main...HEAD]'\"\n\tcase ErrorCodeUnexpectedEOF:\n\t\treturn getUnexpectedEOFHint(query)\n\tcase ErrorCodeIllegalToken:\n\t\treturn \"This character is not recognized. Valid operators: | (union), ! (negation), = (attribute)\"\n\n\t// These have error messages that are pretty self-explanatory and don't need hints.\n\tcase ErrorCodeEmptyGitFilter, ErrorCodeEmptyExpression, ErrorCodeMissingOperand, ErrorCodeInvalidGlob:\n\t\treturn \"\"\n\n\t// These are errors that don't have obvious hints that can be offered.\n\tcase ErrorCodeUnknown:\n\t\treturn \"\"\n\t}\n\n\treturn \"\"\n}\n\n// getUnexpectedTokenHint returns a single hint specific to unexpected token errors.\nfunc getUnexpectedTokenHint(token, query string, position int) string {\n\tswitch token {\n\tcase \"^\":\n\t\treturn getCaretHint(query, position)\n\tcase \"|\":\n\t\treturn \"\"\n\tcase \"=\":\n\t\treturn \"The equals sign is used for attribute filters. e.g. 'name=foo'\"\n\tcase \"]\":\n\t\treturn \"Unexpected ']' without matching '['. Git-based expressions use square brackets. e.g. '[main...HEAD]'\"\n\tcase \"}\":\n\t\treturn \"Unexpected '}' without matching '{'. Explicit path expressions use braces. e.g. '{./my path}'\"\n\tcase \"...\":\n\t\treturn \"The '...' operator must be used in either a graph-based or Git-based expression. e.g. '...foo...' or '[main...HEAD]'\"\n\t}\n\n\t// Generic unexpected token hints\n\tif strings.HasPrefix(token, \".\") || strings.HasPrefix(token, \"/\") {\n\t\treturn \"Path expressions should start with './' for relative or '/' for absolute paths.\"\n\t}\n\n\treturn \"\"\n}\n\n// getCaretHint returns a single hint for caret (^) token errors.\nfunc getCaretHint(query string, position int) string {\n\t// Check if caret is at start - suggests graph exclusion usage\n\tif position == 0 {\n\t\treturn \"The '^' operator excludes the target from graph results. e.g. '^foo...' selects foo's dependents but not foo itself.\"\n\t}\n\n\t// Check if caret follows text (e.g., \"HEAD^\")\n\tif position > 0 {\n\t\tbeforeCaret := strings.TrimSpace(query[:position])\n\n\t\t// Check if it follows an ellipsis - suggest moving caret to left side\n\t\tif targetPart, found := strings.CutSuffix(beforeCaret, \"...\"); found {\n\t\t\t// Extract the target before the ellipsis for a dynamic suggestion\n\t\t\treturn fmt.Sprintf(\"The '^' operator excludes the target from graph results when used on the left side of the expression. Did you mean '^%s...'?\", targetPart)\n\t\t}\n\n\t\t// Find the immediate identifier before caret (split by operators/whitespace)\n\t\tparts := strings.FieldsFunc(beforeCaret, func(r rune) bool {\n\t\t\treturn r == ' ' || r == '\\t' || r == '|' || r == '!' || r == '=' || r == '{' || r == '}' || r == '[' || r == ']'\n\t\t})\n\n\t\tif len(parts) > 0 {\n\t\t\tlastIdent := parts[len(parts)-1]\n\t\t\tif lastIdent != \"\" {\n\t\t\t\treturn fmt.Sprintf(\"Git-based expressions require surrounding references with '[]'. Did you mean '[%s^]'?\", lastIdent)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Caret at start or in unusual position\n\treturn \"The '^' operator must be used in either a graph-based or Git-based expression. e.g. '...^foo...' or '[HEAD^]'\"\n}\n\n// getUnexpectedEOFHint returns a context-aware hint for unexpected end of input.\nfunc getUnexpectedEOFHint(query string) string {\n\ttrimmed := strings.TrimSpace(query)\n\n\tif strings.HasSuffix(trimmed, \"...\") {\n\t\treturn \"The '...' operator must be used in either a graph-based or Git-based expression. e.g. '...foo...' or '[main...HEAD]'\"\n\t}\n\n\tif strings.HasSuffix(trimmed, \"^\") {\n\t\treturn \"The '^' operator must be used in either a graph-based or Git-based expression. e.g. '...^foo...' or '[HEAD^]'\"\n\t}\n\n\treturn \"The expression is incomplete. Make sure all brackets are closed and operators have operands.\"\n}\n\n// getMissingClosingBracketHint returns a dynamic hint for unclosed Git filter expressions.\nfunc getMissingClosingBracketHint(query string) string {\n\tif _, content, found := strings.Cut(query, \"[\"); found {\n\t\treturn fmt.Sprintf(\"Git-based expressions require surrounding references with '[]'. Did you mean '[%s]'?\", content)\n\t}\n\n\treturn \"Git-based expressions require surrounding references with '[]'. e.g. '[main...HEAD]'\"\n}\n\n// getMissingClosingBraceHint returns a dynamic hint for unclosed braced path expressions.\nfunc getMissingClosingBraceHint(query string) string {\n\tif _, content, found := strings.Cut(query, \"{\"); found {\n\t\treturn fmt.Sprintf(\"Explicit path expressions require surrounding paths with '{}'. Did you mean '{%s}'?\", content)\n\t}\n\n\treturn \"Explicit path expressions require surrounding paths with '{}'. e.g. '{path/with spaces}'\"\n}\n"
  },
  {
    "path": "internal/filter/hints_test.go",
    "content": "package filter_test\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestHints_Golden tests the full rendered error messages for golden/regression testing.\nfunc TestHints_Golden(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tquery    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:  \"git syntax caret after ref\",\n\t\t\tquery: \"HEAD^\",\n\t\t\texpected: `Filter parsing error: Unexpected token\n --> --filter 'HEAD^'\n\n     HEAD^\n         ^ Unexpected '^' after expression\n\n  hint: Git-based expressions require surrounding references with '[]'. Did you mean '[HEAD^]'?\n`,\n\t\t},\n\t\t{\n\t\t\tname:  \"unclosed bracket\",\n\t\t\tquery: \"[main...HEAD\",\n\t\t\texpected: `Filter parsing error: Unclosed Git filter expression\n --> --filter '[main...HEAD'\n\n     [main...HEAD\n     ^ This Git-based expression is missing a closing ']'\n\n  hint: Git-based expressions require surrounding references with '[]'. Did you mean '[main...HEAD]'?\n`,\n\t\t},\n\t\t{\n\t\t\tname:  \"unclosed brace\",\n\t\t\tquery: \"{my path\",\n\t\t\texpected: `Filter parsing error: Unclosed path expression\n --> --filter '{my path'\n\n     {my path\n     ^ This braced path expression is missing a closing '}'\n\n  hint: Explicit path expressions require surrounding paths with '{}'. Did you mean '{my path}'?\n`,\n\t\t},\n\t\t{\n\t\t\tname:  \"empty git filter\",\n\t\t\tquery: \"[]\",\n\t\t\texpected: `Filter parsing error: Empty Git filter\n --> --filter '[]'\n\n     []\n      ^ Git filter expression cannot be empty\n`,\n\t\t},\n\t\t{\n\t\t\tname:  \"pipe at start\",\n\t\t\tquery: \"| foo\",\n\t\t\texpected: `Filter parsing error: Unexpected token\n --> --filter '| foo'\n\n     | foo\n     ^ Missing left-hand side of '|' operator\n`,\n\t\t},\n\t\t{\n\t\t\tname:  \"pipe at end\",\n\t\t\tquery: \"foo |\",\n\t\t\texpected: `Filter parsing error: Unexpected end of input\n --> --filter 'foo |'\n\n     foo |\n          ^ Missing right-hand side of '|' operator\n`,\n\t\t},\n\t\t{\n\t\t\tname:  \"bang without operand\",\n\t\t\tquery: \"!\",\n\t\t\texpected: `Filter parsing error: Unexpected end of input\n --> --filter '!'\n\n     !\n      ^ Missing target expression for '!' operator\n`,\n\t\t},\n\t\t{\n\t\t\tname:  \"Unexpected closing bracket\",\n\t\t\tquery: \"]\",\n\t\t\texpected: `Filter parsing error: Unexpected token\n --> --filter ']'\n\n     ]\n     ^ Unexpected ']'\n\n  hint: Unexpected ']' without matching '['. Git-based expressions use square brackets. e.g. '[main...HEAD]'\n`,\n\t\t},\n\t\t{\n\t\t\tname:  \"Unexpected closing brace\",\n\t\t\tquery: \"}\",\n\t\t\texpected: `Filter parsing error: Unexpected token\n --> --filter '}'\n\n     }\n     ^ Unexpected '}'\n\n  hint: Unexpected '}' without matching '{'. Explicit path expressions use braces. e.g. '{./my path}'\n`,\n\t\t},\n\t\t{\n\t\t\tname:  \"equals without context\",\n\t\t\tquery: \"=foo\",\n\t\t\texpected: `Filter parsing error: Unexpected token\n --> --filter '=foo'\n\n     =foo\n     ^ Unexpected '='\n\n  hint: The equals sign is used for attribute filters. e.g. 'name=foo'\n`,\n\t\t},\n\t\t{\n\t\t\tname:  \"caret at start\",\n\t\t\tquery: \"^\",\n\t\t\texpected: `Filter parsing error: Unexpected end of input\n --> --filter '^'\n\n     ^\n      ^ Expression is incomplete\n\n  hint: The '^' operator must be used in either a graph-based or Git-based expression. e.g. '...^foo...' or '[HEAD^]'\n`,\n\t\t},\n\t\t{\n\t\t\tname:  \"ellipsis at start\",\n\t\t\tquery: \"...\",\n\t\t\texpected: `Filter parsing error: Unexpected end of input\n --> --filter '...'\n\n     ...\n        ^ Expression is incomplete\n\n  hint: The '...' operator must be used in either a graph-based or Git-based expression. e.g. '...foo...' or '[main...HEAD]'\n`,\n\t\t},\n\t\t// TODO: Make this not an error. This should just be a path expression pointing at the current directory.\n\t\t{\n\t\t\tname:  \"illegal character\",\n\t\t\tquery: \".\",\n\t\t\texpected: `Filter parsing error: Illegal token\n --> --filter '.'\n\n     .\n     ^ Unrecognized character '.'\n\n  hint: This character is not recognized. Valid operators: | (union), ! (negation), = (attribute)\n`,\n\t\t},\n\t\t{\n\t\t\tname:  \"missing git ref after ellipsis\",\n\t\t\tquery: \"[main...]\",\n\t\t\texpected: `Filter parsing error: Missing Git reference\n --> --filter '[main...]'\n\n     [main...]\n             ^ Expected second Git reference after '...'\n\n  hint: Git filters with '...' require a reference on each side. e.g. '[main...HEAD]'\n`,\n\t\t},\n\t\t{\n\t\t\tname:  \"complex expression with caret\",\n\t\t\tquery: \"./apps/* | HEAD^\",\n\t\t\texpected: `Filter parsing error: Unexpected token\n --> --filter './apps/* | HEAD^'\n\n     ./apps/* | HEAD^\n                    ^ Unexpected '^' after expression\n\n  hint: Git-based expressions require surrounding references with '[]'. Did you mean '[HEAD^]'?\n`,\n\t\t},\n\t\t{\n\t\t\tname:  \"caret after ellipsis\",\n\t\t\tquery: \"./foo...^\",\n\t\t\texpected: `Filter parsing error: Unexpected token\n --> --filter './foo...^'\n\n     ./foo...^\n             ^ Unexpected '^' after expression\n\n  hint: The '^' operator excludes the target from graph results when used on the left side of the expression. Did you mean '^./foo...'?\n`,\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.Parallel()\n\n\t\t\toutput, err := renderParseError(tc.query)\n\t\t\trequire.NoError(t, err)\n\n\t\t\toutput = stripTimestampPrefix(output)\n\n\t\t\tassert.Equal(t, tc.expected, output)\n\t\t})\n\t}\n}\n\n// TestHints_ErrorCodeCoverage verifies that all error codes produce appropriate hints.\nfunc TestHints_ErrorCodeCoverage(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname          string\n\t\ttoken         string\n\t\tquery         string\n\t\thintSubstring string\n\t\tcode          filter.ErrorCode\n\t\tposition      int\n\t\texpectHint    bool\n\t}{\n\t\t{\n\t\t\tname:       \"UnexpectedToken with pipe\",\n\t\t\tcode:       filter.ErrorCodeUnexpectedToken,\n\t\t\ttoken:      \"|\",\n\t\t\tquery:      \"| foo\",\n\t\t\tposition:   0,\n\t\t\texpectHint: false,\n\t\t},\n\t\t{\n\t\t\tname:          \"UnexpectedToken with caret\",\n\t\t\tcode:          filter.ErrorCodeUnexpectedToken,\n\t\t\ttoken:         \"^\",\n\t\t\tquery:         \"HEAD^\",\n\t\t\tposition:      4,\n\t\t\texpectHint:    true,\n\t\t\thintSubstring: \"Git\",\n\t\t},\n\t\t{\n\t\t\tname:          \"UnexpectedToken with equals\",\n\t\t\tcode:          filter.ErrorCodeUnexpectedToken,\n\t\t\ttoken:         \"=\",\n\t\t\tquery:         \"=foo\",\n\t\t\tposition:      0,\n\t\t\texpectHint:    true,\n\t\t\thintSubstring: \"attribute\",\n\t\t},\n\t\t{\n\t\t\tname:          \"UnexpectedToken with closing bracket\",\n\t\t\tcode:          filter.ErrorCodeUnexpectedToken,\n\t\t\ttoken:         \"]\",\n\t\t\tquery:         \"]\",\n\t\t\tposition:      0,\n\t\t\texpectHint:    true,\n\t\t\thintSubstring: \"without matching '['\",\n\t\t},\n\t\t{\n\t\t\tname:          \"UnexpectedToken with closing brace\",\n\t\t\tcode:          filter.ErrorCodeUnexpectedToken,\n\t\t\ttoken:         \"}\",\n\t\t\tquery:         \"}\",\n\t\t\tposition:      0,\n\t\t\texpectHint:    true,\n\t\t\thintSubstring: \"without matching '{'\",\n\t\t},\n\t\t{\n\t\t\tname:          \"UnexpectedToken with ellipsis\",\n\t\t\tcode:          filter.ErrorCodeUnexpectedToken,\n\t\t\ttoken:         \"...\",\n\t\t\tquery:         \"...\",\n\t\t\tposition:      0,\n\t\t\texpectHint:    true,\n\t\t\thintSubstring: \"graph-based\",\n\t\t},\n\t\t{\n\t\t\tname:          \"MissingClosingBracket\",\n\t\t\tcode:          filter.ErrorCodeMissingClosingBracket,\n\t\t\ttoken:         \"\",\n\t\t\tquery:         \"[main\",\n\t\t\tposition:      5,\n\t\t\texpectHint:    true,\n\t\t\thintSubstring: \"require surrounding references with '[]'\",\n\t\t},\n\t\t{\n\t\t\tname:          \"MissingClosingBrace\",\n\t\t\tcode:          filter.ErrorCodeMissingClosingBrace,\n\t\t\ttoken:         \"\",\n\t\t\tquery:         \"{path\",\n\t\t\tposition:      5,\n\t\t\texpectHint:    true,\n\t\t\thintSubstring: \"require surrounding paths with '{}'\",\n\t\t},\n\t\t{\n\t\t\tname:          \"MissingGitRef\",\n\t\t\tcode:          filter.ErrorCodeMissingGitRef,\n\t\t\ttoken:         \"\",\n\t\t\tquery:         \"[main...]\",\n\t\t\tposition:      8,\n\t\t\texpectHint:    true,\n\t\t\thintSubstring: \"require a reference on each side\",\n\t\t},\n\t\t{\n\t\t\tname:       \"MissingOperand\",\n\t\t\tcode:       filter.ErrorCodeMissingOperand,\n\t\t\ttoken:      \"\",\n\t\t\tquery:      \"foo |\",\n\t\t\tposition:   5,\n\t\t\texpectHint: false,\n\t\t},\n\t\t{\n\t\t\tname:          \"UnexpectedEOF\",\n\t\t\tcode:          filter.ErrorCodeUnexpectedEOF,\n\t\t\ttoken:         \"\",\n\t\t\tquery:         \"...\",\n\t\t\tposition:      3,\n\t\t\texpectHint:    true,\n\t\t\thintSubstring: \"expression\",\n\t\t},\n\t\t{\n\t\t\tname:          \"IllegalToken\",\n\t\t\tcode:          filter.ErrorCodeIllegalToken,\n\t\t\ttoken:         \"@\",\n\t\t\tquery:         \"@\",\n\t\t\tposition:      0,\n\t\t\texpectHint:    true,\n\t\t\thintSubstring: \"not recognized\",\n\t\t},\n\t\t{\n\t\t\tname:       \"EmptyGitFilter - no hint\",\n\t\t\tcode:       filter.ErrorCodeEmptyGitFilter,\n\t\t\ttoken:      \"]\",\n\t\t\tquery:      \"[]\",\n\t\t\tposition:   1,\n\t\t\texpectHint: false,\n\t\t},\n\t\t{\n\t\t\tname:       \"EmptyExpression - no hint\",\n\t\t\tcode:       filter.ErrorCodeEmptyExpression,\n\t\t\ttoken:      \"}\",\n\t\t\tquery:      \"{}\",\n\t\t\tposition:   1,\n\t\t\texpectHint: false,\n\t\t},\n\t\t{\n\t\t\tname:       \"Unknown - no hint\",\n\t\t\tcode:       filter.ErrorCodeUnknown,\n\t\t\ttoken:      \"\",\n\t\t\tquery:      \"\",\n\t\t\tposition:   0,\n\t\t\texpectHint: false,\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.Parallel()\n\n\t\t\thint := filter.GetHint(tc.code, tc.token, tc.query, tc.position)\n\n\t\t\tif tc.expectHint {\n\t\t\t\trequire.NotEmpty(\n\t\t\t\t\tt,\n\t\t\t\t\thint,\n\t\t\t\t\t\"expected hint for error code %v\",\n\t\t\t\t\ttc.code,\n\t\t\t\t)\n\t\t\t\tassert.Contains(\n\t\t\t\t\tt,\n\t\t\t\t\thint,\n\t\t\t\t\ttc.hintSubstring,\n\t\t\t\t\t\"hint should contain '%s', got: %s\",\n\t\t\t\t\ttc.hintSubstring,\n\t\t\t\t\thint,\n\t\t\t\t)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Empty(t, hint, \"expected no hint for error code %v\", tc.code)\n\t\t})\n\t}\n}\n\n// TestHints_CaretContextualHints tests that caret hints vary based on context.\nfunc TestHints_CaretContextualHints(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tquery         string\n\t\thintSubstring string\n\t\tposition      int\n\t}{\n\t\t{\n\t\t\tname:          \"caret after identifier suggests Git syntax\",\n\t\t\tquery:         \"HEAD^\",\n\t\t\tposition:      4,\n\t\t\thintSubstring: \"[HEAD^]\",\n\t\t},\n\t\t{\n\t\t\tname:          \"caret after ellipsis suggests graph exclusion\",\n\t\t\tquery:         \"foo...^bar\",\n\t\t\tposition:      6,\n\t\t\thintSubstring: \"excludes the target\",\n\t\t},\n\t\t{\n\t\t\tname:          \"caret at start suggests graph exclusion\",\n\t\t\tquery:         \"^foo\",\n\t\t\tposition:      0,\n\t\t\thintSubstring: \"excludes the target\",\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.Parallel()\n\n\t\t\thint := filter.GetHint(filter.ErrorCodeUnexpectedToken, \"^\", tc.query, tc.position)\n\n\t\t\trequire.NotEmpty(t, hint)\n\t\t\tassert.Contains(t, hint, tc.hintSubstring)\n\t\t})\n\t}\n}\n\n// TestHints_FormatDiagnosticStructure verifies the overall structure of diagnostic output.\nfunc TestHints_FormatDiagnosticStructure(t *testing.T) {\n\tt.Parallel()\n\n\tparseErr := &filter.ParseError{\n\t\tTitle:         \"Test Error\",\n\t\tMessage:       \"test message\",\n\t\tPosition:      5,\n\t\tErrorPosition: 5,\n\t\tQuery:         \"test query\",\n\t\tTokenLiteral:  \"q\",\n\t\tTokenLength:   1,\n\t\tErrorCode:     filter.ErrorCodeMissingClosingBracket,\n\t}\n\n\toutput := filter.FormatDiagnostic(parseErr, 0, false)\n\n\tlines := strings.Split(output, \"\\n\")\n\n\trequire.GreaterOrEqual(t, len(lines), 6, \"diagnostic should have at least 6 lines\")\n\n\tassert.Contains(t, lines[0], \"Filter parsing error:\")\n\tassert.Contains(t, lines[0], \"Test Error\")\n\n\tassert.Contains(t, lines[1], \" --> \")\n\tassert.Contains(t, lines[1], \"--filter\")\n\n\tassert.Empty(t, lines[2])\n\n\tassert.Contains(t, lines[3], \"test query\")\n\n\tassert.Contains(t, lines[4], \"^\")\n\tassert.Contains(t, lines[4], \"test message\")\n\n\tassert.Empty(t, lines[5])\n\n\tassert.Contains(t, lines[6], \"hint:\")\n}\n\n// TestHints_FilterIndexInDiagnostic verifies filter index appears in multi-filter scenarios.\nfunc TestHints_FilterIndexInDiagnostic(t *testing.T) {\n\tt.Parallel()\n\n\tparseErr := &filter.ParseError{\n\t\tTitle:         \"Test Error\",\n\t\tMessage:       \"test message\",\n\t\tPosition:      0,\n\t\tErrorPosition: 0,\n\t\tQuery:         \"bad\",\n\t\tTokenLiteral:  \"b\",\n\t\tTokenLength:   1,\n\t\tErrorCode:     filter.ErrorCodeUnexpectedToken,\n\t}\n\n\toutput0 := filter.FormatDiagnostic(parseErr, 0, false)\n\tassert.Contains(t, output0, \"--filter 'bad'\")\n\tassert.NotContains(t, output0, \"--filter[\")\n\n\toutput2 := filter.FormatDiagnostic(parseErr, 2, false)\n\tassert.Contains(t, output2, \"--filter[2]\")\n}\n\n// stripTimestampPrefix removes any timestamp prefix from log output.\n//\n// Timestamps typically appear at the start of lines in formats like:\n// \"2024-01-15T10:30:00Z\" or \"2024/01/15 10:30:00\"\n//\n// This makes it easier to assert expected output in golden tests.\nfunc stripTimestampPrefix(s string) string {\n\ttimestampPattern := regexp.MustCompile(`(?m)^(\\d{4}[-/]\\d{2}[-/]\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}[^\\s]*\\s+)`)\n\treturn timestampPattern.ReplaceAllString(s, \"\")\n}\n\n// renderParseError parses a filter query and returns the formatted diagnostic.\n// Returns an error if parsing succeeds (no error to render).\nfunc renderParseError(query string) (string, error) {\n\t_, err := filter.Parse(query)\n\tif err == nil {\n\t\treturn \"\", errors.New(\"expected parse error but got none\")\n\t}\n\n\tvar parseErr filter.ParseError\n\tif !errors.As(err, &parseErr) {\n\t\treturn \"\", errors.Errorf(\"expected ParseError but got: %v\", err)\n\t}\n\n\t// Render without colors for consistent golden testing\n\treturn filter.FormatDiagnostic(&parseErr, 0, false), nil\n}\n"
  },
  {
    "path": "internal/filter/lexer.go",
    "content": "package filter\n\nimport (\n\t\"strings\"\n\t\"unicode\"\n)\n\n// Lexer tokenizes a filter query string.\ntype Lexer struct {\n\tinput        string // The input string being tokenized\n\tposition     int    // Current position in input (points to current char)\n\treadPosition int    // Current reading position in input (after current char)\n\tch           byte   // Current char under examination\n\tafterEqual   bool   // True if the last token was EQUAL (for parsing attribute values)\n}\n\n// NewLexer creates a new Lexer for the given input string.\nfunc NewLexer(input string) *Lexer {\n\tl := &Lexer{input: input}\n\tl.readChar() // Initialize by reading the first character\n\n\treturn l\n}\n\n// Input returns the original input string.\nfunc (l *Lexer) Input() string {\n\treturn l.input\n}\n\n// NextToken reads and returns the next token from the input.\nfunc (l *Lexer) NextToken() Token {\n\tl.skipWhitespace()\n\n\tvar tok Token\n\n\tstartPosition := l.position\n\n\tswitch l.ch {\n\tcase '!':\n\t\ttok = NewToken(BANG, string(l.ch), startPosition)\n\t\tl.readChar()\n\tcase '|':\n\t\ttok = NewToken(PIPE, string(l.ch), startPosition)\n\t\tl.readChar()\n\tcase '=':\n\t\ttok = NewToken(EQUAL, string(l.ch), startPosition)\n\t\tl.readChar()\n\t\tl.afterEqual = true\n\n\t\treturn tok\n\tcase '{':\n\t\ttok = NewToken(LBRACE, string(l.ch), startPosition)\n\t\tl.readChar()\n\tcase '}':\n\t\ttok = NewToken(RBRACE, string(l.ch), startPosition)\n\t\tl.readChar()\n\tcase '[':\n\t\ttok = NewToken(LBRACKET, string(l.ch), startPosition)\n\t\tl.readChar()\n\tcase ']':\n\t\ttok = NewToken(RBRACKET, string(l.ch), startPosition)\n\t\tl.readChar()\n\tcase '^':\n\t\ttok = NewToken(CARET, string(l.ch), startPosition)\n\t\tl.readChar()\n\tcase 0:\n\t\ttok = NewToken(EOF, \"\", startPosition)\n\tcase '.':\n\t\tif l.peekChar() == '.' {\n\t\t\t// Check for ellipsis (...)\n\t\t\tif l.readPosition+1 < len(l.input) && l.input[l.readPosition+1] == '.' {\n\t\t\t\tl.readChar()\n\t\t\t\tl.readChar()\n\n\t\t\t\ttok = NewToken(ELLIPSIS, \"...\", startPosition)\n\n\t\t\t\tl.readChar()\n\n\t\t\t\treturn tok\n\t\t\t}\n\n\t\t\t// Check if this is .. followed by / (parent directory path)\n\t\t\tif l.readPosition+1 < len(l.input) && l.input[l.readPosition+1] == '/' {\n\t\t\t\ttok = l.readPath(startPosition)\n\n\t\t\t\treturn tok\n\t\t\t}\n\t\t}\n\n\t\tswitch nextCh := l.peekChar(); {\n\t\tcase nextCh == '/':\n\t\t\ttok = l.readPath(startPosition)\n\t\tcase isIdentifierChar(nextCh):\n\t\t\tliteral := l.readIdentifier()\n\t\t\ttok = NewToken(IDENT, literal, startPosition)\n\n\t\t\treturn tok\n\t\tdefault:\n\t\t\ttok = NewToken(ILLEGAL, string(l.ch), startPosition)\n\t\t\tl.readChar()\n\t\t}\n\tcase '/':\n\t\ttok = l.readPath(startPosition)\n\tdefault:\n\t\tif l.afterEqual {\n\t\t\t// After '=', read as attribute value (can contain slashes)\n\t\t\tliteral := l.readAttributeValue()\n\t\t\ttok = NewToken(IDENT, literal, startPosition)\n\t\t\tl.afterEqual = false\n\n\t\t\treturn tok\n\t\t}\n\n\t\tif isIdentifierChar(l.ch) {\n\t\t\t// Check if this identifier contains a slash - if so, treat it as a path\n\t\t\tif l.containsSlashBeforeSpecialChar() {\n\t\t\t\ttok = l.readPath(startPosition)\n\n\t\t\t\treturn tok\n\t\t\t}\n\n\t\t\tliteral := l.readIdentifier()\n\t\t\ttok = NewToken(IDENT, literal, startPosition)\n\n\t\t\treturn tok\n\t\t}\n\n\t\ttok = NewToken(ILLEGAL, string(l.ch), startPosition)\n\t\tl.readChar()\n\t}\n\n\tl.afterEqual = false\n\n\treturn tok\n}\n\n// readChar advances the lexer's position and updates the current character.\nfunc (l *Lexer) readChar() {\n\tif l.readPosition >= len(l.input) {\n\t\tl.ch = 0 // ASCII code for \"NUL\", signifies end of input\n\t\tl.position = l.readPosition\n\t\tl.readPosition++\n\n\t\treturn\n\t}\n\n\tl.ch = l.input[l.readPosition]\n\tl.position = l.readPosition\n\tl.readPosition++\n}\n\n// peekChar returns the next character without advancing the position.\nfunc (l *Lexer) peekChar() byte {\n\tif l.readPosition >= len(l.input) {\n\t\treturn 0\n\t}\n\n\treturn l.input[l.readPosition]\n}\n\n// skipWhitespace skips over whitespace characters.\nfunc (l *Lexer) skipWhitespace() {\n\tfor l.ch != 0 && unicode.IsSpace(rune(l.ch)) {\n\t\tl.readChar()\n\t}\n}\n\n// readIdentifier reads an identifier from the input.\n// Identifiers can contain letters, numbers, underscores, hyphens, dots, and other non-special chars.\n// This includes hidden files starting with a dot like .gitignore\n// Trailing whitespace is trimmed.\nfunc (l *Lexer) readIdentifier() string {\n\tposition := l.position\n\tfor isIdentifierChar(l.ch) {\n\t\t// stop at ellipsis (...)\n\t\tif l.ch == '.' && l.peekChar() == '.' {\n\t\t\tif l.readPosition+1 < len(l.input) && l.input[l.readPosition+1] == '.' {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tl.readChar()\n\t}\n\n\tliteral := l.input[position:l.position]\n\n\treturn strings.TrimSpace(literal)\n}\n\n// readAttributeValue reads an attribute value from the input.\n// Attribute values can contain slashes, letters, numbers, underscores, hyphens, dots, etc.\n// They stop at special operators (|, !, {, }) or end of input.\n// Trailing whitespace is trimmed.\nfunc (l *Lexer) readAttributeValue() string {\n\tposition := l.position\n\tfor isAttributeValueChar(l.ch) {\n\t\t// stop at ellipsis (...)\n\t\tif l.ch == '.' && l.peekChar() == '.' {\n\t\t\tif l.readPosition+1 < len(l.input) && l.input[l.readPosition+1] == '.' {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tl.readChar()\n\t}\n\n\tliteral := l.input[position:l.position]\n\n\treturn strings.TrimSpace(literal)\n}\n\n// readPath reads a path from the input.\n// Paths can contain any characters except special operators.\n// Trailing whitespace is trimmed.\nfunc (l *Lexer) readPath(startPosition int) Token {\n\tposition := l.position\n\tfor isPathChar(l.ch) {\n\t\t// stop at ellipsis (...)\n\t\tif l.ch == '.' && l.peekChar() == '.' {\n\t\t\tif l.readPosition+1 < len(l.input) && l.input[l.readPosition+1] == '.' {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tl.readChar()\n\t}\n\n\tliteral := l.input[position:l.position]\n\n\tliteral = strings.TrimSpace(literal)\n\n\treturn NewToken(PATH, literal, startPosition)\n}\n\n// containsSlashBeforeSpecialChar checks if there's a slash in the input before\n// we encounter a special character, starting from the current position.\nfunc (l *Lexer) containsSlashBeforeSpecialChar() bool {\n\tpos := l.position\n\tfor pos < len(l.input) {\n\t\tch := l.input[pos]\n\t\tif ch == '/' {\n\t\t\treturn true\n\t\t}\n\n\t\tif isSpecialChar(ch) {\n\t\t\treturn false\n\t\t}\n\n\t\tpos++\n\t}\n\n\treturn false\n}\n\n// isSpecialChar returns true if the character is a special operator or delimiter.\nfunc isSpecialChar(ch byte) bool {\n\treturn ch == '!' || ch == '|' || ch == '=' || ch == '{' || ch == '}' || ch == '[' || ch == ']' || ch == '^' || ch == 0\n}\n\n// isPathSeparator returns true if the character is a path separator.\nfunc isPathSeparator(ch byte) bool {\n\treturn ch == '/'\n}\n\n// isIdentifierChar returns true if the character can be part of an identifier.\nfunc isIdentifierChar(ch byte) bool {\n\treturn !isSpecialChar(ch) && !isPathSeparator(ch)\n}\n\n// isAttributeValueChar returns true if the character can be part of an attribute value.\n// Attribute values can contain slashes (unlike regular identifiers).\nfunc isAttributeValueChar(ch byte) bool {\n\treturn !isSpecialChar(ch)\n}\n\n// isPathChar returns true if the character can be part of a path.\nfunc isPathChar(ch byte) bool {\n\treturn !isSpecialChar(ch)\n}\n"
  },
  {
    "path": "internal/filter/lexer_test.go",
    "content": "package filter_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestLexer_SingleTokens(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected []filter.Token\n\t}{\n\t\t{\n\t\t\tname:  \"bang operator\",\n\t\t\tinput: \"!\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.BANG, Literal: \"!\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 1},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"pipe operator\",\n\t\t\tinput: \"|\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.PIPE, Literal: \"|\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 1},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"left brace\",\n\t\t\tinput: \"{\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.LBRACE, Literal: \"{\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 1},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"right brace\",\n\t\t\tinput: \"}\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.RBRACE, Literal: \"}\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 1},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"equal operator\",\n\t\t\tinput: \"=\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.EQUAL, Literal: \"=\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 1},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"simple identifier\",\n\t\t\tinput: \"foo\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \"foo\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 3},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"identifier with underscore\",\n\t\t\tinput: \"foo_bar\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \"foo_bar\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 7},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"identifier with hyphen\",\n\t\t\tinput: \"foo-bar\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \"foo-bar\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 7},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"hidden file\",\n\t\t\tinput: \".gitignore\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \".gitignore\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 10},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"hidden file with underscore\",\n\t\t\tinput: \".terragrunt-cache\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \".terragrunt-cache\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 17},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"relative path\",\n\t\t\tinput: \"./apps\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.PATH, Literal: \"./apps\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 6},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"absolute path\",\n\t\t\tinput: \"/absolute/path\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.PATH, Literal: \"/absolute/path\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 14},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"glob path with single wildcard\",\n\t\t\tinput: \"./apps/*\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.PATH, Literal: \"./apps/*\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 8},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"glob path with recursive wildcard\",\n\t\t\tinput: \"./apps/**/foo\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.PATH, Literal: \"./apps/**/foo\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 13},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"ellipsis\",\n\t\t\tinput: \"...\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.ELLIPSIS, Literal: \"...\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 3},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"double dots with digit is identifier\",\n\t\t\tinput: \"..1\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \"..1\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 3},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"double dots with multi digit is identifier\",\n\t\t\tinput: \"..25\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \"..25\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 4},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"parent directory path not confused with depth\",\n\t\t\tinput: \"../foo\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.PATH, Literal: \"../foo\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 6},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"identifier with double dots followed by letter\",\n\t\t\tinput: \"foo..bar\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \"foo..bar\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 8},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"identifier with double dots and digit\",\n\t\t\tinput: \"foo..1\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \"foo..1\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 6},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"double dots digit and identifier\",\n\t\t\tinput: \"..2foo\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \"..2foo\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 6},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"ellipsis with identifier\",\n\t\t\tinput: \"...foo\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.ELLIPSIS, Literal: \"...\", Position: 0},\n\t\t\t\t{Type: filter.IDENT, Literal: \"foo\", Position: 3},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 6},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"number ellipsis identifier (dependent depth syntax)\",\n\t\t\tinput: \"1...foo\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \"1\", Position: 0},\n\t\t\t\t{Type: filter.ELLIPSIS, Literal: \"...\", Position: 1},\n\t\t\t\t{Type: filter.IDENT, Literal: \"foo\", Position: 4},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 7},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"identifier ellipsis number (dependency depth syntax)\",\n\t\t\tinput: \"foo...1\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \"foo\", Position: 0},\n\t\t\t\t{Type: filter.ELLIPSIS, Literal: \"...\", Position: 3},\n\t\t\t\t{Type: filter.IDENT, Literal: \"1\", Position: 6},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 7},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"full depth syntax both directions\",\n\t\t\tinput: \"1...foo...2\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \"1\", Position: 0},\n\t\t\t\t{Type: filter.ELLIPSIS, Literal: \"...\", Position: 1},\n\t\t\t\t{Type: filter.IDENT, Literal: \"foo\", Position: 4},\n\t\t\t\t{Type: filter.ELLIPSIS, Literal: \"...\", Position: 7},\n\t\t\t\t{Type: filter.IDENT, Literal: \"2\", Position: 10},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 11},\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\tt.Parallel()\n\n\t\t\tlexer := filter.NewLexer(tt.input)\n\t\t\tfor i, expected := range tt.expected {\n\t\t\t\ttok := lexer.NextToken()\n\t\t\t\tassert.Equal(t, expected.Type, tok.Type, \"token %d type mismatch\", i)\n\t\t\t\tassert.Equal(t, expected.Literal, tok.Literal, \"token %d literal mismatch\", i)\n\t\t\t\tassert.Equal(t, expected.Position, tok.Position, \"token %d position mismatch\", i)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLexer_ComplexQueries(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected []filter.Token\n\t}{\n\t\t{\n\t\t\tname:  \"attribute filter\",\n\t\t\tinput: \"name=foo\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \"name\", Position: 0},\n\t\t\t\t{Type: filter.EQUAL, Literal: \"=\", Position: 4},\n\t\t\t\t{Type: filter.IDENT, Literal: \"foo\", Position: 5},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 8},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"negated attribute filter\",\n\t\t\tinput: \"!name=bar\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.BANG, Literal: \"!\", Position: 0},\n\t\t\t\t{Type: filter.IDENT, Literal: \"name\", Position: 1},\n\t\t\t\t{Type: filter.EQUAL, Literal: \"=\", Position: 5},\n\t\t\t\t{Type: filter.IDENT, Literal: \"bar\", Position: 6},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 9},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"negated path filter\",\n\t\t\tinput: \"!./apps/legacy\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.BANG, Literal: \"!\", Position: 0},\n\t\t\t\t{Type: filter.PATH, Literal: \"./apps/legacy\", Position: 1},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 14},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"union of two filters\",\n\t\t\tinput: \"./apps/* | name=bar\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.PATH, Literal: \"./apps/*\", Position: 0},\n\t\t\t\t{Type: filter.PIPE, Literal: \"|\", Position: 9},\n\t\t\t\t{Type: filter.IDENT, Literal: \"name\", Position: 11},\n\t\t\t\t{Type: filter.EQUAL, Literal: \"=\", Position: 15},\n\t\t\t\t{Type: filter.IDENT, Literal: \"bar\", Position: 16},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 19},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"complex query with whitespace\",\n\t\t\tinput: \"name=foo | !./legacy | ./apps/**\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \"name\", Position: 0},\n\t\t\t\t{Type: filter.EQUAL, Literal: \"=\", Position: 4},\n\t\t\t\t{Type: filter.IDENT, Literal: \"foo\", Position: 5},\n\t\t\t\t{Type: filter.PIPE, Literal: \"|\", Position: 9},\n\t\t\t\t{Type: filter.BANG, Literal: \"!\", Position: 11},\n\t\t\t\t{Type: filter.PATH, Literal: \"./legacy\", Position: 12},\n\t\t\t\t{Type: filter.PIPE, Literal: \"|\", Position: 21},\n\t\t\t\t{Type: filter.PATH, Literal: \"./apps/**\", Position: 23},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 32},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"hidden file with operator\",\n\t\t\tinput: \".env | .gitignore\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \".env\", Position: 0},\n\t\t\t\t{Type: filter.PIPE, Literal: \"|\", Position: 5},\n\t\t\t\t{Type: filter.IDENT, Literal: \".gitignore\", Position: 7},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 17},\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\tt.Parallel()\n\n\t\t\tlexer := filter.NewLexer(tt.input)\n\t\t\tfor i, expected := range tt.expected {\n\t\t\t\ttok := lexer.NextToken()\n\t\t\t\tassert.Equal(t, expected.Type, tok.Type, \"token %d type mismatch\", i)\n\t\t\t\tassert.Equal(t, expected.Literal, tok.Literal, \"token %d literal mismatch\", i)\n\t\t\t\tassert.Equal(t, expected.Position, tok.Position, \"token %d position mismatch\", i)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLexer_EdgeCases(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected []filter.Token\n\t}{\n\t\t{\n\t\t\tname:  \"empty input\",\n\t\t\tinput: \"\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 0},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"only whitespace\",\n\t\t\tinput: \"   \\t\\n  \",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 7},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"single dot (invalid)\",\n\t\t\tinput: \".\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.ILLEGAL, Literal: \".\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 1},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"special character now allowed\",\n\t\t\tinput: \"@username\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \"@username\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 9},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"path with dashes and underscores\",\n\t\t\tinput: \"./my-app_v2/foo-bar\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.PATH, Literal: \"./my-app_v2/foo-bar\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 19},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"tab in identifier\",\n\t\t\tinput: \"foo\\tbar\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \"foo\\tbar\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 7},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"special characters in path\",\n\t\t\tinput: \"./app+test\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.PATH, Literal: \"./app+test\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 10},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"spaces in identifier\",\n\t\t\tinput: \"foo bar\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \"foo bar\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 7},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"spaces in path\",\n\t\t\tinput: \"./my path/to file\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.PATH, Literal: \"./my path/to file\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 17},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"spaces with pipe separator\",\n\t\t\tinput: \"foo bar | baz qux\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \"foo bar\", Position: 0},\n\t\t\t\t{Type: filter.PIPE, Literal: \"|\", Position: 8},\n\t\t\t\t{Type: filter.IDENT, Literal: \"baz qux\", Position: 10},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 17},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"braced path\",\n\t\t\tinput: \"{./apps/*}\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.LBRACE, Literal: \"{\", Position: 0},\n\t\t\t\t{Type: filter.PATH, Literal: \"./apps/*\", Position: 1},\n\t\t\t\t{Type: filter.RBRACE, Literal: \"}\", Position: 9},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 10},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"braced path with spaces\",\n\t\t\tinput: \"{my path/file}\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.LBRACE, Literal: \"{\", Position: 0},\n\t\t\t\t{Type: filter.PATH, Literal: \"my path/file\", Position: 1},\n\t\t\t\t{Type: filter.RBRACE, Literal: \"}\", Position: 13},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 14},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"source filter with slash\",\n\t\t\tinput: \"source=github.com/acme/foo/bar\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.IDENT, Literal: \"source\", Position: 0},\n\t\t\t\t{Type: filter.EQUAL, Literal: \"=\", Position: 6},\n\t\t\t\t{Type: filter.IDENT, Literal: \"github.com/acme/foo/bar\", Position: 7},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 30},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"path filter with slash\",\n\t\t\tinput: \"foo/bar\",\n\t\t\texpected: []filter.Token{\n\t\t\t\t{Type: filter.PATH, Literal: \"foo/bar\", Position: 0},\n\t\t\t\t{Type: filter.EOF, Literal: \"\", Position: 7},\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\tt.Parallel()\n\n\t\t\tlexer := filter.NewLexer(tt.input)\n\t\t\tfor i, expected := range tt.expected {\n\t\t\t\ttok := lexer.NextToken()\n\t\t\t\tassert.Equal(t, expected.Type, tok.Type, \"token %d type mismatch\", i)\n\t\t\t\tassert.Equal(t, expected.Literal, tok.Literal, \"token %d literal mismatch\", i)\n\t\t\t\tassert.Equal(t, expected.Position, tok.Position, \"token %d position mismatch\", i)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTokenType_String(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected  string\n\t\ttokenType filter.TokenType\n\t}{\n\t\t{\"ILLEGAL\", filter.ILLEGAL},\n\t\t{\"EOF\", filter.EOF},\n\t\t{\"IDENT\", filter.IDENT},\n\t\t{\"PATH\", filter.PATH},\n\t\t{\"!\", filter.BANG},\n\t\t{\"|\", filter.PIPE},\n\t\t{\"=\", filter.EQUAL},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.expected, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassert.Equal(t, tt.expected, tt.tokenType.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/filter/matcher.go",
    "content": "package filter\n\nimport (\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n)\n\n// MatchComponent checks if a single component matches an expression.\n// This is the shared core used by both Classifier and Evaluate.\nfunc MatchComponent(c component.Component, expr Expression) bool {\n\tswitch node := expr.(type) {\n\tcase *PathExpression:\n\t\treturn matchPath(c, node)\n\n\tcase *AttributeExpression:\n\t\treturn matchAttribute(c, node)\n\n\tcase *PrefixExpression:\n\t\tif node.Operator != \"!\" {\n\t\t\treturn false\n\t\t}\n\n\t\treturn MatchComponent(c, node.Right)\n\n\tcase *InfixExpression:\n\t\tif node.Operator != \"|\" {\n\t\t\treturn false\n\t\t}\n\n\t\tif !MatchComponent(c, node.Left) {\n\t\t\treturn false\n\t\t}\n\n\t\treturn MatchComponent(c, node.Right)\n\n\tcase *GraphExpression:\n\t\treturn MatchComponent(c, node.Target)\n\n\tcase *GitExpression:\n\t\treturn matchGit(c, node)\n\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// matchPath checks if a component matches a path expression.\nfunc matchPath(c component.Component, expr *PathExpression) bool {\n\tg := expr.Glob()\n\n\tcomponentPath := c.Path()\n\n\t// If the pattern is absolute, match against absolute path\n\tif filepath.IsAbs(expr.Value) {\n\t\treturn g.Match(filepath.ToSlash(componentPath))\n\t}\n\n\t// Try to get relative path from discovery context\n\tdiscoveryCtx := c.DiscoveryContext()\n\tif discoveryCtx != nil && discoveryCtx.WorkingDir != \"\" {\n\t\trelPath, err := filepath.Rel(discoveryCtx.WorkingDir, componentPath)\n\t\tif err == nil {\n\t\t\treturn g.Match(filepath.ToSlash(relPath))\n\t\t}\n\t}\n\n\t// Fall back to matching the path as-is\n\treturn g.Match(filepath.ToSlash(componentPath))\n}\n\n// matchAttribute checks if a component matches an attribute expression.\n// This handles attributes that can be evaluated without parsing (name, type, external).\n// For attributes requiring parsing (reading, source), this returns false.\nfunc matchAttribute(c component.Component, expr *AttributeExpression) bool {\n\tswitch expr.Key {\n\tcase AttributeName:\n\t\treturn expr.Glob().Match(filepath.Base(c.Path()))\n\n\tcase AttributeType:\n\t\tswitch expr.Value {\n\t\tcase AttributeTypeValueUnit:\n\t\t\t_, ok := c.(*component.Unit)\n\t\t\treturn ok\n\t\tcase AttributeTypeValueStack:\n\t\t\t_, ok := c.(*component.Stack)\n\t\t\treturn ok\n\t\t}\n\n\t\treturn false\n\n\tcase AttributeExternal:\n\t\tswitch expr.Value {\n\t\tcase AttributeExternalValueTrue:\n\t\t\treturn c.External()\n\t\tcase AttributeExternalValueFalse:\n\t\t\treturn !c.External()\n\t\t}\n\n\t\treturn false\n\n\tcase AttributeReading:\n\t\t// Reading attribute requires parsing, can't evaluate without parsed data\n\t\treturn false\n\n\tcase AttributeSource:\n\t\t// Source attribute requires parsing, can't evaluate without parsed data\n\t\treturn false\n\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// matchGit checks if a component matches a git expression.\n// Components discovered from worktrees have a Ref set in their discovery context.\nfunc matchGit(c component.Component, expr *GitExpression) bool {\n\tdiscoveryCtx := c.DiscoveryContext()\n\tif discoveryCtx == nil || discoveryCtx.Ref == \"\" {\n\t\treturn false\n\t}\n\n\treturn discoveryCtx.Ref == expr.FromRef || discoveryCtx.Ref == expr.ToRef\n}\n"
  },
  {
    "path": "internal/filter/parser.go",
    "content": "package filter\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\n// Parser parses a filter query string into an AST.\ntype Parser struct {\n\tlexer         *Lexer\n\terrors        []error\n\toriginalQuery string\n\tcurToken      Token\n\tpeekToken     Token\n}\n\n// Operator precedence levels\nconst (\n\t_ int = iota\n\tLOWEST\n\tINTERSECTION // |\n\tPREFIX       // !\n)\n\n// precedences maps token types to their precedence levels\nvar precedences = map[TokenType]int{\n\tPIPE: INTERSECTION,\n}\n\n// NewParser creates a new Parser for the given lexer.\nfunc NewParser(lexer *Lexer) *Parser {\n\tp := &Parser{\n\t\tlexer:         lexer,\n\t\terrors:        []error{},\n\t\toriginalQuery: lexer.Input(), // Capture original input for diagnostics\n\t}\n\n\t// Read two tokens to initialize curToken and peekToken\n\tp.nextToken()\n\tp.nextToken()\n\n\treturn p\n}\n\n// ParseExpression parses and returns an expression from the input.\nfunc (p *Parser) ParseExpression() (Expression, error) {\n\texpr := p.parseExpression(LOWEST)\n\n\tif expr == nil {\n\t\tif len(p.errors) > 0 {\n\t\t\treturn nil, p.errors[0]\n\t\t}\n\n\t\treturn nil, p.createError(ErrorCodeUnknown, \"Parse error\", \"failed to parse expression\")\n\t}\n\n\tif p.curToken.Type != EOF {\n\t\treturn nil, p.createError(ErrorCodeUnexpectedToken, \"Unexpected token\", \"Unexpected '\"+p.curToken.Literal+\"' after expression\")\n\t}\n\n\treturn expr, nil\n}\n\n// createError creates a ParseError with full context for rich diagnostics.\nfunc (p *Parser) createError(code ErrorCode, title, msg string) error {\n\ttokenLen := len(p.curToken.Literal)\n\tif tokenLen == 0 {\n\t\ttokenLen = 1 // Minimum length for underline\n\t}\n\n\treturn NewParseErrorWithContext(\n\t\ttitle,\n\t\tmsg,\n\t\tp.curToken.Position,\n\t\tp.curToken.Position,\n\t\tp.originalQuery,\n\t\tp.curToken.Literal,\n\t\ttokenLen,\n\t\tcode,\n\t)\n}\n\n// Errors returns any parsing errors that occurred.\nfunc (p *Parser) Errors() []error {\n\treturn p.errors\n}\n\n// nextToken advances to the next token.\nfunc (p *Parser) nextToken() {\n\tp.curToken = p.peekToken\n\tp.peekToken = p.lexer.NextToken()\n}\n\n// parseExpression is the core recursive descent parser.\nfunc (p *Parser) parseExpression(precedence int) Expression {\n\t// Check for prefix depth (N...foo) or ellipsis (...foo)\n\tincludeDependents := false\n\tdependentDepth := 0\n\n\t// Check for N... (number followed by ellipsis = dependent depth)\n\tif isPurelyNumeric(p.curToken.Literal) && p.peekToken.Type == ELLIPSIS {\n\t\tincludeDependents = true\n\t\tdependentDepth = parseDepth(p.curToken.Literal)\n\t\tp.nextToken() // consume number\n\t\tp.nextToken() // consume ellipsis\n\t} else if p.curToken.Type == ELLIPSIS {\n\t\tincludeDependents = true\n\n\t\tp.nextToken()\n\t}\n\n\t// Check for caret (^) for exclusion\n\texcludeTarget := false\n\tif p.curToken.Type == CARET {\n\t\texcludeTarget = true\n\n\t\tp.nextToken()\n\t}\n\n\tvar leftExpr Expression\n\n\tswitch p.curToken.Type {\n\tcase BANG:\n\t\tleftExpr = p.parsePrefixExpression()\n\tcase PATH:\n\t\tleftExpr = p.parsePathFilter()\n\tcase LBRACE:\n\t\tleftExpr = p.parseBracedPath()\n\tcase LBRACKET:\n\t\tleftExpr = p.parseGitFilter()\n\tcase IDENT:\n\t\tif p.peekToken.Type == EQUAL {\n\t\t\tleftExpr = p.parseAttributeFilter()\n\n\t\t\tbreak\n\t\t}\n\n\t\tattr, attrErr := NewAttributeExpression(\"name\", p.curToken.Literal)\n\t\tif attrErr != nil {\n\t\t\tp.addErrorWithCode(ErrorCodeInvalidGlob, \"Invalid glob pattern\", \"Invalid glob pattern in name filter: \"+attrErr.Error())\n\t\t\treturn nil\n\t\t}\n\n\t\tleftExpr = attr\n\n\t\tp.nextToken()\n\tcase ILLEGAL:\n\t\tp.addErrorWithCode(ErrorCodeIllegalToken, \"Illegal token\", \"Unrecognized character '\"+p.curToken.Literal+\"'\")\n\t\treturn nil\n\tcase EOF:\n\t\tp.addErrorWithCode(ErrorCodeUnexpectedEOF, \"Unexpected end of input\", \"Expression is incomplete\")\n\t\treturn nil\n\tcase PIPE:\n\t\tp.addErrorWithCode(ErrorCodeUnexpectedToken, \"Unexpected token\", \"Missing left-hand side of '|' operator\")\n\tcase EQUAL, RBRACE, RBRACKET, ELLIPSIS, CARET:\n\t\tp.addErrorWithCode(ErrorCodeUnexpectedToken, \"Unexpected token\", \"Unexpected '\"+p.curToken.Literal+\"'\")\n\t\treturn nil\n\tdefault:\n\t\tp.addErrorWithCode(ErrorCodeUnexpectedToken, \"Unexpected token\", \"Unexpected '\"+p.curToken.Literal+\"'\")\n\t\treturn nil\n\t}\n\n\tif leftExpr == nil {\n\t\treturn nil\n\t}\n\n\ttarget := leftExpr\n\n\t// Check for postfix ellipsis (foo... or foo...N)\n\tincludeDependencies := false\n\tdependencyDepth := 0\n\n\tif p.curToken.Type == ELLIPSIS {\n\t\tincludeDependencies = true\n\n\t\tp.nextToken()\n\n\t\t// Check for ...N (ellipsis followed by number = dependency depth)\n\t\tif isPurelyNumeric(p.curToken.Literal) {\n\t\t\tdependencyDepth = parseDepth(p.curToken.Literal)\n\t\t\tp.nextToken()\n\t\t}\n\t}\n\n\t// If we have any graph operators, wrap in GraphExpression\n\tif includeDependents || includeDependencies || excludeTarget {\n\t\tleftExpr = &GraphExpression{\n\t\t\tTarget:              target,\n\t\t\tIncludeDependents:   includeDependents,\n\t\t\tIncludeDependencies: includeDependencies,\n\t\t\tExcludeTarget:       excludeTarget,\n\t\t\tDependentDepth:      dependentDepth,\n\t\t\tDependencyDepth:     dependencyDepth,\n\t\t}\n\t}\n\n\tfor p.curToken.Type != EOF && precedence < p.curPrecedence() {\n\t\tswitch p.curToken.Type {\n\t\tcase PIPE:\n\t\t\tleftExpr = p.parseInfixExpression(leftExpr)\n\t\tcase ILLEGAL, EOF, IDENT, PATH, BANG, EQUAL, LBRACE, RBRACE, LBRACKET, RBRACKET, ELLIPSIS, CARET:\n\t\t\treturn leftExpr\n\t\tdefault:\n\t\t\treturn leftExpr\n\t\t}\n\t}\n\n\treturn leftExpr\n}\n\n// isPurelyNumeric returns true if the string contains only digits.\nfunc isPurelyNumeric(s string) bool {\n\tif len(s) == 0 {\n\t\treturn false\n\t}\n\n\tfor _, ch := range s {\n\t\tif ch < '0' || ch > '9' {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// parseDepth parses a depth value from a numeric string.\n// Returns 0 (unlimited) if parsing fails. Clamps to MaxTraversalDepth for very large values.\nfunc parseDepth(literal string) int {\n\tdepth, err := strconv.Atoi(literal)\n\tif err != nil || depth < 0 {\n\t\treturn 0\n\t}\n\n\tif depth > MaxTraversalDepth {\n\t\treturn MaxTraversalDepth\n\t}\n\n\treturn depth\n}\n\n// parsePrefixExpression parses a prefix expression (e.g., \"!name=foo\").\n// It collapses consecutive negations: !! becomes positive, !!! becomes negative, etc.\nfunc (p *Parser) parsePrefixExpression() Expression {\n\t// Count consecutive negation operators\n\tnegationCount := 0\n\tfor p.curToken.Type == BANG {\n\t\tnegationCount++\n\n\t\tp.nextToken()\n\t}\n\n\t// Parse the inner expression\n\tinner := p.parseExpression(PREFIX)\n\tif inner == nil {\n\t\t// Clear any errors from parseExpression (like generic EOF error)\n\t\t// and add our specific error with the EOF title for consistency\n\t\tp.errors = nil\n\t\tp.addMissingOperandError(\"Unexpected end of input\", \"Missing target expression for '!' operator\")\n\n\t\treturn nil\n\t}\n\n\t// If even number of negations, they cancel out - return inner expression directly\n\tif negationCount%2 == 0 {\n\t\treturn inner\n\t}\n\n\t// Odd number of negations - wrap in single PrefixExpression\n\treturn &PrefixExpression{\n\t\tOperator: \"!\",\n\t\tRight:    inner,\n\t}\n}\n\n// parseInfixExpression parses an infix expression (e.g., \"./apps/* | name=bar\").\nfunc (p *Parser) parseInfixExpression(left Expression) Expression {\n\texpression := &InfixExpression{\n\t\tOperator: p.curToken.Literal,\n\t\tLeft:     left,\n\t}\n\n\tprecedence := p.curPrecedence()\n\tp.nextToken()\n\texpression.Right = p.parseExpression(precedence)\n\n\tif expression.Right == nil {\n\t\t// Clear any errors from parseExpression (like generic EOF error)\n\t\t// and add our specific error with the EOF title for consistency\n\t\tp.errors = nil\n\t\tp.addMissingOperandError(\"Unexpected end of input\", \"Missing right-hand side of '|' operator\")\n\n\t\treturn nil\n\t}\n\n\treturn expression\n}\n\n// parsePathFilter parses a path filter (e.g., \"./apps/*\").\nfunc (p *Parser) parsePathFilter() Expression {\n\texpr, err := NewPathFilter(p.curToken.Literal)\n\tif err != nil {\n\t\tp.addErrorWithCode(ErrorCodeInvalidGlob, \"Invalid glob pattern\", \"Invalid glob pattern '\"+p.curToken.Literal+\"': \"+err.Error())\n\t\treturn nil\n\t}\n\n\tp.nextToken()\n\n\treturn expr\n}\n\n// parseBracedPath parses a braced path filter (e.g., \"{./apps/*}\" or \"{my path}\").\nfunc (p *Parser) parseBracedPath() Expression {\n\t// Capture opening brace position for error reporting\n\topenBracePos := p.curToken.Position\n\n\t// We're currently at LBRACE, move to the content\n\tp.nextToken()\n\n\tif p.curToken.Type == RBRACE {\n\t\tp.addErrorWithCode(ErrorCodeEmptyExpression, \"Empty path expression\", \"Braced path expression cannot be empty\")\n\t\treturn nil\n\t}\n\n\t// Read everything until RBRACE as the path\n\tvar pathParts []string\n\tfor p.curToken.Type != RBRACE && p.curToken.Type != EOF {\n\t\tpathParts = append(pathParts, p.curToken.Literal)\n\t\tp.nextToken()\n\t}\n\n\tif p.curToken.Type != RBRACE {\n\t\tp.addErrorAtPosition(ErrorCodeMissingClosingBrace, \"Unclosed path expression\", \"This braced path expression is missing a closing '}'\", openBracePos)\n\t\treturn nil\n\t}\n\n\t// Move past RBRACE\n\tp.nextToken()\n\n\t// Join all parts to form the complete path\n\tpathValue := strings.Join(pathParts, \"\")\n\n\texpr, err := NewPathFilter(pathValue)\n\tif err != nil {\n\t\tp.addErrorWithCode(ErrorCodeInvalidGlob, \"Invalid glob pattern\", \"Invalid glob pattern '\"+pathValue+\"': \"+err.Error())\n\t\treturn nil\n\t}\n\n\treturn expr\n}\n\n// parseAttributeFilter parses an attribute filter (e.g., \"name=foo\").\nfunc (p *Parser) parseAttributeFilter() Expression {\n\tkey := p.curToken.Literal\n\n\tif !p.expectPeek(EQUAL) {\n\t\treturn nil\n\t}\n\n\tp.nextToken()\n\n\tif p.curToken.Type != IDENT && p.curToken.Type != PATH {\n\t\tp.addErrorWithCode(ErrorCodeUnexpectedToken, \"Attribute expression missing value\", \"Attribute expressions require a value after '='\")\n\t\treturn nil\n\t}\n\n\tvalue := p.curToken.Literal\n\tp.nextToken()\n\n\texpr, err := NewAttributeExpression(key, value)\n\tif err != nil {\n\t\tp.addErrorWithCode(ErrorCodeInvalidGlob, \"Invalid glob pattern\", \"Invalid glob pattern in \"+key+\" filter: \"+err.Error())\n\t\treturn nil\n\t}\n\n\treturn expr\n}\n\n// parseGitFilter parses a Git filter expression (e.g., \"[main...HEAD]\" or \"[main]\").\nfunc (p *Parser) parseGitFilter() Expression {\n\t// Capture opening bracket position for error reporting\n\topenBracketPos := p.curToken.Position\n\n\t// We're currently at LBRACKET, move to the content\n\tp.nextToken()\n\n\tif p.curToken.Type == RBRACKET {\n\t\tp.addErrorWithCode(ErrorCodeEmptyGitFilter, \"Empty Git filter\", \"Git filter expression cannot be empty\")\n\t\treturn nil\n\t}\n\n\t// Read the first reference (can be IDENT or PATH-like)\n\tvar fromRefParts []string\n\tfor p.curToken.Type != RBRACKET && p.curToken.Type != ELLIPSIS && p.curToken.Type != EOF {\n\t\tfromRefParts = append(fromRefParts, p.curToken.Literal)\n\t\tp.nextToken()\n\t}\n\n\tif len(fromRefParts) == 0 {\n\t\tp.addErrorWithCode(ErrorCodeMissingGitRef, \"Missing Git reference\", \"Expected Git reference in filter\")\n\t\treturn nil\n\t}\n\n\tfromRef := strings.Join(fromRefParts, \"\")\n\n\t// Check if there's an ellipsis and second reference\n\tif p.curToken.Type == ELLIPSIS {\n\t\t// Move past ellipsis\n\t\tp.nextToken()\n\n\t\t// Read the second reference\n\t\tvar toRefParts []string\n\t\tfor p.curToken.Type != RBRACKET && p.curToken.Type != EOF {\n\t\t\ttoRefParts = append(toRefParts, p.curToken.Literal)\n\t\t\tp.nextToken()\n\t\t}\n\n\t\tif len(toRefParts) == 0 {\n\t\t\tp.addErrorWithCode(ErrorCodeMissingGitRef, \"Missing Git reference\", \"Expected second Git reference after '...'\")\n\t\t\treturn nil\n\t\t}\n\n\t\ttoRef := strings.Join(toRefParts, \"\")\n\n\t\tif p.curToken.Type != RBRACKET {\n\t\t\tp.addErrorAtPosition(ErrorCodeMissingClosingBracket, \"Unclosed Git filter expression\", \"This Git-based expression is missing a closing ']'\", openBracketPos)\n\t\t\treturn nil\n\t\t}\n\n\t\t// Move past RBRACKET\n\t\tp.nextToken()\n\n\t\treturn NewGitExpression(fromRef, toRef)\n\t}\n\n\t// Single reference case\n\tif p.curToken.Type != RBRACKET {\n\t\tp.addErrorAtPosition(ErrorCodeMissingClosingBracket, \"Unclosed Git filter expression\", \"This Git-based expression is missing a closing ']'\", openBracketPos)\n\t\treturn nil\n\t}\n\n\t// Move past RBRACKET\n\tp.nextToken()\n\n\treturn NewGitExpression(fromRef, \"HEAD\")\n}\n\n// expectPeek checks if the next token is of the expected type and advances if so.\nfunc (p *Parser) expectPeek(t TokenType) bool {\n\tif p.peekToken.Type == t {\n\t\tp.nextToken()\n\t\treturn true\n\t}\n\n\tp.addError(\"expected next token to be \" + t.String() + \", got \" + p.peekToken.Type.String())\n\n\treturn false\n}\n\n// curPrecedence returns the precedence of the current token.\nfunc (p *Parser) curPrecedence() int {\n\tif p, ok := precedences[p.curToken.Type]; ok {\n\t\treturn p\n\t}\n\n\treturn LOWEST\n}\n\n// addError adds an error to the parser's error list.\nfunc (p *Parser) addError(msg string) {\n\tp.addErrorWithCode(ErrorCodeUnknown, \"Parse error\", msg)\n}\n\n// addErrorWithCode adds an error with a specific error code for hint lookup.\nfunc (p *Parser) addErrorWithCode(code ErrorCode, title, msg string) {\n\ttokenLen := len(p.curToken.Literal)\n\tif tokenLen == 0 {\n\t\ttokenLen = 1 // Minimum length for underline\n\t}\n\n\terr := NewParseErrorWithContext(\n\t\ttitle,\n\t\tmsg,\n\t\tp.curToken.Position,\n\t\tp.curToken.Position,\n\t\tp.originalQuery,\n\t\tp.curToken.Literal,\n\t\ttokenLen,\n\t\tcode,\n\t)\n\tp.errors = append(p.errors, err)\n}\n\n// addMissingOperandError adds a MissingOperand error with a custom title.\n// This is used when a more specific error replaces a generic EOF error.\nfunc (p *Parser) addMissingOperandError(title, msg string) {\n\ttokenLen := len(p.curToken.Literal)\n\tif tokenLen == 0 {\n\t\ttokenLen = 1 // Minimum length for underline\n\t}\n\n\terr := NewParseErrorWithContext(\n\t\ttitle,\n\t\tmsg,\n\t\tp.curToken.Position,\n\t\tp.curToken.Position,\n\t\tp.originalQuery,\n\t\tp.curToken.Literal,\n\t\ttokenLen,\n\t\tErrorCodeMissingOperand,\n\t)\n\tp.errors = append(p.errors, err)\n}\n\n// addErrorAtPosition adds an error with a specific error code and custom error position for caret placement.\nfunc (p *Parser) addErrorAtPosition(code ErrorCode, title, msg string, errorPosition int) {\n\ttokenLen := 1 // Single character underline for bracket errors\n\n\terr := NewParseErrorWithContext(\n\t\ttitle,\n\t\tmsg,\n\t\tp.curToken.Position,\n\t\terrorPosition,\n\t\tp.originalQuery,\n\t\tp.curToken.Literal,\n\t\ttokenLen,\n\t\tcode,\n\t)\n\tp.errors = append(p.errors, err)\n}\n"
  },
  {
    "path": "internal/filter/parser_test.go",
    "content": "package filter_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParser_SimpleExpressions(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected filter.Expression\n\t\tname     string\n\t\tinput    string\n\t}{\n\t\t{\n\t\t\tname:     \"simple name filter\",\n\t\t\tinput:    \"foo\",\n\t\t\texpected: mustAttr(t, \"name\", \"foo\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"attribute filter\",\n\t\t\tinput:    \"name=bar\",\n\t\t\texpected: mustAttr(t, \"name\", \"bar\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"type attribute filter\",\n\t\t\tinput:    \"type=unit\",\n\t\t\texpected: mustAttr(t, \"type\", \"unit\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"path filter relative\",\n\t\t\tinput:    \"./apps/foo\",\n\t\t\texpected: mustPath(t, \"./apps/foo\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"path filter absolute\",\n\t\t\tinput:    \"/absolute/path\",\n\t\t\texpected: mustPath(t, \"/absolute/path\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"path filter with wildcard\",\n\t\t\tinput:    \"./apps/*\",\n\t\t\texpected: mustPath(t, \"./apps/*\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"path filter with recursive wildcard\",\n\t\t\tinput:    \"./apps/**/foo\",\n\t\t\texpected: mustPath(t, \"./apps/**/foo\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"braced path filter\",\n\t\t\tinput:    \"{./apps/*}\",\n\t\t\texpected: mustPath(t, \"./apps/*\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"braced path without prefix\",\n\t\t\tinput:    \"{apps}\",\n\t\t\texpected: mustPath(t, \"apps\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"braced path with spaces\",\n\t\t\tinput:    \"{my path/file}\",\n\t\t\texpected: mustPath(t, \"my path/file\"),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tlexer := filter.NewLexer(tt.input)\n\t\t\tparser := filter.NewParser(lexer)\n\t\t\texpr, err := parser.ParseExpression()\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, expr)\n\t\t})\n\t}\n}\n\nfunc TestParser_PrefixExpressions(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected filter.Expression\n\t\tname     string\n\t\tinput    string\n\t}{\n\t\t{\n\t\t\tname:  \"negated name filter\",\n\t\t\tinput: \"!foo\",\n\t\t\texpected: &filter.PrefixExpression{\n\t\t\t\tOperator: \"!\",\n\t\t\t\tRight:    mustAttr(t, \"name\", \"foo\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"negated attribute filter\",\n\t\t\tinput: \"!name=bar\",\n\t\t\texpected: &filter.PrefixExpression{\n\t\t\t\tOperator: \"!\",\n\t\t\t\tRight:    mustAttr(t, \"name\", \"bar\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"negated path filter\",\n\t\t\tinput: \"!./apps/legacy\",\n\t\t\texpected: &filter.PrefixExpression{\n\t\t\t\tOperator: \"!\",\n\t\t\t\tRight:    mustPath(t, \"./apps/legacy\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"negated braced path filter\",\n\t\t\tinput: \"!{./apps/legacy}\",\n\t\t\texpected: &filter.PrefixExpression{\n\t\t\t\tOperator: \"!\",\n\t\t\t\tRight:    mustPath(t, \"./apps/legacy\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"negated braced path filter with absolute path\",\n\t\t\tinput: \"!{/absolute/path}\",\n\t\t\texpected: &filter.PrefixExpression{\n\t\t\t\tOperator: \"!\",\n\t\t\t\tRight:    mustPath(t, \"/absolute/path\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"double negation collapses to positive\",\n\t\t\tinput:    \"!!foo\",\n\t\t\texpected: mustAttr(t, \"name\", \"foo\"),\n\t\t},\n\t\t{\n\t\t\tname:  \"triple negation collapses to single negative\",\n\t\t\tinput: \"!!!foo\",\n\t\t\texpected: &filter.PrefixExpression{\n\t\t\t\tOperator: \"!\",\n\t\t\t\tRight:    mustAttr(t, \"name\", \"foo\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"quadruple negation collapses to positive\",\n\t\t\tinput:    \"!!!!foo\",\n\t\t\texpected: mustAttr(t, \"name\", \"foo\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"double negation with attribute filter\",\n\t\t\tinput:    \"!!name=bar\",\n\t\t\texpected: mustAttr(t, \"name\", \"bar\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"double negation with path filter\",\n\t\t\tinput:    \"!!./apps/foo\",\n\t\t\texpected: mustPath(t, \"./apps/foo\"),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tlexer := filter.NewLexer(tt.input)\n\t\t\tparser := filter.NewParser(lexer)\n\t\t\texpr, err := parser.ParseExpression()\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, expr)\n\t\t})\n\t}\n}\n\nfunc TestParser_InfixExpressions(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected filter.Expression\n\t\tname     string\n\t\tinput    string\n\t}{\n\t\t{\n\t\t\tname:  \"union of two name filters\",\n\t\t\tinput: \"foo | bar\",\n\t\t\texpected: &filter.InfixExpression{\n\t\t\t\tLeft:     mustAttr(t, \"name\", \"foo\"),\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight:    mustAttr(t, \"name\", \"bar\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"union of attribute filters\",\n\t\t\tinput: \"name=foo | name=bar\",\n\t\t\texpected: &filter.InfixExpression{\n\t\t\t\tLeft:     mustAttr(t, \"name\", \"foo\"),\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight:    mustAttr(t, \"name\", \"bar\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"union of path and name filter\",\n\t\t\tinput: \"./apps/* | name=bar\",\n\t\t\texpected: &filter.InfixExpression{\n\t\t\t\tLeft:     mustPath(t, \"./apps/*\"),\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight:    mustAttr(t, \"name\", \"bar\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"union of three filters\",\n\t\t\tinput: \"foo | bar | baz\",\n\t\t\texpected: &filter.InfixExpression{\n\t\t\t\tLeft: &filter.InfixExpression{\n\t\t\t\t\tLeft:     mustAttr(t, \"name\", \"foo\"),\n\t\t\t\t\tOperator: \"|\",\n\t\t\t\t\tRight:    mustAttr(t, \"name\", \"bar\"),\n\t\t\t\t},\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight:    mustAttr(t, \"name\", \"baz\"),\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\tt.Parallel()\n\n\t\t\tlexer := filter.NewLexer(tt.input)\n\t\t\tparser := filter.NewParser(lexer)\n\t\t\texpr, err := parser.ParseExpression()\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, expr)\n\t\t})\n\t}\n}\n\nfunc TestParser_ComplexExpressions(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected filter.Expression\n\t\tname     string\n\t\tinput    string\n\t}{\n\t\t{\n\t\t\tname:  \"negated filter in union\",\n\t\t\tinput: \"!foo | bar\",\n\t\t\texpected: &filter.InfixExpression{\n\t\t\t\tLeft: &filter.PrefixExpression{\n\t\t\t\t\tOperator: \"!\",\n\t\t\t\t\tRight:    mustAttr(t, \"name\", \"foo\"),\n\t\t\t\t},\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight:    mustAttr(t, \"name\", \"bar\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"union with negated second operand\",\n\t\t\tinput: \"foo | !bar\",\n\t\t\texpected: &filter.InfixExpression{\n\t\t\t\tLeft:     mustAttr(t, \"name\", \"foo\"),\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight: &filter.PrefixExpression{\n\t\t\t\t\tOperator: \"!\",\n\t\t\t\t\tRight:    mustAttr(t, \"name\", \"bar\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"complex mix of paths and attributes\",\n\t\t\tinput: \"./apps/* | !./legacy | name=foo\",\n\t\t\texpected: &filter.InfixExpression{\n\t\t\t\tLeft: &filter.InfixExpression{\n\t\t\t\t\tLeft:     mustPath(t, \"./apps/*\"),\n\t\t\t\t\tOperator: \"|\",\n\t\t\t\t\tRight: &filter.PrefixExpression{\n\t\t\t\t\t\tOperator: \"!\",\n\t\t\t\t\t\tRight:    mustPath(t, \"./legacy\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight:    mustAttr(t, \"name\", \"foo\"),\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\tt.Parallel()\n\n\t\t\tlexer := filter.NewLexer(tt.input)\n\t\t\tparser := filter.NewParser(lexer)\n\t\t\texpr, err := parser.ParseExpression()\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, expr)\n\t\t})\n\t}\n}\n\nfunc TestParser_ErrorCases(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname        string\n\t\tinput       string\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname:        \"empty input\",\n\t\t\tinput:       \"\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"only operator\",\n\t\t\tinput:       \"!\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"missing value after equal\",\n\t\t\tinput:       \"name=\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"missing right side of union\",\n\t\t\tinput:       \"foo |\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid token\",\n\t\t\tinput:       \"foo|\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"trailing pipe\",\n\t\t\tinput:       \"foo | bar |\",\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tlexer := filter.NewLexer(tt.input)\n\t\t\tparser := filter.NewParser(lexer)\n\t\t\texpr, err := parser.ParseExpression()\n\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Nil(t, expr)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.NotNil(t, expr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParser_StringRepresentation(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"simple name filter\",\n\t\t\tinput:    \"foo\",\n\t\t\texpected: \"name=foo\",\n\t\t},\n\t\t{\n\t\t\tname:     \"path filter\",\n\t\t\tinput:    \"./apps/*\",\n\t\t\texpected: \"./apps/*\",\n\t\t},\n\t\t{\n\t\t\tname:     \"negated filter\",\n\t\t\tinput:    \"!foo\",\n\t\t\texpected: \"!name=foo\",\n\t\t},\n\t\t{\n\t\t\tname:     \"union filter\",\n\t\t\tinput:    \"foo | bar\",\n\t\t\texpected: \"name=foo | name=bar\",\n\t\t},\n\t\t{\n\t\t\tname:     \"graph expression with dependents\",\n\t\t\tinput:    \"...foo\",\n\t\t\texpected: \"...name=foo\",\n\t\t},\n\t\t{\n\t\t\tname:     \"graph expression with dependencies\",\n\t\t\tinput:    \"foo...\",\n\t\t\texpected: \"name=foo...\",\n\t\t},\n\t\t{\n\t\t\tname:     \"graph expression with dependent depth\",\n\t\t\tinput:    \"1...foo\",\n\t\t\texpected: \"1...name=foo\",\n\t\t},\n\t\t{\n\t\t\tname:     \"graph expression with dependency depth\",\n\t\t\tinput:    \"foo...1\",\n\t\t\texpected: \"name=foo...1\",\n\t\t},\n\t\t{\n\t\t\tname:     \"graph expression with both depths\",\n\t\t\tinput:    \"2...foo...3\",\n\t\t\texpected: \"2...name=foo...3\",\n\t\t},\n\t\t{\n\t\t\tname:     \"graph expression with caret\",\n\t\t\tinput:    \"...^foo...\",\n\t\t\texpected: \"...^name=foo...\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tlexer := filter.NewLexer(tt.input)\n\t\t\tparser := filter.NewParser(lexer)\n\t\t\texpr, err := parser.ParseExpression()\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, expr.String())\n\t\t})\n\t}\n}\n\nfunc TestParser_GraphExpressions(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected filter.Expression\n\t\tname     string\n\t\tinput    string\n\t}{\n\t\t{\n\t\t\tname:  \"prefix ellipsis - dependents only\",\n\t\t\tinput: \"...foo\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"foo\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: false,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"postfix ellipsis - dependencies only\",\n\t\t\tinput: \"foo...\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"foo\"),\n\t\t\t\tIncludeDependents:   false,\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"both prefix and postfix ellipsis\",\n\t\t\tinput: \"...foo...\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"foo\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"caret - exclude target only\",\n\t\t\tinput: \"^foo\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"foo\"),\n\t\t\t\tIncludeDependents:   false,\n\t\t\t\tIncludeDependencies: false,\n\t\t\t\tExcludeTarget:       true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"caret with prefix ellipsis\",\n\t\t\tinput: \"...^foo\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"foo\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: false,\n\t\t\t\tExcludeTarget:       true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"caret with postfix ellipsis\",\n\t\t\tinput: \"^foo...\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"foo\"),\n\t\t\t\tIncludeDependents:   false,\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tExcludeTarget:       true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"caret with both ellipsis\",\n\t\t\tinput: \"...^foo...\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"foo\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tExcludeTarget:       true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"graph expression with path filter\",\n\t\t\tinput: \"...{./apps/foo}\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustPath(t, \"./apps/foo\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: false,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"graph expression with path filter and postfix ellipsis\",\n\t\t\tinput: \"./apps/foo...\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustPath(t, \"./apps/foo\"),\n\t\t\t\tIncludeDependents:   false,\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"graph expression with attribute filter\",\n\t\t\tinput: \"...name=bar\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"bar\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: false,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"graph expression with braced path and postfix ellipsis\",\n\t\t\tinput: \"{./apps/foo}...\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustPath(t, \"./apps/foo\"),\n\t\t\t\tIncludeDependents:   false,\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"graph expression with braced path and both ellipsis\",\n\t\t\tinput: \"...{./apps/foo}...\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustPath(t, \"./apps/foo\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"graph expression with braced path, caret, and both ellipsis\",\n\t\t\tinput: \"...^{./apps/foo}...\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustPath(t, \"./apps/foo\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tExcludeTarget:       true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"depth-limited prefix - direct dependents only\",\n\t\t\tinput: \"1...foo\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"foo\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: false,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t\tDependentDepth:      1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"depth-limited postfix - direct dependencies only\",\n\t\t\tinput: \"foo...1\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"foo\"),\n\t\t\t\tIncludeDependents:   false,\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t\tDependencyDepth:     1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"depth-limited both directions\",\n\t\t\tinput: \"2...foo...3\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"foo\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t\tDependentDepth:      2,\n\t\t\t\tDependencyDepth:     3,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"depth-limited with caret\",\n\t\t\tinput: \"1...^foo...2\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"foo\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tExcludeTarget:       true,\n\t\t\t\tDependentDepth:      1,\n\t\t\t\tDependencyDepth:     2,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"depth-limited with multi-digit depth\",\n\t\t\tinput: \"10...foo...25\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"foo\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t\tDependentDepth:      10,\n\t\t\t\tDependencyDepth:     25,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"very large depth clamps to max\",\n\t\t\tinput: \"999999999...foo\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"foo\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: false,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t\tDependentDepth:      filter.MaxTraversalDepth,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"overflow depth falls back to unlimited\",\n\t\t\tinput: \"99999999999999999999999...foo\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"foo\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: false,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t\tDependentDepth:      0,\n\t\t\t},\n\t\t},\n\t\t// Numeric directory edge cases - testing disambiguation\n\t\t{\n\t\t\tname:  \"numeric dir with depth - number before ellipsis is depth\",\n\t\t\tinput: \"1...1\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"1\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: false,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t\tDependentDepth:      1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"numeric dir escape hatch - braced path for target with dependency depth\",\n\t\t\tinput: \"{1}...1\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustPath(t, \"1\"),\n\t\t\t\tIncludeDependents:   false,\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t\tDependencyDepth:     1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"numeric dir escape hatch - braced path for target with dependent depth\",\n\t\t\tinput: \"1...{1}\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustPath(t, \"1\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: false,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t\tDependentDepth:      1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"numeric dir escape hatch - explicit name attribute with dependency depth\",\n\t\t\tinput: \"name=1...1\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"1\"),\n\t\t\t\tIncludeDependents:   false,\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t\tDependencyDepth:     1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"numeric dir escape hatch - explicit name attribute with dependent depth\",\n\t\t\tinput: \"1...name=1\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"1\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: false,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t\tDependentDepth:      1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"numeric dir full escape - both directions with braces\",\n\t\t\tinput: \"1...{1}...1\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustPath(t, \"1\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t\tDependentDepth:      1,\n\t\t\t\tDependencyDepth:     1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"alphanumeric dir not confused with depth\",\n\t\t\tinput: \"1...1foo\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"1foo\"),\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: false,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t\tDependentDepth:      1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"alphanumeric dir not confused with depth - postfix\",\n\t\t\tinput: \"foo1...1\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              mustAttr(t, \"name\", \"foo1\"),\n\t\t\t\tIncludeDependents:   false,\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t\tDependencyDepth:     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\tt.Parallel()\n\n\t\t\tlexer := filter.NewLexer(tt.input)\n\t\t\tparser := filter.NewParser(lexer)\n\t\t\texpr, err := parser.ParseExpression()\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\tgraphExpr, ok := expr.(*filter.GraphExpression)\n\t\t\trequire.True(t, ok, \"Expected GraphExpression, got %T\", expr)\n\n\t\t\tassert.Equal(t, tt.expected.(*filter.GraphExpression).IncludeDependents, graphExpr.IncludeDependents)\n\t\t\tassert.Equal(t, tt.expected.(*filter.GraphExpression).IncludeDependencies, graphExpr.IncludeDependencies)\n\t\t\tassert.Equal(t, tt.expected.(*filter.GraphExpression).ExcludeTarget, graphExpr.ExcludeTarget)\n\t\t\tassert.Equal(t, tt.expected.(*filter.GraphExpression).Target, graphExpr.Target)\n\t\t\tassert.Equal(t, tt.expected.(*filter.GraphExpression).DependentDepth, graphExpr.DependentDepth)\n\t\t\tassert.Equal(t, tt.expected.(*filter.GraphExpression).DependencyDepth, graphExpr.DependencyDepth)\n\t\t})\n\t}\n}\n\nfunc TestParser_GraphExpressionCombinations(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected filter.Expression\n\t\tname     string\n\t\tinput    string\n\t}{\n\t\t{\n\t\t\tname:  \"graph expression in union - left side\",\n\t\t\tinput: \"...foo | bar\",\n\t\t\texpected: &filter.InfixExpression{\n\t\t\t\tLeft: &filter.GraphExpression{\n\t\t\t\t\tTarget:              mustAttr(t, \"name\", \"foo\"),\n\t\t\t\t\tIncludeDependents:   true,\n\t\t\t\t\tIncludeDependencies: false,\n\t\t\t\t\tExcludeTarget:       false,\n\t\t\t\t},\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight:    mustAttr(t, \"name\", \"bar\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"graph expression in union - right side\",\n\t\t\tinput: \"foo | bar...\",\n\t\t\texpected: &filter.InfixExpression{\n\t\t\t\tLeft:     mustAttr(t, \"name\", \"foo\"),\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight: &filter.GraphExpression{\n\t\t\t\t\tTarget:              mustAttr(t, \"name\", \"bar\"),\n\t\t\t\t\tIncludeDependents:   false,\n\t\t\t\t\tIncludeDependencies: true,\n\t\t\t\t\tExcludeTarget:       false,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"negated graph expression\",\n\t\t\tinput: \"!...foo\",\n\t\t\texpected: &filter.PrefixExpression{\n\t\t\t\tOperator: \"!\",\n\t\t\t\tRight: &filter.GraphExpression{\n\t\t\t\t\tTarget:              mustAttr(t, \"name\", \"foo\"),\n\t\t\t\t\tIncludeDependents:   true,\n\t\t\t\t\tIncludeDependencies: false,\n\t\t\t\t\tExcludeTarget:       false,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"graph expression with negation inside\",\n\t\t\tinput: \"...!foo\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget: &filter.PrefixExpression{\n\t\t\t\t\tOperator: \"!\",\n\t\t\t\t\tRight:    mustAttr(t, \"name\", \"foo\"),\n\t\t\t\t},\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tIncludeDependencies: false,\n\t\t\t\tExcludeTarget:       false,\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\tt.Parallel()\n\n\t\t\tlexer := filter.NewLexer(tt.input)\n\t\t\tparser := filter.NewParser(lexer)\n\t\t\texpr, err := parser.ParseExpression()\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, expr)\n\t\t})\n\t}\n}\n\nfunc TestParser_GraphExpressionErrors(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname        string\n\t\tinput       string\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname:        \"ellipsis only\",\n\t\t\tinput:       \"...\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"caret only\",\n\t\t\tinput:       \"^\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"ellipsis followed by operator\",\n\t\t\tinput:       \"... |\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"caret followed by operator\",\n\t\t\tinput:       \"^ |\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"incomplete ellipsis\",\n\t\t\tinput:       \"..foo\",\n\t\t\texpectError: false, // This parses as name filter \"..foo\", not an error\n\t\t},\n\t\t{\n\t\t\tname:        \"depth without target\",\n\t\t\tinput:       \"1...\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"depth without target and trailing space\",\n\t\t\tinput:       \"1... \",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"double depth no target\",\n\t\t\tinput:       \"1......2\",\n\t\t\texpectError: true, // 1... then ...2 with no target between\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tlexer := filter.NewLexer(tt.input)\n\t\t\tparser := filter.NewParser(lexer)\n\t\t\texpr, err := parser.ParseExpression()\n\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Nil(t, expr)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// For non-error cases, just verify it parses\n\t\t\tif err != nil {\n\t\t\t\tt.Logf(\"Unexpected error: %v\", err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParser_GitFilterExpressions(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected filter.Expression\n\t\tname     string\n\t\tinput    string\n\t}{\n\t\t{\n\t\t\tname:     \"single Git reference\",\n\t\t\tinput:    \"[main]\",\n\t\t\texpected: filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"two Git references with ellipsis\",\n\t\t\tinput:    \"[main...HEAD]\",\n\t\t\texpected: filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"Git reference with branch name\",\n\t\t\tinput:    \"[feature-branch]\",\n\t\t\texpected: filter.NewGitExpression(\"feature-branch\", \"HEAD\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"Git reference with commit SHA\",\n\t\t\tinput:    \"[abc123...def456]\",\n\t\t\texpected: filter.NewGitExpression(\"abc123\", \"def456\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"Git reference with tag\",\n\t\t\tinput:    \"[v1.0.0...v2.0.0]\",\n\t\t\texpected: filter.NewGitExpression(\"v1.0.0\", \"v2.0.0\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"Git reference with relative ref\",\n\t\t\tinput:    \"[HEAD~1...HEAD]\",\n\t\t\texpected: filter.NewGitExpression(\"HEAD~1\", \"HEAD\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"Git reference with underscore in branch name\",\n\t\t\tinput:    \"[feature_branch]\",\n\t\t\texpected: filter.NewGitExpression(\"feature_branch\", \"HEAD\"),\n\t\t},\n\t\t{\n\t\t\tname:     \"Git reference with slash in branch name\",\n\t\t\tinput:    \"[feature/name]\",\n\t\t\texpected: filter.NewGitExpression(\"feature/name\", \"HEAD\"),\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tlexer := filter.NewLexer(tt.input)\n\t\t\tparser := filter.NewParser(lexer)\n\t\t\texpr, err := parser.ParseExpression()\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, expr)\n\t\t})\n\t}\n}\n\nfunc TestParser_GitFilterWithOtherExpressions(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected filter.Expression\n\t\tname     string\n\t\tinput    string\n\t}{\n\t\t{\n\t\t\tname:  \"Git filter with negation\",\n\t\t\tinput: \"![main...HEAD]\",\n\t\t\texpected: &filter.PrefixExpression{\n\t\t\t\tOperator: \"!\",\n\t\t\t\tRight:    filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"Git filter with path filter intersection\",\n\t\t\tinput: \"[main...HEAD] | ./apps/*\",\n\t\t\texpected: &filter.InfixExpression{\n\t\t\t\tLeft:     filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight:    mustPath(t, \"./apps/*\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"path filter with Git filter intersection\",\n\t\t\tinput: \"./apps/* | [main...HEAD]\",\n\t\t\texpected: &filter.InfixExpression{\n\t\t\t\tLeft:     mustPath(t, \"./apps/*\"),\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight:    filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"Git filter with name filter intersection\",\n\t\t\tinput: \"[main...HEAD] | name=app\",\n\t\t\texpected: &filter.InfixExpression{\n\t\t\t\tLeft:     filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight:    mustAttr(t, \"name\", \"app\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"Git filter with graph expression\",\n\t\t\tinput: \"[main...HEAD] | app...\",\n\t\t\texpected: &filter.InfixExpression{\n\t\t\t\tLeft:     filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\t\tOperator: \"|\",\n\t\t\t\tRight: &filter.GraphExpression{\n\t\t\t\t\tTarget:              mustAttr(t, \"name\", \"app\"),\n\t\t\t\t\tIncludeDependencies: true,\n\t\t\t\t\tIncludeDependents:   false,\n\t\t\t\t\tExcludeTarget:       false,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tlexer := filter.NewLexer(tt.input)\n\t\t\tparser := filter.NewParser(lexer)\n\t\t\texpr, err := parser.ParseExpression()\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, expr)\n\t\t})\n\t}\n}\n\nfunc TestParser_GitFilterErrors(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname        string\n\t\tinput       string\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname:        \"empty Git filter\",\n\t\t\tinput:       \"[]\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"unclosed Git filter\",\n\t\t\tinput:       \"[main\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"Git filter with only ellipsis\",\n\t\t\tinput:       \"[...]\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"Git filter with ellipsis but no second ref\",\n\t\t\tinput:       \"[main...]\",\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"Git filter with only closing bracket\",\n\t\t\tinput:       \"]\",\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tlexer := filter.NewLexer(tt.input)\n\t\t\tparser := filter.NewParser(lexer)\n\t\t\texpr, err := parser.ParseExpression()\n\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Nil(t, expr)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestParser_GitFilterAsGraphExpressionTarget tests parsing of combined git + graph expressions\n// where a GitExpression is used as the target of a GraphExpression.\nfunc TestParser_GitFilterAsGraphExpressionTarget(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected filter.Expression\n\t\tname     string\n\t\tinput    string\n\t}{\n\t\t{\n\t\t\tname:  \"dependencies of git changes - postfix ellipsis\",\n\t\t\tinput: \"[main...HEAD]...\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tIncludeDependents:   false,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"dependents of git changes - prefix ellipsis\",\n\t\t\tinput: \"...[main...HEAD]\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\t\tIncludeDependencies: false,\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"both directions of git changes - issue #5307 pattern\",\n\t\t\tinput: \"...[main...HEAD]...\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"exclude target with dependencies of git changes\",\n\t\t\tinput: \"^[main...HEAD]...\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tIncludeDependents:   false,\n\t\t\t\tExcludeTarget:       true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"exclude target with dependents of git changes\",\n\t\t\tinput: \"...^[main...HEAD]\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\t\tIncludeDependencies: false,\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tExcludeTarget:       true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"exclude target with both directions of git changes\",\n\t\t\tinput: \"...^[main...HEAD]...\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tExcludeTarget:       true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"single git ref with dependencies\",\n\t\t\tinput: \"[main]...\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tIncludeDependents:   false,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"single git ref with dependents\",\n\t\t\tinput: \"...[main]\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              filter.NewGitExpression(\"main\", \"HEAD\"),\n\t\t\t\tIncludeDependencies: false,\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"git ref with commit SHA and both directions\",\n\t\t\tinput: \"...[abc123...def456]...\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              filter.NewGitExpression(\"abc123\", \"def456\"),\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tIncludeDependents:   true,\n\t\t\t\tExcludeTarget:       false,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"git ref with relative ref (HEAD~1) and dependencies\",\n\t\t\tinput: \"[HEAD~1...HEAD]...\",\n\t\t\texpected: &filter.GraphExpression{\n\t\t\t\tTarget:              filter.NewGitExpression(\"HEAD~1\", \"HEAD\"),\n\t\t\t\tIncludeDependencies: true,\n\t\t\t\tIncludeDependents:   false,\n\t\t\t\tExcludeTarget:       false,\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\tt.Parallel()\n\n\t\t\tlexer := filter.NewLexer(tt.input)\n\t\t\tparser := filter.NewParser(lexer)\n\t\t\texpr, err := parser.ParseExpression()\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\tgraphExpr, ok := expr.(*filter.GraphExpression)\n\t\t\trequire.True(t, ok, \"Expected GraphExpression, got %T\", expr)\n\n\t\t\texpectedGraph := tt.expected.(*filter.GraphExpression)\n\t\t\tassert.Equal(t, expectedGraph.IncludeDependents, graphExpr.IncludeDependents, \"IncludeDependents mismatch\")\n\t\t\tassert.Equal(t, expectedGraph.IncludeDependencies, graphExpr.IncludeDependencies, \"IncludeDependencies mismatch\")\n\t\t\tassert.Equal(t, expectedGraph.ExcludeTarget, graphExpr.ExcludeTarget, \"ExcludeTarget mismatch\")\n\n\t\t\tgitExpr, ok := graphExpr.Target.(*filter.GitExpression)\n\t\t\trequire.True(t, ok, \"Expected GitExpression as target, got %T\", graphExpr.Target)\n\n\t\t\texpectedGit := expectedGraph.Target.(*filter.GitExpression)\n\t\t\tassert.Equal(t, expectedGit.FromRef, gitExpr.FromRef, \"FromRef mismatch\")\n\t\t\tassert.Equal(t, expectedGit.ToRef, gitExpr.ToRef, \"ToRef mismatch\")\n\t\t})\n\t}\n}\n\n// TestParser_GitFilterAsGraphExpressionTarget_StringRepresentation tests that\n// combined git + graph expressions produce correct string representations.\nfunc TestParser_GitFilterAsGraphExpressionTarget_StringRepresentation(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"dependencies of git changes\",\n\t\t\tinput:    \"[main...HEAD]...\",\n\t\t\texpected: \"[main...HEAD]...\",\n\t\t},\n\t\t{\n\t\t\tname:     \"dependents of git changes\",\n\t\t\tinput:    \"...[main...HEAD]\",\n\t\t\texpected: \"...[main...HEAD]\",\n\t\t},\n\t\t{\n\t\t\tname:     \"both directions of git changes\",\n\t\t\tinput:    \"...[main...HEAD]...\",\n\t\t\texpected: \"...[main...HEAD]...\",\n\t\t},\n\t\t{\n\t\t\tname:     \"exclude target with both directions\",\n\t\t\tinput:    \"...^[main...HEAD]...\",\n\t\t\texpected: \"...^[main...HEAD]...\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single ref defaults to HEAD\",\n\t\t\tinput:    \"[main]...\",\n\t\t\texpected: \"[main...HEAD]...\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tlexer := filter.NewLexer(tt.input)\n\t\t\tparser := filter.NewParser(lexer)\n\t\t\texpr, err := parser.ParseExpression()\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expected, expr.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/filter/telemetry.go",
    "content": "// Package filter provides telemetry support for git worktree operations and filter evaluation.\npackage filter\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n)\n\n// Telemetry operation names for git worktree and filter operations.\nconst (\n\t// Git worktree operations\n\tTelemetryOpGitWorktreeCreate      = \"git_worktree_create\"\n\tTelemetryOpGitWorktreeRemove      = \"git_worktree_remove\"\n\tTelemetryOpGitWorktreesCreate     = \"git_worktrees_create\"\n\tTelemetryOpGitWorktreesCleanup    = \"git_worktrees_cleanup\"\n\tTelemetryOpGitDiff                = \"git_diff\"\n\tTelemetryOpGitWorktreeDiscovery   = \"git_worktree_discovery\"\n\tTelemetryOpGitWorktreeStackWalk   = \"git_worktree_stack_walk\"\n\tTelemetryOpGitWorktreeFilterApply = \"git_worktree_filter_apply\"\n\n\t// Filter evaluation operations\n\tTelemetryOpFilterEvaluate      = \"filter_evaluate\"\n\tTelemetryOpFilterParse         = \"filter_parse\"\n\tTelemetryOpGitFilterExpand     = \"git_filter_expand\"\n\tTelemetryOpGitFilterEvaluate   = \"git_filter_evaluate\"\n\tTelemetryOpGraphFilterTraverse = \"graph_filter_traverse\"\n)\n\n// Telemetry attribute keys for git worktree operations.\nconst (\n\tAttrGitRef         = \"git.ref\"\n\tAttrGitFromRef     = \"git.from_ref\"\n\tAttrGitToRef       = \"git.to_ref\"\n\tAttrGitWorktreeDir = \"git.worktree_dir\"\n\tAttrGitWorkingDir  = \"git.working_dir\"\n\tAttrGitRefCount    = \"git.ref_count\"\n\tAttrGitDiffAdded   = \"git.diff.added_count\"\n\tAttrGitDiffRemoved = \"git.diff.removed_count\"\n\tAttrGitDiffChanged = \"git.diff.changed_count\"\n\n\t// Repository identification attributes\n\tAttrGitRepoRemote = \"git.repo.remote\"\n\tAttrGitRepoBranch = \"git.repo.branch\"\n\tAttrGitRepoCommit = \"git.repo.commit\"\n\n\tAttrFilterQuery       = \"filter.query\"\n\tAttrFilterType        = \"filter.type\"\n\tAttrFilterCount       = \"filter.count\"\n\tAttrComponentCount    = \"component.count\"\n\tAttrResultCount       = \"result.count\"\n\tAttrWorktreePairCount = \"worktree.pair_count\"\n)\n\n// TraceGitWorktreeCreate wraps a git worktree create operation with telemetry.\n// The underlying Telemeter.Collect handles nil/unconfigured telemetry gracefully.\nfunc TraceGitWorktreeCreate(ctx context.Context, ref, worktreeDir, repoRemote, repoBranch, repoCommit string, fn func(ctx context.Context) error) error {\n\tattrs := map[string]any{\n\t\tAttrGitRef:         ref,\n\t\tAttrGitWorktreeDir: worktreeDir,\n\t}\n\tif repoRemote != \"\" {\n\t\tattrs[AttrGitRepoRemote] = repoRemote\n\t}\n\n\tif repoBranch != \"\" {\n\t\tattrs[AttrGitRepoBranch] = repoBranch\n\t}\n\n\tif repoCommit != \"\" {\n\t\tattrs[AttrGitRepoCommit] = repoCommit\n\t}\n\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpGitWorktreeCreate, attrs, fn)\n}\n\n// TraceGitWorktreeRemove wraps a git worktree remove operation with telemetry.\nfunc TraceGitWorktreeRemove(ctx context.Context, ref, worktreeDir string, fn func(ctx context.Context) error) error {\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpGitWorktreeRemove, map[string]any{\n\t\tAttrGitRef:         ref,\n\t\tAttrGitWorktreeDir: worktreeDir,\n\t}, fn)\n}\n\n// TraceGitWorktreesCreate wraps multiple git worktree create operations with telemetry.\nfunc TraceGitWorktreesCreate(ctx context.Context, workingDir string, refCount int, repoRemote, repoBranch, repoCommit string, fn func(ctx context.Context) error) error {\n\tattrs := map[string]any{\n\t\tAttrGitWorkingDir: workingDir,\n\t\tAttrGitRefCount:   refCount,\n\t}\n\tif repoRemote != \"\" {\n\t\tattrs[AttrGitRepoRemote] = repoRemote\n\t}\n\n\tif repoBranch != \"\" {\n\t\tattrs[AttrGitRepoBranch] = repoBranch\n\t}\n\n\tif repoCommit != \"\" {\n\t\tattrs[AttrGitRepoCommit] = repoCommit\n\t}\n\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpGitWorktreesCreate, attrs, fn)\n}\n\n// TraceGitWorktreesCleanup wraps git worktrees cleanup with telemetry.\nfunc TraceGitWorktreesCleanup(ctx context.Context, pairCount int, repoRemote string, fn func(ctx context.Context) error) error {\n\tattrs := map[string]any{\n\t\tAttrWorktreePairCount: pairCount,\n\t}\n\tif repoRemote != \"\" {\n\t\tattrs[AttrGitRepoRemote] = repoRemote\n\t}\n\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpGitWorktreesCleanup, attrs, fn)\n}\n\n// TraceGitDiff wraps a git diff operation with telemetry.\nfunc TraceGitDiff(ctx context.Context, fromRef, toRef, repoRemote string, fn func(ctx context.Context) error) error {\n\tattrs := map[string]any{\n\t\tAttrGitFromRef: fromRef,\n\t\tAttrGitToRef:   toRef,\n\t}\n\tif repoRemote != \"\" {\n\t\tattrs[AttrGitRepoRemote] = repoRemote\n\t}\n\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpGitDiff, attrs, fn)\n}\n\n// TraceGitWorktreeDiscovery wraps git worktree discovery operations with telemetry.\nfunc TraceGitWorktreeDiscovery(ctx context.Context, pairCount int, fn func(ctx context.Context) error) error {\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpGitWorktreeDiscovery, map[string]any{\n\t\tAttrWorktreePairCount: pairCount,\n\t}, fn)\n}\n\n// TraceGitWorktreeStackWalk wraps git worktree stack walking operations with telemetry.\nfunc TraceGitWorktreeStackWalk(ctx context.Context, fromRef, toRef string, fn func(ctx context.Context) error) error {\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpGitWorktreeStackWalk, map[string]any{\n\t\tAttrGitFromRef: fromRef,\n\t\tAttrGitToRef:   toRef,\n\t}, fn)\n}\n\n// TraceGitWorktreeFilterApply wraps filter application to git worktrees with telemetry.\nfunc TraceGitWorktreeFilterApply(ctx context.Context, filterCount, resultCount int, fn func(ctx context.Context) error) error {\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpGitWorktreeFilterApply, map[string]any{\n\t\tAttrFilterCount: filterCount,\n\t\tAttrResultCount: resultCount,\n\t}, fn)\n}\n\n// TraceFilterEvaluate wraps filter evaluation with telemetry.\nfunc TraceFilterEvaluate(ctx context.Context, filterCount, componentCount int, fn func(ctx context.Context) error) error {\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpFilterEvaluate, map[string]any{\n\t\tAttrFilterCount:    filterCount,\n\t\tAttrComponentCount: componentCount,\n\t}, fn)\n}\n\n// TraceFilterParse wraps filter parsing with telemetry.\nfunc TraceFilterParse(ctx context.Context, query string, fn func(ctx context.Context) error) error {\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpFilterParse, map[string]any{\n\t\tAttrFilterQuery: query,\n\t}, fn)\n}\n\n// TraceGitFilterExpand wraps git filter expansion with telemetry.\nfunc TraceGitFilterExpand(ctx context.Context, fromRef, toRef string, addedCount, removedCount, changedCount int, fn func(ctx context.Context) error) error {\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpGitFilterExpand, map[string]any{\n\t\tAttrGitFromRef:     fromRef,\n\t\tAttrGitToRef:       toRef,\n\t\tAttrGitDiffAdded:   addedCount,\n\t\tAttrGitDiffRemoved: removedCount,\n\t\tAttrGitDiffChanged: changedCount,\n\t}, fn)\n}\n\n// TraceGitFilterEvaluate wraps git filter evaluation with telemetry.\nfunc TraceGitFilterEvaluate(ctx context.Context, fromRef, toRef string, componentCount int, fn func(ctx context.Context) error) error {\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpGitFilterEvaluate, map[string]any{\n\t\tAttrGitFromRef:     fromRef,\n\t\tAttrGitToRef:       toRef,\n\t\tAttrComponentCount: componentCount,\n\t}, fn)\n}\n\n// TraceGraphFilterTraverse wraps graph filter traversal with telemetry.\nfunc TraceGraphFilterTraverse(ctx context.Context, filterType string, componentCount int, fn func(ctx context.Context) error) error {\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpGraphFilterTraverse, map[string]any{\n\t\tAttrFilterType:     filterType,\n\t\tAttrComponentCount: componentCount,\n\t}, fn)\n}\n"
  },
  {
    "path": "internal/filter/telemetry_test.go",
    "content": "package filter_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestTraceGitWorktreeCreate_NoTelemeter(t *testing.T) {\n\tt.Parallel()\n\n\tcalled := false\n\terr := filter.TraceGitWorktreeCreate(context.Background(), \"main\", \"/tmp/worktree\", \"git@github.com:org/repo.git\", \"main\", \"abc123\", func(ctx context.Context) error {\n\t\tcalled = true\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n\tassert.True(t, called, \"callback should be called even without telemeter\")\n}\n\nfunc TestTraceGitWorktreeRemove_NoTelemeter(t *testing.T) {\n\tt.Parallel()\n\n\tcalled := false\n\terr := filter.TraceGitWorktreeRemove(context.Background(), \"main\", \"/tmp/worktree\", func(ctx context.Context) error {\n\t\tcalled = true\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n\tassert.True(t, called, \"callback should be called even without telemeter\")\n}\n\nfunc TestTraceGitWorktreesCreate_NoTelemeter(t *testing.T) {\n\tt.Parallel()\n\n\tcalled := false\n\terr := filter.TraceGitWorktreesCreate(context.Background(), \"/work\", 2, \"git@github.com:org/repo.git\", \"main\", \"abc123\", func(ctx context.Context) error {\n\t\tcalled = true\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n\tassert.True(t, called, \"callback should be called even without telemeter\")\n}\n\nfunc TestTraceGitWorktreesCleanup_NoTelemeter(t *testing.T) {\n\tt.Parallel()\n\n\tcalled := false\n\terr := filter.TraceGitWorktreesCleanup(context.Background(), 2, \"git@github.com:org/repo.git\", func(ctx context.Context) error {\n\t\tcalled = true\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n\tassert.True(t, called, \"callback should be called even without telemeter\")\n}\n\nfunc TestTraceGitDiff_NoTelemeter(t *testing.T) {\n\tt.Parallel()\n\n\tcalled := false\n\terr := filter.TraceGitDiff(context.Background(), \"main\", \"HEAD\", \"git@github.com:org/repo.git\", func(ctx context.Context) error {\n\t\tcalled = true\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n\tassert.True(t, called, \"callback should be called even without telemeter\")\n}\n\nfunc TestTraceGitWorktreeDiscovery_NoTelemeter(t *testing.T) {\n\tt.Parallel()\n\n\tcalled := false\n\terr := filter.TraceGitWorktreeDiscovery(context.Background(), 3, func(ctx context.Context) error {\n\t\tcalled = true\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n\tassert.True(t, called, \"callback should be called even without telemeter\")\n}\n\nfunc TestTraceGitWorktreeStackWalk_NoTelemeter(t *testing.T) {\n\tt.Parallel()\n\n\tcalled := false\n\terr := filter.TraceGitWorktreeStackWalk(context.Background(), \"main\", \"feature\", func(ctx context.Context) error {\n\t\tcalled = true\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n\tassert.True(t, called, \"callback should be called even without telemeter\")\n}\n\nfunc TestTraceGitWorktreeFilterApply_NoTelemeter(t *testing.T) {\n\tt.Parallel()\n\n\tcalled := false\n\terr := filter.TraceGitWorktreeFilterApply(context.Background(), 3, 5, func(ctx context.Context) error {\n\t\tcalled = true\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n\tassert.True(t, called, \"callback should be called even without telemeter\")\n}\n\nfunc TestTraceFilterEvaluate_NoTelemeter(t *testing.T) {\n\tt.Parallel()\n\n\tcalled := false\n\terr := filter.TraceFilterEvaluate(context.Background(), 5, 10, func(ctx context.Context) error {\n\t\tcalled = true\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n\tassert.True(t, called, \"callback should be called even without telemeter\")\n}\n\nfunc TestTraceFilterParse_NoTelemeter(t *testing.T) {\n\tt.Parallel()\n\n\tcalled := false\n\terr := filter.TraceFilterParse(context.Background(), \"name=foo\", func(ctx context.Context) error {\n\t\tcalled = true\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n\tassert.True(t, called, \"callback should be called even without telemeter\")\n}\n\nfunc TestTraceGitFilterExpand_NoTelemeter(t *testing.T) {\n\tt.Parallel()\n\n\tcalled := false\n\terr := filter.TraceGitFilterExpand(context.Background(), \"main\", \"HEAD\", 3, 1, 5, func(ctx context.Context) error {\n\t\tcalled = true\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n\tassert.True(t, called, \"callback should be called even without telemeter\")\n}\n\nfunc TestTraceGitFilterEvaluate_NoTelemeter(t *testing.T) {\n\tt.Parallel()\n\n\tcalled := false\n\terr := filter.TraceGitFilterEvaluate(context.Background(), \"main\", \"HEAD\", 10, func(ctx context.Context) error {\n\t\tcalled = true\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n\tassert.True(t, called, \"callback should be called even without telemeter\")\n}\n\nfunc TestTraceGraphFilterTraverse_NoTelemeter(t *testing.T) {\n\tt.Parallel()\n\n\tcalled := false\n\terr := filter.TraceGraphFilterTraverse(context.Background(), \"dependencies\", 5, func(ctx context.Context) error {\n\t\tcalled = true\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n\tassert.True(t, called, \"callback should be called even without telemeter\")\n}\n\nfunc TestTelemetryConstants(t *testing.T) {\n\tt.Parallel()\n\n\t// Verify operation names are unique and well-formed\n\topNames := []string{\n\t\tfilter.TelemetryOpGitWorktreeCreate,\n\t\tfilter.TelemetryOpGitWorktreeRemove,\n\t\tfilter.TelemetryOpGitWorktreesCreate,\n\t\tfilter.TelemetryOpGitWorktreesCleanup,\n\t\tfilter.TelemetryOpGitDiff,\n\t\tfilter.TelemetryOpGitWorktreeDiscovery,\n\t\tfilter.TelemetryOpGitWorktreeStackWalk,\n\t\tfilter.TelemetryOpGitWorktreeFilterApply,\n\t\tfilter.TelemetryOpFilterEvaluate,\n\t\tfilter.TelemetryOpFilterParse,\n\t\tfilter.TelemetryOpGitFilterExpand,\n\t\tfilter.TelemetryOpGitFilterEvaluate,\n\t\tfilter.TelemetryOpGraphFilterTraverse,\n\t}\n\n\tseen := make(map[string]bool)\n\n\tfor _, name := range opNames {\n\t\tassert.NotEmpty(t, name, \"operation name should not be empty\")\n\t\tassert.False(t, seen[name], \"operation name should be unique: %s\", name)\n\t\tseen[name] = true\n\t}\n\n\t// Verify attribute keys are well-formed\n\tattrKeys := []string{\n\t\tfilter.AttrGitRef,\n\t\tfilter.AttrGitFromRef,\n\t\tfilter.AttrGitToRef,\n\t\tfilter.AttrGitWorktreeDir,\n\t\tfilter.AttrGitWorkingDir,\n\t\tfilter.AttrGitRefCount,\n\t\tfilter.AttrGitDiffAdded,\n\t\tfilter.AttrGitDiffRemoved,\n\t\tfilter.AttrGitDiffChanged,\n\t\tfilter.AttrGitRepoRemote,\n\t\tfilter.AttrGitRepoBranch,\n\t\tfilter.AttrGitRepoCommit,\n\t\tfilter.AttrFilterQuery,\n\t\tfilter.AttrFilterType,\n\t\tfilter.AttrFilterCount,\n\t\tfilter.AttrComponentCount,\n\t\tfilter.AttrResultCount,\n\t\tfilter.AttrWorktreePairCount,\n\t}\n\n\tseenAttrs := make(map[string]bool)\n\n\tfor _, key := range attrKeys {\n\t\tassert.NotEmpty(t, key, \"attribute key should not be empty\")\n\t\tassert.False(t, seenAttrs[key], \"attribute key should be unique: %s\", key)\n\t\tseenAttrs[key] = true\n\t}\n}\n"
  },
  {
    "path": "internal/filter/token.go",
    "content": "package filter\n\n// TokenType represents the type of a token.\ntype TokenType int\n\nconst (\n\t// ILLEGAL represents an unknown token\n\tILLEGAL TokenType = iota\n\n\t// EOF represents the end of the input\n\tEOF\n\n\t// IDENT represents an identifier (e.g., \"foo\", \"name\")\n\tIDENT\n\n\t// PATH represents a path (starts with ./, ../, or /)\n\tPATH\n\n\t// Operators\n\tBANG  // negation operator (!)\n\tPIPE  // intersection operator (|)\n\tEQUAL // attribute assignment (=)\n\n\t// Delimiters\n\tLBRACE   // left brace ({)\n\tRBRACE   // right brace (})\n\tLBRACKET // left bracket ([)\n\tRBRACKET // right bracket (])\n\n\t// Graph operators\n\tELLIPSIS // ellipsis operator (...)\n\tCARET    // caret operator (^)\n)\n\n// String returns a string representation of the token type for debugging.\nfunc (t TokenType) String() string {\n\tswitch t {\n\tcase ILLEGAL:\n\t\treturn \"ILLEGAL\"\n\tcase EOF:\n\t\treturn \"EOF\"\n\tcase IDENT:\n\t\treturn \"IDENT\"\n\tcase PATH:\n\t\treturn \"PATH\"\n\tcase BANG:\n\t\treturn \"!\"\n\tcase PIPE:\n\t\treturn \"|\"\n\tcase EQUAL:\n\t\treturn \"=\"\n\tcase LBRACE:\n\t\treturn \"{\"\n\tcase RBRACE:\n\t\treturn \"}\"\n\tcase LBRACKET:\n\t\treturn \"[\"\n\tcase RBRACKET:\n\t\treturn \"]\"\n\tcase ELLIPSIS:\n\t\treturn \"...\"\n\tcase CARET:\n\t\treturn \"^\"\n\tdefault:\n\t\treturn \"UNKNOWN\"\n\t}\n}\n\n// Token represents a lexical token with its type, literal value, and position.\ntype Token struct {\n\tLiteral  string\n\tType     TokenType\n\tPosition int\n}\n\n// NewToken creates a new token with the given type, literal, and position.\nfunc NewToken(tokenType TokenType, literal string, position int) Token {\n\treturn Token{\n\t\tType:     tokenType,\n\t\tLiteral:  literal,\n\t\tPosition: position,\n\t}\n}\n"
  },
  {
    "path": "internal/filter/walk.go",
    "content": "package filter\n\n// WalkExpressions traverses the expression tree depth-first, calling fn for each node.\n// The traversal continues to child nodes only if fn returns true.\n// For GraphExpression nodes, traversal continues into the Target expression.\n// For PrefixExpression nodes, traversal continues into the Right expression.\n// For InfixExpression nodes, traversal continues into both Left and Right expressions.\nfunc WalkExpressions(expr Expression, fn func(Expression) bool) {\n\tif expr == nil {\n\t\treturn\n\t}\n\n\tif !fn(expr) {\n\t\treturn\n\t}\n\n\tswitch node := expr.(type) {\n\tcase *GraphExpression:\n\t\tWalkExpressions(node.Target, fn)\n\tcase *PrefixExpression:\n\t\tWalkExpressions(node.Right, fn)\n\tcase *InfixExpression:\n\t\tWalkExpressions(node.Left, fn)\n\t\tWalkExpressions(node.Right, fn)\n\t}\n}\n"
  },
  {
    "path": "internal/gcphelper/config.go",
    "content": "// Package gcphelper provides helper functions for working with GCP services.\npackage gcphelper\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"os\"\n\n\t\"cloud.google.com/go/storage\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/jwt\"\n\t\"google.golang.org/api/impersonate\"\n\t\"google.golang.org/api/option\"\n)\n\nconst (\n\ttokenURL = \"https://oauth2.googleapis.com/token\"\n\n\tcredTypeServiceAccount             = \"service_account\"\n\tcredTypeAuthorizedUser             = \"authorized_user\"\n\tcredTypeImpersonatedServiceAccount = \"impersonated_service_account\"\n\tcredTypeExternalAccount            = \"external_account\"\n)\n\n// GCPSessionConfig is a representation of the configuration options for a GCP Config\ntype GCPSessionConfig struct {\n\tCredentials                        string\n\tAccessToken                        string\n\tImpersonateServiceAccount          string\n\tImpersonateServiceAccountDelegates []string\n}\n\n// GCPConfigBuilder constructs GCP client options using the builder pattern.\ntype GCPConfigBuilder struct {\n\tsessionConfig *GCPSessionConfig\n\tenv           map[string]string\n}\n\n// NewGCPConfigBuilder creates a new GCPConfigBuilder.\nfunc NewGCPConfigBuilder() *GCPConfigBuilder {\n\treturn &GCPConfigBuilder{}\n}\n\n// WithSessionConfig sets the GCP session configuration.\nfunc (b *GCPConfigBuilder) WithSessionConfig(config *GCPSessionConfig) *GCPConfigBuilder {\n\tb.sessionConfig = config\n\treturn b\n}\n\n// WithEnv sets the environment variables to use for credential resolution.\nfunc (b *GCPConfigBuilder) WithEnv(env map[string]string) *GCPConfigBuilder {\n\tb.env = env\n\treturn b\n}\n\n// BuildGCSClient builds a GCS storage client from the configured options.\nfunc (b *GCPConfigBuilder) BuildGCSClient(ctx context.Context) (*storage.Client, error) {\n\tclientOpts, err := b.Build(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tgcsClient, err := storage.NewClient(ctx, clientOpts...)\n\tif err != nil {\n\t\treturn nil, errors.Errorf(\"Error creating GCS client: %w\", err)\n\t}\n\n\treturn gcsClient, nil\n}\n\n// Build returns GCP client options from the configured session config and env.\nfunc (b *GCPConfigBuilder) Build(ctx context.Context) ([]option.ClientOption, error) {\n\tgcpCfg := b.sessionConfig\n\tenv := b.env\n\n\tvar clientOpts []option.ClientOption\n\n\tenvCreds, err := createGCPCredentialsFromEnv(env)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif envCreds != nil {\n\t\tclientOpts = append(clientOpts, envCreds)\n\t} else if gcpCfg != nil && gcpCfg.Credentials != \"\" {\n\t\t// Use credentials file from config\n\t\tcredOpt, err := credentialsFileOption(gcpCfg.Credentials)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tclientOpts = append(clientOpts, credOpt)\n\t} else if gcpCfg != nil && gcpCfg.AccessToken != \"\" {\n\t\t// Use access token from config\n\t\ttokenSource := oauth2.StaticTokenSource(&oauth2.Token{\n\t\t\tAccessToken: gcpCfg.AccessToken,\n\t\t})\n\t\tclientOpts = append(clientOpts, option.WithTokenSource(tokenSource))\n\t} else if oauthAccessToken := env[\"GOOGLE_OAUTH_ACCESS_TOKEN\"]; oauthAccessToken != \"\" {\n\t\t// Use OAuth access token from environment\n\t\ttokenSource := oauth2.StaticTokenSource(&oauth2.Token{\n\t\t\tAccessToken: oauthAccessToken,\n\t\t})\n\t\tclientOpts = append(clientOpts, option.WithTokenSource(tokenSource))\n\t} else if env[\"GOOGLE_CREDENTIALS\"] != \"\" {\n\t\t// Use GOOGLE_CREDENTIALS from environment (can be file path or JSON content)\n\t\tclientOpt, err := createGCPCredentialsFromGoogleCredentialsEnv(ctx, env)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif clientOpt != nil {\n\t\t\tclientOpts = append(clientOpts, clientOpt)\n\t\t}\n\t}\n\n\t// Handle service account impersonation.\n\t// When impersonation is configured, the impersonation token source replaces\n\t// any base credentials. The impersonate library uses Application Default\n\t// Credentials internally as the source identity.\n\tif gcpCfg != nil && gcpCfg.ImpersonateServiceAccount != \"\" {\n\t\tts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{\n\t\t\tTargetPrincipal: gcpCfg.ImpersonateServiceAccount,\n\t\t\tScopes:          []string{storage.ScopeFullControl},\n\t\t\tDelegates:       gcpCfg.ImpersonateServiceAccountDelegates,\n\t\t}, clientOpts...)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Errorf(\"Error creating impersonation token source: %w\", err)\n\t\t}\n\n\t\tclientOpts = []option.ClientOption{option.WithTokenSource(ts)}\n\t}\n\n\treturn clientOpts, nil\n}\n\n// createGCPCredentialsFromEnv creates GCP credentials from GOOGLE_APPLICATION_CREDENTIALS environment variable in env\n// It looks for GOOGLE_APPLICATION_CREDENTIALS and returns a ClientOption that can be used\n// with Google Cloud clients. Returns nil if the environment variable is not set.\nfunc createGCPCredentialsFromEnv(env map[string]string) (option.ClientOption, error) {\n\tif len(env) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tcredentialsFile := env[\"GOOGLE_APPLICATION_CREDENTIALS\"]\n\tif credentialsFile == \"\" {\n\t\treturn nil, nil\n\t}\n\n\treturn credentialsFileOption(credentialsFile)\n}\n\n// credentialsFileOption reads a GCP credentials JSON file, detects its type,\n// and returns the appropriate ClientOption.\nfunc credentialsFileOption(filename string) (option.ClientOption, error) {\n\tdata, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn nil, errors.Errorf(\"Error reading credentials file %s: %w\", filename, err)\n\t}\n\n\tvar meta struct {\n\t\tType string `json:\"type\"`\n\t}\n\n\tif err := json.Unmarshal(data, &meta); err != nil {\n\t\treturn nil, errors.Errorf(\"Error parsing credentials file %s: %w\", filename, err)\n\t}\n\n\tcredType, err := credentialsTypeFromString(meta.Type)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn option.WithAuthCredentialsFile(credType, filename), nil\n}\n\n// credentialsTypeFromString maps the \"type\" field in a GCP credentials JSON\n// file to the corresponding option.CredentialsType.\nfunc credentialsTypeFromString(t string) (option.CredentialsType, error) {\n\tswitch t {\n\tcase credTypeServiceAccount:\n\t\treturn option.ServiceAccount, nil\n\tcase credTypeAuthorizedUser:\n\t\treturn option.AuthorizedUser, nil\n\tcase credTypeImpersonatedServiceAccount:\n\t\treturn option.ImpersonatedServiceAccount, nil\n\tcase credTypeExternalAccount:\n\t\treturn option.ExternalAccount, nil\n\tdefault:\n\t\treturn \"\", errors.Errorf(\"Unsupported GCP credentials type: %q\", t)\n\t}\n}\n\n// createGCPCredentialsFromGoogleCredentialsEnv creates GCP credentials from GOOGLE_CREDENTIALS environment variable.\n// This can be either a file path or the JSON content directly (to mirror how Terraform works).\nfunc createGCPCredentialsFromGoogleCredentialsEnv(ctx context.Context, env map[string]string) (option.ClientOption, error) {\n\tvar account = struct {\n\t\tPrivateKeyID string `json:\"private_key_id\"`\n\t\tPrivateKey   string `json:\"private_key\"`\n\t\tClientEmail  string `json:\"client_email\"`\n\t\tClientID     string `json:\"client_id\"`\n\t}{}\n\n\t// to mirror how Terraform works, we have to accept either the file path or the contents\n\tcreds := env[\"GOOGLE_CREDENTIALS\"]\n\n\tcontents, err := util.FileOrData(creds)\n\tif err != nil {\n\t\treturn nil, errors.Errorf(\"Error loading credentials: %w\", err)\n\t}\n\n\tif err := json.Unmarshal([]byte(contents), &account); err != nil {\n\t\treturn nil, errors.Errorf(\"Error parsing GCP credentials.\")\n\t}\n\n\tconf := jwt.Config{\n\t\tEmail:      account.ClientEmail,\n\t\tPrivateKey: []byte(account.PrivateKey),\n\t\t// We need the FullControl scope to be able to add metadata such as labels\n\t\tScopes:   []string{storage.ScopeFullControl},\n\t\tTokenURL: tokenURL,\n\t}\n\n\treturn option.WithHTTPClient(conf.Client(ctx)), nil\n}\n"
  },
  {
    "path": "internal/gcphelper/config_test.go",
    "content": "//go:build gcp\n\npackage gcphelper_test\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/gcphelper\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCreateGcpConfigWithApplicationCredentialsEnv(t *testing.T) {\n\tt.Parallel()\n\n\tctx := context.Background()\n\n\t// Create a temporary credentials file\n\ttmpDir := t.TempDir()\n\tcredsFile := filepath.Join(tmpDir, \"credentials.json\")\n\terr := os.WriteFile(credsFile, []byte(`{\"type\":\"service_account\"}`), 0644)\n\trequire.NoError(t, err)\n\n\tenv := map[string]string{\n\t\t\"GOOGLE_APPLICATION_CREDENTIALS\": credsFile,\n\t}\n\n\tclientOpts, err := gcphelper.NewGCPConfigBuilder().WithEnv(env).Build(ctx)\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, clientOpts)\n}\n\nfunc TestCreateGcpConfigWithOAuthAccessTokenEnv(t *testing.T) {\n\tt.Parallel()\n\n\tctx := context.Background()\n\n\tenv := map[string]string{\n\t\t\"GOOGLE_OAUTH_ACCESS_TOKEN\": \"test-oauth-token\",\n\t}\n\n\tclientOpts, err := gcphelper.NewGCPConfigBuilder().WithEnv(env).Build(ctx)\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, clientOpts)\n}\n\nfunc TestCreateGcpConfigWithGoogleCredentialsEnv(t *testing.T) {\n\tt.Parallel()\n\n\tctx := context.Background()\n\n\t// Test with JSON content directly (not a file path)\n\tcredsJSON := `{\n\t\t\"type\": \"service_account\",\n\t\t\"project_id\": \"test-project\",\n\t\t\"private_key_id\": \"test-key-id\",\n\t\t\"private_key\": \"-----BEGIN PRIVATE KEY-----\\nfake-private-key\\n-----END PRIVATE KEY-----\\n\",\n\t\t\"client_email\": \"test@test-project.iam.gserviceaccount.com\",\n\t\t\"client_id\": \"123456789\",\n\t\t\"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n\t\t\"token_uri\": \"https://oauth2.googleapis.com/token\"\n\t}`\n\n\tenv := map[string]string{\n\t\t\"GOOGLE_CREDENTIALS\": credsJSON,\n\t}\n\n\tclientOpts, err := gcphelper.NewGCPConfigBuilder().WithEnv(env).Build(ctx)\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, clientOpts)\n}\n\nfunc TestCreateGcpConfigWithCredentialsFileFromConfig(t *testing.T) {\n\tt.Parallel()\n\n\tctx := context.Background()\n\n\t// Create a temporary credentials file\n\ttmpDir := t.TempDir()\n\tcredsFile := filepath.Join(tmpDir, \"credentials.json\")\n\terr := os.WriteFile(credsFile, []byte(`{\"type\":\"service_account\"}`), 0644)\n\trequire.NoError(t, err)\n\n\tenv := map[string]string{}\n\n\tgcpCfg := &gcphelper.GCPSessionConfig{\n\t\tCredentials: credsFile,\n\t}\n\n\tclientOpts, err := gcphelper.NewGCPConfigBuilder().WithSessionConfig(gcpCfg).WithEnv(env).Build(ctx)\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, clientOpts)\n}\n\nfunc TestCreateGcpConfigWithAccessTokenFromConfig(t *testing.T) {\n\tt.Parallel()\n\n\tctx := context.Background()\n\n\tenv := map[string]string{}\n\n\tgcpCfg := &gcphelper.GCPSessionConfig{\n\t\tAccessToken: \"test-access-token\",\n\t}\n\n\tclientOpts, err := gcphelper.NewGCPConfigBuilder().WithSessionConfig(gcpCfg).WithEnv(env).Build(ctx)\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, clientOpts)\n}\n\nfunc TestGcpConfigEnvVarsTakePrecedenceOverConfig(t *testing.T) {\n\tt.Parallel()\n\n\tctx := context.Background()\n\n\t// Create temporary credentials files\n\ttmpDir := t.TempDir()\n\tenvCredsFile := filepath.Join(tmpDir, \"env-credentials.json\")\n\tconfigCredsFile := filepath.Join(tmpDir, \"config-credentials.json\")\n\n\terr := os.WriteFile(envCredsFile, []byte(`{\"type\":\"service_account\"}`), 0644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(configCredsFile, []byte(`{\"type\":\"service_account\"}`), 0644)\n\trequire.NoError(t, err)\n\n\t// Set environment variable - this should take precedence over config\n\tenv := map[string]string{\n\t\t\"GOOGLE_APPLICATION_CREDENTIALS\": envCredsFile,\n\t}\n\n\t// Create config with explicit credentials - but env var should be used instead\n\tgcpCfg := &gcphelper.GCPSessionConfig{\n\t\tCredentials: configCredsFile, // This should be ignored in favor of env var\n\t}\n\n\tclientOpts, err := gcphelper.NewGCPConfigBuilder().WithSessionConfig(gcpCfg).WithEnv(env).Build(ctx)\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, clientOpts)\n\n\t// In GCP, environment variables take precedence over config values\n\t// The if-else chain in CreateGcpConfig checks env vars first\n}\n\nfunc TestCreateGcpConfigWithImpersonation(t *testing.T) {\n\tt.Parallel()\n\n\tctx := context.Background()\n\n\tenv := map[string]string{}\n\n\tgcpCfg := &gcphelper.GCPSessionConfig{\n\t\tImpersonateServiceAccount:          \"test@project.iam.gserviceaccount.com\",\n\t\tImpersonateServiceAccountDelegates: []string{\"delegate@project.iam.gserviceaccount.com\"},\n\t}\n\n\t// This will fail because we don't have real credentials, but we can verify\n\t// that the impersonation configuration is attempted\n\t_, err := gcphelper.NewGCPConfigBuilder().WithSessionConfig(gcpCfg).WithEnv(env).Build(ctx)\n\t// We expect an error because impersonation requires valid base credentials\n\t// The error should be about impersonation, not about missing credentials\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"impersonation\")\n}\n\nfunc TestCreateGcpConfigWithNoCredentials(t *testing.T) {\n\tt.Parallel()\n\n\tctx := context.Background()\n\n\tenv := map[string]string{}\n\n\t// No credentials provided - should return empty options (will use default credentials)\n\tclientOpts, err := gcphelper.NewGCPConfigBuilder().WithEnv(env).Build(ctx)\n\trequire.NoError(t, err)\n\t// Should return empty options when no credentials are provided\n\t// (default credentials will be used by GCP client)\n\tassert.Empty(t, clientOpts)\n}\n\nfunc TestCreateGcpConfigWithGoogleCredentialsFile(t *testing.T) {\n\tt.Parallel()\n\n\tctx := context.Background()\n\n\t// Create a temporary credentials file\n\ttmpDir := t.TempDir()\n\tcredsFile := filepath.Join(tmpDir, \"credentials.json\")\n\tcredsJSON := `{\n\t\t\"type\": \"service_account\",\n\t\t\"project_id\": \"test-project\",\n\t\t\"private_key_id\": \"test-key-id\",\n\t\t\"private_key\": \"-----BEGIN PRIVATE KEY-----\\nfake-private-key\\n-----END PRIVATE KEY-----\\n\",\n\t\t\"client_email\": \"test@test-project.iam.gserviceaccount.com\",\n\t\t\"client_id\": \"123456789\"\n\t}`\n\terr := os.WriteFile(credsFile, []byte(credsJSON), 0644)\n\trequire.NoError(t, err)\n\n\t// Test with GOOGLE_CREDENTIALS pointing to a file path\n\tenv := map[string]string{\n\t\t\"GOOGLE_CREDENTIALS\": credsFile,\n\t}\n\n\tclientOpts, err := gcphelper.NewGCPConfigBuilder().WithEnv(env).Build(ctx)\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, clientOpts)\n}\n"
  },
  {
    "path": "internal/git/benchmark_test.go",
    "content": "package git_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc BenchmarkGitOperations(b *testing.B) {\n\t// Setup a git repository for testing\n\trepoDir := b.TempDir()\n\n\tg, err := git.NewGitRunner()\n\trequire.NoError(b, err)\n\n\tg = g.WithWorkDir(repoDir)\n\n\tctx := b.Context()\n\n\terr = g.Clone(ctx, \"https://github.com/gruntwork-io/terragrunt.git\", false, 1, \"main\")\n\trequire.NoError(b, err)\n\n\t// This makes it so that the comparison isn't exactly apples to apples, but we're OK with giving the go-git library\n\t// any advantage it can get.\n\terr = g.GoOpenGitDir()\n\trequire.NoError(b, err)\n\n\tb.Cleanup(func() {\n\t\terr = g.GoCloseStorage()\n\t\tif err != nil {\n\t\t\tb.Logf(\"Error closing storage: %s\", err)\n\t\t}\n\t})\n\n\tb.Run(\"ls-remote\", func(b *testing.B) {\n\t\tfor b.Loop() {\n\t\t\t_, err = g.LsRemote(ctx, \"https://github.com/gruntwork-io/terragrunt.git\", \"HEAD\")\n\t\t\trequire.NoError(b, err)\n\t\t}\n\t})\n\n\tb.Run(\"ls-tree -r\", func(b *testing.B) {\n\t\tfor b.Loop() {\n\t\t\t_, err = g.LsTreeRecursive(ctx, \"HEAD\")\n\t\t\trequire.NoError(b, err)\n\t\t}\n\t})\n\n\tb.Run(\"go-ls-tree -r\", func(b *testing.B) {\n\t\tfor b.Loop() {\n\t\t\t_, err = g.GoLsTreeRecursive(\"HEAD\")\n\t\t\trequire.NoError(b, err)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/git/diff.go",
    "content": "package git\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"strings\"\n)\n\nconst (\n\tminDiffPartsLength = 2\n)\n\n// Diffs represents the diffs between two Git references.\ntype Diffs struct {\n\tAdded   []string\n\tRemoved []string\n\tChanged []string\n}\n\n// ParseDiff parses the stdout of a `git diff --name-status --no-renames` into a Diffs object.\nfunc ParseDiff(output []byte) (*Diffs, error) {\n\tmaxCount := bytes.Count(output, []byte(\"\\n\")) + 1\n\n\tadded := make([]string, 0, maxCount)\n\tremoved := make([]string, 0, maxCount)\n\tchanged := make([]string, 0, maxCount)\n\n\tscanner := bufio.NewScanner(bytes.NewReader(output))\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tparts := strings.Fields(line)\n\t\tif len(parts) < minDiffPartsLength {\n\t\t\treturn nil, &WrappedError{\n\t\t\t\tOp:      \"parse_diff\",\n\t\t\t\tContext: \"invalid diff line\",\n\t\t\t\tErr:     ErrParseDiff,\n\t\t\t}\n\t\t}\n\n\t\tstatus := parts[0]\n\t\tpath := strings.Join(parts[1:], \" \") // Handle paths with spaces\n\n\t\tswitch status {\n\t\tcase \"A\":\n\t\t\tadded = append(added, path)\n\t\tcase \"D\":\n\t\t\tremoved = append(removed, path)\n\t\tcase \"M\":\n\t\t\tchanged = append(changed, path)\n\t\t}\n\t}\n\n\tif err := scanner.Err(); err != nil {\n\t\treturn nil, &WrappedError{\n\t\t\tOp:      \"parse_diff\",\n\t\t\tContext: \"failed to read diff output\",\n\t\t\tErr:     err,\n\t\t}\n\t}\n\n\treturn &Diffs{\n\t\tAdded:   added,\n\t\tRemoved: removed,\n\t\tChanged: changed,\n\t}, nil\n}\n"
  },
  {
    "path": "internal/git/errors.go",
    "content": "package git\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\n// Error types that can be returned by the cas package\ntype Error string\n\nfunc (e Error) Error() string {\n\treturn string(e)\n}\n\nconst (\n\t// ErrParseTree is returned when failing to parse git tree output\n\tErrParseTree Error = \"failed to parse git tree output\"\n\t// ErrParseDiff is returned when failing to parse git diff output\n\tErrParseDiff Error = \"failed to parse git diff output\"\n\t// ErrGitClone is returned when the git clone operation fails\n\tErrGitClone Error = \"failed to complete git clone\"\n\t// ErrCreateTempDir is returned when failing to create a temporary directory\n\tErrCreateTempDir Error = \"failed to create temporary directory\"\n\t// ErrCleanupTempDir is returned when failing to clean up a temporary directory\n\tErrCleanupTempDir Error = \"failed to clean up temporary directory\"\n)\n\n// WrappedError provides additional context for errors\ntype WrappedError struct {\n\tOp      string // Operation that failed\n\tPath    string // File path if applicable\n\tErr     error  // Original error\n\tContext string // Additional context\n}\n\nfunc (e *WrappedError) Error() string {\n\tif e.Context != \"\" {\n\t\treturn fmt.Sprintf(\"%s: %s: %v\", e.Op, e.Context, e.Err)\n\t}\n\n\treturn fmt.Sprintf(\"%s: %v\", e.Op, e.Err)\n}\n\nfunc (e *WrappedError) Unwrap() error {\n\treturn e.Err\n}\n\n// Git operation errors\nvar (\n\tErrCommandSpawn        = errors.New(\"failed to spawn git command\")\n\tErrNoMatchingReference = errors.New(\"no matching reference\")\n\tErrReadTree            = errors.New(\"failed to read tree\")\n\tErrNoWorkDir           = errors.New(\"working directory not set\")\n\tErrNoGoRepo            = errors.New(\"go repository not set\")\n)\n"
  },
  {
    "path": "internal/git/git.go",
    "content": "// Package git provides support for Git operations needed throughout the Terragrunt codebase.\n//\n// The package primarily uses the `git` binary installed on the host system, but experimentally supports\n// the `go-git` library for some operations. As of yet, the performance of the `go-git` library is not\n// as good as the `git` binary, so we don't use it by default. If we can optimize usage of the `go-git` library\n// so that the performance difference is negligible, we can choose to use it instead of the `git` binary for certain\n// operations.\n//\n// Even assuming the performance differences are negligible, we'll still prefer to use the `git` binary for certain\n// operations. For example, operations related to remotes are likely easier to support with the `git` binary, as\n// users might have git configurations for authentication that would be inconvenient to port over to configuration\n// of the `go-git` library. This might change in the future.\n//\n// We'll prefix usage of the `go-git` library with \"Go\" to make it clear when we're using it.\npackage git\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/go-git/go-git/v6\"\n\t\"github.com/go-git/go-git/v6/storage/filesystem\"\n\t\"github.com/gruntwork-io/terragrunt/internal/os/signal\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst (\n\tminGitPartsLength = 2\n)\n\n// GitRunner handles git command execution\ntype GitRunner struct {\n\tgoRepo    *git.Repository\n\tgoStorage *filesystem.Storage\n\tGitPath   string\n\tWorkDir   string\n}\n\n// NewGitRunner creates a new GitRunner instance\nfunc NewGitRunner() (*GitRunner, error) {\n\tgitPath, err := exec.LookPath(\"git\")\n\tif err != nil {\n\t\treturn nil, &WrappedError{\n\t\t\tOp:      \"git\",\n\t\t\tContext: \"git not found\",\n\t\t\tErr:     ErrCommandSpawn,\n\t\t}\n\t}\n\n\treturn &GitRunner{\n\t\tGitPath: gitPath,\n\t}, nil\n}\n\n// WithWorkDir returns a new GitRunner with the specified working directory\nfunc (g *GitRunner) WithWorkDir(workDir string) *GitRunner {\n\tif g == nil {\n\t\treturn &GitRunner{WorkDir: workDir}\n\t}\n\n\tnewRunner := *g\n\tnewRunner.WorkDir = workDir\n\n\treturn &newRunner\n}\n\n// RequiresWorkDir returns an error if no working directory is set\nfunc (g *GitRunner) RequiresWorkDir() error {\n\tif g.WorkDir == \"\" {\n\t\treturn &WrappedError{\n\t\t\tOp:      \"git\",\n\t\t\tContext: \"no working directory set\",\n\t\t\tErr:     ErrNoWorkDir,\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// RequiresGoRepo returns an error if no go repository is set\nfunc (g *GitRunner) RequiresGoRepo() error {\n\tif g.goRepo == nil {\n\t\treturn &WrappedError{\n\t\t\tOp:      \"git\",\n\t\t\tContext: \"no go repository set\",\n\t\t\tErr:     ErrNoGoRepo,\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// GetRepoRoot returns the root directory of the git repository.\nfunc (g *GitRunner) GetRepoRoot(ctx context.Context) (string, error) {\n\tif err := g.RequiresWorkDir(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcmd := g.prepareCommand(ctx, \"rev-parse\", \"--show-toplevel\")\n\n\tvar stdout, stderr bytes.Buffer\n\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", &WrappedError{\n\t\t\tOp:      \"git_rev_parse\",\n\t\t\tContext: stderr.String(),\n\t\t\tErr:     errors.Join(ErrCommandSpawn, err),\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(stdout.String()), nil\n}\n\n// LsRemoteResult represents the output of git ls-remote\ntype LsRemoteResult struct {\n\tHash string\n\tRef  string\n}\n\n// LsRemote runs git ls-remote for a specific reference.\n// If ref is empty, we check HEAD instead.\nfunc (g *GitRunner) LsRemote(ctx context.Context, repo, ref string) ([]LsRemoteResult, error) {\n\tif ref == \"\" {\n\t\tref = \"HEAD\"\n\t}\n\n\targs := []string{repo, ref}\n\n\tcmd := g.prepareCommand(ctx, \"ls-remote\", args...)\n\n\tvar stdout, stderr bytes.Buffer\n\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn nil, &WrappedError{\n\t\t\tOp:      \"git_ls_remote\",\n\t\t\tContext: stderr.String(),\n\t\t\tErr:     errors.Join(ErrCommandSpawn, err),\n\t\t}\n\t}\n\n\tvar results []LsRemoteResult\n\n\tlines := strings.SplitSeq(strings.TrimSpace(stdout.String()), \"\\n\")\n\n\tfor line := range lines {\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tparts := strings.Fields(line)\n\t\tif len(parts) >= minGitPartsLength {\n\t\t\tresults = append(results, LsRemoteResult{\n\t\t\t\tHash: parts[0],\n\t\t\t\tRef:  parts[1],\n\t\t\t})\n\t\t}\n\t}\n\n\tif len(results) == 0 {\n\t\treturn nil, &WrappedError{\n\t\t\tOp:      \"git_ls_remote\",\n\t\t\tContext: \"no matching references\",\n\t\t\tErr:     ErrNoMatchingReference,\n\t\t}\n\t}\n\n\treturn results, nil\n}\n\n// Clone performs a git clone operation\nfunc (g *GitRunner) Clone(ctx context.Context, repo string, bare bool, depth int, branch string) error {\n\tif err := g.RequiresWorkDir(); err != nil {\n\t\treturn err\n\t}\n\n\targs := []string{}\n\n\tif bare {\n\t\targs = append(args, \"--bare\")\n\t}\n\n\tif depth > 0 {\n\t\targs = append(args, \"--depth\", \"1\", \"--single-branch\")\n\t}\n\n\tif branch != \"\" {\n\t\targs = append(args, \"--branch\", branch)\n\t}\n\n\targs = append(args, repo, g.WorkDir)\n\n\tcmd := g.prepareCommand(ctx, \"clone\", args...)\n\n\tvar stderr bytes.Buffer\n\n\tcmd.Stderr = &stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn &WrappedError{\n\t\t\tOp:      \"git_clone\",\n\t\t\tContext: stderr.String(),\n\t\t\tErr:     errors.Join(ErrGitClone, err),\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// CreateTempDir creates a new temporary directory for git operations\nfunc (g *GitRunner) CreateTempDir() (string, func() error, error) {\n\tprefix := \"terragrunt-cas-\"\n\n\t// Add a timestamp to the prefix to avoid conflicts\n\tprefix += strconv.FormatInt(time.Now().UnixNano(), 10)\n\n\ttempDir, err := os.MkdirTemp(\"\", prefix+\"*\")\n\tif err != nil {\n\t\treturn \"\", nil, &WrappedError{\n\t\t\tOp:      \"create_temp_dir\",\n\t\t\tContext: err.Error(),\n\t\t\tErr:     ErrCreateTempDir,\n\t\t}\n\t}\n\n\tg.WorkDir = tempDir\n\n\tcleanup := func() error {\n\t\tif err := os.RemoveAll(tempDir); err != nil {\n\t\t\treturn &WrappedError{\n\t\t\t\tOp:      \"cleanup_temp_dir\",\n\t\t\t\tContext: err.Error(),\n\t\t\t\tErr:     ErrCleanupTempDir,\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn tempDir, cleanup, nil\n}\n\n// ExtractRepoName extracts the repository name from a git URL\nfunc ExtractRepoName(repo string) string {\n\tname := filepath.Base(repo)\n\treturn strings.TrimSuffix(name, \".git\")\n}\n\n// LsTreeRecursive runs git ls-tree -r and returns all blobs recursively\n// This eliminates the need for multiple separate ls-tree calls on subtrees\nfunc (g *GitRunner) LsTreeRecursive(ctx context.Context, ref string) (*Tree, error) {\n\tif err := g.RequiresWorkDir(); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Use recursive ls-tree to get all blobs in a single command\n\tcmd := g.prepareCommand(ctx, \"ls-tree\", \"-r\", ref)\n\n\tvar stdout, stderr bytes.Buffer\n\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn nil, &WrappedError{\n\t\t\tOp:      \"git_ls_tree_recursive\",\n\t\t\tContext: stderr.String(),\n\t\t\tErr:     errors.Join(ErrReadTree, err),\n\t\t}\n\t}\n\n\ttree, err := ParseTree(stdout.Bytes(), \".\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn tree, nil\n}\n\n// CatFile writes the contents of a git object\n// to a given writer.\nfunc (g *GitRunner) CatFile(ctx context.Context, hash string, out io.Writer) error {\n\tif err := g.RequiresWorkDir(); err != nil {\n\t\treturn err\n\t}\n\n\tvar stderr bytes.Buffer\n\n\tcmd := g.prepareCommand(ctx, \"cat-file\", \"-p\", hash)\n\n\tcmd.Stdout = out\n\tcmd.Stderr = &stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn &WrappedError{\n\t\t\tOp:      \"git_cat_file\",\n\t\t\tContext: stderr.String(),\n\t\t\tErr:     errors.Join(ErrCommandSpawn, err),\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// CreateDetachedWorktree creates a new detached worktree for a given reference\n// as a given directory\nfunc (g *GitRunner) CreateDetachedWorktree(ctx context.Context, dir, ref string) error {\n\tif err := g.RequiresWorkDir(); err != nil {\n\t\treturn err\n\t}\n\n\tcmd := g.prepareCommand(ctx, \"worktree\", \"add\", \"--detach\", dir, ref)\n\n\tvar stdout, stderr bytes.Buffer\n\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn &WrappedError{\n\t\t\tOp:      \"git_create_detached_worktree\",\n\t\t\tContext: stderr.String(),\n\t\t\tErr:     errors.Join(ErrCommandSpawn, err),\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// RemoveWorktree removes a Git worktree for a given path\nfunc (g *GitRunner) RemoveWorktree(ctx context.Context, path string) error {\n\tif err := g.RequiresWorkDir(); err != nil {\n\t\treturn err\n\t}\n\n\tcmd := g.prepareCommand(ctx, \"worktree\", \"remove\", \"--force\", path)\n\n\tvar stdout, stderr bytes.Buffer\n\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn &WrappedError{\n\t\t\tOp:      \"git_remove_worktree\",\n\t\t\tContext: stderr.String(),\n\t\t\tErr:     errors.Join(ErrCommandSpawn, err),\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Diff determines the diff between two Git references.\nfunc (g *GitRunner) Diff(ctx context.Context, fromRef, toRef string) (*Diffs, error) {\n\tif err := g.RequiresWorkDir(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tcmd := g.prepareCommand(ctx, \"diff\", \"--name-status\", \"--no-renames\", fromRef, toRef)\n\n\tvar stdout, stderr bytes.Buffer\n\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn nil, &WrappedError{\n\t\t\tOp:      \"git_diff\",\n\t\t\tContext: stderr.String(),\n\t\t\tErr:     errors.Join(ErrCommandSpawn, err),\n\t\t}\n\t}\n\n\treturn ParseDiff(stdout.Bytes())\n}\n\n// Init initializes a Git repository\nfunc (g *GitRunner) Init(ctx context.Context) error {\n\tif err := g.RequiresWorkDir(); err != nil {\n\t\treturn err\n\t}\n\n\tcmd := g.prepareCommand(ctx, \"init\")\n\n\tvar stderr bytes.Buffer\n\n\tcmd.Stderr = &stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn &WrappedError{\n\t\t\tOp:      \"git_init\",\n\t\t\tContext: stderr.String(),\n\t\t\tErr:     errors.Join(ErrCommandSpawn, err),\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// HasUncommittedChanges checks if there are uncommitted changes in the working directory.\n// Returns true if there are uncommitted changes, false otherwise (including if git command fails or not in a git repo).\nfunc (g *GitRunner) HasUncommittedChanges(ctx context.Context) bool {\n\tcmd := g.prepareCommand(ctx, \"status\", \"--porcelain\")\n\n\tvar stdout bytes.Buffer\n\n\tcmd.Stdout = &stdout\n\n\t// If git command fails (e.g., not in a git repo), return false\n\tif err := cmd.Run(); err != nil {\n\t\treturn false\n\t}\n\n\t// Check if there are uncommitted changes (non-empty output)\n\treturn strings.TrimSpace(stdout.String()) != \"\"\n}\n\n// Config gets the configuration of the Git repository\nfunc (g *GitRunner) Config(ctx context.Context, name string) (string, error) {\n\tif err := g.RequiresWorkDir(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcmd := g.prepareCommand(ctx, \"config\", name)\n\n\tvar stdout, stderr bytes.Buffer\n\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", &WrappedError{\n\t\t\tOp:      \"git_config\",\n\t\t\tContext: stderr.String(),\n\t\t\tErr:     ErrCommandSpawn,\n\t\t}\n\t}\n\n\treturn strings.TrimSpace(stdout.String()), nil\n}\n\n// GetRemoteURL returns the origin remote URL, or empty string on error.\nfunc (g *GitRunner) GetRemoteURL(ctx context.Context) string {\n\tremote, _ := g.Config(ctx, \"remote.origin.url\")\n\treturn remote\n}\n\n// GetCurrentBranch returns the current branch name, or empty string on error.\nfunc (g *GitRunner) GetCurrentBranch(ctx context.Context) string {\n\tif err := g.RequiresWorkDir(); err != nil {\n\t\treturn \"\"\n\t}\n\n\tcmd := g.prepareCommand(ctx, \"rev-parse\", \"--abbrev-ref\", \"HEAD\")\n\n\tvar stdout bytes.Buffer\n\n\tcmd.Stdout = &stdout\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn strings.TrimSpace(stdout.String())\n}\n\n// GetHeadCommit returns the current HEAD commit hash, or empty string on error.\nfunc (g *GitRunner) GetHeadCommit(ctx context.Context) string {\n\tif err := g.RequiresWorkDir(); err != nil {\n\t\treturn \"\"\n\t}\n\n\tcmd := g.prepareCommand(ctx, \"rev-parse\", \"HEAD\")\n\n\tvar stdout bytes.Buffer\n\n\tcmd.Stdout = &stdout\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn strings.TrimSpace(stdout.String())\n}\n\n// GetDefaultBranch implements the hybrid approach to detect the default branch:\n// 1. Tries to determine the default branch of the remote repository using the fast local method first\n// 2. Falls back to the network method if the local method fails\n// 3. Attempts to update local cache for future use\n// Returns the branch name (e.g., \"main\") or an error if both methods fail.\nfunc (g *GitRunner) GetDefaultBranch(ctx context.Context, l log.Logger) string {\n\tbranch, err := g.GetDefaultBranchLocal(ctx)\n\tif err == nil && branch != \"\" {\n\t\treturn branch\n\t}\n\n\tbranch, err = g.GetDefaultBranchRemote(ctx)\n\tif err == nil && branch != \"\" {\n\t\terr = g.SetRemoteHeadAuto(ctx)\n\t\tif err != nil {\n\t\t\tl.Warnf(\"Failed to update local cache for default branch: %v\", err)\n\t\t}\n\n\t\treturn branch\n\t}\n\n\tl.Debugf(\"Failed to determine default branch of remote repository, attempting to get default branch of local repository\")\n\n\tif b, err := g.Config(ctx, \"init.defaultBranch\"); err == nil && b != \"\" {\n\t\treturn b\n\t}\n\n\tl.Debugf(\"Failed to determine default branch of local repository, using 'main' as fallback\")\n\n\treturn \"main\"\n}\n\n// GetDefaultBranchLocal attempts to get the default branch using the local cached remote HEAD.\n// Returns the branch name (e.g., \"main\") if successful, or an error if the local ref is not set.\n// This is fast and works offline, but requires that `git remote set-head origin --auto` has been run.\nfunc (g *GitRunner) GetDefaultBranchLocal(ctx context.Context) (string, error) {\n\tif err := g.RequiresWorkDir(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcmd := g.prepareCommand(ctx, \"rev-parse\", \"--abbrev-ref\", \"origin/HEAD\")\n\n\tvar stdout, stderr bytes.Buffer\n\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", &WrappedError{\n\t\t\tOp:      \"git_rev_parse_origin_head\",\n\t\t\tContext: stderr.String(),\n\t\t\tErr:     ErrCommandSpawn,\n\t\t}\n\t}\n\n\tresult := strings.TrimSpace(stdout.String())\n\n\t// If the result is just \"origin/HEAD\", the local ref is not properly set\n\tif result == \"origin/HEAD\" {\n\t\treturn \"\", &WrappedError{\n\t\t\tOp:      \"git_rev_parse_origin_head\",\n\t\t\tContext: \"local origin/HEAD ref not set\",\n\t\t\tErr:     ErrNoMatchingReference,\n\t\t}\n\t}\n\n\tif after, ok := strings.CutPrefix(result, \"origin/\"); ok {\n\t\treturn after, nil\n\t}\n\n\treturn result, nil\n}\n\n// GetDefaultBranchRemote queries the remote repository to determine the default branch.\n// This is the most accurate method but requires network access.\n// Returns the branch name (e.g., \"main\") if successful.\nfunc (g *GitRunner) GetDefaultBranchRemote(ctx context.Context) (string, error) {\n\tif err := g.RequiresWorkDir(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcmd := g.prepareCommand(ctx, \"ls-remote\", \"--symref\", \"origin\", \"HEAD\")\n\n\tvar stdout, stderr bytes.Buffer\n\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", &WrappedError{\n\t\t\tOp:      \"git_ls_remote_symref\",\n\t\t\tContext: stderr.String(),\n\t\t\tErr:     ErrCommandSpawn,\n\t\t}\n\t}\n\n\t// Parse output: \"ref: refs/heads/main    HEAD\"\n\toutput := stdout.String()\n\tlines := strings.SplitSeq(strings.TrimSpace(output), \"\\n\")\n\n\tfor line := range lines {\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.HasPrefix(line, \"ref:\") {\n\t\t\tparts := strings.Fields(line)\n\t\t\tif len(parts) >= 2 { //nolint:mnd\n\t\t\t\tref := parts[1]\n\n\t\t\t\tif after, ok := strings.CutPrefix(ref, \"refs/heads/\"); ok {\n\t\t\t\t\treturn after, nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn \"\", &WrappedError{\n\t\tOp:      \"git_ls_remote_symref\",\n\t\tContext: \"could not parse default branch from ls-remote output\",\n\t\tErr:     ErrNoMatchingReference,\n\t}\n}\n\n// SetRemoteHeadAuto runs `git remote set-head origin --auto` to update the local cached remote HEAD.\n// This makes future calls to GetDefaultBranchLocal faster.\nfunc (g *GitRunner) SetRemoteHeadAuto(ctx context.Context) error {\n\tif err := g.RequiresWorkDir(); err != nil {\n\t\treturn err\n\t}\n\n\tcmd := g.prepareCommand(ctx, \"remote\", \"set-head\", \"origin\", \"--auto\")\n\n\tvar stderr bytes.Buffer\n\n\tcmd.Stderr = &stderr\n\n\tif err := cmd.Run(); err != nil {\n\t\treturn &WrappedError{\n\t\t\tOp:      \"git_remote_set_head\",\n\t\t\tContext: stderr.String(),\n\t\t\tErr:     ErrCommandSpawn,\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (g *GitRunner) prepareCommand(ctx context.Context, name string, args ...string) *exec.Cmd {\n\tcmd := exec.CommandContext(ctx, g.GitPath, append([]string{name}, args...)...)\n\tcmd.Cancel = func() error {\n\t\tif cmd.Process == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tif sig := signal.SignalFromContext(ctx); sig != nil {\n\t\t\treturn cmd.Process.Signal(sig)\n\t\t}\n\n\t\treturn cmd.Process.Signal(os.Kill)\n\t}\n\n\tif g.WorkDir != \"\" {\n\t\tcmd.Dir = g.WorkDir\n\t}\n\n\treturn cmd\n}\n"
  },
  {
    "path": "internal/git/git_test.go",
    "content": "package git_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGitRunner_LsRemote(t *testing.T) {\n\tt.Parallel()\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\tctx := t.Context()\n\n\tt.Run(\"valid repository\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tresults, err := runner.LsRemote(ctx, \"https://github.com/gruntwork-io/terragrunt.git\", \"HEAD\")\n\t\trequire.NoError(t, err)\n\t\trequire.NotEmpty(t, results)\n\t\tassert.Regexp(t, \"^[0-9a-f]{40}$\", results[0].Hash)\n\t\tassert.Equal(t, \"HEAD\", results[0].Ref)\n\t})\n\n\tt.Run(\"invalid repository\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t_, err := runner.LsRemote(ctx, \"https://github.com/nonexistent/repo.git\", \"HEAD\")\n\t\trequire.Error(t, err)\n\n\t\tvar wrappedErr *git.WrappedError\n\t\trequire.ErrorAs(t, err, &wrappedErr)\n\t\tassert.ErrorIs(t, wrappedErr.Err, git.ErrCommandSpawn)\n\t})\n\n\tt.Run(\"nonexistent reference\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t_, err := runner.LsRemote(ctx, \"https://github.com/gruntwork-io/terragrunt.git\", \"nonexistent-branch\")\n\t\trequire.Error(t, err)\n\n\t\tvar wrappedErr *git.WrappedError\n\t\trequire.ErrorAs(t, err, &wrappedErr)\n\t\tassert.ErrorIs(t, wrappedErr.Err, git.ErrNoMatchingReference)\n\t})\n}\n\nfunc TestGitRunner_Clone(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\n\tt.Run(\"shallow clone\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcloneDir := helpers.TmpDirWOSymlinks(t)\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\n\t\trunner = runner.WithWorkDir(cloneDir)\n\t\terr = runner.Clone(ctx, \"https://github.com/gruntwork-io/terragrunt.git\", true, 1, \"main\")\n\t\trequire.NoError(t, err)\n\n\t\t// Verify it's a git repository\n\t\t_, err = os.Stat(filepath.Join(cloneDir, \"HEAD\"))\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"clone without workdir fails\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\t\terr = runner.Clone(ctx, \"https://github.com/gruntwork-io/terragrunt.git\", true, 1, \"main\")\n\t\trequire.Error(t, err)\n\n\t\tvar wrappedErr *git.WrappedError\n\t\trequire.ErrorAs(t, err, &wrappedErr)\n\t\tassert.ErrorIs(t, wrappedErr.Err, git.ErrNoWorkDir)\n\t})\n\n\tt.Run(\"invalid repository\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcloneDir := helpers.TmpDirWOSymlinks(t)\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\n\t\trunner = runner.WithWorkDir(cloneDir)\n\t\terr = runner.Clone(ctx, \"https://github.com/gruntwork-io/terragrunt-fake.git\", false, 1, \"\")\n\t\trequire.Error(t, err)\n\n\t\tvar wrappedErr *git.WrappedError\n\t\trequire.ErrorAs(t, err, &wrappedErr)\n\t\tassert.ErrorIs(t, wrappedErr.Err, git.ErrGitClone)\n\t})\n}\n\nfunc TestCreateTempDir(t *testing.T) {\n\tt.Parallel()\n\n\tgitRunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\tdir, cleanup, err := gitRunner.CreateTempDir()\n\trequire.NoError(t, err)\n\tt.Cleanup(func() {\n\t\tassert.NoError(t, cleanup())\n\t})\n\n\t// Verify directory exists\n\t_, err = os.Stat(dir)\n\trequire.NoError(t, err)\n\n\t// Verify it's empty\n\tentries, err := os.ReadDir(dir)\n\trequire.NoError(t, err)\n\tassert.Empty(t, entries)\n}\n\nfunc TestExtractRepoName(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname string\n\t\trepo string\n\t\twant string\n\t}{\n\t\t{\n\t\t\tname: \"simple repo\",\n\t\t\trepo: \"https://github.com/user/repo.git\",\n\t\t\twant: \"repo\",\n\t\t},\n\t\t{\n\t\t\tname: \"no .git suffix\",\n\t\t\trepo: \"https://github.com/user/repo\",\n\t\t\twant: \"repo\",\n\t\t},\n\t\t{\n\t\t\tname: \"with path\",\n\t\t\trepo: \"/path/to/repo.git\",\n\t\t\twant: \"repo\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, tt.want, git.ExtractRepoName(tt.repo))\n\t\t})\n\t}\n}\n\nfunc TestGitRunner_LsTree(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\n\tt.Run(\"valid repository\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcloneDir := helpers.TmpDirWOSymlinks(t)\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\n\t\trunner = runner.WithWorkDir(cloneDir)\n\n\t\t// First clone a repository\n\t\terr = runner.Clone(ctx, \"https://github.com/gruntwork-io/terragrunt.git\", true, 1, \"main\")\n\t\trequire.NoError(t, err)\n\n\t\t// Then try to ls-tree HEAD\n\t\ttree, err := runner.LsTreeRecursive(ctx, \"HEAD\")\n\t\trequire.NoError(t, err)\n\t\trequire.NotEmpty(t, tree)\n\t})\n\n\tt.Run(\"ls-tree without workdir fails\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\n\t\t_, err = runner.LsTreeRecursive(ctx, \"HEAD\")\n\t\trequire.Error(t, err)\n\n\t\tvar wrappedErr *git.WrappedError\n\t\trequire.ErrorAs(t, err, &wrappedErr)\n\t\tassert.ErrorIs(t, wrappedErr.Err, git.ErrNoWorkDir)\n\t})\n\n\tt.Run(\"invalid reference\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcloneDir := helpers.TmpDirWOSymlinks(t)\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\n\t\trunner = runner.WithWorkDir(cloneDir)\n\n\t\t// First clone a repository\n\t\terr = runner.Clone(ctx, \"https://github.com/gruntwork-io/terragrunt.git\", true, 1, \"main\")\n\t\trequire.NoError(t, err)\n\n\t\t// Try to ls-tree an invalid reference\n\t\t_, err = runner.LsTreeRecursive(ctx, \"nonexistent\")\n\t\trequire.Error(t, err)\n\n\t\tvar wrappedErr *git.WrappedError\n\t\trequire.ErrorAs(t, err, &wrappedErr)\n\t\tassert.ErrorIs(t, wrappedErr.Err, git.ErrReadTree)\n\t})\n\n\tt.Run(\"invalid repository\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\t\trunner = runner.WithWorkDir(helpers.TmpDirWOSymlinks(t))\n\n\t\t// Try to ls-tree in an empty directory\n\t\t_, err = runner.LsTreeRecursive(ctx, \"HEAD\")\n\t\trequire.Error(t, err)\n\n\t\tvar wrappedErr *git.WrappedError\n\t\trequire.ErrorAs(t, err, &wrappedErr)\n\t\tassert.ErrorIs(t, wrappedErr.Err, git.ErrReadTree)\n\t})\n}\n\nfunc TestGitRunner_RequiresWorkDir(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"with workdir\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\t\trunner = runner.WithWorkDir(helpers.TmpDirWOSymlinks(t))\n\t\terr = runner.RequiresWorkDir()\n\t\tassert.NoError(t, err)\n\t})\n\n\tt.Run(\"without workdir\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\t\terr = runner.RequiresWorkDir()\n\t\trequire.Error(t, err)\n\n\t\tvar wrappedErr *git.WrappedError\n\t\trequire.ErrorAs(t, err, &wrappedErr)\n\t\tassert.ErrorIs(t, wrappedErr.Err, git.ErrNoWorkDir)\n\t})\n}\n"
  },
  {
    "path": "internal/git/gogit.go",
    "content": "package git\n\nimport (\n\t\"path/filepath\"\n\n\t\"github.com/go-git/go-billy/v6/osfs\"\n\t\"github.com/go-git/go-git/v6\"\n\t\"github.com/go-git/go-git/v6/plumbing\"\n\t\"github.com/go-git/go-git/v6/plumbing/cache\"\n\t\"github.com/go-git/go-git/v6/plumbing/object\"\n\t\"github.com/go-git/go-git/v6/storage/filesystem\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\n// GoOpenGitDir opens a Git repository using the `go-git` library, but chroots to the `.git` directory if present.\n//\n// Use this for operations that don't need access to the rest of the repository for read-only access, etc.\n//\n// Opening a Git repository leaves the storage open, so it's the responsibility of the caller to\n// close the storage with `GoCloseStorage` when it is no longer needed.\nfunc (g *GitRunner) GoOpenGitDir() error {\n\tif err := g.RequiresWorkDir(); err != nil {\n\t\treturn err\n\t}\n\n\tbaseDir := g.WorkDir\n\n\tfs := osfs.New(baseDir)\n\tif _, err := fs.Stat(git.GitDirName); err == nil {\n\t\tfs, err = fs.Chroot(git.GitDirName)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\ts := filesystem.NewStorageWithOptions(fs, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true})\n\n\trepo, err := git.Open(s, fs)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tg.goRepo = repo\n\tg.goStorage = s\n\n\treturn nil\n}\n\n// GoOpenRepo opens a Git repository using the `go-git` library.\nfunc (g *GitRunner) GoOpenRepo() error {\n\tif err := g.RequiresWorkDir(); err != nil {\n\t\treturn err\n\t}\n\n\tbaseDir := g.WorkDir\n\n\twt := osfs.New(baseDir)\n\n\tdotGitDir := osfs.New(baseDir)\n\tif _, err := dotGitDir.Stat(git.GitDirName); err == nil {\n\t\tdotGitDir, err = dotGitDir.Chroot(git.GitDirName)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\ts := filesystem.NewStorageWithOptions(dotGitDir, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true})\n\n\trepo, err := git.Open(s, wt)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tg.goRepo = repo\n\tg.goStorage = s\n\n\treturn nil\n}\n\n// GoCloseStorage closes the storage for a Git repository.\nfunc (g *GitRunner) GoCloseStorage() error {\n\tif g.goStorage == nil {\n\t\treturn nil\n\t}\n\n\tif err := g.goStorage.Close(); err != nil {\n\t\treturn err\n\t}\n\n\tg.goRepo = nil\n\tg.goStorage = nil\n\n\treturn nil\n}\n\n// GoLsTreeRecursive uses the `go-git` library to recursively list the contents of a git tree.\n//\n// In testing, this is significantly slower than LsTreeRecursive, so we don't use it right now.\n// We'll keep it here and benchmark it again later if we can optimize it.\nfunc (g *GitRunner) GoLsTreeRecursive(ref string) ([]TreeEntry, error) {\n\tif err := g.RequiresGoRepo(); err != nil {\n\t\treturn nil, err\n\t}\n\n\th, err := g.goRepo.ResolveRevision(plumbing.Revision(ref))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc, err := g.goRepo.CommitObject(*h)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttree, err := c.Tree()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tentries, err := g.goLsTreeOnTree(tree, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn entries, nil\n}\n\n// GoAdd adds a file to the Git repository.\nfunc (g *GitRunner) GoAdd(paths ...string) error {\n\tif err := g.RequiresGoRepo(); err != nil {\n\t\treturn err\n\t}\n\n\tw, err := g.goRepo.Worktree()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, path := range paths {\n\t\t_, err := w.Add(path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// GoStatus gets the status of the Git repository.\nfunc (g *GitRunner) GoStatus() (git.Status, error) {\n\tif err := g.RequiresGoRepo(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tw, err := g.goRepo.Worktree()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn w.Status()\n}\n\n// GoCommit commits changes to the Git repository.\nfunc (g *GitRunner) GoCommit(message string, opts *git.CommitOptions) error {\n\tif err := g.RequiresGoRepo(); err != nil {\n\t\treturn err\n\t}\n\n\tif opts == nil {\n\t\treturn errors.New(\"commit options are required for go commits\")\n\t}\n\n\tw, err := g.goRepo.Worktree()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = w.Commit(message, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// GoCheckout checks out a branch in the Git repository.\nfunc (g *GitRunner) GoCheckout(opts *git.CheckoutOptions) error {\n\tif err := g.RequiresGoRepo(); err != nil {\n\t\treturn err\n\t}\n\n\tif opts == nil {\n\t\treturn errors.New(\"checkout options are required for go checkouts\")\n\t}\n\n\tw, err := g.goRepo.Worktree()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = w.Checkout(opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// GoOpenRepoHead gets the head of the Git repository.\nfunc (g *GitRunner) GoOpenRepoHead() (*plumbing.Reference, error) {\n\tif err := g.RequiresGoRepo(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn g.goRepo.Head()\n}\n\n// GoOpenRepoCommitObject gets a commit object from the Git repository.\nfunc (g *GitRunner) GoOpenRepoCommitObject(hash plumbing.Hash) (*object.Commit, error) {\n\tif err := g.RequiresGoRepo(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn g.goRepo.CommitObject(hash)\n}\n\n// goLsTreeOnTree uses the `go-git` library to recursively list the contents of a git tree.\nfunc (g *GitRunner) goLsTreeOnTree(tree *object.Tree, path string) ([]TreeEntry, error) {\n\tentries := make([]TreeEntry, 0, len(tree.Entries))\n\n\tfor _, entry := range tree.Entries {\n\t\tvar entryPath string\n\t\tif path == \"\" {\n\t\t\tentryPath = entry.Name\n\t\t} else {\n\t\t\tentryPath = filepath.Join(path, entry.Name)\n\t\t}\n\n\t\tif entry.Mode.IsFile() {\n\t\t\tentries = append(entries, TreeEntry{\n\t\t\t\tMode: entry.Mode.String(),\n\t\t\t\tType: \"blob\",\n\t\t\t\tHash: entry.Hash.String(),\n\t\t\t\tPath: entryPath,\n\t\t\t})\n\t\t} else {\n\t\t\tmode, err := entry.Mode.ToOSFileMode()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tif mode.IsDir() {\n\t\t\t\tsubTree, err := tree.Tree(entry.Name)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tsubTreeEntries, err := g.goLsTreeOnTree(subTree, entryPath)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tentries = append(entries, subTreeEntries...)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn entries, nil\n}\n"
  },
  {
    "path": "internal/git/gogit_test.go",
    "content": "package git_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\tgogit \"github.com/go-git/go-git/v6\"\n\t\"github.com/go-git/go-git/v6/plumbing/object\"\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGitRunner_GoLsTreeRecursive(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(helpers.TmpDirWOSymlinks(t))\n\n\terr = runner.Clone(ctx, \"https://github.com/gruntwork-io/terragrunt.git\", true, 1, \"main\")\n\trequire.NoError(t, err)\n\n\terr = runner.GoOpenGitDir()\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\terr = runner.GoCloseStorage()\n\t\tif err != nil {\n\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t}\n\t})\n\n\ttree, err := runner.GoLsTreeRecursive(\"HEAD\")\n\trequire.NoError(t, err)\n\n\trequire.NotEmpty(t, tree)\n}\n\nfunc TestGitRunner_GoAdd(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\n\tt.Run(\"add single file\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\n\t\tworkDir := helpers.TmpDirWOSymlinks(t)\n\t\trunner = runner.WithWorkDir(workDir)\n\n\t\terr = runner.Init(ctx)\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoOpenRepo()\n\t\trequire.NoError(t, err)\n\n\t\tt.Cleanup(func() {\n\t\t\terr = runner.GoCloseStorage()\n\t\t\tif err != nil {\n\t\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t\t}\n\t\t})\n\n\t\t// Create a new file\n\t\ttestFile := filepath.Join(workDir, \"test-file.txt\")\n\t\terr = os.WriteFile(testFile, []byte(\"test content\"), 0644)\n\t\trequire.NoError(t, err)\n\n\t\t// Add the file\n\t\terr = runner.GoAdd(\"test-file.txt\")\n\t\trequire.NoError(t, err)\n\n\t\t// Verify file is staged by checking worktree status\n\t\ts, err := runner.GoStatus()\n\t\trequire.NoError(t, err)\n\n\t\tfileStatus, ok := s[\"test-file.txt\"]\n\t\trequire.True(t, ok, \"test-file.txt should be in status\")\n\t\tassert.Equal(t, gogit.Added, fileStatus.Staging)\n\t})\n\n\tt.Run(\"add multiple files\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\n\t\tworkDir := helpers.TmpDirWOSymlinks(t)\n\t\trunner = runner.WithWorkDir(workDir)\n\n\t\terr = runner.Init(t.Context())\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoOpenRepo()\n\t\trequire.NoError(t, err)\n\n\t\tt.Cleanup(func() {\n\t\t\terr = runner.GoCloseStorage()\n\t\t\tif err != nil {\n\t\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t\t}\n\t\t})\n\n\t\t// Create multiple files\n\t\ttestFile1 := filepath.Join(workDir, \"test-file-1.txt\")\n\t\ttestFile2 := filepath.Join(workDir, \"test-file-2.txt\")\n\t\terr = os.WriteFile(testFile1, []byte(\"test content 1\"), 0644)\n\t\trequire.NoError(t, err)\n\t\terr = os.WriteFile(testFile2, []byte(\"test content 2\"), 0644)\n\t\trequire.NoError(t, err)\n\n\t\t// Add both files\n\t\terr = runner.GoAdd(\"test-file-1.txt\", \"test-file-2.txt\")\n\t\trequire.NoError(t, err)\n\n\t\t// Verify both files are staged\n\t\ts, err := runner.GoStatus()\n\t\trequire.NoError(t, err)\n\n\t\tfileStatus1, ok := s[\"test-file-1.txt\"]\n\t\trequire.True(t, ok, \"test-file-1.txt should be in status\")\n\t\tassert.Equal(t, gogit.Added, fileStatus1.Staging)\n\n\t\tfileStatus2, ok := s[\"test-file-2.txt\"]\n\t\trequire.True(t, ok, \"test-file-2.txt should be in status\")\n\t\tassert.Equal(t, gogit.Added, fileStatus2.Staging)\n\t})\n\n\tt.Run(\"add without open repo fails\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\n\t\trunner = runner.WithWorkDir(helpers.TmpDirWOSymlinks(t))\n\n\t\terr = runner.GoAdd(\"test-file.txt\")\n\t\trequire.Error(t, err)\n\n\t\tvar wrappedErr *git.WrappedError\n\t\trequire.ErrorAs(t, err, &wrappedErr)\n\t\tassert.ErrorIs(t, wrappedErr.Err, git.ErrNoGoRepo)\n\t})\n\n\tt.Run(\"add nonexistent file fails\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\n\t\tworkDir := helpers.TmpDirWOSymlinks(t)\n\t\trunner = runner.WithWorkDir(workDir)\n\n\t\terr = runner.Init(ctx)\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoOpenRepo()\n\t\trequire.NoError(t, err)\n\n\t\tt.Cleanup(func() {\n\t\t\terr = runner.GoCloseStorage()\n\t\t\tif err != nil {\n\t\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t\t}\n\t\t})\n\n\t\terr = runner.GoAdd(\"nonexistent-file.txt\")\n\t\trequire.Error(t, err)\n\t})\n}\n\nfunc TestGitRunner_GoCommit(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\n\tt.Run(\"commit staged changes\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\n\t\tworkDir := helpers.TmpDirWOSymlinks(t)\n\t\trunner = runner.WithWorkDir(workDir)\n\n\t\terr = runner.Init(ctx)\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoOpenRepo()\n\t\trequire.NoError(t, err)\n\n\t\tt.Cleanup(func() {\n\t\t\terr = runner.GoCloseStorage()\n\t\t\tif err != nil {\n\t\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t\t}\n\t\t})\n\n\t\t// Create and add a file\n\t\ttestFile := filepath.Join(workDir, \"test-file.txt\")\n\t\terr = os.WriteFile(testFile, []byte(\"test content\"), 0644)\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoAdd(\"test-file.txt\")\n\t\trequire.NoError(t, err)\n\n\t\t// Commit the changes\n\t\tcommitMessage := \"test commit\"\n\t\terr = runner.GoCommit(commitMessage, &gogit.CommitOptions{\n\t\t\tAuthor: &object.Signature{\n\t\t\t\tName:  \"Test Author\",\n\t\t\t\tEmail: \"test@example.com\",\n\t\t\t\tWhen:  time.Now(),\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\t// Verify commit was created\n\t\thead, err := runner.GoOpenRepoHead()\n\t\trequire.NoError(t, err)\n\n\t\tcommit, err := runner.GoOpenRepoCommitObject(head.Hash())\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, commitMessage, commit.Message)\n\t})\n\n\tt.Run(\"commit with options\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\n\t\tworkDir := helpers.TmpDirWOSymlinks(t)\n\t\trunner = runner.WithWorkDir(workDir)\n\n\t\terr = runner.Init(ctx)\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoOpenRepo()\n\t\trequire.NoError(t, err)\n\n\t\tt.Cleanup(func() {\n\t\t\terr = runner.GoCloseStorage()\n\t\t\tif err != nil {\n\t\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t\t}\n\t\t})\n\n\t\t// Create and add a file\n\t\ttestFile := filepath.Join(workDir, \"test-file.txt\")\n\t\terr = os.WriteFile(testFile, []byte(\"test content\"), 0644)\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoAdd(\"test-file.txt\")\n\t\trequire.NoError(t, err)\n\n\t\t// Commit with options\n\t\tcommitMessage := \"test commit with options\"\n\t\topts := &gogit.CommitOptions{\n\t\t\tAuthor: &object.Signature{\n\t\t\t\tName:  \"Test Author\",\n\t\t\t\tEmail: \"test@example.com\",\n\t\t\t},\n\t\t}\n\t\terr = runner.GoCommit(commitMessage, opts)\n\t\trequire.NoError(t, err)\n\n\t\t// Verify commit was created with correct author\n\t\thead, err := runner.GoOpenRepoHead()\n\t\trequire.NoError(t, err)\n\n\t\tcommit, err := runner.GoOpenRepoCommitObject(head.Hash())\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, commitMessage, commit.Message)\n\t\tassert.Equal(t, \"Test Author\", commit.Author.Name)\n\t\tassert.Equal(t, \"test@example.com\", commit.Author.Email)\n\t})\n\n\tt.Run(\"commit without open repo fails\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\n\t\trunner = runner.WithWorkDir(helpers.TmpDirWOSymlinks(t))\n\n\t\terr = runner.GoCommit(\"test commit\", nil)\n\t\trequire.Error(t, err)\n\n\t\tvar wrappedErr *git.WrappedError\n\t\trequire.ErrorAs(t, err, &wrappedErr)\n\t\tassert.ErrorIs(t, wrappedErr.Err, git.ErrNoGoRepo)\n\t})\n\n\tt.Run(\"commit without staged changes fails\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\n\t\tworkDir := helpers.TmpDirWOSymlinks(t)\n\t\trunner = runner.WithWorkDir(workDir)\n\n\t\terr = runner.Init(ctx)\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoOpenRepo()\n\t\trequire.NoError(t, err)\n\n\t\tt.Cleanup(func() {\n\t\t\terr = runner.GoCloseStorage()\n\t\t\tif err != nil {\n\t\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t\t}\n\t\t})\n\n\t\t// Try to commit without options\n\t\terr = runner.GoCommit(\"test commit\", &gogit.CommitOptions{\n\t\t\tAuthor: &object.Signature{\n\t\t\t\tName:  \"Test User\",\n\t\t\t\tEmail: \"test@example.com\",\n\t\t\t\tWhen:  time.Now(),\n\t\t\t},\n\t\t})\n\t\trequire.Error(t, err)\n\t\tassert.ErrorContains(t, err, \"cannot create empty commit: clean working tree\")\n\t})\n\n\tt.Run(\"commit without options fails\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\n\t\tworkDir := helpers.TmpDirWOSymlinks(t)\n\t\trunner = runner.WithWorkDir(workDir)\n\n\t\terr = runner.Init(ctx)\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoOpenRepo()\n\t\trequire.NoError(t, err)\n\n\t\tt.Cleanup(func() {\n\t\t\terr = runner.GoCloseStorage()\n\t\t\tif err != nil {\n\t\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t\t}\n\t\t})\n\n\t\ttestFile := filepath.Join(workDir, \"test-file.txt\")\n\t\terr = os.WriteFile(testFile, []byte(\"test content\"), 0644)\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoAdd(\"test-file.txt\")\n\t\trequire.NoError(t, err)\n\n\t\t// Try to commit without options\n\t\terr = runner.GoCommit(\"test commit\", nil)\n\t\trequire.Error(t, err)\n\t})\n}\n"
  },
  {
    "path": "internal/git/tree.go",
    "content": "package git\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n)\n\nconst (\n\t// The minimum number of parts in the stdout of the `git ls-tree` command\n\tminTreePartsLength = 4\n)\n\n// Tree represents a git tree object with its entries\ntype Tree struct {\n\tentries []TreeEntry\n\tpath    string\n\tdata    []byte\n}\n\n// TreeEntry represents a single entry in a git tree\ntype TreeEntry struct {\n\tMode string\n\tType string\n\tHash string\n\tPath string\n}\n\n// Write writes a tree to a given writer\nfunc (t *Tree) Write(w io.Writer) error {\n\tfor _, entry := range t.entries {\n\t\t_, err := fmt.Fprintf(w, \"%s %s %s\\t%s\\n\", entry.Mode, entry.Type, entry.Hash, entry.Path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Entries returns the tree entries\nfunc (t *Tree) Entries() []TreeEntry {\n\treturn t.entries\n}\n\n// Path returns the tree path\nfunc (t *Tree) Path() string {\n\treturn t.path\n}\n\n// Data returns the tree data\nfunc (t *Tree) Data() []byte {\n\treturn t.data\n}\n\n// ParseTree parses the stdout of git ls-tree [-r] into a Tree object.\nfunc ParseTree(output []byte, path string) (*Tree, error) {\n\tentries := make([]TreeEntry, 0, bytes.Count(output, []byte(\"\\n\"))+1)\n\n\tscanner := bufio.NewScanner(bytes.NewReader(output))\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\t\tif line == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tentry, err := ParseTreeEntry(line)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tentries = append(entries, entry)\n\t}\n\n\tif err := scanner.Err(); err != nil {\n\t\treturn nil, &WrappedError{\n\t\t\tOp:      \"parse_tree\",\n\t\t\tContext: \"failed to read tree output\",\n\t\t\tErr:     err,\n\t\t}\n\t}\n\n\treturn &Tree{\n\t\tentries: entries,\n\t\tpath:    path,\n\t\tdata:    output,\n\t}, nil\n}\n\n// ParseTreeEntry parses a single line from git ls-tree output\nfunc ParseTreeEntry(line string) (TreeEntry, error) {\n\t// Format: <mode> <type> <hash> <path>\n\tparts := strings.Fields(line)\n\tif len(parts) < minTreePartsLength {\n\t\treturn TreeEntry{}, &WrappedError{\n\t\t\tOp:      \"parse_tree_entry\",\n\t\t\tContext: \"invalid tree entry format\",\n\t\t\tErr:     ErrParseTree,\n\t\t}\n\t}\n\n\treturn TreeEntry{\n\t\tMode: parts[0],\n\t\tType: parts[1],\n\t\tHash: parts[2],\n\t\tPath: strings.Join(parts[3:], \" \"), // Handle paths with spaces\n\t}, nil\n}\n"
  },
  {
    "path": "internal/github/client.go",
    "content": "// Package github provides clients for interacting with the GitHub API and downloading GitHub releases.\npackage github\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-getter\"\n\t\"golang.org/x/sync/errgroup\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cache\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// GitHubAPIClient represents a GitHub API client.\ntype GitHubAPIClient struct {\n\tbaseURL        string\n\thttpClient     *http.Client\n\tcache          *cache.ExpiringCache[string]\n\tdefaultHeaders http.Header\n}\n\n// Release represents a GitHub repository release.\ntype Release struct {\n\tTagName string `json:\"tag_name\"`\n\tName    string `json:\"name\"`\n\tURL     string `json:\"html_url\"`\n}\n\n// GitHubAPIClientOption is a function that configures a GitHubAPIClient.\ntype GitHubAPIClientOption func(*GitHubAPIClient)\n\n// WithHTTPClient sets the HTTP client for the GitHub client.\nfunc WithHTTPClient(httpClient *http.Client) GitHubAPIClientOption {\n\treturn func(c *GitHubAPIClient) {\n\t\tc.httpClient = httpClient\n\t}\n}\n\n// WithBaseURL sets the base URL for the GitHub API.\nfunc WithBaseURL(baseURL string) GitHubAPIClientOption {\n\treturn func(c *GitHubAPIClient) {\n\t\tc.baseURL = baseURL\n\t}\n}\n\n// WithGithubToken sets the GitHub token for authentication.\nfunc WithGithubToken(token string) GitHubAPIClientOption {\n\treturn func(c *GitHubAPIClient) {\n\t\tc.defaultHeaders.Set(\"Authorization\", \"Bearer \"+token)\n\t}\n}\n\n// WithGithubComDefaultAuth sets the authentication header based on the assumption\n// we're talking to github.com, and using the same logic as the gh cli:\n// https://cli.github.com/manual/gh_help_environment\nfunc WithGithubComDefaultAuth() GitHubAPIClientOption {\n\treturn func(c *GitHubAPIClient) {\n\t\tif tok := getGithubTokenFromEnv(); tok != \"\" {\n\t\t\tc.defaultHeaders.Set(\"Authorization\", \"Bearer \"+tok)\n\t\t}\n\t}\n}\n\n// getGithubTokenFromEnv retrieves the GitHub token from environment\n// variables using the same logic as the gh cli:\n// https://cli.github.com/manual/gh_help_environment\nfunc getGithubTokenFromEnv() string {\n\tif tok := os.Getenv(\"GH_TOKEN\"); tok != \"\" {\n\t\treturn tok\n\t}\n\n\tif tok := os.Getenv(\"GITHUB_TOKEN\"); tok != \"\" {\n\t\treturn tok\n\t}\n\n\treturn \"\"\n}\n\n// NewGitHubAPIClient creates a new GitHub API client with optional configuration.\nfunc NewGitHubAPIClient(opts ...GitHubAPIClientOption) *GitHubAPIClient {\n\tclient := &GitHubAPIClient{\n\t\tbaseURL:        \"https://api.github.com\",\n\t\thttpClient:     &http.Client{Timeout: 30 * time.Second},\n\t\tcache:          cache.NewExpiringCache[string](\"github_api\"),\n\t\tdefaultHeaders: http.Header{},\n\t}\n\tclient.defaultHeaders.Set(\"X-GitHub-Api-Version\", \"2022-11-28\")\n\n\tfor _, opt := range opts {\n\t\topt(client)\n\t}\n\n\treturn client\n}\n\n// setDefaultHeaders sets default headers for the given request\nfunc (c *GitHubAPIClient) setDefaultHeaders(req *http.Request) {\n\treq.Header = c.defaultHeaders.Clone()\n}\n\n// GetLatestRelease fetches the latest release for a given repository.\n// The repository should be in the format \"owner/repo\".\nfunc (c *GitHubAPIClient) GetLatestRelease(ctx context.Context, repository string) (*Release, error) {\n\tif repository == \"\" {\n\t\treturn nil, errors.Errorf(\"repository cannot be empty\")\n\t}\n\n\tparts := strings.Split(repository, \"/\")\n\tif len(parts) != 2 {\n\t\treturn nil, errors.Errorf(\"repository must be in format 'owner/repo', got: %s\", repository)\n\t}\n\n\turl := fmt.Sprintf(\"%s/repos/%s/releases/latest\", c.baseURL, repository)\n\n\tif cachedTag, found := c.cache.Get(ctx, url); found {\n\t\treturn &Release{TagName: cachedTag}, nil\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", url, nil)\n\tif err != nil {\n\t\treturn nil, errors.Errorf(\"failed to create HTTP request: %w\", err)\n\t}\n\tc.setDefaultHeaders(req)\n\n\treq.Header.Set(\"Accept\", \"application/vnd.github.v3+json\")\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, errors.Errorf(\"GitHub API request failed: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode >= 400 {\n\t\treturn nil, errors.Errorf(\n\t\t\t\"GitHub API request to determine latest release failed with status %d: %s\",\n\t\t\tresp.StatusCode,\n\t\t\tresp.Status,\n\t\t)\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errors.Errorf(\"failed to read response body: %w\", err)\n\t}\n\n\tvar release Release\n\tif err := json.Unmarshal(body, &release); err != nil {\n\t\treturn nil, errors.Errorf(\"failed to parse GitHub API response: %w\", err)\n\t}\n\n\tif release.TagName == \"\" {\n\t\treturn nil, errors.Errorf(\"GitHub API returned empty tag name for latest release\")\n\t}\n\n\tc.cache.Put(ctx, url, release.TagName, time.Now().Add(5*time.Minute))\n\n\treturn &release, nil\n}\n\n// GetLatestReleaseTag is a convenience method that returns just the tag name\n// of the latest release for a repository.\nfunc (c *GitHubAPIClient) GetLatestReleaseTag(ctx context.Context, repository string) (string, error) {\n\trelease, err := c.GetLatestRelease(ctx, repository)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn release.TagName, nil\n}\n\n// GitHubReleasesDownloadClient represents a client for downloading GitHub release assets.\ntype GitHubReleasesDownloadClient struct {\n\tlogger log.Logger\n}\n\n// ReleaseAssets represents the assets to download for a GitHub release.\ntype ReleaseAssets struct {\n\tRepository      string\n\tVersion         string\n\tPackageFile     string\n\tChecksumFile    string\n\tChecksumSigFile string\n}\n\n// DownloadResult represents the result of downloading release assets.\ntype DownloadResult struct {\n\tPackageFile     string\n\tChecksumFile    string\n\tChecksumSigFile string\n}\n\n// GitHubReleasesDownloadClientOption is a function that configures a GitHubReleasesDownloadClient.\ntype GitHubReleasesDownloadClientOption func(*GitHubReleasesDownloadClient)\n\n// WithLogger sets the logger for the download client.\nfunc WithLogger(logger log.Logger) GitHubReleasesDownloadClientOption {\n\treturn func(c *GitHubReleasesDownloadClient) {\n\t\tc.logger = logger\n\t}\n}\n\n// NewGitHubReleasesDownloadClient creates a new GitHub releases download client.\nfunc NewGitHubReleasesDownloadClient(opts ...GitHubReleasesDownloadClientOption) *GitHubReleasesDownloadClient {\n\tclient := &GitHubReleasesDownloadClient{}\n\n\tfor _, opt := range opts {\n\t\topt(client)\n\t}\n\n\treturn client\n}\n\n// DownloadReleaseAssets downloads the specified release assets from a GitHub repository.\n// It supports downloading from either full URLs (when repository contains \"://\") or\n// from GitHub releases using the standard GitHub releases URL format.\nfunc (c *GitHubReleasesDownloadClient) DownloadReleaseAssets(\n\tctx context.Context,\n\tassets *ReleaseAssets,\n) (*DownloadResult, error) {\n\tif assets.Repository == \"\" {\n\t\treturn nil, errors.Errorf(\"repository cannot be empty\")\n\t}\n\n\tif assets.PackageFile == \"\" {\n\t\treturn nil, errors.Errorf(\"package file path cannot be empty\")\n\t}\n\n\tresult := &DownloadResult{\n\t\tPackageFile: assets.PackageFile,\n\t}\n\n\texpectedLen := 1\n\n\tif assets.ChecksumFile != \"\" {\n\t\texpectedLen++\n\t}\n\n\tif assets.ChecksumSigFile != \"\" {\n\t\texpectedLen++\n\t}\n\n\tdownloads := make(map[string]string, expectedLen)\n\n\tif strings.Contains(assets.Repository, \"://\") {\n\t\t// If repository contains \"://\", treat it as a direct URL\n\t\tdownloads[assets.Repository] = assets.PackageFile\n\t} else {\n\t\tif assets.Version == \"\" {\n\t\t\treturn nil, errors.Errorf(\"version cannot be empty for GitHub repository downloads\")\n\t\t}\n\n\t\tbaseURL := fmt.Sprintf(\"https://%s/releases/download/%s\", assets.Repository, assets.Version)\n\t\tpackageFileName := filepath.Base(assets.PackageFile)\n\n\t\tdownloads[fmt.Sprintf(\"%s/%s\", baseURL, packageFileName)] = assets.PackageFile\n\n\t\tif assets.ChecksumFile != \"\" {\n\t\t\tchecksumFileName := filepath.Base(assets.ChecksumFile)\n\t\t\tdownloads[fmt.Sprintf(\"%s/%s\", baseURL, checksumFileName)] = assets.ChecksumFile\n\t\t\tresult.ChecksumFile = assets.ChecksumFile\n\t\t}\n\n\t\tif assets.ChecksumSigFile != \"\" {\n\t\t\tchecksumSigFileName := filepath.Base(assets.ChecksumSigFile)\n\t\t\tdownloads[fmt.Sprintf(\"%s/%s\", baseURL, checksumSigFileName)] = assets.ChecksumSigFile\n\t\t\tresult.ChecksumSigFile = assets.ChecksumSigFile\n\t\t}\n\t}\n\n\tg, downloadCtx := errgroup.WithContext(ctx)\n\n\tfor url, localPath := range downloads {\n\t\tg.Go(func() error {\n\t\t\tif c.logger != nil {\n\t\t\t\tc.logger.Infof(\"Downloading %s to %s\", url, localPath)\n\t\t\t}\n\n\t\t\tclient := &getter.Client{\n\t\t\t\tCtx:           downloadCtx,\n\t\t\t\tSrc:           url,\n\t\t\t\tDst:           localPath,\n\t\t\t\tMode:          getter.ClientModeFile,\n\t\t\t\tDecompressors: map[string]getter.Decompressor{},\n\t\t\t}\n\n\t\t\t// Add GitHub token to HTTP headers if available\n\t\t\tif tok := getGithubTokenFromEnv(); tok != \"\" {\n\t\t\t\t// use the default getters\n\t\t\t\tclient.Getters = maps.Clone(getter.Getters)\n\t\t\t\t// but override the https getter to inject the github token\n\t\t\t\tclient.Getters[\"https\"] = &getter.HttpGetter{\n\t\t\t\t\tNetrc: true,\n\t\t\t\t\tHeader: http.Header{\n\t\t\t\t\t\t\"Authorization\": {\"Bearer \" + tok},\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\t// test servers don't use https, but we don't usually want to send auth tokens unencrypted\n\t\t\t\tif testing.Testing() {\n\t\t\t\t\tclient.Getters[\"http\"] = &getter.HttpGetter{\n\t\t\t\t\t\tNetrc: true,\n\t\t\t\t\t\tHeader: http.Header{\n\t\t\t\t\t\t\t\"Authorization\": {\"Bearer \" + tok},\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 err := client.Get(); err != nil {\n\t\t\t\treturn errors.Errorf(\"failed to download %s: %w\", url, err)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tif err := g.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn result, nil\n}\n"
  },
  {
    "path": "internal/github/client_test.go",
    "content": "package github_test\n\nimport (\n\t\"context\"\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\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/github\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewClient(t *testing.T) {\n\tt.Parallel()\n\n\tclient := github.NewGitHubAPIClient()\n\trequire.NotNil(t, client)\n\n\tassert.NotNil(t, client)\n}\n\nfunc TestGithubAuthPickupOrder(t *testing.T) {\n\n\tt.Run(\"prefer GH_TOKEN\", func(t *testing.T) {\n\t\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tassert.Equal(t, \"Bearer goodtoken\", r.Header.Get(\"Authorization\"))\n\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tresponse := `{\n\t\t\t\t\"tag_name\": \"v1.2.3\",\n\t\t\t\t\"name\": \"Release v1.2.3\",\n\t\t\t\t\"html_url\": \"https://github.com/owner/repo/releases/tag/v1.2.3\"\n\t\t\t}`\n\t\t\t_, err := fmt.Fprint(w, response)\n\t\t\trequire.NoError(t, err)\n\t\t}))\n\t\tdefer server.Close()\n\n\t\tt.Setenv(\"GH_TOKEN\", \"goodtoken\")\n\t\tt.Setenv(\"GITHUB_TOKEN\", \"badtoken\")\n\n\t\tclient := github.NewGitHubAPIClient(github.WithBaseURL(server.URL), github.WithGithubComDefaultAuth())\n\n\t\t_, err := client.GetLatestRelease(t.Context(), \"owner/repo\")\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"use GITHUB_TOKEN\", func(t *testing.T) {\n\t\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tassert.Equal(t, \"Bearer goodtoken\", r.Header.Get(\"Authorization\"))\n\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tresponse := `{\n\t\t\t\t\"tag_name\": \"v1.2.3\",\n\t\t\t\t\"name\": \"Release v1.2.3\",\n\t\t\t\t\"html_url\": \"https://github.com/owner/repo/releases/tag/v1.2.3\"\n\t\t\t}`\n\t\t\t_, err := fmt.Fprint(w, response)\n\t\t\trequire.NoError(t, err)\n\t\t}))\n\t\tdefer server.Close()\n\n\t\tt.Setenv(\"GH_TOKEN\", \"\")\n\t\tt.Setenv(\"GITHUB_TOKEN\", \"goodtoken\")\n\t\tclient := github.NewGitHubAPIClient(github.WithBaseURL(server.URL), github.WithGithubComDefaultAuth())\n\n\t\t_, err := client.GetLatestRelease(t.Context(), \"owner/repo\")\n\t\trequire.NoError(t, err)\n\t})\n}\n\nfunc TestNewClientWithOptions(t *testing.T) {\n\tt.Parallel()\n\n\tcustomHTTPClient := &http.Client{Timeout: 10 * time.Second}\n\tcustomBaseURL := \"https://custom.github.com\"\n\n\tclient := github.NewGitHubAPIClient(\n\t\tgithub.WithHTTPClient(customHTTPClient),\n\t\tgithub.WithBaseURL(customBaseURL),\n\t)\n\n\tassert.NotNil(t, client)\n}\n\nfunc TestGetLatestRelease(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a mock server\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\n\t\tassert.Equal(t, \"/repos/owner/repo/releases/latest\", r.URL.Path)\n\t\tassert.Equal(t, \"application/vnd.github.v3+json\", r.Header.Get(\"Accept\"))\n\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tresponse := `{\n\t\t\t\"tag_name\": \"v1.2.3\",\n\t\t\t\"name\": \"Release v1.2.3\",\n\t\t\t\"html_url\": \"https://github.com/owner/repo/releases/tag/v1.2.3\"\n\t\t}`\n\t\t_, err := fmt.Fprint(w, response)\n\t\tassert.NoError(t, err)\n\t}))\n\tdefer server.Close()\n\n\tclient := github.NewGitHubAPIClient(github.WithBaseURL(server.URL))\n\n\trelease, err := client.GetLatestRelease(t.Context(), \"owner/repo\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"v1.2.3\", release.TagName)\n\tassert.Equal(t, \"Release v1.2.3\", release.Name)\n\tassert.Equal(t, \"https://github.com/owner/repo/releases/tag/v1.2.3\", release.URL)\n}\n\nfunc TestGetLatestReleaseTag(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a mock server\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tresponse := `{\"tag_name\": \"v2.0.0\"}`\n\t\t_, err := fmt.Fprint(w, response)\n\t\tassert.NoError(t, err)\n\t}))\n\tdefer server.Close()\n\n\tclient := github.NewGitHubAPIClient(github.WithBaseURL(server.URL))\n\n\ttag, err := client.GetLatestReleaseTag(t.Context(), \"owner/repo\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"v2.0.0\", tag)\n}\n\nfunc TestGetLatestReleaseInvalidRepository(t *testing.T) {\n\tt.Parallel()\n\n\tclient := github.NewGitHubAPIClient()\n\n\ttestCases := []string{\n\t\t\"\",\n\t\t\"invalid\",\n\t\t\"too/many/parts\",\n\t}\n\n\tfor _, repo := range testCases {\n\t\tt.Run(fmt.Sprintf(\"repo=%s\", repo), func(tt *testing.T) {\n\t\t\t_, err := client.GetLatestRelease(tt.Context(), repo)\n\t\t\trequire.Error(t, err)\n\t\t})\n\t}\n}\n\nfunc TestGetLatestReleaseHTTPError(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a mock server that returns 404\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusNotFound)\n\t\t_, err := fmt.Fprint(w, \"Not Found\")\n\t\trequire.NoError(t, err)\n\t}))\n\tdefer server.Close()\n\n\tclient := github.NewGitHubAPIClient(github.WithBaseURL(server.URL))\n\n\t_, err := client.GetLatestRelease(t.Context(), \"owner/repo\")\n\trequire.Error(t, err)\n\tassert.ErrorContains(t, err, \"GitHub API request to determine latest release failed with status 404\")\n}\n\nfunc TestGetLatestReleaseEmptyTag(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a mock server that returns empty tag\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tresponse := `{\"tag_name\": \"\"}`\n\t\t_, err := fmt.Fprint(w, response)\n\t\trequire.NoError(t, err)\n\t}))\n\tdefer server.Close()\n\n\tclient := github.NewGitHubAPIClient(github.WithBaseURL(server.URL))\n\n\t_, err := client.GetLatestRelease(t.Context(), \"owner/repo\")\n\trequire.Error(t, err)\n\tassert.ErrorContains(t, err, \"GitHub API returned empty tag name for latest release\")\n}\n\nfunc TestGetLatestReleaseCaching(t *testing.T) {\n\tt.Parallel()\n\n\tcallCount := 0\n\t// Create a mock server that tracks how many times it's called\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tcallCount++\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\tresponse := `{\"tag_name\": \"v1.0.0\"}`\n\t\t_, err := fmt.Fprint(w, response)\n\t\trequire.NoError(t, err)\n\t}))\n\tdefer server.Close()\n\n\tclient := github.NewGitHubAPIClient(github.WithBaseURL(server.URL))\n\n\t// First call should hit the server\n\ttag1, err := client.GetLatestReleaseTag(t.Context(), \"owner/repo\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"v1.0.0\", tag1)\n\tassert.Equal(t, 1, callCount)\n\n\t// Second call should use cache\n\ttag2, err := client.GetLatestReleaseTag(t.Context(), \"owner/repo\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"v1.0.0\", tag2)\n\tassert.Equal(t, 1, callCount)\n}\n\n// Tests for GitHubReleasesDownloadClient\n\nfunc TestNewGitHubReleasesDownloadClient(t *testing.T) {\n\tt.Parallel()\n\n\tclient := github.NewGitHubReleasesDownloadClient()\n\trequire.NotNil(t, client)\n}\n\nfunc TestNewGitHubReleasesDownloadClientWithOptions(t *testing.T) {\n\tt.Parallel()\n\n\tlogger := log.New()\n\tclient := github.NewGitHubReleasesDownloadClient(github.WithLogger(logger))\n\trequire.NotNil(t, client)\n}\n\nfunc TestDownloadReleaseAssetsValidation(t *testing.T) {\n\tt.Parallel()\n\n\tclient := github.NewGitHubReleasesDownloadClient()\n\tctx := context.Background()\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tassets   *github.ReleaseAssets\n\t\terrorMsg string\n\t}{\n\t\t{\n\t\t\tname:     \"empty repository\",\n\t\t\tassets:   &github.ReleaseAssets{Repository: \"\", PackageFile: \"/tmp/package.zip\"},\n\t\t\terrorMsg: \"repository cannot be empty\",\n\t\t},\n\t\t{\n\t\t\tname:     \"empty package file\",\n\t\t\tassets:   &github.ReleaseAssets{Repository: \"owner/repo\", PackageFile: \"\"},\n\t\t\terrorMsg: \"package file path cannot be empty\",\n\t\t},\n\t\t{\n\t\t\tname:     \"missing version for GitHub repo\",\n\t\t\tassets:   &github.ReleaseAssets{Repository: \"owner/repo\", Version: \"\", PackageFile: \"/tmp/package.zip\"},\n\t\t\terrorMsg: \"version cannot be empty for GitHub repository downloads\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\t_, err := client.DownloadReleaseAssets(ctx, tc.assets)\n\t\t\trequire.Error(t, err)\n\t\t\tassert.ErrorContains(t, err, tc.errorMsg)\n\t\t})\n\t}\n}\n\nfunc TestDownloadReleaseAssetsGitHubRelease(t *testing.T) {\n\tt.Parallel()\n\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create mock server for GitHub releases\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tpath := r.URL.Path\n\n\t\t// Serve different content based on the requested file\n\t\tif strings.HasSuffix(path, \"package.zip\") {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/zip\")\n\t\t\tfmt.Fprint(w, \"fake-zip-content\")\n\t\t\treturn\n\t\t}\n\n\t\tif strings.HasSuffix(path, \"SHA256SUMS\") {\n\t\t\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\t\t\tfmt.Fprint(w, \"fake-checksum-content\")\n\t\t\treturn\n\t\t}\n\n\t\tif strings.HasSuffix(path, \"SHA256SUMS.sig\") {\n\t\t\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\t\t\tfmt.Fprint(w, \"fake-signature-content\")\n\t\t\treturn\n\t\t}\n\n\t\tw.WriteHeader(http.StatusNotFound)\n\t\treturn\n\t}))\n\tdefer server.Close()\n\n\t// Use direct URL approach for testing since mock servers are complex to set up for GitHub releases format\n\tclient := github.NewGitHubReleasesDownloadClient()\n\n\tassets := &github.ReleaseAssets{\n\t\tRepository:  server.URL + \"/package.zip\", // Direct URL\n\t\tPackageFile: filepath.Join(tempDir, \"package.zip\"),\n\t\t// Direct URLs don't use checksum files\n\t}\n\n\tctx := context.Background()\n\tresult, err := client.DownloadReleaseAssets(ctx, assets)\n\trequire.NoError(t, err)\n\n\t// Verify result\n\tassert.Equal(t, assets.PackageFile, result.PackageFile)\n\tassert.Equal(t, \"\", result.ChecksumFile)\n\tassert.Equal(t, \"\", result.ChecksumSigFile)\n\n\t// Verify package file was created and has expected content\n\tverifyFileContent(t, result.PackageFile, \"fake-zip-content\")\n}\n\nfunc TestDownloadReleaseAssetsGitHubReleaseUsesToken(t *testing.T) {\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\n\t// shared logic for handlers\n\tdoResp := func(w http.ResponseWriter, r *http.Request) {\n\t\t// Serve different content based on the requested file\n\t\tpath := r.URL.Path\n\n\t\tif strings.HasSuffix(path, \"package.zip\") {\n\t\t\tw.Header().Set(\"Content-Type\", \"application/zip\")\n\t\t\tfmt.Fprint(w, \"fake-zip-content\")\n\t\t\treturn\n\t\t}\n\n\t\tif strings.HasSuffix(path, \"SHA256SUMS\") {\n\t\t\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\t\t\tfmt.Fprint(w, \"fake-checksum-content\")\n\t\t\treturn\n\t\t}\n\n\t\tif strings.HasSuffix(path, \"SHA256SUMS.sig\") {\n\t\t\tw.Header().Set(\"Content-Type\", \"text/plain\")\n\t\t\tfmt.Fprint(w, \"fake-signature-content\")\n\t\t\treturn\n\t\t}\n\n\t\tw.WriteHeader(http.StatusNotFound)\n\t\treturn\n\t}\n\n\t// Use direct URL approach for testing since mock servers are complex to set up for GitHub releases format\n\tclient := github.NewGitHubReleasesDownloadClient()\n\n\tt.Run(\"prefer GH_TOKEN\", func(t *testing.T) {\n\t\tt.Setenv(\"GH_TOKEN\", \"goodtoken\")\n\t\tt.Setenv(\"GITHUB_TOKEN\", \"badtoken\")\n\n\t\t// Create mock server for GitHub releases\n\t\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tassert.Equal(t, \"Bearer goodtoken\", r.Header.Get(\"Authorization\"))\n\n\t\t\tdoResp(w, r)\n\t\t}))\n\t\tdefer server.Close()\n\n\t\tctx := t.Context()\n\n\t\tassets := &github.ReleaseAssets{\n\t\t\tRepository:  server.URL + \"/package.zip\", // Direct URL\n\t\t\tPackageFile: filepath.Join(tempDir, \"package.zip\"),\n\t\t\t// Direct URLs don't use checksum files\n\t\t}\n\n\t\t_, err := client.DownloadReleaseAssets(ctx, assets)\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"use GITHUB_TOKEN\", func(t *testing.T) {\n\t\tt.Setenv(\"GH_TOKEN\", \"\")\n\t\tt.Setenv(\"GITHUB_TOKEN\", \"goodtoken\")\n\n\t\t// Create mock server for GitHub releases\n\t\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tassert.Equal(t, \"Bearer goodtoken\", r.Header.Get(\"Authorization\"))\n\n\t\t\tdoResp(w, r)\n\t\t}))\n\t\tdefer server.Close()\n\n\t\tctx := t.Context()\n\n\t\tassets := &github.ReleaseAssets{\n\t\t\tRepository:  server.URL + \"/package.zip\", // Direct URL\n\t\t\tPackageFile: filepath.Join(tempDir, \"package.zip\"),\n\t\t\t// Direct URLs don't use checksum files\n\t\t}\n\t\t_, err := client.DownloadReleaseAssets(ctx, assets)\n\t\trequire.NoError(t, err)\n\t})\n}\n\nfunc TestDownloadReleaseAssetsDirectURL(t *testing.T) {\n\tt.Parallel()\n\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create mock server\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"application/zip\")\n\t\tfmt.Fprint(w, \"direct-url-content\")\n\t}))\n\tdefer server.Close()\n\n\tclient := github.NewGitHubReleasesDownloadClient()\n\n\tassets := &github.ReleaseAssets{\n\t\tRepository:  server.URL + \"/direct-download.zip\",\n\t\tPackageFile: filepath.Join(tempDir, \"direct.zip\"),\n\t\t// Note: No Version, ChecksumFile, or ChecksumSigFile for direct URLs\n\t}\n\n\tresult, err := client.DownloadReleaseAssets(t.Context(), assets)\n\trequire.NoError(t, err)\n\n\t// Verify result\n\tassert.Equal(t, assets.PackageFile, result.PackageFile)\n\tassert.Equal(t, \"\", result.ChecksumFile)\n\tassert.Equal(t, \"\", result.ChecksumSigFile)\n\n\t// Verify file was created and has expected content\n\tverifyFileContent(t, result.PackageFile, \"direct-url-content\")\n}\n\n// Helper function to verify file content\nfunc verifyFileContent(t *testing.T, filePath, expectedContent string) {\n\tt.Helper()\n\n\trequire.FileExists(t, filePath)\n\n\tcontent, err := os.ReadFile(filePath)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, expectedContent, string(content))\n}\n"
  },
  {
    "path": "internal/hclhelper/wrap.go",
    "content": "// Package hclhelper providers helpful tools for working with HCL values.\npackage hclhelper\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n)\n\n// WrapMapToSingleLineHcl - This is a workaround to convert a map[string]any to a single line HCL string.\nfunc WrapMapToSingleLineHcl(m map[string]any) string {\n\tvar attributes = make([]string, 0, len(m))\n\tfor key, value := range m {\n\t\tattributes = append(attributes, fmt.Sprintf(`%s=%s`, key, formatHclValue(value)))\n\t}\n\n\tsort.Strings(attributes)\n\n\treturn fmt.Sprintf(\"{%s}\", strings.Join(attributes, \",\"))\n}\n\n// formatHclValue - Wrap single line HCL values in quotes.\nfunc formatHclValue(value any) string {\n\tswitch v := value.(type) {\n\tcase string:\n\t\tescapedValue := strings.ReplaceAll(v, `\"`, `\\\"`)\n\t\treturn fmt.Sprintf(`\"%s\"`, escapedValue)\n\tcase map[string]any:\n\t\treturn WrapMapToSingleLineHcl(v)\n\tdefault:\n\t\treturn fmt.Sprintf(`%v`, v)\n\t}\n}\n"
  },
  {
    "path": "internal/hclhelper/wrap_test.go",
    "content": "package hclhelper_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/hclhelper\"\n)\n\nfunc TestWrapMapToSingleLineHcl(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tinput    map[string]any\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"SimpleMap\",\n\t\t\tinput:    map[string]any{\"key1\": \"value1\", \"key2\": 46521694, \"key3\": true},\n\t\t\texpected: `{key1=\"value1\",key2=46521694,key3=true}`,\n\t\t},\n\t\t{\n\t\t\tname:     \"NestedMap\",\n\t\t\tinput:    map[string]any{\"key1\": \"value1\", \"key2\": map[string]any{\"nestedKey\": \"nestedValue\"}},\n\t\t\texpected: `{key1=\"value1\",key2={nestedKey=\"nestedValue\"}}`,\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.Parallel()\n\n\t\t\tresult := hclhelper.WrapMapToSingleLineHcl(tc.input)\n\t\t\tif result != tc.expected {\n\t\t\t\tt.Errorf(\"Expected %s, but got %s\", tc.expected, result)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/iacargs/boolean_args_test.go",
    "content": "package iacargs_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRemoveFlagWithLongBoolean(t *testing.T) {\n\tt.Parallel()\n\n\t// --json is an unknown flag (not in valueTakingFlags).\n\t// Unknown flags are treated as boolean, so removing --json\n\t// should NOT consume \"planfile\" as a value.\n\n\targs := &iacargs.IacArgs{\n\t\tFlags: []string{\"--json\", \"planfile\"},\n\t}\n\n\targs.RemoveFlag(\"--json\")\n\n\tassert.Equal(t, []string{\"planfile\"}, args.Flags)\n}\n\nfunc TestRemoveFlagWithUnknownFlag(t *testing.T) {\n\tt.Parallel()\n\n\t// Unknown flag --unknown is not in valueTakingFlags.\n\t// Unknown flags are treated as boolean, so removing --unknown\n\t// should NOT consume \"val\" as a value.\n\n\targs := &iacargs.IacArgs{\n\t\tFlags: []string{\"--unknown\", \"val\", \"other\"},\n\t}\n\n\targs.RemoveFlag(\"--unknown\")\n\n\tassert.Equal(t, []string{\"val\", \"other\"}, args.Flags)\n}\n\nfunc TestHasFlagFalsePositive(t *testing.T) {\n\tt.Parallel()\n\n\t// out=foo should not match -out flag\n\targs := &iacargs.IacArgs{\n\t\tFlags: []string{\"-var\", \"out=foo\"},\n\t}\n\n\tassert.False(t, args.HasFlag(\"-out\"))\n}\n\nfunc TestRemoveFlagFalsePositive(t *testing.T) {\n\tt.Parallel()\n\n\t// Removing -out should not remove out=foo\n\targs := &iacargs.IacArgs{\n\t\tFlags: []string{\"-var\", \"out=foo\"},\n\t}\n\n\targs.RemoveFlag(\"-out\")\n\n\tassert.Equal(t, []string{\"-var\", \"out=foo\"}, args.Flags)\n}\n\nfunc TestHasFlagDoubleDashMatch(t *testing.T) {\n\tt.Parallel()\n\n\targs := &iacargs.IacArgs{\n\t\tFlags: []string{\"--help\"},\n\t}\n\n\tassert.True(t, args.HasFlag(\"-help\"))\n\tassert.True(t, args.HasFlag(\"--help\"))\n}\n\nfunc TestRemoveFlagDoubleDashMatch(t *testing.T) {\n\tt.Parallel()\n\n\targs := &iacargs.IacArgs{\n\t\tFlags: []string{\"--help\", \"planfile\"},\n\t}\n\n\targs.RemoveFlag(\"-help\")\n\n\tassert.Equal(t, []string{\"planfile\"}, args.Flags)\n}\n"
  },
  {
    "path": "internal/iacargs/iacargs.go",
    "content": "// Package iacargs provides types and utilities for handling IaC (terraform/tofu) CLI arguments.\npackage iacargs\n\nimport (\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n)\n\nconst (\n\t// minFlagLen is the minimum length for a valid flag (e.g., \"-x\" has length 2)\n\tminFlagLen = 2\n\n\t// CommandNameDestroy is the terraform destroy command name\n\tCommandNameDestroy = \"destroy\"\n)\n\nconst (\n\tSingleDashFlag NormalizeActsType = iota\n\tDoubleDashFlag\n)\n\nvar (\n\tsingleDashRegexp = regexp.MustCompile(`^-([^-]|$)`)\n\tdoubleDashRegexp = regexp.MustCompile(`^--([^-]|$)`)\n)\n\ntype NormalizeActsType byte\n\n// valueTakingFlags contains flags that require space-separated values.\n// Only flags that commonly use a space-separated format need to be listed here.\nvar valueTakingFlags = []string{\n\t\"chdir\",\n\t\"config\",\n\t\"from-module\",\n\t\"lock-timeout\",\n\t\"out\",\n\t\"parallelism\",\n\t\"plugin-dir\",\n\t\"state\",\n\t\"state-out\",\n\t\"backup\",\n\t\"target\",\n\t\"var\",\n\t\"var-file\",\n}\n\n// normalizeFlag strips leading dashes from a flag name.\nfunc normalizeFlag(flag string) string {\n\treturn strings.TrimLeft(flag, \"-\")\n}\n\n// isFlag returns true if the string starts with \"-\" (is a flag token).\nfunc isFlag(s string) bool {\n\treturn strings.HasPrefix(s, \"-\")\n}\n\n// IacArgs represents parsed IaC (terraform/tofu) CLI arguments\n// with separate command, flags, and arguments fields.\n// Provides a builder pattern for constructing CLI arguments.\n//\n// Structure: [Command] [SubCommand...] [Flags...] [Arguments...]\n// - Command: main terraform command (e.g., \"apply\", \"providers\")\n// - SubCommand: non-flag args before any flags (e.g., \"lock\" in \"providers lock\")\n// - Flags: all flags with their values\n// - Arguments: non-flag args after flags (e.g., plan files)\ntype IacArgs struct {\n\tCommand    string   // e.g., \"apply\", \"plan\", \"providers\"\n\tSubCommand []string // e.g., \"lock\" in \"providers lock -platform=...\"\n\tFlags      []string // e.g., \"-input=false\", \"-auto-approve\"\n\tArguments  []string // e.g., plan files, resource addresses\n}\n\n// New creates IacArgs from strings, parsing command/flags/arguments.\nfunc New(args ...string) *IacArgs {\n\tresult := &IacArgs{\n\t\tSubCommand: make([]string, 0),\n\t\tFlags:      make([]string, 0),\n\t\tArguments:  make([]string, 0),\n\t}\n\tresult.parse(args)\n\n\treturn result\n}\n\n// SetCommand sets the command and returns self for chaining.\nfunc (a *IacArgs) SetCommand(cmd string) *IacArgs {\n\ta.Command = cmd\n\n\treturn a\n}\n\n// AppendFlag adds flag(s) and returns self for chaining.\nfunc (a *IacArgs) AppendFlag(flags ...string) *IacArgs {\n\ta.Flags = append(a.Flags, flags...)\n\n\treturn a\n}\n\n// InsertFlag inserts flag(s) at position and returns self for chaining.\nfunc (a *IacArgs) InsertFlag(pos int, flags ...string) *IacArgs {\n\ta.Flags = slices.Insert(a.Flags, pos, flags...)\n\n\treturn a\n}\n\n// AppendArgument adds argument(s) and returns self for chaining.\nfunc (a *IacArgs) AppendArgument(args ...string) *IacArgs {\n\ta.Arguments = append(a.Arguments, args...)\n\n\treturn a\n}\n\n// InsertArgument inserts an argument at position and returns self for chaining.\nfunc (a *IacArgs) InsertArgument(pos int, arg string) *IacArgs {\n\ta.Arguments = slices.Insert(a.Arguments, pos, arg)\n\n\treturn a\n}\n\n// InsertArguments inserts arguments at position and returns self for chaining.\nfunc (a *IacArgs) InsertArguments(pos int, args ...string) *IacArgs {\n\ta.Arguments = slices.Insert(a.Arguments, pos, args...)\n\n\treturn a\n}\n\n// AppendSubCommand adds subcommand(s) and returns self for chaining.\nfunc (a *IacArgs) AppendSubCommand(subs ...string) *IacArgs {\n\ta.SubCommand = append(a.SubCommand, subs...)\n\n\treturn a\n}\n\n// AddFlagIfNotPresent adds a flag only if not already present.\nfunc (a *IacArgs) AddFlagIfNotPresent(flag string) *IacArgs {\n\tif !slices.Contains(a.Flags, flag) {\n\t\ta.Flags = append(a.Flags, flag)\n\t}\n\n\treturn a\n}\n\n// HasFlag checks if flag exists (handles -flag, --flag and -flag=value formats).\n// Note: Values starting with \"-\" (like -module.resource) are indistinguishable from flags.\nfunc (a *IacArgs) HasFlag(name string) bool {\n\ttarget := normalizeFlag(name)\n\n\treturn slices.ContainsFunc(a.Flags, func(f string) bool {\n\t\tif !isFlag(f) {\n\t\t\treturn false\n\t\t}\n\n\t\treturn normalizeFlag(extractFlagName(f)) == target\n\t})\n}\n\n// RemoveFlag removes a flag by name (handles -flag, --flag, -flag=value, and space-separated -flag value).\nfunc (a *IacArgs) RemoveFlag(name string) *IacArgs {\n\tnewFlags := make([]string, 0, len(a.Flags))\n\ttarget := normalizeFlag(name)\n\n\tfor i := 0; i < len(a.Flags); i++ {\n\t\tf := a.Flags[i]\n\n\t\t// Only treat tokens starting with \"-\" as potential flags\n\t\tif !isFlag(f) {\n\t\t\tnewFlags = append(newFlags, f)\n\t\t\tcontinue\n\t\t}\n\n\t\tcurrent := normalizeFlag(extractFlagName(f))\n\n\t\tif current == target {\n\t\t\t// Skip value too if: no =value, next entry is a value, and it's a known value-taking flag\n\t\t\thasNextValue := !strings.Contains(f, \"=\") && i+1 < len(a.Flags) && !strings.HasPrefix(a.Flags[i+1], \"-\")\n\t\t\tif hasNextValue && slices.Contains(valueTakingFlags, current) {\n\t\t\t\ti++ // skip the value\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\tnewFlags = append(newFlags, f)\n\t}\n\n\ta.Flags = newFlags\n\n\treturn a\n}\n\n// HasPlanFile checks if a plan file is already specified in args.\n// Checks for -out= flag (plan command) or any argument present (apply/destroy).\nfunc (a *IacArgs) HasPlanFile() bool {\n\t// Check for -out= flag (used with plan command)\n\tif a.HasFlag(\"out\") {\n\t\treturn true\n\t}\n\n\t// For apply/destroy: any argument is assumed to be a plan file\n\t// (can't reliably check file existence - path may be relative or created later)\n\treturn len(a.Arguments) > 0\n}\n\n// MergeFlags merges flags from another IacArgs, adding only flags not already present.\n// Handles both -flag=value and space-separated -flag value formats.\n// Returns self for chaining.\nfunc (a *IacArgs) MergeFlags(other *IacArgs) *IacArgs {\n\tfor i := 0; i < len(other.Flags); i++ {\n\t\tflag := other.Flags[i]\n\n\t\t// Check if this is a flag with space-separated value\n\t\thasValue := i+1 < len(other.Flags) &&\n\t\t\t!strings.HasPrefix(other.Flags[i+1], \"-\") &&\n\t\t\t!strings.Contains(flag, \"=\") &&\n\t\t\tstrings.HasPrefix(flag, \"-\")\n\n\t\tif hasValue {\n\t\t\tvalue := other.Flags[i+1]\n\t\t\tif !a.hasFlagWithValue(flag, value) {\n\t\t\t\ta.Flags = append(a.Flags, flag, value)\n\t\t\t}\n\n\t\t\ti++ // skip the value in iteration\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif a.Contains(flag) {\n\t\t\tcontinue\n\t\t}\n\n\t\ta.Flags = append(a.Flags, flag)\n\t}\n\n\treturn a\n}\n\n// hasFlagWithValue checks if a flag with specific value exists in either format.\nfunc (a *IacArgs) hasFlagWithValue(flag, value string) bool {\n\t// Check -flag=value format\n\tif slices.Contains(a.Flags, flag+\"=\"+value) {\n\t\treturn true\n\t}\n\n\t// Check space-separated format: find flag and verify next element is value\n\tif i := slices.Index(a.Flags, flag); i >= 0 && i+1 < len(a.Flags) {\n\t\treturn a.Flags[i+1] == value\n\t}\n\n\treturn false\n}\n\n// Slice returns args in correct order: [command] [flags...] [arguments...]\nfunc (a *IacArgs) Slice() []string {\n\tresult := make([]string, 0, 1+len(a.SubCommand)+len(a.Flags)+len(a.Arguments))\n\n\tif a.Command != \"\" {\n\t\tresult = append(result, a.Command)\n\t}\n\n\tresult = append(result, a.SubCommand...)\n\tresult = append(result, a.Flags...)\n\tresult = append(result, a.Arguments...)\n\n\treturn result\n}\n\n// Clone returns a deep copy of IacArgs.\n// Note: This performs a deep copy of slices (Command, SubCommand, Flags, Arguments).\n// If IacArgs is extended with pointer fields or nested structs in the future,\n// this method must be updated to ensure deep copying of those fields as well.\nfunc (a *IacArgs) Clone() *IacArgs {\n\treturn &IacArgs{\n\t\tCommand:    a.Command,\n\t\tSubCommand: slices.Clone(a.SubCommand),\n\t\tFlags:      slices.Clone(a.Flags),\n\t\tArguments:  slices.Clone(a.Arguments),\n\t}\n}\n\n// First returns the command (first element).\nfunc (a *IacArgs) First() string {\n\treturn a.Command\n}\n\n// Second returns the second element (first subcommand, first flag, or first argument).\nfunc (a *IacArgs) Second() string {\n\tif len(a.SubCommand) > 0 {\n\t\treturn a.SubCommand[0]\n\t}\n\n\tif len(a.Flags) > 0 {\n\t\treturn a.Flags[0]\n\t}\n\n\tif len(a.Arguments) > 0 {\n\t\treturn a.Arguments[0]\n\t}\n\n\treturn \"\"\n}\n\n// Last returns the last element (last argument, or last flag, or command).\nfunc (a *IacArgs) Last() string {\n\tif len(a.Arguments) > 0 {\n\t\treturn a.Arguments[len(a.Arguments)-1]\n\t}\n\n\tif len(a.Flags) > 0 {\n\t\treturn a.Flags[len(a.Flags)-1]\n\t}\n\n\treturn a.Command\n}\n\n// Tail returns everything except the command (subcommand, flags, and arguments) as a slice.\nfunc (a *IacArgs) Tail() []string {\n\tresult := make([]string, 0, len(a.SubCommand)+len(a.Flags)+len(a.Arguments))\n\tresult = append(result, a.SubCommand...)\n\tresult = append(result, a.Flags...)\n\tresult = append(result, a.Arguments...)\n\n\treturn result\n}\n\n// Contains checks if the args contain the target (in command, subcommand, flags, or arguments).\nfunc (a *IacArgs) Contains(target string) bool {\n\treturn a.Command == target ||\n\t\tslices.Contains(a.SubCommand, target) ||\n\t\tslices.Contains(a.Flags, target) ||\n\t\tslices.Contains(a.Arguments, target)\n}\n\n// Normalize formats the flags according to the given actions.\nfunc (a *IacArgs) Normalize(acts ...NormalizeActsType) *IacArgs {\n\tresult := a.Clone()\n\tresult.Flags = make([]string, 0, len(a.Flags))\n\n\tfor _, flag := range a.Flags {\n\t\tnormalized := flag\n\n\t\tfor _, act := range acts {\n\t\t\tswitch act {\n\t\t\tcase SingleDashFlag:\n\t\t\t\tif doubleDashRegexp.MatchString(normalized) {\n\t\t\t\t\tnormalized = normalized[1:]\n\t\t\t\t}\n\t\t\tcase DoubleDashFlag:\n\t\t\t\tif singleDashRegexp.MatchString(normalized) {\n\t\t\t\t\tnormalized = \"-\" + normalized\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tresult.Flags = append(result.Flags, normalized)\n\t}\n\n\treturn result\n}\n\n// IsDestroyCommand returns true if this represents a destroy operation.\n// It checks both the command name and the -destroy flag.\nfunc (a *IacArgs) IsDestroyCommand(cmd string) bool {\n\treturn cmd == CommandNameDestroy || a.Contains(\"-\"+CommandNameDestroy)\n}\n\n// parse parses raw args into Command/SubCommand/Flags/Arguments.\n// Known terraform subcommands before any flags go to SubCommand (stay in place).\n// Other non-flag args go to Arguments (appear at end).\nfunc (a *IacArgs) parse(args []string) {\n\tskipNext := false\n\tseenFlag := false\n\n\tfor i, arg := range args {\n\t\tif skipNext {\n\t\t\tskipNext = false\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.HasPrefix(arg, \"-\") {\n\t\t\tseenFlag = true\n\t\t\tskipNext = a.processFlag(arg, args, i)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif a.Command == \"\" {\n\t\t\ta.Command = arg\n\n\t\t\tcontinue\n\t\t}\n\n\t\t// Known subcommands before flags -> SubCommand (e.g., \"lock\" in \"providers lock\")\n\t\tif !seenFlag && IsKnownSubCommand(arg) {\n\t\t\ta.SubCommand = append(a.SubCommand, arg)\n\t\t\tcontinue\n\t\t}\n\n\t\t// All other non-flag args -> Arguments (e.g., plan files, resource addresses)\n\t\ta.Arguments = append(a.Arguments, arg)\n\t}\n}\n\n// knownSubCommands lists terraform subcommands that appear after the main command.\n// These should NOT be reordered to the end like plan files.\n// Maintainers: Add new Terraform/OpenTofu subcommands here as they are introduced.\nvar knownSubCommands = []string{\n\t// providers subcommands\n\t\"lock\", \"mirror\", \"schema\",\n\t// state subcommands\n\t\"list\", \"mv\", \"pull\", \"push\", \"replace-provider\", \"rm\", \"show\",\n\t// workspace subcommands\n\t\"delete\", \"new\", \"select\",\n\t// force-unlock takes an argument, not a subcommand\n}\n\n// IsKnownSubCommand returns true if arg is a known terraform subcommand.\nfunc IsKnownSubCommand(arg string) bool {\n\treturn slices.Contains(knownSubCommands, arg)\n}\n\n// processFlag handles flag parsing, returns true if next arg should be skipped.\n// Unknown flags are assumed to be boolean. Only known value-taking flags consume the next arg.\nfunc (a *IacArgs) processFlag(arg string, args []string, i int) bool {\n\t// Malformed flag (just \"-\" or empty), or flag with inline value (-flag=value)\n\tif len(arg) < minFlagLen || strings.Contains(arg, \"=\") {\n\t\ta.Flags = append(a.Flags, arg)\n\t\treturn false\n\t}\n\n\t// Known value-taking flag with next arg available that looks like a value\n\tflagName := normalizeFlag(extractFlagName(arg))\n\thasNextValue := i+1 < len(args) && !strings.HasPrefix(args[i+1], \"-\")\n\n\tif slices.Contains(valueTakingFlags, flagName) && hasNextValue {\n\t\ta.Flags = append(a.Flags, arg, args[i+1])\n\t\treturn true\n\t}\n\n\t// Unknown flags and boolean flags are self-contained\n\ta.Flags = append(a.Flags, arg)\n\n\treturn false\n}\n\n// extractFlagName gets flag name before = if present.\nfunc extractFlagName(arg string) string {\n\tname, _, _ := strings.Cut(arg, \"=\")\n\treturn name\n}\n"
  },
  {
    "path": "internal/iacargs/iacargs_test.go",
    "content": "package iacargs_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNew(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname      string\n\t\tinput     []string\n\t\twantCmd   string\n\t\twantFlags []string\n\t\twantArgs  []string\n\t}{\n\t\t{\n\t\t\tname:      \"simple apply\",\n\t\t\tinput:     []string{\"apply\", \"tfplan\"},\n\t\t\twantCmd:   \"apply\",\n\t\t\twantFlags: nil,\n\t\t\twantArgs:  []string{\"tfplan\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"apply with flags\",\n\t\t\tinput:     []string{\"apply\", \"-input=false\", \"tfplan\"},\n\t\t\twantCmd:   \"apply\",\n\t\t\twantFlags: []string{\"-input=false\"},\n\t\t\twantArgs:  []string{\"tfplan\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"issue #5409 - plan file before flags\",\n\t\t\tinput:     []string{\"apply\", \"tfplan\", \"-input=false\", \"-auto-approve\"},\n\t\t\twantCmd:   \"apply\",\n\t\t\twantFlags: []string{\"-input=false\", \"-auto-approve\"},\n\t\t\twantArgs:  []string{\"tfplan\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"destroy case with plan file in middle\",\n\t\t\tinput:     []string{\"apply\", \"-destroy\", \"/tmp/x.tfplan\", \"-auto-approve\"},\n\t\t\twantCmd:   \"apply\",\n\t\t\twantFlags: []string{\"-destroy\", \"-auto-approve\"},\n\t\t\twantArgs:  []string{\"/tmp/x.tfplan\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"full destroy case with all flags\",\n\t\t\tinput:     []string{\"apply\", \"-no-color\", \"-destroy\", \"-input=false\", \"/tmp/plan.tfplan\", \"-auto-approve\"},\n\t\t\twantCmd:   \"apply\",\n\t\t\twantFlags: []string{\"-no-color\", \"-destroy\", \"-input=false\", \"-auto-approve\"},\n\t\t\twantArgs:  []string{\"/tmp/plan.tfplan\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"var with space-separated value\",\n\t\t\tinput:     []string{\"plan\", \"-var\", \"key=value\", \"-out=myplan\"},\n\t\t\twantCmd:   \"plan\",\n\t\t\twantFlags: []string{\"-var\", \"key=value\", \"-out=myplan\"},\n\t\t\twantArgs:  nil,\n\t\t},\n\t\t{\n\t\t\tname:      \"target with space-separated value\",\n\t\t\tinput:     []string{\"plan\", \"-target\", \"module.foo\", \"tfplan\"},\n\t\t\twantCmd:   \"plan\",\n\t\t\twantFlags: []string{\"-target\", \"module.foo\"},\n\t\t\twantArgs:  []string{\"tfplan\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"var-file with space-separated value\",\n\t\t\tinput:     []string{\"apply\", \"-var-file\", \"vars.tfvars\"},\n\t\t\twantCmd:   \"apply\",\n\t\t\twantFlags: []string{\"-var-file\", \"vars.tfvars\"},\n\t\t\twantArgs:  nil,\n\t\t},\n\t\t{\n\t\t\tname:      \"lock-timeout with space-separated value\",\n\t\t\tinput:     []string{\"apply\", \"-lock-timeout\", \"5m\", \"-auto-approve\"},\n\t\t\twantCmd:   \"apply\",\n\t\t\twantFlags: []string{\"-lock-timeout\", \"5m\", \"-auto-approve\"},\n\t\t\twantArgs:  nil,\n\t\t},\n\t\t{\n\t\t\t// Unknown flags are treated as boolean. If a new Terraform flag needs\n\t\t\t// space-separated values, add it to valueTakingFlags list.\n\t\t\tname:      \"unknown flag treated as boolean\",\n\t\t\tinput:     []string{\"apply\", \"-future-flag\", \"value\", \"-auto-approve\"},\n\t\t\twantCmd:   \"apply\",\n\t\t\twantFlags: []string{\"-future-flag\", \"-auto-approve\"},\n\t\t\twantArgs:  []string{\"value\"},\n\t\t},\n\t\t{\n\t\t\t// Unknown flags are boolean, so planfile correctly goes to Arguments.\n\t\t\tname:      \"unknown boolean flag followed by arg\",\n\t\t\tinput:     []string{\"apply\", \"-unknown-bool\", \"planfile\"},\n\t\t\twantCmd:   \"apply\",\n\t\t\twantFlags: []string{\"-unknown-bool\"},\n\t\t\twantArgs:  []string{\"planfile\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"empty args\",\n\t\t\tinput:     []string{},\n\t\t\twantCmd:   \"\",\n\t\t\twantFlags: nil,\n\t\t\twantArgs:  nil,\n\t\t},\n\t\t{\n\t\t\tname:      \"only command\",\n\t\t\tinput:     []string{\"apply\"},\n\t\t\twantCmd:   \"apply\",\n\t\t\twantFlags: nil,\n\t\t\twantArgs:  nil,\n\t\t},\n\t\t{\n\t\t\tname:      \"only flags\",\n\t\t\tinput:     []string{\"-auto-approve\"},\n\t\t\twantCmd:   \"\",\n\t\t\twantFlags: []string{\"-auto-approve\"},\n\t\t\twantArgs:  nil,\n\t\t},\n\t\t{\n\t\t\tname:      \"chdir with space-separated value\",\n\t\t\tinput:     []string{\"-chdir\", \"/tmp/dir\", \"apply\"},\n\t\t\twantCmd:   \"apply\",\n\t\t\twantFlags: []string{\"-chdir\", \"/tmp/dir\"},\n\t\t\twantArgs:  nil,\n\t\t},\n\t\t{\n\t\t\tname:      \"chdir with equals value\",\n\t\t\tinput:     []string{\"-chdir=/tmp/dir\", \"plan\"},\n\t\t\twantCmd:   \"plan\",\n\t\t\twantFlags: []string{\"-chdir=/tmp/dir\"},\n\t\t\twantArgs:  nil,\n\t\t},\n\t\t{\n\t\t\tname:      \"multiple positional args\",\n\t\t\tinput:     []string{\"apply\", \"plan1\", \"plan2\", \"-auto-approve\"},\n\t\t\twantCmd:   \"apply\",\n\t\t\twantFlags: []string{\"-auto-approve\"},\n\t\t\twantArgs:  []string{\"plan1\", \"plan2\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"var with equals format\",\n\t\t\tinput:     []string{\"plan\", \"-var=key=value\", \"tfplan\"},\n\t\t\twantCmd:   \"plan\",\n\t\t\twantFlags: []string{\"-var=key=value\"},\n\t\t\twantArgs:  []string{\"tfplan\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tgot := iacargs.New(tt.input...)\n\n\t\t\tassert.Equal(t, tt.wantCmd, got.Command)\n\n\t\t\tif tt.wantFlags == nil {\n\t\t\t\tassert.Empty(t, got.Flags)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tt.wantFlags, got.Flags)\n\t\t\t}\n\n\t\t\tif tt.wantArgs == nil {\n\t\t\t\tassert.Empty(t, got.Arguments)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tt.wantArgs, got.Arguments)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestIacArgsSlice(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname  string\n\t\tinput *iacargs.IacArgs\n\t\twant  []string\n\t}{\n\t\t{\n\t\t\tname: \"basic apply with flags and args\",\n\t\t\tinput: &iacargs.IacArgs{\n\t\t\t\tCommand:   \"apply\",\n\t\t\t\tFlags:     []string{\"-input=false\", \"-auto-approve\"},\n\t\t\t\tArguments: []string{\"tfplan\"},\n\t\t\t},\n\t\t\twant: []string{\"apply\", \"-input=false\", \"-auto-approve\", \"tfplan\"},\n\t\t},\n\t\t{\n\t\t\tname: \"command only\",\n\t\t\tinput: &iacargs.IacArgs{\n\t\t\t\tCommand: \"plan\",\n\t\t\t},\n\t\t\twant: []string{\"plan\"},\n\t\t},\n\t\t{\n\t\t\tname: \"empty\",\n\t\t\tinput: &iacargs.IacArgs{\n\t\t\t\tFlags:     []string{},\n\t\t\t\tArguments: []string{},\n\t\t\t},\n\t\t\twant: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"flags only\",\n\t\t\tinput: &iacargs.IacArgs{\n\t\t\t\tFlags: []string{\"-auto-approve\"},\n\t\t\t},\n\t\t\twant: []string{\"-auto-approve\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tgot := tt.input.Slice()\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestIacArgsRoundTrip(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname  string\n\t\tinput []string\n\t\twant  []string\n\t}{\n\t\t{\n\t\t\tname:  \"issue #5409 - reorder plan file to end\",\n\t\t\tinput: []string{\"apply\", \"tfplan\", \"-input=false\", \"-auto-approve\"},\n\t\t\twant:  []string{\"apply\", \"-input=false\", \"-auto-approve\", \"tfplan\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"already correct order\",\n\t\t\tinput: []string{\"apply\", \"-input=false\", \"-auto-approve\", \"tfplan\"},\n\t\t\twant:  []string{\"apply\", \"-input=false\", \"-auto-approve\", \"tfplan\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"destroy with plan file in middle\",\n\t\t\tinput: []string{\"apply\", \"-destroy\", \"/tmp/plan.tfplan\", \"-auto-approve\"},\n\t\t\twant:  []string{\"apply\", \"-destroy\", \"-auto-approve\", \"/tmp/plan.tfplan\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"providers lock subcommand preserves order\",\n\t\t\tinput: []string{\"providers\", \"lock\", \"-platform=linux_amd64\", \"-platform=darwin_arm64\"},\n\t\t\twant:  []string{\"providers\", \"lock\", \"-platform=linux_amd64\", \"-platform=darwin_arm64\"},\n\t\t},\n\t\t{\n\t\t\tname:  \"state mv subcommand preserves order\",\n\t\t\tinput: []string{\"state\", \"mv\", \"-lock=false\", \"aws_instance.a\", \"aws_instance.b\"},\n\t\t\twant:  []string{\"state\", \"mv\", \"-lock=false\", \"aws_instance.a\", \"aws_instance.b\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tparsed := iacargs.New(tt.input...)\n\t\t\tgot := parsed.Slice()\n\t\t\tassert.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n\nfunc TestIacArgsAddFlagIfNotPresent(t *testing.T) {\n\tt.Parallel()\n\n\targs := &iacargs.IacArgs{\n\t\tCommand: \"apply\",\n\t\tFlags:   []string{\"-auto-approve\"},\n\t}\n\n\t// Add new flag\n\targs.AddFlagIfNotPresent(\"-input=false\")\n\tassert.Contains(t, args.Flags, \"-input=false\")\n\n\t// Adding duplicate should not add again\n\targs.AddFlagIfNotPresent(\"-auto-approve\")\n\n\tcount := 0\n\n\tfor _, f := range args.Flags {\n\t\tif f == \"-auto-approve\" {\n\t\t\tcount++\n\t\t}\n\t}\n\n\tassert.Equal(t, 1, count)\n}\n\nfunc TestIacArgsHasFlag(t *testing.T) {\n\tt.Parallel()\n\n\targs := &iacargs.IacArgs{\n\t\tFlags: []string{\"-auto-approve\", \"-input=false\"},\n\t}\n\n\tassert.True(t, args.HasFlag(\"-auto-approve\"))\n\tassert.True(t, args.HasFlag(\"-input\"))\n\tassert.False(t, args.HasFlag(\"-destroy\"))\n}\n\nfunc TestIacArgsRemoveFlag(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname          string\n\t\tinitialFlags  []string\n\t\tflagToRemove  string\n\t\texpectedFlags []string\n\t}{\n\t\t{\n\t\t\tname:          \"remove flag with equals value\",\n\t\t\tinitialFlags:  []string{\"-auto-approve\", \"-input=false\", \"-destroy\"},\n\t\t\tflagToRemove:  \"-input\",\n\t\t\texpectedFlags: []string{\"-auto-approve\", \"-destroy\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"remove boolean flag\",\n\t\t\tinitialFlags:  []string{\"-auto-approve\", \"-destroy\"},\n\t\t\tflagToRemove:  \"-auto-approve\",\n\t\t\texpectedFlags: []string{\"-destroy\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"remove flag with space-separated value\",\n\t\t\tinitialFlags:  []string{\"-var\", \"foo=bar\", \"-auto-approve\"},\n\t\t\tflagToRemove:  \"-var\",\n\t\t\texpectedFlags: []string{\"-auto-approve\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"remove flag where next entry looks like flag preserves it\",\n\t\t\tinitialFlags:  []string{\"-target\", \"-module.resource\", \"-auto-approve\"},\n\t\t\tflagToRemove:  \"-target\",\n\t\t\texpectedFlags: []string{\"-module.resource\", \"-auto-approve\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"remove flag preserves other flags with dash-prefixed values\",\n\t\t\tinitialFlags:  []string{\"-var\", \"key=-value\", \"-target\", \"-module.foo\", \"-destroy\"},\n\t\t\tflagToRemove:  \"-var\",\n\t\t\texpectedFlags: []string{\"-target\", \"-module.foo\", \"-destroy\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"remove middle flag with space-separated value\",\n\t\t\tinitialFlags:  []string{\"-auto-approve\", \"-var\", \"x=y\", \"-destroy\"},\n\t\t\tflagToRemove:  \"-var\",\n\t\t\texpectedFlags: []string{\"-auto-approve\", \"-destroy\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"remove nonexistent flag does nothing\",\n\t\t\tinitialFlags:  []string{\"-auto-approve\", \"-destroy\"},\n\t\t\tflagToRemove:  \"-nonexistent\",\n\t\t\texpectedFlags: []string{\"-auto-approve\", \"-destroy\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\targs := &iacargs.IacArgs{\n\t\t\t\tFlags: append([]string{}, tt.initialFlags...),\n\t\t\t}\n\t\t\targs.RemoveFlag(tt.flagToRemove)\n\t\t\tassert.Equal(t, tt.expectedFlags, args.Flags)\n\t\t})\n\t}\n}\n\nfunc TestIacArgsAppendArgument(t *testing.T) {\n\tt.Parallel()\n\n\targs := &iacargs.IacArgs{\n\t\tCommand:   \"apply\",\n\t\tArguments: []string{\"plan1\"},\n\t}\n\n\targs.AppendArgument(\"plan2\")\n\tassert.Equal(t, []string{\"plan1\", \"plan2\"}, args.Arguments)\n}\n\nfunc TestIacArgsClone(t *testing.T) {\n\tt.Parallel()\n\n\toriginal := &iacargs.IacArgs{\n\t\tCommand:   \"apply\",\n\t\tFlags:     []string{\"-auto-approve\"},\n\t\tArguments: []string{\"tfplan\"},\n\t}\n\n\tclone := original.Clone()\n\n\t// Verify values are equal\n\tassert.Equal(t, original.Command, clone.Command)\n\tassert.Equal(t, original.Flags, clone.Flags)\n\tassert.Equal(t, original.Arguments, clone.Arguments)\n\n\t// Verify modifying clone doesn't affect original\n\tclone.Command = \"plan\"\n\tclone.Flags = append(clone.Flags, \"-input=false\")\n\tclone.Arguments = append(clone.Arguments, \"another\")\n\n\tassert.Equal(t, \"apply\", original.Command)\n\tassert.Equal(t, []string{\"-auto-approve\"}, original.Flags)\n\tassert.Equal(t, []string{\"tfplan\"}, original.Arguments)\n}\n\nfunc TestIacArgsContains(t *testing.T) {\n\tt.Parallel()\n\n\targs := &iacargs.IacArgs{\n\t\tCommand:   \"apply\",\n\t\tFlags:     []string{\"-auto-approve\", \"-input=false\"},\n\t\tArguments: []string{\"tfplan\"},\n\t}\n\n\tassert.True(t, args.Contains(\"apply\"))\n\tassert.True(t, args.Contains(\"-auto-approve\"))\n\tassert.True(t, args.Contains(\"tfplan\"))\n\tassert.False(t, args.Contains(\"-destroy\"))\n}\n\nfunc TestIacArgsFirst(t *testing.T) {\n\tt.Parallel()\n\n\targs := iacargs.New(\"apply\", \"-auto-approve\", \"tfplan\")\n\tassert.Equal(t, \"apply\", args.First())\n\n\tempty := iacargs.New()\n\tassert.Empty(t, empty.First())\n}\n\nfunc TestIacArgsTail(t *testing.T) {\n\tt.Parallel()\n\n\targs := iacargs.New(\"apply\", \"-auto-approve\", \"tfplan\")\n\tassert.Equal(t, []string{\"-auto-approve\", \"tfplan\"}, args.Tail())\n\n\tempty := iacargs.New()\n\tassert.Empty(t, empty.Tail())\n}\n\nfunc TestIacArgsHasPlanFile(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\targs     *iacargs.IacArgs\n\t\tname     string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"empty args\",\n\t\t\targs:     iacargs.New(),\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"plan with -out flag\",\n\t\t\targs:     iacargs.New(\"plan\", \"-out=tfplan\"),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"plan without -out flag\",\n\t\t\targs:     iacargs.New(\"plan\", \"-input=false\"),\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"apply with plan file argument\",\n\t\t\targs:     iacargs.New(\"apply\", \"-auto-approve\", \"tfplan\"),\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"apply without plan file\",\n\t\t\targs:     iacargs.New(\"apply\", \"-auto-approve\"),\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"destroy with plan file argument\",\n\t\t\targs:     iacargs.New(\"destroy\", \"tfplan\"),\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, tt.expected, tt.args.HasPlanFile())\n\t\t})\n\t}\n}\n\nfunc TestIacArgsMergeFlags(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname          string\n\t\tbase          *iacargs.IacArgs\n\t\tother         *iacargs.IacArgs\n\t\texpectedFlags []string\n\t}{\n\t\t{\n\t\t\tname:          \"merge into empty\",\n\t\t\tbase:          iacargs.New(\"apply\"),\n\t\t\tother:         iacargs.New(\"apply\", \"-auto-approve\", \"-input=false\"),\n\t\t\texpectedFlags: []string{\"-auto-approve\", \"-input=false\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"skip duplicates\",\n\t\t\tbase:          iacargs.New(\"apply\", \"-auto-approve\"),\n\t\t\tother:         iacargs.New(\"apply\", \"-auto-approve\", \"-input=false\"),\n\t\t\texpectedFlags: []string{\"-auto-approve\", \"-input=false\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"merge from empty\",\n\t\t\tbase:          iacargs.New(\"apply\", \"-auto-approve\"),\n\t\t\tother:         iacargs.New(),\n\t\t\texpectedFlags: []string{\"-auto-approve\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"both have different flags\",\n\t\t\tbase:          iacargs.New(\"apply\", \"-compact-warnings\"),\n\t\t\tother:         iacargs.New(\"apply\", \"-auto-approve\", \"-input=false\"),\n\t\t\texpectedFlags: []string{\"-compact-warnings\", \"-auto-approve\", \"-input=false\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\ttt.base.MergeFlags(tt.other)\n\t\t\tassert.Equal(t, tt.expectedFlags, tt.base.Flags)\n\t\t})\n\t}\n}\n\nfunc TestIacArgsIsDestroyCommand(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\targs     *iacargs.IacArgs\n\t\tcmd      string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname:     \"destroy command\",\n\t\t\targs:     iacargs.New(\"destroy\"),\n\t\t\tcmd:      \"destroy\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"apply with -destroy flag\",\n\t\t\targs:     iacargs.New(\"apply\", \"-destroy\", \"tfplan\"),\n\t\t\tcmd:      \"apply\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"regular apply\",\n\t\t\targs:     iacargs.New(\"apply\", \"-auto-approve\"),\n\t\t\tcmd:      \"apply\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"plan command\",\n\t\t\targs:     iacargs.New(\"plan\"),\n\t\t\tcmd:      \"plan\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"nil args with destroy cmd\",\n\t\t\targs:     nil,\n\t\t\tcmd:      \"destroy\",\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tif tt.args == nil {\n\t\t\t\t// Test nil case separately\n\t\t\t\tassert.Equal(t, tt.expected, tt.cmd == \"destroy\")\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tt.expected, tt.args.IsDestroyCommand(tt.cmd))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/iam/iam.go",
    "content": "// Package iam provides shared types for IAM role configuration used across\n// multiple packages (options, config, awshelper, etc.).\npackage iam\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\nconst (\n\t// DefaultAssumeRoleDuration is the default session duration in seconds for IAM role assumption.\n\tDefaultAssumeRoleDuration = 3600\n)\n\n// GetDefaultAssumeRoleSessionName returns a unique session name for IAM role assumption.\nfunc GetDefaultAssumeRoleSessionName() string {\n\treturn fmt.Sprintf(\"terragrunt-%d\", time.Now().UTC().UnixNano())\n}\n\n// RoleOptions represents options that are used by Terragrunt to assume an IAM role.\ntype RoleOptions struct {\n\tRoleARN               string\n\tWebIdentityToken      string\n\tAssumeRoleSessionName string\n\tAssumeRoleDuration    int64\n}\n\n// MergeRoleOptions merges the source IAM role options into the target, preferring\n// non-zero source values.\nfunc MergeRoleOptions(target RoleOptions, source RoleOptions) RoleOptions {\n\tout := target\n\n\tif source.RoleARN != \"\" {\n\t\tout.RoleARN = source.RoleARN\n\t}\n\n\tif source.AssumeRoleDuration != 0 {\n\t\tout.AssumeRoleDuration = source.AssumeRoleDuration\n\t}\n\n\tif source.AssumeRoleSessionName != \"\" {\n\t\tout.AssumeRoleSessionName = source.AssumeRoleSessionName\n\t}\n\n\tif source.WebIdentityToken != \"\" {\n\t\tout.WebIdentityToken = source.WebIdentityToken\n\t}\n\n\treturn out\n}\n"
  },
  {
    "path": "internal/locks/lock.go",
    "content": "// Package locks contains global locks used throughout Terragrunt.\npackage locks\n\nimport \"sync\"\n\n// EnvLock is the lock acquired when writing environment variables in a way\n// that is not safe for concurrent access.\n//\n// When possible, prefer to spawn a new process with the environment variables\n// you want, or avoid setting environment variables instead of using this lock.\nvar EnvLock sync.Mutex //nolint:gochecknoglobals\n"
  },
  {
    "path": "internal/os/exec/cmd.go",
    "content": "// Package exec runs external commands. It wraps exec.Cmd package with support for allocating a pseudo-terminal.\npackage exec\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/os/signal\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\n// DefaultGracefulShutdownDelay is the default time to wait for a process to exit\n// gracefully after sending an interrupt signal before escalating to SIGKILL.\nconst DefaultGracefulShutdownDelay = 30 * time.Second\n\n// Cmd is a command type.\ntype Cmd struct {\n\tlogger          log.Logger\n\tinterruptSignal os.Signal\n\t*exec.Cmd\n\tfilename                   string\n\tforwardSignalDelay         time.Duration\n\tusePTY                     bool\n\tgracefulShutdownRegistered atomic.Bool\n}\n\n// Command returns the `Cmd` struct to execute the named program with\n// the given arguments.\nfunc Command(ctx context.Context, name string, args ...string) *Cmd {\n\tcmd := &Cmd{\n\t\tCmd:             exec.CommandContext(ctx, name, args...),\n\t\tlogger:          log.Default(),\n\t\tfilename:        filepath.Base(name),\n\t\tinterruptSignal: signal.InterruptSignal,\n\t}\n\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\tcmd.WaitDelay = DefaultGracefulShutdownDelay\n\n\tcmd.Cancel = func() error {\n\t\tif cmd.gracefulShutdownRegistered.Load() {\n\t\t\treturn nil\n\t\t}\n\n\t\tif cmd.Process == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tif sig := signal.SignalFromContext(ctx); sig != nil {\n\t\t\treturn cmd.Process.Signal(sig)\n\t\t}\n\n\t\tif cmd.interruptSignal != nil {\n\t\t\treturn cmd.Process.Signal(cmd.interruptSignal)\n\t\t}\n\n\t\treturn cmd.Process.Signal(os.Kill)\n\t}\n\n\treturn cmd\n}\n\n// Configure sets options to the `Cmd`.\nfunc (cmd *Cmd) Configure(opts ...Option) {\n\tfor _, opt := range opts {\n\t\topt(cmd)\n\t}\n}\n\n// Start starts the specified command but does not wait for it to complete.\nfunc (cmd *Cmd) Start() error {\n\t// If we need to allocate a ptty for the command, route through the ptty routine.\n\t// Otherwise, directly call the command.\n\tif cmd.usePTY {\n\t\tif err := runCommandWithPTY(cmd.logger, cmd.Cmd); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else if err := cmd.Cmd.Start(); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n\n// RegisterGracefullyShutdown registers a graceful shutdown for the command in two ways:\n//  1. If the context cancel contains a cause with a signal, this means that Terragrunt received the signal from the OS,\n//     since our executed command may also receive the same signal, we need to give the command time to gracefully shutting down,\n//     to avoid the command receiving this signal twice.\n//     Thus we will send the signal to the executed command with a delay or immediately if Terragrunt receives this same signal again.\n//  2. If the context does not contain any causes, this means that there was some failure and we need to terminate all executed commands,\n//     in this situation we are sure that commands did not receive any signal, so we send them an interrupt signal immediately.\nfunc (cmd *Cmd) RegisterGracefullyShutdown(ctx context.Context) func() {\n\tcmd.gracefulShutdownRegistered.Store(true)\n\n\tctxShutdown, cancelShutdown := context.WithCancel(context.Background())\n\n\tgo func() {\n\t\tselect {\n\t\tcase <-ctxShutdown.Done():\n\t\tcase <-ctx.Done():\n\t\t\tif cause := new(signal.ContextCanceledError); errors.As(context.Cause(ctx), &cause) && cause.Signal != nil {\n\t\t\t\tcmd.ForwardSignal(ctxShutdown, cause.Signal)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tcmd.SendSignal(cmd.interruptSignal)\n\t\t}\n\t}()\n\n\treturn cancelShutdown\n}\n\n// ForwardSignal forwards a given `sig` with a delay if cmd.forwardSignalDelay is greater than 0,\n// and if the same signal is received again, it is forwarded immediately.\nfunc (cmd *Cmd) ForwardSignal(ctx context.Context, sig os.Signal) {\n\tctxDelay, cancelDelay := context.WithCancel(ctx)\n\tdefer cancelDelay()\n\n\tsignal.NotifierWithContext(ctx, func(_ os.Signal) {\n\t\tcancelDelay()\n\t}, sig)\n\n\tif cmd.forwardSignalDelay > 0 {\n\t\tcmd.logger.Debugf(\"%s signal will be forwarded to %s with delay %s\",\n\t\t\tcases.Title(language.English).String(sig.String()),\n\t\t\tcmd.filename,\n\t\t\tcmd.forwardSignalDelay,\n\t\t)\n\t}\n\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn\n\tcase <-time.After(cmd.forwardSignalDelay):\n\tcase <-ctxDelay.Done():\n\t}\n\n\tcmd.SendSignal(sig)\n}\n\n// SendSignal sends the given `sig` to the executed command.\nfunc (cmd *Cmd) SendSignal(sig os.Signal) {\n\tcmd.logger.Debugf(\"%s signal is forwarded to %s\", cases.Title(language.English).String(sig.String()), cmd.filename)\n\n\tif err := cmd.Process.Signal(sig); err != nil {\n\t\tcmd.logger.Errorf(\"Failed to forwarding signal %s to %s: %v\", sig, cmd.filename, err)\n\t}\n}\n"
  },
  {
    "path": "internal/os/exec/cmd_unix_test.go",
    "content": "//go:build linux || darwin\n// +build linux darwin\n\npackage exec_test\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/os/exec\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar (\n\terrExplicitError = errors.New(\"this is an explicit error\")\n)\n\nfunc TestExitCodeUnix(t *testing.T) {\n\tt.Parallel()\n\n\tfor index := 0; index <= 255; index++ {\n\t\tcmd := exec.Command(t.Context(), \"testdata/test_exit_code.sh\", strconv.Itoa(index))\n\t\terr := cmd.Run()\n\n\t\tif index == 0 {\n\t\t\trequire.NoError(t, err)\n\t\t} else {\n\t\t\trequire.Error(t, err)\n\t\t}\n\n\t\tretCode, err := util.GetExitCode(err)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, index, retCode)\n\t}\n\n\t// assert a non exec.ExitError returns an error\n\tretCode, retErr := util.GetExitCode(errExplicitError)\n\trequire.Error(t, retErr, \"An error was expected\")\n\tassert.Equal(t, errExplicitError, retErr)\n\tassert.Equal(t, 0, retCode)\n}\n\nfunc TestNewSignalsForwarderWaitUnix(t *testing.T) {\n\tt.Parallel()\n\n\texpectedWait := 5\n\n\tcmd := exec.Command(t.Context(), \"testdata/test_sigint_wait.sh\", strconv.Itoa(expectedWait))\n\n\trunChannel := make(chan error)\n\n\tgo func() {\n\t\trunChannel <- cmd.Run()\n\t}()\n\n\ttime.Sleep(time.Second)\n\n\tstart := time.Now()\n\n\tcmd.Process.Signal(os.Interrupt)\n\n\terr := <-runChannel\n\trequire.Error(t, err)\n\n\tretCode, err := util.GetExitCode(err)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, expectedWait, retCode)\n\tassert.WithinDuration(t, time.Now(), start.Add(time.Duration(expectedWait)*time.Second), time.Second,\n\t\t\"Expected to wait 5 (+/-1) seconds after SIGINT\")\n}\n\n// There isn't a proper way to catch interrupts in Windows batch scripts, so this test exists only for Unix.\nfunc TestNewSignalsForwarderMultipleUnix(t *testing.T) {\n\tt.Parallel()\n\n\texpectedInterrupts := 10\n\n\tcmd := exec.Command(t.Context(), \"testdata/test_sigint_multiple.sh\", strconv.Itoa(expectedInterrupts))\n\n\trunChannel := make(chan error)\n\n\tgo func() {\n\t\trunChannel <- cmd.Run()\n\t}()\n\n\ttime.Sleep(time.Second)\n\n\tinterruptAndWaitForProcess := func() (int, error) {\n\t\tvar (\n\t\t\tinterrupts int\n\t\t\terr        error\n\t\t)\n\n\t\tfor {\n\t\t\ttime.Sleep(500 * time.Millisecond)\n\n\t\t\tselect {\n\t\t\tcase err = <-runChannel:\n\t\t\t\treturn interrupts, err\n\t\t\tdefault:\n\t\t\t\tcmd.Process.Signal(os.Interrupt)\n\n\t\t\t\tinterrupts++\n\t\t\t}\n\t\t}\n\t}\n\n\tinterrupts, err := interruptAndWaitForProcess()\n\trequire.Error(t, err)\n\n\tretCode, err := util.GetExitCode(err)\n\trequire.NoError(t, err)\n\tassert.LessOrEqual(t, retCode, interrupts, \"Subprocess received wrong number of signals\")\n\tassert.Equal(t, expectedInterrupts, retCode, \"Subprocess didn't receive multiple signals\")\n}\n\n// TestGracefulShutdownOnContextCancelUnix verifies that when the context is cancelled\n// without a signal cause, the Cancel callback sends SIGINT (not SIGKILL) to allow\n// processes like Terraform to gracefully shutdown their child processes.\n// The test script traps SIGINT and exits with code 42, while SIGKILL would terminate\n// it immediately without running the trap handler.\nfunc TestGracefulShutdownOnContextCancelUnix(t *testing.T) {\n\tt.Parallel()\n\n\tctx, cancel := context.WithCancel(context.Background())\n\n\tcmd := exec.Command(ctx, \"testdata/test_graceful_shutdown.sh\")\n\n\tcmd.Configure(exec.WithGracefulShutdownDelay(5 * time.Second))\n\n\trunChannel := make(chan error)\n\n\tgo func() {\n\t\trunChannel <- cmd.Run()\n\t}()\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\tcancel()\n\n\terr := <-runChannel\n\trequire.Error(t, err)\n\n\tretCode, err := util.GetExitCode(err)\n\trequire.NoError(t, err)\n\n\tassert.Equal(\n\t\tt,\n\t\t42,\n\t\tretCode,\n\t\t\"Expected exit code 42 (SIGINT received), but got %d. \"+\n\t\t\t\"This suggests SIGKILL was sent instead of SIGINT.\",\n\t\tretCode,\n\t)\n}\n"
  },
  {
    "path": "internal/os/exec/cmd_windows_test.go",
    "content": "//go:build windows\n\npackage exec_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/os/exec\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestWindowsConsolePrepare(t *testing.T) {\n\tt.Parallel()\n\n\tstdout := bytes.Buffer{}\n\n\tl := log.New(log.WithOutput(&stdout), log.WithLevel(log.DebugLevel))\n\n\t// In a test environment, handles are not real console handles,\n\t// so PrepareConsole should return false.\n\tresult := exec.PrepareConsole(l)\n\tassert.False(t, result, \"PrepareConsole should return false when handles are invalid\")\n\n\tassert.Contains(t, stdout.String(), \"failed to get console mode\")\n}\n\nfunc TestWindowsExitCode(t *testing.T) {\n\tt.Parallel()\n\n\tfor i := 0; i <= 255; i++ {\n\t\tcmd := exec.Command(t.Context(), `testdata\\test_exit_code.bat`, strconv.Itoa(i))\n\t\terr := cmd.Run()\n\n\t\tif i == 0 {\n\t\t\tassert.NoError(t, err)\n\t\t} else {\n\t\t\tassert.Error(t, err)\n\t\t}\n\t\tretCode, err := util.GetExitCode(err)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, i, retCode)\n\t}\n\n\t// assert a non exec.ExitError returns an error\n\terr := errors.New(\"This is an explicit error\")\n\tretCode, retErr := util.GetExitCode(err)\n\tassert.Error(t, retErr, \"An error was expected\")\n\tassert.Equal(t, err, retErr)\n\tassert.Equal(t, 0, retCode)\n}\n\nfunc TestWindowsNewSignalsForwarderWait(t *testing.T) {\n\tt.Parallel()\n\n\texpectedWait := 5\n\n\tcmd := exec.Command(t.Context(), `testdata\\test_sigint_wait.bat`, strconv.Itoa(expectedWait))\n\n\trunChannel := make(chan error)\n\n\tgo func() {\n\t\trunChannel <- cmd.Run()\n\t}()\n\n\ttime.Sleep(time.Second)\n\t// start := time.Now()\n\t// Note: sending interrupt on Windows is not supported by Windows and not implemented in Go\n\tif cmd.Process != nil { // on some Go versions(Go 1.23, Windows), cmd.Process is nil\n\t\tcmd.Process.Signal(os.Kill)\n\t}\n\n\terr := <-runChannel\n\n\tassert.Error(t, err)\n\n\t// Since we can't send an interrupt on Windows, our test script won't handle it gracefully and exit after the expected wait time,\n\t// so this part of the test process cannot be done on Windows\n\t// retCode, err := GetExitCode(err)\n\t// assert.NoError(t, err)\n\t// assert.Equal(t, retCode, expectedWait)\n\t// assert.WithinDuration(t, start.Add(time.Duration(expectedWait)*time.Second), time.Now(), time.Second,\n\t// \t\"Expected to wait 5 (+/-1) seconds after SIGINT\")\n}\n"
  },
  {
    "path": "internal/os/exec/console_windows_test.go",
    "content": "//go:build windows\n\npackage exec_test\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/os/exec\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/sys/windows\"\n)\n\n// openConsoleOutput opens CONOUT$ directly, which always returns a real\n// console handle even when os.Stdin/os.Stdout are pipes (e.g. in GitHub Actions).\n// Skips the test if no console is attached at all.\nfunc openConsoleOutput(t *testing.T) *os.File {\n\tt.Helper()\n\n\tf, err := os.OpenFile(\"CONOUT$\", os.O_RDWR, 0)\n\tif err != nil {\n\t\tt.Skipf(\"skipping: no console attached (CONOUT$ unavailable): %v\", err)\n\t}\n\n\tt.Cleanup(func() { f.Close() })\n\n\tvar mode uint32\n\tif err := windows.GetConsoleMode(windows.Handle(f.Fd()), &mode); err != nil {\n\t\tf.Close()\n\t\tt.Skipf(\"skipping: CONOUT$ is not a usable console handle: %v\", err)\n\t}\n\n\treturn f\n}\n\nfunc openConsoleInput(t *testing.T) *os.File {\n\tt.Helper()\n\n\tf, err := os.OpenFile(\"CONIN$\", os.O_RDWR, 0)\n\tif err != nil {\n\t\tt.Skipf(\"skipping: no console attached (CONIN$ unavailable): %v\", err)\n\t}\n\n\tt.Cleanup(func() { f.Close() })\n\n\tvar mode uint32\n\tif err := windows.GetConsoleMode(windows.Handle(f.Fd()), &mode); err != nil {\n\t\tf.Close()\n\t\tt.Skipf(\"skipping: CONIN$ is not a usable console handle: %v\", err)\n\t}\n\n\treturn f\n}\n\nfunc getMode(t *testing.T, f *os.File) uint32 {\n\tt.Helper()\n\n\tvar mode uint32\n\trequire.NoError(t, windows.GetConsoleMode(windows.Handle(f.Fd()), &mode))\n\n\treturn mode\n}\n\nfunc setMode(t *testing.T, f *os.File, mode uint32) {\n\tt.Helper()\n\trequire.NoError(t, windows.SetConsoleMode(windows.Handle(f.Fd()), mode))\n}\n\n// TestWindowsConsoleStateOnPipes verifies that SaveConsoleState and Restore\n// work without error when standard handles are pipes (CI). The saved state\n// should round-trip: save then restore should not change the console mode.\nfunc TestWindowsConsoleStateOnPipes(t *testing.T) {\n\tt.Parallel()\n\n\tvar beforeMode uint32\n\tstdoutIsConsole := windows.GetConsoleMode(windows.Handle(os.Stdout.Fd()), &beforeMode) == nil\n\n\tsaved := exec.SaveConsoleState()\n\tsaved.Restore()\n\n\tvar afterMode uint32\n\tafterIsConsole := windows.GetConsoleMode(windows.Handle(os.Stdout.Fd()), &afterMode) == nil\n\n\tassert.Equal(t, stdoutIsConsole, afterIsConsole,\n\t\t\"stdout console status should not change after save/restore\")\n\n\tassert.Equal(t, beforeMode, afterMode,\n\t\t\"stdout console mode should be unchanged after save/restore\")\n}\n\n// TestWindowsConsolePrepareStdinOnPipes verifies PrepareStdinForPrompt handles\n// pipe stdin gracefully and does not corrupt console mode.\nfunc TestWindowsConsolePrepareStdinOnPipes(t *testing.T) {\n\tt.Parallel()\n\n\tvar beforeMode uint32\n\tstdinIsConsole := windows.GetConsoleMode(windows.Handle(os.Stdin.Fd()), &beforeMode) == nil\n\n\tl := log.New(log.WithLevel(log.DebugLevel))\n\texec.PrepareStdinForPrompt(l)\n\n\tvar afterMode uint32\n\tafterIsConsole := windows.GetConsoleMode(windows.Handle(os.Stdin.Fd()), &afterMode) == nil\n\n\tassert.Equal(t, stdinIsConsole, afterIsConsole,\n\t\t\"stdin console status should not change after PrepareStdinForPrompt\")\n\tassert.Equal(t, beforeMode, afterMode,\n\t\t\"stdin console mode should be unchanged after PrepareStdinForPrompt on pipes\")\n}\n\n// TestWindowsConsoleVTProcessingOnCONOUT verifies that VT processing can be\n// toggled on a real console handle via raw API calls.\nfunc TestWindowsConsoleVTProcessingOnCONOUT(t *testing.T) {\n\tt.Parallel()\n\n\tconout := openConsoleOutput(t)\n\toriginal := getMode(t, conout)\n\n\tdefer setMode(t, conout, original)\n\n\tcleared := original &^ windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING\n\tsetMode(t, conout, cleared)\n\n\tassert.Equal(t, uint32(0), getMode(t, conout)&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING,\n\t\t\"VT bit should be cleared before test\")\n\n\tsetMode(t, conout, cleared|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING)\n\n\tassert.NotEqual(t, uint32(0), getMode(t, conout)&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING,\n\t\t\"VT processing should be enabled on CONOUT$\")\n}\n\n// TestWindowsConsoleSaveRestoreOnCONOUT verifies the full save→corrupt→restore\n// cycle using a real console handle from CONOUT$. This is the core regression\n// test: subprocesses like \"terraform version\" clear VT processing, and Restore\n// must bring it back.\nfunc TestWindowsConsoleSaveRestoreOnCONOUT(t *testing.T) {\n\tt.Parallel()\n\n\tconout := openConsoleOutput(t)\n\toriginal := getMode(t, conout)\n\n\tdefer setMode(t, conout, original)\n\n\twithVT := original | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING\n\tsetMode(t, conout, withVT)\n\n\tbefore := getMode(t, conout)\n\trequire.Equal(t, withVT, before)\n\n\tsaved := before\n\n\tcorrupted := before &^ windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING\n\tsetMode(t, conout, corrupted)\n\n\tassert.Equal(t, uint32(0), getMode(t, conout)&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING,\n\t\t\"VT should be cleared after simulated subprocess corruption\")\n\n\tsetMode(t, conout, saved)\n\n\tafter := getMode(t, conout)\n\tassert.Equal(t, before, after,\n\t\t\"console mode must be identical after save→corrupt→restore cycle\")\n}\n\n// TestWindowsConsoleStdinFlagsOnCONIN verifies stdin prompt flags can be\n// cleared and restored via raw API on a real console input handle.\nfunc TestWindowsConsoleStdinFlagsOnCONIN(t *testing.T) {\n\tt.Parallel()\n\n\tconin := openConsoleInput(t)\n\toriginal := getMode(t, conin)\n\n\tdefer setMode(t, conin, original)\n\n\trequired := uint32(windows.ENABLE_LINE_INPUT | windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT)\n\n\tassert.Equal(t, required, original&required,\n\t\t\"a default console input handle should have LINE_INPUT, ECHO_INPUT, PROCESSED_INPUT\")\n\n\tsetMode(t, conin, original&^required)\n\tassert.Equal(t, uint32(0), getMode(t, conin)&required,\n\t\t\"required flags should be cleared after corruption\")\n\n\tsetMode(t, conin, original)\n\tassert.Equal(t, required, getMode(t, conin)&required,\n\t\t\"required flags should be restored\")\n}\n\n// TestWindowsConsoleSubprocessSaveRestore is an integration test that runs a\n// real subprocess and verifies the save→subprocess→restore pattern preserves\n// console modes. Uses CONOUT$ for a real console handle.\nfunc TestWindowsConsoleSubprocessSaveRestore(t *testing.T) {\n\tt.Parallel()\n\n\tconout := openConsoleOutput(t)\n\toriginal := getMode(t, conout)\n\n\tdefer setMode(t, conout, original)\n\n\twithVT := original | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING\n\tsetMode(t, conout, withVT)\n\n\tbefore := getMode(t, conout)\n\n\tcmd := exec.Command(t.Context(), \"cmd.exe\", \"/C\", \"echo hello\")\n\tcmd.Stdout = nil\n\tcmd.Stderr = nil\n\n\tsaved := exec.SaveConsoleState()\n\n\trequire.NoError(t, cmd.Run())\n\n\tsaved.Restore()\n\n\tsetMode(t, conout, before)\n\n\tafter := getMode(t, conout)\n\tassert.Equal(t, before, after,\n\t\t\"CONOUT$ mode should be unchanged after save→subprocess→restore\")\n}\n"
  },
  {
    "path": "internal/os/exec/opts.go",
    "content": "package exec\n\nimport (\n\t\"time\"\n\n\t\"github.com/gruntwork-io/go-commons/collections\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst envVarsListFormat = \"%s=%s\"\n\n// Option is type for passing options to the Cmd.\ntype Option func(*Cmd)\n\n// WithLogger sets Logger to the Cmd.\nfunc WithLogger(logger log.Logger) Option {\n\treturn func(cmd *Cmd) {\n\t\tcmd.logger = logger\n\t}\n}\n\n// WithUsePTY enables a pty for the Cmd.\nfunc WithUsePTY(state bool) Option {\n\treturn func(cmd *Cmd) {\n\t\tcmd.usePTY = state\n\t}\n}\n\n// WithEnv sets envs to the Cmd.\nfunc WithEnv(env map[string]string) Option {\n\treturn func(cmd *Cmd) {\n\t\tcmd.Env = collections.KeyValueStringSliceWithFormat(env, envVarsListFormat)\n\t}\n}\n\n// WithForwardSignalDelay sets forwarding signal delay to the Cmd.\nfunc WithForwardSignalDelay(delay time.Duration) Option {\n\treturn func(cmd *Cmd) {\n\t\tcmd.forwardSignalDelay = delay\n\t}\n}\n\n// WithGracefulShutdownDelay sets the time to wait for a process to exit gracefully\n// after sending an interrupt signal before escalating to SIGKILL.\n// This allows processes like Terraform to clean up child processes (e.g., provider plugins).\nfunc WithGracefulShutdownDelay(delay time.Duration) Option {\n\treturn func(cmd *Cmd) {\n\t\tcmd.WaitDelay = delay\n\t}\n}\n"
  },
  {
    "path": "internal/os/exec/ptty_unix.go",
    "content": "//go:build !windows\n\npackage exec\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"golang.org/x/sync/errgroup\"\n\t\"golang.org/x/term\"\n\n\t\"github.com/creack/pty\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// runCommandWithPTY will allocate a pseudo-tty to run the subcommand in. This is only necessary when running\n// interactive commands, so that terminal features like readline work through the subcommand when stdin, stdout, and\n// stderr is being shared.\n// NOTE: This is based on the quickstart example from https://github.com/creack/pty\nfunc runCommandWithPTY(logger log.Logger, cmd *exec.Cmd) (err error) {\n\tcmdStdout := cmd.Stdout\n\n\tcmd.Stdin = nil\n\tcmd.Stdout = nil\n\tcmd.Stderr = nil\n\n\t// NOTE: in order to ensure we can return errors that occur in cleanup, we use a variable binding for the return\n\t// value so that it can be updated.\n\tpseudoTerminal, err := pty.Start(cmd)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tdefer func() {\n\t\tif closeErr := pseudoTerminal.Close(); closeErr != nil {\n\t\t\tcloseErr = errors.Errorf(\"Error closing pty: %w\", closeErr)\n\n\t\t\t// Only overwrite the previous error if there was no error since this error has lower priority than any\n\t\t\t// errors in the main routine\n\t\t\tif err == nil {\n\t\t\t\terr = closeErr\n\t\t\t} else {\n\t\t\t\tlogger.Error(closeErr)\n\t\t\t}\n\t\t}\n\t}()\n\n\t// Every time the current terminal size changes, we need to make sure the PTY also updates the size.\n\tch := make(chan os.Signal, 1)\n\tsignal.Notify(ch, syscall.SIGWINCH)\n\n\tgo func() {\n\t\tfor range ch {\n\t\t\tif inheritSizeErr := pty.InheritSize(os.Stdin, pseudoTerminal); inheritSizeErr != nil {\n\t\t\t\tinheritSizeErr = errors.Errorf(\"Error resizing pty: %w\", inheritSizeErr)\n\n\t\t\t\t// We don't propagate this error upstream because it does not affect normal operation of the command\n\t\t\t\tlogger.Error(inheritSizeErr)\n\t\t\t}\n\t\t}\n\t}()\n\n\tch <- syscall.SIGWINCH // Make sure the pty matches current size\n\n\t// Set stdin in raw mode so that we preserve readline properties\n\toldState, err := term.MakeRaw(int(os.Stdin.Fd()))\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tdefer func() {\n\t\tif restoreErr := term.Restore(int(os.Stdin.Fd()), oldState); restoreErr != nil {\n\t\t\trestoreErr = errors.Errorf(\"error restoring terminal state: %w\", restoreErr)\n\n\t\t\t// Only overwrite the previous error if there was no error since this error has lower priority than any\n\t\t\t// errors in the main routine\n\t\t\tif err == nil {\n\t\t\t\terr = restoreErr\n\t\t\t} else {\n\t\t\t\tlogger.Error(restoreErr)\n\t\t\t}\n\t\t}\n\t}()\n\n\tctx := context.Background()\n\n\tctx, cancel := context.WithCancel(ctx)\n\tdefer cancel()\n\n\terrGroup, ctx := errgroup.WithContext(ctx)\n\n\t// Copy stdout to the pty.\n\terrGroup.Go(func() error {\n\t\tdefer cancel()\n\n\t\tif _, err := util.Copy(ctx, cmdStdout, pseudoTerminal); err != nil {\n\t\t\treturn errors.Errorf(\"error forwarding stdout: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\t// Copy stdin to the pty.\n\terrGroup.Go(func() error {\n\t\tdefer cancel()\n\n\t\tif _, err := util.Copy(ctx, pseudoTerminal, os.Stdin); err != nil {\n\t\t\treturn errors.Errorf(\"error forwarding stdin: %w\", err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err := errGroup.Wait(); err != nil && !errors.IsError(err, io.EOF) && !errors.IsContextCanceled(err) {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n\n// PrepareConsole is run at the start of the application to set up the console.\n// On Unix, terminals handle ANSI escape sequences natively, so this is a no-op.\n// Returns true to indicate ANSI support is available.\nfunc PrepareConsole(_ log.Logger) bool {\n\treturn true\n}\n\n// ConsoleState is a no-op on Unix. On Windows it saves/restores console modes\n// that subprocesses may modify.\ntype ConsoleState struct{}\n\n// SaveConsoleState is a no-op on Unix.\nfunc SaveConsoleState() ConsoleState { return ConsoleState{} }\n\n// Restore is a no-op on Unix.\nfunc (ConsoleState) Restore() {}\n\n// PrepareStdinForPrompt is a no-op on Unix.\nfunc PrepareStdinForPrompt(_ log.Logger) {}\n"
  },
  {
    "path": "internal/os/exec/ptty_windows.go",
    "content": "//go:build windows\n\npackage exec\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"golang.org/x/sys/windows\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst InvalidHandleErrorMessage = \"The handle is invalid\"\n\n// PrepareConsole enables support for escape sequences on Windows.\n// Returns true if virtual terminal processing was successfully enabled on at least one output handle.\n// https://stackoverflow.com/questions/56460651/golang-fmt-print-033c-and-fmt-print-x1bc-are-not-clearing-screenansi-es\n// https://github.com/containerd/console/blob/f652dc3/console_windows.go#L46\nfunc PrepareConsole(logger log.Logger) bool {\n\tenableVirtualTerminalInput(logger, os.Stdin)\n\n\tstdoutOK := enableVirtualTerminalProcessing(logger, os.Stdout)\n\tstderrOK := enableVirtualTerminalProcessing(logger, os.Stderr)\n\n\tif stdoutOK || stderrOK {\n\t\treturn true\n\t}\n\n\t// If stdout/stderr are not console handles (e.g. pipes), try CONOUT$ directly.\n\t// CONOUT$ always refers to the active console output device. VT processing is a\n\t// screen buffer property that persists after the handle is closed, so enabling it\n\t// here affects all future console output even if stdout itself is a pipe.\n\t// Returning true is correct because stderr may still render to the console.\n\tconout, err := os.OpenFile(\"CONOUT$\", os.O_WRONLY, 0)\n\tif err != nil {\n\t\tlogger.Debugf(\"Could not open CONOUT$: %v\", err)\n\n\t\treturn false\n\t}\n\tdefer conout.Close()\n\n\treturn enableVirtualTerminalProcessing(logger, conout)\n}\n\n// enableVirtualTerminalInput sets ENABLE_VIRTUAL_TERMINAL_INPUT on an input handle (stdin).\n// This is separate from enableVirtualTerminalProcessing because input and output handles\n// use different flag values: ENABLE_VIRTUAL_TERMINAL_INPUT (0x200) for input vs\n// ENABLE_VIRTUAL_TERMINAL_PROCESSING (0x4) for output.\n// VT input is optional — failures are logged at Debug level (not Error) because\n// missing VT input support does not break colored output or core functionality.\nfunc enableVirtualTerminalInput(logger log.Logger, file *os.File) {\n\tvar mode uint32\n\n\thandle := windows.Handle(file.Fd())\n\tif err := windows.GetConsoleMode(handle, &mode); err != nil {\n\t\tlogger.Debugf(\"failed to get console mode for input: %v\", err)\n\t\treturn\n\t}\n\n\tif err := windows.SetConsoleMode(handle, mode|windows.ENABLE_VIRTUAL_TERMINAL_INPUT); err != nil {\n\t\tlogger.Debugf(\"virtual terminal input not supported: %v\", err)\n\t\t// Restore original mode in case the failed call left the handle in a bad state.\n\t\t_ = windows.SetConsoleMode(handle, mode)\n\t}\n}\n\n// PrepareStdinForPrompt ensures stdin has the console mode flags required for\n// interactive line input (line buffering, echo, processed input). Subprocesses\n// on Windows can clear these flags, making stdin unusable for prompts.\nfunc PrepareStdinForPrompt(logger log.Logger) {\n\tvar mode uint32\n\n\thandle := windows.Handle(os.Stdin.Fd())\n\tif err := windows.GetConsoleMode(handle, &mode); err != nil {\n\t\t// stdin is not a console handle (e.g. pipe) — nothing to restore.\n\t\treturn\n\t}\n\n\trequired := uint32(windows.ENABLE_LINE_INPUT | windows.ENABLE_ECHO_INPUT | windows.ENABLE_PROCESSED_INPUT)\n\tif mode&required != required {\n\t\tif err := windows.SetConsoleMode(handle, mode|required); err != nil {\n\t\t\tlogger.Debugf(\"failed to restore stdin console mode for prompt: %v\", err)\n\t\t}\n\t}\n}\n\n// enableVirtualTerminalProcessing sets ENABLE_VIRTUAL_TERMINAL_PROCESSING on an output handle\n// (stdout or stderr) so that ANSI escape sequences are interpreted by the console.\n// Returns true if the flag was successfully set.\nfunc enableVirtualTerminalProcessing(logger log.Logger, file *os.File) bool {\n\tvar mode uint32\n\n\thandle := windows.Handle(file.Fd())\n\tif err := windows.GetConsoleMode(handle, &mode); err != nil {\n\t\tif strings.Contains(err.Error(), InvalidHandleErrorMessage) {\n\t\t\tlogger.Debugf(\"failed to get console mode: %v\", err)\n\t\t} else {\n\t\t\tlogger.Errorf(\"failed to get console mode: %v\", err)\n\t\t}\n\n\t\treturn false\n\t}\n\n\tif err := windows.SetConsoleMode(handle, mode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err != nil {\n\t\tlogger.Errorf(\"failed to set console mode: %v\", err)\n\t\t_ = windows.SetConsoleMode(handle, mode)\n\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// ConsoleState stores the console mode for all standard handles so it can be restored\n// after subprocess execution. Subprocesses on Windows can modify the console mode,\n// which breaks ANSI escape handling and stdin line-input for the parent process.\n//\n// Note: SaveConsoleState and Restore operate on global OS handles (os.Stdin/Stdout/Stderr)\n// without synchronization. This is practically safe for concurrent use in run --all\n// because all goroutines target the same mode values. However, it is not formally\n// synchronized via mutex — a goroutine that saves state after a subprocess has modified\n// the console could capture a corrupted baseline. Impact is cosmetic only (garbled ANSI\n// output, not data corruption).\ntype ConsoleState struct {\n\tstdinMode, stdoutMode, stderrMode uint32\n\tstdinOK, stdoutOK, stderrOK       bool\n}\n\n// SaveConsoleState captures the current console mode for stdin, stdout, and stderr.\nfunc SaveConsoleState() ConsoleState {\n\tvar s ConsoleState\n\n\ts.stdinOK = windows.GetConsoleMode(windows.Handle(os.Stdin.Fd()), &s.stdinMode) == nil\n\ts.stdoutOK = windows.GetConsoleMode(windows.Handle(os.Stdout.Fd()), &s.stdoutMode) == nil\n\ts.stderrOK = windows.GetConsoleMode(windows.Handle(os.Stderr.Fd()), &s.stderrMode) == nil\n\n\treturn s\n}\n\n// Restore restores the saved console modes.\nfunc (s ConsoleState) Restore() {\n\tif s.stdinOK {\n\t\t_ = windows.SetConsoleMode(windows.Handle(os.Stdin.Fd()), s.stdinMode)\n\t}\n\n\tif s.stdoutOK {\n\t\t_ = windows.SetConsoleMode(windows.Handle(os.Stdout.Fd()), s.stdoutMode)\n\t}\n\n\tif s.stderrOK {\n\t\t_ = windows.SetConsoleMode(windows.Handle(os.Stderr.Fd()), s.stderrMode)\n\t}\n}\n\n// For windows, there is no concept of a pseudoTTY so we run as if there is no pseudoTTY.\nfunc runCommandWithPTY(logger log.Logger, cmd *exec.Cmd) error {\n\tlogger.Debug(\"Running command without PTY\")\n\n\tif err := cmd.Start(); err != nil {\n\t\treturn errors.New(err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/os/exec/testdata/infinite_loop.bat",
    "content": "@echo off\r\n\r\n:loop\r\nsleep 0.1\r\ngoto loop"
  },
  {
    "path": "internal/os/exec/testdata/test_exit_code.bat",
    "content": "@echo off\r\n\r\nexit %1"
  },
  {
    "path": "internal/os/exec/testdata/test_exit_code.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\nexit $1"
  },
  {
    "path": "internal/os/exec/testdata/test_graceful_shutdown.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\n# This script traps SIGINT and exits with code 42 when received.\n# It exits with code 1 if terminated by SIGKILL (or any other unexpected termination).\n# This is used to verify that the graceful shutdown sends SIGINT rather than SIGKILL.\n\ntrap 'exit 42' INT\n\nwhile true; do sleep 0.1; done\n"
  },
  {
    "path": "internal/os/exec/testdata/test_sigint_multiple.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\nINT_REQUIRED=$1\nINT_COUNTER=0\n\ntrap int_handler INT\n\nfunction int_handler() {\n    INT_COUNTER=$((INT_COUNTER + 1))\n}\n\nwhile [[ $INT_COUNTER -lt $INT_REQUIRED ]]\n    do sleep 0.1\ndone\n\nexit \"$INT_COUNTER\""
  },
  {
    "path": "internal/os/exec/testdata/test_sigint_wait.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\nWAIT_TIME=$1\n\ntrap int_handler INT\n\nfunction int_handler() {\n        sleep \"$WAIT_TIME\"\n        exit \"$WAIT_TIME\"\n}\n\nwhile true; do sleep 0.1; done"
  },
  {
    "path": "internal/os/signal/context_canceled.go",
    "content": "package signal\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os\"\n)\n\n// ContextCanceledError contains a signal to pass through when the context is cancelled.\ntype ContextCanceledError struct {\n\tSignal os.Signal\n}\n\n// SignalFromContext extracts the signal that caused the context cancellation, if any.\n// Returns nil if the context was not cancelled due to a signal.\nfunc SignalFromContext(ctx context.Context) os.Signal {\n\tcause := context.Cause(ctx)\n\tif cause == nil {\n\t\treturn nil\n\t}\n\n\tvar canceledErr *ContextCanceledError\n\tif errors.As(cause, &canceledErr) && canceledErr.Signal != nil {\n\t\treturn canceledErr.Signal\n\t}\n\n\treturn nil\n}\n\n// NewContextCanceledError returns a new `ContextCanceledError` instance.\nfunc NewContextCanceledError(sig os.Signal) *ContextCanceledError {\n\treturn &ContextCanceledError{Signal: sig}\n}\n\n// Error implements the `Error` method.\nfunc (ContextCanceledError) Error() string {\n\treturn context.Canceled.Error()\n}\n\n// Unwrap implements the `Unwrap` method.\nfunc (ContextCanceledError) Unwrap() error {\n\treturn context.Canceled\n}\n"
  },
  {
    "path": "internal/os/signal/signal.go",
    "content": "// Package signal provides convenience methods for intercepting and handling OS signals.\npackage signal\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"os/signal\"\n)\n\n// NotifyFunc is a callback function for Notifier.\ntype NotifyFunc func(sig os.Signal)\n\n// Notifier registers a handler for receiving signals from the OS.\n// When signal is receiving, it calls the given callback func `notifyFn`.\nfunc Notifier(notifyFn NotifyFunc, trackSignals ...os.Signal) {\n\tNotifierWithContext(context.Background(), notifyFn, trackSignals...)\n}\n\n// NotifierWithContext does the same as `Notifier`, but if the given `ctx` becomes `Done`, the notification is stopped.\nfunc NotifierWithContext(ctx context.Context, notifyFn NotifyFunc, trackSignals ...os.Signal) {\n\tsigCh := make(chan os.Signal, 1)\n\n\tsignal.Notify(sigCh, trackSignals...)\n\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tsignal.Stop(sigCh)\n\t\t\t\tclose(sigCh)\n\n\t\t\t\treturn\n\n\t\t\tcase sig, ok := <-sigCh:\n\t\t\t\tif !ok {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tnotifyFn(sig)\n\t\t\t}\n\t\t}\n\t}()\n}\n"
  },
  {
    "path": "internal/os/signal/signal_unix.go",
    "content": "//go:build !windows\n// +build !windows\n\npackage signal\n\nimport (\n\t\"os\"\n\t\"syscall\"\n)\n\n// InterruptSignal is an interrupt signal.\nvar InterruptSignal = syscall.SIGINT //nolint:gochecknoglobals\n\n// InterruptSignals contains a list of signals that are treated as interrupts.\nvar InterruptSignals = []os.Signal{syscall.SIGTERM, syscall.SIGINT} //nolint:gochecknoglobals\n"
  },
  {
    "path": "internal/os/signal/signal_windows.go",
    "content": "//go:build windows\n// +build windows\n\npackage signal\n\nimport (\n\t\"os\"\n)\n\n// InterruptSignal is an interrupt signal.\nvar InterruptSignal os.Signal = nil\n\n// InterruptSignals contains a list of signals that are treated as interrupts.\nvar InterruptSignals []os.Signal = []os.Signal{}\n"
  },
  {
    "path": "internal/os/stdout/stdout.go",
    "content": "// Package stdout provides utilities for working with stdout.\npackage stdout\n\nimport \"os\"\n\n// IsRedirected returns true if the stdout is redirected.\nfunc IsRedirected() bool {\n\tstat, err := os.Stdout.Stat()\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn (stat.Mode() & os.ModeCharDevice) == 0\n}\n"
  },
  {
    "path": "internal/prepare/prepare.go",
    "content": "// Package prepare provides functionality to prepare downloaded OpenTofu/Terraform source code\n// for use with Terragrunt. This includes reading and parsing Terragrunt configuration, fetching\n// credentials, downloading source code, generating configuration files, and initializing the\n// OpenTofu/Terraform working directory.\n//\n// The preparation process follows a sequence of stages:\n//  1. PrepareConfig - Reads configuration and fetches credentials\n//  2. PrepareSource - Downloads terraform source if specified\n//  3. PrepareGenerate - Generates configuration files (generate blocks and remote_state)\n//  4. PrepareInputsAsEnvVars - Sets inputs as environment variables\n//  5. PrepareInit - Runs terraform init if needed\npackage prepare\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iam\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds/providers/amazonsts\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds/providers/externalcmd\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runcfg\"\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// Config holds the result of preparing a terragrunt configuration.\ntype Config struct {\n\tCfg  *config.TerragruntConfig\n\tOpts *options.TerragruntOptions\n}\n\n// PrepareConfig reads and parses the terragrunt configuration, fetches credentials,\n// and performs version constraint checks. This is the first stage of preparation.\nfunc PrepareConfig(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) (*Config, error) {\n\t// We need to get the credentials from auth-provider-cmd at the very beginning,\n\t// since the locals block may contain `get_aws_account_id()` func.\n\tcredsGetter := creds.NewGetter()\n\tif err := credsGetter.ObtainAndUpdateEnvIfNecessary(ctx, l, opts.Env, externalcmd.NewProvider(l, opts.AuthProviderCmd, configbridge.ShellRunOptsFromOpts(opts))); err != nil {\n\t\treturn nil, err\n\t}\n\n\tctx, pctx := configbridge.NewParsingContext(ctx, l, opts)\n\n\tterragruntConfig, err := config.ReadTerragruntConfig(ctx, l, pctx, pctx.ParserOptions)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Config{\n\t\tCfg:  terragruntConfig,\n\t\tOpts: opts,\n\t}, nil\n}\n\n// PrepareSource downloads terraform source if specified in the configuration.\n// It requires PrepareConfig to have been called first.\nfunc PrepareSource(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tcfg *config.TerragruntConfig,\n\tr *report.Report,\n) (*options.TerragruntOptions, error) {\n\tengine, err := cfg.EngineOptions()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\topts.EngineConfig = engine\n\n\terrConfig, err := cfg.ErrorsConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Only overwrite when the config actually defines error rules;\n\t// otherwise preserve the built-in default retryable errors.\n\tif errConfig != nil {\n\t\topts.Errors = errConfig\n\t}\n\n\trunCfg := cfg.ToRunConfig(l)\n\n\tl, optsClone, err := opts.CloneWithConfigPath(l, opts.TerragruntConfigPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\toptsClone.TerraformCommand = run.CommandNameTerragruntReadConfig\n\n\tif err = optsClone.RunWithErrorHandling(ctx, l, r, func() error {\n\t\treturn run.ProcessHooks(ctx, l, runCfg.Terraform.AfterHooks, configbridge.NewRunOptions(optsClone), runCfg, nil, r)\n\t}); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// We merge the OriginalIAMRoleOptions into the one from the config, because the CLI passed IAMRoleOptions has\n\t// precedence.\n\topts.IAMRoleOptions = iam.MergeRoleOptions(\n\t\tcfg.GetIAMRoleOptions(),\n\t\topts.OriginalIAMRoleOptions,\n\t)\n\n\tcredsGetter := creds.NewGetter()\n\n\tif err = opts.RunWithErrorHandling(ctx, l, r, func() error {\n\t\treturn credsGetter.ObtainAndUpdateEnvIfNecessary(ctx, l, opts.Env, amazonsts.NewProvider(l, opts.IAMRoleOptions, opts.Env))\n\t}); err != nil {\n\t\treturn nil, err\n\t}\n\n\t_, defaultDownloadDir := util.DefaultWorkingAndDownloadDirs(opts.TerragruntConfigPath)\n\n\t// if the download dir hasn't been changed from default, and is set in the config,\n\t// then use it\n\tif opts.DownloadDir == defaultDownloadDir && runCfg.DownloadDir != \"\" {\n\t\topts.DownloadDir = runCfg.DownloadDir\n\t}\n\n\tsourceURL, err := runcfg.GetTerraformSourceURL(opts.Source, opts.SourceMap, opts.OriginalTerragruntConfigPath, runCfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trunOpts := configbridge.NewRunOptions(opts)\n\n\tvar updatedRunOpts *run.Options\n\n\t// Always download/copy source to cache directory for consistency.\n\t// When no source is specified, sourceURL will be \".\" (current directory).\n\terr = telemetry.TelemeterFromContext(ctx).Collect(ctx, \"download_terraform_source\", map[string]any{\n\t\t\"sourceUrl\": sourceURL,\n\t}, func(ctx context.Context) error {\n\t\tupdatedRunOpts, err = run.DownloadTerraformSource(ctx, l, sourceURL, runOpts, runCfg, r)\n\t\treturn err\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// DownloadTerraformSource returns *run.Options; sync the updated WorkingDir\n\t// back to a *options.TerragruntOptions clone for callers that expect that type.\n\t_, updatedTerragruntOptions, err := opts.CloneWithConfigPath(l, opts.TerragruntConfigPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tupdatedTerragruntOptions.WorkingDir = updatedRunOpts.WorkingDir\n\n\treturn updatedTerragruntOptions, nil\n}\n\n// PrepareGenerate handles code generation configs, both generate blocks and generate attribute of remote_state.\n// It requires PrepareSource to have been called first.\nfunc PrepareGenerate(l log.Logger, opts *options.TerragruntOptions, cfg *runcfg.RunConfig) error {\n\treturn run.GenerateConfig(l, configbridge.NewRunOptions(opts), cfg)\n}\n\n// PrepareInputsAsEnvVars sets terragrunt inputs as environment variables.\n// It requires PrepareGenerate to have been called first.\nfunc PrepareInputsAsEnvVars(l log.Logger, opts *options.TerragruntOptions, cfg *runcfg.RunConfig) error {\n\trunOpts := configbridge.NewRunOptions(opts)\n\n\t// Check for terraform code\n\tif err := run.CheckFolderContainsTerraformCode(runOpts); err != nil {\n\t\treturn err\n\t}\n\n\treturn run.SetTerragruntInputsAsEnvVars(l, runOpts, cfg)\n}\n\n// PrepareInit runs terraform init if needed. This is the final preparation stage.\n// It requires PrepareInputsAsEnvVars to have been called first.\nfunc PrepareInit(\n\tctx context.Context,\n\tl log.Logger,\n\toriginalOpts, opts *options.TerragruntOptions,\n\tcfg *runcfg.RunConfig,\n\tr *report.Report,\n) error {\n\trunOpts := configbridge.NewRunOptions(opts)\n\n\t// Check for terraform code\n\tif err := run.CheckFolderContainsTerraformCode(runOpts); err != nil {\n\t\treturn err\n\t}\n\n\tif err := run.SetTerragruntInputsAsEnvVars(l, runOpts, cfg); err != nil {\n\t\treturn err\n\t}\n\n\t// Run terraform init via the non-init command preparation path\n\treturn run.PrepareNonInitCommand(ctx, l, configbridge.NewRunOptions(originalOpts), runOpts, cfg, r)\n}\n"
  },
  {
    "path": "internal/providercache/options/options.go",
    "content": "// Package options groups provider-cache-specific configuration that is\n// resolved at startup and shared with the ProviderCache server and hook\n// functions.  It lives in its own package so that both pkg/options and\n// internal/providercache can import it without creating a cycle.\npackage options\n\n// DefaultRegistryNames is the default set of remote registries cached by the\n// Terragrunt Provider Cache server.\nvar DefaultRegistryNames = []string{\n\t\"registry.terraform.io\",\n\t\"registry.opentofu.org\",\n}\n\n// ProviderCacheOptions holds provider-cache-specific configuration that was\n// previously spread across several fields on TerragruntOptions.\ntype ProviderCacheOptions struct {\n\tDir           string\n\tHostname      string\n\tToken         string\n\tRegistryNames []string\n\tPort          int\n\tEnabled       bool\n}\n"
  },
  {
    "path": "internal/providercache/providercache.go",
    "content": "// Package providercache provides initialization of the Terragrunt provider caching server for caching OpenTofu providers.\npackage providercache\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"maps\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\tpcoptions \"github.com/gruntwork-io/terragrunt/internal/providercache/options\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/handlers\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/services\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cliconfig\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/getproviders\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tfimpl\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/internal/vfs\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst (\n\t// The paths to the automatically generated local CLI configs\n\tlocalCLIFilename = \".terraformrc\"\n\n\t// The status returned when making a request to the caching provider.\n\t// It is needed to prevent further loading of providers by terraform, and at the same time make sure that the request was processed successfully.\n\tCacheProviderHTTPStatusCode = http.StatusLocked\n\n\t// Authentication type on the Terragrunt Provider Cache server.\n\tAPIKeyAuth = \"x-api-key\"\n\n\t// Retry configuration for registry operations during cache warm-up\n\tregistryRetryMaxAttempts   = 3\n\tregistryRetrySleepInterval = 5 * time.Second\n)\n\nvar (\n\t// httpStatusCacheProviderReg is a regular expression to determine the success result of the command `terraform init`.\n\t// The reg matches if the text contains \"423 Locked\", for example:\n\t//\n\t// - registry.terraform.io/hashicorp/template: could not query provider registry for registry.terraform.io/hashicorp/template: 423 Locked.\n\t//\n\t// It also will match cases where terminal window is small enough so that terraform splits output in multiple lines, like following:\n\t//\n\t//    ╷\n\t//    │ Error: Failed to install provider\n\t//    │\n\t//    │ Error while installing snowflake-labs/snowflake v0.89.0: could not query\n\t//    │ provider registry for registry.terraform.io/snowflake-labs/snowflake: 423\n\t//    │ Locked\n\t//    ╵\n\thttpStatusCacheProviderReg = regexp.MustCompile(`(?smi)` + strconv.Itoa(CacheProviderHTTPStatusCode) + `.*` + http.StatusText(CacheProviderHTTPStatusCode))\n\n\t// registryTimeoutPatterns matches transient network errors that should trigger retries\n\tregistryTimeoutPatterns = []*regexp.Regexp{\n\t\tregexp.MustCompile(`(?s).*Client\\.Timeout exceeded while awaiting headers.*`),\n\t\tregexp.MustCompile(`(?s).*TLS handshake timeout.*`),\n\t\tregexp.MustCompile(`(?s).*context deadline exceeded.*`),\n\t\tregexp.MustCompile(`(?s).*connection reset by peer.*`),\n\t\tregexp.MustCompile(`(?s).*tcp.*timeout.*`),\n\t}\n)\n\ntype ProviderCache struct {\n\t*cache.Server\n\topts            *pcoptions.ProviderCacheOptions\n\tcliCfg          *cliconfig.Config\n\tproviderService *services.ProviderService\n\tfs              vfs.FS\n}\n\n// NewProviderCache creates a new ProviderCache with sensible defaults.\n// Use builder methods like WithFS() to customize the configuration.\nfunc NewProviderCache() *ProviderCache {\n\treturn &ProviderCache{\n\t\tfs: vfs.NewOSFS(),\n\t}\n}\n\n// WithFS sets the filesystem for file operations and returns the ProviderCache\n// for method chaining. If not called, defaults to the real OS filesystem.\nfunc (pc *ProviderCache) WithFS(fs vfs.FS) *ProviderCache {\n\tpc.fs = fs\n\treturn pc\n}\n\n// FS returns the configured filesystem.\nfunc (pc *ProviderCache) FS() vfs.FS {\n\treturn pc.fs\n}\n\n// Init initializes the ProviderCache with the given logger and options.\n// Call this after configuring the ProviderCache with builder methods.\nfunc (pc *ProviderCache) Init(l log.Logger, pcOpts *pcoptions.ProviderCacheOptions, rootWorkingDir string) error {\n\tpc.opts = pcOpts\n\n\t// ProviderCacheDir has the same file structure as terraform plugin_cache_dir.\n\t// https://developer.hashicorp.com/terraform/cli/config/config-file#provider-plugin-cache\n\tif pcOpts.Dir == \"\" {\n\t\tcacheDir, err := util.GetCacheDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get cache directory: %w\", err)\n\t\t}\n\n\t\tpcOpts.Dir = filepath.Join(cacheDir, \"providers\")\n\t}\n\n\tif !filepath.IsAbs(pcOpts.Dir) {\n\t\tpcOpts.Dir = filepath.Join(rootWorkingDir, pcOpts.Dir)\n\t}\n\n\tpcOpts.Dir = filepath.Clean(pcOpts.Dir)\n\n\tif pcOpts.Token == \"\" {\n\t\tpcOpts.Token = uuid.New().String()\n\t}\n\t// Currently, the cache server only supports the `x-api-key` token.\n\tif !strings.HasPrefix(strings.ToLower(pcOpts.Token), APIKeyAuth+\":\") {\n\t\tpcOpts.Token = fmt.Sprintf(\"%s:%s\", APIKeyAuth, pcOpts.Token)\n\t}\n\n\t// Pass filesystem to LoadUserConfig\n\tcliCfg, err := cliconfig.LoadUserConfig(cliconfig.WithFS(pc.FS()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tuserProviderDir, err := cliconfig.UserProviderDir()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tproviderService := services.NewProviderService(pcOpts.Dir, userProviderDir, cliCfg.CredentialsSource(), l, services.WithFS(pc.FS()))\n\tproxyProviderHandler := handlers.NewProxyProviderHandler(l, cliCfg.CredentialsSource())\n\n\tproviderHandlers, err := handlers.NewProviderHandlers(cliCfg, l, pcOpts.RegistryNames)\n\tif err != nil {\n\t\treturn errors.Errorf(\"creating provider handlers failed: %w\", err)\n\t}\n\n\tcacheServer := cache.NewServer(\n\t\tcache.WithHostname(pcOpts.Hostname),\n\t\tcache.WithPort(pcOpts.Port),\n\t\tcache.WithToken(pcOpts.Token),\n\t\tcache.WithProviderService(providerService),\n\t\tcache.WithProviderHandlers(providerHandlers...),\n\t\tcache.WithProxyProviderHandler(proxyProviderHandler),\n\t\tcache.WithCacheProviderHTTPStatusCode(CacheProviderHTTPStatusCode),\n\t\tcache.WithLogger(l),\n\t)\n\n\tpc.Server = cacheServer\n\tpc.cliCfg = cliCfg\n\tpc.providerService = providerService\n\n\treturn nil\n}\n\n// InitServer creates and initializes a new ProviderCache with the given logger and options.\n// This is a convenience function that combines NewProviderCache() and Init().\nfunc InitServer(l log.Logger, pcOpts *pcoptions.ProviderCacheOptions, rootWorkingDir string) (*ProviderCache, error) {\n\tpc := NewProviderCache()\n\tif err := pc.Init(l, pcOpts, rootWorkingDir); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn pc, nil\n}\n\n// TerraformCommandHook warms up the providers cache, creates `.terraform.lock.hcl` and runs the `tofu/terraform init`\n// command with using this cache. Used as a hook function that is called after running the target tofu/terraform command.\n// For example, if the target command is `tofu plan`, it will be intercepted before it is run in the `/shell` package,\n// then control will be passed to this function to init the working directory using cached providers.\nfunc (pc *ProviderCache) TerraformCommandHook(\n\tctx context.Context,\n\tl log.Logger,\n\ttfOpts *tf.TFOptions,\n\targs clihelper.Args,\n) (*util.CmdOutput, error) {\n\t// To prevent a loop\n\tctx = tf.ContextWithTerraformCommandHook(ctx, nil)\n\n\tcliConfigFilename := filepath.Join(tfOpts.ShellOptions.WorkingDir, localCLIFilename)\n\n\tvar skipRunTargetCommand bool\n\n\tlockfilePath := filepath.Join(tfOpts.ShellOptions.WorkingDir, tf.TerraformLockFile)\n\tlockfileExists := util.FileExists(lockfilePath)\n\n\t// Use Hook only for the `terraform init` command, which can be run explicitly by the user or Terragrunt's `auto-init` feature.\n\tswitch {\n\tcase args.CommandName() == tf.CommandNameInit:\n\t\t// Provider caching for `terraform init` command.\n\tcase args.CommandName() == tf.CommandNameProviders && args.SubCommandName() == tf.CommandNameLock:\n\t\t// Provider caching for `terraform providers lock` command.\n\t\t// If no lock file exists, Terragrunt generates it.\n\t\t//\n\t\t// If one already exists,\n\t\t// let `tofu/terraform providers lock` run against the filesystem mirror\n\t\t// so OpenTofu/Terraform manages the lock file itself.\n\t\tif !lockfileExists {\n\t\t\tskipRunTargetCommand = true\n\t\t}\n\tdefault:\n\t\t// skip cache creation for all other commands\n\t\treturn tf.RunCommandWithOutput(ctx, l, tfOpts, args...)\n\t}\n\n\tenv := pc.providerCacheEnvironment(tfOpts.ShellOptions.Env, tfOpts.TofuImplementation, cliConfigFilename)\n\n\tif output, err := pc.warmUpCache(ctx, l, tfOpts, cliConfigFilename, args, env, lockfileExists); err != nil {\n\t\treturn output, err\n\t}\n\n\tif skipRunTargetCommand {\n\t\treturn &util.CmdOutput{}, nil\n\t}\n\n\treturn pc.runTerraformWithCache(ctx, l, tfOpts, cliConfigFilename, args, env)\n}\n\nfunc (pc *ProviderCache) warmUpCache(\n\tctx context.Context,\n\tl log.Logger,\n\ttfOpts *tf.TFOptions,\n\tcliConfigFilename string,\n\targs clihelper.Args,\n\tenv map[string]string,\n\tlockfileExists bool,\n) (*util.CmdOutput, error) {\n\tvar (\n\t\tcacheRequestID = uuid.New().String()\n\t\tcommandsArgs   = convertToMultipleCommandsByPlatforms(args)\n\t)\n\n\t// Create terraform cli config file that enables provider caching and does not use provider cache dir\n\tif err := pc.createLocalCLIConfig(ctx, tfOpts.TofuImplementation, cliConfigFilename, cacheRequestID); err != nil {\n\t\treturn nil, err\n\t}\n\n\tl.Infof(\"Caching terraform providers for %s\", tfOpts.ShellOptions.WorkingDir)\n\t// Before each init, we warm up the global cache to ensure that all necessary providers are cached.\n\t// To do this we are using 'terraform providers lock' to force TF to request all the providers from our TG cache, and that's how we know what providers TF needs, and can load them into the cache.\n\t// It's low cost operation, because it does not cache the same provider twice, but only new previously non-existent providers.\n\n\tfor _, args := range commandsArgs {\n\t\tif output, err := pc.runTerraformCommand(ctx, l, tfOpts, args, env); err != nil {\n\t\t\treturn output, err\n\t\t}\n\t}\n\n\tcaches, err := pc.providerService.WaitForCacheReady(cacheRequestID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tproviderConstraints, err := getproviders.ParseProviderConstraints(tfOpts.TofuImplementation, filepath.Dir(tfOpts.TerragruntConfigPath))\n\tif err != nil {\n\t\tl.Debugf(\"Failed to parse provider constraints from %s: %v\", filepath.Dir(tfOpts.TerragruntConfigPath), err)\n\n\t\tproviderConstraints = make(getproviders.ProviderConstraints)\n\t}\n\n\tisUpgrade := tfOpts.TerraformCliArgs != nil && tfOpts.TerraformCliArgs.Contains(\"-upgrade\")\n\n\t// If a lock file already existed before this run, skip writing to it — let\n\t// OpenTofu/Terraform verify and manage the lock file during the actual init.\n\tif lockfileExists && !isUpgrade {\n\t\tl.Debugf(\"Skipping lock file update: %s already exists, letting OpenTofu/Terraform manage it\",\n\t\t\tfilepath.Join(tfOpts.ShellOptions.WorkingDir, tf.TerraformLockFile))\n\n\t\treturn nil, nil\n\t}\n\n\tfor _, provider := range caches {\n\t\tif providerCache, ok := provider.(*services.ProviderCache); ok {\n\t\t\tproviderAddr := provider.Address()\n\t\t\tif constraint, exists := providerConstraints[providerAddr]; exists {\n\t\t\t\tproviderCache.Provider.OriginalConstraints = constraint\n\t\t\t\tl.Debugf(\"Applied constraint %s to provider %s\", constraint, providerAddr)\n\t\t\t} else {\n\t\t\t\tl.Debugf(\"No constraint found for provider %s\", providerAddr)\n\t\t\t}\n\t\t}\n\t}\n\n\terr = getproviders.UpdateLockfile(ctx, tfOpts.ShellOptions.WorkingDir, caches)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// For upgrade scenarios where no providers were newly cached, we still need to update\n\t// the lock file if module constraints have changed. This only happens during upgrades.\n\tif len(caches) == 0 && len(providerConstraints) > 0 && isUpgrade {\n\t\tl.Debugf(\"No new providers cached, but constraints exist. Updating lock file constraints for upgrade scenario.\")\n\n\t\terr = getproviders.UpdateLockfileConstraints(ctx, tfOpts.ShellOptions.WorkingDir, providerConstraints)\n\t}\n\n\treturn nil, err\n}\n\nfunc (pc *ProviderCache) runTerraformWithCache(\n\tctx context.Context,\n\tl log.Logger,\n\ttfOpts *tf.TFOptions,\n\tcliConfigFilename string,\n\targs clihelper.Args,\n\tenv map[string]string,\n) (*util.CmdOutput, error) {\n\t// Create terraform cli config file that uses provider cache dir\n\tif err := pc.createLocalCLIConfig(ctx, tfOpts.TofuImplementation, cliConfigFilename, \"\"); err != nil {\n\t\treturn nil, err\n\t}\n\n\tshellOpts := *tfOpts.ShellOptions // shallow copy\n\tshellOpts.Env = env\n\n\tnewTFOpts := &tf.TFOptions{\n\t\tJSONLogFormat:                tfOpts.JSONLogFormat,\n\t\tOriginalTerragruntConfigPath: tfOpts.OriginalTerragruntConfigPath,\n\t\tTerragruntConfigPath:         tfOpts.TerragruntConfigPath,\n\t\tTofuImplementation:           tfOpts.TofuImplementation,\n\t\tTerraformCliArgs:             tfOpts.TerraformCliArgs,\n\t\tShellOptions:                 &shellOpts,\n\t}\n\n\treturn tf.RunCommandWithOutput(ctx, l, newTFOpts, args...)\n}\n\n// createLocalCLIConfig creates a local CLI config that merges the default/user configuration with our Provider Cache configuration.\n// We don't want to use Terraform's `plugin_cache_dir` feature because the cache is populated by our Terragrunt Provider cache server, and to make sure that no Terraform process ever overwrites the global cache, we clear this value.\n// In order to force Terraform to queries our cache server instead of the original one, we use the section below.\n// https://github.com/hashicorp/terraform/issues/28309 (officially undocumented)\n//\n//\thost \"registry.terraform.io\" {\n//\t\tservices = {\n//\t\t\t\"providers.v1\" = \"http://localhost:5758/v1/providers/registry.terraform.io/\",\n//\t\t}\n//\t}\n//\n// In order to force Terraform to create symlinks from the provider cache instead of downloading large binary files, we use the section below.\n// https://developer.hashicorp.com/terraform/cli/config/config-file#provider-installation\n//\n//\tprovider_installation {\n//\t\tfilesystem_mirror {\n//\t\t\tpath    = \"/path/to/the/provider/cache\"\n//\t\t\tinclude = [\"example.com/*/*\"]\n//\t\t}\n//\t\tdirect {\n//\t\t\texclude = [\"example.com/*/*\"]\n//\t\t}\n//\t}\n//\n// This func doesn't change the default CLI config file, only creates a new one at the given path `filename`. Ultimately, we can assign this path to `TF_CLI_CONFIG_FILE`.\n//\n// It creates two types of configuration depending on the `cacheRequestID` variable set.\n// 1. If `cacheRequestID` is set, `terraform init` does _not_ use the provider cache directory, the cache server creates a cache for requested providers and returns HTTP status 423. Since for each module we create the CLI config, using `cacheRequestID` we have the opportunity later retrieve from the cache server exactly those cached providers that were requested by `terraform init` using this configuration.\n// 2. If `cacheRequestID` is empty, 'terraform init` uses provider cache directory, the cache server acts as a proxy.\nfunc (pc *ProviderCache) createLocalCLIConfig(ctx context.Context, implementation tfimpl.Type, filename string, cacheRequestID string) error {\n\tcfg := pc.cliCfg.Clone()\n\tcfg.PluginCacheDir = \"\"\n\n\t// Filter registries based on OpenTofu or Terraform implementation to avoid contacting unnecessary registries\n\tfilteredRegistryNames := filterRegistriesByImplementation(\n\t\tpc.opts.RegistryNames,\n\t\timplementation,\n\t)\n\n\tvar providerInstallationIncludes = make([]string, 0, len(filteredRegistryNames))\n\n\tfor _, registryName := range filteredRegistryNames {\n\t\tproviderInstallationIncludes = append(providerInstallationIncludes, registryName+\"/*/*\")\n\n\t\tapiURLs, err := pc.DiscoveryURL(ctx, registryName)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcfg.AddHost(registryName, map[string]string{\n\t\t\t\"providers.v1\": fmt.Sprintf(\"%s/%s/%s/\", pc.ProviderController.URL(), cacheRequestID, registryName),\n\t\t\t// Since Terragrunt Provider Cache only caches providers, we need to route module requests to the original registry.\n\t\t\t\"modules.v1\": ResolveModulesURL(registryName, apiURLs.ModulesV1),\n\t\t})\n\t}\n\n\tif cacheRequestID == \"\" {\n\t\tcfg.AddProviderInstallationMethods(\n\t\t\tcliconfig.NewProviderInstallationFilesystemMirror(pc.opts.Dir, providerInstallationIncludes, nil),\n\t\t)\n\t} else {\n\t\tcfg.ProviderInstallation = nil\n\t}\n\n\tcfg.AddProviderInstallationMethods(\n\t\tcliconfig.NewProviderInstallationDirect(nil, nil),\n\t)\n\n\t// Use VFS for directory operations\n\tfs := pc.FS()\n\tcfgDir := filepath.Dir(filename)\n\n\tcfgDirExists, err := vfs.FileExists(fs, cfgDir)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tif !cfgDirExists {\n\t\tif err := fs.MkdirAll(cfgDir, os.ModePerm); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\t}\n\n\treturn cfg.Save(filename)\n}\n\n// isRegistryTimeoutError checks if the error output matches known transient registry timeout patterns\nfunc isRegistryTimeoutError(output []byte) bool {\n\treturn slices.ContainsFunc(registryTimeoutPatterns, func(pattern *regexp.Regexp) bool {\n\t\treturn pattern.Match(output)\n\t})\n}\n\nfunc (pc *ProviderCache) runTerraformCommand(ctx context.Context, l log.Logger, tfOpts *tf.TFOptions, args []string, envs map[string]string) (*util.CmdOutput, error) {\n\t// add -no-color flag to args if it was set in Terragrunt arguments\n\tif tfOpts.TerraformCliArgs != nil && tfOpts.TerraformCliArgs.Contains(tf.FlagNameNoColor) &&\n\t\t!slices.Contains(args, tf.FlagNameNoColor) {\n\t\targs = append(args, tf.FlagNameNoColor)\n\t}\n\n\tshellOpts := *tfOpts.ShellOptions // shallow copy\n\tshellOpts.Writers.Writer = io.Discard\n\tshellOpts.Env = envs\n\n\tnewCliArgs := iacargs.New(args...)\n\n\tnewTFOpts := &tf.TFOptions{\n\t\tJSONLogFormat:                tfOpts.JSONLogFormat,\n\t\tOriginalTerragruntConfigPath: tfOpts.OriginalTerragruntConfigPath,\n\t\tTerragruntConfigPath:         tfOpts.TerragruntConfigPath,\n\t\tTofuImplementation:           tfOpts.TofuImplementation,\n\t\tTerraformCliArgs:             newCliArgs,\n\t\tShellOptions:                 &shellOpts,\n\t}\n\n\tvar finalOutput *util.CmdOutput\n\n\terr := util.DoWithRetry(\n\t\tctx,\n\t\t\"Running terraform providers lock\",\n\t\tregistryRetryMaxAttempts,\n\t\tregistryRetrySleepInterval,\n\t\tl,\n\t\tlog.DebugLevel,\n\t\tfunc(ctx context.Context) error {\n\t\t\terrWriter := util.NewTrapWriter(tfOpts.ShellOptions.Writers.ErrWriter)\n\t\t\tshellOpts.Writers.ErrWriter = errWriter\n\n\t\t\toutput, cmdErr := tf.RunCommandWithOutput(ctx, l, newTFOpts, newCliArgs.Slice()...)\n\t\t\tfinalOutput = output\n\n\t\t\t// If the OpenTofu/Terraform error matches `httpStatusCacheProviderReg` (423 Locked),\n\t\t\t// it means success - the cache recorded the request\n\t\t\tif cmdErr != nil && httpStatusCacheProviderReg.Match(output.Stderr.Bytes()) {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif cmdErr != nil {\n\t\t\t\tif isRegistryTimeoutError(output.Stderr.Bytes()) {\n\t\t\t\t\treturn cmdErr\n\t\t\t\t}\n\n\t\t\t\terr := errWriter.Flush()\n\t\t\t\tif err != nil {\n\t\t\t\t\tl.Warnf(\"Failed to flush stderr: %v\", err)\n\t\t\t\t}\n\n\t\t\t\treturn util.FatalError{Underlying: cmdErr}\n\t\t\t}\n\n\t\t\tif flushErr := errWriter.Flush(); flushErr != nil {\n\t\t\t\treturn util.FatalError{Underlying: flushErr}\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t)\n\tif err != nil {\n\t\t// Unwrap FatalError to return the original error\n\t\tvar fatalErr util.FatalError\n\t\tif errors.As(err, &fatalErr) {\n\t\t\treturn finalOutput, fatalErr.Underlying\n\t\t}\n\n\t\treturn finalOutput, err\n\t}\n\n\treturn finalOutput, nil\n}\n\n// providerCacheEnvironment returns TF_* name/value ENVs, which we use to force terraform processes to make requests through our cache server (proxy) instead of making direct requests to the origin servers.\nfunc (pc *ProviderCache) providerCacheEnvironment(env map[string]string, implementation tfimpl.Type, cliConfigFile string) map[string]string {\n\t// make copy + ensure non-nil\n\tenvs := make(map[string]string, len(env))\n\tmaps.Copy(envs, env)\n\n\t// Filter registries based on OpenTofu or Terraform implementation to avoid setting env vars for unnecessary registries\n\tfilteredRegistryNames := filterRegistriesByImplementation(\n\t\tpc.opts.RegistryNames,\n\t\timplementation,\n\t)\n\n\tfor _, registryName := range filteredRegistryNames {\n\t\tenvName := fmt.Sprintf(tf.EnvNameTFTokenFmt, strings.ReplaceAll(registryName, \".\", \"_\"))\n\n\t\t// delete existing key case insensitive\n\t\tfor key := range envs {\n\t\t\tif strings.EqualFold(key, envName) {\n\t\t\t\tdelete(envs, key)\n\t\t\t}\n\t\t}\n\n\t\t// We use `TF_TOKEN_*` for authentication with our private registry (cache server).\n\t\t// https://developer.hashicorp.com/terraform/cli/config/config-file#environment-variable-credentials\n\t\tenvs[envName] = pc.opts.Token\n\t}\n\n\t// By using `TF_CLI_CONFIG_FILE` we force terraform to use our auto-generated cli configuration file.\n\t// https://developer.hashicorp.com/terraform/cli/config/environment-variables#tf_cli_config_file\n\tenvs[tf.EnvNameTFCLIConfigFile] = cliConfigFile\n\t// Clear this `TF_PLUGIN_CACHE_DIR` value since we are using our own caching mechanism.\n\t// https://developer.hashicorp.com/terraform/cli/config/environment-variables#tf_plugin_cache_dir\n\tenvs[tf.EnvNameTFPluginCacheDir] = \"\"\n\n\treturn envs\n}\n\n// convertToMultipleCommandsByPlatforms converts `providers lock -platform=.. -platform=..` command into multiple commands that include only one platform.\n// for example:\n// `providers lock -platform=linux_amd64 -platform=darwin_arm64 -platform=freebsd_amd64`\n// to\n// `providers lock -platform=linux_amd64`,\n// `providers lock -platform=darwin_arm64`,\n// `providers lock -platform=freebsd_amd64`\nfunc convertToMultipleCommandsByPlatforms(args []string) [][]string {\n\tvar (\n\t\tfilteredArgs = make([]string, 0, len(args))\n\t\tplatformArgs = make([]string, 0, len(args))\n\t)\n\n\tfor _, arg := range args {\n\t\tif strings.HasPrefix(arg, tf.FlagNamePlatform) {\n\t\t\tplatformArgs = append(platformArgs, arg)\n\t\t} else {\n\t\t\tfilteredArgs = append(filteredArgs, arg)\n\t\t}\n\t}\n\n\tif len(platformArgs) == 0 {\n\t\treturn [][]string{args}\n\t}\n\n\tvar commandsArgs = make([][]string, 0, len(platformArgs))\n\n\tfor _, platformArg := range platformArgs {\n\t\tvar commandArgs = make([]string, len(filteredArgs), len(filteredArgs)+1)\n\n\t\tcopy(commandArgs, filteredArgs)\n\t\tcommandsArgs = append(commandsArgs, append(commandArgs, platformArg))\n\t}\n\n\treturn commandsArgs\n}\n\n// filterRegistriesByImplementation filters registry names based on the Terraform implementation being used.\n// If the registry names match the default registries (both registry.terraform.io and registry.opentofu.org),\n// it filters them based on the implementation:\n//   - OpenTofuImpl: returns only registry.opentofu.org\n//   - TerraformImpl: returns only registry.terraform.io\n//   - UnknownImpl: returns both (backward compatibility)\n//\n// If the user has explicitly set registry names (don't match defaults), returns them as-is.\nfunc filterRegistriesByImplementation(registryNames []string, implementation tfimpl.Type) []string {\n\t// Default registries in the same order as defined in options/options.go\n\tdefaultRegistries := []string{\n\t\t\"registry.terraform.io\",\n\t\t\"registry.opentofu.org\",\n\t}\n\n\t// Check if registry names match defaults exactly (order-independent)\n\tif len(registryNames) == len(defaultRegistries) {\n\t\tmatchesDefault := true\n\n\t\tfor _, defaultReg := range defaultRegistries {\n\t\t\tif !slices.Contains(registryNames, defaultReg) {\n\t\t\t\tmatchesDefault = false\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t// If matches defaults, filter based on implementation\n\t\tif matchesDefault {\n\t\t\tswitch implementation {\n\t\t\tcase tfimpl.OpenTofu:\n\t\t\t\treturn []string{\"registry.opentofu.org\"}\n\t\t\tcase tfimpl.Terraform:\n\t\t\t\treturn []string{\"registry.terraform.io\"}\n\t\t\tcase tfimpl.Unknown:\n\t\t\t\t// Backward compatibility: use both registries if implementation is unknown\n\t\t\t\treturn registryNames\n\t\t\tdefault:\n\t\t\t\t// Unknown implementation type, return as-is\n\t\t\t\treturn registryNames\n\t\t\t}\n\t\t}\n\t}\n\n\t// User explicitly set registry names, return as-is\n\treturn registryNames\n}\n\n// ResolveModulesURL resolves the modules.v1 URL from registry discovery.\n// If the URL is already absolute (contains \"://\"), it is returned as-is.\n// Otherwise, it is treated as a relative path and combined with the registry name.\nfunc ResolveModulesURL(registryName, modulesV1 string) string {\n\tif strings.Contains(modulesV1, \"://\") {\n\t\treturn modulesV1\n\t}\n\n\treturn fmt.Sprintf(\"https://%s%s\", registryName, modulesV1)\n}\n"
  },
  {
    "path": "internal/providercache/providercache_test.go",
    "content": "package providercache_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/gruntwork-io/terragrunt/internal/providercache\"\n\tpcoptions \"github.com/gruntwork-io/terragrunt/internal/providercache/options\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/handlers\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/services\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cliconfig\"\n\t\"github.com/gruntwork-io/terragrunt/internal/vfs\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nfunc createFakeProvider(t *testing.T, cacheDir, relativePath string) string {\n\tt.Helper()\n\n\terr := os.MkdirAll(filepath.Join(cacheDir, filepath.Dir(relativePath)), os.ModePerm)\n\trequire.NoError(t, err)\n\n\tfile, err := os.Create(filepath.Join(cacheDir, relativePath))\n\trequire.NoError(t, err)\n\n\tdefer file.Close()\n\n\terr = file.Sync()\n\trequire.NoError(t, err)\n\n\treturn relativePath\n}\n\nfunc TestProviderCache(t *testing.T) {\n\tt.Parallel()\n\n\ttoken := fmt.Sprintf(\"%s:%s\", providercache.APIKeyAuth, uuid.New().String())\n\n\tproviderCacheDir := helpers.TmpDirWOSymlinks(t)\n\tpluginCacheDir := helpers.TmpDirWOSymlinks(t)\n\n\topts := []cache.Option{cache.WithToken(token), cache.WithCacheProviderHTTPStatusCode(providercache.CacheProviderHTTPStatusCode)}\n\n\ttestCases := []struct {\n\t\texpectedBodyReg    *regexp.Regexp\n\t\tfullURLPath        string\n\t\trelURLPath         string\n\t\texpectedCachePath  string\n\t\topts               []cache.Option\n\t\texpectedStatusCode int\n\t}{\n\t\t{\n\t\t\topts:               opts,\n\t\t\tfullURLPath:        \"/.well-known/terraform.json\",\n\t\t\texpectedStatusCode: http.StatusOK,\n\t\t\texpectedBodyReg:    regexp.MustCompile(regexp.QuoteMeta(`{\"providers.v1\":\"/v1/providers\"}`)),\n\t\t},\n\t\t{\n\t\t\topts:               append(opts, cache.WithToken(\"\")),\n\t\t\trelURLPath:         \"/cache/registry.terraform.io/hashicorp/aws/versions\",\n\t\t\texpectedStatusCode: http.StatusUnauthorized,\n\t\t},\n\t\t{\n\t\t\topts:               opts,\n\t\t\trelURLPath:         \"/cache/registry.terraform.io/hashicorp/aws/versions\",\n\t\t\texpectedStatusCode: http.StatusOK,\n\t\t\texpectedBodyReg:    regexp.MustCompile(regexp.QuoteMeta(`\"version\":\"5.36.0\",\"protocols\":[\"5.0\"],\"platforms\"`)),\n\t\t},\n\t\t{\n\t\t\topts:               opts,\n\t\t\trelURLPath:         \"/cache/registry.terraform.io/hashicorp/aws/5.36.0/download/darwin/arm64\",\n\t\t\texpectedStatusCode: http.StatusLocked,\n\t\t\texpectedCachePath:  \"registry.terraform.io/hashicorp/aws/5.36.0/darwin_arm64/terraform-provider-aws_v5.36.0_x5\",\n\t\t},\n\t\t{\n\t\t\topts:               opts,\n\t\t\trelURLPath:         \"/cache/registry.terraform.io/hashicorp/template/2.2.0/download/linux/amd64\",\n\t\t\texpectedStatusCode: http.StatusLocked,\n\t\t\texpectedCachePath:  \"registry.terraform.io/hashicorp/template/2.2.0/linux_amd64/terraform-provider-template_v2.2.0_x4\",\n\t\t},\n\t\t{\n\t\t\topts:               opts,\n\t\t\trelURLPath:         fmt.Sprintf(\"/cache/registry.terraform.io/hashicorp/template/1234.5678.9/download/%s/%s\", runtime.GOOS, runtime.GOARCH),\n\t\t\texpectedStatusCode: http.StatusLocked,\n\t\t\texpectedCachePath:  createFakeProvider(t, pluginCacheDir, fmt.Sprintf(\"registry.terraform.io/hashicorp/template/1234.5678.9/%s_%s/terraform-provider-template_1234.5678.9_x5\", runtime.GOOS, runtime.GOARCH)),\n\t\t},\n\t\t{\n\t\t\topts:               opts,\n\t\t\trelURLPath:         \"//registry.terraform.io/hashicorp/aws/5.36.0/download/darwin/arm64\",\n\t\t\texpectedStatusCode: http.StatusOK,\n\t\t\texpectedBodyReg:    regexp.MustCompile(`\\{.*` + regexp.QuoteMeta(`\"download_url\":\"http://127.0.0.1:`) + `\\d+` + regexp.QuoteMeta(`/downloads/releases.hashicorp.com/terraform-provider-aws/5.36.0/terraform-provider-aws_5.36.0_darwin_arm64.zip\"`) + `.*\\}`),\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t// TODO: Remove this once we can invest time in figuring out why this test is so flaky.\n\t\t\t//\n\t\t\t// It's a pain, but it's not worth the time to fix it.\n\t\t\tmaxRetries := 3\n\n\t\t\tvar lastErr error\n\n\t\t\tfor attempt := 1; attempt <= maxRetries; attempt++ {\n\t\t\t\tif attempt > 1 {\n\t\t\t\t\tt.Logf(\"Retry attempt %d/%d for test case %d\", attempt, maxRetries, i)\n\t\t\t\t}\n\n\t\t\t\t// Create a new context for each test case to avoid interference\n\t\t\t\t//\n\t\t\t\t//nolint:usetesting\n\t\t\t\tctx := context.Background()\n\n\t\t\t\tctx, cancel := context.WithCancel(ctx)\n\t\t\t\tdefer cancel()\n\n\t\t\t\terrGroup, ctx := errgroup.WithContext(ctx)\n\t\t\t\tlogger := logger.CreateLogger()\n\n\t\t\t\tproviderService := services.NewProviderService(providerCacheDir, pluginCacheDir, nil, logger)\n\t\t\t\tproviderHandler := handlers.NewDirectProviderHandler(logger, new(cliconfig.ProviderInstallationDirect), nil)\n\t\t\t\tproxyProviderHandler := handlers.NewProxyProviderHandler(logger, nil)\n\n\t\t\t\ttc.opts = append(tc.opts,\n\t\t\t\t\tcache.WithProviderService(providerService),\n\t\t\t\t\tcache.WithProviderHandlers(providerHandler),\n\t\t\t\t\tcache.WithProxyProviderHandler(proxyProviderHandler),\n\t\t\t\t)\n\n\t\t\t\tserver := cache.NewServer(tc.opts...)\n\n\t\t\t\tln, err := server.Listen(t.Context())\n\t\t\t\tif err != nil {\n\t\t\t\t\tlastErr = err\n\n\t\t\t\t\tif attempt < maxRetries {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\t\t\t\tdefer ln.Close()\n\n\t\t\t\terrGroup.Go(func() error {\n\t\t\t\t\treturn server.Run(ctx, ln)\n\t\t\t\t})\n\n\t\t\t\turlPath := server.ProviderController.URL()\n\t\t\t\turlPath.Path += tc.relURLPath\n\n\t\t\t\tif tc.fullURLPath != \"\" {\n\t\t\t\t\turlPath.Path = tc.fullURLPath\n\t\t\t\t}\n\n\t\t\t\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, urlPath.String(), nil)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlastErr = err\n\n\t\t\t\t\tif attempt < maxRetries {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\n\t\t\t\treq.Header.Set(\"Authorization\", \"Bearer \"+token)\n\n\t\t\t\tresp, err := http.DefaultClient.Do(req)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlastErr = err\n\n\t\t\t\t\tif attempt < maxRetries {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\t\t\t\tdefer resp.Body.Close()\n\n\t\t\t\tif resp.StatusCode != tc.expectedStatusCode {\n\t\t\t\t\tlastErr = fmt.Errorf(\"expected status code %d, got %d\", tc.expectedStatusCode, resp.StatusCode)\n\n\t\t\t\t\tif attempt < maxRetries {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tassert.Equal(t, tc.expectedStatusCode, resp.StatusCode)\n\t\t\t\t}\n\n\t\t\t\tif tc.expectedBodyReg != nil {\n\t\t\t\t\tbody, err := io.ReadAll(resp.Body)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlastErr = err\n\n\t\t\t\t\t\tif attempt < maxRetries {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t}\n\n\t\t\t\t\tif !tc.expectedBodyReg.MatchString(string(body)) {\n\t\t\t\t\t\tlastErr = fmt.Errorf(\"body did not match expected regex: %s\", tc.expectedBodyReg.String())\n\n\t\t\t\t\t\tif attempt < maxRetries {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tassert.Regexp(t, tc.expectedBodyReg, string(body))\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Skip WaitForCacheReady for unauthorized test cases since they don't trigger background operations,\n\t\t\t\t// and we cancel context at the end of the test.\n\t\t\t\tif tc.expectedStatusCode != http.StatusUnauthorized {\n\t\t\t\t\t_, err = providerService.WaitForCacheReady(\"\")\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlastErr = err\n\n\t\t\t\t\t\tif attempt < maxRetries {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif tc.expectedCachePath != \"\" {\n\t\t\t\t\tif !assert.FileExists(t, filepath.Join(providerCacheDir, tc.expectedCachePath)) {\n\t\t\t\t\t\tlastErr = fmt.Errorf(\"expected cache file does not exist: %s\", tc.expectedCachePath)\n\n\t\t\t\t\t\tif attempt < maxRetries {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcancel()\n\n\t\t\t\terr = errGroup.Wait()\n\t\t\t\tif err != nil {\n\t\t\t\t\tlastErr = err\n\n\t\t\t\t\tif attempt < maxRetries {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tt.Fatalf(\"Test case %d failed after %d attempts. Last error: %v\", i, maxRetries, lastErr)\n\t\t})\n\t}\n}\n\nfunc TestProviderCacheHomeless(t *testing.T) {\n\tcacheDir := helpers.TmpDirWOSymlinks(t)\n\n\tt.Setenv(\"HOME\", \"\")\n\trequire.NoError(t, os.Unsetenv(\"HOME\"))\n\n\tt.Setenv(\"XDG_CACHE_HOME\", \"\")\n\trequire.NoError(t, os.Unsetenv(\"XDG_CACHE_HOME\"))\n\n\t_, err := providercache.InitServer(logger.CreateLogger(), &pcoptions.ProviderCacheOptions{\n\t\tDir: cacheDir,\n\t}, \"\")\n\trequire.NoError(t, err, \"ProviderCache shouldn't read HOME environment variable\")\n}\n\nfunc TestProviderCacheWithProviderCacheDir(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"NoNewDirectoriesAtHOME\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Use in-memory filesystem to isolate file operations from the real filesystem.\n\t\t// This ensures InitServer doesn't create any directories on the real filesystem\n\t\t// since all file operations are routed through the VFS.\n\t\tmemFs := vfs.NewMemMapFS()\n\t\tcacheDir := \"/test/provider-cache\"\n\n\t\tserver := providercache.NewProviderCache().WithFS(memFs)\n\t\terr := server.Init(\n\t\t\tlogger.CreateLogger(),\n\t\t\t&pcoptions.ProviderCacheOptions{\n\t\t\t\tDir: cacheDir,\n\t\t\t},\n\t\t\t\"\",\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\t// With VFS, all file operations go through the in-memory filesystem,\n\t\t// so no directories should be created on the real filesystem at all.\n\t\t// We can verify the VFS is being used by checking it's not empty or\n\t\t// by the fact that no errors occurred despite using fake paths.\n\t})\n\n\tt.Run(\"InitServerWithVFS\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tmemFs := vfs.NewMemMapFS()\n\t\tcacheDir := \"/vfs/provider-cache\"\n\n\t\tserver := providercache.NewProviderCache().WithFS(memFs)\n\t\terr := server.Init(\n\t\t\tlogger.CreateLogger(),\n\t\t\t&pcoptions.ProviderCacheOptions{\n\t\t\t\tDir: cacheDir,\n\t\t\t},\n\t\t\t\"\",\n\t\t)\n\t\trequire.NoError(t, err)\n\t\trequire.NotNil(t, server, \"Init should return a valid server when using VFS\")\n\t})\n}\n"
  },
  {
    "path": "internal/providercache/resolve_modules_url_test.go",
    "content": "package providercache_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/providercache\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestResolveModulesURL(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname         string\n\t\tregistryName string\n\t\tmodulesV1    string\n\t\texpected     string\n\t}{\n\t\t{\n\t\t\tname:         \"relative path\",\n\t\t\tregistryName: \"registry.terraform.io\",\n\t\t\tmodulesV1:    \"/v1/modules\",\n\t\t\texpected:     \"https://registry.terraform.io/v1/modules\",\n\t\t},\n\t\t{\n\t\t\tname:         \"relative path with trailing slash\",\n\t\t\tregistryName: \"private.registry.com\",\n\t\t\tmodulesV1:    \"/custom/modules/\",\n\t\t\texpected:     \"https://private.registry.com/custom/modules/\",\n\t\t},\n\t\t{\n\t\t\tname:         \"absolute URL same host\",\n\t\t\tregistryName: \"packages.syncron.team\",\n\t\t\tmodulesV1:    \"https://packages.syncron.team/somepath/modules/\",\n\t\t\texpected:     \"https://packages.syncron.team/somepath/modules/\",\n\t\t},\n\t\t{\n\t\t\tname:         \"absolute URL different host\",\n\t\t\tregistryName: \"registry.example.com\",\n\t\t\tmodulesV1:    \"https://other.host.com/modules/v1/\",\n\t\t\texpected:     \"https://other.host.com/modules/v1/\",\n\t\t},\n\t\t{\n\t\t\tname:         \"absolute URL with http scheme\",\n\t\t\tregistryName: \"registry.example.com\",\n\t\t\tmodulesV1:    \"http://internal.host.com/modules/\",\n\t\t\texpected:     \"http://internal.host.com/modules/\",\n\t\t},\n\t\t{\n\t\t\tname:         \"empty path\",\n\t\t\tregistryName: \"registry.terraform.io\",\n\t\t\tmodulesV1:    \"\",\n\t\t\texpected:     \"https://registry.terraform.io\",\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.Parallel()\n\n\t\t\tresult := providercache.ResolveModulesURL(tc.registryName, tc.modulesV1)\n\t\t\tassert.Equal(t, tc.expected, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/queue/queue.go",
    "content": "// Package queue provides a run queue implementation.\n// The queue is a double-ended queue (deque) that allows for efficient adding and removing of elements from both ends.\n// The queue is used to manage the order of Terragrunt runs.\n//\n// The algorithm for populating the queue is as follows:\n//  1. Given a list of discovered configurations, start with an empty queue.\n//  2. Sort configurations alphabetically to ensure deterministic ordering of independent items.\n//  3. For each discovered configuration:\n//     a. If the configuration has no dependencies, append it to the queue.\n//     b. Otherwise, find the position after its last dependency.\n//     c. Among items that depend on the same dependency, maintain alphabetical order.\n//\n// The resulting queue will have:\n// - Configurations with no dependencies at the front\n// - Configurations with dependents are ordered after their dependencies\n// - Alphabetical ordering only between items that share the same dependencies\n//\n// During operations like applies, entries will be dequeued from the front of the queue and run.\n// During operations like destroys, entries will be dequeued from the back of the queue and run.\n// This ensures that dependencies are satisfied in both cases:\n// - For applies: Dependencies (front) are run before their dependents (back)\n// - For destroys: Dependents (back) are run before their dependencies (front)\npackage queue\n\nimport (\n\t\"errors\"\n\t\"slices\"\n\t\"sort\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// Entry represents a node in the execution queue/DAG. Each Entry corresponds to a single Terragrunt configuration\n// and tracks its execution status and relationships to other entries in the queue.\ntype Entry struct {\n\t// Component is the Terragrunt configuration associated with this entry. It contains all metadata about the unit/stack,\n\t// including its path, dependencies, and discovery context (such as the command being run).\n\tComponent component.Component\n\n\t// Status represents the current lifecycle state of this entry in the queue. It tracks whether the entry is pending,\n\t// blocked, ready, running, succeeded, or failed. Status is updated as dependencies are resolved and as execution progresses.\n\tStatus Status\n}\n\n// Status represents the lifecycle state of a task in the queue.\ntype Status byte\n\nconst (\n\tStatusPending Status = iota\n\tStatusBlocked\n\tStatusUnsorted\n\tStatusReady\n\tStatusRunning\n\tStatusSucceeded\n\tStatusFailed\n\tStatusEarlyExit // Terminal status set on Entries in case of fail fast mode\n)\n\n// UpdateBlocked updates the status of the entry to blocked, if it is blocked.\n// An entry is blocked if:\n//  1. It is an \"up\" command (none of destroy, apply -destroy or plan -destroy)\n//     and it has dependencies that are not ready.\n//  2. It is a \"down\" command (destroy, apply -destroy or plan -destroy)\n//     and it has dependents that are not ready.\n//\n// If the entry isn't blocked, then it is marked as unsorted, and is ready to be sorted.\nfunc (e *Entry) UpdateBlocked(entries Entries) {\n\t// If the entry is already ready, we can skip the rest of the logic.\n\tif e.Status == StatusReady {\n\t\treturn\n\t}\n\n\tif e.IsUp() {\n\t\tfor _, dep := range e.Component.Dependencies() {\n\t\t\tdepEntry := entries.Entry(dep)\n\t\t\tif depEntry == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif !depEntry.IsUp() {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif depEntry.Status != StatusReady {\n\t\t\t\te.Status = StatusBlocked\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\te.Status = StatusUnsorted\n\n\t\treturn\n\t}\n\n\t// If the entry is a \"down\" command, we need to check if all of its dependents are ready.\n\tfor _, qEntry := range entries {\n\t\tif len(qEntry.Component.Dependencies()) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !slices.Contains(qEntry.Component.Dependencies(), e.Component) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif qEntry.IsUp() {\n\t\t\tcontinue\n\t\t}\n\n\t\tif qEntry.Status != StatusReady {\n\t\t\te.Status = StatusBlocked\n\t\t\treturn\n\t\t}\n\t}\n\n\te.Status = StatusUnsorted\n}\n\n// IsUp returns true if the entry is an \"up\" command.\nfunc (e *Entry) IsUp() bool {\n\t// If we don't have a discovery context,\n\t// we should assume the command is an \"up\" command.\n\tif e.Component.DiscoveryContext() == nil {\n\t\treturn true\n\t}\n\n\tif e.Component.DiscoveryContext().Cmd == \"destroy\" {\n\t\treturn false\n\t}\n\n\tif e.Component.DiscoveryContext().Cmd == \"apply\" && slices.Contains(e.Component.DiscoveryContext().Args, \"-destroy\") {\n\t\treturn false\n\t}\n\n\tif e.Component.DiscoveryContext().Cmd == \"plan\" && slices.Contains(e.Component.DiscoveryContext().Args, \"-destroy\") {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\ntype Queue struct {\n\t// Entries is a list of entries in the queue.\n\tEntries Entries\n\t// mu is a mutex used to synchronize access to the queue.\n\tmu sync.RWMutex\n\t// FailFast, if set to true, causes the queue to fail fast if any entry fails.\n\tFailFast bool\n\t// IgnoreDependencyOrder, if set to true, causes the queue to ignore dependencies when fetching ready entries.\n\t// When enabled, GetReadyWithDependencies will return all entries with StatusReady, regardless of dependency status.\n\tIgnoreDependencyOrder bool\n\t// IgnoreDependencyErrors, if set to true, allows scheduling and running entries even if their\n\t// dependencies failed. Additionally, failures will not propagate EarlyExit to dependents/dependencies.\n\tIgnoreDependencyErrors bool\n}\n\ntype Entries []*Entry\n\n// Entry returns a given entry from the queue.\nfunc (e Entries) Entry(cfg component.Component) *Entry {\n\tfor _, entry := range e {\n\t\tif entry.Component.Path() == cfg.Path() {\n\t\t\treturn entry\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Components returns the queue components.\nfunc (q *Queue) Components() component.Components {\n\tresult := make(component.Components, 0, len(q.Entries))\n\tfor _, entry := range q.Entries {\n\t\tresult = append(result, entry.Component)\n\t}\n\n\treturn result\n}\n\n// EntryByPath returns the entry with the given config path, or nil if not found.\nfunc (q *Queue) EntryByPath(path string) *Entry {\n\tq.mu.RLock()\n\tdefer q.mu.RUnlock()\n\n\treturn q.entryByPathUnsafe(path)\n}\n\n// entryByPathUnsafe returns the entry with the given config path without locking.\n// Should only be called when the caller already holds a lock.\nfunc (q *Queue) entryByPathUnsafe(path string) *Entry {\n\tfor _, entry := range q.Entries {\n\t\tif entry.Component.Path() == path {\n\t\t\treturn entry\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// NewQueue creates a new queue from a list of discovered configurations.\n// The queue is populated with the correct Terragrunt run order.\n//\n// Discovered configurations will be sorted based on two criteria:\n//\n//  1. The discovery context of the configuration:\n//     - If the configuration is for an \"up\" command (none of destroy, apply -destroy or plan -destroy),\n//     it will be inserted at the front of the queue, before its dependencies.\n//     - Otherwise, it is considered a \"down\" command, and will be inserted at the back of the queue,\n//     after its dependents.\n//\n//  2. The name of the configuration. Configurations of the same \"level\" are sorted alphabetically.\n//\n// Passing configurations that haven't been checked for cycles in their dependency graph is unsafe.\n// If any cycles are present, the queue construction will halt after N\n// iterations, where N is the number of discovered configs, and throw an error.\nfunc NewQueue(discovered component.Components) (*Queue, error) {\n\tif len(discovered) == 0 {\n\t\treturn &Queue{\n\t\t\tEntries: Entries{},\n\t\t}, nil\n\t}\n\n\t// First, we need to take all the discovered configs\n\t// and assign them a status of pending.\n\tentries := make(Entries, 0, len(discovered))\n\n\tfor _, cfg := range discovered {\n\t\tentry := &Entry{\n\t\t\tComponent: cfg,\n\t\t\tStatus:    StatusPending,\n\t\t}\n\t\tentries = append(entries, entry)\n\t}\n\n\tq := &Queue{\n\t\tEntries: entries,\n\t}\n\n\t// readyPending returns the index of the first pending entry if there is one,\n\t// or -1 if there are no pending entries.\n\treadyPending := func(entries Entries) int {\n\t\t// Next, we need to iterate through the entries\n\t\t// and check if any of them are blocked.\n\t\tfor _, entry := range entries {\n\t\t\tentry.UpdateBlocked(entries)\n\t\t}\n\n\t\t// Next, we need to sort the entries by status and path.\n\t\tsort.SliceStable(entries, func(i, j int) bool {\n\t\t\tif entries[i].Status > entries[j].Status {\n\t\t\t\treturn true\n\t\t\t}\n\n\t\t\tif entries[i].Status == StatusUnsorted && entries[j].Status == StatusUnsorted {\n\t\t\t\treturn entries[i].Component.Path() < entries[j].Component.Path()\n\t\t\t}\n\n\t\t\treturn false\n\t\t})\n\n\t\t// Now, we can mark all unsorted entries as ready,\n\t\t// and check if all entries are ready.\n\t\tfor idx, entry := range entries {\n\t\t\tif entry.Status == StatusUnsorted {\n\t\t\t\tentry.Status = StatusReady\n\t\t\t}\n\n\t\t\tif entry.Status != StatusReady {\n\t\t\t\treturn idx\n\t\t\t}\n\t\t}\n\n\t\treturn -1\n\t}\n\n\t// We need to iterate through the entries until all entries are ready.\n\t// We can use the length of the entries as a safe upper bound for the number of iterations,\n\t// because a cycle-free graph has a maximum depth of N, where N is the number of discovered configs.\n\tmaxIterations := len(entries)\n\n\t// We keep track of the index of the first pending entry\n\t// to save us from iterating through the entire list of entries\n\t// on each iteration.\n\tfirstPending := 0\n\n\tfor range maxIterations {\n\t\tfirstPending = readyPending(entries[firstPending:])\n\t\tif firstPending == -1 {\n\t\t\treturn q, nil\n\t\t}\n\t}\n\n\treturn q, errors.New(\"cycle detected during queue construction\")\n}\n\n// GetReadyWithDependencies returns all entries that are ready to run and have all dependencies completed (or no dependencies).\nfunc (q *Queue) GetReadyWithDependencies(l log.Logger) []*Entry {\n\tq.mu.RLock()\n\tdefer q.mu.RUnlock()\n\n\tif q.IgnoreDependencyOrder {\n\t\tout := make([]*Entry, 0, len(q.Entries))\n\n\t\tfor _, e := range q.Entries {\n\t\t\tif e.Status == StatusReady {\n\t\t\t\tout = append(out, e)\n\t\t\t}\n\t\t}\n\n\t\treturn out\n\t}\n\n\tout := make([]*Entry, 0, len(q.Entries))\n\n\tfor _, e := range q.Entries {\n\t\tif e.Status != StatusReady {\n\t\t\tcontinue\n\t\t}\n\n\t\tif e.IsUp() {\n\t\t\tif q.areDependenciesReadyUnsafe(l, e) {\n\t\t\t\tout = append(out, e)\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif q.areDependentsReadyUnsafe(e) {\n\t\t\tout = append(out, e)\n\t\t}\n\t}\n\n\treturn out\n}\n\n// areDependenciesReadyUnsafe checks if all dependencies of an entry are ready for \"up\" commands.\n// For up commands, all dependencies must be in a succeeded state (or terminal if ignoring errors).\n// If a dependency is not in the queue, it is assumed to have existing state.\n// Should only be called when the caller already holds a read lock.\nfunc (q *Queue) areDependenciesReadyUnsafe(l log.Logger, e *Entry) bool {\n\tfor _, dep := range e.Component.Dependencies() {\n\t\tdepEntry := q.entryByPathUnsafe(dep.Path())\n\t\tif depEntry == nil {\n\t\t\tl.Debugf(\"Dependency %s is not in queue, considering it ready\", dep.Path())\n\n\t\t\tcontinue\n\t\t}\n\n\t\t// When ignoring dependency errors, allow scheduling if dependencies are in a terminal state\n\t\t// (succeeded OR failed), not just succeeded\n\t\tif q.IgnoreDependencyErrors {\n\t\t\tif !isTerminal(depEntry.Status) {\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif depEntry.Status != StatusSucceeded {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// areDependentsReadyUnsafe checks if all dependents of an entry are ready for \"down\" commands.\n// For down commands, all dependents must be in a succeeded state (or terminal if ignoring errors).\n// Should only be called when the caller already holds a read lock.\nfunc (q *Queue) areDependentsReadyUnsafe(e *Entry) bool {\n\tfor _, other := range q.Entries {\n\t\tif other == e || len(other.Component.Dependencies()) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, dep := range other.Component.Dependencies() {\n\t\t\tif dep.Path() == e.Component.Path() {\n\t\t\t\t// When ignoring dependency errors, allow scheduling if dependents are in a terminal state\n\t\t\t\t// (succeeded OR failed), not just succeeded\n\t\t\t\tif q.IgnoreDependencyErrors {\n\t\t\t\t\tif !isTerminal(other.Status) {\n\t\t\t\t\t\treturn false\n\t\t\t\t\t}\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif other.Status != StatusSucceeded {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn true\n}\n\n// SetEntryStatus safely sets the status of an entry with proper synchronization.\n//\n// If the entry is already in a terminal state (StatusSucceeded, StatusFailed, or StatusEarlyExit),\n// this operation is a no-op. This prevents race conditions where a concurrent success could\n// overwrite an early-exit status set by fail-fast mode.\nfunc (q *Queue) SetEntryStatus(e *Entry, status Status) {\n\tq.mu.Lock()\n\tdefer q.mu.Unlock()\n\n\tif isTerminal(e.Status) {\n\t\treturn\n\t}\n\n\te.Status = status\n}\n\n// FailEntry marks the entry as failed and updates related entries if needed.\n// For up commands, this marks entries that come after this one as early exit.\n// For destroy/down commands, this marks entries that come before this one as early exit.\n// Use only for failure transitions. For other status changes, set Status directly.\nfunc (q *Queue) FailEntry(e *Entry) {\n\tq.mu.Lock()\n\tdefer q.mu.Unlock()\n\n\te.Status = StatusFailed\n\n\t// If this entry failed and has dependents/dependencies, we need to propagate the failure.\n\tif q.FailFast {\n\t\tfor _, n := range q.Entries {\n\t\t\tif isTerminalOrRunning(n.Status) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tn.Status = StatusEarlyExit\n\t\t}\n\n\t\treturn\n\t}\n\n\t// If ignoring dependency errors, do not propagate early exit to other entries.\n\tif q.IgnoreDependencyErrors {\n\t\treturn\n\t}\n\n\tif e.IsUp() {\n\t\tq.earlyExitDependents(e)\n\t\treturn\n\t}\n\n\tq.earlyExitDependencies(e)\n}\n\n// earlyExitDependents - Recursively mark all entries that are dependent on this one as early exit.\nfunc (q *Queue) earlyExitDependents(e *Entry) {\n\tfor _, entry := range q.Entries {\n\t\tif len(entry.Component.Dependencies()) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, dep := range entry.Component.Dependencies() {\n\t\t\tif dep.Path() == e.Component.Path() {\n\t\t\t\tif isTerminalOrRunning(entry.Status) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tentry.Status = StatusEarlyExit\n\n\t\t\t\tq.earlyExitDependents(entry)\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n\n// earlyExitDependencies - Recursively mark all entries that are dependencies on this one as early exit.\nfunc (q *Queue) earlyExitDependencies(e *Entry) {\n\tif len(e.Component.Dependencies()) == 0 {\n\t\treturn\n\t}\n\n\tfor _, dep := range e.Component.Dependencies() {\n\t\tdepEntry := q.entryByPathUnsafe(dep.Path())\n\t\tif depEntry == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif isTerminalOrRunning(depEntry.Status) {\n\t\t\tcontinue\n\t\t}\n\n\t\tdepEntry.Status = StatusEarlyExit\n\t\tq.earlyExitDependencies(depEntry)\n\t}\n}\n\n// Finished checks if all entries in the queue are in a terminal state (i.e., not pending, blocked, ready, or running).\nfunc (q *Queue) Finished() bool {\n\tq.mu.RLock()\n\tdefer q.mu.RUnlock()\n\n\tfor _, e := range q.Entries {\n\t\tif !isTerminal(e.Status) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// RemainingDeps Helper to calculate remaining dependencies for an entry.\nfunc (q *Queue) RemainingDeps(e *Entry) int {\n\tif e.Component == nil || len(e.Component.Dependencies()) == 0 {\n\t\treturn 0\n\t}\n\n\tq.mu.RLock()\n\tdefer q.mu.RUnlock()\n\n\tcount := 0\n\n\tfor _, dep := range e.Component.Dependencies() {\n\t\tdepEntry := q.entryByPathUnsafe(dep.Path())\n\t\tif depEntry == nil || depEntry.Status != StatusSucceeded {\n\t\t\tcount++\n\t\t}\n\t}\n\n\treturn count\n}\n\n// isTerminal returns true if the status is terminal.\nfunc isTerminal(status Status) bool {\n\tswitch status {\n\tcase StatusPending, StatusBlocked, StatusUnsorted, StatusReady, StatusRunning:\n\t\treturn false\n\tcase StatusSucceeded, StatusFailed, StatusEarlyExit:\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// isTerminalOrRunning returns true if the status is terminal or running.\nfunc isTerminalOrRunning(status Status) bool {\n\treturn status == StatusRunning || isTerminal(status)\n}\n"
  },
  {
    "path": "internal/queue/queue_test.go",
    "content": "package queue_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/queue\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNoDependenciesMaintainsAlphabeticalOrder(t *testing.T) {\n\tt.Parallel()\n\n\t// Create configs with no dependencies - should maintain alphabetical order at front\n\tconfigs := component.Components{\n\t\tcomponent.NewUnit(\"c\"),\n\t\tcomponent.NewUnit(\"a\"),\n\t\tcomponent.NewUnit(\"b\"),\n\t}\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\tentries := q.Entries\n\n\t// Should be sorted alphabetically at front since none have dependencies\n\tassert.Equal(t, \"a\", entries[0].Component.Path())\n\tassert.Equal(t, \"b\", entries[1].Component.Path())\n\tassert.Equal(t, \"c\", entries[2].Component.Path())\n}\n\nfunc TestDependenciesOrderedByDependencyLevel(t *testing.T) {\n\tt.Parallel()\n\n\t// Create configs with dependencies - should order by dependency level\n\taCfg := component.NewUnit(\"a\")\n\tbCfg := component.NewUnit(\"b\")\n\tbCfg.AddDependency(aCfg)\n\n\tcCfg := component.NewUnit(\"c\")\n\tcCfg.AddDependency(bCfg)\n\n\tconfigs := component.Components{aCfg, bCfg, cCfg}\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\tentries := q.Entries\n\n\t// 'a' has no deps so should be at front\n\t// 'b' depends on 'a' so should be after\n\t// 'c' depends on 'b' so should be at back\n\tassert.Equal(t, \"a\", entries[0].Component.Path())\n\tassert.Equal(t, \"b\", entries[1].Component.Path())\n\tassert.Equal(t, \"c\", entries[2].Component.Path())\n}\n\nfunc TestComplexDagOrderedByDependencyLevelAndAlphabetically(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a more complex dependency graph:\n\t// Create a more complex dependency graph:\n\t//   A (no deps)\n\t//   B (no deps)\n\t//   C -> A\n\t//   D -> A,B\n\t//   E -> C\n\t//   F -> C\n\tA := component.NewUnit(\"A\")\n\tB := component.NewUnit(\"B\")\n\tC := component.NewUnit(\"C\")\n\tC.AddDependency(A)\n\n\tD := component.NewUnit(\"D\")\n\tD.AddDependency(A)\n\tD.AddDependency(B)\n\n\tE := component.NewUnit(\"E\")\n\tE.AddDependency(C)\n\n\tF := component.NewUnit(\"F\")\n\tF.AddDependency(C)\n\n\tconfigs := component.Components{F, E, D, C, B, A}\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\tentries := q.Entries\n\n\t// Verify ordering by dependency level and alphabetically within levels:\n\t// Level 0 (no deps): A, B\n\t// Level 1 (depends on level 0): C, D\n\t// Level 2 (depends on level 1): E, F\n\tassert.Equal(t, \"A\", entries[0].Component.Path())\n\tassert.Equal(t, \"B\", entries[1].Component.Path())\n\tassert.Equal(t, \"C\", entries[2].Component.Path())\n\tassert.Equal(t, \"D\", entries[3].Component.Path())\n\tassert.Equal(t, \"E\", entries[4].Component.Path())\n\tassert.Equal(t, \"F\", entries[5].Component.Path())\n\n\t// Also verify relative ordering\n\taIndex := findIndex(entries, \"A\")\n\tbIndex := findIndex(entries, \"B\")\n\tcIndex := findIndex(entries, \"C\")\n\tdIndex := findIndex(entries, \"D\")\n\teIndex := findIndex(entries, \"E\")\n\tfIndex := findIndex(entries, \"F\")\n\n\t// Level 0 items should be before their dependents\n\tassert.Less(t, aIndex, cIndex, \"A should come before C\")\n\tassert.Less(t, aIndex, dIndex, \"A should come before D\")\n\tassert.Less(t, bIndex, dIndex, \"B should come before D\")\n\n\t// Level 1 items should be before their dependents\n\tassert.Less(t, cIndex, eIndex, \"C should come before E\")\n\tassert.Less(t, cIndex, fIndex, \"C should come before F\")\n}\n\nfunc TestDeterministicOrderingOfParallelDependencies(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a graph with parallel dependencies that could be ordered multiple ways:\n\t// Create a graph with parallel dependencies that could be ordered multiple ways:\n\t//   A (no deps)\n\t//   B -> A\n\t//   C -> A\n\t//   D -> A\n\tA := component.NewUnit(\"A\")\n\tB := component.NewUnit(\"B\")\n\tB.AddDependency(A)\n\n\tC := component.NewUnit(\"C\")\n\tC.AddDependency(A)\n\n\tD := component.NewUnit(\"D\")\n\tD.AddDependency(A)\n\tconfigs := component.Components{D, C, B, A}\n\n\t// Run multiple times to verify deterministic ordering\n\tfor range 5 {\n\t\tq, err := queue.NewQueue(configs)\n\t\trequire.NoError(t, err)\n\n\t\tentries := q.Entries\n\n\t\t// A should be first (no deps)\n\t\tassert.Equal(t, \"A\", entries[0].Component.Path())\n\n\t\t// B, C, D should maintain alphabetical order since they're all at the same level\n\t\tassert.Equal(t, \"B\", entries[1].Component.Path())\n\t\tassert.Equal(t, \"C\", entries[2].Component.Path())\n\t\tassert.Equal(t, \"D\", entries[3].Component.Path())\n\t}\n}\n\nfunc TestDepthBasedOrderingVerification(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a graph where depth matters:\n\t// Create a graph where depth matters:\n\t//   A (no deps, depth 0)\n\t//   B (no deps, depth 0)\n\t//   C -> A (depth 1)\n\t//   D -> B (depth 1)\n\t//   E -> C,D (depth 2)\n\tA := component.NewUnit(\"A\")\n\tB := component.NewUnit(\"B\")\n\tC := component.NewUnit(\"C\")\n\tC.AddDependency(A)\n\n\tD := component.NewUnit(\"D\")\n\tD.AddDependency(B)\n\n\tE := component.NewUnit(\"E\")\n\tE.AddDependency(C)\n\tE.AddDependency(D)\n\tconfigs := component.Components{E, D, C, B, A}\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\tentries := q.Entries\n\n\t// Verify that items are grouped by their depth levels\n\t// Level 0: A,B (no deps)\n\t// Level 1: C,D (depend on level 0)\n\t// Level 2: E (depends on level 1)\n\n\t// First verify the basic ordering\n\tassert.Len(t, entries, 5, \"Should have all 5 entries\")\n\n\t// Find indices\n\taIndex := findIndex(entries, \"A\")\n\tbIndex := findIndex(entries, \"B\")\n\tcIndex := findIndex(entries, \"C\")\n\tdIndex := findIndex(entries, \"D\")\n\teIndex := findIndex(entries, \"E\")\n\n\t// Level 0 items should be at the start (indices 0 or 1)\n\tassert.LessOrEqual(t, aIndex, 1, \"A should be in first two positions\")\n\tassert.LessOrEqual(t, bIndex, 1, \"B should be in first two positions\")\n\n\t// Level 1 items should be in the middle (indices 2 or 3)\n\tassert.True(t, cIndex >= 2 && cIndex <= 3, \"C should be in middle positions\")\n\tassert.True(t, dIndex >= 2 && dIndex <= 3, \"D should be in middle positions\")\n\n\t// Level 2 item should be at the end (index 4)\n\tassert.Equal(t, 4, eIndex, \"E should be in last position\")\n}\n\nfunc TestErrorHandlingCycle(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a cycle: A -> B -> C -> A\n\t// Create a cycle: A -> B -> C -> A\n\tA := component.NewUnit(\"A\")\n\tB := component.NewUnit(\"B\")\n\tC := component.NewUnit(\"C\")\n\tC.AddDependency(B)\n\tB.AddDependency(A)\n\tA.AddDependency(C) // Creates the cycle\n\tconfigs := component.Components{C, B, A}\n\n\tq, err := queue.NewQueue(configs)\n\trequire.Error(t, err)\n\tassert.NotNil(t, q)\n}\n\nfunc TestErrorHandlingEmptyConfigList(t *testing.T) {\n\tt.Parallel()\n\n\t// Create an empty config list\n\tconfigs := component.Components{}\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\tassert.Empty(t, q.Entries)\n}\n\n// findIndex returns the index of the config with the given path in the slice\nfunc findIndex(entries queue.Entries, path string) int {\n\tfor i, cfg := range entries {\n\t\tif cfg.Component.Path() == path {\n\t\t\treturn i\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc TestQueue_LinearDependencyExecution(t *testing.T) {\n\tt.Parallel()\n\t// A -> B -> C\n\tcfgA := component.NewUnit(\"A\")\n\tcfgB := component.NewUnit(\"B\")\n\tcfgB.AddDependency(cfgA)\n\n\tcfgC := component.NewUnit(\"C\")\n\tcfgC.AddDependency(cfgB)\n\tconfigs := component.Components{cfgA, cfgB, cfgC}\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\t// Initially, not all are terminal\n\tassert.False(t, q.Finished(), \"Finished should be false at start\")\n\n\t// Check that all entries are ready initially and in order A, B, C\n\treadyEntries := q.GetReadyWithDependencies(logger.CreateLogger())\n\tassert.Len(t, readyEntries, 1, \"Initially only A should be ready\")\n\tassert.Equal(t, queue.StatusReady, readyEntries[0].Status, \"Entry %s should have StatusReady\", readyEntries[0].Component.Path())\n\tassert.Equal(t, \"A\", readyEntries[0].Component.Path(), \"First ready entry should be A\")\n\n\t// Mark A as running and complete it\n\tentryA := readyEntries[0]\n\tentryA.Status = queue.StatusSucceeded\n\n\tassert.False(t, q.Finished(), \"Finished should be false after A is done\")\n\n\treadyEntries = q.GetReadyWithDependencies(logger.CreateLogger())\n\tassert.Len(t, readyEntries, 1, \"After A is done, only B should be ready\")\n\tassert.Equal(t, \"B\", readyEntries[0].Component.Path(), \"Second ready entry should be B\")\n\n\t// Mark B as running and complete it\n\tentryB := readyEntries[0]\n\tentryB.Status = queue.StatusSucceeded\n\n\tassert.False(t, q.Finished(), \"Finished should be false after B is done\")\n\n\treadyEntries = q.GetReadyWithDependencies(logger.CreateLogger())\n\tassert.Len(t, readyEntries, 1, \"After B is done, only C should be ready\")\n\tassert.Equal(t, \"C\", readyEntries[0].Component.Path(), \"Third ready entry should be C\")\n\n\t// Mark C as running and complete it\n\tentryC := readyEntries[0]\n\tentryC.Status = queue.StatusSucceeded\n\n\t// Now all should be terminal\n\tassert.True(t, q.Finished(), \"Finished should be true after all succeeded\")\n\n\treadyEntries = q.GetReadyWithDependencies(logger.CreateLogger())\n\tassert.Empty(t, readyEntries, \"After C is done, no entries should be ready\")\n}\n\nfunc TestQueue_ParallelExecution(t *testing.T) {\n\tt.Parallel()\n\t//   A\n\t//  / \\\n\t// B   C\n\tcfgA := component.NewUnit(\"A\")\n\tcfgB := component.NewUnit(\"B\")\n\tcfgB.AddDependency(cfgA)\n\n\tcfgC := component.NewUnit(\"C\")\n\tcfgC.AddDependency(cfgA)\n\tconfigs := component.Components{cfgA, cfgB, cfgC}\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\t// 1. Initially, only A should be ready\n\treadyEntries := q.GetReadyWithDependencies(logger.CreateLogger())\n\tassert.Len(t, readyEntries, 1, \"Initially only A should be ready\")\n\tassert.Equal(t, queue.StatusReady, readyEntries[0].Status, \"Entry %s should have StatusReady\", readyEntries[0].Component.Path())\n\tassert.Equal(t, \"A\", readyEntries[0].Component.Path(), \"First ready entry should be A\")\n\n\t// Mark A as running and complete it\n\tentryA := readyEntries[0]\n\tentryA.Status = queue.StatusSucceeded\n\n\t// 2. After A is done, both B and C should be ready (order doesn't matter)\n\treadyEntries = q.GetReadyWithDependencies(logger.CreateLogger())\n\tassert.Len(t, readyEntries, 2, \"After A is done, B and C should be ready\")\n\tpaths := []string{readyEntries[0].Component.Path(), readyEntries[1].Component.Path()}\n\tassert.Contains(t, paths, \"B\")\n\tassert.Contains(t, paths, \"C\")\n\n\tfor _, entry := range readyEntries {\n\t\tassert.Equal(t, queue.StatusReady, entry.Status, \"Entry %s should have StatusReady\", entry.Component.Path())\n\t}\n\n\t// Mark B as running and complete it\n\tvar entryB, entryC *queue.Entry\n\n\tfor _, entry := range readyEntries {\n\t\tif entry.Component.Path() == \"B\" {\n\t\t\tentryB = entry\n\t\t}\n\n\t\tif entry.Component.Path() == \"C\" {\n\t\t\tentryC = entry\n\t\t}\n\t}\n\n\tentryB.Status = queue.StatusSucceeded\n\n\t// After B is done, C should still be ready (if not already marked)\n\treadyEntries = q.GetReadyWithDependencies(logger.CreateLogger())\n\tif entryC.Status != queue.StatusSucceeded {\n\t\tassert.Len(t, readyEntries, 1, \"After B is done, C should still be ready\")\n\t\tassert.Equal(t, \"C\", readyEntries[0].Component.Path())\n\n\t\tentryC.Status = queue.StatusSucceeded\n\t}\n\n\t// After C is done, nothing should be ready\n\treadyEntries = q.GetReadyWithDependencies(logger.CreateLogger())\n\tassert.Empty(t, readyEntries, \"After B and C are done, no entries should be ready\")\n}\n\nfunc TestQueue_FailFast(t *testing.T) {\n\tt.Parallel()\n\t//   A\n\t//  / \\\n\t// B   C\n\tcfgA := component.NewUnit(\"A\")\n\tcfgB := component.NewUnit(\"B\")\n\tcfgB.AddDependency(cfgA)\n\n\tcfgC := component.NewUnit(\"C\")\n\tcfgC.AddDependency(cfgA)\n\tconfigs := component.Components{cfgA, cfgB, cfgC}\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\tq.FailFast = true\n\n\tassert.False(t, q.Finished(), \"Finished should be false at start\")\n\n\t// Simulate A failing\n\tvar entryA *queue.Entry\n\n\tfor _, entry := range q.Entries {\n\t\tif entry.Component.Path() == \"A\" {\n\t\t\tentryA = entry\n\t\t\tbreak\n\t\t}\n\t}\n\n\trequire.NotNil(t, entryA, \"Entry A should exist\")\n\tentryA.Status = queue.StatusRunning\n\tq.FailEntry(entryA)\n\n\t// B and C should be marked as early exit due to fail-fast\n\tfor _, entry := range q.Entries {\n\t\tswitch entry.Component.Path() {\n\t\tcase \"A\":\n\t\t\tassert.Equal(t, queue.StatusFailed, entry.Status, \"Entry %s should have StatusFailed\", entry.Component.Path())\n\t\tcase \"B\", \"C\":\n\t\t\tassert.Equal(t, queue.StatusEarlyExit, entry.Status, \"Entry %s should have StatusEarlyExit\", entry.Component.Path())\n\t\t}\n\t}\n\n\t// All entries should be listed as terminal (A: Failed, B/C: EarlyExit)\n\tfor _, entry := range q.Entries {\n\t\tassert.True(t, entry.Status == queue.StatusFailed || entry.Status == queue.StatusEarlyExit, \"Entry %s should be terminal\", entry.Component.Path())\n\t}\n\n\t// Now all should be terminal\n\tassert.True(t, q.Finished(), \"Finished should be true after fail-fast triggers\")\n\n\t// No entries should be ready after fail-fast\n\treadyEntries := q.GetReadyWithDependencies(logger.CreateLogger())\n\tassert.Empty(t, readyEntries, \"No entries should be ready after fail-fast triggers\")\n}\n\n// buildMultiLevelDependencyTree returns the configs for the following dependency tree:\n//\n//\t  A\n//\t / \\\n//\tB   C\n//\n// / \\\n// D   E\nfunc buildMultiLevelDependencyTree() component.Components {\n\tcfgA := component.NewUnit(\"A\")\n\tcfgB := component.NewUnit(\"B\")\n\tcfgB.AddDependency(cfgA)\n\n\tcfgC := component.NewUnit(\"C\")\n\tcfgC.AddDependency(cfgA)\n\n\tcfgD := component.NewUnit(\"D\")\n\tcfgD.AddDependency(cfgB)\n\n\tcfgE := component.NewUnit(\"E\")\n\tcfgE.AddDependency(cfgB)\n\tcomponents := component.Components{cfgA, cfgB, cfgC, cfgD, cfgE}\n\n\treturn components\n}\n\nfunc TestQueue_AdvancedDependencyOrder(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\tconfigs := buildMultiLevelDependencyTree()\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\t// 1. Initially, only A should be ready\n\treadyEntries := q.GetReadyWithDependencies(l)\n\tassert.Len(t, readyEntries, 1, \"Initially only A should be ready\")\n\tassert.Equal(t, \"A\", readyEntries[0].Component.Path())\n\n\t// Mark A as succeeded\n\tentryA := readyEntries[0]\n\tentryA.Status = queue.StatusSucceeded\n\n\t// 2. After A, B and C should be ready\n\treadyEntries = q.GetReadyWithDependencies(l)\n\tassert.Len(t, readyEntries, 2, \"After A, B and C should be ready\")\n\tpaths := []string{readyEntries[0].Component.Path(), readyEntries[1].Component.Path()}\n\tassert.Contains(t, paths, \"B\")\n\tassert.Contains(t, paths, \"C\")\n\n\t// Mark B as succeeded\n\tvar entryB, entryC *queue.Entry\n\n\tfor _, entry := range readyEntries {\n\t\tif entry.Component.Path() == \"B\" {\n\t\t\tentryB = entry\n\t\t}\n\n\t\tif entry.Component.Path() == \"C\" {\n\t\t\tentryC = entry\n\t\t}\n\t}\n\n\tentryB.Status = queue.StatusSucceeded\n\n\t// 3. After B is done, C should still be ready (if not already marked), and D and E should be ready\n\treadyEntries = q.GetReadyWithDependencies(l)\n\n\treadyPaths := map[string]bool{}\n\tfor _, entry := range readyEntries {\n\t\treadyPaths[entry.Component.Path()] = true\n\t}\n\t// C may still be ready if not yet marked as succeeded\n\tassert.Contains(t, readyPaths, \"C\")\n\tassert.Contains(t, readyPaths, \"D\")\n\tassert.Contains(t, readyPaths, \"E\")\n\tassert.Len(t, readyEntries, 3, \"After B is done, C, D, and E should be ready\")\n\n\t// Mark C as succeeded\n\tentryC.Status = queue.StatusSucceeded\n\n\t// Mark D and E as succeeded\n\tvar entryD, entryE *queue.Entry\n\n\tfor _, entry := range readyEntries {\n\t\tif entry.Component.Path() == \"D\" {\n\t\t\tentryD = entry\n\t\t}\n\n\t\tif entry.Component.Path() == \"E\" {\n\t\t\tentryE = entry\n\t\t}\n\t}\n\n\tentryD.Status = queue.StatusSucceeded\n\tentryE.Status = queue.StatusSucceeded\n\n\t// 4. After all are done, nothing should be ready\n\treadyEntries = q.GetReadyWithDependencies(l)\n\tassert.Empty(t, readyEntries, \"After all are done, no entries should be ready\")\n}\n\nfunc TestQueue_AdvancedDependency_BFails(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\tconfigs := buildMultiLevelDependencyTree()\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\tq.FailFast = true\n\n\t// 1. Initially, only A should be ready\n\treadyEntries := q.GetReadyWithDependencies(l)\n\tassert.Len(t, readyEntries, 1, \"Initially only A should be ready\")\n\tassert.Equal(t, \"A\", readyEntries[0].Component.Path())\n\n\t// Mark A as succeeded\n\tentryA := readyEntries[0]\n\tentryA.Status = queue.StatusSucceeded\n\n\t// 2. After A, B and C should be ready\n\treadyEntries = q.GetReadyWithDependencies(l)\n\n\tvar entryB, entryC *queue.Entry\n\n\tfor _, entry := range readyEntries {\n\t\tif entry.Component.Path() == \"B\" {\n\t\t\tentryB = entry\n\t\t}\n\n\t\tif entry.Component.Path() == \"C\" {\n\t\t\tentryC = entry\n\t\t}\n\t}\n\n\tassert.NotNil(t, entryB)\n\tassert.NotNil(t, entryC)\n\n\t// Mark B as failed\n\tentryB.Status = queue.StatusRunning\n\tq.FailEntry(entryB)\n\n\t// Fail fast should mark all not-yet-started tasks as early exit\n\tassert.Equal(t, queue.StatusFailed, q.EntryByPath(\"B\").Status)\n\tassert.Equal(t, queue.StatusEarlyExit, q.EntryByPath(\"D\").Status)\n\tassert.Equal(t, queue.StatusEarlyExit, q.EntryByPath(\"E\").Status)\n\tassert.Equal(t, queue.StatusEarlyExit, q.EntryByPath(\"C\").Status)\n\n\treadyEntries = q.GetReadyWithDependencies(l)\n\tassert.Empty(t, readyEntries, \"All entries should be terminal\")\n}\n\nfunc TestQueue_AdvancedDependency_BFails_NoFailFast(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\tconfigs := buildMultiLevelDependencyTree()\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\tq.FailFast = false\n\n\tassert.False(t, q.Finished(), \"Finished should be false at start\")\n\n\t// 1. Initially, only A should be ready\n\treadyEntries := q.GetReadyWithDependencies(l)\n\tassert.Len(t, readyEntries, 1, \"Initially only A should be ready\")\n\tassert.Equal(t, \"A\", readyEntries[0].Component.Path())\n\n\t// Mark A as succeeded\n\tentryA := readyEntries[0]\n\tentryA.Status = queue.StatusSucceeded\n\n\tassert.False(t, q.Finished(), \"Finished should be false after A is done\")\n\n\t// 2. After A, B and C should be ready\n\treadyEntries = q.GetReadyWithDependencies(l)\n\n\tvar entryB, entryC *queue.Entry\n\n\tfor _, entry := range readyEntries {\n\t\tif entry.Component.Path() == \"B\" {\n\t\t\tentryB = entry\n\t\t}\n\n\t\tif entry.Component.Path() == \"C\" {\n\t\t\tentryC = entry\n\t\t}\n\t}\n\n\tassert.NotNil(t, entryB)\n\tassert.NotNil(t, entryC)\n\n\t// Mark B as failed\n\tentryB.Status = queue.StatusRunning\n\tq.FailEntry(entryB)\n\n\tassert.False(t, q.Finished(), \"Finished should be false after B fails if C is not done\")\n\n\t// D and E should be marked as early exit due to dependency on B\n\tassert.Equal(t, queue.StatusFailed, q.EntryByPath(\"B\").Status)\n\tassert.Equal(t, queue.StatusEarlyExit, q.EntryByPath(\"D\").Status)\n\tassert.Equal(t, queue.StatusEarlyExit, q.EntryByPath(\"E\").Status)\n\n\t// C should still be ready\n\treadyEntries = q.GetReadyWithDependencies(l)\n\tassert.Len(t, readyEntries, 1, \"Only C should be ready after B fails\")\n\tassert.Equal(t, \"C\", readyEntries[0].Component.Path())\n\n\t// Mark C as succeeded\n\tentryC.Status = queue.StatusSucceeded\n\n\t// After C is done, now all should be terminal\n\tassert.True(t, q.Finished(), \"Finished should be true after all entries are terminal\")\n\n\t// After C is done, nothing should be ready\n\treadyEntries = q.GetReadyWithDependencies(l)\n\tassert.Empty(t, readyEntries, \"After C is done, no entries should be ready\")\n}\n\nfunc TestQueue_FailFast_SequentialOrder(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\t// A -> B -> C, where A fails and fail-fast is enabled\n\tcfgA := component.NewUnit(\"A\")\n\tcfgB := component.NewUnit(\"B\")\n\tcfgB.AddDependency(cfgA)\n\n\tcfgC := component.NewUnit(\"C\")\n\tcfgC.AddDependency(cfgB)\n\tconfigs := component.Components{cfgA, cfgB, cfgC}\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\tq.FailFast = true\n\n\tassert.False(t, q.Finished(), \"Finished should be false at start\")\n\n\t// Only A should be ready\n\treadyEntries := q.GetReadyWithDependencies(l)\n\tassert.Len(t, readyEntries, 1, \"Initially only A should be ready\")\n\tassert.Equal(t, \"A\", readyEntries[0].Component.Path())\n\n\t// Mark A as running and then failed\n\tentryA := readyEntries[0]\n\tentryA.Status = queue.StatusRunning\n\tq.FailEntry(entryA)\n\n\t// After fail-fast, B and C should be early exit, A should be failed\n\tfor _, entry := range q.Entries {\n\t\tswitch entry.Component.Path() {\n\t\tcase \"A\":\n\t\t\tassert.Equal(t, queue.StatusFailed, entry.Status, \"Entry %s should have StatusFailed\", entry.Component.Path())\n\t\tcase \"B\", \"C\":\n\t\t\tassert.Equal(t, queue.StatusEarlyExit, entry.Status, \"Entry %s should have StatusEarlyExit\", entry.Component.Path())\n\t\t}\n\t}\n\n\t// Finished should be true\n\tassert.True(t, q.Finished(), \"Finished should be true after fail-fast triggers\")\n\n\t// No entries should be ready\n\treadyEntries = q.GetReadyWithDependencies(l)\n\tassert.Empty(t, readyEntries, \"No entries should be ready after fail-fast triggers\")\n}\n\nfunc TestQueue_IgnoreDependencyOrder_MultiLevel(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\tconfigs := buildMultiLevelDependencyTree()\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\tq.IgnoreDependencyOrder = true\n\n\treadyEntries := q.GetReadyWithDependencies(l)\n\tassert.Len(t, readyEntries, 5, \"Should be ready all entries\")\n}\n\nfunc TestFailEntry_DirectAndRecursive(t *testing.T) {\n\tt.Parallel()\n\t// Build a graph: A -> B -> C, A -> D\n\tcfgA := component.NewUnit(\"A\")\n\tcfgB := component.NewUnit(\"B\")\n\tcfgB.AddDependency(cfgA)\n\n\tcfgC := component.NewUnit(\"C\")\n\tcfgC.AddDependency(cfgB)\n\n\tcfgD := component.NewUnit(\"D\")\n\tcfgD.AddDependency(cfgA)\n\tconfigs := component.Components{cfgA, cfgB, cfgC, cfgD}\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\t// Non-fail-fast: Should recursively mark all dependencies as StatusEarlyExit\n\tq.FailFast = false\n\tentryA := q.EntryByPath(\"A\")\n\tq.FailEntry(entryA)\n\tassert.Equal(t, queue.StatusFailed, q.EntryByPath(\"A\").Status)\n\tassert.Equal(t, queue.StatusEarlyExit, q.EntryByPath(\"B\").Status)\n\tassert.Equal(t, queue.StatusEarlyExit, q.EntryByPath(\"C\").Status)\n\tassert.Equal(t, queue.StatusEarlyExit, q.EntryByPath(\"D\").Status)\n\n\t// Reset statuses for fail-fast test\n\tq, err = queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\tq.FailFast = true\n\tentryA = q.EntryByPath(\"A\")\n\tq.FailEntry(entryA)\n\tassert.Equal(t, queue.StatusFailed, q.EntryByPath(\"A\").Status)\n\tassert.Equal(t, queue.StatusEarlyExit, q.EntryByPath(\"B\").Status)\n\tassert.Equal(t, queue.StatusEarlyExit, q.EntryByPath(\"C\").Status)\n\tassert.Equal(t, queue.StatusEarlyExit, q.EntryByPath(\"D\").Status)\n}\n\nfunc TestQueue_DestroyFail_PropagatesToDependencies_NonFailFast(t *testing.T) {\n\tt.Parallel()\n\t// Build a graph: A -> B -> C, A -> D\n\tcfgA := component.NewUnit(\"A\")\n\tcfgB := component.NewUnit(\"B\")\n\tcfgB.AddDependency(cfgA)\n\n\tcfgC := component.NewUnit(\"C\")\n\tcfgC.AddDependency(cfgB)\n\n\tcfgD := component.NewUnit(\"D\")\n\tcfgD.AddDependency(cfgA)\n\tconfigs := component.Components{cfgA, cfgB, cfgC, cfgD}\n\n\t// Set all configs to destroy (down) command\n\tfor _, cfg := range configs {\n\t\tcfg.SetDiscoveryContext(&component.DiscoveryContext{Cmd: \"destroy\"})\n\t}\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\tq.FailFast = false\n\n\t// Fail C (should mark B and A as early exit, D should remain ready)\n\tentryC := q.EntryByPath(\"C\")\n\tq.FailEntry(entryC)\n\tassert.Equal(t, queue.StatusFailed, q.EntryByPath(\"C\").Status)\n\tassert.Equal(t, queue.StatusEarlyExit, q.EntryByPath(\"B\").Status)\n\tassert.Equal(t, queue.StatusEarlyExit, q.EntryByPath(\"A\").Status)\n\tassert.Equal(t, queue.StatusReady, q.EntryByPath(\"D\").Status)\n}\n\nfunc TestQueue_DestroyFail_PropagatesToDependencies(t *testing.T) {\n\tt.Parallel()\n\t// Build a graph: A -> B -> C, A -> D\n\tcfgA := component.NewUnit(\"A\")\n\tcfgB := component.NewUnit(\"B\")\n\tcfgB.AddDependency(cfgA)\n\n\tcfgC := component.NewUnit(\"C\")\n\tcfgC.AddDependency(cfgB)\n\n\tcfgD := component.NewUnit(\"D\")\n\tcfgD.AddDependency(cfgA)\n\tconfigs := component.Components{cfgA, cfgB, cfgC, cfgD}\n\n\t// Set all configs to destroy (down) command\n\tfor _, cfg := range configs {\n\t\tcfg.SetDiscoveryContext(&component.DiscoveryContext{Cmd: \"destroy\"})\n\t}\n\n\t// Only test fail-fast mode here\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\tq.FailFast = true\n\tentryC := q.EntryByPath(\"C\")\n\tq.FailEntry(entryC)\n\t// All non-terminal entries should be early exit\n\tassert.Equal(t, queue.StatusFailed, q.EntryByPath(\"C\").Status)\n\tassert.Equal(t, queue.StatusEarlyExit, q.EntryByPath(\"B\").Status)\n\tassert.Equal(t, queue.StatusEarlyExit, q.EntryByPath(\"A\").Status)\n\tassert.Equal(t, queue.StatusEarlyExit, q.EntryByPath(\"D\").Status)\n}\n\nfunc TestDestroyCommandQueueOrderIsReverseOfDependencies(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a simple chain: A -> B -> C\n\tcfgA := component.NewUnit(\"A\")\n\tcfgB := component.NewUnit(\"B\")\n\tcfgB.AddDependency(cfgA)\n\n\tcfgC := component.NewUnit(\"C\")\n\tcfgC.AddDependency(cfgB)\n\n\t// Set all configs to destroy (down) command\n\tcfgA.SetDiscoveryContext(&component.DiscoveryContext{Cmd: \"destroy\"})\n\tcfgB.SetDiscoveryContext(&component.DiscoveryContext{Cmd: \"destroy\"})\n\tcfgC.SetDiscoveryContext(&component.DiscoveryContext{Cmd: \"destroy\"})\n\n\tconfigs := component.Components{cfgA, cfgB, cfgC}\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\tentries := q.Entries\n\n\t// For destroy, the queue should be in reverse dependency order: C, B, A\n\tassert.Equal(t, \"C\", entries[0].Component.Path())\n\tassert.Equal(t, \"B\", entries[1].Component.Path())\n\tassert.Equal(t, \"A\", entries[2].Component.Path())\n}\n\nfunc TestDestroyCommandQueueOrder_MultiLevelDependencyTree(t *testing.T) {\n\tt.Parallel()\n\n\tconfigs := buildMultiLevelDependencyTree()\n\tfor _, cfg := range configs {\n\t\tcfg.SetDiscoveryContext(&component.DiscoveryContext{Cmd: \"destroy\"})\n\t}\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\tvar processed []string\n\n\tfor {\n\t\tready := q.GetReadyWithDependencies(logger.CreateLogger())\n\t\tif len(ready) == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, entry := range ready {\n\t\t\tprocessed = append(processed, entry.Component.Path())\n\t\t\tentry.Status = queue.StatusSucceeded\n\t\t}\n\t}\n\n\t// For destruction, the queue should be in reverse dependency order: E, D, C, B, A\n\texpected := []string{\"C\", \"D\", \"E\", \"B\", \"A\"}\n\tassert.Equal(t, expected, processed)\n}\n\n// TestQueue_DestroyWithIgnoreDependencyErrors_MaintainsOrder tests that when IgnoreDependencyErrors is true,\n// the queue still respects dependency order for destroy operations. This is the bug reported in issue #4947.\n// When a dependent fails, we should still wait for it to be in a terminal state before destroying the dependency.\nfunc TestQueue_DestroyWithIgnoreDependencyErrors_MaintainsOrder(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\t// Build a graph: A -> B -> C\n\t// For destroy, the order should be: C (destroyed first), then B, then A\n\tcfgA := component.NewUnit(\"A\")\n\tcfgB := component.NewUnit(\"B\")\n\tcfgB.AddDependency(cfgA)\n\n\tcfgC := component.NewUnit(\"C\")\n\tcfgC.AddDependency(cfgB)\n\n\t// Set all configs to destroy (down) command\n\tcfgA.SetDiscoveryContext(&component.DiscoveryContext{Cmd: \"destroy\"})\n\tcfgB.SetDiscoveryContext(&component.DiscoveryContext{Cmd: \"destroy\"})\n\tcfgC.SetDiscoveryContext(&component.DiscoveryContext{Cmd: \"destroy\"})\n\n\tconfigs := component.Components{cfgA, cfgB, cfgC}\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\t// Enable IgnoreDependencyErrors - this is the --queue-ignore-errors flag\n\tq.IgnoreDependencyErrors = true\n\n\t// Step 1: Only C should be ready (it has no dependents)\n\treadyEntries := q.GetReadyWithDependencies(l)\n\tassert.Len(t, readyEntries, 1, \"Initially only C should be ready for destruction\")\n\tassert.Equal(t, \"C\", readyEntries[0].Component.Path(), \"C should be the first entry ready for destruction\")\n\n\t// Mark C as succeeded\n\tentryC := readyEntries[0]\n\tentryC.Status = queue.StatusSucceeded\n\n\t// Step 2: After C is destroyed, B should be ready (but NOT A yet, as A is a dependency of B)\n\treadyEntries = q.GetReadyWithDependencies(l)\n\tassert.Len(t, readyEntries, 1, \"After C is destroyed, only B should be ready\")\n\tassert.Equal(t, \"B\", readyEntries[0].Component.Path(), \"B should be ready after C is destroyed\")\n\n\t// Mark B as succeeded\n\tentryB := readyEntries[0]\n\tentryB.Status = queue.StatusSucceeded\n\n\t// Step 3: After B is destroyed, A should be ready\n\treadyEntries = q.GetReadyWithDependencies(l)\n\tassert.Len(t, readyEntries, 1, \"After B is destroyed, only A should be ready\")\n\tassert.Equal(t, \"A\", readyEntries[0].Component.Path(), \"A should be ready last\")\n\n\t// Mark A as succeeded\n\tentryA := readyEntries[0]\n\tentryA.Status = queue.StatusSucceeded\n\n\t// Step 4: All entries should be finished\n\treadyEntries = q.GetReadyWithDependencies(l)\n\tassert.Empty(t, readyEntries, \"After all are destroyed, no entries should be ready\")\n\tassert.True(t, q.Finished(), \"Queue should be finished\")\n}\n\n// TestQueue_DestroyWithIgnoreDependencyErrors_AllowsProgressAfterFailure tests that when IgnoreDependencyErrors is true\n// and a dependent fails, we can still destroy the dependency once the dependent is in a terminal state.\nfunc TestQueue_DestroyWithIgnoreDependencyErrors_AllowsProgressAfterFailure(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\t// Build a graph: A -> B -> C\n\tcfgA := component.NewUnit(\"A\")\n\tcfgB := component.NewUnit(\"B\")\n\tcfgB.AddDependency(cfgA)\n\n\tcfgC := component.NewUnit(\"C\")\n\tcfgC.AddDependency(cfgB)\n\n\t// Set all configs to destroy (down) command\n\tcfgA.SetDiscoveryContext(&component.DiscoveryContext{Cmd: \"destroy\"})\n\tcfgB.SetDiscoveryContext(&component.DiscoveryContext{Cmd: \"destroy\"})\n\tcfgC.SetDiscoveryContext(&component.DiscoveryContext{Cmd: \"destroy\"})\n\n\tconfigs := component.Components{cfgA, cfgB, cfgC}\n\n\tq, err := queue.NewQueue(configs)\n\trequire.NoError(t, err)\n\n\tq.IgnoreDependencyErrors = true\n\n\t// Step 1: Only C should be ready\n\treadyEntries := q.GetReadyWithDependencies(l)\n\tassert.Len(t, readyEntries, 1, \"Initially only C should be ready\")\n\tassert.Equal(t, \"C\", readyEntries[0].Component.Path())\n\n\t// Mark C as FAILED (simulating a destroy failure)\n\tentryC := readyEntries[0]\n\tentryC.Status = queue.StatusRunning\n\tq.FailEntry(entryC)\n\n\t// With IgnoreDependencyErrors = true, B should NOT be marked as early exit\n\t// Instead, B should still be ready to run\n\tassert.Equal(t, queue.StatusFailed, q.EntryByPath(\"C\").Status, \"C should be failed\")\n\tassert.Equal(t, queue.StatusReady, q.EntryByPath(\"B\").Status, \"B should still be ready (not early exit)\")\n\n\t// Step 2: B should now be ready even though C failed\n\treadyEntries = q.GetReadyWithDependencies(l)\n\tassert.Len(t, readyEntries, 1, \"After C fails, B should still be ready due to IgnoreDependencyErrors\")\n\tassert.Equal(t, \"B\", readyEntries[0].Component.Path())\n\n\t// Mark B as succeeded\n\tentryB := readyEntries[0]\n\tentryB.Status = queue.StatusSucceeded\n\n\t// Step 3: After B succeeds, A should be ready\n\treadyEntries = q.GetReadyWithDependencies(l)\n\tassert.Len(t, readyEntries, 1, \"After B succeeds, A should be ready\")\n\tassert.Equal(t, \"A\", readyEntries[0].Component.Path())\n\n\t// Mark A as succeeded\n\tentryA := readyEntries[0]\n\tentryA.Status = queue.StatusSucceeded\n\n\t// Queue should be finished\n\tassert.True(t, q.Finished(), \"Queue should be finished\")\n}\n\nfunc TestSetEntryStatus_TerminalGuard(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tinitial   queue.Status\n\t\tattempted queue.Status\n\t}{\n\t\t{queue.StatusSucceeded, queue.StatusFailed},\n\t\t{queue.StatusSucceeded, queue.StatusEarlyExit},\n\t\t{queue.StatusSucceeded, queue.StatusRunning},\n\t\t{queue.StatusFailed, queue.StatusSucceeded},\n\t\t{queue.StatusFailed, queue.StatusEarlyExit},\n\t\t{queue.StatusEarlyExit, queue.StatusSucceeded},\n\t\t{queue.StatusEarlyExit, queue.StatusFailed},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"%v_to_%v\", tc.initial, tc.attempted), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tconfigs := component.Components{component.NewUnit(\"Test\")}\n\t\t\tq, err := queue.NewQueue(configs)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tentry := q.EntryByPath(\"Test\")\n\n\t\t\t// Set initial terminal state directly\n\t\t\tentry.Status = tc.initial\n\n\t\t\t// Attempt transition via SetEntryStatus\n\t\t\tq.SetEntryStatus(entry, tc.attempted)\n\n\t\t\tassert.Equal(t, tc.initial, entry.Status, \"Terminal status should not change\")\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/remotestate/backend/backend.go",
    "content": "// Package backend represents a backend for interacting with remote state.\npackage backend\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/iam\"\n\t\"github.com/gruntwork-io/terragrunt/internal/writer\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// Options contains the subset of configuration needed by backend operations.\ntype Options struct {\n\tWriters                      writer.Writers\n\tEnv                          map[string]string\n\tIAMRoleOptions               iam.RoleOptions\n\tNonInteractive               bool\n\tFailIfBucketCreationRequired bool\n}\n\ntype Backends []Backend\n\n// Get returns the backend by the given name.\nfunc (backends Backends) Get(name string) Backend {\n\tfor _, backend := range backends {\n\t\tif backend.Name() == name {\n\t\t\treturn backend\n\t\t}\n\t}\n\n\treturn nil\n}\n\ntype Backend interface {\n\t// Names returns the backend name.\n\tName() string\n\n\t// IsVersionControlEnabled returns true if the version control is enabled.\n\tIsVersionControlEnabled(ctx context.Context, l log.Logger, config Config, opts *Options) (bool, error)\n\n\t// NeedsBootstrap returns true if remote state needs to be bootstrapped.\n\tNeedsBootstrap(ctx context.Context, l log.Logger, config Config, opts *Options) (bool, error)\n\n\t// Bootstrap bootstraps the remote state.\n\tBootstrap(ctx context.Context, l log.Logger, config Config, opts *Options) error\n\n\t// Migrate determines where the remote state resources exist for source backend config and migrate them to dest backend config.\n\tMigrate(ctx context.Context, l log.Logger, srcConfig, dstConfig Config, opts *Options) error\n\n\t// Delete deletes the remote state.\n\tDelete(ctx context.Context, l log.Logger, config Config, opts *Options) error\n\n\t// DeleteBucket deletes the entire bucket.\n\tDeleteBucket(ctx context.Context, l log.Logger, config Config, opts *Options) error\n\n\t// GetTFInitArgs returns the config that should be passed on to `tofu -backend-config` cmd line param\n\t// Allows the Backends to filter and/or modify the configuration given from the user.\n\tGetTFInitArgs(config Config) map[string]any\n}\n"
  },
  {
    "path": "internal/remotestate/backend/common.go",
    "content": "package backend\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/puzpuzpuz/xsync/v3\"\n)\n\nvar _ Backend = new(CommonBackend)\n\ntype CommonBackend struct {\n\tbucketLocks   *xsync.MapOf[string, *sync.Mutex]\n\tinitedConfigs *xsync.MapOf[string, bool]\n\tname          string\n}\n\nfunc NewCommonBackend(name string) *CommonBackend {\n\treturn &CommonBackend{\n\t\tname:          name,\n\t\tbucketLocks:   xsync.NewMapOf[string, *sync.Mutex](),\n\t\tinitedConfigs: xsync.NewMapOf[string, bool](),\n\t}\n}\n\n// Name implements `backends.Backend` interface.\nfunc (backend *CommonBackend) Name() string {\n\treturn backend.name\n}\n\nfunc (backend *CommonBackend) IsVersionControlEnabled(ctx context.Context, l log.Logger, config Config, opts *Options) (bool, error) {\n\tl.Warnf(\"Checking version control for %s backend not implemented.\", backend.Name())\n\n\treturn false, nil\n}\n\n// NeedsBootstrap implements `backends.NeedsBootstrap` interface.\nfunc (backend *CommonBackend) NeedsBootstrap(ctx context.Context, l log.Logger, config Config, opts *Options) (bool, error) {\n\treturn false, nil\n}\n\n// Bootstrap implements `backends.Bootstrap` interface.\nfunc (backend *CommonBackend) Bootstrap(ctx context.Context, l log.Logger, config Config, opts *Options) error {\n\tl.Warnf(\"Bootstrap for %s backend not implemented.\", backend.Name())\n\n\treturn nil\n}\n\n// Migrate implements `backends.Migrate` interface.\nfunc (backend *CommonBackend) Migrate(ctx context.Context, l log.Logger, srcConfig, dstConfig Config, opts *Options) error {\n\tl.Warnf(\"Migrate for %s backend not implemented.\", backend.Name())\n\n\treturn nil\n}\n\n// Delete implements `backends.Delete` interface.\nfunc (backend *CommonBackend) Delete(ctx context.Context, l log.Logger, config Config, opts *Options) error {\n\tl.Warnf(\"Delete for %s backend not implemented.\", backend.Name())\n\n\treturn nil\n}\n\n// DeleteBucket implements `backends.DeleteBucket` interface.\nfunc (backend *CommonBackend) DeleteBucket(ctx context.Context, l log.Logger, config Config, opts *Options) error {\n\tl.Warnf(\"Deleting entire bucket for %s backend not implemented.\", backend.Name())\n\n\treturn nil\n}\n\n// GetTFInitArgs implements `backends.GetTFInitArgs` interface.\nfunc (backend *CommonBackend) GetTFInitArgs(config Config) map[string]any {\n\treturn config\n}\n\nfunc (backend *CommonBackend) GetBucketMutex(bucketName string) *sync.Mutex {\n\tmu, _ := backend.bucketLocks.LoadOrCompute(bucketName, func() *sync.Mutex {\n\t\treturn new(sync.Mutex)\n\t})\n\n\treturn mu\n}\n\nfunc (backend *CommonBackend) IsConfigInited(config interface{ CacheKey() string }) bool {\n\tstatus, ok := backend.initedConfigs.Load(config.CacheKey())\n\n\treturn ok && status\n}\n\nfunc (backend *CommonBackend) MarkConfigInited(config interface{ CacheKey() string }) {\n\tbackend.initedConfigs.Store(config.CacheKey(), true)\n}\n"
  },
  {
    "path": "internal/remotestate/backend/config.go",
    "content": "package backend\n\nimport (\n\t\"reflect\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst (\n\tconfigPathKey = \"path\"\n)\n\ntype Config map[string]any\n\n// Path returns the `patha field value.\nfunc (cfg Config) Path() string {\n\treturn getConfigValueByKey[string](cfg, configPathKey)\n}\n\n// IsEqual returns true if the given `targetCfg` config is in any way different than what is configured for the backend.\nfunc (cfg Config) IsEqual(targetCfg Config, backendName string, logger log.Logger) bool {\n\tif len(cfg) == 0 && len(targetCfg) == 0 {\n\t\treturn true\n\t}\n\n\ttargetCfgNonNil := cfg.CopyNotNullValues(targetCfg)\n\n\tif reflect.DeepEqual(targetCfgNonNil, map[string]any(cfg)) {\n\t\tlogger.Debugf(\"Backend %s has not changed.\", backendName)\n\n\t\treturn true\n\t}\n\n\tlogger.Debugf(\"Backend config %s has changed from %s to %s\", backendName, targetCfgNonNil, cfg)\n\n\treturn false\n}\n\n// CopyNotNullValues copies the non-nil values from the `targetCfg` whose keys also exist in the `cfg` to the new map.\nfunc (cfg Config) CopyNotNullValues(targetCfg map[string]any) map[string]any {\n\tif targetCfg == nil {\n\t\treturn nil\n\t}\n\n\ttargetCfgNonNil := map[string]any{}\n\n\tfor existingKey, existingValue := range targetCfg {\n\t\tnewValue, newValueIsSet := cfg[existingKey]\n\t\tif existingValue == nil && !newValueIsSet {\n\t\t\tcontinue\n\t\t}\n\t\t// if newValue and existingValue are both maps, we need to recursively copy the non-nil values\n\t\tif existingValueMap, existingValueIsMap := existingValue.(map[string]any); existingValueIsMap {\n\t\t\tif newValueMap, newValueIsMap := newValue.(map[string]any); newValueIsMap {\n\t\t\t\texistingValue = Config(newValueMap).CopyNotNullValues(existingValueMap)\n\t\t\t}\n\t\t}\n\n\t\ttargetCfgNonNil[existingKey] = existingValue\n\t}\n\n\treturn targetCfgNonNil\n}\n\nfunc getConfigValueByKey[T any](m map[string]any, key string) T {\n\tif val, ok := m[key]; ok {\n\t\tif val, ok := val.(T); ok {\n\t\t\treturn val\n\t\t}\n\t}\n\n\treturn *new(T)\n}\n"
  },
  {
    "path": "internal/remotestate/backend/config_test.go",
    "content": "package backend_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestConfig_IsEqual(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct { //nolint: govet\n\t\tname            string\n\t\texistingBackend backend.Config\n\t\tcfg             backend.Config\n\t\texpected        bool\n\t}{\n\t\t{\n\t\t\t\"both empty\",\n\t\t\tbackend.Config{},\n\t\t\tbackend.Config{},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"identical S3 configs\",\n\t\t\tbackend.Config{\"bucket\": \"foo\", \"key\": \"bar\", \"region\": \"us-east-1\"},\n\t\t\tbackend.Config{\"bucket\": \"foo\", \"key\": \"bar\", \"region\": \"us-east-1\"},\n\t\t\ttrue,\n\t\t}, {\n\t\t\t\"identical GCS configs\",\n\t\t\tbackend.Config{\"project\": \"foo-123456\", \"location\": \"europe-west3\", \"bucket\": \"foo\", \"prefix\": \"bar\"},\n\t\t\tbackend.Config{\"project\": \"foo-123456\", \"location\": \"europe-west3\", \"bucket\": \"foo\", \"prefix\": \"bar\"},\n\t\t\ttrue,\n\t\t}, {\n\t\t\t\"different s3 bucket values\",\n\t\t\tbackend.Config{\"bucket\": \"foo\", \"key\": \"bar\", \"region\": \"us-east-1\"},\n\t\t\tbackend.Config{\"bucket\": \"different\", \"key\": \"bar\", \"region\": \"us-east-1\"},\n\t\t\tfalse,\n\t\t}, {\n\t\t\t\"different gcs bucket values\",\n\t\t\tbackend.Config{\"project\": \"foo-123456\", \"location\": \"europe-west3\", \"bucket\": \"foo\", \"prefix\": \"bar\"},\n\t\t\tbackend.Config{\"project\": \"foo-123456\", \"location\": \"europe-west3\", \"bucket\": \"different\", \"prefix\": \"bar\"},\n\t\t\tfalse,\n\t\t}, {\n\t\t\t\"different s3 key values\",\n\t\t\tbackend.Config{\"bucket\": \"foo\", \"key\": \"bar\", \"region\": \"us-east-1\"},\n\t\t\tbackend.Config{\"bucket\": \"foo\", \"key\": \"different\", \"region\": \"us-east-1\"},\n\t\t\tfalse,\n\t\t}, {\n\t\t\t\"different gcs prefix values\",\n\t\t\tbackend.Config{\"project\": \"foo-123456\", \"location\": \"europe-west3\", \"bucket\": \"foo\", \"prefix\": \"bar\"},\n\t\t\tbackend.Config{\"project\": \"foo-123456\", \"location\": \"europe-west3\", \"bucket\": \"foo\", \"prefix\": \"different\"},\n\t\t\tfalse,\n\t\t}, {\n\t\t\t\"different s3 region values\",\n\t\t\tbackend.Config{\"bucket\": \"foo\", \"key\": \"bar\", \"region\": \"us-east-1\"},\n\t\t\tbackend.Config{\"bucket\": \"foo\", \"key\": \"bar\", \"region\": \"different\"},\n\t\t\tfalse,\n\t\t}, {\n\t\t\t\"different gcs location values\",\n\t\t\tbackend.Config{\"project\": \"foo-123456\", \"location\": \"europe-west3\", \"bucket\": \"foo\", \"prefix\": \"bar\"},\n\t\t\tbackend.Config{\"project\": \"foo-123456\", \"location\": \"different\", \"bucket\": \"foo\", \"prefix\": \"bar\"},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"different boolean values and boolean conversion\",\n\t\t\tbackend.Config{\"something\": \"true\"},\n\t\t\tbackend.Config{\"something\": false},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"different gcs boolean values and boolean conversion\",\n\t\t\tbackend.Config{\"something\": \"true\"},\n\t\t\tbackend.Config{\"something\": false},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"null values ignored\",\n\t\t\tbackend.Config{\"something\": \"foo\", \"set-to-nil-should-be-ignored\": nil},\n\t\t\tbackend.Config{\"something\": \"foo\"},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"gcs null values ignored\",\n\t\t\tbackend.Config{\"something\": \"foo\", \"set-to-nil-should-be-ignored\": nil},\n\t\t\tbackend.Config{\"something\": \"foo\"},\n\t\t\ttrue,\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.Parallel()\n\n\t\t\tactual := tc.cfg.IsEqual(tc.existingBackend, \"\", log.Default())\n\t\t\tassert.Equal(t, tc.expected, actual, \"Expect differsFrom to return %t but got %t for existingRemoteState %v and remoteStateFromTerragruntConfig %v\", tc.expected, actual, tc.existingBackend, tc.cfg)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/remotestate/backend/errors.go",
    "content": "package backend\n\nimport (\n\t\"fmt\"\n)\n\ntype BucketCreationNotAllowed string\n\nfunc (bucketName BucketCreationNotAllowed) Error() string {\n\treturn fmt.Sprintf(\"Creation of remote state bucket %s is not allowed\", string(bucketName))\n}\n\n// BucketDoesNotExistError is the error that is returned when the bucket does not exist.\ntype BucketDoesNotExistError struct {\n\tbucketName string\n}\n\n// NewBucketDoesNotExistError creates a new `BucketDoesNotExistError` instance.\nfunc NewBucketDoesNotExistError(bucketName string) *BucketDoesNotExistError {\n\treturn &BucketDoesNotExistError{bucketName: bucketName}\n}\n\n// Error implements `error` interface.\nfunc (err BucketDoesNotExistError) Error() string {\n\treturn fmt.Sprintf(\"S3 bucket %s does not exist\", err.bucketName)\n}\n"
  },
  {
    "path": "internal/remotestate/backend/gcs/backend.go",
    "content": "// Package gcs represents GCS backend for interacting with remote state.\npackage gcs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst (\n\tBackendName = \"gcs\"\n\n\tdefaultTfState = \"default.tfstate\"\n)\n\nvar _ backend.Backend = new(Backend)\n\ntype Backend struct {\n\t*backend.CommonBackend\n}\n\nfunc NewBackend() *Backend {\n\treturn &Backend{\n\t\tCommonBackend: backend.NewCommonBackend(BackendName),\n\t}\n}\n\n// NeedsBootstrap returns true if the GCS bucket specified in the given config does not exist or if the bucket\n// exists but versioning is not enabled.\n//\n// Returns true if:\n//\n// 1. Any of the existing backend settings are different than the current config\n// 2. The configured GCS bucket does not exist\nfunc (backend *Backend) NeedsBootstrap(ctx context.Context, l log.Logger, backendConfig backend.Config, opts *backend.Options) (bool, error) {\n\textGCSCfg, err := Config(backendConfig).ExtendedGCSConfig()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tvar (\n\t\tgcsCfg     = &extGCSCfg.RemoteStateConfigGCS\n\t\tbucketName = gcsCfg.Bucket\n\t)\n\n\tclient, err := NewClient(ctx, extGCSCfg, opts)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tdefer func() {\n\t\tif err := client.Close(); err != nil {\n\t\t\tl.Warnf(\"Error closing GCS client: %v\", err)\n\t\t}\n\t}()\n\n\tif !client.DoesGCSBucketExist(ctx, bucketName) {\n\t\treturn true, nil\n\t}\n\n\treturn false, nil\n}\n\n// Bootstrap the remote state GCS bucket specified in the given config. This function will validate the config\n// parameters, create the GCS bucket if it doesn't already exist, and check that versioning is enabled.\nfunc (backend *Backend) Bootstrap(ctx context.Context, l log.Logger, backendConfig backend.Config, opts *backend.Options) error {\n\textGCSCfg, err := Config(backendConfig).ExtendedGCSConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := NewClient(ctx, extGCSCfg, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer func() {\n\t\tif err := client.Close(); err != nil {\n\t\t\tl.Warnf(\"Error closing GCS client: %v\", err)\n\t\t}\n\t}()\n\n\tvar (\n\t\tgcsCfg     = &extGCSCfg.RemoteStateConfigGCS\n\t\tbucketName = gcsCfg.Bucket\n\t)\n\n\t// ensure that only one goroutine can initialize bucket\n\tmu := backend.GetBucketMutex(bucketName)\n\n\tmu.Lock()\n\tdefer mu.Unlock()\n\n\tif backend.IsConfigInited(gcsCfg) {\n\t\tl.Debugf(\"%s bucket %s has already been confirmed to be initialized, skipping initialization checks\", backend.Name(), bucketName)\n\n\t\treturn nil\n\t}\n\n\t// If bucket is specified and skip_bucket_creation is false then check if Bucket needs to be created\n\tif !extGCSCfg.SkipBucketCreation && bucketName != \"\" {\n\t\tif err := client.CreateGCSBucketIfNecessary(ctx, l, bucketName, opts); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t// If bucket is specified and skip_bucket_versioning is false then warn user if versioning is disabled on bucket\n\tif !extGCSCfg.SkipBucketVersioning && bucketName != \"\" {\n\t\tif _, err := client.CheckIfGCSVersioningEnabled(ctx, l, bucketName); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tbackend.MarkConfigInited(gcsCfg)\n\n\treturn nil\n}\n\n// IsVersionControlEnabled returns true if version control for gcs bucket is enabled.\nfunc (backend *Backend) IsVersionControlEnabled(ctx context.Context, l log.Logger, backendConfig backend.Config, opts *backend.Options) (bool, error) {\n\textGCSCfg, err := Config(backendConfig).ExtendedGCSConfig()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tvar bucketName = extGCSCfg.RemoteStateConfigGCS.Bucket\n\n\tclient, err := NewClient(ctx, extGCSCfg, opts)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treturn client.CheckIfGCSVersioningEnabled(ctx, l, bucketName)\n}\n\nfunc (backend *Backend) Migrate(ctx context.Context, l log.Logger, srcBackendConfig, dstBackendConfig backend.Config, opts *backend.Options) error {\n\tsrcExtGCSCfg, err := Config(srcBackendConfig).ExtendedGCSConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdstExtGCSCfg, err := Config(dstBackendConfig).ExtendedGCSConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar (\n\t\tsrcBucketName = srcExtGCSCfg.RemoteStateConfigGCS.Bucket\n\t\tsrcBucketKey  = path.Join(srcExtGCSCfg.RemoteStateConfigGCS.Prefix, defaultTfState)\n\n\t\tdstBucketName = dstExtGCSCfg.RemoteStateConfigGCS.Bucket\n\t\tdstBucketKey  = path.Join(dstExtGCSCfg.RemoteStateConfigGCS.Prefix, defaultTfState)\n\t)\n\n\tclient, err := NewClient(ctx, srcExtGCSCfg, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn client.MoveGCSObjectIfNecessary(ctx, l, srcBucketName, srcBucketKey, dstBucketName, dstBucketKey)\n}\n\n// Delete deletes the remote state specified in the given config.\nfunc (backend *Backend) Delete(ctx context.Context, l log.Logger, backendConfig backend.Config, opts *backend.Options) error {\n\textGCSCfg, err := Config(backendConfig).ExtendedGCSConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar (\n\t\tbucketName = extGCSCfg.RemoteStateConfigGCS.Bucket\n\t\tprefix     = extGCSCfg.RemoteStateConfigGCS.Prefix\n\t)\n\n\tclient, err := NewClient(ctx, extGCSCfg, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tprompt := fmt.Sprintf(\"GCS bucket %s objects with prefix %s will be deleted. Do you want to continue?\", bucketName, prefix)\n\tif yes, err := shell.PromptUserForYesNo(ctx, l, prompt, opts.NonInteractive, opts.Writers.ErrWriter); err != nil {\n\t\treturn err\n\t} else if yes {\n\t\treturn client.DeleteGCSObjectIfNecessary(ctx, l, bucketName, prefix)\n\t}\n\n\treturn nil\n}\n\n// DeleteBucket deletes the entire bucket specified in the given config.\nfunc (backend *Backend) DeleteBucket(ctx context.Context, l log.Logger, backendConfig backend.Config, opts *backend.Options) error {\n\textGCSCfg, err := Config(backendConfig).ExtendedGCSConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := NewClient(ctx, extGCSCfg, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar bucketName = extGCSCfg.RemoteStateConfigGCS.Bucket\n\n\tprompt := fmt.Sprintf(\"GCS bucket %s will be completely deleted. Do you want to continue?\", bucketName)\n\tif yes, err := shell.PromptUserForYesNo(ctx, l, prompt, opts.NonInteractive, opts.Writers.ErrWriter); err != nil {\n\t\treturn err\n\t} else if yes {\n\t\treturn client.DeleteGCSBucketIfNecessary(ctx, l, bucketName)\n\t}\n\n\treturn nil\n}\n\n// GetTFInitArgs returns the subset of the given config that should be passed to terraform init\n// when initializing the remote state.\nfunc (backend *Backend) GetTFInitArgs(config backend.Config) map[string]any {\n\treturn Config(config).GetTFInitArgs()\n}\n"
  },
  {
    "path": "internal/remotestate/backend/gcs/backend_test.go",
    "content": "package gcs_test\n\nimport (\n\t\"testing\"\n\n\tbackend \"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\tgcsbackend \"github.com/gruntwork-io/terragrunt/internal/remotestate/backend/gcs\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestBackend_GetTFInitArgs(t *testing.T) {\n\tt.Parallel()\n\n\tremoteBackend := gcsbackend.NewBackend()\n\n\ttestCases := []struct {\n\t\tconfig   backend.Config\n\t\texpected map[string]any\n\t\tname     string\n\t}{\n\t\t{\n\t\t\tname:     \"empty-no-values\",\n\t\t\tconfig:   backend.Config{},\n\t\t\texpected: map[string]any{},\n\t\t},\n\t\t{\n\t\t\tname: \"valid-gcs-configuration-keys\",\n\t\t\tconfig: backend.Config{\n\t\t\t\t\"bucket\":      \"my-bucket\",\n\t\t\t\t\"prefix\":      \"terraform/state\",\n\t\t\t\t\"credentials\": \"path/to/creds.json\",\n\t\t\t},\n\t\t\texpected: map[string]any{\n\t\t\t\t\"bucket\":      \"my-bucket\",\n\t\t\t\t\"prefix\":      \"terraform/state\",\n\t\t\t\t\"credentials\": \"path/to/creds.json\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"terragrunt-keys-filtered\",\n\t\t\tconfig: backend.Config{\n\t\t\t\t\"bucket\":                    \"my-bucket\",\n\t\t\t\t\"prefix\":                    \"terraform/state\",\n\t\t\t\t\"project\":                   \"my-project\",\n\t\t\t\t\"location\":                  \"us-central1\",\n\t\t\t\t\"gcs_bucket_labels\":         map[string]string{\"env\": \"prod\"},\n\t\t\t\t\"skip_bucket_versioning\":    true,\n\t\t\t\t\"skip_bucket_creation\":      true,\n\t\t\t\t\"enable_bucket_policy_only\": true,\n\t\t\t},\n\t\t\texpected: map[string]any{\n\t\t\t\t\"bucket\": \"my-bucket\",\n\t\t\t\t\"prefix\": \"terraform/state\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"empty-after-all-terragrunt-keys-filtered\",\n\t\t\tconfig: backend.Config{\n\t\t\t\t\"project\":                   \"my-project\",\n\t\t\t\t\"location\":                  \"us-central1\",\n\t\t\t\t\"gcs_bucket_labels\":         map[string]string{},\n\t\t\t\t\"skip_bucket_versioning\":    true,\n\t\t\t\t\"skip_bucket_creation\":      false,\n\t\t\t\t\"enable_bucket_policy_only\": false,\n\t\t\t},\n\t\t\texpected: map[string]any{},\n\t\t},\n\t\t{\n\t\t\tname: \"string-bool-normalization-passthrough\",\n\t\t\tconfig: backend.Config{\n\t\t\t\t\"bucket\": \"my-bucket\",\n\t\t\t\t\"prefix\": \"terraform/state\",\n\t\t\t},\n\t\t\texpected: map[string]any{\n\t\t\t\t\"bucket\": \"my-bucket\",\n\t\t\t\t\"prefix\": \"terraform/state\",\n\t\t\t},\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.Parallel()\n\n\t\t\tactual := remoteBackend.GetTFInitArgs(tc.config)\n\n\t\t\tassert.Equal(t, tc.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/remotestate/backend/gcs/client.go",
    "content": "package gcs\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path\"\n\t\"time\"\n\n\t\"cloud.google.com/go/storage\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/gcphelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"google.golang.org/api/iterator\"\n)\n\nconst (\n\tmaxRetriesWaitingForGcsBucket          = 12\n\tsleepBetweenRetriesWaitingForGcsBucket = 5 * time.Second\n\n\tgcpMaxRetries          = 3\n\tgcpSleepBetweenRetries = 10 * time.Second\n)\n\ntype Client struct {\n\t*ExtendedRemoteStateConfigGCS\n\t*storage.Client\n}\n\n// NewClient inits GCS client.\nfunc NewClient(\n\tctx context.Context,\n\tconfig *ExtendedRemoteStateConfigGCS,\n\topts *backend.Options,\n) (*Client, error) {\n\tgcsClient, err := gcphelper.NewGCPConfigBuilder().\n\t\tWithSessionConfig(config.GetGCPSessionConfig()).\n\t\tWithEnv(opts.Env).\n\t\tBuildGCSClient(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := &Client{\n\t\tExtendedRemoteStateConfigGCS: config,\n\t\tClient:                       gcsClient,\n\t}\n\n\treturn client, nil\n}\n\n// CreateGCSBucketIfNecessary prompts the user to create the given bucket if it doesn't already exist and if the user\n// confirms, creates the bucket and enables versioning for it.\nfunc (client *Client) CreateGCSBucketIfNecessary(ctx context.Context, l log.Logger, bucketName string, opts *backend.Options) error {\n\tif client.DoesGCSBucketExist(ctx, bucketName) {\n\t\treturn nil\n\t}\n\n\t// A project must be specified in order for terragrunt to automatically create a storage bucket.\n\tif client.Project == \"\" {\n\t\treturn errors.New(MissingRequiredGCSRemoteStateConfig(\"project\"))\n\t}\n\n\t// A location must be specified in order for terragrunt to automatically create a storage bucket.\n\tif client.Location == \"\" {\n\t\treturn errors.New(MissingRequiredGCSRemoteStateConfig(\"location\"))\n\t}\n\n\tl.Debugf(\"Remote state GCS bucket %s does not exist. Attempting to create it\", bucketName)\n\n\tif opts.FailIfBucketCreationRequired {\n\t\treturn backend.BucketCreationNotAllowed(bucketName)\n\t}\n\n\tprompt := fmt.Sprintf(\"Remote state GCS bucket %s does not exist or you don't have permissions to access it. Would you like Terragrunt to create it?\", bucketName)\n\n\tshouldCreateBucket, err := shell.PromptUserForYesNo(ctx, l, prompt, opts.NonInteractive, opts.Writers.ErrWriter)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif shouldCreateBucket {\n\t\t// To avoid any eventual consistency issues with creating a GCS bucket we use a retry loop.\n\t\tdescription := \"Create GCS bucket \" + bucketName\n\n\t\treturn util.DoWithRetry(ctx, description, gcpMaxRetries, gcpSleepBetweenRetries, l, log.DebugLevel, func(ctx context.Context) error {\n\t\t\treturn client.CreateGCSBucketWithVersioning(ctx, l, bucketName)\n\t\t})\n\t}\n\n\treturn nil\n}\n\n// CheckIfGCSVersioningEnabled checks if versioning is enabled for the GCS bucket specified in the given config and warn the user if it is not\nfunc (client *Client) CheckIfGCSVersioningEnabled(ctx context.Context, l log.Logger, bucketName string) (bool, error) {\n\tbucket := client.Bucket(bucketName)\n\n\tif !client.DoesGCSBucketExist(ctx, bucketName) {\n\t\treturn false, backend.NewBucketDoesNotExistError(bucketName)\n\t}\n\n\tattrs, err := bucket.Attrs(ctx)\n\tif err != nil {\n\t\t// ErrBucketNotExist\n\t\treturn false, errors.New(err)\n\t}\n\n\tif !attrs.VersioningEnabled {\n\t\tl.Warnf(\"Versioning is not enabled for the remote state GCS bucket %s. We recommend enabling versioning so that you can roll back to previous versions of your OpenTofu/Terraform state in case of error.\", bucketName)\n\t}\n\n\treturn attrs.VersioningEnabled, nil\n}\n\n// CreateGCSBucketWithVersioning creates the given GCS bucket and enables versioning for it.\nfunc (client *Client) CreateGCSBucketWithVersioning(ctx context.Context, l log.Logger, bucketName string) error {\n\tif err := client.CreateGCSBucket(ctx, l, bucketName); err != nil {\n\t\treturn err\n\t}\n\n\tif err := client.WaitUntilGCSBucketExists(ctx, l, bucketName); err != nil {\n\t\treturn err\n\t}\n\n\tif err := client.AddLabelsToGCSBucket(ctx, l, bucketName, client.GCSBucketLabels); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// AddLabelsToGCSBucket adds the given labels to the GCS bucket.\nfunc (client *Client) AddLabelsToGCSBucket(ctx context.Context, l log.Logger, bucketName string, labels map[string]string) error {\n\tif len(labels) == 0 {\n\t\tl.Debugf(\"No labels specified for bucket %s.\", bucketName)\n\t\treturn nil\n\t}\n\n\tl.Debugf(\"Adding labels to GCS bucket with %s\", labels)\n\n\tbucket := client.Bucket(bucketName)\n\n\tbucketAttrs := storage.BucketAttrsToUpdate{}\n\n\tfor key, value := range labels {\n\t\tbucketAttrs.SetLabel(key, value)\n\t}\n\n\t_, err := bucket.Update(ctx, bucketAttrs)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n\n// CreateGCSBucket creates the GCS bucket specified in the given config.\nfunc (client *Client) CreateGCSBucket(ctx context.Context, l log.Logger, bucketName string) error {\n\tl.Debugf(\"Creating GCS bucket %s in project %s\", bucketName, client.Project)\n\n\t// The project ID to which the bucket belongs. This is only used when creating a new bucket during initialization.\n\t// Since buckets have globally unique names, the project ID is not required to access the bucket during normal\n\t// operation.\n\tprojectID := client.Project\n\n\tbucket := client.Bucket(bucketName)\n\n\tbucketAttrs := &storage.BucketAttrs{}\n\n\tif client.Location != \"\" {\n\t\tl.Debugf(\"Creating GCS bucket in location %s.\", client.Location)\n\t\tbucketAttrs.Location = client.Location\n\t}\n\n\tif client.SkipBucketVersioning {\n\t\tl.Debugf(\"Versioning is disabled for the remote state GCS bucket %s using 'skip_bucket_versioning' config.\", bucketName)\n\t} else {\n\t\tl.Debugf(\"Enabling versioning on GCS bucket %s\", bucketName)\n\n\t\tbucketAttrs.VersioningEnabled = true\n\t}\n\n\tif client.EnableBucketPolicyOnly {\n\t\tl.Debugf(\"Enabling uniform bucket-level access on GCS bucket %s\", bucketName)\n\n\t\tbucketAttrs.BucketPolicyOnly = storage.BucketPolicyOnly{Enabled: true}\n\t}\n\n\tif err := bucket.Create(ctx, projectID, bucketAttrs); err != nil {\n\t\treturn errors.Errorf(\"error creating GCS bucket %s: %w\", bucketName, err)\n\t}\n\n\treturn nil\n}\n\n// WaitUntilGCSBucketExists waits for the GCS bucket specified in the given config to be created.\n//\n// GCP is eventually consistent, so after creating a GCS bucket, this method can be used to wait until the information\n// about that GCS bucket has propagated everywhere.\nfunc (client *Client) WaitUntilGCSBucketExists(ctx context.Context, l log.Logger, bucketName string) error {\n\tl.Debugf(\"Waiting for bucket %s to be created\", bucketName)\n\n\tfor retries := range maxRetriesWaitingForGcsBucket {\n\t\tif client.DoesGCSBucketExist(ctx, bucketName) {\n\t\t\tl.Debugf(\"GCS bucket %s created.\", bucketName)\n\t\t\treturn nil\n\t\t}\n\n\t\tif retries < maxRetriesWaitingForGcsBucket-1 {\n\t\t\tl.Debugf(\"GCS bucket %s has not been created yet. Sleeping for %s and will check again.\", bucketName, sleepBetweenRetriesWaitingForGcsBucket)\n\t\t\ttime.Sleep(sleepBetweenRetriesWaitingForGcsBucket)\n\t\t}\n\t}\n\n\treturn errors.New(MaxRetriesWaitingForGCSBucketExceeded(bucketName))\n}\n\n// DoesGCSBucketExist returns true if the GCS bucket specified in the given config exists and the current user has the\n// ability to access it.\nfunc (client *Client) DoesGCSBucketExist(ctx context.Context, bucketName string) bool {\n\tbucketHandle := client.Bucket(bucketName)\n\n\t// TODO - the code below attempts to determine whether the storage bucket exists by making a making a number of API\n\t// calls, then attempting to list the contents of the bucket. It was adapted from Google's own integration tests and\n\t// should be improved once the appropriate API call is added. For more info see:\n\t// https://github.com/GoogleCloudPlatform/google-cloud-go/blob/de879f7be552d57556875b8aaa383bce9396cc8c/storage/integration_test.go#L1231\n\tif _, err := bucketHandle.Attrs(ctx); err != nil {\n\t\t// ErrBucketNotExist\n\t\treturn false\n\t}\n\n\tit := bucketHandle.Objects(ctx, nil)\n\tif _, err := it.Next(); errors.Is(err, storage.ErrBucketNotExist) {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// DeleteGCSBucketIfNecessary deletes the given GCS bucket with all its objects if it exists.\nfunc (client *Client) DeleteGCSBucketIfNecessary(ctx context.Context, l log.Logger, bucketName string) error {\n\tif !client.DoesGCSBucketExist(ctx, bucketName) {\n\t\treturn nil\n\t}\n\n\tdescription := fmt.Sprintf(\"Delete GCS bucket %s with retry\", bucketName)\n\n\treturn util.DoWithRetry(ctx, description, gcpMaxRetries, gcpSleepBetweenRetries, l, log.DebugLevel, func(ctx context.Context) error {\n\t\tif err := client.DeleteGCSObjects(ctx, l, bucketName, \"\", true); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn client.DeleteGCSBucket(ctx, l, bucketName)\n\t})\n}\n\nfunc (client *Client) DeleteGCSBucket(ctx context.Context, l log.Logger, bucketName string) error {\n\tbucket := client.Bucket(bucketName)\n\n\tl.Debugf(\"Deleting GCS bucket %s\", bucketName)\n\n\tif err := bucket.Delete(ctx); err != nil {\n\t\treturn errors.Errorf(\"error deleting GCS bucket %s: %w\", bucketName, err)\n\t}\n\n\tl.Debugf(\"Deleted GCS bucket %s\", bucketName)\n\n\treturn client.WaitUntilGCSBucketDeleted(ctx, l, bucketName)\n}\n\n// WaitUntilGCSBucketDeleted waits for the GCS bucket specified in the given config to be deleted.\nfunc (client *Client) WaitUntilGCSBucketDeleted(ctx context.Context, l log.Logger, bucketName string) error {\n\tl.Debugf(\"Waiting for bucket %s to be deleted\", bucketName)\n\n\tfor retries := range maxRetriesWaitingForGcsBucket {\n\t\tif !client.DoesGCSBucketExist(ctx, bucketName) {\n\t\t\tl.Debugf(\"GCS bucket %s deleted.\", bucketName)\n\t\t\treturn nil\n\t\t} else if retries < maxRetriesWaitingForGcsBucket-1 {\n\t\t\tl.Debugf(\"GCS bucket %s has not been deleted yet. Sleeping for %s and will check again.\", bucketName, sleepBetweenRetriesWaitingForGcsBucket)\n\t\t\ttime.Sleep(sleepBetweenRetriesWaitingForGcsBucket)\n\t\t}\n\t}\n\n\treturn errors.New(MaxRetriesWaitingForGCSBucketExceeded(bucketName))\n}\n\n// DeleteGCSObjectIfNecessary deletes the bucket objects with the given prefix if they exist.\nfunc (client *Client) DeleteGCSObjectIfNecessary(ctx context.Context, l log.Logger, bucketName, prefix string) error {\n\tif !client.DoesGCSBucketExist(ctx, bucketName) {\n\t\treturn nil\n\t}\n\n\tdescription := fmt.Sprintf(\"Delete GCS objects with prefix %s in bucket %s with retry\", prefix, bucketName)\n\n\treturn util.DoWithRetry(ctx, description, gcpMaxRetries, gcpSleepBetweenRetries, l, log.DebugLevel, func(ctx context.Context) error {\n\t\treturn client.DeleteGCSObjects(ctx, l, bucketName, prefix, false)\n\t})\n}\n\n// DeleteGCSObjects deletes the bucket objects with the given prefix.\nfunc (client *Client) DeleteGCSObjects(ctx context.Context, l log.Logger, bucketName, prefix string, withVersions bool) error {\n\tbucket := client.Bucket(bucketName)\n\n\tit := bucket.Objects(ctx, &storage.Query{\n\t\tPrefix:   prefix,\n\t\tVersions: withVersions,\n\t})\n\n\tfor {\n\t\tattrs, err := it.Next()\n\t\tif err != nil {\n\t\t\tif errors.Is(err, iterator.Done) {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\treturn errors.Errorf(\"failed to get GCS object attrs: %w\", err)\n\t\t}\n\n\t\tl.Debugf(\"Deleting GCS object %s with generation %d in bucket %s\", attrs.Name, attrs.Generation, bucketName)\n\n\t\tif err := bucket.Object(attrs.Name).Generation(attrs.Generation).Delete(ctx); err != nil {\n\t\t\treturn errors.Errorf(\"failed to delete object %s with generation %d in bucket %s: %w\", attrs.Name, attrs.Generation, bucketName, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// MoveGCSObjectIfNecessary moves the GCS object at the specified srcBucketName and srcKey to dstBucketName and dstKey.\nfunc (client *Client) MoveGCSObjectIfNecessary(ctx context.Context, l log.Logger, srcBucketName, srcKey, dstBucketName, dstKey string) error {\n\tif exists, err := client.DoesGCSObjectExistWithLogging(ctx, l, srcBucketName, srcKey); err != nil || !exists {\n\t\treturn err\n\t}\n\n\tif exists, err := client.DoesGCSObjectExist(ctx, dstBucketName, dstKey); err != nil {\n\t\treturn err\n\t} else if exists {\n\t\treturn errors.Errorf(\"destination GCS bucket %s object %s already exists\", dstBucketName, dstKey)\n\t}\n\n\tdescription := fmt.Sprintf(\"Move GCS bucket object from %s to %s\", path.Join(srcBucketName, srcKey), path.Join(dstBucketName, dstKey))\n\n\treturn util.DoWithRetry(ctx, description, gcpMaxRetries, gcpSleepBetweenRetries, l, log.DebugLevel, func(ctx context.Context) error {\n\t\treturn client.MoveGCSObject(ctx, l, srcBucketName, srcKey, dstBucketName, dstKey)\n\t})\n}\n\n// DoesGCSObjectExist returns true if the specified GCS object exists otherwise false.\nfunc (client *Client) DoesGCSObjectExist(ctx context.Context, bucketName, key string) (bool, error) {\n\tbucket := client.Bucket(bucketName)\n\n\tobj := bucket.Object(key)\n\n\tif _, err := obj.Attrs(ctx); err != nil {\n\t\tif errors.Is(err, storage.ErrObjectNotExist) {\n\t\t\treturn false, nil\n\t\t}\n\n\t\treturn false, err\n\t}\n\n\treturn true, nil\n}\n\nfunc (client *Client) DoesGCSObjectExistWithLogging(ctx context.Context, l log.Logger, bucketName, key string) (bool, error) {\n\tif exists, err := client.DoesGCSObjectExist(ctx, bucketName, key); err != nil || exists {\n\t\treturn exists, err\n\t}\n\n\tl.Debugf(\"Remote state GCS bucket %s object %s does not exist or you don't have permissions to access it.\", bucketName, key)\n\n\treturn false, nil\n}\n\n// MoveGCSObject copies the GCS object at the specified srcKey to dstKey and then removes srcKey.\nfunc (client *Client) MoveGCSObject(ctx context.Context, l log.Logger, srcBucketName, srcKey, dstBucketName, dstKey string) error {\n\tif err := client.CopyGCSBucketObject(ctx, l, srcBucketName, srcKey, dstBucketName, dstKey); err != nil {\n\t\treturn err\n\t}\n\n\treturn client.DeleteGCSObjects(ctx, l, srcBucketName, srcKey, false)\n}\n\n// CopyGCSBucketObject copies the GCS object at the specified srcKey to dstKey.\nfunc (client *Client) CopyGCSBucketObject(ctx context.Context, l log.Logger, srcBucketName, srcKey, dstBucketName, dstKey string) error {\n\tl.Debugf(\"Copying GCS bucket object from %s to %s\", path.Join(srcBucketName, srcKey), path.Join(dstBucketName, dstKey))\n\n\tsrc := client.Bucket(srcBucketName).Object(srcKey)\n\tdst := client.Bucket(dstBucketName).Object(dstKey)\n\n\tif _, err := dst.CopierFrom(src).Run(ctx); err != nil {\n\t\treturn errors.Errorf(\"failed to copy object: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/remotestate/backend/gcs/config.go",
    "content": "package gcs\n\nimport (\n\t\"maps\"\n\t\"reflect\"\n\t\"slices\"\n\t\"strconv\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/mitchellh/mapstructure\"\n)\n\ntype Config map[string]any\n\n// GetTFInitArgs returns the config filtered and normalized for terraform init.\nfunc (cfg Config) GetTFInitArgs() Config {\n\tfiltered := cfg.FilterOutTerragruntKeys()\n\n\treturn Config(backend.NormalizeBoolValues(backend.Config(filtered), &ExtendedRemoteStateConfigGCS{}))\n}\n\nfunc (cfg Config) FilterOutTerragruntKeys() Config {\n\tvar filtered = make(Config)\n\n\tfor key, val := range cfg {\n\t\tif slices.Contains(terragruntOnlyConfigs, key) {\n\t\t\tcontinue\n\t\t}\n\n\t\tfiltered[key] = val\n\t}\n\n\treturn filtered\n}\n\nfunc (cfg Config) IsEqual(targetCfg Config, logger log.Logger) bool {\n\t// If other keys in config are bools, DeepEqual also will consider the maps to be different.\n\tfor key, value := range targetCfg {\n\t\tif util.KindOf(targetCfg[key]) == reflect.String && util.KindOf(cfg[key]) == reflect.Bool {\n\t\t\tif convertedValue, err := strconv.ParseBool(value.(string)); err == nil {\n\t\t\t\ttargetCfg[key] = convertedValue\n\t\t\t}\n\t\t}\n\t}\n\n\t// Construct a new map excluding custom GCS labels that are only used in Terragrunt config and not in Terraform's backend\n\tnewConfig := backend.Config{}\n\n\tmaps.Copy(newConfig, cfg.FilterOutTerragruntKeys())\n\n\treturn newConfig.IsEqual(backend.Config(targetCfg), BackendName, logger)\n}\n\n// ParseExtendedGCSConfig parses the given map into a GCS config.\nfunc (cfg Config) ParseExtendedGCSConfig() (*ExtendedRemoteStateConfigGCS, error) {\n\tvar (\n\t\tgcsConfig      RemoteStateConfigGCS\n\t\textendedConfig ExtendedRemoteStateConfigGCS\n\t)\n\n\tif err := mapstructure.WeakDecode(cfg, &gcsConfig); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tif err := mapstructure.WeakDecode(cfg, &extendedConfig); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\textendedConfig.RemoteStateConfigGCS = gcsConfig\n\n\treturn &extendedConfig, nil\n}\n\n// ExtendedGCSConfig parses the given map into an extended GCS config and validates this config.\nfunc (cfg Config) ExtendedGCSConfig() (*ExtendedRemoteStateConfigGCS, error) {\n\textGCSCfg, err := cfg.ParseExtendedGCSConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn extGCSCfg, extGCSCfg.Validate()\n}\n"
  },
  {
    "path": "internal/remotestate/backend/gcs/config_test.go",
    "content": "package gcs_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend/gcs\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestConfig_IsEqual(t *testing.T) {\n\tt.Parallel()\n\n\tlogger := logger.CreateLogger()\n\n\ttestCases := []struct { //nolint: govet\n\t\tname          string\n\t\tcfg           gcs.Config\n\t\tcomparableCfg gcs.Config\n\t\tshouldBeEqual bool\n\t}{\n\t\t{\n\t\t\t\"equal-both-empty\",\n\t\t\tgcs.Config{},\n\t\t\tgcs.Config{},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"equal-empty-and-nil\",\n\t\t\tgcs.Config{},\n\t\t\tnil,\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"equal-one-key\",\n\t\t\tgcs.Config{\"foo\": \"bar\"},\n\t\t\tgcs.Config{\"foo\": \"bar\"},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"equal-multiple-keys\",\n\t\t\tgcs.Config{\"foo\": \"bar\", \"baz\": []string{\"a\", \"b\", \"c\"}, \"blah\": 123, \"bool\": true},\n\t\t\tgcs.Config{\"foo\": \"bar\", \"baz\": []string{\"a\", \"b\", \"c\"}, \"blah\": 123, \"bool\": true},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"equal-encrypt-bool-handling\",\n\t\t\tgcs.Config{\"encrypt\": true},\n\t\t\tgcs.Config{\"encrypt\": \"true\"},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"equal-general-bool-handling\",\n\t\t\tgcs.Config{\"something\": true, \"encrypt\": true},\n\t\t\tgcs.Config{\"something\": \"true\", \"encrypt\": \"true\"},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"equal-ignore-gcs-labels\",\n\t\t\tgcs.Config{\"foo\": \"bar\", \"gcs_bucket_labels\": []map[string]string{{\"foo\": \"bar\"}}},\n\t\t\tgcs.Config{\"foo\": \"bar\"},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"unequal-values\",\n\t\t\tgcs.Config{\"foo\": \"bar\"},\n\t\t\tgcs.Config{\"foo\": \"different\"},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"unequal-non-empty-cfg-nil\",\n\t\t\tgcs.Config{\"foo\": \"bar\"},\n\t\t\tnil,\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"unequal-general-bool-handling\",\n\t\t\tgcs.Config{\"something\": true},\n\t\t\tgcs.Config{\"something\": \"false\"},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"equal-null-ignored\",\n\t\t\tgcs.Config{\"something\": \"foo\"},\n\t\t\tgcs.Config{\"something\": \"foo\", \"ignored-because-null\": nil},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"terragrunt-only-configs-remain-intact\",\n\t\t\tgcs.Config{\"something\": \"foo\", \"skip_bucket_creation\": true},\n\t\t\tgcs.Config{\"something\": \"foo\"},\n\t\t\ttrue,\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.Parallel()\n\n\t\t\tactual := tc.cfg.IsEqual(tc.comparableCfg, logger)\n\t\t\tassert.Equal(t, tc.shouldBeEqual, actual)\n\t\t})\n\t}\n}\n\n// TestParseExtendedGCSConfig_StringBoolCoercion verifies that boolean config values\n// passed as strings (e.g. from HCL ternary type unification) are correctly parsed.\n// See https://github.com/gruntwork-io/terragrunt/issues/5475\nfunc TestParseExtendedGCSConfig_StringBoolCoercion(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct { //nolint: govet\n\t\tname   string\n\t\tconfig gcs.Config\n\t\tcheck  func(t *testing.T, cfg *gcs.ExtendedRemoteStateConfigGCS)\n\t}{\n\t\t{\n\t\t\t\"skip-bucket-versioning-string-true\",\n\t\t\tgcs.Config{\n\t\t\t\t\"bucket\":                 \"my-bucket\",\n\t\t\t\t\"skip_bucket_versioning\": \"true\",\n\t\t\t},\n\t\t\tfunc(t *testing.T, cfg *gcs.ExtendedRemoteStateConfigGCS) {\n\t\t\t\tt.Helper()\n\t\t\t\tassert.True(t, cfg.SkipBucketVersioning)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"skip-bucket-versioning-string-false\",\n\t\t\tgcs.Config{\n\t\t\t\t\"bucket\":                 \"my-bucket\",\n\t\t\t\t\"skip_bucket_versioning\": \"false\",\n\t\t\t},\n\t\t\tfunc(t *testing.T, cfg *gcs.ExtendedRemoteStateConfigGCS) {\n\t\t\t\tt.Helper()\n\t\t\t\tassert.False(t, cfg.SkipBucketVersioning)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"skip-bucket-creation-string-true\",\n\t\t\tgcs.Config{\n\t\t\t\t\"bucket\":               \"my-bucket\",\n\t\t\t\t\"skip_bucket_creation\": \"true\",\n\t\t\t},\n\t\t\tfunc(t *testing.T, cfg *gcs.ExtendedRemoteStateConfigGCS) {\n\t\t\t\tt.Helper()\n\t\t\t\tassert.True(t, cfg.SkipBucketCreation)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"enable-bucket-policy-only-string-true\",\n\t\t\tgcs.Config{\n\t\t\t\t\"bucket\":                    \"my-bucket\",\n\t\t\t\t\"enable_bucket_policy_only\": \"true\",\n\t\t\t},\n\t\t\tfunc(t *testing.T, cfg *gcs.ExtendedRemoteStateConfigGCS) {\n\t\t\t\tt.Helper()\n\t\t\t\tassert.True(t, cfg.EnableBucketPolicyOnly)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"native-bool-still-works\",\n\t\t\tgcs.Config{\n\t\t\t\t\"bucket\":                 \"my-bucket\",\n\t\t\t\t\"skip_bucket_versioning\": true,\n\t\t\t},\n\t\t\tfunc(t *testing.T, cfg *gcs.ExtendedRemoteStateConfigGCS) {\n\t\t\t\tt.Helper()\n\t\t\t\tassert.True(t, cfg.SkipBucketVersioning)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"empty-string-coerces-to-false\",\n\t\t\tgcs.Config{\n\t\t\t\t\"bucket\":                 \"my-bucket\",\n\t\t\t\t\"skip_bucket_versioning\": \"\",\n\t\t\t},\n\t\t\tfunc(t *testing.T, cfg *gcs.ExtendedRemoteStateConfigGCS) {\n\t\t\t\tt.Helper()\n\t\t\t\tassert.False(t, cfg.SkipBucketVersioning)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"numeric-one-coerces-to-true\",\n\t\t\tgcs.Config{\n\t\t\t\t\"bucket\":                 \"my-bucket\",\n\t\t\t\t\"skip_bucket_versioning\": \"1\",\n\t\t\t},\n\t\t\tfunc(t *testing.T, cfg *gcs.ExtendedRemoteStateConfigGCS) {\n\t\t\t\tt.Helper()\n\t\t\t\tassert.True(t, cfg.SkipBucketVersioning)\n\t\t\t},\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.Parallel()\n\n\t\t\textGCSCfg, err := tc.config.ParseExtendedGCSConfig()\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttc.check(t, extGCSCfg)\n\t\t})\n\t}\n}\n\n// TestParseExtendedGCSConfig_InvalidStringBool verifies invalid string values\n// for bool fields are rejected (e.g. \"maybe\" is not a valid bool).\nfunc TestParseExtendedGCSConfig_InvalidStringBool(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := gcs.Config{\n\t\t\"bucket\":                 \"my-bucket\",\n\t\t\"skip_bucket_versioning\": \"maybe\",\n\t}\n\n\t_, err := cfg.ParseExtendedGCSConfig()\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "internal/remotestate/backend/gcs/errors.go",
    "content": "package gcs\n\nimport \"fmt\"\n\ntype MissingRequiredGCSRemoteStateConfig string\n\nfunc (configName MissingRequiredGCSRemoteStateConfig) Error() string {\n\treturn \"Missing required GCS remote state configuration \" + string(configName)\n}\n\ntype MaxRetriesWaitingForGCSBucketExceeded string\n\nfunc (err MaxRetriesWaitingForGCSBucketExceeded) Error() string {\n\treturn fmt.Sprintf(\"Exceeded max retries (%d) waiting for bucket GCS bucket %s\", maxRetriesWaitingForGcsBucket, string(err))\n}\n"
  },
  {
    "path": "internal/remotestate/backend/gcs/remote_state_config.go",
    "content": "package gcs\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/gcphelper\"\n)\n\n// These are settings that can appear in the remote_state config that are ONLY used by Terragrunt and NOT forwarded\n// to the underlying Terraform backend configuration.\nvar terragruntOnlyConfigs = []string{\n\t\"project\",\n\t\"location\",\n\t\"gcs_bucket_labels\",\n\t\"skip_bucket_versioning\",\n\t\"skip_bucket_creation\",\n\t\"enable_bucket_policy_only\",\n}\n\n/* ExtendedRemoteStateConfigGCS is a struct that contains the GCS specific configuration options.\n *\n * We use this construct to separate the config key 'gcs_bucket_labels' from the others, as they\n * are specific to the gcs backend, but only used by terragrunt to tag the gcs bucket in case it\n * has to create them.\n */\ntype ExtendedRemoteStateConfigGCS struct {\n\tGCSBucketLabels        map[string]string    `mapstructure:\"gcs_bucket_labels\"`\n\tProject                string               `mapstructure:\"project\"`\n\tLocation               string               `mapstructure:\"location\"`\n\tRemoteStateConfigGCS   RemoteStateConfigGCS `mapstructure:\",squash\"`\n\tSkipBucketVersioning   bool                 `mapstructure:\"skip_bucket_versioning\"`\n\tSkipBucketCreation     bool                 `mapstructure:\"skip_bucket_creation\"`\n\tEnableBucketPolicyOnly bool                 `mapstructure:\"enable_bucket_policy_only\"`\n}\n\n// Validate validates the configuration for GCS remote state.\nfunc (cfg *ExtendedRemoteStateConfigGCS) Validate() error {\n\tvar bucketName = cfg.RemoteStateConfigGCS.Bucket\n\n\t// Bucket is always a required configuration parameter when not skipping bucket creation\n\t// so we check it here to make sure we have handle to the bucket\n\t// before we start validating the rest of the configuration.\n\tif bucketName == \"\" {\n\t\treturn errors.New(MissingRequiredGCSRemoteStateConfig(\"bucket\"))\n\t}\n\n\treturn nil\n}\n\n// RemoteStateConfigGCS is a representation of the configuration\n// options available for GCS remote state.\ntype RemoteStateConfigGCS struct {\n\tBucket        string `mapstructure:\"bucket\"`\n\tCredentials   string `mapstructure:\"credentials\"`\n\tAccessToken   string `mapstructure:\"access_token\"`\n\tPrefix        string `mapstructure:\"prefix\"`\n\tPath          string `mapstructure:\"path\"`\n\tEncryptionKey string `mapstructure:\"encryption_key\"`\n\n\tImpersonateServiceAccount          string   `mapstructure:\"impersonate_service_account\"`\n\tImpersonateServiceAccountDelegates []string `mapstructure:\"impersonate_service_account_delegates\"`\n}\n\n// CacheKey returns a unique key for the given GCS config that can be used to cache the initialization.\nfunc (cfg *RemoteStateConfigGCS) CacheKey() string {\n\treturn cfg.Bucket\n}\n\n// GetGCPSessionConfig returns a GcpSessionConfig from the ExtendedRemoteStateConfigGCS configuration.\nfunc (cfg *ExtendedRemoteStateConfigGCS) GetGCPSessionConfig() *gcphelper.GCPSessionConfig {\n\treturn &gcphelper.GCPSessionConfig{\n\t\tCredentials:                        cfg.RemoteStateConfigGCS.Credentials,\n\t\tAccessToken:                        cfg.RemoteStateConfigGCS.AccessToken,\n\t\tImpersonateServiceAccount:          cfg.RemoteStateConfigGCS.ImpersonateServiceAccount,\n\t\tImpersonateServiceAccountDelegates: cfg.RemoteStateConfigGCS.ImpersonateServiceAccountDelegates,\n\t}\n}\n"
  },
  {
    "path": "internal/remotestate/backend/normalize.go",
    "content": "package backend\n\nimport (\n\t\"maps\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// NormalizeBoolValues converts string boolean values (\"true\"/\"false\") in the\n// config map back to native Go bools. HCL ternary type unification can convert\n// bools to strings, which causes generated backend blocks to contain quoted\n// literals that Terraform/OpenTofu rejects.\n//\n// The target parameter should be a pointer to the config struct (e.g.\n// &ExtendedRemoteStateConfigS3{}); its mapstructure tags determine which\n// keys are boolean fields.\nfunc NormalizeBoolValues(m Config, target any) Config {\n\tboolKeys := collectBoolKeys(reflect.TypeOf(target))\n\n\tif len(boolKeys) == 0 {\n\t\treturn m\n\t}\n\n\tnormalized := make(Config)\n\tmaps.Copy(normalized, m)\n\n\tfor key, val := range normalized {\n\t\tstrVal, ok := val.(string)\n\t\tif _, isBool := boolKeys[key]; !ok || !isBool {\n\t\t\tcontinue\n\t\t}\n\n\t\tif boolVal, err := strconv.ParseBool(strVal); err == nil {\n\t\t\tnormalized[key] = boolVal\n\t\t}\n\t}\n\n\treturn normalized\n}\n\n// collectBoolKeys walks a struct type via reflection, reading mapstructure tags\n// to build a set of config key names that map to bool fields.\nfunc collectBoolKeys(t reflect.Type) map[string]struct{} {\n\tif t == nil {\n\t\treturn nil\n\t}\n\n\tfor t.Kind() == reflect.Ptr {\n\t\tt = t.Elem()\n\t}\n\n\tif t.Kind() != reflect.Struct {\n\t\treturn nil\n\t}\n\n\tkeys := make(map[string]struct{})\n\n\tfor i := range t.NumField() {\n\t\tfield := t.Field(i)\n\t\ttag := field.Tag.Get(\"mapstructure\")\n\n\t\t// Handle squashed embedded structs\n\t\tif tag == \",squash\" || (tag == \"\" && field.Anonymous) {\n\t\t\tmaps.Copy(keys, collectBoolKeys(field.Type))\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif key, ok := collectFieldBoolKey(&field, tag); ok {\n\t\t\tkeys[key] = struct{}{}\n\t\t}\n\t}\n\n\treturn keys\n}\n\n// collectFieldBoolKey returns the config key name for a bool field,\n// or empty string and false if the field is not a bool.\nfunc collectFieldBoolKey(field *reflect.StructField, tag string) (string, bool) {\n\tif tag == \"\" || tag == \"-\" {\n\t\treturn \"\", false\n\t}\n\n\tkey, _, _ := strings.Cut(tag, \",\")\n\tif key == \"\" {\n\t\treturn \"\", false\n\t}\n\n\tfieldType := field.Type\n\tif fieldType.Kind() == reflect.Ptr {\n\t\tfieldType = fieldType.Elem()\n\t}\n\n\treturn key, fieldType.Kind() == reflect.Bool\n}\n"
  },
  {
    "path": "internal/remotestate/backend/normalize_test.go",
    "content": "package backend_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype testConfig struct {\n\tName    string `mapstructure:\"name\"`\n\tEncrypt bool   `mapstructure:\"encrypt\"`\n\tEnabled bool   `mapstructure:\"enabled\"`\n}\n\ntype testConfigWithSquash struct {\n\tRegion  string     `mapstructure:\"region\"`\n\tInner   testConfig `mapstructure:\",squash\"`\n\tVerbose bool       `mapstructure:\"verbose\"`\n}\n\nfunc TestNormalizeBoolValues_StringToTrue(t *testing.T) {\n\tt.Parallel()\n\n\tm := backend.Config{\"encrypt\": \"true\", \"name\": \"test\"}\n\tresult := backend.NormalizeBoolValues(m, &testConfig{})\n\n\tassert.Equal(t, true, result[\"encrypt\"])\n\tassert.Equal(t, \"test\", result[\"name\"])\n}\n\nfunc TestNormalizeBoolValues_StringToFalse(t *testing.T) {\n\tt.Parallel()\n\n\tm := backend.Config{\"encrypt\": \"false\"}\n\tresult := backend.NormalizeBoolValues(m, &testConfig{})\n\n\tassert.Equal(t, false, result[\"encrypt\"])\n}\n\nfunc TestNormalizeBoolValues_NativeBoolUnchanged(t *testing.T) {\n\tt.Parallel()\n\n\tm := backend.Config{\"encrypt\": true, \"enabled\": false}\n\tresult := backend.NormalizeBoolValues(m, &testConfig{})\n\n\tassert.Equal(t, true, result[\"encrypt\"])\n\tassert.Equal(t, false, result[\"enabled\"])\n}\n\nfunc TestNormalizeBoolValues_NonBoolStringUntouched(t *testing.T) {\n\tt.Parallel()\n\n\tm := backend.Config{\"name\": \"true\", \"encrypt\": \"true\"}\n\tresult := backend.NormalizeBoolValues(m, &testConfig{})\n\n\t// \"name\" is a string field in the struct, should NOT be converted\n\tassert.Equal(t, \"true\", result[\"name\"])\n\t// \"encrypt\" is a bool field, should be converted\n\tassert.Equal(t, true, result[\"encrypt\"])\n}\n\nfunc TestNormalizeBoolValues_InvalidBoolStringLeftAsIs(t *testing.T) {\n\tt.Parallel()\n\n\tm := backend.Config{\"encrypt\": \"maybe\"}\n\tresult := backend.NormalizeBoolValues(m, &testConfig{})\n\n\tassert.Equal(t, \"maybe\", result[\"encrypt\"])\n}\n\nfunc TestNormalizeBoolValues_SquashedStructFields(t *testing.T) {\n\tt.Parallel()\n\n\tm := backend.Config{\n\t\t\"encrypt\": \"true\",\n\t\t\"enabled\": \"false\",\n\t\t\"verbose\": \"true\",\n\t\t\"name\":    \"test\",\n\t\t\"region\":  \"us-east-1\",\n\t}\n\tresult := backend.NormalizeBoolValues(m, &testConfigWithSquash{})\n\n\tassert.Equal(t, true, result[\"encrypt\"])\n\tassert.Equal(t, false, result[\"enabled\"])\n\tassert.Equal(t, true, result[\"verbose\"])\n\tassert.Equal(t, \"test\", result[\"name\"])\n\tassert.Equal(t, \"us-east-1\", result[\"region\"])\n}\n\nfunc TestNormalizeBoolValues_OriginalMapUnmutated(t *testing.T) {\n\tt.Parallel()\n\n\tm := backend.Config{\"encrypt\": \"true\"}\n\t_ = backend.NormalizeBoolValues(m, &testConfig{})\n\n\t// Original map should still have string\n\tassert.Equal(t, \"true\", m[\"encrypt\"])\n}\n\nfunc TestNormalizeBoolValues_EmptyMap(t *testing.T) {\n\tt.Parallel()\n\n\tm := backend.Config{}\n\tresult := backend.NormalizeBoolValues(m, &testConfig{})\n\n\tassert.Empty(t, result)\n}\n\ntype testConfigWithPtrBool struct {\n\tEncrypt  *bool  `mapstructure:\"encrypt\"`\n\tOptional *bool  `mapstructure:\"optional\"`\n\tName     string `mapstructure:\"name\"`\n}\n\nfunc TestNormalizeBoolValues_PtrBoolFields(t *testing.T) {\n\tt.Parallel()\n\n\tm := backend.Config{\"encrypt\": \"true\", \"optional\": \"false\", \"name\": \"test\"}\n\tresult := backend.NormalizeBoolValues(m, &testConfigWithPtrBool{})\n\n\tassert.Equal(t, true, result[\"encrypt\"])\n\tassert.Equal(t, false, result[\"optional\"])\n\tassert.Equal(t, \"test\", result[\"name\"])\n}\n\nfunc TestNormalizeBoolValues_NilMap(t *testing.T) {\n\tt.Parallel()\n\n\tresult := backend.NormalizeBoolValues(nil, &testConfig{})\n\n\tassert.Empty(t, result)\n}\n\nfunc TestNormalizeBoolValues_NilTarget(t *testing.T) {\n\tt.Parallel()\n\n\tm := backend.Config{\"encrypt\": \"true\", \"name\": \"test\"}\n\tresult := backend.NormalizeBoolValues(m, nil)\n\n\t// With nil target, no bool keys are detected, so values are returned as-is\n\tassert.Equal(t, \"true\", result[\"encrypt\"])\n\tassert.Equal(t, \"test\", result[\"name\"])\n}\n\nfunc TestNormalizeBoolValues_NumericBoolStrings(t *testing.T) {\n\tt.Parallel()\n\n\tm := backend.Config{\"encrypt\": \"1\", \"enabled\": \"0\"}\n\tresult := backend.NormalizeBoolValues(m, &testConfig{})\n\n\tassert.Equal(t, true, result[\"encrypt\"])\n\tassert.Equal(t, false, result[\"enabled\"])\n}\n"
  },
  {
    "path": "internal/remotestate/backend/s3/backend.go",
    "content": "// Package s3 represents AWS S3 backend for interacting with remote state.\npackage s3\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst BackendName = \"s3\"\n\nvar _ backend.Backend = new(Backend)\n\ntype Backend struct {\n\t*backend.CommonBackend\n}\n\nfunc NewBackend() *Backend {\n\treturn &Backend{\n\t\tCommonBackend: backend.NewCommonBackend(BackendName),\n\t}\n}\n\n// NeedsBootstrap returns true if the remote state S3 bucket specified in the given config needs to be bootstrapped.\n//\n// Returns true if:\n//\n// 1. Any of the existing backend settings are different than the current config\n// 2. The configured S3 bucket or DynamoDB table does not exist\nfunc (backend *Backend) NeedsBootstrap(ctx context.Context, l log.Logger, backendConfig backend.Config, opts *backend.Options) (bool, error) {\n\tcfg := Config(backendConfig).Normalize(l)\n\n\textS3Cfg, err := cfg.ExtendedS3Config(l)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tclient, err := NewClient(ctx, l, extS3Cfg, opts)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tvar (\n\t\tbucketName = extS3Cfg.RemoteStateConfigS3.Bucket\n\t\ttableName  = extS3Cfg.RemoteStateConfigS3.GetLockTableName()\n\t)\n\n\tif exists, err := client.DoesS3BucketExist(ctx, bucketName); err != nil || !exists {\n\t\treturn true, err\n\t}\n\n\tif tableName != \"\" {\n\t\tif exists, err := client.DoesLockTableExistAndIsActive(ctx, tableName); err != nil || !exists {\n\t\t\treturn true, err\n\t\t}\n\t}\n\n\treturn false, nil\n}\n\n// Bootstrap the remote state S3 bucket specified in the given config. This function will validate the config\n// parameters, create the S3 bucket if it doesn't already exist, and check that versioning is enabled.\nfunc (backend *Backend) Bootstrap(\n\tctx context.Context,\n\tl log.Logger,\n\tbackendConfig backend.Config,\n\topts *backend.Options,\n) error {\n\textS3Cfg, err := Config(backendConfig).ExtendedS3Config(l)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar (\n\t\ts3Cfg      = &extS3Cfg.RemoteStateConfigS3\n\t\tbucketName = s3Cfg.Bucket\n\t)\n\n\t// ensure that only one goroutine can initialize bucket\n\tmu := backend.GetBucketMutex(bucketName)\n\n\tmu.Lock()\n\tdefer mu.Unlock()\n\n\tif backend.IsConfigInited(s3Cfg) {\n\t\tl.Debugf(\n\t\t\t\"%s bucket %s has already been confirmed to be initialized, skipping initialization checks\",\n\t\t\tbackend.Name(), bucketName,\n\t\t)\n\n\t\treturn nil\n\t}\n\n\tclient, err := NewClient(ctx, l, extS3Cfg, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := client.CreateS3BucketIfNecessary(ctx, l, bucketName, opts); err != nil {\n\t\treturn err\n\t}\n\n\tif !extS3Cfg.DisableBucketUpdate {\n\t\tif err := client.UpdateS3BucketIfNecessary(ctx, l, bucketName, opts); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif !extS3Cfg.SkipBucketVersioning {\n\t\tif _, err := client.CheckIfVersioningEnabled(ctx, l, bucketName); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif tableName := extS3Cfg.RemoteStateConfigS3.GetLockTableName(); tableName != \"\" {\n\t\tif err := client.CreateLockTableIfNecessary(ctx, l, tableName, extS3Cfg.DynamotableTags); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif extS3Cfg.EnableLockTableSSEncryption {\n\t\t\tif err := client.UpdateLockTableSetSSEncryptionOnIfNecessary(ctx, l, tableName); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tbackend.MarkConfigInited(s3Cfg)\n\n\treturn nil\n}\n\n// IsVersionControlEnabled returns true if version control for s3 bucket is enabled.\nfunc (backend *Backend) IsVersionControlEnabled(ctx context.Context, l log.Logger, backendConfig backend.Config, opts *backend.Options) (bool, error) {\n\textS3Cfg, err := Config(backendConfig).ExtendedS3Config(l)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tvar bucketName = extS3Cfg.RemoteStateConfigS3.Bucket\n\n\tclient, err := NewClient(ctx, l, extS3Cfg, opts)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treturn client.CheckIfVersioningEnabled(ctx, l, bucketName)\n}\n\n// Migrate copies the s3 bucket object located at src config to dst config and deletes the src object.\n// Creates a new DynamoDB table item for dst config and deletes the table item from the src config.\nfunc (backend *Backend) Migrate(ctx context.Context, l log.Logger, srcBackendConfig, dstBackendConfig backend.Config, opts *backend.Options) error {\n\tsrcExtS3Cfg, err := Config(srcBackendConfig).ExtendedS3Config(l)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdstExtS3Cfg, err := Config(dstBackendConfig).ExtendedS3Config(l)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar (\n\t\tsrcBucketName = srcExtS3Cfg.RemoteStateConfigS3.Bucket\n\t\tsrcBucketKey  = srcExtS3Cfg.RemoteStateConfigS3.Key\n\t\tsrcTableName  = srcExtS3Cfg.RemoteStateConfigS3.GetLockTableName()\n\t\tsrcTableKey   = path.Join(srcBucketName, srcBucketKey+stateIDSuffix)\n\n\t\tdstBucketName = dstExtS3Cfg.RemoteStateConfigS3.Bucket\n\t\tdstBucketKey  = dstExtS3Cfg.RemoteStateConfigS3.Key\n\t\tdstTableName  = dstExtS3Cfg.RemoteStateConfigS3.GetLockTableName()\n\t\tdstTableKey   = path.Join(dstBucketName, dstBucketKey+stateIDSuffix)\n\t)\n\n\tclient, err := NewClient(ctx, l, srcExtS3Cfg, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err = client.MoveS3ObjectIfNecessary(ctx, l, srcBucketName, srcBucketKey, dstBucketName, dstBucketKey); err != nil {\n\t\treturn err\n\t}\n\n\tif dstTableName != \"\" {\n\t\tif err := client.CreateTableItemIfNecessary(ctx, l, dstTableName, dstTableKey); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif srcTableName != \"\" {\n\t\treturn client.DeleteTableItemIfNecessary(ctx, l, srcTableName, srcTableKey)\n\t}\n\n\treturn nil\n}\n\n// Delete deletes the remote state specified in the given config.\nfunc (backend *Backend) Delete(ctx context.Context, l log.Logger, backendConfig backend.Config, opts *backend.Options) error {\n\textS3Cfg, err := Config(backendConfig).ExtendedS3Config(l)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar (\n\t\tbucketName = extS3Cfg.RemoteStateConfigS3.Bucket\n\t\tbucketKey  = extS3Cfg.RemoteStateConfigS3.Key\n\t\ttableName  = extS3Cfg.RemoteStateConfigS3.GetLockTableName()\n\t)\n\n\tclient, err := NewClient(ctx, l, extS3Cfg, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif tableName != \"\" {\n\t\ttableKey := path.Join(bucketName, bucketKey+stateIDSuffix)\n\n\t\tprompt := fmt.Sprintf(\"DynamoDB table %s key %s will be deleted. Do you want to continue?\", tableName, tableKey)\n\t\tif yes, err := shell.PromptUserForYesNo(ctx, l, prompt, opts.NonInteractive, opts.Writers.ErrWriter); err != nil {\n\t\t\treturn err\n\t\t} else if yes {\n\t\t\tif err := client.DeleteTableItemIfNecessary(ctx, l, tableName, tableKey); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tprompt := fmt.Sprintf(\"S3 bucket %s key %s will be deleted. Do you want to continue?\", bucketName, bucketKey)\n\tif yes, err := shell.PromptUserForYesNo(ctx, l, prompt, opts.NonInteractive, opts.Writers.ErrWriter); err != nil {\n\t\treturn err\n\t} else if yes {\n\t\treturn client.DeleteS3ObjectIfNecessary(ctx, l, bucketName, bucketKey)\n\t}\n\n\treturn nil\n}\n\n// DeleteBucket deletes the entire bucket specified in the given config.\nfunc (backend *Backend) DeleteBucket(ctx context.Context, l log.Logger, backendConfig backend.Config, opts *backend.Options) error {\n\textS3Cfg, err := Config(backendConfig).ExtendedS3Config(l)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tclient, err := NewClient(ctx, l, extS3Cfg, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar (\n\t\tbucketName = extS3Cfg.RemoteStateConfigS3.Bucket\n\t\ttableName  = extS3Cfg.RemoteStateConfigS3.GetLockTableName()\n\t)\n\n\tif tableName != \"\" {\n\t\tprompt := fmt.Sprintf(\"DynamoDB table %s will be completely deleted. Do you want to continue?\", tableName)\n\t\tif yes, err := shell.PromptUserForYesNo(ctx, l, prompt, opts.NonInteractive, opts.Writers.ErrWriter); err != nil {\n\t\t\treturn err\n\t\t} else if yes {\n\t\t\tif err := client.DeleteTableIfNecessary(ctx, l, tableName); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tprompt := fmt.Sprintf(\"S3 bucket %s will be completely deleted. Do you want to continue?\", bucketName)\n\tif yes, err := shell.PromptUserForYesNo(ctx, l, prompt, opts.NonInteractive, opts.Writers.ErrWriter); err != nil {\n\t\treturn err\n\t} else if yes {\n\t\treturn client.DeleteS3BucketIfNecessary(ctx, l, bucketName)\n\t}\n\n\treturn nil\n}\n\nfunc (backend *Backend) GetTFInitArgs(config backend.Config) map[string]any {\n\treturn Config(config).GetTFInitArgs()\n}\n"
  },
  {
    "path": "internal/remotestate/backend/s3/backend_test.go",
    "content": "package s3_test\n\nimport (\n\t\"testing\"\n\n\tbackend \"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\ts3backend \"github.com/gruntwork-io/terragrunt/internal/remotestate/backend/s3\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestBackend_GetTFInitArgs(t *testing.T) {\n\tt.Parallel()\n\n\tremoteBackend := s3backend.NewBackend()\n\n\ttestCases := []struct { //nolint: govet\n\t\tname          string\n\t\tconfig        backend.Config\n\t\texpected      map[string]any\n\t\tshouldBeEqual bool\n\t}{\n\t\t{\n\t\t\t\"empty-no-values\",\n\t\t\tbackend.Config{},\n\t\t\tmap[string]any{},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"valid-s3-configuration-keys\",\n\t\t\tbackend.Config{\n\t\t\t\t\"bucket\":  \"foo\",\n\t\t\t\t\"encrypt\": \"bar\",\n\t\t\t\t\"key\":     \"baz\",\n\t\t\t\t\"region\":  \"quux\",\n\t\t\t},\n\t\t\tmap[string]any{\n\t\t\t\t\"bucket\":  \"foo\",\n\t\t\t\t\"encrypt\": \"bar\",\n\t\t\t\t\"key\":     \"baz\",\n\t\t\t\t\"region\":  \"quux\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"terragrunt-keys-filtered\",\n\t\t\tbackend.Config{\n\t\t\t\t\"bucket\":                      \"foo\",\n\t\t\t\t\"encrypt\":                     \"bar\",\n\t\t\t\t\"key\":                         \"baz\",\n\t\t\t\t\"region\":                      \"quux\",\n\t\t\t\t\"skip_credentials_validation\": true,\n\t\t\t\t\"s3_bucket_tags\":              map[string]string{},\n\t\t\t},\n\t\t\tmap[string]any{\n\t\t\t\t\"bucket\":                      \"foo\",\n\t\t\t\t\"encrypt\":                     \"bar\",\n\t\t\t\t\"key\":                         \"baz\",\n\t\t\t\t\"region\":                      \"quux\",\n\t\t\t\t\"skip_credentials_validation\": true,\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"empty-no-values-all-terragrunt-keys-filtered\",\n\t\t\tbackend.Config{\n\t\t\t\t\"s3_bucket_tags\":                                    map[string]string{},\n\t\t\t\t\"dynamodb_table_tags\":                               map[string]string{},\n\t\t\t\t\"accesslogging_bucket_tags\":                         map[string]string{},\n\t\t\t\t\"skip_bucket_versioning\":                            true,\n\t\t\t\t\"skip_bucket_ssencryption\":                          false,\n\t\t\t\t\"skip_bucket_root_access\":                           false,\n\t\t\t\t\"skip_bucket_enforced_tls\":                          false,\n\t\t\t\t\"skip_bucket_public_access_blocking\":                false,\n\t\t\t\t\"disable_bucket_update\":                             true,\n\t\t\t\t\"enable_lock_table_ssencryption\":                    true,\n\t\t\t\t\"disable_aws_client_checksums\":                      false,\n\t\t\t\t\"accesslogging_bucket_name\":                         \"test\",\n\t\t\t\t\"accesslogging_target_object_partition_date_source\": \"EventTime\",\n\t\t\t\t\"accesslogging_target_prefix\":                       \"test\",\n\t\t\t\t\"skip_accesslogging_bucket_acl\":                     false,\n\t\t\t\t\"skip_accesslogging_bucket_enforced_tls\":            false,\n\t\t\t\t\"skip_accesslogging_bucket_public_access_blocking\":  false,\n\t\t\t\t\"skip_accesslogging_bucket_ssencryption\":            false,\n\t\t\t},\n\t\t\tmap[string]any{},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"lock-table-replaced-with-dynamodb-table\",\n\t\t\tbackend.Config{\n\t\t\t\t\"bucket\":     \"foo\",\n\t\t\t\t\"encrypt\":    \"bar\",\n\t\t\t\t\"key\":        \"baz\",\n\t\t\t\t\"region\":     \"quux\",\n\t\t\t\t\"lock_table\": \"xyzzy\",\n\t\t\t},\n\t\t\tmap[string]any{\n\t\t\t\t\"bucket\":         \"foo\",\n\t\t\t\t\"encrypt\":        \"bar\",\n\t\t\t\t\"key\":            \"baz\",\n\t\t\t\t\"region\":         \"quux\",\n\t\t\t\t\"dynamodb_table\": \"xyzzy\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"dynamodb-table-not-replaced-with-lock-table\",\n\t\t\tbackend.Config{\n\t\t\t\t\"bucket\":         \"foo\",\n\t\t\t\t\"encrypt\":        \"bar\",\n\t\t\t\t\"key\":            \"baz\",\n\t\t\t\t\"region\":         \"quux\",\n\t\t\t\t\"dynamodb_table\": \"xyzzy\",\n\t\t\t},\n\t\t\tmap[string]any{\n\t\t\t\t\"bucket\":     \"foo\",\n\t\t\t\t\"encrypt\":    \"bar\",\n\t\t\t\t\"key\":        \"baz\",\n\t\t\t\t\"region\":     \"quux\",\n\t\t\t\t\"lock_table\": \"xyzzy\",\n\t\t\t},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"assume-role\",\n\t\t\tbackend.Config{\n\t\t\t\t\"bucket\": \"foo\",\n\t\t\t\t\"assume_role\": map[string]any{\n\t\t\t\t\t\"role_arn\":     \"arn:aws:iam::123:role/role\",\n\t\t\t\t\t\"external_id\":  \"123\",\n\t\t\t\t\t\"session_name\": \"qwe\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tmap[string]any{\n\t\t\t\t\"bucket\":      \"foo\",\n\t\t\t\t\"assume_role\": \"{external_id=\\\"123\\\",role_arn=\\\"arn:aws:iam::123:role/role\\\",session_name=\\\"qwe\\\"}\",\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"use-lockfile-native-s3-locking\",\n\t\t\tbackend.Config{\n\t\t\t\t\"bucket\":       \"foo\",\n\t\t\t\t\"key\":          \"bar\",\n\t\t\t\t\"region\":       \"us-east-1\",\n\t\t\t\t\"use_lockfile\": true,\n\t\t\t},\n\t\t\tmap[string]any{\n\t\t\t\t\"bucket\":       \"foo\",\n\t\t\t\t\"key\":          \"bar\",\n\t\t\t\t\"region\":       \"us-east-1\",\n\t\t\t\t\"use_lockfile\": true,\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"use-lockfile-false\",\n\t\t\tbackend.Config{\n\t\t\t\t\"bucket\":       \"foo\",\n\t\t\t\t\"key\":          \"bar\",\n\t\t\t\t\"region\":       \"us-east-1\",\n\t\t\t\t\"use_lockfile\": false,\n\t\t\t},\n\t\t\tmap[string]any{\n\t\t\t\t\"bucket\":       \"foo\",\n\t\t\t\t\"key\":          \"bar\",\n\t\t\t\t\"region\":       \"us-east-1\",\n\t\t\t\t\"use_lockfile\": false,\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"dual-locking-dynamodb-and-s3\",\n\t\t\tbackend.Config{\n\t\t\t\t\"bucket\":         \"foo\",\n\t\t\t\t\"key\":            \"bar\",\n\t\t\t\t\"region\":         \"us-east-1\",\n\t\t\t\t\"dynamodb_table\": \"my-lock-table\",\n\t\t\t\t\"use_lockfile\":   true,\n\t\t\t},\n\t\t\tmap[string]any{\n\t\t\t\t\"bucket\":         \"foo\",\n\t\t\t\t\"key\":            \"bar\",\n\t\t\t\t\"region\":         \"us-east-1\",\n\t\t\t\t\"dynamodb_table\": \"my-lock-table\",\n\t\t\t\t\"use_lockfile\":   true,\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"string-bool-use-lockfile-true\",\n\t\t\tbackend.Config{\n\t\t\t\t\"bucket\":       \"foo\",\n\t\t\t\t\"key\":          \"bar\",\n\t\t\t\t\"region\":       \"us-east-1\",\n\t\t\t\t\"use_lockfile\": \"true\",\n\t\t\t},\n\t\t\tmap[string]any{\n\t\t\t\t\"bucket\":       \"foo\",\n\t\t\t\t\"key\":          \"bar\",\n\t\t\t\t\"region\":       \"us-east-1\",\n\t\t\t\t\"use_lockfile\": true,\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"string-bool-use-lockfile-false\",\n\t\t\tbackend.Config{\n\t\t\t\t\"bucket\":       \"foo\",\n\t\t\t\t\"key\":          \"bar\",\n\t\t\t\t\"region\":       \"us-east-1\",\n\t\t\t\t\"use_lockfile\": \"false\",\n\t\t\t},\n\t\t\tmap[string]any{\n\t\t\t\t\"bucket\":       \"foo\",\n\t\t\t\t\"key\":          \"bar\",\n\t\t\t\t\"region\":       \"us-east-1\",\n\t\t\t\t\"use_lockfile\": false,\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"string-bool-encrypt-and-use-lockfile\",\n\t\t\tbackend.Config{\n\t\t\t\t\"bucket\":       \"foo\",\n\t\t\t\t\"key\":          \"bar\",\n\t\t\t\t\"region\":       \"us-east-1\",\n\t\t\t\t\"encrypt\":      \"true\",\n\t\t\t\t\"use_lockfile\": \"true\",\n\t\t\t},\n\t\t\tmap[string]any{\n\t\t\t\t\"bucket\":       \"foo\",\n\t\t\t\t\"key\":          \"bar\",\n\t\t\t\t\"region\":       \"us-east-1\",\n\t\t\t\t\"encrypt\":      true,\n\t\t\t\t\"use_lockfile\": true,\n\t\t\t},\n\t\t\ttrue,\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.Parallel()\n\n\t\t\tactual := remoteBackend.GetTFInitArgs(tc.config)\n\n\t\t\tif !tc.shouldBeEqual {\n\t\t\t\tassert.NotEqual(t, tc.expected, actual)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/remotestate/backend/s3/client.go",
    "content": "package s3\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/dynamodb\"\n\tdynamodbtypes \"github.com/aws/aws-sdk-go-v2/service/dynamodb/types\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3/types\"\n\t\"github.com/aws/smithy-go\"\n\t\"github.com/gruntwork-io/terragrunt/internal/awshelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst (\n\tSidRootPolicy        = \"RootAccess\"\n\tSidEnforcedTLSPolicy = \"EnforcedTLS\"\n\n\ts3TimeBetweenRetries  = 5 * time.Second\n\ts3MaxRetries          = 3\n\ts3SleepBetweenRetries = 10 * time.Second\n\n\tmaxRetriesWaitingForS3Bucket          = 12\n\tsleepBetweenRetriesWaitingForS3Bucket = 5 * time.Second\n\n\t// To enable access logging in an S3 bucket, you must grant WRITE and READ_ACP permissions to the Log Delivery Group,\n\t// which is represented by the following URI. For more info, see:\n\t// https://docs.aws.amazon.com/AmazonS3/latest/dev/enable-logging-programming.html\n\ts3LogDeliveryGranteeURI = \"http://acs.amazonaws.com/groups/s3/LogDelivery\"\n\n\t// DynamoDB only allows 10 table creates/deletes simultaneously. To ensure we don't hit this error, especially when\n\t// running many automated tests in parallel, we use a counting semaphore\n\tdynamoParallelOperations = 10\n\n\t// AttrLockID is the name of the primary key for the lock table in DynamoDB.\n\t// OpenTofu/Terraform requires the DynamoDB table to have a primary key with this name\n\tAttrLockID = \"LockID\"\n\n\t// stateIDSuffix is last saved serial in tablestore with this suffix for consistency checks.\n\tstateIDSuffix = \"-md5\"\n\n\t// MaxRetriesWaitingForTableToBeActive is the maximum number of times we\n\t// will retry waiting for a table to be active.\n\t//\n\t// Default is to retry for up to 5 minutes\n\tMaxRetriesWaitingForTableToBeActive = 30\n\n\t// SleepBetweenTableStatusChecks is the amount of time we will sleep between\n\t// checks to see if a table is active.\n\tSleepBetweenTableStatusChecks = 10 * time.Second\n\n\t// DynamodbPayPerRequestBillingMode is the billing mode for DynamoDB tables that allows for pay-per-request billing\n\t// instead of provisioned capacity.\n\tDynamodbPayPerRequestBillingMode = \"PAY_PER_REQUEST\"\n\n\tsleepBetweenRetriesWaitingForEncryption = 20 * time.Second\n\tmaxRetriesWaitingForEncryption          = 15\n)\n\nvar tableCreateDeleteSemaphore = NewCountingSemaphore(dynamoParallelOperations)\n\ntype Client struct {\n\t*ExtendedRemoteStateConfigS3\n\n\ts3Client     *s3.Client\n\tdynamoClient *dynamodb.Client\n\tawsConfig    aws.Config\n\n\tfailIfBucketCreationRequired bool\n}\n\nfunc NewClient(ctx context.Context, l log.Logger, config *ExtendedRemoteStateConfigS3, opts *backend.Options) (*Client, error) {\n\tawsConfig := config.GetAwsSessionConfig()\n\n\tbuilder := awshelper.NewAWSConfigBuilder().\n\t\tWithSessionConfig(awsConfig).\n\t\tWithEnv(opts.Env).\n\t\tWithIAMRoleOptions(opts.IAMRoleOptions)\n\n\tcfg, err := builder.Build(ctx, l)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tif !config.SkipCredentialsValidation {\n\t\tif err = awshelper.ValidateAwsConfig(ctx, &cfg); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\ts3Client, err := builder.BuildS3Client(ctx, l)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tdynamoDBClient := dynamodb.NewFromConfig(cfg)\n\tif awsConfig.CustomDynamoDBEndpoint != \"\" {\n\t\tdynamoDBClient = dynamodb.NewFromConfig(cfg, func(o *dynamodb.Options) {\n\t\t\to.BaseEndpoint = aws.String(awsConfig.CustomDynamoDBEndpoint)\n\t\t})\n\t}\n\n\tclient := &Client{\n\t\tExtendedRemoteStateConfigS3:  config,\n\t\ts3Client:                     s3Client,\n\t\tdynamoClient:                 dynamoDBClient,\n\t\tawsConfig:                    cfg,\n\t\tfailIfBucketCreationRequired: opts.FailIfBucketCreationRequired,\n\t}\n\n\treturn client, nil\n}\n\n// CreateS3BucketIfNecessary prompts the user to create the given bucket if it doesn't already exist and if the user\n// confirms, creates the bucket and enables versioning for it.\nfunc (client *Client) CreateS3BucketIfNecessary(ctx context.Context, l log.Logger, bucketName string, opts *backend.Options) error {\n\tif client.ExtendedRemoteStateConfigS3 == nil {\n\t\treturn errors.Errorf(\"client configuration is nil - cannot create S3 bucket if necessary\")\n\t}\n\n\tcfg := &client.ExtendedRemoteStateConfigS3.RemoteStateConfigS3\n\n\tif exists, err := client.DoesS3BucketExistWithLogging(ctx, l, cfg.Bucket); err != nil || exists {\n\t\treturn err\n\t}\n\n\tif opts.FailIfBucketCreationRequired {\n\t\treturn backend.BucketCreationNotAllowed(bucketName)\n\t}\n\n\tprompt := fmt.Sprintf(\"Remote state S3 bucket %s does not exist or you don't have permissions to access it. Would you like Terragrunt to create it?\", bucketName)\n\n\tshouldCreateBucket, err := shell.PromptUserForYesNo(ctx, l, prompt, opts.NonInteractive, opts.Writers.ErrWriter)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif shouldCreateBucket {\n\t\t// Creating the S3 bucket occasionally fails with eventual consistency errors: e.g., the S3 HeadBucket\n\t\t// operation says the bucket exists, but a subsequent call to enable versioning on that bucket fails with\n\t\t// the error \"NoSuchBucket: The specified bucket does not exist.\" Therefore, when creating and configuring\n\t\t// the S3 bucket, we do so in a retry loop with a sleep between retries that will hopefully work around the\n\t\t// eventual consistency issues. Each S3 operation should be idempotent, so redoing steps that have already\n\t\t// been performed should be a no-op.\n\t\tdescription := \"Create S3 bucket with retry \" + bucketName\n\n\t\treturn util.DoWithRetry(ctx, description, s3MaxRetries, s3SleepBetweenRetries, l, log.DebugLevel, func(ctx context.Context) error {\n\t\t\terr := client.CreateS3BucketWithVersioningSSEncryptionAndAccessLogging(ctx, l, opts)\n\t\t\tif err != nil {\n\t\t\t\tif isBucketErrorRetriable(l, err) {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\t// return FatalError so that retry loop will not continue\n\t\t\t\treturn util.FatalError{Underlying: err}\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\treturn nil\n}\n\nfunc (client *Client) UpdateS3BucketIfNecessary(ctx context.Context, l log.Logger, bucketName string, opts *backend.Options) error {\n\tif exists, err := client.DoesS3BucketExistWithLogging(ctx, l, bucketName); err != nil {\n\t\treturn err\n\t} else if !exists && opts.FailIfBucketCreationRequired {\n\t\treturn backend.BucketCreationNotAllowed(bucketName)\n\t}\n\n\tneedsUpdate, bucketUpdatesRequired, err := client.checkIfS3BucketNeedsUpdate(ctx, l, bucketName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !needsUpdate {\n\t\tl.Debug(\"S3 bucket is already up to date\")\n\t\treturn nil\n\t}\n\n\tprompt := fmt.Sprintf(\"Remote state S3 bucket %s is out of date. Would you like Terragrunt to update it?\", bucketName)\n\n\tshouldUpdateBucket, err := shell.PromptUserForYesNo(ctx, l, prompt, opts.NonInteractive, opts.Writers.ErrWriter)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !shouldUpdateBucket {\n\t\treturn nil\n\t}\n\n\tif bucketUpdatesRequired.Versioning {\n\t\tif client.SkipBucketVersioning {\n\t\t\tl.Debugf(\"Versioning is disabled for the remote state S3 bucket %s using 'skip_bucket_versioning' config.\", bucketName)\n\t\t} else if err := client.EnableVersioningForS3Bucket(ctx, l, bucketName); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif bucketUpdatesRequired.SSEEncryption {\n\t\tmsg := fmt.Sprintf(\"Encryption is not enabled on the S3 remote state bucket %s. Terraform state files may contain secrets, so we STRONGLY recommend enabling encryption!\", bucketName)\n\n\t\tif client.SkipBucketSSEncryption {\n\t\t\tl.Debug(msg)\n\t\t\tl.Debugf(\"Server-Side Encryption enabling is disabled for the remote state AWS S3 bucket %s using 'skip_bucket_ssencryption' config.\", bucketName)\n\n\t\t\treturn nil\n\t\t} else {\n\t\t\tl.Warn(msg)\n\t\t}\n\n\t\tl.Infof(\"Enabling Server-Side Encryption for the remote state AWS S3 bucket %s.\", bucketName)\n\n\t\tif err := client.EnableSSEForS3BucketWide(ctx, l, bucketName, client.FetchEncryptionAlgorithm()); err != nil {\n\t\t\tl.Errorf(\"Failed to enable Server-Side Encryption for the remote state AWS S3 bucket %s: %v\", bucketName, err)\n\t\t\treturn err\n\t\t}\n\n\t\tl.Infof(\"Successfully enabled Server-Side Encryption for the remote state AWS S3 bucket %s.\", bucketName)\n\t}\n\n\tif bucketUpdatesRequired.RootAccess {\n\t\tif client.SkipBucketRootAccess {\n\t\t\tl.Debugf(\"Root access is disabled for the remote state S3 bucket %s using 'skip_bucket_root_access' config.\", bucketName)\n\t\t} else if err := client.EnableRootAccesstoS3Bucket(ctx, l); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif bucketUpdatesRequired.EnforcedTLS {\n\t\tif client.SkipBucketEnforcedTLS {\n\t\t\tl.Debugf(\"Enforced TLS is disabled for the remote state AWS S3 bucket %s using 'skip_bucket_enforced_tls' config.\", bucketName)\n\t\t} else if err := client.EnableEnforcedTLSAccesstoS3Bucket(ctx, l, bucketName); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif bucketUpdatesRequired.AccessLogging {\n\t\tif client.SkipBucketAccessLogging {\n\t\t\tl.Debugf(\"Access logging is disabled for the remote state AWS S3 bucket %s using 'skip_bucket_access_logging' config.\", bucketName)\n\t\t} else {\n\t\t\tif client.AccessLoggingBucketName != \"\" {\n\t\t\t\tif err := client.configureAccessLogBucket(ctx, l, opts); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tl.Debugf(\"Access Logging is disabled for the remote state AWS S3 bucket %s\", bucketName)\n\t\t\t}\n\t\t}\n\t}\n\n\tif bucketUpdatesRequired.PublicAccess {\n\t\tif client.SkipBucketPublicAccessBlocking {\n\t\t\tl.Debugf(\"Public access blocking is disabled for the remote state AWS S3 bucket %s using 'skip_bucket_public_access_blocking' config.\", bucketName)\n\t\t} else if err := client.EnablePublicAccessBlockingForS3Bucket(ctx, l, bucketName); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// configureAccessLogBucket - configure access log bucket.\nfunc (client *Client) configureAccessLogBucket(ctx context.Context, l log.Logger, opts *backend.Options) error {\n\tif client.ExtendedRemoteStateConfigS3 == nil {\n\t\treturn errors.Errorf(\"client configuration is nil - cannot configure access log bucket\")\n\t}\n\n\tcfg := &client.ExtendedRemoteStateConfigS3.RemoteStateConfigS3\n\n\tl.Debugf(\"Enabling bucket-wide Access Logging on AWS S3 bucket %s - using as TargetBucket %s\", cfg.Bucket, client.AccessLoggingBucketName)\n\n\tif err := client.CreateLogsS3BucketIfNecessary(ctx, l, client.AccessLoggingBucketName, opts); err != nil {\n\t\tl.Errorf(\"Could not create logs bucket %s for AWS S3 bucket %s\\n%s\", client.AccessLoggingBucketName, cfg.Bucket, err.Error())\n\n\t\treturn err\n\t}\n\n\tif !client.SkipAccessLoggingBucketPublicAccessBlocking {\n\t\tif err := client.EnablePublicAccessBlockingForS3Bucket(ctx, l, client.AccessLoggingBucketName); err != nil {\n\t\t\tl.Errorf(\"Could not enable public access blocking on %s\\n%s\", client.AccessLoggingBucketName, err.Error())\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif err := client.EnableAccessLoggingForS3BucketWide(ctx, l); err != nil {\n\t\tl.Errorf(\"Could not enable access logging on %s\\n%s\", cfg.Bucket, err.Error())\n\n\t\treturn err\n\t}\n\n\tif !client.SkipAccessLoggingBucketSSEncryption {\n\t\tif err := client.EnableSSEForS3BucketWide(ctx, l, client.AccessLoggingBucketName, string(types.ServerSideEncryptionAes256)); err != nil {\n\t\t\tl.Errorf(\"Could not enable encryption on %s\\n%s\", client.AccessLoggingBucketName, err.Error())\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif !client.SkipAccessLoggingBucketEnforcedTLS {\n\t\tif err := client.EnableEnforcedTLSAccesstoS3Bucket(ctx, l, client.AccessLoggingBucketName); err != nil {\n\t\t\tl.Errorf(\"Could not enable TLS access on %s\\n%s\", client.AccessLoggingBucketName, err.Error())\n\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif client.SkipBucketVersioning {\n\t\tl.Debugf(\"Versioning is disabled for the remote state S3 bucket %s using 'skip_bucket_versioning' config.\", client.AccessLoggingBucketName)\n\t} else if err := client.EnableVersioningForS3Bucket(ctx, l, client.AccessLoggingBucketName); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\ntype S3BucketUpdatesRequired struct {\n\tVersioning    bool\n\tSSEEncryption bool\n\tRootAccess    bool\n\tEnforcedTLS   bool\n\tAccessLogging bool\n\tPublicAccess  bool\n}\n\nfunc (client *Client) checkIfS3BucketNeedsUpdate(ctx context.Context, l log.Logger, bucketName string) (bool, S3BucketUpdatesRequired, error) {\n\tvar (\n\t\tupdates  []string\n\t\ttoUpdate S3BucketUpdatesRequired\n\t)\n\n\tif !client.SkipBucketVersioning {\n\t\tenabled, err := client.CheckIfVersioningEnabled(ctx, l, bucketName)\n\t\tif err != nil {\n\t\t\treturn false, toUpdate, err\n\t\t}\n\n\t\tif !enabled {\n\t\t\ttoUpdate.Versioning = true\n\n\t\t\tupdates = append(updates, \"Bucket Versioning\")\n\t\t}\n\t}\n\n\tif !client.SkipBucketSSEncryption {\n\t\tmatches, err := client.checkIfSSEForS3MatchesConfig(ctx, bucketName)\n\t\tif err != nil {\n\t\t\treturn false, toUpdate, err\n\t\t}\n\n\t\tif !matches {\n\t\t\ttoUpdate.SSEEncryption = true\n\n\t\t\tupdates = append(updates, \"Bucket Server-Side Encryption\")\n\t\t}\n\t}\n\n\tif !client.SkipBucketRootAccess {\n\t\tenabled, err := client.checkIfBucketRootAccess(ctx, l, bucketName)\n\t\tif err != nil {\n\t\t\treturn false, toUpdate, err\n\t\t}\n\n\t\tif !enabled {\n\t\t\ttoUpdate.RootAccess = true\n\n\t\t\tupdates = append(updates, \"Bucket Root Access\")\n\t\t}\n\t}\n\n\tif !client.SkipBucketEnforcedTLS {\n\t\tenabled, err := client.checkIfBucketEnforcedTLS(ctx, l, bucketName)\n\t\tif err != nil {\n\t\t\treturn false, toUpdate, err\n\t\t}\n\n\t\tif !enabled {\n\t\t\ttoUpdate.EnforcedTLS = true\n\n\t\t\tupdates = append(updates, \"Bucket Enforced TLS\")\n\t\t}\n\t}\n\n\tif !client.SkipBucketAccessLogging && client.AccessLoggingBucketName != \"\" {\n\t\tenabled, err := client.checkS3AccessLoggingConfiguration(ctx, bucketName)\n\t\tif err != nil {\n\t\t\treturn false, toUpdate, err\n\t\t}\n\n\t\tif !enabled {\n\t\t\ttoUpdate.AccessLogging = true\n\n\t\t\tupdates = append(updates, \"Bucket Access Logging\")\n\t\t}\n\t}\n\n\tif !client.SkipBucketPublicAccessBlocking {\n\t\tenabled, err := client.checkIfS3PublicAccessBlockingEnabled(ctx, bucketName)\n\t\tif err != nil {\n\t\t\treturn false, toUpdate, err\n\t\t}\n\n\t\tif !enabled {\n\t\t\ttoUpdate.PublicAccess = true\n\n\t\t\tupdates = append(updates, \"Bucket Public Access Blocking\")\n\t\t}\n\t}\n\n\t// show update message if any of the above configs are not set\n\tif len(updates) > 0 {\n\t\tl.Warnf(\"The remote state S3 bucket %s needs to be updated:\", bucketName)\n\n\t\tfor _, update := range updates {\n\t\t\tl.Warnf(\"  - %s\", update)\n\t\t}\n\n\t\treturn true, toUpdate, nil\n\t}\n\n\treturn false, toUpdate, nil\n}\n\n// CheckIfVersioningEnabled checks if versioning is enabled for the S3 bucket specified in the given config and warn the user if it is not\nfunc (client *Client) CheckIfVersioningEnabled(ctx context.Context, l log.Logger, bucketName string) (bool, error) {\n\tif exists, err := client.DoesS3BucketExist(ctx, bucketName); err != nil {\n\t\treturn false, err\n\t} else if !exists {\n\t\treturn false, backend.NewBucketDoesNotExistError(bucketName)\n\t}\n\n\tl.Debugf(\"Verifying AWS S3 bucket versioning %s\", bucketName)\n\n\tres, err := client.s3Client.GetBucketVersioning(ctx, &s3.GetBucketVersioningInput{Bucket: aws.String(bucketName)})\n\tif err != nil {\n\t\treturn false, errors.New(err)\n\t}\n\n\t// NOTE: There must be a bug in the AWS SDK since res == nil when versioning is not enabled. In the future,\n\t// check the AWS SDK for updates to see if we can remove \"res == nil ||\".\n\tif res == nil || res.Status != types.BucketVersioningStatusEnabled {\n\t\tl.Warnf(\"Versioning is not enabled for the remote state S3 bucket %s. We recommend enabling versioning so that you can roll back to previous versions of your OpenTofu/Terraform state in case of error.\", bucketName)\n\t\treturn false, nil\n\t}\n\n\treturn true, nil\n}\n\n// CreateS3BucketWithVersioningSSEncryptionAndAccessLogging creates the given S3 bucket and enable versioning for it.\nfunc (client *Client) CreateS3BucketWithVersioningSSEncryptionAndAccessLogging(ctx context.Context, l log.Logger, opts *backend.Options) error {\n\tif client.ExtendedRemoteStateConfigS3 == nil {\n\t\treturn errors.Errorf(\"client configuration is nil - cannot create S3 bucket\")\n\t}\n\n\tcfg := &client.ExtendedRemoteStateConfigS3.RemoteStateConfigS3\n\n\tl.Debugf(\"Create S3 bucket %s with versioning, SSE encryption, and access logging.\", cfg.Bucket)\n\n\terr := client.CreateS3Bucket(ctx, l, cfg.Bucket, CreateS3BucketOpts{Tags: client.S3BucketTags})\n\tif err != nil {\n\t\tif accessError := client.checkBucketAccess(ctx, cfg.Bucket, cfg.Key); accessError != nil {\n\t\t\treturn accessError\n\t\t}\n\n\t\tif isBucketAlreadyOwnedByYouError(err) {\n\t\t\tl.Debugf(\"Looks like you're already creating bucket %s at the same time. Will not attempt to create it again.\", cfg.Bucket)\n\t\t\treturn client.WaitUntilS3BucketExists(ctx, l, cfg.Bucket)\n\t\t}\n\n\t\treturn err\n\t}\n\n\tif err := client.WaitUntilS3BucketExists(ctx, l, cfg.Bucket); err != nil {\n\t\treturn err\n\t}\n\n\tif client.SkipBucketRootAccess {\n\t\tl.Debugf(\"Root access is disabled for the remote state S3 bucket %s using 'skip_bucket_root_access' config.\", cfg.Bucket)\n\t} else if err := client.EnableRootAccesstoS3Bucket(ctx, l); err != nil {\n\t\treturn err\n\t}\n\n\tif client.SkipBucketEnforcedTLS {\n\t\tl.Debugf(\"TLS enforcement is disabled for the remote state S3 bucket %s using 'skip_bucket_enforced_tls' config.\", cfg.Bucket)\n\t} else if err := client.EnableEnforcedTLSAccesstoS3Bucket(ctx, l, cfg.Bucket); err != nil {\n\t\treturn err\n\t}\n\n\tif client.SkipBucketPublicAccessBlocking {\n\t\tl.Debugf(\"Public access blocking is disabled for the remote state AWS S3 bucket %s using 'skip_bucket_public_access_blocking' config.\", cfg.Bucket)\n\t} else if err := client.EnablePublicAccessBlockingForS3Bucket(ctx, l, cfg.Bucket); err != nil {\n\t\treturn err\n\t}\n\n\tif err := client.TagS3Bucket(ctx, l); err != nil {\n\t\treturn err\n\t}\n\n\tif client.SkipBucketVersioning {\n\t\tl.Debugf(\"Versioning is disabled for the remote state S3 bucket %s using 'skip_bucket_versioning' config.\", cfg.Bucket)\n\t} else if err := client.EnableVersioningForS3Bucket(ctx, l, cfg.Bucket); err != nil {\n\t\treturn err\n\t}\n\n\tif client.SkipBucketSSEncryption {\n\t\tl.Debugf(\"Server-Side Encryption is disabled for the remote state AWS S3 bucket %s using 'skip_bucket_ssencryption' config.\", cfg.Bucket)\n\t} else if err := client.EnableSSEForS3BucketWide(ctx, l, cfg.Bucket, client.FetchEncryptionAlgorithm()); err != nil {\n\t\treturn err\n\t}\n\n\tif client.SkipBucketAccessLogging {\n\t\tl.Warnf(\"Terragrunt configuration option 'skip_bucket_accesslogging' is now deprecated. Access logging for the state bucket %s is disabled by default. To enable access logging for bucket %s, please provide property `accesslogging_bucket_name` in the terragrunt config file. For more details, please refer to the Terragrunt documentation.\", cfg.Bucket, cfg.Bucket)\n\t}\n\n\tif client.AccessLoggingBucketName != \"\" {\n\t\tif err := client.configureAccessLogBucket(ctx, l, opts); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tl.Debugf(\"Access Logging is disabled for the remote state AWS S3 bucket %s\", cfg.Bucket)\n\t}\n\n\tif err := client.TagS3BucketAccessLogging(ctx, l); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (client *Client) CreateLogsS3BucketIfNecessary(ctx context.Context, l log.Logger, logsBucketName string, opts *backend.Options) error {\n\tif exists, err := client.DoesS3BucketExistWithLogging(ctx, l, logsBucketName); err != nil || exists {\n\t\treturn err\n\t}\n\n\tif client.failIfBucketCreationRequired {\n\t\treturn backend.BucketCreationNotAllowed(logsBucketName)\n\t}\n\n\tprompt := fmt.Sprintf(\"Logs S3 bucket %s for the remote state does not exist or you don't have permissions to access it. Would you like Terragrunt to create it?\", logsBucketName)\n\n\tshouldCreateBucket, err := shell.PromptUserForYesNo(ctx, l, prompt, opts.NonInteractive, opts.Writers.ErrWriter)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif shouldCreateBucket {\n\t\treturn client.CreateS3BucketWithRetry(ctx, l, logsBucketName, CreateS3BucketOpts{Tags: client.AccessLoggingBucketTags})\n\t}\n\n\treturn nil\n}\n\nfunc (client *Client) TagS3BucketAccessLogging(ctx context.Context, l log.Logger) error {\n\tif len(client.AccessLoggingBucketTags) == 0 {\n\t\tl.Debugf(\"No tags specified for bucket %s.\", client.AccessLoggingBucketName)\n\t\treturn nil\n\t}\n\n\t// There must be one entry in the list\n\tvar tagsConverted = convertTags(client.AccessLoggingBucketTags)\n\n\tl.Debugf(\"Tagging S3 bucket with %s\", client.AccessLoggingBucketTags)\n\n\tputBucketTaggingInput := s3.PutBucketTaggingInput{\n\t\tBucket: aws.String(client.AccessLoggingBucketName),\n\t\tTagging: &types.Tagging{\n\t\t\tTagSet: tagsConverted,\n\t\t},\n\t}\n\n\t_, err := client.s3Client.PutBucketTagging(ctx, &putBucketTaggingInput)\n\tif err != nil {\n\t\tif handleS3TaggingMethodNotAllowed(err, l, \"access logging bucket\") {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn errors.New(err)\n\t}\n\n\tl.Debugf(\"Tagged S3 bucket with %s\", client.AccessLoggingBucketTags)\n\n\treturn nil\n}\n\n// TagS3Bucket tags the S3 bucket with the tags specified in the config.\nfunc (client *Client) TagS3Bucket(ctx context.Context, l log.Logger) error {\n\tif client.ExtendedRemoteStateConfigS3 == nil {\n\t\treturn errors.Errorf(\"client configuration is nil - cannot tag S3 bucket\")\n\t}\n\n\tcfg := &client.ExtendedRemoteStateConfigS3.RemoteStateConfigS3\n\n\tif len(client.S3BucketTags) == 0 {\n\t\tl.Debugf(\"No tags to apply to S3 bucket %s\", cfg.Bucket)\n\t\treturn nil\n\t}\n\n\tl.Debugf(\"Tagging S3 bucket %s with %s\", cfg.Bucket, client.S3BucketTags)\n\n\ttagsConverted := convertTags(client.S3BucketTags)\n\n\tputBucketTaggingInput := s3.PutBucketTaggingInput{\n\t\tBucket: aws.String(cfg.Bucket),\n\t\tTagging: &types.Tagging{\n\t\t\tTagSet: tagsConverted,\n\t\t},\n\t}\n\n\t_, err := client.s3Client.PutBucketTagging(ctx, &putBucketTaggingInput)\n\tif err != nil {\n\t\tif handleS3TaggingMethodNotAllowed(err, l, cfg.Bucket) {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn errors.New(err)\n\t}\n\n\tl.Debugf(\"Tagged S3 bucket with %s\", client.S3BucketTags)\n\n\treturn nil\n}\n\nfunc convertTags(tags map[string]string) []types.Tag {\n\tvar tagsConverted = make([]types.Tag, 0, len(tags))\n\n\tfor k, v := range tags {\n\t\tvar tag = types.Tag{\n\t\t\tKey:   aws.String(k),\n\t\t\tValue: aws.String(v)}\n\n\t\ttagsConverted = append(tagsConverted, tag)\n\t}\n\n\treturn tagsConverted\n}\n\n// WaitUntilS3BucketExists waits until the S3 bucket with the given name exists.\n//\n// AWS is eventually consistent, so after creating an S3 bucket, this method can be used to wait until the information\n// about that S3 bucket has propagated everywhere.\nfunc (client *Client) WaitUntilS3BucketExists(ctx context.Context, l log.Logger, bucketName string) error {\n\tl.Debugf(\"Waiting for bucket %s to be created\", bucketName)\n\n\tfor retries := range maxRetriesWaitingForS3Bucket {\n\t\tif exists, err := client.DoesS3BucketExistWithLogging(ctx, l, bucketName); err != nil {\n\t\t\treturn err\n\t\t} else if exists {\n\t\t\tl.Debugf(\"S3 bucket %s created.\", bucketName)\n\t\t\treturn nil\n\t\t} else if retries < maxRetriesWaitingForS3Bucket-1 {\n\t\t\tl.Debugf(\"S3 bucket %s has not been created yet. Sleeping for %s and will check again.\", bucketName, sleepBetweenRetriesWaitingForS3Bucket)\n\t\t\ttime.Sleep(sleepBetweenRetriesWaitingForS3Bucket)\n\t\t}\n\t}\n\n\treturn errors.New(MaxRetriesWaitingForS3BucketExceeded(bucketName))\n}\n\n// CreateS3BucketOpts holds optional parameters for CreateS3Bucket.\ntype CreateS3BucketOpts struct {\n\t// Tags to apply at bucket creation time via CreateBucketConfiguration.Tags.\n\t// This is required in environments where an AWS SCP or tag policy enforces\n\t// mandatory tags on s3:CreateBucket.\n\tTags map[string]string\n}\n\n// CreateS3Bucket creates the S3 bucket specified in the given config.\nfunc (client *Client) CreateS3Bucket(ctx context.Context, l log.Logger, bucket string, opts ...CreateS3BucketOpts) error {\n\tif client.s3Client == nil {\n\t\treturn errors.Errorf(\"S3 client is nil - cannot create S3 bucket %s\", bucket)\n\t}\n\n\tl.Debugf(\"Creating S3 bucket %s\", bucket)\n\n\tinput := &s3.CreateBucketInput{\n\t\tBucket:          aws.String(bucket),\n\t\tObjectOwnership: types.ObjectOwnershipObjectWriter,\n\t}\n\n\t// For regions other than us-east-1, we need to specify the location constraint\n\t// to avoid IllegalLocationConstraintException\n\tregion := client.awsConfig.Region\n\tif region != \"us-east-1\" && region != \"\" {\n\t\tl.Debugf(\"Creating S3 bucket %s in region %s\", bucket, region)\n\t\tinput.CreateBucketConfiguration = &types.CreateBucketConfiguration{\n\t\t\tLocationConstraint: types.BucketLocationConstraint(region),\n\t\t}\n\t}\n\n\tif len(opts) > 0 && len(opts[0].Tags) > 0 {\n\t\tl.Debugf(\"Including %d tag(s) in CreateBucket request for %s\", len(opts[0].Tags), bucket)\n\n\t\tsdkTags := convertTags(opts[0].Tags)\n\n\t\tif input.CreateBucketConfiguration == nil {\n\t\t\tinput.CreateBucketConfiguration = &types.CreateBucketConfiguration{}\n\t\t}\n\n\t\tinput.CreateBucketConfiguration.Tags = sdkTags\n\t}\n\n\t_, err := client.s3Client.CreateBucket(ctx, input)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tl.Debugf(\"Created S3 bucket %s\", bucket)\n\n\treturn nil\n}\n\n// CreateS3BucketWithRetry creates an S3 bucket with full safeguards:\n// - Retry logic for transient errors\n// - Concurrent creation handling (BucketAlreadyOwnedByYou)\n// - Eventual consistency waiting after creation\nfunc (client *Client) CreateS3BucketWithRetry(ctx context.Context, l log.Logger, bucketName string, opts ...CreateS3BucketOpts) error {\n\tdescription := \"Create S3 bucket '\" + bucketName + \"' with retry\"\n\n\treturn util.DoWithRetry(ctx, description, s3MaxRetries, s3SleepBetweenRetries, l, log.DebugLevel, func(ctx context.Context) error {\n\t\terr := client.CreateS3Bucket(ctx, l, bucketName, opts...)\n\t\tif err != nil {\n\t\t\tif isBucketAlreadyOwnedByYouError(err) {\n\t\t\t\tl.Debugf(\"Looks like you're already creating bucket %s at the same time. Will not attempt to create it again.\", bucketName)\n\t\t\t\treturn client.WaitUntilS3BucketExists(ctx, l, bucketName)\n\t\t\t}\n\n\t\t\tif isBucketErrorRetriable(l, err) {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn util.FatalError{Underlying: err}\n\t\t}\n\n\t\treturn client.WaitUntilS3BucketExists(ctx, l, bucketName)\n\t})\n}\n\n// or is in progress. This usually happens when running many tests in parallel or xxx-all commands.\nfunc isBucketAlreadyOwnedByYouError(err error) bool {\n\tvar apiErr smithy.APIError\n\tif errors.As(err, &apiErr) {\n\t\treturn apiErr.ErrorCode() == \"BucketAlreadyOwnedByYou\" || apiErr.ErrorCode() == \"OperationAborted\"\n\t}\n\n\treturn false\n}\n\n// isBucketErrorRetriable returns true if the error is temporary and can be retried.\nfunc isBucketErrorRetriable(l log.Logger, err error) bool {\n\tvar apiErr smithy.APIError\n\tif errors.As(err, &apiErr) {\n\t\tunrecoverable := apiErr.ErrorCode() == \"InternalError\" || apiErr.ErrorCode() == \"OperationAborted\" || apiErr.ErrorCode() == \"InvalidParameter\"\n\n\t\tif !unrecoverable {\n\t\t\tl.Debugf(\n\t\t\t\t\"Encountered AWS API error '%s' during bucket creation. Assuming it's retriable and will retry.\",\n\t\t\t\tapiErr.ErrorCode(),\n\t\t\t)\n\t\t}\n\n\t\treturn unrecoverable\n\t}\n\n\tl.Debugf(\n\t\t\"Encountered error '%s'. Assuming it's retriable and will retry.\",\n\t\terr.Error(),\n\t)\n\n\treturn true\n}\n\n// EnableRootAccesstoS3Bucket adds a policy to allow root access to the bucket.\nfunc (client *Client) EnableRootAccesstoS3Bucket(ctx context.Context, l log.Logger) error {\n\tif client.ExtendedRemoteStateConfigS3 == nil {\n\t\treturn errors.Errorf(\"client configuration is nil - cannot enable root access to S3 bucket\")\n\t}\n\n\tif client.s3Client == nil {\n\t\treturn errors.Errorf(\"S3 client is nil - cannot enable root access to S3 bucket\")\n\t}\n\n\t// Access bucket name safely through defensive checking\n\tconfig := client.ExtendedRemoteStateConfigS3\n\n\tbucket := config.RemoteStateConfigS3.Bucket\n\tif bucket == \"\" {\n\t\treturn errors.Errorf(\"S3 bucket name is empty - cannot enable root access to S3 bucket\")\n\t}\n\n\tl.Debugf(\"Enabling root access to S3 bucket %s\", bucket)\n\n\tif client.awsConfig.Region == \"\" {\n\t\treturn errors.Errorf(\"AWS config region is empty - cannot enable root access to S3 bucket %s\", bucket)\n\t}\n\n\taccountID, err := awshelper.GetAWSAccountID(ctx, &client.awsConfig)\n\tif err != nil {\n\t\treturn errors.Errorf(\"error getting AWS account ID %s for bucket %s: %w\", accountID, bucket, err)\n\t}\n\n\tif accountID == \"\" {\n\t\treturn errors.Errorf(\"AWS account ID is empty - cannot enable root access to S3 bucket %s\", bucket)\n\t}\n\n\tpartition, err := awshelper.GetAWSPartition(ctx, &client.awsConfig)\n\tif err != nil {\n\t\treturn errors.Errorf(\"error getting AWS partition %s for bucket %s: %w\", partition, bucket, err)\n\t}\n\n\tif partition == \"\" {\n\t\treturn errors.Errorf(\"AWS partition is empty - cannot enable root access to S3 bucket %s\", bucket)\n\t}\n\n\tvar policyInBucket awshelper.Policy\n\n\tpolicyOutput, err := client.s3Client.GetBucketPolicy(ctx, &s3.GetBucketPolicyInput{\n\t\tBucket: aws.String(bucket),\n\t})\n\n\t// If there's no policy, we need to create one\n\tif err != nil {\n\t\tl.Debugf(\"Policy not exists for bucket %s\", bucket)\n\t}\n\n\tif policyOutput != nil && policyOutput.Policy != nil {\n\t\tl.Debugf(\"Policy already exists for bucket %s\", bucket)\n\n\t\tpolicyInBucket, err = awshelper.UnmarshalPolicy(*policyOutput.Policy)\n\t\tif err != nil {\n\t\t\treturn errors.Errorf(\"error unmarshalling policy for bucket %s: %w\", bucket, err)\n\t\t}\n\t}\n\n\t// Ensure Statement is never nil to avoid nil pointer dereference\n\tif policyInBucket.Statement == nil {\n\t\tpolicyInBucket.Statement = []awshelper.Statement{}\n\t}\n\n\t// Iterate over statements to check if root policy already exists\n\tfor _, statement := range policyInBucket.Statement {\n\t\tif statement.Sid == SidRootPolicy {\n\t\t\tl.Debugf(\"Policy for RootAccess already exists for bucket %s\", bucket)\n\t\t\treturn nil\n\t\t}\n\t}\n\n\trootS3Policy := awshelper.Policy{\n\t\tVersion: \"2012-10-17\",\n\t\tStatement: []awshelper.Statement{\n\t\t\t{\n\t\t\t\tSid:    SidRootPolicy,\n\t\t\t\tEffect: \"Allow\",\n\t\t\t\tAction: \"s3:*\",\n\t\t\t\tResource: []string{\n\t\t\t\t\t\"arn:\" + partition + \":s3:::\" + bucket,\n\t\t\t\t\t\"arn:\" + partition + \":s3:::\" + bucket + \"/*\",\n\t\t\t\t},\n\t\t\t\tPrincipal: map[string][]string{\n\t\t\t\t\t\"AWS\": {\n\t\t\t\t\t\t\"arn:\" + partition + \":iam::\" + accountID + \":root\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\t// Append the root s3 policy to the existing policy in the bucket\n\trootS3Policy.Statement = append(rootS3Policy.Statement, policyInBucket.Statement...)\n\n\tpolicy, err := awshelper.MarshalPolicy(rootS3Policy)\n\tif err != nil {\n\t\treturn errors.Errorf(\"error marshalling policy for bucket %s: %w\", bucket, err)\n\t}\n\n\t_, err = client.s3Client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{\n\t\tBucket: aws.String(bucket),\n\t\tPolicy: aws.String(string(policy)),\n\t})\n\tif err != nil {\n\t\treturn errors.Errorf(\"error putting policy for bucket %s: %w\", bucket, err)\n\t}\n\n\tl.Debugf(\"Enabled root access to bucket %s\", bucket)\n\n\treturn nil\n}\n\nfunc (client *Client) EnableEnforcedTLSAccesstoS3Bucket(ctx context.Context, l log.Logger, bucket string) error {\n\tpartition, err := awshelper.GetAWSPartition(ctx, &client.awsConfig)\n\tif err != nil {\n\t\treturn errors.Errorf(\"error getting AWS partition %s for bucket %s: %w\", partition, bucket, err)\n\t}\n\n\tpolicyOutput, err := client.s3Client.GetBucketPolicy(ctx, &s3.GetBucketPolicyInput{\n\t\tBucket: aws.String(bucket),\n\t})\n\tif err != nil {\n\t\tl.Debugf(\"Policy not exists for bucket %s\", bucket)\n\t}\n\n\tvar policyInBucket awshelper.Policy\n\tif policyOutput != nil && policyOutput.Policy != nil {\n\t\tpolicyInBucket, err = awshelper.UnmarshalPolicy(*policyOutput.Policy)\n\t\tif err != nil {\n\t\t\treturn errors.Errorf(\"error unmarshalling policy for bucket %s: %w\", bucket, err)\n\t\t}\n\t}\n\n\t// Ensure Statement is never nil to avoid nil pointer dereference\n\tif policyInBucket.Statement == nil {\n\t\tpolicyInBucket.Statement = []awshelper.Statement{}\n\t}\n\n\tfor _, statement := range policyInBucket.Statement {\n\t\tif statement.Sid == SidEnforcedTLSPolicy {\n\t\t\tl.Debugf(\"Policy for EnforcedTLS already exists for bucket %s\", bucket)\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tenforcedTLSPolicy := awshelper.Policy{\n\t\tVersion: \"2012-10-17\",\n\t\tStatement: []awshelper.Statement{\n\t\t\t{\n\t\t\t\tSid:    SidEnforcedTLSPolicy,\n\t\t\t\tEffect: \"Deny\",\n\t\t\t\tAction: \"s3:*\",\n\t\t\t\tResource: []string{\n\t\t\t\t\t\"arn:\" + partition + \":s3:::\" + bucket,\n\t\t\t\t\t\"arn:\" + partition + \":s3:::\" + bucket + \"/*\",\n\t\t\t\t},\n\t\t\t\tPrincipal: map[string][]string{\"*\": {\"*\"}},\n\t\t\t\tCondition: &map[string]any{\n\t\t\t\t\t\"Bool\": map[string]any{\"aws:SecureTransport\": \"false\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tenforcedTLSPolicy.Statement = append(enforcedTLSPolicy.Statement, policyInBucket.Statement...)\n\n\tpolicy, err := awshelper.MarshalPolicy(enforcedTLSPolicy)\n\tif err != nil {\n\t\treturn errors.Errorf(\"error marshalling policy for bucket %s: %w\", bucket, err)\n\t}\n\n\t_, err = client.s3Client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{\n\t\tBucket: aws.String(bucket),\n\t\tPolicy: aws.String(string(policy)),\n\t})\n\tif err != nil {\n\t\treturn errors.Errorf(\"error putting policy for bucket %s: %w\", bucket, err)\n\t}\n\n\tl.Debugf(\"Enabled enforced TLS access to bucket %s\", bucket)\n\n\treturn nil\n}\n\nfunc (client *Client) EnablePublicAccessBlockingForS3Bucket(ctx context.Context, l log.Logger, bucketName string) error {\n\tinput := &s3.PutPublicAccessBlockInput{\n\t\tBucket: aws.String(bucketName),\n\t\tPublicAccessBlockConfiguration: &types.PublicAccessBlockConfiguration{\n\t\t\tBlockPublicAcls:       aws.Bool(true),\n\t\t\tIgnorePublicAcls:      aws.Bool(true),\n\t\t\tBlockPublicPolicy:     aws.Bool(true),\n\t\t\tRestrictPublicBuckets: aws.Bool(true),\n\t\t},\n\t}\n\n\t_, err := client.s3Client.PutPublicAccessBlock(ctx, input)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tl.Debugf(\"Enabled public access blocking for S3 bucket %s\", bucketName)\n\n\treturn nil\n}\n\nfunc (client *Client) EnableAccessLoggingForS3BucketWide(ctx context.Context, l log.Logger) error {\n\tif client.ExtendedRemoteStateConfigS3 == nil {\n\t\treturn errors.Errorf(\"client configuration is nil - cannot enable access logging for S3 bucket\")\n\t}\n\n\tcfg := client.ExtendedRemoteStateConfigS3\n\tbucket := cfg.RemoteStateConfigS3.Bucket\n\tlogsBucket := cfg.AccessLoggingBucketName\n\tlogsBucketPrefix := cfg.AccessLoggingTargetPrefix\n\n\tif logsBucket == \"\" {\n\t\treturn errors.Errorf(\"AccessLoggingBucketName is required for bucket-wide Access Logging on AWS S3 bucket %s\", cfg.RemoteStateConfigS3.Bucket)\n\t}\n\n\tif !client.SkipAccessLoggingBucketACL {\n\t\tif err := client.configureBucketAccessLoggingACL(ctx, l, logsBucket); err != nil {\n\t\t\treturn errors.Errorf(\"error configuring bucket access logging ACL on S3 bucket %s: %w\", cfg.RemoteStateConfigS3.Bucket, err)\n\t\t}\n\t}\n\n\tloggingInput := client.CreateS3LoggingInput()\n\tl.Debugf(\"Putting bucket logging on S3 bucket %s with TargetBucket %s and TargetPrefix %s\\n%v\", bucket, logsBucket, logsBucketPrefix, loggingInput)\n\n\tif _, err := client.s3Client.PutBucketLogging(ctx, &loggingInput); err != nil {\n\t\treturn errors.Errorf(\"error enabling bucket-wide Access Logging on AWS S3 bucket %s: %w\", cfg.RemoteStateConfigS3.Bucket, err)\n\t}\n\n\tl.Debugf(\"Enabled bucket-wide Access Logging on AWS S3 bucket %s\", bucket)\n\n\treturn nil\n}\n\nfunc (client *Client) configureBucketAccessLoggingACL(ctx context.Context, l log.Logger, bucketName string) error {\n\tl.Debugf(\"Granting WRITE and READ_ACP permissions to S3 Log Delivery (%s) for bucket %s. This is required for access logging.\", s3LogDeliveryGranteeURI, bucketName)\n\n\turi := \"uri=\" + s3LogDeliveryGranteeURI\n\taclInput := s3.PutBucketAclInput{\n\t\tBucket:       aws.String(bucketName),\n\t\tGrantWrite:   aws.String(uri),\n\t\tGrantReadACP: aws.String(uri),\n\t}\n\n\tif _, err := client.s3Client.PutBucketAcl(ctx, &aclInput); err != nil {\n\t\treturn errors.Errorf(\"error granting WRITE and READ_ACP permissions to S3 Log Delivery (%s) for bucket %s: %w\", s3LogDeliveryGranteeURI, bucketName, err)\n\t}\n\n\treturn client.waitUntilBucketHasAccessLoggingACL(ctx, l, bucketName)\n}\n\nfunc (client *Client) waitUntilBucketHasAccessLoggingACL(ctx context.Context, l log.Logger, bucketName string) error {\n\tl.Debugf(\"Waiting for ACL bucket %s to have the updated ACL for access logging.\", bucketName)\n\n\tmaxRetries := 10\n\n\tfor range maxRetries {\n\t\tres, err := client.s3Client.GetBucketAcl(ctx, &s3.GetBucketAclInput{Bucket: aws.String(bucketName)})\n\t\tif err != nil {\n\t\t\treturn errors.Errorf(\"error getting ACL for bucket %s: %w\", bucketName, err)\n\t\t}\n\n\t\thasReadAcp := false\n\t\thasWrite := false\n\n\t\tfor _, grant := range res.Grants {\n\t\t\tif aws.ToString(grant.Grantee.URI) == s3LogDeliveryGranteeURI {\n\t\t\t\tif string(grant.Permission) == \"READ_ACP\" {\n\t\t\t\t\thasReadAcp = true\n\t\t\t\t}\n\n\t\t\t\tif string(grant.Permission) == \"WRITE\" {\n\t\t\t\t\thasWrite = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif hasReadAcp && hasWrite {\n\t\t\tl.Debugf(\"Bucket %s now has the proper ACL permissions for access logging!\", bucketName)\n\t\t\treturn nil\n\t\t}\n\n\t\tl.Debugf(\"Bucket %s still does not have the ACL permissions for access logging. Will sleep for %v and check again.\", bucketName, s3TimeBetweenRetries)\n\t\ttime.Sleep(s3TimeBetweenRetries)\n\t}\n\n\treturn errors.New(MaxRetriesWaitingForS3ACLExceeded(bucketName))\n}\n\n// checkBucketAccess checks if the current user has the ability to access the S3 bucket keys.\nfunc (client *Client) checkBucketAccess(ctx context.Context, bucket, key string) error {\n\t_, err := client.s3Client.GetObject(ctx, &s3.GetObjectInput{Key: aws.String(key), Bucket: aws.String(bucket)})\n\tif err == nil {\n\t\treturn nil\n\t}\n\n\tvar apiErr smithy.APIError\n\n\tif ok := errors.As(err, &apiErr); !ok {\n\t\treturn errors.Errorf(\"error checking access to S3 bucket %s: %w\", bucket, err)\n\t}\n\n\treturn errors.Errorf(\"error checking access to S3 bucket %s: %w\", bucket, err)\n}\n\n// DeleteS3BucketIfNecessary deletes the given S3 bucket with all its objects if it exists.\nfunc (client *Client) DeleteS3BucketIfNecessary(ctx context.Context, l log.Logger, bucketName string) error {\n\tif exists, err := client.DoesS3BucketExistWithLogging(ctx, l, bucketName); err != nil || !exists {\n\t\treturn err\n\t}\n\n\tdescription := fmt.Sprintf(\"Delete S3 bucket %s with retry\", bucketName)\n\n\treturn util.DoWithRetry(ctx, description, s3MaxRetries, s3SleepBetweenRetries, l, log.DebugLevel, func(ctx context.Context) error {\n\t\terr := client.DeleteS3BucketWithAllObjects(ctx, l, bucketName)\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tif isBucketErrorRetriable(l, err) {\n\t\t\treturn err\n\t\t}\n\t\t// return FatalError so that retry loop will not continue\n\t\treturn util.FatalError{Underlying: err}\n\t})\n}\n\n// DeleteS3BucketWithAllObjects deletes the given S3 bucket with all its objects.\nfunc (client *Client) DeleteS3BucketWithAllObjects(ctx context.Context, l log.Logger, bucketName string) error {\n\tl.Debugf(\"Delete S3 bucket %s with all objects.\", bucketName)\n\n\tif err := client.DeleteS3BucketObjects(ctx, l, bucketName); err != nil {\n\t\treturn err\n\t}\n\n\treturn client.DeleteS3Bucket(ctx, l, bucketName)\n}\n\n// DeleteS3BucketObject deletes S3 bucket object by the given key.\nfunc (client *Client) DeleteS3BucketObject(ctx context.Context, l log.Logger, bucketName, key string, versionID *string) error {\n\tobjectInput := &s3.DeleteObjectInput{\n\t\tBucket:    aws.String(bucketName),\n\t\tKey:       aws.String(key),\n\t\tVersionId: versionID,\n\t}\n\n\tif _, err := client.s3Client.DeleteObject(ctx, objectInput); err != nil {\n\t\treturn errors.Errorf(\"failed to delete object: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// DeleteS3BucketV2Objects deletes S3 bucket object by the given key.\nfunc (client *Client) DeleteS3BucketV2Objects(ctx context.Context, l log.Logger, bucketName string) error {\n\tvar v2Input = &s3.ListObjectsV2Input{Bucket: aws.String(bucketName)}\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tres, err := client.s3Client.ListObjectsV2(ctx, v2Input)\n\t\tif err != nil {\n\t\t\treturn errors.Errorf(\"failed to list objects: %w\", err)\n\t\t}\n\n\t\tfor _, item := range res.Contents {\n\t\t\tif err := client.DeleteS3BucketObject(ctx, l, bucketName, aws.ToString(item.Key), nil); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif !aws.ToBool(res.IsTruncated) {\n\t\t\tbreak\n\t\t}\n\n\t\tv2Input.ContinuationToken = res.ContinuationToken\n\t}\n\n\treturn nil\n}\n\n// DeleteS3BucketVersionObjects deletes S3 bucket object versions by the given key.\nfunc (client *Client) DeleteS3BucketVersionObjects(ctx context.Context, l log.Logger, bucketName string, keys ...string) error {\n\tvar versionsInput = &s3.ListObjectVersionsInput{Bucket: aws.String(bucketName)}\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tres, err := client.s3Client.ListObjectVersions(ctx, versionsInput)\n\t\tif err != nil {\n\t\t\treturn errors.Errorf(\"failed to list version objects: %w\", err)\n\t\t}\n\n\t\tfor _, item := range res.DeleteMarkers {\n\t\t\tif len(keys) != 0 && !slices.Contains(keys, aws.ToString(item.Key)) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif err := client.DeleteS3BucketObject(ctx, l, bucketName, aws.ToString(item.Key), item.VersionId); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tfor i := range res.Versions {\n\t\t\titem := &res.Versions[i]\n\t\t\tif len(keys) != 0 && !slices.Contains(keys, aws.ToString(item.Key)) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif err := client.DeleteS3BucketObject(ctx, l, bucketName, aws.ToString(item.Key), item.VersionId); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif !aws.ToBool(res.IsTruncated) {\n\t\t\tbreak\n\t\t}\n\n\t\tversionsInput.VersionIdMarker = res.NextVersionIdMarker\n\t\tversionsInput.KeyMarker = res.NextKeyMarker\n\t}\n\n\treturn nil\n}\n\n// DeleteS3BucketObjects deletes the S3 bucket contents.\nfunc (client *Client) DeleteS3BucketObjects(ctx context.Context, l log.Logger, bucketName string) error {\n\tif err := client.DeleteS3BucketV2Objects(ctx, l, bucketName); err != nil {\n\t\treturn err\n\t}\n\n\treturn client.DeleteS3BucketVersionObjects(ctx, l, bucketName)\n}\n\n// DeleteS3Bucket deletes the S3 bucket specified in the given config.\nfunc (client *Client) DeleteS3Bucket(ctx context.Context, l log.Logger, bucketName string) error {\n\tvar (\n\t\tcfg         = &client.ExtendedRemoteStateConfigS3.RemoteStateConfigS3\n\t\tkey         = cfg.Key\n\t\tbucketInput = &s3.DeleteBucketInput{Bucket: aws.String(bucketName)}\n\t)\n\n\tl.Debugf(\"Deleting S3 bucket %s\", bucketName)\n\n\tif _, err := client.s3Client.DeleteBucket(ctx, bucketInput); err != nil {\n\t\tif err := client.checkBucketAccess(ctx, bucketName, key); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn errors.New(err)\n\t}\n\n\tl.Debugf(\"Deleted S3 bucket %s\", bucketName)\n\n\treturn client.WaitUntilS3BucketDeleted(ctx, l, bucketName)\n}\n\n// WaitUntilS3BucketDeleted waits until the given S3 bucket is deleted.\nfunc (client *Client) WaitUntilS3BucketDeleted(ctx context.Context, l log.Logger, bucketName string) error {\n\tl.Debugf(\"Waiting for bucket %s to be deleted\", bucketName)\n\n\tfor retries := range maxRetriesWaitingForS3Bucket {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tdefault:\n\t\t}\n\n\t\tif exists, err := client.DoesS3BucketExist(ctx, bucketName); err != nil {\n\t\t\treturn err\n\t\t} else if !exists {\n\t\t\tl.Debugf(\"S3 bucket %s deleted.\", bucketName)\n\t\t\treturn nil\n\t\t} else if retries < maxRetriesWaitingForS3Bucket-1 {\n\t\t\tl.Debugf(\"S3 bucket %s has not been deleted yet. Sleeping for %s and will check again.\", bucketName, sleepBetweenRetriesWaitingForS3Bucket)\n\t\t\ttime.Sleep(sleepBetweenRetriesWaitingForS3Bucket)\n\t\t}\n\t}\n\n\treturn errors.New(MaxRetriesWaitingForS3BucketExceeded(bucketName))\n}\n\n// DeleteS3ObjectIfNecessary deletes the S3 object by the specified key if it exists.\nfunc (client *Client) DeleteS3ObjectIfNecessary(ctx context.Context, l log.Logger, bucketName, key string) error {\n\tif exists, err := client.DoesS3BucketExistWithLogging(ctx, l, bucketName); err != nil || !exists {\n\t\treturn err\n\t}\n\n\tif exists, err := client.DoesS3ObjectExist(ctx, bucketName, key); err != nil || !exists {\n\t\treturn err\n\t}\n\n\tdescription := fmt.Sprintf(\"Delete S3 object %s in bucket %s with retry\", key, bucketName)\n\n\treturn util.DoWithRetry(ctx, description, s3MaxRetries, s3SleepBetweenRetries, l, log.DebugLevel, func(ctx context.Context) error {\n\t\tif err := client.DeleteS3BucketObject(ctx, l, bucketName, key, nil); err != nil {\n\t\t\tif isBucketErrorRetriable(l, err) {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// return FatalError so that retry loop will not continue\n\t\t\treturn util.FatalError{Underlying: err}\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\n// DoesS3ObjectExist returns true if the specified S3 object exists otherwise false.\nfunc (client *Client) DoesS3ObjectExist(ctx context.Context, bucketName, key string) (bool, error) {\n\tinput := &s3.HeadObjectInput{\n\t\tBucket: aws.String(bucketName),\n\t\tKey:    aws.String(key),\n\t}\n\n\tif _, err := client.s3Client.HeadObject(ctx, input); err != nil {\n\t\tvar apiErr smithy.APIError\n\t\tif ok := errors.As(err, &apiErr); ok {\n\t\t\tif apiErr.ErrorCode() == \"NotFound\" { // s3.ErrCodeNoSuchKey does not work, aws is missing this error code so we hardwire a string\n\t\t\t\treturn false, nil\n\t\t\t}\n\t\t}\n\n\t\treturn false, err\n\t}\n\n\treturn true, nil\n}\n\nfunc (client *Client) DoesS3ObjectExistWithLogging(ctx context.Context, l log.Logger, bucketName, key string) (bool, error) {\n\tif client.s3Client == nil {\n\t\treturn false, errors.Errorf(\"S3 client is nil - cannot check if S3 bucket %s exists\", bucketName)\n\t}\n\n\tl.Debugf(\"Checking if bucket %s exists\", bucketName)\n\n\tif exists, err := client.DoesS3ObjectExist(ctx, bucketName, key); err != nil || exists {\n\t\treturn exists, err\n\t}\n\n\tl.Debugf(\"Remote state S3 bucket %s object %s does not exist or you don't have permissions to access it.\", bucketName, key)\n\n\treturn false, nil\n}\n\n// CreateLockTableIfNecessary creates the lock table in DynamoDB if it doesn't already exist.\nfunc (client *Client) CreateLockTableIfNecessary(ctx context.Context, l log.Logger, tableName string, tags map[string]string) error {\n\ttableExists, err := client.DoesLockTableExistAndIsActive(ctx, tableName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !tableExists {\n\t\tl.Debugf(\"Lock table %s does not exist in DynamoDB. Will need to create it just this first time.\", tableName)\n\t\treturn client.CreateLockTable(ctx, l, tableName, tags)\n\t}\n\n\treturn nil\n}\n\n// DeleteTableIfNecessary deletes the given table if it exists.\nfunc (client *Client) DeleteTableIfNecessary(ctx context.Context, l log.Logger, tableName string) error {\n\tif exists, err := client.DoesLockTableExist(ctx, tableName); err != nil || !exists {\n\t\treturn err\n\t}\n\n\treturn client.DeleteTable(ctx, l, tableName)\n}\n\n// DoesLockTableExistAndIsActive returns true if the specified DynamoDB table exists and is active otherwise false.\nfunc (client *Client) DoesLockTableExistAndIsActive(ctx context.Context, tableName string) (bool, error) {\n\tinput := &dynamodb.DescribeTableInput{\n\t\tTableName: aws.String(tableName),\n\t}\n\n\tres, err := client.dynamoClient.DescribeTable(ctx, input)\n\tif err != nil {\n\t\tif isAWSResourceNotFoundError(err) {\n\t\t\t// Table doesn't exist, so it's not active\n\t\t\treturn false, nil\n\t\t}\n\n\t\treturn false, err\n\t}\n\n\treturn res.Table.TableStatus == dynamodbtypes.TableStatusActive, nil\n}\n\n// DoesLockTableExist returns true if the lock table exists.\nfunc (client *Client) DoesLockTableExist(ctx context.Context, tableName string) (bool, error) {\n\tinput := &dynamodb.DescribeTableInput{\n\t\tTableName: aws.String(tableName),\n\t}\n\n\t_, err := client.dynamoClient.DescribeTable(ctx, input)\n\tif err != nil {\n\t\tif isAWSResourceNotFoundError(err) {\n\t\t\treturn false, nil\n\t\t} else {\n\t\t\treturn false, errors.New(err)\n\t\t}\n\t}\n\n\treturn true, nil\n}\n\n// LockTableCheckSSEncryptionIsOn returns true if the lock table's SSEncryption is turned on\nfunc (client *Client) LockTableCheckSSEncryptionIsOn(ctx context.Context, tableName string) (bool, error) {\n\tinput := &dynamodb.DescribeTableInput{\n\t\tTableName: aws.String(tableName),\n\t}\n\n\toutput, err := client.dynamoClient.DescribeTable(ctx, input)\n\tif err != nil {\n\t\tif isAWSResourceNotFoundError(err) {\n\t\t\t// Table doesn't exist, so encryption is not enabled\n\t\t\treturn false, nil\n\t\t}\n\n\t\treturn false, errors.New(err)\n\t}\n\n\treturn output.Table.SSEDescription != nil && string(output.Table.SSEDescription.Status) == string(dynamodbtypes.SSEStatusEnabled), nil\n}\n\n// CreateLockTable creates a lock table in DynamoDB and wait until it is in \"active\" state.\n// If the table already exists, merely wait until it is in \"active\" state.\nfunc (client *Client) CreateLockTable(ctx context.Context, l log.Logger, tableName string, tags map[string]string) error {\n\ttableCreateDeleteSemaphore.Acquire()\n\tdefer tableCreateDeleteSemaphore.Release()\n\n\tl.Debugf(\"Creating table %s in DynamoDB\", tableName)\n\n\tattributeDefinitions := []dynamodbtypes.AttributeDefinition{\n\t\t{AttributeName: aws.String(AttrLockID), AttributeType: dynamodbtypes.ScalarAttributeTypeS},\n\t}\n\n\tkeySchema := []dynamodbtypes.KeySchemaElement{\n\t\t{AttributeName: aws.String(AttrLockID), KeyType: dynamodbtypes.KeyTypeHash},\n\t}\n\n\tinput := &dynamodb.CreateTableInput{\n\t\tTableName:            aws.String(tableName),\n\t\tBillingMode:          dynamodbtypes.BillingMode(DynamodbPayPerRequestBillingMode),\n\t\tAttributeDefinitions: attributeDefinitions,\n\t\tKeySchema:            keySchema,\n\t}\n\n\tcreateTableOutput, err := client.dynamoClient.CreateTable(ctx, input)\n\tif err != nil {\n\t\tif isTableAlreadyBeingCreatedOrUpdatedError(err) {\n\t\t\tl.Debugf(\"Looks like someone created table %s at the same time. Will wait for it to be in active state.\", tableName)\n\t\t} else {\n\t\t\treturn errors.New(err)\n\t\t}\n\t}\n\n\terr = client.waitForTableToBeActive(ctx, l, tableName, MaxRetriesWaitingForTableToBeActive, SleepBetweenTableStatusChecks)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif createTableOutput != nil && createTableOutput.TableDescription != nil && createTableOutput.TableDescription.TableArn != nil {\n\t\t// Do not tag in case somebody else had created the table\n\t\terr = client.tagTableIfTagsGiven(ctx, l, tags, createTableOutput.TableDescription.TableArn)\n\t\tif err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (client *Client) tagTableIfTagsGiven(ctx context.Context, l log.Logger, tags map[string]string, tableArn *string) error {\n\tif len(tags) == 0 {\n\t\tl.Debugf(\"No tags for lock table given.\")\n\t\treturn nil\n\t}\n\n\t// we were able to create the table successfully, now add tags\n\tl.Debugf(\"Adding tags to lock table: %s\", tags)\n\n\tvar tagsConverted = make([]dynamodbtypes.Tag, 0, len(tags))\n\n\tfor k, v := range tags {\n\t\ttagsConverted = append(tagsConverted, dynamodbtypes.Tag{Key: aws.String(k), Value: aws.String(v)})\n\t}\n\n\tvar input = dynamodb.TagResourceInput{\n\t\tResourceArn: tableArn,\n\t\tTags:        tagsConverted}\n\n\t_, err := client.dynamoClient.TagResource(ctx, &input)\n\n\treturn err\n}\n\n// DeleteTable deletes the given table in DynamoDB.\nfunc (client *Client) DeleteTable(ctx context.Context, l log.Logger, tableName string) error {\n\ttableCreateDeleteSemaphore.Acquire()\n\tdefer tableCreateDeleteSemaphore.Release()\n\n\tl.Debugf(\"Deleting DynamoD table %s\", tableName)\n\n\tinput := &dynamodb.DeleteTableInput{TableName: aws.String(tableName)}\n\n\t// It is not always able to delete a table the first attempt, as we can get a 400 from tags still being updated\n\t// while the table is being deleted.\n\t//\n\t// We retry to handle this race condition.\n\tconst (\n\t\tmaxRetries = 5\n\t\tdelay      = 2 * time.Second\n\t)\n\n\tfor i := range maxRetries {\n\t\t_, err := client.dynamoClient.DeleteTable(ctx, input)\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tif isTableAlreadyBeingCreatedOrUpdatedError(err) {\n\t\t\tif i < maxRetries-1 {\n\t\t\t\tl.Debugf(\"Table %s is still being updated (likely tags). Will retry deletion after %s (attempt %d/%d)\", tableName, delay, i+1, maxRetries)\n\t\t\t\ttime.Sleep(delay)\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\treturn err\n\t}\n\n\treturn errors.\n\t\tErrorf(\"Failed to delete table %s after %d attempts\", tableName, maxRetries)\n}\n\n// Return true if the given error is the error message returned by AWS when the resource already exists and is being\n// updated by someone else\nfunc isTableAlreadyBeingCreatedOrUpdatedError(err error) bool {\n\tvar apiErr smithy.APIError\n\n\tok := errors.As(err, &apiErr)\n\n\treturn ok && apiErr.ErrorCode() == \"ResourceInUseException\"\n}\n\n// Wait for the given DynamoDB table to be in the \"active\" state. If it's not in \"active\" state, sleep for the\n// specified amount of time, and try again, up to a maximum of maxRetries retries.\nfunc (client *Client) waitForTableToBeActive(ctx context.Context, l log.Logger, tableName string, maxRetries int, sleepBetweenRetries time.Duration) error {\n\treturn client.WaitForTableToBeActiveWithRandomSleep(ctx, l, tableName, maxRetries, sleepBetweenRetries, sleepBetweenRetries)\n}\n\n// WaitForTableToBeActiveWithRandomSleep waits for the given table as described above,\n// but sleeps a random amount of time greater than sleepBetweenRetriesMin\n// and less than sleepBetweenRetriesMax between tries. This is to avoid an AWS issue where all waiting requests fire at\n// the same time, which continually triggered AWS's \"subscriber limit exceeded\" API error.\nfunc (client *Client) WaitForTableToBeActiveWithRandomSleep(ctx context.Context, l log.Logger, tableName string, maxRetries int, sleepBetweenRetriesMin time.Duration, sleepBetweenRetriesMax time.Duration) error {\n\tfor range maxRetries {\n\t\ttableReady, err := client.DoesLockTableExistAndIsActive(ctx, tableName)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif tableReady {\n\t\t\tl.Debugf(\"Success! Table %s is now in active state.\", tableName)\n\t\t\treturn nil\n\t\t}\n\n\t\tsleepBetweenRetries := util.GetRandomTime(sleepBetweenRetriesMin, sleepBetweenRetriesMax)\n\t\tl.Debugf(\"Table %s is not yet in active state. Will check again after %s.\", tableName, sleepBetweenRetries)\n\t\ttime.Sleep(sleepBetweenRetries)\n\t}\n\n\treturn errors.New(TableActiveRetriesExceeded{TableName: tableName, Retries: maxRetries})\n}\n\n// UpdateLockTableSetSSEncryptionOnIfNecessary encrypts the TFState Lock table - If Necessary\nfunc (client *Client) UpdateLockTableSetSSEncryptionOnIfNecessary(ctx context.Context, l log.Logger, tableName string) error {\n\ttableSSEncrypted, err := client.LockTableCheckSSEncryptionIsOn(ctx, tableName)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tif tableSSEncrypted {\n\t\tl.Debugf(\"Table %s already has encryption enabled\", tableName)\n\t\treturn nil\n\t}\n\n\ttableCreateDeleteSemaphore.Acquire()\n\tdefer tableCreateDeleteSemaphore.Release()\n\n\tl.Debugf(\"Enabling server-side encryption on table %s in AWS DynamoDB\", tableName)\n\n\tinput := &dynamodb.UpdateTableInput{\n\t\tSSESpecification: &dynamodbtypes.SSESpecification{\n\t\t\tEnabled: aws.Bool(true),\n\t\t\tSSEType: dynamodbtypes.SSETypeKms,\n\t\t},\n\t\tTableName: aws.String(tableName),\n\t}\n\n\tif _, err := client.dynamoClient.UpdateTable(ctx, input); err != nil {\n\t\tif isTableAlreadyBeingCreatedOrUpdatedError(err) {\n\t\t\tl.Debugf(\"Looks like someone is already updating table %s at the same time. Will wait for that update to complete.\", tableName)\n\t\t} else {\n\t\t\treturn errors.New(err)\n\t\t}\n\t}\n\n\tif err := client.waitForEncryptionToBeEnabled(ctx, l, tableName); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn client.waitForTableToBeActive(ctx, l, tableName, MaxRetriesWaitingForTableToBeActive, SleepBetweenTableStatusChecks)\n}\n\n// Wait until encryption is enabled for the given table\nfunc (client *Client) waitForEncryptionToBeEnabled(ctx context.Context, l log.Logger, tableName string) error {\n\tl.Debugf(\"Waiting for encryption to be enabled on table %s\", tableName)\n\n\tfor range maxRetriesWaitingForEncryption {\n\t\ttableSSEncrypted, err := client.LockTableCheckSSEncryptionIsOn(ctx, tableName)\n\t\tif err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\tif tableSSEncrypted {\n\t\t\tl.Debugf(\"Encryption is now enabled for table %s!\", tableName)\n\t\t\treturn nil\n\t\t}\n\n\t\tl.Debugf(\"Encryption is still not enabled for table %s. Will sleep for %v and try again.\", tableName, sleepBetweenRetriesWaitingForEncryption)\n\t\ttime.Sleep(sleepBetweenRetriesWaitingForEncryption)\n\t}\n\n\treturn errors.New(TableEncryptedRetriesExceeded{TableName: tableName, Retries: maxRetriesWaitingForEncryption})\n}\n\n// DeleteTableItemIfNecessary deletes the given DynamoDB table key, if the table exists.\nfunc (client *Client) DeleteTableItemIfNecessary(ctx context.Context, l log.Logger, tableName, key string) error {\n\tif exists, err := client.DoesTableItemExist(ctx, tableName, key); err != nil || !exists {\n\t\treturn err\n\t}\n\n\tdescription := fmt.Sprintf(\"Delete DynamoDB table %s item %s\", tableName, key)\n\n\treturn util.DoWithRetry(ctx, description, s3MaxRetries, s3SleepBetweenRetries, l, log.DebugLevel, func(ctx context.Context) error {\n\t\tif err := client.DeleteTableItem(ctx, l, tableName, key); err != nil {\n\t\t\tif isBucketErrorRetriable(l, err) {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// return FatalError so that retry loop will not continue\n\t\t\treturn util.FatalError{Underlying: err}\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\n// DeleteTableItem deletes the given DynamoDB table key.\nfunc (client *Client) DeleteTableItem(ctx context.Context, l log.Logger, tableName, key string) error {\n\tl.Debugf(\"Deleting DynamoDB table %s item %s\", tableName, key)\n\n\tinput := &dynamodb.DeleteItemInput{\n\t\tTableName: aws.String(tableName),\n\t\tKey: map[string]dynamodbtypes.AttributeValue{\n\t\t\tAttrLockID: &dynamodbtypes.AttributeValueMemberS{\n\t\t\t\tValue: key,\n\t\t\t},\n\t\t},\n\t}\n\n\tif _, err := client.dynamoClient.DeleteItem(ctx, input); err != nil {\n\t\treturn errors.Errorf(\"failed to remove item by key %s of table %s: %w\", key, tableName, err)\n\t}\n\n\treturn nil\n}\n\n// DoesTableItemExist returns true if the given DynamoDB table and its key exist otherwise false.\nfunc (client *Client) DoesTableItemExist(ctx context.Context, tableName, key string) (bool, error) {\n\tif exists, err := client.DoesLockTableExist(ctx, tableName); err != nil || !exists {\n\t\treturn false, err\n\t}\n\n\tinput := &dynamodb.GetItemInput{\n\t\tTableName: aws.String(tableName),\n\t\tKey: map[string]dynamodbtypes.AttributeValue{\n\t\t\tAttrLockID: &dynamodbtypes.AttributeValueMemberS{\n\t\t\t\tValue: key,\n\t\t\t},\n\t\t},\n\t}\n\n\tres, err := client.dynamoClient.GetItem(ctx, input)\n\tif err != nil {\n\t\treturn false, errors.Errorf(\"failed to get item by key %s of table %s: %w\", key, tableName, err)\n\t}\n\n\texists := len(res.Item) != 0\n\n\treturn exists, nil\n}\n\n// MoveS3Object copies the S3 object at the specified srcKey to dstKey and then removes srcKey.\nfunc (client *Client) MoveS3Object(ctx context.Context, l log.Logger, srcBucketName, srcKey, dstBucketName, dstKey string) error {\n\tif err := client.CopyS3BucketObject(ctx, l, srcBucketName, srcKey, dstBucketName, dstKey); err != nil {\n\t\treturn err\n\t}\n\n\treturn client.DeleteS3BucketObject(ctx, l, srcBucketName, srcKey, nil)\n}\n\n// CreateTableItem creates a new table item `key` in DynamoDB.\nfunc (client *Client) CreateTableItem(ctx context.Context, l log.Logger, tableName, key string) error {\n\tl.Debugf(\"Creating DynamoDB %s item %s\", tableName, key)\n\n\tinput := &dynamodb.PutItemInput{\n\t\tTableName: aws.String(tableName),\n\t\tItem: map[string]dynamodbtypes.AttributeValue{\n\t\t\tAttrLockID: &dynamodbtypes.AttributeValueMemberS{Value: key},\n\t\t},\n\t}\n\tif _, err := client.dynamoClient.PutItem(ctx, input); err != nil {\n\t\treturn errors.Errorf(\"failed to create table item: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// EnableVersioningForS3Bucket enables versioning for the S3 bucket specified in the given config.\nfunc (client *Client) EnableVersioningForS3Bucket(ctx context.Context, l log.Logger, bucketName string) error {\n\tl.Debugf(\"Enabling versioning for S3 bucket %s\", bucketName)\n\tinput := s3.PutBucketVersioningInput{\n\t\tBucket: aws.String(bucketName),\n\t\tVersioningConfiguration: &types.VersioningConfiguration{\n\t\t\tStatus: types.BucketVersioningStatusEnabled,\n\t\t},\n\t}\n\n\t_, err := client.s3Client.PutBucketVersioning(ctx, &input)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tl.Debugf(\"Enabled versioning for S3 bucket %s\", bucketName)\n\n\treturn nil\n}\n\n// EnableSSEForS3BucketWide enables server-side encryption for the S3 bucket specified in the given config.\nfunc (client *Client) EnableSSEForS3BucketWide(ctx context.Context, l log.Logger, bucketName string, algorithm string) error {\n\tl.Debugf(\"Enabling server-side encryption for S3 bucket %s\", bucketName)\n\n\taccountID, err := awshelper.GetAWSAccountID(ctx, &client.awsConfig)\n\tif err != nil {\n\t\treturn errors.Errorf(\"error getting AWS account ID %s for bucket %s: %w\", accountID, bucketName, err)\n\t}\n\n\tpartition, err := awshelper.GetAWSPartition(ctx, &client.awsConfig)\n\tif err != nil {\n\t\treturn errors.Errorf(\"error getting AWS partition %s for bucket %s: %w\", partition, bucketName, err)\n\t}\n\n\tinput := &s3.PutBucketEncryptionInput{\n\t\tBucket: aws.String(bucketName),\n\t\tServerSideEncryptionConfiguration: &types.ServerSideEncryptionConfiguration{\n\t\t\tRules: []types.ServerSideEncryptionRule{\n\t\t\t\t{\n\t\t\t\t\tApplyServerSideEncryptionByDefault: &types.ServerSideEncryptionByDefault{\n\t\t\t\t\t\tSSEAlgorithm: types.ServerSideEncryption(algorithm),\n\t\t\t\t\t},\n\t\t\t\t\tBucketKeyEnabled: aws.Bool(true),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\t// If using KMS encryption and a specific KMS key ID is configured, set it\n\tif algorithm == string(types.ServerSideEncryptionAwsKms) && client.BucketSSEKMSKeyID != \"\" {\n\t\tinput.ServerSideEncryptionConfiguration.Rules[0].ApplyServerSideEncryptionByDefault.KMSMasterKeyID = aws.String(client.BucketSSEKMSKeyID)\n\t}\n\n\t_, err = client.s3Client.PutBucketEncryption(ctx, input)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tl.Debugf(\"Enabled server-side encryption for S3 bucket %s\", bucketName)\n\n\treturn nil\n}\n\n// checkIfSSEForS3MatchesConfig checks if the SSE configuration matches the expected configuration.\nfunc (client *Client) checkIfSSEForS3MatchesConfig(ctx context.Context, bucketName string) (bool, error) {\n\toutput, err := client.s3Client.GetBucketEncryption(ctx, &s3.GetBucketEncryptionInput{\n\t\tBucket: aws.String(bucketName),\n\t})\n\tif err != nil {\n\t\tvar apiErr smithy.APIError\n\t\tif errors.As(err, &apiErr) && apiErr.ErrorCode() == \"ServerSideEncryptionConfigurationNotFoundError\" {\n\t\t\treturn false, nil\n\t\t}\n\n\t\treturn false, errors.New(err)\n\t}\n\n\texpectedAlgorithm := client.FetchEncryptionAlgorithm()\n\n\tfor _, rule := range output.ServerSideEncryptionConfiguration.Rules {\n\t\tif rule.ApplyServerSideEncryptionByDefault == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif string(rule.ApplyServerSideEncryptionByDefault.SSEAlgorithm) != expectedAlgorithm {\n\t\t\tcontinue\n\t\t}\n\n\t\tif expectedAlgorithm != string(types.ServerSideEncryptionAwsKms) {\n\t\t\treturn true, nil\n\t\t}\n\n\t\tif client.BucketSSEKMSKeyID == \"\" {\n\t\t\treturn true, nil\n\t\t}\n\n\t\tif rule.ApplyServerSideEncryptionByDefault.KMSMasterKeyID == nil {\n\t\t\treturn false, nil\n\t\t}\n\n\t\tif aws.ToString(rule.ApplyServerSideEncryptionByDefault.KMSMasterKeyID) != client.BucketSSEKMSKeyID {\n\t\t\treturn false, nil\n\t\t}\n\n\t\treturn true, nil\n\t}\n\n\treturn false, nil\n}\n\n// checkIfBucketPolicyStatementExists checks if a specific policy statement exists in the bucket policy\nfunc (client *Client) checkIfBucketPolicyStatementExists(ctx context.Context, bucketName, statementSid string) (bool, error) {\n\tpolicyOutput, err := client.s3Client.GetBucketPolicy(ctx, &s3.GetBucketPolicyInput{\n\t\tBucket: aws.String(bucketName),\n\t})\n\tif err != nil {\n\t\tvar apiErr smithy.APIError\n\t\tif errors.As(err, &apiErr) && apiErr.ErrorCode() == \"NoSuchBucketPolicy\" {\n\t\t\treturn false, nil\n\t\t}\n\n\t\treturn false, errors.New(err)\n\t}\n\n\tif policyOutput.Policy == nil {\n\t\treturn false, nil\n\t}\n\n\tpolicyInBucket, err := awshelper.UnmarshalPolicy(*policyOutput.Policy)\n\tif err != nil {\n\t\treturn false, errors.New(err)\n\t}\n\n\t// Safety check to avoid nil pointer dereference\n\tif policyInBucket.Statement == nil {\n\t\treturn false, nil\n\t}\n\n\tfor _, statement := range policyInBucket.Statement {\n\t\tif statement.Sid == statementSid {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\treturn false, nil\n}\n\n// checkIfBucketRootAccess checks if the root access policy is enabled for the bucket.\nfunc (client *Client) checkIfBucketRootAccess(ctx context.Context, l log.Logger, bucketName string) (bool, error) {\n\tl.Debugf(\"Checking if bucket %s has root access\", bucketName)\n\treturn client.checkIfBucketPolicyStatementExists(ctx, bucketName, SidRootPolicy)\n}\n\n// checkIfBucketEnforcedTLS checks if the enforced TLS policy is enabled for the bucket.\nfunc (client *Client) checkIfBucketEnforcedTLS(ctx context.Context, l log.Logger, bucketName string) (bool, error) {\n\tl.Debugf(\"Checking if bucket %s has enforced TLS\", bucketName)\n\treturn client.checkIfBucketPolicyStatementExists(ctx, bucketName, SidEnforcedTLSPolicy)\n}\n\n// DoesS3BucketExist checks if the S3 bucket exists and is accessible.\nfunc (client *Client) DoesS3BucketExist(ctx context.Context, bucketName string) (bool, error) {\n\tinput := &s3.HeadBucketInput{Bucket: aws.String(bucketName)}\n\n\t_, err := client.s3Client.HeadBucket(ctx, input)\n\tif err != nil {\n\t\tvar apiErr smithy.APIError\n\t\tif errors.As(err, &apiErr) && apiErr.ErrorCode() == \"NotFound\" {\n\t\t\treturn false, nil\n\t\t}\n\n\t\treturn false, errors.New(err)\n\t}\n\n\treturn true, nil\n}\n\n// DoesS3BucketExistWithLogging checks if the S3 bucket exists and logs if not.\nfunc (client *Client) DoesS3BucketExistWithLogging(ctx context.Context, l log.Logger, bucketName string) (bool, error) {\n\tif client.s3Client == nil {\n\t\treturn false, errors.Errorf(\"S3 client is nil - cannot check if S3 bucket %s exists\", bucketName)\n\t}\n\n\tl.Debugf(\"Checking if bucket %s exists\", bucketName)\n\n\texists, err := client.DoesS3BucketExist(ctx, bucketName)\n\tif err != nil || !exists {\n\t\tl.Debugf(\"Remote state S3 bucket %s does not exist or you don't have permissions to access it.\", bucketName)\n\t}\n\n\treturn exists, err\n}\n\n// checkS3AccessLoggingConfiguration checks if access logging is enabled for the S3 bucket.\nfunc (client *Client) checkS3AccessLoggingConfiguration(ctx context.Context, bucketName string) (bool, error) {\n\tinput := &s3.GetBucketLoggingInput{Bucket: aws.String(bucketName)}\n\n\toutput, err := client.s3Client.GetBucketLogging(ctx, input)\n\tif err != nil {\n\t\treturn false, errors.New(err)\n\t}\n\n\treturn output.LoggingEnabled != nil, nil\n}\n\n// checkIfS3PublicAccessBlockingEnabled checks if public access blocking is enabled for the S3 bucket.\nfunc (client *Client) checkIfS3PublicAccessBlockingEnabled(ctx context.Context, bucketName string) (bool, error) {\n\toutput, err := client.s3Client.GetPublicAccessBlock(ctx, &s3.GetPublicAccessBlockInput{\n\t\tBucket: aws.String(bucketName),\n\t})\n\tif err != nil {\n\t\tvar apiErr smithy.APIError\n\t\tif errors.As(err, &apiErr) && apiErr.ErrorCode() == \"NoSuchPublicAccessBlockConfiguration\" {\n\t\t\treturn false, nil\n\t\t}\n\n\t\treturn false, errors.New(err)\n\t}\n\n\treturn output.PublicAccessBlockConfiguration != nil &&\n\t\taws.ToBool(output.PublicAccessBlockConfiguration.BlockPublicAcls) &&\n\t\taws.ToBool(output.PublicAccessBlockConfiguration.IgnorePublicAcls) &&\n\t\taws.ToBool(output.PublicAccessBlockConfiguration.BlockPublicPolicy) &&\n\t\taws.ToBool(output.PublicAccessBlockConfiguration.RestrictPublicBuckets), nil\n}\n\n// CopyS3BucketObject copies the S3 object at the specified srcBucketName and srcKey to dstBucketName and dstKey.\nfunc (client *Client) CopyS3BucketObject(ctx context.Context, l log.Logger, srcBucketName, srcKey, dstBucketName, dstKey string) error {\n\tl.Debugf(\"Copying S3 bucket object from %s to %s\", path.Join(srcBucketName, srcKey), path.Join(dstBucketName, dstKey))\n\n\tinput := &s3.CopyObjectInput{\n\t\tBucket:     aws.String(dstBucketName),\n\t\tKey:        aws.String(dstKey),\n\t\tCopySource: aws.String(path.Join(srcBucketName, srcKey)),\n\t}\n\tif _, err := client.s3Client.CopyObject(ctx, input); err != nil {\n\t\treturn errors.Errorf(\"failed to copy object: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// MoveS3ObjectIfNecessary moves the S3 object at the specified srcBucketName and srcKey to dstBucketName and dstKey, only if it exists and does not already exist at the destination.\nfunc (client *Client) MoveS3ObjectIfNecessary(ctx context.Context, l log.Logger, srcBucketName, srcKey, dstBucketName, dstKey string) error {\n\texists, err := client.DoesS3ObjectExistWithLogging(ctx, l, srcBucketName, srcKey)\n\tif err != nil || !exists {\n\t\treturn err\n\t}\n\n\texists, err = client.DoesS3ObjectExist(ctx, dstBucketName, dstKey)\n\tif err != nil {\n\t\treturn err\n\t} else if exists {\n\t\treturn errors.Errorf(\"destination S3 bucket %s object %s already exists\", dstBucketName, dstKey)\n\t}\n\n\tdescription := fmt.Sprintf(\"Move S3 bucket object from %s to %s\", path.Join(srcBucketName, srcKey), path.Join(dstBucketName, dstKey))\n\n\treturn util.DoWithRetry(ctx, description, s3MaxRetries, s3SleepBetweenRetries, l, log.DebugLevel, func(ctx context.Context) error {\n\t\tif err := client.MoveS3Object(ctx, l, srcBucketName, srcKey, dstBucketName, dstKey); err != nil {\n\t\t\tif isBucketErrorRetriable(l, err) {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// return FatalError so that retry loop will not continue\n\t\t\treturn util.FatalError{Underlying: err}\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\n// CreateTableItemIfNecessary creates the DynamoDB table item with the specified key, only if it does not already exist.\nfunc (client *Client) CreateTableItemIfNecessary(ctx context.Context, l log.Logger, tableName, key string) error {\n\texists, err := client.DoesTableItemExist(ctx, tableName, key)\n\tif err != nil {\n\t\treturn err\n\t} else if exists {\n\t\treturn errors.Errorf(\"DynamoDB table %s item %s already exists\", tableName, key)\n\t}\n\n\tdescription := fmt.Sprintf(\"Create DynamoDB table %s item %s\", tableName, key)\n\n\treturn util.DoWithRetry(ctx, description, s3MaxRetries, s3SleepBetweenRetries, l, log.DebugLevel, func(ctx context.Context) error {\n\t\tif err := client.CreateTableItem(ctx, l, tableName, key); err != nil {\n\t\t\tif isBucketErrorRetriable(l, err) {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\t// return FatalError so that retry loop will not continue\n\t\t\treturn util.FatalError{Underlying: err}\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\n// GetDynamoDBClient returns the DynamoDB client for testing purposes.\nfunc (client *Client) GetDynamoDBClient() *dynamodb.Client {\n\treturn client.dynamoClient\n}\n\n// GetS3Client returns the S3 client for testing purposes.\nfunc (client *Client) GetS3Client() *s3.Client {\n\treturn client.s3Client\n}\n\n// isAWSResourceNotFoundError checks if an error indicates that an AWS resource was not found\nfunc isAWSResourceNotFoundError(err error) bool {\n\tvar apiErr smithy.APIError\n\treturn errors.As(err, &apiErr) && apiErr.ErrorCode() == \"ResourceNotFoundException\"\n}\n\n// handleS3TaggingMethodNotAllowed handles MethodNotAllowed errors for S3 bucket tagging operations\n// Returns true if the error was handled (caller should return nil), false otherwise\nfunc handleS3TaggingMethodNotAllowed(err error, l log.Logger, bucketName string) bool {\n\tvar apiErr smithy.APIError\n\tif errors.As(err, &apiErr) && apiErr.ErrorCode() == \"MethodNotAllowed\" {\n\t\tl.Warnf(\"S3 bucket tagging is not supported for bucket %s - skipping tagging (this is normal for some AWS configurations)\", bucketName)\n\t\treturn true\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "internal/remotestate/backend/s3/client_test.go",
    "content": "//go:build aws\n\npackage s3_test\n\nimport (\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/dynamodb\"\n\tdynamodbtypes \"github.com/aws/aws-sdk-go-v2/service/dynamodb/types\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\ts3backend \"github.com/gruntwork-io/terragrunt/internal/remotestate/backend/s3\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// defaultTestRegion is for simplicity, do all testing in the us-east-1 region\nconst defaultTestRegion = \"us-east-1\"\n\n// CreateS3ClientForTest creates a DynamoDB client we can use at test time. If there are any errors creating the client, fail the test.\nfunc CreateS3ClientForTest(t *testing.T) *s3backend.Client {\n\tt.Helper()\n\n\tmockOptions := &backend.Options{}\n\n\textS3Cfg := &s3backend.ExtendedRemoteStateConfigS3{\n\t\tRemoteStateConfigS3: s3backend.RemoteStateConfigS3{\n\t\t\tRegion: defaultTestRegion,\n\t\t},\n\t}\n\n\tl := logger.CreateLogger()\n\n\tclient, err := s3backend.NewClient(t.Context(), l, extS3Cfg, mockOptions)\n\trequire.NoError(t, err, \"Error creating S3 client\")\n\n\treturn client\n}\n\nfunc TestAwsCreateLockTableIfNecessaryTableDoesntAlreadyExist(t *testing.T) {\n\tt.Parallel()\n\n\tclient := CreateS3ClientForTest(t)\n\n\tWithLockTable(t, client, func(tableName string, client *s3backend.Client) {\n\t\tAssertCanWriteToTable(t, tableName, client)\n\t})\n}\n\nfunc TestAwsCreateLockTableConcurrency(t *testing.T) {\n\tt.Parallel()\n\n\tclient := CreateS3ClientForTest(t)\n\ttableName := UniqueTableNameForTest()\n\n\tdefer CleanupTableForTest(t, tableName, client)\n\n\t// Use a WaitGroup to ensure the test doesn't exit before all goroutines finish.\n\tvar waitGroup sync.WaitGroup\n\n\tl := logger.CreateLogger()\n\n\t// Launch a bunch of goroutines who will all try to create the same table at more or less the same time.\n\t// DynamoDB will, of course, only allow a single table to be created, but we still need to make sure none of\n\t// the goroutines report an error.\n\tfor i := 0; i < 20; i++ {\n\t\twaitGroup.Add(1)\n\n\t\tgo func() {\n\t\t\tdefer waitGroup.Done()\n\n\t\t\terr := client.CreateLockTableIfNecessary(t.Context(), l, tableName, nil)\n\t\t\tassert.NoError(t, err, \"Unexpected error: %v\", err)\n\t\t}()\n\t}\n\n\twaitGroup.Wait()\n}\n\nfunc TestAwsWaitForTableToBeActiveTableDoesNotExist(t *testing.T) {\n\tt.Parallel()\n\n\tclient := CreateS3ClientForTest(t)\n\ttableName := \"terragrunt-table-does-not-exist\"\n\tretries := 5\n\n\tl := logger.CreateLogger()\n\n\terr := client.WaitForTableToBeActiveWithRandomSleep(t.Context(), l, tableName, retries, 1*time.Millisecond, 500*time.Millisecond)\n\n\terrorMatchs := errors.IsError(err, s3backend.TableActiveRetriesExceeded{TableName: tableName, Retries: retries})\n\tassert.True(t, errorMatchs, \"Unexpected error of type %s: %s\", reflect.TypeOf(err), err)\n}\n\nfunc TestAwsCreateLockTableIfNecessaryTableAlreadyExists(t *testing.T) {\n\tt.Parallel()\n\n\tclient := CreateS3ClientForTest(t)\n\n\t// Create the table the first time\n\tWithLockTable(t, client, func(tableName string, client *s3backend.Client) {\n\t\tAssertCanWriteToTable(t, tableName, client)\n\n\t\tl := logger.CreateLogger()\n\n\t\t// Try to create the table the second time and make sure you get no errors\n\t\terr := client.CreateLockTableIfNecessary(t.Context(), l, tableName, nil)\n\t\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n\t})\n}\n\nfunc TestAwsTableTagging(t *testing.T) {\n\tt.Parallel()\n\n\tclient := CreateS3ClientForTest(t)\n\ttags := map[string]string{\"team\": \"team A\"}\n\n\t// Create the table the first time\n\tWithLockTableTagged(t, tags, client, func(tableName string, client *s3backend.Client) {\n\t\tAssertCanWriteToTable(t, tableName, client)\n\n\t\tassertTags(t, tags, tableName, client)\n\n\t\tl := logger.CreateLogger()\n\n\t\t// Try to create the table the second time and make sure you get no errors\n\t\terr := client.CreateLockTableIfNecessary(t.Context(), l, tableName, nil)\n\t\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n\t})\n}\n\nfunc assertTags(t *testing.T, expectedTags map[string]string, tableName string, client *s3backend.Client) {\n\tt.Helper()\n\n\t// Access the dynamodb client directly from the S3 client\n\tdynamoClient := client.GetDynamoDBClient()\n\n\tvar description, err = dynamoClient.DescribeTable(t.Context(), &dynamodb.DescribeTableInput{TableName: aws.String(tableName)})\n\tif err != nil {\n\t\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n\t}\n\n\tvar tags = listTagsOfResourceWithRetry(t, client, description.Table.TableArn)\n\n\tvar actualTags = make(map[string]string)\n\n\tfor _, element := range tags.Tags {\n\t\tactualTags[*element.Key] = *element.Value\n\t}\n\n\tassert.Equal(t, expectedTags, actualTags, \"Did not find expected tags on dynamo table.\")\n}\n\nfunc listTagsOfResourceWithRetry(t *testing.T, client *s3backend.Client, resourceArn *string) *dynamodb.ListTagsOfResourceOutput {\n\tt.Helper()\n\n\tconst (\n\t\tdelay   = 1 * time.Second\n\t\tretries = 5\n\t)\n\n\t// Access the dynamodb client directly from the S3 client\n\tdynamoClient := client.GetDynamoDBClient()\n\n\tfor range retries {\n\t\tvar tags, err = dynamoClient.ListTagsOfResource(t.Context(), &dynamodb.ListTagsOfResourceInput{ResourceArn: resourceArn})\n\t\tif err != nil {\n\t\t\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n\t\t}\n\n\t\tif len(tags.Tags) > 0 {\n\t\t\treturn tags\n\t\t}\n\n\t\ttime.Sleep(delay)\n\t}\n\n\trequire.Failf(t, \"Could not list tags of resource after %s retries.\", strconv.Itoa(retries))\n\n\treturn nil\n}\n\nfunc UniqueTableNameForTest() string {\n\treturn \"terragrunt_test_\" + util.UniqueID()\n}\n\nfunc CleanupTableForTest(t *testing.T, tableName string, client *s3backend.Client) {\n\tt.Helper()\n\n\tl := logger.CreateLogger()\n\n\terr := client.DeleteTable(t.Context(), l, tableName)\n\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n}\n\nfunc AssertCanWriteToTable(t *testing.T, tableName string, client *s3backend.Client) {\n\tt.Helper()\n\n\titem := CreateKeyFromItemID(util.UniqueID())\n\n\t// Access the dynamodb client directly from the S3 client\n\tdynamoClient := client.GetDynamoDBClient()\n\n\t_, err := dynamoClient.PutItem(t.Context(), &dynamodb.PutItemInput{\n\t\tTableName: aws.String(tableName),\n\t\tItem:      item,\n\t})\n\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n}\n\nfunc WithLockTable(t *testing.T, client *s3backend.Client, action func(tableName string, client *s3backend.Client)) {\n\tt.Helper()\n\tWithLockTableTagged(t, nil, client, action)\n}\n\nfunc WithLockTableTagged(t *testing.T, tags map[string]string, client *s3backend.Client, action func(tableName string, client *s3backend.Client)) {\n\tt.Helper()\n\n\ttableName := UniqueTableNameForTest()\n\tdefer CleanupTableForTest(t, tableName, client)\n\n\tl := logger.CreateLogger()\n\n\terr := client.CreateLockTableIfNecessary(t.Context(), l, tableName, tags)\n\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n\n\taction(tableName, client)\n}\n\nfunc CreateKeyFromItemID(itemID string) map[string]dynamodbtypes.AttributeValue {\n\treturn map[string]dynamodbtypes.AttributeValue{\n\t\t\"LockID\": &dynamodbtypes.AttributeValueMemberS{Value: itemID},\n\t}\n}\n\n// TestAwsCreateS3BucketWithTagsAtCreation verifies that tags passed via\n// CreateS3BucketOpts are applied at bucket creation time (via\n// CreateBucketConfiguration.Tags), without relying on a subsequent\n// PutBucketTagging call.\nfunc TestAwsCreateS3BucketWithTagsAtCreation(t *testing.T) {\n\tt.Parallel()\n\n\tclient := CreateS3ClientForTest(t)\n\tbucketName := \"terragrunt-test-\" + strings.ToLower(util.UniqueID())\n\n\tl := logger.CreateLogger()\n\n\texpectedTags := map[string]string{\n\t\t\"team\": \"platform\",\n\t\t\"env\":  \"test\",\n\t}\n\n\t// Create bucket with tags supplied only at creation time.\n\terr := client.CreateS3Bucket(t.Context(), l, bucketName, s3backend.CreateS3BucketOpts{Tags: expectedTags})\n\trequire.NoError(t, err)\n\n\tdefer func() {\n\t\trequire.NoError(t, client.DeleteS3BucketWithAllObjects(t.Context(), l, bucketName))\n\t}()\n\n\terr = client.WaitUntilS3BucketExists(t.Context(), l, bucketName)\n\trequire.NoError(t, err)\n\n\t// Verify tags are present — no PutBucketTagging was called, so these\n\t// must have come from CreateBucketConfiguration.Tags at creation time.\n\ts3Client := client.GetS3Client()\n\n\ttagsOut, err := s3Client.GetBucketTagging(t.Context(), &s3.GetBucketTaggingInput{\n\t\tBucket: aws.String(bucketName),\n\t})\n\trequire.NoError(t, err)\n\n\tactualTags := make(map[string]string)\n\tfor _, tag := range tagsOut.TagSet {\n\t\tactualTags[*tag.Key] = *tag.Value\n\t}\n\n\tassert.Equal(t, expectedTags, actualTags, \"Tags should be present from creation-time CreateBucketConfiguration.Tags\")\n}\n"
  },
  {
    "path": "internal/remotestate/backend/s3/config.go",
    "content": "package s3\n\nimport (\n\t\"maps\"\n\t\"reflect\"\n\t\"slices\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/hclhelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/mitchellh/mapstructure\"\n)\n\nconst (\n\tconfigLockTableKey                 = \"lock_table\"\n\tconfigDynamoDBTableKey             = \"dynamodb_table\"\n\tconfigAssumeRoleKey                = \"assume_role\"\n\tconfigAssumeRoleWithWebIdentityKey = \"assume_role_with_web_identity\"\n\tconfigAccessloggingTargetPrefixKey = \"accesslogging_target_prefix\"\n\n\tDefaultS3BucketAccessLoggingTargetPrefix = \"TFStateLogs/\"\n\n\tlockTableDeprecationMessage = \"Remote state configuration 'lock_table' attribute is deprecated; use 'dynamodb_table' instead.\"\n)\n\ntype Config map[string]any\n\nfunc (cfg Config) FilterOutTerragruntKeys() Config {\n\tvar filtered = make(Config)\n\n\tfor key, val := range cfg {\n\t\tif slices.Contains(terragruntOnlyConfigs, key) {\n\t\t\tcontinue\n\t\t}\n\n\t\tfiltered[key] = val\n\t}\n\n\treturn filtered\n}\n\nfunc (cfg Config) GetTFInitArgs() Config {\n\tvar filtered = make(Config)\n\n\tfor key, val := range cfg.FilterOutTerragruntKeys() {\n\t\t// Remove the deprecated \"lock_table\" attribute so that it\n\t\t// will not be passed either when generating a backend block\n\t\t// or as a command-line argument.\n\t\tif key == configLockTableKey {\n\t\t\tfiltered[configDynamoDBTableKey] = val\n\t\t\tcontinue\n\t\t}\n\n\t\tif key == configAssumeRoleKey {\n\t\t\tif mapVal, ok := val.(map[string]any); ok {\n\t\t\t\tfiltered[key] = hclhelper.WrapMapToSingleLineHcl(mapVal)\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif key == configAssumeRoleWithWebIdentityKey {\n\t\t\tif mapVal, ok := val.(map[string]any); ok {\n\t\t\t\tfiltered[key] = hclhelper.WrapMapToSingleLineHcl(mapVal)\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tfiltered[key] = val\n\t}\n\n\t// Normalize string boolean values to native Go bools using reflection\n\t// on the S3 config structs. HCL ternary type unification can convert\n\t// bools to strings, which causes generated backend blocks to contain\n\t// \"true\"/\"false\" string literals instead of true/false boolean literals.\n\treturn Config(backend.NormalizeBoolValues(backend.Config(filtered), &ExtendedRemoteStateConfigS3{}))\n}\n\nfunc (cfg Config) Normalize(logger log.Logger) Config {\n\tvar normalized = make(Config)\n\n\tmaps.Copy(normalized, cfg)\n\n\t// Nowadays it only makes sense to set the \"dynamodb_table\" attribute as it has\n\t// been supported in Terraform since the release of version 0.10. The deprecated\n\t// \"lock_table\" attribute is either set to NULL in the state file or missing\n\t// from it altogether. Display a deprecation warning when the \"lock_table\"\n\t// attribute is being used.\n\tif util.KindOf(normalized[configLockTableKey]) == reflect.String && normalized[configLockTableKey] != \"\" {\n\t\tlogger.Warnf(\"%s\\n\", lockTableDeprecationMessage)\n\n\t\tnormalized[configDynamoDBTableKey] = normalized[configLockTableKey]\n\t\tdelete(normalized, configLockTableKey)\n\t}\n\n\treturn normalized\n}\n\n// ParseExtendedS3Config parses the given map into an extended S3 config.\nfunc (cfg Config) ParseExtendedS3Config() (*ExtendedRemoteStateConfigS3, error) {\n\tvar (\n\t\ts3Config       RemoteStateConfigS3\n\t\textendedConfig ExtendedRemoteStateConfigS3\n\t)\n\n\tif err := mapstructure.WeakDecode(cfg, &s3Config); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tif err := mapstructure.WeakDecode(cfg, &extendedConfig); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\t_, targetPrefixExists := cfg[configAccessloggingTargetPrefixKey]\n\tif !targetPrefixExists {\n\t\textendedConfig.AccessLoggingTargetPrefix = DefaultS3BucketAccessLoggingTargetPrefix\n\t}\n\n\textendedConfig.RemoteStateConfigS3 = s3Config\n\n\treturn &extendedConfig, nil\n}\n\n// ExtendedS3Config parses the given map into an extended S3 config and validates this config.\nfunc (cfg Config) ExtendedS3Config(logger log.Logger) (*ExtendedRemoteStateConfigS3, error) {\n\textS3Cfg, err := cfg.Normalize(logger).ParseExtendedS3Config()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn extS3Cfg, extS3Cfg.Validate()\n}\n"
  },
  {
    "path": "internal/remotestate/backend/s3/config_test.go",
    "content": "package s3_test\n\nimport (\n\t\"testing\"\n\n\ts3backend \"github.com/gruntwork-io/terragrunt/internal/remotestate/backend/s3\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestParseExtendedS3Config_StringBoolCoercion verifies that boolean config values\n// passed as strings (e.g. from HCL ternary type unification) are correctly parsed.\nfunc TestParseExtendedS3Config_StringBoolCoercion(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct { //nolint: govet\n\t\tname   string\n\t\tconfig s3backend.Config\n\t\tcheck  func(t *testing.T, cfg *s3backend.ExtendedRemoteStateConfigS3)\n\t}{\n\t\t{\n\t\t\t\"use-lockfile-string-true\",\n\t\t\ts3backend.Config{\n\t\t\t\t\"bucket\":       \"my-bucket\",\n\t\t\t\t\"key\":          \"my-key\",\n\t\t\t\t\"region\":       \"us-east-1\",\n\t\t\t\t\"use_lockfile\": \"true\",\n\t\t\t},\n\t\t\tfunc(t *testing.T, cfg *s3backend.ExtendedRemoteStateConfigS3) {\n\t\t\t\tt.Helper()\n\t\t\t\tassert.True(t, cfg.RemoteStateConfigS3.UseLockfile)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"use-lockfile-string-false\",\n\t\t\ts3backend.Config{\n\t\t\t\t\"bucket\":       \"my-bucket\",\n\t\t\t\t\"key\":          \"my-key\",\n\t\t\t\t\"region\":       \"us-east-1\",\n\t\t\t\t\"use_lockfile\": \"false\",\n\t\t\t},\n\t\t\tfunc(t *testing.T, cfg *s3backend.ExtendedRemoteStateConfigS3) {\n\t\t\t\tt.Helper()\n\t\t\t\tassert.False(t, cfg.RemoteStateConfigS3.UseLockfile)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"encrypt-string-true\",\n\t\t\ts3backend.Config{\n\t\t\t\t\"bucket\":  \"my-bucket\",\n\t\t\t\t\"key\":     \"my-key\",\n\t\t\t\t\"region\":  \"us-east-1\",\n\t\t\t\t\"encrypt\": \"true\",\n\t\t\t},\n\t\t\tfunc(t *testing.T, cfg *s3backend.ExtendedRemoteStateConfigS3) {\n\t\t\t\tt.Helper()\n\t\t\t\tassert.True(t, cfg.RemoteStateConfigS3.Encrypt)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"force-path-style-string-true\",\n\t\t\ts3backend.Config{\n\t\t\t\t\"bucket\":           \"my-bucket\",\n\t\t\t\t\"key\":              \"my-key\",\n\t\t\t\t\"region\":           \"us-east-1\",\n\t\t\t\t\"force_path_style\": \"true\",\n\t\t\t},\n\t\t\tfunc(t *testing.T, cfg *s3backend.ExtendedRemoteStateConfigS3) {\n\t\t\t\tt.Helper()\n\t\t\t\tassert.True(t, cfg.RemoteStateConfigS3.S3ForcePathStyle)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"skip-bucket-versioning-string-true\",\n\t\t\ts3backend.Config{\n\t\t\t\t\"bucket\":                 \"my-bucket\",\n\t\t\t\t\"key\":                    \"my-key\",\n\t\t\t\t\"region\":                 \"us-east-1\",\n\t\t\t\t\"skip_bucket_versioning\": \"true\",\n\t\t\t},\n\t\t\tfunc(t *testing.T, cfg *s3backend.ExtendedRemoteStateConfigS3) {\n\t\t\t\tt.Helper()\n\t\t\t\tassert.True(t, cfg.SkipBucketVersioning)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"native-bool-still-works\",\n\t\t\ts3backend.Config{\n\t\t\t\t\"bucket\":       \"my-bucket\",\n\t\t\t\t\"key\":          \"my-key\",\n\t\t\t\t\"region\":       \"us-east-1\",\n\t\t\t\t\"use_lockfile\": true,\n\t\t\t},\n\t\t\tfunc(t *testing.T, cfg *s3backend.ExtendedRemoteStateConfigS3) {\n\t\t\t\tt.Helper()\n\t\t\t\tassert.True(t, cfg.RemoteStateConfigS3.UseLockfile)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"empty-string-coerces-to-false\",\n\t\t\ts3backend.Config{\n\t\t\t\t\"bucket\":       \"my-bucket\",\n\t\t\t\t\"key\":          \"my-key\",\n\t\t\t\t\"region\":       \"us-east-1\",\n\t\t\t\t\"use_lockfile\": \"\",\n\t\t\t},\n\t\t\tfunc(t *testing.T, cfg *s3backend.ExtendedRemoteStateConfigS3) {\n\t\t\t\tt.Helper()\n\t\t\t\tassert.False(t, cfg.RemoteStateConfigS3.UseLockfile)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"numeric-one-coerces-to-true\",\n\t\t\ts3backend.Config{\n\t\t\t\t\"bucket\":       \"my-bucket\",\n\t\t\t\t\"key\":          \"my-key\",\n\t\t\t\t\"region\":       \"us-east-1\",\n\t\t\t\t\"use_lockfile\": \"1\",\n\t\t\t},\n\t\t\tfunc(t *testing.T, cfg *s3backend.ExtendedRemoteStateConfigS3) {\n\t\t\t\tt.Helper()\n\t\t\t\tassert.True(t, cfg.RemoteStateConfigS3.UseLockfile)\n\t\t\t},\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.Parallel()\n\n\t\t\textS3Cfg, err := tc.config.Normalize(logger.CreateLogger()).ParseExtendedS3Config()\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttc.check(t, extS3Cfg)\n\t\t})\n\t}\n}\n\n// TestParseExtendedS3Config_InvalidStringBool verifies that WeakDecode rejects\n// invalid string values for bool fields (e.g. \"maybe\" is not a valid bool).\nfunc TestParseExtendedS3Config_InvalidStringBool(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := s3backend.Config{\n\t\t\"bucket\":       \"my-bucket\",\n\t\t\"key\":          \"my-key\",\n\t\t\"region\":       \"us-east-1\",\n\t\t\"use_lockfile\": \"maybe\",\n\t}\n\n\t_, err := cfg.Normalize(logger.CreateLogger()).ParseExtendedS3Config()\n\trequire.Error(t, err)\n}\n"
  },
  {
    "path": "internal/remotestate/backend/s3/counting_semaphore.go",
    "content": "package s3\n\ntype empty struct{}\ntype CountingSemaphore chan empty\n\n// NewCountingSemaphore is a bare-bones counting semaphore implementation\n// based on: http://www.golangpatterns.info/concurrency/semaphores\nfunc NewCountingSemaphore(size int) CountingSemaphore {\n\treturn make(CountingSemaphore, size)\n}\n\nfunc (semaphore CountingSemaphore) Acquire() {\n\tsemaphore <- empty{}\n}\n\nfunc (semaphore CountingSemaphore) Release() {\n\t<-semaphore\n}\n"
  },
  {
    "path": "internal/remotestate/backend/s3/counting_semaphore_test.go",
    "content": "//nolint:govet\npackage s3_test\n\nimport (\n\t\"math/rand\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\ts3backend \"github.com/gruntwork-io/terragrunt/internal/remotestate/backend/s3\"\n)\n\nfunc TestAwsCountingSemaphoreHappyPath(t *testing.T) {\n\tt.Parallel()\n\n\tsemaphore := s3backend.NewCountingSemaphore(1)\n\tsemaphore.Acquire()\n\tsemaphore.Release()\n}\n\n// This method tries to verify our counting semaphore works. It does this by creating a counting semaphore of size N\n// and then firing up M >> N goroutines that all try to Acquire the semaphore. As each goroutine executes, it uses an\n// atomic increment operation to record how many goroutines are running simultaneously. We check the number of running\n// goroutines to ensure that it goes up to N, but does not exceed it.\nfunc TestAwsCountingSemaphoreConcurrency(t *testing.T) {\n\tt.Parallel()\n\n\tpermits := 10\n\tgoroutines := 100\n\tsemaphore := s3backend.NewCountingSemaphore(permits)\n\n\tvar (\n\t\tgoRoutinesExecutingSimultaneously uint32\n\t\twaitForAllGoRoutinesToFinish      sync.WaitGroup\n\t)\n\n\tendGoRoutine := func() {\n\t\t// Decrement the number of running goroutines. Note that decrementing an unsigned int is a bit odd.\n\t\t// This is copied from the docs: https://golang.org/pkg/sync/atomic/#AddUint32\n\t\tatomic.AddUint32(&goRoutinesExecutingSimultaneously, ^uint32(0))\n\n\t\tsemaphore.Release()\n\t\twaitForAllGoRoutinesToFinish.Done()\n\t}\n\n\trunGoRoutine := func() {\n\t\tdefer endGoRoutine()\n\n\t\tsemaphore.Acquire()\n\n\t\t// Increment the total number of running goroutines\n\t\ttotalGoRoutinesExecutingSimultaneously := atomic.AddUint32(&goRoutinesExecutingSimultaneously, 1)\n\n\t\tif totalGoRoutinesExecutingSimultaneously > uint32(permits) {\n\t\t\tt.Fatalf(\"The semaphore was only supposed to allow %d goroutines to run simultaneously, but has allowed %d\", permits, totalGoRoutinesExecutingSimultaneously)\n\t\t}\n\n\t\t// Sleep for a random amount of time to represent this goroutine doing work\n\t\trandomSleepTime := rand.Intn(100)\n\t\ttime.Sleep(time.Duration(randomSleepTime) * time.Millisecond)\n\t}\n\n\t// Fire up a whole bunch of goroutines that will all try to acquire the semaphore at the same time\n\tfor range goroutines {\n\t\twaitForAllGoRoutinesToFinish.Add(1)\n\n\t\tgo runGoRoutine()\n\t}\n\n\twaitForAllGoRoutinesToFinish.Wait()\n}\n"
  },
  {
    "path": "internal/remotestate/backend/s3/errors.go",
    "content": "package s3\n\nimport \"fmt\"\n\ntype MissingRequiredS3RemoteStateConfig string\n\nfunc (configName MissingRequiredS3RemoteStateConfig) Error() string {\n\treturn \"Missing required S3 remote state configuration \" + string(configName)\n}\n\ntype MultipleTagsDeclarations string\n\nfunc (target MultipleTagsDeclarations) Error() string {\n\treturn fmt.Sprintf(\"Tags for %s declared multiple times. Please only declare tags in one block.\", string(target))\n}\n\ntype MaxRetriesWaitingForS3BucketExceeded string\n\nfunc (err MaxRetriesWaitingForS3BucketExceeded) Error() string {\n\treturn fmt.Sprintf(\"Exceeded max retries (%d) waiting for bucket S3 bucket %s\", maxRetriesWaitingForS3Bucket, string(err))\n}\n\ntype MaxRetriesWaitingForS3ACLExceeded string\n\nfunc (err MaxRetriesWaitingForS3ACLExceeded) Error() string {\n\treturn fmt.Sprintf(\"Exceeded max retries waiting for S3 bucket %s to have the proper ACL for access logging\", string(err))\n}\n\ntype InvalidAccessLoggingBucketEncryption struct {\n\tBucketSSEAlgorithm string\n}\n\nfunc (err InvalidAccessLoggingBucketEncryption) Error() string {\n\treturn fmt.Sprintf(\"Encryption algorithm %s is not supported for access logging bucket. Please use a supported algorithm, like AES256\", err.BucketSSEAlgorithm)\n}\n\ntype TableActiveRetriesExceeded struct {\n\tTableName string\n\tRetries   int\n}\n\nfunc (err TableActiveRetriesExceeded) Error() string {\n\treturn fmt.Sprintf(\"Table %s failed to reach the 'active' state after %d retries.\", err.TableName, err.Retries)\n}\n\ntype TableDoesNotExist struct {\n\tUnderlying error\n\tTableName  string\n}\n\nfunc (err TableDoesNotExist) Error() string {\n\treturn fmt.Sprintf(\"DynamoDB table %s does not exist! Original error from AWS: %v\", err.TableName, err.Underlying)\n}\n\ntype TableEncryptedRetriesExceeded struct {\n\tTableName string\n\tRetries   int\n}\n\nfunc (err TableEncryptedRetriesExceeded) Error() string {\n\treturn fmt.Sprintf(\"Failed to confirm that DynamoDB table %s has encryption enabled after %d retries.\", err.TableName, err.Retries)\n}\n"
  },
  {
    "path": "internal/remotestate/backend/s3/remote_state_config.go",
    "content": "package s3\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\ts3types \"github.com/aws/aws-sdk-go-v2/service/s3/types\"\n\t\"github.com/gruntwork-io/terragrunt/internal/awshelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\n// These are settings that can appear in the remote_state config that are ONLY used by Terragrunt and NOT forwarded\n// to the underlying Terraform backend configuration\nvar terragruntOnlyConfigs = []string{\n\t\"s3_bucket_tags\",\n\t\"dynamodb_table_tags\",\n\t\"accesslogging_bucket_tags\",\n\t\"skip_bucket_versioning\",\n\t\"skip_bucket_ssencryption\",\n\t\"skip_bucket_accesslogging\",\n\t\"skip_bucket_root_access\",\n\t\"skip_bucket_enforced_tls\",\n\t\"skip_bucket_public_access_blocking\",\n\t\"disable_bucket_update\",\n\t\"enable_lock_table_ssencryption\",\n\t\"disable_aws_client_checksums\",\n\t\"accesslogging_bucket_name\",\n\t\"accesslogging_target_object_partition_date_source\",\n\t\"accesslogging_target_prefix\",\n\t\"skip_accesslogging_bucket_acl\",\n\t\"skip_accesslogging_bucket_enforced_tls\",\n\t\"skip_accesslogging_bucket_public_access_blocking\",\n\t\"skip_accesslogging_bucket_ssencryption\",\n\t\"bucket_sse_algorithm\",\n\t\"bucket_sse_kms_key_id\",\n}\n\n/* ExtendedRemoteStateConfigS3 is a struct that contains the RemoteStateConfigS3 struct and additional\n * configuration options that are specific to the S3 backend. This struct is used to parse the configuration\n * from the Terragrunt configuration file.\n *\n * We use this construct to separate the three config keys 's3_bucket_tags', 'dynamodb_table_tags'\n * and 'accesslogging_bucket_tags' from the others, as they are specific to the s3 backend,\n * but only used by terragrunt to tag the s3 bucket, the dynamo db and the s3 bucket used to the\n * access logs, in case it has to create them.\n */\ntype ExtendedRemoteStateConfigS3 struct {\n\tS3BucketTags                                 map[string]string   `mapstructure:\"s3_bucket_tags\"`\n\tDynamotableTags                              map[string]string   `mapstructure:\"dynamodb_table_tags\"`\n\tAccessLoggingBucketTags                      map[string]string   `mapstructure:\"accesslogging_bucket_tags\"`\n\tAccessLoggingBucketName                      string              `mapstructure:\"accesslogging_bucket_name\"`\n\tBucketSSEKMSKeyID                            string              `mapstructure:\"bucket_sse_kms_key_id\"`\n\tBucketSSEAlgorithm                           string              `mapstructure:\"bucket_sse_algorithm\"`\n\tAccessLoggingTargetPrefix                    string              `mapstructure:\"accesslogging_target_prefix\"`\n\tAccessLoggingTargetObjectPartitionDateSource string              `mapstructure:\"accesslogging_target_object_partition_date_source\"`\n\tRemoteStateConfigS3                          RemoteStateConfigS3 `mapstructure:\",squash\"`\n\tSkipBucketVersioning                         bool                `mapstructure:\"skip_bucket_versioning\"`\n\tSkipBucketAccessLogging                      bool                `mapstructure:\"skip_bucket_accesslogging\"`\n\tDisableBucketUpdate                          bool                `mapstructure:\"disable_bucket_update\"`\n\tEnableLockTableSSEncryption                  bool                `mapstructure:\"enable_lock_table_ssencryption\"`\n\tDisableAWSClientChecksums                    bool                `mapstructure:\"disable_aws_client_checksums\"`\n\tSkipBucketEnforcedTLS                        bool                `mapstructure:\"skip_bucket_enforced_tls\"`\n\tSkipBucketRootAccess                         bool                `mapstructure:\"skip_bucket_root_access\"`\n\tSkipBucketPublicAccessBlocking               bool                `mapstructure:\"skip_bucket_public_access_blocking\"`\n\tSkipAccessLoggingBucketACL                   bool                `mapstructure:\"skip_accesslogging_bucket_acl\"`\n\tSkipAccessLoggingBucketEnforcedTLS           bool                `mapstructure:\"skip_accesslogging_bucket_enforced_tls\"`\n\tSkipAccessLoggingBucketPublicAccessBlocking  bool                `mapstructure:\"skip_accesslogging_bucket_public_access_blocking\"`\n\tSkipAccessLoggingBucketSSEncryption          bool                `mapstructure:\"skip_accesslogging_bucket_ssencryption\"`\n\tSkipBucketSSEncryption                       bool                `mapstructure:\"skip_bucket_ssencryption\"`\n\tSkipCredentialsValidation                    bool                `mapstructure:\"skip_credentials_validation\"`\n}\n\nfunc (cfg *ExtendedRemoteStateConfigS3) FetchEncryptionAlgorithm() string {\n\t// Encrypt with KMS by default\n\talgorithm := string(s3types.ServerSideEncryptionAwsKms)\n\tif cfg.BucketSSEAlgorithm != \"\" {\n\t\talgorithm = cfg.BucketSSEAlgorithm\n\t}\n\n\treturn algorithm\n}\n\n// GetAwsSessionConfig builds a session config for AWS related requests\n// from the RemoteStateConfigS3 configuration.\nfunc (cfg *ExtendedRemoteStateConfigS3) GetAwsSessionConfig() *awshelper.AwsSessionConfig {\n\ts3Endpoint := cfg.RemoteStateConfigS3.Endpoint\n\tif cfg.RemoteStateConfigS3.Endpoints.S3 != \"\" {\n\t\ts3Endpoint = cfg.RemoteStateConfigS3.Endpoints.S3\n\t}\n\n\tdynamoDBEndpoint := cfg.RemoteStateConfigS3.DynamoDBEndpoint\n\tif cfg.RemoteStateConfigS3.Endpoints.DynamoDB != \"\" {\n\t\tdynamoDBEndpoint = cfg.RemoteStateConfigS3.Endpoints.DynamoDB\n\t}\n\n\treturn &awshelper.AwsSessionConfig{\n\t\tRegion:                  cfg.RemoteStateConfigS3.Region,\n\t\tCustomS3Endpoint:        s3Endpoint,\n\t\tCustomDynamoDBEndpoint:  dynamoDBEndpoint,\n\t\tProfile:                 cfg.RemoteStateConfigS3.Profile,\n\t\tRoleArn:                 cfg.RemoteStateConfigS3.GetSessionRoleArn(),\n\t\tTags:                    cfg.RemoteStateConfigS3.GetSessionTags(),\n\t\tExternalID:              cfg.RemoteStateConfigS3.GetExternalID(),\n\t\tSessionName:             cfg.RemoteStateConfigS3.GetSessionName(),\n\t\tCredsFilename:           cfg.RemoteStateConfigS3.CredsFilename,\n\t\tS3ForcePathStyle:        cfg.RemoteStateConfigS3.S3ForcePathStyle,\n\t\tDisableComputeChecksums: cfg.DisableAWSClientChecksums,\n\t}\n}\n\n// CreateS3LoggingInput builds AWS S3 logging input struct from the configuration.\nfunc (cfg *ExtendedRemoteStateConfigS3) CreateS3LoggingInput() s3.PutBucketLoggingInput {\n\tloggingInput := s3.PutBucketLoggingInput{\n\t\tBucket: aws.String(cfg.RemoteStateConfigS3.Bucket),\n\t\tBucketLoggingStatus: &s3types.BucketLoggingStatus{\n\t\t\tLoggingEnabled: &s3types.LoggingEnabled{\n\t\t\t\tTargetBucket: aws.String(cfg.AccessLoggingBucketName),\n\t\t\t},\n\t\t},\n\t}\n\n\tif cfg.AccessLoggingTargetPrefix != \"\" {\n\t\tloggingInput.BucketLoggingStatus.LoggingEnabled.TargetPrefix = aws.String(cfg.AccessLoggingTargetPrefix)\n\t}\n\n\tif cfg.AccessLoggingTargetObjectPartitionDateSource != \"\" {\n\t\tloggingInput.BucketLoggingStatus.LoggingEnabled.TargetObjectKeyFormat = &s3types.TargetObjectKeyFormat{\n\t\t\tPartitionedPrefix: &s3types.PartitionedPrefix{\n\t\t\t\tPartitionDateSource: s3types.PartitionDateSource(cfg.AccessLoggingTargetObjectPartitionDateSource),\n\t\t\t},\n\t\t}\n\t}\n\n\treturn loggingInput\n}\n\n// Validate validates all the parameters of the given S3 remote state configuration.\nfunc (cfg *ExtendedRemoteStateConfigS3) Validate() error {\n\tvar config = cfg.RemoteStateConfigS3\n\n\tif config.Region == \"\" {\n\t\treturn errors.New(MissingRequiredS3RemoteStateConfig(\"region\"))\n\t}\n\n\tif config.Bucket == \"\" {\n\t\treturn errors.New(MissingRequiredS3RemoteStateConfig(\"bucket\"))\n\t}\n\n\tif config.Key == \"\" {\n\t\treturn errors.New(MissingRequiredS3RemoteStateConfig(\"key\"))\n\t}\n\n\treturn nil\n}\n\ntype RemoteStateConfigS3AssumeRole struct {\n\tRoleArn           string            `mapstructure:\"role_arn\"`\n\tDuration          string            `mapstructure:\"duration\"`\n\tExternalID        string            `mapstructure:\"external_id\"`\n\tPolicy            string            `mapstructure:\"policy\"`\n\tPolicyArns        []string          `mapstructure:\"policy_arns\"`\n\tSessionName       string            `mapstructure:\"session_name\"`\n\tSourceIdentity    string            `mapstructure:\"source_identity\"`\n\tTags              map[string]string `mapstructure:\"tags\"`\n\tTransitiveTagKeys []string          `mapstructure:\"transitive_tag_keys\"`\n}\n\ntype RemoteStateConfigS3Endpoints struct {\n\tS3       string `mapstructure:\"s3\"`\n\tDynamoDB string `mapstructure:\"dynamodb\"`\n}\n\n// RemoteStateConfigS3 is a representation of the\n// configuration options available for S3 remote state.\ntype RemoteStateConfigS3 struct {\n\tEndpoints        RemoteStateConfigS3Endpoints  `mapstructure:\"endpoints\"`\n\tRoleArn          string                        `mapstructure:\"role_arn\"`\n\tExternalID       string                        `mapstructure:\"external_id\"`\n\tRegion           string                        `mapstructure:\"region\"`\n\tEndpoint         string                        `mapstructure:\"endpoint\"`\n\tDynamoDBEndpoint string                        `mapstructure:\"dynamodb_endpoint\"`\n\tBucket           string                        `mapstructure:\"bucket\"`\n\tKey              string                        `mapstructure:\"key\"`\n\tCredsFilename    string                        `mapstructure:\"shared_credentials_file\"`\n\tProfile          string                        `mapstructure:\"profile\"`\n\tSessionName      string                        `mapstructure:\"session_name\"`\n\tLockTable        string                        `mapstructure:\"lock_table\"`\n\tDynamoDBTable    string                        `mapstructure:\"dynamodb_table\"`\n\tAssumeRole       RemoteStateConfigS3AssumeRole `mapstructure:\"assume_role\"`\n\tEncrypt          bool                          `mapstructure:\"encrypt\"`\n\tS3ForcePathStyle bool                          `mapstructure:\"force_path_style\"`\n\tUseLockfile      bool                          `mapstructure:\"use_lockfile\"`\n}\n\n// CacheKey returns a unique key for the given S3 config that can be used to cache the initialization\nfunc (cfg *RemoteStateConfigS3) CacheKey() string {\n\treturn fmt.Sprintf(\n\t\t\"%s-%s-%s-%s\",\n\t\tcfg.Bucket,\n\t\tcfg.Region,\n\t\tcfg.LockTable,\n\t\tcfg.DynamoDBTable,\n\t)\n}\n\n// GetLockTableName returns the name of the DynamoDB table used for locking.\n//\n// The DynamoDB lock table attribute used to be called \"lock_table\", but has since been renamed to \"dynamodb_table\", and\n// the old attribute name deprecated. The old attribute name has been eventually removed from Terraform starting with\n// release 0.13. To maintain backwards compatibility, we support both names.\nfunc (cfg *RemoteStateConfigS3) GetLockTableName() string {\n\tif cfg.DynamoDBTable != \"\" {\n\t\treturn cfg.DynamoDBTable\n\t}\n\n\treturn cfg.LockTable\n}\n\n// GetSessionRoleArn returns the role defined in the AssumeRole struct\n// or fallback to the top level argument deprecated in Terraform 1.6\nfunc (cfg *RemoteStateConfigS3) GetSessionRoleArn() string {\n\tif cfg.AssumeRole.RoleArn != \"\" {\n\t\treturn cfg.AssumeRole.RoleArn\n\t}\n\n\treturn cfg.RoleArn\n}\n\nfunc (cfg *RemoteStateConfigS3) GetSessionTags() map[string]string {\n\tif len(cfg.AssumeRole.Tags) != 0 {\n\t\treturn cfg.AssumeRole.Tags\n\t}\n\n\treturn nil\n}\n\n// GetExternalID returns the external ID defined in the AssumeRole struct\n// or fallback to the top level argument deprecated in Terraform 1.6\n// The external ID is used to prevent confused deputy attacks.\nfunc (cfg *RemoteStateConfigS3) GetExternalID() string {\n\tif cfg.AssumeRole.ExternalID != \"\" {\n\t\treturn cfg.AssumeRole.ExternalID\n\t}\n\n\treturn cfg.ExternalID\n}\n\nfunc (cfg *RemoteStateConfigS3) GetSessionName() string {\n\tif cfg.AssumeRole.SessionName != \"\" {\n\t\treturn cfg.AssumeRole.SessionName\n\t}\n\n\treturn cfg.SessionName\n}\n"
  },
  {
    "path": "internal/remotestate/backend/s3/remote_state_config_test.go",
    "content": "package s3_test\n\nimport (\n\t\"bytes\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\ts3types \"github.com/aws/aws-sdk-go-v2/service/s3/types\"\n\t\"github.com/gruntwork-io/terragrunt/internal/awshelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\ts3backend \"github.com/gruntwork-io/terragrunt/internal/remotestate/backend/s3\"\n)\n\nfunc TestConfig_CreateS3LoggingInput(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct { //nolint: govet\n\t\tname          string\n\t\tconfig        s3backend.Config\n\t\tloggingInput  s3.PutBucketLoggingInput\n\t\tshouldBeEqual bool\n\t}{\n\t\t{\n\t\t\t\"equal-default-prefix-no-partition-date-source\",\n\t\t\ts3backend.Config{\n\t\t\t\t\"bucket\":                    \"source-bucket\",\n\t\t\t\t\"accesslogging_bucket_name\": \"logging-bucket\",\n\t\t\t},\n\t\t\ts3.PutBucketLoggingInput{\n\t\t\t\tBucket: aws.String(\"source-bucket\"),\n\t\t\t\tBucketLoggingStatus: &s3types.BucketLoggingStatus{\n\t\t\t\t\tLoggingEnabled: &s3types.LoggingEnabled{\n\t\t\t\t\t\tTargetBucket: aws.String(\"logging-bucket\"),\n\t\t\t\t\t\tTargetPrefix: aws.String(s3backend.DefaultS3BucketAccessLoggingTargetPrefix),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"equal-no-prefix-no-partition-date-source\",\n\t\t\ts3backend.Config{\n\t\t\t\t\"bucket\":                      \"source-bucket\",\n\t\t\t\t\"accesslogging_bucket_name\":   \"logging-bucket\",\n\t\t\t\t\"accesslogging_target_prefix\": \"\",\n\t\t\t},\n\t\t\ts3.PutBucketLoggingInput{\n\t\t\t\tBucket: aws.String(\"source-bucket\"),\n\t\t\t\tBucketLoggingStatus: &s3types.BucketLoggingStatus{\n\t\t\t\t\tLoggingEnabled: &s3types.LoggingEnabled{\n\t\t\t\t\t\tTargetBucket: aws.String(\"logging-bucket\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"equal-custom-prefix-no-partition-date-source\",\n\t\t\ts3backend.Config{\n\t\t\t\t\"bucket\":                      \"source-bucket\",\n\t\t\t\t\"accesslogging_bucket_name\":   \"logging-bucket\",\n\t\t\t\t\"accesslogging_target_prefix\": \"custom-prefix/\",\n\t\t\t},\n\t\t\ts3.PutBucketLoggingInput{\n\t\t\t\tBucket: aws.String(\"source-bucket\"),\n\t\t\t\tBucketLoggingStatus: &s3types.BucketLoggingStatus{\n\t\t\t\t\tLoggingEnabled: &s3types.LoggingEnabled{\n\t\t\t\t\t\tTargetBucket: aws.String(\"logging-bucket\"),\n\t\t\t\t\t\tTargetPrefix: aws.String(\"custom-prefix/\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"equal-custom-prefix-custom-partition-date-source\",\n\t\t\ts3backend.Config{\n\t\t\t\t\"bucket\":                    \"source-bucket\",\n\t\t\t\t\"accesslogging_bucket_name\": \"logging-bucket\",\n\t\t\t\t\"accesslogging_target_object_partition_date_source\": \"EventTime\",\n\t\t\t\t\"accesslogging_target_prefix\":                       \"custom-prefix/\",\n\t\t\t},\n\t\t\ts3.PutBucketLoggingInput{\n\t\t\t\tBucket: aws.String(\"source-bucket\"),\n\t\t\t\tBucketLoggingStatus: &s3types.BucketLoggingStatus{\n\t\t\t\t\tLoggingEnabled: &s3types.LoggingEnabled{\n\t\t\t\t\t\tTargetBucket: aws.String(\"logging-bucket\"),\n\t\t\t\t\t\tTargetPrefix: aws.String(\"custom-prefix/\"),\n\t\t\t\t\t\tTargetObjectKeyFormat: &s3types.TargetObjectKeyFormat{\n\t\t\t\t\t\t\tPartitionedPrefix: &s3types.PartitionedPrefix{\n\t\t\t\t\t\t\t\tPartitionDateSource: s3types.PartitionDateSource(\"EventTime\"),\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\ttrue,\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.Parallel()\n\n\t\t\textS3Cfg, err := tc.config.Normalize(log.Default()).ParseExtendedS3Config()\n\t\t\trequire.NoError(t, err, \"Unexpected error parsing config for test: %v\", err)\n\n\t\t\tcreatedLoggingInput := extS3Cfg.CreateS3LoggingInput()\n\n\t\t\tactual := reflect.DeepEqual(createdLoggingInput, tc.loggingInput)\n\t\t\tif !assert.Equal(t, tc.shouldBeEqual, actual) {\n\t\t\t\tt.Errorf(\"s3.PutBucketLoggingInput mismatch:\\ncreated: %+v\\nexpected: %+v\", createdLoggingInput, tc.loggingInput)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConfig_ForcePathStyleClientSession(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct { //nolint: govet\n\t\tname     string\n\t\tconfig   s3backend.Config\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\t\"path-style-true\",\n\t\t\ts3backend.Config{\"force_path_style\": true},\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"path-style-false\",\n\t\t\ts3backend.Config{\"force_path_style\": false},\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"path-style-non-existent\",\n\t\t\ts3backend.Config{},\n\t\t\tfalse,\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.Parallel()\n\n\t\t\textS3Cfg, err := tc.config.Normalize(log.Default()).ParseExtendedS3Config()\n\t\t\trequire.NoError(t, err, \"Unexpected error parsing config for test: %v\", err)\n\n\t\t\tawsSessionConfig := extS3Cfg.GetAwsSessionConfig()\n\n\t\t\tactual := awsSessionConfig.S3ForcePathStyle\n\t\t\tassert.Equal(t, tc.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestConfig_CustomStateEndpoints(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct { //nolint: govet\n\t\tname     string\n\t\tconfig   s3backend.Config\n\t\texpected *awshelper.AwsSessionConfig\n\t}{\n\t\t{\n\t\t\tname:   \"using pre 1.6.x settings only\",\n\t\t\tconfig: s3backend.Config{\"endpoint\": \"foo\", \"dynamodb_endpoint\": \"bar\"},\n\t\t\texpected: &awshelper.AwsSessionConfig{\n\t\t\t\tCustomS3Endpoint:       \"foo\",\n\t\t\t\tCustomDynamoDBEndpoint: \"bar\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"using 1.6+ settings\",\n\t\t\tconfig: s3backend.Config{\n\t\t\t\t\"endpoint\": \"foo\", \"dynamodb_endpoint\": \"bar\",\n\t\t\t\t\"endpoints\": s3backend.Config{\n\t\t\t\t\t\"s3\":       \"fooBar\",\n\t\t\t\t\t\"dynamodb\": \"barFoo\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: &awshelper.AwsSessionConfig{\n\t\t\t\tCustomS3Endpoint:       \"fooBar\",\n\t\t\t\tCustomDynamoDBEndpoint: \"barFoo\",\n\t\t\t},\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.Parallel()\n\n\t\t\textS3Cfg, err := tc.config.Normalize(log.Default()).ParseExtendedS3Config()\n\t\t\trequire.NoError(t, err, \"Unexpected error parsing config for test: %v\", err)\n\n\t\t\tactual := extS3Cfg.GetAwsSessionConfig()\n\t\t\tassert.Equal(t, tc.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestConfig_GetAwsSessionConfig(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct { //nolint: govet\n\t\tname   string\n\t\tconfig s3backend.Config\n\t}{\n\t\t{\n\t\t\t\"all-values\",\n\t\t\ts3backend.Config{\"region\": \"foo\", \"endpoint\": \"bar\", \"profile\": \"baz\", \"role_arn\": \"arn::it\", \"shared_credentials_file\": \"my-file\", \"force_path_style\": true},\n\t\t},\n\t\t{\n\t\t\t\"no-values\",\n\t\t\ts3backend.Config{},\n\t\t},\n\t\t{\n\t\t\t\"extra-values\",\n\t\t\ts3backend.Config{\"something\": \"unexpected\", \"region\": \"foo\", \"endpoint\": \"bar\", \"dynamodb_endpoint\": \"foobar\", \"profile\": \"baz\", \"role_arn\": \"arn::it\", \"shared_credentials_file\": \"my-file\", \"force_path_style\": false},\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.Parallel()\n\n\t\t\textS3Cfg, err := tc.config.Normalize(log.Default()).ParseExtendedS3Config()\n\t\t\trequire.NoError(t, err, \"Unexpected error parsing config for test: %v\", err)\n\n\t\t\texpected := &awshelper.AwsSessionConfig{\n\t\t\t\tRegion:                  extS3Cfg.RemoteStateConfigS3.Region,\n\t\t\t\tCustomS3Endpoint:        extS3Cfg.RemoteStateConfigS3.Endpoint,\n\t\t\t\tCustomDynamoDBEndpoint:  extS3Cfg.RemoteStateConfigS3.DynamoDBEndpoint,\n\t\t\t\tProfile:                 extS3Cfg.RemoteStateConfigS3.Profile,\n\t\t\t\tRoleArn:                 extS3Cfg.RemoteStateConfigS3.RoleArn,\n\t\t\t\tCredsFilename:           extS3Cfg.RemoteStateConfigS3.CredsFilename,\n\t\t\t\tS3ForcePathStyle:        extS3Cfg.RemoteStateConfigS3.S3ForcePathStyle,\n\t\t\t\tDisableComputeChecksums: extS3Cfg.DisableAWSClientChecksums,\n\t\t\t}\n\n\t\t\tactual := extS3Cfg.GetAwsSessionConfig()\n\t\t\tassert.Equal(t, expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestConfig_GetAwsSessionConfigWithAssumeRole(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct { //nolint: govet\n\t\tname   string\n\t\tconfig s3backend.Config\n\t}{\n\t\t{\n\t\t\t\"all-values\",\n\t\t\ts3backend.Config{\"role_arn\": \"arn::it\", \"external_id\": \"123\", \"session_name\": \"foobar\", \"tags\": map[string]string{\"foo\": \"bar\"}},\n\t\t},\n\t\t{\n\t\t\t\"no-tags\",\n\t\t\ts3backend.Config{\"role_arn\": \"arn::it\", \"external_id\": \"123\", \"session_name\": \"foobar\"},\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.Parallel()\n\n\t\t\tconfig := s3backend.Config{\"assume_role\": tc.config}\n\n\t\t\textS3Cfg, err := config.Normalize(log.Default()).ParseExtendedS3Config()\n\t\t\trequire.NoError(t, err, \"Unexpected error parsing config for test: %v\", err)\n\n\t\t\texpected := &awshelper.AwsSessionConfig{\n\t\t\t\tRoleArn:     extS3Cfg.RemoteStateConfigS3.AssumeRole.RoleArn,\n\t\t\t\tExternalID:  extS3Cfg.RemoteStateConfigS3.AssumeRole.ExternalID,\n\t\t\t\tSessionName: extS3Cfg.RemoteStateConfigS3.AssumeRole.SessionName,\n\t\t\t\tTags:        extS3Cfg.RemoteStateConfigS3.AssumeRole.Tags,\n\t\t\t}\n\n\t\t\tactual := extS3Cfg.GetAwsSessionConfig()\n\t\t\tassert.Equal(t, expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestConfig_Validate(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname           string\n\t\textConfig      *s3backend.ExtendedRemoteStateConfigS3\n\t\texpectedErr    error\n\t\texpectedOutput string\n\t}{\n\t\t{\n\t\t\tname:        \"no-region\",\n\t\t\textConfig:   &s3backend.ExtendedRemoteStateConfigS3{},\n\t\t\texpectedErr: s3backend.MissingRequiredS3RemoteStateConfig(\"region\"),\n\t\t},\n\t\t{\n\t\t\tname: \"no-bucket\",\n\t\t\textConfig: &s3backend.ExtendedRemoteStateConfigS3{\n\t\t\t\tRemoteStateConfigS3: s3backend.RemoteStateConfigS3{\n\t\t\t\t\tRegion: \"us-west-2\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedErr: s3backend.MissingRequiredS3RemoteStateConfig(\"bucket\"),\n\t\t},\n\t\t{\n\t\t\tname: \"no-key\",\n\t\t\textConfig: &s3backend.ExtendedRemoteStateConfigS3{\n\t\t\t\tRemoteStateConfigS3: s3backend.RemoteStateConfigS3{\n\t\t\t\t\tRegion: \"us-west-2\",\n\t\t\t\t\tBucket: \"state-bucket\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedErr: s3backend.MissingRequiredS3RemoteStateConfig(\"key\"),\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tbuf := &bytes.Buffer{}\n\t\t\tlogger := logrus.New()\n\t\t\tlogger.SetLevel(logrus.DebugLevel)\n\t\t\tlogger.SetOutput(buf)\n\n\t\t\terr := tc.extConfig.Validate()\n\t\t\trequire.ErrorIs(t, err, tc.expectedErr)\n\n\t\t\tassert.Contains(t, buf.String(), tc.expectedOutput)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/remotestate/backend/s3/retryer.go",
    "content": "package s3\n\nimport (\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n)\n\ntype Retryer struct {\n\taws.Retryer\n}\n\n// IsErrorRetryable checks if the given error is retryable according to AWS SDK v2 retry logic.\n// AWS SDK v2 doesn't expose the same retry helper functions as v1\n// The retry logic is handled internally by the SDK\n// This is a simplified retryer that delegates to the underlying AWS retryer\nfunc (retryer Retryer) IsErrorRetryable(err error) bool {\n\treturn retryer.Retryer.IsErrorRetryable(err)\n}\n"
  },
  {
    "path": "internal/remotestate/config.go",
    "content": "package remotestate\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/codegen\"\n\t\"github.com/gruntwork-io/terragrunt/internal/ctyhelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/zclconf/go-cty/cty\"\n)\n\nvar (\n\tErrRemoteBackendMissing             = errors.New(\"the remote_state.backend field cannot be empty\")\n\tErrGenerateCalledWithNoGenerateAttr = errors.New(\"generate code routine called when no generate attribute is configured\")\n)\n\n// ConfigGenerate is code gen configuration for Terraform remote state.\ntype ConfigGenerate struct {\n\tPath     string `cty:\"path\" mapstructure:\"path\"`\n\tIfExists string `cty:\"if_exists\" mapstructure:\"if_exists\"`\n}\n\n// Config is the configuration for Terraform remote state.\n// NOTE: If any attributes are added here, be sure to add it to `ConfigFile` struct.\ntype Config struct {\n\tBackendConfig                 backend.Config  `mapstructure:\"config\" json:\"Config\"`\n\tGenerate                      *ConfigGenerate `mapstructure:\"generate\" json:\"Generate\"`\n\tEncryption                    map[string]any  `mapstructure:\"encryption\" json:\"Encryption\"`\n\tBackendName                   string          `mapstructure:\"backend\" json:\"Backend\"`\n\tDisableInit                   bool            `mapstructure:\"disable_init\" json:\"DisableInit\"`\n\tDisableDependencyOptimization bool            `mapstructure:\"disable_dependency_optimization\" json:\"DisableDependencyOptimization\"`\n}\n\nfunc (cfg *Config) String() string {\n\treturn fmt.Sprintf(\n\t\t\"RemoteState{Backend = %v, DisableInit = %v, DisableDependencyOptimization = %v, Generate = %v, Config = %v, Encryption = %v}\",\n\t\tcfg.BackendName,\n\t\tcfg.DisableInit,\n\t\tcfg.DisableDependencyOptimization,\n\t\tcfg.Generate,\n\t\tcfg.BackendConfig,\n\t\tcfg.Encryption,\n\t)\n}\n\n// Validate validates that the remote state is configured correctly.\nfunc (cfg *Config) Validate() error {\n\tif cfg.BackendName == \"\" {\n\t\treturn errors.New(ErrRemoteBackendMissing)\n\t}\n\n\treturn nil\n}\n\n// GenerateOpenTofuCode generates the OpenTofu/Terraform code for configuring remote state backend.\nfunc (cfg *Config) GenerateOpenTofuCode(l log.Logger, workingDir string, backendConfig map[string]any) error {\n\tif cfg.Generate == nil {\n\t\treturn errors.New(ErrGenerateCalledWithNoGenerateAttr)\n\t}\n\n\tswitch {\n\tcase cfg.Encryption == nil:\n\t\tl.Debug(\"No encryption block in remote_state config\")\n\tcase len(cfg.Encryption) == 0:\n\t\tl.Debug(\"Empty encryption block in remote_state config\")\n\tdefault:\n\t\t_, ok := cfg.Encryption[codegen.EncryptionKeyProviderKey].(string)\n\t\tif !ok {\n\t\t\treturn errors.New(\"key_provider not found in encryption config\")\n\t\t}\n\t}\n\n\t// Convert the IfExists setting to the internal enum representation before calling generate.\n\tifExistsEnum, err := codegen.GenerateConfigExistsFromString(cfg.Generate.IfExists)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tconfigBytes, err := codegen.RemoteStateConfigToTerraformCode(cfg.BackendName, backendConfig, cfg.Encryption)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcodegenConfig := codegen.GenerateConfig{\n\t\tPath:          cfg.Generate.Path,\n\t\tIfExists:      ifExistsEnum,\n\t\tIfExistsStr:   cfg.Generate.IfExists,\n\t\tContents:      string(configBytes),\n\t\tCommentPrefix: codegen.DefaultCommentPrefix,\n\t}\n\n\treturn codegen.WriteToFile(l, workingDir, &codegenConfig)\n}\n\ntype ConfigFileGenerate struct {\n\t// We use cty instead of hcl, since we are using this type to convert an attr and not a block.\n\tPath     string `cty:\"path\"`\n\tIfExists string `cty:\"if_exists\"`\n}\n\n// ConfigFile is configuration for Terraform remote state as parsed from a terragrunt.hcl config file.\ntype ConfigFile struct {\n\tBackendConfig                 cty.Value           `hcl:\"config,attr\"`\n\tDisableInit                   *bool               `hcl:\"disable_init,attr\"`\n\tDisableDependencyOptimization *bool               `hcl:\"disable_dependency_optimization,attr\"`\n\tGenerate                      *ConfigFileGenerate `hcl:\"generate,attr\"`\n\tEncryption                    *cty.Value          `hcl:\"encryption,attr\"`\n\tBackendName                   string              `hcl:\"backend,attr\"`\n}\n\nfunc (cfgFile *ConfigFile) String() string {\n\treturn fmt.Sprintf(\"ConfigFile{Backend = %v, Config = %v}\",\n\t\tcfgFile.BackendName,\n\t\tcfgFile.BackendConfig,\n\t)\n}\n\n// Config converts the parsed config file remote state struct to the internal representation struct of remote state\n// configurations.\nfunc (cfgFile *ConfigFile) Config() (*Config, error) {\n\tremoteStateConfig, err := ctyhelper.ParseCtyValueToMap(cfgFile.BackendConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcfg := &Config{}\n\n\tcfg.BackendName = cfgFile.BackendName\n\tif cfgFile.Generate != nil {\n\t\tcfg.Generate = &ConfigGenerate{\n\t\t\tPath:     cfgFile.Generate.Path,\n\t\t\tIfExists: cfgFile.Generate.IfExists,\n\t\t}\n\t}\n\n\tcfg.BackendConfig = remoteStateConfig\n\n\tif cfgFile.Encryption != nil && !cfgFile.Encryption.IsNull() {\n\t\tremoteStateEncryption, err := ctyhelper.ParseCtyValueToMap(*cfgFile.Encryption)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tcfg.Encryption = remoteStateEncryption\n\t} else {\n\t\tcfg.Encryption = nil\n\t}\n\n\tif cfgFile.DisableInit != nil {\n\t\tcfg.DisableInit = *cfgFile.DisableInit\n\t}\n\n\tif cfgFile.DisableDependencyOptimization != nil {\n\t\tcfg.DisableDependencyOptimization = *cfgFile.DisableDependencyOptimization\n\t}\n\n\treturn cfg, cfg.Validate()\n}\n"
  },
  {
    "path": "internal/remotestate/remote_state.go",
    "content": "// Package remotestate contains code for configuring remote state storage.\npackage remotestate\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend/gcs\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend/s3\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nvar backends = backend.Backends{\n\ts3.NewBackend(),\n\tgcs.NewBackend(),\n}\n\n// Options contains the subset of configuration needed by RemoteState operations.\ntype Options struct {\n\tTFRunOpts *tf.TFOptions\n\tbackend.Options\n\tDisableBucketUpdate bool\n}\n\n// RemoteState is the configuration for Terraform remote state.\ntype RemoteState struct {\n\t*Config `mapstructure:\",squash\"`\n\tbackend backend.Backend\n}\n\n// New creates a new `RemoteState` instance.\nfunc New(config *Config) *RemoteState {\n\tremote := &RemoteState{\n\t\tConfig:  config,\n\t\tbackend: backend.NewCommonBackend(config.BackendName),\n\t}\n\n\tif backend := backends.Get(config.BackendName); backend != nil {\n\t\tremote.backend = backend\n\t}\n\n\treturn remote\n}\n\n// String implements `fmt.Stringer` interface.\nfunc (remote *RemoteState) String() string {\n\treturn remote.Config.String()\n}\n\nfunc (remote *RemoteState) IsVersionControlEnabled(ctx context.Context, l log.Logger, opts *Options) (bool, error) {\n\tl.Debugf(\"Checking if version control is enabled for the %s backend\", remote.BackendName)\n\n\treturn remote.backend.IsVersionControlEnabled(ctx, l, remote.BackendConfig, &opts.Options)\n}\n\n// Delete deletes the remote state.\nfunc (remote *RemoteState) Delete(ctx context.Context, l log.Logger, opts *Options) error {\n\tl.Debugf(\"Deleting remote state for the %s backend\", remote.BackendName)\n\n\treturn remote.backend.Delete(ctx, l, remote.BackendConfig, &opts.Options)\n}\n\n// DeleteBucket deletes the entire bucket.\nfunc (remote *RemoteState) DeleteBucket(ctx context.Context, l log.Logger, opts *Options) error {\n\tl.Debugf(\"Deleting the entire bucket for the %s backend\", remote.BackendName)\n\n\treturn remote.backend.DeleteBucket(ctx, l, remote.BackendConfig, &opts.Options)\n}\n\n// Bootstrap performs any actions necessary to bootstrap remote state before it's used for storage. For example, if you're\n// using S3 or GCS for remote state storage, this may create the bucket if it doesn't exist already.\nfunc (remote *RemoteState) Bootstrap(ctx context.Context, l log.Logger, opts *Options) error {\n\tl.Debugf(\"Bootstrapping remote state for the %s backend\", remote.BackendName)\n\n\treturn remote.backend.Bootstrap(ctx, l, remote.BackendConfig, &opts.Options)\n}\n\n// Migrate determines where the remote state resources exist for source backend config and migrate them to dest backend config.\nfunc (remote *RemoteState) Migrate(ctx context.Context, l log.Logger, opts, dstOpts *Options, dstRemote *RemoteState) error {\n\tl.Debugf(\"Migrate remote state for the %s backend\", remote.BackendName)\n\n\tif remote.BackendName == dstRemote.BackendName {\n\t\treturn remote.backend.Migrate(ctx, l, remote.BackendConfig, dstRemote.BackendConfig, &opts.Options)\n\t}\n\n\tstateFile, err := remote.pullState(ctx, l, opts.TFRunOpts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer func() {\n\t\tos.Remove(stateFile) // nolint: errcheck\n\t}()\n\n\treturn dstRemote.pushState(ctx, l, dstOpts.TFRunOpts, stateFile)\n}\n\n// NeedsBootstrap returns true if remote state needs to be configured. This will be the case when:\n//\n// 1. Remote state auto-initialization has been disabled.\n// 2. Remote state has not already been configured.\n// 3. Remote state has been configured, but with a different configuration.\n// 4. The remote state bootstrapper for this backend type, if there is one, says bootstrap is necessary.\nfunc (remote *RemoteState) NeedsBootstrap(ctx context.Context, l log.Logger, opts *Options) (bool, error) {\n\tif opts.DisableBucketUpdate {\n\t\tl.Debug(\"Skipping remote state bootstrap\")\n\t\treturn false, nil\n\t}\n\n\tif remote.DisableInit {\n\t\treturn false, nil\n\t}\n\n\t// The specific backend type will check if bootstrap is necessary.\n\tl.Debugf(\"Checking if remote state bootstrap is necessary for the %s backend\", remote.BackendName)\n\n\treturn remote.backend.NeedsBootstrap(ctx, l, remote.BackendConfig, &opts.Options)\n}\n\n// GetTFInitArgs converts the RemoteState config into the format used by the `tofu init` command.\nfunc (remote *RemoteState) GetTFInitArgs() []string {\n\tif remote.Generate != nil {\n\t\t// When in generate mode, we don't need to use `-backend-config` to initialize the remote state backend.\n\t\treturn []string{}\n\t}\n\n\tconfig := remote.backend.GetTFInitArgs(remote.BackendConfig)\n\n\tvar backendConfigArgs = make([]string, 0, len(config))\n\n\tfor key, value := range config {\n\t\targ := fmt.Sprintf(\"-backend-config=%s=%v\", key, value)\n\t\tbackendConfigArgs = append(backendConfigArgs, arg)\n\t}\n\n\treturn backendConfigArgs\n}\n\n// GenerateOpenTofuCode generates the OpenTofu/Terraform code for configuring remote state backend.\nfunc (remote *RemoteState) GenerateOpenTofuCode(l log.Logger, workingDir string) error {\n\tbackendConfig := remote.backend.GetTFInitArgs(remote.BackendConfig)\n\n\treturn remote.Config.GenerateOpenTofuCode(l, workingDir, backendConfig)\n}\n\nfunc (remote *RemoteState) pullState(ctx context.Context, l log.Logger, tfOpts *tf.TFOptions) (string, error) {\n\tl.Debugf(\"Pulling state from %s backend\", remote.BackendName)\n\n\targs := []string{tf.CommandNameState, tf.CommandNamePull}\n\n\toutput, err := tf.RunCommandWithOutput(ctx, l, tfOpts, args...)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tl.Debugf(\"Creating temporary state file for migration\")\n\n\tfile, err := os.CreateTemp(\"\", \"*.tfstate\")\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\tdefer func() {\n\t\tfile.Close() // nolint: errcheck\n\t}()\n\n\tif _, err := file.Write(output.Stdout.Bytes()); err != nil {\n\t\treturn file.Name(), errors.New(err)\n\t}\n\n\treturn file.Name(), nil\n}\n\nfunc (remote *RemoteState) pushState(ctx context.Context, l log.Logger, tfOpts *tf.TFOptions, stateFile string) error {\n\tl.Debugf(\"Pushing state to %s backend\", remote.BackendName)\n\n\targs := []string{tf.CommandNameState, tf.CommandNamePush, stateFile}\n\n\treturn tf.RunCommand(ctx, l, tfOpts, args...)\n}\n"
  },
  {
    "path": "internal/remotestate/remote_state_test.go",
    "content": "package remotestate_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n/**\n * Test for s3, also tests that the terragrunt-specific options are not passed on to terraform\n */\nfunc TestGetTFInitArgs(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := &remotestate.Config{\n\t\tBackendName: \"s3\",\n\t\tBackendConfig: map[string]any{\n\t\t\t\"encrypt\": true,\n\t\t\t\"bucket\":  \"my-bucket\",\n\t\t\t\"key\":     \"terraform.tfstate\",\n\t\t\t\"region\":  \"us-east-1\",\n\n\t\t\t\"s3_bucket_tags\": map[string]any{\n\t\t\t\t\"team\":    \"team name\",\n\t\t\t\t\"name\":    \"Terraform state storage\",\n\t\t\t\t\"service\": \"Terraform\"},\n\n\t\t\t\"dynamodb_table_tags\": map[string]any{\n\t\t\t\t\"team\":    \"team name\",\n\t\t\t\t\"name\":    \"Terraform lock table\",\n\t\t\t\t\"service\": \"Terraform\"},\n\n\t\t\t\"accesslogging_bucket_tags\": map[string]any{\n\t\t\t\t\"team\":    \"team name\",\n\t\t\t\t\"name\":    \"Terraform access log storage\",\n\t\t\t\t\"service\": \"Terraform\"},\n\n\t\t\t\"skip_bucket_versioning\": true,\n\n\t\t\t\"shared_credentials_file\": \"my-file\",\n\t\t\t\"force_path_style\":        true,\n\t\t},\n\t}\n\targs := remotestate.New(cfg).GetTFInitArgs()\n\n\t// must not contain s3_bucket_tags or dynamodb_table_tags or accesslogging_bucket_tags or skip_bucket_versioning\n\tassertTerraformInitArgsEqual(t, args, \"-backend-config=encrypt=true -backend-config=bucket=my-bucket -backend-config=key=terraform.tfstate -backend-config=region=us-east-1 -backend-config=force_path_style=true -backend-config=shared_credentials_file=my-file\")\n}\n\nfunc TestGetTFInitArgsForGCS(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := &remotestate.Config{\n\t\tBackendName: \"gcs\",\n\t\tBackendConfig: map[string]any{\n\t\t\t\"project\":  \"my-project-123456\",\n\t\t\t\"location\": \"US\",\n\t\t\t\"bucket\":   \"my-bucket\",\n\t\t\t\"prefix\":   \"terraform.tfstate\",\n\n\t\t\t\"gcs_bucket_labels\": map[string]any{\n\t\t\t\t\"team\":    \"team name\",\n\t\t\t\t\"name\":    \"Terraform state storage\",\n\t\t\t\t\"service\": \"Terraform\"},\n\n\t\t\t\"skip_bucket_versioning\": true,\n\n\t\t\t\"credentials\":  \"my-file\",\n\t\t\t\"access_token\": \"xxxxxxxx\",\n\t\t},\n\t}\n\targs := remotestate.New(cfg).GetTFInitArgs()\n\n\t// must not contain project, location gcs_bucket_labels or skip_bucket_versioning\n\tassertTerraformInitArgsEqual(t, args, \"-backend-config=bucket=my-bucket -backend-config=prefix=terraform.tfstate -backend-config=credentials=my-file -backend-config=access_token=xxxxxxxx\")\n}\n\nfunc TestGetTFInitArgsUnknownBackend(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := &remotestate.Config{\n\t\tBackendName: \"s4\",\n\t\tBackendConfig: map[string]any{\n\t\t\t\"encrypt\": true,\n\t\t\t\"bucket\":  \"my-bucket\",\n\t\t\t\"key\":     \"terraform.tfstate\",\n\t\t\t\"region\":  \"us-east-1\"},\n\t}\n\targs := remotestate.New(cfg).GetTFInitArgs()\n\n\t// no Backend initializer available, but command line args should still be passed on\n\tassertTerraformInitArgsEqual(t, args, \"-backend-config=encrypt=true -backend-config=bucket=my-bucket -backend-config=key=terraform.tfstate -backend-config=region=us-east-1\")\n}\n\nfunc TestGetTFInitArgsInitDisabled(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := &remotestate.Config{\n\t\tBackendName: \"s3\",\n\t\tDisableInit: true,\n\t\tBackendConfig: map[string]any{\n\t\t\t\"encrypt\": true,\n\t\t\t\"bucket\":  \"my-bucket\",\n\t\t\t\"key\":     \"terraform.tfstate\",\n\t\t\t\"region\":  \"us-east-1\"},\n\t}\n\targs := remotestate.New(cfg).GetTFInitArgs()\n\n\tassertTerraformInitArgsEqual(t, args, \"-backend-config=encrypt=true -backend-config=bucket=my-bucket -backend-config=key=terraform.tfstate -backend-config=region=us-east-1\")\n}\n\nfunc TestGetTFInitArgsNoBackendConfigs(t *testing.T) {\n\tt.Parallel()\n\n\tcfgs := []*remotestate.Config{\n\t\t{BackendName: \"s3\"},\n\t\t{BackendName: \"gcs\"},\n\t}\n\n\tfor _, cfg := range cfgs {\n\t\targs := remotestate.New(cfg).GetTFInitArgs()\n\t\tassert.Empty(t, args)\n\t}\n}\n\n// TestGetTFInitArgs_StringBoolCoercion verifies that string boolean values\n// (from HCL ternary type unification) pass through correctly to terraform init -backend-config args.\nfunc TestGetTFInitArgs_StringBoolCoercion(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname         string\n\t\tbackendName  string\n\t\tconfig       map[string]any\n\t\texpectedArgs []string\n\t}{\n\t\t{\n\t\t\t\"s3-string-bool-use-lockfile\",\n\t\t\t\"s3\",\n\t\t\tmap[string]any{\n\t\t\t\t\"bucket\":       \"my-bucket\",\n\t\t\t\t\"key\":          \"terraform.tfstate\",\n\t\t\t\t\"region\":       \"us-east-1\",\n\t\t\t\t\"encrypt\":      \"true\",\n\t\t\t\t\"use_lockfile\": \"true\",\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t\"-backend-config=bucket=my-bucket\",\n\t\t\t\t\"-backend-config=key=terraform.tfstate\",\n\t\t\t\t\"-backend-config=region=us-east-1\",\n\t\t\t\t\"-backend-config=encrypt=true\",\n\t\t\t\t\"-backend-config=use_lockfile=true\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"s3-native-bool-use-lockfile\",\n\t\t\t\"s3\",\n\t\t\tmap[string]any{\n\t\t\t\t\"bucket\":       \"my-bucket\",\n\t\t\t\t\"key\":          \"terraform.tfstate\",\n\t\t\t\t\"region\":       \"us-east-1\",\n\t\t\t\t\"encrypt\":      true,\n\t\t\t\t\"use_lockfile\": true,\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t\"-backend-config=bucket=my-bucket\",\n\t\t\t\t\"-backend-config=key=terraform.tfstate\",\n\t\t\t\t\"-backend-config=region=us-east-1\",\n\t\t\t\t\"-backend-config=encrypt=true\",\n\t\t\t\t\"-backend-config=use_lockfile=true\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"s3-string-bool-false\",\n\t\t\t\"s3\",\n\t\t\tmap[string]any{\n\t\t\t\t\"bucket\":       \"my-bucket\",\n\t\t\t\t\"key\":          \"terraform.tfstate\",\n\t\t\t\t\"region\":       \"us-east-1\",\n\t\t\t\t\"use_lockfile\": \"false\",\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t\"-backend-config=bucket=my-bucket\",\n\t\t\t\t\"-backend-config=key=terraform.tfstate\",\n\t\t\t\t\"-backend-config=region=us-east-1\",\n\t\t\t\t\"-backend-config=use_lockfile=false\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"gcs-string-bool-skip-versioning\",\n\t\t\t\"gcs\",\n\t\t\tmap[string]any{\n\t\t\t\t\"bucket\":                 \"my-bucket\",\n\t\t\t\t\"prefix\":                 \"terraform.tfstate\",\n\t\t\t\t\"skip_bucket_versioning\": \"true\",\n\t\t\t},\n\t\t\t[]string{\n\t\t\t\t\"-backend-config=bucket=my-bucket\",\n\t\t\t\t\"-backend-config=prefix=terraform.tfstate\",\n\t\t\t},\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.Parallel()\n\n\t\t\tcfg := &remotestate.Config{\n\t\t\t\tBackendName:   tc.backendName,\n\t\t\t\tBackendConfig: tc.config,\n\t\t\t}\n\t\t\targs := remotestate.New(cfg).GetTFInitArgs()\n\n\t\t\tassert.ElementsMatch(t, tc.expectedArgs, args)\n\t\t})\n\t}\n}\n\nfunc TestNeedsBootstrapDisableInit(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := &remotestate.Config{\n\t\tBackendName: \"s3\",\n\t\tDisableInit: true,\n\t\tBackendConfig: map[string]any{\n\t\t\t\"bucket\": \"my-bucket\",\n\t\t\t\"key\":    \"terraform.tfstate\",\n\t\t\t\"region\": \"us-east-1\",\n\t\t},\n\t}\n\n\tremote := remotestate.New(cfg)\n\tneedsBootstrap, err := remote.NeedsBootstrap(t.Context(), logger.CreateLogger(), &remotestate.Options{})\n\n\trequire.NoError(t, err)\n\tassert.False(t, needsBootstrap, \"NeedsBootstrap must return false when DisableInit=true\")\n}\n\nfunc assertTerraformInitArgsEqual(t *testing.T, actualArgs []string, expectedArgs string) {\n\tt.Helper()\n\n\texpected := strings.Split(expectedArgs, \" \")\n\tassert.Len(t, actualArgs, len(expected))\n\n\tfor _, expectedArg := range expected {\n\t\tassert.Contains(t, actualArgs, expectedArg)\n\t}\n}\n"
  },
  {
    "path": "internal/remotestate/terraform_state_file.go",
    "content": "package remotestate\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n)\n\n// TODO: this file could be changed to use the Terraform Go code to read state files, but that code is relatively\n// complicated and doesn't seem to be designed for standalone use. Fortunately, the .tfstate format is a fairly simple\n// JSON format, so hopefully this simple parsing code will not be a maintenance burden.\n\n// DefaultPathToLocalStateFile is the default path to the tfstate file when storing Terraform state locally.\nconst DefaultPathToLocalStateFile = \"terraform.tfstate\"\n\n// DefaultPathToRemoteStateFile is the default folder location for the local copy of the state file when using remote\n// state storage in Terraform.\nconst DefaultPathToRemoteStateFile = \"terraform.tfstate\"\n\n// TerraformState - represents the structure of the Terraform .tfstate file.\ntype TerraformState struct {\n\tBackend *TerraformBackend      `json:\"Backend\"`\n\tModules []TerraformStateModule `json:\"Modules\"`\n\tVersion int                    `json:\"Version\"`\n\tSerial  int                    `json:\"Serial\"`\n}\n\n// TerraformBackend represents the structure of the \"backend\" section in the Terraform .tfstate file.\ntype TerraformBackend struct {\n\tConfig map[string]any `json:\"Config\"`\n\tType   string         `json:\"Type\"`\n}\n\n// TerraformStateModule represents the structure of a \"module\" section in the Terraform .tfstate file.\ntype TerraformStateModule struct {\n\tOutputs   map[string]any `json:\"Outputs\"`\n\tResources map[string]any `json:\"Resources\"`\n\tPath      []string       `json:\"Path\"`\n}\n\n// IsRemote returns true if this Terraform state is configured for remote state storage.\nfunc (state *TerraformState) IsRemote() bool {\n\treturn state != nil && state.Backend != nil && state.Backend.Type != \"local\"\n}\n\n// ParseTerraformStateFileFromLocation parses the Terraform .tfstate file. If a local backend is used then search\n// the given path, or return nil if the file is missing. If the backend is not local then parse the Terraform .tfstate\n// file from the location specified by workingDir. If no location is specified, search the current\n// directory. If the file doesn't exist at any of the default locations, return nil.\nfunc ParseTerraformStateFileFromLocation(backend string, config backend.Config, workingDir, dataDir string) (*TerraformState, error) {\n\tif stateFile := config.Path(); backend == \"local\" && stateFile != \"\" && util.FileExists(stateFile) {\n\t\treturn ParseTerraformStateFile(stateFile)\n\t}\n\n\tif util.FileExists(filepath.Join(dataDir, DefaultPathToRemoteStateFile)) {\n\t\treturn ParseTerraformStateFile(filepath.Join(dataDir, DefaultPathToRemoteStateFile))\n\t}\n\n\tif util.FileExists(filepath.Join(workingDir, DefaultPathToLocalStateFile)) {\n\t\treturn ParseTerraformStateFile(filepath.Join(workingDir, DefaultPathToLocalStateFile))\n\t}\n\n\treturn nil, nil\n}\n\n// ParseTerraformStateFile parses the Terraform .tfstate file located at the specified path.\nfunc ParseTerraformStateFile(path string) (*TerraformState, error) {\n\tbytes, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, errors.New(CantParseTerraformStateFileError{Path: path, UnderlyingErr: err})\n\t}\n\n\tstate, err := ParseTerraformState(bytes)\n\tif err != nil {\n\t\treturn nil, errors.New(CantParseTerraformStateFileError{Path: path, UnderlyingErr: err})\n\t}\n\n\treturn state, nil\n}\n\n// ParseTerraformState parses the Terraform state file data from the provided byte slice.\nfunc ParseTerraformState(terraformStateData []byte) (*TerraformState, error) {\n\tterraformState := &TerraformState{}\n\n\tif len(terraformStateData) == 0 {\n\t\treturn terraformState, nil\n\t}\n\n\tif err := json.Unmarshal(terraformStateData, terraformState); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\treturn terraformState, nil\n}\n\n// CantParseTerraformStateFileError error that occurs when we can't parse the Terraform state file.\ntype CantParseTerraformStateFileError struct {\n\tUnderlyingErr error\n\tPath          string\n}\n\nfunc (err CantParseTerraformStateFileError) Error() string {\n\treturn fmt.Sprintf(\"Error parsing Terraform state file %s: %s\", err.Path, err.UnderlyingErr.Error())\n}\n"
  },
  {
    "path": "internal/remotestate/terraform_state_file_test.go",
    "content": "package remotestate_test\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\n\t\"errors\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParseTerraformStateLocal(t *testing.T) {\n\tt.Parallel()\n\n\tstateFile :=\n\t\t`\n\t{\n\t\t\"version\": 1,\n\t\t\"serial\": 0,\n\t\t\"modules\": [\n\t\t\t{\n\t\t\t\t\"path\": [\n\t\t\t\t\t\"root\"\n\t\t\t\t],\n\t\t\t\t\"outputs\": {},\n\t\t\t\t\"resources\": {}\n\t\t\t}\n\t\t]\n\t}\n\t`\n\n\texpectedTerraformState := &remotestate.TerraformState{\n\t\tVersion: 1,\n\t\tSerial:  0,\n\t\tBackend: nil,\n\t\tModules: []remotestate.TerraformStateModule{\n\t\t\t{\n\t\t\t\tPath:      []string{\"root\"},\n\t\t\t\tOutputs:   map[string]any{},\n\t\t\t\tResources: map[string]any{},\n\t\t\t},\n\t\t},\n\t}\n\n\tactualTerraformState, err := remotestate.ParseTerraformState([]byte(stateFile))\n\n\trequire.NoError(t, err)\n\tassert.Equal(t, expectedTerraformState, actualTerraformState)\n\tassert.False(t, actualTerraformState.IsRemote())\n}\n\nfunc TestParseTerraformStateRemote(t *testing.T) {\n\tt.Parallel()\n\n\tstateFile :=\n\t\t`\n\t{\n\t\t\"version\": 5,\n\t\t\"serial\": 12,\n\t\t\"backend\": {\n\t\t\t\"type\": \"s3\",\n\t\t\t\"config\": {\n\t\t\t\t\"bucket\": \"bucket\",\n\t\t\t\t\"encrypt\": true,\n\t\t\t\t\"key\": \"experiment-1.tfstate\",\n\t\t\t\t\"region\": \"us-east-1\"\n\t\t\t}\n\t\t},\n\t\t\"modules\": [\n\t\t\t{\n\t\t\t\t\"path\": [\n\t\t\t\t\t\"root\"\n\t\t\t\t],\n\t\t\t\t\"outputs\": {},\n\t\t\t\t\"resources\": {}\n\t\t\t}\n\t\t]\n\t}\n\t`\n\n\texpectedTerraformState := &remotestate.TerraformState{\n\t\tVersion: 5,\n\t\tSerial:  12,\n\t\tBackend: &remotestate.TerraformBackend{\n\t\t\tType: \"s3\",\n\t\t\tConfig: map[string]any{\n\t\t\t\t\"bucket\":  \"bucket\",\n\t\t\t\t\"encrypt\": true,\n\t\t\t\t\"key\":     \"experiment-1.tfstate\",\n\t\t\t\t\"region\":  \"us-east-1\",\n\t\t\t},\n\t\t},\n\t\tModules: []remotestate.TerraformStateModule{\n\t\t\t{\n\t\t\t\tPath:      []string{\"root\"},\n\t\t\t\tOutputs:   map[string]any{},\n\t\t\t\tResources: map[string]any{},\n\t\t\t},\n\t\t},\n\t}\n\n\tactualTerraformState, err := remotestate.ParseTerraformState([]byte(stateFile))\n\n\trequire.NoError(t, err)\n\tassert.Equal(t, expectedTerraformState, actualTerraformState)\n\tassert.True(t, actualTerraformState.IsRemote())\n}\n\nfunc TestParseTerraformStateRemoteFull(t *testing.T) {\n\tt.Parallel()\n\n\t// This is a small snippet (with lots of editing) of Terraform templates that created a VPC\n\tstateFile :=\n\t\t`\n\t{\n\t    \"version\": 1,\n\t    \"serial\": 51,\n\t    \"backend\": {\n\t\t\"type\": \"s3\",\n\t\t\"config\": {\n\t\t    \"bucket\": \"bucket\",\n\t\t    \"encrypt\": true,\n\t\t    \"key\": \"terraform.tfstate\",\n\t\t    \"region\": \"us-east-1\"\n\t\t}\n\t    },\n\t    \"modules\": [\n\t\t{\n\t\t    \"path\": [\n\t\t\t\"root\"\n\t\t    ],\n\t\t    \"outputs\": {\n\t\t\t\"key1\": \"value1\",\n\t\t\t\"key2\": \"value2\",\n\t\t\t\"key3\": \"value3\"\n\t\t    },\n\t\t    \"resources\": {}\n\t\t},\n\t\t{\n\t\t    \"path\": [\n\t\t\t\"root\",\n\t\t\t\"module_with_outputs_no_resources\"\n\t\t    ],\n\t\t    \"outputs\": {\n\t\t\t\"key1\": \"\",\n\t\t\t\"key2\": \"\"\n\t\t    },\n\t\t    \"resources\": {}\n\t\t},\n\t\t{\n\t\t    \"path\": [\n\t\t\t\"root\",\n\t\t\t\"module_with_resources_no_outputs\"\n\t\t    ],\n\t\t    \"outputs\": {},\n\t\t    \"resources\": {\n\t\t\t\"aws_eip.nat.0\": {\n\t\t\t    \"type\": \"aws_eip\",\n\t\t\t    \"depends_on\": [\n\t\t\t\t\"aws_internet_gateway.main\"\n\t\t\t    ],\n\t\t\t    \"primary\": {\n\t\t\t\t\"id\": \"eipalloc-b421becd\",\n\t\t\t\t\"attributes\": {\n\t\t\t\t    \"association_id\": \"\",\n\t\t\t\t    \"domain\": \"vpc\",\n\t\t\t\t    \"id\": \"eipalloc-b421becd\",\n\t\t\t\t    \"instance\": \"\",\n\t\t\t\t    \"network_interface\": \"\",\n\t\t\t\t    \"private_ip\": \"\",\n\t\t\t\t    \"public_ip\": \"23.20.182.117\",\n\t\t\t\t    \"vpc\": \"true\"\n\t\t\t\t}\n\t\t\t    }\n\t\t\t},\n\t\t\t\"aws_eip.nat.1\": {\n\t\t\t    \"type\": \"aws_eip\",\n\t\t\t    \"depends_on\": [\n\t\t\t\t\"aws_internet_gateway.main\"\n\t\t\t    ],\n\t\t\t    \"primary\": {\n\t\t\t\t\"id\": \"eipalloc-95d846ec\",\n\t\t\t\t\"attributes\": {\n\t\t\t\t    \"association_id\": \"\",\n\t\t\t\t    \"domain\": \"vpc\",\n\t\t\t\t    \"id\": \"eipalloc-95d846ec\",\n\t\t\t\t    \"instance\": \"\",\n\t\t\t\t    \"network_interface\": \"\",\n\t\t\t\t    \"private_ip\": \"\",\n\t\t\t\t    \"public_ip\": \"52.21.82.253\",\n\t\t\t\t    \"vpc\": \"true\"\n\t\t\t\t}\n\t\t\t    }\n\t\t\t}\n\t\t    }\n\t\t},\n\t\t{\n\t\t    \"path\": [\n\t\t\t\"root\",\n\t\t\t\"module_level_1\",\n\t\t\t\"module_level_2\"\n\t\t    ],\n\t\t    \"outputs\": {},\n\t\t    \"resources\": {}\n\t\t}\n\t    ]\n\t}\n\n\t`\n\n\texpectedTerraformState := &remotestate.TerraformState{\n\t\tVersion: 1,\n\t\tSerial:  51,\n\t\tBackend: &remotestate.TerraformBackend{\n\t\t\tType: \"s3\",\n\t\t\tConfig: map[string]any{\n\t\t\t\t\"bucket\":  \"bucket\",\n\t\t\t\t\"encrypt\": true,\n\t\t\t\t\"key\":     \"terraform.tfstate\",\n\t\t\t\t\"region\":  \"us-east-1\",\n\t\t\t},\n\t\t},\n\t\tModules: []remotestate.TerraformStateModule{\n\t\t\t{\n\t\t\t\tPath: []string{\"root\"},\n\t\t\t\tOutputs: map[string]any{\n\t\t\t\t\t\"key1\": \"value1\",\n\t\t\t\t\t\"key2\": \"value2\",\n\t\t\t\t\t\"key3\": \"value3\",\n\t\t\t\t},\n\t\t\t\tResources: map[string]any{},\n\t\t\t},\n\t\t\t{\n\t\t\t\tPath: []string{\"root\", \"module_with_outputs_no_resources\"},\n\t\t\t\tOutputs: map[string]any{\n\t\t\t\t\t\"key1\": \"\",\n\t\t\t\t\t\"key2\": \"\",\n\t\t\t\t},\n\t\t\t\tResources: map[string]any{},\n\t\t\t},\n\t\t\t{\n\t\t\t\tPath:    []string{\"root\", \"module_with_resources_no_outputs\"},\n\t\t\t\tOutputs: map[string]any{},\n\t\t\t\tResources: map[string]any{\n\t\t\t\t\t\"aws_eip.nat.0\": map[string]any{\n\t\t\t\t\t\t\"type\":       \"aws_eip\",\n\t\t\t\t\t\t\"depends_on\": []any{\"aws_internet_gateway.main\"},\n\t\t\t\t\t\t\"primary\": map[string]any{\n\t\t\t\t\t\t\t\"id\": \"eipalloc-b421becd\",\n\t\t\t\t\t\t\t\"attributes\": map[string]any{\n\t\t\t\t\t\t\t\t\"association_id\":    \"\",\n\t\t\t\t\t\t\t\t\"domain\":            \"vpc\",\n\t\t\t\t\t\t\t\t\"id\":                \"eipalloc-b421becd\",\n\t\t\t\t\t\t\t\t\"instance\":          \"\",\n\t\t\t\t\t\t\t\t\"network_interface\": \"\",\n\t\t\t\t\t\t\t\t\"private_ip\":        \"\",\n\t\t\t\t\t\t\t\t\"public_ip\":         \"23.20.182.117\",\n\t\t\t\t\t\t\t\t\"vpc\":               \"true\",\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\"aws_eip.nat.1\": map[string]any{\n\t\t\t\t\t\t\"type\":       \"aws_eip\",\n\t\t\t\t\t\t\"depends_on\": []any{\"aws_internet_gateway.main\"},\n\t\t\t\t\t\t\"primary\": map[string]any{\n\t\t\t\t\t\t\t\"id\": \"eipalloc-95d846ec\",\n\t\t\t\t\t\t\t\"attributes\": map[string]any{\n\t\t\t\t\t\t\t\t\"association_id\":    \"\",\n\t\t\t\t\t\t\t\t\"domain\":            \"vpc\",\n\t\t\t\t\t\t\t\t\"id\":                \"eipalloc-95d846ec\",\n\t\t\t\t\t\t\t\t\"instance\":          \"\",\n\t\t\t\t\t\t\t\t\"network_interface\": \"\",\n\t\t\t\t\t\t\t\t\"private_ip\":        \"\",\n\t\t\t\t\t\t\t\t\"public_ip\":         \"52.21.82.253\",\n\t\t\t\t\t\t\t\t\"vpc\":               \"true\",\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\tPath:      []string{\"root\", \"module_level_1\", \"module_level_2\"},\n\t\t\t\tOutputs:   map[string]any{},\n\t\t\t\tResources: map[string]any{},\n\t\t\t},\n\t\t},\n\t}\n\n\tactualTerraformState, err := remotestate.ParseTerraformState([]byte(stateFile))\n\n\trequire.NoError(t, err)\n\tassert.Equal(t, expectedTerraformState, actualTerraformState)\n\tassert.True(t, actualTerraformState.IsRemote())\n}\n\nfunc TestParseTerraformStateEmpty(t *testing.T) {\n\tt.Parallel()\n\n\tstateFile := `{}`\n\n\texpectedTerraformState := &remotestate.TerraformState{}\n\n\tactualTerraformState, err := remotestate.ParseTerraformState([]byte(stateFile))\n\n\trequire.NoError(t, err)\n\tassert.Equal(t, expectedTerraformState, actualTerraformState)\n\tassert.False(t, actualTerraformState.IsRemote())\n}\n\nfunc TestParseTerraformStateInvalid(t *testing.T) {\n\tt.Parallel()\n\n\tstateFile := `not-valid-json`\n\n\tactualTerraformState, err := remotestate.ParseTerraformState([]byte(stateFile))\n\n\tassert.Nil(t, actualTerraformState)\n\trequire.Error(t, err)\n\n\tvar jsonSyntaxError *json.SyntaxError\n\n\tok := errors.As(err, &jsonSyntaxError)\n\tassert.True(t, ok)\n}\n"
  },
  {
    "path": "internal/report/colors.go",
    "content": "package report\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/mgutz/ansi\"\n)\n\n// Colorizer is a colorizer for the run summary output.\ntype Colorizer struct {\n\theadingTitleColorizer func(string) string\n\theadingUnitColorizer  func(string) string\n\tsuccessColorizer      func(string) string\n\tfailureColorizer      func(string) string\n\texitColorizer         func(string) string\n\texcludeColorizer      func(string) string\n\tsuccessUnitColorizer  func(string) string\n\tfailureUnitColorizer  func(string) string\n\texitUnitColorizer     func(string) string\n\texcludeUnitColorizer  func(string) string\n\tnanosecondColorizer   func(string) string\n\tmicrosecondColorizer  func(string) string\n\tmillisecondColorizer  func(string) string\n\tsecondColorizer       func(string) string\n\tminuteColorizer       func(string) string\n\tdefaultColorizer      func(string) string\n\tpaddingColorizer      func(string) string\n}\n\n// NewColorizer creates a new Colorizer.\nfunc NewColorizer(shouldColor bool) *Colorizer {\n\tif !shouldColor {\n\t\treturn &Colorizer{\n\t\t\theadingTitleColorizer: func(s string) string { return s },\n\t\t\theadingUnitColorizer:  func(s string) string { return s },\n\t\t\tsuccessColorizer:      func(s string) string { return s },\n\t\t\tfailureColorizer:      func(s string) string { return s },\n\t\t\texitColorizer:         func(s string) string { return s },\n\t\t\texcludeColorizer:      func(s string) string { return s },\n\t\t\tsuccessUnitColorizer:  func(s string) string { return s },\n\t\t\tfailureUnitColorizer:  func(s string) string { return s },\n\t\t\texitUnitColorizer:     func(s string) string { return s },\n\t\t\texcludeUnitColorizer:  func(s string) string { return s },\n\t\t\tnanosecondColorizer:   func(s string) string { return s },\n\t\t\tmicrosecondColorizer:  func(s string) string { return s },\n\t\t\tmillisecondColorizer:  func(s string) string { return s },\n\t\t\tsecondColorizer:       func(s string) string { return s },\n\t\t\tminuteColorizer:       func(s string) string { return s },\n\t\t\tdefaultColorizer:      func(s string) string { return s },\n\t\t\tpaddingColorizer:      func(s string) string { return s },\n\t\t}\n\t}\n\n\t// Define unit colorizers based on environment variable\n\tvar successUnitColorizer, failureUnitColorizer, exitUnitColorizer, excludeUnitColorizer func(string) string\n\tif shouldColor {\n\t\tsuccessUnitColorizer = ansi.ColorFunc(\"green+h\")\n\t\tfailureUnitColorizer = ansi.ColorFunc(\"red+h\")\n\t\texitUnitColorizer = ansi.ColorFunc(\"yellow+h\")\n\t\texcludeUnitColorizer = ansi.ColorFunc(\"blue+h\")\n\t} else {\n\t\tsuccessUnitColorizer = func(s string) string { return s }\n\t\tfailureUnitColorizer = func(s string) string { return s }\n\t\texitUnitColorizer = func(s string) string { return s }\n\t\texcludeUnitColorizer = func(s string) string { return s }\n\t}\n\n\treturn &Colorizer{\n\t\theadingTitleColorizer: ansi.ColorFunc(\"yellow+bh\"),\n\t\theadingUnitColorizer:  ansi.ColorFunc(\"white+bh\"),\n\t\tsuccessColorizer:      ansi.ColorFunc(\"green+bh\"),\n\t\tfailureColorizer:      ansi.ColorFunc(\"red+bh\"),\n\t\texitColorizer:         ansi.ColorFunc(\"yellow+bh\"),\n\t\texcludeColorizer:      ansi.ColorFunc(\"blue+bh\"),\n\t\tsuccessUnitColorizer:  successUnitColorizer,\n\t\tfailureUnitColorizer:  failureUnitColorizer,\n\t\texitUnitColorizer:     exitUnitColorizer,\n\t\texcludeUnitColorizer:  excludeUnitColorizer,\n\t\tnanosecondColorizer:   ansi.ColorFunc(\"cyan+bh\"),\n\t\tmicrosecondColorizer:  ansi.ColorFunc(\"cyan+bh\"),\n\t\tmillisecondColorizer:  ansi.ColorFunc(\"cyan+bh\"),\n\t\tsecondColorizer:       ansi.ColorFunc(\"green+bh\"),\n\t\tminuteColorizer:       ansi.ColorFunc(\"yellow+bh\"),\n\t\tdefaultColorizer:      ansi.ColorFunc(\"white+bh\"),\n\t\tpaddingColorizer:      ansi.ColorFunc(\"gray\"),\n\t}\n}\n\n// colorDuration returns the duration as a string, colored based on the duration.\nfunc (c *Colorizer) colorDuration(duration time.Duration) string {\n\t// if duration is negative, return \"N/A\" in default color\n\tif duration < 0 {\n\t\treturn c.defaultColorizer(\"N/A\")\n\t}\n\n\tif duration < time.Microsecond {\n\t\treturn c.nanosecondColorizer(fmt.Sprintf(\"%dns\", duration.Nanoseconds()))\n\t}\n\n\tif duration < time.Millisecond {\n\t\treturn c.microsecondColorizer(fmt.Sprintf(\"%dµs\", duration.Microseconds()))\n\t}\n\n\tif duration < time.Second {\n\t\treturn c.millisecondColorizer(fmt.Sprintf(\"%dms\", duration.Milliseconds()))\n\t}\n\n\tif duration < time.Minute {\n\t\treturn c.secondColorizer(fmt.Sprintf(\"%ds\", int(duration.Seconds())))\n\t}\n\n\treturn c.minuteColorizer(fmt.Sprintf(\"%dm\", int(duration.Minutes())))\n}\n"
  },
  {
    "path": "internal/report/report.go",
    "content": "// Package report provides a mechanism for collecting data on runs and generating a reports and summaries on that data.\npackage report\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// Report captures data for a report/summary.\ntype Report struct {\n\tworkingDir           string\n\tformat               Format\n\tRuns                 []*Run\n\tmu                   sync.RWMutex\n\tshouldColor          bool\n\tshowUnitLevelSummary bool\n}\n\n// Run captures data for a run.\ntype Run struct {\n\tStarted             time.Time\n\tEnded               time.Time\n\tReason              *Reason\n\tCause               *Cause\n\tPath                string\n\tResult              Result\n\tDiscoveryWorkingDir string\n\tRef                 string\n\tCmd                 string\n\tArgs                []string\n\tmu                  sync.RWMutex\n}\n\n// Result captures the result of a run.\ntype Result string\n\n// Reason captures the reason for a run.\ntype Reason string\n\n// Cause captures the cause of a run.\ntype Cause string\n\n// Format captures the format of a report.\ntype Format string\n\nconst (\n\tFormatCSV  Format = \"csv\"\n\tFormatJSON Format = \"json\"\n)\n\nconst (\n\tResultSucceeded Result = \"succeeded\"\n\tResultFailed    Result = \"failed\"\n\tResultEarlyExit Result = \"early exit\"\n\tResultExcluded  Result = \"excluded\"\n)\n\nconst (\n\tReasonRetrySucceeded Reason = \"retry succeeded\"\n\tReasonErrorIgnored   Reason = \"error ignored\"\n\tReasonRunError       Reason = \"run error\"\n\tReasonExcludeBlock   Reason = \"exclude block\"\n\tReasonAncestorError  Reason = \"ancestor error\"\n)\n\n// NewReport creates a new report.\nfunc NewReport() *Report {\n\treport := &Report{\n\t\tRuns:        make([]*Run, 0),\n\t\tshouldColor: true,\n\t}\n\n\treturn report\n}\n\n// NewReportOption is an option for creating a new report.\ntype NewReportOption func(*Report)\n\n// WithDisableColor sets the shouldColor flag for the report.\nfunc (r *Report) WithDisableColor() *Report {\n\tr.shouldColor = false\n\n\treturn r\n}\n\n// WithWorkingDir sets the working directory for the report.\nfunc (r *Report) WithWorkingDir(workingDir string) *Report {\n\tr.workingDir = workingDir\n\n\treturn r\n}\n\n// WithFormat sets the format for the report.\nfunc (r *Report) WithFormat(format Format) *Report {\n\tr.format = format\n\n\treturn r\n}\n\n// WithShowUnitLevelSummary sets the showUnitLevelSummary flag for the report.\n//\n// When enabled, the summary of the report will include timings for each unit.\nfunc (r *Report) WithShowUnitLevelSummary() *Report {\n\tr.showUnitLevelSummary = true\n\n\treturn r\n}\n\n// ErrPathMustBeAbsolute is returned when a report run path is not absolute.\nvar ErrPathMustBeAbsolute = errors.New(\"report run path must be absolute\")\n\n// NewRun creates a new run.\n// The path passed in must be an absolute path to ensure that the run can be uniquely identified.\nfunc NewRun(path string) (*Run, error) {\n\tif !filepath.IsAbs(path) {\n\t\treturn nil, ErrPathMustBeAbsolute\n\t}\n\n\treturn &Run{\n\t\tPath:    path,\n\t\tStarted: time.Now(),\n\t}, nil\n}\n\n// ErrRunAlreadyExists is returned when a run already exists in the report.\nvar ErrRunAlreadyExists = errors.New(\"run already exists\")\n\n// AddRun adds a run to the report.\n// If the run already exists, it returns the ErrRunAlreadyExists error.\nfunc (r *Report) AddRun(l log.Logger, run *Run) error {\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\n\tfor _, existingRun := range r.Runs {\n\t\tif existingRun.Path == run.Path {\n\t\t\treturn fmt.Errorf(\"%w: %s\", ErrRunAlreadyExists, run.Path)\n\t\t}\n\t}\n\n\tl.Debugf(\"Adding report run %s\", run.Path)\n\n\tr.Runs = append(r.Runs, run)\n\n\treturn nil\n}\n\n// ErrRunNotFound is returned when a run is not found in the report.\nvar ErrRunNotFound = errors.New(\"run not found in report\")\n\n// GetRun returns a run from the report.\n// The path passed in must be an absolute path to ensure that the run can be uniquely identified.\nfunc (r *Report) GetRun(path string) (*Run, error) {\n\tr.mu.RLock()\n\tdefer r.mu.RUnlock()\n\n\tif !filepath.IsAbs(path) {\n\t\treturn nil, ErrPathMustBeAbsolute\n\t}\n\n\tfor _, run := range r.Runs {\n\t\tif run.Path == path {\n\t\t\treturn run, nil\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"%w: %s\", ErrRunNotFound, path)\n}\n\n// EnsureRun tries to get a run from the report.\n// If the run does not exist, it creates a new run and adds it to the report, then returns the run.\n// This is useful when a run is being ended that might not have been started due to exclusion, etc.\nfunc (r *Report) EnsureRun(l log.Logger, path string, opts ...EndOption) (*Run, error) {\n\trun, err := r.GetRun(path)\n\tif err == nil {\n\t\tl.Debugf(\"Report run %s already exists, returning existing run\", path)\n\n\t\trun.mu.Lock()\n\t\tdefer run.mu.Unlock()\n\n\t\tfor _, opt := range opts {\n\t\t\topt(run)\n\t\t}\n\n\t\treturn run, nil\n\t}\n\n\tif !errors.Is(err, ErrRunNotFound) {\n\t\treturn run, err\n\t}\n\n\tl.Debugf(\"Report run %s not found, creating new run\", path)\n\n\trun, err = NewRun(path)\n\tif err != nil {\n\t\treturn run, err\n\t}\n\n\tfor _, opt := range opts {\n\t\topt(run)\n\t}\n\n\tif err = r.AddRun(l, run); err != nil {\n\t\treturn run, err\n\t}\n\n\treturn run, nil\n}\n\n// EndRun ends a run and adds it to the report.\n// If the run does not exist, it returns the ErrRunNotFound error.\n// By default, the run is assumed to have succeeded. To change this, pass WithResult to the function.\n// If the run has already ended from an early exit, it does nothing.\nfunc (r *Report) EndRun(l log.Logger, path string, endOptions ...EndOption) error {\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\n\tif !filepath.IsAbs(path) {\n\t\treturn ErrPathMustBeAbsolute\n\t}\n\n\tvar run *Run\n\n\tfor _, existingRun := range r.Runs {\n\t\tif existingRun.Path == path {\n\t\t\trun = existingRun\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif run == nil {\n\t\treturn fmt.Errorf(\"%w: %s\", ErrRunNotFound, path)\n\t}\n\n\t// If the run has already ended from an early exit or excluded, we don't need to do anything.\n\tif !run.Ended.IsZero() && (run.Result == ResultEarlyExit || run.Result == ResultExcluded) {\n\t\treturn nil\n\t}\n\n\trun.mu.Lock()\n\tdefer run.mu.Unlock()\n\n\trun.Ended = time.Now()\n\trun.Result = ResultSucceeded\n\n\tfor _, endOption := range endOptions {\n\t\tendOption(run)\n\t}\n\n\tl.Debugf(\"Ending report run %s with result %s\", path, run.Result)\n\n\treturn nil\n}\n\nfunc (r *Report) SortRuns() {\n\tslices.SortFunc(r.Runs, func(a, b *Run) int {\n\t\treturn a.Started.Compare(b.Started)\n\t})\n}\n\n// EndOption are optional configurations for ending a run.\ntype EndOption func(*Run)\n\n// WithResult sets the result of a run.\nfunc WithResult(result Result) EndOption {\n\treturn func(run *Run) {\n\t\trun.Result = result\n\t}\n}\n\n// WithReason sets the reason of a run.\nfunc WithReason(reason Reason) EndOption {\n\treturn func(run *Run) {\n\t\trun.Reason = &reason\n\t}\n}\n\n// WithCauseRetryBlock sets the cause of a run to the name of a particular retry block.\n//\n// This function is a wrapper around withCause, just to make sure that authors always use consistent\n// reasons for causes.\nfunc WithCauseRetryBlock(name string) EndOption {\n\treturn withCause(name)\n}\n\n// WithCauseIgnoreBlock sets the cause of a run to the name of a particular ignore block.\n//\n// This function is a wrapper around withCause, just to make sure that authors always use consistent\n// reasons for causes.\nfunc WithCauseIgnoreBlock(name string) EndOption {\n\treturn withCause(name)\n}\n\n// WithCauseExcludeBlock sets the cause of a run to the name of a particular exclude block.\n//\n// This function is a wrapper around withCause, just to make sure that authors always use consistent\n// reasons for causes.\nfunc WithCauseExcludeBlock(name string) EndOption {\n\treturn withCause(name)\n}\n\n// WithCauseAncestorExit sets the cause of a run to the name of a particular ancestor that exited.\n//\n// This function is a wrapper around withCause, just to make sure that authors always use consistent\n// reasons for causes.\nfunc WithCauseAncestorExit(name string) EndOption {\n\treturn withCause(name)\n}\n\n// WithCauseRunError sets the cause of a run to the name of a particular run error.\n//\n// This function is a wrapper around withCause, just to make sure that authors always use consistent\n// reasons for causes.\nfunc WithCauseRunError(name string) EndOption {\n\treturn withCause(name)\n}\n\n// WithDiscoveryWorkingDir sets the discovery working directory for a run.\n// This is used to compute relative paths for units discovered in worktrees.\nfunc WithDiscoveryWorkingDir(workingDir string) EndOption {\n\treturn func(run *Run) {\n\t\trun.DiscoveryWorkingDir = workingDir\n\t}\n}\n\n// WithRef sets the worktree reference for a run.\n// This is typically a git commit, branch, or tag.\nfunc WithRef(ref string) EndOption {\n\treturn func(run *Run) {\n\t\trun.Ref = ref\n\t}\n}\n\n// WithCmd sets the tofu/terraform command for a run.\n// This is the main tofu/terraform command being executed (e.g., plan, apply).\nfunc WithCmd(cmd string) EndOption {\n\treturn func(run *Run) {\n\t\trun.Cmd = cmd\n\t}\n}\n\n// WithArgs sets the terraform CLI arguments for a run.\nfunc WithArgs(args []string) EndOption {\n\treturn func(run *Run) {\n\t\trun.Args = args\n\t}\n}\n\n// withCause sets the cause of a run to the name of a particular cause.\nfunc withCause(name string) EndOption {\n\treturn func(run *Run) {\n\t\tcause := Cause(name)\n\t\trun.Cause = &cause\n\t}\n}\n"
  },
  {
    "path": "internal/report/report_test.go",
    "content": "package report_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/csv\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n\t\"testing\"\n\t\"testing/synctest\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/xeipuuv/gojsonschema\"\n)\n\nfunc TestNewReport(t *testing.T) {\n\tt.Parallel()\n\n\treport := report.NewReport()\n\tassert.NotNil(t, report)\n\tassert.NotNil(t, report.Runs)\n\tassert.Empty(t, report.Runs)\n}\n\nfunc TestNewRun(t *testing.T) {\n\tt.Parallel()\n\n\ttmp := helpers.TmpDirWOSymlinks(t)\n\n\tpath := filepath.Join(tmp, \"test-run\")\n\trun := newRun(t, path)\n\tassert.NotNil(t, run)\n\tassert.Equal(t, path, run.Path)\n\tassert.False(t, run.Started.IsZero())\n\tassert.True(t, run.Ended.IsZero())\n\tassert.Empty(t, run.Result)\n\tassert.Nil(t, run.Reason)\n\tassert.Nil(t, run.Cause)\n}\n\nfunc TestAddRun(t *testing.T) {\n\tt.Parallel()\n\n\ttmp := helpers.TmpDirWOSymlinks(t)\n\n\tl := logger.CreateLogger()\n\n\tpath := filepath.Join(tmp, \"test-run\")\n\n\tr := report.NewReport()\n\n\terr := r.AddRun(l, newRun(t, path))\n\trequire.NoError(t, err)\n\tassert.Len(t, r.Runs, 1)\n\n\terr = r.AddRun(l, newRun(t, path))\n\trequire.Error(t, err)\n\tassert.ErrorIs(t, err, report.ErrRunAlreadyExists)\n}\n\nfunc TestGetRun(t *testing.T) {\n\tt.Parallel()\n\n\ttmp := helpers.TmpDirWOSymlinks(t)\n\n\tl := logger.CreateLogger()\n\n\tr := report.NewReport()\n\trun := newRun(t, filepath.Join(tmp, \"test-run\"))\n\tr.AddRun(l, run)\n\n\ttests := []struct {\n\t\texpectedErr error\n\t\tname        string\n\t\trunName     string\n\t}{\n\t\t{\n\t\t\tname:    \"existing run\",\n\t\t\trunName: filepath.Join(tmp, \"test-run\"),\n\t\t},\n\t\t{\n\t\t\tname:        \"non-existent run\",\n\t\t\trunName:     filepath.Join(tmp, \"non-existent\"),\n\t\t\texpectedErr: report.ErrRunNotFound,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\trun, err := r.GetRun(tt.runName)\n\t\t\tif tt.expectedErr != nil {\n\t\t\t\tassert.ErrorIs(t, err, tt.expectedErr)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tt.runName, run.Path)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEnsureRun(t *testing.T) {\n\tt.Parallel()\n\n\ttmp := helpers.TmpDirWOSymlinks(t)\n\n\tl := logger.CreateLogger()\n\n\ttests := []struct {\n\t\texpectedErrIs error\n\t\tsetupFunc     func(*report.Report) *report.Run\n\t\tname          string\n\t\trunName       string\n\t\texistingRun   bool\n\t\texpectError   bool\n\t}{\n\t\t{\n\t\t\tname:        \"creates new run when run does not exist\",\n\t\t\trunName:     filepath.Join(tmp, \"new-run\"),\n\t\t\texistingRun: false,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"returns existing run when it exists\",\n\t\t\trunName:     filepath.Join(tmp, \"existing-run\"),\n\t\t\texistingRun: true,\n\t\t\texpectError: false,\n\t\t\tsetupFunc: func(r *report.Report) *report.Run {\n\t\t\t\trun := newRun(t, filepath.Join(tmp, \"existing-run\"))\n\t\t\t\terr := r.AddRun(l, run)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\treturn run\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:          \"returns error for invalid path\",\n\t\t\trunName:       \"relative-path\",\n\t\t\texistingRun:   false,\n\t\t\texpectError:   true,\n\t\t\texpectedErrIs: report.ErrPathMustBeAbsolute,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tr := report.NewReport()\n\n\t\t\tvar existingRun *report.Run\n\n\t\t\tif tt.setupFunc != nil {\n\t\t\t\texistingRun = tt.setupFunc(r)\n\t\t\t}\n\n\t\t\trun, err := r.EnsureRun(l, tt.runName)\n\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Nil(t, run)\n\n\t\t\t\tif tt.expectedErrIs != nil {\n\t\t\t\t\trequire.ErrorIs(t, err, tt.expectedErrIs)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\trequire.NotNil(t, run)\n\t\t\t\tassert.Equal(t, tt.runName, run.Path)\n\t\t\t\tassert.False(t, run.Started.IsZero())\n\n\t\t\t\tif tt.existingRun {\n\t\t\t\t\t// Should return the same instance as the existing run\n\t\t\t\t\tassert.Equal(t, existingRun.Started, run.Started)\n\t\t\t\t}\n\n\t\t\t\t// Verify the run was added to the report\n\t\t\t\tretrievedRun, err := r.GetRun(tt.runName)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, run, retrievedRun)\n\n\t\t\t\t// Verify that calling EnsureRun again returns the same run\n\t\t\t\tsecondRun, err := r.EnsureRun(l, tt.runName)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, run, secondRun)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEndRun(t *testing.T) {\n\tt.Parallel()\n\n\ttmp := helpers.TmpDirWOSymlinks(t)\n\n\tl := logger.CreateLogger()\n\n\ttests := []struct {\n\t\twantReason *report.Reason\n\t\twantCause  *report.Cause\n\t\tname       string\n\t\trunName    string\n\t\twantResult report.Result\n\t\toptions    []report.EndOption\n\t\twantErr    bool\n\t}{\n\t\t{\n\t\t\tname:       \"successful end\",\n\t\t\trunName:    filepath.Join(tmp, \"test-run\"),\n\t\t\toptions:    []report.EndOption{},\n\t\t\twantErr:    false,\n\t\t\twantResult: report.ResultSucceeded,\n\t\t},\n\t\t{\n\t\t\tname:    \"non-existent run\",\n\t\t\trunName: filepath.Join(tmp, \"non-existent\"),\n\t\t\toptions: []report.EndOption{},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"with result\",\n\t\t\trunName:    filepath.Join(tmp, \"with-result\"),\n\t\t\toptions:    []report.EndOption{report.WithResult(report.ResultFailed)},\n\t\t\twantErr:    false,\n\t\t\twantResult: report.ResultFailed,\n\t\t},\n\t\t{\n\t\t\tname:       \"with reason\",\n\t\t\trunName:    filepath.Join(tmp, \"with-reason\"),\n\t\t\toptions:    []report.EndOption{report.WithReason(report.ReasonRunError)},\n\t\t\twantErr:    false,\n\t\t\twantResult: report.ResultSucceeded,\n\t\t\twantReason: func() *report.Reason { r := report.ReasonRunError; return &r }(),\n\t\t},\n\t\t{\n\t\t\tname:       \"with cause\",\n\t\t\trunName:    filepath.Join(tmp, \"with-cause\"),\n\t\t\toptions:    []report.EndOption{report.WithCauseRetryBlock(\"test-block\")},\n\t\t\twantErr:    false,\n\t\t\twantResult: report.ResultSucceeded,\n\t\t\twantCause:  func() *report.Cause { c := report.Cause(\"test-block\"); return &c }(),\n\t\t},\n\t}\n\n\tr := report.NewReport()\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tif !tt.wantErr {\n\t\t\t\trun := newRun(t, tt.runName)\n\t\t\t\tr.AddRun(l, run)\n\t\t\t}\n\n\t\t\terr := r.EndRun(l, tt.runName, tt.options...)\n\t\t\tif tt.wantErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\trun, err := r.GetRun(tt.runName)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, tt.wantResult, run.Result)\n\n\t\t\t\tif tt.wantReason != nil {\n\t\t\t\t\tassert.NotNil(t, run.Reason)\n\t\t\t\t\tassert.Equal(t, *tt.wantReason, *run.Reason)\n\t\t\t\t}\n\n\t\t\t\tif tt.wantCause != nil {\n\t\t\t\t\tassert.NotNil(t, run.Cause)\n\t\t\t\t\tassert.Equal(t, *tt.wantCause, *run.Cause)\n\t\t\t\t}\n\n\t\t\t\tassert.False(t, run.Ended.IsZero())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestEndRunAlreadyEnded(t *testing.T) {\n\tt.Parallel()\n\n\ttmp := helpers.TmpDirWOSymlinks(t)\n\n\tl := logger.CreateLogger()\n\n\ttests := []struct {\n\t\tname           string\n\t\tinitialResult  report.Result\n\t\texpectedResult report.Result\n\t\tsecondResult   report.Result\n\t\tinitialOptions []report.EndOption\n\t\tsecondOptions  []report.EndOption\n\t}{\n\t\t{\n\t\t\tname:           \"already ended with early exit is not overwritten\",\n\t\t\tinitialResult:  report.ResultEarlyExit,\n\t\t\tsecondResult:   report.ResultSucceeded,\n\t\t\texpectedResult: report.ResultEarlyExit,\n\t\t},\n\t\t{\n\t\t\tname:           \"already ended with excluded is not overwritten\",\n\t\t\tinitialResult:  report.ResultExcluded,\n\t\t\tsecondResult:   report.ResultSucceeded,\n\t\t\texpectedResult: report.ResultExcluded,\n\t\t},\n\t\t{\n\t\t\tname:           \"already ended with retry succeeded is overwritten\",\n\t\t\tinitialResult:  report.ResultSucceeded,\n\t\t\tinitialOptions: []report.EndOption{report.WithReason(report.ReasonRetrySucceeded)},\n\t\t\tsecondResult:   report.ResultSucceeded,\n\t\t\texpectedResult: report.ResultSucceeded,\n\t\t},\n\t\t{\n\t\t\tname:           \"already ended with retry failed is overwritten\",\n\t\t\tinitialResult:  report.ResultSucceeded,\n\t\t\tinitialOptions: []report.EndOption{report.WithReason(report.ReasonRetrySucceeded)},\n\t\t\tsecondResult:   report.ResultFailed,\n\t\t\texpectedResult: report.ResultFailed,\n\t\t},\n\t\t{\n\t\t\tname:           \"already ended with error ignored is overwritten\",\n\t\t\tinitialResult:  report.ResultSucceeded,\n\t\t\tinitialOptions: []report.EndOption{report.WithReason(report.ReasonErrorIgnored)},\n\t\t\tsecondResult:   report.ResultSucceeded,\n\t\t\texpectedResult: report.ResultSucceeded,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t// Create a new report and run for each test case\n\t\t\tr := report.NewReport()\n\t\t\trunName := filepath.Join(tmp, tt.name)\n\t\t\trun := newRun(t, runName)\n\t\t\tr.AddRun(l, run)\n\n\t\t\t// Set up initial options with the initial result\n\t\t\tinitialOptions := slices.Concat(tt.initialOptions, []report.EndOption{report.WithResult(tt.initialResult)})\n\n\t\t\t// End the run with the initial state\n\t\t\terr := r.EndRun(l, runName, initialOptions...)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Set up second options with the second result\n\t\t\tsecondOptions := slices.Concat(tt.secondOptions, []report.EndOption{report.WithResult(tt.secondResult)})\n\n\t\t\t// Then try to end it again with a different state\n\t\t\terr = r.EndRun(l, runName, secondOptions...)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Verify that the result is the expected one\n\t\t\tendedRun, err := r.GetRun(runName)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tt.expectedResult, endedRun.Result)\n\t\t})\n\t}\n}\n\nfunc TestSummarize(t *testing.T) {\n\tt.Parallel()\n\n\ttmp := helpers.TmpDirWOSymlinks(t)\n\n\tl := logger.CreateLogger()\n\n\ttests := []struct {\n\t\tname    string\n\t\tresults []struct {\n\t\t\tname   string\n\t\t\tresult report.Result\n\t\t}\n\t\twantTotalUnits int\n\t\twantSucceeded  int\n\t\twantFailed     int\n\t\twantEarlyExits int\n\t\twantExcluded   int\n\t}{\n\t\t{\n\t\t\tname: \"empty report\",\n\t\t\tresults: []struct {\n\t\t\t\tname   string\n\t\t\t\tresult report.Result\n\t\t\t}{},\n\t\t\twantTotalUnits: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"single successful run\",\n\t\t\tresults: []struct {\n\t\t\t\tname   string\n\t\t\t\tresult report.Result\n\t\t\t}{\n\t\t\t\t{filepath.Join(tmp, \"single-successful-run\"), report.ResultSucceeded},\n\t\t\t},\n\t\t\twantTotalUnits: 1,\n\t\t\twantSucceeded:  1,\n\t\t},\n\t\t{\n\t\t\tname: \"mixed results\",\n\t\t\tresults: []struct {\n\t\t\t\tname   string\n\t\t\t\tresult report.Result\n\t\t\t}{\n\t\t\t\t{filepath.Join(tmp, \"successful-run\"), report.ResultSucceeded},\n\t\t\t\t{filepath.Join(tmp, \"failed-run\"), report.ResultFailed},\n\t\t\t\t{filepath.Join(tmp, \"early-exit-run\"), report.ResultEarlyExit},\n\t\t\t\t{filepath.Join(tmp, \"excluded-run\"), report.ResultExcluded},\n\t\t\t},\n\t\t\twantTotalUnits: 4,\n\t\t\twantSucceeded:  1,\n\t\t\twantFailed:     1,\n\t\t\twantEarlyExits: 1,\n\t\t\twantExcluded:   1,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tr := report.NewReport()\n\n\t\t\tfor _, result := range tt.results {\n\t\t\t\trun := newRun(t, result.name)\n\t\t\t\tr.AddRun(l, run)\n\t\t\t\tr.EndRun(l, result.name, report.WithResult(result.result))\n\t\t\t}\n\n\t\t\tsummary := r.Summarize()\n\t\t\tassert.Equal(t, tt.wantTotalUnits, summary.TotalUnits())\n\t\t\tassert.Equal(t, tt.wantSucceeded, summary.UnitsSucceeded)\n\t\t\tassert.Equal(t, tt.wantFailed, summary.UnitsFailed)\n\t\t\tassert.Equal(t, tt.wantEarlyExits, summary.EarlyExits)\n\t\t\tassert.Equal(t, tt.wantExcluded, summary.Excluded)\n\t\t})\n\t}\n}\n\nfunc TestWriteCSV(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tsetup    func(l log.Logger, dir string, r *report.Report)\n\t\texpected [][]string\n\t}{\n\t\t{\n\t\t\tname: \"single successful run\",\n\t\t\tsetup: func(l log.Logger, dir string, r *report.Report) {\n\t\t\t\trun := newRun(t, filepath.Join(dir, \"successful-run\"))\n\t\t\t\tr.AddRun(l, run)\n\t\t\t\tr.EndRun(l, run.Path)\n\t\t\t},\n\t\t\texpected: [][]string{\n\t\t\t\t{\"Name\", \"Started\", \"Ended\", \"Result\", \"Reason\", \"Cause\", \"Ref\", \"Cmd\", \"Args\"},\n\t\t\t\t{\"successful-run\", \"\", \"\", \"succeeded\", \"\", \"\", \"\", \"\", \"\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"complex mixed results\",\n\t\t\tsetup: func(l log.Logger, dir string, r *report.Report) {\n\t\t\t\t// Add successful run\n\t\t\t\tsuccessRun := newRun(t, filepath.Join(dir, \"success-run\"))\n\t\t\t\tr.AddRun(l, successRun)\n\t\t\t\tr.EndRun(l, successRun.Path)\n\n\t\t\t\t// Add failed run with reason\n\t\t\t\tfailedRun := newRun(t, filepath.Join(dir, \"failed-run\"))\n\t\t\t\tr.AddRun(l, failedRun)\n\t\t\t\tr.EndRun(l, failedRun.Path, report.WithResult(report.ResultFailed), report.WithReason(report.ReasonRunError))\n\n\t\t\t\t// Add excluded run with cause\n\t\t\t\texcludedRun := newRun(t, filepath.Join(dir, \"excluded-run\"))\n\t\t\t\tr.AddRun(l, excludedRun)\n\t\t\t\tr.EndRun(l, excludedRun.Path, report.WithResult(report.ResultExcluded), report.WithCauseRetryBlock(\"test-block\"))\n\n\t\t\t\t// Add early exit run with both reason and cause\n\t\t\t\tearlyExitRun := newRun(t, filepath.Join(dir, \"early-exit-run\"))\n\t\t\t\tr.AddRun(l, earlyExitRun)\n\t\t\t\tr.EndRun(l, earlyExitRun.Path,\n\t\t\t\t\treport.WithResult(report.ResultEarlyExit),\n\t\t\t\t\treport.WithReason(report.ReasonRunError),\n\t\t\t\t\treport.WithCauseRetryBlock(\"another-block\"),\n\t\t\t\t)\n\t\t\t},\n\t\t\texpected: [][]string{\n\t\t\t\t{\"Name\", \"Started\", \"Ended\", \"Result\", \"Reason\", \"Cause\", \"Ref\", \"Cmd\", \"Args\"},\n\t\t\t\t{\"success-run\", \"\", \"\", \"succeeded\", \"\", \"\", \"\", \"\", \"\"},\n\t\t\t\t{\"failed-run\", \"\", \"\", \"failed\", \"run error\", \"\", \"\", \"\", \"\"},\n\t\t\t\t{\"excluded-run\", \"\", \"\", \"excluded\", \"\", \"test-block\", \"\", \"\", \"\"},\n\t\t\t\t{\"early-exit-run\", \"\", \"\", \"early exit\", \"run error\", \"another-block\", \"\", \"\", \"\"},\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\tt.Parallel()\n\n\t\t\ttmp := helpers.TmpDirWOSymlinks(t)\n\n\t\t\tl := logger.CreateLogger()\n\n\t\t\t// Create a temporary file for the CSV\n\t\t\tcsvFile := filepath.Join(tmp, \"report.csv\")\n\t\t\tfile, err := os.Create(csvFile)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tdefer file.Close()\n\n\t\t\t// Setup and write the report\n\t\t\tr := report.NewReport().WithWorkingDir(tmp)\n\t\t\ttt.setup(l, tmp, r)\n\n\t\t\terr = r.WriteCSV(file)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Close the file before reading\n\t\t\tfile.Close()\n\n\t\t\t// Read the CSV file\n\t\t\tfile, err = os.Open(csvFile)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tdefer file.Close()\n\n\t\t\treader := csv.NewReader(file)\n\t\t\trecords, err := reader.ReadAll()\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Verify the number of records\n\t\t\trequire.Len(t, records, len(tt.expected))\n\n\t\t\t// Verify each record\n\t\t\tfor i, record := range records {\n\t\t\t\texpected := tt.expected[i]\n\t\t\t\trequire.Len(t, record, len(expected), \"Record %d has wrong number of fields\", i)\n\n\t\t\t\t// For the header row, verify exact match\n\t\t\t\tif i == 0 {\n\t\t\t\t\tassert.Equal(t, expected, record)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// For data rows, verify fields individually\n\t\t\t\tassert.Equal(t, expected[0], record[0], \"Name mismatch in record %d\", i)\n\t\t\t\t// Skip timestamp verification for Started and Ended fields\n\t\t\t\tassert.Equal(t, expected[3], record[3], \"Result mismatch in record %d\", i)\n\t\t\t\tassert.Equal(t, expected[4], record[4], \"Reason mismatch in record %d\", i)\n\t\t\t\tassert.Equal(t, expected[5], record[5], \"Cause mismatch in record %d\", i)\n\t\t\t\tassert.Equal(t, expected[6], record[6], \"Ref mismatch in record %d\", i)\n\t\t\t\tassert.Equal(t, expected[7], record[7], \"Cmd mismatch in record %d\", i)\n\t\t\t\tassert.Equal(t, expected[8], record[8], \"Args mismatch in record %d\", i)\n\n\t\t\t\t// Verify that timestamps are in RFC3339 format\n\t\t\t\tif record[1] != \"\" {\n\t\t\t\t\t_, err := time.Parse(time.RFC3339, record[1])\n\t\t\t\t\trequire.NoError(t, err, \"Started timestamp in record %d is not in RFC3339 format\", i)\n\t\t\t\t}\n\n\t\t\t\tif record[2] != \"\" {\n\t\t\t\t\t_, err := time.Parse(time.RFC3339, record[2])\n\t\t\t\t\trequire.NoError(t, err, \"Ended timestamp in record %d is not in RFC3339 format\", i)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestWriteJSON(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\ttests := []struct {\n\t\tname     string\n\t\tsetup    func(l log.Logger, dir string, r *report.Report)\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"single successful run\",\n\t\t\tsetup: func(l log.Logger, dir string, r *report.Report) {\n\t\t\t\trun := newRun(t, filepath.Join(dir, \"successful-run\"))\n\t\t\t\tr.AddRun(l, run)\n\t\t\t\tr.EndRun(l, run.Path)\n\t\t\t},\n\t\t\texpected: `[\n  {\n    \"Name\": \"successful-run\",\n    \"Started\": \"2024-03-21T10:00:00Z\",\n    \"Ended\": \"2024-03-21T10:01:00Z\",\n    \"Result\": \"succeeded\"\n  }\n]`,\n\t\t},\n\t\t{\n\t\t\tname: \"complex mixed results\",\n\t\t\tsetup: func(l log.Logger, dir string, r *report.Report) {\n\t\t\t\t// Add successful run\n\t\t\t\tsuccessRun := newRun(t, filepath.Join(dir, \"success-run\"))\n\t\t\t\tr.AddRun(l, successRun)\n\t\t\t\tr.EndRun(l, successRun.Path)\n\n\t\t\t\t// Add failed run with reason\n\t\t\t\tfailedRun := newRun(t, filepath.Join(dir, \"failed-run\"))\n\t\t\t\tr.AddRun(l, failedRun)\n\t\t\t\tr.EndRun(\n\t\t\t\t\tl,\n\t\t\t\t\tfailedRun.Path,\n\t\t\t\t\treport.WithResult(report.ResultFailed),\n\t\t\t\t\treport.WithReason(report.ReasonRunError),\n\t\t\t\t)\n\n\t\t\t\t// Add excluded run with cause\n\t\t\t\tretriedRun := newRun(t, filepath.Join(dir, \"retried-run\"))\n\t\t\t\tr.AddRun(l, retriedRun)\n\t\t\t\tr.EndRun(\n\t\t\t\t\tl,\n\t\t\t\t\tretriedRun.Path,\n\t\t\t\t\treport.WithResult(report.ResultSucceeded),\n\t\t\t\t\treport.WithReason(report.ReasonRetrySucceeded),\n\t\t\t\t)\n\n\t\t\t\t// Add excluded run with cause\n\t\t\t\texcludedRun := newRun(t, filepath.Join(dir, \"excluded-run\"))\n\t\t\t\tr.AddRun(l, excludedRun)\n\t\t\t\tr.EndRun(\n\t\t\t\t\tl,\n\t\t\t\t\texcludedRun.Path,\n\t\t\t\t\treport.WithResult(report.ResultExcluded),\n\t\t\t\t\treport.WithReason(report.ReasonExcludeBlock),\n\t\t\t\t\treport.WithCauseExcludeBlock(\"test-block\"),\n\t\t\t\t)\n\t\t\t},\n\t\t\texpected: `[\n  {\n    \"Name\": \"success-run\",\n    \"Started\": \"2024-03-21T10:00:00Z\",\n    \"Ended\": \"2024-03-21T10:01:00Z\",\n    \"Result\": \"succeeded\"\n  },\n  {\n    \"Name\": \"failed-run\",\n    \"Started\": \"2024-03-21T10:01:00Z\",\n    \"Ended\": \"2024-03-21T10:02:00Z\",\n    \"Result\": \"failed\",\n    \"Reason\": \"run error\"\n  },\n  {\n    \"Name\": \"retried-run\",\n    \"Started\": \"2024-03-21T10:03:00Z\",\n    \"Ended\": \"2024-03-21T10:04:00Z\",\n    \"Result\": \"succeeded\",\n    \"Reason\": \"retry succeeded\"\n  },\n  {\n    \"Name\": \"excluded-run\",\n    \"Started\": \"2024-03-21T10:02:00Z\",\n    \"Ended\": \"2024-03-21T10:03:00Z\",\n    \"Result\": \"excluded\",\n    \"Reason\": \"exclude block\",\n    \"Cause\": \"test-block\"\n  }\n]`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmp := helpers.TmpDirWOSymlinks(t)\n\n\t\t\t// Create a temporary file for the JSON\n\t\t\tjsonFile := filepath.Join(tmp, \"report.json\")\n\t\t\tfile, err := os.Create(jsonFile)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tdefer file.Close()\n\n\t\t\t// Setup and write the report\n\t\t\tr := report.NewReport().WithWorkingDir(tmp)\n\t\t\ttt.setup(l, tmp, r)\n\n\t\t\terr = r.WriteJSON(file)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Close the file before reading\n\t\t\tfile.Close()\n\n\t\t\t// Read the JSON file\n\t\t\tfile, err = os.Open(jsonFile)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tdefer file.Close()\n\n\t\t\t// Read the actual output\n\t\t\tactualBytes, err := os.ReadFile(jsonFile)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Parse both expected and actual JSON to compare them\n\t\t\tvar expectedJSON, actualJSON []map[string]any\n\n\t\t\terr = json.Unmarshal([]byte(tt.expected), &expectedJSON)\n\t\t\trequire.NoError(t, err)\n\t\t\terr = json.Unmarshal(actualBytes, &actualJSON)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Verify the number of records\n\t\t\trequire.Len(t, actualJSON, len(expectedJSON))\n\n\t\t\t// Verify each record\n\t\t\tfor i, actualRecord := range actualJSON {\n\t\t\t\texpectedRecord := expectedJSON[i]\n\n\t\t\t\t// Verify name\n\t\t\t\tassert.Equal(t, expectedRecord[\"Name\"], actualRecord[\"Name\"], \"Name mismatch in record %d\", i)\n\n\t\t\t\t// Verify result\n\t\t\t\tassert.Equal(t, expectedRecord[\"Result\"], actualRecord[\"Result\"], \"Result mismatch in record %d\", i)\n\n\t\t\t\t// Verify reason if present\n\t\t\t\tif expectedReason, ok := expectedRecord[\"Reason\"]; ok {\n\t\t\t\t\tassert.Equal(t, expectedReason, actualRecord[\"Reason\"], \"Reason mismatch in record %d\", i)\n\t\t\t\t} else {\n\t\t\t\t\tassert.NotContains(t, actualRecord, \"Reason\", \"Unexpected reason in record %d\", i)\n\t\t\t\t}\n\n\t\t\t\t// Verify cause if present\n\t\t\t\tif expectedCause, ok := expectedRecord[\"Cause\"]; ok {\n\t\t\t\t\tassert.Equal(t, expectedCause, actualRecord[\"Cause\"], \"Cause mismatch in record %d\", i)\n\t\t\t\t} else {\n\t\t\t\t\tassert.NotContains(t, actualRecord, \"Cause\", \"Unexpected cause in record %d\", i)\n\t\t\t\t}\n\n\t\t\t\t// Verify timestamps are in RFC3339 format\n\t\t\t\tif started, ok := actualRecord[\"Started\"].(string); ok {\n\t\t\t\t\t_, err := time.Parse(time.RFC3339, started)\n\t\t\t\t\trequire.NoError(t, err, \"Started timestamp in record %d is not in RFC3339 format\", i)\n\t\t\t\t}\n\n\t\t\t\tif ended, ok := actualRecord[\"Ended\"].(string); ok {\n\t\t\t\t\t_, err := time.Parse(time.RFC3339, ended)\n\t\t\t\t\trequire.NoError(t, err, \"Ended timestamp in record %d is not in RFC3339 format\", i)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nconst ExpectedSchema = `{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$id\": \"https://docs.terragrunt.com/schemas/run/report/v4/schema.json\",\n  \"items\": {\n    \"properties\": {\n      \"Started\": {\n        \"type\": \"string\",\n        \"format\": \"date-time\"\n      },\n      \"Ended\": {\n        \"type\": \"string\",\n        \"format\": \"date-time\"\n      },\n      \"Reason\": {\n        \"type\": \"string\",\n        \"enum\": [\n          \"retry succeeded\",\n          \"error ignored\",\n          \"run error\",\n          \"exclude block\",\n          \"ancestor error\"\n        ]\n      },\n      \"Cause\": {\n        \"type\": \"string\"\n      },\n      \"Name\": {\n        \"type\": \"string\"\n      },\n      \"Result\": {\n        \"type\": \"string\",\n        \"enum\": [\n          \"succeeded\",\n          \"failed\",\n          \"early exit\",\n          \"excluded\"\n        ]\n      },\n      \"Ref\": {\n        \"type\": \"string\"\n      },\n      \"Cmd\": {\n        \"type\": \"string\"\n      },\n      \"Args\": {\n        \"items\": {\n          \"type\": \"string\"\n        },\n        \"type\": \"array\"\n      }\n    },\n    \"additionalProperties\": false,\n    \"type\": \"object\",\n    \"required\": [\n      \"Started\",\n      \"Ended\",\n      \"Name\",\n      \"Result\"\n    ],\n    \"title\": \"Terragrunt Run Report Schema\",\n    \"description\": \"Schema for Terragrunt run report\"\n  },\n  \"type\": \"array\",\n  \"title\": \"Terragrunt Run Report Schema\",\n  \"description\": \"Array of Terragrunt runs\"\n}\n`\n\nfunc TestWriteSchema(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a buffer to write the schema to\n\tvar buf bytes.Buffer\n\n\t// Write the schema\n\terr := report.WriteSchema(&buf)\n\trequire.NoError(t, err)\n\n\t// Assert the contents of the schema\n\tassert.JSONEq(t, ExpectedSchema, buf.String())\n\n\t// Parse the schema\n\tvar schema map[string]any\n\n\terr = json.Unmarshal(buf.Bytes(), &schema)\n\trequire.NoError(t, err)\n\n\t// Verify the schema structure\n\tassert.Equal(t, \"array\", schema[\"type\"])\n\tassert.Equal(t, \"Array of Terragrunt runs\", schema[\"description\"])\n\tassert.Equal(t, \"Terragrunt Run Report Schema\", schema[\"title\"])\n\n\t// Verify the items schema\n\titems, ok := schema[\"items\"].(map[string]any)\n\trequire.True(t, ok)\n\n\t// Verify the properties\n\tproperties, ok := items[\"properties\"].(map[string]any)\n\trequire.True(t, ok)\n\n\t// Verify required fields\n\trequired, ok := items[\"required\"].([]any)\n\trequire.True(t, ok)\n\tassert.Contains(t, required, \"Name\")\n\tassert.Contains(t, required, \"Started\")\n\tassert.Contains(t, required, \"Ended\")\n\tassert.Contains(t, required, \"Result\")\n\n\t// Verify field types\n\tassert.Equal(t, \"string\", properties[\"Name\"].(map[string]any)[\"type\"])\n\tassert.Equal(t, \"string\", properties[\"Result\"].(map[string]any)[\"type\"])\n\tassert.Equal(t, \"string\", properties[\"Started\"].(map[string]any)[\"type\"])\n\tassert.Equal(t, \"string\", properties[\"Ended\"].(map[string]any)[\"type\"])\n\n\t// Verify optional fields\n\treason, ok := properties[\"Reason\"].(map[string]any)\n\trequire.True(t, ok)\n\tassert.Equal(t, \"string\", reason[\"type\"])\n\n\tcause, ok := properties[\"Cause\"].(map[string]any)\n\trequire.True(t, ok)\n\tassert.Equal(t, \"string\", cause[\"type\"])\n}\n\nfunc TestExpectedSchemaIsInDocs(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname string\n\t\tfile string\n\t}{\n\t\t{\n\t\t\tname: \"starlight\",\n\t\t\tfile: filepath.Join(\n\t\t\t\t\"..\",\n\t\t\t\t\"..\",\n\t\t\t\t\"docs\",\n\t\t\t\t\"public\",\n\t\t\t\t\"schemas\",\n\t\t\t\t\"run\",\n\t\t\t\t\"report\",\n\t\t\t\t\"v4\",\n\t\t\t\t\"schema.json\",\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\tt.Parallel()\n\n\t\t\tschema, err := os.ReadFile(tt.file)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.JSONEq(t, ExpectedSchema, string(schema))\n\t\t})\n\t}\n}\n\nfunc TestWriteSummary(t *testing.T) {\n\tt.Parallel()\n\n\ttmp := helpers.TmpDirWOSymlinks(t)\n\n\tl := logger.CreateLogger()\n\n\ttests := []struct {\n\t\tname     string\n\t\tsetup    func(l log.Logger, r *report.Report)\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"single successful run\",\n\t\t\tsetup: func(l log.Logger, r *report.Report) {\n\t\t\t\trun := newRun(t, filepath.Join(tmp, \"successful-run\"))\n\t\t\t\tr.AddRun(l, run)\n\t\t\t\tr.EndRun(l, run.Path)\n\t\t\t},\n\t\t\texpected: `\n❯❯ Run Summary  1 units  x\n   ────────────────────────────\n   Succeeded    1\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"complex mixed results\",\n\t\t\tsetup: func(l log.Logger, r *report.Report) {\n\t\t\t\t// Add successful runs\n\t\t\t\tfirstSuccessfulRun := newRun(t, filepath.Join(tmp, \"first-successful-run\"))\n\t\t\t\tr.AddRun(l, firstSuccessfulRun)\n\t\t\t\tr.EndRun(l, firstSuccessfulRun.Path)\n\n\t\t\t\tsecondSuccessfulRun := newRun(t, filepath.Join(tmp, \"second-successful-run\"))\n\t\t\t\tr.AddRun(l, secondSuccessfulRun)\n\t\t\t\tr.EndRun(l, secondSuccessfulRun.Path)\n\n\t\t\t\t// Add failed runs\n\t\t\t\tfirstFailedRun := newRun(t, filepath.Join(tmp, \"first-failed-run\"))\n\t\t\t\tr.AddRun(l, firstFailedRun)\n\t\t\t\tr.EndRun(l, firstFailedRun.Path, report.WithResult(report.ResultFailed))\n\n\t\t\t\tsecondFailedRun := newRun(t, filepath.Join(tmp, \"second-failed-run\"))\n\t\t\t\tr.AddRun(l, secondFailedRun)\n\t\t\t\tr.EndRun(l, secondFailedRun.Path, report.WithResult(report.ResultFailed))\n\n\t\t\t\t// Add excluded runs\n\t\t\t\tfirstExcludedRun := newRun(t, filepath.Join(tmp, \"first-excluded-run\"))\n\t\t\t\tr.AddRun(l, firstExcludedRun)\n\t\t\t\tr.EndRun(l, firstExcludedRun.Path, report.WithResult(report.ResultExcluded))\n\n\t\t\t\tsecondExcludedRun := newRun(t, filepath.Join(tmp, \"second-excluded-run\"))\n\t\t\t\tr.AddRun(l, secondExcludedRun)\n\t\t\t\tr.EndRun(l, secondExcludedRun.Path, report.WithResult(report.ResultExcluded))\n\n\t\t\t\t// Add early exit runs\n\t\t\t\tfirstEarlyExitRun := newRun(t, filepath.Join(tmp, \"first-early-exit-run\"))\n\t\t\t\tr.AddRun(l, firstEarlyExitRun)\n\t\t\t\tr.EndRun(l, firstEarlyExitRun.Path, report.WithResult(report.ResultEarlyExit))\n\n\t\t\t\tsecondEarlyExitRun := newRun(t, filepath.Join(tmp, \"second-early-exit-run\"))\n\t\t\t\tr.AddRun(l, secondEarlyExitRun)\n\t\t\t\tr.EndRun(l, secondEarlyExitRun.Path, report.WithResult(report.ResultEarlyExit))\n\t\t\t},\n\t\t\texpected: `\n❯❯ Run Summary  8 units  x\n   ────────────────────────────\n   Succeeded    2\n   Failed       2\n   Early Exits  2\n   Excluded     2\n`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tr := report.NewReport().WithDisableColor()\n\t\t\ttt.setup(l, r)\n\n\t\t\tvar buf bytes.Buffer\n\n\t\t\terr := r.WriteSummary(&buf)\n\t\t\trequire.NoError(t, err)\n\n\t\t\toutput := buf.String()\n\n\t\t\t// Replace the duration in the header with x\n\t\t\t// Pattern matches: \"❯❯ Run Summary  8 units  42µs\" -> \"❯❯ Run Summary  8 units  x\"\n\t\t\tre := regexp.MustCompile(`(❯❯ Run Summary\\s+\\d+\\s+units\\s+)[^\\n]+`)\n\t\t\toutput = re.ReplaceAllString(output, \"${1}x\")\n\n\t\t\texpected := strings.TrimSpace(tt.expected)\n\t\t\tassert.Equal(t, expected, strings.TrimSpace(output))\n\t\t})\n\t}\n}\n\nfunc TestSchemaIsValid(t *testing.T) {\n\tt.Parallel()\n\n\ttmp := helpers.TmpDirWOSymlinks(t)\n\n\tl := logger.CreateLogger()\n\n\t// Create a new report with working directory\n\tr := report.NewReport().WithWorkingDir(tmp)\n\n\t// Add a simple run that succeeds\n\tsimpleRun := newRun(t, filepath.Join(tmp, \"simple-run\"))\n\tr.AddRun(l, simpleRun)\n\tr.EndRun(l, simpleRun.Path,\n\t\treport.WithResult(report.ResultSucceeded),\n\t)\n\n\t// Add a complex run that tests all possible fields and states\n\tcomplexRun := newRun(t, filepath.Join(tmp, \"complex-run\"))\n\tr.AddRun(l, complexRun)\n\tr.EndRun(l, complexRun.Path,\n\t\treport.WithResult(report.ResultFailed),\n\t\treport.WithReason(report.ReasonRunError),\n\t\treport.WithCauseAncestorExit(\"some-error\"),\n\t)\n\n\t// Create an excluded run with exclude block\n\texcludedRun := newRun(t, filepath.Join(tmp, \"excluded-run\"))\n\tr.AddRun(l, excludedRun)\n\tr.EndRun(l, excludedRun.Path,\n\t\treport.WithResult(report.ResultExcluded),\n\t\treport.WithReason(report.ReasonExcludeBlock),\n\t\treport.WithCauseExcludeBlock(\"test-block\"),\n\t)\n\n\t// Create a retry run that succeeded\n\tretryRun := newRun(t, filepath.Join(tmp, \"retry-run\"))\n\tr.AddRun(l, retryRun)\n\tr.EndRun(l, retryRun.Path,\n\t\treport.WithResult(report.ResultSucceeded),\n\t\treport.WithReason(report.ReasonRetrySucceeded),\n\t\treport.WithCauseRetryBlock(\"retry-block\"),\n\t)\n\n\t// Create an early exit run\n\tearlyExitRun := newRun(t, filepath.Join(tmp, \"early-exit-run\"))\n\tr.AddRun(l, earlyExitRun)\n\tr.EndRun(l, earlyExitRun.Path,\n\t\treport.WithResult(report.ResultEarlyExit),\n\t\treport.WithReason(report.ReasonAncestorError),\n\t\treport.WithCauseAncestorExit(\"parent-unit\"),\n\t)\n\n\t// Create a run with ignored error\n\tignoredRun := newRun(t, filepath.Join(tmp, \"ignored-run\"))\n\tr.AddRun(l, ignoredRun)\n\tr.EndRun(l, ignoredRun.Path,\n\t\treport.WithResult(report.ResultSucceeded),\n\t\treport.WithReason(report.ReasonErrorIgnored),\n\t\treport.WithCauseIgnoreBlock(\"ignore-block\"),\n\t)\n\n\t// Write the report to a JSON file\n\treportFile := filepath.Join(tmp, \"report.json\")\n\tfile, err := os.Create(reportFile)\n\trequire.NoError(t, err)\n\n\tdefer file.Close()\n\n\terr = r.WriteJSON(file)\n\trequire.NoError(t, err)\n\tfile.Close()\n\n\t// Write the schema to a file\n\tschemaFile := filepath.Join(tmp, \"schema.json\")\n\tfile, err = os.Create(schemaFile)\n\trequire.NoError(t, err)\n\n\tdefer file.Close()\n\n\terr = report.WriteSchema(file)\n\trequire.NoError(t, err)\n\tfile.Close()\n\n\t// Read the schema and report files\n\tschemaBytes, err := os.ReadFile(schemaFile)\n\trequire.NoError(t, err)\n\n\treportBytes, err := os.ReadFile(reportFile)\n\trequire.NoError(t, err)\n\n\t// Create a new schema loader\n\tschemaLoader := gojsonschema.NewBytesLoader(schemaBytes)\n\tdocumentLoader := gojsonschema.NewBytesLoader(reportBytes)\n\n\t// Validate the report against the schema\n\tresult, err := gojsonschema.Validate(schemaLoader, documentLoader)\n\trequire.NoError(t, err)\n\n\t// Check if the validation was successful\n\tassert.True(t, result.Valid(), \"JSON report does not match schema: %v\", result.Errors())\n\n\t// Additional validation of the report content\n\tvar reportData []report.JSONRun\n\n\terr = json.Unmarshal(reportBytes, &reportData)\n\trequire.NoError(t, err)\n\n\t// Verify we have all the expected runs\n\trequire.Len(t, reportData, 6)\n\n\t// Helper function to find a run by name\n\tfindRun := func(name string) *report.JSONRun {\n\t\tfor _, run := range reportData {\n\t\t\tif run.Name == name {\n\t\t\t\treturn &run\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\t// Verify simple run\n\tsimple := findRun(\"simple-run\")\n\trequire.NotNil(t, simple)\n\tassert.Equal(t, \"succeeded\", simple.Result)\n\tassert.Nil(t, simple.Reason)\n\tassert.Nil(t, simple.Cause)\n\tassert.False(t, simple.Started.IsZero())\n\tassert.False(t, simple.Ended.IsZero())\n\n\t// Verify complex run\n\tcomplex := findRun(\"complex-run\")\n\trequire.NotNil(t, complex)\n\tassert.Equal(t, \"failed\", complex.Result)\n\tassert.Equal(t, \"run error\", *complex.Reason)\n\tassert.Equal(t, \"some-error\", *complex.Cause)\n\tassert.False(t, complex.Started.IsZero())\n\tassert.False(t, complex.Ended.IsZero())\n\n\t// Verify excluded run\n\texcluded := findRun(\"excluded-run\")\n\trequire.NotNil(t, excluded)\n\tassert.Equal(t, \"excluded\", excluded.Result)\n\tassert.Equal(t, \"exclude block\", *excluded.Reason)\n\tassert.Equal(t, \"test-block\", *excluded.Cause)\n\n\t// Verify retry run\n\tretry := findRun(\"retry-run\")\n\trequire.NotNil(t, retry)\n\tassert.Equal(t, \"succeeded\", retry.Result)\n\tassert.Equal(t, \"retry succeeded\", *retry.Reason)\n\tassert.Equal(t, \"retry-block\", *retry.Cause)\n\n\t// Verify early exit run\n\tearlyExit := findRun(\"early-exit-run\")\n\trequire.NotNil(t, earlyExit)\n\tassert.Equal(t, \"early exit\", earlyExit.Result)\n\tassert.Equal(t, \"ancestor error\", *earlyExit.Reason)\n\tassert.Equal(t, \"parent-unit\", *earlyExit.Cause)\n\n\t// Verify ignored run\n\tignored := findRun(\"ignored-run\")\n\trequire.NotNil(t, ignored)\n\tassert.Equal(t, \"succeeded\", ignored.Result)\n\tassert.Equal(t, \"error ignored\", *ignored.Reason)\n\tassert.Equal(t, \"ignore-block\", *ignored.Cause)\n}\n\nfunc TestWriteUnitLevelSummary(t *testing.T) {\n\tt.Parallel()\n\n\ttmp := helpers.TmpDirWOSymlinks(t)\n\n\tl := logger.CreateLogger()\n\n\ttests := []struct {\n\t\tname     string\n\t\tsetup    func(l log.Logger, r *report.Report)\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname: \"empty runs\",\n\t\t\tsetup: func(l log.Logger, r *report.Report) {\n\t\t\t\t// No runs added\n\t\t\t},\n\t\t\texpected: ``,\n\t\t},\n\t\t{\n\t\t\tname: \"single run\",\n\t\t\tsetup: func(l log.Logger, r *report.Report) {\n\t\t\t\trun := newRun(t, filepath.Join(tmp, \"single-run\"))\n\t\t\t\tr.AddRun(l, run)\n\t\t\t\tr.EndRun(l, run.Path)\n\t\t\t},\n\t\t\texpected: `\n❯❯ Run Summary  1 units  x\n   ────────────────────────────\n   Succeeded (1)\n      single-run ....... x\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple runs sorted by duration\",\n\t\t\tsetup: func(l log.Logger, r *report.Report) {\n\t\t\t\t// Use syntest.Test so that we can artificially manipulate the clock for duration testing.\n\t\t\t\tsynctest.Test(t, func(t *testing.T) {\n\t\t\t\t\tt.Helper()\n\n\t\t\t\t\tlongRun := newRun(t, filepath.Join(tmp, \"long-run\"))\n\t\t\t\t\tr.AddRun(l, longRun)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tmediumRun := newRun(t, filepath.Join(tmp, \"medium-run\"))\n\t\t\t\t\tr.AddRun(l, mediumRun)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tshortRun := newRun(t, filepath.Join(tmp, \"short-run\"))\n\t\t\t\t\tr.AddRun(l, shortRun)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tr.EndRun(l, shortRun.Path)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tr.EndRun(l, mediumRun.Path)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tr.EndRun(l, longRun.Path)\n\t\t\t\t})\n\t\t\t},\n\t\t\texpected: `\n❯❯ Run Summary  3 units  x\n   ────────────────────────────\n   Succeeded (3)\n      long-run ......... x\n      medium-run ....... x\n      short-run ........ x\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"mixed results grouped by category\",\n\t\t\tsetup: func(l log.Logger, r *report.Report) {\n\t\t\t\t// Use syntest.Test so that we can artificially manipulate the clock for duration testing.\n\t\t\t\tsynctest.Test(t, func(t *testing.T) {\n\t\t\t\t\tt.Helper()\n\n\t\t\t\t\tsuccessRun1 := newRun(t, filepath.Join(tmp, \"success-1\"))\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tsuccessRun2 := newRun(t, filepath.Join(tmp, \"success-2\"))\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tfailRun := newRun(t, filepath.Join(tmp, \"fail-run\"))\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\texcludedRun := newRun(t, filepath.Join(tmp, \"excluded-run\"))\n\n\t\t\t\t\tr.AddRun(l, successRun1)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tr.AddRun(l, successRun2)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tr.AddRun(l, failRun)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tr.AddRun(l, excludedRun)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\t\t\t\t\tr.EndRun(l, successRun1.Path)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\t\t\t\t\tr.EndRun(l, successRun2.Path)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\t\t\t\t\tr.EndRun(l, failRun.Path, report.WithResult(report.ResultFailed))\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\t\t\t\t\tr.EndRun(l, excludedRun.Path, report.WithResult(report.ResultExcluded))\n\t\t\t\t})\n\t\t\t},\n\t\t\texpected: `\n❯❯ Run Summary  4 units  x\n   ────────────────────────────\n   Succeeded (2)\n      success-1 ........ x\n      success-2 ........ x\n   Failed (1)\n      fail-run ......... x\n   Excluded (1)\n      excluded-run ..... x\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"very short unit names\",\n\t\t\tsetup: func(l log.Logger, r *report.Report) {\n\t\t\t\t// Use syntest.Test so that we can artificially manipulate the clock for duration testing.\n\t\t\t\tsynctest.Test(t, func(t *testing.T) {\n\t\t\t\t\tt.Helper()\n\n\t\t\t\t\ta := newRun(t, filepath.Join(tmp, \"a\"))\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tb := newRun(t, filepath.Join(tmp, \"b\"))\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tc := newRun(t, filepath.Join(tmp, \"c\"))\n\n\t\t\t\t\tr.AddRun(l, a)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tr.AddRun(l, b)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tr.AddRun(l, c)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tr.EndRun(l, a.Path)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tr.EndRun(l, b.Path)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tr.EndRun(l, c.Path)\n\t\t\t\t})\n\t\t\t},\n\t\t\texpected: `\n❯❯ Run Summary  3 units  x\n   ────────────────────────────\n   Succeeded (3)\n      a ................ x\n      b ................ x\n      c ................ x\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"very long unit names\",\n\t\t\tsetup: func(l log.Logger, r *report.Report) {\n\t\t\t\t// Use syntest.Test so that we can artificially manipulate the clock for duration testing.\n\t\t\t\tsynctest.Test(t, func(t *testing.T) {\n\t\t\t\t\tt.Helper()\n\n\t\t\t\t\tlongName1 := newRun(t, filepath.Join(tmp, \"this-is-a-very-long-name-1\"))\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tlongName2 := newRun(t, filepath.Join(tmp, \"this-is-a-very-long-name-2\"))\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tlongName3 := newRun(t, filepath.Join(tmp, \"this-is-a-very-long-name-3\"))\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tr.AddRun(l, longName1)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tr.AddRun(l, longName2)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tr.AddRun(l, longName3)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tr.EndRun(l, longName1.Path)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tr.EndRun(l, longName2.Path)\n\n\t\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\t\t\tr.EndRun(l, longName3.Path)\n\t\t\t\t})\n\t\t\t},\n\t\t\texpected: `\n❯❯ Run Summary  3 units           x\n   ───────────────────────────────────\n   Succeeded (3)\n      this-is-a-very-long-name-1  x\n      this-is-a-very-long-name-2  x\n      this-is-a-very-long-name-3  x\n`,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tr := report.NewReport().\n\t\t\t\tWithDisableColor().\n\t\t\t\tWithShowUnitLevelSummary().\n\t\t\t\tWithWorkingDir(tmp)\n\n\t\t\ttt.setup(l, r)\n\n\t\t\tvar buf bytes.Buffer\n\n\t\t\terr := r.WriteSummary(&buf)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Replace the duration with x since we can't control the actual duration in tests\n\t\t\toutput := buf.String()\n\n\t\t\t// Replace the header duration with x\n\t\t\tre := regexp.MustCompile(`❯❯ Run Summary  (\\d+) units(\\s+)(\\d+.+)`)\n\t\t\toutput = re.ReplaceAllString(output, \"❯❯ Run Summary  ${1} units${2}x\")\n\n\t\t\t// Replace all the unit level summaries\n\t\t\tre = regexp.MustCompile(`([ ]{6})([^ ]+)( )([^ ]*)( )(\\d+.+)`)\n\t\t\toutput = re.ReplaceAllString(output, \"${1}${2}${3}${4}${5}x\")\n\n\t\t\texpected := strings.TrimSpace(tt.expected)\n\t\t\tassert.Equal(t, expected, strings.TrimSpace(output))\n\t\t})\n\t}\n}\n\n// TestWriteJSONWithDiscoveryWorkingDir verifies that when a run has a DiscoveryWorkingDir set,\n// the report writer uses it instead of the report's workingDir for path computation.\n// This is critical for worktree scenarios where units are discovered in temporary worktree directories.\nfunc TestWriteJSONWithDiscoveryWorkingDir(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\t// Simulate a worktree scenario:\n\t// - Original working dir: /original/repo\n\t// - Worktree path: /tmp/terragrunt-worktree-xxx/original/repo\n\t// - Unit path in worktree: /tmp/terragrunt-worktree-xxx/original/repo/module/unit\n\n\t// Create temp directories\n\toriginalRepoDir := helpers.TmpDirWOSymlinks(t)\n\tworktreeDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create the \"unit\" path in the worktree\n\tunitPath := filepath.Join(worktreeDir, \"module\", \"unit\")\n\terr := os.MkdirAll(unitPath, 0755)\n\trequire.NoError(t, err)\n\n\t// Create a report with the original repo as working dir (simulating non-worktree scenario)\n\tr := report.NewReport().WithWorkingDir(originalRepoDir)\n\n\t// Add a run that was discovered in the worktree\n\t// Set the DiscoveryWorkingDir to the worktree path\n\trun := newRun(t, unitPath)\n\tr.AddRun(l, run)\n\n\t// Ensure the run and set the DiscoveryWorkingDir\n\tr.EnsureRun(l, unitPath, report.WithDiscoveryWorkingDir(worktreeDir))\n\n\t// End the run\n\tr.EndRun(l, unitPath, report.WithResult(report.ResultSucceeded))\n\n\t// Write JSON and verify the name is relative to DiscoveryWorkingDir, not report.workingDir\n\tvar buf bytes.Buffer\n\n\terr = r.WriteJSON(&buf)\n\trequire.NoError(t, err)\n\n\t// Parse the JSON\n\tvar runs []report.JSONRun\n\n\terr = json.Unmarshal(buf.Bytes(), &runs)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, runs, 1)\n\n\t// The name should be relative to the worktree dir, not the original repo dir\n\t// Without the fix, this would be the full absolute path since unitPath doesn't start with originalRepoDir\n\tassert.Equal(t, \"module/unit\", runs[0].Name, \"Run name should be relative to DiscoveryWorkingDir, not report.workingDir\")\n}\n\n// TestWriteCSVWithDiscoveryWorkingDir verifies that CSV output also uses DiscoveryWorkingDir.\nfunc TestWriteCSVWithDiscoveryWorkingDir(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\t// Create temp directories (simulating worktree scenario)\n\toriginalRepoDir := helpers.TmpDirWOSymlinks(t)\n\tworktreeDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create the \"unit\" path in the worktree\n\tunitPath := filepath.Join(worktreeDir, \"module\", \"unit\")\n\terr := os.MkdirAll(unitPath, 0755)\n\trequire.NoError(t, err)\n\n\t// Create a report with the original repo as working dir\n\tr := report.NewReport().WithWorkingDir(originalRepoDir)\n\n\t// Add a run with DiscoveryWorkingDir set to worktree path\n\trun := newRun(t, unitPath)\n\tr.AddRun(l, run)\n\tr.EnsureRun(l, unitPath, report.WithDiscoveryWorkingDir(worktreeDir))\n\tr.EndRun(l, unitPath, report.WithResult(report.ResultSucceeded))\n\n\t// Write CSV\n\tvar buf bytes.Buffer\n\n\terr = r.WriteCSV(&buf)\n\trequire.NoError(t, err)\n\n\t// Parse the CSV\n\treader := csv.NewReader(&buf)\n\trecords, err := reader.ReadAll()\n\trequire.NoError(t, err)\n\n\trequire.Len(t, records, 2) // header + 1 data row\n\n\t// The name (first column) should be relative to worktree dir\n\tassert.Equal(t, \"module/unit\", records[1][0], \"Run name should be relative to DiscoveryWorkingDir, not report.workingDir\")\n}\n\n// TestParseJSONRuns verifies that JSON report data can be parsed from bytes.\nfunc TestParseJSONRuns(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname        string\n\t\tinput       string\n\t\texpected    report.JSONRuns\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname:     \"empty array\",\n\t\t\tinput:    \"[]\",\n\t\t\texpected: report.JSONRuns{},\n\t\t},\n\t\t{\n\t\t\tname: \"single run\",\n\t\t\tinput: `[{\n\t\t\t\t\"Name\": \"module/unit\",\n\t\t\t\t\"Started\": \"2024-01-01T10:00:00Z\",\n\t\t\t\t\"Ended\": \"2024-01-01T10:01:00Z\",\n\t\t\t\t\"Result\": \"succeeded\"\n\t\t\t}]`,\n\t\t\texpected: report.JSONRuns{\n\t\t\t\t{\n\t\t\t\t\tName:   \"module/unit\",\n\t\t\t\t\tResult: \"succeeded\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple runs with all fields\",\n\t\t\tinput: `[\n\t\t\t\t{\n\t\t\t\t\t\"Name\": \"unit-a\",\n\t\t\t\t\t\"Started\": \"2024-01-01T10:00:00Z\",\n\t\t\t\t\t\"Ended\": \"2024-01-01T10:01:00Z\",\n\t\t\t\t\t\"Result\": \"succeeded\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"Name\": \"unit-b\",\n\t\t\t\t\t\"Started\": \"2024-01-01T10:01:00Z\",\n\t\t\t\t\t\"Ended\": \"2024-01-01T10:02:00Z\",\n\t\t\t\t\t\"Result\": \"failed\",\n\t\t\t\t\t\"Reason\": \"run error\",\n\t\t\t\t\t\"Cause\": \"some error\"\n\t\t\t\t}\n\t\t\t]`,\n\t\t\texpected: report.JSONRuns{\n\t\t\t\t{Name: \"unit-a\", Result: \"succeeded\"},\n\t\t\t\t{Name: \"unit-b\", Result: \"failed\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid json\",\n\t\t\tinput:       \"not valid json\",\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\truns, err := report.ParseJSONRuns([]byte(tt.input))\n\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, runs, len(tt.expected))\n\n\t\t\tfor i, expected := range tt.expected {\n\t\t\t\tassert.Equal(t, expected.Name, runs[i].Name)\n\t\t\t\tassert.Equal(t, expected.Result, runs[i].Result)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestParseJSONRunsFromFile verifies that JSON report data can be parsed from a file.\nfunc TestParseJSONRunsFromFile(t *testing.T) {\n\tt.Parallel()\n\n\ttmp := helpers.TmpDirWOSymlinks(t)\n\n\tt.Run(\"valid file\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\treportFile := filepath.Join(tmp, \"valid-report.json\")\n\t\tcontent := `[{\"Name\": \"test-unit\", \"Started\": \"2024-01-01T10:00:00Z\", \"Ended\": \"2024-01-01T10:01:00Z\", \"Result\": \"succeeded\"}]`\n\n\t\terr := os.WriteFile(reportFile, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\n\t\truns, err := report.ParseJSONRunsFromFile(reportFile)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, runs, 1)\n\t\tassert.Equal(t, \"test-unit\", runs[0].Name)\n\t})\n\n\tt.Run(\"non-existent file\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t_, err := report.ParseJSONRunsFromFile(filepath.Join(tmp, \"does-not-exist.json\"))\n\t\trequire.Error(t, err)\n\t})\n}\n\n// TestJSONRunsFindByName verifies that runs can be found by name.\nfunc TestJSONRunsFindByName(t *testing.T) {\n\tt.Parallel()\n\n\truns := report.JSONRuns{\n\t\t{Name: \"unit-a\", Result: \"succeeded\"},\n\t\t{Name: \"module/unit-b\", Result: \"failed\"},\n\t\t{Name: \"nested/path/unit-c\", Result: \"excluded\"},\n\t}\n\n\ttests := []struct {\n\t\texpected *report.JSONRun\n\t\tname     string\n\t\tsearch   string\n\t}{\n\t\t{\n\t\t\tname:     \"find first unit\",\n\t\t\tsearch:   \"unit-a\",\n\t\t\texpected: &report.JSONRun{Name: \"unit-a\", Result: \"succeeded\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"find nested path\",\n\t\t\tsearch:   \"module/unit-b\",\n\t\t\texpected: &report.JSONRun{Name: \"module/unit-b\", Result: \"failed\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"find deeply nested\",\n\t\t\tsearch:   \"nested/path/unit-c\",\n\t\t\texpected: &report.JSONRun{Name: \"nested/path/unit-c\", Result: \"excluded\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"not found\",\n\t\t\tsearch:   \"does-not-exist\",\n\t\t\texpected: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresult := runs.FindByName(tt.search)\n\n\t\t\tif tt.expected == nil {\n\t\t\t\tassert.Nil(t, result)\n\t\t\t} else {\n\t\t\t\trequire.NotNil(t, result)\n\t\t\t\tassert.Equal(t, tt.expected.Name, result.Name)\n\t\t\t\tassert.Equal(t, tt.expected.Result, result.Result)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestJSONRunsNames verifies that run names can be extracted from a slice of runs.\nfunc TestJSONRunsNames(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\truns     report.JSONRuns\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"empty slice\",\n\t\t\truns:     report.JSONRuns{},\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple runs\",\n\t\t\truns: report.JSONRuns{\n\t\t\t\t{Name: \"unit-a\"},\n\t\t\t\t{Name: \"module/unit-b\"},\n\t\t\t\t{Name: \"nested/path/unit-c\"},\n\t\t\t},\n\t\t\texpected: []string{\"unit-a\", \"module/unit-b\", \"nested/path/unit-c\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tnames := tt.runs.Names()\n\t\t\tassert.Equal(t, tt.expected, names)\n\t\t})\n\t}\n}\n\n// TestParseCSVRuns verifies that CSV report data can be parsed from bytes.\nfunc TestParseCSVRuns(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname        string\n\t\tinput       string\n\t\texpected    report.CSVRuns\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname:     \"header only\",\n\t\t\tinput:    \"Name,Started,Ended,Result,Reason,Cause,Ref,Cmd,Args\\n\",\n\t\t\texpected: report.CSVRuns{},\n\t\t},\n\t\t{\n\t\t\tname:     \"single run\",\n\t\t\tinput:    \"Name,Started,Ended,Result,Reason,Cause,Ref,Cmd,Args\\nmodule/unit,2024-01-01T10:00:00Z,2024-01-01T10:01:00Z,succeeded,,,,,\\n\",\n\t\t\texpected: report.CSVRuns{{Name: \"module/unit\", Started: \"2024-01-01T10:00:00Z\", Ended: \"2024-01-01T10:01:00Z\", Result: \"succeeded\"}},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple runs with all fields\",\n\t\t\tinput: `Name,Started,Ended,Result,Reason,Cause,Ref,Cmd,Args\nunit-a,2024-01-01T10:00:00Z,2024-01-01T10:01:00Z,succeeded,,,HEAD~1,plan,-out=plan.tfplan|-var=foo=bar\nunit-b,2024-01-01T10:01:00Z,2024-01-01T10:02:00Z,failed,run error,some error,main,apply,\n`,\n\t\t\texpected: report.CSVRuns{\n\t\t\t\t{Name: \"unit-a\", Started: \"2024-01-01T10:00:00Z\", Ended: \"2024-01-01T10:01:00Z\", Result: \"succeeded\", Ref: \"HEAD~1\", Cmd: \"plan\", Args: \"-out=plan.tfplan|-var=foo=bar\"},\n\t\t\t\t{Name: \"unit-b\", Started: \"2024-01-01T10:01:00Z\", Ended: \"2024-01-01T10:02:00Z\", Result: \"failed\", Reason: \"run error\", Cause: \"some error\", Ref: \"main\", Cmd: \"apply\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid csv - missing fields\",\n\t\t\tinput:       \"Name,Started,Ended,Result,Reason,Cause,Ref,Cmd,Args\\nunit-a,2024-01-01T10:00:00Z\\n\",\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\truns, err := report.ParseCSVRuns([]byte(tt.input))\n\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Len(t, runs, len(tt.expected))\n\n\t\t\tfor i, expected := range tt.expected {\n\t\t\t\tassert.Equal(t, expected.Name, runs[i].Name)\n\t\t\t\tassert.Equal(t, expected.Result, runs[i].Result)\n\t\t\t\tassert.Equal(t, expected.Reason, runs[i].Reason)\n\t\t\t\tassert.Equal(t, expected.Cause, runs[i].Cause)\n\t\t\t\tassert.Equal(t, expected.Ref, runs[i].Ref)\n\t\t\t\tassert.Equal(t, expected.Cmd, runs[i].Cmd)\n\t\t\t\tassert.Equal(t, expected.Args, runs[i].Args)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestParseCSVRunsFromFile verifies that CSV report data can be parsed from a file.\nfunc TestParseCSVRunsFromFile(t *testing.T) {\n\tt.Parallel()\n\n\ttmp := helpers.TmpDirWOSymlinks(t)\n\n\tt.Run(\"valid file\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\treportFile := filepath.Join(tmp, \"valid-report.csv\")\n\t\tcontent := \"Name,Started,Ended,Result,Reason,Cause,Ref,Cmd,Args\\ntest-unit,2024-01-01T10:00:00Z,2024-01-01T10:01:00Z,succeeded,,,,,\\n\"\n\n\t\terr := os.WriteFile(reportFile, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\n\t\truns, err := report.ParseCSVRunsFromFile(reportFile)\n\t\trequire.NoError(t, err)\n\t\trequire.Len(t, runs, 1)\n\t\tassert.Equal(t, \"test-unit\", runs[0].Name)\n\t})\n\n\tt.Run(\"non-existent file\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t_, err := report.ParseCSVRunsFromFile(filepath.Join(tmp, \"does-not-exist.csv\"))\n\t\trequire.Error(t, err)\n\t})\n}\n\n// TestCSVRunsFindByName verifies that CSV runs can be found by name.\nfunc TestCSVRunsFindByName(t *testing.T) {\n\tt.Parallel()\n\n\truns := report.CSVRuns{\n\t\t{Name: \"unit-a\", Result: \"succeeded\"},\n\t\t{Name: \"module/unit-b\", Result: \"failed\"},\n\t\t{Name: \"nested/path/unit-c\", Result: \"excluded\"},\n\t}\n\n\ttests := []struct {\n\t\texpected *report.CSVRun\n\t\tname     string\n\t\tsearch   string\n\t}{\n\t\t{\n\t\t\tname:     \"find first unit\",\n\t\t\tsearch:   \"unit-a\",\n\t\t\texpected: &report.CSVRun{Name: \"unit-a\", Result: \"succeeded\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"find nested path\",\n\t\t\tsearch:   \"module/unit-b\",\n\t\t\texpected: &report.CSVRun{Name: \"module/unit-b\", Result: \"failed\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"not found\",\n\t\t\tsearch:   \"does-not-exist\",\n\t\t\texpected: nil,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresult := runs.FindByName(tt.search)\n\n\t\t\tif tt.expected == nil {\n\t\t\t\tassert.Nil(t, result)\n\t\t\t} else {\n\t\t\t\trequire.NotNil(t, result)\n\t\t\t\tassert.Equal(t, tt.expected.Name, result.Name)\n\t\t\t\tassert.Equal(t, tt.expected.Result, result.Result)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestCSVRunsNames verifies that run names can be extracted from a slice of CSV runs.\nfunc TestCSVRunsNames(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\truns     report.CSVRuns\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"empty slice\",\n\t\t\truns:     report.CSVRuns{},\n\t\t\texpected: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple runs\",\n\t\t\truns: report.CSVRuns{\n\t\t\t\t{Name: \"unit-a\"},\n\t\t\t\t{Name: \"module/unit-b\"},\n\t\t\t\t{Name: \"nested/path/unit-c\"},\n\t\t\t},\n\t\t\texpected: []string{\"unit-a\", \"module/unit-b\", \"nested/path/unit-c\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tnames := tt.runs.Names()\n\t\t\tassert.Equal(t, tt.expected, names)\n\t\t})\n\t}\n}\n\n// TestParseJSONRunsFromFileValidation verifies that JSON report validation works correctly\n// when parsing files. Validation is performed as the first step in parsing.\nfunc TestParseJSONRunsFromFileValidation(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname        string\n\t\tinput       string\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tname:        \"valid empty report\",\n\t\t\tinput:       \"[]\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid single run\",\n\t\t\tinput: `[{\n\t\t\t\t\"Name\": \"module/unit\",\n\t\t\t\t\"Started\": \"2024-01-01T10:00:00Z\",\n\t\t\t\t\"Ended\": \"2024-01-01T10:01:00Z\",\n\t\t\t\t\"Result\": \"succeeded\"\n\t\t\t}]`,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid run with all fields\",\n\t\t\tinput: `[{\n\t\t\t\t\"Name\": \"module/unit\",\n\t\t\t\t\"Started\": \"2024-01-01T10:00:00Z\",\n\t\t\t\t\"Ended\": \"2024-01-01T10:01:00Z\",\n\t\t\t\t\"Result\": \"failed\",\n\t\t\t\t\"Reason\": \"run error\",\n\t\t\t\t\"Cause\": \"some error\"\n\t\t\t}]`,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid multiple runs\",\n\t\t\tinput: `[\n\t\t\t\t{\"Name\": \"unit-a\", \"Started\": \"2024-01-01T10:00:00Z\", \"Ended\": \"2024-01-01T10:01:00Z\", \"Result\": \"succeeded\"},\n\t\t\t\t{\"Name\": \"unit-b\", \"Started\": \"2024-01-01T10:01:00Z\", \"Ended\": \"2024-01-01T10:02:00Z\", \"Result\": \"failed\", \"Reason\": \"run error\"}\n\t\t\t]`,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid - not an array\",\n\t\t\tinput:       `{\"Name\": \"unit\", \"Result\": \"succeeded\"}`,\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid - missing required field Name\",\n\t\t\tinput:       `[{\"Started\": \"2024-01-01T10:00:00Z\", \"Ended\": \"2024-01-01T10:01:00Z\", \"Result\": \"succeeded\"}]`,\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid - missing required field Result\",\n\t\t\tinput:       `[{\"Name\": \"unit\", \"Started\": \"2024-01-01T10:00:00Z\", \"Ended\": \"2024-01-01T10:01:00Z\"}]`,\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid json\",\n\t\t\tinput:       \"not valid json\",\n\t\t\texpectError: true,\n\t\t},\n\t}\n\n\ttmp := helpers.TmpDirWOSymlinks(t)\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\treportFile := filepath.Join(tmp, strings.ReplaceAll(tt.name, \" \", \"-\")+\".json\")\n\t\t\terr := os.WriteFile(reportFile, []byte(tt.input), 0644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t_, err = report.ParseJSONRunsFromFile(reportFile)\n\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"schema validation error details\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\treportFile := filepath.Join(tmp, \"schema-error-details.json\")\n\t\tcontent := `[{\"Name\": \"test-unit\"}]` // missing required fields\n\n\t\terr := os.WriteFile(reportFile, []byte(content), 0644)\n\t\trequire.NoError(t, err)\n\n\t\t_, err = report.ParseJSONRunsFromFile(reportFile)\n\t\trequire.Error(t, err)\n\n\t\tvar schemaErr *report.SchemaValidationError\n\t\trequire.ErrorAs(t, err, &schemaErr)\n\t\tassert.NotEmpty(t, schemaErr.Errors)\n\t})\n}\n\n// TestSchemaValidationError verifies the error type works correctly.\nfunc TestSchemaValidationError(t *testing.T) {\n\tt.Parallel()\n\n\terr := &report.SchemaValidationError{\n\t\tErrors: []string{\"error 1\", \"error 2\"},\n\t}\n\n\tassert.Contains(t, err.Error(), \"2 error(s)\")\n\tassert.Contains(t, err.Error(), \"error 1\")\n\tassert.Contains(t, err.Error(), \"error 2\")\n}\n\n// newRun creates a new run, and asserts that it doesn't error.\nfunc newRun(t *testing.T, name string) *report.Run {\n\tt.Helper()\n\n\trun, err := report.NewRun(name)\n\trequire.NoError(t, err)\n\n\treturn run\n}\n"
  },
  {
    "path": "internal/report/summary.go",
    "content": "package report\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Summary formats data from a report for output as a summary.\ntype Summary struct {\n\tfirstRunStart        *time.Time\n\tlastRunEnd           *time.Time\n\tpadder               string\n\tworkingDir           string\n\truns                 []*Run\n\tUnitsSucceeded       int\n\tUnitsFailed          int\n\tEarlyExits           int\n\tExcluded             int\n\tshouldColor          bool\n\tshowUnitLevelSummary bool\n}\n\n// Summarize returns a summary of the report.\nfunc (r *Report) Summarize() *Summary {\n\tsummary := &Summary{\n\t\tworkingDir:           r.workingDir,\n\t\tshouldColor:          r.shouldColor,\n\t\tshowUnitLevelSummary: r.showUnitLevelSummary,\n\t\tpadder:               \".\",\n\t\truns:                 r.Runs,\n\t}\n\n\tif len(r.Runs) == 0 {\n\t\treturn summary\n\t}\n\n\tfor _, run := range r.Runs {\n\t\tsummary.Update(run)\n\t}\n\n\treturn summary\n}\n\nfunc (s *Summary) TotalUnits() int {\n\treturn len(s.runs)\n}\n\nfunc (s *Summary) Update(run *Run) {\n\trun.mu.RLock()\n\tdefer run.mu.RUnlock()\n\n\tswitch run.Result {\n\tcase ResultSucceeded:\n\t\ts.UnitsSucceeded++\n\tcase ResultFailed:\n\t\ts.UnitsFailed++\n\tcase ResultEarlyExit:\n\t\ts.EarlyExits++\n\tcase ResultExcluded:\n\t\ts.Excluded++\n\t}\n\n\tif s.firstRunStart == nil || run.Started.Before(*s.firstRunStart) {\n\t\ts.firstRunStart = &run.Started\n\t}\n\n\tif !run.Ended.IsZero() && (s.lastRunEnd == nil || run.Ended.After(*s.lastRunEnd)) {\n\t\ts.lastRunEnd = &run.Ended\n\t}\n}\n\n// TotalDuration returns the total duration of all runs in the report.\nfunc (s *Summary) TotalDuration() time.Duration {\n\tif s.firstRunStart == nil || s.lastRunEnd == nil {\n\t\treturn 0\n\t}\n\n\treturn s.lastRunEnd.Sub(*s.firstRunStart)\n}\n\n// TotalDurationString returns the total duration of all runs in the report as a string.\n// It returns the duration in the format that is easy to understand by humans.\nfunc (s *Summary) TotalDurationString(colorizer *Colorizer) string {\n\tduration := s.TotalDuration()\n\n\treturn colorizer.colorDuration(duration)\n}\n\n// WriteSummary writes the summary to a writer.\nfunc (r *Report) WriteSummary(w io.Writer) error {\n\tsummary := r.Summarize()\n\n\t// Don't write anything if there are no units\n\tif summary.TotalUnits() == 0 {\n\t\treturn nil\n\t}\n\n\t_, err := fmt.Fprintf(w, \"\\n\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = summary.Write(w)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = fmt.Fprintf(w, \"\\n\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Write writes the summary to a writer.\nfunc (s *Summary) Write(w io.Writer) error {\n\tcolorizer := NewColorizer(s.shouldColor)\n\n\tif s.showUnitLevelSummary {\n\t\treturn s.writeUnitLevelSummary(w, colorizer)\n\t}\n\n\theader := fmt.Sprintf(\"%s  %s  %s\",\n\t\tcolorizer.headingTitleColorizer(runSummaryHeader),\n\t\tcolorizer.headingUnitColorizer(fmt.Sprintf(\"%d units\", s.TotalUnits())),\n\t\ts.TotalDurationString(colorizer),\n\t)\n\tif err := s.writeSummaryHeader(w, header); err != nil {\n\t\treturn err\n\t}\n\n\tseparatorLine := fmt.Sprintf(\"%s%s\", prefix, strings.Repeat(\"─\", separatorLineLength))\n\tif err := s.writeSummaryHeader(w, separatorLine); err != nil {\n\t\treturn err\n\t}\n\n\tif s.UnitsSucceeded > 0 {\n\t\tif err := s.writeSummaryEntry(\n\t\t\tw,\n\t\t\tcolorizer.successColorizer(successLabel),\n\t\t\tcolorizer.successUnitColorizer(strconv.Itoa(s.UnitsSucceeded)),\n\t\t); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif s.UnitsFailed > 0 {\n\t\tif err := s.writeSummaryEntry(\n\t\t\tw,\n\t\t\tcolorizer.failureColorizer(failureLabel),\n\t\t\tcolorizer.failureUnitColorizer(strconv.Itoa(s.UnitsFailed)),\n\t\t); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif s.EarlyExits > 0 {\n\t\tif err := s.writeSummaryEntry(\n\t\t\tw,\n\t\t\tcolorizer.exitColorizer(earlyExitLabel),\n\t\t\tcolorizer.exitUnitColorizer(strconv.Itoa(s.EarlyExits)),\n\t\t); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif s.Excluded > 0 {\n\t\tif err := s.writeSummaryEntry(\n\t\t\tw,\n\t\t\tcolorizer.excludeColorizer(excludeLabel),\n\t\t\tcolorizer.excludeUnitColorizer(strconv.Itoa(s.Excluded)),\n\t\t); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nconst (\n\tprefix                     = \"   \"\n\tunitPrefixMultiplier       = 2\n\trunSummaryHeader           = \"❯❯ Run Summary\"\n\tsuccessLabel               = \"Succeeded\"\n\tfailureLabel               = \"Failed\"\n\tearlyExitLabel             = \"Early Exits\"\n\texcludeLabel               = \"Excluded\"\n\tseparatorLineLength        = 28\n\tdurationAlignmentOffset    = 4\n\theaderUnitCountSpacing     = 2\n\tdefaultUnitNameLength      = 20\n\theaderPaddingAdjustment    = 3\n\tseparatorPaddingAdjustment = 2\n)\n\nfunc (s *Summary) writeSummaryHeader(w io.Writer, value string) error {\n\t_, err := fmt.Fprintf(w, \"%s\\n\", value)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s *Summary) writeSummaryEntry(w io.Writer, label string, value string) error {\n\t_, err := fmt.Fprintf(w, \"%s%s%s%s\\n\", prefix, label, s.padding(label), value)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// writeUnitLevelSummary writes the summary with unit level summaries grouped by categories\nfunc (s *Summary) writeUnitLevelSummary(w io.Writer, colorizer *Colorizer) error {\n\tmaxUnitNameLength := 0\n\n\tfor _, run := range s.runs {\n\t\tname := run.Path\n\t\tif s.workingDir != \"\" {\n\t\t\tname = strings.TrimPrefix(name, s.workingDir+string(os.PathSeparator))\n\t\t}\n\n\t\tif len(name) > maxUnitNameLength {\n\t\t\tmaxUnitNameLength = len(name)\n\t\t}\n\t}\n\n\theaderPadding := 0\n\tif maxUnitNameLength > defaultUnitNameLength {\n\t\theaderPadding = maxUnitNameLength - defaultUnitNameLength + headerPaddingAdjustment\n\t}\n\n\theader := fmt.Sprintf(\n\t\t\"%s  %s%s  %s\",\n\t\trunSummaryHeader,\n\t\tcolorizer.headingUnitColorizer(fmt.Sprintf(\"%d units\", s.TotalUnits())),\n\t\tstrings.Repeat(\" \", headerPadding),\n\t\ts.TotalDurationString(colorizer),\n\t)\n\tif err := s.writeSummaryHeader(w, colorizer.headingTitleColorizer(header)); err != nil {\n\t\treturn err\n\t}\n\n\tseparatorAdjustment := 0\n\tif headerPadding > 0 {\n\t\tseparatorAdjustment = headerPadding - separatorPaddingAdjustment\n\t}\n\n\tseparatorLine := fmt.Sprintf(\"%s%s\", prefix, strings.Repeat(\"─\", separatorLineLength+separatorAdjustment))\n\tif err := s.writeSummaryHeader(w, separatorLine); err != nil {\n\t\treturn err\n\t}\n\n\tresultGroups := map[Result][]*Run{\n\t\tResultSucceeded: {},\n\t\tResultFailed:    {},\n\t\tResultEarlyExit: {},\n\t\tResultExcluded:  {},\n\t}\n\n\tfor _, run := range s.runs {\n\t\tresultGroups[run.Result] = append(resultGroups[run.Result], run)\n\t}\n\n\tcategories := []struct {\n\t\tcolorizer     func(string) string\n\t\tunitColorizer func(string) string\n\t\tresult        Result\n\t\tlabel         string\n\t\tcount         int\n\t}{\n\t\t{\n\t\t\tcolorizer:     colorizer.successColorizer,\n\t\t\tunitColorizer: colorizer.successUnitColorizer,\n\t\t\tresult:        ResultSucceeded,\n\t\t\tlabel:         successLabel,\n\t\t\tcount:         s.UnitsSucceeded,\n\t\t},\n\t\t{\n\t\t\tcolorizer:     colorizer.failureColorizer,\n\t\t\tunitColorizer: colorizer.failureUnitColorizer,\n\t\t\tresult:        ResultFailed,\n\t\t\tlabel:         failureLabel,\n\t\t\tcount:         s.UnitsFailed,\n\t\t},\n\t\t{\n\t\t\tcolorizer:     colorizer.exitColorizer,\n\t\t\tunitColorizer: colorizer.exitUnitColorizer,\n\t\t\tresult:        ResultEarlyExit,\n\t\t\tlabel:         earlyExitLabel,\n\t\t\tcount:         s.EarlyExits,\n\t\t},\n\t\t{\n\t\t\tcolorizer:     colorizer.excludeColorizer,\n\t\t\tunitColorizer: colorizer.excludeUnitColorizer,\n\t\t\tresult:        ResultExcluded,\n\t\t\tlabel:         excludeLabel,\n\t\t\tcount:         s.Excluded,\n\t\t},\n\t}\n\n\tfor _, category := range categories {\n\t\tif category.count > 0 {\n\t\t\tcategoryHeader := fmt.Sprintf(\"%s (%d)\", category.label, category.count)\n\n\t\t\tcategoryHeaderColored := category.colorizer(categoryHeader)\n\t\t\tif _, err := fmt.Fprintf(w, \"%s%s\\n\", prefix, categoryHeaderColored); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\truns := resultGroups[category.result]\n\t\t\tslices.SortFunc(runs, func(a, b *Run) int {\n\t\t\t\taDuration := a.Ended.Sub(a.Started)\n\t\t\t\tbDuration := b.Ended.Sub(b.Started)\n\n\t\t\t\treturn int(bDuration - aDuration)\n\t\t\t})\n\n\t\t\tfor _, run := range runs {\n\t\t\t\tif err := s.writeUnitDuration(w, run, colorizer, category.unitColorizer); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// writeUnitDuration writes unit duration with cleaner formatting\nfunc (s *Summary) writeUnitDuration(w io.Writer, run *Run, colorizer *Colorizer, unitColorizer func(string) string) error {\n\tduration := run.Ended.Sub(run.Started)\n\n\tname := run.Path\n\tif s.workingDir != \"\" {\n\t\tname = strings.TrimPrefix(name, s.workingDir+string(os.PathSeparator))\n\t}\n\n\tpadding := s.unitDurationPadding(name, colorizer)\n\n\t_, err := fmt.Fprintf(\n\t\tw, \"%s%s%s%s\\n\",\n\t\tstrings.Repeat(prefix, unitPrefixMultiplier),\n\t\tunitColorizer(name),\n\t\tpadding,\n\t\tcolorizer.colorDuration(duration),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (s *Summary) padding(label string) string {\n\theaderUnitCountVisualPosition := s.visualLength(runSummaryHeader) + headerUnitCountSpacing\n\n\tcurrentLabelLength := s.visualLength(label)\n\tcurrentPosition := len(prefix) + currentLabelLength\n\n\tpaddingNeeded := headerUnitCountVisualPosition - currentPosition\n\n\tpaddingNeeded -= 4\n\n\tif paddingNeeded < 0 {\n\t\tpaddingNeeded = 0\n\t}\n\n\tpadding := strings.Repeat(s.padder, paddingNeeded)\n\n\twhitespaceLen := 2\n\n\tif len(padding) < whitespaceLen {\n\t\treturn \"  \"\n\t}\n\n\tpadding = \" \" + padding[1:len(padding)-1] + \" \"\n\n\treturn strings.ReplaceAll(padding, s.padder, \" \")\n}\n\n// ansiRegex is used to remove ANSI escape codes from strings.\n// We compile it here to avoid re-compiling it on every call to visualLength.\nvar ansiRegex = regexp.MustCompile(`\\x1b\\[[0-9;]*m`)\n\n// visualLength calculates the visual length of a string by removing ANSI escape codes\nfunc (s *Summary) visualLength(text string) int {\n\tcleanText := ansiRegex.ReplaceAllString(text, \"\")\n\n\treturn len(cleanText)\n}\n\n// unitDurationPadding calculates padding for unit names to align durations with header\nfunc (s *Summary) unitDurationPadding(name string, colorizer *Colorizer) string {\n\tmaxUnitNameLength := 0\n\n\tfor _, run := range s.runs {\n\t\trunName := run.Path\n\t\tif s.workingDir != \"\" {\n\t\t\trunName = strings.TrimPrefix(runName, s.workingDir+string(os.PathSeparator))\n\t\t}\n\n\t\tif len(runName) > maxUnitNameLength {\n\t\t\tmaxUnitNameLength = len(runName)\n\t\t}\n\t}\n\n\theaderPadding := 0\n\tif maxUnitNameLength > defaultUnitNameLength {\n\t\theaderPadding = maxUnitNameLength - defaultUnitNameLength + headerPaddingAdjustment\n\t}\n\n\theaderPrefix := fmt.Sprintf(\"%s  %d units  \", runSummaryHeader, s.TotalUnits())\n\theaderDurationColumn := len(headerPrefix) + headerPadding\n\n\tunitPrefix := strings.Repeat(prefix, unitPrefixMultiplier)\n\tcurrentPosition := len(unitPrefix) + len(name)\n\n\tpaddingNeeded := max(1, headerDurationColumn-currentPosition-durationAlignmentOffset)\n\n\tpadding := strings.Repeat(s.padder, paddingNeeded)\n\n\twhitespaceLen := 2\n\n\tif len(padding) < whitespaceLen {\n\t\treturn \"  \"\n\t}\n\n\tpadding = \" \" + padding[1:len(padding)-1] + \" \"\n\n\treturn colorizer.paddingColorizer(padding)\n}\n"
  },
  {
    "path": "internal/report/writer.go",
    "content": "package report\n\nimport (\n\t\"bytes\"\n\t\"encoding/csv\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/invopop/jsonschema\"\n\t\"github.com/xeipuuv/gojsonschema\"\n)\n\nconst (\n\t// csvFieldCount is the expected number of fields in a CSV report row.\n\tcsvFieldCount = 9\n\t// csvRowOffset accounts for: 0-indexed loop (i starts at 0) + skipped header row.\n\tcsvRowOffset = 2\n)\n\n// JSONRun represents a run in JSON format.\ntype JSONRun struct {\n\t// Started is the time when the run started.\n\tStarted time.Time `json:\"Started\" jsonschema:\"required\"`\n\t// Ended is the time when the run ended.\n\tEnded time.Time `json:\"Ended\" jsonschema:\"required\"`\n\t// Reason is the reason for the run result, if any.\n\tReason *string `json:\"Reason,omitempty\" jsonschema:\"enum=retry succeeded,enum=error ignored,enum=run error,enum=exclude block,enum=ancestor error\"`\n\t// Cause is the cause of the run result, if any.\n\tCause *string `json:\"Cause,omitempty\"`\n\t// Name is the name of the run.\n\tName string `json:\"Name\" jsonschema:\"required\"`\n\t// Result is the result of the run.\n\tResult string `json:\"Result\" jsonschema:\"required,enum=succeeded,enum=failed,enum=early exit,enum=excluded\"`\n\t// Ref is the worktree reference (e.g., git commit, branch).\n\tRef string `json:\"Ref,omitempty\"`\n\t// Cmd is the terraform command (plan, apply, etc.).\n\tCmd string `json:\"Cmd,omitempty\"`\n\t// Args are the terraform CLI arguments.\n\tArgs []string `json:\"Args,omitempty\"`\n}\n\n// JSONRuns is a slice of JSONRun entries with helper methods.\ntype JSONRuns []JSONRun\n\n// ParseJSONRuns parses a JSON report from a byte slice.\n// Returns a slice of JSONRun entries or an error if parsing fails.\nfunc ParseJSONRuns(data []byte) (JSONRuns, error) {\n\tvar runs JSONRuns\n\tif err := json.Unmarshal(data, &runs); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse JSON report: %w\", err)\n\t}\n\n\treturn runs, nil\n}\n\n// ParseJSONRunsFromFile reads and parses a JSON report from a file.\n// Returns a slice of JSONRun entries or an error if reading, validation, or parsing fails.\n// The report is validated against the JSON schema before parsing.\nfunc ParseJSONRunsFromFile(path string) (JSONRuns, error) {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read report file %s: %w\", path, err)\n\t}\n\n\tif err := validateJSONReport(data); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn ParseJSONRuns(data)\n}\n\n// FindByName searches for a run by name.\n// Returns the run if found, or nil if not found.\nfunc (runs JSONRuns) FindByName(name string) *JSONRun {\n\tfor i := range runs {\n\t\tif runs[i].Name == name {\n\t\t\treturn &runs[i]\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Names returns a slice of all run names.\n// Useful for debugging and assertions in tests.\nfunc (runs JSONRuns) Names() []string {\n\tnames := make([]string, len(runs))\n\tfor i := range runs {\n\t\tnames[i] = runs[i].Name\n\t}\n\n\treturn names\n}\n\n// CSVRun represents a run parsed from CSV format.\ntype CSVRun struct {\n\tName    string\n\tStarted string\n\tEnded   string\n\tResult  string\n\tReason  string\n\tCause   string\n\tRef     string\n\tCmd     string\n\tArgs    string\n}\n\n// CSVRuns is a slice of CSVRun entries with helper methods.\ntype CSVRuns []CSVRun\n\n// ParseCSVRuns parses a CSV report from a byte slice.\n// Returns a slice of CSVRun entries or an error if parsing fails.\n// The first row is expected to be a header row and is skipped.\nfunc ParseCSVRuns(data []byte) (CSVRuns, error) {\n\treader := csv.NewReader(bytes.NewReader(data))\n\n\trecords, err := reader.ReadAll()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse CSV report: %w\", err)\n\t}\n\n\t// Skip header row\n\tif len(records) < 1 {\n\t\treturn CSVRuns{}, nil\n\t}\n\n\truns := make(CSVRuns, 0, len(records)-1)\n\n\tfor i, record := range records[1:] {\n\t\tif len(record) < csvFieldCount {\n\t\t\treturn nil, fmt.Errorf(\"invalid CSV record at row %d: expected %d fields, got %d\", i+csvRowOffset, csvFieldCount, len(record))\n\t\t}\n\n\t\truns = append(runs, CSVRun{\n\t\t\tName:    record[0],\n\t\t\tStarted: record[1],\n\t\t\tEnded:   record[2],\n\t\t\tResult:  record[3],\n\t\t\tReason:  record[4],\n\t\t\tCause:   record[5],\n\t\t\tRef:     record[6],\n\t\t\tCmd:     record[7],\n\t\t\tArgs:    record[8],\n\t\t})\n\t}\n\n\treturn runs, nil\n}\n\n// ParseCSVRunsFromFile reads and parses a CSV report from a file.\n// Returns a slice of CSVRun entries or an error if reading or parsing fails.\nfunc ParseCSVRunsFromFile(path string) (CSVRuns, error) {\n\tdata, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read report file %s: %w\", path, err)\n\t}\n\n\treturn ParseCSVRuns(data)\n}\n\n// FindByName searches for a run by name.\n// Returns the run if found, or nil if not found.\nfunc (runs CSVRuns) FindByName(name string) *CSVRun {\n\tfor i := range runs {\n\t\tif runs[i].Name == name {\n\t\t\treturn &runs[i]\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Names returns a slice of all run names.\n// Useful for debugging and assertions in tests.\nfunc (runs CSVRuns) Names() []string {\n\tnames := make([]string, len(runs))\n\tfor i := range runs {\n\t\tnames[i] = runs[i].Name\n\t}\n\n\treturn names\n}\n\n// SchemaValidationError represents a schema validation error with details.\ntype SchemaValidationError struct {\n\tErrors []string\n}\n\nfunc (e *SchemaValidationError) Error() string {\n\treturn fmt.Sprintf(\"schema validation failed with %d error(s): %v\", len(e.Errors), e.Errors)\n}\n\n// validateJSONReport validates a JSON report against the schema.\n// Returns nil if valid, or a SchemaValidationError with details if invalid.\nfunc validateJSONReport(data []byte) error {\n\tschemaBytes, err := json.Marshal(generateReportSchema())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate schema: %w\", err)\n\t}\n\n\tschemaLoader := gojsonschema.NewBytesLoader(schemaBytes)\n\tdocumentLoader := gojsonschema.NewBytesLoader(data)\n\n\tresult, err := gojsonschema.Validate(schemaLoader, documentLoader)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to validate report: %w\", err)\n\t}\n\n\tif !result.Valid() {\n\t\terrors := make([]string, len(result.Errors()))\n\t\tfor i, validationErr := range result.Errors() {\n\t\t\terrors[i] = validationErr.String()\n\t\t}\n\n\t\treturn &SchemaValidationError{Errors: errors}\n\t}\n\n\treturn nil\n}\n\n// WriteToFile writes the report to a file.\nfunc (r *Report) WriteToFile(path string) error {\n\ttmpFile, err := os.CreateTemp(\"\", \"terragrunt-report-*\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tr.mu.Lock()\n\tr.SortRuns()\n\tr.mu.Unlock()\n\n\tswitch r.format {\n\tcase FormatCSV:\n\t\terr = r.WriteCSV(tmpFile)\n\tcase FormatJSON:\n\t\terr = r.WriteJSON(tmpFile)\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported format: %s\", r.format)\n\t}\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write report: %w\", err)\n\t}\n\n\tif err := tmpFile.Close(); err != nil {\n\t\treturn fmt.Errorf(\"failed to close report file: %w\", err)\n\t}\n\n\tif r.workingDir != \"\" && !filepath.IsAbs(path) {\n\t\tpath = filepath.Join(r.workingDir, path)\n\t}\n\n\treturn util.MoveFile(tmpFile.Name(), path)\n}\n\n// WriteCSV writes the report to a writer in CSV format.\nfunc (r *Report) WriteCSV(w io.Writer) error {\n\tr.mu.RLock()\n\tdefer r.mu.RUnlock()\n\n\tcsvWriter := csv.NewWriter(w)\n\tdefer csvWriter.Flush()\n\n\terr := csvWriter.Write([]string{\n\t\t\"Name\",\n\t\t\"Started\",\n\t\t\"Ended\",\n\t\t\"Result\",\n\t\t\"Reason\",\n\t\t\"Cause\",\n\t\t\"Ref\",\n\t\t\"Cmd\",\n\t\t\"Args\",\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, run := range r.Runs {\n\t\trun.mu.RLock()\n\t\tdefer run.mu.RUnlock()\n\n\t\tworkingDir := effectiveWorkingDir(run, r.workingDir)\n\t\tname := nameOfPath(run.Path, workingDir)\n\n\t\tstarted := run.Started.Format(time.RFC3339)\n\t\tended := run.Ended.Format(time.RFC3339)\n\t\tresult := string(run.Result)\n\t\treason := \"\"\n\n\t\tif run.Reason != nil {\n\t\t\treason = string(*run.Reason)\n\t\t}\n\n\t\tcause := \"\"\n\t\tif run.Cause != nil {\n\t\t\tcause = string(*run.Cause)\n\n\t\t\tif reason == string(ReasonAncestorError) && workingDir != \"\" {\n\t\t\t\tcause = strings.TrimPrefix(cause, workingDir+string(os.PathSeparator))\n\t\t\t}\n\t\t}\n\n\t\t// Format Args as pipe-separated string for CSV to avoid conflicts with CSV column separator\n\t\targs := strings.Join(run.Args, \"|\")\n\n\t\terr := csvWriter.Write([]string{\n\t\t\tname,\n\t\t\tstarted,\n\t\t\tended,\n\t\t\tresult,\n\t\t\treason,\n\t\t\tcause,\n\t\t\trun.Ref,\n\t\t\trun.Cmd,\n\t\t\targs,\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// WriteJSON writes the report to a writer in JSON format.\nfunc (r *Report) WriteJSON(w io.Writer) error {\n\tr.mu.RLock()\n\tdefer r.mu.RUnlock()\n\n\truns := make([]JSONRun, 0, len(r.Runs))\n\n\tfor _, run := range r.Runs {\n\t\trun.mu.RLock()\n\t\tdefer run.mu.RUnlock()\n\n\t\tworkingDir := effectiveWorkingDir(run, r.workingDir)\n\t\tname := nameOfPath(run.Path, workingDir)\n\n\t\tjsonRun := JSONRun{\n\t\t\tName:    name,\n\t\t\tStarted: run.Started,\n\t\t\tEnded:   run.Ended,\n\t\t\tRef:     run.Ref,\n\t\t\tCmd:     run.Cmd,\n\t\t\tArgs:    run.Args,\n\t\t\tResult:  string(run.Result),\n\t\t}\n\n\t\tif run.Reason != nil {\n\t\t\treason := string(*run.Reason)\n\t\t\tjsonRun.Reason = &reason\n\t\t}\n\n\t\tif run.Cause != nil {\n\t\t\tcause := string(*run.Cause)\n\t\t\tif run.Reason != nil && *run.Reason == ReasonAncestorError && workingDir != \"\" {\n\t\t\t\tcause = strings.TrimPrefix(cause, workingDir+string(os.PathSeparator))\n\t\t\t}\n\n\t\t\tjsonRun.Cause = &cause\n\t\t}\n\n\t\truns = append(runs, jsonRun)\n\t}\n\n\tjsonBytes, err := json.MarshalIndent(runs, \"\", \"  \")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tjsonBytes = append(jsonBytes, '\\n')\n\n\t_, err = w.Write(jsonBytes)\n\n\treturn err\n}\n\n// WriteSchemaToFile writes a JSON schema for the report to a file.\nfunc (r *Report) WriteSchemaToFile(path string) error {\n\ttmpFile, err := os.CreateTemp(\"\", \"terragrunt-schema-*\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := WriteSchema(tmpFile); err != nil {\n\t\treturn fmt.Errorf(\"failed to write schema: %w\", err)\n\t}\n\n\tif err := tmpFile.Close(); err != nil {\n\t\treturn fmt.Errorf(\"failed to close schema file: %w\", err)\n\t}\n\n\tif r.workingDir != \"\" && !filepath.IsAbs(path) {\n\t\tpath = filepath.Join(r.workingDir, path)\n\t}\n\n\treturn util.MoveFile(tmpFile.Name(), path)\n}\n\n// WriteSchema writes a JSON schema for the report to a writer.\nfunc WriteSchema(w io.Writer) error {\n\tarraySchema := generateReportSchema()\n\n\tjsonBytes, err := json.MarshalIndent(arraySchema, \"\", \"  \")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tjsonBytes = append(jsonBytes, '\\n')\n\n\t_, err = w.Write(jsonBytes)\n\n\treturn err\n}\n\n// nameOfPath returns a name for a path given a working directory.\n//\n// The logic for determining the name of a given path is:\n//\n//   - If the path is the same as the working directory, return the base name of the path.\n//     This is usually only relevant when performing a `run --all` in a unit directory.\n//\n//   - If the path is not a subdirectory of the working directory, return the path as is.\n//\n//   - Otherwise, return the path relative to the working directory, with any leading slashes removed.\nfunc nameOfPath(path string, workingDir string) string {\n\t// If the path is the same as the working directory,\n\t// return the base name of the path.\n\tif path == workingDir {\n\t\treturn filepath.Base(path)\n\t}\n\n\t// If the path is not a subdirectory of the working directory,\n\t// return the path as is.\n\tif !strings.HasPrefix(path, workingDir) {\n\t\treturn path\n\t}\n\n\tpath = strings.TrimPrefix(path, workingDir)\n\tpath = strings.TrimPrefix(path, string(os.PathSeparator))\n\n\treturn path\n}\n\n// effectiveWorkingDir returns the working directory to use for path computation.\n// If the run has a DiscoveryWorkingDir set (for worktree scenarios), use that.\n// Otherwise, fall back to the report's workingDir.\nfunc effectiveWorkingDir(run *Run, reportWorkingDir string) string {\n\tif run.DiscoveryWorkingDir != \"\" {\n\t\treturn run.DiscoveryWorkingDir\n\t}\n\n\treturn reportWorkingDir\n}\n\n// generateReportSchema generates the JSON schema for report validation.\nfunc generateReportSchema() *jsonschema.Schema {\n\treflector := jsonschema.Reflector{\n\t\tDoNotReference: true,\n\t}\n\n\tschema := reflector.Reflect(&JSONRun{})\n\tschema.Description = \"Schema for Terragrunt run report\"\n\tschema.Title = \"Terragrunt Run Report Schema\"\n\tschema.Version = \"\"\n\tschema.ID = \"\"\n\n\treturn &jsonschema.Schema{\n\t\tVersion:     \"https://json-schema.org/draft/2020-12/schema\",\n\t\tID:          \"https://docs.terragrunt.com/schemas/run/report/v4/schema.json\",\n\t\tType:        \"array\",\n\t\tTitle:       \"Terragrunt Run Report Schema\",\n\t\tDescription: \"Array of Terragrunt runs\",\n\t\tItems:       schema,\n\t}\n}\n"
  },
  {
    "path": "internal/retry/defaults.go",
    "content": "// Package retry provides default retry configuration for Terragrunt.\npackage retry\n\nimport \"time\"\n\n// DefaultMaxAttempts is the default number of retry attempts.\nconst DefaultMaxAttempts = 3\n\n// DefaultSleepInterval is the default sleep interval between retries.\nconst DefaultSleepInterval = 5 * time.Second\n\n// DefaultRetryableErrors is a list of errors that are considered transient and\n// should be retried.\n//\n// It's a list of recurring transient errors encountered when calling terraform.\n// If any of these match, we'll retry the command.\nvar DefaultRetryableErrors = []string{\n\t\"(?s).*Failed to load state.*tcp.*timeout.*\",\n\t\"(?s).*Failed to load backend.*TLS handshake timeout.*\",\n\t\"(?s).*Creating metric alarm failed.*request to update this alarm is in progress.*\",\n\t\"(?s).*Error installing provider.*TLS handshake timeout.*\",\n\t\"(?s).*Error configuring the backend.*TLS handshake timeout.*\",\n\t\"(?s).*Error installing provider.*tcp.*timeout.*\",\n\t\"(?s).*Error installing provider.*tcp.*connection reset by peer.*\",\n\t\"NoSuchBucket: The specified bucket does not exist\",\n\t\"(?s).*Error creating SSM parameter: TooManyUpdates:.*\",\n\t\"(?s).*app.terraform.io.*: 429 Too Many Requests.*\",\n\t\"(?s).*ssh_exchange_identification.*Connection closed by remote host.*\",\n\t\"(?s).*Client\\\\.Timeout exceeded while awaiting headers.*\",\n\t\"(?s).*Could not download module.*The requested URL returned error: 429.*\",\n\t\"(?s).*net/http: TLS.*handshake timeout.*\",\n\t\"(?s).*could not query provider registry.*context deadline exceeded.*\",\n}\n"
  },
  {
    "path": "internal/runner/common/options.go",
    "content": "package common\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/worktrees\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n)\n\n// Option applies configuration to a StackRunner.\ntype Option interface {\n\tApply(stack StackRunner)\n}\n\n// optionImpl is a lightweight Option implementation that wraps an apply function\n// and optionally carries HCL parser options.\ntype optionImpl struct {\n\tapply         func(StackRunner)\n\tparserOptions []hclparse.Option\n}\n\nfunc (o optionImpl) Apply(stack StackRunner) {\n\tif o.apply != nil {\n\t\to.apply(stack)\n\t}\n}\n\n// ParseOptionsProvider exposes HCL parser options carried by an Option.\ntype ParseOptionsProvider interface {\n\tGetParseOptions() []hclparse.Option\n}\n\n// GetParseOptions returns the HCL parser options attached to the option, if any.\nfunc (o optionImpl) GetParseOptions() []hclparse.Option {\n\tif len(o.parserOptions) > 0 {\n\t\treturn o.parserOptions\n\t}\n\n\treturn nil\n}\n\n// WithParseOptions provides custom HCL parser options to both discovery and stack execution.\nfunc WithParseOptions(parserOptions []hclparse.Option) Option {\n\treturn optionImpl{\n\t\t// No-op apply for runner; discovery picks up parser options via GetParseOptions\n\t\tapply:         func(StackRunner) {},\n\t\tparserOptions: parserOptions,\n\t}\n}\n\n// WorktreeOption carries worktrees through the runner pipeline for git filter expressions.\ntype WorktreeOption struct {\n\tWorktrees *worktrees.Worktrees\n}\n\n// Apply is a no-op for runner (worktrees are used in discovery, not runner execution).\nfunc (o WorktreeOption) Apply(stack StackRunner) {}\n\n// WithWorktrees provides git worktrees to discovery for git filter expressions.\nfunc WithWorktrees(w *worktrees.Worktrees) Option {\n\treturn WorktreeOption{Worktrees: w}\n}\n"
  },
  {
    "path": "internal/runner/common/runner.go",
    "content": "// Package common defines minimal runner interfaces to allow multiple implementations.\npackage common\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// StackRunner is the abstraction for running a stack of units.\n// Implemented by runnerpool.Runner and any alternate runner implementations.\ntype StackRunner interface {\n\t// Run executes all units in the stack according to the specified Terraform command and options.\n\tRun(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, r *report.Report) error\n\t// LogUnitDeployOrder logs the order in which units will be deployed for the given Terraform command.\n\tLogUnitDeployOrder(l log.Logger, terraformCmd string, isDestroy bool, showAbsPaths bool) error\n\t// JSONUnitDeployOrder returns the deployment order of units as a JSON string.\n\tJSONUnitDeployOrder(isDestroy bool, showAbsPaths bool) (string, error)\n\t// ListStackDependentUnits returns a map of each unit to the list of units that depend on it.\n\tListStackDependentUnits() map[string][]string\n\t// GetStack retrieves the underlying Stack object managed by this runner.\n\tGetStack() *component.Stack\n}\n"
  },
  {
    "path": "internal/runner/common/unit_runner.go",
    "content": "package common\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runcfg\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// UnitStatus represents the status of a unit during execution.\ntype UnitStatus int\n\nconst (\n\tWaiting UnitStatus = iota\n\tRunning\n\tFinished\n)\n\n// UnitRunner handles the logic for running a single component.Unit.\ntype UnitRunner struct {\n\tErr    error\n\tUnit   *component.Unit\n\tStatus UnitStatus\n}\n\n// NewUnitRunner creates a UnitRunner from a component.Unit.\nfunc NewUnitRunner(unit *component.Unit) *UnitRunner {\n\treturn &UnitRunner{\n\t\tUnit:   unit,\n\t\tStatus: Waiting,\n\t}\n}\n\nfunc (runner *UnitRunner) runTerragrunt(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tr *report.Report,\n\tcfg *runcfg.RunConfig,\n\tcredsGetter *creds.Getter,\n) error {\n\tl.Debugf(\"Running %s\", util.RelPathForLog(opts.RootWorkingDir, runner.Unit.Path(), opts.Writers.LogShowAbsPaths))\n\n\tdefer func() {\n\t\t// Flush buffered output for this unit, if the writer supports it.\n\t\tif err := component.FlushOutput(runner.Unit, opts.Writers.Writer); err != nil {\n\t\t\tl.Errorf(\"Error flushing output for unit %s: %v\", runner.Unit.Path(), err)\n\t\t}\n\t}()\n\n\t// Only create report entries if report is not nil\n\tif r != nil {\n\t\tunitPath := runner.Unit.Path()\n\t\tunitPath = filepath.Clean(unitPath)\n\n\t\t// Pass the discovery context fields for worktree scenarios\n\t\tvar ensureOpts []report.EndOption\n\n\t\tif discoveryCtx := runner.Unit.DiscoveryContext(); discoveryCtx != nil {\n\t\t\tensureOpts = append(\n\t\t\t\tensureOpts,\n\t\t\t\treport.WithDiscoveryWorkingDir(discoveryCtx.WorkingDir),\n\t\t\t\treport.WithRef(discoveryCtx.Ref),\n\t\t\t\treport.WithCmd(discoveryCtx.Cmd),\n\t\t\t\treport.WithArgs(discoveryCtx.Args),\n\t\t\t)\n\t\t}\n\n\t\tif _, err := r.EnsureRun(l, unitPath, ensureOpts...); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Use a unit-scoped detailed exit code so retries in this unit don't clobber global state\n\tglobalExitCode := tf.DetailedExitCodeFromContext(ctx)\n\n\tunitExitCode := tf.NewDetailedExitCodeMap()\n\n\tctx = tf.ContextWithDetailedExitCode(ctx, unitExitCode)\n\n\trunErr := run.Run(ctx, l, configbridge.NewRunOptions(opts), r, cfg, credsGetter)\n\n\t// Store the unit exit code in the global map using the unit path as key.\n\tif globalExitCode != nil {\n\t\tunitPath := runner.Unit.Path()\n\t\tcode := unitExitCode.Get(unitPath)\n\t\tglobalExitCode.Set(unitPath, code)\n\t}\n\n\t// End the run with appropriate result (only if report is not nil)\n\tif r != nil {\n\t\tunitPath := runner.Unit.Path()\n\t\tunitPath = filepath.Clean(unitPath)\n\n\t\tif runErr != nil {\n\t\t\tif endErr := r.EndRun(\n\t\t\t\tl,\n\t\t\t\tunitPath,\n\t\t\t\treport.WithResult(report.ResultFailed),\n\t\t\t\treport.WithReason(report.ReasonRunError),\n\t\t\t\treport.WithCauseRunError(runErr.Error()),\n\t\t\t); endErr != nil {\n\t\t\t\tl.Errorf(\"Error ending run for unit %s: %v\", unitPath, endErr)\n\t\t\t}\n\t\t} else {\n\t\t\tif endErr := r.EndRun(\n\t\t\t\tl,\n\t\t\t\tunitPath,\n\t\t\t\treport.WithResult(report.ResultSucceeded),\n\t\t\t); endErr != nil {\n\t\t\t\tl.Errorf(\"Error ending run for unit %s: %v\", unitPath, endErr)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn runErr\n}\n\n// Run executes a component.Unit right now.\nfunc (runner *UnitRunner) Run(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tr *report.Report,\n\tcfg *runcfg.RunConfig,\n\tcredsGetter *creds.Getter,\n) error {\n\trunner.Status = Running\n\n\tif opts == nil {\n\t\treturn nil\n\t}\n\n\tif err := runner.runTerragrunt(ctx, l, opts, r, cfg, credsGetter); err != nil {\n\t\treturn err\n\t}\n\n\t// convert terragrunt output to json\n\tif runner.Unit.OutputJSONFile(opts.RootWorkingDir, opts.JSONOutputFolder) != \"\" {\n\t\tjsonLogger, jsonOptions, err := opts.CloneWithConfigPath(\n\t\t\tl,\n\t\t\topts.TerragruntConfigPath,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tstdout := bytes.Buffer{}\n\t\tjsonOptions.ForwardTFStdout = true\n\t\tjsonOptions.JSONLogFormat = false\n\t\tjsonOptions.Writers.Writer = &stdout\n\t\tjsonOptions.TerraformCommand = tf.CommandNameShow\n\t\tjsonOptions.TerraformCliArgs = iacargs.New(tf.CommandNameShow, \"-json\", runner.Unit.PlanFile(opts.RootWorkingDir, opts.OutputFolder, opts.JSONOutputFolder, opts.TerraformCommand))\n\n\t\t// Use an ad-hoc report to avoid polluting the main report\n\t\tadhocReport := report.NewReport()\n\t\tif err := run.Run(ctx, jsonLogger, configbridge.NewRunOptions(jsonOptions), adhocReport, cfg, credsGetter); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// save the json output to the file plan file\n\t\toutputFile := runner.Unit.OutputJSONFile(opts.RootWorkingDir, opts.JSONOutputFolder)\n\t\tjsonDir := filepath.Dir(outputFile)\n\n\t\tif err := os.MkdirAll(jsonDir, os.ModePerm); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := os.WriteFile(outputFile, stdout.Bytes(), os.ModePerm); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/runner/graph/graph.go",
    "content": "// Package graph implements the logic for running commands against the\n// graph of dependencies for the unit in the current working directory.\npackage graph\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/common\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runall\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/os/stdout\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nfunc Run(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\t// Get credentials BEFORE config parsing — sops_decrypt_file() and\n\t// get_aws_account_id() in locals need auth-provider credentials\n\t// available in opts.Env during HCL evaluation.\n\t// *Getter discarded: graph.Run only needs creds in opts.Env for initial config parse.\n\t// Per-unit creds are re-fetched in runnerpool task (intentional: each unit may have\n\t// different opts after clone).\n\tif _, err := creds.ObtainCredsForParsing(ctx, l, opts.AuthProviderCmd, opts.Env, configbridge.ShellRunOptsFromOpts(opts)); err != nil {\n\t\treturn err\n\t}\n\n\tctx, pctx := configbridge.NewParsingContext(ctx, l, opts)\n\n\tcfg, err := config.ReadTerragruntConfig(ctx, l, pctx, pctx.ParserOptions)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif cfg == nil {\n\t\treturn errors.New(\"terragrunt was not able to render the config as json because it received no config. This is almost certainly a bug in Terragrunt. Please open an issue on github.com/gruntwork-io/terragrunt with this message and the contents of your terragrunt.hcl\")\n\t}\n\t// consider root for graph identification passed destroy-graph-root argument\n\trootDir := opts.GraphRoot\n\n\t// if destroy-graph-root is empty, use git to find top level dir.\n\t// may cause issues if in the same repo exist unrelated modules which will generate errors when scanning.\n\tif rootDir == \"\" {\n\t\tgitRoot, gitRootErr := shell.GitTopLevelDir(ctx, l, opts.Env, opts.WorkingDir)\n\t\tif gitRootErr != nil {\n\t\t\treturn gitRootErr\n\t\t}\n\n\t\trootDir = gitRoot\n\t}\n\n\t// Clone options and set RootWorkingDir to rootDir so discovery starts from the graph root\n\t// This allows discovering all modules including dependents (modules that depend on the working dir)\n\tgraphOpts := opts.Clone()\n\tgraphOpts.RootWorkingDir = rootDir\n\n\trunnerOpts := make([]common.Option, 0, 1)\n\n\tr := report.NewReport().WithWorkingDir(opts.WorkingDir)\n\n\tif l.Formatter().DisabledColors() || stdout.IsRedirected() {\n\t\tr.WithDisableColor()\n\t}\n\n\tif opts.ReportFormat != \"\" {\n\t\tr.WithFormat(opts.ReportFormat)\n\t}\n\n\tif opts.SummaryPerUnit {\n\t\tr.WithShowUnitLevelSummary()\n\t}\n\n\t// Limit graph to the working directory and its dependents.\n\t// The prefix ellipsis means \"include dependents\"; target is included by default.\n\tpathExpr, err := filter.NewPathFilter(opts.WorkingDir)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create path filter for %s: %w\", opts.WorkingDir, err)\n\t}\n\n\tgraphExpr := filter.NewGraphExpression(pathExpr).WithDependents()\n\tgraphOpts.Filters = filter.Filters{filter.NewFilter(graphExpr, graphExpr.String())}\n\n\tif opts.ReportSchemaFile != \"\" {\n\t\tdefer r.WriteSchemaToFile(opts.ReportSchemaFile) //nolint:errcheck\n\t}\n\n\tif opts.ReportFile != \"\" {\n\t\tdefer r.WriteToFile(opts.ReportFile) //nolint:errcheck\n\t}\n\n\tif !opts.SummaryDisable {\n\t\tdefer func() {\n\t\t\tif err := r.WriteSummary(opts.Writers.Writer); err != nil {\n\t\t\t\tl.Warnf(\"Failed to write summary: %v\", err)\n\t\t\t}\n\t\t}()\n\t}\n\n\trnr, err := runner.NewStackRunner(ctx, l, graphOpts, runnerOpts...)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn runall.RunAllOnStack(ctx, l, graphOpts, rnr, r)\n}\n"
  },
  {
    "path": "internal/runner/run/context.go",
    "content": "package run\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cache\"\n)\n\ntype configKey byte\n\nconst (\n\tversionCacheContextKey configKey = iota\n\tversionCacheName                 = \"versionCache\"\n)\n\n// WithRunVersionCache initializes the version cache in the context for the run package.\nfunc WithRunVersionCache(ctx context.Context) context.Context {\n\tctx = context.WithValue(ctx, versionCacheContextKey, cache.NewCache[string](versionCacheName))\n\treturn ctx\n}\n\n// GetRunVersionCache retrieves the version cache from the context for the run package.\nfunc GetRunVersionCache(ctx context.Context) *cache.Cache[string] {\n\treturn cache.ContextCache[string](ctx, versionCacheContextKey)\n}\n"
  },
  {
    "path": "internal/runner/run/creds/getter.go",
    "content": "// Package creds provides a way to obtain credentials through different providers and set them to `opts.Env`.\npackage creds\n\nimport (\n\t\"context\"\n\t\"maps\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds/providers\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds/providers/externalcmd\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\ntype Getter struct {\n\tobtainedCreds map[string]*providers.Credentials\n}\n\nfunc NewGetter() *Getter {\n\treturn &Getter{\n\t\tobtainedCreds: make(map[string]*providers.Credentials),\n\t}\n}\n\n// ObtainAndUpdateEnvIfNecessary obtains credentials through different providers and sets them to the provided env map.\nfunc (getter *Getter) ObtainAndUpdateEnvIfNecessary(ctx context.Context, l log.Logger, env map[string]string, authProviders ...providers.Provider) error {\n\tfor _, provider := range authProviders {\n\t\tcreds, err := provider.GetCredentials(ctx, l)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif creds == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor providerName, prevCreds := range getter.obtainedCreds {\n\t\t\tif prevCreds.Name == creds.Name {\n\t\t\t\tl.Warnf(\"%s credentials obtained using %s are overwritten by credentials obtained using %s.\", creds.Name, providerName, provider.Name())\n\t\t\t}\n\t\t}\n\n\t\tgetter.obtainedCreds[provider.Name()] = creds\n\n\t\tmaps.Copy(env, creds.Envs)\n\t}\n\n\treturn nil\n}\n\n// ObtainCredsForParsing creates a new Getter, obtains external-command\n// credentials, and populates env before HCL parsing.\n// Use when sops_decrypt_file() or get_aws_account_id() may appear in locals.\n// See https://github.com/gruntwork-io/terragrunt/issues/5515\nfunc ObtainCredsForParsing(ctx context.Context, l log.Logger, authProviderCmd string, env map[string]string, shellOpts *shell.ShellOptions) (*Getter, error) {\n\tg := NewGetter()\n\tif err := g.ObtainAndUpdateEnvIfNecessary(ctx, l, env, externalcmd.NewProvider(l, authProviderCmd, shellOpts)); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn g, nil\n}\n"
  },
  {
    "path": "internal/runner/run/creds/providers/amazonsts/provider.go",
    "content": "// Package amazonsts provides a credentials provider that obtains credentials by making API requests to Amazon STS.\npackage amazonsts\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/gruntwork-io/terragrunt/internal/awshelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cache\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iam\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds/providers\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// Provider obtains credentials by making API requests to Amazon STS.\ntype Provider struct {\n\tenv         map[string]string\n\tiamRoleOpts iam.RoleOptions\n}\n\n// NewProvider returns a new Provider instance.\nfunc NewProvider(l log.Logger, iamRoleOpts iam.RoleOptions, env map[string]string) providers.Provider {\n\treturn &Provider{\n\t\tiamRoleOpts: iamRoleOpts,\n\t\tenv:         env,\n\t}\n}\n\n// Name implements providers.Name\nfunc (provider *Provider) Name() string {\n\treturn \"API calls to Amazon STS\"\n}\n\n// GetCredentials implements providers.GetCredentials\nfunc (provider *Provider) GetCredentials(ctx context.Context, l log.Logger) (*providers.Credentials, error) {\n\tiamRoleOpts := provider.iamRoleOpts\n\tif iamRoleOpts.RoleARN == \"\" {\n\t\treturn nil, nil\n\t}\n\n\tif cached, hit := credentialsCache.Get(ctx, iamRoleOpts.RoleARN); hit {\n\t\tl.Debugf(\"Using cached credentials for IAM role %s.\", iamRoleOpts.RoleARN)\n\t\treturn cached, nil\n\t}\n\n\tl.Debugf(\"Assuming IAM role %s with a session duration of %d seconds.\", iamRoleOpts.RoleARN, iamRoleOpts.AssumeRoleDuration)\n\n\tresp, err := awshelper.AssumeIamRole(ctx, iamRoleOpts, \"\", provider.env)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcreds := &providers.Credentials{\n\t\tName: providers.AWSCredentials,\n\t\tEnvs: map[string]string{\n\t\t\t\"AWS_ACCESS_KEY_ID\":     aws.ToString(resp.AccessKeyId),\n\t\t\t\"AWS_SECRET_ACCESS_KEY\": aws.ToString(resp.SecretAccessKey),\n\t\t\t\"AWS_SESSION_TOKEN\":     aws.ToString(resp.SessionToken),\n\t\t\t\"AWS_SECURITY_TOKEN\":    aws.ToString(resp.SessionToken),\n\t\t},\n\t}\n\n\tcredentialsCache.Put(ctx, iamRoleOpts.RoleARN, creds, time.Now().Add(time.Duration(iamRoleOpts.AssumeRoleDuration)*time.Second))\n\n\treturn creds, nil\n}\n\n// credentialsCache is a cache of credentials.\nvar credentialsCache = cache.NewExpiringCache[*providers.Credentials](\"credentialsCache\")\n"
  },
  {
    "path": "internal/runner/run/creds/providers/externalcmd/provider.go",
    "content": "// Package externalcmd provides a provider that runs an external command that returns a json string with credentials.\npackage externalcmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"maps\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iam\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds/providers\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds/providers/amazonsts\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/mattn/go-shellwords\"\n)\n\n// Provider runs external command that returns a json string with credentials.\ntype Provider struct {\n\trunOpts         *shell.ShellOptions\n\tauthProviderCmd string\n}\n\n// NewProvider returns a new Provider instance.\nfunc NewProvider(l log.Logger, authProviderCmd string, runOpts *shell.ShellOptions) providers.Provider {\n\treturn &Provider{\n\t\tauthProviderCmd: authProviderCmd,\n\t\trunOpts:         runOpts,\n\t}\n}\n\n// Name implements providers.Name\nfunc (provider *Provider) Name() string {\n\treturn fmt.Sprintf(\"external %s command\", provider.authProviderCmd)\n}\n\n// GetCredentials implements providers.GetCredentials\nfunc (provider *Provider) GetCredentials(ctx context.Context, l log.Logger) (*providers.Credentials, error) {\n\tif provider.authProviderCmd == \"\" {\n\t\treturn nil, nil\n\t}\n\n\tparser := shellwords.NewParser()\n\n\t// Normalize Windows paths before parsing - shellwords treats backslashes as escape characters\n\tparts, err := parser.Parse(filepath.ToSlash(provider.authProviderCmd))\n\tif err != nil {\n\t\treturn nil, errors.Errorf(\"failed to parse auth provider command: %w\", err)\n\t}\n\n\tcommand := parts[0]\n\n\targs := []string{}\n\tif len(parts) > 1 {\n\t\targs = parts[1:]\n\t}\n\n\toutput, err := shell.RunCommandWithOutput(ctx, l, provider.runOpts, \"\", true, false, command, args...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif output.Stdout.String() == \"\" {\n\t\treturn nil, errors.Errorf(\n\t\t\t\"command %s completed successfully, but the response does not contain JSON string\",\n\t\t\tprovider.authProviderCmd,\n\t\t)\n\t}\n\n\tresp := &Response{Envs: make(map[string]string)}\n\n\tif err := json.Unmarshal(output.Stdout.Bytes(), &resp); err != nil {\n\t\treturn nil, errors.Errorf(\"command %s returned a response with invalid JSON format\", command)\n\t}\n\n\tcreds := &providers.Credentials{\n\t\tName: providers.AWSCredentials,\n\t\tEnvs: resp.Envs,\n\t}\n\n\tif resp.AWSCredentials != nil {\n\t\tif envs := resp.AWSCredentials.Envs(ctx, l, provider.authProviderCmd); envs != nil {\n\t\t\tl.Debugf(\"Obtaining AWS credentials from the %s.\", provider.Name())\n\t\t\tmaps.Copy(creds.Envs, envs)\n\t\t}\n\n\t\treturn creds, nil\n\t}\n\n\tif resp.AWSRole != nil {\n\t\tif envs := resp.AWSRole.Envs(ctx, l, provider.authProviderCmd); envs != nil {\n\t\t\tl.Debugf(\"Assuming AWS role %s using the %s.\", resp.AWSRole.RoleARN, provider.Name())\n\t\t\tmaps.Copy(creds.Envs, envs)\n\t\t}\n\n\t\treturn creds, nil\n\t}\n\n\treturn creds, nil\n}\n\n// Response is the JSON response expected from an auth provider command.\ntype Response struct {\n\t// AWSCredentials contains AWS credentials to set as environment variables.\n\tAWSCredentials *AWSCredentials `json:\"awsCredentials,omitempty\"`\n\t// AWSRole contains AWS role information for role assumption.\n\tAWSRole *AWSRole `json:\"awsRole,omitempty\"`\n\t// Envs contains additional environment variables to set.\n\tEnvs map[string]string `json:\"envs,omitempty\"`\n}\n\n// AWSCredentials is the JSON schema for direct AWS credentials.\ntype AWSCredentials struct {\n\t// AccessKeyID is the AWS access key ID.\n\tAccessKeyID string `json:\"ACCESS_KEY_ID\" jsonschema:\"required\"`\n\t// SecretAccessKey is the AWS secret access key.\n\tSecretAccessKey string `json:\"SECRET_ACCESS_KEY\" jsonschema:\"required\"`\n\t// SessionToken is the AWS session token (optional).\n\tSessionToken string `json:\"SESSION_TOKEN,omitempty\"`\n}\n\n// AWSRole is the JSON schema for AWS role assumption.\ntype AWSRole struct {\n\t// RoleARN is the ARN of the IAM role to assume.\n\tRoleARN string `json:\"roleARN\" jsonschema:\"required\"`\n\t// RoleSessionName is the session name for the assumed role.\n\tRoleSessionName string `json:\"roleSessionName,omitempty\"`\n\t// WebIdentityToken is the web identity token for OIDC-based role assumption.\n\tWebIdentityToken string `json:\"webIdentityToken,omitempty\"`\n\t// Duration is the duration in seconds for the assumed role session.\n\tDuration int64 `json:\"duration,omitempty\" jsonschema:\"minimum=0\"`\n}\n\nfunc (role *AWSRole) Envs(ctx context.Context, l log.Logger, authProviderCmd string) map[string]string {\n\tif role.RoleARN == \"\" {\n\t\tl.Warnf(\"The command %s completed successfully, but AWS role assumption contains empty required value: roleARN, nothing is being done.\", authProviderCmd)\n\t\treturn nil\n\t}\n\n\tsessionName := role.RoleSessionName\n\tif sessionName == \"\" {\n\t\tsessionName = iam.GetDefaultAssumeRoleSessionName()\n\t}\n\n\tduration := role.Duration\n\tif duration == 0 {\n\t\tduration = iam.DefaultAssumeRoleDuration\n\t}\n\n\tiamRoleOpts := iam.RoleOptions{\n\t\tRoleARN:               role.RoleARN,\n\t\tAssumeRoleDuration:    duration,\n\t\tAssumeRoleSessionName: sessionName,\n\t}\n\n\tif role.WebIdentityToken != \"\" {\n\t\tiamRoleOpts.WebIdentityToken = role.WebIdentityToken\n\t}\n\n\tprovider := amazonsts.NewProvider(l, iamRoleOpts, nil)\n\n\tcreds, err := provider.GetCredentials(ctx, l)\n\tif err != nil {\n\t\tl.Warnf(\"Failed to assume role %s: %v\", role.RoleARN, err)\n\t\treturn nil\n\t}\n\n\tif creds == nil {\n\t\tl.Warnf(\"The command %s completed successfully, but failed to assume role %s, nothing is being done.\", authProviderCmd, role.RoleARN)\n\t\treturn nil\n\t}\n\n\tenvs := map[string]string{\n\t\t\"AWS_ACCESS_KEY_ID\":     creds.Envs[\"AWS_ACCESS_KEY_ID\"],\n\t\t\"AWS_SECRET_ACCESS_KEY\": creds.Envs[\"AWS_SECRET_ACCESS_KEY\"],\n\t\t\"AWS_SESSION_TOKEN\":     creds.Envs[\"AWS_SESSION_TOKEN\"],\n\t\t\"AWS_SECURITY_TOKEN\":    creds.Envs[\"AWS_SESSION_TOKEN\"],\n\t}\n\n\treturn envs\n}\n\nfunc (creds *AWSCredentials) Envs(_ context.Context, l log.Logger, authProviderCmd string) map[string]string {\n\tvar emptyFields []string\n\n\tif creds.AccessKeyID == \"\" {\n\t\temptyFields = append(emptyFields, \"ACCESS_KEY_ID\")\n\t}\n\n\tif creds.SecretAccessKey == \"\" {\n\t\temptyFields = append(emptyFields, \"SECRET_ACCESS_KEY\")\n\t}\n\n\tif len(emptyFields) > 0 {\n\t\tl.Warnf(\"The command %s completed successfully, but AWS credentials contains empty required values: %s, nothing is being done.\", authProviderCmd, strings.Join(emptyFields, \", \"))\n\t\treturn nil\n\t}\n\n\tenvs := map[string]string{\n\t\t\"AWS_ACCESS_KEY_ID\":     creds.AccessKeyID,\n\t\t\"AWS_SECRET_ACCESS_KEY\": creds.SecretAccessKey,\n\t\t\"AWS_SESSION_TOKEN\":     creds.SessionToken,\n\t\t\"AWS_SECURITY_TOKEN\":    creds.SessionToken,\n\t}\n\n\treturn envs\n}\n"
  },
  {
    "path": "internal/runner/run/creds/providers/externalcmd/schema.go",
    "content": "package externalcmd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/invopop/jsonschema\"\n\t\"github.com/xeipuuv/gojsonschema\"\n)\n\n// SchemaValidationError represents a schema validation error with details.\ntype SchemaValidationError struct {\n\tErrors []string\n}\n\nfunc (e *SchemaValidationError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"Auth provider command response schema validation failed with %d error(s): %v\",\n\t\tlen(e.Errors),\n\t\te.Errors,\n\t)\n}\n\n// ValidateResponse validates a JSON response against the auth provider command schema.\n// Returns nil if valid, or a SchemaValidationError with details if invalid.\nfunc ValidateResponse(data []byte) error {\n\tschemaBytes, err := json.Marshal(generateResponseSchema())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate schema: %w\", err)\n\t}\n\n\tschemaLoader := gojsonschema.NewBytesLoader(schemaBytes)\n\tdocumentLoader := gojsonschema.NewBytesLoader(data)\n\n\tresult, err := gojsonschema.Validate(schemaLoader, documentLoader)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to validate response: %w\", err)\n\t}\n\n\tif !result.Valid() {\n\t\terrors := make([]string, len(result.Errors()))\n\t\tfor i, validationErr := range result.Errors() {\n\t\t\terrors[i] = validationErr.String()\n\t\t}\n\n\t\treturn &SchemaValidationError{Errors: errors}\n\t}\n\n\treturn nil\n}\n\n// generateResponseSchema generates the JSON schema for auth provider command response validation.\nfunc generateResponseSchema() *jsonschema.Schema {\n\treflector := jsonschema.Reflector{\n\t\tDoNotReference: true,\n\t}\n\n\tschema := reflector.Reflect(&Response{})\n\tschema.Description = \"Schema for the JSON response expected from an auth provider command\"\n\tschema.Title = \"Terragrunt Auth Provider Command Response Schema\"\n\tschema.ID = \"https://docs.terragrunt.com/schemas/auth-provider-cmd/v2/schema.json\"\n\n\treturn schema\n}\n"
  },
  {
    "path": "internal/runner/run/creds/providers/externalcmd/schema_test.go",
    "content": "package externalcmd_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds/providers/externalcmd\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestValidateResponse(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname        string\n\t\tinput       string\n\t\texpectError bool\n\t\terrorCount  int\n\t}{\n\t\t{\n\t\t\tname:        \"empty object is valid\",\n\t\t\tinput:       `{}`,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid awsCredentials\",\n\t\t\tinput: `{\n\t\t\t\t\"awsCredentials\": {\n\t\t\t\t\t\"ACCESS_KEY_ID\": \"fake-access-key-id\",\n\t\t\t\t\t\"SECRET_ACCESS_KEY\": \"fake-secret-access-key\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid awsCredentials with session token\",\n\t\t\tinput: `{\n\t\t\t\t\"awsCredentials\": {\n\t\t\t\t\t\"ACCESS_KEY_ID\": \"fake-access-key-id\",\n\t\t\t\t\t\"SECRET_ACCESS_KEY\": \"fake-secret-access-key\",\n\t\t\t\t\t\"SESSION_TOKEN\": \"fake-session-token\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid awsRole\",\n\t\t\tinput: `{\n\t\t\t\t\"awsRole\": {\n\t\t\t\t\t\"roleARN\": \"arn:aws:iam::123456789012:role/MyRole\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid awsRole with all fields\",\n\t\t\tinput: `{\n\t\t\t\t\"awsRole\": {\n\t\t\t\t\t\"roleARN\": \"arn:aws:iam::123456789012:role/MyRole\",\n\t\t\t\t\t\"roleSessionName\": \"my-session\",\n\t\t\t\t\t\"duration\": 3600,\n\t\t\t\t\t\"webIdentityToken\": \"fake-web-identity-token\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid envs\",\n\t\t\tinput: `{\n\t\t\t\t\"envs\": {\n\t\t\t\t\t\"MY_VAR\": \"my-value\",\n\t\t\t\t\t\"ANOTHER_VAR\": \"another-value\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"valid combined response\",\n\t\t\tinput: `{\n\t\t\t\t\"awsCredentials\": {\n\t\t\t\t\t\"ACCESS_KEY_ID\": \"fake-access-key-id\",\n\t\t\t\t\t\"SECRET_ACCESS_KEY\": \"fake-secret-access-key\"\n\t\t\t\t},\n\t\t\t\t\"envs\": {\n\t\t\t\t\t\"CUSTOM_VAR\": \"custom-value\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid awsCredentials missing ACCESS_KEY_ID\",\n\t\t\tinput: `{\n\t\t\t\t\"awsCredentials\": {\n\t\t\t\t\t\"SECRET_ACCESS_KEY\": \"fake-secret-access-key\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectError: true,\n\t\t\terrorCount:  1,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid awsCredentials missing SECRET_ACCESS_KEY\",\n\t\t\tinput: `{\n\t\t\t\t\"awsCredentials\": {\n\t\t\t\t\t\"ACCESS_KEY_ID\": \"fake-access-key-id\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectError: true,\n\t\t\terrorCount:  1,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid awsRole missing roleARN\",\n\t\t\tinput: `{\n\t\t\t\t\"awsRole\": {\n\t\t\t\t\t\"roleSessionName\": \"my-session\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectError: true,\n\t\t\terrorCount:  1,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid additional property at root\",\n\t\t\tinput: `{\n\t\t\t\t\"unknownField\": \"value\"\n\t\t\t}`,\n\t\t\texpectError: true,\n\t\t\terrorCount:  1,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid additional property in awsCredentials\",\n\t\t\tinput: `{\n\t\t\t\t\"awsCredentials\": {\n\t\t\t\t\t\"ACCESS_KEY_ID\": \"fake-access-key-id\",\n\t\t\t\t\t\"SECRET_ACCESS_KEY\": \"fake-secret-access-key\",\n\t\t\t\t\t\"unknownField\": \"value\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectError: true,\n\t\t\terrorCount:  1,\n\t\t},\n\t\t{\n\t\t\tname: \"invalid duration negative\",\n\t\t\tinput: `{\n\t\t\t\t\"awsRole\": {\n\t\t\t\t\t\"roleARN\": \"arn:aws:iam::123456789012:role/MyRole\",\n\t\t\t\t\t\"duration\": -1\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectError: true,\n\t\t\terrorCount:  1,\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid json\",\n\t\t\tinput:       `{invalid`,\n\t\t\texpectError: true,\n\t\t},\n\t\t{\n\t\t\tname: \"awsCredentials with envs\",\n\t\t\tinput: `{\n\t\t\t\t\"awsCredentials\": {\n\t\t\t\t\t\"ACCESS_KEY_ID\": \"fake-access-key-id\",\n\t\t\t\t\t\"SECRET_ACCESS_KEY\": \"fake-secret-access-key\",\n\t\t\t\t\t\"SESSION_TOKEN\": \"session-token-value\"\n\t\t\t\t},\n\t\t\t\t\"envs\": {\n\t\t\t\t\t\"TF_VAR_foo\": \"bar\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"awsRole with webIdentityToken\",\n\t\t\tinput: `{\n\t\t\t\t\"awsRole\": {\n\t\t\t\t\t\"roleARN\": \"arn:aws:iam::123456789012:role/OIDCRole\",\n\t\t\t\t\t\"webIdentityToken\": \"fake-web-identity-token\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname: \"envs only\",\n\t\t\tinput: `{\n\t\t\t\t\"envs\": {\n\t\t\t\t\t\"AWS_ACCESS_KEY_ID\": \"fake-access-key\",\n\t\t\t\t\t\"AWS_SECRET_ACCESS_KEY\": \"fake-secret-key\",\n\t\t\t\t\t\"AWS_SESSION_TOKEN\": \"fake-session-token\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectError: false,\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\terr := externalcmd.ValidateResponse([]byte(tc.input))\n\n\t\t\tif !tc.expectError {\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.Error(t, err)\n\n\t\t\tif tc.errorCount > 0 {\n\t\t\t\tvar schemaErr *externalcmd.SchemaValidationError\n\n\t\t\t\trequire.ErrorAs(t, err, &schemaErr)\n\n\t\t\t\tassert.Len(t, schemaErr.Errors, tc.errorCount)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSchemaValidationError_Error(t *testing.T) {\n\tt.Parallel()\n\n\terr := &externalcmd.SchemaValidationError{\n\t\tErrors: []string{\"error1\", \"error2\"},\n\t}\n\n\tassert.Contains(t, err.Error(), \"2 error(s)\")\n\tassert.Contains(t, err.Error(), \"error1\")\n\tassert.Contains(t, err.Error(), \"error2\")\n}\n\n// TestValidateResponse_NoSensitiveDataInErrors verifies that validation error messages\n// do not leak sensitive credential values to users.\nfunc TestValidateResponse_NoSensitiveDataInErrors(t *testing.T) {\n\tt.Parallel()\n\n\t// These are fake sensitive values that should NEVER appear in error messages\n\tsensitiveValues := []string{\n\t\t\"fake-access-key-id\",\n\t\t\"fake-secret-key\",\n\t\t\"fake-session-token\",\n\t\t\"fake-web-identity-token\",\n\t\t\"super-secret-env-value-12345\",\n\t}\n\n\ttests := []struct {\n\t\tname  string\n\t\tinput string\n\t}{\n\t\t{\n\t\t\tname: \"wrong type for ACCESS_KEY_ID should not leak value\",\n\t\t\tinput: `{\n\t\t\t\t\"awsCredentials\": {\n\t\t\t\t\t\"ACCESS_KEY_ID\": {\"nested\": \"fake-access-key-id\"},\n\t\t\t\t\t\"SECRET_ACCESS_KEY\": \"fake-secret-key\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t},\n\t\t{\n\t\t\tname: \"wrong type for SECRET_ACCESS_KEY should not leak value\",\n\t\t\tinput: `{\n\t\t\t\t\"awsCredentials\": {\n\t\t\t\t\t\"ACCESS_KEY_ID\": \"fake-access-key-id\",\n\t\t\t\t\t\"SECRET_ACCESS_KEY\": [\"fake-secret-key\"]\n\t\t\t\t}\n\t\t\t}`,\n\t\t},\n\t\t{\n\t\t\tname: \"malformed SECRET_ACCESS_KEY should not leak value\",\n\t\t\tinput: `{\n\t\t\t\t\"awsCredentials\": {\n\t\t\t\t\t\"ACCESS_KEY_ID\": \"fake-access-key-id\",\n\t\t\t\t\t\"SECRET_ACCESS_KEY\": [\"fake-secret-key\n\t\t\t\t}\n\t\t\t}`,\n\t\t},\n\t\t{\n\t\t\tname: \"wrong type for SESSION_TOKEN should not leak value\",\n\t\t\tinput: `{\n\t\t\t\t\"awsCredentials\": {\n\t\t\t\t\t\"ACCESS_KEY_ID\": \"fake-access-key-id\",\n\t\t\t\t\t\"SECRET_ACCESS_KEY\": \"fake-secret-key\",\n\t\t\t\t\t\"SESSION_TOKEN\": {\"token\": \"fake-session-token\"}\n\t\t\t\t}\n\t\t\t}`,\n\t\t},\n\t\t{\n\t\t\tname: \"wrong type for webIdentityToken should not leak value\",\n\t\t\tinput: `{\n\t\t\t\t\"awsRole\": {\n\t\t\t\t\t\"roleARN\": \"arn:aws:iam::123456789012:role/MyRole\",\n\t\t\t\t\t\"webIdentityToken\": [\"fake-web-identity-token\"]\n\t\t\t\t}\n\t\t\t}`,\n\t\t},\n\t\t{\n\t\t\tname: \"additional property with sensitive value should not leak\",\n\t\t\tinput: `{\n\t\t\t\t\"awsCredentials\": {\n\t\t\t\t\t\"ACCESS_KEY_ID\": \"fake-access-key-id\",\n\t\t\t\t\t\"SECRET_ACCESS_KEY\": \"fake-secret-key\",\n\t\t\t\t\t\"SUPER_SECRET_FIELD\": \"super-secret-env-value-12345\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t},\n\t\t{\n\t\t\tname: \"additional property at root with sensitive value should not leak\",\n\t\t\tinput: `{\n\t\t\t\t\"secretField\": \"super-secret-env-value-12345\"\n\t\t\t}`,\n\t\t},\n\t\t{\n\t\t\tname: \"wrong type for envs value should not leak\",\n\t\t\tinput: `{\n\t\t\t\t\"envs\": {\n\t\t\t\t\t\"SECRET_VAR\": {\"secret\": \"super-secret-env-value-12345\"}\n\t\t\t\t}\n\t\t\t}`,\n\t\t},\n\t\t{\n\t\t\tname: \"wrong type for duration with credentials present should not leak credentials\",\n\t\t\tinput: `{\n\t\t\t\t\"awsRole\": {\n\t\t\t\t\t\"roleARN\": \"arn:aws:iam::123456789012:role/MyRole\",\n\t\t\t\t\t\"webIdentityToken\": \"fake-web-identity-token\",\n\t\t\t\t\t\"duration\": \"not-a-number\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\terr := externalcmd.ValidateResponse([]byte(tc.input))\n\t\t\trequire.Error(t, err, \"expected validation error\")\n\n\t\t\terrMsg := err.Error()\n\n\t\t\tfor _, sensitive := range sensitiveValues {\n\t\t\t\tassert.NotContains(t, errMsg, sensitive,\n\t\t\t\t\t\"error message should not contain sensitive value\")\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestValidateResponse_ErrorMessagesAreDescriptive verifies that error messages\n// provide useful information about what went wrong without exposing values.\nfunc TestValidateResponse_ErrorMessagesAreDescriptive(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname            string\n\t\tinput           string\n\t\texpectedPhrases []string\n\t}{\n\t\t{\n\t\t\tname: \"missing required field mentions field path\",\n\t\t\tinput: `{\n\t\t\t\t\"awsCredentials\": {\n\t\t\t\t\t\"SECRET_ACCESS_KEY\": \"secret\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectedPhrases: []string{\"ACCESS_KEY_ID\"},\n\t\t},\n\t\t{\n\t\t\tname: \"type error mentions expected type\",\n\t\t\tinput: `{\n\t\t\t\t\"awsRole\": {\n\t\t\t\t\t\"roleARN\": \"arn:aws:iam::123456789012:role/MyRole\",\n\t\t\t\t\t\"duration\": \"not-a-number\"\n\t\t\t\t}\n\t\t\t}`,\n\t\t\texpectedPhrases: []string{\"duration\"},\n\t\t},\n\t\t{\n\t\t\tname: \"additional property error mentions the property name but not value\",\n\t\t\tinput: `{\n\t\t\t\t\"unknownField\": \"secret-value\"\n\t\t\t}`,\n\t\t\texpectedPhrases: []string{\"unknownField\"},\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\terr := externalcmd.ValidateResponse([]byte(tc.input))\n\t\t\trequire.Error(t, err)\n\n\t\t\terrMsg := err.Error()\n\n\t\t\tfor _, phrase := range tc.expectedPhrases {\n\t\t\t\tassert.Contains(\n\t\t\t\t\tt,\n\t\t\t\t\terrMsg,\n\t\t\t\t\tphrase,\n\t\t\t\t\t\"error message should mention the field/property name for debugging\",\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/runner/run/creds/providers/provider.go",
    "content": "// Package providers defines the interface for a provider.\npackage providers\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst (\n\tAWSCredentials CredentialsName = \"AWS\"\n)\n\ntype CredentialsName string\n\ntype Credentials struct {\n\tEnvs map[string]string\n\tName CredentialsName\n}\n\ntype Provider interface {\n\t// Name returns the name of the provider.\n\tName() string\n\t// GetCredentials returns a set of credentials.\n\tGetCredentials(ctx context.Context, l log.Logger) (*Credentials, error)\n}\n"
  },
  {
    "path": "internal/runner/run/debug.go",
    "content": "package run\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runcfg\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst TerragruntTFVarsFile = \"terragrunt-debug.tfvars.json\"\n\nconst defaultPermissions = int(0600)\n\n// WriteTerragruntDebugFile will create a tfvars file that can be used to invoke the tofu/terraform module in the same way\n// that terragrunt invokes the module, so that users can debug issues with the terragrunt config.\nfunc WriteTerragruntDebugFile(l log.Logger, opts *Options, cfg *runcfg.RunConfig) error {\n\tl.Infof(\n\t\t\"Debug mode requested: generating debug file %s in working dir %s\",\n\t\tTerragruntTFVarsFile,\n\t\topts.WorkingDir,\n\t)\n\n\trequired, optional, err := tf.ModuleVariables(opts.WorkingDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvariables := slices.Concat(required, optional)\n\n\ttofuImpl := \"tofu\"\n\tif opts.TofuImplementation != \"\" {\n\t\ttofuImpl = string(opts.TofuImplementation)\n\t}\n\n\tl.Debugf(\"The following variables were detected in the %s module:\", tofuImpl)\n\tl.Debugf(\"%v\", variables)\n\n\tfileContents, err := terragruntDebugFileContents(l, opts, cfg, variables)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tconfigFolder := filepath.Dir(opts.TerragruntConfigPath)\n\n\tfileName := filepath.Join(configFolder, TerragruntTFVarsFile)\n\tif err := os.WriteFile(fileName, fileContents, os.FileMode(defaultPermissions)); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tl.Debugf(\"Variables passed to %s are located in \\\"%s\\\"\", tofuImpl, fileName)\n\tl.Debugf(\"Run this command to replicate how %s was invoked:\", tofuImpl)\n\tl.Debugf(\n\t\t\"\\t%s -chdir=\\\"%s\\\" %s -var-file=\\\"%s\\\" \",\n\t\ttofuImpl,\n\t\topts.WorkingDir,\n\t\tstrings.Join(opts.TerraformCliArgs.Slice(), \" \"),\n\t\tfileName,\n\t)\n\n\treturn nil\n}\n\n// terragruntDebugFileContents will return a tfvars file in json format of all the terragrunt rendered variables values\n// that should be set to invoke the tofu/terraform module in the same way as terragrunt. Note that this will only include the\n// values of variables that are actually defined in the module.\nfunc terragruntDebugFileContents(\n\tl log.Logger,\n\topts *Options,\n\tcfg *runcfg.RunConfig,\n\tmoduleVariables []string,\n) ([]byte, error) {\n\tenvVars := map[string]string{}\n\tif opts.Env != nil {\n\t\tenvVars = opts.Env\n\t}\n\n\tjsonValuesByKey := make(map[string]any)\n\n\tfor varName, varValue := range cfg.Inputs {\n\t\tnameAsEnvVar := fmt.Sprintf(tf.EnvNameTFVarFmt, varName)\n\t\t_, varIsInEnv := envVars[nameAsEnvVar]\n\t\tvarIsDefined := slices.Contains(moduleVariables, varName)\n\n\t\t// Only add to the file if the explicit env var does NOT exist and the variable is defined in the module.\n\t\t// We must do this in order to avoid overriding the env var when the user follows up with a direct invocation to\n\t\t// tofu/terraform using this file (due to the order in which tofu/terraform resolves config sources).\n\t\tswitch {\n\t\tcase !varIsInEnv && varIsDefined:\n\t\t\tjsonValuesByKey[varName] = varValue\n\t\tcase varIsInEnv:\n\t\t\tl.Debugf(\n\t\t\t\t\"WARN: The variable %s was omitted from the debug file because the env var %s is already set.\",\n\t\t\t\tvarName, nameAsEnvVar,\n\t\t\t)\n\t\tcase !varIsDefined:\n\t\t\tl.Debugf(\n\t\t\t\t\"WARN: The variable %s was omitted because it is not defined in the OpenTofu/Terraform module.\",\n\t\t\t\tvarName,\n\t\t\t)\n\t\t}\n\t}\n\n\tjsonContent, err := json.MarshalIndent(jsonValuesByKey, \"\", \"  \")\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\treturn jsonContent, nil\n}\n"
  },
  {
    "path": "internal/runner/run/download_source.go",
    "content": "package run\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/go-getter\"\n\tgetterv2 \"github.com/hashicorp/go-getter/v2\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cas\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runcfg\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// ModuleManifestName is the manifest for files copied from terragrunt module folder (i.e., the folder that contains the current terragrunt.hcl).\nconst (\n\tModuleManifestName = \".terragrunt-module-manifest\"\n\n\t// ModuleInitRequiredFile is a file to indicate that init should be executed.\n\tModuleInitRequiredFile = \".terragrunt-init-required\"\n\n\ttfLintConfig = \".tflint.hcl\"\n\n\tfileURIScheme = \"file://\"\n)\n\n// DownloadTerraformSource downloads the given source URL, which should use Terraform's module source syntax,\n// into a temporary folder, then:\n// 1. Check if module directory exists in temporary folder\n// 2. Copy the contents of opts.WorkingDir into the temporary folder.\n// 3. Set opts.WorkingDir to the temporary folder.\n//\n// See the NewTerraformSource method for how we determine the temporary folder so we can reuse it across multiple\n// runs of Terragrunt to avoid downloading everything from scratch every time.\nfunc DownloadTerraformSource(\n\tctx context.Context,\n\tl log.Logger,\n\tsource string,\n\topts *Options,\n\tcfg *runcfg.RunConfig,\n\tr *report.Report,\n) (*Options, error) {\n\twalkWithSymlinks := opts.Experiments.Evaluate(experiment.Symlinks)\n\n\tterraformSource, err := tf.NewSource(l, source, opts.DownloadDir, opts.WorkingDir, walkWithSymlinks)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = DownloadTerraformSourceIfNecessary(ctx, l, terraformSource, opts, cfg, r); err != nil {\n\t\treturn nil, err\n\t}\n\n\tl.Debugf(\n\t\t\"Copying files from %s into %s\",\n\t\tutil.RelPathForLog(opts.WorkingDir, opts.WorkingDir, opts.Writers.LogShowAbsPaths),\n\t\tutil.RelPathForLog(opts.RootWorkingDir, terraformSource.WorkingDir, opts.Writers.LogShowAbsPaths),\n\t)\n\n\t// Always include the .tflint.hcl file, if it exists\n\tincludeInCopy := slices.Concat(cfg.Terraform.IncludeInCopy, []string{tfLintConfig})\n\n\terr = util.CopyFolderContents(\n\t\tl,\n\t\topts.WorkingDir,\n\t\tterraformSource.WorkingDir,\n\t\tModuleManifestName,\n\t\tincludeInCopy,\n\t\tcfg.Terraform.ExcludeFromCopy,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tl, updatedOpts, err := opts.CloneWithConfigPath(l, opts.TerragruntConfigPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tl.Debugf(\n\t\t\"Setting working directory to %s\",\n\t\tutil.RelPathForLog(\n\t\t\topts.RootWorkingDir,\n\t\t\tterraformSource.WorkingDir,\n\t\t\topts.Writers.LogShowAbsPaths,\n\t\t),\n\t)\n\tupdatedOpts.WorkingDir = terraformSource.WorkingDir\n\n\treturn updatedOpts, nil\n}\n\n// DownloadTerraformSourceIfNecessary downloads the specified TerraformSource if the latest code hasn't already been downloaded.\nfunc DownloadTerraformSourceIfNecessary(\n\tctx context.Context,\n\tl log.Logger,\n\tterraformSource *tf.Source,\n\topts *Options,\n\tcfg *runcfg.RunConfig,\n\tr *report.Report,\n) error {\n\tif opts.SourceUpdate {\n\t\tl.Debugf(\"The --source-update flag is set, so deleting the temporary folder %s before downloading source.\", terraformSource.DownloadDir)\n\n\t\tif err := os.RemoveAll(terraformSource.DownloadDir); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\t} else {\n\t\talreadyLatest, err := AlreadyHaveLatestCode(l, terraformSource, opts)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif alreadyLatest {\n\t\t\tif err := ValidateWorkingDir(terraformSource); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tl.Debugf(\n\t\t\t\t\"%s files in %s are up to date. Will not download again.\",\n\t\t\t\topts.TofuImplementation,\n\t\t\t\tutil.RelPathForLog(\n\t\t\t\t\topts.RootWorkingDir,\n\t\t\t\t\tterraformSource.WorkingDir,\n\t\t\t\t\topts.Writers.LogShowAbsPaths,\n\t\t\t\t),\n\t\t\t)\n\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tvar previousVersion = \"\"\n\t// read previous source version\n\t// https://github.com/gruntwork-io/terragrunt/issues/1921\n\tif util.FileExists(terraformSource.VersionFile) {\n\t\tvar err error\n\n\t\tpreviousVersion, err = readVersionFile(terraformSource)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// When downloading source, we need to process any hooks waiting on `init-from-module`. Therefore, we clone the\n\t// options struct, set the command to the value the hooks are expecting, and run the download action surrounded by\n\t// before and after hooks (if any).\n\tl, optsForDownload, err := opts.CloneWithConfigPath(l, opts.TerragruntConfigPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\toptsForDownload.TerraformCommand = tf.CommandNameInitFromModule\n\n\tdownloadErr := RunActionWithHooks(\n\t\tctx,\n\t\tl,\n\t\t\"download source\",\n\t\toptsForDownload,\n\t\tcfg,\n\t\tr,\n\t\tfunc(childCtx context.Context) error {\n\t\t\treturn downloadSource(childCtx, l, terraformSource, opts, cfg, r)\n\t\t},\n\t)\n\tif downloadErr != nil {\n\t\treturn DownloadingTerraformSourceErr{ErrMsg: downloadErr, URL: terraformSource.CanonicalSourceURL.String()}\n\t}\n\n\tif err := terraformSource.WriteVersionFile(l); err != nil {\n\t\treturn err\n\t}\n\n\tif err := ValidateWorkingDir(terraformSource); err != nil {\n\t\treturn err\n\t}\n\n\tcurrentVersion, err := terraformSource.EncodeSourceVersion(l)\n\t// if source versions are different or calculating version failed, create file to run init\n\t// https://github.com/gruntwork-io/terragrunt/issues/1921\n\tif (previousVersion != \"\" && previousVersion != currentVersion) || err != nil {\n\t\tl.Debugf(\"Requesting re-init, source version has changed from %s to %s recently.\", previousVersion, currentVersion)\n\n\t\tinitFile := filepath.Join(terraformSource.WorkingDir, ModuleInitRequiredFile)\n\n\t\tf, createErr := os.Create(initFile)\n\t\tif createErr != nil {\n\t\t\treturn createErr\n\t\t}\n\n\t\tdefer f.Close()\n\t}\n\n\treturn nil\n}\n\n// AlreadyHaveLatestCode returns true if the specified TerraformSource, of the exact same version, has already been downloaded into the\n// DownloadFolder. This helps avoid downloading the same code multiple times. Note that if the TerraformSource points\n// to a local file path, a hash will be generated from the contents of the source dir. See the ProcessTerraformSource method for more info.\nfunc AlreadyHaveLatestCode(l log.Logger, terraformSource *tf.Source, opts *Options) (bool, error) {\n\tif !util.FileExists(terraformSource.DownloadDir) ||\n\t\t!util.FileExists(terraformSource.WorkingDir) ||\n\t\t!util.FileExists(terraformSource.VersionFile) {\n\t\treturn false, nil\n\t}\n\n\thasFiles, err := util.DirContainsTFFiles(terraformSource.WorkingDir)\n\tif err != nil {\n\t\treturn false, errors.New(err)\n\t}\n\n\tif !hasFiles {\n\t\tl.Debugf(\"Working dir %s exists but contains no Terraform or OpenTofu files, so assuming code needs to be downloaded again.\", terraformSource.WorkingDir)\n\t\treturn false, nil\n\t}\n\n\tcurrentVersion, err := terraformSource.EncodeSourceVersion(l)\n\t// If we fail to calculate the source version (e.g. because walking the\n\t// directory tree failed) use a random version instead, bypassing the cache.\n\tif err != nil {\n\t\tcurrentVersion, err = util.GenerateRandomSha256()\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t}\n\n\tpreviousVersion, err := readVersionFile(terraformSource)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treturn previousVersion == currentVersion, nil\n}\n\n// Return the version number stored in the DownloadDir. This version number can be used to check if the Terraform code\n// that has already been downloaded is the same as the version the user is currently requesting. The version number is\n// calculated using the encodeSourceVersion method.\nfunc readVersionFile(terraformSource *tf.Source) (string, error) {\n\treturn util.ReadFileAsString(terraformSource.VersionFile)\n}\n\n// UpdateGetters returns the customized go-getter interfaces that Terragrunt relies on. Specifically:\n//   - Local file path getter is updated to copy the files instead of creating symlinks, which is what go-getter defaults\n//     to.\n//   - Include the customized getter for fetching sources from the Terraform Registry.\n//\n// This creates a closure that returns a function so that we have access to the terragrunt configuration, which is\n// necessary for customizing the behavior of the file getter.\nfunc UpdateGetters(l log.Logger, opts *Options, cfg *runcfg.RunConfig) func(*getter.Client) error {\n\treturn func(client *getter.Client) error {\n\t\t// We iterate over the global getter.Getters map and clone each getter\n\t\t// to avoid race conditions. The global map contains shared getter\n\t\t// instances, and when SetClient is called on them from multiple\n\t\t// goroutines, it causes data races. Cloning via dereference ensures\n\t\t// each client has its own getter state, while automatically picking\n\t\t// up any new getter types registered by go-getter.\n\t\tclient.Getters = make(map[string]getter.Getter, len(getter.Getters))\n\t\tfor name, g := range getter.Getters {\n\t\t\tv := reflect.ValueOf(g).Elem()\n\t\t\tclone := reflect.New(v.Type())\n\t\t\tclone.Elem().Set(v)\n\t\t\tclient.Getters[name] = clone.Interface().(getter.Getter)\n\t\t}\n\n\t\t// Override with Terragrunt-specific customizations\n\t\tclient.Getters[\"file\"] = &FileCopyGetter{\n\t\t\tLogger:          l,\n\t\t\tIncludeInCopy:   cfg.Terraform.IncludeInCopy,\n\t\t\tExcludeFromCopy: cfg.Terraform.ExcludeFromCopy,\n\t\t}\n\t\tclient.Getters[\"http\"] = &getter.HttpGetter{Netrc: true}\n\t\tclient.Getters[\"https\"] = &getter.HttpGetter{Netrc: true}\n\n\t\t// Load in custom getters that are only supported in Terragrunt\n\t\tclient.Getters[\"tfr\"] = &tf.RegistryGetter{\n\t\t\tTofuImplementation: opts.TofuImplementation,\n\t\t}\n\n\t\treturn nil\n\t}\n}\n\n// preserveSymlinksOption is a custom client option that ensures DisableSymlinks\n// setting is preserved during git operations\nfunc preserveSymlinksOption() getter.ClientOption {\n\treturn func(c *getter.Client) error {\n\t\t// Create a custom git getter that preserves symlink settings\n\t\tif c.Getters != nil {\n\t\t\tif _, exists := c.Getters[\"git\"]; exists {\n\t\t\t\t// Replace with a wrapper that preserves symlink settings.\n\t\t\t\t// We create a fresh GitGetter instance instead of wrapping the\n\t\t\t\t// existing one to avoid race conditions when multiple goroutines\n\t\t\t\t// share the same getter from the global getter.Getters map.\n\t\t\t\tc.Getters[\"git\"] = &symlinkPreservingGitGetter{\n\t\t\t\t\toriginal: &getter.GitGetter{},\n\t\t\t\t\tclient:   c,\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Ensure DisableSymlinks is set to false\n\t\tc.DisableSymlinks = false\n\n\t\treturn nil\n\t}\n}\n\n// Download the code from the Canonical Source URL into the Download Folder using the go-getter library\nfunc downloadSource(\n\tctx context.Context,\n\tl log.Logger,\n\tsrc *tf.Source,\n\topts *Options,\n\tcfg *runcfg.RunConfig,\n\tr *report.Report,\n) error {\n\tcanonicalSourceURL := src.CanonicalSourceURL.String()\n\n\t// Since we convert abs paths to rel in logs, `file://../../path/to/dir` doesn't look good,\n\t// so it's better to get rid of it.\n\tcanonicalSourceURL = strings.TrimPrefix(canonicalSourceURL, fileURIScheme)\n\n\tl.Infof(\n\t\t\"Downloading Terraform configurations from %s into %s\",\n\t\tutil.RelPathForLog(opts.RootWorkingDir, canonicalSourceURL, opts.Writers.LogShowAbsPaths),\n\t\tutil.RelPathForLog(opts.RootWorkingDir, src.DownloadDir, opts.Writers.LogShowAbsPaths))\n\n\tallowCAS := opts.Experiments.Evaluate(experiment.CAS)\n\n\tisLocalSource := tf.IsLocalSource(src.CanonicalSourceURL)\n\n\tif allowCAS && !isLocalSource {\n\t\tl.Debugf(\"CAS experiment enabled: attempting to use Content Addressable Storage for source: %s\", canonicalSourceURL)\n\n\t\tc, err := cas.New(cas.Options{})\n\t\tif err != nil {\n\t\t\tl.Warnf(\"Failed to initialize CAS: %v. Falling back to standard getter.\", err)\n\t\t} else {\n\t\t\tcloneOpts := cas.CloneOptions{\n\t\t\t\tDir:              src.DownloadDir,\n\t\t\t\tIncludedGitFiles: []string{\"HEAD\", \"config\"},\n\t\t\t}\n\n\t\t\tcasGetter := cas.NewCASGetter(l, c, &cloneOpts)\n\n\t\t\t// Use go-getter v2 Client to properly process the Request\n\t\t\tclient := getterv2.Client{\n\t\t\t\tGetters: []getterv2.Getter{casGetter},\n\t\t\t}\n\n\t\t\t// Set Pwd to the working directory so go-getter v2 can resolve relative paths\n\t\t\treq := &getterv2.Request{\n\t\t\t\tSrc: src.CanonicalSourceURL.String(),\n\t\t\t\tDst: src.DownloadDir,\n\t\t\t\tPwd: opts.WorkingDir,\n\t\t\t}\n\n\t\t\tif _, casErr := client.Get(ctx, req); casErr == nil {\n\t\t\t\tl.Debugf(\"Successfully downloaded source using CAS: %s\", canonicalSourceURL)\n\t\t\t\treturn nil\n\t\t\t} else {\n\t\t\t\tl.Warnf(\"CAS download failed: %v. Falling back to standard getter.\", casErr)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Fallback to standard go-getter\n\treturn opts.RunWithErrorHandling(ctx, l, r, func() error {\n\t\treturn getter.GetAny(src.DownloadDir, src.CanonicalSourceURL.String(), UpdateGetters(l, opts, cfg), preserveSymlinksOption())\n\t})\n}\n\n// ValidateWorkingDir checks if working terraformSource.WorkingDir exists and is a directory\nfunc ValidateWorkingDir(terraformSource *tf.Source) error {\n\tworkingLocalDir := strings.ReplaceAll(terraformSource.WorkingDir, terraformSource.DownloadDir+filepath.FromSlash(\"/\"), \"\")\n\tif util.IsFile(terraformSource.WorkingDir) {\n\t\treturn WorkingDirNotDir{Dir: workingLocalDir, Source: terraformSource.CanonicalSourceURL.String()}\n\t}\n\n\tif !util.IsDir(terraformSource.WorkingDir) {\n\t\treturn WorkingDirNotFound{Dir: workingLocalDir, Source: terraformSource.CanonicalSourceURL.String()}\n\t}\n\n\treturn nil\n}\n\ntype WorkingDirNotFound struct {\n\tSource string\n\tDir    string\n}\n\nfunc (err WorkingDirNotFound) Error() string {\n\treturn fmt.Sprintf(\"Working dir %s from source %s does not exist\", err.Dir, err.Source)\n}\n\ntype WorkingDirNotDir struct {\n\tSource string\n\tDir    string\n}\n\nfunc (err WorkingDirNotDir) Error() string {\n\treturn fmt.Sprintf(\"Valid working dir %s from source %s\", err.Dir, err.Source)\n}\n\ntype DownloadingTerraformSourceErr struct {\n\tErrMsg error\n\tURL    string\n}\n\nfunc (err DownloadingTerraformSourceErr) Error() string {\n\treturn fmt.Sprintf(\"downloading source url %s\\n%v\", err.URL, err.ErrMsg)\n}\n"
  },
  {
    "path": "internal/runner/run/download_source_test.go",
    "content": "package run_test\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\n\t\"github.com/gruntwork-io/go-commons/env\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runcfg\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/hashicorp/go-getter\"\n)\n\nfunc TestAlreadyHaveLatestCodeLocalFilePathWithNoModifiedFiles(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"file://\" + absPath(t, \"../../../test/fixtures/download-source/hello-world-local-hash\")\n\n\tdownloadDir := helpers.TmpDirWOSymlinks(t)\n\tdefer os.Remove(downloadDir)\n\n\tcopyFolder(t, \"../../../test/fixtures/download-source/download-dir-version-file-local-hash\", downloadDir)\n\ttestAlreadyHaveLatestCode(t, canonicalURL, downloadDir, false)\n\n\t// Write out a version file so we can test a cache hit\n\tterraformSource, _, _, err := createConfig(t, canonicalURL, downloadDir, false)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr = terraformSource.WriteVersionFile(logger.CreateLogger())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttestAlreadyHaveLatestCode(t, canonicalURL, downloadDir, true)\n}\n\nfunc TestAlreadyHaveLatestCodeLocalFilePathHashingFailure(t *testing.T) {\n\tt.Parallel()\n\n\tfixturePath := absPath(t, \"../../../test/fixtures/download-source/hello-world-local-hash-failed\")\n\tcanonicalURL := \"file://\" + fixturePath\n\n\tdownloadDir := helpers.TmpDirWOSymlinks(t)\n\tdefer os.Remove(downloadDir)\n\n\tcopyFolder(t, \"../../../test/fixtures/download-source/hello-world-local-hash-failed\", downloadDir)\n\n\tfileInfo, err := os.Stat(fixturePath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\terr = os.Chmod(fixturePath, 0000)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ttestAlreadyHaveLatestCode(t, canonicalURL, downloadDir, false)\n\n\terr = os.Chmod(fixturePath, fileInfo.Mode())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\nfunc TestAlreadyHaveLatestCodeLocalFilePathWithHashChanged(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"file://\" + absPath(t, \"../../../test/fixtures/download-source/hello-world-local-hash\")\n\n\tdownloadDir := helpers.TmpDirWOSymlinks(t)\n\tdefer os.Remove(downloadDir)\n\n\tcopyFolder(t, \"../../../test/fixtures/download-source/download-dir-version-file-local-hash\", downloadDir)\n\n\tf, err := os.OpenFile(downloadDir+\"/version-file.txt\", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer f.Close()\n\n\t// Modify content of file to simulate change\n\tfmt.Fprintln(f, \"CHANGED\")\n\n\ttestAlreadyHaveLatestCode(t, canonicalURL, downloadDir, false)\n}\n\nfunc TestAlreadyHaveLatestCodeLocalFilePath(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"file://\" + absPath(t, \"../../../test/fixtures/download-source/hello-world\")\n\tdownloadDir := \"does-not-exist\"\n\n\ttestAlreadyHaveLatestCode(t, canonicalURL, downloadDir, false)\n}\n\nfunc TestAlreadyHaveLatestCodeRemoteFilePathDownloadDirDoesNotExist(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"http://www.some-url.com\"\n\tdownloadDir := \"does-not-exist\"\n\n\ttestAlreadyHaveLatestCode(t, canonicalURL, downloadDir, false)\n}\n\nfunc TestAlreadyHaveLatestCodeRemoteFilePathDownloadDirExistsNoVersionNoVersionFile(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"http://www.some-url.com\"\n\tdownloadDir := \"../../../test/fixtures/download-source/download-dir-empty\"\n\n\ttestAlreadyHaveLatestCode(t, canonicalURL, downloadDir, false)\n}\n\nfunc TestAlreadyHaveLatestCodeRemoteFilePathDownloadDirExistsNoVersionWithVersionFile(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"http://www.some-url.com\"\n\tdownloadDir := \"../../../test/fixtures/download-source/download-dir-version-file-no-query\"\n\n\ttestAlreadyHaveLatestCode(t, canonicalURL, downloadDir, true)\n}\n\nfunc TestAlreadyHaveLatestCodeRemoteFilePathDownloadDirExistsWithVersionNoVersionFile(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"http://www.some-url.com?ref=v0.0.1\"\n\tdownloadDir := \"../../../test/fixtures/download-source/download-dir-empty\"\n\n\ttestAlreadyHaveLatestCode(t, canonicalURL, downloadDir, false)\n}\n\nfunc TestAlreadyHaveLatestCodeRemoteFilePathDownloadDirExistsWithVersionAndVersionFile(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"http://www.some-url.com?ref=v0.0.1\"\n\tdownloadDir := \"../../../test/fixtures/download-source/download-dir-version-file\"\n\n\ttestAlreadyHaveLatestCode(t, canonicalURL, downloadDir, false)\n}\n\nfunc TestAlreadyHaveLatestCodeRemoteFilePathDownloadDirExistsWithVersionAndVersionFileAndTfCode(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"http://www.some-url.com?ref=v0.0.1\"\n\tdownloadDir := \"../../../test/fixtures/download-source/download-dir-version-file-tf-code\"\n\n\ttestAlreadyHaveLatestCode(t, canonicalURL, downloadDir, true)\n}\n\nfunc TestDownloadTerraformSourceIfNecessaryLocalDirToEmptyDir(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"file://\" + absPath(t, \"../../../test/fixtures/download-source/hello-world\")\n\n\tdownloadDir := helpers.TmpDirWOSymlinks(t)\n\tdefer os.Remove(downloadDir)\n\n\ttestDownloadTerraformSourceIfNecessary(t, canonicalURL, downloadDir, false, \"# Hello, World\", false)\n}\n\nfunc TestDownloadTerraformSourceIfNecessaryLocalDirToAlreadyDownloadedDir(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"file://\" + absPath(t, \"../../../test/fixtures/download-source/hello-world\")\n\n\tdownloadDir := helpers.TmpDirWOSymlinks(t)\n\tdefer os.Remove(downloadDir)\n\n\tcopyFolder(t, \"../../../test/fixtures/download-source/hello-world-2\", downloadDir)\n\n\ttestDownloadTerraformSourceIfNecessary(t, canonicalURL, downloadDir, false, \"# Hello, World\", false)\n}\n\nfunc TestDownloadTerraformSourceIfNecessaryRemoteUrlToEmptyDir(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"github.com/gruntwork-io/terragrunt//test/fixtures/download-source/hello-world\"\n\n\tdownloadDir := helpers.TmpDirWOSymlinks(t)\n\tdefer os.Remove(downloadDir)\n\n\ttestDownloadTerraformSourceIfNecessary(t, canonicalURL, downloadDir, false, \"# Hello, World\", false)\n}\n\nfunc TestDownloadTerraformSourceIfNecessaryRemoteUrlToAlreadyDownloadedDir(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"github.com/gruntwork-io/terragrunt//test/fixtures/download-source/hello-world\"\n\n\tdownloadDir := helpers.TmpDirWOSymlinks(t)\n\tdefer os.Remove(downloadDir)\n\n\tcopyFolder(t, \"../../../test/fixtures/download-source/hello-world-2\", downloadDir)\n\n\ttestDownloadTerraformSourceIfNecessary(t, canonicalURL, downloadDir, false, \"# Hello, World 2\", false)\n}\n\nfunc TestDownloadTerraformSourceIfNecessaryRemoteUrlToAlreadyDownloadedDirDifferentVersion(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"github.com/gruntwork-io/terragrunt//test/fixtures/download-source/hello-world?ref=v0.83.2\"\n\n\tdownloadDir := helpers.TmpDirWOSymlinks(t)\n\tdefer os.Remove(downloadDir)\n\n\tcopyFolder(t, \"../../../test/fixtures/download-source/hello-world-2\", downloadDir)\n\n\ttestDownloadTerraformSourceIfNecessary(t, canonicalURL, downloadDir, false, \"# Hello, World\", true)\n}\n\nfunc TestDownloadTerraformSourceIfNecessaryRemoteUrlToAlreadyDownloadedDirSameVersion(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"github.com/gruntwork-io/terragrunt//test/fixtures/download-source/hello-world-version-remote?ref=v0.83.2\"\n\n\tdownloadDir := helpers.TmpDirWOSymlinks(t)\n\tdefer os.Remove(downloadDir)\n\n\tcopyFolder(t, \"../../../test/fixtures/download-source/hello-world-version-remote\", downloadDir)\n\n\ttestDownloadTerraformSourceIfNecessary(t, canonicalURL, downloadDir, false, \"# Hello, World version remote\", false)\n}\n\nfunc TestDownloadTerraformSourceIfNecessaryRemoteUrlOverrideSource(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"github.com/gruntwork-io/terragrunt//test/fixtures/download-source/hello-world?ref=v0.83.2\"\n\n\tdownloadDir := helpers.TmpDirWOSymlinks(t)\n\tdefer os.Remove(downloadDir)\n\n\tcopyFolder(t, \"../../../test/fixtures/download-source/hello-world-version-remote\", downloadDir)\n\n\ttestDownloadTerraformSourceIfNecessary(t, canonicalURL, downloadDir, true, \"# Hello, World\", false)\n}\n\nfunc TestDownloadTerraformSourceIfNecessaryInvalidTerraformSource(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"github.com/totallyfakedoesnotexist/notreal.git//foo?ref=v1.2.3\"\n\n\tdownloadDir := helpers.TmpDirWOSymlinks(t)\n\tdefer os.Remove(downloadDir)\n\n\tcopyFolder(t, \"../../../test/fixtures/download-source/hello-world-version-remote\", downloadDir)\n\n\tterraformSource, opts, cfg, err := createConfig(t, canonicalURL, downloadDir, false)\n\n\trequire.NoError(t, err)\n\n\terr = run.DownloadTerraformSourceIfNecessary(\n\t\tt.Context(),\n\t\tlogger.CreateLogger(),\n\t\tterraformSource,\n\t\tconfigbridge.NewRunOptions(opts),\n\t\tcfg,\n\t\treport.NewReport(),\n\t)\n\trequire.Error(t, err)\n\n\tvar downloadingTerraformSourceErr run.DownloadingTerraformSourceErr\n\n\tok := errors.As(err, &downloadingTerraformSourceErr)\n\tassert.True(t, ok)\n}\n\nfunc TestInvalidModulePath(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"github.com/gruntwork-io/terragrunt//test/fixtures/download-source/hello-world-version-remote/non-existent-path?ref=v0.83.2\"\n\n\tdownloadDir := helpers.TmpDirWOSymlinks(t)\n\tdefer os.Remove(downloadDir)\n\n\tcopyFolder(t, \"../../../test/fixtures/download-source/hello-world-version-remote\", downloadDir)\n\n\tterraformSource, _, _, err := createConfig(t, canonicalURL, downloadDir, false)\n\trequire.NoError(t, err)\n\n\tterraformSource.WorkingDir += \"/not-existing-path\"\n\n\terr = run.ValidateWorkingDir(terraformSource)\n\trequire.Error(t, err)\n\n\tvar workingDirNotFound run.WorkingDirNotFound\n\n\tok := errors.As(err, &workingDirNotFound)\n\tassert.True(t, ok)\n}\n\nfunc TestDownloadInvalidPathToFilePath(t *testing.T) {\n\tt.Parallel()\n\n\tcanonicalURL := \"github.com/gruntwork-io/terragrunt//test/fixtures/download-source/hello-world/main.tf?ref=v0.83.2\"\n\n\tdownloadDir := helpers.TmpDirWOSymlinks(t)\n\tdefer os.Remove(downloadDir)\n\n\tcopyFolder(t, \"../../../test/fixtures/download-source/hello-world-version-remote\", downloadDir)\n\n\tterraformSource, _, _, err := createConfig(t, canonicalURL, downloadDir, false)\n\trequire.NoError(t, err)\n\n\tterraformSource.WorkingDir += \"/main.tf\"\n\n\terr = run.ValidateWorkingDir(terraformSource)\n\trequire.Error(t, err)\n\n\tvar workingDirNotDir run.WorkingDirNotDir\n\n\tok := errors.As(err, &workingDirNotDir)\n\tassert.True(t, ok)\n}\n\n// The test cases are run sequentially because they depend on each other.\n//\n//nolint:tparallel\nfunc TestDownloadTerraformSourceFromLocalFolderWithManifest(t *testing.T) {\n\tt.Parallel()\n\n\tdownloadDir := helpers.TmpDirWOSymlinks(t)\n\tt.Cleanup(func() {\n\t\tos.RemoveAll(downloadDir)\n\t})\n\n\t// used to test if an empty folder gets copied\n\ttestDir := helpers.TmpDirWOSymlinks(t)\n\trequire.NoError(t, os.Mkdir(path.Join(testDir, \"sub2\"), 0700))\n\tt.Cleanup(func() {\n\t\tos.Remove(testDir)\n\t})\n\n\ttestCases := []struct {\n\t\tcomp      assert.Comparison\n\t\tname      string\n\t\tsourceURL string\n\t}{\n\t\t{\n\t\t\tname:      \"test-stale-file-exists\",\n\t\t\tsourceURL: \"../../../test/fixtures/manifest/version-1\",\n\t\t\tcomp: func() bool {\n\t\t\t\treturn util.FileExists(filepath.Join(downloadDir, \"stale.tf\"))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"test-stale-file-doesnt-exist-after-source-update\",\n\t\t\tsourceURL: \"../../../test/fixtures/manifest/version-2\",\n\t\t\tcomp: func() bool {\n\t\t\t\treturn !util.FileExists(filepath.Join(downloadDir, \"stale.tf\"))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"test-tffile-exists-in-subfolder\",\n\t\t\tsourceURL: \"../../../test/fixtures/manifest/version-3-subfolder\",\n\t\t\tcomp: func() bool {\n\t\t\t\treturn util.FileExists(filepath.Join(downloadDir, \"sub\", \"main.tf\"))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"test-tffile-doesnt-exist-in-subfolder\",\n\t\t\tsourceURL: \"../../../test/fixtures/manifest/version-4-subfolder-empty\",\n\t\t\tcomp: func() bool {\n\t\t\t\treturn !util.FileExists(filepath.Join(downloadDir, \"sub\", \"main.tf\"))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"test-empty-folder-gets-copied\",\n\t\t\tsourceURL: testDir,\n\t\t\tcomp: func() bool {\n\t\t\t\treturn util.FileExists(filepath.Join(downloadDir, \"sub2\"))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"test-empty-folder-gets-populated\",\n\t\t\tsourceURL: \"../../../test/fixtures/manifest/version-5-not-empty-subfolder\",\n\t\t\tcomp: func() bool {\n\t\t\t\treturn util.FileExists(filepath.Join(downloadDir, \"sub2\", \"main.tf\"))\n\t\t\t},\n\t\t},\n\t}\n\n\t// The test cases are run sequentially because they depend on each other.\n\t//\n\t//nolint:paralleltest\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tcopyFolder(t, tc.sourceURL, downloadDir)\n\t\t\tassert.Condition(t, tc.comp)\n\t\t})\n\t}\n}\n\nfunc testDownloadTerraformSourceIfNecessary(\n\tt *testing.T,\n\tcanonicalURL string,\n\tdownloadDir string,\n\tsourceUpdate bool,\n\texpectedFileContents string,\n\trequireInitFile bool,\n) {\n\tt.Helper()\n\n\tterraformSource, opts, cfg, err := createConfig(\n\t\tt,\n\t\tcanonicalURL,\n\t\tdownloadDir,\n\t\tsourceUpdate,\n\t)\n\n\trequire.NoError(t, err)\n\n\terr = run.DownloadTerraformSourceIfNecessary(\n\t\tt.Context(),\n\t\tlogger.CreateLogger(),\n\t\tterraformSource,\n\t\tconfigbridge.NewRunOptions(opts),\n\t\tcfg,\n\t\treport.NewReport(),\n\t)\n\trequire.NoError(t, err, \"For terraform source %v: %v\", terraformSource, err)\n\n\texpectedFilePath := filepath.Join(downloadDir, \"main.tf\")\n\tif assert.True(t, util.FileExists(expectedFilePath), \"For terraform source %v\", terraformSource) {\n\t\tactualFileContents := readFile(t, expectedFilePath)\n\t\tassert.Equal(t, expectedFileContents, actualFileContents, \"For terraform source %v\", terraformSource)\n\t}\n\n\tif requireInitFile {\n\t\texistsInitFile := util.FileExists(filepath.Join(terraformSource.WorkingDir, run.ModuleInitRequiredFile))\n\t\trequire.True(t, existsInitFile)\n\t}\n}\n\nfunc createConfig(\n\tt *testing.T,\n\tcanonicalURL string,\n\tdownloadDir string,\n\tsourceUpdate bool,\n) (*tf.Source, *options.TerragruntOptions, *runcfg.RunConfig, error) {\n\tt.Helper()\n\n\tlogger := logger.CreateLogger()\n\tlogger.SetOptions(log.WithOutput(io.Discard))\n\n\tterraformSource := &tf.Source{\n\t\tCanonicalSourceURL: parseURL(t, canonicalURL),\n\t\tDownloadDir:        downloadDir,\n\t\tWorkingDir:         downloadDir,\n\t\tVersionFile:        filepath.Join(downloadDir, \"version-file.txt\"),\n\t}\n\n\topts, err := options.NewTerragruntOptionsForTest(\"./should-not-be-used\")\n\trequire.NoError(t, err)\n\n\topts.SourceUpdate = sourceUpdate\n\topts.Env = env.Parse(os.Environ())\n\n\tcfg := &runcfg.RunConfig{\n\t\tTerraform: runcfg.TerraformConfig{\n\t\t\tExtraArgs: []runcfg.TerraformExtraArguments{},\n\t\t},\n\t}\n\n\t_, ver, impl, err := run.PopulateTFVersion(t.Context(), logger, opts.WorkingDir, opts.VersionManagerFileName, configbridge.TFRunOptsFromOpts(opts))\n\trequire.NoError(t, err)\n\n\topts.TerraformVersion = ver\n\topts.TofuImplementation = impl\n\n\treturn terraformSource, opts, cfg, err\n}\n\nfunc testAlreadyHaveLatestCode(t *testing.T, canonicalURL string, downloadDir string, expected bool) {\n\tt.Helper()\n\n\tlogger := logger.CreateLogger()\n\tlogger.SetOptions(log.WithOutput(io.Discard))\n\n\tterraformSource := &tf.Source{\n\t\tCanonicalSourceURL: parseURL(t, canonicalURL),\n\t\tDownloadDir:        downloadDir,\n\t\tWorkingDir:         downloadDir,\n\t\tVersionFile:        filepath.Join(downloadDir, \"version-file.txt\"),\n\t}\n\n\topts, err := options.NewTerragruntOptionsForTest(\"./should-not-be-used\")\n\trequire.NoError(t, err)\n\n\tactual, err := run.AlreadyHaveLatestCode(logger, terraformSource, configbridge.NewRunOptions(opts))\n\trequire.NoError(t, err)\n\tassert.Equal(t, expected, actual, \"For terraform source %v\", terraformSource)\n}\n\nfunc absPath(t *testing.T, path string) string {\n\tt.Helper()\n\n\tif filepath.IsAbs(path) {\n\t\treturn filepath.Clean(path)\n\t}\n\n\tabsPath, err := filepath.Abs(path)\n\trequire.NoError(t, err)\n\n\treturn filepath.Clean(absPath)\n}\n\nfunc parseURL(t *testing.T, str string) *url.URL {\n\tt.Helper()\n\n\t// URLs should have only forward slashes, whereas on Windows, the file paths may be backslashes\n\trawURL := strings.Join(strings.Split(str, string(filepath.Separator)), \"/\")\n\n\tparsed, err := url.Parse(rawURL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn parsed\n}\n\nfunc readFile(t *testing.T, path string) string {\n\tt.Helper()\n\n\tcontents, err := util.ReadFileAsString(path)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn contents\n}\n\nfunc copyFolder(t *testing.T, src string, dest string) {\n\tt.Helper()\n\n\tl := logger.CreateLogger()\n\tl.SetOptions(log.WithOutput(io.Discard))\n\n\terr := util.CopyFolderContents(\n\t\tl,\n\t\tfilepath.FromSlash(src),\n\t\tfilepath.FromSlash(dest),\n\t\t\".terragrunt-test\",\n\t\tnil,\n\t\tnil,\n\t)\n\trequire.NoError(t, err)\n}\n\n// TestUpdateGettersExcludeFromCopy verifies the correct behavior of updateGetters with ExcludeFromCopy configuration\nfunc TestUpdateGettersExcludeFromCopy(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname                 string\n\t\tcfg                  *runcfg.RunConfig\n\t\texpectedExcludeFiles []string\n\t}{\n\t\t{\n\t\t\tname: \"Nil ExcludeFromCopy\",\n\t\t\tcfg: &runcfg.RunConfig{\n\t\t\t\tTerraform: runcfg.TerraformConfig{\n\t\t\t\t\tExcludeFromCopy: []string{},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedExcludeFiles: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"Non-Nil ExcludeFromCopy\",\n\t\t\tcfg: &runcfg.RunConfig{\n\t\t\t\tTerraform: runcfg.TerraformConfig{\n\t\t\t\t\tExcludeFromCopy: []string{\"*.tfstate\", \"excluded_dir/\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedExcludeFiles: []string{\"*.tfstate\", \"excluded_dir/\"},\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.Parallel()\n\n\t\t\tterragruntOptions, err := options.NewTerragruntOptionsForTest(\"./test\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tclient := &getter.Client{}\n\n\t\t\t// Call updateGetters\n\t\t\tupdateGettersFunc := run.UpdateGetters(logger.CreateLogger(), configbridge.NewRunOptions(terragruntOptions), tc.cfg)\n\t\t\terr = updateGettersFunc(client)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Find the file getter\n\t\t\tfileGetter, ok := client.Getters[\"file\"].(*run.FileCopyGetter)\n\t\t\trequire.True(t, ok, \"File getter should be of type FileCopyGetter\")\n\n\t\t\t// Verify ExcludeFromCopy\n\t\t\tassert.Equal(\n\t\t\t\tt,\n\t\t\t\ttc.expectedExcludeFiles,\n\t\t\t\tfileGetter.ExcludeFromCopy,\n\t\t\t\t\"ExcludeFromCopy should match expected value\",\n\t\t\t)\n\t\t})\n\t}\n}\n\n// TestUpdateGettersHTTPNetrc verifies that HTTP/HTTPS getters have Netrc enabled\n// for authentication via ~/.netrc files.\nfunc TestUpdateGettersHTTPNetrc(t *testing.T) {\n\tt.Parallel()\n\n\tterragruntOptions, err := options.NewTerragruntOptionsForTest(\"./test\")\n\trequire.NoError(t, err)\n\n\tcfg := &runcfg.RunConfig{\n\t\tTerraform: runcfg.TerraformConfig{},\n\t}\n\n\tclient := &getter.Client{}\n\n\tupdateGettersFunc := run.UpdateGetters(logger.CreateLogger(), configbridge.NewRunOptions(terragruntOptions), cfg)\n\terr = updateGettersFunc(client)\n\trequire.NoError(t, err)\n\n\t// Verify HTTP getter has Netrc enabled\n\thttpGetter, ok := client.Getters[\"http\"].(*getter.HttpGetter)\n\trequire.True(t, ok, \"HTTP getter should be of type HttpGetter\")\n\tassert.True(t, httpGetter.Netrc, \"HTTP getter should have Netrc enabled for ~/.netrc authentication\")\n\n\t// Verify HTTPS getter has Netrc enabled\n\thttpsGetter, ok := client.Getters[\"https\"].(*getter.HttpGetter)\n\trequire.True(t, ok, \"HTTPS getter should be of type HttpGetter\")\n\tassert.True(t, httpsGetter.Netrc, \"HTTPS getter should have Netrc enabled for ~/.netrc authentication\")\n}\n\n// TestUpdateGettersIncludesAllGlobalGetters verifies that every scheme registered in the global\n// getter.Getters map is present in client.Getters after calling UpdateGetters. This guards against\n// regressions where the reflect-based approach might silently fail to create an instance.\nfunc TestUpdateGettersIncludesAllGlobalGetters(t *testing.T) {\n\tt.Parallel()\n\n\tterragruntOptions, err := options.NewTerragruntOptionsForTest(\"./test\")\n\trequire.NoError(t, err)\n\n\tcfg := &runcfg.RunConfig{\n\t\tTerraform: runcfg.TerraformConfig{},\n\t}\n\n\tclient := &getter.Client{}\n\n\tupdateGettersFunc := run.UpdateGetters(logger.CreateLogger(), configbridge.NewRunOptions(terragruntOptions), cfg)\n\terr = updateGettersFunc(client)\n\trequire.NoError(t, err)\n\n\t// Every scheme from the global getter.Getters map must be present\n\tfor scheme := range getter.Getters {\n\t\tassert.Contains(t, client.Getters, scheme,\n\t\t\t\"client.Getters should contain the %q scheme from the global getter.Getters map\", scheme)\n\t}\n\n\t// Terragrunt-specific getters must also be present\n\tassert.Contains(t, client.Getters, \"tfr\", \"client.Getters should contain the Terragrunt registry getter\")\n}\n\n// TestDownloadWithNoSourceCreatesCache tests that when sourceURL is \".\" (no source specified),\n// DownloadTerraformSource creates cache and copies files from the working directory.\n// This tests the behavior when terragrunt.hcl doesn't have a terraform { source = \"...\" } block.\nfunc TestDownloadWithNoSourceCreatesCache(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a temp directory to act as the source/working directory\n\tsourceDir := helpers.TmpDirWOSymlinks(t)\n\tdefer os.RemoveAll(sourceDir)\n\n\t// Create a simple terraform file in the source directory\n\tmainTfContent := \"# Test file for no-source cache creation\\n\"\n\terr := os.WriteFile(filepath.Join(sourceDir, \"main.tf\"), []byte(mainTfContent), 0644)\n\trequire.NoError(t, err)\n\n\t// Create the download directory where cache will be created\n\tdownloadDir := helpers.TmpDirWOSymlinks(t)\n\tdefer os.RemoveAll(downloadDir)\n\n\topts, err := options.NewTerragruntOptionsForTest(filepath.Join(sourceDir, \"terragrunt.hcl\"))\n\trequire.NoError(t, err)\n\n\topts.WorkingDir = sourceDir\n\topts.DownloadDir = downloadDir\n\topts.Experiments = experiment.NewExperiments()\n\n\tcfg := &runcfg.RunConfig{\n\t\tTerraform: runcfg.TerraformConfig{\n\t\t\tExtraArgs: []runcfg.TerraformExtraArguments{},\n\t\t},\n\t}\n\n\tl := logger.CreateLogger()\n\tl.SetOptions(log.WithOutput(io.Discard))\n\n\tr := report.NewReport()\n\n\t// sourceURL \".\" represents the current directory (no terraform.source specified)\n\tupdatedOpts, err := run.DownloadTerraformSource(t.Context(), l, \".\", configbridge.NewRunOptions(opts), cfg, r)\n\trequire.NoError(t, err)\n\n\t// Verify that the working directory was changed to the cache directory (inside downloadDir)\n\tassert.NotEqual(t, sourceDir, updatedOpts.WorkingDir, \"Working dir should be changed to cache\")\n\tassert.True(t, strings.HasPrefix(updatedOpts.WorkingDir, downloadDir), \"Working dir should be under download dir\")\n\n\t// Verify that the main.tf file was copied to the cache\n\tcachedMainTf := filepath.Join(updatedOpts.WorkingDir, \"main.tf\")\n\tassert.FileExists(t, cachedMainTf, \"main.tf should exist in cache directory\")\n\n\t// Verify the contents were copied correctly\n\tcachedContent, err := os.ReadFile(cachedMainTf)\n\trequire.NoError(t, err)\n\tassert.Equal(t, mainTfContent, string(cachedContent), \"File contents should match\")\n}\n\n// TestDownloadSourceWithCASExperimentDisabled tests that CAS is not used when the experiment is disabled\nfunc TestDownloadSourceWithCASExperimentDisabled(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tlocalSourcePath := absPath(t, \"../../../test/fixtures/download-source/hello-world\")\n\tsrc := &tf.Source{\n\t\tCanonicalSourceURL: parseURL(t, \"file://\"+localSourcePath),\n\t\tDownloadDir:        tmpDir,\n\t\tWorkingDir:         tmpDir,\n\t\tVersionFile:        filepath.Join(tmpDir, \"version-file.txt\"),\n\t}\n\n\topts, err := options.NewTerragruntOptionsForTest(\"./should-not-be-used\")\n\trequire.NoError(t, err)\n\n\t// Ensure CAS experiment is not enabled\n\topts.Experiments = experiment.NewExperiments()\n\n\tcfg := &runcfg.RunConfig{\n\t\tTerraform: runcfg.TerraformConfig{\n\t\t\tExtraArgs: []runcfg.TerraformExtraArguments{},\n\t\t},\n\t}\n\n\tl := logger.CreateLogger()\n\tl.SetOptions(log.WithOutput(io.Discard))\n\n\t// Mock the download source function call\n\tr := report.NewReport()\n\n\terr = run.DownloadTerraformSourceIfNecessary(t.Context(), l, src, configbridge.NewRunOptions(opts), cfg, r)\n\n\trequire.NoError(t, err)\n\n\t// Verify the file was downloaded\n\texpectedFilePath := filepath.Join(tmpDir, \"main.tf\")\n\tassert.FileExists(t, expectedFilePath)\n}\n\n// TestDownloadSourceWithCASExperimentEnabled tests that CAS is attempted when the experiment is enabled\nfunc TestDownloadSourceWithCASExperimentEnabled(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tlocalSourcePath := absPath(t, \"../../../test/fixtures/download-source/hello-world\")\n\tsrc := &tf.Source{\n\t\tCanonicalSourceURL: parseURL(t, \"file://\"+localSourcePath),\n\t\tDownloadDir:        tmpDir,\n\t\tWorkingDir:         tmpDir,\n\t\tVersionFile:        filepath.Join(tmpDir, \"version-file.txt\"),\n\t}\n\n\t// Create options with CAS experiment enabled\n\topts, err := options.NewTerragruntOptionsForTest(\"./should-not-be-used\")\n\trequire.NoError(t, err)\n\n\t// Enable CAS experiment\n\topts.Experiments = experiment.NewExperiments()\n\terr = opts.Experiments.EnableExperiment(experiment.CAS)\n\trequire.NoError(t, err)\n\n\tcfg := &runcfg.RunConfig{\n\t\tTerraform: runcfg.TerraformConfig{\n\t\t\tExtraArgs: []runcfg.TerraformExtraArguments{},\n\t\t},\n\t}\n\n\tl := logger.CreateLogger()\n\tl.SetOptions(log.WithOutput(io.Discard))\n\n\tr := report.NewReport()\n\n\terr = run.DownloadTerraformSourceIfNecessary(t.Context(), l, src, configbridge.NewRunOptions(opts), cfg, r)\n\trequire.NoError(t, err)\n\n\texpectedFilePath := filepath.Join(tmpDir, \"main.tf\")\n\tassert.FileExists(t, expectedFilePath)\n}\n\n// TestDownloadSourceWithCASGitSource tests CAS functionality with a Git source\nfunc TestDownloadSourceWithCASGitSource(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tsrc := &tf.Source{\n\t\tCanonicalSourceURL: parseURL(\n\t\t\tt,\n\t\t\t\"github.com/gruntwork-io/terragrunt//test/fixtures/download/hello-world\",\n\t\t),\n\t\tDownloadDir: tmpDir,\n\t\tWorkingDir:  tmpDir,\n\t\tVersionFile: filepath.Join(tmpDir, \"version-file.txt\"),\n\t}\n\n\topts, err := options.NewTerragruntOptionsForTest(\"./should-not-be-used\")\n\trequire.NoError(t, err)\n\n\t// Enable CAS experiment\n\topts.Experiments = experiment.NewExperiments()\n\terr = opts.Experiments.EnableExperiment(experiment.CAS)\n\trequire.NoError(t, err)\n\n\tcfg := &runcfg.RunConfig{\n\t\tTerraform: runcfg.TerraformConfig{\n\t\t\tExtraArgs: []runcfg.TerraformExtraArguments{},\n\t\t},\n\t}\n\n\tl := logger.CreateLogger()\n\tl.SetOptions(log.WithOutput(io.Discard))\n\n\tr := report.NewReport()\n\n\terr = run.DownloadTerraformSourceIfNecessary(t.Context(), l, src, configbridge.NewRunOptions(opts), cfg, r)\n\trequire.NoError(t, err)\n\n\t// Verify the file was downloaded\n\texpectedFilePath := filepath.Join(tmpDir, \"main.tf\")\n\tassert.FileExists(t, expectedFilePath)\n}\n\n// TestDownloadSourceCASInitializationFailure tests the fallback behavior when CAS initialization fails\nfunc TestDownloadSourceCASInitializationFailure(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tlocalSourcePath := absPath(t, \"../../../test/fixtures/download-source/hello-world\")\n\tsrc := &tf.Source{\n\t\tCanonicalSourceURL: parseURL(t, \"file://\"+localSourcePath),\n\t\tDownloadDir:        tmpDir,\n\t\tWorkingDir:         tmpDir,\n\t\tVersionFile:        filepath.Join(tmpDir, \"version-file.txt\"),\n\t}\n\n\topts, err := options.NewTerragruntOptionsForTest(\"./should-not-be-used\")\n\trequire.NoError(t, err)\n\n\t// Enable CAS experiment\n\topts.Experiments = experiment.NewExperiments()\n\terr = opts.Experiments.EnableExperiment(experiment.CAS)\n\trequire.NoError(t, err)\n\n\tcfg := &runcfg.RunConfig{\n\t\tTerraform: runcfg.TerraformConfig{\n\t\t\tExtraArgs: []runcfg.TerraformExtraArguments{},\n\t\t},\n\t}\n\n\tl := logger.CreateLogger()\n\tl.SetOptions(log.WithOutput(io.Discard))\n\n\tr := report.NewReport()\n\n\terr = run.DownloadTerraformSourceIfNecessary(t.Context(), l, src, configbridge.NewRunOptions(opts), cfg, r)\n\trequire.NoError(t, err)\n\n\texpectedFilePath := filepath.Join(tmpDir, \"main.tf\")\n\tassert.FileExists(t, expectedFilePath)\n}\n\n// TestDownloadSourceWithCASMultipleSources tests that CAS works with multiple different sources\nfunc TestDownloadSourceWithCASMultipleSources(t *testing.T) {\n\tt.Parallel()\n\n\topts, err := options.NewTerragruntOptionsForTest(\"./should-not-be-used\")\n\trequire.NoError(t, err)\n\n\topts.Env = env.Parse(os.Environ())\n\n\t// Enable CAS experiment\n\topts.Experiments = experiment.NewExperiments()\n\terr = opts.Experiments.EnableExperiment(experiment.CAS)\n\trequire.NoError(t, err)\n\n\tcfg := &runcfg.RunConfig{\n\t\tTerraform: runcfg.TerraformConfig{\n\t\t\tExtraArgs: []runcfg.TerraformExtraArguments{},\n\t\t},\n\t}\n\n\tl := logger.CreateLogger()\n\tl.SetOptions(log.WithOutput(io.Discard))\n\n\tr := report.NewReport()\n\n\ttestCases := []struct {\n\t\tname      string\n\t\tsourceURL string\n\t\texpectCAS bool\n\t}{\n\t\t{\n\t\t\tname:      \"Local file source\",\n\t\t\tsourceURL: \"file://\" + absPath(t, \"../../../test/fixtures/download-source/hello-world\"),\n\t\t\texpectCAS: false, // CAS doesn't handle file:// URLs\n\t\t},\n\t\t{\n\t\t\tname:      \"HTTP source\",\n\t\t\tsourceURL: \"https://example.com/repo.tar.gz\",\n\t\t\texpectCAS: false, // CAS doesn't handle HTTP sources\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.Parallel()\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\tsrc := &tf.Source{\n\t\t\t\tCanonicalSourceURL: parseURL(t, tc.sourceURL),\n\t\t\t\tDownloadDir:        tmpDir,\n\t\t\t\tWorkingDir:         tmpDir,\n\t\t\t\tVersionFile:        filepath.Join(tmpDir, \"version-file.txt\"),\n\t\t\t}\n\n\t\t\terr = run.DownloadTerraformSourceIfNecessary(t.Context(), l, src, configbridge.NewRunOptions(opts), cfg, r)\n\n\t\t\tif tc.name == \"Local file source\" {\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\texpectedFilePath := filepath.Join(tmpDir, \"main.tf\")\n\t\t\t\tassert.FileExists(t, expectedFilePath)\n\t\t\t} else {\n\t\t\t\tt.Logf(\"Source %s result: %v\", tc.sourceURL, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestHTTPGetterNetrcAuthentication verifies that HTTP/HTTPS getters correctly authenticate\n// using ~/.netrc credentials when downloading OpenTofu/Terraform sources.\n//\n// Does not use `t.Parallel()` because we need to set the `NETRC` environment variable\n// to point to a temporary `~/.netrc` file for the test to pass.\nfunc TestHTTPGetterNetrcAuthentication(t *testing.T) {\n\texpectedUser := \"testuser\"\n\texpectedPass := \"testpassword\"\n\tfileContent := \"# test tofu content\"\n\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tuser, pass, ok := r.BasicAuth()\n\t\tif !ok || user != expectedUser || pass != expectedPass {\n\t\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\n\t\tw.Write([]byte(fileContent))\n\t}))\n\tdefer server.Close()\n\n\tserverURL, err := url.Parse(server.URL)\n\trequire.NoError(t, err)\n\n\tnetrcContent := fmt.Sprintf(\"machine %s\\nlogin %s\\npassword %s\\n\",\n\t\tserverURL.Host, expectedUser, expectedPass)\n\n\tnetrcFile := filepath.Join(t.TempDir(), \".netrc\")\n\trequire.NoError(t, os.WriteFile(netrcFile, []byte(netrcContent), 0600))\n\n\tt.Setenv(\"NETRC\", netrcFile)\n\n\topts, err := options.NewTerragruntOptionsForTest(\"./test\")\n\trequire.NoError(t, err)\n\n\tcfg := &runcfg.RunConfig{Terraform: runcfg.TerraformConfig{}}\n\n\tclient := &getter.Client{\n\t\tSrc:  server.URL + \"/module.tf\",\n\t\tDst:  filepath.Join(t.TempDir(), \"module.tf\"),\n\t\tMode: getter.ClientModeFile,\n\t}\n\n\tupdateFn := run.UpdateGetters(logger.CreateLogger(), configbridge.NewRunOptions(opts), cfg)\n\trequire.NoError(t, updateFn(client))\n\n\trequire.NoError(t, client.Get())\n\n\tdownloaded, err := os.ReadFile(client.Dst)\n\trequire.NoError(t, err)\n\tassert.Equal(t, fileContent, string(downloaded))\n}\n"
  },
  {
    "path": "internal/runner/run/errors.go",
    "content": "package run\n\nimport (\n\t\"fmt\"\n)\n\n// Custom error types\n\ntype MissingCommand struct{}\n\nfunc (err MissingCommand) Error() string {\n\treturn \"Missing terraform command (Example: terragrunt run plan)\"\n}\n\ntype WrongTerraformCommand string\n\nfunc (name WrongTerraformCommand) Error() string {\n\treturn fmt.Sprintf(\"Terraform has no command named %q. To see all of Terraform's top-level commands, run: terraform -help\", string(name))\n}\n\ntype WrongTofuCommand string\n\nfunc (name WrongTofuCommand) Error() string {\n\treturn fmt.Sprintf(\"OpenTofu has no command named %q. To see all of OpenTofu's top-level commands, run: tofu -help\", string(name))\n}\n\ntype BackendNotDefined struct {\n\tConfigPath  string\n\tWorkingDir  string\n\tBackendType string\n}\n\nfunc (err BackendNotDefined) Error() string {\n\treturn fmt.Sprintf(\"Found remote_state settings in %s but no backend block in the Terraform code in %s. You must define a backend block (it can be empty!) in your Terraform code or your remote state settings will have no effect! It should look something like this:\\n\\nterraform {\\n  backend \\\"%s\\\" {}\\n}\\n\\n\", err.ConfigPath, err.WorkingDir, err.BackendType)\n}\n\ntype NoTerraformFilesFound string\n\nfunc (path NoTerraformFilesFound) Error() string {\n\treturn \"Did not find any Terraform files (*.tf) or OpenTofu files (*.tofu) in \" + string(path)\n}\n\ntype ModuleIsProtected struct {\n\tConfigPath string\n}\n\nfunc (err ModuleIsProtected) Error() string {\n\treturn fmt.Sprintf(\"Unit is protected by the prevent_destroy flag in %s. Set it to false or remove it to allow destruction of the unit.\", err.ConfigPath)\n}\n\n// Legacy retry error removed in favor of error handling via options.Errors\n\ntype RunAllDisabledErr struct {\n\tcommand string\n\treason  string\n}\n\nfunc (err RunAllDisabledErr) Error() string {\n\treturn fmt.Sprintf(\"%s with run --all is disabled: %s\", err.command, err.reason)\n}\n"
  },
  {
    "path": "internal/runner/run/file_copy_getter.go",
    "content": "package run\n\nimport (\n\t\"net/url\"\n\t\"os\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/hashicorp/go-getter\"\n)\n\n// SourceManifestName is the manifest for files copied from the URL specified in the terraform { source = \"<URL>\" } config\nconst SourceManifestName = \".terragrunt-source-manifest\"\n\n// FileCopyGetter is a custom getter.Getter implementation that uses file copying instead of symlinks. Symlinks are\n// faster and use less disk space, but they cause issues in Windows and with infinite loops, so we copy files/folders\n// instead.\ntype FileCopyGetter struct {\n\tLogger log.Logger\n\tgetter.FileGetter\n\n\t// List of glob paths that should be included in the copy. This can be used to override the default behavior of\n\t// Terragrunt, which will skip hidden folders.\n\tIncludeInCopy   []string\n\tExcludeFromCopy []string\n}\n\n// Get replaces the original FileGetter\n// The original FileGetter does NOT know how to do folder copying (it only does symlinks), so we provide a copy\n// implementation here\nfunc (g *FileCopyGetter) Get(dst string, u *url.URL) error {\n\tpath := u.Path\n\tif u.RawPath != \"\" {\n\t\tpath = u.RawPath\n\t}\n\n\t// The source path must exist and be a directory to be usable.\n\tif fi, err := os.Stat(path); err != nil {\n\t\treturn errors.Errorf(\"source path error: %s\", err)\n\t} else if !fi.IsDir() {\n\t\treturn errors.Errorf(\"source path must be a directory\")\n\t}\n\n\treturn util.CopyFolderContents(g.Logger, path, dst, SourceManifestName, g.IncludeInCopy, g.ExcludeFromCopy)\n}\n\n// GetFile The original FileGetter already knows how to do file copying so long as we set the Copy flag to true, so just\n// delegate to it\nfunc (g *FileCopyGetter) GetFile(dst string, u *url.URL) error {\n\tunderlying := &getter.FileGetter{Copy: true}\n\tif err := underlying.GetFile(dst, u); err != nil {\n\t\treturn errors.Errorf(\"failed to copy file to %s: %w\", dst, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/runner/run/hook.go",
    "content": "package run\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cloner\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runcfg\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tflint\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/hashicorp/go-multierror\"\n)\n\nconst (\n\tHookCtxTFPathEnvName   = \"TG_CTX_TF_PATH\"\n\tHookCtxCommandEnvName  = \"TG_CTX_COMMAND\"\n\tHookCtxHookNameEnvName = \"TG_CTX_HOOK_NAME\"\n)\n\n// hookErrorMessage extracts command, args and output from the error\n// so users see WHY a hook failed, not just the exit code.\nfunc hookErrorMessage(hookName string, err error) string {\n\tvar processErr util.ProcessExecutionError\n\tif !errors.As(err, &processErr) {\n\t\treturn fmt.Sprintf(\"Hook %q failed to execute: %v\", hookName, err)\n\t}\n\n\texitCode, exitCodeErr := processErr.ExitStatus()\n\tif exitCodeErr != nil {\n\t\treturn fmt.Sprintf(\"Hook %q failed to execute: %v\", hookName, err)\n\t}\n\n\tcmd := strings.Join(append([]string{processErr.Command}, processErr.Args...), \" \")\n\n\toutput := strings.TrimSpace(processErr.Output.Stderr.String())\n\tif output == \"\" {\n\t\toutput = strings.TrimSpace(processErr.Output.Stdout.String())\n\t}\n\n\tif output != \"\" {\n\t\treturn fmt.Sprintf(\"Hook %q (command: %s) exited with non-zero exit code %d:\\n%s\", hookName, cmd, exitCode, output)\n\t}\n\n\treturn fmt.Sprintf(\"Hook %q (command: %s) exited with non-zero exit code %d\", hookName, cmd, exitCode)\n}\n\nfunc processErrorHooks(\n\tctx context.Context,\n\tl log.Logger,\n\thooks []runcfg.ErrorHook,\n\topts *Options,\n\tpreviousExecErrors *errors.MultiError,\n) error {\n\tif len(hooks) == 0 || previousExecErrors.ErrorOrNil() == nil {\n\t\treturn nil\n\t}\n\n\tvar errorsOccured *multierror.Error\n\n\tl.Debugf(\"Detected %d error Hooks\", len(hooks))\n\n\tcustomMultierror := multierror.Error{\n\t\tErrors: previousExecErrors.WrappedErrors(),\n\t\tErrorFormat: func(err []error) string {\n\t\t\terrorMessages := make([]string, 0, len(err))\n\n\t\t\tfor _, e := range err {\n\t\t\t\terrorMessage := e.Error()\n\t\t\t\t// Check if is process execution error and try to extract output\n\t\t\t\t// https://github.com/gruntwork-io/terragrunt/issues/2045\n\t\t\t\toriginalError := errors.Unwrap(e)\n\t\t\t\tif originalError != nil {\n\t\t\t\t\tvar processError util.ProcessExecutionError\n\t\t\t\t\tif ok := errors.As(originalError, &processError); ok {\n\t\t\t\t\t\terrorMessage = fmt.Sprintf(\"%s\\n%s\", processError.Error(), processError.Output.Stdout.String())\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\terrorMessages = append(errorMessages, errorMessage)\n\t\t\t}\n\n\t\t\treturn strings.Join(errorMessages, \"\\n\")\n\t\t},\n\t}\n\terrorMessage := customMultierror.Error()\n\n\tfor _, curHook := range hooks {\n\t\tif util.MatchesAny(curHook.OnErrors, errorMessage) && slices.Contains(curHook.Commands, opts.TerraformCommand) {\n\t\t\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"error_hook_\"+curHook.Name, map[string]any{\n\t\t\t\t\"hook\": curHook.Name,\n\t\t\t\t\"dir\":  curHook.WorkingDir,\n\t\t\t}, func(ctx context.Context) error {\n\t\t\t\tl.Infof(\"Executing hook: %s\", curHook.Name)\n\n\t\t\t\tactionToExecute := curHook.Execute[0]\n\t\t\t\tactionParams := curHook.Execute[1:]\n\t\t\t\thookOpts := optsWithHookEnvs(opts, curHook.Name)\n\n\t\t\t\t_, possibleError := shell.RunCommandWithOutput(\n\t\t\t\t\tctx,\n\t\t\t\t\tl,\n\t\t\t\t\thookOpts.shellRunOptions(),\n\t\t\t\t\tcurHook.WorkingDir,\n\t\t\t\t\tcurHook.SuppressStdout,\n\t\t\t\t\tfalse,\n\t\t\t\t\tactionToExecute, actionParams...,\n\t\t\t\t)\n\t\t\t\tif possibleError != nil {\n\t\t\t\t\tl.Errorf(\"%s\", hookErrorMessage(curHook.Name, possibleError))\n\t\t\t\t\treturn possibleError\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\terrorsOccured = multierror.Append(errorsOccured, err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn errorsOccured.ErrorOrNil()\n}\n\n// ProcessHooks processes a list of hooks, executing each one that matches the current command.\nfunc ProcessHooks(\n\tctx context.Context,\n\tl log.Logger,\n\thooks []runcfg.Hook,\n\topts *Options,\n\tcfg *runcfg.RunConfig,\n\tpreviousExecErrors *errors.MultiError,\n\t_ *report.Report,\n) error {\n\tif len(hooks) == 0 {\n\t\treturn nil\n\t}\n\n\tvar errorsOccured *multierror.Error\n\n\tl.Debugf(\"Detected %d Hooks\", len(hooks))\n\n\tfor i := range hooks {\n\t\tcurHook := &hooks[i]\n\t\tif !curHook.If {\n\t\t\tl.Debugf(\"Skipping hook: %s\", curHook.Name)\n\t\t\tcontinue\n\t\t}\n\n\t\tallPreviousErrors := previousExecErrors.Append(errorsOccured)\n\t\tif shouldRunHook(curHook, opts, allPreviousErrors) {\n\t\t\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hook_\"+curHook.Name, map[string]any{\n\t\t\t\t\"hook\": curHook.Name,\n\t\t\t\t\"dir\":  curHook.WorkingDir,\n\t\t\t}, func(ctx context.Context) error {\n\t\t\t\treturn runHook(ctx, l, opts, cfg, curHook)\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\terrorsOccured = multierror.Append(errorsOccured, err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn errorsOccured.ErrorOrNil()\n}\n\nfunc shouldRunHook(\n\thook *runcfg.Hook,\n\topts *Options,\n\tpreviousExecErrors *errors.MultiError,\n) bool {\n\t// if there's no previous error, execute command\n\t// OR if a previous error DID happen AND we want to run anyways\n\t// then execute.\n\t// Skip execution if there was an error AND we care about errors\n\t//\n\t// resolves: https://github.com/gruntwork-io/terragrunt/issues/459\n\thasErrors := previousExecErrors.ErrorOrNil() != nil\n\tisCommandInHook := slices.Contains(hook.Commands, opts.TerraformCommand)\n\n\treturn isCommandInHook && (!hasErrors || hook.RunOnError)\n}\n\nfunc runHook(\n\tctx context.Context,\n\tl log.Logger,\n\topts *Options,\n\tcfg *runcfg.RunConfig,\n\tcurHook *runcfg.Hook,\n) error {\n\tl.Infof(\"Executing hook: %s\", curHook.Name)\n\n\tworkingDir := curHook.WorkingDir\n\tsuppressStdout := curHook.SuppressStdout\n\n\tactionToExecute := curHook.Execute[0]\n\tactionParams := curHook.Execute[1:]\n\thookOpts := optsWithHookEnvs(opts, curHook.Name)\n\n\tif actionToExecute == \"tflint\" {\n\t\treturn executeTFLint(ctx, l, opts, cfg, curHook, workingDir)\n\t}\n\n\t_, possibleError := shell.RunCommandWithOutput(\n\t\tctx,\n\t\tl,\n\t\thookOpts.shellRunOptions(),\n\t\tworkingDir,\n\t\tsuppressStdout,\n\t\tfalse,\n\t\tactionToExecute, actionParams...,\n\t)\n\tif possibleError != nil {\n\t\tl.Errorf(\"%s\", hookErrorMessage(curHook.Name, possibleError))\n\t}\n\n\treturn possibleError\n}\n\nfunc executeTFLint(\n\tctx context.Context,\n\tl log.Logger,\n\topts *Options,\n\tcfg *runcfg.RunConfig,\n\tcurHook *runcfg.Hook,\n\tworkingDir string,\n) error {\n\t// fetching source code changes lock since tflint is not thread safe\n\trawActualLock, _ := sourceChangeLocks.LoadOrStore(workingDir, &sync.Mutex{})\n\tactualLock := rawActualLock.(*sync.Mutex)\n\n\tactualLock.Lock()\n\tdefer actualLock.Unlock()\n\n\terr := tflint.RunTflintWithOpts(ctx, l, opts.tflintRunOptions(), cfg, curHook)\n\tif err != nil {\n\t\tl.Errorf(\"%s\", hookErrorMessage(curHook.Name, err))\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc optsWithHookEnvs(opts *Options, hookName string) *Options {\n\tnewOpts := *opts\n\tnewOpts.Env = cloner.Clone(opts.Env)\n\tnewOpts.Env[HookCtxTFPathEnvName] = opts.TFPath\n\tnewOpts.Env[HookCtxCommandEnvName] = opts.TerraformCommand\n\tnewOpts.Env[HookCtxHookNameEnvName] = hookName\n\n\treturn &newOpts\n}\n"
  },
  {
    "path": "internal/runner/run/hook_internal_test.go",
    "content": "package run\n\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tflint\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc getExitError(t *testing.T, exitCode int) *exec.ExitError {\n\tt.Helper()\n\n\tcmd := exec.CommandContext(t.Context(), \"sh\", \"-c\", fmt.Sprintf(\"exit %d\", exitCode))\n\terr := cmd.Run()\n\trequire.Error(t, err)\n\n\tvar exitErr *exec.ExitError\n\n\trequire.True(t, errors.As(err, &exitErr))\n\n\treturn exitErr\n}\n\nfunc TestHookErrorMessage_WithStderr(t *testing.T) {\n\tt.Parallel()\n\n\tvar output util.CmdOutput\n\toutput.Stderr.WriteString(\"resource missing required tags\")\n\n\terr := util.ProcessExecutionError{\n\t\tErr:        getExitError(t, 2),\n\t\tCommand:    \"tflint\",\n\t\tArgs:       []string{\"--config\", \".tflint.hcl\"},\n\t\tWorkingDir: \"/tmp\",\n\t\tOutput:     output,\n\t}\n\n\tmsg := hookErrorMessage(\"my-lint\", errors.New(err))\n\tassert.Contains(t, msg, `Hook \"my-lint\"`)\n\tassert.Contains(t, msg, \"tflint --config .tflint.hcl\")\n\tassert.Contains(t, msg, \"non-zero exit code 2\")\n\tassert.Contains(t, msg, \"resource missing required tags\")\n}\n\nfunc TestHookErrorMessage_StdoutFallback(t *testing.T) {\n\tt.Parallel()\n\n\tvar output util.CmdOutput\n\toutput.Stdout.WriteString(\"warning: deprecated feature\")\n\n\terr := util.ProcessExecutionError{\n\t\tErr:        getExitError(t, 1),\n\t\tCommand:    \"custom-lint\",\n\t\tArgs:       []string{\"--fix\"},\n\t\tWorkingDir: \"/tmp\",\n\t\tOutput:     output,\n\t}\n\n\tmsg := hookErrorMessage(\"lint-hook\", errors.New(err))\n\tassert.Contains(t, msg, `Hook \"lint-hook\"`)\n\tassert.Contains(t, msg, \"custom-lint --fix\")\n\tassert.Contains(t, msg, \"non-zero exit code 1\")\n\tassert.Contains(t, msg, \"warning: deprecated feature\")\n}\n\nfunc TestHookErrorMessage_NoOutput(t *testing.T) {\n\tt.Parallel()\n\n\terr := util.ProcessExecutionError{\n\t\tErr:        getExitError(t, 3),\n\t\tCommand:    \"check\",\n\t\tArgs:       []string{\"-strict\"},\n\t\tWorkingDir: \"/tmp\",\n\t}\n\n\tmsg := hookErrorMessage(\"my-hook\", errors.New(err))\n\tassert.Contains(t, msg, `Hook \"my-hook\"`)\n\tassert.Contains(t, msg, \"check -strict\")\n\tassert.Contains(t, msg, \"non-zero exit code 3\")\n}\n\nfunc TestHookErrorMessage_TflintWrapped(t *testing.T) {\n\tt.Parallel()\n\n\tvar output util.CmdOutput\n\toutput.Stderr.WriteString(\"3 issue(s) found\")\n\n\tprocessErr := util.ProcessExecutionError{\n\t\tErr:        getExitError(t, 2),\n\t\tCommand:    \"tflint\",\n\t\tArgs:       []string{\"--config\", \".tflint.hcl\"},\n\t\tWorkingDir: \"/tmp\",\n\t\tOutput:     output,\n\t}\n\n\t// Simulate the real tflint error chain: ErrorRunningTflint wraps ProcessExecutionError\n\ttflintErr := tflint.ErrorRunningTflint{\n\t\tArgs: []string{\"tflint\", \"--config\", \".tflint.hcl\"},\n\t\tErr:  errors.New(processErr),\n\t}\n\n\tmsg := hookErrorMessage(\"tflint\", errors.New(tflintErr))\n\tassert.Contains(t, msg, `Hook \"tflint\"`)\n\tassert.Contains(t, msg, \"tflint --config .tflint.hcl\")\n\tassert.Contains(t, msg, \"non-zero exit code 2\")\n\tassert.Contains(t, msg, \"3 issue(s) found\")\n}\n\nfunc TestHookErrorMessage_NonProcessError(t *testing.T) {\n\tt.Parallel()\n\n\terr := errors.New(\"exec: \\\"tflint\\\": executable file not found in $PATH\")\n\n\tmsg := hookErrorMessage(\"my-hook\", err)\n\tassert.Equal(t, `Hook \"my-hook\" failed to execute: exec: \"tflint\": executable file not found in $PATH`, msg)\n}\n"
  },
  {
    "path": "internal/runner/run/options.go",
    "content": "package run\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/puzpuzpuz/xsync/v3\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cloner\"\n\t\"github.com/gruntwork-io/terragrunt/internal/engine\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errorconfig\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iam\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict\"\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tfimpl\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tflint\"\n\t\"github.com/gruntwork-io/terragrunt/internal/writer\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\"\n)\n\nconst (\n\tdefaultTFDataDir   = \".terraform\"\n\tdefaultSignalsFile = \"error-signals.json\"\n)\n\n// Options contains the configuration needed by run.Run and its helpers.\n// This is a focused subset of options.TerragruntOptions.\ntype Options struct {\n\tTerraformCliArgs             *iacargs.IacArgs\n\tEngineConfig                 *engine.EngineConfig\n\tEngineOptions                *engine.EngineOptions\n\tErrors                       *errorconfig.Config\n\tFeatureFlags                 *xsync.MapOf[string, string]\n\tTelemetry                    *telemetry.Options\n\tSourceMap                    map[string]string\n\tEnv                          map[string]string\n\tWriters                      writer.Writers\n\tTFPath                       string\n\tTerraformCommand             string\n\tTofuImplementation           tfimpl.Type\n\tTerragruntConfigPath         string\n\tOriginalTerragruntConfigPath string\n\tWorkingDir                   string\n\tDownloadDir                  string\n\tRootWorkingDir               string\n\tOriginalTerraformCommand     string\n\tSource                       string\n\tAuthProviderCmd              string\n\tOriginalIAMRoleOptions       iam.RoleOptions\n\tIAMRoleOptions               iam.RoleOptions\n\tExperiments                  experiment.Experiments\n\tStrictControls               strict.Controls\n\tMaxFoldersToCheck            int\n\tAutoRetry                    bool\n\tHeadless                     bool\n\tNonInteractive               bool\n\tDebug                        bool\n\tAutoInit                     bool\n\tJSONLogFormat                bool\n\tBackendBootstrap             bool\n\tFailIfBucketCreationRequired bool\n\tDisableBucketUpdate          bool\n\tSourceUpdate                 bool\n\tForwardTFStdout              bool\n}\n\n// Clone performs a deep copy of Options.\nfunc (o *Options) Clone() *Options {\n\treturn cloner.Clone(o)\n}\n\n// CloneWithConfigPath creates a copy of Options with updated config path and working directory.\nfunc (o *Options) CloneWithConfigPath(l log.Logger, configPath string) (log.Logger, *Options, error) {\n\tnewOpts := o.Clone()\n\n\tconfigPath = filepath.Clean(configPath)\n\tif !filepath.IsAbs(configPath) {\n\t\tabsConfigPath, err := filepath.Abs(configPath)\n\t\tif err != nil {\n\t\t\treturn l, nil, err\n\t\t}\n\n\t\tconfigPath = filepath.Clean(absConfigPath)\n\t}\n\n\tworkingDir := filepath.Dir(configPath)\n\n\tif workingDir != o.WorkingDir {\n\t\tl = l.WithField(placeholders.WorkDirKeyName, workingDir)\n\t}\n\n\tnewOpts.TerragruntConfigPath = configPath\n\tnewOpts.WorkingDir = workingDir\n\n\treturn l, newOpts, nil\n}\n\n// InsertTerraformCliArgs inserts the given args after the terraform command argument.\nfunc (o *Options) InsertTerraformCliArgs(argsToInsert ...string) {\n\tif o.TerraformCliArgs == nil {\n\t\to.TerraformCliArgs = iacargs.New()\n\t}\n\n\tparsed := iacargs.New(argsToInsert...)\n\n\to.TerraformCliArgs.InsertFlag(0, parsed.Flags...)\n\n\t// Handle command field\n\tswitch {\n\tcase o.TerraformCliArgs.Command == \"\":\n\t\to.TerraformCliArgs.Command = parsed.Command\n\tcase parsed.Command == \"\" || parsed.Command == o.TerraformCliArgs.Command:\n\t\t// no-op\n\tcase iacargs.IsKnownSubCommand(parsed.Command):\n\t\to.TerraformCliArgs.SubCommand = []string{parsed.Command}\n\tdefault:\n\t\to.TerraformCliArgs.InsertArguments(0, parsed.Command)\n\t}\n\n\tif len(parsed.SubCommand) > 0 {\n\t\to.TerraformCliArgs.SubCommand = parsed.SubCommand\n\t}\n\n\to.TerraformCliArgs.InsertArguments(0, parsed.Arguments...)\n}\n\n// AppendTerraformCliArgs appends the given args after the current TerraformCliArgs.\nfunc (o *Options) AppendTerraformCliArgs(argsToAppend ...string) {\n\tif o.TerraformCliArgs == nil {\n\t\to.TerraformCliArgs = iacargs.New()\n\t}\n\n\tparsed := iacargs.New(argsToAppend...)\n\n\to.TerraformCliArgs.AppendFlag(parsed.Flags...)\n\n\tif parsed.Command != \"\" {\n\t\to.TerraformCliArgs.AppendArgument(parsed.Command)\n\t}\n\n\to.TerraformCliArgs.AppendArgument(parsed.Arguments...)\n\n\tif len(parsed.SubCommand) > 0 {\n\t\to.TerraformCliArgs.SubCommand = parsed.SubCommand\n\t}\n}\n\n// TerraformDataDir returns Terraform data directory (.terraform by default, overridden by $TF_DATA_DIR envvar)\nfunc (o *Options) TerraformDataDir() string {\n\tif tfDataDir, ok := o.Env[\"TF_DATA_DIR\"]; ok {\n\t\treturn tfDataDir\n\t}\n\n\treturn defaultTFDataDir\n}\n\n// DataDir returns the Terraform data directory prepended with the working directory path.\nfunc (o *Options) DataDir() string {\n\ttfDataDir := o.TerraformDataDir()\n\tif filepath.IsAbs(tfDataDir) {\n\t\treturn tfDataDir\n\t}\n\n\treturn filepath.Join(o.WorkingDir, tfDataDir)\n}\n\n// shellRunOptions builds a *shell.ShellOptions from this Options.\nfunc (o *Options) shellRunOptions() *shell.ShellOptions {\n\treturn &shell.ShellOptions{\n\t\tWriters:         o.Writers,\n\t\tWorkingDir:      o.WorkingDir,\n\t\tEnv:             o.Env,\n\t\tTFPath:          o.TFPath,\n\t\tEngineConfig:    o.EngineConfig,\n\t\tEngineOptions:   o.EngineOptions,\n\t\tExperiments:     o.Experiments,\n\t\tTelemetry:       o.Telemetry,\n\t\tRootWorkingDir:  o.RootWorkingDir,\n\t\tHeadless:        o.Headless,\n\t\tForwardTFStdout: o.ForwardTFStdout,\n\t}\n}\n\n// tfRunOptions builds a *tf.TFOptions from this Options.\nfunc (o *Options) tfRunOptions() *tf.TFOptions {\n\treturn &tf.TFOptions{\n\t\tJSONLogFormat:                o.JSONLogFormat,\n\t\tOriginalTerragruntConfigPath: o.OriginalTerragruntConfigPath,\n\t\tTerragruntConfigPath:         o.TerragruntConfigPath,\n\t\tTofuImplementation:           o.TofuImplementation,\n\t\tTerraformCliArgs:             o.TerraformCliArgs,\n\t\tShellOptions:                 o.shellRunOptions(),\n\t}\n}\n\n// remoteStateOpts builds a *remotestate.Options from this Options.\nfunc (o *Options) remoteStateOpts() *remotestate.Options {\n\treturn &remotestate.Options{\n\t\tOptions: backend.Options{\n\t\t\tWriters:                      o.Writers,\n\t\t\tEnv:                          o.Env,\n\t\t\tIAMRoleOptions:               o.IAMRoleOptions,\n\t\t\tNonInteractive:               o.NonInteractive,\n\t\t\tFailIfBucketCreationRequired: o.FailIfBucketCreationRequired,\n\t\t},\n\t\tTFRunOpts:           o.tfRunOptions(),\n\t\tDisableBucketUpdate: o.DisableBucketUpdate,\n\t}\n}\n\n// tflintRunOptions builds a *tflint.TFLintOptions from this Options.\nfunc (o *Options) tflintRunOptions() *tflint.TFLintOptions {\n\treturn &tflint.TFLintOptions{\n\t\tShellOptions:         o.shellRunOptions(),\n\t\tWriters:              o.Writers,\n\t\tWorkingDir:           o.WorkingDir,\n\t\tRootWorkingDir:       o.RootWorkingDir,\n\t\tTerragruntConfigPath: o.TerragruntConfigPath,\n\t\tMaxFoldersToCheck:    o.MaxFoldersToCheck,\n\t}\n}\n\n// RunWithErrorHandling runs the given operation and handles errors according to the configuration.\nfunc (o *Options) RunWithErrorHandling(\n\tctx context.Context,\n\tl log.Logger,\n\tr *report.Report,\n\toperation func() error,\n) error {\n\tif o.Errors == nil {\n\t\treturn operation()\n\t}\n\n\tcurrentAttempt := 1\n\n\treportWorkingDir := o.WorkingDir\n\tif o.OriginalTerragruntConfigPath != \"\" {\n\t\treportWorkingDir = filepath.Dir(o.OriginalTerragruntConfigPath)\n\t}\n\n\treportDir := filepath.Clean(reportWorkingDir)\n\n\tfor {\n\t\terr := operation()\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\taction, recoveryErr := o.Errors.AttemptErrorRecovery(l, err, currentAttempt)\n\t\tif recoveryErr != nil {\n\t\t\tvar maxAttemptsReachedError *errorconfig.MaxAttemptsReachedError\n\t\t\tif errors.As(recoveryErr, &maxAttemptsReachedError) {\n\t\t\t\treturn maxAttemptsReachedError\n\t\t\t}\n\n\t\t\treturn fmt.Errorf(\"encountered error while attempting error recovery: %w\", recoveryErr)\n\t\t}\n\n\t\tif action == nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif action.ShouldIgnore {\n\t\t\tl.Warnf(\"Ignoring error, reason: %s\", action.IgnoreMessage)\n\n\t\t\tif len(action.IgnoreSignals) > 0 {\n\t\t\t\tif err := o.handleIgnoreSignals(l, action.IgnoreSignals); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\trun, err := r.EnsureRun(l, reportDir)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := r.EndRun(\n\t\t\t\tl,\n\t\t\t\trun.Path,\n\t\t\t\treport.WithResult(report.ResultSucceeded),\n\t\t\t\treport.WithReason(report.ReasonErrorIgnored),\n\t\t\t\treport.WithCauseIgnoreBlock(action.IgnoreBlockName),\n\t\t\t); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\n\t\tif action.ShouldRetry {\n\t\t\tif !o.AutoRetry {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tl.Warnf(\n\t\t\t\t\"Encountered retryable error: %s\\nAttempt %d of %d. Waiting %d second(s) before retrying...\",\n\t\t\t\taction.RetryBlockName,\n\t\t\t\tcurrentAttempt,\n\t\t\t\taction.RetryAttempts,\n\t\t\t\taction.RetrySleepSecs,\n\t\t\t)\n\n\t\t\trun, err := r.EnsureRun(l, reportDir)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := r.EndRun(\n\t\t\t\tl,\n\t\t\t\trun.Path,\n\t\t\t\treport.WithResult(report.ResultSucceeded),\n\t\t\t\treport.WithReason(report.ReasonRetrySucceeded),\n\t\t\t\treport.WithCauseRetryBlock(action.RetryBlockName),\n\t\t\t); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tselect {\n\t\t\tcase <-time.After(time.Duration(action.RetrySleepSecs) * time.Second):\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn errors.New(ctx.Err())\n\t\t\t}\n\n\t\t\tcurrentAttempt++\n\n\t\t\tcontinue\n\t\t}\n\n\t\treturn err\n\t}\n}\n\nfunc (o *Options) handleIgnoreSignals(l log.Logger, signals map[string]any) error {\n\tsignalsFile := filepath.Join(o.WorkingDir, defaultSignalsFile)\n\n\tsignalsJSON, err := json.MarshalIndent(signals, \"\", \"  \")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tconst ownerPerms = 0644\n\n\tl.Warnf(\"Writing error signals to %s\", signalsFile)\n\n\tif err := os.WriteFile(signalsFile, signalsJSON, ownerPerms); err != nil {\n\t\treturn fmt.Errorf(\"failed to write signals file %s: %w\", signalsFile, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/runner/run/prepare_internal_test.go",
    "content": "package run\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runcfg\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestPrepareInitCommandRunCfg verifies that prepareInitCommandRunCfg inserts\n// the correct CLI args for various remote_state configurations.\n//\n// Bootstrap skip behavior (DisableInit=true preventing bootstrap even when\n// BackendBootstrap=true) is tested by:\n//   - TestNeedsBootstrapDisableInit in internal/remotestate/remote_state_test.go\n//   - TestAwsDisableInitS3Backend in test/integration_aws_test.go\nfunc TestPrepareInitCommandRunCfg(t *testing.T) {\n\tt.Parallel()\n\n\ts3Config := backend.Config{\n\t\t\"bucket\": \"test-bucket\",\n\t\t\"key\":    \"test.tfstate\",\n\t\t\"region\": \"us-east-1\",\n\t}\n\n\ttestCases := []struct {\n\t\tremoteStateCfg    *remotestate.Config\n\t\tname              string\n\t\tbackendBootstrap  bool\n\t\texpectBackendArgs bool\n\t}{\n\t\t{\n\t\t\tname:              \"nil remote state config - no args inserted\",\n\t\t\tremoteStateCfg:    nil,\n\t\t\tbackendBootstrap:  false,\n\t\t\texpectBackendArgs: false,\n\t\t},\n\t\t{\n\t\t\tname: \"disable_init=false, bootstrap=false - backend-config args inserted\",\n\t\t\tremoteStateCfg: &remotestate.Config{\n\t\t\t\tBackendName:   \"s3\",\n\t\t\t\tDisableInit:   false,\n\t\t\t\tBackendConfig: s3Config,\n\t\t\t},\n\t\t\tbackendBootstrap:  false,\n\t\t\texpectBackendArgs: true,\n\t\t},\n\t\t{\n\t\t\tname: \"disable_init=true, bootstrap=false - backend-config args inserted\",\n\t\t\tremoteStateCfg: &remotestate.Config{\n\t\t\t\tBackendName:   \"s3\",\n\t\t\t\tDisableInit:   true,\n\t\t\t\tBackendConfig: s3Config,\n\t\t\t},\n\t\t\tbackendBootstrap:  false,\n\t\t\texpectBackendArgs: true,\n\t\t},\n\t\t{\n\t\t\tname: \"disable_init=true, bootstrap=true - backend-config args inserted\",\n\t\t\tremoteStateCfg: &remotestate.Config{\n\t\t\t\tBackendName:   \"s3\",\n\t\t\t\tDisableInit:   true,\n\t\t\t\tBackendConfig: s3Config,\n\t\t\t},\n\t\t\tbackendBootstrap:  true,\n\t\t\texpectBackendArgs: true,\n\t\t},\n\t\t{\n\t\t\t// When generate is set, backend config goes into the generated .tf file,\n\t\t\t// not via -backend-config= CLI args.\n\t\t\tname: \"disable_init=true, generate=true - no backend-config args\",\n\t\t\tremoteStateCfg: &remotestate.Config{\n\t\t\t\tBackendName:   \"s3\",\n\t\t\t\tDisableInit:   true,\n\t\t\t\tGenerate:      &remotestate.ConfigGenerate{Path: \"backend.tf\", IfExists: \"overwrite\"},\n\t\t\t\tBackendConfig: s3Config,\n\t\t\t},\n\t\t\tbackendBootstrap:  false,\n\t\t\texpectBackendArgs: false,\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.Parallel()\n\n\t\t\topts := &Options{\n\t\t\t\tBackendBootstrap: tc.backendBootstrap,\n\t\t\t\tTerraformCliArgs: iacargs.New(),\n\t\t\t}\n\n\t\t\tvar cfg runcfg.RunConfig\n\t\t\tif tc.remoteStateCfg != nil {\n\t\t\t\tcfg.RemoteState = *remotestate.New(tc.remoteStateCfg)\n\t\t\t}\n\n\t\t\terr := prepareInitCommandRunCfg(t.Context(), logger.CreateLogger(), opts, &cfg)\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\tallArgs := opts.TerraformCliArgs.Slice()\n\t\t\tif tc.expectBackendArgs {\n\t\t\t\tassert.NotContains(t, allArgs, \"-backend=false\", \"disable_init should not pass -backend=false to terraform\")\n\n\t\t\t\thasBackendConfig := false\n\n\t\t\t\tfor _, f := range allArgs {\n\t\t\t\t\tif strings.HasPrefix(f, \"-backend-config=\") {\n\t\t\t\t\t\thasBackendConfig = true\n\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tassert.True(t, hasBackendConfig, \"expected -backend-config= flag in CLI args, got: %v\", allArgs)\n\t\t\t} else {\n\t\t\t\tassert.Empty(t, allArgs, \"expected no CLI args, got: %v\", allArgs)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/runner/run/run.go",
    "content": "// Package run provides the main entry point for running orchestrated runs.\n//\n// These runs are typically OpenTofu/Terraform invocations, but they might be other commands as well.\npackage run\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/codegen\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iam\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds/providers/amazonsts\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runcfg\"\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\n\t\"github.com/hashicorp/go-multierror\"\n)\n\nconst (\n\tCommandNameTerragruntReadConfig = \"terragrunt-read-config\"\n\tNullTFVarsFile                  = \".terragrunt-null-vars.auto.tfvars.json\"\n)\n\nvar TerraformCommandsThatUseState = []string{\n\t\"init\",\n\t\"apply\",\n\t\"destroy\",\n\t\"env\",\n\t\"import\",\n\t\"graph\",\n\t\"output\",\n\t\"plan\",\n\t\"push\",\n\t\"refresh\",\n\t\"show\",\n\t\"taint\",\n\t\"untaint\",\n\t\"validate\",\n\t\"force-unlock\",\n\t\"state\",\n}\n\n// TerraformCommandsThatDoNotNeedInit is a list of Terraform commands that do not require 'terraform init' to be executed.\nvar TerraformCommandsThatDoNotNeedInit = []string{\n\t\"version\",\n\n\t// The engine command is a special command that engines can implement to provide additional functionality.\n\t// It's not part of the OpenTofu/Terraform CLI API, so we know that we don't consistently need to run 'init'.\n\t// Engines can decide to selectively perform inits based on the logic of their engine commands.\n\t\"engine\",\n}\n\nvar ModuleRegex = regexp.MustCompile(`module[[:blank:]]+\".+\"`)\n\n// sourceChangeLocks is a map that keeps track of locks for source changes, to ensure we aren't overriding the generated\n// code while another hook (e.g. `tflint`) is running. We use sync.Map to ensure atomic updates during concurrent access.\nvar sourceChangeLocks = sync.Map{}\n\n// Run downloads terraform source if necessary, then runs terraform with the given options and CLI args.\n// This will forward all the args and extra_arguments directly to Terraform.\nfunc Run(\n\tctx context.Context,\n\tl log.Logger,\n\topts *Options,\n\tr *report.Report,\n\tcfg *runcfg.RunConfig,\n\tcredsGetter *creds.Getter,\n) error {\n\tengine, err := cfg.EngineOptions()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\topts.EngineConfig = engine\n\n\terrConfig, err := cfg.ErrorsConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Only overwrite when the config actually defines error rules;\n\t// otherwise preserve the built-in default retryable errors.\n\tif errConfig != nil {\n\t\topts.Errors = errConfig\n\t}\n\n\tl, terragruntOptionsClone, err := opts.CloneWithConfigPath(l, opts.TerragruntConfigPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tterragruntOptionsClone.TerraformCommand = CommandNameTerragruntReadConfig\n\n\tif err = terragruntOptionsClone.RunWithErrorHandling(ctx, l, r, func() error {\n\t\treturn ProcessHooks(ctx, l, cfg.Terraform.AfterHooks, terragruntOptionsClone, cfg, nil, r)\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\t// We merge the OriginalIAMRoleOptions into the one from the config, because the CLI passed IAMRoleOptions has\n\t// precedence.\n\topts.IAMRoleOptions = iam.MergeRoleOptions(\n\t\tcfg.GetIAMRoleOptions(),\n\t\topts.OriginalIAMRoleOptions,\n\t)\n\n\tif err = opts.RunWithErrorHandling(ctx, l, r, func() error {\n\t\treturn credsGetter.ObtainAndUpdateEnvIfNecessary(ctx, l, opts.Env, amazonsts.NewProvider(l, opts.IAMRoleOptions, opts.Env))\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\t// get the default download dir\n\t_, defaultDownloadDir := util.DefaultWorkingAndDownloadDirs(opts.TerragruntConfigPath)\n\n\t// if the download dir hasn't been changed from default, and is set in the config,\n\t// then use it\n\tif opts.DownloadDir == defaultDownloadDir && cfg.DownloadDir != \"\" {\n\t\topts.DownloadDir = cfg.DownloadDir\n\t}\n\n\tupdatedOpts := opts\n\n\tsourceURL, err := runcfg.GetTerraformSourceURL(opts.Source, opts.SourceMap, opts.OriginalTerragruntConfigPath, cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Always download/copy source to cache directory for consistency.\n\t// When no source is specified, sourceURL will be \".\" (current directory).\n\terr = telemetry.TelemeterFromContext(ctx).Collect(ctx, \"download_terraform_source\", map[string]any{\n\t\t\"sourceUrl\": sourceURL,\n\t}, func(ctx context.Context) error {\n\t\tupdatedOpts, err = DownloadTerraformSource(ctx, l, sourceURL, opts, cfg, r)\n\t\treturn err\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Handle code generation configs, both generate blocks and generate attribute of remote_state.\n\t// Note that relative paths are relative to the terragrunt working dir (where terraform is called).\n\tif err = GenerateConfig(l, updatedOpts, cfg); err != nil {\n\t\treturn err\n\t}\n\n\t// We do the debug file generation here, after all the terragrunt generated terraform files are created so that we\n\t// can ensure the tfvars json file only includes the vars that are defined in the module.\n\tif updatedOpts.Debug {\n\t\tif err := WriteTerragruntDebugFile(l, updatedOpts, cfg); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif err := CheckFolderContainsTerraformCode(updatedOpts); err != nil {\n\t\treturn err\n\t}\n\n\tif err := opts.RunWithErrorHandling(ctx, l, r, func() error {\n\t\treturn runTerragruntWithConfig(ctx, l, opts, updatedOpts, cfg, r)\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// GenerateConfig handles code generation using config types (for backwards compatibility).\nfunc GenerateConfig(l log.Logger, opts *Options, cfg *runcfg.RunConfig) error {\n\trawActualLock, _ := sourceChangeLocks.LoadOrStore(opts.DownloadDir, &sync.Mutex{})\n\n\tactualLock := rawActualLock.(*sync.Mutex)\n\tactualLock.Lock()\n\tdefer actualLock.Unlock()\n\n\tfor _, genCfg := range cfg.GenerateConfigs {\n\t\tif err := codegen.WriteToFile(l, opts.WorkingDir, &genCfg); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif cfg.RemoteState.Config != nil && cfg.RemoteState.Generate != nil {\n\t\tif err := cfg.RemoteState.GenerateOpenTofuCode(l, opts.WorkingDir); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else if cfg.RemoteState.Config != nil {\n\t\t// We use else if here because we don't need to check the backend configuration is defined when the remote state\n\t\t// block has a `generate` attribute configured.\n\t\tif err := checkTerraformCodeDefinesBackend(opts, cfg.RemoteState.BackendName); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Runs tofu/terraform with the given options and CLI args.\n// This will forward all the args and extra_arguments directly to Terraform.\n//\n// This function takes in the \"original\" options which has the unmodified 'WorkingDir' from before downloading the code from the source URL,\n// and the \"updated\" options that will contain the updated 'WorkingDir' into which the code has been downloaded\nfunc runTerragruntWithConfig(\n\tctx context.Context,\n\tl log.Logger,\n\toriginalOpts *Options,\n\topts *Options,\n\tcfg *runcfg.RunConfig,\n\tr *report.Report,\n) error {\n\tif cfg.Exclude.ShouldPreventRun(opts.TerraformCommand) {\n\t\tl.Infof(\"Early exit in terragrunt unit %s due to exclude block with no_run = true\", opts.WorkingDir)\n\n\t\treturn nil\n\t}\n\n\tif len(cfg.Terraform.ExtraArgs) > 0 {\n\t\targs := FilterTerraformExtraArgs(l, opts, cfg)\n\n\t\topts.InsertTerraformCliArgs(args...)\n\n\t\tmaps.Copy(opts.Env, filterTerraformEnvVarsFromExtraArgsRunCfg(opts, cfg))\n\t}\n\n\tif err := SetTerragruntInputsAsEnvVars(l, opts, cfg); err != nil {\n\t\treturn err\n\t}\n\n\tif opts.TerraformCliArgs.First() == tf.CommandNameInit {\n\t\tif err := prepareInitCommandRunCfg(ctx, l, opts, cfg); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tif err := PrepareNonInitCommand(ctx, l, originalOpts, opts, cfg, r); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Write null-valued inputs to a tfvars.json file that OpenTofu/Terraform will auto-load.\n\tnullVarsFile, err := setTerragruntNullValuesRunCfg(opts, cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer func() {\n\t\tif nullVarsFile != \"\" {\n\t\t\tif removeErr := os.Remove(nullVarsFile); removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {\n\t\t\t\tl.Debugf(\"Failed to remove null values file %s: %v\", nullVarsFile, removeErr)\n\t\t\t}\n\t\t}\n\t}()\n\n\t// Now that we've run 'init' and have all the source code locally, we can finally run the patch command\n\tif err := checkProtectedModuleRunCfg(opts, cfg); err != nil {\n\t\treturn err\n\t}\n\n\treturn RunActionWithHooks(ctx, l, \"terraform\", opts, cfg, r, func(childCtx context.Context) error {\n\t\t// Execute the underlying command once; retries and ignores are handled by outer RunWithErrorHandling\n\t\tout, runTerraformError := tf.RunCommandWithOutput(childCtx, l, opts.tfRunOptions(), opts.TerraformCliArgs.Slice()...)\n\n\t\tvar lockFileError error\n\t\tif ShouldCopyLockFile(opts.TerraformCliArgs, &cfg.Terraform) {\n\t\t\t// Copy the lock file from the Terragrunt working dir (e.g., .terragrunt-cache/xxx/<some-module>) to the\n\t\t\t// user's working dir (e.g., /live/stage/vpc). That way, the lock file will end up right next to the user's\n\t\t\t// terragrunt.hcl and can be checked into version control. Note that in the past, Terragrunt allowed the\n\t\t\t// user's working dir to be different than the directory where the terragrunt.hcl file lived, so just in\n\t\t\t// case, we are using the user's working dir here, rather than just looking at the parent dir of the\n\t\t\t// terragrunt.hcl. However, the default value for the user's working dir, set in options.go, IS just the\n\t\t\t// parent dir of terragrunt.hcl, so these will likely always be the same.\n\t\t\t// Use directory from OriginalTerragruntConfigPath to copy locks since WorkingDir point to cache directory\n\t\t\tlockFileError = runcfg.CopyLockFile(l, opts.RootWorkingDir, opts.Writers.LogShowAbsPaths, opts.WorkingDir, filepath.Dir(opts.OriginalTerragruntConfigPath))\n\t\t}\n\n\t\t// If command failed, log a helpful message\n\t\tif runTerraformError != nil {\n\t\t\tif out == nil {\n\t\t\t\tl.Errorf(\"%s invocation failed in %s\", opts.TofuImplementation, opts.WorkingDir)\n\t\t\t}\n\t\t}\n\n\t\treturn multierror.Append(runTerraformError, lockFileError).ErrorOrNil()\n\t})\n}\n\n// ShouldCopyLockFile verifies if the lock file should be copied to the user's working directory\n// Terraform 0.14 now manages a lock file for providers. This can be updated\n// in three ways:\n// * `terraform init` in a module where no `.terraform.lock.hcl` exists\n// * `terraform init -upgrade`\n// * `terraform providers lock`\n//\n// In any of these cases, terragrunt should attempt to copy the generated\n// `.terraform.lock.hcl`\n//\n// terraform init is not guaranteed to pull all checksums depending on platforms,\n// if you already have the provider requested in a cache, or if you are using a mirror.\n// There are lots of details at [hashicorp/terraform#27264](https://github.com/hashicorp/terraform/issues/27264#issuecomment-743389837)\n// The `providers lock` sub command enables you to ensure that the lock file is\n// fully populated.\nfunc ShouldCopyLockFile(args *iacargs.IacArgs, terraformConfig *runcfg.TerraformConfig) bool {\n\t// If the user has explicitly set NoCopyTerraformLockFile to true, then we should not copy the lock file on any command\n\t// This is useful for users who want to manage the lock file themselves outside the working directory\n\tif terraformConfig != nil && terraformConfig.NoCopyTerraformLockFile {\n\t\treturn false\n\t}\n\n\tif args.First() == tf.CommandNameInit {\n\t\treturn true\n\t}\n\n\tif args.First() == tf.CommandNameProviders && args.Second() == tf.CommandNameLock {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// RunActionWithHooks runs the given action function surrounded by hooks. That is, run the before hooks first, then, if there were no\n// errors, run the action, and finally, run the after hooks. Return any errors hit from the hooks or action.\nfunc RunActionWithHooks(\n\tctx context.Context,\n\tl log.Logger,\n\tdescription string,\n\topts *Options,\n\tcfg *runcfg.RunConfig,\n\tr *report.Report,\n\taction func(ctx context.Context) error,\n) error {\n\tvar allErrors *errors.MultiError\n\n\tbeforeHookErrors := ProcessHooks(ctx, l, cfg.Terraform.BeforeHooks, opts, cfg, allErrors, r)\n\tallErrors = allErrors.Append(beforeHookErrors)\n\n\tvar actionErrors error\n\tif beforeHookErrors == nil {\n\t\tactionErrors = action(ctx)\n\t\tallErrors = allErrors.Append(actionErrors)\n\t} else {\n\t\tl.Errorf(\"Errors encountered running before_hooks. Not running '%s'.\", description)\n\t}\n\n\tpostHookErrors := ProcessHooks(ctx, l, cfg.Terraform.AfterHooks, opts, cfg, allErrors, r)\n\terrorHookErrors := processErrorHooks(ctx, l, cfg.Terraform.ErrorHooks, opts, allErrors)\n\tallErrors = allErrors.Append(postHookErrors, errorHookErrors)\n\n\treturn allErrors.ErrorOrNil()\n}\n\n// SetTerragruntInputsAsEnvVars sets the inputs from Terragrunt configurations to TF_VAR_* environment variables for\n// OpenTofu/Terraform.\nfunc SetTerragruntInputsAsEnvVars(l log.Logger, opts *Options, cfg *runcfg.RunConfig) error {\n\tasEnvVars, err := ToTerraformEnvVars(l, cfg.Inputs)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif opts.Env == nil {\n\t\topts.Env = map[string]string{}\n\t}\n\n\tfor key, value := range asEnvVars {\n\t\t// Don't override any env vars the user has already set\n\t\tif _, envVarAlreadySet := opts.Env[key]; !envVarAlreadySet {\n\t\t\topts.Env[key] = value\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// CheckFolderContainsTerraformCode checks if the folder contains Terraform/OpenTofu code\nfunc CheckFolderContainsTerraformCode(opts *Options) error {\n\tfound, err := util.DirContainsTFFiles(opts.WorkingDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !found {\n\t\treturn errors.New(NoTerraformFilesFound(opts.WorkingDir))\n\t}\n\n\treturn nil\n}\n\n// Check that the specified Terraform code defines a backend { ... } block and return an error if doesn't\nfunc checkTerraformCodeDefinesBackend(opts *Options, backendType string) error {\n\tterraformBackendRegexp, err := regexp.Compile(fmt.Sprintf(`backend[[:blank:]]+\"%s\"`, backendType))\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t// Check for backend definitions in .tf and .tofu files using WalkDir\n\tdefinesBackend, err := util.RegexFoundInTFFiles(opts.WorkingDir, terraformBackendRegexp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif definesBackend {\n\t\treturn nil\n\t}\n\n\tterraformJSONBackendRegexp, err := regexp.Compile(fmt.Sprintf(`(?m)\"backend\":[[:space:]]*{[[:space:]]*\"%s\"`, backendType))\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tdefinesJSONBackend, err := util.Grep(terraformJSONBackendRegexp, opts.WorkingDir+\"/**/*.tf.json\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif definesJSONBackend {\n\t\treturn nil\n\t}\n\n\treturn errors.New(BackendNotDefined{ConfigPath: opts.TerragruntConfigPath, WorkingDir: opts.WorkingDir, BackendType: backendType})\n}\n\n// Returns true if we need to run `terraform init` to download providers\nfunc providersNeedInit(opts *Options) bool {\n\tpluginsPath := filepath.Join(opts.DataDir(), \"plugins\")\n\tprovidersPath := filepath.Join(opts.DataDir(), \"providers\")\n\tterraformLockPath := filepath.Join(opts.WorkingDir, tf.TerraformLockFile)\n\n\treturn (!util.FileExists(pluginsPath) && !util.FileExists(providersPath)) || !util.FileExists(terraformLockPath)\n}\n\nfunc prepareInitOptions(l log.Logger, opts *Options) (log.Logger, *Options, error) {\n\t// Need to clone the options, so the TerraformCliArgs can be configured to run the init command\n\tl, initOptions, err := opts.CloneWithConfigPath(l, opts.TerragruntConfigPath)\n\tif err != nil {\n\t\treturn l, nil, err\n\t}\n\n\tinitOptions.TerraformCliArgs = iacargs.New().SetCommand(tf.CommandNameInit)\n\tinitOptions.WorkingDir = opts.WorkingDir\n\tinitOptions.TerraformCommand = tf.CommandNameInit\n\tinitOptions.Headless = true\n\n\tinitOutputForCommands := []string{tf.CommandNamePlan, tf.CommandNameApply}\n\tterraformCommand := opts.TerraformCliArgs.First()\n\n\tif !slices.Contains(initOutputForCommands, terraformCommand) {\n\t\t// Since some command can return a json string, it is necessary to suppress output to stdout of the `terraform init` command.\n\t\tinitOptions.Writers.Writer = io.Discard\n\t}\n\n\tif l.Formatter().DisabledColors() || opts.TerraformCliArgs.Contains(tf.FlagNameNoColor) {\n\t\tinitOptions.TerraformCliArgs.AppendFlag(tf.FlagNameNoColor)\n\t}\n\n\treturn l, initOptions, nil\n}\n\n// Return true if modules aren't already downloaded and the Terraform templates in this project reference modules.\n// Note that to keep the logic in this code very simple, this code ONLY detects the case where you haven't downloaded\n// modules at all. Detecting if your downloaded modules are out of date (as opposed to missing entirely) is more\n// complicated and not something we handle at the moment.\nfunc modulesNeedInit(opts *Options) (bool, error) {\n\tmodulesPath := filepath.Join(opts.DataDir(), \"modules\")\n\tif util.FileExists(modulesPath) {\n\t\treturn false, nil\n\t}\n\n\tmoduleNeedInit := filepath.Join(opts.WorkingDir, ModuleInitRequiredFile)\n\tif util.FileExists(moduleNeedInit) {\n\t\treturn true, nil\n\t}\n\n\t// Check for module definitions in .tf and .tofu files using WalkDir\n\thasModuleDefinition, err := util.RegexFoundInTFFiles(opts.WorkingDir, ModuleRegex)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treturn hasModuleDefinition, nil\n}\n\n// remoteStateNeedsInit determines whether remote state initialization is required before running a Terraform command.\n// It returns true if:\n//   - BackendBootstrap is enabled in options\n//   - Remote state configuration is provided\n//   - The Terraform command uses state (e.g., plan, apply, destroy, output, etc.)\n//   - The remote state backend needs bootstrapping\nfunc remoteStateNeedsInit(\n\tctx context.Context,\n\tl log.Logger,\n\tremoteState *remotestate.RemoteState,\n\topts *Options,\n) (bool, error) {\n\t// If backend bootstrap is disabled, we don't need to initialize remote state\n\tif !opts.BackendBootstrap {\n\t\treturn false, nil\n\t}\n\t// We only configure remote state for the commands that use the tfstate files. We do not configure it for\n\t// commands such as \"get\" or \"version\".\n\tif remoteState == nil || remoteState.Config == nil || !slices.Contains(\n\t\tTerraformCommandsThatUseState,\n\t\topts.TerraformCliArgs.First(),\n\t) {\n\t\treturn false, nil\n\t}\n\n\tif ok, err := remoteState.NeedsBootstrap(ctx, l, opts.remoteStateOpts()); err != nil || !ok {\n\t\treturn false, err\n\t}\n\n\treturn true, nil\n}\n\n// FilterTerraformExtraArgs extracts terraform extra arguments using runcfg types.\nfunc FilterTerraformExtraArgs(l log.Logger, opts *Options, cfg *runcfg.RunConfig) []string {\n\tout := []string{}\n\tcmd := opts.TerraformCliArgs.First()\n\n\tfor i := range cfg.Terraform.ExtraArgs {\n\t\targ := &cfg.Terraform.ExtraArgs[i]\n\t\tfor _, argCmd := range arg.Commands {\n\t\t\tif cmd == argCmd {\n\t\t\t\tlastArg := opts.TerraformCliArgs.Last()\n\t\t\t\tskipVars := (cmd == tf.CommandNameApply || cmd == tf.CommandNameDestroy) && util.IsFile(lastArg)\n\n\t\t\t\tif len(arg.Arguments) > 0 {\n\t\t\t\t\tif skipVars {\n\t\t\t\t\t\tfor _, a := range arg.Arguments {\n\t\t\t\t\t\t\tif !strings.HasPrefix(a, \"-var\") {\n\t\t\t\t\t\t\t\tout = append(out, a)\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\tout = append(out, arg.Arguments...)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif !skipVars {\n\t\t\t\t\tfor _, file := range arg.VarFiles {\n\t\t\t\t\t\tout = append(out, \"-var-file=\"+file)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn out\n}\n\n// ToTerraformEnvVars converts the given variables to a map of environment variables that will expose those variables to Terraform. The\n// keys will be of the format TF_VAR_xxx and the values will be converted to JSON, which Terraform knows how to read\n// natively.\nfunc ToTerraformEnvVars(l log.Logger, vars map[string]any) (map[string]string, error) {\n\tout := map[string]string{}\n\n\tfor varName, varValue := range vars {\n\t\tif varValue == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tenvVarName := fmt.Sprintf(tf.EnvNameTFVarFmt, varName)\n\n\t\tenvVarValue, err := util.AsTerraformEnvVarJSONValue(varValue)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tout[envVarName] = envVarValue\n\t}\n\n\treturn out, nil\n}\n\n// filterTerraformEnvVarsFromExtraArgsRunCfg extracts terraform env vars from extra args using runcfg types.\nfunc filterTerraformEnvVarsFromExtraArgsRunCfg(opts *Options, cfg *runcfg.RunConfig) map[string]string {\n\tout := map[string]string{}\n\tcmd := opts.TerraformCliArgs.First()\n\n\tfor i := range cfg.Terraform.ExtraArgs {\n\t\targ := &cfg.Terraform.ExtraArgs[i]\n\t\tif len(arg.EnvVars) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, argcmd := range arg.Commands {\n\t\t\tif cmd == argcmd {\n\t\t\t\tmaps.Copy(out, arg.EnvVars)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn out\n}\n\n// prepareInitCommandRunCfg prepares for terraform init using runcfg types.\nfunc prepareInitCommandRunCfg(ctx context.Context, l log.Logger, opts *Options, cfg *runcfg.RunConfig) error {\n\tif cfg.RemoteState.Config == nil {\n\t\treturn nil\n\t}\n\n\topts.InsertTerraformCliArgs(cfg.RemoteState.GetTFInitArgs()...)\n\n\t// Bootstrap is skipped when either BackendBootstrap is false (the default) or DisableInit is true.\n\t// DisableInit is also enforced in RemoteState.NeedsBootstrap (non-init auto-init path);\n\t// both must stay in sync to ensure consistent behavior across all command types.\n\tif !opts.BackendBootstrap || cfg.RemoteState.DisableInit {\n\t\treturn nil\n\t}\n\n\tif err := cfg.RemoteState.Bootstrap(ctx, l, opts.remoteStateOpts()); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// PrepareNonInitCommand prepares for non-init commands using runcfg types.\nfunc PrepareNonInitCommand(\n\tctx context.Context,\n\tl log.Logger,\n\toriginalOpts *Options,\n\topts *Options,\n\tcfg *runcfg.RunConfig,\n\tr *report.Report,\n) error {\n\tneedsInit, err := needsInitRunCfg(ctx, l, opts, cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif needsInit {\n\t\tif err := runTerraformInitRunCfg(ctx, l, originalOpts, opts, cfg, r); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// needsInitRunCfg determines if terraform init is needed using runcfg types.\nfunc needsInitRunCfg(ctx context.Context, l log.Logger, opts *Options, cfg *runcfg.RunConfig) (bool, error) {\n\tif slices.Contains(TerraformCommandsThatDoNotNeedInit, opts.TerraformCliArgs.First()) {\n\t\treturn false, nil\n\t}\n\n\tif providersNeedInit(opts) {\n\t\treturn true, nil\n\t}\n\n\tmodulesNeedsInit, err := modulesNeedInit(opts)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tif modulesNeedsInit {\n\t\treturn true, nil\n\t}\n\n\tif cfg.RemoteState.Config == nil {\n\t\treturn false, nil\n\t}\n\n\treturn remoteStateNeedsInit(ctx, l, &cfg.RemoteState, opts)\n}\n\n// runTerraformInitRunCfg runs terraform init using runcfg types.\nfunc runTerraformInitRunCfg(\n\tctx context.Context,\n\tl log.Logger,\n\toriginalOpts *Options,\n\topts *Options,\n\tcfg *runcfg.RunConfig,\n\tr *report.Report,\n) error {\n\tif opts.TerraformCliArgs.First() != tf.CommandNameInit && !opts.AutoInit {\n\t\tl.Warnf(\"Detected that init is needed, but Auto-Init is disabled. Continuing with further actions, but subsequent terraform commands may fail.\")\n\t\treturn nil\n\t}\n\n\tl, initOptions, err := prepareInitOptions(l, opts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err := runTerragruntWithConfig(ctx, l, originalOpts, initOptions, cfg, r); err != nil {\n\t\treturn err\n\t}\n\n\tmoduleNeedInit := filepath.Join(opts.WorkingDir, ModuleInitRequiredFile)\n\tif util.FileExists(moduleNeedInit) {\n\t\treturn os.Remove(moduleNeedInit)\n\t}\n\n\treturn nil\n}\n\n// checkProtectedModuleRunCfg checks if module is protected using runcfg types.\nfunc checkProtectedModuleRunCfg(opts *Options, cfg *runcfg.RunConfig) error {\n\tvar destroyFlag = false\n\tif opts.TerraformCliArgs.First() == tf.CommandNameDestroy {\n\t\tdestroyFlag = true\n\t}\n\n\tif opts.TerraformCliArgs.Contains(\"-\" + tf.CommandNameDestroy) {\n\t\tdestroyFlag = true\n\t}\n\n\tif !destroyFlag {\n\t\treturn nil\n\t}\n\n\tif cfg.PreventDestroy {\n\t\treturn errors.New(ModuleIsProtected{ConfigPath: opts.TerragruntConfigPath})\n\t}\n\n\treturn nil\n}\n\n// setTerragruntNullValuesRunCfg writes null-valued inputs to a tfvars.json file\n// that OpenTofu/Terraform will auto-load. This is necessary because OpenTofu/Terraform\n// cannot accept null values via environment variables (TF_VAR_*), but it can read them\n// from .auto.tfvars.json files.\nfunc setTerragruntNullValuesRunCfg(opts *Options, cfg *runcfg.RunConfig) (string, error) {\n\tjsonEmptyVars := make(map[string]any)\n\n\tfor varName, varValue := range cfg.Inputs {\n\t\tif varValue == nil {\n\t\t\tjsonEmptyVars[varName] = nil\n\t\t}\n\t}\n\n\tif len(jsonEmptyVars) == 0 {\n\t\treturn \"\", nil\n\t}\n\n\tjsonContents, err := json.MarshalIndent(jsonEmptyVars, \"\", \"  \")\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\tvarFile := filepath.Join(opts.WorkingDir, NullTFVarsFile)\n\n\tconst ownerReadWritePermissions = 0600\n\n\tif err := os.WriteFile(varFile, jsonContents, os.FileMode(ownerReadWritePermissions)); err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\treturn varFile, nil\n}\n"
  },
  {
    "path": "internal/runner/run/run_test.go",
    "content": "package run_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runcfg\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSetTerragruntInputsAsEnvVars(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tenvVarsInOpts  map[string]string\n\t\tinputsInConfig map[string]any\n\t\texpected       map[string]string\n\t\tdescription    string\n\t}{\n\t\t{\n\t\t\tdescription:    \"No env vars in opts, no inputs\",\n\t\t\tenvVarsInOpts:  nil,\n\t\t\tinputsInConfig: nil,\n\t\t\texpected:       map[string]string{},\n\t\t},\n\t\t{\n\t\t\tdescription:    \"A few env vars in opts, no inputs\",\n\t\t\tenvVarsInOpts:  map[string]string{\"foo\": \"bar\"},\n\t\t\tinputsInConfig: nil,\n\t\t\texpected:       map[string]string{\"foo\": \"bar\"},\n\t\t},\n\t\t{\n\t\t\tdescription:    \"No env vars in opts, one input\",\n\t\t\tenvVarsInOpts:  nil,\n\t\t\tinputsInConfig: map[string]any{\"foo\": \"bar\"},\n\t\t\texpected:       map[string]string{\"TF_VAR_foo\": \"bar\"},\n\t\t},\n\t\t{\n\t\t\tdescription:    \"No env vars in opts, a few inputs\",\n\t\t\tenvVarsInOpts:  nil,\n\t\t\tinputsInConfig: map[string]any{\"foo\": \"bar\", \"list\": []int{1, 2, 3}, \"map\": map[string]any{\"a\": \"b\"}},\n\t\t\texpected:       map[string]string{\"TF_VAR_foo\": \"bar\", \"TF_VAR_list\": \"[1,2,3]\", \"TF_VAR_map\": `{\"a\":\"b\"}`},\n\t\t},\n\t\t{\n\t\t\tdescription:    \"A few env vars in opts, a few inputs, no overlap\",\n\t\t\tenvVarsInOpts:  map[string]string{\"foo\": \"bar\", \"something\": \"else\"},\n\t\t\tinputsInConfig: map[string]any{\"foo\": \"bar\", \"list\": []int{1, 2, 3}, \"map\": map[string]any{\"a\": \"b\"}},\n\t\t\texpected:       map[string]string{\"foo\": \"bar\", \"something\": \"else\", \"TF_VAR_foo\": \"bar\", \"TF_VAR_list\": \"[1,2,3]\", \"TF_VAR_map\": `{\"a\":\"b\"}`},\n\t\t},\n\t\t{\n\t\t\tdescription:    \"A few env vars in opts, a few inputs, with overlap\",\n\t\t\tenvVarsInOpts:  map[string]string{\"foo\": \"bar\", \"TF_VAR_foo\": \"original\", \"TF_VAR_list\": \"original\"},\n\t\t\tinputsInConfig: map[string]any{\"foo\": \"bar\", \"list\": []int{1, 2, 3}, \"map\": map[string]any{\"a\": \"b\"}},\n\t\t\texpected:       map[string]string{\"foo\": \"bar\", \"TF_VAR_foo\": \"original\", \"TF_VAR_list\": \"original\", \"TF_VAR_map\": `{\"a\":\"b\"}`},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\topts, err := options.NewTerragruntOptionsForTest(\"mock-path-for-test.hcl\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\topts.Env = tc.envVarsInOpts\n\n\t\t\trunOpts := configbridge.NewRunOptions(opts)\n\n\t\t\tcfg := &runcfg.RunConfig{Inputs: tc.inputsInConfig}\n\n\t\t\tl := logger.CreateLogger()\n\t\t\trequire.NoError(t, run.SetTerragruntInputsAsEnvVars(l, runOpts, cfg))\n\n\t\t\tassert.Equal(t, tc.expected, runOpts.Env)\n\t\t})\n\t}\n}\n\nfunc TestTerragruntTerraformCodeCheck(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tfiles       map[string]string\n\t\tdescription string\n\t\tvalid       bool\n\t}{\n\t\t{\n\t\t\tdescription: \"Directory with plain Terraform\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tf\": `# Terraform file`,\n\t\t\t},\n\t\t\tvalid: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with plain OpenTofu\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tofu\": `# OpenTofu file`,\n\t\t\t},\n\t\t\tvalid: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with plain Terraform and OpenTofu\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tf\":   `# Terraform file`,\n\t\t\t\t\"main.tofu\": `# OpenTofu file`,\n\t\t\t},\n\t\t\tvalid: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with JSON formatted Terraform\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tf.json\": `{\"terraform\": {\"backend\": {\"s3\": {}}}}`,\n\t\t\t},\n\t\t\tvalid: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with JSON formatted OpenTofu\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tofu.json\": `{\"terraform\": {\"backend\": {\"s3\": {}}}}`,\n\t\t\t},\n\t\t\tvalid: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with JSON formatted Terraform and OpenTofu\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tf.json\":   `{\"terraform\": {\"backend\": {\"s3\": {}}}}`,\n\t\t\t\t\"main.tofu.json\": `{\"terraform\": {\"backend\": {\"s3\": {}}}}`,\n\t\t\t},\n\t\t\tvalid: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with no Terraform or OpenTofu\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.yaml\": `# Not a terraform file`,\n\t\t\t},\n\t\t\tvalid: false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with no files\",\n\t\t\tfiles:       map[string]string{},\n\t\t\tvalid:       false,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\t\t\tfor filename, content := range tc.files {\n\t\t\t\tfilePath := filepath.Join(tmpDir, filename)\n\t\t\t\trequire.NoError(t, os.WriteFile(filePath, []byte(content), 0644))\n\t\t\t}\n\n\t\t\topts, err := options.NewTerragruntOptionsForTest(\"mock-path-for-test.hcl\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\topts.WorkingDir = tmpDir\n\n\t\t\terr = run.CheckFolderContainsTerraformCode(configbridge.NewRunOptions(opts))\n\t\t\tif (err != nil) && tc.valid {\n\t\t\t\tt.Error(\"valid terraform returned error\")\n\t\t\t}\n\n\t\t\tif (err == nil) && !tc.valid {\n\t\t\t\tt.Error(\"invalid terraform did not return error\")\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Legacy retry tests removed; retries now handled via errors blocks\n\nfunc TestToTerraformEnvVars(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tvars        map[string]any\n\t\texpected    map[string]string\n\t\tdescription string\n\t}{\n\t\t{\n\t\t\tdescription: \"empty\",\n\t\t\tvars:        map[string]any{},\n\t\t\texpected:    map[string]string{},\n\t\t},\n\t\t{\n\t\t\tdescription: \"string value\",\n\t\t\tvars:        map[string]any{\"foo\": \"bar\"},\n\t\t\texpected:    map[string]string{\"TF_VAR_foo\": `bar`},\n\t\t},\n\t\t{\n\t\t\tdescription: \"int value\",\n\t\t\tvars:        map[string]any{\"foo\": 42},\n\t\t\texpected:    map[string]string{\"TF_VAR_foo\": `42`},\n\t\t},\n\t\t{\n\t\t\tdescription: \"bool value\",\n\t\t\tvars:        map[string]any{\"foo\": true},\n\t\t\texpected:    map[string]string{\"TF_VAR_foo\": `true`},\n\t\t},\n\t\t{\n\t\t\tdescription: \"list value\",\n\t\t\tvars:        map[string]any{\"foo\": []string{\"a\", \"b\", \"c\"}},\n\t\t\texpected:    map[string]string{\"TF_VAR_foo\": `[\"a\",\"b\",\"c\"]`},\n\t\t},\n\t\t{\n\t\t\tdescription: \"map value\",\n\t\t\tvars:        map[string]any{\"foo\": map[string]any{\"a\": \"b\", \"c\": \"d\"}},\n\t\t\texpected:    map[string]string{\"TF_VAR_foo\": `{\"a\":\"b\",\"c\":\"d\"}`},\n\t\t},\n\t\t{\n\t\t\tdescription: \"nested map value\",\n\t\t\tvars:        map[string]any{\"foo\": map[string]any{\"a\": []int{1, 2, 3}, \"b\": \"c\", \"d\": map[string]any{\"e\": \"f\"}}},\n\t\t\texpected:    map[string]string{\"TF_VAR_foo\": `{\"a\":[1,2,3],\"b\":\"c\",\"d\":{\"e\":\"f\"}}`},\n\t\t},\n\t\t{\n\t\t\tdescription: \"multiple values\",\n\t\t\tvars:        map[string]any{\"str\": \"bar\", \"int\": 42, \"bool\": false, \"list\": []int{1, 2, 3}, \"map\": map[string]any{\"a\": \"b\"}},\n\t\t\texpected:    map[string]string{\"TF_VAR_str\": `bar`, \"TF_VAR_int\": `42`, \"TF_VAR_bool\": `false`, \"TF_VAR_list\": `[1,2,3]`, \"TF_VAR_map\": `{\"a\":\"b\"}`},\n\t\t},\n\t\t{\n\t\t\tdescription: \"map value with interpolation pattern\",\n\t\t\tvars:        map[string]any{\"stuff\": map[string]any{\"foo\": \"test ${bar} test\"}},\n\t\t\texpected:    map[string]string{\"TF_VAR_stuff\": `{\"foo\":\"test $${bar} test\"}`},\n\t\t},\n\t\t{\n\t\t\tdescription: \"plain string with interpolation pattern not escaped\",\n\t\t\tvars:        map[string]any{\"mystr\": \"plain ${bar} string\"},\n\t\t\texpected:    map[string]string{\"TF_VAR_mystr\": `plain ${bar} string`},\n\t\t},\n\t\t{\n\t\t\tdescription: \"typed slice with interpolation pattern\",\n\t\t\tvars:        map[string]any{\"list\": []string{\"${a}\", \"b\"}},\n\t\t\texpected:    map[string]string{\"TF_VAR_list\": `[\"$${a}\",\"b\"]`},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tl := logger.CreateLogger()\n\t\t\tactual, err := run.ToTerraformEnvVars(l, tc.vars)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestFilterTerraformExtraArgs(t *testing.T) {\n\tt.Parallel()\n\n\tworkingDir := helpers.TmpDirWOSymlinks(t)\n\n\ttemporaryFile := createTempFile(t)\n\n\ttestCases := []struct {\n\t\toptions      *options.TerragruntOptions\n\t\textraArgs    runcfg.TerraformExtraArguments\n\t\texpectedArgs []string\n\t}{\n\t\t// Standard scenario\n\t\t{\n\t\t\tmockCmdOptions(t, workingDir, []string{\"apply\"}),\n\t\t\tmockExtraArgs([]string{\"--foo\", \"bar\"}, []string{\"apply\", \"plan\", \"destroy\"}, []string{}, []string{}),\n\t\t\t[]string{\"--foo\", \"bar\"},\n\t\t},\n\t\t// optional existing var file\n\t\t{\n\t\t\tmockCmdOptions(t, workingDir, []string{\"apply\"}),\n\t\t\tmockExtraArgs([]string{\"--foo\", \"bar\"}, []string{\"apply\", \"plan\"}, []string{}, []string{temporaryFile}),\n\t\t\t[]string{\"--foo\", \"bar\", \"-var-file=\" + temporaryFile},\n\t\t},\n\t\t// required var file + optional existing var file\n\t\t{\n\t\t\tmockCmdOptions(t, workingDir, []string{\"apply\"}),\n\t\t\tmockExtraArgs([]string{\"--foo\", \"bar\"}, []string{\"apply\", \"plan\"}, []string{\"required.tfvars\"}, []string{temporaryFile}),\n\t\t\t[]string{\"--foo\", \"bar\", \"-var-file=required.tfvars\", \"-var-file=\" + temporaryFile},\n\t\t},\n\t\t// non existing required var file + non existing optional var file\n\t\t{\n\t\t\tmockCmdOptions(t, workingDir, []string{\"apply\"}),\n\t\t\tmockExtraArgs([]string{\"--foo\", \"bar\"}, []string{\"apply\", \"plan\"}, []string{\"required.tfvars\"}, []string{\"optional.tfvars\"}),\n\t\t\t[]string{\"--foo\", \"bar\", \"-var-file=required.tfvars\"},\n\t\t},\n\t\t// plan providing a folder, var files should stay included\n\t\t{\n\t\t\tmockCmdOptions(t, workingDir, []string{\"plan\", workingDir}),\n\t\t\tmockExtraArgs([]string{\"--foo\", \"bar\"}, []string{\"plan\", \"apply\"}, []string{\"required.tfvars\"}, []string{temporaryFile}),\n\t\t\t[]string{\"--foo\", \"bar\", \"-var-file=required.tfvars\", \"-var-file=\" + temporaryFile},\n\t\t},\n\t\t// apply providing a folder, var files should stay included\n\t\t{\n\t\t\tmockCmdOptions(t, workingDir, []string{\"apply\", workingDir}),\n\t\t\tmockExtraArgs([]string{\"--foo\", \"-var-file=test.tfvars\", \"-var='key=value'\"}, []string{\"plan\", \"apply\"}, []string{\"required.tfvars\"}, []string{temporaryFile}),\n\t\t\t[]string{\"--foo\", \"-var-file=test.tfvars\", \"-var='key=value'\", \"-var-file=required.tfvars\", \"-var-file=\" + temporaryFile},\n\t\t},\n\t\t// apply providing a file, no var files included\n\t\t{\n\t\t\tmockCmdOptions(t, workingDir, []string{\"apply\", temporaryFile}),\n\t\t\tmockExtraArgs([]string{\"--foo\", \"-var-file=test.tfvars\", \"bar\", \"-var='key=value'\", \"foo\"}, []string{\"plan\", \"apply\"}, []string{\"required.tfvars\"}, []string{temporaryFile}),\n\t\t\t[]string{\"--foo\", \"bar\", \"foo\"},\n\t\t},\n\n\t\t// apply providing no params, var files should stay included\n\t\t{\n\t\t\tmockCmdOptions(t, workingDir, []string{\"apply\"}),\n\t\t\tmockExtraArgs([]string{\"--foo\", \"-var-file=test.tfvars\", \"bar\", \"-var='key=value'\", \"foo\"}, []string{\"plan\", \"apply\"}, []string{\"required.tfvars\"}, []string{temporaryFile}),\n\t\t\t[]string{\"--foo\", \"-var-file=test.tfvars\", \"bar\", \"-var='key=value'\", \"foo\", \"-var-file=required.tfvars\", \"-var-file=\" + temporaryFile},\n\t\t},\n\t\t// apply with some parameters, providing a file => no var files included\n\t\t{\n\t\t\tmockCmdOptions(t, workingDir, []string{\"apply\", \"-no-color\", \"-foo\", temporaryFile}),\n\t\t\tmockExtraArgs([]string{\"--foo\", \"-var-file=test.tfvars\", \"bar\", \"-var='key=value'\", \"foo\"}, []string{\"plan\", \"apply\"}, []string{\"required.tfvars\"}, []string{temporaryFile}),\n\t\t\t[]string{\"--foo\", \"bar\", \"foo\"},\n\t\t},\n\t\t// destroy providing a folder, var files should stay included\n\t\t{\n\t\t\tmockCmdOptions(t, workingDir, []string{\"destroy\", workingDir}),\n\t\t\tmockExtraArgs([]string{\"--foo\", \"-var-file=test.tfvars\", \"-var='key=value'\"}, []string{\"plan\", \"destroy\"}, []string{\"required.tfvars\"}, []string{temporaryFile}),\n\t\t\t[]string{\"--foo\", \"-var-file=test.tfvars\", \"-var='key=value'\", \"-var-file=required.tfvars\", \"-var-file=\" + temporaryFile},\n\t\t},\n\t\t// destroy providing a file, no var files included\n\t\t{\n\t\t\tmockCmdOptions(t, workingDir, []string{\"destroy\", temporaryFile}),\n\t\t\tmockExtraArgs([]string{\"--foo\", \"-var-file=test.tfvars\", \"bar\", \"-var='key=value'\", \"foo\"}, []string{\"plan\", \"destroy\"}, []string{\"required.tfvars\"}, []string{temporaryFile}),\n\t\t\t[]string{\"--foo\", \"bar\", \"foo\"},\n\t\t},\n\n\t\t// destroy providing no params, var files should stay included\n\t\t{\n\t\t\tmockCmdOptions(t, workingDir, []string{\"destroy\"}),\n\t\t\tmockExtraArgs([]string{\"--foo\", \"-var-file=test.tfvars\", \"bar\", \"-var='key=value'\", \"foo\"}, []string{\"plan\", \"destroy\"}, []string{\"required.tfvars\"}, []string{temporaryFile}),\n\t\t\t[]string{\"--foo\", \"-var-file=test.tfvars\", \"bar\", \"-var='key=value'\", \"foo\", \"-var-file=required.tfvars\", \"-var-file=\" + temporaryFile},\n\t\t},\n\t\t// destroy with some parameters, providing a file => no var files included\n\t\t{\n\t\t\tmockCmdOptions(t, workingDir, []string{\"destroy\", \"-no-color\", \"-foo\", temporaryFile}),\n\t\t\tmockExtraArgs([]string{\"--foo\", \"-var-file=test.tfvars\", \"bar\", \"-var='key=value'\", \"foo\"}, []string{\"plan\", \"destroy\"}, []string{\"required.tfvars\"}, []string{temporaryFile}),\n\t\t\t[]string{\"--foo\", \"bar\", \"foo\"},\n\t\t},\n\n\t\t// Command not included in commands list\n\t\t{\n\t\t\tmockCmdOptions(t, workingDir, []string{\"apply\"}),\n\t\t\tmockExtraArgs([]string{\"--foo\", \"bar\"}, []string{\"plan\", \"destroy\"}, []string{\"required.tfvars\"}, []string{\"optional.tfvars\"}),\n\t\t\t[]string{},\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tconfig := runcfg.RunConfig{\n\t\t\tTerraform: runcfg.TerraformConfig{ExtraArgs: []runcfg.TerraformExtraArguments{tc.extraArgs}},\n\t\t}\n\t\tl := logger.CreateLogger()\n\t\tout := run.FilterTerraformExtraArgs(l, configbridge.NewRunOptions(tc.options), &config)\n\t\tassert.Equal(t, tc.expectedArgs, out)\n\t}\n}\n\nvar defaultLogLevel = log.DebugLevel\n\nfunc mockCmdOptions(t *testing.T, workingDir string, terraformCliArgs []string) *options.TerragruntOptions {\n\tt.Helper()\n\n\to := mockOptions(\n\t\tt,\n\t\tfilepath.Join(\n\t\t\tworkingDir,\n\t\t\t\"terragrunt.hcl\",\n\t\t),\n\t\tworkingDir,\n\t\tterraformCliArgs,\n\t\ttrue,\n\t\t\"\",\n\t\tfalse,\n\t\tfalse,\n\t\tdefaultLogLevel,\n\t\tfalse,\n\t)\n\n\treturn o\n}\n\nfunc mockExtraArgs(arguments, commands, requiredVarFiles, optionalVarFiles []string) runcfg.TerraformExtraArguments {\n\t// Compute VarFiles from RequiredVarFiles and OptionalVarFiles, matching what happens\n\t// during config translation in pkg/config/translate.go\n\tvar varFiles []string\n\n\t// Include all specified RequiredVarFiles\n\tif len(requiredVarFiles) > 0 {\n\t\tvarFiles = append(varFiles, util.RemoveDuplicatesKeepLast(requiredVarFiles)...)\n\t}\n\n\t// Include OptionalVarFiles only if they exist\n\tif len(optionalVarFiles) > 0 {\n\t\tfor _, file := range util.RemoveDuplicatesKeepLast(optionalVarFiles) {\n\t\t\tif !util.FileExists(file) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvarFiles = append(varFiles, file)\n\t\t}\n\t}\n\n\ta := runcfg.TerraformExtraArguments{\n\t\tName:             \"test\",\n\t\tArguments:        arguments,\n\t\tCommands:         commands,\n\t\tRequiredVarFiles: requiredVarFiles,\n\t\tOptionalVarFiles: optionalVarFiles,\n\t\tVarFiles:         varFiles,\n\t}\n\n\treturn a\n}\n\nfunc mockOptions(t *testing.T, terragruntConfigPath string, workingDir string, terraformCliArgs []string, nonInteractive bool, terragruntSource string, ignoreDependencyErrors bool, includeExternalDependencies bool, _ log.Level, debug bool) *options.TerragruntOptions {\n\tt.Helper()\n\n\topts, err := options.NewTerragruntOptionsForTest(terragruntConfigPath)\n\tif err != nil {\n\t\tt.Fatalf(\"error: %v\\n\", errors.New(err))\n\t}\n\n\topts.WorkingDir = workingDir\n\topts.TerraformCliArgs = iacargs.New(terraformCliArgs...)\n\topts.NonInteractive = nonInteractive\n\topts.Source = terragruntSource\n\topts.IgnoreDependencyErrors = ignoreDependencyErrors\n\topts.Debug = debug\n\n\treturn opts\n}\n\nfunc createTempFile(t *testing.T) string {\n\tt.Helper()\n\n\ttmpFile, err := os.CreateTemp(helpers.TmpDirWOSymlinks(t), \"\")\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create temp directory: %s\\n\", err.Error())\n\t}\n\n\treturn tmpFile.Name()\n}\n\nfunc TestShouldCopyLockFile(t *testing.T) {\n\tt.Parallel()\n\n\ttype args struct {\n\t\tterraformConfig *runcfg.TerraformConfig\n\t\targs            []string\n\t}\n\n\ttests := []struct {\n\t\tname string\n\t\targs args\n\t\twant bool\n\t}{\n\t\t{\n\t\t\tname: \"init without terraform config\",\n\t\t\targs: args{\n\t\t\t\targs: []string{\"init\"},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"providers lock without terraform config\",\n\t\t\targs: args{\n\t\t\t\targs: []string{\"providers\", \"lock\"},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"providers schema without terraform config\",\n\t\t\targs: args{\n\t\t\t\targs: []string{\"providers\", \"schema\"},\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"plan without terraform config\",\n\t\t\targs: args{\n\t\t\t\targs: []string{\"plan\"},\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t\t{\n\t\t\tname: \"init with empty terraform config\",\n\t\t\targs: args{\n\t\t\t\targs:            []string{\"init\"},\n\t\t\t\tterraformConfig: &runcfg.TerraformConfig{},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"init with CopyTerraformLockFile enabled\",\n\t\t\targs: args{\n\t\t\t\targs: []string{\"init\"},\n\t\t\t\tterraformConfig: &runcfg.TerraformConfig{\n\t\t\t\t\tNoCopyTerraformLockFile: false,\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\t\t{\n\t\t\tname: \"init with CopyTerraformLockFile disabled\",\n\t\t\targs: args{\n\t\t\t\targs: []string{\"init\"},\n\t\t\t\tterraformConfig: &runcfg.TerraformConfig{\n\t\t\t\t\tNoCopyTerraformLockFile: true,\n\t\t\t\t},\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equalf(\n\t\t\t\tt,\n\t\t\t\ttt.want,\n\t\t\t\trun.ShouldCopyLockFile(\n\t\t\t\t\tiacargs.New(tt.args.args...),\n\t\t\t\t\ttt.args.terraformConfig,\n\t\t\t\t),\n\t\t\t\t\"shouldCopyLockFile(%v, %v)\", tt.args.args, tt.args.terraformConfig)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/runner/run/symlink_preserving_git_getter.go",
    "content": "package run\n\nimport (\n\t\"net/url\"\n\n\t\"github.com/hashicorp/go-getter\"\n)\n\n// Since go-getter v1.7.9, symbolic links are disabled by default and are automatically\n// disabled during git submodule operations. This wrapper preserves the original\n// DisableSymlinks setting to ensure symlinks remain enabled when configured.\n\n// symlinkPreservingGitGetter wraps the original git getter to preserve symlink settings\ntype symlinkPreservingGitGetter struct {\n\toriginal getter.Getter\n\tclient   *getter.Client\n}\n\n// Get overrides the original GitGetter to preserve symlink settings\nfunc (g *symlinkPreservingGitGetter) Get(dst string, u *url.URL) error {\n\t// Store the original DisableSymlinks setting\n\toriginalDisableSymlinks := g.client.DisableSymlinks\n\n\t// Call the original getter\n\terr := g.original.Get(dst, u)\n\n\t// Restore the original DisableSymlinks setting\n\tg.client.DisableSymlinks = originalDisableSymlinks\n\n\treturn err\n}\n\n// GetFile overrides the original GitGetter to preserve symlink settings\nfunc (g *symlinkPreservingGitGetter) GetFile(dst string, u *url.URL) error {\n\treturn g.original.GetFile(dst, u)\n}\n\n// ClientMode overrides the original GitGetter to preserve symlink settings\nfunc (g *symlinkPreservingGitGetter) ClientMode(u *url.URL) (getter.ClientMode, error) {\n\treturn g.original.ClientMode(u)\n}\n\n// SetClient overrides the original GitGetter to preserve symlink settings\nfunc (g *symlinkPreservingGitGetter) SetClient(c *getter.Client) {\n\tg.client = c\n\tg.original.SetClient(c)\n}\n"
  },
  {
    "path": "internal/runner/run/tofu_extensions_test.go",
    "content": "//go:build tofu\n\npackage run_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestTofuBackendDetectionWithRegex(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tdescription   string\n\t\tfiles         map[string]string\n\t\tbackendType   string\n\t\texpectBackend bool\n\t}{\n\t\t{\n\t\t\tdescription: \"Backend in .tofu file\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tofu\": `\nterraform {\n  backend \"s3\" {\n    bucket = \"my-terraform-state\"\n    key    = \"opentofu/terraform.tfstate\"\n    region = \"us-west-2\"\n  }\n}\n\nresource \"aws_instance\" \"example\" {\n  ami           = \"ami-0c55b159cbfafe1d0\"\n  instance_type = \"t2.micro\"\n}`,\n\t\t\t},\n\t\t\tbackendType:   \"s3\",\n\t\t\texpectBackend: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Backend in mixed .tf/.tofu files - backend in .tf\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"backend.tf\": `\nterraform {\n  required_version = \">= 1.0\"\n\n  backend \"s3\" {\n    bucket = \"terraform-state-bucket\"\n    key    = \"mixed/terraform.tfstate\"\n    region = \"us-east-1\"\n  }\n}`,\n\t\t\t\t\"resources.tofu\": `\nresource \"aws_vpc\" \"main\" {\n  cidr_block = \"10.0.0.0/16\"\n}`,\n\t\t\t},\n\t\t\tbackendType:   \"s3\",\n\t\t\texpectBackend: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"No backend in .tofu files\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tofu\": `\nresource \"aws_instance\" \"example\" {\n  ami           = \"ami-0c55b159cbfafe1d0\"\n  instance_type = \"t2.micro\"\n}`,\n\t\t\t},\n\t\t\tbackendType:   \"s3\",\n\t\t\texpectBackend: false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Wrong backend type in .tofu files\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tofu\": `\nterraform {\n  backend \"s3\" {\n    bucket = \"my-terraform-state\"\n    key    = \"opentofu/terraform.tfstate\"\n    region = \"us-west-2\"\n  }\n}`,\n\t\t\t},\n\t\t\tbackendType:   \"gcs\",\n\t\t\texpectBackend: false,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\t\t\tfor filename, content := range tc.files {\n\t\t\t\tfilePath := filepath.Join(tmpDir, filename)\n\t\t\t\trequire.NoError(t, os.WriteFile(filePath, []byte(content), 0644))\n\t\t\t}\n\n\t\t\tterraformBackendRegexp := regexp.MustCompile(fmt.Sprintf(`backend[[:blank:]]+\"%s\"`, tc.backendType))\n\n\t\t\thasBackend, err := util.RegexFoundInTFFiles(tmpDir, terraformBackendRegexp)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.expectBackend, hasBackend, \"For test case: %s\", tc.description)\n\t\t})\n\t}\n}\n\nfunc TestTofuModuleDetectionWithRegex(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tfiles         map[string]string\n\t\tdescription   string\n\t\texpectModules bool\n\t}{\n\t\t{\n\t\t\tdescription: \"Modules in .tofu file\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tofu\": `\nmodule \"vpc\" {\n  source = \"./modules/vpc\"\n\n  cidr_block = \"10.0.0.0/16\"\n}\n\nmodule \"security_group\" {\n  source = \"git::https://github.com/example/terraform-modules.git//security-group\"\n\n  vpc_id = module.vpc.vpc_id\n}\n\noutput \"vpc_id\" {\n  value = module.vpc.vpc_id\n}`,\n\t\t\t},\n\t\t\texpectModules: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Modules in mixed .tf/.tofu files\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tf\": `\nmodule \"network\" {\n  source = \"./modules/network\"\n\n  vpc_cidr = \"10.0.0.0/16\"\n}`,\n\t\t\t\t\"compute.tofu\": `\nmodule \"web_servers\" {\n  source = \"git::https://github.com/example/terraform-modules.git//web-server\"\n\n  vpc_id = module.network.vpc_id\n}`,\n\t\t\t},\n\t\t\texpectModules: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"No modules in .tofu files\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tofu\": `\nresource \"aws_instance\" \"example\" {\n  ami           = \"ami-0c55b159cbfafe1d0\"\n  instance_type = \"t2.micro\"\n}`,\n\t\t\t},\n\t\t\texpectModules: false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Backend only (no modules) in .tofu files\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tofu\": `\nterraform {\n  backend \"s3\" {\n    bucket = \"my-terraform-state\"\n    key    = \"opentofu/terraform.tfstate\"\n    region = \"us-west-2\"\n  }\n}\n\nresource \"aws_instance\" \"example\" {\n  ami           = \"ami-0c55b159cbfafe1d0\"\n  instance_type = \"t2.micro\"\n}`,\n\t\t\t},\n\t\t\texpectModules: false,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\t\t\tfor filename, content := range tc.files {\n\t\t\t\tfilePath := filepath.Join(tmpDir, filename)\n\t\t\t\trequire.NoError(t, os.WriteFile(filePath, []byte(content), 0644))\n\t\t\t}\n\n\t\t\tmoduleRegex := regexp.MustCompile(`module[[:blank:]]+\".+\"`)\n\n\t\t\thasModules, err := util.RegexFoundInTFFiles(tmpDir, moduleRegex)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.expectModules, hasModules, \"For test case: %s\", tc.description)\n\t\t})\n\t}\n}\n\nfunc TestTofuCodeCheck(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tfiles       map[string]string\n\t\tdescription string\n\t\texpectValid bool\n\t}{\n\t\t{\n\t\t\tdescription: \"Directory with .tofu backend file\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tofu\": `\nterraform {\n  backend \"s3\" {\n    bucket = \"my-terraform-state\"\n    key    = \"opentofu/terraform.tfstate\"\n    region = \"us-west-2\"\n  }\n}\n\nresource \"aws_instance\" \"example\" {\n  ami           = \"ami-0c55b159cbfafe1d0\"\n  instance_type = \"t2.micro\"\n}`,\n\t\t\t},\n\t\t\texpectValid: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with .tofu modules file\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tofu\": `\nmodule \"vpc\" {\n  source = \"./modules/vpc\"\n  cidr_block = \"10.0.0.0/16\"\n}`,\n\t\t\t},\n\t\t\texpectValid: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with mixed .tf/.tofu files\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"backend.tf\": `\nterraform {\n  backend \"s3\" {\n    bucket = \"terraform-state-bucket\"\n    key    = \"mixed/terraform.tfstate\"\n    region = \"us-east-1\"\n  }\n}`,\n\t\t\t\t\"resources.tofu\": `\nresource \"aws_vpc\" \"main\" {\n  cidr_block = \"10.0.0.0/16\"\n}`,\n\t\t\t},\n\t\t\texpectValid: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with existing .tofu file\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tofu\": `# Simple tofu file`,\n\t\t\t},\n\t\t\texpectValid: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with existing .tf file\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tf\": `# Simple tf file`,\n\t\t\t},\n\t\t\texpectValid: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with both .tf and .tofu files\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tf\":   `# Terraform file`,\n\t\t\t\t\"main.tofu\": `# OpenTofu file`,\n\t\t\t},\n\t\t\texpectValid: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with no Terraform/OpenTofu files\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.yaml\": `# Not a terraform file`,\n\t\t\t},\n\t\t\texpectValid: false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Empty directory\",\n\t\t\tfiles:       map[string]string{},\n\t\t\texpectValid: false,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\t\t\tfor filename, content := range tc.files {\n\t\t\t\tfilePath := filepath.Join(tmpDir, filename)\n\t\t\t\trequire.NoError(t, os.WriteFile(filePath, []byte(content), 0644))\n\t\t\t}\n\n\t\t\topts, err := options.NewTerragruntOptionsForTest(\"mock-path-for-test.hcl\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\topts.WorkingDir = tmpDir\n\n\t\t\terr = run.CheckFolderContainsTerraformCode(configbridge.NewRunOptions(opts))\n\n\t\t\tif tc.expectValid {\n\t\t\t\tassert.NoError(t, err, \"Expected no error for valid directory: %s\", tc.description)\n\t\t\t} else {\n\t\t\t\tassert.Error(t, err, \"Expected error for invalid directory: %s\", tc.description)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTofuCacheValidation(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tfiles          map[string]string\n\t\tdescription    string\n\t\texpectHasFiles bool\n\t\texpectError    bool\n\t}{\n\t\t{\n\t\t\tdescription: \"Directory with .tofu files should be detected\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tofu\": `# Simple tofu file`,\n\t\t\t},\n\t\t\texpectHasFiles: true,\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with .tofu backend files should be detected\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tofu\": `\nterraform {\n  backend \"s3\" {\n    bucket = \"my-terraform-state\"\n    key    = \"opentofu/terraform.tfstate\"\n    region = \"us-west-2\"\n  }\n}`,\n\t\t\t},\n\t\t\texpectHasFiles: true,\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with mixed files should be detected\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"backend.tf\": `\nterraform {\n  backend \"s3\" {\n    bucket = \"terraform-state-bucket\"\n    key    = \"mixed/terraform.tfstate\"\n    region = \"us-east-1\"\n  }\n}`,\n\t\t\t\t\"resources.tofu\": `\nresource \"aws_vpc\" \"main\" {\n  cidr_block = \"10.0.0.0/16\"\n}`,\n\t\t\t},\n\t\t\texpectHasFiles: true,\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with no TF files should not be detected\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.yaml\": `# Not a terraform file`,\n\t\t\t\t\"script.sh\": `#!/bin/bash\\necho \"hello\"`,\n\t\t\t},\n\t\t\texpectHasFiles: false,\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tdescription:    \"Empty directory should not be detected\",\n\t\t\tfiles:          map[string]string{},\n\t\t\texpectHasFiles: false,\n\t\t\texpectError:    false,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\t\t\tfor filename, content := range tc.files {\n\t\t\t\tfilePath := filepath.Join(tmpDir, filename)\n\t\t\t\trequire.NoError(t, os.WriteFile(filePath, []byte(content), 0644))\n\t\t\t}\n\n\t\t\thasFiles, err := util.DirContainsTFFiles(tmpDir)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.expectHasFiles, hasFiles, \"For test case: %s\", tc.description)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/runner/run/version_check.go",
    "content": "package run\n\nimport (\n\t\"context\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tfimpl\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/hashicorp/go-version\"\n)\n\n// DefaultTerraformVersionConstraint uses the constraint syntax from https://github.com/hashicorp/go-version\n// This version of Terragrunt was tested to work with Terraform 0.12.0 and above only\nconst DefaultTerraformVersionConstraint = \">= v0.12.0\"\n\n// TerraformVersionRegex verifies that terraform --version output is in one of the following formats:\n// - OpenTofu v1.6.0-dev\n// - Terraform v0.9.5-dev (cad024a5fe131a546936674ef85445215bbc4226+CHANGES)\n// - Terraform v0.13.0-beta2\n// - Terraform v0.12.27\n// We only make sure the \"v#.#.#\" part is present in the output.\nvar TerraformVersionRegex = regexp.MustCompile(`^(\\S+)\\s(v?\\d+\\.\\d+\\.\\d+)`)\n\nconst versionParts = 3\n\n// PopulateTFVersion discovers the currently installed version of OpenTofu/Terraform.\n// It uses a cache keyed by workingDir and versionFiles to avoid repeated invocations.\n// Returns the discovered version and implementation type; the caller is responsible\n// for storing them on *options.TerragruntOptions.\nfunc PopulateTFVersion(ctx context.Context, l log.Logger, workingDir string, versionFiles []string, tfOpts *tf.TFOptions) (log.Logger, *version.Version, tfimpl.Type, error) {\n\tversionCache := GetRunVersionCache(ctx)\n\tcacheKey := computeVersionFilesCacheKey(workingDir, versionFiles)\n\tl.Debugf(\"using cache key for version files: %s\", cacheKey)\n\n\tif cachedOutput, found := versionCache.Get(ctx, cacheKey); found {\n\t\ttfImplementation, terraformVersion, err := parseVersionFromCache(cachedOutput)\n\t\tif err != nil {\n\t\t\treturn l, nil, tfimpl.Unknown, err\n\t\t}\n\n\t\treturn l, terraformVersion, tfImplementation, nil\n\t}\n\n\tl, terraformVersion, tfImplementation, err := GetTFVersion(ctx, l, tfOpts)\n\tif err != nil {\n\t\treturn l, nil, tfimpl.Unknown, err\n\t}\n\n\tcacheData := formatVersionForCache(tfImplementation, terraformVersion)\n\tversionCache.Put(ctx, cacheKey, cacheData)\n\n\treturn l, terraformVersion, tfImplementation, nil\n}\n\n// formatVersionForCache formats the implementation and version for the cache\nfunc formatVersionForCache(implementation tfimpl.Type, version *version.Version) string {\n\tvar implStr string\n\n\tswitch implementation {\n\tcase tfimpl.Terraform:\n\t\timplStr = \"terraform\"\n\tcase tfimpl.OpenTofu:\n\t\timplStr = \"opentofu\"\n\tcase tfimpl.Unknown:\n\t\timplStr = \"unknown\"\n\t}\n\n\treturn fmt.Sprintf(\"%s:%s\", implStr, version.String())\n}\n\n// parseVersionFromCache parses the cache format back to implementation and version for options\nfunc parseVersionFromCache(cachedData string) (tfimpl.Type, *version.Version, error) {\n\tconst expectedParts = 2\n\n\tparts := strings.SplitN(cachedData, \":\", expectedParts)\n\tif len(parts) != expectedParts {\n\t\treturn tfimpl.Unknown, nil, errors.New(InvalidTerraformVersionSyntax(cachedData))\n\t}\n\n\timplStr := strings.ToLower(parts[0])\n\tversionStr := parts[1]\n\n\tvar implementation tfimpl.Type\n\n\tswitch implStr {\n\tcase \"terraform\":\n\t\timplementation = tfimpl.Terraform\n\tcase \"opentofu\":\n\t\timplementation = tfimpl.OpenTofu\n\tdefault:\n\t\timplementation = tfimpl.Unknown\n\t}\n\n\tversion, err := version.NewVersion(versionStr)\n\tif err != nil {\n\t\treturn tfimpl.Unknown, nil, err\n\t}\n\n\treturn implementation, version, nil\n}\n\n// GetTFVersion checks the OpenTofu/Terraform version directly without using cache.\n// It takes pre-built *tf.TFOptions and runs \"terraform version\", discarding output\n// and stripping TF_CLI_ARGS env vars to avoid interference.\nfunc GetTFVersion(ctx context.Context, l log.Logger, tfOpts *tf.TFOptions) (log.Logger, *version.Version, tfimpl.Type, error) {\n\t// Clone to avoid mutating the caller's options.\n\toptsCopy := *tfOpts\n\tshellCopy := *optsCopy.ShellOptions\n\toptsCopy.ShellOptions = &shellCopy\n\n\t// Discard output — we only need the parsed version string.\n\toptsCopy.ShellOptions.Writers.Writer = io.Discard\n\toptsCopy.ShellOptions.Writers.ErrWriter = io.Discard\n\n\t// Remove TF_CLI_ARGS* so they don't interfere with \"--version\".\n\tenvCopy := make(map[string]string, len(shellCopy.Env))\n\tfor key, val := range shellCopy.Env {\n\t\tif !strings.HasPrefix(key, \"TF_CLI_ARGS\") {\n\t\t\tenvCopy[key] = val\n\t\t}\n\t}\n\n\toptsCopy.ShellOptions.Env = envCopy\n\n\toutput, err := tf.RunCommandWithOutput(ctx, l, &optsCopy, tf.FlagNameVersion)\n\tif err != nil {\n\t\treturn l, nil, tfimpl.Unknown, err\n\t}\n\n\tterraformVersion, err := ParseTerraformVersion(output.Stdout.String())\n\tif err != nil {\n\t\treturn l, nil, tfimpl.Unknown, err\n\t}\n\n\ttfImplementation, err := parseTerraformImplementationType(output.Stdout.String())\n\tif err != nil {\n\t\treturn l, nil, tfimpl.Unknown, err\n\t}\n\n\tif tfImplementation == tfimpl.Unknown {\n\t\ttfImplementation = tfimpl.Terraform\n\n\t\tl.Warnf(\"Failed to identify Terraform implementation, fallback to terraform version: %s\", terraformVersion)\n\t} else {\n\t\tl.Debugf(\"%s version: %s\", tfImplementation, terraformVersion)\n\t}\n\n\treturn l, terraformVersion, tfImplementation, nil\n}\n\n// CheckTerragruntVersionMeetsConstraint checks that the current version of Terragrunt meets the specified constraint and return an error if it doesn't\nfunc CheckTerragruntVersionMeetsConstraint(currentVersion *version.Version, constraint string) error {\n\tversionConstraint, err := version.NewConstraint(constraint)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcheckedVersion := currentVersion\n\n\tif currentVersion.Prerelease() != \"\" {\n\t\t// The logic in hashicorp/go-version is such that it will not consider a prerelease version to be\n\t\t// compatible with a constraint that does not have a prerelease version. This is not the behavior we want\n\t\t// for Terragrunt, so we strip the prerelease version before checking the constraint.\n\t\t//\n\t\t// https://github.com/hashicorp/go-version/issues/130\n\t\tcheckedVersion = currentVersion.Core()\n\t}\n\n\tif !versionConstraint.Check(checkedVersion) {\n\t\treturn errors.New(InvalidTerragruntVersion{CurrentVersion: currentVersion, VersionConstraints: versionConstraint})\n\t}\n\n\treturn nil\n}\n\n// CheckTerraformVersionMeetsConstraint checks that the current version of Terraform meets the specified constraint and return an error if it doesn't\nfunc CheckTerraformVersionMeetsConstraint(currentVersion *version.Version, constraint string) error {\n\tversionConstraint, err := version.NewConstraint(constraint)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !versionConstraint.Check(currentVersion) {\n\t\treturn errors.New(InvalidTerraformVersion{CurrentVersion: currentVersion, VersionConstraints: versionConstraint})\n\t}\n\n\treturn nil\n}\n\n// ParseTerraformVersion parses the output of the terraform --version command\nfunc ParseTerraformVersion(versionCommandOutput string) (*version.Version, error) {\n\tmatches := TerraformVersionRegex.FindStringSubmatch(versionCommandOutput)\n\n\tif len(matches) != versionParts {\n\t\treturn nil, errors.New(InvalidTerraformVersionSyntax(versionCommandOutput))\n\t}\n\n\treturn version.NewVersion(matches[2])\n}\n\n// parseTerraformImplementationType - Parse terraform implementation from --version command output\nfunc parseTerraformImplementationType(versionCommandOutput string) (tfimpl.Type, error) {\n\tmatches := TerraformVersionRegex.FindStringSubmatch(versionCommandOutput)\n\n\tif len(matches) != versionParts {\n\t\treturn tfimpl.Unknown, errors.New(InvalidTerraformVersionSyntax(versionCommandOutput))\n\t}\n\n\trawType := strings.ToLower(matches[1])\n\tswitch rawType {\n\tcase \"terraform\":\n\t\treturn tfimpl.Terraform, nil\n\tcase \"opentofu\":\n\t\treturn tfimpl.OpenTofu, nil\n\tdefault:\n\t\treturn tfimpl.Unknown, nil\n\t}\n}\n\n// Helper to compute a cache key from the checksums of provided files\nfunc computeVersionFilesCacheKey(workingDir string, versionFiles []string) string {\n\tvar hashes []string\n\n\tfor _, file := range versionFiles {\n\t\tpath := filepath.Join(workingDir, file)\n\n\t\tif !util.FileExists(path) {\n\t\t\tcontinue\n\t\t}\n\n\t\tsanitizedPath, err := util.SanitizePath(workingDir, file)\n\t\tif err != nil {\n\t\t\tsanitizedPath = path\n\t\t}\n\n\t\thash, err := util.FileSHA256(sanitizedPath)\n\t\tif err == nil {\n\t\t\thashes = append(hashes, file+\":\"+hex.EncodeToString(hash))\n\t\t}\n\t}\n\n\tcacheKey := \"no-version-files\"\n\n\tif len(hashes) != 0 {\n\t\tcacheKey = strings.Join(hashes, \"|\")\n\t}\n\n\treturn util.EncodeBase64Sha1(cacheKey)\n}\n\n// Custom error types\n\ntype InvalidTerraformVersionSyntax string\n\nfunc (err InvalidTerraformVersionSyntax) Error() string {\n\treturn \"Unable to parse Terraform version output: \" + string(err)\n}\n\ntype InvalidTerraformVersion struct {\n\tCurrentVersion     *version.Version\n\tVersionConstraints version.Constraints\n}\n\ntype InvalidTerragruntVersion struct {\n\tCurrentVersion     *version.Version\n\tVersionConstraints version.Constraints\n}\n\nfunc (err InvalidTerraformVersion) Error() string {\n\treturn fmt.Sprintf(\"The currently installed version of Terraform (%s) is not compatible with the version Terragrunt requires (%s).\", err.CurrentVersion.String(), err.VersionConstraints.String())\n}\n\nfunc (err InvalidTerragruntVersion) Error() string {\n\treturn fmt.Sprintf(\"The currently installed version of Terragrunt (%s) is not compatible with the version constraint requiring (%s).\", err.CurrentVersion.String(), err.VersionConstraints.String())\n}\n"
  },
  {
    "path": "internal/runner/run/version_check_internal_test.go",
    "content": "package run\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_computeVersionFilesCacheKey(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname         string\n\t\tworkingDir   string\n\t\twant         string\n\t\tversionFiles []string\n\t}{\n\t\t{\n\t\t\tname:         \"version files slice is empty\",\n\t\t\tworkingDir:   \"\",\n\t\t\tversionFiles: nil,\n\t\t\twant:         \"r01AJjVD7VSXCQk1ORuh_no_NRY\", // \"no-version-files\"\n\t\t},\n\t\t{\n\t\t\tname:       \"workdir contains version files\",\n\t\t\tworkingDir: \"../../../test/fixtures/version-files-cache-key\",\n\t\t\tversionFiles: []string{\n\t\t\t\t\".terraform-version\",\n\t\t\t\t\".tool-versions\",\n\t\t\t},\n\t\t\twant: \"XBE-VO9pOnQjPQDmLQCvSCdckSQ\",\n\t\t},\n\t\t{\n\t\t\tname:       \"workdir contains version files and we try to escape the working dir\",\n\t\t\tworkingDir: \"../../../test/fixtures/version-files-cache-key\",\n\t\t\tversionFiles: []string{\n\t\t\t\t\".terraform-version\",\n\t\t\t\t\".tool-versions\",\n\t\t\t\t\"../../../dev/random\",\n\t\t\t},\n\t\t\twant: \"XBE-VO9pOnQjPQDmLQCvSCdckSQ\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassert.Equalf(\n\t\t\t\tt,\n\t\t\t\ttt.want,\n\t\t\t\tcomputeVersionFilesCacheKey(tt.workingDir, tt.versionFiles),\n\t\t\t\t\"computeVersionFilesCacheKey(%v, %v)\",\n\t\t\t\ttt.workingDir,\n\t\t\t\ttt.versionFiles,\n\t\t\t)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/runner/run/version_check_test.go",
    "content": "//nolint:unparam\npackage run_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run\"\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Terraform Version Checking\nfunc TestCheckTerraformVersionMeetsConstraintEqual(t *testing.T) {\n\tt.Parallel()\n\ttestCheckTerraformVersionMeetsConstraint(t, \"v0.9.3\", \">= v0.9.3\", true)\n}\n\nfunc TestCheckTerraformVersionMeetsConstraintGreaterPatch(t *testing.T) {\n\tt.Parallel()\n\ttestCheckTerraformVersionMeetsConstraint(t, \"v0.9.4\", \">= v0.9.3\", true)\n}\n\nfunc TestCheckTerraformVersionMeetsConstraintGreaterMajor(t *testing.T) {\n\tt.Parallel()\n\ttestCheckTerraformVersionMeetsConstraint(t, \"v1.0.0\", \">= v0.9.3\", true)\n}\n\nfunc TestCheckTerraformVersionMeetsConstraintLessPatch(t *testing.T) {\n\tt.Parallel()\n\ttestCheckTerraformVersionMeetsConstraint(t, \"v0.9.2\", \">= v0.9.3\", false)\n}\n\nfunc TestCheckTerraformVersionMeetsConstraintLessMajor(t *testing.T) {\n\tt.Parallel()\n\ttestCheckTerraformVersionMeetsConstraint(t, \"v0.8.8\", \">= v0.9.3\", false)\n}\n\nfunc TestParseOpenTofuVersionNormal(t *testing.T) {\n\tt.Parallel()\n\ttestParseTerraformVersion(t, \"OpenTofu v1.6.0\", \"v1.6.0\", nil)\n}\n\nfunc TestParseOpenTofuVersionDev(t *testing.T) {\n\tt.Parallel()\n\ttestParseTerraformVersion(t, \"OpenTofu v1.6.0-dev\", \"v1.6.0\", nil)\n}\n\nfunc TestParseTerraformVersionNormal(t *testing.T) {\n\tt.Parallel()\n\ttestParseTerraformVersion(t, \"Terraform v0.9.3\", \"v0.9.3\", nil)\n}\n\nfunc TestParseTerraformVersionWithoutV(t *testing.T) {\n\tt.Parallel()\n\ttestParseTerraformVersion(t, \"Terraform 0.9.3\", \"0.9.3\", nil)\n}\n\nfunc TestParseTerraformVersionWithDebug(t *testing.T) {\n\tt.Parallel()\n\ttestParseTerraformVersion(t, \"Terraform v0.9.4 cad024a5fe131a546936674ef85445215bbc4226\", \"v0.9.4\", nil)\n}\n\nfunc TestParseTerraformVersionWithChanges(t *testing.T) {\n\tt.Parallel()\n\ttestParseTerraformVersion(t, \"Terraform v0.9.4-dev (cad024a5fe131a546936674ef85445215bbc4226+CHANGES)\", \"v0.9.4\", nil)\n}\n\nfunc TestParseTerraformVersionWithDev(t *testing.T) {\n\tt.Parallel()\n\ttestParseTerraformVersion(t, \"Terraform v0.9.4-dev\", \"v0.9.4\", nil)\n}\n\nfunc TestParseTerraformVersionWithBeta(t *testing.T) {\n\tt.Parallel()\n\ttestParseTerraformVersion(t, \"Terraform v0.13.0-beta1\", \"v0.13.0\", nil)\n}\n\nfunc TestParseTerraformVersionWithUnexpectedName(t *testing.T) {\n\tt.Parallel()\n\ttestParseTerraformVersion(t, \"Terraform v0.15.0-rc1\", \"v0.15.0\", nil)\n}\n\nfunc TestParseTerraformVersionInvalidSyntax(t *testing.T) {\n\tt.Parallel()\n\ttestParseTerraformVersion(t, \"invalid-syntax\", \"\", run.InvalidTerraformVersionSyntax(\"invalid-syntax\"))\n}\n\nfunc testCheckTerraformVersionMeetsConstraint(t *testing.T, currentVersion string, versionConstraint string, versionMeetsConstraint bool) {\n\tt.Helper()\n\n\tcurrent, err := version.NewVersion(currentVersion)\n\tif err != nil {\n\t\tt.Fatalf(\"Invalid current version specified in test: %v\", err)\n\t}\n\n\terr = run.CheckTerraformVersionMeetsConstraint(current, versionConstraint)\n\tif versionMeetsConstraint && err != nil {\n\t\tassert.NoError(t, err, \"Expected Terraform version %s to meet constraint %s, but got error: %v\", currentVersion, versionConstraint, err)\n\t} else if !versionMeetsConstraint && err == nil {\n\t\tassert.Error(t, err, \"Expected Terraform version %s to NOT meet constraint %s, but got back a nil error\", currentVersion, versionConstraint)\n\t}\n}\n\nfunc testParseTerraformVersion(t *testing.T, versionString string, expectedVersion string, expectedErr error) {\n\tt.Helper()\n\n\tactualVersion, actualErr := run.ParseTerraformVersion(versionString)\n\n\tif expectedErr == nil {\n\t\texpected, err := version.NewVersion(expectedVersion)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"Invalid expected version specified in test: %v\", err)\n\t\t}\n\n\t\trequire.NoError(t, actualErr)\n\t\tassert.Equal(t, expected, actualVersion)\n\t} else {\n\t\tassert.True(t, errors.IsError(actualErr, expectedErr))\n\t}\n}\n\n// TODO: Refactor these into a test table.\n\n// Terragrunt Version Checking\nfunc TestCheckTerragruntVersionMeetsConstraintEqual(t *testing.T) {\n\tt.Parallel()\n\ttestCheckTerragruntVersionMeetsConstraint(t, \"v0.23.18\", \">= v0.23.18\", true)\n}\n\nfunc TestCheckTerragruntVersionMeetsConstraintGreaterPatch(t *testing.T) {\n\tt.Parallel()\n\ttestCheckTerragruntVersionMeetsConstraint(t, \"v0.23.18\", \">= v0.23.9\", true)\n}\n\nfunc TestCheckTerragruntVersionMeetsConstraintGreaterMajor(t *testing.T) {\n\tt.Parallel()\n\ttestCheckTerragruntVersionMeetsConstraint(t, \"v1.0.0\", \">= v0.23.18\", true)\n}\n\nfunc TestCheckTerragruntVersionMeetsConstraintLessPatch(t *testing.T) {\n\tt.Parallel()\n\ttestCheckTerragruntVersionMeetsConstraint(t, \"v0.23.17\", \">= v0.23.18\", false)\n}\n\nfunc TestCheckTerragruntVersionMeetsConstraintLessMajor(t *testing.T) {\n\tt.Parallel()\n\ttestCheckTerragruntVersionMeetsConstraint(t, \"v0.22.15\", \">= v0.23.18\", false)\n}\n\nfunc TestCheckTerragruntVersionMeetsConstraintPrerelease(t *testing.T) {\n\tt.Parallel()\n\ttestCheckTerragruntVersionMeetsConstraint(t, \"v0.23.18-alpha202409013\", \">= v0.23.18\", true)\n}\n\nfunc testCheckTerragruntVersionMeetsConstraint(t *testing.T, currentVersion string, versionConstraint string, versionMeetsConstraint bool) {\n\tt.Helper()\n\n\tcurrent, err := version.NewVersion(currentVersion)\n\tif err != nil {\n\t\tt.Fatalf(\"Invalid current version specified in test: %v\", err)\n\t}\n\n\terr = run.CheckTerragruntVersionMeetsConstraint(current, versionConstraint)\n\tif versionMeetsConstraint && err != nil {\n\t\tt.Fatalf(\"Expected Terragrunt version %s to meet constraint %s, but got error: %v\", currentVersion, versionConstraint, err)\n\t} else if !versionMeetsConstraint && err == nil {\n\t\tt.Fatalf(\"Expected Terragrunt version %s to NOT meet constraint %s, but got back a nil error\", currentVersion, versionConstraint)\n\t}\n}\n"
  },
  {
    "path": "internal/runner/runall/errors.go",
    "content": "package runall\n\nimport \"fmt\"\n\ntype RunAllDisabledErr struct {\n\tcommand string\n\treason  string\n}\n\nfunc (err RunAllDisabledErr) Error() string {\n\treturn fmt.Sprintf(\"%s with run --all is disabled: %s\", err.command, err.reason)\n}\n\ntype MissingCommand struct{}\n\nfunc (err MissingCommand) Error() string {\n\treturn \"Missing run --all command argument (Example: terragrunt run --all plan)\"\n}\n"
  },
  {
    "path": "internal/runner/runall/runall.go",
    "content": "// Package runall implements the logic for running commands across all units in a stack.\npackage runall\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/runner\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/common\"\n\t\"github.com/gruntwork-io/terragrunt/internal/stacks/clean\"\n\t\"github.com/gruntwork-io/terragrunt/internal/stacks/generate\"\n\t\"github.com/gruntwork-io/terragrunt/internal/worktrees\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\t\"github.com/gruntwork-io/terragrunt/internal/os/stdout\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// Known terraform commands that are explicitly not supported in run --all due to the nature of the command. This is\n// tracked as a map that maps the terraform command to the reasoning behind disallowing the command in run --all.\nvar runAllDisabledCommands = map[string]string{\n\ttf.CommandNameImport:      \"terraform import should only be run against a single state representation to avoid injecting the wrong object in the wrong state representation.\",\n\ttf.CommandNameTaint:       \"terraform taint should only be run against a single state representation to avoid using the wrong state address.\",\n\ttf.CommandNameUntaint:     \"terraform untaint should only be run against a single state representation to avoid using the wrong state address.\",\n\ttf.CommandNameConsole:     \"terraform console requires stdin, which is shared across all instances of run --all when multiple modules run concurrently.\",\n\ttf.CommandNameForceUnlock: \"lock IDs are unique per state representation and thus should not be run with run --all.\",\n}\n\nfunc Run(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {\n\tif opts.TerraformCommand == \"\" {\n\t\treturn errors.New(MissingCommand{})\n\t}\n\n\treason, isDisabled := runAllDisabledCommands[opts.TerraformCommand]\n\tif isDisabled {\n\t\treturn RunAllDisabledErr{\n\t\t\tcommand: opts.TerraformCommand,\n\t\t\treason:  reason,\n\t\t}\n\t}\n\n\trunnerOpts := []common.Option{}\n\n\tr := report.NewReport().WithWorkingDir(opts.WorkingDir)\n\n\tif l.Formatter().DisabledColors() || stdout.IsRedirected() {\n\t\tr.WithDisableColor()\n\t}\n\n\tif opts.ReportFormat != \"\" {\n\t\tr.WithFormat(opts.ReportFormat)\n\t}\n\n\tif opts.SummaryPerUnit {\n\t\tr.WithShowUnitLevelSummary()\n\t}\n\n\tif opts.ReportSchemaFile != \"\" {\n\t\tdefer r.WriteSchemaToFile(opts.ReportSchemaFile) //nolint:errcheck\n\t}\n\n\tif opts.ReportFile != \"\" {\n\t\tdefer r.WriteToFile(opts.ReportFile) //nolint:errcheck\n\t}\n\n\t// Skip summary for programmatic interactions:\n\t// - When JSON output is requested (--json or report format is JSON)\n\t// - When running 'output' command (typically for programmatic consumption)\n\tif !opts.SummaryDisable && !shouldSkipSummary(opts) {\n\t\tdefer func() {\n\t\t\tif err := r.WriteSummary(opts.Writers.Writer); err != nil {\n\t\t\t\tl.Warnf(\"Failed to write summary: %v\", err)\n\t\t\t}\n\t\t}()\n\t}\n\n\tgitFilters := opts.Filters.UniqueGitFilters()\n\n\t// Only create worktrees when git filter expressions are present\n\tvar (\n\t\twts *worktrees.Worktrees\n\t\terr error\n\t)\n\tif len(gitFilters) > 0 {\n\t\twts, err = worktrees.NewWorktrees(ctx, l, opts.WorkingDir, gitFilters)\n\t\tif err != nil {\n\t\t\treturn errors.Errorf(\"failed to create worktrees: %w\", err)\n\t\t}\n\n\t\tdefer func() {\n\t\t\tcleanupErr := wts.Cleanup(ctx, l)\n\t\t\tif cleanupErr != nil {\n\t\t\t\tl.Errorf(\"failed to cleanup worktrees: %v\", cleanupErr)\n\t\t\t}\n\t\t}()\n\t}\n\n\tif !opts.NoStackGenerate {\n\t\t// Set the stack config path to the default location in the working directory\n\t\topts.TerragruntStackConfigPath = filepath.Join(opts.WorkingDir, config.DefaultStackFile)\n\n\t\t// Clean stack folders before calling `generate` when the `--source-update` flag is passed\n\t\tif opts.SourceUpdate {\n\t\t\terrClean := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"stack_clean\", map[string]any{\n\t\t\t\t\"stack_config_path\": opts.TerragruntStackConfigPath,\n\t\t\t\t\"working_dir\":       opts.WorkingDir,\n\t\t\t}, func(ctx context.Context) error {\n\t\t\t\tl.Debugf(\"Running stack clean for %s, as part of generate command\", opts.WorkingDir)\n\t\t\t\treturn clean.CleanStacks(l, opts)\n\t\t\t})\n\t\t\tif errClean != nil {\n\t\t\t\treturn errors.Errorf(\"failed to clean stack directories under %q: %w\", opts.WorkingDir, errClean)\n\t\t\t}\n\t\t}\n\n\t\t// Generate the stack configuration with telemetry tracking\n\t\terr = telemetry.TelemeterFromContext(ctx).Collect(ctx, \"stack_generate\", map[string]any{\n\t\t\t\"stack_config_path\": opts.TerragruntStackConfigPath,\n\t\t\t\"working_dir\":       opts.WorkingDir,\n\t\t}, func(ctx context.Context) error {\n\t\t\treturn generate.GenerateStacks(ctx, l, opts, wts)\n\t\t})\n\n\t\t// Handle any errors during stack generation\n\t\tif err != nil {\n\t\t\treturn errors.Errorf(\"failed to generate stack file: %w\", err)\n\t\t}\n\t} else {\n\t\tl.Debugf(\"Skipping stack generation in %s\", opts.WorkingDir)\n\t}\n\n\t// Pass worktrees to runner for git filter expressions\n\tif wts != nil && len(wts.WorktreePairs) > 0 {\n\t\trunnerOpts = append(runnerOpts, common.WithWorktrees(wts))\n\t}\n\n\trnr, err := runner.NewStackRunner(ctx, l, opts, runnerOpts...)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn RunAllOnStack(ctx, l, opts, rnr, r)\n}\n\nfunc RunAllOnStack(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, rnr common.StackRunner, r *report.Report) error {\n\tl.Debugf(\"%s\", rnr.GetStack().String())\n\n\tisDestroy := opts.TerraformCliArgs.IsDestroyCommand(opts.TerraformCommand)\n\tif err := rnr.LogUnitDeployOrder(l, opts.TerraformCommand, isDestroy, opts.Writers.LogShowAbsPaths); err != nil {\n\t\treturn err\n\t}\n\n\tvar prompt string\n\n\tswitch opts.TerraformCommand {\n\tcase tf.CommandNameApply:\n\t\tprompt = \"Are you sure you want to run 'terragrunt apply' in each unit of the run queue displayed above?\"\n\tcase tf.CommandNameDestroy:\n\t\tprompt = \"WARNING: Are you sure you want to run `terragrunt destroy` in each unit of the run queue displayed above? There is no undo!\"\n\tcase tf.CommandNameState:\n\t\tprompt = \"Are you sure you want to manipulate the state with `terragrunt state` in each unit of the run queue displayed above? Note that absolute paths are shared, while relative paths will be relative to each working directory.\"\n\t}\n\n\tif prompt != \"\" {\n\t\tshouldRunAll, err := shell.PromptUserForYesNo(ctx, l, prompt, opts.NonInteractive, opts.Writers.ErrWriter)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !shouldRunAll {\n\t\t\t// We explicitly exit here to avoid running any defers that might be registered, like from the run summary.\n\t\t\tos.Exit(0)\n\t\t}\n\t}\n\n\tvar runErr error\n\n\ttelemetryErr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"run_all_on_stack\", map[string]any{\n\t\t\"terraform_command\": opts.TerraformCommand,\n\t\t\"working_dir\":       opts.WorkingDir,\n\t}, func(ctx context.Context) error {\n\t\terr := rnr.Run(ctx, l, opts, r)\n\t\tif err != nil {\n\t\t\t// At this stage, we can't handle the error any further, so we just log it and return nil.\n\t\t\t// After this point, we'll need to report on what happened, and we want that to happen\n\t\t\t// after the error summary.\n\t\t\tl.Errorf(\"Run failed: %v\", err)\n\n\t\t\t// Save error to potentially return after telemetry completes\n\t\t\trunErr = err\n\n\t\t\t// Return nil to allow telemetry and reporting to complete\n\t\t\treturn nil\n\t\t}\n\n\t\treturn nil\n\t})\n\n\t// log telemetry error and continue execution\n\tif telemetryErr != nil {\n\t\tl.Warnf(\"Telemetry collection failed: %v\", telemetryErr)\n\t}\n\n\treturn runErr\n}\n\n// shouldSkipSummary determines if summary output should be skipped for programmatic interactions.\n// Summary is skipped when:\n// - The command is 'output' (typically used for programmatic consumption)\n// - JSON output is requested via terraform CLI args (-json flag)\n// - JSON report format is specified (--report-format=json)\nfunc shouldSkipSummary(opts *options.TerragruntOptions) bool {\n\t// Skip summary for 'output' command as it's typically used programmatically\n\tif opts.TerraformCommand == tf.CommandNameOutput {\n\t\treturn true\n\t}\n\n\t// Skip summary when JSON output is requested via -json flag\n\tif opts.TerraformCliArgs.Normalize(iacargs.SingleDashFlag).Contains(tf.FlagNameJSON) {\n\t\treturn true\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "internal/runner/runall/runall_test.go",
    "content": "package runall_test\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runall\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMissingRunAllArguments(t *testing.T) {\n\tt.Parallel()\n\n\ttgOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\ttgOptions.TerraformCommand = \"\"\n\n\terr = runall.Run(t.Context(), logger.CreateLogger(), tgOptions)\n\trequire.Error(t, err)\n\n\tvar missingCommand runall.MissingCommand\n\n\tok := errors.As(err, &missingCommand)\n\tfmt.Println(err, errors.Unwrap(err))\n\tassert.True(t, ok)\n}\n"
  },
  {
    "path": "internal/runner/runcfg/types.go",
    "content": "// Package runcfg provides configuration types for running terragrunt commands.\n// This package exists to break import cycles between config and run packages.\n// The `run` package should only import `runcfg`, never `pkg/config`.\n//\n// Breaking up the imports this way also allows us to ensure that we never do any config parsing in the `run` package,\n// which is slow and needs to be handled carefully.\npackage runcfg\n\nimport (\n\t\"github.com/zclconf/go-cty/cty\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/codegen\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iam\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate\"\n)\n\n// RunConfig contains all configuration data needed to execute terragrunt commands.\n// This is the primary configuration struct passed to runner packages.\ntype RunConfig struct {\n\t// RemoteState contains remote state backend configuration\n\tRemoteState remotestate.RemoteState\n\t// ProcessedIncludes contains processed include configurations\n\tProcessedIncludes map[string]IncludeConfig\n\t// GenerateConfigs contains code generation configurations\n\tGenerateConfigs map[string]codegen.GenerateConfig\n\t// Inputs contains input variables to pass to terraform\n\tInputs map[string]any\n\t// Engine contains engine-specific settings\n\tEngine EngineConfig\n\t// DownloadDir is the custom download directory for terraform source\n\tDownloadDir string\n\t// TerragruntVersionConstraint specifies version constraints for terragrunt\n\tTerragruntVersionConstraint string\n\t// TerraformVersionConstraint specifies version constraints for terraform\n\tTerraformVersionConstraint string\n\t// TerraformBinary is the path to the terraform/tofu binary\n\tTerraformBinary string\n\t// IAMRole contains IAM role options for AWS authentication\n\tIAMRole iam.RoleOptions\n\t// Errors contains error handling configuration\n\tErrors ErrorsConfig\n\t// Dependencies contains paths to dependent modules\n\tDependencies ModuleDependencies\n\t// Terraform contains terraform-specific settings\n\tTerraform TerraformConfig\n\t// Exclude contains exclusion rules\n\tExclude ExcludeConfig\n\t// PreventDestroy prevents terraform destroy from running\n\tPreventDestroy bool\n}\n\n// TerraformConfig contains terraform-specific settings.\ntype TerraformConfig struct {\n\t// Source is the terraform source URL\n\tSource string\n\n\t// IncludeInCopy lists files to include when copying source\n\tIncludeInCopy []string\n\n\t// ExcludeFromCopy lists files to exclude when copying source\n\tExcludeFromCopy []string\n\n\t// ExtraArgs contains extra terraform CLI arguments\n\tExtraArgs []TerraformExtraArguments\n\n\t// BeforeHooks are hooks to run before terraform commands\n\tBeforeHooks []Hook\n\n\t// AfterHooks are hooks to run after terraform commands\n\tAfterHooks []Hook\n\n\t// ErrorHooks are hooks to run on terraform errors\n\tErrorHooks []ErrorHook\n\n\t// NoCopyTerraformLockFile specifies whether to skip copying the lock file\n\t// Defaults to false (copy the lock file) when not set\n\tNoCopyTerraformLockFile bool\n}\n\n// Hook represents a lifecycle hook (before/after).\ntype Hook struct {\n\t// WorkingDir is the directory to run the hook in\n\tWorkingDir string\n\t// Name is the hook identifier\n\tName string\n\t// Commands are terraform commands that trigger this hook\n\tCommands []string\n\t// Execute is the command to execute\n\tExecute []string\n\t// RunOnError specifies whether to run on error\n\tRunOnError bool\n\t// If is a condition for running the hook\n\tIf bool\n\t// SuppressStdout suppresses stdout output\n\tSuppressStdout bool\n}\n\n// ErrorHook represents an error handling hook.\ntype ErrorHook struct {\n\t// WorkingDir is the directory to run the hook in\n\tWorkingDir string\n\t// Name is the hook identifier\n\tName string\n\t// Commands are terraform commands that trigger this hook\n\tCommands []string\n\t// Execute is the command to execute\n\tExecute []string\n\t// OnErrors are error patterns that trigger this hook\n\tOnErrors []string\n\t// SuppressStdout suppresses stdout output\n\tSuppressStdout bool\n}\n\n// TerraformExtraArguments represents extra CLI arguments for terraform.\ntype TerraformExtraArguments struct {\n\t// Arguments are the extra CLI arguments\n\tArguments []string\n\t// RequiredVarFiles are required variable files\n\tRequiredVarFiles []string\n\t// OptionalVarFiles are optional variable files\n\tOptionalVarFiles []string\n\t// EnvVars are environment variables to set\n\tEnvVars map[string]string\n\t// Name is the identifier for this set of arguments\n\tName string\n\t// VarFiles contains the computed list of variable files (required + existing optional files)\n\t// This is computed during config translation.\n\tVarFiles []string\n\t// Commands are terraform commands these arguments apply to\n\tCommands []string\n}\n\n// ExcludeConfig contains exclusion rules.\ntype ExcludeConfig struct {\n\t// Actions are the actions to exclude\n\tActions []string\n\t// If is the condition for exclusion\n\tIf bool\n\t// NoRun specifies whether to skip running\n\tNoRun bool\n\t// ExcludeDependencies specifies whether to exclude dependencies\n\tExcludeDependencies bool\n}\n\n// IsActionListed checks if the action is listed in the exclude block.\nfunc (e *ExcludeConfig) IsActionListed(action string) bool {\n\treturn IsActionListedInExclude(e.Actions, action)\n}\n\n// ShouldPreventRun returns true if execution should be prevented.\nfunc (e *ExcludeConfig) ShouldPreventRun(command string) bool {\n\treturn ShouldPreventRunBasedOnExclude(e.Actions, &e.NoRun, e.If, command)\n}\n\n// IncludeConfig represents an included configuration.\ntype IncludeConfig struct {\n\t// MergeStrategy specifies how to merge the include\n\tMergeStrategy string\n\t// Name is the include name/label\n\tName string\n\t// Path is the path to the included config\n\tPath string\n\t// Expose specifies whether to expose the include\n\tExpose bool\n}\n\n// ModuleDependencies represents paths to dependent modules.\ntype ModuleDependencies struct {\n\t// Paths are the paths to dependent modules\n\tPaths []string\n}\n\n// EngineConfig represents engine-specific configuration.\ntype EngineConfig struct {\n\t// Version is the engine version\n\tVersion string\n\t// Type is the engine type\n\tType string\n\t// Meta contains engine metadata\n\tMeta *cty.Value\n\t// Source is the engine source URL\n\tSource string\n\t// Enable indicates whether the engine block was specified,\n\t// meaning that we should be using the engine.\n\tEnable bool\n}\n\n// ErrorsConfig represents the top-level errors configuration.\ntype ErrorsConfig struct {\n\t// Retry contains retry block configurations\n\tRetry []*RetryBlock\n\t// Ignore contains ignore block configurations\n\tIgnore []*IgnoreBlock\n}\n\n// RetryBlock represents a labeled retry block.\ntype RetryBlock struct {\n\t// Label is the name of the retry block\n\tLabel string\n\t// RetryableErrors are error patterns that trigger retry\n\tRetryableErrors []string\n\t// MaxAttempts is the maximum number of retry attempts\n\tMaxAttempts int\n\t// SleepIntervalSec is the sleep interval between retries in seconds\n\tSleepIntervalSec int\n}\n\n// IgnoreBlock represents a labeled ignore block.\ntype IgnoreBlock struct {\n\t// Signals contains signal mappings\n\tSignals map[string]cty.Value\n\t// Label is the name of the ignore block\n\tLabel string\n\t// Message is an optional message for ignored errors\n\tMessage string\n\t// IgnorableErrors are error patterns that should be ignored\n\tIgnorableErrors []string\n}\n"
  },
  {
    "path": "internal/runner/runcfg/util.go",
    "content": "package runcfg\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/zclconf/go-cty/cty\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/ctyhelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/engine\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errorconfig\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iam\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/hashicorp/go-getter\"\n)\n\n// DefaultEngineType is the default engine type.\nconst DefaultEngineType = \"rpc\"\n\n// CopyLockFile copies the lock file from the source folder to the destination folder.\n//\n// Terraform 0.14 now generates a lock file when you run `terraform init`.\n// If any such file exists, this function will copy the lock file to the destination folder.\nfunc CopyLockFile(l log.Logger, rootWorkingDir string, logShowAbsPaths bool, sourceFolder, destinationFolder string) error {\n\tsourceLockFilePath := filepath.Join(sourceFolder, tf.TerraformLockFile)\n\tdestinationLockFilePath := filepath.Join(destinationFolder, tf.TerraformLockFile)\n\n\tif util.FileExists(sourceLockFilePath) {\n\t\tl.Debugf(\n\t\t\t\"Copying lock file from %s to %s\",\n\t\t\tutil.RelPathForLog(\n\t\t\t\trootWorkingDir,\n\t\t\t\tsourceLockFilePath,\n\t\t\t\tlogShowAbsPaths,\n\t\t\t),\n\t\t\tutil.RelPathForLog(\n\t\t\t\trootWorkingDir,\n\t\t\t\tdestinationLockFilePath,\n\t\t\t\tlogShowAbsPaths,\n\t\t\t),\n\t\t)\n\n\t\treturn util.CopyFile(sourceLockFilePath, destinationLockFilePath)\n\t}\n\n\treturn nil\n}\n\n// GetTerraformSourceURL returns the source URL for OpenTofu/Terraform configuration.\n//\n// There are two ways a user can tell Terragrunt that it needs to download Terraform configurations from a specific\n// URL: via a command-line option or via an entry in the Terragrunt configuration. If the user used one of these, this\n// method returns the source URL. If neither is specified, returns \".\" to indicate the current directory should be\n// used as the source, ensuring a .terragrunt-cache directory is always created for consistency.\nfunc GetTerraformSourceURL(source string, sourceMap map[string]string, originalConfigPath string, cfg *RunConfig) (string, error) {\n\tswitch {\n\tcase source != \"\":\n\t\treturn source, nil\n\tcase cfg != nil && cfg.Terraform.Source != \"\":\n\t\treturn AdjustSourceWithMap(sourceMap, cfg.Terraform.Source, originalConfigPath)\n\tdefault:\n\t\treturn \".\", nil\n\t}\n}\n\n// AdjustSourceWithMap implements the --terragrunt-source-map feature. This function will check if the URL portion of a\n// terraform source matches any entry in the provided source map and if it does, replace it with the configured source\n// in the map. Note that this only performs literal matches with the URL portion.\n//\n// Example:\n// Suppose terragrunt is called with:\n//\n//\t--terragrunt-source-map git::ssh://git@github.com/gruntwork-io/i-dont-exist.git=/path/to/local-modules\n//\n// and the terraform source is:\n//\n//\tgit::ssh://git@github.com/gruntwork-io/i-dont-exist.git//fixtures/source-map/modules/app?ref=master\n//\n// This function will take that source and transform it to:\n//\n//\t/path/to/local-modules//fixtures/source-map/modules/app\nfunc AdjustSourceWithMap(sourceMap map[string]string, source string, modulePath string) (string, error) {\n\t// Skip logic if source map is not configured\n\tif len(sourceMap) == 0 {\n\t\treturn source, nil\n\t}\n\n\t// use go-getter to split the module source string into a valid URL and subdirectory (if // is present)\n\tmoduleURL, moduleSubdir := getter.SourceDirSubdir(source)\n\n\t// if both URL and subdir are missing, something went terribly wrong\n\tif moduleURL == \"\" && moduleSubdir == \"\" {\n\t\treturn \"\", errors.New(InvalidSourceURLWithMapError{ModulePath: modulePath, ModuleSourceURL: source})\n\t}\n\n\t// If module URL is missing, return the source as is as it will not match anything in the map.\n\tif moduleURL == \"\" {\n\t\treturn source, nil\n\t}\n\n\t// Before looking up in sourceMap, make sure to drop any query parameters.\n\tmoduleURLParsed, err := url.Parse(moduleURL)\n\tif err != nil {\n\t\treturn source, err\n\t}\n\n\tmoduleURLParsed.RawQuery = \"\"\n\tmoduleURLQuery := moduleURLParsed.String()\n\n\t// Check if there is an entry to replace the URL portion in the map. Return the source as is if there is no entry in\n\t// the map.\n\tsourcePath, hasKey := sourceMap[moduleURLQuery]\n\tif !hasKey {\n\t\treturn source, nil\n\t}\n\n\t// Since there is a source mapping, replace the module URL portion with the entry in the map, and join with the\n\t// subdir.\n\t// If subdir is missing, check if we can obtain a valid module name from the URL portion.\n\tif moduleSubdir == \"\" {\n\t\tmoduleSubdirFromURL, err := GetModulePathFromSourceURL(moduleURL)\n\t\tif err != nil {\n\t\t\treturn moduleSubdirFromURL, err\n\t\t}\n\n\t\tmoduleSubdir = moduleSubdirFromURL\n\t}\n\n\treturn util.JoinTerraformModulePath(sourcePath, moduleSubdir), nil\n}\n\n// InvalidSourceURLWithMapError is an error type for invalid source URLs when using source map.\ntype InvalidSourceURLWithMapError struct {\n\tModulePath      string\n\tModuleSourceURL string\n}\n\nfunc (err InvalidSourceURLWithMapError) Error() string {\n\treturn fmt.Sprintf(\"The --source-map parameter was passed in, but the source URL in the module at '%s' is invalid: '%s'. Note that the module URL must have a double-slash to separate the repo URL from the path within the repo!\", err.ModulePath, err.ModuleSourceURL)\n}\n\n// ParsingModulePathError is an error type for when module path cannot be parsed from source URL.\ntype ParsingModulePathError struct {\n\tModuleSourceURL string\n}\n\nfunc (err ParsingModulePathError) Error() string {\n\treturn fmt.Sprintf(\"Unable to obtain the module path from the source URL '%s'. Ensure that the URL is in a supported format.\", err.ModuleSourceURL)\n}\n\n// Regexp for module name extraction. It assumes that the query string has already been stripped off.\n// Then we simply capture anything after the last slash, and before `.` or end of string.\nvar moduleNameRegexp = regexp.MustCompile(`(?:.+/)(.+?)(?:\\.|$)`)\n\n// GetModulePathFromSourceURL parses sourceUrl not containing '//', and attempt to obtain a module path.\n// Example:\n//\n// sourceUrl = \"git::ssh://git@ghe.ourcorp.com/OurOrg/module-name.git\"\n// will return \"module-name\".\nfunc GetModulePathFromSourceURL(sourceURL string) (string, error) {\n\t// strip off the query string if present\n\tsourceURL = strings.Split(sourceURL, \"?\")[0]\n\n\tmatches := moduleNameRegexp.FindStringSubmatch(sourceURL)\n\n\t// if regexp returns less/more than the full match + 1 capture group, then something went wrong with regex (invalid source string)\n\tconst matchedPats = 2\n\tif len(matches) != matchedPats {\n\t\treturn \"\", errors.New(ParsingModulePathError{ModuleSourceURL: sourceURL})\n\t}\n\n\treturn matches[1], nil\n}\n\n// EngineOptions fetches engine options from the RunConfig.\nfunc (cfg *RunConfig) EngineOptions() (*engine.EngineConfig, error) {\n\tif !cfg.Engine.Enable {\n\t\treturn nil, nil\n\t}\n\n\t// in case of Meta is null, set empty meta\n\tmeta := map[string]any{}\n\n\tif cfg.Engine.Meta != nil {\n\t\tparsedMeta, err := ctyhelper.ParseCtyValueToMap(*cfg.Engine.Meta)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tmeta = parsedMeta\n\t}\n\n\tversion := cfg.Engine.Version\n\tengineType := cfg.Engine.Type\n\t// if type is null or empty, set to \"rpc\"\n\tif len(engineType) == 0 {\n\t\tengineType = DefaultEngineType\n\t}\n\n\treturn &engine.EngineConfig{\n\t\tSource:  cfg.Engine.Source,\n\t\tVersion: version,\n\t\tType:    engineType,\n\t\tMeta:    meta,\n\t}, nil\n}\n\n// GetIAMRoleOptions returns the IAM role options from the RunConfig.\nfunc (cfg *RunConfig) GetIAMRoleOptions() iam.RoleOptions {\n\treturn cfg.IAMRole\n}\n\n// ErrorsConfig fetches errors configuration from the RunConfig.\n// Returns nil when no retry or ignore blocks are defined, so callers\n// can preserve default error handling (e.g. built-in retryable errors).\nfunc (cfg *RunConfig) ErrorsConfig() (*errorconfig.Config, error) {\n\tif len(cfg.Errors.Retry) == 0 && len(cfg.Errors.Ignore) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tresult := &errorconfig.Config{\n\t\tRetry:  make(map[string]*errorconfig.RetryConfig),\n\t\tIgnore: make(map[string]*errorconfig.IgnoreConfig),\n\t}\n\n\tfor _, retryBlock := range cfg.Errors.Retry {\n\t\tif retryBlock == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Validate retry settings\n\t\tif retryBlock.MaxAttempts < 1 {\n\t\t\treturn nil, errors.Errorf(\"cannot have less than 1 max retry in errors.retry %q, but you specified %d\", retryBlock.Label, retryBlock.MaxAttempts)\n\t\t}\n\n\t\tif retryBlock.SleepIntervalSec < 0 {\n\t\t\treturn nil, errors.Errorf(\"cannot sleep for less than 0 seconds in errors.retry %q, but you specified %d\", retryBlock.Label, retryBlock.SleepIntervalSec)\n\t\t}\n\n\t\tcompiledPatterns := make([]*errorconfig.Pattern, 0, len(retryBlock.RetryableErrors))\n\n\t\tfor _, pattern := range retryBlock.RetryableErrors {\n\t\t\tvalue, err := errorsPattern(pattern)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Errorf(\"invalid retry pattern %q in block %q: %w\",\n\t\t\t\t\tpattern, retryBlock.Label, err)\n\t\t\t}\n\n\t\t\tcompiledPatterns = append(compiledPatterns, value)\n\t\t}\n\n\t\tresult.Retry[retryBlock.Label] = &errorconfig.RetryConfig{\n\t\t\tName:             retryBlock.Label,\n\t\t\tRetryableErrors:  compiledPatterns,\n\t\t\tMaxAttempts:      retryBlock.MaxAttempts,\n\t\t\tSleepIntervalSec: retryBlock.SleepIntervalSec,\n\t\t}\n\t}\n\n\tfor _, ignoreBlock := range cfg.Errors.Ignore {\n\t\tif ignoreBlock == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar signals map[string]any\n\n\t\tif ignoreBlock.Signals != nil {\n\t\t\tvalue := convertValuesMapToCtyVal(ignoreBlock.Signals)\n\n\t\t\tvar err error\n\n\t\t\tsignals, err = ctyhelper.ParseCtyValueToMap(value)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tcompiledPatterns := make([]*errorconfig.Pattern, 0, len(ignoreBlock.IgnorableErrors))\n\n\t\tfor _, pattern := range ignoreBlock.IgnorableErrors {\n\t\t\tvalue, err := errorsPattern(pattern)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Errorf(\"invalid ignore pattern %q in block %q: %w\",\n\t\t\t\t\tpattern, ignoreBlock.Label, err)\n\t\t\t}\n\n\t\t\tcompiledPatterns = append(compiledPatterns, value)\n\t\t}\n\n\t\tresult.Ignore[ignoreBlock.Label] = &errorconfig.IgnoreConfig{\n\t\t\tName:            ignoreBlock.Label,\n\t\t\tIgnorableErrors: compiledPatterns,\n\t\t\tMessage:         ignoreBlock.Message,\n\t\t\tSignals:         signals,\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\n// errorsPattern builds an ErrorsPattern from a string pattern.\nfunc errorsPattern(pattern string) (*errorconfig.Pattern, error) {\n\tisNegative := false\n\tp := pattern\n\n\tif len(p) > 0 && p[0] == '!' {\n\t\tisNegative = true\n\t\tp = p[1:]\n\t}\n\n\tcompiled, err := regexp.Compile(p)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &errorconfig.Pattern{\n\t\tPattern:  compiled,\n\t\tNegative: isNegative,\n\t}, nil\n}\n\n// convertValuesMapToCtyVal takes a map of name - cty.Value pairs and converts to a single cty.Value object.\nfunc convertValuesMapToCtyVal(valMap map[string]cty.Value) cty.Value {\n\tif len(valMap) == 0 {\n\t\t// Return an empty object instead of NilVal for empty maps.\n\t\treturn cty.EmptyObjectVal\n\t}\n\n\t// Use cty.ObjectVal directly instead of gocty.ToCtyValue to preserve marks (like sensitive())\n\treturn cty.ObjectVal(valMap)\n}\n\n// Exclude action constants\nconst (\n\tAllActions              = \"all\"\n\tAllExcludeOutputActions = \"all_except_output\"\n\tTgOutput                = \"output\"\n)\n\n// IsActionListedInExclude checks if the action is listed in the exclude block actions.\n// This is a shared utility function that provides a single source of truth for exclude action matching logic.\n// It handles special action values:\n//   - \"all\": matches any action\n//   - \"all_except_output\": matches any action except \"output\"\n//   - Case-insensitive matching for regular actions\nfunc IsActionListedInExclude(actions []string, action string) bool {\n\tif len(actions) == 0 {\n\t\treturn false\n\t}\n\n\tactionLower := strings.ToLower(action)\n\n\tfor _, checkAction := range actions {\n\t\tif checkAction == AllActions {\n\t\t\treturn true\n\t\t}\n\n\t\tif checkAction == AllExcludeOutputActions && actionLower != TgOutput {\n\t\t\treturn true\n\t\t}\n\n\t\tif strings.ToLower(checkAction) == actionLower {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// ShouldPreventRunBasedOnExclude determines if execution should be prevented based on exclude configuration.\n// This is a shared utility function that provides a single source of truth for exclude run prevention logic.\n// Parameters:\n//   - actions: list of actions in the exclude block\n//   - noRun: pointer to no_run flag (nil means not set)\n//   - ifCondition: the if condition value\n//   - command: the command/action to check\nfunc ShouldPreventRunBasedOnExclude(actions []string, noRun *bool, ifCondition bool, command string) bool {\n\tif !ifCondition {\n\t\treturn false\n\t}\n\n\tswitch {\n\tcase noRun == nil:\n\t\t// When no_run isn't set, preserve legacy behavior: only exact action matches prevent a run.\n\t\treturn slices.Contains(actions, command)\n\tcase !*noRun:\n\t\t// When no_run is explicitly false, never prevent the run.\n\t\treturn false\n\tdefault:\n\t\t// When no_run is explicitly true, use the shared action matcher (supports special values).\n\t\treturn IsActionListedInExclude(actions, command)\n\t}\n}\n"
  },
  {
    "path": "internal/runner/runcfg/util_test.go",
    "content": "package runcfg_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runcfg\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAdjustSourceWithMap(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname           string\n\t\tsourceMap      map[string]string\n\t\tsource         string\n\t\tmodulePath     string\n\t\texpectedResult string\n\t\texpectedError  string\n\t}{\n\t\t{\n\t\t\tname:           \"empty source map returns source unchanged\",\n\t\t\tsourceMap:      nil,\n\t\t\tsource:         \"git::ssh://git@github.com/org/repo.git//path/to/module\",\n\t\t\tmodulePath:     \"/path/to/config.hcl\",\n\t\t\texpectedResult: \"git::ssh://git@github.com/org/repo.git//path/to/module\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"basic source map match with subdirectory\",\n\t\t\tsourceMap: map[string]string{\n\t\t\t\t\"git::ssh://git@github.com/org/repo.git\": \"/local/path\",\n\t\t\t},\n\t\t\tsource:         \"git::ssh://git@github.com/org/repo.git//path/to/module\",\n\t\t\tmodulePath:     \"/path/to/config.hcl\",\n\t\t\texpectedResult: \"/local/path//path/to/module\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"source map match with query parameters\",\n\t\t\tsourceMap: map[string]string{\n\t\t\t\t\"git::ssh://git@github.com/org/repo.git\": \"/local/path\",\n\t\t\t},\n\t\t\tsource:         \"git::ssh://git@github.com/org/repo.git//path/to/module?ref=master\",\n\t\t\tmodulePath:     \"/path/to/config.hcl\",\n\t\t\texpectedResult: \"/local/path//path/to/module\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"source map match without subdirectory - extracts module name\",\n\t\t\tsourceMap: map[string]string{\n\t\t\t\t\"git::ssh://git@github.com/org/module-name.git\": \"/local/path\",\n\t\t\t},\n\t\t\tsource:         \"git::ssh://git@github.com/org/module-name.git?ref=v1.0.0\",\n\t\t\tmodulePath:     \"/path/to/config.hcl\",\n\t\t\texpectedResult: \"/local/path//module-name\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"no match in source map returns source unchanged\",\n\t\t\tsourceMap: map[string]string{\n\t\t\t\t\"git::ssh://git@github.com/org/other-repo.git\": \"/local/path\",\n\t\t\t},\n\t\t\tsource:         \"git::ssh://git@github.com/org/repo.git//path/to/module\",\n\t\t\tmodulePath:     \"/path/to/config.hcl\",\n\t\t\texpectedResult: \"git::ssh://git@github.com/org/repo.git//path/to/module\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"empty URL and subdir returns error\",\n\t\t\tsourceMap: map[string]string{\n\t\t\t\t\"git::ssh://git@github.com/org/repo.git\": \"/local/path\",\n\t\t\t},\n\t\t\tsource:         \"\",\n\t\t\tmodulePath:     \"/path/to/config.hcl\",\n\t\t\texpectedResult: \"\",\n\t\t\texpectedError:  \"invalid\",\n\t\t},\n\t\t{\n\t\t\tname: \"empty URL but has subdir returns source unchanged\",\n\t\t\tsourceMap: map[string]string{\n\t\t\t\t\"git::ssh://git@github.com/org/repo.git\": \"/local/path\",\n\t\t\t},\n\t\t\tsource:         \"//path/to/module\",\n\t\t\tmodulePath:     \"/path/to/config.hcl\",\n\t\t\texpectedResult: \"//path/to/module\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple source map entries - matches correct one\",\n\t\t\tsourceMap: map[string]string{\n\t\t\t\t\"git::ssh://git@github.com/org/repo1.git\": \"/local/path1\",\n\t\t\t\t\"git::ssh://git@github.com/org/repo2.git\": \"/local/path2\",\n\t\t\t},\n\t\t\tsource:         \"git::ssh://git@github.com/org/repo2.git//path/to/module\",\n\t\t\tmodulePath:     \"/path/to/config.hcl\",\n\t\t\texpectedResult: \"/local/path2//path/to/module\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"source map with trailing slash in mapped path\",\n\t\t\tsourceMap: map[string]string{\n\t\t\t\t\"git::ssh://git@github.com/org/repo.git\": \"/local/path/\",\n\t\t\t},\n\t\t\tsource:         \"git::ssh://git@github.com/org/repo.git//module\",\n\t\t\tmodulePath:     \"/path/to/config.hcl\",\n\t\t\texpectedResult: \"/local/path//module\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"source map with leading slash in subdirectory\",\n\t\t\tsourceMap: map[string]string{\n\t\t\t\t\"git::ssh://git@github.com/org/repo.git\": \"/local/path\",\n\t\t\t},\n\t\t\tsource:         \"git::ssh://git@github.com/org/repo.git///module\",\n\t\t\tmodulePath:     \"/path/to/config.hcl\",\n\t\t\texpectedResult: \"/local/path//module\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"complex URL with multiple query parameters\",\n\t\t\tsourceMap: map[string]string{\n\t\t\t\t\"git::ssh://git@github.com/org/repo.git\": \"/local/path\",\n\t\t\t},\n\t\t\tsource:         \"git::ssh://git@github.com/org/repo.git//path/to/module?ref=master&depth=1\",\n\t\t\tmodulePath:     \"/path/to/config.hcl\",\n\t\t\texpectedResult: \"/local/path//path/to/module\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"module name extraction from URL with .git extension\",\n\t\t\tsourceMap: map[string]string{\n\t\t\t\t\"git::ssh://git@github.com/org/my-terraform-module.git\": \"/local/path\",\n\t\t\t},\n\t\t\tsource:         \"git::ssh://git@github.com/org/my-terraform-module.git\",\n\t\t\tmodulePath:     \"/path/to/config.hcl\",\n\t\t\texpectedResult: \"/local/path//my-terraform-module\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"module name extraction from URL without .git extension\",\n\t\t\tsourceMap: map[string]string{\n\t\t\t\t\"git::ssh://git@github.com/org/my-module\": \"/local/path\",\n\t\t\t},\n\t\t\tsource:         \"git::ssh://git@github.com/org/my-module\",\n\t\t\tmodulePath:     \"/path/to/config.hcl\",\n\t\t\texpectedResult: \"/local/path//my-module\",\n\t\t\texpectedError:  \"\",\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.Parallel()\n\n\t\t\tresult, err := runcfg.AdjustSourceWithMap(tc.sourceMap, tc.source, tc.modulePath)\n\n\t\t\tif tc.expectedError != \"\" {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), tc.expectedError)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.expectedResult, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetModulePathFromSourceURL(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname           string\n\t\tsourceURL      string\n\t\texpectedResult string\n\t\texpectedError  string\n\t}{\n\t\t{\n\t\t\tname:           \"extract module name from git URL with .git\",\n\t\t\tsourceURL:      \"git::ssh://git@github.com/org/module-name.git\",\n\t\t\texpectedResult: \"module-name\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname:           \"extract module name from git URL without .git\",\n\t\t\tsourceURL:      \"git::ssh://git@github.com/org/module-name\",\n\t\t\texpectedResult: \"module-name\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname:           \"extract module name with query parameters\",\n\t\t\tsourceURL:      \"git::ssh://git@github.com/org/my-module.git?ref=master\",\n\t\t\texpectedResult: \"my-module\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname:           \"extract module name with dashes\",\n\t\t\tsourceURL:      \"git::ssh://git@github.com/org/my-terraform-module.git\",\n\t\t\texpectedResult: \"my-terraform-module\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname:           \"extract module name with underscores\",\n\t\t\tsourceURL:      \"git::ssh://git@github.com/org/my_terraform_module.git\",\n\t\t\texpectedResult: \"my_terraform_module\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname:           \"invalid URL format returns error\",\n\t\t\tsourceURL:      \"invalid-url\",\n\t\t\texpectedResult: \"\",\n\t\t\texpectedError:  \"Unable to obtain the module path\",\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.Parallel()\n\n\t\t\tresult, err := runcfg.GetModulePathFromSourceURL(tc.sourceURL)\n\n\t\t\tif tc.expectedError != \"\" {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), tc.expectedError)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.expectedResult, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestGetTerraformSourceURL(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname               string\n\t\tsource             string\n\t\tsourceMap          map[string]string\n\t\toriginalConfigPath string\n\t\tcfg                *runcfg.RunConfig\n\t\texpectedResult     string\n\t\texpectedError      string\n\t}{\n\t\t{\n\t\t\tname:      \"source from options takes precedence\",\n\t\t\tsource:    \"git::ssh://git@github.com/org/repo.git\",\n\t\t\tsourceMap: map[string]string{},\n\t\t\tcfg: &runcfg.RunConfig{\n\t\t\t\tTerraform: runcfg.TerraformConfig{\n\t\t\t\t\tSource: \"git::ssh://git@github.com/org/other-repo.git\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult: \"git::ssh://git@github.com/org/repo.git\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname:   \"source from config with source map\",\n\t\t\tsource: \"\",\n\t\t\tsourceMap: map[string]string{\n\t\t\t\t\"git::ssh://git@github.com/org/repo.git\": \"/local/path\",\n\t\t\t},\n\t\t\toriginalConfigPath: \"/path/to/config.hcl\",\n\t\t\tcfg: &runcfg.RunConfig{\n\t\t\t\tTerraform: runcfg.TerraformConfig{\n\t\t\t\t\tSource: \"git::ssh://git@github.com/org/repo.git//module?ref=master\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedResult: \"/local/path//module\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname:           \"no source returns current directory\",\n\t\t\tsource:         \"\",\n\t\t\tsourceMap:      map[string]string{},\n\t\t\tcfg:            &runcfg.RunConfig{},\n\t\t\texpectedResult: \".\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t\t{\n\t\t\tname:           \"nil terraform config returns current directory\",\n\t\t\tsource:         \"\",\n\t\t\tsourceMap:      map[string]string{},\n\t\t\tcfg:            &runcfg.RunConfig{},\n\t\t\texpectedResult: \".\",\n\t\t\texpectedError:  \"\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\ttc := tc\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresult, err := runcfg.GetTerraformSourceURL(tc.source, tc.sourceMap, tc.originalConfigPath, tc.cfg)\n\n\t\t\tif tc.expectedError != \"\" {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), tc.expectedError)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.expectedResult, result)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestInvalidSourceURLWithMapError(t *testing.T) {\n\tt.Parallel()\n\n\terr := runcfg.InvalidSourceURLWithMapError{\n\t\tModulePath:      \"/path/to/config.hcl\",\n\t\tModuleSourceURL: \"invalid-source\",\n\t}\n\n\terrorMsg := err.Error()\n\tassert.Contains(t, errorMsg, \"/path/to/config.hcl\")\n\tassert.Contains(t, errorMsg, \"invalid-source\")\n\tassert.Contains(t, errorMsg, \"invalid\")\n}\n\nfunc TestParsingModulePathError(t *testing.T) {\n\tt.Parallel()\n\n\terr := runcfg.ParsingModulePathError{\n\t\tModuleSourceURL: \"git::invalid-url\",\n\t}\n\n\terrorMsg := err.Error()\n\tassert.Contains(t, errorMsg, \"git::invalid-url\")\n\tassert.Contains(t, errorMsg, \"Unable to obtain the module path\")\n}\n"
  },
  {
    "path": "internal/runner/runner.go",
    "content": "// Package runner provides logic for applying Stacks and Units Terragrunt.\npackage runner\n\nimport (\n\t\"context\"\n\t\"maps\"\n\t\"path/filepath\"\n\t\"slices\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/common\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runnerpool\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// NewStackRunner discovers all Terragrunt units under the working directory and\n// assembles them into a StackRunner that can apply or destroy them.\nfunc NewStackRunner(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\trunnerOpts ...common.Option,\n) (common.StackRunner, error) {\n\treturn runnerpool.Build(ctx, l, opts, runnerOpts...)\n}\n\n// BuildUnitOpts is a facade for runnerpool.BuildUnitOpts.\nfunc BuildUnitOpts(l log.Logger, stackOpts *options.TerragruntOptions, unit *component.Unit) (*options.TerragruntOptions, log.Logger, error) {\n\treturn runnerpool.BuildUnitOpts(l, stackOpts, unit)\n}\n\n// FindDependentUnits - find dependent units for a given unit.\n// 1. Find root git top level directory and build list of units\n// 2. Iterate over includes from opts if git top level directory detection failed\n// 3. Filter found units for those that have dependencies on the unit in the working directory\nfunc FindDependentUnits(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tcfg *config.TerragruntConfig,\n) []*component.Unit {\n\tmatchedUnitsMap := make(map[string]*component.Unit)\n\tpathsToCheck := discoverPathsToCheck(ctx, l, opts, cfg)\n\n\tfor _, dir := range pathsToCheck {\n\t\tmaps.Copy(\n\t\t\tmatchedUnitsMap,\n\t\t\tfindMatchingUnitsInPath(\n\t\t\t\tctx,\n\t\t\t\tl,\n\t\t\t\tdir,\n\t\t\t\topts,\n\t\t\t),\n\t\t)\n\t}\n\n\tmatchedUnits := make([]*component.Unit, 0, len(matchedUnitsMap))\n\tfor _, unit := range matchedUnitsMap {\n\t\tmatchedUnits = append(matchedUnits, unit)\n\t}\n\n\treturn matchedUnits\n}\n\n// discoverPathsToCheck finds root git top level directory and builds list of units, or iterates over includes if git detection fails.\nfunc discoverPathsToCheck(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, terragruntConfig *config.TerragruntConfig) []string {\n\tvar pathsToCheck []string\n\n\tif gitTopLevelDir, err := shell.GitTopLevelDir(ctx, l, opts.Env, opts.WorkingDir); err == nil {\n\t\tpathsToCheck = append(pathsToCheck, gitTopLevelDir)\n\t} else {\n\t\tuniquePaths := make(map[string]bool)\n\t\tfor _, includePath := range terragruntConfig.ProcessedIncludes {\n\t\t\tuniquePaths[filepath.Dir(includePath.Path)] = true\n\t\t}\n\n\t\tfor path := range uniquePaths {\n\t\t\tpathsToCheck = append(pathsToCheck, path)\n\t\t}\n\t}\n\n\treturn pathsToCheck\n}\n\n// findMatchingUnitsInPath builds the stack from the config directory and filters units by working dir dependencies.\nfunc findMatchingUnitsInPath(ctx context.Context, l log.Logger, dir string, opts *options.TerragruntOptions) map[string]*component.Unit {\n\tmatchedUnitsMap := make(map[string]*component.Unit)\n\n\t// Construct the full path to terragrunt.hcl in the directory\n\tconfigPath := filepath.Join(dir, filepath.Base(opts.TerragruntConfigPath))\n\n\tcfgOpts, err := options.NewTerragruntOptionsWithConfigPath(configPath)\n\tif err != nil {\n\t\tl.Debugf(\"Failed to build terragrunt options from %s %v\", configPath, err)\n\t\treturn matchedUnitsMap\n\t}\n\n\tcfgOpts.Env = opts.Env\n\tcfgOpts.OriginalTerragruntConfigPath = opts.OriginalTerragruntConfigPath\n\tcfgOpts.TerraformCommand = opts.TerraformCommand\n\tcfgOpts.TerraformCliArgs = opts.TerraformCliArgs\n\tcfgOpts.CheckDependentUnits = opts.CheckDependentUnits\n\tcfgOpts.NonInteractive = true\n\n\tl.Infof(\"Discovering dependent units for %s\", opts.TerragruntConfigPath)\n\n\trnr, err := NewStackRunner(ctx, l, cfgOpts)\n\tif err != nil {\n\t\tl.Debugf(\"Failed to build unit stack %v\", err)\n\t\treturn matchedUnitsMap\n\t}\n\n\tstack := rnr.GetStack()\n\tdependentUnits := rnr.ListStackDependentUnits()\n\n\tdeps, found := dependentUnits[opts.WorkingDir]\n\tif found {\n\t\tfor _, unit := range stack.Units {\n\t\t\tif slices.Contains(deps, unit.Path()) {\n\t\t\t\tmatchedUnitsMap[unit.Path()] = unit\n\t\t\t}\n\t\t}\n\t}\n\n\treturn matchedUnitsMap\n}\n"
  },
  {
    "path": "internal/runner/runnerpool/builder.go",
    "content": "package runnerpool\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/common\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// Build stack runner using discovery and queueing mechanisms.\nfunc Build(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\trunnerOpts ...common.Option,\n) (common.StackRunner, error) {\n\tdiscovered, err := discoverWithRetry(ctx, l, opts, runnerOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trnr, err := createRunner(ctx, l, opts, discovered, runnerOpts...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := checkVersionConstraints(ctx, l, opts, rnr.GetStack().Units); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn rnr, nil\n}\n"
  },
  {
    "path": "internal/runner/runnerpool/builder_helpers.go",
    "content": "package runnerpool\n\nimport (\n\t\"context\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\n\t\"golang.org/x/sync/errgroup\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/common\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/internal/worktrees\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// telemetry event names used in this file\nconst (\n\ttelemetryDiscovery = \"runner_pool_discovery\"\n\ttelemetryCreation  = \"runner_pool_creation\"\n)\n\n// doWithTelemetry is a small helper to standardize telemetry collection calls.\nfunc doWithTelemetry(ctx context.Context, name string, fields map[string]any, fn func(context.Context) error) error {\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, name, fields, fn)\n}\n\n// resolveWorkingDir determines the canonical working directory for discovery.\nfunc resolveWorkingDir(opts *options.TerragruntOptions) string {\n\tif opts.RootWorkingDir != \"\" {\n\t\treturn opts.RootWorkingDir\n\t}\n\n\treturn opts.WorkingDir\n}\n\n// buildConfigFilenames returns the list of config filenames to consider, including custom if provided.\nfunc buildConfigFilenames(opts *options.TerragruntOptions) []string {\n\tconfigFilenames := append([]string{}, discovery.DefaultConfigFilenames...)\n\tcustomConfigName := filepath.Base(opts.TerragruntConfigPath)\n\tisCustom := !slices.Contains(discovery.DefaultConfigFilenames, customConfigName)\n\n\tif isCustom && customConfigName != \"\" && customConfigName != \".\" {\n\t\tconfigFilenames = append(configFilenames, customConfigName)\n\t}\n\n\treturn configFilenames\n}\n\n// extractWorktrees finds WorktreeOption in options and returns worktrees.\nfunc extractWorktrees(opts []common.Option) *worktrees.Worktrees {\n\tfor _, opt := range opts {\n\t\tif wo, ok := opt.(common.WorktreeOption); ok {\n\t\t\treturn wo.Worktrees\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// newBaseDiscovery constructs the base discovery with common immutable options.\nfunc newBaseDiscovery(\n\topts *options.TerragruntOptions,\n\tworkingDir string,\n\tconfigFilenames []string,\n\trunnerOpts ...common.Option,\n) *discovery.Discovery {\n\tanyOpts := make([]any, len(runnerOpts))\n\tfor i, v := range runnerOpts {\n\t\tanyOpts[i] = v\n\t}\n\n\td := discovery.\n\t\tNewDiscovery(workingDir).\n\t\tWithOptions(anyOpts...).\n\t\tWithConfigFilenames(configFilenames).\n\t\tWithRelationships().\n\t\tWithDiscoveryContext(&component.DiscoveryContext{\n\t\t\tWorkingDir: workingDir,\n\t\t\tCmd:        opts.TerraformCliArgs.First(),\n\t\t\tArgs:       opts.TerraformCliArgs.Tail(),\n\t\t})\n\n\treturn d\n}\n\n// prepareDiscovery constructs a configured discovery instance based on Terragrunt options and flags.\nfunc prepareDiscovery(\n\topts *options.TerragruntOptions,\n\trunnerOpts ...common.Option,\n) *discovery.Discovery {\n\tworkingDir := resolveWorkingDir(opts)\n\tconfigFilenames := buildConfigFilenames(opts)\n\n\td := newBaseDiscovery(opts, workingDir, configFilenames, runnerOpts...)\n\n\t// Apply pre-parsed filters when provided\n\tif len(opts.Filters) > 0 {\n\t\td = d.WithFilters(opts.Filters)\n\t}\n\n\t// Apply worktrees for git filter expressions\n\tif w := extractWorktrees(runnerOpts); w != nil {\n\t\td = d.WithWorktrees(w)\n\t}\n\n\treturn d\n}\n\n// discoverWithRetry runs discovery and retries without exclude-by-default if zero results\n// are found and modules-that-include / units-reading flags are set.\nfunc discoverWithRetry(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\trunnerOpts ...common.Option,\n) (component.Components, error) {\n\t// Initial discovery with current excludeByDefault setting\n\td := prepareDiscovery(opts, runnerOpts...)\n\n\tvar discovered component.Components\n\n\terr := doWithTelemetry(ctx, telemetryDiscovery, map[string]any{\n\t\t\"working_dir\":       opts.WorkingDir,\n\t\t\"terraform_command\": opts.TerraformCommand,\n\t}, func(childCtx context.Context) error {\n\t\tvar discoveryErr error\n\n\t\tdiscovered, discoveryErr = d.Discover(childCtx, l, opts)\n\t\tif discoveryErr == nil {\n\t\t\tl.Debugf(\"Runner pool discovery found %d configs\", len(discovered))\n\t\t}\n\n\t\treturn discoveryErr\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn discovered, nil\n}\n\n// createRunner wraps runner creation with telemetry and returns the stack runner.\nfunc createRunner(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tcomps component.Components,\n\trunnerOpts ...common.Option,\n) (common.StackRunner, error) {\n\tvar rnr common.StackRunner\n\n\terr := doWithTelemetry(ctx, telemetryCreation, map[string]any{\n\t\t\"discovered_configs\": len(comps),\n\t\t\"terraform_command\":  opts.TerraformCommand,\n\t}, func(childCtx context.Context) error {\n\t\tvar err2 error\n\n\t\trnr, err2 = NewRunnerPoolStack(childCtx, l, opts, comps, runnerOpts...)\n\n\t\treturn err2\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn rnr, nil\n}\n\n// checkVersionConstraints performs version constraint checks on all discovered units concurrently.\n// It uses errgroup to coordinate concurrent checks and returns the first error encountered.\nfunc checkVersionConstraints(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tunits []*component.Unit,\n) error {\n\tg, checkCtx := errgroup.WithContext(ctx)\n\n\tmaxWorkers := min(runtime.NumCPU(), opts.Parallelism)\n\tg.SetLimit(maxWorkers)\n\n\tfor _, unit := range units {\n\t\tg.Go(func() error {\n\t\t\tunitOpts, unitLogger, err := BuildUnitOpts(l, opts, unit)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn checkUnitVersionConstraints(\n\t\t\t\tcheckCtx,\n\t\t\t\tl,\n\t\t\t\tunitOpts,\n\t\t\t\tunitLogger,\n\t\t\t\tunit,\n\t\t\t)\n\t\t})\n\t}\n\n\treturn g.Wait()\n}\n\n// checkUnitVersionConstraints checks version constraints for a single unit.\n// It handles config parsing if needed and performs version constraint validation.\nfunc checkUnitVersionConstraints(\n\tctx context.Context,\n\tl log.Logger,\n\tunitOpts *options.TerragruntOptions,\n\tunitLogger log.Logger,\n\tunit *component.Unit,\n) error {\n\tunitConfig := unit.Config()\n\n\t// This is almost definitely already parsed, but we'll check just in case.\n\tif unitConfig == nil {\n\t\tconfigCtx, pctx := configbridge.NewParsingContext(ctx, l, unitOpts)\n\t\tpctx = pctx.WithDecodeList(\n\t\t\tconfig.TerragruntVersionConstraints,\n\t\t\tconfig.FeatureFlagsBlock,\n\t\t)\n\n\t\tvar err error\n\n\t\tunitConfig, err = config.PartialParseConfigFile(\n\t\t\tconfigCtx,\n\t\t\tpctx,\n\t\t\tl,\n\t\t\tunit.ConfigFile(),\n\t\t\tnil,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn errors.Errorf(\"failed to parse config for unit %s: %w\", unit.DisplayPath(), err)\n\t\t}\n\t}\n\n\tif !unitOpts.TFPathExplicitlySet && unitConfig.TerraformBinary != \"\" {\n\t\tunitOpts.TFPath = unitConfig.TerraformBinary\n\t}\n\n\tif unitLogger != nil {\n\t\tl = unitLogger\n\t}\n\n\t_, ver, impl, err := run.PopulateTFVersion(ctx, l, unitOpts.WorkingDir, unitOpts.VersionManagerFileName, configbridge.TFRunOptsFromOpts(unitOpts))\n\tif err != nil {\n\t\treturn errors.Errorf(\"failed to populate Terraform version for unit %s: %w\", unit.DisplayPath(), err)\n\t}\n\n\tunitOpts.TerraformVersion = ver\n\tunitOpts.TofuImplementation = impl\n\n\tterraformVersionConstraint := run.DefaultTerraformVersionConstraint\n\tif unitConfig.TerraformVersionConstraint != \"\" {\n\t\tterraformVersionConstraint = unitConfig.TerraformVersionConstraint\n\t}\n\n\tif err := run.CheckTerraformVersionMeetsConstraint(unitOpts.TerraformVersion, terraformVersionConstraint); err != nil {\n\t\treturn errors.Errorf(\"Terraform version check failed for unit %s: %w\", unit.DisplayPath(), err)\n\t}\n\n\tif unitConfig.TerragruntVersionConstraint != \"\" {\n\t\tif err := run.CheckTerragruntVersionMeetsConstraint(\n\t\t\tunitOpts.TerragruntVersion,\n\t\t\tunitConfig.TerragruntVersionConstraint,\n\t\t); err != nil {\n\t\t\treturn errors.Errorf(\"Terragrunt version check failed for unit %s: %w\", unit.DisplayPath(), err)\n\t\t}\n\t}\n\n\tif unitConfig.FeatureFlags != nil {\n\t\tfor _, flag := range unitConfig.FeatureFlags {\n\t\t\tflagName := flag.Name\n\n\t\t\tdefaultValue, err := flag.DefaultAsString()\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Errorf(\"failed to get default value for feature flag %s in unit %s: %w\", flagName, unit.DisplayPath(), err)\n\t\t\t}\n\n\t\t\tif _, exists := unitOpts.FeatureFlags.Load(flagName); !exists {\n\t\t\t\tunitOpts.FeatureFlags.Store(flagName, defaultValue)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/runner/runnerpool/controller.go",
    "content": "package runnerpool\n\nimport (\n\t\"context\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/queue\"\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\n\t\"github.com/puzpuzpuz/xsync/v3\"\n)\n\n// UnitRunner defines a function type that executes a Unit within a given context and returns an error.\ntype UnitRunner func(ctx context.Context, u *component.Unit) error\n\n// Controller orchestrates concurrent execution over a DAG.\ntype Controller struct {\n\tq           *queue.Queue\n\trunner      UnitRunner\n\treadyCh     chan struct{}\n\tunitsMap    map[string]*component.Unit\n\tconcurrency int\n}\n\n// ControllerOption is a function that modifies a Controller.\ntype ControllerOption func(*Controller)\n\n// WithRunner sets the UnitRunner for the Controller.\nfunc WithRunner(runner UnitRunner) ControllerOption {\n\treturn func(dr *Controller) {\n\t\tdr.runner = runner\n\t}\n}\n\n// WithMaxConcurrency sets the concurrency for the Controller.\nfunc WithMaxConcurrency(concurrency int) ControllerOption {\n\treturn func(dr *Controller) {\n\t\tif concurrency <= 0 {\n\t\t\tconcurrency = 1\n\t\t}\n\n\t\tdr.concurrency = concurrency\n\t}\n}\n\n// NewController creates a new Controller with the given options and a pre-built queue.\nfunc NewController(q *queue.Queue, units []*component.Unit, opts ...ControllerOption) *Controller {\n\tdr := &Controller{\n\t\tq:           q,\n\t\treadyCh:     make(chan struct{}, 1), // buffered to avoid blocking\n\t\tconcurrency: options.DefaultParallelism,\n\t}\n\t// Map to link runner Units and Queue Entries\n\tunitsMap := make(map[string]*component.Unit)\n\n\tfor _, u := range units {\n\t\tif u != nil && u.Path() != \"\" {\n\t\t\tunitsMap[u.Path()] = u\n\t\t}\n\t}\n\n\tdr.unitsMap = unitsMap\n\tfor _, opt := range opts {\n\t\topt(dr)\n\t}\n\n\tif dr.q == nil {\n\t\t// If the queue was not set, create an empty queue\n\t\tdr.q = &queue.Queue{Entries: []*queue.Entry{}}\n\t}\n\n\treturn dr\n}\n\n// Run executes the Queue return error summarizing all entries that failed to run.\nfunc (dr *Controller) Run(ctx context.Context, l log.Logger) error {\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, \"runner_pool_controller\", map[string]any{\n\t\t\"total_tasks\":             len(dr.q.Entries),\n\t\t\"concurrency\":             dr.concurrency,\n\t\t\"fail_fast\":               dr.q.FailFast,\n\t\t\"ignore_dependency_order\": dr.q.IgnoreDependencyOrder,\n\t}, func(childCtx context.Context) error {\n\t\tvar (\n\t\t\twg      sync.WaitGroup\n\t\t\tsem     = make(chan struct{}, dr.concurrency)\n\t\t\tresults = xsync.NewMapOf[string, error]()\n\t\t)\n\n\t\tif dr.runner == nil {\n\t\t\treturn errors.Errorf(\"Runner Pool Controller: runner is not set, cannot run\")\n\t\t}\n\n\t\tl.Debugf(\"Runner Pool Controller: starting with %d tasks, concurrency %d\",\n\t\t\tlen(dr.q.Entries), dr.concurrency)\n\n\t\t// Initial signal to start scheduling\n\t\tselect {\n\t\tcase dr.readyCh <- struct{}{}:\n\t\tdefault:\n\t\t}\n\n\t\tfor {\n\t\t\treadyEntries := dr.q.GetReadyWithDependencies(l)\n\t\t\tl.Debugf(\"Runner Pool Controller: found %d readyEntries tasks\", len(readyEntries))\n\n\t\t\tfor _, e := range readyEntries {\n\t\t\t\t// log debug which entry is running\n\t\t\t\tl.Debugf(\"Runner Pool Controller: running %s\", e.Component.Path())\n\t\t\t\tdr.q.SetEntryStatus(e, queue.StatusRunning)\n\n\t\t\t\tsem <- struct{}{}\n\n\t\t\t\twg.Add(1)\n\n\t\t\t\tgo func(ent *queue.Entry) {\n\t\t\t\t\tdefer func() {\n\t\t\t\t\t\t<-sem\n\t\t\t\t\t\twg.Done()\n\n\t\t\t\t\t\tselect {\n\t\t\t\t\t\tcase dr.readyCh <- struct{}{}:\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t}\n\t\t\t\t\t}()\n\n\t\t\t\t\tunit := dr.unitsMap[ent.Component.Path()]\n\t\t\t\t\tif unit == nil {\n\t\t\t\t\t\terr := errors.Errorf(\"unit for path %s not found in discovered units\", ent.Component.Path())\n\t\t\t\t\t\tl.Errorf(\"Runner Pool Controller: unit for path %s not found in discovered units, skipping execution\", ent.Component.Path())\n\t\t\t\t\t\tdr.q.FailEntry(ent)\n\t\t\t\t\t\tresults.Store(ent.Component.Path(), err)\n\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\terr := dr.runner(childCtx, unit)\n\t\t\t\t\tresults.Store(ent.Component.Path(), err)\n\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tl.Debugf(\"Runner Pool Controller: %s failed\", ent.Component.Path())\n\t\t\t\t\t\tdr.q.FailEntry(ent)\n\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tl.Debugf(\"Runner Pool Controller: %s succeeded\", ent.Component.Path())\n\t\t\t\t\tdr.q.SetEntryStatus(ent, queue.StatusSucceeded)\n\t\t\t\t}(e)\n\t\t\t}\n\n\t\t\tif dr.q.Finished() {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tselect {\n\t\t\tcase <-dr.readyCh:\n\t\t\tcase <-childCtx.Done():\n\t\t\t\twg.Wait()\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\twg.Wait()\n\n\t\t// Collect errors from results map and check for errors\n\t\terrCollector := &errors.MultiError{}\n\n\t\tfor _, entry := range dr.q.Entries {\n\t\t\tif err, ok := results.Load(entry.Component.Path()); ok {\n\t\t\t\tif err == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\terrCollector = errCollector.Append(err)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif entry.Status == queue.StatusEarlyExit {\n\t\t\t\tfailedDep := findFailedDependency(entry, dr.q)\n\t\t\t\terrCollector = errCollector.Append(NewUnitEarlyExitError(entry.Component.Path(), failedDep))\n\t\t\t}\n\n\t\t\tif entry.Status == queue.StatusFailed {\n\t\t\t\terrCollector = errCollector.Append(NewUnitFailedError(entry.Component.Path()))\n\t\t\t}\n\t\t}\n\n\t\treturn errCollector.ErrorOrNil()\n\t})\n}\n"
  },
  {
    "path": "internal/runner/runnerpool/controller_test.go",
    "content": "package runnerpool_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runnerpool\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/queue\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// buildComponentUnits creates component units and wires dependencies based on path relationships.\nfunc buildComponentUnits(paths []string, depMap map[string][]string) []*component.Unit {\n\tunitMap := make(map[string]*component.Unit)\n\n\t// First pass: create units\n\tfor _, path := range paths {\n\t\tunitMap[path] = component.NewUnit(path)\n\t}\n\n\t// Second pass: wire dependencies\n\tfor path, deps := range depMap {\n\t\tunit := unitMap[path]\n\t\tfor _, depPath := range deps {\n\t\t\tif depUnit, ok := unitMap[depPath]; ok {\n\t\t\t\tunit.AddDependency(depUnit)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Collect in order\n\tunits := make([]*component.Unit, 0, len(paths))\n\tfor _, path := range paths {\n\t\tunits = append(units, unitMap[path])\n\t}\n\n\treturn units\n}\n\nfunc TestRunnerPool_LinearDependency(t *testing.T) {\n\tt.Parallel()\n\n\t// A -> B -> C\n\tunits := buildComponentUnits(\n\t\t[]string{\"A\", \"B\", \"C\"},\n\t\tmap[string][]string{\n\t\t\t\"B\": {\"A\"},\n\t\t\t\"C\": {\"B\"},\n\t\t},\n\t)\n\n\tcomponents := make(component.Components, len(units))\n\tfor i, u := range units {\n\t\tcomponents[i] = u\n\t}\n\n\trunner := func(ctx context.Context, u *component.Unit) error {\n\t\treturn nil\n\t}\n\n\tq, err := queue.NewQueue(components)\n\trequire.NoError(t, err)\n\n\tdagRunner := runnerpool.NewController(\n\t\tq,\n\t\tunits,\n\t\trunnerpool.WithRunner(runner),\n\t\trunnerpool.WithMaxConcurrency(2),\n\t)\n\terr = dagRunner.Run(t.Context(), logger.CreateLogger())\n\trequire.NoError(t, err)\n}\n\nfunc TestRunnerPool_ParallelExecution(t *testing.T) {\n\tt.Parallel()\n\t//   A\n\t//  / \\\n\t// B   C\n\tunits := buildComponentUnits(\n\t\t[]string{\"A\", \"B\", \"C\"},\n\t\tmap[string][]string{\n\t\t\t\"B\": {\"A\"},\n\t\t\t\"C\": {\"A\"},\n\t\t},\n\t)\n\n\trunner := func(ctx context.Context, u *component.Unit) error {\n\t\treturn nil\n\t}\n\n\tcomponents := make(component.Components, len(units))\n\tfor i, u := range units {\n\t\tcomponents[i] = u\n\t}\n\n\tq, err := queue.NewQueue(components)\n\trequire.NoError(t, err)\n\n\tdagRunner := runnerpool.NewController(\n\t\tq,\n\t\tunits,\n\t\trunnerpool.WithRunner(runner),\n\t\trunnerpool.WithMaxConcurrency(2),\n\t)\n\terr = dagRunner.Run(t.Context(), logger.CreateLogger())\n\trequire.NoError(t, err)\n}\n\nfunc TestRunnerPool_FailFast(t *testing.T) {\n\tt.Parallel()\n\t// A -> B -> C\n\tunits := buildComponentUnits(\n\t\t[]string{\"A\", \"B\", \"C\"},\n\t\tmap[string][]string{\n\t\t\t\"B\": {\"A\"},\n\t\t\t\"C\": {\"B\"},\n\t\t},\n\t)\n\n\trunner := func(ctx context.Context, u *component.Unit) error {\n\t\tif u.Path() == \"A\" {\n\t\t\treturn errors.New(\"unit A failed\")\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tcomponents := make(component.Components, len(units))\n\tfor i, u := range units {\n\t\tcomponents[i] = u\n\t}\n\n\tq, err := queue.NewQueue(components)\n\trequire.NoError(t, err)\n\n\tq.FailFast = true\n\tdagRunner := runnerpool.NewController(\n\t\tq,\n\t\tunits,\n\t\trunnerpool.WithRunner(runner),\n\t\trunnerpool.WithMaxConcurrency(2),\n\t)\n\terr = dagRunner.Run(t.Context(), logger.CreateLogger())\n\trequire.Error(t, err)\n\n\tfor _, want := range []string{\"unit A failed\", \"Unit 'B' did not run\", \"Unit 'C' did not run\"} {\n\t\tassert.Contains(t, err.Error(), want, \"Expected error message '%s' in errors\", want)\n\t}\n}\n\n// Helper to build a more complex dependency graph:\n//\n//\t   A\n//\t  / \\\n//\t B   C\n//\t/ \\\n//\n// D   E\nfunc buildComplexUnits() []*component.Unit {\n\treturn buildComponentUnits(\n\t\t[]string{\"A\", \"B\", \"C\", \"D\", \"E\"},\n\t\tmap[string][]string{\n\t\t\t\"B\": {\"A\"},\n\t\t\t\"C\": {\"A\"},\n\t\t\t\"D\": {\"B\"},\n\t\t\t\"E\": {\"B\"},\n\t\t},\n\t)\n}\n\nfunc TestRunnerPool_ComplexDependency_BFails(t *testing.T) {\n\tt.Parallel()\n\n\tunits := buildComplexUnits()\n\n\trunner := func(ctx context.Context, u *component.Unit) error {\n\t\tif u.Path() == \"B\" {\n\t\t\treturn errors.New(\"unit B failed\")\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tcomponents := make(component.Components, len(units))\n\tfor i, u := range units {\n\t\tcomponents[i] = u\n\t}\n\n\tq, err := queue.NewQueue(components)\n\trequire.NoError(t, err)\n\n\tdagRunner := runnerpool.NewController(\n\t\tq,\n\t\tunits,\n\t\trunnerpool.WithRunner(runner),\n\t\trunnerpool.WithMaxConcurrency(8),\n\t)\n\terr = dagRunner.Run(t.Context(), logger.CreateLogger())\n\trequire.Error(t, err)\n\n\tfor _, want := range []string{\"unit B failed\", \"Unit 'D' did not run\", \"Unit 'E' did not run\"} {\n\t\tassert.Contains(t, err.Error(), want, \"Expected error message '%s' in errors\", want)\n\t}\n}\n\nfunc TestRunnerPool_ComplexDependency_AFails_FailFast(t *testing.T) {\n\tt.Parallel()\n\n\tunits := buildComplexUnits()\n\n\trunner := func(ctx context.Context, u *component.Unit) error {\n\t\tif u.Path() == \"A\" {\n\t\t\treturn errors.New(\"unit A failed\")\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tcomponents := make(component.Components, len(units))\n\tfor i, u := range units {\n\t\tcomponents[i] = u\n\t}\n\n\tq, err := queue.NewQueue(components)\n\trequire.NoError(t, err)\n\n\tq.FailFast = true\n\tdagRunner := runnerpool.NewController(\n\t\tq,\n\t\tunits,\n\t\trunnerpool.WithRunner(runner),\n\t\trunnerpool.WithMaxConcurrency(8),\n\t)\n\terr = dagRunner.Run(t.Context(), logger.CreateLogger())\n\trequire.Error(t, err)\n\n\tfor _, want := range []string{\n\t\t\"unit A failed\",\n\t\t\"Unit 'B' did not run\",\n\t\t\"Unit 'C' did not run\",\n\t\t\"Unit 'D' did not run\",\n\t\t\"Unit 'E' did not run\",\n\t} {\n\t\tassert.Contains(t, err.Error(), want, \"Expected error message '%s' in errors\", want)\n\t}\n}\n\nfunc TestRunnerPool_ComplexDependency_BFails_FailFast(t *testing.T) {\n\tt.Parallel()\n\n\tunits := buildComplexUnits()\n\n\trunner := func(ctx context.Context, u *component.Unit) error {\n\t\tif u.Path() == \"B\" {\n\t\t\treturn errors.New(\"unit B failed\")\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tcomponents := make(component.Components, len(units))\n\tfor i, u := range units {\n\t\tcomponents[i] = u\n\t}\n\n\tq, err := queue.NewQueue(components)\n\trequire.NoError(t, err)\n\n\tq.FailFast = true\n\tdagRunner := runnerpool.NewController(\n\t\tq,\n\t\tunits,\n\t\trunnerpool.WithRunner(runner),\n\t\trunnerpool.WithMaxConcurrency(8),\n\t)\n\terr = dagRunner.Run(t.Context(), logger.CreateLogger())\n\trequire.Error(t, err)\n\n\tfor _, want := range []string{\"unit B failed\", \"Unit 'D' did not run\", \"Unit 'E' did not run\"} {\n\t\tassert.Contains(t, err.Error(), want, \"Expected error message '%s' in errors\", want)\n\t}\n}\n"
  },
  {
    "path": "internal/runner/runnerpool/errors.go",
    "content": "package runnerpool\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/queue\"\n)\n\n// UnitEarlyExitError is an error type for units that didn't run due to dependency failure.\ntype UnitEarlyExitError struct {\n\tUnitPath         string\n\tFailedDependency string // The dependency that caused the early exit (optional)\n}\n\nfunc (e UnitEarlyExitError) Error() string {\n\tif e.FailedDependency != \"\" {\n\t\treturn fmt.Sprintf(\"Unit '%s' did not run due to a failure in '%s'\",\n\t\t\te.UnitPath, e.FailedDependency)\n\t}\n\n\treturn fmt.Sprintf(\"Unit '%s' did not run due to an earlier failure\", e.UnitPath)\n}\n\n// NewUnitEarlyExitError creates a new UnitEarlyExitError.\nfunc NewUnitEarlyExitError(unitPath, failedDep string) error {\n\treturn errors.New(UnitEarlyExitError{\n\t\tUnitPath:         unitPath,\n\t\tFailedDependency: failedDep,\n\t})\n}\n\n// UnitFailedError is an error type for units that failed during execution.\ntype UnitFailedError struct {\n\tUnitPath string\n}\n\nfunc (e UnitFailedError) Error() string {\n\treturn fmt.Sprintf(\"Unit '%s' encountered an error during its run\", e.UnitPath)\n}\n\n// NewUnitFailedError creates a new UnitFailedError.\nfunc NewUnitFailedError(unitPath string) error {\n\treturn errors.New(UnitFailedError{UnitPath: unitPath})\n}\n\n// findFailedDependency finds the first failed dependency for a given entry.\nfunc findFailedDependency(entry *queue.Entry, q *queue.Queue) string {\n\tfor _, dep := range entry.Component.Dependencies() {\n\t\tfor _, e := range q.Entries {\n\t\t\tif e.Component.Path() == dep.Path() {\n\t\t\t\tif e.Status == queue.StatusFailed {\n\t\t\t\t\treturn dep.Path()\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn \"\"\n}\n"
  },
  {
    "path": "internal/runner/runnerpool/graph_fallback_test.go",
    "content": "package runnerpool_test\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runnerpool\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\tthlogger \"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n)\n\n// Test that the runner-level fallback (WithGraphTarget) limits the stack to target + dependents,\n// and that this matches the discovery-based graph filter behavior when the filter experiment is enabled.\nfunc TestGraphFallbackMatchesFilterExperiment(t *testing.T) {\n\tt.Parallel()\n\n\tctx := context.Background()\n\tl := thlogger.CreateLogger()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\t// Make tmpDir a git repository so graph root detection works consistently\n\thelpers.CreateGitRepo(t, tmpDir)\n\n\t// Create a simple dependency chain: vpc -> db -> app\n\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\tdbDir := filepath.Join(tmpDir, \"db\")\n\tappDir := filepath.Join(tmpDir, \"app\")\n\n\tfor _, dir := range []string{vpcDir, dbDir, appDir} {\n\t\trequire.NoError(t, os.MkdirAll(dir, 0o755))\n\t}\n\n\t// Minimal terragrunt.hcl files to express dependencies\n\trequire.NoError(t, os.WriteFile(filepath.Join(vpcDir, \"terragrunt.hcl\"), []byte(``), 0o644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(dbDir, \"terragrunt.hcl\"), []byte(`\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n`), 0o644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(appDir, \"terragrunt.hcl\"), []byte(`\ndependency \"db\" {\n  config_path = \"../db\"\n}\n`), 0o644))\n\n\t// Ensure each unit directory has at least one Terraform file to avoid being skipped during unit resolution.\n\tfor _, dir := range []string{vpcDir, dbDir, appDir} {\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(dir, \"main.tf\"), []byte(\"\"), 0o644))\n\t}\n\n\t// Path set we expect when targeting vpc: {vpc, db, app}\n\texpected := []string{vpcDir, dbDir, appDir}\n\n\t// Path 1: experiment ON, use discovery filter\n\toptsOn := options.NewTerragruntOptions()\n\toptsOn.WorkingDir = vpcDir\n\toptsOn.RootWorkingDir = tmpDir\n\t// Enable the filter-flag experiment\n\trequire.NoError(t, optsOn.Experiments.EnableExperiment(\"filter-flag\"))\n\t// Inject graph filter for dependents of target\n\tparsedFilters, parseErr := filter.ParseFilterQueries(l, []string{`...{` + vpcDir + `}`})\n\trequire.NoError(t, parseErr)\n\n\toptsOn.Filters = parsedFilters\n\t// Build runner\n\trunnerOn, err := runnerpool.Build(ctx, l, optsOn)\n\trequire.NoError(t, err)\n\t// Collect unit paths\n\tonPaths := make([]string, 0, len(runnerOn.GetStack().Units))\n\tfor _, u := range runnerOn.GetStack().Units {\n\t\tonPaths = append(onPaths, u.Path())\n\t}\n\n\t// Path 2: experiment OFF, use fallback option\n\toptsOff := options.NewTerragruntOptions()\n\toptsOff.WorkingDir = vpcDir\n\toptsOff.RootWorkingDir = tmpDir\n\t// No filter queries; rely on fallback graph target option\n\trunnerOff, err := runnerpool.Build(ctx, l, optsOff, discovery.WithGraphTarget(vpcDir))\n\trequire.NoError(t, err)\n\n\toffPaths := make([]string, 0, len(runnerOff.GetStack().Units))\n\tfor _, u := range runnerOff.GetStack().Units {\n\t\toffPaths = append(offPaths, u.Path())\n\t}\n\n\t// Both paths should include exactly target + dependents (order not guaranteed)\n\tassert.ElementsMatch(t, expected, onPaths)\n\tassert.ElementsMatch(t, expected, offPaths)\n\tassert.ElementsMatch(t, onPaths, offPaths)\n}\n"
  },
  {
    "path": "internal/runner/runnerpool/helpers_test.go",
    "content": "package runnerpool_test\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runnerpool\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\tthlogger \"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n)\n\nfunc TestCloneUnitOptions_WithStackOpts(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\tconfigPath := filepath.Join(tmpDir, \"terragrunt.hcl\")\n\n\tstackOpts, err := options.NewTerragruntOptionsForTest(filepath.Join(tmpDir, \"stack\", \"terragrunt.hcl\"))\n\trequire.NoError(t, err)\n\n\tunit := component.NewUnit(tmpDir)\n\tl := thlogger.CreateLogger()\n\n\topts, logger, err := runnerpool.CloneUnitOptions(stackOpts, unit, configPath, \"\", l)\n\n\trequire.NoError(t, err)\n\trequire.NotNil(t, opts)\n\tassert.NotNil(t, logger)\n\tassert.Equal(t, configPath, opts.OriginalTerragruntConfigPath)\n\tassert.NotEmpty(t, opts.DownloadDir)\n}\n"
  },
  {
    "path": "internal/runner/runnerpool/runner.go",
    "content": "// Package runnerpool provides a runner implementation based on a pool pattern for executing multiple units concurrently.\npackage runnerpool\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\ttgerrors \"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/queue\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/common\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds\"\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// Runner implements the Stack interface for runner pool execution.\ntype Runner struct {\n\tStack *component.Stack\n\tqueue *queue.Queue\n}\n\n// CloneUnitOptions clones TerragruntOptions for a specific unit.\n// It handles CloneWithConfigPath, per-unit DownloadDir fallback, and OriginalTerragruntConfigPath.\n// Returns the cloned options and logger, or the original logger if stackOpts is nil.\nfunc CloneUnitOptions(\n\tstackOpts *options.TerragruntOptions,\n\tunit *component.Unit,\n\tcanonicalConfigPath string,\n\tstackDefaultDownloadDir string,\n\tl log.Logger,\n) (*options.TerragruntOptions, log.Logger, error) {\n\tclonedLogger, clonedOpts, err := stackOpts.CloneWithConfigPath(l, canonicalConfigPath)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// Override logger prefix with display path (relative to discovery context) for cleaner logs\n\t// unless --log-show-abs-paths is set\n\tif !stackOpts.Writers.LogShowAbsPaths {\n\t\tclonedLogger = clonedLogger.WithField(placeholders.WorkDirKeyName, unit.DisplayPath())\n\t}\n\n\t// Use a per-unit default download directory when the stack is using its own default\n\t// (i.e., no custom download dir was provided). This mirrors unit resolver behaviour\n\t// so each unit caches to its own .terragrunt-cache next to the config.\n\tif clonedOpts.DownloadDir == \"\" || (stackDefaultDownloadDir != \"\" && clonedOpts.DownloadDir == stackDefaultDownloadDir) {\n\t\t_, unitDefaultDownloadDir := util.DefaultWorkingAndDownloadDirs(canonicalConfigPath)\n\n\t\tclonedOpts.DownloadDir = unitDefaultDownloadDir\n\t}\n\n\tclonedOpts.OriginalTerragruntConfigPath = canonicalConfigPath\n\n\treturn clonedOpts, clonedLogger, nil\n}\n\n// BuildUnitOpts creates per-unit opts and logger for a single unit on demand.\n// It computes the canonical config path, clones options, applies source overrides,\n// and transfers discovery context command/args.\nfunc BuildUnitOpts(l log.Logger, stackOpts *options.TerragruntOptions, unit *component.Unit) (*options.TerragruntOptions, log.Logger, error) {\n\tvar stackDefaultDownloadDir string\n\tif stackOpts != nil {\n\t\t_, stackDefaultDownloadDir = util.DefaultWorkingAndDownloadDirs(stackOpts.TerragruntConfigPath)\n\t}\n\n\t// Compute config path from already-canonical unit.Path() + unit.ConfigFile()\n\tconfigPath := unit.Path()\n\tif !strings.HasSuffix(configPath, \".hcl\") && !strings.HasSuffix(configPath, \".json\") {\n\t\tfileName := config.DefaultTerragruntConfigPath\n\t\tif unit.ConfigFile() != \"\" {\n\t\t\tfileName = unit.ConfigFile()\n\t\t}\n\n\t\tconfigPath = filepath.Join(unit.Path(), fileName)\n\t}\n\n\t// Clone options for this unit\n\tunitOpts, unitLogger, err := CloneUnitOptions(stackOpts, unit, configPath, stackDefaultDownloadDir, l)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// If --source is provided, compute the per-unit source\n\tif stackOpts != nil && stackOpts.Source != \"\" {\n\t\tunitConfig := unit.Config()\n\t\tif unitConfig != nil {\n\t\t\tunitSource, sourceErr := config.GetTerragruntSourceForModule(\n\t\t\t\tstackOpts.Source,\n\t\t\t\tconfigPath,\n\t\t\t\tunitConfig,\n\t\t\t)\n\t\t\tif sourceErr != nil {\n\t\t\t\treturn nil, nil, tgerrors.Errorf(\"failed to compute source for unit %s: %w\", unit.DisplayPath(), sourceErr)\n\t\t\t}\n\n\t\t\tif unitSource != \"\" {\n\t\t\t\tunitOpts.Source = unitSource\n\t\t\t}\n\t\t}\n\t}\n\n\t// Transfer discovery context command and args to unit options if available\n\tif discoveryCtx := unit.DiscoveryContext(); discoveryCtx != nil {\n\t\tif discoveryCtx.Cmd != \"\" {\n\t\t\tunitOpts.TerraformCommand = discoveryCtx.Cmd\n\t\t}\n\n\t\tif len(discoveryCtx.Args) > 0 {\n\t\t\tterraformCliArgs := make([]string, 0, 1+len(discoveryCtx.Args))\n\t\t\tif discoveryCtx.Cmd != \"\" {\n\t\t\t\tterraformCliArgs = append(terraformCliArgs, discoveryCtx.Cmd)\n\t\t\t}\n\n\t\t\tterraformCliArgs = append(terraformCliArgs, discoveryCtx.Args...)\n\t\t\tunitOpts.TerraformCliArgs = iacargs.New(terraformCliArgs...)\n\t\t}\n\t}\n\n\treturn unitOpts, unitLogger, nil\n}\n\n// syncUnitCliArgs applies CLI argument synchronization for a single unit.\n// It merges/clones flags from stackOpts and computes and appends the plan file if needed.\nfunc syncUnitCliArgs(l log.Logger, stackOpts *options.TerragruntOptions, unitOpts *options.TerragruntOptions, unit *component.Unit) {\n\tdiscoveryCtx := unit.DiscoveryContext()\n\tif discoveryCtx != nil && len(discoveryCtx.Args) > 0 {\n\t\t// Merge stack-level flags that aren't already present\n\t\tunitOpts.TerraformCliArgs.MergeFlags(stackOpts.TerraformCliArgs)\n\t} else {\n\t\tunitOpts.TerraformCliArgs = stackOpts.TerraformCliArgs.Clone()\n\t}\n\n\tplanFile := unit.PlanFile(stackOpts.RootWorkingDir, stackOpts.OutputFolder, unitOpts.JSONOutputFolder, unitOpts.TerraformCommand)\n\n\tif planFile != \"\" {\n\t\tl.Debugf(\"Using output file %s for unit %s\", planFile, unitOpts.TerragruntConfigPath)\n\n\t\t// Check if plan file already exists in args\n\t\tif unitOpts.TerraformCliArgs.HasPlanFile() {\n\t\t\treturn\n\t\t}\n\n\t\tif unitOpts.TerraformCommand == tf.CommandNamePlan {\n\t\t\t// for plan command add -out=<file> to the terraform cli args\n\t\t\tunitOpts.TerraformCliArgs.AppendFlag(\"-out=\" + planFile)\n\n\t\t\treturn\n\t\t}\n\n\t\tunitOpts.TerraformCliArgs.AppendArgument(planFile)\n\t}\n}\n\n// checkLocalStateWithGitRefs checks if any unit has a Git ref in its discovery context\n// but no remote state configuration, and logs a warning if so.\nfunc checkLocalStateWithGitRefs(l log.Logger, units []*component.Unit) {\n\tfor _, unit := range units {\n\t\tdiscoveryCtx := unit.DiscoveryContext()\n\t\tif discoveryCtx == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif discoveryCtx.Ref == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tunitConfig := unit.Config()\n\t\tif unitConfig == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif unitConfig.RemoteState == nil || (unitConfig.RemoteState.Config != nil && unitConfig.RemoteState.BackendName == \"local\") {\n\t\t\tl.Warnf(\n\t\t\t\t\"One or more units discovered using Git-based filter expressions (e.g. [HEAD~1...HEAD]) do not have a remote_state configuration. This may result in unexpected outcomes, such as outputs for dependencies returning empty. It is strongly recommended to use remote state when working with Git-based filter expressions.\",\n\t\t\t)\n\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// NewRunnerPoolStack creates a new stack from discovered units.\nfunc NewRunnerPoolStack(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tdiscovered component.Components,\n\trunnerOpts ...common.Option,\n) (common.StackRunner, error) {\n\t// Filter out Stack components - we only want Unit components\n\t// Stack components (terragrunt.stack.hcl files) are for stack generation, not execution\n\tnonStackComponents := make(component.Components, 0, len(discovered))\n\tfor _, c := range discovered {\n\t\tif _, ok := c.(*component.Unit); ok {\n\t\t\tnonStackComponents = append(nonStackComponents, c)\n\t\t}\n\t}\n\n\tif len(nonStackComponents) == 0 {\n\t\tl.Warnf(\"No units discovered. Creating an empty runner.\")\n\n\t\tstack := component.NewStack(opts.WorkingDir)\n\n\t\trnr := &Runner{\n\t\t\tStack: stack,\n\t\t}\n\n\t\t// Create an empty queue\n\t\tq, queueErr := queue.NewQueue(component.Components{})\n\t\tif queueErr != nil {\n\t\t\treturn nil, queueErr\n\t\t}\n\n\t\trnr.queue = q\n\n\t\treturn rnr.WithOptions(runnerOpts...), nil\n\t}\n\n\t// Initialize stack; queue will be constructed after resolving units so we can filter excludes first.\n\tstack := component.NewStack(opts.WorkingDir)\n\n\trnr := &Runner{\n\t\tStack: stack,\n\t}\n\n\t// Apply options (including report) BEFORE resolving units so that\n\t// the report is available during unit resolution for tracking exclusions\n\trnr = rnr.WithOptions(runnerOpts...)\n\n\t// Resolve units from discovery\n\tunits := make([]*component.Unit, 0, len(nonStackComponents))\n\tfor _, c := range nonStackComponents {\n\t\tunit, ok := c.(*component.Unit)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tif unit.DiscoveryContext() != nil && unit.Config() == nil {\n\t\t\tl.Debugf(\"Unit %s has no config from discovery\", unit.DisplayPath())\n\t\t}\n\n\t\tunits = append(units, unit)\n\t}\n\n\t// Check for units with Git refs but no remote state configuration\n\tcheckLocalStateWithGitRefs(l, units)\n\trnr.Stack.Units = units\n\n\tif opts.TerraformCliArgs.IsDestroyCommand(opts.TerraformCommand) {\n\t\tapplyPreventDestroyExclusions(l, units)\n\t}\n\n\t// Apply filter-allow-destroy exclusions for plan and apply commands\n\tif opts.TerraformCommand == tf.CommandNamePlan || opts.TerraformCommand == tf.CommandNameApply {\n\t\tapplyFilterAllowDestroyExclusions(l, opts, units)\n\t}\n\n\t// Build queue from resolved units (which have canonical absolute paths).\n\t// Filter out excluded units so they are not shown in lists or scheduled.\n\tfiltered := filterUnitsToComponents(units)\n\n\tq, queueErr := queue.NewQueue(filtered)\n\tif queueErr != nil {\n\t\treturn nil, queueErr\n\t}\n\n\trnr.queue = q\n\n\treturn rnr, nil\n}\n\n// filterUnitsToComponents converts resolved units to Components.\n// Excluded units that are assumed already applied are kept in the queue\n// so their dependents can run (they will be immediately marked as succeeded).\n// Only truly excluded units are filtered out.\nfunc filterUnitsToComponents(units []*component.Unit) component.Components {\n\tresult := make(component.Components, 0, len(units))\n\tfor _, u := range units {\n\t\tif u.Excluded() {\n\t\t\t// Truly excluded - skip entirely\n\t\t\tcontinue\n\t\t}\n\n\t\tresult = append(result, u)\n\t}\n\n\treturn result\n}\n\n// Run executes the stack according to TerragruntOptions and returns the first\n// error (or a joined error) once execution is finished.\nfunc (rnr *Runner) Run(ctx context.Context, l log.Logger, stackOpts *options.TerragruntOptions, r *report.Report) error {\n\tterraformCmd := stackOpts.TerraformCommand\n\n\tif stackOpts.OutputFolder != \"\" {\n\t\tfor _, u := range rnr.Stack.Units {\n\t\t\tplanFile := u.OutputFile(stackOpts.RootWorkingDir, stackOpts.OutputFolder)\n\t\t\tif err := os.MkdirAll(filepath.Dir(planFile), os.ModePerm); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\t// Mutate stackOpts CLI args at the top level - these get cloned into per-unit opts later\n\tif slices.Contains(config.TerraformCommandsNeedInput, terraformCmd) {\n\t\tstackOpts.TerraformCliArgs.InsertFlag(0, \"-input=false\")\n\t}\n\n\tneedsCliSync := false\n\tisPlan := false\n\n\tswitch terraformCmd {\n\tcase tf.CommandNameApply, tf.CommandNameDestroy:\n\t\tif stackOpts.RunAllAutoApprove {\n\t\t\tstackOpts.TerraformCliArgs.InsertFlag(0, \"-auto-approve\")\n\t\t}\n\n\t\tneedsCliSync = true\n\tcase tf.CommandNameShow:\n\t\tneedsCliSync = true\n\tcase tf.CommandNamePlan:\n\t\tisPlan = true\n\t\tneedsCliSync = true\n\t}\n\n\tif slices.Contains(config.TerraformCommandsNeedInput, terraformCmd) {\n\t\tneedsCliSync = true\n\t}\n\n\t// Pre-allocate plan error buffers keyed by unit path\n\tvar planErrorBuffers map[string]*bytes.Buffer\n\tif isPlan {\n\t\tplanErrorBuffers = make(map[string]*bytes.Buffer, len(rnr.Stack.Units))\n\t\tfor _, u := range rnr.Stack.Units {\n\t\t\tplanErrorBuffers[u.Path()] = &bytes.Buffer{}\n\t\t}\n\n\t\tdefer rnr.summarizePlanAllErrors(l, planErrorBuffers)\n\t}\n\n\t// Emit report entries for excluded units that haven't been reported yet.\n\t// Units excluded by CLI flags or exclude blocks are already reported during unit resolution,\n\t// but we still need to report units excluded by other mechanisms (e.g., external dependencies).\n\tif r != nil {\n\t\tfor _, u := range rnr.Stack.Units {\n\t\t\tif u.Excluded() {\n\t\t\t\tunitPath := u.Path()\n\n\t\t\t\t// Pass the discovery context fields for worktree scenarios\n\t\t\t\tvar ensureOpts []report.EndOption\n\n\t\t\t\tif discoveryCtx := u.DiscoveryContext(); discoveryCtx != nil {\n\t\t\t\t\tensureOpts = append(\n\t\t\t\t\t\tensureOpts,\n\t\t\t\t\t\treport.WithDiscoveryWorkingDir(discoveryCtx.WorkingDir),\n\t\t\t\t\t\treport.WithRef(discoveryCtx.Ref),\n\t\t\t\t\t\treport.WithCmd(discoveryCtx.Cmd),\n\t\t\t\t\t\treport.WithArgs(discoveryCtx.Args),\n\t\t\t\t\t)\n\t\t\t\t}\n\n\t\t\t\trun, err := r.EnsureRun(l, unitPath, ensureOpts...)\n\t\t\t\tif err != nil {\n\t\t\t\t\tl.Errorf(\"Error ensuring run for unit %s: %v\", unitPath, err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Only report exclusion if it hasn't been reported yet\n\t\t\t\t// Units excluded by --queue-exclude-dir or exclude blocks are already reported\n\t\t\t\t// during unit resolution with the correct reason\n\t\t\t\tif run.Result == \"\" {\n\t\t\t\t\t// Determine the reason for exclusion\n\t\t\t\t\t// External dependencies that are assumed already applied are excluded with --queue-exclude-external\n\t\t\t\t\treason := report.ReasonExcludeBlock\n\n\t\t\t\t\tif err := r.EndRun(\n\t\t\t\t\t\tl,\n\t\t\t\t\t\trun.Path,\n\t\t\t\t\t\treport.WithResult(report.ResultExcluded),\n\t\t\t\t\t\treport.WithReason(reason),\n\t\t\t\t\t); err != nil {\n\t\t\t\t\t\tl.Errorf(\"Error ending run for unit %s: %v\", unitPath, err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\ttask := func(ctx context.Context, u *component.Unit) error {\n\t\t// Build per-unit opts and logger on demand\n\t\tunitOpts, unitLogger, err := BuildUnitOpts(l, stackOpts, u)\n\t\tif err != nil {\n\t\t\treturn tgerrors.Errorf(\"failed to build opts for unit %s: %w\", u.Path(), err)\n\t\t}\n\n\t\t// Sync CLI args from stackOpts into unit opts\n\t\tif needsCliSync {\n\t\t\tsyncUnitCliArgs(l, stackOpts, unitOpts, u)\n\t\t}\n\n\t\t// Wrap ErrWriter with plan error buffer for plan commands\n\t\tif isPlan {\n\t\t\tif buf := planErrorBuffers[u.Path()]; buf != nil {\n\t\t\t\tunitOpts.Writers.ErrWriter = io.MultiWriter(buf, unitOpts.Writers.ErrWriter)\n\t\t\t}\n\t\t}\n\n\t\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, \"runner_pool_task\", map[string]any{\n\t\t\t\"terraform_command\":      unitOpts.TerraformCommand,\n\t\t\t\"terraform_cli_args\":     unitOpts.TerraformCliArgs,\n\t\t\t\"working_dir\":            unitOpts.WorkingDir,\n\t\t\t\"terragrunt_config_path\": unitOpts.TerragruntConfigPath,\n\t\t}, func(childCtx context.Context) error {\n\t\t\t// Wrap the writer to buffer unit-scoped output\n\t\t\tunitWriter := NewUnitWriter(unitOpts.Writers.Writer)\n\t\t\tunitOpts.Writers.Writer = unitWriter\n\t\t\tunitRunner := common.NewUnitRunner(u)\n\n\t\t\t// Get credentials BEFORE config parsing — sops_decrypt_file() and\n\t\t\t// get_aws_account_id() in locals need auth-provider credentials\n\t\t\t// available in opts.Env during HCL evaluation.\n\t\t\t// See https://github.com/gruntwork-io/terragrunt/issues/5515\n\t\t\tcredsGetter, err := creds.ObtainCredsForParsing(childCtx, unitLogger, unitOpts.AuthProviderCmd, unitOpts.Env, configbridge.ShellRunOptsFromOpts(unitOpts))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tparseCtx, pctx := configbridge.NewParsingContext(childCtx, unitLogger, unitOpts)\n\n\t\t\tcfg, err := config.ReadTerragruntConfig(\n\t\t\t\tparseCtx,\n\t\t\t\tunitLogger,\n\t\t\t\tpctx,\n\t\t\t\tpctx.ParserOptions,\n\t\t\t)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\trunCfg := cfg.ToRunConfig(unitLogger)\n\n\t\t\terr = unitRunner.Run(\n\t\t\t\tchildCtx,\n\t\t\t\tunitLogger,\n\t\t\t\tunitOpts,\n\t\t\t\tr,\n\t\t\t\trunCfg,\n\t\t\t\tcredsGetter,\n\t\t\t)\n\n\t\t\t// Flush any remaining buffered output\n\t\t\tif flushErr := unitWriter.Flush(); flushErr != nil && err == nil {\n\t\t\t\terr = flushErr\n\t\t\t}\n\n\t\t\treturn err\n\t\t})\n\t}\n\n\trnr.queue.FailFast = stackOpts.FailFast\n\trnr.queue.IgnoreDependencyOrder = stackOpts.IgnoreDependencyOrder\n\t// Allow continuing the queue when dependencies fail if requested via CLI\n\trnr.queue.IgnoreDependencyErrors = stackOpts.IgnoreDependencyErrors\n\tcontroller := NewController(\n\t\trnr.queue,\n\t\trnr.Stack.Units,\n\t\tWithRunner(task),\n\t\tWithMaxConcurrency(stackOpts.Parallelism),\n\t)\n\n\terr := controller.Run(ctx, l)\n\n\t// Emit report entries for early exit and failed units after controller completes\n\tif r != nil {\n\t\t// Build a quick lookup of queue entry status by path to avoid nested scans\n\t\tstatusByPath := make(map[string]queue.Status, len(rnr.queue.Entries))\n\t\tfor _, qe := range rnr.queue.Entries {\n\t\t\tstatusByPath[qe.Component.Path()] = qe.Status\n\t\t}\n\n\t\tfor _, entry := range rnr.queue.Entries {\n\t\t\t// Handle both early exit and failed units to ensure they're in the report\n\t\t\tif entry.Status == queue.StatusEarlyExit || entry.Status == queue.StatusFailed {\n\t\t\t\tunit := rnr.Stack.FindUnitByPath(entry.Component.Path())\n\t\t\t\tif unit == nil {\n\t\t\t\t\tl.Warnf(\"Could not find unit for entry: %s\", entry.Component.Path())\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tunitPath := unit.Path()\n\n\t\t\t\t// Pass the discovery context fields for worktree scenarios\n\t\t\t\tvar ensureOpts []report.EndOption\n\n\t\t\t\tif discoveryCtx := unit.DiscoveryContext(); discoveryCtx != nil {\n\t\t\t\t\tensureOpts = append(\n\t\t\t\t\t\tensureOpts,\n\t\t\t\t\t\treport.WithDiscoveryWorkingDir(discoveryCtx.WorkingDir),\n\t\t\t\t\t\treport.WithRef(discoveryCtx.Ref),\n\t\t\t\t\t\treport.WithCmd(discoveryCtx.Cmd),\n\t\t\t\t\t\treport.WithArgs(discoveryCtx.Args),\n\t\t\t\t\t)\n\t\t\t\t}\n\n\t\t\t\trun, reportErr := r.EnsureRun(l, unitPath, ensureOpts...)\n\t\t\t\tif reportErr != nil {\n\t\t\t\t\tl.Errorf(\"Error ensuring run for unit %s: %v\", unitPath, reportErr)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t// Find the immediate failed or early-exited ancestor to set as cause\n\t\t\t\t// If a dependency failed, use it; otherwise if a dependency exited early, use it\n\t\t\t\tvar failedAncestor string\n\n\t\t\t\tfor _, dep := range entry.Component.Dependencies() {\n\t\t\t\t\tstatus := statusByPath[dep.Path()]\n\t\t\t\t\tif status == queue.StatusFailed {\n\t\t\t\t\t\tfailedAncestor = filepath.Base(dep.Path())\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\n\t\t\t\t\tif status == queue.StatusEarlyExit && failedAncestor == \"\" {\n\t\t\t\t\t\t// Use early exit dependency as fallback\n\t\t\t\t\t\tfailedAncestor = filepath.Base(dep.Path())\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tswitch entry.Status { //nolint:exhaustive\n\t\t\t\tcase queue.StatusEarlyExit:\n\t\t\t\t\tendOpts := []report.EndOption{\n\t\t\t\t\t\treport.WithResult(report.ResultEarlyExit),\n\t\t\t\t\t\treport.WithReason(report.ReasonAncestorError),\n\t\t\t\t\t}\n\t\t\t\t\tif failedAncestor != \"\" {\n\t\t\t\t\t\tendOpts = append(endOpts, report.WithCauseAncestorExit(failedAncestor))\n\t\t\t\t\t}\n\n\t\t\t\t\tif endErr := r.EndRun(l, run.Path, endOpts...); endErr != nil {\n\t\t\t\t\t\tl.Errorf(\"Error ending run for early exit unit %s: %v\", unitPath, endErr)\n\t\t\t\t\t}\n\t\t\t\tcase queue.StatusFailed:\n\t\t\t\t\t// For failed units, check if they failed due to dependency errors\n\t\t\t\t\t// If so, mark them as early exit; otherwise mark as failed\n\t\t\t\t\tendOpts := []report.EndOption{\n\t\t\t\t\t\treport.WithResult(report.ResultFailed),\n\t\t\t\t\t\treport.WithReason(report.ReasonRunError),\n\t\t\t\t\t}\n\t\t\t\t\tif failedAncestor != \"\" {\n\t\t\t\t\t\t// If a dependency failed, treat this as early exit due to ancestor error\n\t\t\t\t\t\tendOpts = []report.EndOption{\n\t\t\t\t\t\t\treport.WithResult(report.ResultEarlyExit),\n\t\t\t\t\t\t\treport.WithReason(report.ReasonAncestorError),\n\t\t\t\t\t\t\treport.WithCauseAncestorExit(failedAncestor),\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif endErr := r.EndRun(l, run.Path, endOpts...); endErr != nil {\n\t\t\t\t\t\tl.Errorf(\"Error ending run for failed unit %s: %v\", unitPath, endErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn err\n}\n\n// LogUnitDeployOrder logs the order of units to be processed for a given Terraform command.\nfunc (rnr *Runner) LogUnitDeployOrder(l log.Logger, terraformCmd string, isDestroy bool, showAbsPaths bool) error {\n\toutStr := fmt.Sprintf(\n\t\t\"Unit queue will be processed for %s in this order:\\n\",\n\t\tterraformCmd,\n\t)\n\n\t// For destroy commands, reflect the actual processing order (reverse of apply order).\n\t// NOTE: This is display-only. The queue scheduler dynamically handles destroy order via\n\t// IsUp() checks - dependents must complete before their dependencies are processed.\n\tentries := slices.Clone(rnr.queue.Entries)\n\tif isDestroy {\n\t\tslices.Reverse(entries)\n\t}\n\n\tvar outStrSb strings.Builder\n\n\tfor _, unit := range entries {\n\t\tunitPath := unit.Component.DisplayPath()\n\t\tif showAbsPaths {\n\t\t\tunitPath = unit.Component.Path()\n\t\t}\n\n\t\tfmt.Fprintf(&outStrSb, \"- Unit %s\\n\", unitPath)\n\t}\n\n\toutStr += outStrSb.String()\n\n\tl.Info(outStr)\n\n\treturn nil\n}\n\n// JSONUnitDeployOrder returns the order of units to be processed for a given Terraform command in JSON format.\nfunc (rnr *Runner) JSONUnitDeployOrder(isDestroy bool, showAbsPaths bool) (string, error) {\n\tentries := slices.Clone(rnr.queue.Entries)\n\tif isDestroy {\n\t\tslices.Reverse(entries)\n\t}\n\n\torderedUnits := make([]string, 0, len(entries))\n\tfor _, unit := range entries {\n\t\tunitPath := unit.Component.DisplayPath()\n\t\tif showAbsPaths {\n\t\t\tunitPath = unit.Component.Path()\n\t\t}\n\n\t\torderedUnits = append(orderedUnits, unitPath)\n\t}\n\n\tj, err := json.MarshalIndent(orderedUnits, \"\", \"  \")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(j), nil\n}\n\n// ListStackDependentUnits returns a map of units and their dependent units in the stack.\nfunc (rnr *Runner) ListStackDependentUnits() map[string][]string {\n\tdependentUnits := make(map[string][]string)\n\n\tfor _, unit := range rnr.queue.Entries {\n\t\tif len(unit.Component.Dependencies()) != 0 {\n\t\t\tfor _, dep := range unit.Component.Dependencies() {\n\t\t\t\tdependentUnits[dep.Path()] = util.RemoveDuplicates(append(dependentUnits[dep.Path()], unit.Component.Path()))\n\t\t\t}\n\t\t}\n\t}\n\n\tfor {\n\t\tnoUpdates := true\n\n\t\tfor unit, dependents := range dependentUnits {\n\t\t\tfor _, dependent := range dependents {\n\t\t\t\tinitialSize := len(dependentUnits[unit])\n\t\t\t\tlist := util.RemoveDuplicates(append(dependentUnits[unit], dependentUnits[dependent]...))\n\t\t\t\tlist = slices.DeleteFunc(list, func(path string) bool { return path == unit })\n\t\t\t\tdependentUnits[unit] = list\n\n\t\t\t\tif initialSize != len(dependentUnits[unit]) {\n\t\t\t\t\tnoUpdates = false\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif noUpdates {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn dependentUnits\n}\n\n// summarizePlanAllErrors summarizes all errors encountered during the plan phase across all units in the stack.\nfunc (rnr *Runner) summarizePlanAllErrors(l log.Logger, errorStreams map[string]*bytes.Buffer) {\n\tfor _, unit := range rnr.Stack.Units {\n\t\tbuf := errorStreams[unit.Path()]\n\t\tif buf == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\toutput := buf.String()\n\n\t\tif len(output) == 0 {\n\t\t\t// We get Finished buffer if runner execution completed without errors, so skip that to avoid logging too much\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.Contains(output, \"Error running plan:\") && strings.Contains(output, \": Resource 'data.terraform_remote_state.\") {\n\t\t\tvar dependenciesMsg string\n\n\t\t\tif len(unit.Dependencies()) > 0 {\n\t\t\t\tcfg := unit.Config()\n\t\t\t\tif cfg != nil && cfg.Dependencies != nil && len(cfg.Dependencies.Paths) > 0 {\n\t\t\t\t\tdependenciesMsg = fmt.Sprintf(\" contains dependencies to %v and\", cfg.Dependencies.Paths)\n\t\t\t\t} else {\n\t\t\t\t\tdependenciesMsg = \" contains dependencies and\"\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tl.Infof(\"%v%v refers to remote State \"+\n\t\t\t\t\"you may have to apply your changes in the dependencies prior running terragrunt run --all plan.\\n\",\n\t\t\t\tunit.Path(),\n\t\t\t\tdependenciesMsg,\n\t\t\t)\n\t\t}\n\t}\n}\n\n// FilterDiscoveredUnits removes configs for units flagged as excluded and prunes dependencies\n// that point to excluded units. This keeps the execution queue and any user-facing listings\n// free from units not intended to run.\n//\n// Inputs:\n//   - discovered: raw discovery results (paths and dependency edges)\n//   - units: resolved units (slice), where exclude rules have already been applied\n//\n// Behavior:\n//   - A config is included only if there's a corresponding unit and it is not excluded.\n//   - For each included config, its Dependencies list is filtered to only include included configs.\n//   - The function returns a new slice with shallow-copied entries so the original discovery\n//     results remain unchanged.\nfunc FilterDiscoveredUnits(discovered component.Components, units []*component.Unit) component.Components {\n\t// Build allowlist from non-excluded unit paths (already canonical from discovery)\n\tallowed := make(map[string]struct{}, len(units))\n\tfor _, u := range units {\n\t\tif !u.Excluded() {\n\t\t\tallowed[u.Path()] = struct{}{}\n\t\t}\n\t}\n\n\t// First pass: keep only allowed configs and prune their dependencies to allowed ones\n\t// NOTE: Unit paths should already be canonical after discovery\n\tfiltered := make(component.Components, 0, len(discovered))\n\tpresent := make(map[string]*component.Unit, len(discovered))\n\n\tfor _, c := range discovered {\n\t\tunit, ok := c.(*component.Unit)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Path should already be canonical from discovery\n\t\tunitPath := unit.Path()\n\n\t\tif _, ok := allowed[unitPath]; !ok {\n\t\t\t// Drop configs that map to excluded/missing units\n\t\t\tcontinue\n\t\t}\n\n\t\t// Create new unit with the path (already canonical)\n\t\tcopyCfg := component.NewUnit(unitPath)\n\t\tcopyCfg.SetDiscoveryContext(unit.DiscoveryContext())\n\t\tcopyCfg.SetReading(unit.Reading()...)\n\n\t\tif unit.External() {\n\t\t\tcopyCfg.SetExternal()\n\t\t}\n\n\t\tif len(unit.Dependencies()) > 0 {\n\t\t\tfor _, dep := range unit.Dependencies() {\n\t\t\t\t// Dependency paths should also be canonical\n\t\t\t\tdepPath := dep.Path()\n\t\t\t\tif _, ok := allowed[depPath]; ok {\n\t\t\t\t\t// Create dependency with the path\n\t\t\t\t\tdepCfg := component.NewUnit(depPath)\n\t\t\t\t\tcopyCfg.AddDependency(depCfg)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfiltered = append(filtered, copyCfg)\n\t\tpresent[copyCfg.Path()] = copyCfg\n\t}\n\n\t// Ensure every allowed unit exists in the filtered set, even if discovery didn't include it (or it was pruned)\n\tfor _, u := range units {\n\t\tif u.Excluded() {\n\t\t\tcontinue\n\t\t}\n\n\t\tif _, ok := present[u.Path()]; ok {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Create a minimal discovered config for the missing unit\n\t\tcopyCfg := component.NewUnit(u.Path())\n\n\t\tfiltered = append(filtered, copyCfg)\n\t\tpresent[u.Path()] = copyCfg\n\t}\n\n\t// Augment dependencies from resolved units to ensure DAG edges are complete\n\tfor _, u := range units {\n\t\tif u.Excluded() {\n\t\t\tcontinue\n\t\t}\n\n\t\tcfg := present[u.Path()]\n\t\tif cfg == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Build a set of existing dependency paths on cfg to avoid duplicates\n\t\texisting := make(map[string]struct{}, len(cfg.Dependencies()))\n\t\tfor _, dep := range cfg.Dependencies() {\n\t\t\texisting[dep.Path()] = struct{}{}\n\t\t}\n\n\t\t// Add any missing allowed dependencies from the resolved unit graph\n\t\tfor _, dep := range u.Dependencies() {\n\t\t\tdepUnit, okDep := dep.(*component.Unit)\n\t\t\tif !okDep || depUnit == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif _, allowedOK := allowed[depUnit.Path()]; !allowedOK {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif _, existsOK := existing[depUnit.Path()]; existsOK {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Ensure the dependency config exists in the filtered set\n\t\t\tdepCfg, presentOK := present[depUnit.Path()]\n\t\t\tif !presentOK {\n\t\t\t\tdepCfg = component.NewUnit(depUnit.Path())\n\t\t\t\tfiltered = append(filtered, depCfg)\n\t\t\t\tpresent[depUnit.Path()] = depCfg\n\t\t\t}\n\n\t\t\tcfg.AddDependency(depCfg)\n\t\t}\n\t}\n\n\treturn filtered\n}\n\n// WithOptions updates the stack with the provided options.\nfunc (rnr *Runner) WithOptions(opts ...common.Option) *Runner {\n\tfor _, opt := range opts {\n\t\topt.Apply(rnr)\n\t}\n\n\treturn rnr\n}\n\n// GetStack returns the stack associated with the runner.\nfunc (rnr *Runner) GetStack() *component.Stack {\n\treturn rnr.Stack\n}\n\n// applyPreventDestroyExclusions excludes units with prevent_destroy=true and their dependencies\n// from being destroyed. This prevents accidental destruction of protected infrastructure.\nfunc applyPreventDestroyExclusions(l log.Logger, units []*component.Unit) {\n\t// First pass: identify units with prevent_destroy=true\n\tprotectedUnits := make(map[string]bool)\n\n\tfor _, unit := range units {\n\t\tcfg := unit.Config()\n\t\tif cfg != nil && cfg.PreventDestroy != nil && *cfg.PreventDestroy {\n\t\t\tprotectedUnits[unit.Path()] = true\n\t\t\tunit.SetExcluded(true)\n\n\t\t\tl.Debugf(\"Unit %s is protected by prevent_destroy flag\", unit.Path())\n\t\t}\n\t}\n\n\tif len(protectedUnits) == 0 {\n\t\treturn\n\t}\n\n\t// Second pass: find all dependencies of protected units\n\t// We need to prevent destruction of any unit that a protected unit depends on\n\tdependencyPaths := make(map[string]bool)\n\n\tfor _, unit := range units {\n\t\tif protectedUnits[unit.Path()] {\n\t\t\tcollectDependencies(unit, dependencyPaths)\n\t\t}\n\t}\n\n\t// Third pass: mark dependencies as excluded\n\tfor _, unit := range units {\n\t\tif dependencyPaths[unit.Path()] && !protectedUnits[unit.Path()] {\n\t\t\tunit.SetExcluded(true)\n\n\t\t\tl.Debugf(\"Unit %s is excluded because it's a dependency of a protected unit\", unit.Path())\n\t\t}\n\t}\n}\n\n// maxDependencyTraversalDepth bounds the depth of dependency traversal to prevent excessive recursion.\nconst maxDependencyTraversalDepth = 256\n\n// applyFilterAllowDestroyExclusions excludes units with destroy runs from Git-based filters\n// when the --filter-allow-destroy flag is not set. This prevents accidental destruction\n// of infrastructure when using Git-based filters.\nfunc applyFilterAllowDestroyExclusions(l log.Logger, opts *options.TerragruntOptions, units []*component.Unit) {\n\tif opts.FilterAllowDestroy {\n\t\treturn\n\t}\n\n\tfor _, unit := range units {\n\t\tdiscoveryCtx := unit.DiscoveryContext()\n\t\tif discoveryCtx == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif discoveryCtx.Ref != \"\" && iacargs.New(discoveryCtx.Args...).IsDestroyCommand(discoveryCtx.Cmd) {\n\t\t\tunit.SetExcluded(true)\n\n\t\t\tl.Warnf(\"The `%s` unit was removed in the `%s` Git reference, but the `--filter-allow-destroy` flag was not used. The unit will be excluded during applies unless --filter-allow-destroy is used.\", unit.DisplayPath(), discoveryCtx.Ref)\n\t\t}\n\t}\n}\n\n// collectDependencies collects dependency paths for a unit with a bounded recursion depth.\nfunc collectDependencies(unit *component.Unit, paths map[string]bool) {\n\tcollectDependenciesBounded(unit, paths, 0)\n}\n\n// collectDependenciesBounded recursively collects all dependency paths for a unit up to maxDependencyTraversalDepth.\nfunc collectDependenciesBounded(unit *component.Unit, paths map[string]bool, depth int) {\n\tif depth >= maxDependencyTraversalDepth {\n\t\treturn\n\t}\n\n\tfor _, dep := range unit.Dependencies() {\n\t\tdepUnit, ok := dep.(*component.Unit)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !paths[depUnit.Path()] {\n\t\t\tpaths[depUnit.Path()] = true\n\t\t\tcollectDependenciesBounded(depUnit, paths, depth+1)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/runner/runnerpool/runner_test.go",
    "content": "package runnerpool_test\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runnerpool\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\tthlogger \"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n)\n\nfunc TestDiscoveryResolverMatchesLegacyPaths(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create a trivial tf file so the resolver doesn't skip the unit\n\trequire.NoError(t, os.WriteFile(filepath.Join(tmpDir, \"main.tf\"), []byte(\"\"), 0o600))\n\ttgPath := filepath.Join(tmpDir, \"terragrunt.hcl\")\n\trequire.NoError(t, os.WriteFile(tgPath, []byte(\"\"), 0o600))\n\n\t// Discovery produces a component with or without config; using empty config is fine here\n\tdiscUnit := component.NewUnit(tmpDir).WithConfig(&config.TerragruntConfig{})\n\tdiscovered := component.Components{discUnit}\n\n\t// Build runner stack from discovery and verify units\n\topts, err := options.NewTerragruntOptionsForTest(tgPath)\n\trequire.NoError(t, err)\n\n\tl := thlogger.CreateLogger()\n\n\trunner, err := runnerpool.NewRunnerPoolStack(context.Background(), l, opts, discovered)\n\trequire.NoError(t, err)\n\n\tunits := runner.GetStack().Units\n\trequire.Len(t, units, 1)\n\trequire.Equal(t, tmpDir, units[0].Path())\n}\n"
  },
  {
    "path": "internal/runner/runnerpool/writer.go",
    "content": "package runnerpool\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"sync\"\n)\n\n// UnitWriter buffers output for a single unit and flushes incrementally during execution.\n// This prevents interleaved output when multiple units run in parallel while ensuring\n// output appears in real-time during execution, not just at completion.\ntype UnitWriter struct {\n\tout    io.Writer\n\tbuffer bytes.Buffer\n\tmu     sync.Mutex\n}\n\n// NewUnitWriter returns a new UnitWriter instance.\nfunc NewUnitWriter(out io.Writer) *UnitWriter {\n\treturn &UnitWriter{\n\t\tout: out,\n\t}\n}\n\nfunc (writer *UnitWriter) Write(p []byte) (int, error) {\n\twriter.mu.Lock()\n\tdefer writer.mu.Unlock()\n\n\tn, err := writer.buffer.Write(p)\n\tif err != nil {\n\t\treturn n, err\n\t}\n\n\tif flushErr := writer.flushCompleteLines(); flushErr != nil {\n\t\treturn n, flushErr\n\t}\n\n\treturn n, err\n}\n\n// flushCompleteLines flushes any complete lines (ending with newline) from the buffer.\n// Partial lines (without trailing newline) remain in the buffer.\nfunc (writer *UnitWriter) flushCompleteLines() error {\n\tif writer.out == nil {\n\t\treturn nil\n\t}\n\n\tbuf := writer.buffer.Bytes()\n\tlastNewline := bytes.LastIndexByte(buf, '\\n')\n\n\tif lastNewline >= 0 {\n\t\tlineCount := lastNewline + 1\n\t\tlines := writer.buffer.Next(lineCount)\n\n\t\tif _, err := writer.out.Write(lines); err != nil {\n\t\t\twriter.buffer.Write(lines)\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Flush flushes all buffered data to the output writer.\nfunc (writer *UnitWriter) Flush() error {\n\twriter.mu.Lock()\n\tdefer writer.mu.Unlock()\n\n\tif writer.out != nil {\n\t\tif _, err := writer.buffer.WriteTo(writer.out); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Unwrap returns the underlying output writer that this UnitWriter wraps.\nfunc (writer *UnitWriter) Unwrap() io.Writer {\n\treturn writer.out\n}\n"
  },
  {
    "path": "internal/runner/runnerpool/writer_test.go",
    "content": "package runnerpool_test\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runnerpool\"\n)\n\nfunc TestUnitWriter_WriteErrorPropagation(t *testing.T) {\n\tt.Parallel()\n\n\twriteErr := errors.New(\"write failed\")\n\tfailingWriter := &failingWriter{err: writeErr}\n\n\twriter := runnerpool.NewUnitWriter(failingWriter)\n\n\tdata := []byte(\"line 1\\nline 2\\n\")\n\tn, err := writer.Write(data)\n\n\trequire.Error(t, err)\n\trequire.Equal(t, writeErr, err)\n\trequire.Equal(t, len(data), n)\n\n\terr = writer.Flush()\n\trequire.Error(t, err)\n\trequire.Equal(t, writeErr, err)\n}\n\nfunc TestUnitWriter_FlushCompleteLines(t *testing.T) {\n\tt.Parallel()\n\n\tvar buf strings.Builder\n\n\twriter := runnerpool.NewUnitWriter(&buf)\n\n\tdata := []byte(\"line 1\\nline 2\\npartial\")\n\t_, err := writer.Write(data)\n\trequire.NoError(t, err)\n\n\toutput := buf.String()\n\trequire.Contains(t, output, \"line 1\")\n\trequire.Contains(t, output, \"line 2\")\n\trequire.NotContains(t, output, \"partial\")\n\n\terr = writer.Flush()\n\trequire.NoError(t, err)\n\trequire.Contains(t, buf.String(), \"partial\")\n}\n\ntype failingWriter struct {\n\terr error\n}\n\nfunc (w *failingWriter) Write(_ []byte) (int, error) {\n\treturn 0, w.err\n}\n"
  },
  {
    "path": "internal/services/catalog/catalog.go",
    "content": "// Package catalog provides the core functionality for the Terragrunt catalog command.\n// It handles the logic for fetching and processing module information from remote repositories.\n//\n// This logic is intentionally isolated from the CLI package, as that package is focused on\n// spinning up the Terminal User Interface (TUI), and forwarding user input to the catalog service.\n//\n// This should result in an implementation that is easier to test, and more maintainable.\npackage catalog\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/scaffold\"\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog/module\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// NewRepoFunc defines the signature for a function that creates a new repository.\n// This allows for mocking in tests.\ntype NewRepoFunc func(ctx context.Context, l log.Logger, cloneURL, path string, walkWithSymlinks, allowCAS bool, rootWorkingDir string) (*module.Repo, error)\n\nconst (\n\t// tempDirFormat is used to create unique temporary directory names for catalog repositories.\n\t// It uses a hexadecimal representation of a SHA1 hash of the repo URL.\n\ttempDirFormat = \"catalog-%s\" // Changed from catalog%x to catalog-%s for clarity with Sprintf.\n)\n\n// CatalogService defines the interface for the catalog service.\n// It's responsible for fetching and processing module information.\ntype CatalogService interface {\n\t// Load retrieves all modules from the configured repositories.\n\t// It stores discovered modules internally.\n\tLoad(ctx context.Context, l log.Logger) error\n\n\t// Modules returns the discovered modules.\n\tModules() module.Modules\n\n\t// Scaffold scaffolds a module.\n\tScaffold(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, module *module.Module) error\n\n\t// WithNewRepoFunc allows overriding the default function used to create repository instances.\n\t// This is primarily useful for testing.\n\tWithNewRepoFunc(fn NewRepoFunc) CatalogService\n\n\t// WithRepoURL allows overriding the repository URL.\n\t// This is primarily useful for testing.\n\tWithRepoURL(repoURL string) CatalogService\n}\n\n// catalogServiceImpl is the concrete implementation of CatalogService.\n// It holds the necessary options and configuration to perform its tasks.\ntype catalogServiceImpl struct {\n\topts    *options.TerragruntOptions\n\tnewRepo NewRepoFunc\n\trepoURL string\n\tmodules module.Modules\n}\n\n// NewCatalogService creates a new instance of catalogServiceImpl with default settings.\n// It requires TerragruntOptions and an optional initial repository URL.\n// Configuration methods like WithNewRepoFunc can be chained to customize the service.\nfunc NewCatalogService(opts *options.TerragruntOptions) *catalogServiceImpl {\n\treturn &catalogServiceImpl{\n\t\topts:    opts,\n\t\tnewRepo: module.NewRepo,\n\t}\n}\n\n// WithNewRepoFunc allows overriding the default function used to create repository instances.\n// This is primarily useful for testing.\nfunc (s *catalogServiceImpl) WithNewRepoFunc(fn NewRepoFunc) CatalogService {\n\ts.newRepo = fn\n\n\treturn s\n}\n\n// WithRepoURL allows overriding the repository URL.\n// This is primarily useful for testing.\nfunc (s *catalogServiceImpl) WithRepoURL(repoURL string) CatalogService {\n\ts.repoURL = repoURL\n\n\treturn s\n}\n\n// Load implements the CatalogService interface.\n// It contains the core logic for cloning/updating repositories and finding Terragrunt modules within them.\nfunc (s *catalogServiceImpl) Load(ctx context.Context, l log.Logger) error {\n\trepoURLs := []string{s.repoURL}\n\n\t// If no specific repoURL was provided to the service, try to read from catalog config.\n\tif s.repoURL == \"\" {\n\t\t_, pctx := configbridge.NewParsingContext(ctx, l, s.opts)\n\n\t\tcatalogCfg, err := config.ReadCatalogConfig(ctx, l, pctx)\n\t\tif err != nil {\n\t\t\treturn errors.Errorf(\"failed to read catalog configuration: %w\", err)\n\t\t}\n\n\t\tif catalogCfg != nil && len(catalogCfg.URLs) > 0 {\n\t\t\trepoURLs = catalogCfg.URLs\n\t\t} else {\n\t\t\treturn errors.Errorf(\"no catalog URLs provided\")\n\t\t}\n\t}\n\n\t// Remove duplicates\n\trepoURLs = util.RemoveDuplicates(repoURLs)\n\tif len(repoURLs) == 0 || (len(repoURLs) == 1 && repoURLs[0] == \"\") {\n\t\treturn errors.Errorf(\"no valid repository URLs specified after configuration and flag processing\")\n\t}\n\n\tvar allModules module.Modules\n\n\t// Evaluate experimental features for symlinks and content-addressable storage.\n\twalkWithSymlinks := s.opts.Experiments.Evaluate(experiment.Symlinks)\n\tallowCAS := s.opts.Experiments.Evaluate(experiment.CAS)\n\n\tvar errs []error\n\n\tfor _, currentRepoURL := range repoURLs {\n\t\tif currentRepoURL == \"\" {\n\t\t\tl.Warnf(\"Empty repository URL encountered, skipping.\")\n\t\t\tcontinue\n\t\t}\n\n\t\t// Create a unique path in the system's temporary directory for this repository.\n\t\t// The path is based on a SHA1 hash of the repository URL to ensure uniqueness and idempotency.\n\t\tencodedRepoURL := util.EncodeBase64Sha1(currentRepoURL)\n\t\ttempPath := filepath.Join(os.TempDir(), fmt.Sprintf(tempDirFormat, encodedRepoURL))\n\n\t\tl.Debugf(\"Processing repository %s in temporary path %s\", currentRepoURL, tempPath)\n\n\t\t// Initialize the repository. This might involve cloning or updating.\n\t\t// Use the newRepo function stored in the service instance.\n\t\trepo, err := s.newRepo(ctx, l, currentRepoURL, tempPath, walkWithSymlinks, allowCAS, s.opts.RootWorkingDir)\n\t\tif err != nil {\n\t\t\tl.Errorf(\"Failed to initialize repository %s: %v\", currentRepoURL, err)\n\n\t\t\terrs = append(errs, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\t// Find modules within the initialized repository.\n\t\trepoModules, err := repo.FindModules(ctx)\n\t\tif err != nil {\n\t\t\tl.Errorf(\"Failed to find modules in repository %s: %v\", currentRepoURL, err)\n\n\t\t\terrs = append(errs, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tl.Infof(\"Found %d module(s) in repository %q\", len(repoModules), currentRepoURL)\n\t\tallModules = append(allModules, repoModules...)\n\t}\n\n\ts.modules = allModules\n\n\tif len(errs) > 0 {\n\t\treturn errors.Errorf(\"failed to find modules in some repositories: %v\", errs)\n\t}\n\n\tif len(allModules) == 0 {\n\t\treturn errors.Errorf(\"no modules found in any of the configured repositories\")\n\t}\n\n\treturn nil\n}\n\nfunc (s *catalogServiceImpl) Modules() module.Modules {\n\treturn s.modules\n}\n\nfunc (s *catalogServiceImpl) Scaffold(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, module *module.Module) error {\n\tl.Infof(\"Scaffolding module: %q\", module.TerraformSourcePath())\n\n\treturn scaffold.Run(ctx, l, opts, module.TerraformSourcePath(), \"\")\n}\n"
  },
  {
    "path": "internal/services/catalog/catalog_test.go",
    "content": "package catalog_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog\"\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog/module\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestListModules_HappyPath(t *testing.T) {\n\tt.Parallel()\n\n\topts := options.NewTerragruntOptions()\n\topts.ScaffoldRootFileName = config.RecommendedParentConfigName\n\n\tmockNewRepo := func(ctx context.Context, logger log.Logger, repoURL, path string, walkWithSymlinks, allowCAS bool, rootWorkingDir string) (*module.Repo, error) {\n\t\t// Use a temp dir for the dummyRepoDir to ensure cleanup and parallelism safety.\n\t\tdummyRepoDir := filepath.Join(helpers.TmpDirWOSymlinks(t), strings.ReplaceAll(repoURL, \"github.com/gruntwork-io/\", \"\"))\n\t\tos.MkdirAll(filepath.Join(dummyRepoDir, \".git\"), 0755)\n\t\tos.WriteFile(filepath.Join(dummyRepoDir, \".git\", \"config\"), []byte(\"[remote \\\"origin\\\"]\\nurl = \"+repoURL), 0644)\n\t\tos.WriteFile(filepath.Join(dummyRepoDir, \".git\", \"HEAD\"), []byte(\"ref: refs/heads/main\"), 0644)\n\n\t\tif repoURL == \"github.com/gruntwork-io/repo1\" {\n\t\t\treadme1Path := filepath.Join(dummyRepoDir, \"README.md\")\n\t\t\tos.WriteFile(readme1Path, []byte(\"# module1-title\\nThis is module1.\"), 0644)\n\t\t\tos.WriteFile(filepath.Join(dummyRepoDir, \"module1.tf\"), []byte{}, 0644)\n\n\t\t\treturn module.NewRepo(ctx, logger, dummyRepoDir, path, walkWithSymlinks, allowCAS, \"\")\n\t\t}\n\n\t\tif repoURL == \"github.com/gruntwork-io/repo2\" {\n\t\t\treadme2Path := filepath.Join(dummyRepoDir, \"README.md\")\n\t\t\tos.WriteFile(readme2Path, []byte(\"# module2-title\\nThis is module2.\"), 0644)\n\t\t\tos.WriteFile(filepath.Join(dummyRepoDir, \"module2.tf\"), []byte{}, 0644)\n\n\t\t\treturn module.NewRepo(ctx, logger, dummyRepoDir, path, walkWithSymlinks, allowCAS, \"\")\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"unexpected repoURL in mock newRepoFunc: %s\", repoURL)\n\t}\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\trootFile := filepath.Join(tmpDir, \"root.hcl\")\n\terr := os.WriteFile(rootFile, []byte(`catalog {\n\turls = [\n\t\t\"github.com/gruntwork-io/repo1\",\n\t\t\"github.com/gruntwork-io/repo2\",\n\t]\n}`), 0600)\n\trequire.NoError(t, err)\n\n\tunitDir := filepath.Join(tmpDir, \"unit\")\n\tos.MkdirAll(unitDir, 0755)\n\topts.TerragruntConfigPath = filepath.Join(unitDir, \"terragrunt.hcl\")\n\n\tsvc := catalog.NewCatalogService(opts).WithNewRepoFunc(mockNewRepo)\n\n\tl := logger.CreateLogger()\n\n\terr = svc.Load(t.Context(), l)\n\trequire.NoError(t, err)\n\n\tmodules := svc.Modules()\n\n\trequire.NotNil(t, modules)\n\tassert.Len(t, modules, 2)\n\tassert.Equal(t, \"module1-title\", modules[0].Title())\n\tassert.Equal(t, \"module2-title\", modules[1].Title())\n}\n\nfunc TestListModules_NoRepositoriesConfigured(t *testing.T) {\n\tt.Parallel()\n\n\topts := options.NewTerragruntOptions()\n\topts.ScaffoldRootFileName = config.RecommendedParentConfigName\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\topts.TerragruntConfigPath = filepath.Join(tmpDir, \"nonexistent-terragrunt.hcl\")\n\n\t// No customNewRepoFunc needed as it should error before trying to create a repo.\n\tsvc := catalog.NewCatalogService(opts)\n\tl := logger.CreateLogger()\n\terr := svc.Load(t.Context(), l)\n\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"no catalog URLs provided\")\n}\n\nfunc TestListModules_SingleRepoFromFlag(t *testing.T) {\n\tt.Parallel()\n\n\topts := options.NewTerragruntOptions()\n\topts.ScaffoldRootFileName = config.RecommendedParentConfigName\n\n\tmockNewRepo := func(ctx context.Context, logger log.Logger, repoURL, path string, walkWithSymlinks, allowCAS bool, rootWorkingDir string) (*module.Repo, error) {\n\t\tif repoURL == \"github.com/gruntwork-io/only-repo\" {\n\t\t\tdummyRepoDir := filepath.Join(helpers.TmpDirWOSymlinks(t), \"only-repo\")\n\t\t\tos.MkdirAll(filepath.Join(dummyRepoDir, \".git\"), 0755)\n\t\t\tos.WriteFile(filepath.Join(dummyRepoDir, \".git\", \"config\"), []byte(\"[remote \\\"origin\\\"]\\nurl = \"+repoURL), 0644)\n\t\t\tos.WriteFile(filepath.Join(dummyRepoDir, \".git\", \"HEAD\"), []byte(\"ref: refs/heads/main\"), 0644)\n\t\t\tos.WriteFile(filepath.Join(dummyRepoDir, \"README.md\"), []byte(\"# moduleA-title\"), 0644)\n\t\t\tos.WriteFile(filepath.Join(dummyRepoDir, \"moduleA.tf\"), []byte{}, 0644)\n\n\t\t\treturn module.NewRepo(ctx, logger, dummyRepoDir, path, walkWithSymlinks, allowCAS, \"\")\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"unexpected repoURL: %s\", repoURL)\n\t}\n\n\tsvc := catalog.NewCatalogService(opts).WithNewRepoFunc(mockNewRepo).WithRepoURL(\"github.com/gruntwork-io/only-repo\")\n\tl := logger.CreateLogger()\n\terr := svc.Load(t.Context(), l)\n\n\tmodules := svc.Modules()\n\n\trequire.NoError(t, err)\n\trequire.NotNil(t, modules)\n\tassert.Len(t, modules, 1)\n\tassert.Equal(t, \"moduleA-title\", modules[0].Title())\n}\n\nfunc TestListModules_ErrorFromNewRepo(t *testing.T) {\n\tt.Parallel()\n\n\topts := options.NewTerragruntOptions()\n\topts.ScaffoldRootFileName = config.RecommendedParentConfigName\n\n\texpectedErr := errors.Errorf(\"failed to clone repo\")\n\tmockNewRepo := func(ctx context.Context, logger log.Logger, repoURL, path string, walkWithSymlinks, allowCAS bool, rootWorkingDir string) (*module.Repo, error) {\n\t\treturn nil, expectedErr\n\t}\n\n\tsvc := catalog.NewCatalogService(opts).WithNewRepoFunc(mockNewRepo).WithRepoURL(\"github.com/gruntwork-io/error-repo\")\n\tl := logger.CreateLogger()\n\terr := svc.Load(t.Context(), l)\n\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"failed to find modules in some repositories\", \"Error message mismatch: %v\", err)\n\tassert.True(t, errors.Is(err, expectedErr) || strings.Contains(err.Error(), expectedErr.Error()), \"Original error not wrapped correctly: %v\", err)\n}\n\nfunc TestListModules_ErrorFromFindModules(t *testing.T) {\n\tt.Parallel()\n\n\topts := options.NewTerragruntOptions()\n\topts.ScaffoldRootFileName = config.RecommendedParentConfigName\n\n\tmockNewRepo := func(ctx context.Context, logger log.Logger, repoURL, path string, walkWithSymlinks, allowCAS bool, rootWorkingDir string) (*module.Repo, error) {\n\t\tif repoURL == \"github.com/gruntwork-io/find-error-repo\" {\n\t\t\tdummyRepoDir := filepath.Join(helpers.TmpDirWOSymlinks(t), \"find-error-repo-dir\")\n\t\t\tos.MkdirAll(filepath.Join(dummyRepoDir, \".git\"), 0755)\n\t\t\tos.WriteFile(filepath.Join(dummyRepoDir, \".git\", \"config\"), []byte(\"[remote \\\"origin\\\"]\\nurl = \"+repoURL), 0644)\n\t\t\tos.WriteFile(filepath.Join(dummyRepoDir, \".git\", \"HEAD\"), []byte(\"ref: refs/heads/main\"), 0644)\n\n\t\t\tmoduleDirWithBadReadme := filepath.Join(dummyRepoDir, \"problem_module\")\n\t\t\tos.MkdirAll(moduleDirWithBadReadme, 0755)\n\t\t\tos.WriteFile(filepath.Join(moduleDirWithBadReadme, \"main.tf\"), []byte(\"{}\"), 0644)\n\t\t\tos.Mkdir(filepath.Join(moduleDirWithBadReadme, \"README.md\"), 0755)\n\n\t\t\treturn module.NewRepo(ctx, logger, dummyRepoDir, path, walkWithSymlinks, allowCAS, \"\")\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"unexpected repoURL: %s\", repoURL)\n\t}\n\n\tsvc := catalog.NewCatalogService(opts).WithNewRepoFunc(mockNewRepo).WithRepoURL(\"github.com/gruntwork-io/find-error-repo\")\n\tl := logger.CreateLogger()\n\terr := svc.Load(t.Context(), l)\n\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"no modules found in any of the configured repositories\")\n}\n\nfunc TestListModules_TofuExtension(t *testing.T) {\n\tt.Parallel()\n\n\topts := options.NewTerragruntOptions()\n\topts.ScaffoldRootFileName = config.RecommendedParentConfigName\n\n\tmockNewRepo := func(ctx context.Context, logger log.Logger, repoURL, path string, walkWithSymlinks, allowCAS bool, rootWorkingDir string) (*module.Repo, error) {\n\t\tif repoURL == \"github.com/gruntwork-io/tofu-repo\" {\n\t\t\tdummyRepoDir := filepath.Join(helpers.TmpDirWOSymlinks(t), \"tofu-repo\")\n\t\t\tos.MkdirAll(filepath.Join(dummyRepoDir, \".git\"), 0755)\n\t\t\tos.WriteFile(filepath.Join(dummyRepoDir, \".git\", \"config\"), []byte(\"[remote \\\"origin\\\"]\\nurl = \"+repoURL), 0644)\n\t\t\tos.WriteFile(filepath.Join(dummyRepoDir, \".git\", \"HEAD\"), []byte(\"ref: refs/heads/main\"), 0644)\n\t\t\tos.WriteFile(filepath.Join(dummyRepoDir, \"README.md\"), []byte(\"# tofu-module\\nOpenTofu module using .tofu extensions.\"), 0644)\n\t\t\tos.WriteFile(filepath.Join(dummyRepoDir, \"main.tofu\"), []byte(\"resource \\\"null_resource\\\" \\\"test\\\" {}\"), 0644)\n\t\t\tos.WriteFile(filepath.Join(dummyRepoDir, \"variables.tofu\"), []byte(\"variable \\\"name\\\" {}\"), 0644)\n\n\t\t\treturn module.NewRepo(ctx, logger, dummyRepoDir, path, walkWithSymlinks, allowCAS, \"\")\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"unexpected repoURL: %s\", repoURL)\n\t}\n\n\tsvc := catalog.NewCatalogService(opts).WithNewRepoFunc(mockNewRepo).WithRepoURL(\"github.com/gruntwork-io/tofu-repo\")\n\tl := logger.CreateLogger()\n\terr := svc.Load(t.Context(), l)\n\n\tmodules := svc.Modules()\n\n\trequire.NoError(t, err)\n\trequire.NotNil(t, modules)\n\tassert.Len(t, modules, 1)\n\tassert.Equal(t, \"tofu-module\", modules[0].Title())\n}\n\nfunc TestListModules_MixedTfAndTofu(t *testing.T) {\n\tt.Parallel()\n\n\topts := options.NewTerragruntOptions()\n\topts.ScaffoldRootFileName = config.RecommendedParentConfigName\n\n\tmockNewRepo := func(ctx context.Context, logger log.Logger, repoURL, path string, walkWithSymlinks, allowCAS bool, rootWorkingDir string) (*module.Repo, error) {\n\t\tif repoURL == \"github.com/gruntwork-io/mixed-repo\" {\n\t\t\tdummyRepoDir := filepath.Join(helpers.TmpDirWOSymlinks(t), \"mixed-repo\")\n\t\t\tos.MkdirAll(filepath.Join(dummyRepoDir, \".git\"), 0755)\n\t\t\tos.WriteFile(filepath.Join(dummyRepoDir, \".git\", \"config\"), []byte(\"[remote \\\"origin\\\"]\\nurl = \"+repoURL), 0644)\n\t\t\tos.WriteFile(filepath.Join(dummyRepoDir, \".git\", \"HEAD\"), []byte(\"ref: refs/heads/main\"), 0644)\n\n\t\t\t// Module with .tf files\n\t\t\ttfModDir := filepath.Join(dummyRepoDir, \"modules\", \"tf-module\")\n\t\t\tos.MkdirAll(tfModDir, 0755)\n\t\t\tos.WriteFile(filepath.Join(tfModDir, \"README.md\"), []byte(\"# tf-module\\nTerraform module.\"), 0644)\n\t\t\tos.WriteFile(filepath.Join(tfModDir, \"main.tf\"), []byte(\"resource \\\"null_resource\\\" \\\"test\\\" {}\"), 0644)\n\n\t\t\t// Module with .tofu files\n\t\t\ttofuModDir := filepath.Join(dummyRepoDir, \"modules\", \"tofu-module\")\n\t\t\tos.MkdirAll(tofuModDir, 0755)\n\t\t\tos.WriteFile(filepath.Join(tofuModDir, \"README.md\"), []byte(\"# tofu-module\\nOpenTofu module.\"), 0644)\n\t\t\tos.WriteFile(filepath.Join(tofuModDir, \"main.tofu\"), []byte(\"resource \\\"null_resource\\\" \\\"test\\\" {}\"), 0644)\n\n\t\t\treturn module.NewRepo(ctx, logger, dummyRepoDir, path, walkWithSymlinks, allowCAS, \"\")\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"unexpected repoURL: %s\", repoURL)\n\t}\n\n\tsvc := catalog.NewCatalogService(opts).WithNewRepoFunc(mockNewRepo).WithRepoURL(\"github.com/gruntwork-io/mixed-repo\")\n\tl := logger.CreateLogger()\n\terr := svc.Load(t.Context(), l)\n\n\tmodules := svc.Modules()\n\n\trequire.NoError(t, err)\n\trequire.NotNil(t, modules)\n\trequire.Len(t, modules, 2)\n\n\ttitles := []string{modules[0].Title(), modules[1].Title()}\n\tassert.Contains(t, titles, \"tf-module\")\n\tassert.Contains(t, titles, \"tofu-module\")\n}\n\nfunc TestListModules_NoModulesFound(t *testing.T) {\n\tt.Parallel()\n\n\topts := options.NewTerragruntOptions()\n\topts.ScaffoldRootFileName = config.RecommendedParentConfigName\n\n\tmockNewRepo := func(ctx context.Context, logger log.Logger, repoURL, path string, walkWithSymlinks, allowCAS bool, rootWorkingDir string) (*module.Repo, error) {\n\t\tdummyRepoDir := filepath.Join(helpers.TmpDirWOSymlinks(t), \"empty-repo-dir\")\n\t\tos.MkdirAll(filepath.Join(dummyRepoDir, \".git\"), 0755)\n\t\tos.WriteFile(filepath.Join(dummyRepoDir, \".git\", \"config\"), []byte(\"[remote \\\"origin\\\"]\\nurl = \"+repoURL), 0644)\n\t\tos.WriteFile(filepath.Join(dummyRepoDir, \".git\", \"HEAD\"), []byte(\"ref: refs/heads/main\"), 0644)\n\n\t\treturn module.NewRepo(ctx, logger, dummyRepoDir, path, walkWithSymlinks, allowCAS, \"\")\n\t}\n\n\tsvc := catalog.NewCatalogService(opts).WithNewRepoFunc(mockNewRepo).WithRepoURL(\"github.com/gruntwork-io/empty-repo\")\n\tl := logger.CreateLogger()\n\terr := svc.Load(t.Context(), l)\n\trequire.Error(t, err)\n\n\tmodules := svc.Modules()\n\n\tassert.Contains(t, err.Error(), \"no modules found in any of the configured repositories\")\n\tassert.Empty(t, modules, \"Should return empty modules slice on 'no modules found' error\")\n}\n"
  },
  {
    "path": "internal/services/catalog/module/doc.go",
    "content": "package module\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\nconst (\n\tmdExt   = \".md\"\n\tadocExt = \".adoc\"\n\n\tdocTitle docDataKey = iota\n\tdocDescription\n\tdocContent\n\n\ttagH1Block docTagName = iota\n\ttagH2Block\n)\n\nvar (\n\t// `strings.EqualFold` is used (case insensitive) while comparing\n\tdocFiles = []string{\"README.md\", \"README.adoc\"}\n\n\tfrontmatterKeys = map[string]docDataKey{\n\t\t\"name\":        docTitle,\n\t\t\"description\": docDescription,\n\t}\n)\n\ntype docDataKey byte\ntype docTagName byte\n\ntype DocRegs []*regexp.Regexp\n\nfunc (regs DocRegs) Replace(str string) string {\n\tfor _, reg := range regs {\n\t\tstr = reg.ReplaceAllString(str, \"$1\")\n\t}\n\n\treturn str\n}\n\ntype Doc struct {\n\ttagCache         map[docDataKey]string\n\ttagRegs          map[docTagName]*regexp.Regexp\n\tfrontmatterCache map[docDataKey]string\n\tfrontmatterReg   *regexp.Regexp\n\trawContent       string\n\tfileExt          string\n\ttagStripRegs     DocRegs\n}\n\nfunc NewDoc(rawContent, fileExt string) *Doc {\n\tdoc := &Doc{\n\t\trawContent: rawContent,\n\t\tfileExt:    fileExt,\n\n\t\ttagRegs:        make(map[docTagName]*regexp.Regexp),\n\t\tfrontmatterReg: regexp.MustCompile(`(?i)^[\\s\\n]*<!-- frontmatter[\\s\\n]*([\\S\\s]*?)[\\s\\n]*-->`),\n\t}\n\n\tswitch fileExt {\n\tcase mdExt:\n\t\tdoc.tagRegs[tagH1Block] = regexp.MustCompile(`(?:^|\\n)\\#{1}\\s([\\S\\s]+?)(?:[\\r\\n]+\\#|[\\r\\n]*$)`)\n\t\tdoc.tagRegs[tagH2Block] = regexp.MustCompile(`(?:^|\\n)\\#{2}\\s([\\S\\s]+?)(?:[\\r\\n]+\\#|[\\r\\n]*$)`)\n\t\tdoc.tagStripRegs = DocRegs{\n\t\t\t// code\n\t\t\tregexp.MustCompile(\"`{3}\" + `.*[\\r\\n]+`),\n\t\t\tregexp.MustCompile(\"`(.+?)`\"),\n\t\t\t// html\n\t\t\tregexp.MustCompile(\"<(.*?)>\"),\n\t\t\t// bold\n\t\t\tregexp.MustCompile(`\\*\\*([^*]+)\\*\\*`),\n\t\t\tregexp.MustCompile(`__([^_]+)__`),\n\t\t\t// italic\n\t\t\tregexp.MustCompile(`\\*([^*]+)\\*`),\n\t\t\tregexp.MustCompile(`_([^_]+)_`),\n\t\t\t// setext header\n\t\t\tregexp.MustCompile(`^[=\\-]{2,}\\s*$`),\n\t\t\t// foot note\n\t\t\tregexp.MustCompile(`\\[\\^.+?\\](\\: .*?$)?`),\n\t\t\tregexp.MustCompile(`\\s{0,2}\\[.*?\\]: .*?$`),\n\t\t\t// image\n\t\t\tregexp.MustCompile(`\\!\\[(?:.*?)\\]\\s?[\\[\\(].*?[\\]\\)]`),\n\t\t\t// link\n\t\t\tregexp.MustCompile(`\\[([\\S\\s]*?)\\][\\[\\(].*?[\\]\\)]`),\n\t\t\t// blockquote\n\t\t\tregexp.MustCompile(`>\\s*`),\n\t\t\t// ref link\n\t\t\tregexp.MustCompile(`^\\s{1,2}\\[(.*?)\\]: (\\S+)( \".*?\")?\\s*$`),\n\t\t\t// header\n\t\t\tregexp.MustCompile(`(?m)^\\#{1,6}\\s*([^#]+)\\s*(\\#{1,6})?$`),\n\t\t\t// horizontal rule\n\t\t\tregexp.MustCompile(`^[-\\*_]{3,}\\s*$`),\n\t\t}\n\n\tcase adocExt:\n\t\tdoc.tagRegs[tagH1Block] = regexp.MustCompile(`(?:^|\\n)\\={1}\\s([\\S\\s]+?)(?:[\\r\\n]+\\=|[\\r\\n]*$)`)\n\t\tdoc.tagRegs[tagH2Block] = regexp.MustCompile(`(?:^|\\n)\\={2}\\s([\\S\\s]+?)(?:[\\r\\n]+\\=|[\\r\\n]*$)`)\n\t\tdoc.tagStripRegs = DocRegs{\n\t\t\t// html\n\t\t\tregexp.MustCompile(\"<(.*?)>\"),\n\t\t\t// ifdef endif\n\t\t\tregexp.MustCompile(`ifdef::env-github\\[\\][\\S\\s]*?endif::\\[\\]`),\n\t\t\t// comment\n\t\t\tregexp.MustCompile(`(?m)^/{2}.*$`),\n\t\t\t// ex. :name:value\n\t\t\tregexp.MustCompile(`(?m)^:[-!\\w]+:.*$`),\n\t\t\t// ex. toc::[]\n\t\t\tregexp.MustCompile(`\\w+::\\[.*?\\]`),\n\t\t\t// bold\n\t\t\tregexp.MustCompile(`\\*\\*([^\\s][^*]+[^\\s])\\*\\*`),\n\t\t\tregexp.MustCompile(`\\*([^\\s][^*]+[^\\s])\\*`),\n\t\t\t// italic\n\t\t\tregexp.MustCompile(`_{1,2}([^\\s][^_]+[^\\s])_{1,2}`),\n\t\t\t// image\n\t\t\tregexp.MustCompile(`image:[^\\]]+]`),\n\t\t\t// link\n\t\t\tregexp.MustCompile(`(?:link|https):[\\S\\s]+?\\[([\\S\\s]+?)\\]`),\n\t\t\t// header\n\t\t\tregexp.MustCompile(`(?m)^\\={1,6}\\s*([^=]+)\\s*(\\={1,6})?$`),\n\t\t\t// multiple line break\n\t\t\tregexp.MustCompile(`((?:\\r\\n?|\\n){2})(?:\\r\\n?|\\n)*`),\n\t\t}\n\t}\n\n\treturn doc\n}\n\nfunc FindDoc(dir string) (*Doc, error) {\n\tvar filePath, fileExt string\n\n\tfiles, err := os.ReadDir(dir)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tfor _, file := range files {\n\t\tif file.IsDir() {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, readmeFile := range docFiles {\n\t\t\tif strings.EqualFold(readmeFile, file.Name()) {\n\t\t\t\tfilePath = filepath.Join(dir, file.Name())\n\t\t\t\tfileExt = filepath.Ext(filePath)\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t// `md` files have priority over `adoc` files\n\t\tif strings.EqualFold(fileExt, mdExt) {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif filePath == \"\" {\n\t\treturn &Doc{}, nil\n\t}\n\n\tcontentByte, err := os.ReadFile(filePath)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\trawContent := string(contentByte)\n\n\treturn NewDoc(rawContent, fileExt), nil\n}\n\nfunc (doc *Doc) Title() string {\n\tif title := doc.parseFrontmatter(docTitle); title != \"\" {\n\t\treturn title\n\t}\n\n\treturn doc.parseTag(docTitle)\n}\n\nfunc (doc *Doc) Description(maxLenght int) string {\n\tdesc := doc.parseFrontmatter(docDescription)\n\n\tif desc == \"\" {\n\t\tdesc = doc.parseTag(docDescription)\n\t}\n\n\tif maxLenght == 0 {\n\t\treturn desc\n\t}\n\n\tvar (\n\t\tsentences = strings.Split(desc, \".\")\n\t\tsymbols   int\n\t)\n\n\tfor i, sentence := range sentences {\n\t\tsymbols += len(sentence)\n\n\t\tif symbols > maxLenght {\n\t\t\tif i == 0 {\n\t\t\t\tdesc = sentence\n\t\t\t} else {\n\t\t\t\tdesc = strings.Join(sentences[:i], \".\")\n\t\t\t}\n\n\t\t\tdesc += \".\"\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn desc\n}\n\nfunc (doc *Doc) Content(stripTags bool) string {\n\tif !stripTags {\n\t\treturn doc.rawContent\n\t}\n\n\treturn doc.parseTag(docContent)\n}\n\nfunc (doc *Doc) IsMarkDown() bool {\n\treturn doc.fileExt == mdExt\n}\n\n// parseFrontmatter parses Markdown files with frontmatter, which we use as the preferred title/description source.\nfunc (doc *Doc) parseFrontmatter(key docDataKey) string {\n\tif doc.frontmatterReg == nil {\n\t\treturn \"\"\n\t}\n\n\tif doc.frontmatterCache == nil {\n\t\tdoc.frontmatterCache = make(map[docDataKey]string)\n\n\t\tmatch := doc.frontmatterReg.FindStringSubmatch(doc.rawContent)\n\t\tif len(match) == 0 {\n\t\t\treturn \"\"\n\t\t}\n\n\t\tlines := strings.SplitSeq(match[1], \"\\n\")\n\n\t\tfor line := range lines {\n\t\t\tif parts := strings.Split(line, \":\"); len(parts) > 1 {\n\t\t\t\tkey := strings.ToLower(strings.TrimSpace(parts[0]))\n\t\t\t\tval := strings.TrimSpace(parts[1])\n\n\t\t\t\tif key, ok := frontmatterKeys[key]; ok {\n\t\t\t\t\tdoc.frontmatterCache[key] = val\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn doc.frontmatterCache[key]\n}\n\n// parseTag parses Markdown/AsciiDoc files, stips tags and extracts the H1 header as the title and the H1+H2 bodies as the description.\nfunc (doc *Doc) parseTag(key docDataKey) string {\n\tif doc.tagRegs == nil {\n\t\treturn \"\"\n\t}\n\n\tif doc.tagCache == nil {\n\t\tdoc.tagCache = make(map[docDataKey]string)\n\n\t\tvar h1Body, h2Body string\n\n\t\tfor tagName, tagReg := range doc.tagRegs {\n\t\t\tmatch := tagReg.FindStringSubmatch(doc.rawContent)\n\t\t\tif len(match) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlines := strings.Split(match[1], \"\\n\")\n\n\t\t\tswitch tagName {\n\t\t\tcase tagH1Block:\n\t\t\t\t// header title\n\t\t\t\tdoc.tagCache[docTitle] = lines[0]\n\n\t\t\t\tif len(lines) > 1 {\n\t\t\t\t\th1Body = strings.Join(lines[1:], \"\\n\")\n\t\t\t\t}\n\n\t\t\tcase tagH2Block:\n\t\t\t\tif len(lines) > 1 {\n\t\t\t\t\th2Body = strings.Join(lines[1:], \"\\n\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tdesc := h1Body + \" \" + h2Body\n\n\t\t// strip doc tags\n\t\tdesc = doc.tagStripRegs.Replace(desc)\n\n\t\t// remove redundant spaces and new lines\n\t\tdesc = strings.Join(strings.Fields(desc), \" \")\n\n\t\tdoc.tagCache[docDescription] = desc\n\n\t\t// strip doc tags\n\t\tcontent := doc.tagStripRegs.Replace(doc.rawContent)\n\n\t\tdoc.tagCache[docContent] = content\n\t}\n\n\treturn doc.tagCache[key]\n}\n"
  },
  {
    "path": "internal/services/catalog/module/doc_test.go",
    "content": "package module_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog/module\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar testFrontmatterEcsCluster = `\n<!-- Frontmatter\ntype: service\nname: Amazon ECS Cluster\ndescription: Deploy an Amazon ECS Cluster.\ncategory: docker-orchestration\ncloud: aws\ntags: [\"docker\", \"orchestration\", \"ecs\", \"containers\"]\nlicense: gruntwork\nbuilt-with: terraform, bash, python, go\n-->\n# Amazon ECS Cluster\n\n[![Maintained by Gruntwork](https://img.shields.io/badge/maintained%20by-gruntwork.io-%235849a6.svg)](https://gruntwork.io)\n`\n\nvar testFrontmatterAsgService = `\n\n\n  <!-- Frontmatter\ndescription: Deploy an AMI across an Auto Scaling Group (ASG), with support for zero-downtime, rolling deployment, load balancing, health checks, service discovery, and auto scaling.\ntype: service\nname: Auto Scaling Group (ASG)\ncategory: services\ncloud: aws\ntags: [\"asg\", \"ec2\"]\nlicense: gruntwork\nbuilt-with: terraform\n-->\n\n# Auto Scaling Group\n\n[![Maintained by Gruntwork](https://img.shields.io/badge/maintained%20by-gruntwork.io-%235849a6.svg)](https://gruntwork.io)\n`\n\nfunc TestFrontmatter(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tcontent      string\n\t\texpectedName string\n\t\texpectedDesc string\n\t}{\n\t\t{\n\t\t\ttestFrontmatterEcsCluster,\n\t\t\t\"Amazon ECS Cluster\",\n\t\t\t\"Deploy an Amazon ECS Cluster.\",\n\t\t},\n\t\t{\n\t\t\ttestFrontmatterAsgService,\n\t\t\t\"Auto Scaling Group (ASG)\",\n\t\t\t\"Deploy an AMI across an Auto Scaling Group (ASG), with support for zero-downtime, rolling deployment, load balancing, health checks, service discovery, and auto scaling.\",\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tdoc := module.NewDoc(tc.content, \"\")\n\n\t\t\tassert.Equal(t, tc.expectedName, doc.Title(), \"Frontmatter Name\")\n\t\t\tassert.Equal(t, tc.expectedDesc, doc.Description(0), \"Frontmatter Description\")\n\t\t})\n\t}\n}\n\nvar testH1EksK8sArgocd = `\n# EKS K8s GitOps Module\nThis module deploys [Argo CD](https://argo-cd.readthedocs.io/en/stable/) to an EKS cluster. Argo CD is a declarative GitOps continuous delivery tool for Kubernetes. See the [Argo CD](https://argo-cd.readthedocs.io/en/stable/) for more details. This module supports deploying the Argo CD resources to Fargate in addition to EC2 Worker Nodes.\n\n\n# Gruntwork GitOps \"GruntOps\"\n\nGitOps is an operational framework that is built around DevOps best practices for a standardized approach to managing the lifecycle of Kubernetes based deployments. GitOps provides a unified approach to the deployment and management of container workloads, with Git being the single source of truth for the state of the container infrastructure. GitOps is a very developer-centric workflow that works best when adopted by individuals and teams that follow a git based development lifecycle. The core principles of GitOps have been at the center of Gruntwork from the beginning!\n\n\n## Getting Started\nTo use this module, you will need to have a running EKS cluster prior to deploying this module. See the [Argo CD Example](/examples/eks-cluster-with-argocd/) for an example of how to deploy this module.\n`\n\nvar testH1EksCloudwatchAgent = `\n# EKS CloudWatch Agent Module\n\nThis Terraform Module installs and configures\n[Amazon CloudWatch Agent](https://github.com/aws/amazon-cloudwatch-agent/) on an EKS cluster, so that\neach node runs the agent to collect more system-level metrics from Amazon EC2 instances and ship them to Amazon CloudWatch.\nThis extra metric data allows using [CloudWatch Container Insights](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/ContainerInsights.html)\nfor a single pane of glass for application, performance, host, control plane, data plane insights.\n\nThis module uses the [community helm chart](https://github.com/aws/eks-charts/tree/8b063ec/stable/aws-cloudwatch-metrics),\nwith a set of best practices inputs.\n\n**This module is for setting up CloudWatch Agent for EKS clusters with worker nodes (self-managed or managed node groups) that\nhave support for [` + \"`DaemonSets`\" + `](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/). CloudWatch Container\nInsights is [not supported for EKS Fargate](https://github.com/aws/containers-roadmap/issues/920).**\n\n\n## How does this work?\n\nCloudWatch automatically collects metrics for many resources, such as CPU, memory, disk, and network.\nContainer Insights also provides diagnostic information, such as container restart failures,\nto help you isolate issues and resolve them quickly.\n`\n\nvar testH1EcsCluster = `\n# Amazon ECS Cluster\n\n[![Maintained by Gruntwork](https://img.shields.io/badge/maintained%20by-gruntwork.io-%235849a6.svg)](https://gruntwork.io)\n![Terraform version](https://img.shields.io/badge/tf-%3E%3D1.1.0-blue.svg)\n[![Docs](https://img.shields.io/badge/docs-docs.gruntwork.io-informational)](https://docs.gruntwork.io/reference/services/app-orchestration/amazon-ecs-cluster)\n\n## Overview\n\nThis service contains [Terraform](https://www.terraform.io) code to deploy a production-grade ECS cluster on\n[AWS](https://aws.amazon.com) using [Elastic Container Service (ECS)](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html).\n\nThis service launches an ECS cluster on top of an Auto Scaling Group that you manage. If you wish to launch an ECS\ncluster on top of Fargate that is completely managed by AWS, refer to the\n[ecs-fargate-cluster module](../ecs-fargate-cluster). Refer to the section\n[EC2 vs Fargate Launch Types](https://github.com/gruntwork-io/terraform-aws-ecs/blob/master/core-concepts.md#ec2-vs-fargate-launch-types)\nfor more information on the differences between the two flavors.\n`\n\nvar testH1EksAWSAuthMerger = `\n:type: service\n:name: EKS AWS Auth Merger\n:description: Manage the aws-auth ConfigMap across multiple independent ConfigMaps.\n\n// AsciiDoc TOC settings\n:toc:\n:toc-placement!:\n:toc-title:\n\n// GitHub specific settings. See https://gist.github.com/dcode/0cfbf2699a1fe9b46ff04c41721dda74 for details.\nifdef::env-github[]\n:tip-caption: :bulb:\n:note-caption: :information_source:\nendif::[]\n\n= EKS AWS Auth Merger\n\nimage:https://img.shields.io/badge/maintained%20by-gruntwork.io-%235849a6.svg[link=\"https://gruntwork.io/?ref=repo_aws_eks\"]\nimage:https://img.shields.io/badge/tf-%3E%3D1.1.0-blue[Terraform version]\nimage:https://img.shields.io/badge/k8s-1.24%20~%201.28-5dbcd2[K8s version]\n\nThis module contains a go CLI, docker container, and terraform module for deploying a Kubernetes controller for managing\nmappings between AWS IAM roles and users to RBAC groups in Kubernetes. The official way to manage the mapping is to add\nvalues in a single, central ` + \"`ConfigMap`\" + `. This module allows you to break up the central ` + \"`ConfigMap`\" + ` across multiple,\nseparate ` + \"`ConfigMaps`\" + ` each configuring a subset of the mappings you ultimately want to use, allowing you to update\nentries in the ` + \"`ConfigMap`\" + ` in isolated modules (e.g., when you add a new IAM role in a separate module from the EKS\n\tcluster). The ` + \"`aws-auth-merger`\" + ` watches for ` + \"`aws-auth`\" + ` compatible ` + \"`ConfigMaps`\" + ` that can be merged to manage the\n` + \"`aws-auth`\" + ` authentication ` + \"`ConfigMap`\" + ` for EKS.\n\n\ntoc::[]\n\n\n\n\n== Features\n\n* Break up the ` + \"`aws-auth`\" + ` Kubernetes ` + \"`ConfigMap`\" + ` across multiple objects.\n* Automatically merge new ` + \"`ConfigMaps`\" + ` as they are added and removed.\n* Track automatically generated ` + \"`aws-auth`\" + ` source ` + \"`ConfigMaps`\" + ` that are generated by EKS.\n`\n\nfunc TestElement(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tcontent              string\n\t\tfileExt              string\n\t\texpectedTitle        string\n\t\texpectedDescription  string\n\t\tmaxDescriptionLength int\n\t}{\n\t\t{\n\t\t\tcontent:              testH1EksK8sArgocd,\n\t\t\tfileExt:              \".md\",\n\t\t\tmaxDescriptionLength: 200,\n\t\t\texpectedTitle:        \"EKS K8s GitOps Module\",\n\t\t\texpectedDescription:  \"This module deploys Argo CD to an EKS cluster. Argo CD is a declarative GitOps continuous delivery tool for Kubernetes. See the Argo CD for more details.\",\n\t\t},\n\t\t{\n\t\t\tcontent:              testH1EksCloudwatchAgent,\n\t\t\tfileExt:              \".md\",\n\t\t\tmaxDescriptionLength: 200,\n\t\t\texpectedTitle:        \"EKS CloudWatch Agent Module\",\n\t\t\texpectedDescription:  \"This Terraform Module installs and configures Amazon CloudWatch Agent on an EKS cluster, so that each node runs the agent to collect more system-level metrics from Amazon EC2 instances and ship them to Amazon CloudWatch.\",\n\t\t},\n\t\t{\n\t\t\tcontent:              testH1EcsCluster,\n\t\t\tfileExt:              \".md\",\n\t\t\tmaxDescriptionLength: 200,\n\t\t\texpectedTitle:        \"Amazon ECS Cluster\",\n\t\t\texpectedDescription:  \"This service contains Terraform code to deploy a production-grade ECS cluster on AWS using Elastic Container Service (ECS).\",\n\t\t},\n\t\t{\n\t\t\tcontent:              testH1EksAWSAuthMerger,\n\t\t\tfileExt:              \".adoc\",\n\t\t\tmaxDescriptionLength: 200,\n\t\t\texpectedTitle:        \"EKS AWS Auth Merger\",\n\t\t\texpectedDescription:  \"This module contains a go CLI, docker container, and terraform module for deploying a Kubernetes controller for managing mappings between AWS IAM roles and users to RBAC groups in Kubernetes.\",\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tdoc := module.NewDoc(tc.content, tc.fileExt)\n\n\t\t\tassert.Equal(t, tc.expectedTitle, doc.Title(), \"Title\")\n\t\t\tassert.Equal(t, tc.expectedDescription, doc.Description(tc.maxDescriptionLength), \"Description\")\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/services/catalog/module/module.go",
    "content": "// Package module provides a struct to represent an OpenTofu/Terraform module.\npackage module\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/go-commons/collections\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst (\n\tdefaultDescription   = \"(no description found)\"\n\tmaxDescriptionLength = 200\n)\n\nvar ignoreFiles = []string{\"terraform-cloud-enterprise-private-module-registry-placeholder.tf\"}\n\ntype Modules []*Module\n\ntype Module struct {\n\t*Repo\n\t*Doc\n\n\tcloneURL  string\n\trepoPath  string\n\tmoduleDir string\n\turl       string\n}\n\n// NewModule returns a module instance if the given `moduleDir` path contains an OpenTofu/Terraform module, otherwise returns nil.\nfunc NewModule(repo *Repo, moduleDir string) (*Module, error) {\n\tmodule := &Module{\n\t\tRepo:      repo,\n\t\tcloneURL:  repo.cloneURL,\n\t\trepoPath:  repo.path,\n\t\tmoduleDir: moduleDir,\n\t}\n\n\tif ok, err := module.isValid(); !ok || err != nil {\n\t\treturn nil, err\n\t}\n\n\trepo.logger.Debugf(\"Found module in directory %q\", moduleDir)\n\n\tmodule.url = repo.ModuleURL(moduleDir)\n\n\trepo.logger.Debugf(\"Module URL: %s\", module.url)\n\n\tmodulePath := filepath.Join(module.repoPath, module.moduleDir)\n\n\tdoc, err := FindDoc(modulePath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmodule.Doc = doc\n\n\treturn module, nil\n}\n\nfunc (module *Module) Logger() log.Logger {\n\treturn module.logger\n}\n\n// FilterValue implements /github.com/charmbracelet/bubbles.list.Item.FilterValue\nfunc (module *Module) FilterValue() string {\n\treturn module.Title()\n}\n\n// Title implements /github.com/charmbracelet/bubbles.list.DefaultItem.Title\nfunc (module *Module) Title() string {\n\tif title := module.Doc.Title(); title != \"\" {\n\t\treturn strings.TrimSpace(title)\n\t}\n\n\treturn filepath.Base(module.moduleDir)\n}\n\n// Description implements /github.com/charmbracelet/bubbles.list.DefaultItem.Description\nfunc (module *Module) Description() string {\n\tif desc := module.Doc.Description(maxDescriptionLength); desc != \"\" {\n\t\treturn desc\n\t}\n\n\treturn defaultDescription\n}\n\nfunc (module *Module) URL() string {\n\treturn module.url\n}\n\n// TerraformSourcePath returns the module source URL in the format expected by go-getter:\n// baseURL//moduleDir?query (e.g., git::https://github.com/org/repo.git//modules/foo?ref=v1.0.0)\nfunc (module *Module) TerraformSourcePath() string {\n\tif module.moduleDir == \"\" {\n\t\treturn module.cloneURL\n\t}\n\n\t// Split on ? to separate base URL from query string\n\tbase, query, _ := strings.Cut(module.cloneURL, \"?\")\n\n\tresult := base + \"//\" + module.moduleDir\n\tif query != \"\" {\n\t\tresult += \"?\" + query\n\t}\n\n\treturn result\n}\n\nfunc (module *Module) isValid() (bool, error) {\n\tfiles, err := os.ReadDir(filepath.Join(module.repoPath, module.moduleDir))\n\tif err != nil {\n\t\treturn false, errors.New(err)\n\t}\n\n\tfor _, file := range files {\n\t\tif file.IsDir() {\n\t\t\tcontinue\n\t\t}\n\n\t\tif collections.ListContainsElement(ignoreFiles, file.Name()) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif util.IsTFFile(file.Name()) {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\treturn false, nil\n}\n\nfunc (module *Module) ModuleDir() string {\n\treturn module.moduleDir\n}\n\n// NewModuleForTest creates a Module for testing purposes.\nfunc NewModuleForTest(cloneURL, moduleDir string) *Module {\n\treturn &Module{\n\t\tcloneURL:  cloneURL,\n\t\tmoduleDir: moduleDir,\n\t}\n}\n"
  },
  {
    "path": "internal/services/catalog/module/module_test.go",
    "content": "package module_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog/module\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTerraformSourcePath(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname      string\n\t\tcloneURL  string\n\t\tmoduleDir string\n\t\texpected  string\n\t}{\n\t\t{\n\t\t\tname:      \"root module without ref\",\n\t\t\tcloneURL:  \"git::https://github.com/org/repo.git\",\n\t\t\tmoduleDir: \"\",\n\t\t\texpected:  \"git::https://github.com/org/repo.git\",\n\t\t},\n\t\t{\n\t\t\tname:      \"root module with ref\",\n\t\t\tcloneURL:  \"git::https://github.com/org/repo.git?ref=v1.0.0\",\n\t\t\tmoduleDir: \"\",\n\t\t\texpected:  \"git::https://github.com/org/repo.git?ref=v1.0.0\",\n\t\t},\n\t\t{\n\t\t\tname:      \"submodule without ref\",\n\t\t\tcloneURL:  \"git::https://github.com/org/repo.git\",\n\t\t\tmoduleDir: \"modules/foo\",\n\t\t\texpected:  \"git::https://github.com/org/repo.git//modules/foo\",\n\t\t},\n\t\t{\n\t\t\tname:      \"submodule with ref\",\n\t\t\tcloneURL:  \"git::https://github.com/org/repo.git?ref=v1.0.0\",\n\t\t\tmoduleDir: \"modules/foo\",\n\t\t\texpected:  \"git::https://github.com/org/repo.git//modules/foo?ref=v1.0.0\",\n\t\t},\n\t\t{\n\t\t\tname:      \"ssh url with ref\",\n\t\t\tcloneURL:  \"git::ssh://git@github.com/org/repo.git?ref=v1.0.0\",\n\t\t\tmoduleDir: \"modules/bar\",\n\t\t\texpected:  \"git::ssh://git@github.com/org/repo.git//modules/bar?ref=v1.0.0\",\n\t\t},\n\t\t{\n\t\t\tname:      \"multiple query params\",\n\t\t\tcloneURL:  \"git::https://github.com/org/repo.git?ref=v1.0.0&depth=1\",\n\t\t\tmoduleDir: \"modules/foo\",\n\t\t\texpected:  \"git::https://github.com/org/repo.git//modules/foo?ref=v1.0.0&depth=1\",\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.Parallel()\n\n\t\t\tm := module.NewModuleForTest(tc.cloneURL, tc.moduleDir)\n\t\t\tassert.Equal(t, tc.expected, m.TerraformSourcePath())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/services/catalog/module/repo.go",
    "content": "package module\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\n\t\"github.com/gitsight/go-vcsurl\"\n\t\"github.com/gruntwork-io/go-commons/files\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cas\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/hashicorp/go-getter/v2\"\n\t\"gopkg.in/ini.v1\"\n)\n\nconst (\n\tgithubHost            = \"github.com\"\n\tgithubEnterpriseRegex = `^(github\\.(.+))$`\n\tgitlabHost            = \"gitlab.com\"\n\tazuredevHost          = \"dev.azure.com\"\n\tbitbucketHost         = \"bitbucket.org\"\n\tgitlabSelfHostedRegex = `^(gitlab\\.(.+))$`\n\n\tcloneCompleteSentinel = \".catalog-clone-complete\"\n)\n\nvar (\n\tgitHeadBranchNameReg    = regexp.MustCompile(`^.*?([^/]+)$`)\n\trepoNameFromCloneURLReg = regexp.MustCompile(`(?i)^.*?([-a-z0-9_.]+)[^/]*?(?:\\.git)?$`)\n\n\tmodulesPaths = []string{\"modules\"}\n\n\tincludedGitFiles = []string{\"HEAD\", \"config\"}\n)\n\ntype Repo struct {\n\tlogger log.Logger\n\n\tcloneURL       string\n\tpath           string\n\trootWorkingDir string\n\n\tRemoteURL  string\n\tBranchName string\n\n\twalkWithSymlinks bool\n\tallowCAS         bool\n}\n\nfunc NewRepo(ctx context.Context, l log.Logger, cloneURL, path string, walkWithSymlinks bool, allowCAS bool, rootWorkingDir string) (*Repo, error) {\n\trepo := &Repo{\n\t\tlogger:           l,\n\t\tcloneURL:         cloneURL,\n\t\tpath:             path,\n\t\twalkWithSymlinks: walkWithSymlinks,\n\t\tallowCAS:         allowCAS,\n\t\trootWorkingDir:   rootWorkingDir,\n\t}\n\n\tif err := repo.clone(ctx, l); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := repo.parseRemoteURL(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := repo.parseBranchName(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn repo, nil\n}\n\n// FindModules clones the repository if `repoPath` is a URL, searches for Terragrunt modules, indexes their README.* files, and returns module instances.\nfunc (repo *Repo) FindModules(ctx context.Context) (Modules, error) {\n\tvar modules Modules\n\n\t// check if root repo path is a module dir\n\tif module, err := NewModule(repo, \"\"); err != nil {\n\t\treturn nil, err\n\t} else if module != nil {\n\t\tmodules = append(modules, module)\n\t}\n\n\tfor _, modulesPath := range modulesPaths {\n\t\tmodulesPath = filepath.Join(repo.path, modulesPath)\n\n\t\tif !files.FileExists(modulesPath) {\n\t\t\tcontinue\n\t\t}\n\n\t\twalkFunc := filepath.WalkDir\n\t\tif repo.walkWithSymlinks {\n\t\t\twalkFunc = util.WalkDirWithSymlinks\n\t\t}\n\n\t\terr := walkFunc(modulesPath,\n\t\t\tfunc(dir string, d fs.DirEntry, err error) error {\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif !d.IsDir() {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tmoduleDir, err := filepath.Rel(repo.path, dir)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn errors.New(err)\n\t\t\t\t}\n\n\t\t\t\tmoduleDir = filepath.ToSlash(moduleDir)\n\n\t\t\t\tif module, err := NewModule(repo, moduleDir); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t} else if module != nil {\n\t\t\t\t\tmodules = append(modules, module)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn modules, nil\n}\n\nvar githubEnterprisePatternReg = regexp.MustCompile(githubEnterpriseRegex)\nvar gitlabSelfHostedPatternReg = regexp.MustCompile(gitlabSelfHostedRegex)\n\n// ModuleURL returns the URL to view this module in a browser.\n// When the module provided is in a format that is not supported by the catalog, it returns an empty string.\nfunc (repo *Repo) ModuleURL(moduleDir string) string {\n\tif repo.RemoteURL == \"\" {\n\t\treturn filepath.Join(repo.path, moduleDir)\n\t}\n\n\tremote, err := vcsurl.Parse(repo.RemoteURL)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\t// Simple, predictable hosts\n\tswitch remote.Host {\n\tcase githubHost:\n\t\treturn fmt.Sprintf(\"https://%s/%s/tree/%s/%s\", remote.Host, remote.FullName, repo.BranchName, moduleDir)\n\tcase gitlabHost:\n\t\treturn fmt.Sprintf(\"https://%s/%s/-/tree/%s/%s\", remote.Host, remote.FullName, repo.BranchName, moduleDir)\n\tcase bitbucketHost:\n\t\treturn fmt.Sprintf(\"https://%s/%s/browse/%s?at=%s\", remote.Host, remote.FullName, moduleDir, repo.BranchName)\n\tcase azuredevHost:\n\t\treturn fmt.Sprintf(\"https://%s/_git/%s?path=%s&version=GB%s\", remote.Host, remote.FullName, moduleDir, repo.BranchName)\n\t}\n\n\t// // Hosts that require special handling\n\tif githubEnterprisePatternReg.MatchString(string(remote.Host)) {\n\t\treturn fmt.Sprintf(\"https://%s/%s/tree/%s/%s\", remote.Host, remote.FullName, repo.BranchName, moduleDir)\n\t}\n\n\tif gitlabSelfHostedPatternReg.MatchString(string(remote.Host)) {\n\t\treturn fmt.Sprintf(\"https://%s/%s/-/tree/%s/%s\", remote.Host, remote.FullName, repo.BranchName, moduleDir)\n\t}\n\n\treturn \"\"\n}\n\ntype CloneOptions struct {\n\tContext    context.Context\n\tLogger     log.Logger\n\tSourceURL  string\n\tTargetPath string\n}\n\nfunc (repo *Repo) clone(ctx context.Context, l log.Logger) error {\n\tcloneURL := repo.resolveCloneURL()\n\n\t// Handle local directory case\n\tif files.IsDir(cloneURL) {\n\t\treturn repo.handleLocalDir(cloneURL)\n\t}\n\n\t// Prepare clone options\n\topts := CloneOptions{\n\t\tSourceURL:  cloneURL,\n\t\tTargetPath: repo.path,\n\t\tContext:    ctx,\n\t\tLogger:     repo.logger,\n\t}\n\n\tif err := repo.prepareCloneDirectory(); err != nil {\n\t\treturn err\n\t}\n\n\tif repo.cloneCompleted() {\n\t\trepo.logger.Debugf(\"The repo dir exists and %q exists. Skipping cloning.\", cloneCompleteSentinel)\n\n\t\treturn nil\n\t}\n\n\treturn repo.performClone(ctx, l, &opts)\n}\n\nfunc (repo *Repo) resolveCloneURL() string {\n\tif repo.cloneURL == \"\" {\n\t\treturn repo.rootWorkingDir\n\t}\n\n\treturn repo.cloneURL\n}\n\nfunc (repo *Repo) handleLocalDir(repoPath string) error {\n\tif !filepath.IsAbs(repoPath) {\n\t\tabsRepoPath := filepath.Join(repo.rootWorkingDir, repoPath)\n\t\trepo.logger.Debugf(\"Converting relative path %q to absolute %q\", repoPath, absRepoPath)\n\t\trepo.path = absRepoPath\n\n\t\treturn nil\n\t}\n\n\trepo.path = repoPath\n\n\treturn nil\n}\n\nfunc (repo *Repo) prepareCloneDirectory() error {\n\tif err := os.MkdirAll(repo.path, os.ModePerm); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\trepoName := repo.extractRepoName()\n\trepo.path = filepath.Join(repo.path, repoName)\n\n\t// Clean up incomplete clones\n\tif repo.shouldCleanupIncompleteClone() {\n\t\trepo.logger.Debugf(\"The repo dir exists but %q does not. Removing the repo dir for cloning from the remote source.\", cloneCompleteSentinel)\n\n\t\tif err := os.RemoveAll(repo.path); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (repo *Repo) extractRepoName() string {\n\trepoName := \"temp\"\n\tif match := repoNameFromCloneURLReg.FindStringSubmatch(repo.cloneURL); len(match) > 0 && match[1] != \"\" {\n\t\trepoName = match[1]\n\t}\n\n\treturn repoName\n}\n\nfunc (repo *Repo) shouldCleanupIncompleteClone() bool {\n\treturn files.FileExists(repo.path) && !repo.cloneCompleted()\n}\n\nfunc (repo *Repo) cloneCompleted() bool {\n\treturn files.FileExists(filepath.Join(repo.path, cloneCompleteSentinel))\n}\n\nfunc (repo *Repo) performClone(ctx context.Context, l log.Logger, opts *CloneOptions) error {\n\tclient := getter.DefaultClient\n\n\tif repo.allowCAS {\n\t\tc, err := cas.New(cas.Options{})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcloneOpts := cas.CloneOptions{\n\t\t\tDir:              repo.path,\n\t\t\tIncludedGitFiles: includedGitFiles,\n\t\t}\n\n\t\tclient.Getters = append([]getter.Getter{cas.NewCASGetter(l, c, &cloneOpts)}, client.Getters...)\n\t}\n\n\tsourceURL, err := tf.ToSourceURL(opts.SourceURL, \"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trepo.cloneURL = sourceURL.String()\n\topts.Logger.Infof(\"Cloning repository %q to temporary directory %q\", repo.cloneURL, repo.path)\n\n\t// Check first if the query param ref is already set\n\tq := sourceURL.Query()\n\n\tref := q.Get(\"ref\")\n\tif ref == \"\" {\n\t\tq.Set(\"ref\", \"HEAD\")\n\t}\n\n\tsourceURL.RawQuery = q.Encode()\n\n\t_, err = client.Get(ctx, &getter.Request{\n\t\tSrc:     sourceURL.String(),\n\t\tDst:     repo.path,\n\t\tGetMode: getter.ModeDir,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Create the sentinel file to indicate that the clone is complete\n\tf, err := os.Create(filepath.Join(repo.path, cloneCompleteSentinel))\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tif err := f.Close(); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n\n// parseRemoteURL reads the git config `.git/config` and parses the first URL of the remote URLs, the remote name \"origin\" has the highest priority.\nfunc (repo *Repo) parseRemoteURL() error {\n\tgitConfigPath := filepath.Join(repo.path, \".git\", \"config\")\n\n\tif !files.FileExists(gitConfigPath) {\n\t\treturn errors.Errorf(\"the specified path %q is not a git repository (no .git/config file found)\", repo.path)\n\t}\n\n\trepo.logger.Debugf(\"Parsing git config %q\", gitConfigPath)\n\n\tinidata, err := ini.Load(gitConfigPath)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tvar sectionName string\n\n\tfor _, name := range inidata.SectionStrings() {\n\t\tif !strings.HasPrefix(name, \"remote\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tsectionName = name\n\n\t\tif sectionName == `remote \"origin\"` {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// no git remotes found\n\tif sectionName == \"\" {\n\t\treturn nil\n\t}\n\n\trepo.RemoteURL = inidata.Section(sectionName).Key(\"url\").String()\n\trepo.logger.Debugf(\"Remote url: %q for repo: %q\", repo.RemoteURL, repo.path)\n\n\treturn nil\n}\n\nfunc (repo *Repo) gitHeadfile() string {\n\treturn filepath.Join(repo.path, \".git\", \"HEAD\")\n}\n\n// parseBranchName reads `.git/HEAD` file and parses a branch name.\nfunc (repo *Repo) parseBranchName() error {\n\tdata, err := files.ReadFileAsString(repo.gitHeadfile())\n\tif err != nil {\n\t\treturn errors.Errorf(\"the specified path %q is not a git repository (no .git/HEAD file found)\", repo.path)\n\t}\n\n\tif match := gitHeadBranchNameReg.FindStringSubmatch(data); len(match) > 0 {\n\t\trepo.BranchName = strings.TrimSpace(match[1])\n\n\t\treturn nil\n\t}\n\n\treturn errors.Errorf(\"could not get branch name for repo %q\", repo.path)\n}\n"
  },
  {
    "path": "internal/services/catalog/module/repo_test.go",
    "content": "package module_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog/module\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFindModules(t *testing.T) {\n\tt.Parallel()\n\n\ttype moduleData struct {\n\t\ttitle       string\n\t\tdescription string\n\t\turl         string\n\t\tmoduleDir   string\n\t}\n\n\ttestCases := []struct {\n\t\texpectedErr  error\n\t\trepoPath     string\n\t\texpectedData []moduleData\n\t}{\n\t\t{\n\t\t\trepoPath: \"testdata/find_modules\",\n\t\t\texpectedData: []moduleData{\n\t\t\t\t{\n\t\t\t\t\ttitle:       \"ALB Ingress Controller Module\",\n\t\t\t\t\tdescription: \"This Terraform Module installs and configures the AWS ALB Ingress Controller on an EKS cluster, so that you can configure an ALB using Ingress resources.\",\n\t\t\t\t\turl:         \"https://github.com/gruntwork-io/terraform-aws-eks/tree/master/modules/eks-alb-ingress-controller\",\n\t\t\t\t\tmoduleDir:   \"modules/eks-alb-ingress-controller\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttitle:       \"ALB Ingress Controller IAM Policy Module\",\n\t\t\t\t\tdescription: \"This Terraform Module defines an IAM policy that defines the minimal set of permissions necessary for the AWS ALB Ingress Controller.\",\n\t\t\t\t\turl:         \"https://github.com/gruntwork-io/terraform-aws-eks/tree/master/modules/eks-alb-ingress-controller-iam-policy\",\n\t\t\t\t\tmoduleDir:   \"modules/eks-alb-ingress-controller-iam-policy\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttitle:       \"EKS AWS Auth Merger\",\n\t\t\t\t\tdescription: \"This module contains a go CLI, docker container, and terraform module for deploying a Kubernetes controller for managing mappings between AWS IAM roles and users to RBAC groups in Kubernetes.\",\n\t\t\t\t\turl:         \"https://github.com/gruntwork-io/terraform-aws-eks/tree/master/modules/eks-aws-auth-merger\",\n\t\t\t\t\tmoduleDir:   \"modules/eks-aws-auth-merger\",\n\t\t\t\t}},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.repoPath, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\t// Unfortunately, we are unable to commit the `.git` directory. We have to temporarily rename it while running the tests.\n\t\t\tos.Rename(filepath.Join(tc.repoPath, \"gitdir\"), filepath.Join(tc.repoPath, \".git\"))\n\t\t\tdefer os.Rename(filepath.Join(tc.repoPath, \".git\"), filepath.Join(tc.repoPath, \"gitdir\"))\n\n\t\t\tctx := t.Context()\n\n\t\t\trepo, err := module.NewRepo(ctx, logger.CreateLogger(), tc.repoPath, \"\", false, false, \"\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tmodules, err := repo.FindModules(ctx)\n\t\t\tassert.Equal(t, tc.expectedErr, err)\n\n\t\t\trealData := make([]moduleData, 0, len(modules))\n\n\t\t\tfor _, module := range modules {\n\t\t\t\trealData = append(realData, moduleData{\n\t\t\t\t\ttitle:       module.Title(),\n\t\t\t\t\tdescription: module.Description(),\n\t\t\t\t\turl:         module.URL(),\n\t\t\t\t\tmoduleDir:   module.ModuleDir(),\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectedData, realData)\n\t\t})\n\t}\n}\n\nfunc TestModuleURL(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedErr error\n\t\trepo        *module.Repo\n\t\tname        string\n\t\tmoduleDir   string\n\t\texpectedURL string\n\t}{\n\t\t{\n\t\t\tname:        \"github\",\n\t\t\trepo:        newRepo(t, \"https://github.com/acme/terraform-aws-modules\"),\n\t\t\tmoduleDir:   \".\",\n\t\t\texpectedURL: \"https://github.com/acme/terraform-aws-modules/tree/main/.\",\n\t\t},\n\t\t{\n\t\t\tname:        \"github enterprise\",\n\t\t\trepo:        newRepo(t, \"https://github.acme.com/acme/terraform-aws-modules\"),\n\t\t\tmoduleDir:   \".\",\n\t\t\texpectedURL: \"https://github.acme.com/acme/terraform-aws-modules/tree/main/.\",\n\t\t},\n\t\t{\n\t\t\tname:        \"gitlab\",\n\t\t\trepo:        newRepo(t, \"https://gitlab.com/acme/terraform-aws-modules\"),\n\t\t\tmoduleDir:   \".\",\n\t\t\texpectedURL: \"https://gitlab.com/acme/terraform-aws-modules/-/tree/main/.\",\n\t\t},\n\t\t{\n\t\t\tname:        \"gitlab self-hosted\",\n\t\t\trepo:        newRepo(t, \"https://gitlab.acme.com/acme/terraform-aws-modules\"),\n\t\t\tmoduleDir:   \".\",\n\t\t\texpectedURL: \"https://gitlab.acme.com/acme/terraform-aws-modules/-/tree/main/.\",\n\t\t},\n\t\t{\n\t\t\tname:        \"bitbucket\",\n\t\t\trepo:        newRepo(t, \"https://bitbucket.org/acme/terraform-aws-modules\"),\n\t\t\tmoduleDir:   \".\",\n\t\t\texpectedURL: \"https://bitbucket.org/acme/terraform-aws-modules/browse/.?at=main\",\n\t\t},\n\t\t{\n\t\t\tname:        \"azuredev\",\n\t\t\trepo:        newRepo(t, \"https://dev.azure.com/acme/terraform-aws-modules\"),\n\t\t\tmoduleDir:   \".\",\n\t\t\texpectedURL: \"https://dev.azure.com/_git/acme/terraform-aws-modules?path=.&version=GBmain\",\n\t\t},\n\t\t{\n\t\t\tname:        \"unsupported\",\n\t\t\trepo:        newRepo(t, \"https://fake.com/acme/terraform-aws-modules\"),\n\t\t\tmoduleDir:   \".\",\n\t\t\texpectedURL: \"\",\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.Parallel()\n\n\t\t\turl := tc.repo.ModuleURL(tc.moduleDir)\n\t\t\tassert.Equal(t, tc.expectedURL, url)\n\t\t})\n\t}\n}\n\nfunc newRepo(t *testing.T, url string) *module.Repo {\n\tt.Helper()\n\n\treturn &module.Repo{\n\t\tRemoteURL:  url,\n\t\tBranchName: \"main\",\n\t}\n}\n"
  },
  {
    "path": "internal/services/catalog/module/testdata/find_modules/gitdir/HEAD",
    "content": "ref: refs/heads/master\n"
  },
  {
    "path": "internal/services/catalog/module/testdata/find_modules/gitdir/config",
    "content": "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n\tlogallrefupdates = true\n\tignorecase = true\n\tprecomposeunicode = true\n[remote \"origin\"]\n\turl = https://github.com/gruntwork-io/terraform-aws-eks\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n[branch \"master\"]\n\tremote = origin\n\tmerge = refs/heads/master\n"
  },
  {
    "path": "internal/services/catalog/module/testdata/find_modules/modules/eks-alb-ingress-controller/README.md",
    "content": "# ALB Ingress Controller Module\n\nThis Terraform Module installs and configures the [AWS ALB Ingress\nController](https://github.com/kubernetes-sigs/aws-alb-ingress-controller) on an EKS cluster, so that you can configure\nan ALB using [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) resources.\n\nThis module uses the [community helm chart](https://github.com/aws/eks-charts), with a set of best practices input.\n\n#### Note: v2\nWe're now supporting v2 of the AWS Load Balancer Ingress Controller. The AWS Load Balancer Ingress Controller v2 has many new features, and is considered backwards incompatible with the existing AWS resources it manages. Please note, that it can't coexist with the existing/older version, so you must fully undeploy the old version prior to updating. For the migration steps, please refer to the [relevant Release notes for this module](https://github.com/gruntwork-io/terraform-aws-eks/releases/tag/v0.28.0).\n\n## How does this work?\n\nThis module solves the problem of integrating Kubernetes `Service` endpoints with an\n[ALB](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html). Out of the box Kubernetes\nsupports tying [a `Service` to an ELB or NLB using the `LoadBalancer`\ntype](https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/). However, the\n`LoadBalancer` `Service` type does not support ALBs, and thus you can not implement complex routing rules based on\ndomain or paths.\n\nKubernetes uses `Ingress` resources to configure and implement \"Layer 7\" load balancers (where ALBs fit in the [OSI\nmodel](https://en.wikipedia.org/wiki/OSI_model#Layer_7:_Application_Layer)). Kubernetes `Ingress` works by providing a\nconfiguration framework to configure routing rules from a load balancer to `Services` within Kubernetes. For example,\nsuppose you wanted to provision a `Service` for your backend, fronted by a load balancer that routes any request made to\nthe path `/service` to the backend. To do so, in addition to creating your `Service`, you would create an `Ingress`\nresource in Kubernetes that configures the routing rule:\n\n```yaml\n---\nkind: Service\napiVersion: v1\nmetadata:\n  name: backend\nspec:\n  selector:\n    app: backend\n  ports:\n  - protocol: TCP\n    port: 80\n    targetPort: 80\n---\napiVersion: extensions/v1beta1\nkind: Ingress\nmetadata:\n  name: service-ingress\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /service\n        backend:\n          serviceName: backend\n          servicePort: 80\n```\n\nIn the above configuration, we create a Cluster IP based `Service` (so that it is only available internally to the\nKubernetes cluster) that routes requests to port 80 to any `Pod` that matches the label `app=backend` on port 80. Then,\nwe configure an `Ingress` rule that routes any requests prefixed with `/service` to that `Service` endpoint on port 80.\n\nThe actual load balancer that is configured by the `Ingress` resource is defined by the particular [Ingress\nController](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/) that you deploy onto your\nKubernetes cluster. Ingress Controllers are separate processes that run on your Kubernetes cluster that will watch for\n`Ingress` resources and reflect them by provisioning or configuring load balancers. Depending on which controller you\nuse, the particular load balancer that is provisioned will be different. For example, if you use the [official nginx\ncontroller](https://github.com/kubernetes/ingress-nginx/blob/e222b74/README.md), each `Ingress` resource translates into\nan nginx `Pod` that implements the routing rules.\n\nNote that each `Ingress` resource defines a separate load balancer. This means that each time you create a new `Ingress`\nresource in Kubernetes, Kubernetes will provision a new load balancer configured with the rules defined by the `Ingress`\nresource.\n\nThis module deploys the AWS ALB Ingress Controller, which will reflect each `Ingress` resource into an ALB resource\ndeployed into your AWS account.\n\n## How do you use this module?\n\n* See the [root README](/README.adoc) for instructions on using Terraform modules.\n* See the [eks-cluster-with-supporting-services example](/examples/eks-cluster-with-supporting-services) for example\n  usage.\n* See [variables.tf](./variables.tf) for all the variables you can set on this module.\n* This module uses [the `kubernetes` provider](https://www.terraform.io/docs/providers/kubernetes/index.html).\n* This module uses [the `helm` provider](https://www.terraform.io/docs/providers/helm/index.html).\n\n## Prerequisites\n\n### Helm setup\n\nThis module uses [`helm` v3](https://helm.sh/docs/) to deploy the controller to the Kubernetes cluster.\n\n### ALB Target Type\n\nThe ALB Ingress Controller application can configure ALBs to send work either to Node IPs (`instance`) or Pod IPs (`ip`) as backend targets. This can be specified in the Ingress object using the [`alb.ingress.kubernetes.io/target-type`](https://kubernetes-sigs.github.io/aws-alb-ingress-controller/guide/ingress/annotation/#target-type). The default is `instance`.\n\nWhen using the default `instance` target type, the `Services` intended to be consumed by the `Ingress` resource must be\nprovisioned using the `NodePort` type. This is not required when using the `ip` target type.\n\nNote that the controller will take care of setting up the target groups on the provisioned ALB so that everything routes\ncorrectly.\n\n### Subnets\n\nYou can use the `alb.ingress.kubernetes.io/subnets` annotation on `Ingress` resources to specify which subnets the controller should configure the ALB for.\n\nYou can also omit the `alb.ingress.kubernetes.io/subnets` annotation, and the controller will [automatically discover subnets](https://kubernetes-sigs.github.io/aws-alb-ingress-controller/guide/controller/config/#subnet-auto-discovery) based on their tags. This method should work \"out of the box\", so long as you are using the [`eks-vpc-tags`](../eks-vpc-tags) module to tag your VPC subnets.\n\n### Security Groups\n\nAs mentioned above under the [ALB Target Type](#alb-target-type) section, the default ALB target type uses node ports to connect to the\n`Services`. As such if you have restricted security groups that prevent access to the provisioned ports on the worker\nnodes, the ALBs will not be able to reach the `Services`.\n\nTo ensure the provisioned ALBs can access the node ports, we recommend using dedicated subnets for load balancing and\nconfiguring your security groups so that resources provisioned in those subnets can access the node ports of the worker\nnodes.\n\n### IAM permissions\n\nThe container deployed in this module requires IAM permissions to manage ALB resources. See [the\neks-alb-ingress-controller-iam-policy module](../eks-alb-ingress-controller-iam-policy) for more information.\n\n## Using the Ingress Controller\n\nIn order for the `Ingress` resources to properly map into an ALB, the `Ingress` resources created need to be annotated\nto use the `alb` `Ingress` class. You can do this by adding the following annotation to your `Ingress` resources:\n\n```yaml\nannotations:\n  kubernetes.io/ingress.class: alb\n```\n\nThe ALB Ingress Controller supports a wide range of configuration options via annotations on the `Ingress` object, including setting up Cognito for\nauthentication. For example, you can add the annotation `alb.ingress.kubernetes.io/scheme: internet-facing` to provision\na public ALB. You can refer to the [official\ndocumentation](https://kubernetes-sigs.github.io/aws-alb-ingress-controller/guide/ingress/annotation/) for the full\nreference of configuration options supported by the controller.\n\n## Getting the ALB endpoint\n\nThe ALB endpoint is recorded on the `Ingress` resource. You can use `kubectl` or the Kubernetes API to retrieve the\n`Ingress` resource and view the endpoint for the ALB under the `Address` attribute.\n\nFor example, suppose you provisioned the following `Ingress` resource in the default namespace:\n\n```yaml\n---\napiVersion: extensions/v1beta1\nkind: Ingress\nmetadata:\n  name: service-ingress\n  annotations:\n    kubernetes.io/ingress.class: alb\nspec:\n  rules:\n  - http:\n      paths:\n      - path: /service\n        backend:\n          serviceName: backend\n          servicePort: 80\n```\n\nTo get the ALB endpoint, call `kubectl` to describe the `Ingress` resource:\n\n```\n$ kubectl describe ing service-ingress\nName:                   service-ingress\nNamespace:              default\nAddress:                QZVpvauzhSuRBRMfjAGnbgaCaLeANaoe.us-east-2.elb.amazonaws.com\nDefault backend:        default-http-backend:80 (10.2.1.28:8080)\nRules:\n  Host                          Path      Backends\n  ----                          ----      --------\n                                /service  backend:80 (<none>)\nAnnotations:\nEvents:\n  FirstSeen     LastSeen        Count   From                    SubObjectPath   Type            Reason  Message\n  ---------     --------        -----   ----                    -------------   --------        ------  -------\n  3m            3m              1       ingress-controller                      Normal          CREATE  Ingress service-ingress/backend\n  3m            32s             3       ingress-controller                      Normal          UPDATE  Ingress service-ingress/backend\n```\n\nNote how the ALB endpoint is recorded under the `Address` column. You can hit that endpoint to access the service\nexternally.\n\n## DNS records for the ALB\n\nIn order for the host based routing rules to work with the ALB, you need to configure your DNS records to point to the\nALB endpoint. This can be tricky if you are managing your DNS records externally, especially given the asynchronous\nnature of the controller in provisioning the ALBs.\n\nThe AWS ALB Ingress Controller has first class support for\n[external-dns](https://github.com/kubernetes-incubator/external-dns), a third party tool that configures external DNS\nproviders with domains to route to `Services` and `Ingresses` in Kubernetes. See our [eks-k8s-external-dns\nmodule](../eks-k8s-external-dns) for more information on how to setup the tool.\n\n\n## How do I deploy the Pods to Fargate?\n\nTo deploy the Pods to Fargate, you can use the `create_fargate_profile` variable to `true` and specify the subnet IDs\nfor Fargate using `vpc_worker_subnet_ids`. Note that if you are using Fargate, you must rely on the IAM Roles for\nService Accounts (IRSA) feature to grant the necessary AWS IAM permissions to the Pod. This is configured using the\n`use_iam_role_for_service_accounts`, `eks_openid_connect_provider_arn`, and `eks_openid_connect_provider_url` input\nvariables.\n\n\n## How does the ALB route to Fargate?\n\nFor Pods deployed to Fargate, you must specify the annotation\n\n```\nalb.ingress.kubernetes.io/target-type: ip\n```\n\nto the Ingress resource in order for the ALB to route properly. This is because Fargate does not have actual EC2\ninstances under the hood, and thus the ALB can not be configured to route by instance (the default configuration).\n"
  },
  {
    "path": "internal/services/catalog/module/testdata/find_modules/modules/eks-alb-ingress-controller/main.tf",
    "content": ""
  },
  {
    "path": "internal/services/catalog/module/testdata/find_modules/modules/eks-alb-ingress-controller/variables.tf",
    "content": ""
  },
  {
    "path": "internal/services/catalog/module/testdata/find_modules/modules/eks-alb-ingress-controller-iam-policy/README.md",
    "content": "# ALB Ingress Controller IAM Policy Module\n\nThis Terraform Module defines an [IAM\npolicy](http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/QuickStartEC2Instance.html#d0e22325) that\ndefines the minimal set of permissions necessary for the [AWS ALB Ingress\nController](https://github.com/kubernetes-sigs/aws-alb-ingress-controller). This policy can then be attached to EC2\ninstances or IAM roles so that the controller deployed has enough permissions to manage an ALB.\n\nSee [the eks-alb-ingress-controller module](/modules/eks-alb-ingress-controller) for a module that deploys the Ingress\nController on to your EKS cluster.\n\n\n## How do you use this module?\n\n* See the [root README](/README.adoc) for instructions on using Terraform modules.\n* See the [eks-cluster-with-supporting-services example](/examples/eks-cluster-with-supporting-services) for example\n  usage.\n* See [variables.tf](./variables.tf) for all the variables you can set on this module.\n* See [outputs.tf](./outputs.tf) for all the variables that are outputted by this module.\n\n\n## Attaching IAM policy to workers\n\nTo allow the ALB Ingress Controller to manage ALBs, it needs IAM permissions to use the AWS API to manage ALBs.\nCurrently, the way to grant Pods IAM privileges is to use the worker IAM profiles provisioned by [the\neks-cluster-workers module](/modules/eks-cluster-workers/README.md#how-do-you-add-additional-iam-policies).\n\nThe Terraform templates in this module create an IAM policy that has the required permissions. You then need to use an\n[aws_iam_policy_attachment](https://www.terraform.io/docs/providers/aws/r/iam_policy_attachment.html) to attach that\npolicy to the IAM roles of your EC2 Instances.\n\n```hcl\nmodule \"eks_workers\" {\n  # (arguments omitted)\n}\n\nmodule \"alb_ingress_controller_iam_policy\" {\n  # (arguments omitted)\n}\n\nresource \"aws_iam_role_policy_attachment\" \"attach_alb_ingress_controller_iam_policy\" {\n    role = \"${module.eks_workers.eks_worker_iam_role_name}\"\n    policy_arn = \"${module.alb_ingress_controller_iam_policy.alb_ingress_controller_policy_arn}\"\n}\n```\n"
  },
  {
    "path": "internal/services/catalog/module/testdata/find_modules/modules/eks-alb-ingress-controller-iam-policy/main.tf",
    "content": ""
  },
  {
    "path": "internal/services/catalog/module/testdata/find_modules/modules/eks-alb-ingress-controller-iam-policy/variables.tf",
    "content": ""
  },
  {
    "path": "internal/services/catalog/module/testdata/find_modules/modules/eks-aws-auth-merger/README.adoc",
    "content": ":type: service\n:name: EKS AWS Auth Merger\n:description: Manage the aws-auth ConfigMap across multiple independent ConfigMaps.\n:icon: /_docs/iam-role-icon.png\n:category: docker-orchestration\n:cloud: aws\n:tags: docker, orchestration, kubernetes, containers\n:license: gruntwork\n:built-with: go, terraform\n\n// AsciiDoc TOC settings\n:toc:\n:toc-placement!:\n:toc-title:\n\n// GitHub specific settings. See https://gist.github.com/dcode/0cfbf2699a1fe9b46ff04c41721dda74 for details.\nifdef::env-github[]\n:tip-caption: :bulb:\n:note-caption: :information_source:\n:important-caption: :heavy_exclamation_mark:\n:caution-caption: :fire:\n:warning-caption: :warning:\nendif::[]\n\n= EKS AWS Auth Merger\n\nimage:https://img.shields.io/badge/maintained%20by-gruntwork.io-%235849a6.svg[link=\"https://gruntwork.io/?ref=repo_aws_eks\"]\nimage:https://img.shields.io/badge/tf-%3E%3D1.1.0-blue[Terraform version]\nimage:https://img.shields.io/badge/k8s-1.24%20~%201.28-5dbcd2[K8s version]\n\nThis module contains a go CLI, docker container, and terraform module for deploying a Kubernetes controller for managing\nmappings between AWS IAM roles and users to RBAC groups in Kubernetes. The official way to manage the mapping is to add\nvalues in a single, central `ConfigMap`. This module allows you to break up the central `ConfigMap` across multiple,\nseparate `ConfigMaps` each configuring a subset of the mappings you ultimately want to use, allowing you to update\nentries in the `ConfigMap` in isolated modules (e.g., when you add a new IAM role in a separate module from the EKS\ncluster). The `aws-auth-merger` watches for `aws-auth` compatible `ConfigMaps` that can be merged to manage the\n`aws-auth` authentication `ConfigMap` for EKS.\n\n\ntoc::[]\n\n\n\n\n== Features\n\n* Break up the `aws-auth` Kubernetes `ConfigMap` across multiple objects.\n* Automatically merge new `ConfigMaps` as they are added and removed.\n* Track automatically generated `aws-auth` source `ConfigMaps` that are generated by EKS.\n\n\n\n== Learn\n\nNOTE: This repo is a part of https://gruntwork.io/infrastructure-as-code-library/[the Gruntwork Infrastructure as Code\nLibrary], a collection of reusable, battle-tested, production ready infrastructure code. If you've never used the Infrastructure as Code Library before, make sure to read https://gruntwork.io/guides/foundations/how-to-use-gruntwork-infrastructure-as-code-library/[How to use the Gruntwork Infrastructure as Code Library]!\n\n=== Core concepts\n\n* _link:/modules/eks-k8s-role-mapping/README.md#what-is-kubernetes-role-based-access-control-rbac[What is Kubernetes\n  RBAC?]_: overview of Kubernetes RBAC, the underlying system managing authentication and authorization in Kubernetes.\n\n* _link:/modules/eks-k8s-role-mapping/README.md#what-is-aws-iam-role[What is AWS IAM role?]_: overview of AWS IAM Roles,\n  the underlying system managing authentication and authorization in AWS.\n\n* _https://docs.aws.amazon.com/eks/latest/userguide/add-user-role.html[Managing users or IAM roles for your cluster]_:\n  The official AWS docs on how the `aws-auth` Kubernetes `ConfigMap` works.\n\n* _link:core-concepts.md#what-is-the-aws-auth-merger[What is the aws-auth-merger?]_: overview of the `aws-auth-merger`\n  and how it works to manage the `aws-auth` Kubernetes `ConfigMap`.\n\n\n=== Repo organization\n\n* link:/modules[modules]: the main implementation code for this repo, broken down into multiple standalone, orthogonal submodules.\n* link:/examples[examples]: This folder contains working examples of how to use the submodules.\n* link:/test[test]: Automated tests for the modules and examples.\n\n\n== Deploy\n\n=== Non-production deployment (quick start for learning)\n\nIf you just want to try this repo out for experimenting and learning, check out the following resources:\n\n* link:/examples[examples folder]: The `examples` folder contains sample code optimized for learning, experimenting, and testing (but not production usage).\n\n=== Production deployment\n\nIf you want to deploy this repo in production, check out the following resources:\n\n* https://gruntwork.io/guides/kubernetes/how-to-deploy-production-grade-kubernetes-cluster-aws/#deployment_walkthrough[How to deploy a production-grade Kubernetes cluster on AWS]: A step-by-step guide for deploying a production-grade EKS cluster on AWS using the code in this repo.\n\n**EKS Cluster**: Production-ready example code from the Reference Architecture:\n* https://github.com/gruntwork-io/terraform-aws-service-catalog/blob/main/examples/for-production/infrastructure-live/prod/us-west-2/prod/services/eks-cluster/terragrunt.hcl[app account configuration]\n* https://github.com/gruntwork-io/terraform-aws-service-catalog/blob/main/examples/for-production/infrastructure-live/_envcommon/services/eks-cluster.hcl[base configuration]\n\n\n\n\n== Manage\n\n* link:core-concepts.md#how-do-i-use-the-aws-auth-merger[How to deploy and use the aws-auth-merger]\n* link:core-concepts.md#how-do-i-handle-conflicts-with-automatic-updates-by-eks[How to handle conflicts with automatic\n  updates to the aws-auth ConfigMap by EKS]\n* link:/modules/eks-k8s-role-mapping/README.md#restricting-specific-actions[How to restrict users to specific actions on the EKS cluster]\n* link:/modules/eks-k8s-role-mapping/README.md#restricting-by-namespace[How to restrict users to specific namespaces on the EKS cluster]\n* link:/core-concepts.md#how-to-authenticate-kubectl[How to authenticate kubectl to EKS]\n\n\n\n\n== Support\n\nIf you need help with this repo or anything else related to infrastructure or DevOps, Gruntwork offers https://gruntwork.io/support/[Commercial Support] via Slack, email, and phone/video. If you're already a Gruntwork customer, hop on Slack and ask away! If not, https://www.gruntwork.io/pricing/[subscribe now]. If you're not sure, feel free to email us at link:mailto:support@gruntwork.io[support@gruntwork.io].\n\n\n\n\n== Contributions\n\nContributions to this repo are very welcome and appreciated! If you find a bug or want to add a new feature or even contribute an entirely new module, we are very happy to accept pull requests, provide feedback, and run your changes through our automated test suite.\n\nPlease see https://gruntwork.io/guides/foundations/how-to-use-gruntwork-infrastructure-as-code-library/#contributing-to-the-gruntwork-infrastructure-as-code-library[Contributing to the Gruntwork Infrastructure as Code Library] for instructions.\n\n\n\n\n== License\n\nPlease see link:LICENSE.md[LICENSE.md] for details on how the code in this repo is licensed.\n"
  },
  {
    "path": "internal/services/catalog/module/testdata/find_modules/modules/eks-aws-auth-merger/core-concepts.md",
    "content": "## What is the aws-auth-merger?\n\nThe `aws-auth-merger` is a go CLI intended to be run inside a Pod in an EKS cluster (as opposed to a CLI tool used by the\noperator) for managing mappings between AWS IAM roles and users to RBAC groups in Kubernetes, and is an alternative to\n[the official way AWS recommends managing the\nmappings](https://docs.aws.amazon.com/eks/latest/userguide/add-user-role.html).\nThe official way to manage the mapping is to add values in a single, central `ConfigMap`. This central `ConfigMap` has a\nfew challenges:\n\n- The updates are not managed as code if you are manually updating the `ConfigMap`. This can be a problem when you want\n  to spin up a new cluster with the same configuration, as you now have to download the `ConfigMap` and replicate it\n  into the new cluster.\n\n- The [eks-k8s-role-mapping module](../eks-k8s-role-mapping) allows you to manage the central `ConfigMap` as code.\n  However, EKS will create the `ConfigMap` under certain conditions (e.g. to allow access to Fargate), and depending on\n  timing, you can end up with an error where terraform is not able to create the `ConfigMap` until you import it into\n  the state.\n\n- A single typo or mistake can disable the entire `ConfigMap`. For example, if you have a syntactic yaml error in the\n  central `ConfigMap`, it will prevent EKS from being able to read the `ConfigMap`, thereby disabling access to all\n  the users captured in the `ConfigMap`.\n\nThe `aws-auth-merger` can be used to address these challenges by breaking up the central `ConfigMap` across multiple\n`ConfigMaps` that are tracked in a separate place. The `aws-auth-merger` watches for `aws-auth` compatible `ConfigMaps`\nthat can be merged to manage the `aws-auth` authentication `ConfigMap` for EKS.\n\nThe `aws-auth-merger` works as follows:\n\n- When starting up, the `aws-auth-merger` will scan if the main `aws-auth` `ConfigMap` already exists in the\n  `kube-system` namespace. The `aws-auth-merger` checks if the `ConfigMap` was created by the merger, and if not, will\n  snapshot the `ConfigMap` so that it will be included in the merge.\n- The `aws-auth-merger` then does an initial merger of all the `ConfigMaps` in the configured namespace to create the\n  initial version of the main `aws-auth` `ConfigMap`.\n- The `aws-auth-merger` then enters an infinite event loop that watches for changes to the `ConfigMaps` in the\n  configured namespace. The syncing routine will run every time the merger detects changes in the namespace.\n\n## How do I use the aws-auth-merger?\n\nTo deploy the `aws-auth-merger`, follow the following steps:\n\n1. Create a docker repository to house the `aws-auth-merger`. We recommend using ECR.\n1. Build a Docker image that runs the `aws-auth-merger` and push the container to ECR.\n1. Deploy this module using terraform:\n    1. Set the `aws_auth_merger_image` variable to point to the ECR repo and tag for the `aws-auth-merger` docker image.\n    1. Set additional variables as needed.\n\nIf you wish to manually deploy the `aws-auth-merger` without using Terraform, you can deploy a `Deployment` with a\nsingle replica using the image. The `ServiceAccount` that you associate with the `Pods` in the `Deployment` needs to be\nable to:\n\n- `get`, `list`, `create`, and `watch` for `ConfigMaps` in the namespace that it is watching.\n- `get`, `create`, and `update` the `aws-auth` `ConfigMap` in the `kube-system`.\n\nOnce the `aws-auth-merger` is deployed, you can create `ConfigMaps` in the watched namespace that mimic the `aws-auth`\n`ConfigMap`. Refer to [the official AWS docs](https://docs.aws.amazon.com/eks/latest/userguide/add-user-role.html) for\nmore information on the format of the `aws-auth` `ConfigMap`.\n\nFor convenience, you can use the [eks-k8s-role-mapping](../eks-k8s-role-mapping) module to manage each individual\n`aws-auth` `ConfigMap` to be merged by the merger. Refer to the [eks-cluster-with-iam-role-mappings\nexample](/example/eks-cluster-with-iam-role-mappings) for an example of how to integrate the two modules.\n\n## How do I handle conflicts with automatic updates by EKS?\n\nEKS will automatically update or create the central `aws-auth` `ConfigMap`. This can lead to conflicts with the\n`aws-auth-merger`, including potential data loss that locks out Fargate or Managed Node Group workers. To handle these\nconflicts, we recommend the following approach:\n\n- If you are using Fargate for the Control Plane components (e.g. CoreDNS) or for the `aws-auth-merger` itself, ensure\n  that the relevant Fargate Profiles are created prior to the initial deployment of the `aws-auth-merger`. This ensures\n  that AWS constructs the `aws-auth` `ConfigMap` before the `aws-auth-merger` comes online, allowing it to snapshot the\n  existing `ConfigMap` to be merged in to the managed central `ConfigMap`.\n\n- If you are using Fargate outside of the `aws-auth-merger`, ensure that you create the Fargate Profile after the\n  `aws-auth-merger` is deployed. Then, create an `aws-auth` `ConfigMap` in the merger namespace that includes the\n  Fargate execution role (the input variable `eks_fargate_profile_executor_iam_role_arns` in the\n  `eks-k8s-role-mapping` module). This ensures that the Fargate execution role is included in the merged `ConfigMap`.\n\n- If you are using Managed Node Groups, you have two options:\n    - Ensure that the Managed Node Group is created prior to the `aws-auth-merger` being deployed. This ensures that AWS\n      constructs the `aws-auth` `ConfigMap` before the `aws-auth-merger` comes online.\n    - If you wish to create Managed Node Groups after the `aws-auth-merger` is deployed, ensure that the worker IAM role\n      of the Managed Node Group is included in an `aws-auth` `ConfigMap` in the merger namespace (the input variable\n      `eks_worker_iam_role_arns`).\n"
  },
  {
    "path": "internal/services/catalog/module/testdata/find_modules/modules/eks-aws-auth-merger/main.tf",
    "content": ""
  },
  {
    "path": "internal/services/catalog/module/testdata/find_modules/modules/eks-aws-auth-merger/variables.tf",
    "content": ""
  },
  {
    "path": "internal/shell/error_explainer.go",
    "content": "package shell\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\n\t\"github.com/gruntwork-io/go-commons/collections\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\n// terraformErrorsMatcher List of errors that we know how to explain to the user. The key is a regex that matches the error message, and the value is the explanation.\nvar terraformErrorsMatcher = map[string]string{\n\t\"(?s).*Error refreshing state: AccessDenied: Access Denied(?s).*\":                     \"You don't have access to the S3 bucket where the state is stored. Check your credentials and permissions.\",\n\t\"(?s).*AllAccessDisabled: All access to this object has been disabled(?s).*\":          \"You don't have access to the S3 bucket where the state is stored. Check your credentials and permissions.\",\n\t\"(?s).*operation error S3: ListObjectsV2, https response error StatusCode: 301(?s).*\": \"You don't have access to the S3 bucket where the state is stored. Check your credentials and permissions.\",\n\t\"(?s).*The authorization header is malformed(?s).*\":                                   \"You don't have access to the S3 bucket where the state is stored. Check your credentials and permissions.\",\n\t\"(?s).*Unable to list objects in S3 bucket(?s).*\":                                     \"You don't have access to the S3 bucket where the state is stored. Check your credentials and permissions.\",\n\t\"(?s).*Error: Initialization required(?s).*\":                                          \"You need to run terragrunt (run --all) init to initialize working directory.\",\n\t\"(?s).*Unit source has changed(?s).*\":                                                 \"You need to run terragrunt (run --all) init install all required modules.\",\n\t\"(?s).*Error finding AWS credentials(?s).*\":                                           \"Missing AWS credentials. Provide credentials to proceed.\",\n\t\"(?s).*Error: No valid credential sources found(?s).*\":                                \"Missing AWS credentials. Provide credentials to proceed.\",\n\t\"(?s).*Error: validating provider credentials(?s).*\":                                  \"Missing AWS credentials. Provide credentials to proceed.\",\n\t\"(?s).*NoCredentialProviders(?s).*\":                                                   \"Missing AWS credentials. Provide credentials to proceed.\",\n\t\"(?s).*client: no valid credential sources(?s).*\":                                     \"Missing AWS credentials. Provide credentials to proceed.\",\n\t\"(?s).*exec: \\\"(tofu|terraform)\\\": executable file not found(?s).*\":                   \"The executables 'terraform' and 'tofu' are missing from your $PATH. Please add at least one of these to your $PATH.\",\n\t\"(?s).*bucket must have been previously created.*\":                                    \"Remote state bucket not found, create it manually or rerun with --backend-bootstrap to provision automatically.\",\n\t\"(?s).*specified bucket does not exist.*\":                                             \"Remote state bucket not found, create it manually or rerun with --backend-bootstrap to provision automatically.\",\n\t\"(?s).*S3 bucket does not exist.*\":                                                    \"Remote state bucket not found, create it manually or rerun with --backend-bootstrap to provision automatically.\",\n}\n\n// ExplainError will try to explain the error to the user, if we know how to do so.\nfunc ExplainError(err error) string {\n\texplanations := map[string]string{}\n\n\t// iterate over each error, unwrap it, and check for error output\n\tfor _, err := range errors.UnwrapErrors(err) {\n\t\tmessage := err.Error()\n\n\t\t// extract process output, if it is the case\n\t\tvar processError util.ProcessExecutionError\n\t\tif ok := errors.As(err, &processError); ok {\n\t\t\terrorOutput := processError.Output.Stderr.String()\n\t\t\tstdOut := processError.Output.Stdout.String()\n\t\t\tmessage = fmt.Sprintf(\"%s\\n%s\", stdOut, errorOutput)\n\t\t}\n\n\t\tfor regex, explanation := range terraformErrorsMatcher {\n\t\t\tif match, _ := regexp.MatchString(regex, message); match {\n\t\t\t\t// collect matched explanations\n\t\t\t\texplanations[explanation] = \"1\"\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strings.Join(collections.Keys(explanations), \"\\n\")\n}\n"
  },
  {
    "path": "internal/shell/error_explainer_test.go",
    "content": "package shell_test\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestExplainError(t *testing.T) {\n\tt.Parallel()\n\n\tvar testCases = []struct {\n\t\terrorOutput string\n\t\texplanation string\n\t}{\n\t\t{\n\t\t\terrorOutput: \"Error refreshing state: AccessDenied: Access Denied\",\n\t\t\texplanation: \"Check your credentials and permissions\",\n\t\t},\n\t\t{\n\t\t\terrorOutput: \"Error: Initialization required\",\n\t\t\texplanation: \"You need to run terragrunt (run --all) init to initialize working directory\",\n\t\t},\n\t\t{\n\t\t\terrorOutput: \"Unit source has changed\",\n\t\t\texplanation: \"You need to run terragrunt (run --all) init install all required modules\",\n\t\t},\n\t\t{\n\t\t\terrorOutput: \"Error: Failed to get existing workspaces: Unable to list objects in S3 bucket \\\"mybucket\\\": operation error S3: ListObjectsV2, https response error StatusCode: 301, RequestID: GH67DSB7KB8H578N, HostID: vofohiXBwNhR8Im+Dj7RpUPCPnOq9IDfn1rsUHHCzN9HgVMFfuIH5epndgLQvDeJPz2DrlUh0tA=, requested bucket from \\\"us-east-1\\\", actual location \\\"eu-west-1\\\"\\n\",\n\t\t\texplanation: \"You don't have access to the S3 bucket where the state is stored. Check your credentials and permissions.\",\n\t\t},\n\t\t{\n\t\t\terrorOutput: \"exec: \\\"tofu\\\": executable file not found in $PATH\",\n\t\t\texplanation: \"The executables 'terraform' and 'tofu' are missing from your $PATH. Please add at least one of these to your $PATH.\",\n\t\t},\n\t}\n\n\tfor _, tt := range testCases {\n\t\tt.Run(tt.errorOutput, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\toutput := util.CmdOutput{}\n\t\t\toutput.Stderr = *bytes.NewBufferString(tt.errorOutput)\n\n\t\t\terrs := new(errors.MultiError)\n\t\t\terrs = errs.Append(util.ProcessExecutionError{\n\t\t\t\tErr:    errors.New(\"\"),\n\t\t\t\tOutput: output,\n\t\t\t})\n\t\t\texplanation := shell.ExplainError(errs)\n\t\t\tassert.Contains(t, explanation, tt.explanation)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/shell/git.go",
    "content": "package shell\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cache\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/writer\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/hashicorp/go-version\"\n)\n\nconst (\n\tgitPrefix = \"git::\"\n\trefsTags  = \"refs/tags/\"\n\n\ttagSplitPart = 2\n)\n\n// GitTopLevelDir fetches git repository path from passed directory.\nfunc GitTopLevelDir(ctx context.Context, l log.Logger, env map[string]string, path string) (string, error) {\n\trunCache := cache.ContextCache[string](ctx, cache.RunCmdCacheContextKey)\n\tcacheKey := \"top-level-dir-\" + path\n\n\tif gitTopLevelDir, found := runCache.Get(ctx, cacheKey); found {\n\t\treturn gitTopLevelDir, nil\n\t}\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\tgitRunOpts := &ShellOptions{\n\t\tWriters:    writer.Writers{Writer: &stdout, ErrWriter: &stderr},\n\t\tWorkingDir: path,\n\t\tEnv:        env,\n\t}\n\n\tcmd, err := RunCommandWithOutput(ctx, l, gitRunOpts, path, true, false, \"git\", \"rev-parse\", \"--show-toplevel\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcmdOutput := strings.TrimSpace(cmd.Stdout.String())\n\n\tif stderrString := strings.TrimSpace(stderr.String()); stderrString != \"\" {\n\t\tl.Warnf(\"git rev-parse --show-toplevel resulted in stderr output: \\n%v\\n\", stderrString)\n\t}\n\n\tl.Debugf(\"git show-toplevel result: %s\", cmdOutput)\n\n\trunCache.Put(ctx, cacheKey, cmdOutput)\n\n\treturn cmdOutput, nil\n}\n\n// GitRepoTags fetches git repository tags from passed url.\nfunc GitRepoTags(ctx context.Context, l log.Logger, env map[string]string, workingDir string, gitRepo *url.URL) ([]string, error) {\n\trepoPath := gitRepo.String()\n\t// remove git:: part if present\n\trepoPath = strings.TrimPrefix(repoPath, gitPrefix)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\tgitRunOpts := &ShellOptions{\n\t\tWriters:    writer.Writers{Writer: &stdout, ErrWriter: &stderr},\n\t\tWorkingDir: workingDir,\n\t\tEnv:        env,\n\t}\n\n\toutput, err := RunCommandWithOutput(ctx, l, gitRunOpts, workingDir, true, false, \"git\", \"ls-remote\", \"--tags\", repoPath)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tvar tags []string\n\n\ttagLines := strings.SplitSeq(output.Stdout.String(), \"\\n\")\n\n\tfor line := range tagLines {\n\t\tfields := strings.Fields(line)\n\t\tif len(fields) >= tagSplitPart {\n\t\t\ttags = append(tags, fields[1])\n\t\t}\n\t}\n\n\treturn tags, nil\n}\n\n// GitLastReleaseTag fetches git repository last release tag.\nfunc GitLastReleaseTag(ctx context.Context, l log.Logger, env map[string]string, workingDir string, gitRepo *url.URL) (string, error) {\n\ttags, err := GitRepoTags(ctx, l, env, workingDir, gitRepo)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif len(tags) == 0 {\n\t\treturn \"\", nil\n\t}\n\n\treturn LastReleaseTag(tags), nil\n}\n\n// LastReleaseTag returns last release tag from passed tags slice.\nfunc LastReleaseTag(tags []string) string {\n\tsemverTags := extractSemVerTags(tags)\n\tif len(semverTags) == 0 {\n\t\treturn \"\"\n\t}\n\t// find last semver tag\n\tlastVersion := semverTags[0]\n\tfor _, ver := range semverTags {\n\t\tif ver.GreaterThanOrEqual(lastVersion) {\n\t\t\tlastVersion = ver\n\t\t}\n\t}\n\n\treturn lastVersion.Original()\n}\n\n// extractSemVerTags - extract semver tags from passed tags slice.\nfunc extractSemVerTags(tags []string) []*version.Version {\n\tvar semverTags []*version.Version\n\n\tfor _, tag := range tags {\n\t\tt := strings.TrimPrefix(tag, refsTags)\n\t\tif v, err := version.NewVersion(t); err == nil {\n\t\t\t// consider only semver tags\n\t\t\tsemverTags = append(semverTags, v)\n\t\t}\n\t}\n\n\treturn semverTags\n}\n"
  },
  {
    "path": "internal/shell/prompt.go",
    "content": "package shell\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/os/exec\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// PromptUserForInput prompts the user for text in the CLI. Returns the text entered by the user.\nfunc PromptUserForInput(ctx context.Context, l log.Logger, prompt string, nonInteractive bool, errWriter io.Writer) (string, error) {\n\t// We are writing directly to ErrWriter so the prompt is always visible\n\t// no matter what logLevel is configured. If `--non-interactive` is set, we log both prompt and\n\t// a message about assuming `yes` to Debug, so\n\tif nonInteractive {\n\t\tl.Debugf(\"%s\", prompt)\n\t\tl.Debugf(\"The non-interactive flag is set to true, so assuming 'yes' for all prompts\")\n\n\t\treturn \"yes\", nil\n\t}\n\n\tn, err := errWriter.Write([]byte(prompt))\n\tif err != nil {\n\t\tl.Error(err)\n\n\t\treturn \"\", errors.New(err)\n\t}\n\n\tif n != len(prompt) {\n\t\tl.Errorln(\"Failed to write data\")\n\n\t\treturn \"\", errors.New(err)\n\t}\n\n\texec.PrepareStdinForPrompt(l)\n\n\treader := bufio.NewReader(os.Stdin)\n\n\tinputCh := make(chan string)\n\terrCh := make(chan error)\n\n\tgo func() {\n\t\tinput, err := reader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\terrCh <- errors.New(err)\n\t\t\treturn\n\t\t}\n\n\t\tinputCh <- strings.TrimSpace(input)\n\t}()\n\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn \"\", ctx.Err()\n\tcase err := <-errCh:\n\t\treturn \"\", err\n\tcase input := <-inputCh:\n\t\treturn input, nil\n\t}\n}\n\n// PromptUserForYesNo prompts the user for a yes/no response and return true if they entered yes.\nfunc PromptUserForYesNo(ctx context.Context, l log.Logger, prompt string, nonInteractive bool, errWriter io.Writer) (bool, error) {\n\tresp, err := PromptUserForInput(ctx, l, prompt+\" (y/n) \", nonInteractive, errWriter)\n\tif err != nil {\n\t\treturn false, errors.New(err)\n\t}\n\n\tswitch strings.ToLower(resp) {\n\tcase \"y\", \"yes\":\n\t\treturn true, nil\n\tdefault:\n\t\treturn false, nil\n\t}\n}\n"
  },
  {
    "path": "internal/shell/run_cmd.go",
    "content": "// Package shell provides functions to run shell commands and Terraform commands.\npackage shell\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/engine\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/os/exec\"\n\t\"github.com/gruntwork-io/terragrunt/internal/writer\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n)\n\n// SignalForwardingDelay is the time to wait before forwarding the signal to the subcommand.\n//\n// The signal can be sent to the main process (only `terragrunt`) as well as the process group (`terragrunt` and `terraform`), for example:\n// kill -INT <pid>  # sends SIGINT only to the main process\n// kill -INT -<pid> # sends SIGINT to the process group\n// Since we cannot know how the signal is sent, we should give `tofu`/`terraform` time to gracefully exit\n// if it receives the signal directly from the shell, to avoid sending the second interrupt signal to `tofu`/`terraform`.\nconst SignalForwardingDelay = time.Second * 15\n\n// ShellOptions contains the configuration needed to run shell commands.\ntype ShellOptions struct {\n\tWriters       writer.Writers\n\tEngineOptions *engine.EngineOptions\n\tEngineConfig  *engine.EngineConfig\n\tTelemetry     *telemetry.Options\n\tEnv           map[string]string\n\n\tRootWorkingDir  string\n\tWorkingDir      string\n\tTFPath          string\n\tExperiments     experiment.Experiments\n\tHeadless        bool\n\tForwardTFStdout bool\n}\n\n// NoEngine returns true if the user explicitly disabled the engine via --no-engine.\n// Returns false when EngineOptions is nil (default: don't disable), letting the\n// other guards (EngineConfig != nil, experiment enabled) decide whether to run.\nfunc (o *ShellOptions) NoEngine() bool {\n\treturn o.EngineOptions != nil && o.EngineOptions.NoEngine\n}\n\n// RunCommand runs the given shell command.\nfunc RunCommand(ctx context.Context, l log.Logger, runOpts *ShellOptions, command string, args ...string) error {\n\t_, err := RunCommandWithOutput(ctx, l, runOpts, \"\", false, false, command, args...)\n\n\treturn err\n}\n\n// RunCommandWithOutput runs the specified shell command with the specified arguments.\n//\n// Connect the command's stdin, stdout, and stderr to\n// the currently running app. The command can be executed in a custom working directory by using the parameter\n// `workingDir`. Terragrunt working directory will be assumed if empty string.\nfunc RunCommandWithOutput(\n\tctx context.Context,\n\tl log.Logger,\n\trunOpts *ShellOptions,\n\tworkingDir string,\n\tsuppressStdout bool,\n\tneedsPTY bool,\n\tcommand string,\n\targs ...string,\n) (*util.CmdOutput, error) {\n\tvar (\n\t\toutput     = util.CmdOutput{}\n\t\tcommandDir = workingDir\n\t)\n\n\tif workingDir == \"\" {\n\t\tcommandDir = runOpts.WorkingDir\n\t}\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"run_\"+command, map[string]any{\n\t\t\"command\": command,\n\t\t\"args\":    fmt.Sprintf(\"%v\", args),\n\t\t\"dir\":     commandDir,\n\t}, func(ctx context.Context) error {\n\t\tl.Debugf(\"Running command: %s %s\", command, strings.Join(args, \" \"))\n\n\t\tvar (\n\t\t\tcmdStderr = io.MultiWriter(runOpts.Writers.ErrWriter, &output.Stderr)\n\t\t\tcmdStdout = io.MultiWriter(runOpts.Writers.Writer, &output.Stdout)\n\t\t)\n\n\t\t// Pass the traceparent to the child process if it is available in the context.\n\t\ttraceParent := telemetry.TraceParentFromContext(ctx, runOpts.Telemetry)\n\n\t\tif traceParent != \"\" {\n\t\t\tl.Debugf(\"Setting trace parent=%q for command %s\", traceParent, fmt.Sprintf(\"%s %v\", command, args))\n\t\t\trunOpts.Env[telemetry.TraceParentEnv] = traceParent\n\t\t}\n\n\t\tif suppressStdout {\n\t\t\tl.Debugf(\"Command output will be suppressed.\")\n\n\t\t\tcmdStdout = io.MultiWriter(&output.Stdout)\n\t\t}\n\n\t\tif command == runOpts.TFPath {\n\t\t\t// If the engine is enabled and the command is IaC executable, use the engine to run the command.\n\t\t\tif runOpts.EngineConfig != nil && runOpts.Experiments.Evaluate(experiment.IacEngine) && !runOpts.NoEngine() {\n\t\t\t\tl.Debugf(\"Using engine to run command: %s %s\", command, strings.Join(args, \" \"))\n\n\t\t\t\tcmdOutput, err := engine.Run(ctx, l, &engine.ExecutionOptions{\n\t\t\t\t\tWriters: writer.Writers{\n\t\t\t\t\t\tWriter:                 writer.NewWrappedWriter(cmdStdout, runOpts.Writers.Writer),\n\t\t\t\t\t\tErrWriter:              writer.NewWrappedWriter(cmdStderr, runOpts.Writers.ErrWriter),\n\t\t\t\t\t\tLogShowAbsPaths:        runOpts.Writers.LogShowAbsPaths,\n\t\t\t\t\t\tLogDisableErrorSummary: runOpts.Writers.LogDisableErrorSummary,\n\t\t\t\t\t},\n\t\t\t\t\tEngineOptions:     runOpts.EngineOptions,\n\t\t\t\t\tEngineConfig:      runOpts.EngineConfig,\n\t\t\t\t\tEnv:               runOpts.Env,\n\t\t\t\t\tWorkingDir:        commandDir,\n\t\t\t\t\tRootWorkingDir:    runOpts.RootWorkingDir,\n\t\t\t\t\tCommand:           command,\n\t\t\t\t\tArgs:              args,\n\t\t\t\t\tHeadless:          runOpts.Headless,\n\t\t\t\t\tForwardTFStdout:   runOpts.ForwardTFStdout,\n\t\t\t\t\tSuppressStdout:    suppressStdout,\n\t\t\t\t\tAllocatePseudoTty: needsPTY,\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn errors.New(err)\n\t\t\t\t}\n\n\t\t\t\toutput = *cmdOutput\n\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tcmd := exec.Command(ctx, command, args...)\n\t\tcmd.Dir = commandDir\n\t\tcmd.Stdout = cmdStdout\n\t\tcmd.Stderr = cmdStderr\n\t\tcmd.Configure(\n\t\t\texec.WithLogger(l),\n\t\t\texec.WithUsePTY(needsPTY),\n\t\t\texec.WithEnv(runOpts.Env),\n\t\t\texec.WithForwardSignalDelay(SignalForwardingDelay),\n\t\t)\n\n\t\t// Save/restore console mode around subprocess — Windows subprocesses can reset it.\n\t\tsavedConsole := exec.SaveConsoleState()\n\t\tdefer savedConsole.Restore()\n\n\t\tif err := cmd.Start(); err != nil { //nolint:contextcheck // context already passed to exec.Command\n\t\t\terr = util.ProcessExecutionError{\n\t\t\t\tErr:             err,\n\t\t\t\tArgs:            args,\n\t\t\t\tCommand:         command,\n\t\t\t\tWorkingDir:      cmd.Dir,\n\t\t\t\tRootWorkingDir:  runOpts.RootWorkingDir,\n\t\t\t\tLogShowAbsPaths: runOpts.Writers.LogShowAbsPaths,\n\t\t\t\tDisableSummary:  runOpts.Writers.LogDisableErrorSummary,\n\t\t\t}\n\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\tcancelShutdown := cmd.RegisterGracefullyShutdown(ctx)\n\t\tdefer cancelShutdown()\n\n\t\tif err := cmd.Wait(); err != nil {\n\t\t\terr = util.ProcessExecutionError{\n\t\t\t\tErr:             err,\n\t\t\t\tArgs:            args,\n\t\t\t\tCommand:         command,\n\t\t\t\tOutput:          output,\n\t\t\t\tWorkingDir:      cmd.Dir,\n\t\t\t\tRootWorkingDir:  runOpts.RootWorkingDir,\n\t\t\t\tLogShowAbsPaths: runOpts.Writers.LogShowAbsPaths,\n\t\t\t\tDisableSummary:  runOpts.Writers.LogDisableErrorSummary,\n\t\t\t}\n\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\treturn &output, err\n}\n"
  },
  {
    "path": "internal/shell/run_cmd_output_test.go",
    "content": "//go:build linux || darwin\n// +build linux darwin\n\npackage shell_test\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nfunc TestCommandOutputOrder(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"withPtty\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestCommandOutputOrder(t, true,\n\t\t\t[]string{\"stdout1\", \"stderr1\", \"stdout2\", \"stderr2\", \"stderr3\"},\n\t\t\t[]string{\"stdout1\", \"stdout2\"},\n\t\t\t[]string{\"stderr1\", \"stderr2\", \"stderr3\"},\n\t\t)\n\t})\n\tt.Run(\"withoutPtty\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\ttestCommandOutputOrder(t, false,\n\t\t\t[]string{\"stderr1\", \"stderr2\", \"stderr3\"},\n\t\t\t[]string{\"stdout1\", \"stdout2\"},\n\t\t\t[]string{\"stderr1\", \"stderr2\", \"stderr3\"},\n\t\t)\n\t})\n}\n\nfunc noop[T any](t T) {}\n\nfunc testCommandOutputOrder(t *testing.T, withPtty bool, fullOutput []string, stdout []string, stderr []string) {\n\tt.Helper()\n\n\ttestCommandOutput(t, noop[*options.TerragruntOptions], assertOutputs(t, fullOutput, stdout, stderr), withPtty)\n}\n\nfunc testCommandOutput(t *testing.T, withOptions func(*options.TerragruntOptions), assertResults func(string, *util.CmdOutput), allocateStdout bool) {\n\tt.Helper()\n\n\tterragruntOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err, \"Unexpected error creating NewTerragruntOptionsForTest: %v\", err)\n\n\t// Specify a single (locking) buffer for both as a way to check that the output is being written in the correct\n\t// order\n\tvar allOutputBuffer BufferWithLocking\n\n\tterragruntOptions.Writers.Writer = &allOutputBuffer\n\tterragruntOptions.Writers.ErrWriter = &allOutputBuffer\n\n\tterragruntOptions.TerraformCliArgs.AppendArgument(\"same\")\n\n\twithOptions(terragruntOptions)\n\n\tl := logger.CreateLogger()\n\n\tout, err := shell.RunCommandWithOutput(t.Context(), l, configbridge.ShellRunOptsFromOpts(terragruntOptions), \"\", !allocateStdout, false, \"testdata/test_outputs.sh\", \"same\")\n\n\tassert.NotNil(t, out, \"Should get output\")\n\trequire.NoError(t, err, \"Should have no error\")\n\n\tassert.NotNil(t, out, \"Should get output\")\n\tassertResults(allOutputBuffer.String(), out)\n}\n\nfunc assertOutputs(\n\tt *testing.T,\n\texpectedAllOutputs []string,\n\texpectedStdOutputs []string,\n\texpectedStdErrs []string,\n) func(string, *util.CmdOutput) {\n\tt.Helper()\n\n\treturn func(allOutput string, out *util.CmdOutput) {\n\t\tallOutputs := strings.Split(strings.TrimSpace(allOutput), \"\\n\")\n\t\tassert.Len(t, allOutputs, len(expectedAllOutputs))\n\n\t\tfor i := range allOutputs {\n\t\t\tassert.Contains(t, allOutputs[i], expectedAllOutputs[i], allOutputs[i])\n\t\t}\n\n\t\tstdOutputs := strings.Split(strings.TrimSpace(out.Stdout.String()), \"\\n\")\n\t\tassert.Equal(t, expectedStdOutputs, stdOutputs)\n\n\t\tstdErrs := strings.Split(strings.TrimSpace(out.Stderr.String()), \"\\n\")\n\t\tassert.Equal(t, expectedStdErrs, stdErrs)\n\t}\n}\n\n// A goroutine-safe bytes.Buffer\ntype BufferWithLocking struct {\n\tbuffer bytes.Buffer\n\tmutex  sync.Mutex\n}\n\n// Write appends the contents of p to the buffer, growing the buffer as needed. It returns\n// the number of bytes written.\nfunc (s *BufferWithLocking) Write(p []byte) (n int, err error) {\n\ts.mutex.Lock()\n\tdefer s.mutex.Unlock()\n\n\treturn s.buffer.Write(p)\n}\n\n// String returns the contents of the unread portion of the buffer\n// as a string.  If the Buffer is a nil pointer, it returns \"<nil>\".\nfunc (s *BufferWithLocking) String() string {\n\ts.mutex.Lock()\n\tdefer s.mutex.Unlock()\n\n\treturn s.buffer.String()\n}\n"
  },
  {
    "path": "internal/shell/run_cmd_test.go",
    "content": "package shell_test\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cache\"\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\nfunc TestRunShellCommand(t *testing.T) {\n\tt.Parallel()\n\n\tterragruntOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err, \"Unexpected error creating NewTerragruntOptionsForTest: %v\", err)\n\n\tl := logger.CreateLogger()\n\n\tcmd := shell.RunCommand(t.Context(), l, configbridge.ShellRunOptsFromOpts(terragruntOptions), \"tofu\", \"--version\")\n\trequire.NoError(t, cmd)\n\n\tcmd = shell.RunCommand(t.Context(), l, configbridge.ShellRunOptsFromOpts(terragruntOptions), \"tofu\", \"not-a-real-command\")\n\trequire.Error(t, cmd)\n}\n\nfunc TestRunShellOutputToStderrAndStdout(t *testing.T) {\n\tt.Parallel()\n\n\tterragruntOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err, \"Unexpected error creating NewTerragruntOptionsForTest: %v\", err)\n\n\tstdout := new(bytes.Buffer)\n\tstderr := new(bytes.Buffer)\n\n\tterragruntOptions.TerraformCliArgs.AppendFlag(\"--version\")\n\tterragruntOptions.Writers.Writer = stdout\n\tterragruntOptions.Writers.ErrWriter = stderr\n\n\tl := logger.CreateLogger()\n\n\tcmd := shell.RunCommand(t.Context(), l, configbridge.ShellRunOptsFromOpts(terragruntOptions), \"tofu\", \"--version\")\n\trequire.NoError(t, cmd)\n\n\tassert.Contains(t, stdout.String(), \"OpenTofu\", \"Output directed to stdout\")\n\tassert.Empty(t, stderr.String(), \"No output to stderr\")\n\n\tstdout = new(bytes.Buffer)\n\tstderr = new(bytes.Buffer)\n\n\tterragruntOptions.TerraformCliArgs = iacargs.New()\n\tterragruntOptions.Writers.Writer = stderr\n\tterragruntOptions.Writers.ErrWriter = stderr\n\n\tcmd = shell.RunCommand(t.Context(), l, configbridge.ShellRunOptsFromOpts(terragruntOptions), \"tofu\", \"--version\")\n\trequire.NoError(t, cmd)\n\n\tassert.Contains(t, stderr.String(), \"OpenTofu\", \"Output directed to stderr\")\n\tassert.Empty(t, stdout.String(), \"No output to stdout\")\n}\n\nfunc TestLastReleaseTag(t *testing.T) {\n\tt.Parallel()\n\n\tvar tags = []string{\n\t\t\"refs/tags/v0.0.1\",\n\t\t\"refs/tags/v0.0.2\",\n\t\t\"refs/tags/v0.10.0\",\n\t\t\"refs/tags/v20.0.1\",\n\t\t\"refs/tags/v0.3.1\",\n\t\t\"refs/tags/v20.1.2\",\n\t\t\"refs/tags/v0.5.1\",\n\t}\n\n\tlastTag := shell.LastReleaseTag(tags)\n\tassert.NotEmpty(t, lastTag)\n\tassert.Equal(t, \"v20.1.2\", lastTag)\n}\n\nfunc TestGitLevelTopDirCaching(t *testing.T) {\n\tt.Parallel()\n\tctx := t.Context()\n\tctx = cache.ContextWithCache(ctx)\n\tc := cache.ContextCache[string](ctx, cache.RunCmdCacheContextKey)\n\tassert.NotNil(t, c)\n\tassert.Empty(t, c.Cache)\n\n\tterragruntOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err)\n\n\tl := logger.CreateLogger()\n\tpath := \".\"\n\tpath1, err := shell.GitTopLevelDir(ctx, l, terragruntOptions.Env, path)\n\trequire.NoError(t, err)\n\tpath2, err := shell.GitTopLevelDir(ctx, l, terragruntOptions.Env, path)\n\trequire.NoError(t, err)\n\tassert.Equal(t, path1, path2)\n\tassert.Len(t, c.Cache, 1)\n}\n"
  },
  {
    "path": "internal/shell/run_cmd_unix_test.go",
    "content": "//go:build linux || darwin\n// +build linux darwin\n\npackage shell_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"syscall\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/os/signal\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRunCommandWithOutputInterrupt(t *testing.T) {\n\tt.Parallel()\n\n\tterragruntOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err, \"Unexpected error creating NewTerragruntOptionsForTest: %v\", err)\n\n\tl := logger.CreateLogger()\n\n\terrCh := make(chan error)\n\texpectedWait := 5\n\n\tctx, cancel := context.WithCancelCause(t.Context())\n\n\tcmdPath := \"testdata/test_sigint_wait.sh\"\n\n\tgo func() {\n\t\t_, err := shell.RunCommandWithOutput(ctx, l, configbridge.ShellRunOptsFromOpts(terragruntOptions), \"\", false, false, cmdPath, strconv.Itoa(expectedWait))\n\t\terrCh <- err\n\t}()\n\n\ttime.AfterFunc(3*time.Second, func() {\n\t\tcancel(signal.NewContextCanceledError(syscall.SIGINT))\n\t})\n\n\tactualErr := <-errCh\n\trequire.Error(t, actualErr, \"Expected an error but got none\")\n\n\t// The process might either exit with the expected status code or be killed by a signal\n\t// depending on timing and system conditions\n\texpectedExitStatusErr := fmt.Sprintf(\"Failed to execute \\\"%s 5\\\" in .\\n\\nexit status %d\", cmdPath, expectedWait)\n\texpectedKilledErr := fmt.Sprintf(\"Failed to execute \\\"%s 5\\\" in .\\n\\nsignal: killed\", cmdPath)\n\n\tif actualErr.Error() == expectedKilledErr {\n\t\tt.Errorf(\"Expected process to gracefully terminate but got\\n: %s\", actualErr.Error())\n\t} else if actualErr.Error() != expectedExitStatusErr {\n\t\tt.Errorf(\"Expected error to be:\\n  %s\\nbut got:\\n  %s\",\n\t\t\texpectedExitStatusErr, actualErr.Error())\n\t}\n}\n"
  },
  {
    "path": "internal/shell/run_cmd_windows_test.go",
    "content": "//go:build windows\n// +build windows\n\npackage shell_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/os/signal\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestWindowsRunCommandWithOutputInterrupt(t *testing.T) {\n\tt.Parallel()\n\n\tterragruntOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\tassert.Nil(t, err, \"Unexpected error creating NewTerragruntOptionsForTest: %v\", err)\n\n\tl := logger.CreateLogger()\n\n\terrCh := make(chan error)\n\texpectedWait := 5\n\n\tctx, cancel := context.WithCancelCause(t.Context())\n\n\tcmdPath := \"testdata\\\\test_sigint_wait.bat\"\n\n\tgo func() {\n\t\t_, err := shell.RunCommandWithOutput(ctx, l, configbridge.ShellRunOptsFromOpts(terragruntOptions), \"\", false, false, cmdPath, strconv.Itoa(expectedWait))\n\t\terrCh <- err\n\t}()\n\n\ttime.AfterFunc(3*time.Second, func() {\n\t\tcancel(signal.NewContextCanceledError(os.Kill))\n\t})\n\n\tactualErr := <-errCh\n\trequire.Error(t, actualErr, \"Expected an error but got none\")\n\n\t// The process might either exit with the expected status code or be killed by a signal\n\t// depending on timing and system conditions. On Windows, the error message might also\n\t// include stderr output from the batch file execution.\n\n\t// Check if the error contains the expected patterns rather than exact matches\n\t// since Windows batch files might include additional stderr output\n\tactualErrStr := actualErr.Error()\n\tcontainsExitStatus5 := strings.Contains(actualErrStr, \"exit status 5\")\n\tcontainsExitStatus1 := strings.Contains(actualErrStr, \"exit status 1\")\n\tcontainsKilled := strings.Contains(actualErrStr, \"signal: killed\")\n\tcontainsFailedExecute := strings.Contains(actualErrStr, fmt.Sprintf(\"Failed to execute \\\"%s\", cmdPath))\n\n\tif containsKilled {\n\t\tt.Errorf(\"Expected process to gracefully terminate but got\\n: %s\", actualErrStr)\n\t}\n\n\t// On Windows, the batch file might exit with status 1 when interrupted, or be killed by signal\n\tif !containsFailedExecute || (!containsExitStatus5 && !containsExitStatus1) {\n\t\tt.Errorf(\"Expected error to contain 'Failed to execute \\\"%s' and either 'exit status 5', or 'exit status 1', but got:\\n  %s\",\n\t\t\tcmdPath, actualErrStr)\n\t}\n}\n"
  },
  {
    "path": "internal/shell/testdata/test_outputs.sh",
    "content": "#!/usr/bin/env bash\necho 'stdout1'\nsleep 1\n>&2 echo 'stderr1'\nsleep 1\necho 'stdout2'\nsleep 1\n>&2 echo 'stderr2'\nsleep 1\n>&2 echo 'stderr3'\n"
  },
  {
    "path": "internal/shell/testdata/test_sigint_wait.bat",
    "content": "@echo off\r\n\r\nset wait_time=%1\r\n\r\nrem Simple infinite loop that can be interrupted\r\n:loop\r\nrem Use ping to create a 1-second delay (ping localhost -n 2 creates ~1 second delay)\r\nping -n 2 127.0.0.1 >nul 2>&1\r\ngoto loop"
  },
  {
    "path": "internal/shell/testdata/test_sigint_wait.sh",
    "content": "#!/usr/bin/env bash\n\nset -e\n\nWAIT_TIME=$1\n\ntrap int_handler INT\n\nfunction int_handler() {\n        sleep \"$WAIT_TIME\"\n        exit \"$WAIT_TIME\"\n}\n\nwhile true; do sleep 0.1; done"
  },
  {
    "path": "internal/stacks/clean/clean.go",
    "content": "// Package clean provides the logic for cleaning up stack configurations.\npackage clean\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// CleanStacks removes stack directories within the specified working directory, unless the command is \"destroy\".\n// It returns an error if any issues occur during the deletion process, or nil if successful.\nfunc CleanStacks(l log.Logger, opts *options.TerragruntOptions) error {\n\tif opts.TerraformCommand == tf.CommandNameDestroy {\n\t\tl.Debugf(\"Skipping stack clean for %s, as part of delete command\", opts.WorkingDir)\n\t\treturn nil\n\t}\n\n\terrs := &errors.MultiError{}\n\n\twalkFn := func(path string, d os.DirEntry, walkErr error) error {\n\t\tif walkErr != nil {\n\t\t\tl.Warnf(\"Error accessing path %s: %v\", path, walkErr)\n\n\t\t\terrs = errs.Append(walkErr)\n\n\t\t\treturn nil\n\t\t}\n\n\t\tif d.IsDir() && d.Name() == \".terragrunt-stack\" {\n\t\t\trelPath, relErr := filepath.Rel(opts.WorkingDir, path)\n\t\t\tif relErr != nil {\n\t\t\t\trelPath = path // fallback to absolute if error\n\t\t\t}\n\n\t\t\tl.Infof(\"Deleting stack directory: %s\", relPath)\n\n\t\t\tif rmErr := os.RemoveAll(path); rmErr != nil {\n\t\t\t\tl.Errorf(\"Failed to delete stack directory %s: %v\", relPath, rmErr)\n\n\t\t\t\terrs = errs.Append(rmErr)\n\t\t\t}\n\n\t\t\treturn filepath.SkipDir\n\t\t}\n\n\t\treturn nil\n\t}\n\tif walkErr := filepath.WalkDir(opts.WorkingDir, walkFn); walkErr != nil {\n\t\terrs = errs.Append(walkErr)\n\t}\n\n\treturn errs.ErrorOrNil()\n}\n"
  },
  {
    "path": "internal/stacks/generate/generate.go",
    "content": "// Package generate provides functionality for generating stacks from stack files.\npackage generate\n\nimport (\n\t\"context\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/internal/worker\"\n\t\"github.com/gruntwork-io/terragrunt/internal/worktrees\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// StackNode represents a stack file in the file system.\n// The parent is the node that generates the current node,\n// and children are the nodes that are generated by the current node.\ntype StackNode struct {\n\tParent   *StackNode\n\tFilePath string\n\tChildren []*StackNode\n\tLevel    int\n}\n\n// NewStackNode creates a new stack node.\nfunc NewStackNode(filePath string) *StackNode {\n\treturn &StackNode{\n\t\tFilePath: filePath,\n\t\tLevel:    -1,\n\t\tChildren: make([]*StackNode, 0),\n\t}\n}\n\n// GenerateStacks generates the stack files using topological ordering to prevent race conditions.\n// Stack files are generated level by level, ensuring parent stacks complete before their children.\n// Worktrees must be provided by the caller if needed; this function will never create worktrees internally.\nfunc GenerateStacks(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\twts *worktrees.Worktrees,\n) error {\n\tfoundFiles, err := ListStackFiles(ctx, l, opts, opts.WorkingDir, wts)\n\tif err != nil {\n\t\treturn errors.Errorf(\"Failed to list stack files in %s %w\", opts.WorkingDir, err)\n\t}\n\n\tif len(foundFiles) == 0 {\n\t\tif opts.StackAction == \"generate\" {\n\t\t\tl.Warnf(\"No stack files found in %s Nothing to generate.\", opts.WorkingDir)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tgeneratedFiles := make(map[string]bool)\n\n\tstackTrees := BuildStackTopology(l, foundFiles, opts.WorkingDir)\n\n\tconst maxLevel = 1024\n\tfor level := range maxLevel {\n\t\tif level == maxLevel-1 {\n\t\t\treturn errors.Errorf(\"Cycle detected: maximum level (%d) exceeded\", maxLevel)\n\t\t}\n\n\t\tlevelNodes := getNodesAtLevel(stackTrees, level)\n\t\tif len(levelNodes) == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tif err := generateLevel(ctx, l, opts, level, levelNodes, generatedFiles); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := discoverAndAddNewNodes(ctx, l, opts, wts, stackTrees, generatedFiles, level+1); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// generateLevel handles the concurrent generation of all stack files at a given level.\nfunc generateLevel(ctx context.Context, l log.Logger, opts *options.TerragruntOptions, level int, levelNodes []*StackNode, generatedFiles map[string]bool) error {\n\tl.Debugf(\"Generating stack level %d with %d files\", level, len(levelNodes))\n\n\twp := worker.NewWorkerPool(opts.Parallelism)\n\tdefer wp.Stop()\n\n\tfor _, node := range levelNodes {\n\t\tif generatedFiles[node.FilePath] {\n\t\t\tcontinue\n\t\t}\n\n\t\tgeneratedFiles[node.FilePath] = true\n\n\t\t// Before attempting to generate the stack file, we need to double-check that the file exists.\n\t\t// Generation at a higher level might have resulted in this file being removed.\n\t\tif !util.FileExists(node.FilePath) {\n\t\t\tcontinue\n\t\t}\n\n\t\twp.Submit(func() error {\n\t\t\t_, pctx := configbridge.NewParsingContext(ctx, l, opts)\n\t\t\treturn config.GenerateStackFile(ctx, l, pctx, wp, node.FilePath)\n\t\t})\n\t}\n\n\treturn wp.Wait()\n}\n\n// discoverAndAddNewNodes discovers new stack files and adds them to the dependency graph.\nfunc discoverAndAddNewNodes(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tworktrees *worktrees.Worktrees,\n\tdependencyGraph map[string]*StackNode,\n\tgeneratedFiles map[string]bool,\n\tminLevel int,\n) error {\n\tnewFiles, listErr := ListStackFiles(ctx, l, opts, opts.WorkingDir, worktrees)\n\tif listErr != nil {\n\t\treturn errors.Errorf(\"Failed to list stack files after level %d: %w\", minLevel-1, listErr)\n\t}\n\n\taddNewNodesToGraph(l, dependencyGraph, newFiles, generatedFiles, opts.WorkingDir)\n\n\treturn nil\n}\n\n// BuildStackTopology creates a topological tree based on directory hierarchy.\nfunc BuildStackTopology(l log.Logger, stackFiles []string, workingDir string) map[string]*StackNode {\n\tnodes := make(map[string]*StackNode)\n\n\tfor _, file := range stackFiles {\n\t\tnodes[file] = NewStackNode(file)\n\t}\n\n\tfor _, node := range nodes {\n\t\tassignNodeLevel(l, node, nodes, workingDir)\n\t}\n\n\treturn nodes\n}\n\n// assignNodeLevel recursively assigns levels to nodes based on directory depth.\nfunc assignNodeLevel(l log.Logger, node *StackNode, allNodes map[string]*StackNode, workingDir string) int {\n\tif node.Level != -1 {\n\t\treturn node.Level\n\t}\n\n\tnodeDir := filepath.Dir(node.FilePath)\n\tparentPath := findParentStackFile(nodeDir, allNodes, workingDir)\n\n\tif parentPath == \"\" {\n\t\tnode.Level = 0\n\n\t\treturn node.Level\n\t}\n\n\tparent := allNodes[parentPath]\n\tif parent == nil {\n\t\tnode.Level = 0\n\n\t\treturn node.Level\n\t}\n\n\tparentLevel := assignNodeLevel(l, parent, allNodes, workingDir)\n\tnode.Level = parentLevel + 1\n\tnode.Parent = parent\n\tparent.Children = append(parent.Children, node)\n\n\tl.Debugf(\"Stack %s (level %d) is child of %s (level %d)\", node.FilePath, node.Level, parent.FilePath, parent.Level)\n\n\treturn node.Level\n}\n\n// findParentStackFile finds the parent stack file for a given directory.\nfunc findParentStackFile(childDir string, allNodes map[string]*StackNode, workingDir string) string {\n\tcurrentDir := childDir\n\n\tfor {\n\t\tparentDir := filepath.Dir(currentDir)\n\t\tif parentDir == currentDir {\n\t\t\tbreak\n\t\t}\n\n\t\tif parentDir == workingDir {\n\t\t\tpotentialParent := filepath.Join(workingDir, config.DefaultStackFile)\n\t\t\tif _, exists := allNodes[potentialParent]; exists {\n\t\t\t\treturn potentialParent\n\t\t\t}\n\n\t\t\tbreak\n\t\t}\n\n\t\tpotentialParent := filepath.Join(parentDir, config.DefaultStackFile)\n\t\tif _, exists := allNodes[potentialParent]; exists {\n\t\t\treturn potentialParent\n\t\t}\n\n\t\tcurrentDir = parentDir\n\t}\n\n\treturn \"\"\n}\n\n// getNodesAtLevel returns all nodes at a specific level.\nfunc getNodesAtLevel(nodes map[string]*StackNode, level int) []*StackNode {\n\tvar levelNodes []*StackNode\n\n\tfor _, node := range nodes {\n\t\tif node.Level == level {\n\t\t\tlevelNodes = append(levelNodes, node)\n\t\t}\n\t}\n\n\treturn levelNodes\n}\n\n// addNewNodesToGraph adds newly discovered stack files to the dependency graph.\nfunc addNewNodesToGraph(\n\tl log.Logger,\n\texistingNodes map[string]*StackNode,\n\tallFiles []string,\n\tgeneratedFiles map[string]bool,\n\tworkingDir string,\n) {\n\tnewFiles := make([]string, 0)\n\n\tfor _, file := range allFiles {\n\t\tif _, exists := existingNodes[file]; !exists && !generatedFiles[file] {\n\t\t\tnewFiles = append(newFiles, file)\n\t\t}\n\t}\n\n\tif len(newFiles) == 0 {\n\t\treturn\n\t}\n\n\tl.Debugf(\"Adding %d new stack files to topology graph\", len(newFiles))\n\n\tfor _, file := range newFiles {\n\t\texistingNodes[file] = NewStackNode(file)\n\t}\n\n\tfor _, file := range newFiles {\n\t\tnode := existingNodes[file]\n\t\tassignNodeLevel(l, node, existingNodes, workingDir)\n\t}\n}\n\n// ListStackFiles searches for stack files in the specified directory.\n//\n// We only want to use the discovery package when the filter flag experiment is enabled, as we need to filter discovery\n// results to ensure that we get the right files back for generation.\nfunc ListStackFiles(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tdir string,\n\tworktrees *worktrees.Worktrees,\n) ([]string, error) {\n\tdiscovery, err := discovery.NewForStackGenerate(l, discovery.StackGenerateOptions{\n\t\tWorkingDir:  opts.WorkingDir,\n\t\tFilters:     opts.Filters,\n\t\tExperiments: opts.Experiments,\n\t})\n\tif err != nil {\n\t\treturn nil, errors.Errorf(\"Failed to create discovery for stack generate: %w\", err)\n\t}\n\n\tdiscoveredComponents, err := discovery.Discover(ctx, l, opts)\n\tif err != nil {\n\t\treturn nil, errors.Errorf(\"Failed to discover stack files: %w\", err)\n\t}\n\n\tworktreeStacks, err := worktreeStacksToGenerate(ctx, l, opts, worktrees, opts.Experiments)\n\tif err != nil {\n\t\treturn nil, errors.Errorf(\"Failed to get worktree stacks to generate: %w\", err)\n\t}\n\n\tfoundFiles := make([]string, 0, len(discoveredComponents)+len(worktreeStacks))\n\tfor _, c := range discoveredComponents {\n\t\tif _, ok := c.(*component.Stack); !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tfoundFiles = append(foundFiles, filepath.Join(c.Path(), config.DefaultStackFile))\n\t}\n\n\tfor _, c := range worktreeStacks {\n\t\tfoundFiles = append(foundFiles, filepath.Join(c.Path(), config.DefaultStackFile))\n\t}\n\n\treturn foundFiles, nil\n}\n\n// worktreeStacksToGenerate returns a slice of stacks that need to be generated from the worktree stacks.\nfunc worktreeStacksToGenerate(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n\tw *worktrees.Worktrees,\n\texperiments experiment.Experiments,\n) (component.Components, error) {\n\t// If worktrees is nil, there are no worktrees to process, return empty components.\n\tif w == nil {\n\t\treturn component.Components{}, nil\n\t}\n\n\tstacksToGenerate := component.NewThreadSafeComponents(component.Components{})\n\n\t// If we edit a stack in a worktree, we need to generate it, at the minimum.\n\tstackDiff := w.Stacks()\n\n\teditedStacks := slices.Concat(\n\t\tstackDiff.Added,\n\t\tstackDiff.Removed,\n\t)\n\n\tfor _, changed := range stackDiff.Changed {\n\t\teditedStacks = append(editedStacks, changed.FromStack, changed.ToStack)\n\t}\n\n\tfor _, stack := range editedStacks {\n\t\tstacksToGenerate.EnsureComponent(stack)\n\t}\n\n\t// When the expanded filter for a given Git expression requires parsing,\n\t// we need to generate all the stacks in the given worktree, as units within the generated stack might be affected.\n\t//\n\t// Based on business logic, the from branch here should never be used, but we'll check it anyways for completeness.\n\t// We only require parsing for reading filters, and those only trigger in expanded Git expressions when\n\t// the file is modified (which would result in a toFilter being returned).\n\n\tfullDiscoveries := map[string]*discovery.Discovery{}\n\n\tfor _, pair := range w.WorktreePairs {\n\t\tfromFilters, toFilters, err := pair.Expand()\n\t\tif err != nil {\n\t\t\treturn nil, errors.Errorf(\"failed to expand worktree pair: %w\", err)\n\t\t}\n\n\t\tif _, requiresParse := fromFilters.RequiresParse(); requiresParse {\n\t\t\tdisc, err := discovery.NewForStackGenerate(l, discovery.StackGenerateOptions{\n\t\t\t\tWorkingDir:  pair.FromWorktree.Path,\n\t\t\t\tFilters:     stackTypeFilter(),\n\t\t\t\tExperiments: experiments,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Errorf(\"Failed to create discovery for worktree %s: %w\", pair.FromWorktree.Ref, err)\n\t\t\t}\n\n\t\t\tfullDiscoveries[pair.FromWorktree.Ref] = disc\n\t\t}\n\n\t\tif _, requiresParse := toFilters.RequiresParse(); requiresParse {\n\t\t\tdisc, err := discovery.NewForStackGenerate(l, discovery.StackGenerateOptions{\n\t\t\t\tWorkingDir:  pair.ToWorktree.Path,\n\t\t\t\tFilters:     stackTypeFilter(),\n\t\t\t\tExperiments: experiments,\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Errorf(\"Failed to create discovery for worktree %s: %w\", pair.ToWorktree.Ref, err)\n\t\t\t}\n\n\t\t\tfullDiscoveries[pair.ToWorktree.Ref] = disc\n\t\t}\n\t}\n\n\tg, ctx := errgroup.WithContext(ctx)\n\tg.SetLimit(min(runtime.NumCPU(), len(fullDiscoveries)))\n\n\tvar (\n\t\tmu   sync.Mutex\n\t\terrs []error\n\t)\n\n\tfor ref, disc := range fullDiscoveries {\n\t\t// Create per-iteration local copies to avoid closure capture bug\n\t\trefCopy := ref\n\t\tdiscCopy := disc\n\n\t\tg.Go(func() error {\n\t\t\tcomponents, err := discCopy.Discover(ctx, l, opts)\n\t\t\tif err != nil {\n\t\t\t\tmu.Lock()\n\n\t\t\t\terrs = append(errs, errors.Errorf(\"Failed to discover stacks in worktree %s: %w\", refCopy, err))\n\n\t\t\t\tmu.Unlock()\n\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tfor _, c := range components {\n\t\t\t\tstacksToGenerate.EnsureComponent(c)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tif err := g.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn stacksToGenerate.ToComponents(), errors.Join(errs...)\n\t}\n\n\treturn stacksToGenerate.ToComponents(), nil\n}\n\n// stackTypeFilter returns a filter.Filters that restricts to stack components.\nfunc stackTypeFilter() filter.Filters {\n\tattrExpr := filter.NewTypeExpression(component.StackKind)\n\n\treturn filter.Filters{filter.NewFilter(attrExpr, attrExpr.String())}\n}\n"
  },
  {
    "path": "internal/stacks/output/output.go",
    "content": "// Package output provides functionality for collecting and collating the\n// unit outputs for all units in a stack hierarchy.\npackage output\n\nimport (\n\t\"context\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/stacks/generate\"\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/internal/worktrees\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/zclconf/go-cty/cty\"\n)\n\n// StackOutput collects and returns the OpenTofu/Terraform output values for all declared units in a stack hierarchy.\n//\n// This function is a central component of Terragrunt's stack output system, providing a mechanism to\n// aggregate and organize outputs from multiple deployments in a hierarchical structure. It's particularly\n// useful when working with complex infrastructure composed of multiple interconnected OpenTofu/Terraform units.\n//\n// The function performs several key operations:\n//\n//  1. Discovers all stack definition files (terragrunt.stack.hcl) in the working directory and its subdirectories.\n//  2. For each stack file, parses the configuration and extracts the declared stacks and units.\n//  3. For each unit, reads its OpenTofu/Terraform outputs from the corresponding directory within .terragrunt-stack.\n//  4. Constructs a hierarchical map of outputs by organizing units according to their position in the stack hierarchy.\n//     Units are keyed using dot notation that reflects the stack path (e.g., \"parent.child.unit\").\n//  5. Orders stack names from the highest level (shortest path) to deepest nested (longest path).\n//  6. Nests the flat output map into a hierarchical structure and converts it to a cty.Value object.\n//\n// The returned cty.Value object contains a structured representation of all outputs, preserving the\n// nested relationship between stacks and units. This makes it easy to access outputs from specific\n// parts of the infrastructure while maintaining awareness of the overall architecture.\n//\n// For telemetry and debugging purposes, the function logs various events at the debug level, including\n// when outputs are added for specific units and stack keys.\n//\n// Parameters:\n//   - ctx: Context for the operation, which may include telemetry collection.\n//   - opts: TerragruntOptions containing configuration settings and the working directory path.\n//\n// Returns:\n//   - cty.Value: A hierarchical object containing all outputs from the stack units, organized by stack path.\n//   - error: An error if any operation fails during discovery, parsing, output collection, or conversion.\n//\n// Errors can occur during stack file listing, value reading, stack config parsing, output reading,\n// or when converting the final output structure to cty.Value format.\nfunc StackOutput(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n) (cty.Value, error) {\n\tl.Debugf(\"Generating output from %s\", opts.WorkingDir)\n\n\t// Create worktrees internally if filter-flag experiment is enabled and git filters are present\n\twts, err := buildWorktreesIfNeeded(ctx, l, opts)\n\tif err != nil {\n\t\treturn cty.NilVal, errors.Errorf(\"failed to create worktrees: %w\", err)\n\t}\n\n\tif wts != nil {\n\t\tdefer func() {\n\t\t\tif cleanupErr := wts.Cleanup(ctx, l); cleanupErr != nil {\n\t\t\t\tl.Errorf(\"failed to cleanup worktrees: %v\", cleanupErr)\n\t\t\t}\n\t\t}()\n\t}\n\n\tfoundFiles, err := generate.ListStackFiles(ctx, l, opts, opts.WorkingDir, wts)\n\tif err != nil {\n\t\treturn cty.NilVal, errors.Errorf(\"Failed to list stack files in %s: %w\", opts.WorkingDir, err)\n\t}\n\n\tif len(foundFiles) == 0 {\n\t\tl.Warnf(\"No stack files found in %s Nothing to generate.\", opts.WorkingDir)\n\t\treturn cty.NilVal, nil\n\t}\n\n\toutputs := make(map[string]map[string]cty.Value)\n\tdeclaredStacks := make(map[string]string)\n\tdeclaredUnits := make(map[string]*config.Unit)\n\n\t// save parsed stacks\n\tparsedStackFiles := make(map[string]*config.StackConfig, len(foundFiles))\n\n\tfor _, path := range foundFiles {\n\t\tdir := filepath.Dir(path)\n\n\t\tctx, pctx := configbridge.NewParsingContext(ctx, l, opts)\n\n\t\tvalues, valuesErr := config.ReadValues(ctx, pctx, l, dir)\n\t\tif valuesErr != nil {\n\t\t\treturn cty.NilVal, errors.Errorf(\"Failed to read values from %s: %w\", dir, valuesErr)\n\t\t}\n\n\t\tstackFile, stackErr := config.ReadStackConfigFile(ctx, l, pctx, path, values)\n\t\tif stackErr != nil {\n\t\t\treturn cty.NilVal, errors.Errorf(\"Failed to read stack file %s: %w\", path, stackErr)\n\t\t}\n\n\t\tparsedStackFiles[path] = stackFile\n\n\t\ttargetDir := filepath.Join(dir, config.StackDir)\n\n\t\tfor _, stack := range stackFile.Stacks {\n\t\t\tdeclaredStacks[filepath.Join(targetDir, stack.Path)] = stack.Name\n\t\t\tl.Debugf(\"Registered stack %s at path %s\", stack.Name, filepath.Join(targetDir, stack.Path))\n\t\t}\n\n\t\tfor _, unit := range stackFile.Units {\n\t\t\tunitDir := config.GetUnitDir(dir, unit)\n\n\t\t\tvar output map[string]cty.Value\n\n\t\t\ttelemetryErr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"unit_output\", map[string]any{\n\t\t\t\t\"unit_name\":   unit.Name,\n\t\t\t\t\"unit_source\": unit.Source,\n\t\t\t\t\"unit_path\":   unit.Path,\n\t\t\t}, func(ctx context.Context) error {\n\t\t\t\tvar outputErr error\n\n\t\t\t\toutput, outputErr = unit.ReadOutputs(ctx, l, pctx, unitDir)\n\n\t\t\t\treturn outputErr\n\t\t\t})\n\t\t\tif telemetryErr != nil {\n\t\t\t\treturn cty.NilVal, errors.New(telemetryErr)\n\t\t\t}\n\n\t\t\tkey := filepath.Join(targetDir, unit.Path)\n\t\t\tdeclaredUnits[key] = unit\n\t\t\toutputs[key] = output\n\n\t\t\tl.Debugf(\"Added output for %s\", key)\n\t\t}\n\t}\n\n\tunitOutputs := make(map[string]map[string]cty.Value)\n\n\t// Build stack list separated by stacks, find all nested stacks, and build a dotted path. If no stack is found, use the unit name.\n\tfor path, unit := range declaredUnits {\n\t\toutput, found := outputs[path]\n\t\tif !found {\n\t\t\tl.Debugf(\"No output found for %s\", path)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Implement more logic to find all stacks in which the path is located\n\t\tstackNames := []string{}\n\t\tnameToPath := make(map[string]string) // Map to track which path each stack name came from\n\n\t\tfor stackPath, stackName := range declaredStacks {\n\t\t\tif strings.Contains(path, stackPath) {\n\t\t\t\tstackNames = append(stackNames, stackName)\n\t\t\t\tnameToPath[stackName] = stackPath\n\t\t\t}\n\t\t}\n\n\t\t// Sort stackNames based on the length of stackPath to ensure correct order\n\t\tstackNamesSorted := make([]string, len(stackNames))\n\t\tcopy(stackNamesSorted, stackNames)\n\n\t\tfor i := range stackNamesSorted {\n\t\t\tfor j := i + 1; j < len(stackNamesSorted); j++ {\n\t\t\t\t// Compare lengths of the actual paths from the nameToPath map, not the declaredStacks lookup\n\t\t\t\tif len(nameToPath[stackNamesSorted[i]]) < len(nameToPath[stackNamesSorted[j]]) {\n\t\t\t\t\tstackNamesSorted[i], stackNamesSorted[j] = stackNamesSorted[j], stackNamesSorted[i]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tstackKey := unit.Name\n\t\tif len(stackNamesSorted) > 0 {\n\t\t\tstackKey = strings.Join(stackNamesSorted, \".\") + \".\" + unit.Name\n\t\t}\n\n\t\tunitOutputs[stackKey] = output\n\n\t\tl.Debugf(\"Added output for stack key %s\", stackKey)\n\t}\n\n\t// Convert finalMap into a cty.ObjectVal\n\tresult := make(map[string]cty.Value)\n\n\tnestedOutputs, err := nestUnitOutputs(unitOutputs)\n\tif err != nil {\n\t\treturn cty.NilVal, errors.Errorf(\"Failed to nest unit outputs: %w\", err)\n\t}\n\n\tctyResult, err := config.GoTypeToCty(nestedOutputs)\n\tif err != nil {\n\t\treturn cty.NilVal, errors.Errorf(\"Failed to convert unit output to cty value: %s %w\", result, err)\n\t}\n\n\treturn ctyResult, nil\n}\n\n// nestUnitOutputs transforms a flat map of unit outputs into a nested hierarchical structure.\n//\n// This function is a critical part of Terragrunt/Opentofu's stack output system, converting flat key-value pairs\n// with dot notation into a proper nested object hierarchy. It processes each flattened key (e.g., \"parent.child.unit\")\n// by splitting it into path segments and recursively building the corresponding nested structure.\n//\n// The algorithm works as follows:\n//  1. For each entry in the flat map, split its key by dots to get the path segments\n//  2. Iteratively traverse the nested structure, creating intermediate maps as needed\n//  3. When reaching the final path segment, convert the map of cty.Values to a Go interface{}\n//     representation and store it at that location\n//  4. Continue until all flat entries have been properly nested\n//\n// This approach preserves the hierarchical relationship between stacks and units while making\n// the data structure easier to navigate and query programmatically.\n//\n// Parameters:\n//   - flat: A map where keys are dot-separated paths (e.g., \"parent.child.unit\") and values are\n//     maps of cty.Value representing the OpenTofu/Terraform outputs for each unit\n//\n// Returns:\n//   - map[string]any: A nested map structure reflecting the hierarchy implied by the dot notation\n//   - error: An error if conversion fails, particularly when building the nested structure\n//\n// Errors can occur during cty.Value conversion or when attempting to traverse the nested structure\n// if the path contains contradictory type information (e.g., a path segment is both a leaf and a branch).\nfunc nestUnitOutputs(flat map[string]map[string]cty.Value) (map[string]any, error) {\n\tnested := make(map[string]any)\n\n\tfor flatKey, value := range flat {\n\t\tparts := strings.Split(flatKey, \".\")\n\t\tcurrent := nested\n\n\t\tfor i, part := range parts {\n\t\t\tif i == len(parts)-1 {\n\t\t\t\tctyValue, err := config.ConvertValuesMapToCtyVal(value)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, errors.Errorf(\"Failed to convert unit output to cty value: %s %w\", flatKey, err)\n\t\t\t\t}\n\n\t\t\t\tcurrent[part] = ctyValue\n\t\t\t} else {\n\t\t\t\tif _, exists := current[part]; !exists { // Traverse or create next level\n\t\t\t\t\tcurrent[part] = make(map[string]any)\n\t\t\t\t}\n\n\t\t\t\tvar ok bool\n\n\t\t\t\tcurrent, ok = current[part].(map[string]any)\n\n\t\t\t\tif !ok {\n\t\t\t\t\treturn nil, errors.Errorf(\"Failed to traverse unit output: %v %s\", flat, part)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nested, nil\n}\n\n// buildWorktreesIfNeeded creates worktrees if the filter-flag experiment is enabled and git filters exist.\n// Returns nil worktrees if the experiment is not enabled or no git filters are present.\nfunc buildWorktreesIfNeeded(\n\tctx context.Context,\n\tl log.Logger,\n\topts *options.TerragruntOptions,\n) (*worktrees.Worktrees, error) {\n\tgitFilters := opts.Filters.UniqueGitFilters()\n\tif len(gitFilters) == 0 {\n\t\treturn nil, nil\n\t}\n\n\treturn worktrees.NewWorktrees(ctx, l, opts.WorkingDir, gitFilters)\n}\n"
  },
  {
    "path": "internal/strict/category.go",
    "content": "package strict\n\nimport (\n\t\"slices\"\n\t\"sort\"\n)\n\n// Categories is multiple of DeprecatedFlag Category.\ntype Categories []*Category\n\n// FilterByNames filters `categories` by the given `names`.\nfunc (categories Categories) FilterByNames(names ...string) Categories {\n\tvar filtered Categories\n\n\tfor _, category := range categories {\n\t\tif slices.Contains(names, category.Name) {\n\t\t\tfiltered = append(filtered, category)\n\t\t}\n\t}\n\n\treturn filtered\n}\n\n// FilterNotHidden filters `categories` by the `Hidden:false` field.\nfunc (categories Categories) FilterNotHidden() Categories {\n\tvar filtered Categories\n\n\tfor _, category := range categories {\n\t\tif !category.Hidden {\n\t\t\tfiltered = append(filtered, category)\n\t\t}\n\t}\n\n\treturn filtered\n}\n\n// Find search category by given `name`, returns nil if not found.\nfunc (categories Categories) Find(name string) *Category {\n\tfor _, category := range categories {\n\t\tif category.Name == name {\n\t\t\treturn category\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Len implements `sort.Interface` interface.\nfunc (categories Categories) Len() int {\n\treturn len(categories)\n}\n\n// Less implements `sort.Interface` interface.\nfunc (categories Categories) Less(i, j int) bool {\n\t// Handle empty names: empty names should come last\n\tif categories[i].Name == \"\" {\n\t\treturn false\n\t}\n\n\tif categories[j].Name == \"\" {\n\t\treturn true\n\t}\n\t// Normal lexicographical comparison\n\treturn categories[i].Name < categories[j].Name\n}\n\n// Swap implements `sort.Interface` interface.\nfunc (categories Categories) Swap(i, j int) {\n\t(categories)[i], (categories)[j] = (categories)[j], (categories)[i]\n}\n\n// Sort returns `categories` in sorted order by `Name`.\nfunc (categories Categories) Sort() Categories {\n\tsort.Sort(categories)\n\n\treturn categories\n}\n\n// Category represents a strict control category. Used to group controls when they are displayed.\ntype Category struct {\n\t// Name is the name of the category.\n\tName string\n\t// Hidden specifies whether controls belonging to this category should be displayed.\n\tHidden bool\n}\n\n// String implements `fmt.Stringer` interface.\nfunc (category *Category) String() string {\n\treturn category.Name\n}\n"
  },
  {
    "path": "internal/strict/control.go",
    "content": "package strict\n\nimport (\n\t\"context\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst CompletedControlsFmt = \"The following strict control(s) are already completed: %s. Please remove any completed strict controls, as setting them no longer does anything. For a list of all ongoing strict controls, and the outcomes of previous strict controls, see https://docs.terragrunt.com/reference/strict-mode or get the actual list by running the `terragrunt info strict` command.\"\n\ntype ControlNames []string\n\nfunc (names ControlNames) String() string {\n\treturn strings.Join(names, \", \")\n}\n\n// Control represents an interface that can be enabled or disabled in strict mode.\n// When the Control is Enabled, Terragrunt will behave in a way that is not backwards compatible.\ntype Control interface {\n\t// GetName returns the name of the strict control.\n\tGetName() string\n\n\t// GetDescription returns the description of the strict control.\n\tGetDescription() string\n\n\t// GetStatus returns the status of the strict control.\n\tGetStatus() Status\n\n\t// Enable enables the control.\n\tEnable()\n\n\t// GetEnabled returns true if the control is enabled.\n\tGetEnabled() bool\n\n\t// GetCategory returns category of the strict control.\n\tGetCategory() *Category\n\n\t// SetCategory sets the category.\n\tSetCategory(category *Category)\n\n\t// GetSubcontrols returns all subcontrols.\n\tGetSubcontrols() Controls\n\n\t// AddSubcontrols adds the given `newCtrls` as subcontrols.\n\tAddSubcontrols(newCtrls ...Control)\n\n\t// SuppressWarning suppresses the warning message from being displayed.\n\tSuppressWarning()\n\n\t// Evaluate evaluates the strict control.\n\tEvaluate(ctx context.Context) error\n}\n\n// Controls are multiple of Controls.\ntype Controls []Control\n\n// Names returns names of all `ctrls`.\nfunc (ctrls Controls) Names() ControlNames {\n\tvar names ControlNames\n\n\tfor _, ctrl := range ctrls {\n\t\tif name := ctrl.GetName(); name != \"\" {\n\t\t\tnames = append(names, name)\n\t\t}\n\t}\n\n\tslices.Sort(names)\n\n\treturn names\n}\n\n// SuppressWarning suppresses the warning message from being displayed.\nfunc (ctrls Controls) SuppressWarning() Controls {\n\tfor _, ctrl := range ctrls {\n\t\tctrl.SuppressWarning()\n\t}\n\n\treturn ctrls\n}\n\n// FilterByStatus filters `ctrls` by given statuses.\nfunc (ctrls Controls) FilterByStatus(statuses ...Status) Controls {\n\tvar filtered Controls\n\n\tfor _, ctrl := range ctrls {\n\t\tif slices.Contains(statuses, ctrl.GetStatus()) {\n\t\t\tfiltered = append(filtered, ctrl)\n\t\t}\n\t}\n\n\treturn filtered\n}\n\n// RemoveDuplicates removes controls with duplicate names.\nfunc (ctrls Controls) RemoveDuplicates() Controls {\n\tvar unique Controls\n\n\tfor _, ctrl := range ctrls {\n\t\tskip := false\n\n\t\tfor _, uniqueCtrl := range unique {\n\t\t\tif uniqueCtrl.GetName() == ctrl.GetName() && uniqueCtrl.GetCategory().Name == ctrl.GetCategory().Name {\n\t\t\t\tskip = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif !skip {\n\t\t\tunique = append(unique, ctrl)\n\t\t}\n\t}\n\n\treturn unique\n}\n\n// FilterByEnabled filters `ctrls` by `Enabled: true` field.\nfunc (ctrls Controls) FilterByEnabled() Controls {\n\tvar filtered Controls\n\n\tfor _, ctrl := range ctrls {\n\t\tif ctrl.GetEnabled() {\n\t\t\tfiltered = append(filtered, ctrl)\n\t\t}\n\t}\n\n\treturn filtered\n}\n\n// FilterByNames filters `ctrls` by the given `names`.\nfunc (ctrls Controls) FilterByNames(names ...string) Controls {\n\tvar filtered Controls\n\n\tfor _, ctrl := range ctrls {\n\t\tif slices.Contains(names, ctrl.GetName()) {\n\t\t\tfiltered = append(filtered, ctrl)\n\t\t}\n\t}\n\n\treturn filtered\n}\n\n// FilterByCategories filters `ctrls` by the given `categories`.\nfunc (ctrls Controls) FilterByCategories(categories ...*Category) Controls {\n\tvar filtered Controls\n\n\tfor _, ctrl := range ctrls {\n\t\tif category := ctrl.GetCategory(); (category == nil && len(categories) == 0) || (category != nil && slices.Contains(categories, category)) {\n\t\t\tfiltered = append(filtered, ctrl)\n\t\t}\n\t}\n\n\treturn filtered\n}\n\n// GetCategories returns a unique list of the `ctrls` categories.\nfunc (ctrls Controls) GetCategories() Categories {\n\tvar categories Categories\n\n\tfor _, ctrl := range ctrls {\n\t\tif category := ctrl.GetCategory(); category != nil && !slices.Contains(categories, category) {\n\t\t\tcategories = append(categories, ctrl.GetCategory())\n\t\t}\n\t}\n\n\treturn categories\n}\n\n// SetCategory sets the given category for all `ctrls`.\nfunc (ctrls Controls) SetCategory(category *Category) {\n\tfor _, ctrl := range ctrls {\n\t\tctrl.SetCategory(category)\n\t}\n}\n\n// Enable recursively enables all `ctrls`.\nfunc (ctrls Controls) Enable() {\n\tfor _, ctrl := range ctrls {\n\t\tctrl.Enable()\n\t\tctrl.GetSubcontrols().Enable()\n\t}\n}\n\n// EnableControl validates that the specified control name is valid and enables `ctrl`.\nfunc (ctrls Controls) EnableControl(name string) error {\n\tif ctrl := ctrls.Find(name); ctrl != nil {\n\t\tctrl.Enable()\n\t\tctrl.GetSubcontrols().Enable()\n\n\t\treturn nil\n\t}\n\n\treturn NewInvalidControlNameError(ctrls.FilterByStatus(ActiveStatus).Names())\n}\n\n// LogEnabled logs the control names that are enabled.\nfunc (ctrls Controls) LogEnabled(logger log.Logger) {\n\tenabledControls := ctrls.FilterByEnabled()\n\n\tif len(enabledControls) > 0 {\n\t\tlogger.Debugf(\"Enabled strict control(s): %s\", enabledControls.Names())\n\t}\n}\n\n// LogCompletedControls warns about any completed controls from the given explicitly requested names.\nfunc (ctrls Controls) LogCompletedControls(logger log.Logger, requestedNames []string) {\n\tcompletedControls := ctrls.FilterByNames(requestedNames...).FilterByStatus(CompletedStatus)\n\n\tif len(completedControls) > 0 {\n\t\tlogger.Warnf(CompletedControlsFmt, completedControls.Names().String())\n\t}\n}\n\n// Evaluate returns an error if the one of the controls is enabled otherwise logs warning messages and returns nil.\nfunc (ctrls Controls) Evaluate(ctx context.Context) error {\n\tfor _, ctrl := range ctrls {\n\t\tif err := ctrl.Evaluate(ctx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// AddSubcontrols adds the given `newCtrls` as subcontrols into all `ctrls`.\nfunc (ctrls Controls) AddSubcontrols(newCtrls ...Control) {\n\tfor _, ctrl := range ctrls {\n\t\tctrl.AddSubcontrols(newCtrls...)\n\t}\n}\n\n// GetSubcontrols returns all subcontrols from all `ctrls`.\nfunc (ctrls Controls) GetSubcontrols() Controls {\n\tfound := make(Controls, 0, len(ctrls))\n\n\tfor _, ctrl := range ctrls {\n\t\tfound = append(found, ctrl.GetSubcontrols()...)\n\t}\n\n\treturn found\n}\n\nfunc (ctrls Controls) AddSubcontrolsToCategory(categoryName string, controls ...Control) {\n\tfor _, ctrl := range ctrls {\n\t\tcategory := ctrl.GetSubcontrols().GetCategories().Find(categoryName)\n\n\t\tif category == nil {\n\t\t\tcategory = &Category{Name: categoryName}\n\t\t}\n\n\t\tControls(controls).SetCategory(category)\n\n\t\tctrl.AddSubcontrols(controls...)\n\t}\n}\n\n// Find search control by given `name`, returns nil if not found.\nfunc (ctrls Controls) Find(name string) Control {\n\tfor _, ctrl := range ctrls {\n\t\tif ctrl != nil && ctrl.GetName() == name {\n\t\t\treturn ctrl\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Len implements `sort.Interface` interface.\nfunc (ctrls Controls) Len() int {\n\treturn len(ctrls)\n}\n\n// Less implements `sort.Interface` interface.\nfunc (ctrls Controls) Less(i, j int) bool {\n\tif len((ctrls)[j].GetName()) == 0 {\n\t\treturn false\n\t} else if len((ctrls)[i].GetName()) == 0 {\n\t\treturn true\n\t}\n\n\tif (ctrls)[i].GetStatus() == (ctrls)[j].GetStatus() {\n\t\treturn (ctrls)[i].GetName() < (ctrls)[j].GetName()\n\t}\n\n\treturn (ctrls)[i].GetStatus() < (ctrls)[j].GetStatus()\n}\n\n// Swap implements `sort.Interface` interface.\nfunc (ctrls Controls) Swap(i, j int) {\n\t(ctrls)[i], (ctrls)[j] = (ctrls)[j], (ctrls)[i]\n}\n\n// Sort returns `ctrls` in sorted order by `Name` and `Status`.\nfunc (ctrls Controls) Sort() Controls {\n\tsort.Sort(ctrls)\n\n\treturn ctrls\n}\n"
  },
  {
    "path": "internal/strict/control_test.go",
    "content": "package strict_test\n\n// Add some basic tests that confirm that by default, a warning is emitted when strict mode is disabled,\n// and an error is emitted when a specific control is enabled.\n// Make sure to test both when the specific control is enabled, and when the global strict mode is enabled.\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestParentAName     = \"test-parent-a\"\n\ttestOngoingAName    = \"test-ongoing-a\"\n\ttestOngoingSubAName = \"test-ongoing-sub-a\"\n\ttestOngoingBName    = \"test-ongoing-b\"\n\ttestOngoingCName    = \"test-ongoing-c\"\n\ttestCompletedAName  = \"test-completed-a\"\n\ttestCompletedBName  = \"test-completed-b\"\n\ttestCompletedCName  = \"test-completed-c\"\n)\n\nvar (\n\ttestOngoingSubA = func() *controls.Control {\n\t\treturn &controls.Control{\n\t\t\tName:    testOngoingSubAName,\n\t\t\tError:   errors.New(\"a error ongoing\"),\n\t\t\tWarning: \"sub a warning ongoing\",\n\t\t}\n\t}\n\n\ttestParentA = func() *controls.Control {\n\t\treturn &controls.Control{\n\t\t\tName: testParentAName,\n\t\t\tSubcontrols: strict.Controls{\n\t\t\t\ttestOngoingSubA(),\n\t\t\t},\n\t\t}\n\t}\n\n\ttestOngoingA = func() *controls.Control {\n\t\treturn &controls.Control{\n\t\t\tName: testOngoingAName,\n\t\t\tSubcontrols: strict.Controls{\n\t\t\t\ttestOngoingSubA(),\n\t\t\t},\n\t\t\tError:   errors.New(\"a error ongoing\"),\n\t\t\tWarning: \"a warning ongoing\",\n\t\t}\n\t}\n\ttestOngoingB = func() *controls.Control {\n\t\treturn &controls.Control{\n\t\t\tName:    testOngoingBName,\n\t\t\tError:   errors.New(\"error ongoing b\"),\n\t\t\tWarning: \"warning ongoing b\",\n\t\t}\n\t}\n\ttestOngoingC = func() *controls.Control {\n\t\treturn &controls.Control{\n\t\t\tName:    testOngoingCName,\n\t\t\tError:   errors.New(\"error ongoing c\"),\n\t\t\tWarning: \"warning ongoing c\",\n\t\t}\n\t}\n\ttestCompletedA = func() *controls.Control {\n\t\treturn &controls.Control{\n\t\t\tName:    testCompletedAName,\n\t\t\tStatus:  strict.CompletedStatus,\n\t\t\tError:   errors.New(\"no matter\"),\n\t\t\tWarning: \"no matter\",\n\t\t}\n\t}\n\ttestCompletedB = func() *controls.Control {\n\t\treturn &controls.Control{\n\t\t\tName:    testCompletedBName,\n\t\t\tStatus:  strict.CompletedStatus,\n\t\t\tError:   errors.New(\"no matter\"),\n\t\t\tWarning: \"no matter\",\n\t\t}\n\t}\n\ttestCompletedC = func() *controls.Control {\n\t\treturn &controls.Control{\n\t\t\tName:    testCompletedCName,\n\t\t\tStatus:  strict.CompletedStatus,\n\t\t\tError:   errors.New(\"no matter\"),\n\t\t\tWarning: \"no matter\",\n\t\t}\n\t}\n)\n\nfunc newTestLogger() (log.Logger, *bytes.Buffer) {\n\tformatter := format.NewFormatter(placeholders.Placeholders{placeholders.Message()})\n\toutput := new(bytes.Buffer)\n\tlogger := log.New(log.WithOutput(output), log.WithLevel(log.InfoLevel), log.WithFormatter(formatter))\n\n\treturn logger, output\n}\n\nfunc newTestControls() strict.Controls {\n\treturn strict.Controls{\n\t\ttestParentA(),\n\t\ttestOngoingA(),\n\t\ttestOngoingB(),\n\t\ttestOngoingC(),\n\t\ttestCompletedA(),\n\t\ttestCompletedB(),\n\t\ttestCompletedC(),\n\t}\n}\n\nfunc TestEnableControl(t *testing.T) {\n\tt.Parallel()\n\n\ttype testEnableControl struct {\n\t\texpectedErr error\n\t\tcontrolName string\n\t}\n\n\ttestCases := []struct {\n\t\texpectedCompletedMsg    string\n\t\tenableControls          []testEnableControl\n\t\texpectedEnabledControls []string\n\t}{\n\t\t{\n\t\t\tenableControls: []testEnableControl{\n\t\t\t\t{\n\t\t\t\t\tcontrolName: testOngoingAName,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tcontrolName: testOngoingCName,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tcontrolName: testCompletedAName,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tcontrolName: testCompletedCName,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tcontrolName: \"invalid\",\n\t\t\t\t\texpectedErr: strict.NewInvalidControlNameError([]string{testOngoingAName, testOngoingBName, testOngoingCName, testParentAName}),\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedEnabledControls: []string{testOngoingAName, testOngoingSubAName, testOngoingCName, testCompletedAName, testCompletedCName},\n\t\t\texpectedCompletedMsg:    fmt.Sprintf(strict.CompletedControlsFmt, strict.ControlNames([]string{testCompletedAName, testCompletedCName})),\n\t\t},\n\t\t{\n\t\t\tenableControls: []testEnableControl{\n\t\t\t\t{\n\t\t\t\t\tcontrolName: testOngoingBName,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tcontrolName: testCompletedBName,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedEnabledControls: []string{testOngoingBName, testCompletedBName},\n\t\t\texpectedCompletedMsg:    fmt.Sprintf(strict.CompletedControlsFmt, strict.ControlNames([]string{testCompletedBName})),\n\t\t},\n\t\t{\n\t\t\tenableControls:          []testEnableControl{},\n\t\t\texpectedEnabledControls: []string{},\n\t\t},\n\t\t{\n\t\t\tenableControls: []testEnableControl{\n\t\t\t\t{\n\t\t\t\t\tcontrolName: testParentAName,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedEnabledControls: []string{testParentAName, testOngoingSubAName},\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tcontrols := newTestControls()\n\n\t\t\tfor _, testEnableControl := range tc.enableControls {\n\t\t\t\terr := controls.EnableControl(testEnableControl.controlName)\n\n\t\t\t\tif testEnableControl.expectedErr != nil {\n\t\t\t\t\trequire.EqualError(t, err, testEnableControl.expectedErr.Error())\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tvar actualEnabledControls []string\n\n\t\t\tfor _, control := range controls {\n\t\t\t\tif control.GetEnabled() {\n\t\t\t\t\tactualEnabledControls = append(actualEnabledControls, control.GetName())\n\t\t\t\t}\n\n\t\t\t\tfor _, subcontrol := range control.GetSubcontrols() {\n\t\t\t\t\tif subcontrol.GetEnabled() {\n\t\t\t\t\t\tactualEnabledControls = append(actualEnabledControls, subcontrol.GetName())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tassert.ElementsMatch(t, tc.expectedEnabledControls, actualEnabledControls)\n\n\t\t\tlogger, output := newTestLogger()\n\n\t\t\trequestedNames := make([]string, 0, len(tc.enableControls))\n\n\t\t\tfor _, ec := range tc.enableControls {\n\t\t\t\tif ec.expectedErr == nil {\n\t\t\t\t\trequestedNames = append(requestedNames, ec.controlName)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcontrols.LogEnabled(logger)\n\t\t\tcontrols.LogCompletedControls(logger, requestedNames)\n\n\t\t\tif tc.expectedCompletedMsg == \"\" {\n\t\t\t\tassert.Empty(t, output.String())\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tassert.Contains(t, strings.TrimSpace(output.String()), tc.expectedCompletedMsg)\n\t\t})\n\t}\n}\n\nfunc TestEnableStrictMode(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedEnabledControls []string\n\t\tenableStrictMode        bool\n\t}{\n\t\t{\n\t\t\tenableStrictMode:        true,\n\t\t\texpectedEnabledControls: []string{testParentAName, testOngoingSubAName, testOngoingAName, testOngoingSubAName, testOngoingBName, testOngoingCName},\n\t\t},\n\t\t{\n\t\t\tenableStrictMode:        false,\n\t\t\texpectedEnabledControls: []string{},\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tcontrols := newTestControls()\n\n\t\t\tif tc.enableStrictMode {\n\t\t\t\tcontrols.FilterByStatus(strict.ActiveStatus).Enable()\n\t\t\t}\n\n\t\t\tvar actualEnabledControls []string\n\n\t\t\tfor _, control := range controls {\n\t\t\t\tif control.GetEnabled() {\n\t\t\t\t\tactualEnabledControls = append(actualEnabledControls, control.GetName())\n\t\t\t\t}\n\n\t\t\t\tfor _, subcontrol := range control.GetSubcontrols() {\n\t\t\t\t\tif subcontrol.GetEnabled() {\n\t\t\t\t\t\tactualEnabledControls = append(actualEnabledControls, subcontrol.GetName())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tassert.ElementsMatch(t, tc.expectedEnabledControls, actualEnabledControls)\n\n\t\t\tlogger, output := newTestLogger()\n\n\t\t\tcontrols.LogEnabled(logger)\n\t\t\tassert.Empty(t, output.String())\n\t\t})\n\t}\n}\n\n// TestLogCompletedControlsWithParentAndCompletedSubcontrol tests that LogCompletedControls\n// only warns about explicitly requested controls, not implicitly enabled subcontrols.\n// This is a regression test for https://github.com/gruntwork-io/terragrunt/issues/5293\nfunc TestLogCompletedControlsWithParentAndCompletedSubcontrol(t *testing.T) {\n\tt.Parallel()\n\n\tcompletedSubControl := &controls.Control{\n\t\tName:    \"completed-sub\",\n\t\tStatus:  strict.CompletedStatus,\n\t\tError:   errors.New(\"completed error\"),\n\t\tWarning: \"completed warning\",\n\t}\n\n\tparentControl := &controls.Control{\n\t\tName: \"parent-control\",\n\t\tSubcontrols: strict.Controls{\n\t\t\tcompletedSubControl,\n\t\t},\n\t}\n\n\ttestControls := strict.Controls{\n\t\tparentControl,\n\t\tcompletedSubControl,\n\t}\n\n\terr := testControls.EnableControl(\"parent-control\")\n\trequire.NoError(t, err)\n\n\tassert.True(t, completedSubControl.GetEnabled(), \"subcontrol should be enabled\")\n\n\tlogger, output := newTestLogger()\n\ttestControls.LogCompletedControls(logger, []string{\"parent-control\"})\n\n\tassert.Empty(t, output.String(), \"should not warn about implicitly enabled completed subcontrols\")\n}\n\n// TestLogCompletedControlsWithExplicitlyRequestedCompletedControl tests that LogCompletedControls\n// DOES warn when a completed control is explicitly requested.\nfunc TestLogCompletedControlsWithExplicitlyRequestedCompletedControl(t *testing.T) {\n\tt.Parallel()\n\n\tcompletedControl := &controls.Control{\n\t\tName:    \"completed-control\",\n\t\tStatus:  strict.CompletedStatus,\n\t\tError:   errors.New(\"completed error\"),\n\t\tWarning: \"completed warning\",\n\t}\n\n\ttestControls := strict.Controls{\n\t\tcompletedControl,\n\t}\n\n\terr := testControls.EnableControl(\"completed-control\")\n\trequire.NoError(t, err)\n\n\tlogger, output := newTestLogger()\n\ttestControls.LogCompletedControls(logger, []string{\"completed-control\"})\n\n\tassert.Contains(\n\t\tt,\n\t\toutput.String(),\n\t\t\"completed-control\",\n\t\t\"should warn about explicitly requested completed control\",\n\t)\n}\n\nfunc TestEvaluateControl(t *testing.T) {\n\tt.Parallel()\n\n\ttype testEvaluateControl struct {\n\t\texpectedErr error\n\t\tname        string\n\t}\n\n\ttestCases := []struct {\n\t\tenableControls   []string\n\t\tevaluateControls []testEvaluateControl\n\t\texpectedWarns    []string\n\t}{\n\t\t{\n\t\t\tenableControls: []string{testOngoingAName, testOngoingBName},\n\t\t\tevaluateControls: []testEvaluateControl{\n\t\t\t\t{\n\t\t\t\t\tname:        testOngoingAName,\n\t\t\t\t\texpectedErr: testOngoingA().Error,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedWarns: []string{\"\"},\n\t\t},\n\t\t{\n\t\t\tenableControls: []string{testOngoingBName},\n\t\t\tevaluateControls: []testEvaluateControl{\n\t\t\t\t{\n\t\t\t\t\tname:        testOngoingBName,\n\t\t\t\t\texpectedErr: testOngoingB().Error,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedWarns: []string{\"\"},\n\t\t},\n\t\t{\n\t\t\t// Testing output warning message once.\n\t\t\tenableControls: []string{testOngoingBName},\n\t\t\tevaluateControls: []testEvaluateControl{\n\t\t\t\t{\n\t\t\t\t\tname: testOngoingAName,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tname: testOngoingAName,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedWarns: []string{testOngoingA().Warning, testOngoingSubA().Warning},\n\t\t},\n\t\t{\n\t\t\tenableControls: []string{testCompletedAName},\n\t\t\tevaluateControls: []testEvaluateControl{\n\t\t\t\t{\n\t\t\t\t\tname: testOngoingAName,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedWarns: []string{testOngoingA().Warning, testOngoingSubA().Warning},\n\t\t},\n\t\t{\n\t\t\tenableControls: []string{testCompletedAName},\n\t\t\tevaluateControls: []testEvaluateControl{\n\t\t\t\t{\n\t\t\t\t\tname: testCompletedAName,\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedWarns: []string{\"\"},\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tlogger, output := newTestLogger()\n\t\t\tcontrols := newTestControls()\n\n\t\t\tctx := t.Context()\n\t\t\tctx = log.ContextWithLogger(ctx, logger)\n\n\t\t\tfor _, name := range tc.enableControls {\n\t\t\t\terr := controls.EnableControl(name)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tfor _, control := range tc.evaluateControls {\n\t\t\t\terr := controls.Find(control.name).Evaluate(ctx)\n\t\t\t\tif control.expectedErr != nil {\n\t\t\t\t\trequire.EqualError(t, err, control.expectedErr.Error())\n\t\t\t\t\tassert.Empty(t, output.String())\n\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tif len(tc.expectedWarns) == 0 {\n\t\t\t\tassert.Empty(t, output.String())\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tactualWarns := strings.Split(strings.TrimSpace(output.String()), \"\\n\")\n\t\t\tassert.ElementsMatch(t, actualWarns, tc.expectedWarns)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/strict/controls/control.go",
    "content": "package controls\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nvar _ = strict.Control(new(Control))\n\n// Control is the simplest implementation of the `strict.Control` interface.\ntype Control struct {\n\t// Error is the Error that will be returned when the Control is Enabled.\n\tError error\n\n\t// Category is the category of the control.\n\tCategory *strict.Category\n\n\t// Name is the name of the control.\n\tName string\n\n\t// Description is the description of the control.\n\tDescription string\n\n\t// Warning is a Warning that will be logged when the Control is not Enabled.\n\tWarning string\n\n\t// Subcontrols are child controls.\n\tSubcontrols strict.Controls\n\n\t// OnceWarn is used to prevent the warning message from being displayed multiple times.\n\tOnceWarn sync.Once\n\n\t// Status of the strict Control.\n\tStatus strict.Status\n\n\t// Enabled indicates whether the control is enabled.\n\tEnabled bool\n\n\t// Suppress suppresses the warning message from being displayed.\n\t// Uses int32 for atomic operations (0 = false, 1 = true)\n\tsuppress int32\n}\n\n// String implements `fmt.Stringer` interface.\nfunc (ctrl *Control) String() string {\n\treturn ctrl.GetName()\n}\n\n// GetName implements `strict.Control` interface.\nfunc (ctrl *Control) GetName() string {\n\treturn ctrl.Name\n}\n\n// GetDescription implements `strict.Control` interface.\nfunc (ctrl *Control) GetDescription() string {\n\treturn ctrl.Description\n}\n\n// GetStatus implements `strict.Control` interface.\nfunc (ctrl *Control) GetStatus() strict.Status {\n\treturn ctrl.Status\n}\n\n// GetEnabled implements `strict.Control` interface.\nfunc (ctrl *Control) GetEnabled() bool {\n\treturn ctrl.Enabled\n}\n\n// GetCategory implements `strict.Control` interface.\nfunc (ctrl *Control) GetCategory() *strict.Category {\n\treturn ctrl.Category\n}\n\n// SetCategory implements `strict.Control` interface.\nfunc (ctrl *Control) SetCategory(category *strict.Category) {\n\tctrl.Category = category\n}\n\n// Enable implements `strict.Control` interface.\nfunc (ctrl *Control) Enable() {\n\tctrl.Enabled = true\n}\n\n// GetSubcontrols implements `strict.Control` interface.\nfunc (ctrl *Control) GetSubcontrols() strict.Controls {\n\treturn ctrl.Subcontrols\n}\n\n// AddSubcontrols implements `strict.Control` interface.\nfunc (ctrl *Control) AddSubcontrols(newCtrls ...strict.Control) {\n\tif ctrl.Subcontrols == nil {\n\t\tctrl.Subcontrols = make([]strict.Control, 0, len(newCtrls))\n\t}\n\n\tctrl.Subcontrols = append(ctrl.Subcontrols, newCtrls...)\n}\n\n// SuppressWarning suppresses the warning message from being displayed.\nfunc (ctrl *Control) SuppressWarning() {\n\tatomic.StoreInt32(&ctrl.suppress, 1)\n}\n\n// isSuppressed returns true if warning is suppressed.\nfunc (ctrl *Control) isSuppressed() bool {\n\treturn atomic.LoadInt32(&ctrl.suppress) == 1\n}\n\n// Evaluate implements `strict.Control` interface.\nfunc (ctrl *Control) Evaluate(ctx context.Context) error {\n\tif err := ctx.Err(); err != nil {\n\t\treturn errors.Errorf(\"context error during evaluation: %w\", err)\n\t}\n\n\tif ctrl == nil {\n\t\treturn nil\n\t}\n\n\tif ctrl.Enabled {\n\t\tif ctrl.Status != strict.ActiveStatus || ctrl.Error == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn ctrl.Error\n\t}\n\n\tif logger := log.LoggerFromContext(ctx); logger != nil && ctrl.Warning != \"\" && !ctrl.isSuppressed() {\n\t\tctrl.OnceWarn.Do(func() {\n\t\t\tlogger.Warn(ctrl.Warning)\n\t\t})\n\t}\n\n\tif ctrl.Subcontrols == nil {\n\t\treturn nil\n\t}\n\n\treturn ctrl.Subcontrols.Evaluate(ctx)\n}\n"
  },
  {
    "path": "internal/strict/controls/controls.go",
    "content": "// Package controls contains strict controls.\npackage controls\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict\"\n)\n\nconst (\n\t// DeprecatedCommands is the control that prevents the use of deprecated commands.\n\tDeprecatedCommands = \"deprecated-commands\"\n\n\t// DeprecatedFlags is the control that prevents the use of deprecated flag names.\n\tDeprecatedFlags = \"deprecated-flags\"\n\n\t// DeprecatedEnvVars is the control that prevents the use of deprecated env vars.\n\tDeprecatedEnvVars = \"deprecated-env-vars\"\n\n\t// DeprecatedConfigs is the control that prevents the use of deprecated config fields/section/..., anything related to config syntax.\n\tDeprecatedConfigs = \"deprecated-configs\"\n\n\t// LegacyLogs is a control group for legacy log flags that were in use before the log was redesign.\n\tLegacyLogs = \"legacy-logs\"\n\n\t// TerragruntPrefixFlags is a control group for flags that used to have the `terragrunt-` prefix.\n\tTerragruntPrefixFlags = \"terragrunt-prefix-flags\"\n\n\t// TerragruntPrefixEnvVars is a control group for env vars that used to have the `TERRAGRUNT_` prefix.\n\tTerragruntPrefixEnvVars = \"terragrunt-prefix-env-vars\"\n\n\t// DefaultCommands is a control group for TF commands that were used as default commands,\n\t// namely without using the parent `run` commands and were not shortcuts commands.\n\tDefaultCommands = \"default-commands\"\n\n\t// RootTerragruntHCL is the control that prevents usage of a `terragrunt.hcl` file as the root of Terragrunt configurations.\n\tRootTerragruntHCL = \"root-terragrunt-hcl\"\n\n\t// SkipDependenciesInputs is the control related to the deprecated dependency inputs feature.\n\t// Dependency inputs are now disabled by default for performance.\n\tSkipDependenciesInputs = \"skip-dependencies-inputs\"\n\n\t// RequireExplicitBootstrap is the control that prevents the backend for remote state from being bootstrapped unless the `--backend-bootstrap` flag is specified.\n\tRequireExplicitBootstrap = \"require-explicit-bootstrap\"\n\n\t// CLIRedesign is the control that prevents the use of commands deprecated as part of the CLI Redesign.\n\tCLIRedesign = \"cli-redesign\"\n\n\t// LegacyAll is a control group for the legacy *-all commands.\n\t// This control is marked as completed since the commands have been removed.\n\tLegacyAll = \"legacy-all\"\n\n\t// BareInclude is the control that prevents the use of the `include` block without a label.\n\tBareInclude = \"bare-include\"\n\n\t// DoubleStar enables the use of the `**` glob pattern as a way to match files in subdirectories.\n\t// and will log a warning when using **/*\n\tDoubleStar = \"double-star\"\n\n\t// QueueExcludeExternal is the control that prevents the use of the deprecated `--queue-exclude-external` flag.\n\tQueueExcludeExternal = \"queue-exclude-external\"\n\n\t// QueueStrictInclude is the control that prevents the use of the deprecated `--queue-strict-include` flag.\n\tQueueStrictInclude = \"queue-strict-include\"\n\n\t// UnitsThatInclude is the control that prevents the use of the deprecated `--units-that-include` flag.\n\tUnitsThatInclude = \"units-that-include\"\n\n\t// DisableCommandValidation is the control that prevents the use of the deprecated `--disable-command-validation` flag.\n\tDisableCommandValidation = \"disable-command-validation\"\n\n\t// NoDestroyDependenciesCheck is the control that prevents the use of the deprecated `--no-destroy-dependencies-check` flag.\n\tNoDestroyDependenciesCheck = \"no-destroy-dependencies-check\"\n\n\t// InternalTFLint is the control that prevents the use of the deprecated embedded version of tflint.\n\tInternalTFLint = \"legacy-internal-tflint\"\n\n\t// DeprecatedHiddenFlag is the control that prevents the use of the deprecated `--hidden` flag.\n\tDeprecatedHiddenFlag = \"deprecated-hidden-flag\"\n\n\t// DisableDependentModules is the control that prevents the use of the deprecated `--disable-dependent-modules` flag.\n\tDisableDependentModules = \"disable-dependent-modules\"\n)\n\n//nolint:lll\nfunc New() strict.Controls {\n\tlifecycleCategory := &strict.Category{\n\t\tName: \"Lifecycle controls\",\n\t}\n\tstageCategory := &strict.Category{\n\t\tName: \"Stage controls\",\n\t}\n\n\tskipDependenciesInputsControl := &Control{\n\t\tName:        SkipDependenciesInputs,\n\t\tDescription: \"Controls whether to allow the deprecated dependency inputs feature. Dependency inputs are now disabled by default for performance. Use dependency outputs instead.\",\n\t\tError:       errors.Errorf(\"Reading inputs from dependencies is no longer supported. To acquire values from dependencies, use outputs.\"),\n\t\tWarning:     \"Reading inputs from dependencies has been deprecated and is now disabled by default for performance. Use dependency outputs instead.\",\n\t\tCategory:    stageCategory,\n\t\tStatus:      strict.CompletedStatus,\n\t}\n\n\trequireExplicitBootstrapControl := &Control{\n\t\tName:        RequireExplicitBootstrap,\n\t\tDescription: \"Don't bootstrap backends by default. When enabled, users must supply `--backend-bootstrap` explicitly to automatically bootstrap backend resources.\",\n\t\tError:       errors.Errorf(\"Bootstrap backend for remote state by default is no longer supported. Use `--backend-bootstrap` flag instead.\"),\n\t\tWarning:     \"Bootstrapping backend resources by default is deprecated functionality, and will not be the default behavior in a future version of Terragrunt. Use the explicit `--backend-bootstrap` flag to automatically provision backend resources before they're needed.\",\n\t\tCategory:    stageCategory,\n\t\tStatus:      strict.CompletedStatus,\n\t}\n\n\tcontrols := strict.Controls{\n\t\t&Control{\n\t\t\tName:        DeprecatedCommands,\n\t\t\tDescription: \"Prevents deprecated commands from being used.\",\n\t\t\tCategory:    lifecycleCategory,\n\t\t},\n\t\t&Control{\n\t\t\tName:        DeprecatedFlags,\n\t\t\tDescription: \"Prevents deprecated flags from being used.\",\n\t\t\tCategory:    lifecycleCategory,\n\t\t},\n\t\t&Control{\n\t\t\tName:        DeprecatedEnvVars,\n\t\t\tDescription: \"Prevents deprecated env vars from being used.\",\n\t\t\tCategory:    lifecycleCategory,\n\t\t},\n\t\t&Control{\n\t\t\tName:        DeprecatedConfigs,\n\t\t\tDescription: \"Prevents deprecated config syntax from being used.\",\n\t\t\tCategory:    lifecycleCategory,\n\t\t\tSubcontrols: strict.Controls{\n\t\t\t\tskipDependenciesInputsControl,\n\t\t\t\trequireExplicitBootstrapControl,\n\t\t\t},\n\t\t},\n\t\tskipDependenciesInputsControl,\n\t\trequireExplicitBootstrapControl,\n\t\t&Control{\n\t\t\tName:        CLIRedesign,\n\t\t\tDescription: \"Prevents the use of commands deprecated as part of the CLI Redesign.\",\n\t\t\tCategory:    stageCategory,\n\t\t},\n\t\t&Control{\n\t\t\tName:        LegacyAll,\n\t\t\tDescription: \"Prevents old *-all commands such as plan-all from being used.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tStatus:      strict.CompletedStatus,\n\t\t},\n\t\t&Control{\n\t\t\tName:        \"spin-up\",\n\t\t\tDescription: \"Prevents the deprecated spin-up command from being used.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tStatus:      strict.CompletedStatus,\n\t\t},\n\t\t&Control{\n\t\t\tName:        \"tear-down\",\n\t\t\tDescription: \"Prevents the deprecated tear-down command from being used.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tStatus:      strict.CompletedStatus,\n\t\t},\n\t\t&Control{\n\t\t\tName:        \"plan-all\",\n\t\t\tDescription: \"Prevents the deprecated plan-all command from being used.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tStatus:      strict.CompletedStatus,\n\t\t},\n\t\t&Control{\n\t\t\tName:        \"apply-all\",\n\t\t\tDescription: \"Prevents the deprecated apply-all command from being used.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tStatus:      strict.CompletedStatus,\n\t\t},\n\t\t&Control{\n\t\t\tName:        \"destroy-all\",\n\t\t\tDescription: \"Prevents the deprecated destroy-all command from being used.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tStatus:      strict.CompletedStatus,\n\t\t},\n\t\t&Control{\n\t\t\tName:        \"output-all\",\n\t\t\tDescription: \"Prevents the deprecated output-all command from being used.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tStatus:      strict.CompletedStatus,\n\t\t},\n\t\t&Control{\n\t\t\tName:        \"validate-all\",\n\t\t\tDescription: \"Prevents the deprecated validate-all command from being used.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tStatus:      strict.CompletedStatus,\n\t\t},\n\n\t\t&Control{\n\t\t\tName:        TerragruntPrefixFlags,\n\t\t\tDescription: \"Prevents deprecated flags with `terragrunt-` prefixes from being used.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tStatus:      strict.CompletedStatus,\n\t\t},\n\t\t&Control{\n\t\t\tName:        TerragruntPrefixEnvVars,\n\t\t\tDescription: \"Prevents deprecated env vars with `TERRAGRUNT_` prefixes from being used.\",\n\t\t\tCategory:    stageCategory,\n\t\t},\n\t\t&Control{\n\t\t\tName:        DefaultCommands,\n\t\t\tDescription: \"Prevents default commands from being used.\",\n\t\t\tCategory:    stageCategory,\n\t\t},\n\t\t&Control{\n\t\t\tName:        LegacyLogs,\n\t\t\tDescription: \"Prevents old log flags from being used.\",\n\t\t\tCategory:    stageCategory,\n\t\t},\n\t\t&Control{\n\t\t\tName:        RootTerragruntHCL,\n\t\t\tDescription: \"Throw an error when users try to reference a root terragrunt.hcl file using find_in_parent_folders.\",\n\t\t\tError:       errors.New(\"Using `terragrunt.hcl` as the root of Terragrunt configurations is an anti-pattern, and no longer supported. Use a differently named file like `root.hcl` instead. For more information, see https://docs.terragrunt.com/migrate/migrating-from-root-terragrunt-hcl\"),\n\t\t\tWarning:     \"Using `terragrunt.hcl` as the root of Terragrunt configurations is an anti-pattern, and no longer recommended. In a future version of Terragrunt, this will result in an error. You are advised to use a differently named file like `root.hcl` instead. For more information, see https://docs.terragrunt.com/migrate/migrating-from-root-terragrunt-hcl\",\n\t\t\tCategory:    stageCategory,\n\t\t},\n\n\t\t&Control{\n\t\t\tName:        BareInclude,\n\t\t\tDescription: \"Prevents the use of the `include` block without a label.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tError:       errors.New(\"Using an `include` block without a label is deprecated. Please use the `include` block with a label instead.\"),\n\t\t\tWarning:     \"Using an `include` block without a label is deprecated. Please use the `include` block with a label instead. For more information, see https://docs.terragrunt.com/migrate/bare-include/\",\n\t\t},\n\n\t\t&Control{\n\t\t\tName:        DoubleStar,\n\t\t\tDescription: \"Use the `**` glob pattern to select all files in a directory and its subdirectories.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tError:       errors.New(\"Using `**` to select all files in a directory and its subdirectories is enabled. **/* now matches subdirectories with at least a depth of one.\"),\n\t\t\tWarning:     \"Using `**` to select all files in a directory and its subdirectories is enabled. **/* now matches subdirectories with at least a depth of one.\",\n\t\t\tStatus:      strict.CompletedStatus,\n\t\t},\n\t\t&Control{\n\t\t\tName:        QueueExcludeExternal,\n\t\t\tDescription: \"Prevents the use of the deprecated `--queue-exclude-external` flag.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tError:       errors.New(\"The `--queue-exclude-external` flag is no longer supported. External dependencies are now excluded by default. Use --queue-include-external to include them.\"),\n\t\t\tWarning:     \"The `--queue-exclude-external` flag is deprecated and will be removed in a future version of Terragrunt. External dependencies are now excluded by default.\",\n\t\t},\n\t\t&Control{\n\t\t\tName:        QueueStrictInclude,\n\t\t\tDescription: \"Prevents the use of the deprecated `--queue-strict-include` flag.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tError:       errors.New(\"The `--queue-strict-include` flag is no longer supported. The behavior of Terragrunt when using `--queue-strict-include` is now the default behavior.\"),\n\t\t\tWarning:     \"The `--queue-strict-include` flag is deprecated and will be removed in a future version of Terragrunt. The behavior of Terragrunt when using `--queue-strict-include` is now the default behavior.\",\n\t\t},\n\t\t&Control{\n\t\t\tName:        UnitsThatInclude,\n\t\t\tDescription: \"Prevents the use of the deprecated `--units-that-include` flag.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tError:       errors.New(\"The `--units-that-include` flag is no longer supported. Use `--filter='reading=<path>'` to include units that include or read the specified configuration.\"),\n\t\t\tWarning:     \"The `--units-that-include` flag is deprecated and will be removed in a future version of Terragrunt. Use `--filter='reading=<path>'` to include units that include or read the specified configuration.\",\n\t\t},\n\t\t&Control{\n\t\t\tName:        DisableCommandValidation,\n\t\t\tDescription: \"Prevents the use of the deprecated `--disable-command-validation` flag. Command validation has been removed entirely.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tError:       errors.New(\"The `--disable-command-validation` flag is no longer supported. Command validation has been removed entirely, and you can pass any command to `terragrunt run`.\"),\n\t\t\tWarning:     \"The `--disable-command-validation` flag is deprecated and will be removed in a future version of Terragrunt. Command validation has been removed entirely, and you can pass any command to `terragrunt run`.\",\n\t\t},\n\t\t&Control{\n\t\t\tName:        NoDestroyDependenciesCheck,\n\t\t\tDescription: \"Prevents the use of the deprecated `--no-destroy-dependencies-check` flag. This flag is now ignored. Use `--destroy-dependencies-check` to enable dependency checks during destroy operations.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tError:       errors.New(\"The `--no-destroy-dependencies-check` flag is no longer supported. Use `--destroy-dependencies-check` to enable dependency checks during destroy operations.\"),\n\t\t\tWarning:     \"The `--no-destroy-dependencies-check` flag is deprecated and will be removed in a future version of Terragrunt. This flag is now ignored. Use `--destroy-dependencies-check` to enable dependency checks during destroy operations.\",\n\t\t},\n\t\t&Control{\n\t\t\tName:        InternalTFLint,\n\t\t\tDescription: \"Prevents the use of the deprecated embedded version of tflint, instead treating `tflint` as a normal hook.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tError:       errors.New(\"The embedded version of tflint is no longer supported. Use the `--terragrunt-external-tflint` flag in your hook to opt in to running tflint externally.\"),\n\t\t\tWarning:     \"The embedded version of tflint is deprecated and will be removed in a future version of Terragrunt. Use the `--terragrunt-external-tflint` flag in your hook to opt in to running tflint externally and avoid this warning.\",\n\t\t},\n\t\t&Control{\n\t\t\tName:        DeprecatedHiddenFlag,\n\t\t\tDescription: \"Prevents the use of the deprecated `--hidden` flag.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tError:       errors.New(\"The `--hidden` flag is no longer supported. Hidden directories are now included by default. Use `--no-hidden` to exclude them.\"),\n\t\t\tWarning:     \"The `--hidden` flag is deprecated and will be removed in a future version of Terragrunt. Hidden directories are now included by default. Use `--no-hidden` to exclude them.\",\n\t\t},\n\t\t&Control{\n\t\t\tName:        DisableDependentModules,\n\t\t\tDescription: \"Prevents the use of the deprecated `--disable-dependent-modules` flag.\",\n\t\t\tCategory:    stageCategory,\n\t\t\tError:       errors.New(\"The `--disable-dependent-modules` flag is no longer supported. Dependent modules discovery has been removed from `terragrunt render`.\"),\n\t\t\tWarning:     \"The `--disable-dependent-modules` flag is deprecated and will be removed in a future version of Terragrunt. Dependent modules discovery has been removed from `terragrunt render`, so this flag has no effect.\",\n\t\t},\n\t}\n\n\treturn controls.Sort()\n}\n"
  },
  {
    "path": "internal/strict/controls/deprecated_command.go",
    "content": "package controls\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\nconst (\n\tCLIRedesignCommandsCategoryName = \"CLI redesign commands\"\n)\n\n// NewDeprecatedReplacedCommand declares the deprecated command that has an alternative command.\nfunc NewDeprecatedReplacedCommand(command, newCommand string) *Control {\n\treturn &Control{\n\t\tName:        command,\n\t\tDescription: \"replaced with: \" + newCommand,\n\t\tError:       errors.Errorf(\"The `%s` command is no longer supported. Use `%s` instead.\", command, newCommand),\n\t\tWarning:     fmt.Sprintf(\"The `%s` command is deprecated and will be removed in a future version of Terragrunt. Use `%s` instead.\", command, newCommand),\n\t}\n}\n\n// NewDeprecatedCommand declares the deprecated command.\nfunc NewDeprecatedCommand(command string) *Control {\n\treturn &Control{\n\t\tName:        command,\n\t\tDescription: \"no replaced command\",\n\t\tError:       errors.Errorf(\"The `%s` command is no longer supported.\", command),\n\t\tWarning:     fmt.Sprintf(\"The `%s` command is deprecated and will be removed in a future version of Terragrunt.\", command),\n\t}\n}\n"
  },
  {
    "path": "internal/strict/controls/deprecated_env_var.go",
    "content": "package controls\n\nimport (\n\t\"context\"\n\t\"slices\"\n\t\"strconv\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst (\n\tGlobalEnvVarsCategoryName     = \"Global env vars\"\n\tCommandEnvVarsCategoryNameFmt = \"`%s` command env vars\"\n)\n\nvar _ = strict.Control(new(DeprecatedEnvVar))\n\n// DeprecatedEnvVar is strict control for deprecated environment variables.\ntype DeprecatedEnvVar struct {\n\tdeprecatedFlag clihelper.Flag\n\tnewFlag        clihelper.Flag\n\t*Control\n\tErrorFmt   string\n\tWarningFmt string\n}\n\n// NewDeprecatedEnvVar returns a new `DeprecatedEnvVar` instance.\n// Since we don't know which env vars can be used at the time of definition,\n// we take the first env var from the list `GetEnvVars()` for the name and description to display it in `info strict`.\nfunc NewDeprecatedEnvVar(deprecatedFlag, newFlag clihelper.Flag, newValue string) *DeprecatedEnvVar {\n\tvar (\n\t\tdeprecatedName = util.FirstNonEmpty(deprecatedFlag.GetEnvVars())\n\t\tnewName        = util.FirstNonEmpty(newFlag.GetEnvVars())\n\t)\n\n\tif newValue != \"\" {\n\t\tnewName += \"=\" + newValue\n\t}\n\n\treturn &DeprecatedEnvVar{\n\t\tControl: &Control{\n\t\t\tName:        deprecatedName,\n\t\t\tDescription: \"replaced with: \" + newName,\n\t\t},\n\t\tErrorFmt: \"The `%s` environment variable is no longer supported. Use `%s` instead.\",\n\t\t// The `TERRAGRUNT_LOG_LEVEL` environment variable is deprecated and will be removed in a future version of Terragrunt. Use `TG_LOG_LEVEL=trace` instead.\n\t\tWarningFmt:     \"The `%s` environment variable is deprecated and will be removed in a future version of Terragrunt. Use `%s` instead.\",\n\t\tdeprecatedFlag: deprecatedFlag,\n\t\tnewFlag:        newFlag,\n\t}\n}\n\n// Evaluate implements `strict.Control` interface.\nfunc (ctrl *DeprecatedEnvVar) Evaluate(ctx context.Context) error {\n\tvar (\n\t\tvalueName = ctrl.deprecatedFlag.Value().GetName()\n\t\tenvName   string\n\t)\n\n\tif valueName == \"\" || !ctrl.deprecatedFlag.Value().IsEnvSet() || !slices.Contains(ctrl.deprecatedFlag.GetEnvVars(), valueName) {\n\t\treturn nil\n\t}\n\n\tif names := ctrl.newFlag.GetEnvVars(); len(names) > 0 {\n\t\tenvName = names[0]\n\n\t\tvalue := ctrl.newFlag.Value().String()\n\n\t\tif v, ok := ctrl.newFlag.Value().Get().(bool); ok && ctrl.newFlag.Value().IsNegativeBoolFlag() {\n\t\t\tvalue = strconv.FormatBool(!v)\n\t\t}\n\n\t\tif value == \"\" {\n\t\t\tvalue = ctrl.deprecatedFlag.Value().String()\n\t\t}\n\n\t\tenvName += \"=\" + value\n\t}\n\n\tif ctrl.Enabled {\n\t\tif ctrl.Status != strict.ActiveStatus || ctrl.ErrorFmt == \"\" {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn errors.Errorf(ctrl.ErrorFmt, valueName, envName)\n\t}\n\n\tif logger := log.LoggerFromContext(ctx); logger != nil && ctrl.WarningFmt != \"\" && !ctrl.isSuppressed() {\n\t\tctrl.OnceWarn.Do(func() {\n\t\t\tlogger.Warnf(ctrl.WarningFmt, valueName, envName)\n\t\t})\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/strict/controls/deprecated_flag_name.go",
    "content": "package controls\n\nimport (\n\t\"context\"\n\t\"slices\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst (\n\tGlobalFlagsCategoryName     = \"Global flags\"\n\tCommandFlagsCategoryNameFmt = \"`%s` command flags\"\n)\n\nvar _ = strict.Control(new(DeprecatedFlagName))\n\n// DeprecatedFlagName is strict control for deprecated flag names.\ntype DeprecatedFlagName struct {\n\tdeprecatedFlag clihelper.Flag\n\tnewFlag        clihelper.Flag\n\t*Control\n\tErrorFmt   string\n\tWarningFmt string\n}\n\n// NewDeprecatedFlagName returns a new `DeprecatedFlagName` instance.\n// Since we don't know which names can be used at the time of definition,\n// we take the first name from the list `Names()` for the name and description to display it in `info strict`.\nfunc NewDeprecatedFlagName(deprecatedFlag, newFlag clihelper.Flag, newValue string) *DeprecatedFlagName {\n\tvar (\n\t\tdeprecatedName = util.FirstNonEmpty(deprecatedFlag.Names())\n\t\tnewName        = util.FirstNonEmpty(newFlag.Names())\n\t)\n\n\tif newValue != \"\" {\n\t\tnewName += \"=\" + newValue\n\t}\n\n\treturn &DeprecatedFlagName{\n\t\tControl: &Control{\n\t\t\tName:        deprecatedName,\n\t\t\tDescription: \"replaced with: \" + newName,\n\t\t},\n\t\tErrorFmt: \"The `--%s` flag is no longer supported. Use `--%s` instead.\",\n\t\t// Output example:\n\t\t// The `--terragrunt-working-dir` flag is deprecated and will be removed in a future version of Terragrunt. Use `--working-dir=./test/fixtures/extra-args/` instead.\n\t\tWarningFmt:     \"The `--%s` flag is deprecated and will be removed in a future version of Terragrunt. Use `--%s` instead.\",\n\t\tdeprecatedFlag: deprecatedFlag,\n\t\tnewFlag:        newFlag,\n\t}\n}\n\n// Evaluate implements `strict.Control` interface.\nfunc (ctrl *DeprecatedFlagName) Evaluate(ctx context.Context) error {\n\tvar (\n\t\tvalueName = ctrl.deprecatedFlag.Value().GetName()\n\t\tflagName  string\n\t)\n\n\tif valueName == \"\" || !ctrl.deprecatedFlag.Value().IsArgSet() || !slices.Contains(ctrl.deprecatedFlag.Names(), valueName) {\n\t\treturn nil\n\t}\n\n\tif names := ctrl.newFlag.Names(); len(names) > 0 {\n\t\tflagName = names[0]\n\n\t\tif ctrl.newFlag.TakesValue() {\n\t\t\tvalue := ctrl.newFlag.Value().String()\n\n\t\t\tif value == \"\" {\n\t\t\t\tvalue = ctrl.deprecatedFlag.Value().String()\n\t\t\t}\n\n\t\t\tflagName += \"=\" + value\n\t\t}\n\t}\n\n\tif ctrl.Enabled {\n\t\tif ctrl.Status != strict.ActiveStatus || ctrl.ErrorFmt == \"\" {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn errors.Errorf(ctrl.ErrorFmt, valueName, flagName)\n\t}\n\n\tif logger := log.LoggerFromContext(ctx); logger != nil && ctrl.WarningFmt != \"\" && !ctrl.isSuppressed() {\n\t\tctrl.OnceWarn.Do(func() {\n\t\t\tlogger.Warnf(ctrl.WarningFmt, valueName, flagName)\n\t\t})\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/strict/errors.go",
    "content": "package strict\n\n// InvalidControlNameError is an error that is returned when an invalid control name is requested.\ntype InvalidControlNameError struct {\n\tallowedNames ControlNames\n}\n\nfunc NewInvalidControlNameError(allowedNames ControlNames) *InvalidControlNameError {\n\treturn &InvalidControlNameError{\n\t\tallowedNames: allowedNames,\n\t}\n}\n\nfunc (err InvalidControlNameError) Error() string {\n\treturn \"allowed control(s): \" + err.allowedNames.String()\n}\n"
  },
  {
    "path": "internal/strict/status.go",
    "content": "package strict\n\nimport \"slices\"\n\nconst (\n\t// ActiveStatus is the Status of a control that is ongoing.\n\tActiveStatus Status = iota\n\t// CompletedStatus is the Status of a Control that is completed.\n\tCompletedStatus\n\t// SuspendedStatus is the Status of a Control that is suspended.\n\t// It does nothing and is assigned to a control only to avoid returning the `InvalidControlNameError`.\n\tSuspendedStatus\n)\n\nvar statusNames = map[Status]string{\n\tActiveStatus:    \"Active\",\n\tCompletedStatus: \"Completed\",\n\tSuspendedStatus: \"Suspended\",\n}\n\n// Statuses are a set of Statuses.\ntype Statuses []Status\n\n// Contains returns true if the `statuses` slice contains the given `status`.\nfunc (statuses Statuses) Contains(status Status) bool {\n\treturn slices.Contains(statuses, status)\n}\n\n// Status represents the status of the Control.\ntype Status byte\n\n// String implements `fmt.Stringer` interface.\nfunc (status Status) String() string {\n\tif name, ok := statusNames[status]; ok {\n\t\treturn name\n\t}\n\n\treturn \"unknown\"\n}\n\nconst (\n\tgreenColor  = \"\\033[0;32m\"\n\tyellowColor = \"\\033[0;33m\"\n\tresetColor  = \"\\033[0m\"\n)\n\n// StringWithANSIColor returns a colored text representation of the status.\nfunc (status Status) StringWithANSIColor() string {\n\tstr := status.String()\n\n\tswitch status {\n\tcase ActiveStatus:\n\t\treturn greenColor + str + resetColor\n\tcase CompletedStatus, SuspendedStatus:\n\t\treturn yellowColor + str + resetColor\n\t}\n\n\treturn str\n}\n"
  },
  {
    "path": "internal/strict/strict.go",
    "content": "// Package strict provides utilities used by Terragrunt to support a \"strict\" mode.\n// By default strict mode is disabled, but when Enabled, any breaking changes\n// to Terragrunt behavior that is not backwards compatible will result in an error.\n//\n// Note that any behavior outlined here should be documented in docs/src/content/docs/04-reference/03-strict-controls.mdx\n//\n// That is how users will know what to expect when they enable strict mode, and how to customize it.\npackage strict\n"
  },
  {
    "path": "internal/strict/view/plaintext/plaintext.go",
    "content": "// Package plaintext implements the view.Render interface for displaying strict controls in plaintext format.\npackage plaintext\n"
  },
  {
    "path": "internal/strict/view/plaintext/render.go",
    "content": "package plaintext\n\nimport (\n\t\"bytes\"\n\t\"text/tabwriter\"\n\t\"text/template\"\n\n\t\"maps\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/view\"\n)\n\nconst (\n\ttabMinWidth = 1\n\ttabWidth    = 8\n\ttabPadding  = 2\n)\n\nvar _ = view.Render(new(Render))\n\ntype Render struct{}\n\nfunc NewRender() *Render {\n\treturn &Render{}\n}\n\n// List implements view.Render interface.\nfunc (render *Render) List(controls strict.Controls) (string, error) {\n\tresult, err := render.executeTemplate(listTemplate, map[string]any{\n\t\t\"controls\": controls,\n\t}, nil)\n\tif err != nil {\n\t\treturn \"\", errors.Errorf(\"failed to render controls list: %w\", err)\n\t}\n\n\treturn result, nil\n}\n\n// DetailControl implements view.Render interface.\nfunc (render *Render) DetailControl(control strict.Control) (string, error) {\n\treturn render.executeTemplate(detailControlTemplate, map[string]any{\"control\": control}, nil)\n}\n\nfunc (render *Render) buildTemplate(templ string, customFuncs map[string]any) (*template.Template, error) {\n\tfuncMap := template.FuncMap{}\n\tmaps.Copy(funcMap, customFuncs)\n\n\tt := template.Must(template.New(\"template\").Funcs(funcMap).Parse(templ))\n\ttemplates := map[string]string{\n\t\t\"subcontrolTemplate\":       subcontrolTemplate,\n\t\t\"controlTemplate\":          controlTemplate,\n\t\t\"rangeSubcontrolsTemplate\": rangeSubcontrolsTemplate,\n\t\t\"rangeControlsTemplate\":    rangeControlsTemplate,\n\t}\n\n\tfor name, value := range templates {\n\t\tif _, err := t.New(name).Parse(value); err != nil {\n\t\t\treturn nil, errors.Errorf(\"failed to parse template %s: %w\", name, err)\n\t\t}\n\t}\n\n\treturn t, nil\n}\n\nfunc (render *Render) formatOutput(t *template.Template, data any) (string, error) {\n\tout := new(bytes.Buffer)\n\ttabOut := tabwriter.NewWriter(out, tabMinWidth, tabWidth, tabPadding, ' ', 0)\n\n\tif err := t.Execute(tabOut, data); err != nil {\n\t\treturn \"\", errors.Errorf(\"failed to execute template: %w\", err)\n\t}\n\n\tif err := tabOut.Flush(); err != nil {\n\t\treturn \"\", errors.Errorf(\"failed to flush output: %w\", err)\n\t}\n\n\treturn out.String(), nil\n}\n\nfunc (render *Render) executeTemplate(templ string, data any, customFuncs map[string]any) (string, error) {\n\tt, err := render.buildTemplate(templ, customFuncs)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn render.formatOutput(t, data)\n}\n"
  },
  {
    "path": "internal/strict/view/plaintext/template.go",
    "content": "package plaintext\n\nconst controlTemplate = `{{ .Name }}{{ \"\\t\" }}{{ .Status.StringWithANSIColor }}{{ \"\\t\" }}{{ if .Description }}{{ .Description }}{{ else }}{{ .Warning }}{{ end }}`\n\nconst rangeControlsTemplate = `{{ range $index, $control := .Sort }}{{ if $index }}\n   {{ end }}{{ template \"controlTemplate\" $control }}{{ end }}`\n\nconst subcontrolTemplate = `{{ .Name }}{{ \"\\t\" }}{{ if .Description }}{{ .Description }}{{ else }}{{ .Warning }}{{ end }}`\n\nconst rangeSubcontrolsTemplate = `{{ range $index, $control := .Sort }}{{ if $index }}\n   {{ end }}{{ template \"subcontrolTemplate\" $control }}{{ end }}`\n\nconst listTemplate = `\n   {{ $controls := .controls }}{{ $categories := $controls.GetCategories.FilterNotHidden.Sort }}{{ range $index, $category := $categories }}{{ if $index }}\n   {{ end }}{{ $category.Name }}:\n   {{ $categoryControls := $controls.FilterByCategories $category }}{{ template \"rangeControlsTemplate\" $categoryControls }}\n   {{ end }}{{ $noCategoryControls := $controls.FilterByCategories }}{{ if $noCategoryControls }}\n   {{ template \"rangeControlsTemplate\" $noCategoryControls }}\n   {{ end }}\n`\nconst detailControlTemplate = `\n   {{ $controls := .control.GetSubcontrols.RemoveDuplicates }}{{ $categories := $controls.GetCategories.FilterNotHidden.Sort }}{{ range $index, $category := $categories }}{{ if $index }}\n   {{ end }}{{ $category.Name }}:\n   {{ $categoryControls := $controls.FilterByCategories $category }}{{ template \"rangeSubcontrolsTemplate\" $categoryControls }}\n   {{ end }}{{ $noCategoryControls := $controls.FilterByCategories }}{{ if and $categories $noCategoryControls }}\n   {{ end }}{{ if $noCategoryControls }}{{ template \"rangeSubcontrolsTemplate\" $noCategoryControls }}\n   {{ end }}\n`\n"
  },
  {
    "path": "internal/strict/view/render.go",
    "content": "package view\n\nimport \"github.com/gruntwork-io/terragrunt/internal/strict\"\n\ntype Render interface {\n\t// List renders the list of controls.\n\tList(controls strict.Controls) (string, error)\n\n\t// DetailControl renders the detailed information about the control, including its subcontrols.\n\tDetailControl(control strict.Control) (string, error)\n}\n"
  },
  {
    "path": "internal/strict/view/view.go",
    "content": "// Package view contains the rendering logic for printing strict controls.\npackage view\n"
  },
  {
    "path": "internal/strict/view/writer.go",
    "content": "package view\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict\"\n)\n\n// Writer is the base layer for command views, encapsulating a set of I/O streams and implementing a human friendly view for strict controls.\ntype Writer struct {\n\tio.Writer\n\trender Render\n}\n\n// NewWriter returns a new Writer instance that uses the provided io.Writer for output\n// and the Render interface for formatting controls.\nfunc NewWriter(writer io.Writer, render Render) *Writer {\n\treturn &Writer{\n\t\tWriter: writer,\n\t\trender: render,\n\t}\n}\n\n// List renders the given list of controls.\nfunc (writer *Writer) List(controls strict.Controls) error {\n\toutput, err := writer.render.List(controls)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn writer.output(output)\n}\n\n// DetailControl renders the detailed information about the control, including its subcontrols.\nfunc (writer *Writer) DetailControl(control strict.Control) error {\n\toutput, err := writer.render.DetailControl(control)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn writer.output(output)\n}\n\nfunc (writer *Writer) output(output string) error {\n\tif _, err := fmt.Fprint(writer, output); err != nil {\n\t\treturn errors.Errorf(\"failed to write output: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/telemetry/context.go",
    "content": "package telemetry\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"go.opentelemetry.io/otel/trace\"\n)\n\ntype contextKey byte\n\nconst (\n\ttelemeterContextKey contextKey = iota\n\tTraceParentEnv                 = \"TRACEPARENT\"\n)\n\n// ContextWithTelemeter returns a new context with the provided Telemeter attached.\nfunc ContextWithTelemeter(ctx context.Context, telemeter *Telemeter) context.Context {\n\treturn context.WithValue(ctx, telemeterContextKey, telemeter)\n}\n\n// TelemeterFromContext retrieves the Telemeter from the context, or nil if not present.\nfunc TelemeterFromContext(ctx context.Context) *Telemeter {\n\tif val := ctx.Value(telemeterContextKey); val != nil {\n\t\tif telemeter, ok := val.(*Telemeter); ok {\n\t\t\treturn telemeter\n\t\t}\n\t}\n\n\treturn new(Telemeter)\n}\n\n// TraceParentFromContext returns the W3C traceparent header value from the context's span, or an error if not available.\nfunc TraceParentFromContext(ctx context.Context, telemetry *Options) string {\n\tspan := trace.SpanFromContext(ctx)\n\tspanContext := span.SpanContext()\n\n\tif !spanContext.IsValid() {\n\t\treturn \"\"\n\t}\n\n\tif len(telemetry.TraceParent) > 0 {\n\t\treturn telemetry.TraceParent\n\t}\n\n\ttraceID := spanContext.TraceID().String()\n\tspanID := spanContext.SpanID().String()\n\tflags := \"00\"\n\n\tif spanContext.TraceFlags().IsSampled() {\n\t\tflags = \"01\"\n\t}\n\n\treturn fmt.Sprintf(\"00-%s-%s-%s\", traceID, spanID, flags)\n}\n"
  },
  {
    "path": "internal/telemetry/errors.go",
    "content": "package telemetry\n\nimport \"fmt\"\n\n// ErrorMissingEnvVariable error for missing environment variable.\ntype ErrorMissingEnvVariable struct {\n\tVars []string\n}\n\nfunc (e *ErrorMissingEnvVariable) Error() string {\n\treturn fmt.Sprintf(\"missing environment variable: %v\", e.Vars)\n}\n"
  },
  {
    "path": "internal/telemetry/meter.go",
    "content": "package telemetry\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"regexp\"\n\t\"time\"\n\n\t\"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp\"\n\totelmetric \"go.opentelemetry.io/otel/metric\"\n\t\"go.opentelemetry.io/otel/sdk/metric\"\n\n\t\"go.opentelemetry.io/otel/sdk/resource\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.40.0\"\n)\n\nconst (\n\tnoneMetricsExporterType     metricsExporterType = \"none\"\n\tconsoleMetricsExporterType  metricsExporterType = \"console\"\n\toltpHTTPMetricsExporterType metricsExporterType = \"otlpHttp\"\n\tgrpcHTTPMetricsExporterType metricsExporterType = \"grpcHttp\"\n\n\tErrorsCounter = \"errors\"\n\n\treaderInterval = 1 * time.Second\n)\n\nvar (\n\tmetricNameCleanPattern     = regexp.MustCompile(`[^A-Za-z0-9_.-/]`)\n\tmultipleUnderscoresPattern = regexp.MustCompile(`_+`)\n)\n\ntype metricsExporterType string\n\ntype Meter struct {\n\totelmetric.Meter\n\tprovider *metric.MeterProvider\n\texporter metric.Exporter\n}\n\n// NewMeter creates and configures the metrics collection.\nfunc NewMeter(ctx context.Context, appName, appVersion string, writer io.Writer, opts *Options) (*Meter, error) {\n\texporter, err := NewMetricsExporter(ctx, writer, opts)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tif exporter == nil {\n\t\treturn nil, nil\n\t}\n\n\tprovider, err := newMetricsProvider(exporter, appName, appVersion)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\totel.SetMeterProvider(provider)\n\n\tmeter := &Meter{\n\t\tMeter:    otel.GetMeterProvider().Meter(appName),\n\t\tprovider: provider,\n\t\texporter: exporter,\n\t}\n\n\treturn meter, nil\n}\n\n// Time collects time for function execution\nfunc (meter *Meter) Time(ctx context.Context, name string, attrs map[string]any, fn func(childCtx context.Context) error) error {\n\tif meter == nil || meter.exporter == nil {\n\t\treturn fn(ctx)\n\t}\n\n\tmetricAttrs := mapToAttributes(attrs)\n\n\thistogram, err := meter.Int64Histogram(CleanMetricName(name + \"_duration\"))\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tstartTime := time.Now()\n\terr = fn(ctx)\n\n\thistogram.Record(ctx, time.Since(startTime).Milliseconds(), otelmetric.WithAttributes(metricAttrs...))\n\n\tif err != nil {\n\t\t// count errors\n\t\tmeter.Count(ctx, ErrorsCounter, 1)\n\t\tmeter.Count(ctx, name+\"_errors\", 1)\n\t} else {\n\t\tmeter.Count(ctx, name+\"_success\", 1)\n\t}\n\n\treturn err\n}\n\n// Count adds to counter provided value.\nfunc (meter *Meter) Count(ctx context.Context, name string, value int64) {\n\tif ctx == nil || meter == nil || meter.exporter == nil {\n\t\treturn\n\t}\n\n\tcounter, err := meter.Int64Counter(CleanMetricName(name + \"_count\"))\n\tif err != nil {\n\t\treturn\n\t}\n\n\tcounter.Add(ctx, value)\n}\n\n// NewMetricsExporter - create a new exporter based on the telemetry options.\nfunc NewMetricsExporter(ctx context.Context, writer io.Writer, opts *Options) (metric.Exporter, error) {\n\texporterType := metricsExporterType(opts.MetricExporter)\n\tif exporterType == \"\" {\n\t\texporterType = noneMetricsExporterType\n\t}\n\n\t// TODO: Remove this lint suppression\n\tswitch exporterType { //nolint:exhaustive\n\tcase oltpHTTPMetricsExporterType:\n\t\tvar config []otlpmetrichttp.Option\n\t\tif opts.MetricExporterInsecureEndpoint {\n\t\t\tconfig = append(config, otlpmetrichttp.WithInsecure())\n\t\t}\n\n\t\treturn otlpmetrichttp.New(ctx, config...)\n\tcase grpcHTTPMetricsExporterType:\n\t\tvar config []otlpmetricgrpc.Option\n\t\tif opts.MetricExporterInsecureEndpoint {\n\t\t\tconfig = append(config, otlpmetricgrpc.WithInsecure())\n\t\t}\n\n\t\treturn otlpmetricgrpc.New(ctx, config...)\n\tcase consoleMetricsExporterType:\n\t\treturn stdoutmetric.New(stdoutmetric.WithWriter(writer))\n\tdefault:\n\t\treturn nil, nil\n\t}\n}\n\n// newMetricsProvider creates a new metrics provider.\nfunc newMetricsProvider(exp metric.Exporter, appName, appVersion string) (*metric.MeterProvider, error) {\n\tr, err := resource.Merge(\n\t\tresource.Default(),\n\t\tresource.NewWithAttributes(\n\t\t\tsemconv.SchemaURL,\n\t\t\tsemconv.ServiceName(appName),\n\t\t\tsemconv.ServiceVersion(appVersion),\n\t\t),\n\t)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tmeterProvider := metric.NewMeterProvider(\n\t\tmetric.WithResource(r),\n\t\tmetric.WithReader(metric.NewPeriodicReader(exp, metric.WithInterval(readerInterval))),\n\t)\n\n\treturn meterProvider, nil\n}\n"
  },
  {
    "path": "internal/telemetry/meter_test.go",
    "content": "package telemetry_test\n\nimport (\n\t\"io\"\n\t\"testing\"\n\n\t\"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp\"\n)\n\nfunc TestNewMetricsExporter(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\n\tstdout, err := stdoutmetric.New()\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\texpectedType any\n\t\tname         string\n\t\texporterType string\n\t\tinsecure     bool\n\t\texpectNil    bool\n\t}{\n\t\t{\n\t\t\tname:         \"OTLP HTTP Exporter\",\n\t\t\texporterType: \"otlpHttp\",\n\t\t\tinsecure:     false,\n\t\t\texpectedType: (*otlpmetrichttp.Exporter)(nil),\n\t\t},\n\t\t{\n\t\t\tname:         \"gRPC HTTP Exporter\",\n\t\t\texporterType: \"grpcHttp\",\n\t\t\tinsecure:     true,\n\t\t\texpectedType: (*otlpmetricgrpc.Exporter)(nil),\n\t\t},\n\t\t{\n\t\t\tname:         \"Console Exporter\",\n\t\t\texporterType: \"console\",\n\t\t\tinsecure:     false,\n\t\t\texpectedType: stdout,\n\t\t},\n\t\t{\n\t\t\tname:         \"None Exporter\",\n\t\t\texporterType: \"none\",\n\t\t\tinsecure:     false,\n\t\t\texpectNil:    true,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\topts := options.NewTerragruntOptionsWithWriters(io.Discard, io.Discard)\n\t\t\topts.Telemetry.MetricExporter = tt.exporterType\n\t\t\topts.Telemetry.MetricExporterInsecureEndpoint = tt.insecure\n\n\t\t\texporter, err := telemetry.NewMetricsExporter(ctx, io.Discard, opts.Telemetry)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif tt.expectNil {\n\t\t\t\tassert.Nil(t, exporter)\n\t\t\t} else {\n\t\t\t\tassert.IsType(t, tt.expectedType, exporter)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestCleanMetricName(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"Normal case\",\n\t\t\tinput:    \"metricName_1.2-34\",\n\t\t\texpected: \"metricName_1.2_34\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Starts with invalid characters\",\n\t\t\tinput:    \"!@#metricName\",\n\t\t\texpected: \"metricName\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Ends with invalid characters\",\n\t\t\tinput:    \"metricName!@#\",\n\t\t\texpected: \"metricName\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Only invalid characters\",\n\t\t\tinput:    \"!@#$%^&*()\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Empty string\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Leading underscore from replacement\",\n\t\t\tinput:    \"!metricName\",\n\t\t\texpected: \"metricName\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Multiple replacements\",\n\t\t\tinput:    \"metric!@#Name\",\n\t\t\texpected: \"metric_Name\",\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.Parallel()\n\n\t\t\tresult := telemetry.CleanMetricName(tc.input)\n\t\t\tassert.Equal(t, tc.expected, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/telemetry/opts.go",
    "content": "package telemetry\n\n// Options are Telemetry options.\ntype Options struct {\n\t// TraceExporter is the type of trace exporter to be used.\n\tTraceExporter string\n\t// TraceExporterHTTPEndpoint is the endpoint to which traces will be sent.\n\tTraceExporterHTTPEndpoint string\n\t// TraceParent is used as a parent trace context.\n\tTraceParent string\n\t// MetricExporter is the type of metrics exporter.\n\tMetricExporter string\n\t// TraceExporterInsecureEndpoint is useful for collecting traces locally. If set to true, the exporter will not validate the server certificate.\n\tTraceExporterInsecureEndpoint bool\n\t// MetricExporterInsecureEndpoint is useful for local metrics collection. if set to true, the exporter will not validate the server's certificate.\n\tMetricExporterInsecureEndpoint bool\n}\n"
  },
  {
    "path": "internal/telemetry/telemeter.go",
    "content": "// Package telemetry provides a way to collect telemetry from function execution - metrics and traces.\npackage telemetry\n\nimport (\n\t\"context\"\n\t\"io\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\ntype Telemeter struct {\n\t*Tracer\n\t*Meter\n}\n\n// NewTelemeter initializes the telemetry collector.\nfunc NewTelemeter(ctx context.Context, appName, appVersion string, writer io.Writer, opts *Options) (*Telemeter, error) {\n\ttracer, err := NewTracer(ctx, appName, appVersion, writer, opts)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tmeter, err := NewMeter(ctx, appName, appVersion, writer, opts)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\treturn &Telemeter{\n\t\tTracer: tracer,\n\t\tMeter:  meter,\n\t}, nil\n}\n\n// Shutdown shutdowns the telemetry provider.\nfunc (tlm *Telemeter) Shutdown(ctx context.Context) error {\n\tif tlm.Tracer != nil && tlm.Tracer.provider != nil {\n\t\tif err := tlm.Tracer.provider.Shutdown(ctx); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\ttlm.Tracer.provider = nil\n\t}\n\n\tif tlm.Meter != nil && tlm.Meter.provider != nil {\n\t\tif err := tlm.Meter.provider.Shutdown(ctx); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\ttlm.Meter.provider = nil\n\t}\n\n\treturn nil\n}\n\n// Collect collects telemetry from function execution metrics and traces.\nfunc (tlm *Telemeter) Collect(ctx context.Context, name string, attrs map[string]any, fn func(childCtx context.Context) error) error {\n\t// wrap telemetry collection with trace and time metric\n\treturn tlm.Trace(ctx, name, attrs, func(ctx context.Context) error {\n\t\treturn tlm.Time(ctx, name, attrs, fn)\n\t})\n}\n"
  },
  {
    "path": "internal/telemetry/tracer.go",
    "content": "package telemetry\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc\"\n\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"go.opentelemetry.io/otel\"\n\t\"go.opentelemetry.io/otel/exporters/stdout/stdouttrace\"\n\t\"go.opentelemetry.io/otel/sdk/resource\"\n\tsdktrace \"go.opentelemetry.io/otel/sdk/trace\"\n\tsemconv \"go.opentelemetry.io/otel/semconv/v1.40.0\"\n\t\"go.opentelemetry.io/otel/trace\"\n)\n\nconst (\n\tnoneTraceExporterType     traceExporterType = \"none\"\n\tconsoleTraceExporterType  traceExporterType = \"console\"\n\totlpHTTPTraceExporterType traceExporterType = \"otlpHttp\"\n\totlpGrpcTraceExporterType traceExporterType = \"otlpGrpc\"\n\thttpTraceExporterType     traceExporterType = \"http\"\n\n\ttraceParentParts = 4\n)\n\ntype traceExporterType string\n\ntype Tracer struct {\n\ttrace.Tracer\n\tprovider         *sdktrace.TracerProvider\n\tspanExporter     sdktrace.SpanExporter\n\tparentTraceID    *trace.TraceID\n\tparentSpanID     *trace.SpanID\n\tparentTraceFlags *trace.TraceFlags\n}\n\n// NewTracer creates and configures the traces collection.\nfunc NewTracer(ctx context.Context, appName, appVersion string, writer io.Writer, opts *Options) (*Tracer, error) {\n\tspanExporter, err := NewTraceExporter(ctx, writer, opts)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tif spanExporter == nil { // no exporter\n\t\treturn nil, nil\n\t}\n\n\tprovider, err := newTraceProvider(spanExporter, appName, appVersion, opts)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\totel.SetTracerProvider(provider)\n\n\tvar (\n\t\tparentTraceID    *trace.TraceID\n\t\tparentSpanID     *trace.SpanID\n\t\tparentTraceFlags *trace.TraceFlags\n\t)\n\n\tif opts.TraceParent != \"\" {\n\t\t// parse trace parent values\n\t\tparts := strings.Split(opts.TraceParent, \"-\")\n\t\tif len(parts) != traceParentParts {\n\t\t\treturn nil, fmt.Errorf(\"invalid TRACEPARENT value %s\", opts.TraceParent)\n\t\t}\n\n\t\t_, traceIDHex, spanIDHex, traceFlagsStr := parts[0], parts[1], parts[2], parts[3]\n\n\t\tparsedFlag, err := strconv.Atoi(traceFlagsStr)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Errorf(\"invalid trace flags: %w\", err)\n\t\t}\n\n\t\ttraceFlags := trace.FlagsSampled\n\t\tif parsedFlag == 0 {\n\t\t\ttraceFlags = 0\n\t\t}\n\n\t\ttraceID, err := trace.TraceIDFromHex(traceIDHex)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(err)\n\t\t}\n\n\t\tspanID, err := trace.SpanIDFromHex(spanIDHex)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(err)\n\t\t}\n\n\t\tparentTraceID = &traceID\n\t\tparentSpanID = &spanID\n\t\tparentTraceFlags = &traceFlags\n\t}\n\n\ttracer := &Tracer{\n\t\tTracer:           provider.Tracer(appName),\n\t\tprovider:         provider,\n\t\tspanExporter:     spanExporter,\n\t\tparentTraceID:    parentTraceID,\n\t\tparentSpanID:     parentSpanID,\n\t\tparentTraceFlags: parentTraceFlags,\n\t}\n\n\treturn tracer, nil\n}\n\n// newTraceProvider creates a new trace tracer with terragrunt version.\nfunc newTraceProvider(exp sdktrace.SpanExporter, appName, appVersion string, opts *Options) (*sdktrace.TracerProvider, error) {\n\tr, err := resource.Merge(\n\t\tresource.Default(),\n\t\tresource.NewWithAttributes(\n\t\t\tsemconv.SchemaURL,\n\t\t\tsemconv.ServiceName(appName),\n\t\t\tsemconv.ServiceVersion(appVersion),\n\t\t),\n\t)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\texporterType := traceExporterType(opts.TraceExporter)\n\n\tvar processor sdktrace.SpanProcessor\n\tif exporterType == consoleTraceExporterType {\n\t\tprocessor = sdktrace.NewSimpleSpanProcessor(exp)\n\t} else {\n\t\tprocessor = sdktrace.NewBatchSpanProcessor(exp)\n\t}\n\n\treturn sdktrace.NewTracerProvider(\n\t\tsdktrace.WithSpanProcessor(processor),\n\t\tsdktrace.WithResource(r),\n\t), nil\n}\n\n// NewTraceExporter creates a new exporter based on the telemetry options.\nfunc NewTraceExporter(ctx context.Context, writer io.Writer, opts *Options) (sdktrace.SpanExporter, error) {\n\texporterType := traceExporterType(opts.TraceExporter)\n\tif exporterType == \"\" {\n\t\texporterType = noneTraceExporterType\n\t}\n\n\t// TODO: Remove lint suppression\n\tswitch exporterType { //nolint:exhaustive\n\tcase httpTraceExporterType:\n\t\tif opts.TraceExporterHTTPEndpoint == \"\" {\n\t\t\treturn nil, &ErrorMissingEnvVariable{\n\t\t\t\tVars: []string{\"TG_TELEMETRY_TRACE_EXPORTER_HTTP_ENDPOINT\"},\n\t\t\t}\n\t\t}\n\n\t\tendpointOpt := otlptracehttp.WithEndpoint(opts.TraceExporterHTTPEndpoint)\n\t\tconfig := []otlptracehttp.Option{endpointOpt}\n\n\t\tif opts.TraceExporterInsecureEndpoint {\n\t\t\tconfig = append(config, otlptracehttp.WithInsecure())\n\t\t}\n\n\t\treturn otlptracehttp.New(ctx, config...)\n\tcase otlpHTTPTraceExporterType:\n\t\tvar config []otlptracehttp.Option\n\t\tif opts.TraceExporterInsecureEndpoint {\n\t\t\tconfig = append(config, otlptracehttp.WithInsecure())\n\t\t}\n\n\t\treturn otlptracehttp.New(ctx, config...)\n\tcase otlpGrpcTraceExporterType:\n\t\tvar config []otlptracegrpc.Option\n\t\tif opts.TraceExporterInsecureEndpoint {\n\t\t\tconfig = append(config, otlptracegrpc.WithInsecure())\n\t\t}\n\n\t\treturn otlptracegrpc.New(ctx, config...)\n\tcase consoleTraceExporterType:\n\t\treturn stdouttrace.New(stdouttrace.WithWriter(writer))\n\tdefault:\n\t\treturn nil, nil\n\t}\n}\n\n// Trace collects traces for method execution.\nfunc (tracer *Tracer) Trace(ctx context.Context, name string, attrs map[string]any, fn func(childCtx context.Context) error) error {\n\tif tracer == nil || tracer.spanExporter == nil || tracer.provider == nil { // invoke function without tracing\n\t\treturn fn(ctx)\n\t}\n\n\tctx, span := tracer.openSpan(ctx, name, attrs)\n\tdefer span.End()\n\n\tif err := fn(ctx); err != nil {\n\t\t// record error in span\n\t\tspan.RecordError(err)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// openSpan creates a new span with attributes.\nfunc (tracer *Tracer) openSpan(ctx context.Context, name string, attrs map[string]any) (context.Context, trace.Span) {\n\tif tracer.provider == nil {\n\t\treturn ctx, nil\n\t}\n\n\tif tracer.parentTraceID != nil && tracer.parentSpanID != nil {\n\t\texistingSpan := trace.SpanFromContext(ctx)\n\t\tif !existingSpan.SpanContext().IsValid() {\n\t\t\tspanContext := trace.NewSpanContext(trace.SpanContextConfig{\n\t\t\t\tTraceID:    *tracer.parentTraceID,\n\t\t\t\tSpanID:     *tracer.parentSpanID,\n\t\t\t\tRemote:     true,\n\t\t\t\tTraceFlags: *tracer.parentTraceFlags,\n\t\t\t})\n\n\t\t\t// create a new context with the parent span context\n\t\t\tctx = trace.ContextWithSpanContext(ctx, spanContext)\n\t\t}\n\t}\n\n\t// This lint is suppressed because we definitely do close the span\n\t// in a defer statement everywhere openSpan is called. It seems like\n\t// a useful lint, though. We should consider removing the suppression\n\t// and fixing the lint.\n\n\tctx, span := tracer.Start(ctx, name) // nolint:spancheck\n\t// convert attrs map to span.SetAttributes\n\tspan.SetAttributes(mapToAttributes(attrs)...)\n\n\treturn ctx, span //nolint:spancheck\n}\n"
  },
  {
    "path": "internal/telemetry/tracer_test.go",
    "content": "package telemetry_test\n\nimport (\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc\"\n\t\"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp\"\n\t\"go.opentelemetry.io/otel/exporters/stdout/stdouttrace\"\n)\n\nfunc TestNewTraceExporter(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\n\thttp, err := otlptracehttp.New(ctx)\n\trequire.NoError(t, err)\n\n\tgrpc, err := otlptracegrpc.New(ctx)\n\trequire.NoError(t, err)\n\n\tstdoutrace, err := stdouttrace.New()\n\trequire.NoError(t, err)\n\n\ttests := []struct {\n\t\texpectedType              any\n\t\ttraceExporter             string\n\t\ttraceExporterHTTPEndpoint string\n\t\tname                      string\n\t\texpectError               bool\n\t}{\n\t\t{\n\t\t\tname:          \"HTTP Trace Exporter\",\n\t\t\ttraceExporter: \"otlpHttp\",\n\t\t\texpectedType:  http,\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:                      \"Custom HTTP endpoint\",\n\t\t\ttraceExporter:             \"http\",\n\t\t\ttraceExporterHTTPEndpoint: \"http://localhost:4317\",\n\t\t\texpectedType:              http,\n\t\t\texpectError:               false,\n\t\t},\n\t\t{\n\t\t\tname:          \"Custom HTTP endpoint without endpoint\",\n\t\t\ttraceExporter: \"http\",\n\t\t\texpectedType:  http,\n\t\t\texpectError:   true,\n\t\t},\n\t\t{\n\t\t\tname:          \"Grpc Trace Exporter\",\n\t\t\ttraceExporter: \"otlpGrpc\",\n\t\t\texpectedType:  grpc,\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"Console Trace Exporter\",\n\t\t\ttraceExporter: \"console\",\n\t\t\texpectedType:  stdoutrace,\n\t\t\texpectError:   false,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\topts := options.NewTerragruntOptionsWithWriters(io.Discard, io.Discard)\n\t\t\topts.Telemetry.TraceExporter = tt.traceExporter\n\t\t\topts.Telemetry.TraceExporterHTTPEndpoint = tt.traceExporterHTTPEndpoint\n\n\t\t\texporter, err := telemetry.NewTraceExporter(ctx, io.Discard, opts.Telemetry)\n\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.IsType(t, tt.expectedType, exporter)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/telemetry/util.go",
    "content": "package telemetry\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"go.opentelemetry.io/otel/attribute\"\n)\n\n// mapToAttributes converts map to attributes to pass to span.SetAttributes.\nfunc mapToAttributes(data map[string]any) []attribute.KeyValue {\n\tvar attrs []attribute.KeyValue\n\n\tfor k, v := range data {\n\t\tswitch val := v.(type) {\n\t\tcase string:\n\t\t\tattrs = append(attrs, attribute.String(k, val))\n\t\tcase int:\n\t\t\tattrs = append(attrs, attribute.Int64(k, int64(val)))\n\t\tcase int64:\n\t\t\tattrs = append(attrs, attribute.Int64(k, val))\n\t\tcase float64:\n\t\t\tattrs = append(attrs, attribute.Float64(k, val))\n\t\tcase bool:\n\t\t\tattrs = append(attrs, attribute.Bool(k, val))\n\t\tdefault:\n\t\t\tattrs = append(attrs, attribute.String(k, fmt.Sprintf(\"%v\", val)))\n\t\t}\n\t}\n\n\treturn attrs\n}\n\n// CleanMetricName cleans metric name from invalid characters.\nfunc CleanMetricName(metricName string) string {\n\tcleanedName := metricNameCleanPattern.ReplaceAllString(metricName, \"_\")\n\tcleanedName = multipleUnderscoresPattern.ReplaceAllString(cleanedName, \"_\")\n\n\treturn strings.Trim(cleanedName, \"_\")\n}\n"
  },
  {
    "path": "internal/tf/cache/config.go",
    "content": "package cache\n\nimport (\n\t\"net\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/handlers\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/services\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst (\n\tdefaultHostname        = \"localhost\"\n\tdefaultShutdownTimeout = time.Second * 30\n)\n\ntype Option func(Config) Config\n\nfunc WithHostname(hostname string) Option {\n\treturn func(cfg Config) Config {\n\t\tif hostname != \"\" {\n\t\t\tcfg.hostname = hostname\n\t\t}\n\n\t\treturn cfg\n\t}\n}\n\nfunc WithPort(port int) Option {\n\treturn func(cfg Config) Config {\n\t\tif port != 0 {\n\t\t\tcfg.port = port\n\t\t}\n\n\t\treturn cfg\n\t}\n}\n\nfunc WithToken(token string) Option {\n\treturn func(cfg Config) Config {\n\t\tcfg.token = token\n\t\treturn cfg\n\t}\n}\n\nfunc WithProviderService(service *services.ProviderService) Option {\n\treturn func(cfg Config) Config {\n\t\tcfg.providerService = service\n\t\treturn cfg\n\t}\n}\n\nfunc WithProviderHandlers(handlers ...handlers.ProviderHandler) Option {\n\treturn func(cfg Config) Config {\n\t\tcfg.providerHandlers = handlers\n\t\treturn cfg\n\t}\n}\n\nfunc WithProxyProviderHandler(handler *handlers.ProxyProviderHandler) Option {\n\treturn func(cfg Config) Config {\n\t\tcfg.proxyProviderHandler = handler\n\t\treturn cfg\n\t}\n}\n\nfunc WithCacheProviderHTTPStatusCode(statusCode int) Option {\n\treturn func(cfg Config) Config {\n\t\tcfg.cacheProviderHTTPStatusCode = statusCode\n\t\treturn cfg\n\t}\n}\n\nfunc WithLogger(logger log.Logger) Option {\n\treturn func(cfg Config) Config {\n\t\tcfg.logger = logger\n\t\treturn cfg\n\t}\n}\n\ntype Config struct {\n\tlogger                      log.Logger\n\tproviderService             *services.ProviderService\n\tproxyProviderHandler        *handlers.ProxyProviderHandler\n\thostname                    string\n\ttoken                       string\n\tproviderHandlers            handlers.ProviderHandlers\n\tport                        int\n\tshutdownTimeout             time.Duration\n\tcacheProviderHTTPStatusCode int\n}\n\nfunc NewConfig(opts ...Option) *Config {\n\tcfg := &Config{\n\t\thostname:        defaultHostname,\n\t\tshutdownTimeout: defaultShutdownTimeout,\n\t\tlogger:          log.Default(),\n\t}\n\n\treturn cfg.WithOptions(opts...)\n}\n\nfunc (cfg *Config) WithOptions(opts ...Option) *Config {\n\tfor _, opt := range opts {\n\t\t*cfg = opt(*cfg)\n\t}\n\n\treturn cfg\n}\n\nfunc (cfg *Config) Addr() string {\n\treturn net.JoinHostPort(cfg.hostname, strconv.Itoa(cfg.port))\n}\n"
  },
  {
    "path": "internal/tf/cache/controllers/discovery.go",
    "content": "package controllers\n\nimport (\n\t\"net/http\"\n\n\t\"maps\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/router\"\n\t\"github.com/labstack/echo/v4\"\n)\n\nconst (\n\tdiscoveryPath = \"/.well-known\"\n)\n\ntype Endpointer interface {\n\t// Endpoints returns controller endpoints.\n\tEndpoints() map[string]any\n}\n\ntype DiscoveryController struct {\n\t*router.Router\n\n\tEndpointers []Endpointer\n}\n\n// Register implements router.Controller.Register\nfunc (controller *DiscoveryController) Register(router *router.Router) {\n\tcontroller.Router = router.Group(discoveryPath)\n\n\t// Discovery Process\n\t// https://developer.hashicorp.com/terraform/internals/remote-service-discovery#discovery-process\n\tcontroller.GET(\"/terraform.json\", controller.terraformAction)\n}\n\nfunc (controller *DiscoveryController) terraformAction(ctx echo.Context) error {\n\tendpoints := make(map[string]any)\n\n\tfor _, endpointer := range controller.Endpointers {\n\t\tmaps.Copy(endpoints, endpointer.Endpoints())\n\t}\n\n\treturn ctx.JSON(http.StatusOK, endpoints)\n}\n"
  },
  {
    "path": "internal/tf/cache/controllers/downloader.go",
    "content": "package controllers\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/handlers\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/models\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/router\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/services\"\n\t\"github.com/labstack/echo/v4\"\n)\n\nconst (\n\tdownloadPath = \"/downloads\"\n)\n\ntype DownloaderController struct {\n\t*router.Router\n\n\tProviderService      *services.ProviderService\n\tProxyProviderHandler *handlers.ProxyProviderHandler\n}\n\n// Register implements router.Controller.Register\nfunc (controller *DownloaderController) Register(router *router.Router) {\n\tcontroller.Router = router.Group(downloadPath)\n\n\t// Download provider\n\tcontroller.GET(\"/:remote_host/:remote_path\", controller.downloadProviderAction)\n}\n\nfunc (controller *DownloaderController) downloadProviderAction(ctx echo.Context) error {\n\tvar (\n\t\tremoteHost = ctx.Param(\"remote_host\")\n\t\tremotePath = ctx.Param(\"remote_path\")\n\t)\n\n\tdownloadURL := url.URL{\n\t\tScheme: \"https\",\n\t\tHost:   remoteHost,\n\t\tPath:   \"/\" + remotePath,\n\t}\n\tprovider := &models.Provider{\n\t\tResponseBody: &models.ResponseBody{\n\t\t\tDownloadURL: downloadURL.String(),\n\t\t},\n\t}\n\n\tif cache := controller.ProviderService.GetProviderCache(provider); cache != nil {\n\t\tif path := cache.ArchivePath(); path != \"\" {\n\t\t\tcontroller.ProviderService.Logger().Debugf(\"Download cached provider %s\", cache.Provider)\n\t\t\treturn ctx.File(path)\n\t\t}\n\t}\n\n\tif err := controller.ProxyProviderHandler.Download(ctx, provider); err != nil {\n\t\treturn err\n\t}\n\n\treturn ctx.NoContent(http.StatusNotFound)\n}\n"
  },
  {
    "path": "internal/tf/cache/controllers/provider.go",
    "content": "// Package controllers provides the implementation of the controller for the provider endpoints.\npackage controllers\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/handlers\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/models\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/router\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/services\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/labstack/echo/v4\"\n)\n\nconst (\n\t// name using for the discovery\n\tproviderName = \"providers.v1\"\n\t// URL path to this controller\n\tproviderPath = \"/providers\"\n)\n\ntype ProviderController struct {\n\tLogger               log.Logger\n\tDownloaderController router.Controller\n\t*router.Router\n\tAuthMiddleware              echo.MiddlewareFunc\n\tProxyProviderHandler        *handlers.ProxyProviderHandler\n\tProviderService             *services.ProviderService\n\tProviderHandlers            []handlers.ProviderHandler\n\tServer                      http.Server\n\tCacheProviderHTTPStatusCode int\n}\n\n// Endpoints implements controllers.Endpointer.Endpoints\nfunc (controller *ProviderController) Endpoints() map[string]any {\n\treturn map[string]any{providerName: controller.URL().Path}\n}\n\n// Register implements router.Controller.Register\nfunc (controller *ProviderController) Register(router *router.Router) {\n\tcontroller.Router = router.Group(providerPath)\n\n\tif controller.AuthMiddleware != nil {\n\t\tcontroller.Use(controller.AuthMiddleware)\n\t}\n\n\t// Api should be compliant with the Terraform Registry Protocol for providers.\n\t// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/private-registry/provider-versions-platforms\n\n\t// Get All Versions for a Single Provider\n\t// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/private-registry/provider-versions-platforms#get-all-versions-for-a-single-provider\n\tcontroller.GET(\"/:cache_request_id/:registry_name/:namespace/:name/versions\", controller.getVersionsAction)\n\n\t// Get a Platform\n\t// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/private-registry/provider-versions-platforms#get-a-platform\n\tcontroller.GET(\"/:cache_request_id/:registry_name/:namespace/:name/:version/download/:os/:arch\", controller.getPlatformsAction)\n}\n\nfunc (controller *ProviderController) getVersionsAction(ctx echo.Context) error {\n\tvar (\n\t\tregistryName = ctx.Param(\"registry_name\")\n\t\tnamespace    = ctx.Param(\"namespace\")\n\t\tname         = ctx.Param(\"name\")\n\t)\n\n\tprovider := &models.Provider{\n\t\tRegistryName: registryName,\n\t\tNamespace:    namespace,\n\t\tName:         name,\n\t}\n\n\tvar allVersions models.Versions\n\n\tfor _, handler := range controller.ProviderHandlers {\n\t\tif handler.CanHandleProvider(provider) {\n\t\t\tversions, err := handler.GetVersions(ctx.Request().Context(), provider)\n\t\t\tif err != nil {\n\t\t\t\tcontroller.Logger.Errorf(\"Failed to get provider versions from %q: %s\", handler, err.Error())\n\t\t\t}\n\n\t\t\tif versions != nil {\n\t\t\t\tallVersions = append(allVersions, versions...)\n\t\t\t}\n\t\t}\n\t}\n\n\tvalidVersions, invalidVersions := allVersions.FilterValid()\n\tfor _, v := range invalidVersions {\n\t\tcontroller.Logger.Warnf(\"Skipping invalid version %q for provider %s\", v, provider.Address())\n\t}\n\n\tversions := struct {\n\t\tID       string          `json:\"id\"`\n\t\tVersions models.Versions `json:\"versions\"`\n\t}{\n\t\tID:       provider.Address(),\n\t\tVersions: validVersions,\n\t}\n\n\treturn ctx.JSON(http.StatusOK, versions)\n}\n\nfunc (controller *ProviderController) getPlatformsAction(ctx echo.Context) (er error) {\n\tvar (\n\t\tregistryName   = ctx.Param(\"registry_name\")\n\t\tnamespace      = ctx.Param(\"namespace\")\n\t\tname           = ctx.Param(\"name\")\n\t\tversion        = ctx.Param(\"version\")\n\t\tos             = ctx.Param(\"os\")\n\t\tarch           = ctx.Param(\"arch\")\n\t\tcacheRequestID = ctx.Param(\"cache_request_id\")\n\t)\n\n\tprovider := &models.Provider{\n\t\tRegistryName: registryName,\n\t\tNamespace:    namespace,\n\t\tName:         name,\n\t\tVersion:      version,\n\t\tOS:           os,\n\t\tArch:         arch,\n\t}\n\n\tif cacheRequestID == \"\" {\n\t\treturn controller.ProxyProviderHandler.GetPlatform(ctx, provider, controller.DownloaderController)\n\t}\n\n\tvar (\n\t\tresp *models.ResponseBody\n\t\terr  error\n\t)\n\n\tfor _, handler := range controller.ProviderHandlers {\n\t\tif handler.CanHandleProvider(provider) {\n\t\t\tresp, err = handler.GetPlatform(ctx.Request().Context(), provider)\n\t\t\tif err != nil {\n\t\t\t\tcontroller.Logger.Errorf(\"Failed to get provider platform from %q: %s\", handler, err.Error())\n\t\t\t}\n\n\t\t\tif resp != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tprovider.ResponseBody = resp\n\n\t// start caching and return 423 status\n\tcontroller.ProviderService.CacheProvider(ctx.Request().Context(), cacheRequestID, provider)\n\n\treturn ctx.NoContent(controller.CacheProviderHTTPStatusCode)\n}\n"
  },
  {
    "path": "internal/tf/cache/handlers/common_provider.go",
    "content": "// Package handlers provides the interfaces and common implementations for handling provider requests.\npackage handlers\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/models\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/puzpuzpuz/xsync/v3\"\n)\n\ntype CommonProviderHandler struct {\n\tlogger log.Logger\n\n\t// registryURLCache stores discovered registry URLs\n\t// We use [xsync.MapOf](https://github.com/puzpuzpuz/xsync?tab=readme-ov-file#map)\n\t// instead of standard `sync.Map` since it's faster and has generic types.\n\tregistryURLCache *xsync.MapOf[string, *RegistryURLs]\n\n\t// includeProviders and excludeProviders are sets of provider matching patterns that together define which providers are eligible to be potentially installed from the corresponding Source.\n\tincludeProviders models.Providers\n\texcludeProviders models.Providers\n}\n\n// NewCommonProviderHandler returns a new `CommonProviderHandler` instance with the defined values.\nfunc NewCommonProviderHandler(logger log.Logger, includes, excludes *[]string) *CommonProviderHandler {\n\tvar includeProviders, excludeProviders models.Providers\n\n\tif includes != nil {\n\t\tincludeProviders = models.ParseProviders(*includes...)\n\t}\n\n\tif excludes != nil {\n\t\texcludeProviders = models.ParseProviders(*excludes...)\n\t}\n\n\treturn &CommonProviderHandler{\n\t\tlogger:           logger,\n\t\tincludeProviders: includeProviders,\n\t\texcludeProviders: excludeProviders,\n\t\tregistryURLCache: xsync.NewMapOf[string, *RegistryURLs](),\n\t}\n}\n\n// CanHandleProvider implements ProviderHandler.CanHandleProvider\nfunc (handler *CommonProviderHandler) CanHandleProvider(provider *models.Provider) bool {\n\tswitch {\n\tcase handler.excludeProviders.Find(provider) != nil:\n\t\treturn false\n\tcase len(handler.includeProviders) > 0:\n\t\treturn handler.includeProviders.Find(provider) != nil\n\tdefault:\n\t\treturn true\n\t}\n}\n\n// DiscoveryURL implements ProviderHandler.DiscoveryURL.\nfunc (handler *CommonProviderHandler) DiscoveryURL(ctx context.Context, registryName string) (*RegistryURLs, error) {\n\tif urls, ok := handler.registryURLCache.Load(registryName); ok {\n\t\treturn urls, nil\n\t}\n\n\turls, err := DiscoveryURL(ctx, registryName)\n\tif err != nil {\n\t\tif !IsOfflineError(err) {\n\t\t\treturn nil, err\n\t\t}\n\n\t\turls = DefaultRegistryURLs\n\t\thandler.logger.Debugf(\"Unable to discover %q registry URLs, reason: %q, use default URLs: %s\", registryName, err, urls)\n\t} else {\n\t\thandler.logger.Debugf(\"Discovered %q registry URLs: %s\", registryName, urls)\n\t}\n\n\thandler.registryURLCache.Store(registryName, urls)\n\n\treturn urls, nil\n}\n"
  },
  {
    "path": "internal/tf/cache/handlers/direct_provider.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/models\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cliconfig\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nvar _ ProviderHandler = new(DirectProviderHandler)\n\ntype DirectProviderHandler struct {\n\t*CommonProviderHandler\n\n\tclient *helpers.Client\n}\n\nfunc NewDirectProviderHandler(logger log.Logger, method *cliconfig.ProviderInstallationDirect, credsSource *cliconfig.CredentialsSource) *DirectProviderHandler {\n\treturn &DirectProviderHandler{\n\t\tCommonProviderHandler: NewCommonProviderHandler(logger, method.Include, method.Exclude),\n\t\tclient:                helpers.NewClient(credsSource),\n\t}\n}\n\nfunc (handler *DirectProviderHandler) String() string {\n\treturn \"direct\"\n}\n\n// GetVersions implements ProviderHandler.GetVersions\n// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/private-registry/provider-versions-platforms#get-all-versions-for-a-single-provider\n//\n//nolint:lll\nfunc (handler *DirectProviderHandler) GetVersions(ctx context.Context, provider *models.Provider) (models.Versions, error) {\n\tapiURLs, err := handler.DiscoveryURL(ctx, provider.RegistryName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treqURL := &url.URL{\n\t\tScheme: \"https\",\n\t\tHost:   provider.RegistryName,\n\t\tPath:   path.Join(apiURLs.ProvidersV1, provider.Namespace, provider.Name, \"versions\"),\n\t}\n\n\tversions := struct {\n\t\tVersions models.Versions `json:\"versions\"`\n\t}{}\n\n\tif err := handler.client.Do(ctx, http.MethodGet, reqURL.String(), &versions); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn versions.Versions, nil\n}\n\n// GetPlatform implements ProviderHandler.GetPlatform\nfunc (handler *DirectProviderHandler) GetPlatform(ctx context.Context, provider *models.Provider) (*models.ResponseBody, error) {\n\tapiURLs, err := handler.DiscoveryURL(ctx, provider.RegistryName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tplatformURL := &url.URL{\n\t\tScheme: \"https\",\n\t\tHost:   provider.RegistryName,\n\t\tPath:   path.Join(apiURLs.ProvidersV1, provider.Namespace, provider.Name, provider.Version, \"download\", provider.OS, provider.Arch),\n\t}\n\n\tvar resp = new(models.ResponseBody)\n\n\tif err := handler.client.Do(ctx, http.MethodGet, platformURL.String(), resp); err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp = resp.ResolveRelativeReferences(platformURL)\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "internal/tf/cache/handlers/errors.go",
    "content": "package handlers\n\ntype NotFoundWellKnownURLError struct {\n\turl string\n}\n\nfunc (err NotFoundWellKnownURLError) Error() string {\n\treturn err.url + \" not found\"\n}\n"
  },
  {
    "path": "internal/tf/cache/handlers/filesystem_mirror_provider.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/models\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cliconfig\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nvar _ ProviderHandler = new(FilesystemMirrorProviderHandler)\n\ntype FilesystemMirrorProviderHandler struct {\n\t*CommonProviderHandler\n\n\tfilesystemMirrorPath string\n}\n\nfunc NewFilesystemMirrorProviderHandler(logger log.Logger, method *cliconfig.ProviderInstallationFilesystemMirror) *FilesystemMirrorProviderHandler {\n\treturn &FilesystemMirrorProviderHandler{\n\t\tCommonProviderHandler: NewCommonProviderHandler(logger, method.Include, method.Exclude),\n\t\tfilesystemMirrorPath:  method.Path,\n\t}\n}\n\nfunc (handler *FilesystemMirrorProviderHandler) String() string {\n\treturn \"filesystem_mirror '\" + handler.filesystemMirrorPath + \"'\"\n}\n\n// GetVersions implements ProviderHandler.GetVersions\nfunc (handler *FilesystemMirrorProviderHandler) GetVersions(_ context.Context, provider *models.Provider) (models.Versions, error) {\n\tvar mirrorData struct {\n\t\tVersions map[string]struct{} `json:\"versions\"`\n\t}\n\n\tfilename := filepath.Join(provider.RegistryName, provider.Namespace, provider.Name, \"index.json\")\n\tif err := handler.readMirrorData(filename, &mirrorData); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar versions = make(models.Versions, 0, len(mirrorData.Versions))\n\n\tfor version := range mirrorData.Versions {\n\t\tversions = append(versions, &models.Version{\n\t\t\tVersion:   version,\n\t\t\tPlatforms: availablePlatforms,\n\t\t})\n\t}\n\n\treturn versions, nil\n}\n\n// GetPlatform implements ProviderHandler.GetPlatform\nfunc (handler *FilesystemMirrorProviderHandler) GetPlatform(_ context.Context, provider *models.Provider) (*models.ResponseBody, error) {\n\tvar mirrorData struct {\n\t\tArchives map[string]struct {\n\t\t\tURL    string   `json:\"url\"`\n\t\t\tHashes []string `json:\"hashes\"`\n\t\t} `json:\"archives\"`\n\t}\n\n\tfilename := filepath.Join(provider.RegistryName, provider.Namespace, provider.Name, provider.Version+\".json\")\n\tif err := handler.readMirrorData(filename, &mirrorData); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar resp *models.ResponseBody\n\n\tif archive, ok := mirrorData.Archives[provider.Platform()]; ok {\n\t\t// check if the URL contains http scheme, it may just be a filename and we need to build the URL\n\t\tif !strings.Contains(archive.URL, \"://\") {\n\t\t\tarchive.URL = filepath.Join(handler.filesystemMirrorPath, provider.RegistryName, provider.Namespace, provider.Name, archive.URL)\n\t\t}\n\n\t\tresp = &models.ResponseBody{\n\t\t\tFilename:    filepath.Base(archive.URL),\n\t\t\tDownloadURL: archive.URL,\n\t\t}\n\t}\n\n\treturn resp, nil\n}\n\nfunc (handler *FilesystemMirrorProviderHandler) readMirrorData(filename string, value any) error {\n\tfilename = filepath.Join(handler.filesystemMirrorPath, filename)\n\n\tif !util.FileExists(filename) {\n\t\treturn nil\n\t}\n\n\tdata, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tif err := json.Unmarshal(data, value); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/tf/cache/handlers/network_mirror_provider.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/models\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cliconfig\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nvar _ ProviderHandler = new(NetworkMirrorProviderHandler)\n\ntype NetworkMirrorProviderHandler struct {\n\t*CommonProviderHandler\n\n\tclient           *helpers.Client\n\tnetworkMirrorURL *url.URL\n}\n\nfunc NewNetworkMirrorProviderHandler(logger log.Logger, networkMirror *cliconfig.ProviderInstallationNetworkMirror, credsSource *cliconfig.CredentialsSource) (*NetworkMirrorProviderHandler, error) {\n\tnetworkMirrorURL, err := url.Parse(networkMirror.URL)\n\tif err != nil {\n\t\treturn nil, errors.Errorf(\"failed to parse network mirror URL %q: %w\", networkMirror.URL, err)\n\t}\n\n\treturn &NetworkMirrorProviderHandler{\n\t\tCommonProviderHandler: NewCommonProviderHandler(logger, networkMirror.Include, networkMirror.Exclude),\n\t\tclient:                helpers.NewClient(credsSource),\n\t\tnetworkMirrorURL:      networkMirrorURL,\n\t}, nil\n}\n\nfunc (handler *NetworkMirrorProviderHandler) String() string {\n\treturn \"network_mirror '\" + handler.networkMirrorURL.String() + \"'\"\n}\n\n// GetVersions implements ProviderHandler.GetVersions\nfunc (handler *NetworkMirrorProviderHandler) GetVersions(ctx context.Context, provider *models.Provider) (models.Versions, error) {\n\tvar mirrorData struct {\n\t\tVersions map[string]struct{} `json:\"versions\"`\n\t}\n\n\treqPath := path.Join(provider.RegistryName, provider.Namespace, provider.Name, \"index.json\")\n\treqURL := fmt.Sprintf(\"%s/%s\", strings.TrimRight(handler.networkMirrorURL.String(), \"/\"), reqPath)\n\n\tif err := handler.client.Do(ctx, http.MethodGet, reqURL, &mirrorData); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar versions = make(models.Versions, 0, len(mirrorData.Versions))\n\n\tfor version := range mirrorData.Versions {\n\t\tversions = append(versions, &models.Version{\n\t\t\tVersion:   version,\n\t\t\tPlatforms: availablePlatforms,\n\t\t})\n\t}\n\n\treturn versions, nil\n}\n\n// GetPlatform implements ProviderHandler.GetPlatform\nfunc (handler *NetworkMirrorProviderHandler) GetPlatform(ctx context.Context, provider *models.Provider) (*models.ResponseBody, error) {\n\tvar mirrorData struct {\n\t\tArchives map[string]struct {\n\t\t\tURL    string   `json:\"url\"`\n\t\t\tHashes []string `json:\"hashes\"`\n\t\t} `json:\"archives\"`\n\t}\n\n\treqPath := path.Join(provider.RegistryName, provider.Namespace, provider.Name, provider.Version+\".json\")\n\treqURL := fmt.Sprintf(\"%s/%s\", strings.TrimRight(handler.networkMirrorURL.String(), \"/\"), reqPath)\n\n\tif err := handler.client.Do(ctx, http.MethodGet, reqURL, &mirrorData); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar resp *models.ResponseBody\n\n\tif archive, ok := mirrorData.Archives[provider.Platform()]; ok {\n\t\tresp = (&models.ResponseBody{\n\t\t\tFilename:    filepath.Base(archive.URL),\n\t\t\tDownloadURL: archive.URL,\n\t\t}).ResolveRelativeReferences(handler.networkMirrorURL.ResolveReference(&url.URL{\n\t\t\tPath: path.Join(handler.networkMirrorURL.Path, provider.Address()),\n\t\t}))\n\t}\n\n\treturn resp, nil\n}\n"
  },
  {
    "path": "internal/tf/cache/handlers/provider.go",
    "content": "// Package handlers provides the interfaces and common implementations for handling provider requests.\npackage handlers\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/models\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cliconfig\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nvar availablePlatforms []*models.Platform = []*models.Platform{\n\t{OS: \"solaris\", Arch: \"amd64\"},\n\t{OS: \"openbsd\", Arch: \"386\"},\n\t{OS: \"openbsd\", Arch: \"arm\"},\n\t{OS: \"openbsd\", Arch: \"amd64\"},\n\t{OS: \"freebsd\", Arch: \"386\"},\n\t{OS: \"freebsd\", Arch: \"arm\"},\n\t{OS: \"freebsd\", Arch: \"amd64\"},\n\t{OS: \"linux\", Arch: \"386\"},\n\t{OS: \"linux\", Arch: \"arm\"},\n\t{OS: \"linux\", Arch: \"arm64\"},\n\t{OS: \"linux\", Arch: \"amd64\"},\n\t{OS: \"darwin\", Arch: \"amd64\"},\n\t{OS: \"darwin\", Arch: \"arm64\"},\n\t{OS: \"windows\", Arch: \"386\"},\n\t{OS: \"windows\", Arch: \"amd64\"},\n}\n\n// ProviderHandlers is a slice of ProviderHandler.\ntype ProviderHandlers []ProviderHandler\n\nfunc NewProviderHandlers(cliCfg *cliconfig.Config, logger log.Logger, registryNames []string) (ProviderHandlers, error) {\n\tvar (\n\t\tproviderHandlers = make([]ProviderHandler, 0, len(cliCfg.ProviderInstallation.Methods))\n\t\texcludeAddrs     = make([]string, 0, len(cliCfg.ProviderInstallation.Methods))\n\t\tdirectIsDefined  bool\n\t)\n\n\tfor _, registryName := range registryNames {\n\t\texcludeAddrs = append(excludeAddrs, registryName+\"/*/*\")\n\t}\n\n\tfor _, method := range cliCfg.ProviderInstallation.Methods {\n\t\tswitch method := method.(type) {\n\t\tcase *cliconfig.ProviderInstallationFilesystemMirror:\n\t\t\tproviderHandlers = append(providerHandlers, NewFilesystemMirrorProviderHandler(logger, method))\n\t\tcase *cliconfig.ProviderInstallationNetworkMirror:\n\t\t\tnetworkMirrorHandler, err := NewNetworkMirrorProviderHandler(logger, method, cliCfg.CredentialsSource())\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tproviderHandlers = append(providerHandlers, networkMirrorHandler)\n\t\tcase *cliconfig.ProviderInstallationDirect:\n\t\t\tproviderHandlers = append(providerHandlers, NewDirectProviderHandler(logger, method, cliCfg.CredentialsSource()))\n\t\t\tdirectIsDefined = true\n\t\t}\n\n\t\tmethod.AppendExclude(excludeAddrs)\n\t}\n\n\tif !directIsDefined {\n\t\t// In a case if none of direct provider installation methods `cliCfg.ProviderInstallation.Methods` are specified.\n\t\tproviderHandlers = append(providerHandlers, NewDirectProviderHandler(logger, new(cliconfig.ProviderInstallationDirect), cliCfg.CredentialsSource()))\n\t}\n\n\treturn providerHandlers, nil\n}\n\n// DiscoveryURL looks for the first handler that can handle the given `registryName`,\n// which is determined by the include and exclude settings in the `.terraformrc` CLI config file.\n// If the handler is found, tries to discover its API endpoints otherwise return the default registry URLs.\nfunc (handlers ProviderHandlers) DiscoveryURL(ctx context.Context, registryName string) (*RegistryURLs, error) {\n\tprovider := models.ParseProvider(registryName)\n\n\tfor _, handler := range handlers {\n\t\tif handler.CanHandleProvider(provider) {\n\t\t\treturn handler.DiscoveryURL(ctx, registryName)\n\t\t}\n\t}\n\n\treturn DefaultRegistryURLs, nil\n}\n\ntype ProviderHandler interface {\n\t// CanHandleProvider returns true if the given provider can be handled by this handler.\n\tCanHandleProvider(provider *models.Provider) bool\n\n\t// GetVersions serves a request that returns all versions for a single provider.\n\tGetVersions(ctx context.Context, provider *models.Provider) (models.Versions, error)\n\n\t// GetPlatform serves a request that returns a provider for a specific platform.\n\tGetPlatform(ctx context.Context, provider *models.Provider) (*models.ResponseBody, error)\n\n\t// DiscoveryURL discovers modules and providers API endpoints for the specified `registryName`.\n\t// https://developer.hashicorp.com/terraform/internals/remote-service-discovery#discovery-process\n\tDiscoveryURL(ctx context.Context, registryName string) (*RegistryURLs, error)\n}\n"
  },
  {
    "path": "internal/tf/cache/handlers/provider_test.go",
    "content": "package handlers_test\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"net/url\"\n\t\"syscall\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/handlers\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cliconfig\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestIsOfflineError(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\terr      error\n\t\tdesc     string\n\t\texpected bool\n\t}{\n\t\t// *url.Error wrapping various transport failures — all caught by the single *url.Error check.\n\t\t{err: &url.Error{Op: \"Get\", URL: \"https://registry.terraform.io/.well-known/terraform.json\", Err: syscall.ECONNREFUSED}, desc: \"connection refused\", expected: true},\n\t\t{err: &url.Error{Op: \"Get\", URL: \"https://registry.terraform.io/.well-known/terraform.json\", Err: syscall.ECONNRESET}, desc: \"connection reset\", expected: true},\n\t\t{err: &url.Error{Op: \"Get\", URL: \"https://registry.terraform.io/.well-known/terraform.json\", Err: syscall.ENETUNREACH}, desc: \"network unreachable\", expected: true},\n\t\t{err: &url.Error{Op: \"Get\", URL: \"https://registry.terraform.io/.well-known/terraform.json\", Err: &net.DNSError{Err: \"no such host\", Name: \"registry.terraform.io\", IsNotFound: true}}, desc: \"DNS not found\", expected: true},\n\t\t{err: &url.Error{Op: \"Get\", URL: \"https://registry.terraform.io/.well-known/terraform.json\", Err: &net.DNSError{Err: \"server misbehaving\", Name: \"blocked-registry.invalid\"}}, desc: \"DNS temporary failure\", expected: true},\n\t\t{err: &url.Error{Op: \"Get\", URL: \"https://registry.terraform.io/.well-known/terraform.json\", Err: errors.New(\"tls: failed to verify certificate\")}, desc: \"TLS error\", expected: true},\n\t\t// Non-transport errors — should NOT be treated as offline.\n\t\t{err: errors.New(\"random error\"), desc: \"a random error that should not be offline\", expected: false},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.desc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tresult := handlers.IsOfflineError(tc.err)\n\t\t\tassert.Equal(t, tc.expected, result, \"Expected result for %v is %v\", tc.desc, tc.expected)\n\t\t})\n\t}\n}\n\n// TestProviderHandlers_DiscoveryURL_WithNetworkMirrorForBlockedRegistry reproduces the bug from issue #5613.\n// When a network_mirror is configured for a registry that is unreachable (e.g., blocked by DNS),\n// DiscoveryURL should return DefaultRegistryURLs without attempting to contact the registry.\n// The \".invalid\" TLD is guaranteed to never resolve per RFC 2606.\nfunc TestProviderHandlers_DiscoveryURL_WithNetworkMirrorForBlockedRegistry(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := &cliconfig.Config{\n\t\tProviderInstallation: &cliconfig.ProviderInstallation{\n\t\t\tMethods: cliconfig.ProviderInstallationMethods{\n\t\t\t\tcliconfig.NewProviderInstallationNetworkMirror(\n\t\t\t\t\t\"https://mirror.example.com/providers/\",\n\t\t\t\t\t[]string{\"blocked-registry.invalid/*/*\"},\n\t\t\t\t\tnil,\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t}\n\n\tproviderHandlers, err := handlers.NewProviderHandlers(cfg, log.New(), nil)\n\trequire.NoError(t, err)\n\n\turls, err := providerHandlers.DiscoveryURL(context.Background(), \"blocked-registry.invalid\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, handlers.DefaultRegistryURLs, urls)\n}\n"
  },
  {
    "path": "internal/tf/cache/handlers/proxy_provider.go",
    "content": "package handlers\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/models\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/router\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cliconfig\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/labstack/echo/v4\"\n)\n\nconst (\n\t// Provider's assets consist of three files/URLs: zipped binary, hashes and signature\n\tProviderDownloadURLName         providerURLName = \"download_url\"\n\tProviderSHASumsURLName          providerURLName = \"shasums_url\"\n\tProviderSHASumsSignatureURLName providerURLName = \"shasums_signature_url\"\n)\n\nvar (\n\t// providerURLNames contains urls that must be modified to forward terraform requests through this server.\n\tproviderURLNames = []providerURLName{\n\t\tProviderDownloadURLName,\n\t\tProviderSHASumsURLName,\n\t\tProviderSHASumsSignatureURLName,\n\t}\n)\n\ntype providerURLName string\n\ntype ProxyProviderHandler struct {\n\t*CommonProviderHandler\n\t*helpers.ReverseProxy\n}\n\nfunc NewProxyProviderHandler(logger log.Logger, credsSource *cliconfig.CredentialsSource) *ProxyProviderHandler {\n\treturn &ProxyProviderHandler{\n\t\tCommonProviderHandler: NewCommonProviderHandler(logger, nil, nil),\n\t\tReverseProxy:          &helpers.ReverseProxy{CredsSource: credsSource, Logger: logger},\n\t}\n}\n\nfunc (handler *ProxyProviderHandler) String() string {\n\treturn \"proxy\"\n}\n\n// GetVersions implements ProviderHandler.GetVersions\n// https://developer.hashicorp.com/terraform/cloud-docs/api-docs/private-registry/provider-versions-platforms#get-all-versions-for-a-single-provider\n//\n//nolint:lll\nfunc (handler *ProxyProviderHandler) GetVersions(ctx echo.Context, provider *models.Provider) error {\n\tapiURLs, err := handler.DiscoveryURL(ctx.Request().Context(), provider.RegistryName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treqURL := &url.URL{\n\t\tScheme: \"https\",\n\t\tHost:   provider.RegistryName,\n\t\tPath:   path.Join(apiURLs.ProvidersV1, provider.Namespace, provider.Name, \"versions\"),\n\t}\n\n\treturn handler.NewRequest(ctx, reqURL)\n}\n\n// GetPlatform implements ProviderHandler.GetPlatform\nfunc (handler *ProxyProviderHandler) GetPlatform(ctx echo.Context, provider *models.Provider, downloaderController router.Controller) error {\n\tapiURLs, err := handler.DiscoveryURL(ctx.Request().Context(), provider.RegistryName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tplatformURL := &url.URL{\n\t\tScheme: \"https\",\n\t\tHost:   provider.RegistryName,\n\t\tPath:   path.Join(apiURLs.ProvidersV1, provider.Namespace, provider.Name, provider.Version, \"download\", provider.OS, provider.Arch),\n\t}\n\n\treturn handler.ReverseProxy.\n\t\tWithModifyResponse(func(resp *http.Response) error {\n\t\t\treturn modifyDownloadURLsInJSONBody(resp, downloaderController)\n\t\t}).\n\t\tNewRequest(ctx, platformURL)\n}\n\n// Download implements ProviderHandler.Download\nfunc (handler *ProxyProviderHandler) Download(ctx echo.Context, provider *models.Provider) error {\n\t// check if the URL contains http scheme, it may just be a filename and we need to build the URL\n\tif !strings.Contains(provider.DownloadURL, \"://\") {\n\t\tapiURLs, err := handler.DiscoveryURL(ctx.Request().Context(), provider.RegistryName)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdownloadURL := &url.URL{\n\t\t\tScheme: \"https\",\n\t\t\tHost:   provider.RegistryName,\n\t\t\tPath:   filepath.Join(apiURLs.ProvidersV1, provider.RegistryName, provider.Namespace, provider.Name, provider.DownloadURL),\n\t\t}\n\n\t\treturn handler.NewRequest(ctx, downloadURL)\n\t}\n\n\tdownloadURL, err := url.Parse(provider.DownloadURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn handler.NewRequest(ctx, downloadURL)\n}\n\n// modifyDownloadURLsInJSONBody modifies the response to redirect the download URLs to the local server.\nfunc modifyDownloadURLsInJSONBody(resp *http.Response, downloaderController router.Controller) error {\n\tvar data map[string]json.RawMessage\n\n\treturn helpers.ModifyJSONBody(resp, &data, func() error {\n\t\tfor _, name := range providerURLNames {\n\t\t\tlinkBytes, ok := data[string(name)]\n\t\t\tif !ok || linkBytes == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlink := string(linkBytes)\n\n\t\t\tlink, err := strconv.Unquote(link)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tlinkURL, err := url.Parse(link)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// Modify link to http://{localhost_host}/downloads/provider/{remote_host}/{remote_path}\n\t\t\tlinkURL.Path = path.Join(downloaderController.URL().Path, linkURL.Host, linkURL.Path)\n\t\t\tlinkURL.Scheme = downloaderController.URL().Scheme\n\t\t\tlinkURL.Host = downloaderController.URL().Host\n\n\t\t\tlink = strconv.Quote(linkURL.String())\n\t\t\tdata[string(name)] = []byte(link)\n\t\t}\n\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "internal/tf/cache/handlers/registry_urls.go",
    "content": "package handlers\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\nconst (\n\t// well-known address for discovery URLs\n\twellKnownURL = \".well-known/terraform.json\"\n)\n\nvar (\n\tDefaultRegistryURLs = &RegistryURLs{\n\t\tModulesV1:   \"/v1/modules\",\n\t\tProvidersV1: \"/v1/providers\",\n\t}\n)\n\ntype RegistryURLs struct {\n\tModulesV1   string `json:\"modules.v1\"`\n\tProvidersV1 string `json:\"providers.v1\"`\n}\n\nfunc (urls *RegistryURLs) String() string {\n\tif b, err := json.Marshal(urls); err == nil {\n\t\treturn string(b)\n\t}\n\n\treturn fmt.Sprintf(\"%v, %v\", urls.ModulesV1, urls.ProvidersV1)\n}\n\nfunc DiscoveryURL(ctx context.Context, registryName string) (*RegistryURLs, error) {\n\turl := fmt.Sprintf(\"https://%s/%s\", registryName, wellKnownURL)\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tresp, err := (&http.Client{}).Do(req)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\tdefer resp.Body.Close() //nolint:errcheck\n\n\tswitch resp.StatusCode {\n\tcase http.StatusNotFound, http.StatusInternalServerError:\n\t\treturn nil, errors.New(NotFoundWellKnownURLError{wellKnownURL})\n\tcase http.StatusOK:\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"%s returned %s\", url, resp.Status)\n\t}\n\n\tcontent, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\turls := new(RegistryURLs)\n\tif err := json.Unmarshal(content, urls); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\treturn urls, nil\n}\n\n// IsOfflineError returns true if the given error indicates that the registry\n// could not be reached, and default URLs should be used instead.\nfunc IsOfflineError(err error) bool {\n\tif errors.As(err, &NotFoundWellKnownURLError{}) {\n\t\treturn true\n\t}\n\n\tvar urlErr *url.Error\n\n\treturn errors.As(err, &urlErr)\n}\n"
  },
  {
    "path": "internal/tf/cache/helpers/client.go",
    "content": "package helpers\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cliconfig\"\n\tsvchost \"github.com/hashicorp/terraform-svchost\"\n\t\"github.com/puzpuzpuz/xsync/v3\"\n)\n\n// Client is an HTTP client.\ntype Client struct {\n\t*http.Client\n\n\tcredsSource *cliconfig.CredentialsSource\n\tcache       *xsync.MapOf[string, []byte]\n}\n\nfunc NewClient(credsSource *cliconfig.CredentialsSource) *Client {\n\treturn &Client{\n\t\tClient:      &http.Client{},\n\t\tcredsSource: credsSource,\n\t\tcache:       xsync.NewMapOf[string, []byte](),\n\t}\n}\n\n// Do sends an HTTP request and decodes an HTTP response to the given `value`.\nfunc (client *Client) Do(ctx context.Context, method, reqURL string, value any) error {\n\tif bodyBytes, ok := client.cache.Load(reqURL); ok {\n\t\treturn unmarshalBody(bodyBytes, value)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, method, reqURL, nil)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tif client.credsSource != nil {\n\t\thostname := svchost.Hostname(req.URL.Hostname())\n\t\tif creds := client.credsSource.ForHost(hostname); creds != nil {\n\t\t\tcreds.PrepareRequest(req)\n\t\t}\n\t}\n\n\tresp, err := client.Client.Do(req)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\tdefer resp.Body.Close() //nolint:errcheck\n\n\tbodyBytes, err := decodeResponse(resp)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tclient.cache.Store(reqURL, bodyBytes)\n\n\treturn unmarshalBody(bodyBytes, value)\n}\n\nfunc unmarshalBody(data []byte, value any) error {\n\tif data == nil {\n\t\treturn nil\n\t}\n\n\tif err := json.Unmarshal(data, value); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n\nfunc decodeResponse(resp *http.Response) ([]byte, error) {\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, nil\n\t}\n\n\tbuffer, err := ResponseBuffer(resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbodyBytes, err := io.ReadAll(buffer)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tresp.Body = io.NopCloser(buffer)\n\n\treturn bodyBytes, nil\n}\n"
  },
  {
    "path": "internal/tf/cache/helpers/http.go",
    "content": "// Package helpers provides utility functions for working with HTTP requests and responses.\npackage helpers\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n)\n\nfunc Fetch(ctx context.Context, req *http.Request, dst io.Writer) error {\n\treq.Header.Add(\"Accept-Encoding\", \"gzip\")\n\n\tresp, err := (&http.Client{}).Do(req)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\tdefer resp.Body.Close() //nolint:errcheck\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"%s returned from %s\", resp.Status, req.URL)\n\t}\n\n\treader, err := ResponseReader(resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif written, err := util.Copy(ctx, dst, reader); err != nil {\n\t\treturn errors.New(err)\n\t} else if resp.ContentLength != -1 && written != resp.ContentLength {\n\t\treturn errors.Errorf(\"incorrect response size: expected %d bytes, but got %d bytes\", resp.ContentLength, written)\n\t}\n\n\treturn nil\n}\n\n// FetchToFile downloads the file from the given `url` into the specified `dst` file.\nfunc FetchToFile(ctx context.Context, req *http.Request, dst string) error {\n\tfile, err := os.Create(dst)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\tdefer file.Close() //nolint:errcheck\n\n\tif err := Fetch(ctx, req, file); err != nil {\n\t\treturn err\n\t}\n\n\tif err := file.Sync(); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n\nfunc ResponseReader(resp *http.Response) (io.ReadCloser, error) {\n\t// Check that the server actually sent compressed data\n\tswitch resp.Header.Get(\"Content-Encoding\") {\n\tcase \"gzip\":\n\t\treader, err := gzip.NewReader(resp.Body)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tresp.Header.Del(\"Content-Encoding\")\n\t\tresp.Header.Del(\"Content-Length\")\n\t\tresp.ContentLength = -1\n\t\tresp.Uncompressed = true\n\n\t\treturn reader, nil\n\tdefault:\n\t\treturn resp.Body, nil\n\t}\n}\n\nfunc ResponseBuffer(resp *http.Response) (*bytes.Buffer, error) {\n\treader, err := ResponseReader(resp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer reader.Close() //nolint:errcheck\n\n\tbuffer := new(bytes.Buffer)\n\n\tif _, err := buffer.ReadFrom(reader); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\treturn buffer, nil\n}\n\nfunc ModifyJSONBody(resp *http.Response, value any, fn func() error) error {\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil\n\t}\n\n\tbuffer, err := ResponseBuffer(resp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdecoder := json.NewDecoder(buffer)\n\tif err := decoder.Decode(value); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tif fn == nil {\n\t\treturn nil\n\t}\n\n\tif err := fn(); err != nil {\n\t\treturn err\n\t}\n\n\tencoder := json.NewEncoder(buffer)\n\tif err := encoder.Encode(value); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tresp.Body = io.NopCloser(buffer)\n\tresp.ContentLength = int64(buffer.Len())\n\tresp.Header.Set(\"Content-Length\", strconv.Itoa(buffer.Len()))\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/tf/cache/helpers/reverse_proxy.go",
    "content": "package helpers\n\nimport (\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cliconfig\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\tsvchost \"github.com/hashicorp/terraform-svchost\"\n\t\"github.com/labstack/echo/v4\"\n)\n\ntype ReverseProxy struct {\n\tServerURL   *url.URL\n\tCredsSource *cliconfig.CredentialsSource\n\n\tRewrite        func(*httputil.ProxyRequest)\n\tModifyResponse func(resp *http.Response) error\n\tErrorHandler   func(http.ResponseWriter, *http.Request, error)\n\n\tLogger log.Logger\n}\n\nfunc (reverseProxy ReverseProxy) WithModifyResponse(fn func(resp *http.Response) error) *ReverseProxy {\n\treverseProxy.ModifyResponse = fn\n\treturn &reverseProxy\n}\n\nfunc (reverseProxy *ReverseProxy) NewRequest(ctx echo.Context, targetURL *url.URL) (er error) {\n\tproxy := &httputil.ReverseProxy{\n\t\tRewrite: func(req *httputil.ProxyRequest) {\n\t\t\treq.Out.Host = targetURL.Host\n\t\t\treq.Out.URL = targetURL\n\n\t\t\tif reverseProxy.CredsSource != nil {\n\t\t\t\thostname := svchost.Hostname(req.Out.URL.Hostname())\n\t\t\t\tif creds := reverseProxy.CredsSource.ForHost(hostname); creds != nil {\n\t\t\t\t\tcreds.PrepareRequest(req.Out)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif reverseProxy.Rewrite != nil {\n\t\t\t\treverseProxy.Rewrite(req)\n\t\t\t}\n\t\t},\n\t\tModifyResponse: func(resp *http.Response) error {\n\t\t\tif reverseProxy.ModifyResponse != nil {\n\t\t\t\treturn reverseProxy.ModifyResponse(resp)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t\tErrorHandler: func(resp http.ResponseWriter, req *http.Request, err error) {\n\t\t\treverseProxy.Logger.Errorf(\"remote %s unreachable, could not forward: %v\", targetURL, err)\n\t\t\tctx.Error(echo.NewHTTPError(http.StatusServiceUnavailable))\n\n\t\t\tif reverseProxy.ErrorHandler != nil {\n\t\t\t\treverseProxy.ErrorHandler(resp, req, err)\n\t\t\t}\n\t\t},\n\t}\n\n\tproxy.ServeHTTP(ctx.Response(), ctx.Request())\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/tf/cache/middleware/key_auth.go",
    "content": "package middleware\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/labstack/echo/v4/middleware\"\n)\n\ntype Authorization struct {\n\tToken string\n}\n\n// Validator validates tokens.\n//\n// To enhance security, we use token-based authentication to connect to the cache server in order to prevent unauthorized connections from third-party applications.\n// Currently, the cache server only supports `x-api-key` token, the value of which can be any text.\nfunc (auth *Authorization) Validator(bearerToken string, ctx echo.Context) (bool, error) {\n\tif bearerToken != auth.Token {\n\t\treturn false, errors.Errorf(\"Authorization: token either expired or inexistent\")\n\t}\n\n\treturn true, nil\n}\n\n// KeyAuth returns an KeyAuth middleware.\nfunc KeyAuth(token string) echo.MiddlewareFunc {\n\tauth := Authorization{\n\t\tToken: token,\n\t}\n\n\treturn middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{\n\t\tSkipper:    middleware.DefaultSkipper,\n\t\tKeyLookup:  \"header:\" + echo.HeaderAuthorization,\n\t\tAuthScheme: \"Bearer\",\n\t\tValidator:  auth.Validator,\n\t})\n}\n"
  },
  {
    "path": "internal/tf/cache/middleware/logger.go",
    "content": "package middleware\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\"\n\t\"github.com/labstack/echo/v4\"\n\t\"github.com/labstack/echo/v4/middleware\"\n)\n\nfunc Logger(logger log.Logger) echo.MiddlewareFunc {\n\treturn middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{\n\t\tLogStatus:   true,\n\t\tLogURI:      true,\n\t\tLogError:    true,\n\t\tHandleError: true, // forwards error to the global error handler, so it can decide appropriate status code\n\t\tLogValuesFunc: func(_ echo.Context, req middleware.RequestLoggerValues) error {\n\t\t\tlogger := logger.\n\t\t\t\tWithField(placeholders.CacheServerURLKeyName, req.URI).\n\t\t\t\tWithField(placeholders.CacheServerStatusKeyName, req.Status)\n\t\t\tif req.Error != nil {\n\t\t\t\tlogger.Errorf(\"Cache server was unable to process the received request, %s\", req.Error.Error())\n\t\t\t} else {\n\t\t\t\tlogger.Tracef(\"Cache server received request\")\n\t\t\t}\n\n\t\t\treturn nil\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "internal/tf/cache/middleware/package.go",
    "content": "// Package middleware provides a set of middleware for the Terragrunt provider cache server.\npackage middleware\n"
  },
  {
    "path": "internal/tf/cache/middleware/recover.go",
    "content": "package middleware\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/labstack/echo/v4\"\n)\n\nfunc Recover(logger log.Logger) echo.MiddlewareFunc {\n\treturn func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\treturn func(ctx echo.Context) (er error) {\n\t\t\tdefer errors.Recover(func(err error) {\n\t\t\t\tlogger.Debug(errors.ErrorStack(err))\n\t\t\t\ter = err\n\t\t\t})\n\n\t\t\treturn next(ctx)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/tf/cache/models/helper.go",
    "content": "package models\n\nimport (\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n)\n\nfunc resolveRelativeReference(base *url.URL, link string) string {\n\tif link == \"\" {\n\t\treturn link\n\t}\n\n\tif strings.Contains(link, \"://\") {\n\t\treturn link\n\t}\n\n\tif strings.HasPrefix(link, \"/\") {\n\t\treturn (&url.URL{\n\t\t\tScheme: base.Scheme,\n\t\t\tHost:   base.Host,\n\t\t\tPath:   link,\n\t\t}).String()\n\t}\n\n\treturn base.ResolveReference(\n\t\t&url.URL{\n\t\t\tPath: path.Join(\n\t\t\t\tbase.Path,\n\t\t\t\tlink,\n\t\t\t),\n\t\t},\n\t).String()\n}\n"
  },
  {
    "path": "internal/tf/cache/models/provider.go",
    "content": "// Package models provides the data structures used to represent Terraform providers and their details.\npackage models\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n\n\tgoversion \"github.com/hashicorp/go-version\"\n)\n\ntype Providers []*Provider\n\nfunc ParseProviders(strs ...string) Providers {\n\tvar prvoiders Providers\n\n\tfor _, str := range strs {\n\t\tif provider := ParseProvider(str); provider != nil {\n\t\t\tprvoiders = append(prvoiders, provider)\n\t\t}\n\t}\n\n\treturn prvoiders\n}\n\nfunc (providers Providers) Find(target *Provider) *Provider {\n\tfor _, provider := range providers {\n\t\tif provider.Match(target) {\n\t\t\treturn provider\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// SigningKey represents a key used to sign packages from a registry, along with an optional trust signature from the registry operator. These are both in ASCII armored OpenPGP format.\ntype SigningKey struct {\n\tASCIIArmor     string `json:\"ascii_armor\"`\n\tTrustSignature string `json:\"trust_signature\"`\n}\n\ntype SigningKeyList struct {\n\tGPGPublicKeys []*SigningKey `json:\"gpg_public_keys\"`\n}\n\nfunc (list SigningKeyList) Keys() map[string]string {\n\tkeys := make(map[string]string)\n\n\tfor _, key := range list.GPGPublicKeys {\n\t\tkeys[key.ASCIIArmor] = key.TrustSignature\n\t}\n\n\treturn keys\n}\n\ntype Versions []*Version\n\ntype Version struct {\n\tVersion   string    `json:\"version\"`\n\tProtocols []string  `json:\"protocols\"`\n\tPlatforms Platforms `json:\"platforms\"`\n}\n\nfunc (version Version) String() string {\n\treturn fmt.Sprintf(\"%s/%s/%s\", version.Version, version.Protocols, version.Platforms)\n}\n\n// FilterValid returns only versions with valid semver strings that conform to\n// the Terraform registry protocol (no \"v\" prefix, no empty strings).\n// The second return value contains the invalid version strings that were filtered out.\nfunc (versions Versions) FilterValid() (Versions, []string) {\n\tvalid := make(Versions, 0, len(versions))\n\tinvalid := make([]string, 0, len(versions))\n\n\tfor _, v := range versions {\n\t\tif v.Version == \"\" || strings.HasPrefix(v.Version, \"v\") {\n\t\t\tinvalid = append(invalid, v.Version)\n\t\t\tcontinue\n\t\t}\n\n\t\tif _, err := goversion.NewVersion(v.Version); err != nil {\n\t\t\tinvalid = append(invalid, v.Version)\n\t\t\tcontinue\n\t\t}\n\n\t\tvalid = append(valid, v)\n\t}\n\n\treturn valid, invalid\n}\n\ntype Platforms []*Platform\n\ntype Platform struct {\n\tOS   string `json:\"os\"`\n\tArch string `json:\"arch\"`\n}\n\nfunc (platform Platform) String() string {\n\treturn fmt.Sprintf(\"%s/%s\", platform.OS, platform.Arch)\n}\n\n// ResponseBody represents the details of the Terraform provider received from a registry.\ntype ResponseBody struct {\n\tPlatform\n\n\tProtocols []string `json:\"protocols,omitempty\"`\n\tFilename  string   `json:\"filename\"`\n\n\tDownloadURL            string `json:\"download_url\"`\n\tSHA256SumsURL          string `json:\"shasums_url,omitempty\"`\n\tSHA256SumsSignatureURL string `json:\"shasums_signature_url,omitempty\"`\n\n\tSHA256Sum   string         `json:\"shasum,omitempty\"`\n\tSigningKeys SigningKeyList `json:\"signing_keys\"`\n}\n\nfunc (body *ResponseBody) ResolveRelativeReferences(base *url.URL) *ResponseBody {\n\tclone := *body\n\tclone.DownloadURL = resolveRelativeReference(base, body.DownloadURL)\n\tclone.SHA256SumsSignatureURL = resolveRelativeReference(base, body.SHA256SumsSignatureURL)\n\tclone.SHA256SumsURL = resolveRelativeReference(base, body.SHA256SumsURL)\n\n\treturn &clone\n}\n\n// Provider represents the details of the Terraform provider.\ntype Provider struct {\n\t*ResponseBody\n\n\tRegistryName string\n\tNamespace    string\n\tName         string\n\tVersion      string\n\tOS           string\n\tArch         string\n\n\t// OriginalConstraints holds the version constraints from the module's required_providers block\n\tOriginalConstraints string\n}\n\nfunc ParseProvider(str string) *Provider {\n\tparts := strings.Split(str, \"/\")\n\tfor i := range parts {\n\t\tif parts[i] == \"*\" {\n\t\t\tparts[i] = \"\"\n\t\t}\n\t}\n\n\tconst twoVals = 2\n\n\tswitch {\n\tcase len(parts) == twoVals:\n\t\treturn &Provider{\n\t\t\tNamespace: parts[0],\n\t\t\tName:      parts[1],\n\t\t}\n\tcase len(parts) > twoVals:\n\t\treturn &Provider{\n\t\t\tRegistryName: parts[0],\n\t\t\tNamespace:    parts[1],\n\t\t\tName:         parts[2],\n\t\t}\n\t}\n\n\treturn &Provider{\n\t\tRegistryName: parts[0],\n\t}\n}\n\nfunc (provider *Provider) String() string {\n\tif provider.Version != \"\" {\n\t\treturn fmt.Sprintf(\"%s/%s/%s v%s\", provider.RegistryName, provider.Namespace, provider.Name, provider.Version)\n\t}\n\n\treturn fmt.Sprintf(\"%s/%s/%s\", provider.RegistryName, provider.Namespace, provider.Name)\n}\n\nfunc (provider *Provider) Platform() string {\n\treturn fmt.Sprintf(\"%s_%s\", provider.OS, provider.Arch)\n}\n\nfunc (provider *Provider) Address() string {\n\treturn path.Join(provider.RegistryName, provider.Namespace, provider.Name)\n}\n\nfunc (provider *Provider) Constraints() string {\n\treturn provider.OriginalConstraints\n}\n\n// Match returns true if all defined provider properties are matched.\nfunc (provider *Provider) Match(target *Provider) bool {\n\tregistryNameMatch := provider.RegistryName == \"\" || target.RegistryName == \"\" || provider.RegistryName == target.RegistryName\n\tnamespaceMatch := provider.Namespace == \"\" || target.Namespace == \"\" || provider.Namespace == target.Namespace\n\tnameMatch := provider.Name == \"\" || target.Name == \"\" || provider.Name == target.Name\n\tosMatch := provider.OS == \"\" || target.OS == \"\" || provider.OS == target.OS\n\tarchMatch := provider.Arch == \"\" || target.Arch == \"\" || provider.Arch == target.Arch\n\tdownloadURLMatch := provider.ResponseBody == nil || provider.DownloadURL == \"\" || target.DownloadURL == \"\" || provider.DownloadURL == target.DownloadURL\n\n\tif registryNameMatch && namespaceMatch && nameMatch && osMatch && archMatch && downloadURLMatch {\n\t\treturn true\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "internal/tf/cache/models/provider_test.go",
    "content": "package models_test\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/models\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFilterValid(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname            string\n\t\tinput           models.Versions\n\t\texpectedValid   []string\n\t\texpectedInvalid []string\n\t}{\n\t\t{\n\t\t\tname: \"all valid versions\",\n\t\t\tinput: models.Versions{\n\t\t\t\t{Version: \"1.0.0\"},\n\t\t\t\t{Version: \"2.5.2\"},\n\t\t\t\t{Version: \"0.1.0-beta1\"},\n\t\t\t},\n\t\t\texpectedValid:   []string{\"1.0.0\", \"2.5.2\", \"0.1.0-beta1\"},\n\t\t\texpectedInvalid: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"v-prefixed versions are filtered\",\n\t\t\tinput: models.Versions{\n\t\t\t\t{Version: \"1.0.0\"},\n\t\t\t\t{Version: \"v2.5.3\"},\n\t\t\t\t{Version: \"v1.0.0\"},\n\t\t\t},\n\t\t\texpectedValid:   []string{\"1.0.0\"},\n\t\t\texpectedInvalid: []string{\"v2.5.3\", \"v1.0.0\"},\n\t\t},\n\t\t{\n\t\t\tname: \"empty strings are filtered\",\n\t\t\tinput: models.Versions{\n\t\t\t\t{Version: \"1.0.0\"},\n\t\t\t\t{Version: \"\"},\n\t\t\t\t{Version: \"2.0.0\"},\n\t\t\t},\n\t\t\texpectedValid:   []string{\"1.0.0\", \"2.0.0\"},\n\t\t\texpectedInvalid: []string{\"\"},\n\t\t},\n\t\t{\n\t\t\tname: \"garbage strings are filtered\",\n\t\t\tinput: models.Versions{\n\t\t\t\t{Version: \"1.0.0\"},\n\t\t\t\t{Version: \"not-a-version\"},\n\t\t\t\t{Version: \"latest\"},\n\t\t\t},\n\t\t\texpectedValid:   []string{\"1.0.0\"},\n\t\t\texpectedInvalid: []string{\"not-a-version\", \"latest\"},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed valid and invalid\",\n\t\t\tinput: models.Versions{\n\t\t\t\t{Version: \"1.0.0\"},\n\t\t\t\t{Version: \"v2.5.3-alpha1\"},\n\t\t\t\t{Version: \"\"},\n\t\t\t\t{Version: \"3.1.4\"},\n\t\t\t\t{Version: \"not-a-version\"},\n\t\t\t\t{Version: \"0.1.0-beta1\"},\n\t\t\t},\n\t\t\texpectedValid:   []string{\"1.0.0\", \"3.1.4\", \"0.1.0-beta1\"},\n\t\t\texpectedInvalid: []string{\"v2.5.3-alpha1\", \"\", \"not-a-version\"},\n\t\t},\n\t\t{\n\t\t\tname:            \"all invalid\",\n\t\t\tinput:           models.Versions{{Version: \"v1.0.0\"}, {Version: \"\"}, {Version: \"bad\"}},\n\t\t\texpectedValid:   []string{},\n\t\t\texpectedInvalid: []string{\"v1.0.0\", \"\", \"bad\"},\n\t\t},\n\t\t{\n\t\t\tname:            \"empty input\",\n\t\t\tinput:           models.Versions{},\n\t\t\texpectedValid:   []string{},\n\t\t\texpectedInvalid: []string{},\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.Parallel()\n\n\t\t\tvalid, invalid := tc.input.FilterValid()\n\n\t\t\tvalidStrs := make([]string, 0, len(valid))\n\t\t\tfor _, v := range valid {\n\t\t\t\tvalidStrs = append(validStrs, v.Version)\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectedValid, validStrs)\n\t\t\tassert.Equal(t, tc.expectedInvalid, invalid)\n\t\t})\n\t}\n}\n\nfunc TestResolveRelativeReferences(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tbaseURL          string\n\t\tbody             models.ResponseBody\n\t\texpectedResolved models.ResponseBody\n\t}{\n\t\t{\n\t\t\t\"https://releases.hashicorp.com/terraform-provider-local/2.5.1\",\n\t\t\tmodels.ResponseBody{\n\t\t\t\tDownloadURL:            \"terraform-provider-local_2.5.1_darwin_amd64.zip\",\n\t\t\t\tSHA256SumsURL:          \"terraform-provider-local_2.5.1_SHA256SUMS\",\n\t\t\t\tSHA256SumsSignatureURL: \"terraform-provider-local_2.5.1_SHA256SUMS.72D7468F.sig\",\n\t\t\t},\n\t\t\tmodels.ResponseBody{\n\t\t\t\tDownloadURL:            \"https://releases.hashicorp.com/terraform-provider-local/2.5.1/terraform-provider-local_2.5.1_darwin_amd64.zip\",\n\t\t\t\tSHA256SumsURL:          \"https://releases.hashicorp.com/terraform-provider-local/2.5.1/terraform-provider-local_2.5.1_SHA256SUMS\",\n\t\t\t\tSHA256SumsSignatureURL: \"https://releases.hashicorp.com/terraform-provider-local/2.5.1/terraform-provider-local_2.5.1_SHA256SUMS.72D7468F.sig\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"https://somehost.com\",\n\t\t\tmodels.ResponseBody{\n\t\t\t\tDownloadURL:            \"https://releases.hashicorp.com/terraform-provider-local/2.5.1/terraform-provider-local_2.5.1_darwin_amd64.zip\",\n\t\t\t\tSHA256SumsURL:          \"https://releases.hashicorp.com/terraform-provider-local/2.5.1/terraform-provider-local_2.5.1_SHA256SUMS\",\n\t\t\t\tSHA256SumsSignatureURL: \"https://releases.hashicorp.com/terraform-provider-local/2.5.1/terraform-provider-local_2.5.1_SHA256SUMS.72D7468F.sig\",\n\t\t\t},\n\t\t\tmodels.ResponseBody{\n\t\t\t\tDownloadURL:            \"https://releases.hashicorp.com/terraform-provider-local/2.5.1/terraform-provider-local_2.5.1_darwin_amd64.zip\",\n\t\t\t\tSHA256SumsURL:          \"https://releases.hashicorp.com/terraform-provider-local/2.5.1/terraform-provider-local_2.5.1_SHA256SUMS\",\n\t\t\t\tSHA256SumsSignatureURL: \"https://releases.hashicorp.com/terraform-provider-local/2.5.1/terraform-provider-local_2.5.1_SHA256SUMS.72D7468F.sig\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"https://registry.company.com/v1/providers/ns/name/1.0/download/linux/amd64\",\n\t\t\tmodels.ResponseBody{\n\t\t\t\tDownloadURL:            \"/v1/providers/ns/name/1.0/download/linux/amd64/terraform-provider.zip\",\n\t\t\t\tSHA256SumsURL:          \"/v1/providers/ns/name/1.0/download/linux/amd64/terraform-provider_SHA256SUMS\",\n\t\t\t\tSHA256SumsSignatureURL: \"/v1/providers/ns/name/1.0/download/linux/amd64/terraform-provider_SHA256SUMS.sig\",\n\t\t\t},\n\t\t\tmodels.ResponseBody{\n\t\t\t\tDownloadURL:            \"https://registry.company.com/v1/providers/ns/name/1.0/download/linux/amd64/terraform-provider.zip\",\n\t\t\t\tSHA256SumsURL:          \"https://registry.company.com/v1/providers/ns/name/1.0/download/linux/amd64/terraform-provider_SHA256SUMS\",\n\t\t\t\tSHA256SumsSignatureURL: \"https://registry.company.com/v1/providers/ns/name/1.0/download/linux/amd64/terraform-provider_SHA256SUMS.sig\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tbaseURL, err := url.Parse(tc.baseURL)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tactualResolved := tc.body.ResolveRelativeReferences(baseURL)\n\t\t\tassert.Equal(t, tc.expectedResolved, *actualResolved)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/tf/cache/router/controller.go",
    "content": "package router\n\nimport \"net/url\"\n\n// Controller is an interface implemented by a REST controller\ntype Controller interface {\n\t// Register is the method called by the router, passing the router\n\t// groups to let the controller register its methods\n\tRegister(router *Router)\n\n\t// URL returns the controller fqdn.\n\tURL() *url.URL\n}\n"
  },
  {
    "path": "internal/tf/cache/router/router.go",
    "content": "// Package router provides a simple wrapper around the echo framework to create a REST API.\npackage router\n\nimport (\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/labstack/echo/v4\"\n)\n\ntype Router struct {\n\t*echo.Echo\n\n\t// urlPath is the router urlPath\n\turlPath string\n}\n\nfunc New() *Router {\n\treturn &Router{\n\t\tEcho:    echo.New(),\n\t\turlPath: \"/\",\n\t}\n}\n\nfunc (router *Router) Group(urlPath string) *Router {\n\treturn &Router{\n\t\tEcho:    router.Echo,\n\t\turlPath: path.Join(router.urlPath, urlPath),\n\t}\n}\n\nfunc (router *Router) URL() *url.URL {\n\treturn &url.URL{\n\t\tScheme: \"http\",\n\t\tHost:   router.Server.Addr,\n\t\tPath:   router.urlPath,\n\t}\n}\n\n// Register registers controller's endpoints\nfunc (router *Router) Register(controllers ...Controller) {\n\tfor _, controller := range controllers {\n\t\tcontroller.Register(router)\n\t}\n}\n\n// Use adds middleware to the chain which is run after router.\nfunc (router *Router) Use(middlewares ...echo.MiddlewareFunc) {\n\tfor _, middleware := range middlewares {\n\t\tmiddleware := func(next echo.HandlerFunc) echo.HandlerFunc {\n\t\t\treturn func(ctx echo.Context) error {\n\t\t\t\tif strings.HasPrefix(strings.Trim(ctx.Path(), \"/\"), strings.Trim(router.urlPath, \"/\")) {\n\t\t\t\t\treturn middleware(next)(ctx)\n\t\t\t\t}\n\n\t\t\t\treturn next(ctx)\n\t\t\t}\n\t\t}\n\t\trouter.Echo.Use(middleware)\n\t}\n}\n\n// GET registers a new GET route for a path with matching handler in the router\n// with optional route-level middleware.\nfunc (router *Router) GET(urlPath string, handle echo.HandlerFunc) {\n\trouter.Echo.GET(path.Join(router.urlPath, urlPath), handle)\n}\n\n// HEAD registers a new HEAD route for a path with matching handler in the\n// router with optional route-level middleware.\nfunc (router *Router) HEAD(urlPath string, handle echo.HandlerFunc) {\n\trouter.Echo.HEAD(path.Join(router.urlPath, urlPath), handle)\n}\n\n// OPTIONS registers a new OPTIONS route for a path with matching handler in the\n// router with optional route-level middleware.\nfunc (router *Router) OPTIONS(urlPath string, handle echo.HandlerFunc) {\n\trouter.Echo.OPTIONS(path.Join(router.urlPath, urlPath), handle)\n}\n\n// POST registers a new POST route for a path with matching handler in the\n// router with optional route-level middleware.\nfunc (router *Router) POST(urlPath string, handle echo.HandlerFunc) {\n\trouter.Echo.POST(path.Join(router.urlPath, urlPath), handle)\n}\n\n// PUT registers a new PUT route for a path with matching handler in the\n// router with optional route-level middleware.\nfunc (router *Router) PUT(urlPath string, handle echo.HandlerFunc) {\n\trouter.Echo.PUT(path.Join(router.urlPath, urlPath), handle)\n}\n\n// PATCH registers a new PATCH route for a path with matching handler in the\n// router with optional route-level middleware.\nfunc (router *Router) PATCH(urlPath string, handle echo.HandlerFunc) {\n\trouter.Echo.PATCH(path.Join(router.urlPath, urlPath), handle)\n}\n\n// DELETE registers a new DELETE route for a path with matching handler in the router\n// with optional route-level middleware.\nfunc (router *Router) DELETE(urlPath string, handle echo.HandlerFunc) {\n\trouter.Echo.DELETE(path.Join(router.urlPath, urlPath), handle)\n}\n"
  },
  {
    "path": "internal/tf/cache/server.go",
    "content": "// Package cache provides a private OpenTofu/Terraform provider cache server.\npackage cache\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/http\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/controllers\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/handlers\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/middleware\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/router\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/services\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// Server is a private Terraform cache for provider caching.\ntype Server struct {\n\t*router.Router\n\t*Config\n\tProviderController *controllers.ProviderController\n\tservices           []services.Service\n}\n\n// NewServer returns a new Server instance.\nfunc NewServer(opts ...Option) *Server {\n\tcfg := NewConfig(opts...)\n\n\tauthMiddleware := middleware.KeyAuth(cfg.token)\n\n\tdownloaderController := &controllers.DownloaderController{\n\t\tProxyProviderHandler: cfg.proxyProviderHandler,\n\t\tProviderService:      cfg.providerService,\n\t}\n\n\tproviderController := &controllers.ProviderController{\n\t\tAuthMiddleware:              authMiddleware,\n\t\tDownloaderController:        downloaderController,\n\t\tProviderHandlers:            cfg.providerHandlers,\n\t\tProxyProviderHandler:        cfg.proxyProviderHandler,\n\t\tProviderService:             cfg.providerService,\n\t\tCacheProviderHTTPStatusCode: cfg.cacheProviderHTTPStatusCode,\n\t\tLogger:                      cfg.logger,\n\t}\n\n\tdiscoveryController := &controllers.DiscoveryController{\n\t\tEndpointers: []controllers.Endpointer{providerController},\n\t}\n\n\trootRouter := router.New()\n\trootRouter.Use(middleware.Logger(cfg.logger))\n\trootRouter.Use(middleware.Recover(cfg.logger))\n\trootRouter.Register(discoveryController, downloaderController)\n\n\tv1Group := rootRouter.Group(\"v1\")\n\tv1Group.Register(providerController)\n\n\treturn &Server{\n\t\tRouter:             rootRouter,\n\t\tConfig:             cfg,\n\t\tservices:           []services.Service{cfg.providerService},\n\t\tProviderController: providerController,\n\t}\n}\n\n// DiscoveryURL looks for the first handler that can handle the given `registryName`,\n// which is determined by the include and exclude settings in the `.terraformrc` CLI config file.\n// If the handler is found, tries to discover its API endpoints otherwise return the default registry URLs.\nfunc (server *Server) DiscoveryURL(ctx context.Context, registryName string) (*handlers.RegistryURLs, error) {\n\treturn server.providerHandlers.DiscoveryURL(ctx, registryName)\n}\n\n// Listen starts listening to the given configuration address. It also automatically chooses a free port if not explicitly specified.\nfunc (server *Server) Listen(ctx context.Context) (net.Listener, error) {\n\tlc := &net.ListenConfig{}\n\n\tln, err := lc.Listen(ctx, \"tcp\", server.Addr())\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tserver.Server.Addr = ln.Addr().String()\n\n\tserver.logger.Infof(\"Terragrunt Cache server is listening on %s\", ln.Addr())\n\n\treturn ln, nil\n}\n\n// Run starts the webserver and workers.\nfunc (server *Server) Run(ctx context.Context, ln net.Listener) error {\n\tserver.logger.Infof(\"Start Terragrunt Cache server\")\n\n\terrGroup, ctx := errgroup.WithContext(ctx)\n\n\tfor _, service := range server.services {\n\t\terrGroup.Go(func() error {\n\t\t\treturn service.Run(ctx)\n\t\t})\n\t}\n\n\terrGroup.Go(func() error {\n\t\t<-ctx.Done()\n\t\tserver.logger.Infof(\"Shutting down Terragrunt Cache server...\")\n\n\t\tctx, cancel := context.WithTimeout(ctx, server.shutdownTimeout)\n\t\tdefer cancel()\n\n\t\tif err := server.Shutdown(ctx); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif err := server.Server.Serve(ln); err != nil && err != http.ErrServerClosed {\n\t\treturn errors.Errorf(\"error starting terragrunt cache server: %w\", err)\n\t}\n\n\tdefer server.logger.Infof(\"Terragrunt Cache server stopped\")\n\n\treturn errGroup.Wait()\n}\n"
  },
  {
    "path": "internal/tf/cache/services/provider_cache.go",
    "content": "// Package services provides services\n// that can be run in the background.\npackage services\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cache/models\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cliconfig\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/getproviders\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/internal/vfs\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\tsvchost \"github.com/hashicorp/terraform-svchost\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\nconst (\n\tunzipFileMode = os.FileMode(0000)\n\n\tretryDelayLockFile = time.Second * 5\n\tmaxRetriesLockFile = 60\n\n\tretryDelayFetchFile = time.Second * 2\n\tmaxRetriesFetchFile = 5\n\n\tproviderCacheWarmUpChBufferSize = 100\n\n\t// DefaultProviderFileSizeLimit is the maximum total decompressed size for provider archives (1 GB)\n\tDefaultProviderFileSizeLimit = 1 << 30 // 1 GiB\n\n\t// DefaultProviderFilesLimit is the maximum number of files in a provider archive\n\tDefaultProviderFilesLimit = 100\n)\n\ntype ProviderCaches []*ProviderCache\n\nfunc (caches ProviderCaches) Find(target *models.Provider) *ProviderCache {\n\tfor _, cache := range caches {\n\t\tif cache.Match(target) {\n\t\t\treturn cache\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (caches ProviderCaches) FindByRequestID(requestID string) ProviderCaches {\n\tvar foundCaches ProviderCaches\n\n\tfor _, cache := range caches {\n\t\tif cache.containsRequestID(requestID) {\n\t\t\tfoundCaches = append(foundCaches, cache)\n\t\t}\n\t}\n\n\treturn foundCaches\n}\n\nfunc (caches ProviderCaches) removeArchive() error {\n\tfor _, cache := range caches {\n\t\tif err := cache.removeArchive(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\ntype ProviderCache struct {\n\terr error\n\t*models.Provider\n\t*ProviderService\n\tstarted            chan struct{}\n\tuserProviderDir    string\n\tpackageDir         string\n\tlockfilePath       string\n\tarchivePath        string\n\tsignature          []byte\n\tdocumentSHA256Sums []byte\n\trequestIDs         []string\n\tarchiveCached      bool\n\tready              bool\n\tmu                 sync.RWMutex\n}\n\nfunc (cache *ProviderCache) DocumentSHA256Sums(ctx context.Context) ([]byte, error) {\n\tif existing := cache.getDocumentSHA256Sums(); existing != nil || cache.SHA256SumsURL == \"\" {\n\t\treturn existing, nil\n\t}\n\n\treturn cache.setDocumentSHA256Sums(ctx)\n}\n\nfunc (cache *ProviderCache) Signature(ctx context.Context) ([]byte, error) {\n\tif existing := cache.getSignature(); existing != nil || cache.SHA256SumsSignatureURL == \"\" {\n\t\treturn existing, nil\n\t}\n\n\treturn cache.setSignature(ctx)\n}\n\nfunc (cache *ProviderCache) Version() string {\n\treturn cache.Provider.Version\n}\n\nfunc (cache *ProviderCache) Address() string {\n\treturn cache.Provider.Address()\n}\n\nfunc (cache *ProviderCache) Constraints() string {\n\treturn cache.Provider.Constraints()\n}\n\nfunc (cache *ProviderCache) PackageDir() string {\n\treturn cache.packageDir\n}\n\nfunc (cache *ProviderCache) AuthenticatePackage(ctx context.Context) (*getproviders.PackageAuthenticationResult, error) {\n\tvar (\n\t\tchecksum           [sha256.Size]byte\n\t\tdocumentSHA256Sums []byte\n\t\tsignature          []byte\n\t\terr                error\n\t)\n\n\tif documentSHA256Sums, err = cache.DocumentSHA256Sums(ctx); err != nil || documentSHA256Sums == nil {\n\t\treturn nil, err\n\t}\n\n\tif signature, err = cache.Signature(ctx); err != nil || signature == nil {\n\t\treturn nil, err\n\t}\n\n\tif _, err := hex.Decode(checksum[:], []byte(cache.SHA256Sum)); err != nil {\n\t\treturn nil, errors.Errorf(\"registry response includes invalid SHA256 hash %q for provider %q: %w\", cache.SHA256Sum, cache.Provider, err)\n\t}\n\n\tchecks := []getproviders.PackageAuthentication{\n\t\tgetproviders.NewMatchingChecksumAuthentication(documentSHA256Sums, cache.Filename, checksum),\n\t\tgetproviders.NewArchiveChecksumAuthentication(checksum),\n\t}\n\n\tif len(cache.SigningKeys.Keys()) != 0 {\n\t\tchecks = append(checks, getproviders.NewSignatureAuthentication(documentSHA256Sums, signature, cache.SigningKeys.Keys()))\n\t} else {\n\t\t// `registry.opentofu.org` does not have signatures for some providers.\n\t\tcache.logger.Warnf(\"Signature validation was skipped due to the registry not containing GPG keys for the provider %s\", cache.Provider)\n\t}\n\n\treturn getproviders.PackageAuthenticationAll(checks...).Authenticate(cache.archivePath)\n}\n\nfunc (cache *ProviderCache) ArchivePath() string {\n\texists, err := vfs.FileExists(cache.ProviderService.FS(), cache.archivePath)\n\tif err != nil {\n\t\tcache.logger.Warnf(\"Error checking archive path %s: %v\", cache.archivePath, err)\n\t\treturn \"\"\n\t}\n\n\tif exists {\n\t\treturn cache.archivePath\n\t}\n\n\treturn \"\"\n}\n\nfunc (cache *ProviderCache) addRequestID(requestID string) {\n\tcache.mu.Lock()\n\tdefer cache.mu.Unlock()\n\n\tcache.requestIDs = append(cache.requestIDs, requestID)\n}\n\nfunc (cache *ProviderCache) containsRequestID(requestID string) bool {\n\tcache.mu.RLock()\n\tdefer cache.mu.RUnlock()\n\n\treturn slices.Contains(cache.requestIDs, requestID)\n}\n\nfunc (cache *ProviderCache) getRequestIDs() []string {\n\tcache.mu.RLock()\n\tdefer cache.mu.RUnlock()\n\n\tresult := make([]string, len(cache.requestIDs))\n\tcopy(result, cache.requestIDs)\n\n\treturn result\n}\n\nfunc (cache *ProviderCache) isReady() bool {\n\tcache.mu.RLock()\n\tdefer cache.mu.RUnlock()\n\n\treturn cache.ready\n}\n\nfunc (cache *ProviderCache) setReady(ready bool) {\n\tcache.mu.Lock()\n\tdefer cache.mu.Unlock()\n\n\tcache.ready = ready\n}\n\nfunc (cache *ProviderCache) getDocumentSHA256Sums() []byte {\n\tcache.mu.RLock()\n\tdefer cache.mu.RUnlock()\n\n\treturn cache.documentSHA256Sums\n}\n\nfunc (cache *ProviderCache) setDocumentSHA256Sums(ctx context.Context) ([]byte, error) {\n\tcache.mu.Lock()\n\tdefer cache.mu.Unlock()\n\n\tif cache.documentSHA256Sums != nil {\n\t\treturn cache.documentSHA256Sums, nil\n\t}\n\n\tvar documentSHA256Sums = new(bytes.Buffer)\n\n\treq, err := cache.newRequest(ctx, cache.SHA256SumsURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := helpers.Fetch(ctx, req, documentSHA256Sums); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to retrieve authentication checksums for provider %q: %w\", cache.Provider, err)\n\t}\n\n\tcache.documentSHA256Sums = documentSHA256Sums.Bytes()\n\n\treturn cache.documentSHA256Sums, nil\n}\n\nfunc (cache *ProviderCache) getSignature() []byte {\n\tcache.mu.RLock()\n\tdefer cache.mu.RUnlock()\n\n\treturn cache.signature\n}\n\nfunc (cache *ProviderCache) setSignature(ctx context.Context) ([]byte, error) {\n\tcache.mu.Lock()\n\tdefer cache.mu.Unlock()\n\n\tif cache.signature != nil {\n\t\treturn cache.signature, nil\n\t}\n\n\tvar signature = new(bytes.Buffer)\n\n\treq, err := cache.newRequest(ctx, cache.SHA256SumsSignatureURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := helpers.Fetch(ctx, req, signature); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to retrieve authentication signature for provider %q: %w\", cache.Provider, err)\n\t}\n\n\tcache.signature = signature.Bytes()\n\n\treturn cache.signature, nil\n}\n\n// warmUp checks if the required provider already exists in the cache directory, if not:\n// 1. Checks if the required provider exists in the user plugins directory, located at %APPDATA%\\terraform.d\\plugins on Windows and ~/.terraform.d/plugins on other systems. If so, creates a symlink to this folder. (Some providers are not available for darwin_arm64, in this case we can use https://github.com/kreuzwerker/m1-terraform-provider-helper which compiles and saves providers to the user plugins directory)\n// 2. Downloads the provider from the original registry, unpacks and saves it into the cache directory.\nfunc (cache *ProviderCache) warmUp(ctx context.Context) error {\n\tfs := cache.ProviderService.FS()\n\n\texists, err := vfs.FileExists(fs, cache.packageDir)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tif exists {\n\t\treturn nil\n\t}\n\n\tif err := fs.MkdirAll(filepath.Dir(cache.packageDir), os.ModePerm); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tuserProviderExists, err := vfs.FileExists(fs, cache.userProviderDir)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tif userProviderExists {\n\t\tcache.logger.Debugf(\"Create symlink file %s to %s\", cache.packageDir, cache.userProviderDir)\n\n\t\tif err := vfs.Symlink(fs, cache.userProviderDir, cache.packageDir); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\tcache.logger.Infof(\"Cached %s from user plugins directory\", cache.Provider)\n\n\t\treturn nil\n\t}\n\n\tif cache.DownloadURL == \"\" {\n\t\treturn errors.Errorf(\"not found provider download url\")\n\t}\n\n\tdownloadURLExists, err := vfs.FileExists(fs, cache.DownloadURL)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tif downloadURLExists {\n\t\tcache.archivePath = cache.DownloadURL\n\t} else {\n\t\tif err := util.DoWithRetry(ctx, fmt.Sprintf(\"Fetching provider %s\", cache.Provider), maxRetriesFetchFile, retryDelayFetchFile, cache.logger, log.DebugLevel, func(ctx context.Context) error {\n\t\t\treq, err := cache.newRequest(ctx, cache.DownloadURL)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn helpers.FetchToFile(ctx, req, cache.archivePath)\n\t\t}); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcache.archiveCached = true\n\t}\n\n\tcache.logger.Debugf(\"Unpack provider archive %s\", cache.archivePath)\n\n\tif err := vfs.NewZipDecompressor(\n\t\tvfs.WithFileSizeLimit(DefaultProviderFileSizeLimit),\n\t\tvfs.WithFilesLimit(DefaultProviderFilesLimit),\n\t).Unzip(\n\t\tcache.logger,\n\t\tfs,\n\t\tcache.packageDir,\n\t\tcache.archivePath,\n\t\tunzipFileMode,\n\t); err != nil {\n\t\treturn err\n\t}\n\n\tauth, err := cache.AuthenticatePackage(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcache.logger.Infof(\"Cached %s (%s)\", cache.Provider, auth)\n\n\treturn nil\n}\n\nfunc (cache *ProviderCache) newRequest(ctx context.Context, url string) (*http.Request, error) {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tif cache.credsSource == nil {\n\t\treturn req, nil\n\t}\n\n\thostname := svchost.Hostname(req.URL.Hostname())\n\tif creds := cache.credsSource.ForHost(hostname); creds != nil {\n\t\tcreds.PrepareRequest(req)\n\t}\n\n\treturn req, nil\n}\n\nfunc (cache *ProviderCache) removeArchive() error {\n\tfs := cache.ProviderService.FS()\n\n\tif cache.archiveCached {\n\t\texists, err := vfs.FileExists(fs, cache.archivePath)\n\t\tif err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\tif exists {\n\t\t\tcache.logger.Debugf(\"Remove provider cached archive %s\", cache.archivePath)\n\n\t\t\tif err := fs.Remove(cache.archivePath); err != nil {\n\t\t\t\treturn errors.New(err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (cache *ProviderCache) acquireLockFile(ctx context.Context) (*util.Lockfile, error) {\n\tlockfile := util.NewLockfile(cache.lockfilePath)\n\n\tif err := cache.ProviderService.FS().MkdirAll(filepath.Dir(cache.lockfilePath), os.ModePerm); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tif err := util.DoWithRetry(ctx, \"Acquiring lock file \"+cache.lockfilePath, maxRetriesLockFile, retryDelayLockFile, cache.logger, log.DebugLevel, func(ctx context.Context) error {\n\t\treturn lockfile.TryLock()\n\t}); err != nil {\n\t\treturn nil, errors.Errorf(\"unable to acquire lock file %s (already locked?) try to remove the file manually: %w\", cache.lockfilePath, err)\n\t}\n\n\treturn lockfile, nil\n}\n\n// ProviderServiceOption configures a ProviderService.\ntype ProviderServiceOption func(*ProviderService)\n\n// WithFS sets the filesystem for file operations.\n// If not set, defaults to the real OS filesystem.\nfunc WithFS(fs vfs.FS) ProviderServiceOption {\n\treturn func(ps *ProviderService) {\n\t\tps.fs = fs\n\t}\n}\n\ntype ProviderService struct {\n\tlogger                log.Logger\n\tproviderCacheWarmUpCh chan *ProviderCache\n\tcredsSource           *cliconfig.CredentialsSource\n\n\t// fs is the filesystem for file operations.\n\tfs vfs.FS\n\n\t// The path to store unpacked providers. The file structure is the same as terraform plugin cache dir.\n\tcacheDir string\n\n\t// The path to a predictable temporary directory for provider archives and lock files.\n\ttempDir string\n\n\t// the user plugins directory, by default: %APPDATA%\\terraform.d\\plugins on Windows, ~/.terraform.d/plugins on other systems.\n\tuserCacheDir   string\n\tproviderCaches ProviderCaches\n\tcacheMu        sync.RWMutex\n\tcacheReadyMu   sync.RWMutex\n}\n\n// FS returns the configured filesystem.\nfunc (service *ProviderService) FS() vfs.FS {\n\treturn service.fs\n}\n\nfunc NewProviderService(\n\tcacheDir,\n\tuserCacheDir string,\n\tcredsSource *cliconfig.CredentialsSource,\n\tl log.Logger,\n\topts ...ProviderServiceOption,\n) *ProviderService {\n\tservice := &ProviderService{\n\t\tcacheDir:              cacheDir,\n\t\tuserCacheDir:          userCacheDir,\n\t\tproviderCacheWarmUpCh: make(chan *ProviderCache, providerCacheWarmUpChBufferSize),\n\t\tcredsSource:           credsSource,\n\t\tlogger:                l,\n\t\tfs:                    vfs.NewOSFS(),\n\t}\n\n\tfor _, opt := range opts {\n\t\topt(service)\n\t}\n\n\tl.Debugf(\"Provider service initialized with cache dir: %s, user cache dir: %s\", cacheDir, userCacheDir)\n\n\treturn service\n}\n\nfunc (service *ProviderService) Logger() log.Logger {\n\treturn service.logger\n}\n\n// WaitForCacheReady returns cached providers that were requested by `terraform init` from the cache server, with an  URL containing the given `requestID` value.\n// The function returns the value only when all cache requests have been processed.\nfunc (service *ProviderService) WaitForCacheReady(requestID string) ([]getproviders.Provider, error) {\n\tservice.cacheReadyMu.Lock()\n\tdefer service.cacheReadyMu.Unlock()\n\n\tvar (\n\t\tproviders []getproviders.Provider\n\t\terrs      = &errors.MultiError{}\n\t)\n\n\tservice.logger.Debugf(\"Waiting for cache ready with requestID: %s\", requestID)\n\n\tcaches := service.providerCaches.FindByRequestID(requestID)\n\tservice.logger.Debugf(\"Found %d caches for requestID: %s\", len(caches), requestID)\n\n\t// Add debug logging for all provider caches\n\tservice.logger.Debugf(\"Total provider caches: %d\", len(service.providerCaches))\n\n\tfor i, cache := range service.providerCaches {\n\t\tservice.logger.Debugf(\"Cache %d: %s, requestIDs: %v, ready: %v, err: %v\",\n\t\t\ti, cache.Provider, cache.getRequestIDs(), cache.isReady(), cache.err)\n\t}\n\n\tfor _, provider := range caches {\n\t\tif provider.err != nil {\n\t\t\terrs = errs.Append(fmt.Errorf(\"unable to cache provider: %s, err: %w\", provider, provider.err))\n\t\t\tservice.logger.Errorf(\"Provider cache error for %s: %v\", provider, provider.err)\n\t\t}\n\n\t\tif provider.isReady() {\n\t\t\tproviders = append(providers, provider)\n\t\t\tservice.logger.Debugf(\"Provider %s is ready\", provider)\n\t\t} else {\n\t\t\tservice.logger.Debugf(\"Provider %s is not ready yet\", provider)\n\t\t}\n\t}\n\n\tservice.logger.Debugf(\"Returning %d ready providers for requestID: %s\", len(providers), requestID)\n\n\treturn providers, errs.ErrorOrNil()\n}\n\n// CacheProvider starts caching the given provider using non-blocking approach.\nfunc (service *ProviderService) CacheProvider(ctx context.Context, requestID string, provider *models.Provider) *ProviderCache {\n\tservice.cacheMu.Lock()\n\tdefer service.cacheMu.Unlock()\n\n\tservice.logger.Debugf(\"CacheProvider called for %s with requestID: %s\", provider, requestID)\n\n\tif cache := service.providerCaches.Find(provider); cache != nil {\n\t\tservice.logger.Debugf(\"Found existing cache for provider %s\", provider)\n\t\tcache.addRequestID(requestID)\n\n\t\treturn cache\n\t}\n\n\tpackageName := fmt.Sprintf(\"%s-%s-%s-%s-%s\", provider.RegistryName, provider.Namespace, provider.Name, provider.Version, provider.Platform())\n\n\tcache := &ProviderCache{\n\t\tProviderService: service,\n\t\tProvider:        provider,\n\t\tstarted:         make(chan struct{}, 1),\n\n\t\tuserProviderDir: filepath.Join(service.userCacheDir, provider.Address(), provider.Version, provider.Platform()),\n\t\tpackageDir:      filepath.Join(service.cacheDir, provider.Address(), provider.Version, provider.Platform()),\n\t\tlockfilePath:    filepath.Join(service.tempDir, packageName+\".lock\"),\n\t\tarchivePath:     filepath.Join(service.tempDir, packageName+path.Ext(provider.Filename)),\n\t}\n\n\tservice.logger.Debugf(\"Sending provider %s to warm up channel\", provider)\n\n\tselect {\n\tcase service.providerCacheWarmUpCh <- cache:\n\t\tservice.logger.Debugf(\"Successfully sent provider %s to warm up channel\", provider)\n\t\t// We need to wait for caching to start and only then release the client (Terraform) requestID. Otherwise, the client may call `WaitForCacheReady()` faster than `service.ReadyMuReady` will be lock.\n\t\t<-cache.started\n\t\tservice.providerCaches = append(service.providerCaches, cache)\n\t\tservice.logger.Debugf(\"Added provider %s to provider caches list\", provider)\n\tcase <-ctx.Done():\n\t\tservice.logger.Debugf(\"Context cancelled while trying to cache provider %s\", provider)\n\t}\n\n\tcache.addRequestID(requestID)\n\tservice.logger.Debugf(\"Added requestID %s to provider %s\", requestID, provider)\n\n\treturn cache\n}\n\n// GetProviderCache returns the requested provider archive cache, if it exists.\nfunc (service *ProviderService) GetProviderCache(provider *models.Provider) *ProviderCache {\n\tservice.cacheMu.RLock()\n\tdefer service.cacheMu.RUnlock()\n\n\tcache := service.providerCaches.Find(provider)\n\tif cache != nil && cache.isReady() {\n\t\treturn cache\n\t}\n\n\treturn nil\n}\n\n// Run is responsible to handle a new caching requestID and removing temporary files upon completion.\nfunc (service *ProviderService) Run(ctx context.Context) error {\n\tif service.cacheDir == \"\" {\n\t\treturn errors.Errorf(\"provider cache directory not specified\")\n\t}\n\n\tservice.logger.Debugf(\"Starting provider cache service with cache dir: %q\", service.cacheDir)\n\n\tif err := service.FS().MkdirAll(service.cacheDir, os.ModePerm); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\ttempDir, err := util.GetTempDir()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tservice.tempDir = filepath.Join(tempDir, \"providers\")\n\tservice.logger.Debugf(\"Provider cache service temp dir: %s\", service.tempDir)\n\n\terrs := &errors.MultiError{}\n\terrGroup, ctx := errgroup.WithContext(ctx)\n\n\tservice.logger.Debugf(\"Provider cache service is ready to process requests\")\n\n\tfor {\n\t\tselect {\n\t\tcase cache := <-service.providerCacheWarmUpCh:\n\t\t\tservice.logger.Debugf(\"Received provider cache request for: %s\", cache.Provider)\n\t\t\terrGroup.Go(func() error {\n\t\t\t\tif err := service.startProviderCaching(ctx, cache); err != nil {\n\t\t\t\t\tservice.logger.Errorf(\"Failed to start provider caching for %s: %v\", cache.Provider, err)\n\t\t\t\t\terrs = errs.Append(err)\n\t\t\t\t} else {\n\t\t\t\t\tservice.logger.Debugf(\"Successfully started provider caching for %s\", cache.Provider)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\t\tcase <-ctx.Done():\n\t\t\tservice.logger.Debugf(\"Provider cache service shutting down...\")\n\n\t\t\tif err := errGroup.Wait(); err != nil {\n\t\t\t\terrs = errs.Append(err)\n\t\t\t}\n\n\t\t\tif err := service.providerCaches.removeArchive(); err != nil {\n\t\t\t\terrs = errs.Append(err)\n\t\t\t}\n\n\t\t\tservice.logger.Debugf(\"Provider cache service shutdown complete\")\n\n\t\t\treturn errs.ErrorOrNil()\n\t\t}\n\t}\n}\n\nfunc (service *ProviderService) startProviderCaching(ctx context.Context, cache *ProviderCache) error {\n\tservice.cacheReadyMu.RLock()\n\tdefer service.cacheReadyMu.RUnlock()\n\n\tservice.logger.Debugf(\"Starting provider caching for: %s\", cache.Provider)\n\n\tcache.started <- struct{}{}\n\n\t// We need to use a locking mechanism between Terragrunt processes to prevent simultaneous write access to the same provider.\n\tlockfile, err := cache.acquireLockFile(ctx)\n\tif err != nil {\n\t\tservice.logger.Errorf(\"Failed to acquire lock file for %s: %v\", cache.Provider, err)\n\t\treturn err\n\t}\n\tdefer lockfile.Unlock() //nolint:errcheck\n\n\tservice.logger.Debugf(\"Acquired lock file for %s, starting warm up\", cache.Provider)\n\n\tif cache.err = cache.warmUp(ctx); cache.err != nil {\n\t\tservice.logger.Errorf(\"Failed to warm up provider %s: %v\", cache.Provider, cache.err)\n\n\t\tif err := service.FS().RemoveAll(cache.packageDir); err != nil {\n\t\t\tservice.logger.Warnf(\"Failed to clean up package dir %q: %v\", cache.packageDir, err)\n\t\t}\n\n\t\tif err := service.FS().Remove(cache.archivePath); err != nil {\n\t\t\tservice.logger.Warnf(\"Failed to clean up archive %q: %v\", cache.archivePath, err)\n\t\t}\n\n\t\treturn cache.err\n\t}\n\n\tcache.setReady(true)\n\n\tservice.logger.Debugf(\"Successfully cached provider: %s\", cache.Provider)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/tf/cache/services/service.go",
    "content": "// Package services provides the interface for services\n// that can be run in the background.\npackage services\n\nimport (\n\t\"context\"\n)\n\ntype Service interface {\n\tRun(ctx context.Context) error\n}\n"
  },
  {
    "path": "internal/tf/cliconfig/config.go",
    "content": "// Package cliconfig provides methods to create an OpenTofu/Terraform CLI configuration file.\npackage cliconfig\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/vfs\"\n\t\"github.com/hashicorp/hcl/v2/gohcl\"\n\t\"github.com/hashicorp/hcl/v2/hclwrite\"\n\tsvchost \"github.com/hashicorp/terraform-svchost\"\n)\n\n// ConfigHost is the structure of the \"host\" nested block within the CLI configuration, which can be used to override the default service host discovery behavior for a particular hostname.\ntype ConfigHost struct {\n\tServices map[string]string `hcl:\"services,attr\"`\n\tName     string            `hcl:\",label\"`\n}\n\n// ConfigCredentials is the structure of the \"credentials\" nested block within the CLI configuration.\ntype ConfigCredentials struct {\n\tName  string `hcl:\",label\"`\n\tToken string `hcl:\"token\"`\n}\n\n// ConfigCredentialsHelper is the structure of the \"credentials_helper\" nested block within the CLI configuration.\ntype ConfigCredentialsHelper struct {\n\tName string   `hcl:\",label\"`\n\tArgs []string `hcl:\"args\"`\n}\n\n// ConfigOption configures a Config.\ntype ConfigOption func(*Config) *Config\n\n// WithFS sets the filesystem for file operations.\n// If not set, defaults to the real OS filesystem.\nfunc WithFS(fs vfs.FS) ConfigOption {\n\treturn func(cfg *Config) *Config {\n\t\tcfg.fs = fs\n\t\treturn cfg\n\t}\n}\n\n// NewConfig creates a new Config with default values.\nfunc NewConfig() *Config {\n\treturn &Config{\n\t\tfs: vfs.NewOSFS(),\n\t}\n}\n\n// Config provides methods to create a terraform [CLI config file](https://developer.hashicorp.com/terraform/cli/config/config-file).\n// The main purpose of which is to create a local config that will inherit the default user CLI config and adding new sections to force Terraform to send requests through the Terragrunt Cache server and use the provider cache directory.\ntype Config struct {\n\tCredentialsHelpers   *ConfigCredentialsHelper `hcl:\"credentials_helper,block\"`\n\tProviderInstallation *ProviderInstallation    `hcl:\"provider_installation,block\"`\n\n\t// fs is the filesystem for saving config. Unexported to skip HCL encoding.\n\t// Defaults to vfs.NewOsFs() if nil.\n\tfs vfs.FS\n\n\tPluginCacheDir             string              `hcl:\"plugin_cache_dir\"`\n\tCredentials                []ConfigCredentials `hcl:\"credentials,block\"`\n\tHosts                      []ConfigHost        `hcl:\"host,block\"`\n\tDisableCheckpoint          bool                `hcl:\"disable_checkpoint\"`\n\tDisableCheckpointSignature bool                `hcl:\"disable_checkpoint_signature\"`\n}\n\n// WithOptions applies options to the Config.\nfunc (cfg *Config) WithOptions(opts ...ConfigOption) *Config {\n\tfor _, opt := range opts {\n\t\tcfg = opt(cfg)\n\t}\n\n\treturn cfg\n}\n\n// FS returns the configured filesystem.\nfunc (cfg *Config) FS() vfs.FS {\n\treturn cfg.fs\n}\n\n// WithDisableCheckpoint sets DisableCheckpoint to true and returns the Config for chaining.\nfunc (cfg *Config) WithDisableCheckpoint() *Config {\n\tcfg.DisableCheckpoint = true\n\treturn cfg\n}\n\n// WithDisableCheckpointSignature sets DisableCheckpointSignature to true and returns the Config for chaining.\nfunc (cfg *Config) WithDisableCheckpointSignature() *Config {\n\tcfg.DisableCheckpointSignature = true\n\treturn cfg\n}\n\n// WithPluginCacheDir sets PluginCacheDir and returns the Config for chaining.\nfunc (cfg *Config) WithPluginCacheDir(dir string) *Config {\n\tcfg.PluginCacheDir = dir\n\treturn cfg\n}\n\n// WithCredentials sets Credentials and returns the Config for chaining.\nfunc (cfg *Config) WithCredentials(credentials []ConfigCredentials) *Config {\n\tcfg.Credentials = credentials\n\treturn cfg\n}\n\n// WithCredentialsHelpers sets CredentialsHelpers and returns the Config for chaining.\nfunc (cfg *Config) WithCredentialsHelpers(helpers *ConfigCredentialsHelper) *Config {\n\tcfg.CredentialsHelpers = helpers\n\treturn cfg\n}\n\n// WithHosts sets Hosts and returns the Config for chaining.\nfunc (cfg *Config) WithHosts(hosts []ConfigHost) *Config {\n\tcfg.Hosts = hosts\n\treturn cfg\n}\n\n// WithProviderInstallation sets ProviderInstallation and returns the Config for chaining.\nfunc (cfg *Config) WithProviderInstallation(installation *ProviderInstallation) *Config {\n\tcfg.ProviderInstallation = installation\n\treturn cfg\n}\n\nfunc (cfg *Config) Clone() *Config {\n\tvar providerInstallation *ProviderInstallation\n\n\thosts := make([]ConfigHost, 0, len(cfg.Hosts))\n\n\thosts = append(hosts, cfg.Hosts...)\n\n\tif cfg.ProviderInstallation != nil {\n\t\tproviderInstallation = &ProviderInstallation{\n\t\t\tMethods: cfg.ProviderInstallation.Methods.Clone(),\n\t\t}\n\t}\n\n\treturn &Config{\n\t\tPluginCacheDir:             cfg.PluginCacheDir,\n\t\tDisableCheckpoint:          cfg.DisableCheckpoint,\n\t\tDisableCheckpointSignature: cfg.DisableCheckpointSignature,\n\t\tCredentials:                cfg.Credentials,\n\t\tCredentialsHelpers:         cfg.CredentialsHelpers,\n\t\tHosts:                      hosts,\n\t\tProviderInstallation:       providerInstallation,\n\t\tfs:                         cfg.fs,\n\t}\n}\n\n// AddHost adds a host (officially undocumented), https://github.com/hashicorp/terraform/issues/28309\n// It gives us opportunity rewrite path to the remote registry and the most important thing is that it works smoothly with HTTP (without HTTPS)\n//\n//\thost \"registry.terraform.io\" {\n//\t\tservices = {\n//\t\t\t\"providers.v1\" = \"http://localhost:5758/v1/providers/registry.terraform.io/\",\n//\t\t}\n//\t}\nfunc (cfg *Config) AddHost(name string, services map[string]string) {\n\tcfg.Hosts = append(cfg.Hosts, ConfigHost{\n\t\tName:     name,\n\t\tServices: services,\n\t})\n}\n\n// AddProviderInstallationMethods merges new installation methods with the current ones, https://developer.hashicorp.com/terraform/cli/config/config-file#provider-installation\n//\n//\tprovider_installation {\n//\t\tfilesystem_mirror {\n//\t\t\tpath    = \"/path/to/the/provider/cache\"\n//\t\t\tinclude = [\"example.com/*/*\"]\n//\t\t}\n//\t\tdirect {\n//\t\t\texclude = [\"example.com/*/*\"]\n//\t\t}\n//\t}\n\nfunc (cfg *Config) AddProviderInstallationMethods(newMethods ...ProviderInstallationMethod) {\n\tif cfg.ProviderInstallation == nil {\n\t\tcfg.ProviderInstallation = &ProviderInstallation{}\n\t}\n\n\tcfg.ProviderInstallation.Methods = cfg.ProviderInstallation.Methods.Merge(newMethods...)\n}\n\n// Save marshalls and saves CLI config to the given path.\nfunc (cfg *Config) Save(configPath string) error {\n\tfile := hclwrite.NewEmptyFile()\n\tgohcl.EncodeIntoBody(cfg, file.Body())\n\n\tconst ownerWriteGlobalReadPerms = 0644\n\tif err := vfs.WriteFile(cfg.FS(), configPath, file.Bytes(), ownerWriteGlobalReadPerms); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n\n// CredentialsSource creates and returns a service credentials source whose behavior depends on which \"credentials\" if are present in the receiving config.\nfunc (cfg *Config) CredentialsSource() *CredentialsSource {\n\tconfigured := make(map[svchost.Hostname]string)\n\n\tfor _, creds := range cfg.Credentials {\n\t\thost, err := svchost.ForComparison(creds.Name)\n\t\tif err != nil {\n\t\t\t// We expect the config was already validated by the time we get here, so we'll just ignore invalid hostnames.\n\t\t\tcontinue\n\t\t}\n\n\t\tconfigured[host] = creds.Token\n\t}\n\n\treturn &CredentialsSource{\n\t\tconfigured: configured,\n\t}\n}\n"
  },
  {
    "path": "internal/tf/cliconfig/config_test.go",
    "content": "package cliconfig_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cliconfig\"\n\t\"github.com/gruntwork-io/terragrunt/internal/vfs\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestConfig(t *testing.T) {\n\tt.Parallel()\n\n\tvar (\n\t\tinclude = []string{\"registry.terraform.io/*/*\"}\n\t\texclude = []string{\"registry.opentofu.org/*/*\"}\n\t)\n\n\t// Use a fixed path for the cache dir since we're using an in-memory filesystem\n\ttempCacheDir := \"/tmp/provider-cache\"\n\ttestCases := []struct {\n\t\tconfig                      *cliconfig.Config\n\t\texpectedHCL                 string\n\t\tproviderInstallationMethods []cliconfig.ProviderInstallationMethod\n\t\thosts                       []cliconfig.ConfigHost\n\t}{\n\t\t{\n\t\t\tproviderInstallationMethods: []cliconfig.ProviderInstallationMethod{\n\t\t\t\tcliconfig.NewProviderInstallationFilesystemMirror(tempCacheDir, include, exclude),\n\t\t\t\tcliconfig.NewProviderInstallationNetworkMirror(\"https://network-mirror.io/providers/\", include, exclude),\n\t\t\t\tcliconfig.NewProviderInstallationDirect(include, exclude),\n\t\t\t},\n\t\t\thosts: []cliconfig.ConfigHost{\n\t\t\t\t{Name: \"registry.terraform.io\", Services: map[string]string{\"providers.v1\": \"http://localhost:5758/v1/providers/registry.terraform.io/\"}},\n\t\t\t},\n\t\t\tconfig: cliconfig.NewConfig().\n\t\t\t\tWithDisableCheckpoint().\n\t\t\t\tWithPluginCacheDir(\"path/to/plugin/cache/dir1\"),\n\t\t\texpectedHCL: `\nprovider_installation {\n\n   \"filesystem_mirror\" {\n    include = [\"registry.terraform.io/*/*\"]\n    exclude = [\"registry.opentofu.org/*/*\"]\n    path    = \"/tmp/provider-cache\"\n  }\n   \"network_mirror\" {\n    include = [\"registry.terraform.io/*/*\"]\n    exclude = [\"registry.opentofu.org/*/*\"]\n    url     = \"https://network-mirror.io/providers/\"\n  }\n   \"direct\" {\n    include = [\"registry.terraform.io/*/*\"]\n    exclude = [\"registry.opentofu.org/*/*\"]\n  }\n}\n\nplugin_cache_dir = \"path/to/plugin/cache/dir1\"\n\nhost \"registry.terraform.io\" {\n  services = {\n    \"providers.v1\" = \"http://localhost:5758/v1/providers/registry.terraform.io/\"\n  }\n}\n\ndisable_checkpoint           = true\ndisable_checkpoint_signature = false\n`,\n\t\t},\n\t\t{\n\t\t\tconfig: cliconfig.NewConfig().\n\t\t\t\tWithPluginCacheDir(tempCacheDir),\n\t\t\texpectedHCL: `\nprovider_installation {\n}\n\nplugin_cache_dir             = \"/tmp/provider-cache\"\ndisable_checkpoint           = false\ndisable_checkpoint_signature = false\n`,\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t// Use an in-memory filesystem for faster, isolated tests\n\t\t\tmemFs := vfs.NewMemMapFS()\n\t\t\tconfigFile := \"/config/.terraformrc\"\n\n\t\t\tfor _, host := range tc.hosts {\n\t\t\t\ttc.config.AddHost(host.Name, host.Services)\n\t\t\t}\n\n\t\t\ttc.config.AddProviderInstallationMethods(tc.providerInstallationMethods...)\n\n\t\t\t// Inject filesystem via options - same Save() method as production\n\t\t\ttc.config.WithOptions(cliconfig.WithFS(memFs))\n\n\t\t\terr := tc.config.Save(configFile)\n\t\t\trequire.NoError(t, err)\n\n\t\t\thclBytes, err := vfs.ReadFile(memFs, configFile)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tactualHCL := string(hclBytes)\n\t\t\tassert.Equal(t, tc.expectedHCL, actualHCL)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/tf/cliconfig/credentials.go",
    "content": "package cliconfig\n\nimport (\n\t\"os\"\n\t\"strings\"\n\n\tsvchost \"github.com/hashicorp/terraform-svchost\"\n\tsvcauth \"github.com/hashicorp/terraform-svchost/auth\"\n)\n\ntype CredentialsSource struct {\n\t// configured describes the credentials explicitly configured in the CLI config via \"credentials\" blocks.\n\tconfigured map[svchost.Hostname]string\n}\n\nfunc (s *CredentialsSource) ForHost(host svchost.Hostname) svcauth.HostCredentials {\n\t// The first order of precedence for credentials is a host-specific environment variable\n\tif envCreds := hostCredentialsFromEnv(host); envCreds != nil {\n\t\treturn envCreds\n\t}\n\n\t// Then, any credentials block present in the CLI config\n\tif token, ok := s.configured[host]; ok {\n\t\treturn svcauth.HostCredentialsToken(token)\n\t}\n\n\treturn nil\n}\n\n// hostCredentialsFromEnv returns a token credential by searching for a hostname-specific environment variable. The host parameter is expected to be in the \"comparison\" form, for example, hostnames containing non-ASCII characters like \"café.fr\" should be expressed as \"xn--caf-dma.fr\". If the variable based on the hostname is not defined, nil is returned.\n//\n// Hyphen and period characters are allowed in environment variable names, but are not valid POSIX variable names. However, it's still possible to set variable names with these characters using utilities like env or docker. Variable names may have periods translated to underscores and hyphens translated to double underscores in the variable name. For the example \"café.fr\", you may use the variable names \"TF_TOKEN_xn____caf__dma_fr\", \"TF_TOKEN_xn--caf-dma_fr\", or \"TF_TOKEN_xn--caf-dma.fr\"\nfunc hostCredentialsFromEnv(host svchost.Hostname) svcauth.HostCredentials {\n\ttoken, ok := collectCredentialsFromEnv()[host]\n\tif !ok {\n\t\treturn nil\n\t}\n\n\treturn svcauth.HostCredentialsToken(token)\n}\n\nfunc collectCredentialsFromEnv() map[svchost.Hostname]string {\n\tconst prefix = \"TF_TOKEN_\"\n\n\tret := make(map[svchost.Hostname]string)\n\n\tfor _, ev := range os.Environ() {\n\t\teqIdx := strings.Index(ev, \"=\")\n\t\tif eqIdx < 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tname := ev[:eqIdx]\n\t\tvalue := ev[eqIdx+1:]\n\n\t\tif !strings.HasPrefix(name, prefix) {\n\t\t\tcontinue\n\t\t}\n\n\t\trawHost := name[len(prefix):]\n\n\t\t// We accept double underscores in place of hyphens because hyphens are not valid identifiers in most shells and are therefore hard to set.\n\t\trawHost = strings.ReplaceAll(rawHost, \"__\", \"-\")\n\n\t\t// We accept underscores in place of dots because dots are not valid identifiers in most shells and are therefore hard to set.\n\t\t// Underscores are not valid in hostnames, so this is unambiguous for valid hostnames.\n\t\trawHost = strings.ReplaceAll(rawHost, \"_\", \".\")\n\n\t\t// Because environment variables are often set indirectly by OS libraries that might interfere with how they are encoded, we'll be tolerant of them being given either directly as UTF-8 IDNs or in Punycode form, normalizing to Punycode form here because that is what the OpenTofu credentials helper protocol will use in its requests.\n\t\tdispHost := svchost.ForDisplay(rawHost)\n\n\t\thostname, err := svchost.ForComparison(dispHost)\n\t\tif err != nil {\n\t\t\t// Ignore invalid hostnames\n\t\t\tcontinue\n\t\t}\n\n\t\tret[hostname] = value\n\t}\n\n\treturn ret\n}\n"
  },
  {
    "path": "internal/tf/cliconfig/provider_installation.go",
    "content": "package cliconfig\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"slices\"\n\t\"sort\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n)\n\n// ProviderInstallation is the structure of the \"provider_installation\" nested block within the CLI configuration.\ntype ProviderInstallation struct {\n\tMethods ProviderInstallationMethods `hcl:\",block\"`\n}\n\ntype ProviderInstallationMethods []ProviderInstallationMethod\n\nfunc (methods ProviderInstallationMethods) Merge(withMethods ...ProviderInstallationMethod) ProviderInstallationMethods {\n\tmergedMethods := methods\n\n\tfor _, withMethod := range withMethods {\n\t\tvar isMerged bool\n\n\t\tfor _, method := range methods {\n\t\t\tif method.Merge(withMethod) {\n\t\t\t\tisMerged = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif !isMerged {\n\t\t\tmergedMethods = append(mergedMethods, withMethod)\n\t\t}\n\t}\n\n\t// place the `direct` method at the very end.\n\tsort.Slice(mergedMethods, func(i, j int) bool {\n\t\tif _, ok := mergedMethods[j].(*ProviderInstallationDirect); ok {\n\t\t\treturn true\n\t\t}\n\n\t\treturn false\n\t})\n\n\treturn mergedMethods\n}\n\nfunc (methods ProviderInstallationMethods) Clone() ProviderInstallationMethods {\n\tvar cloned = make(ProviderInstallationMethods, len(methods))\n\n\tfor i, method := range methods {\n\t\tcloned[i] = method.Clone()\n\t}\n\n\treturn cloned\n}\n\n// ProviderInstallationMethod is an interface type representing the different installation path types and represents an installation method block inside a provider_installation block. The concrete implementations of this interface are:\n//\n//\tProviderInstallationDirect:           install from the provider's origin registry\n//\tProviderInstallationFilesystemMirror: install from a local filesystem mirror\ntype ProviderInstallationMethod interface {\n\tfmt.Stringer\n\tAppendInclude(addrs []string)\n\tAppendExclude(addrs []string)\n\tRemoveInclude(addrs []string)\n\tRemoveExclude(addrs []string)\n\tMerge(with ProviderInstallationMethod) bool\n\tClone() ProviderInstallationMethod\n}\n\ntype ProviderInstallationDirect struct {\n\tInclude *[]string `hcl:\"include,optional\" json:\"Include\"`\n\tExclude *[]string `hcl:\"exclude,optional\" json:\"Exclude\"`\n\tName    string    `hcl:\",label\" json:\"Name\"`\n}\n\nfunc NewProviderInstallationDirect(include, exclude []string) *ProviderInstallationDirect {\n\tres := &ProviderInstallationDirect{\n\t\tName: \"direct\",\n\t}\n\n\tif len(include) > 0 {\n\t\tres.Include = &include\n\t}\n\n\tif len(exclude) > 0 {\n\t\tres.Exclude = &exclude\n\t}\n\n\treturn res\n}\n\nfunc (method *ProviderInstallationDirect) Clone() ProviderInstallationMethod {\n\tcloned := &ProviderInstallationDirect{\n\t\tName: method.Name,\n\t}\n\n\tif method.Include != nil {\n\t\tinclude := *method.Include\n\t\tcloned.Include = &include\n\t}\n\n\tif method.Exclude != nil {\n\t\texclude := *method.Exclude\n\t\tcloned.Exclude = &exclude\n\t}\n\n\treturn cloned\n}\n\nfunc (method *ProviderInstallationDirect) Merge(with ProviderInstallationMethod) bool {\n\tif with, ok := with.(*ProviderInstallationDirect); ok {\n\t\tif with.Exclude != nil {\n\t\t\tmethod.AppendExclude(*with.Exclude)\n\t\t}\n\n\t\tif with.Include != nil {\n\t\t\tmethod.AppendInclude(*with.Include)\n\t\t}\n\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc (method *ProviderInstallationDirect) AppendInclude(addrs []string) {\n\tif len(addrs) == 0 {\n\t\treturn\n\t}\n\n\tif method.Include == nil {\n\t\tmethod.Include = &[]string{}\n\t}\n\n\t*method.Include = util.RemoveDuplicates(append(*method.Include, addrs...))\n}\n\nfunc (method *ProviderInstallationDirect) AppendExclude(addrs []string) {\n\tif len(addrs) == 0 {\n\t\treturn\n\t}\n\n\tif method.Exclude == nil {\n\t\tmethod.Exclude = &[]string{}\n\t}\n\n\t*method.Exclude = util.RemoveDuplicates(append(*method.Exclude, addrs...))\n}\n\nfunc (method *ProviderInstallationDirect) RemoveExclude(addrs []string) {\n\tif len(addrs) == 0 || method.Exclude == nil {\n\t\treturn\n\t}\n\n\t*method.Exclude = slices.DeleteFunc(*method.Exclude, func(item string) bool { return slices.Contains(addrs, item) })\n\n\tif len(*method.Exclude) == 0 {\n\t\tmethod.Exclude = nil\n\t}\n}\n\nfunc (method *ProviderInstallationDirect) RemoveInclude(addrs []string) {\n\tif len(addrs) == 0 || method.Include == nil {\n\t\treturn\n\t}\n\n\t*method.Include = slices.DeleteFunc(*method.Include, func(item string) bool { return slices.Contains(addrs, item) })\n\n\tif len(*method.Include) == 0 {\n\t\tmethod.Include = nil\n\t}\n}\n\nfunc (method *ProviderInstallationDirect) String() string {\n\t// Odd that this err isn't checked. There should be an explanation why.\n\tb, _ := json.Marshal(method) //nolint:errchkjson\n\treturn string(b)\n}\n\ntype ProviderInstallationFilesystemMirror struct {\n\tInclude *[]string `hcl:\"include,optional\" json:\"Include\"`\n\tExclude *[]string `hcl:\"exclude,optional\" json:\"Exclude\"`\n\tName    string    `hcl:\",label\" json:\"Name\"`\n\tPath    string    `hcl:\"path,attr\" json:\"Path\"`\n}\n\nfunc NewProviderInstallationFilesystemMirror(path string, include, exclude []string) *ProviderInstallationFilesystemMirror {\n\tres := &ProviderInstallationFilesystemMirror{\n\t\tName: \"filesystem_mirror\",\n\t\tPath: path,\n\t}\n\n\tif len(include) > 0 {\n\t\tres.Include = &include\n\t}\n\n\tif len(exclude) > 0 {\n\t\tres.Exclude = &exclude\n\t}\n\n\treturn res\n}\n\nfunc (method *ProviderInstallationFilesystemMirror) Clone() ProviderInstallationMethod {\n\tcloned := &ProviderInstallationFilesystemMirror{\n\t\tName: method.Name,\n\t\tPath: method.Path,\n\t}\n\n\tif method.Include != nil {\n\t\tinclude := *method.Include\n\t\tcloned.Include = &include\n\t}\n\n\tif method.Exclude != nil {\n\t\texclude := *method.Exclude\n\t\tcloned.Exclude = &exclude\n\t}\n\n\treturn cloned\n}\n\nfunc (method *ProviderInstallationFilesystemMirror) Merge(with ProviderInstallationMethod) bool {\n\tif with, ok := with.(*ProviderInstallationFilesystemMirror); ok && method.Path == with.Path {\n\t\tif with.Exclude != nil {\n\t\t\tmethod.AppendExclude(*with.Exclude)\n\t\t}\n\n\t\tif with.Include != nil {\n\t\t\tmethod.AppendInclude(*with.Include)\n\t\t}\n\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc (method *ProviderInstallationFilesystemMirror) AppendInclude(addrs []string) {\n\tif len(addrs) == 0 {\n\t\treturn\n\t}\n\n\tif method.Include == nil {\n\t\tmethod.Include = &[]string{}\n\t}\n\n\t*method.Include = util.RemoveDuplicates(append(*method.Include, addrs...))\n}\n\nfunc (method *ProviderInstallationFilesystemMirror) AppendExclude(addrs []string) {\n\tif len(addrs) == 0 {\n\t\treturn\n\t}\n\n\tif method.Exclude == nil {\n\t\tmethod.Exclude = &[]string{}\n\t}\n\n\t*method.Exclude = util.RemoveDuplicates(append(*method.Exclude, addrs...))\n}\n\nfunc (method *ProviderInstallationFilesystemMirror) RemoveExclude(addrs []string) {\n\tif len(addrs) == 0 || method.Exclude == nil {\n\t\treturn\n\t}\n\n\t*method.Exclude = slices.DeleteFunc(*method.Exclude, func(item string) bool { return slices.Contains(addrs, item) })\n\n\tif len(*method.Exclude) == 0 {\n\t\tmethod.Exclude = nil\n\t}\n}\n\nfunc (method *ProviderInstallationFilesystemMirror) RemoveInclude(addrs []string) {\n\tif len(addrs) == 0 || method.Include == nil {\n\t\treturn\n\t}\n\n\t*method.Include = slices.DeleteFunc(*method.Include, func(item string) bool { return slices.Contains(addrs, item) })\n\n\tif len(*method.Include) == 0 {\n\t\tmethod.Include = nil\n\t}\n}\n\nfunc (method *ProviderInstallationFilesystemMirror) String() string {\n\t// Odd that this err isn't checked. There should be an explanation why.\n\tb, _ := json.Marshal(method) //nolint:errchkjson\n\treturn string(b)\n}\n\ntype ProviderInstallationNetworkMirror struct {\n\tInclude *[]string `hcl:\"include,optional\" json:\"Include\"`\n\tExclude *[]string `hcl:\"exclude,optional\" json:\"Exclude\"`\n\tName    string    `hcl:\",label\" json:\"Name\"`\n\tURL     string    `hcl:\"url,attr\" json:\"URL\"`\n}\n\nfunc NewProviderInstallationNetworkMirror(url string, include, exclude []string) *ProviderInstallationNetworkMirror {\n\tres := &ProviderInstallationNetworkMirror{\n\t\tName: \"network_mirror\",\n\t\tURL:  url,\n\t}\n\n\tif len(include) > 0 {\n\t\tres.Include = &include\n\t}\n\n\tif len(exclude) > 0 {\n\t\tres.Exclude = &exclude\n\t}\n\n\treturn res\n}\n\nfunc (method *ProviderInstallationNetworkMirror) Clone() ProviderInstallationMethod {\n\tcloned := &ProviderInstallationNetworkMirror{\n\t\tName: method.Name,\n\t\tURL:  method.URL,\n\t}\n\n\tif method.Include != nil {\n\t\tinclude := *method.Include\n\t\tcloned.Include = &include\n\t}\n\n\tif method.Exclude != nil {\n\t\texclude := *method.Exclude\n\t\tcloned.Exclude = &exclude\n\t}\n\n\treturn cloned\n}\n\nfunc (method *ProviderInstallationNetworkMirror) Merge(with ProviderInstallationMethod) bool {\n\tif with, ok := with.(*ProviderInstallationNetworkMirror); ok && method.URL == with.URL {\n\t\tif with.Exclude != nil {\n\t\t\tmethod.AppendExclude(*with.Exclude)\n\t\t}\n\n\t\tif with.Include != nil {\n\t\t\tmethod.AppendInclude(*with.Include)\n\t\t}\n\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc (method *ProviderInstallationNetworkMirror) AppendInclude(addrs []string) {\n\tif len(addrs) == 0 {\n\t\treturn\n\t}\n\n\tif method.Include == nil {\n\t\tmethod.Include = &[]string{}\n\t}\n\n\t*method.Include = util.RemoveDuplicates(append(*method.Include, addrs...))\n}\n\nfunc (method *ProviderInstallationNetworkMirror) AppendExclude(addrs []string) {\n\tif len(addrs) == 0 {\n\t\treturn\n\t}\n\n\tif method.Exclude == nil {\n\t\tmethod.Exclude = &[]string{}\n\t}\n\n\t*method.Exclude = util.RemoveDuplicatesKeepLast(append(*method.Exclude, addrs...))\n}\n\nfunc (method *ProviderInstallationNetworkMirror) RemoveExclude(addrs []string) {\n\tif len(addrs) == 0 || method.Exclude == nil {\n\t\treturn\n\t}\n\n\t*method.Exclude = slices.DeleteFunc(*method.Exclude, func(item string) bool { return slices.Contains(addrs, item) })\n\n\tif len(*method.Exclude) == 0 {\n\t\tmethod.Exclude = nil\n\t}\n}\n\nfunc (method *ProviderInstallationNetworkMirror) RemoveInclude(addrs []string) {\n\tif len(addrs) == 0 || method.Include == nil {\n\t\treturn\n\t}\n\n\t*method.Include = slices.DeleteFunc(*method.Include, func(item string) bool { return slices.Contains(addrs, item) })\n\n\tif len(*method.Include) == 0 {\n\t\tmethod.Include = nil\n\t}\n}\n\nfunc (method *ProviderInstallationNetworkMirror) String() string {\n\t// Odd that this err isn't checked. There should be an explanation why.\n\tb, _ := json.Marshal(method) //nolint:errchkjson\n\treturn string(b)\n}\n"
  },
  {
    "path": "internal/tf/cliconfig/user_config.go",
    "content": "package cliconfig\n\nimport (\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/hashicorp/terraform/command/cliconfig\"\n\t\"github.com/hashicorp/terraform/tfdiags\"\n)\n\n// LoadUserConfig loads the user configuration is read as raw data and stored at the top of the saved configuration file.\n// The location of the default config is different for each OS https://developer.hashicorp.com/terraform/cli/config/config-file#locations\nfunc LoadUserConfig(opts ...ConfigOption) (*Config, error) {\n\treturn loadUserConfig(cliconfig.LoadConfig, opts...)\n}\n\nfunc loadUserConfig(\n\tloadConfigFn func() (*cliconfig.Config, tfdiags.Diagnostics),\n\topts ...ConfigOption,\n) (*Config, error) {\n\tcfg, diag := loadConfigFn()\n\tif diag.HasErrors() {\n\t\treturn nil, diag.Err()\n\t}\n\n\tconfig := NewConfig().\n\t\tWithPluginCacheDir(cfg.PluginCacheDir).\n\t\tWithCredentials(getUserCredentials(cfg)).\n\t\tWithCredentialsHelpers(getUserCredentialsHelpers(cfg)).\n\t\tWithProviderInstallation(&ProviderInstallation{Methods: getUserProviderInstallationMethods(cfg)}).\n\t\tWithHosts(getUserHosts(cfg))\n\n\tif cfg.DisableCheckpoint {\n\t\tconfig.WithDisableCheckpoint()\n\t}\n\n\tif cfg.DisableCheckpointSignature {\n\t\tconfig.WithDisableCheckpointSignature()\n\t}\n\n\treturn config.WithOptions(opts...), nil\n}\n\nfunc UserProviderDir() (string, error) {\n\tconfigDir, err := cliconfig.ConfigDir()\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\treturn filepath.Join(configDir, \"plugins\"), nil\n}\n\nfunc getUserCredentials(cfg *cliconfig.Config) []ConfigCredentials {\n\tvar credentials = make([]ConfigCredentials, 0, len(cfg.Credentials))\n\n\tfor name, credential := range cfg.Credentials {\n\t\tvar token string\n\n\t\tif val, ok := credential[\"token\"]; ok {\n\t\t\tif val, ok := val.(string); ok {\n\t\t\t\ttoken = val\n\t\t\t}\n\t\t}\n\n\t\tcredential := ConfigCredentials{\n\t\t\tName:  name,\n\t\t\tToken: token,\n\t\t}\n\t\tcredentials = append(credentials, credential)\n\t}\n\n\treturn credentials\n}\n\nfunc getUserCredentialsHelpers(cfg *cliconfig.Config) *ConfigCredentialsHelper {\n\tvar credentialsHelpers *ConfigCredentialsHelper\n\n\tfor name, helper := range cfg.CredentialsHelpers {\n\t\tvar args []string\n\t\tif helper != nil {\n\t\t\targs = helper.Args\n\t\t}\n\n\t\tcredentialsHelpers = &ConfigCredentialsHelper{\n\t\t\tName: name,\n\t\t\tArgs: args,\n\t\t}\n\t}\n\n\treturn credentialsHelpers\n}\n\nfunc getUserHosts(cfg *cliconfig.Config) []ConfigHost {\n\tvar hosts = make([]ConfigHost, 0, len(cfg.Hosts))\n\n\tfor name, host := range cfg.Hosts {\n\t\tservices := make(map[string]string)\n\n\t\tif host != nil {\n\t\t\tfor key, val := range host.Services {\n\t\t\t\tif val, ok := val.(string); ok {\n\t\t\t\t\tservices[key] = val\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\thost := ConfigHost{Name: name, Services: services}\n\t\thosts = append(hosts, host)\n\t}\n\n\treturn hosts\n}\n\nfunc getUserProviderInstallationMethods(cfg *cliconfig.Config) []ProviderInstallationMethod {\n\tvar methods []ProviderInstallationMethod\n\n\tfor _, providerInstallation := range cfg.ProviderInstallation {\n\t\tfor _, method := range providerInstallation.Methods {\n\t\t\tswitch location := method.Location.(type) {\n\t\t\tcase cliconfig.ProviderInstallationFilesystemMirror:\n\t\t\t\tmethods = append(methods, NewProviderInstallationFilesystemMirror(string(location), method.Include, method.Exclude))\n\t\t\tcase cliconfig.ProviderInstallationNetworkMirror:\n\t\t\t\tmethods = append(methods, NewProviderInstallationNetworkMirror(string(location), method.Include, method.Exclude))\n\t\t\tdefault:\n\t\t\t\tmethods = append(methods, NewProviderInstallationDirect(method.Include, method.Exclude))\n\t\t\t}\n\t\t}\n\t}\n\n\treturn methods\n}\n"
  },
  {
    "path": "internal/tf/context.go",
    "content": "package tf\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cache\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst (\n\tTerraformCommandContextKey ctxKey = iota\n\tDetailedExitCodeContextKey\n)\n\ntype ctxKey byte\n\n// RunShellCommandFunc is a context value for `TerraformCommandContextKey` key, used to intercept shell commands.\ntype RunShellCommandFunc func(ctx context.Context, l log.Logger, tfOpts *TFOptions, args clihelper.Args) (*util.CmdOutput, error)\n\nfunc ContextWithTerraformCommandHook(ctx context.Context, fn RunShellCommandFunc) context.Context {\n\tctx = cache.ContextWithCache(ctx)\n\treturn context.WithValue(ctx, TerraformCommandContextKey, fn)\n}\n\n// TerraformCommandHookFromContext returns `RunShellCommandFunc` from the context if it has been set, otherwise returns nil.\nfunc TerraformCommandHookFromContext(ctx context.Context) RunShellCommandFunc {\n\tif val := ctx.Value(TerraformCommandContextKey); val != nil {\n\t\tif val, ok := val.(RunShellCommandFunc); ok {\n\t\t\treturn val\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// ContextWithDetailedExitCode returns a new context containing the given DetailedExitCodeMap.\nfunc ContextWithDetailedExitCode(ctx context.Context, detailedExitCode *DetailedExitCodeMap) context.Context {\n\treturn context.WithValue(ctx, DetailedExitCodeContextKey, detailedExitCode)\n}\n\n// DetailedExitCodeFromContext returns DetailedExitCodeMap if the given context contains it.\nfunc DetailedExitCodeFromContext(ctx context.Context) *DetailedExitCodeMap {\n\tif val := ctx.Value(DetailedExitCodeContextKey); val != nil {\n\t\tif val, ok := val.(*DetailedExitCodeMap); ok {\n\t\t\treturn val\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/tf/detailed_exitcode.go",
    "content": "package tf\n\nimport (\n\t\"sync\"\n)\n\nconst (\n\tDetailedExitCodeSuccess = 0\n\tDetailedExitCodeError   = 1\n\tDetailedExitCodeChanges = 2\n)\n\n// DetailedExitCodeMap stores exit codes per unit path. https://opentofu.org/docs/cli/commands/plan/\ntype DetailedExitCodeMap struct {\n\tcodes map[string]int\n\tmu    sync.RWMutex\n}\n\n// NewDetailedExitCodeMap creates a new DetailedExitCodeMap.\nfunc NewDetailedExitCodeMap() *DetailedExitCodeMap {\n\treturn &DetailedExitCodeMap{\n\t\tcodes: make(map[string]int),\n\t}\n}\n\n// Set stores the exit code for the given path. Always updates the map without conditional logic.\nfunc (m *DetailedExitCodeMap) Set(path string, code int) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tif m.codes == nil {\n\t\tm.codes = make(map[string]int)\n\t}\n\n\tm.codes[path] = code\n}\n\n// Get returns the exit code for the given path, or 0 if not found.\nfunc (m *DetailedExitCodeMap) Get(path string) int {\n\tm.mu.RLock()\n\tdefer m.mu.RUnlock()\n\n\tif m.codes == nil {\n\t\treturn 0\n\t}\n\n\treturn m.codes[path]\n}\n\n// GetFinalDetailedExitCode computes the final exit code following OpenTofu's exit code convention:\n// - 0 = Success\n// - 1 = Error\n// - 2 = Success with changes pending\n// Aggregation rules for run --all:\n// - If any exit code is 1 (or > 2), return the max exit code\n// - If all exit codes are 0 or 2, return 2\n// - If all exit codes are 0, return 0\nfunc (m *DetailedExitCodeMap) GetFinalDetailedExitCode() int {\n\tm.mu.RLock()\n\tdefer m.mu.RUnlock()\n\n\tif len(m.codes) == 0 {\n\t\treturn 0\n\t}\n\n\thasError := false\n\thasChanges := false\n\tmaxCode := 0\n\n\tfor _, code := range m.codes {\n\t\tif code == DetailedExitCodeError || code > DetailedExitCodeChanges {\n\t\t\thasError = true\n\n\t\t\tmaxCode = max(maxCode, code)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif code == DetailedExitCodeChanges {\n\t\t\thasChanges = true\n\t\t}\n\t}\n\n\tif hasError {\n\t\treturn maxCode\n\t}\n\n\tif hasChanges {\n\t\treturn DetailedExitCodeChanges\n\t}\n\n\treturn DetailedExitCodeSuccess\n}\n\n// GetFinalExitCode computes the final exit code assuming the user hasn't supplied the -detailed-exitcode flag.\n//\n// In this case, we only care about any non-zero exit codes, so we'll return the highest exit code we can find.\nfunc (m *DetailedExitCodeMap) GetFinalExitCode() int {\n\tm.mu.RLock()\n\tdefer m.mu.RUnlock()\n\n\tmaxCode := 0\n\tfor _, code := range m.codes {\n\t\tmaxCode = max(maxCode, code)\n\t}\n\n\treturn maxCode\n}\n"
  },
  {
    "path": "internal/tf/doc.go",
    "content": "// Package tf contains functions and routines for interacting with OpenTofu/Terraform.\n//\n// MAINTAINER'S NOTE: Ideally we would be able to reuse code from Terraform. However, terraform has moved to packaging\n// all its libraries under internal so that you can't use them as a library outside of Terraform. To respect the\n// direction and spirit of the Terraform team, we opted for not doing anything funky to workaround the limitation (like\n// copying those files in here). We also opted to keep this functionality internal to align with the Terraform team's\n// decision to not support client libraries for accessing the Terraform Registry.\npackage tf\n"
  },
  {
    "path": "internal/tf/errors.go",
    "content": "package tf\n\nimport \"fmt\"\n\n// MalformedRegistryURLErr is returned if the Terraform Registry URL passed to the Getter is malformed.\ntype MalformedRegistryURLErr struct {\n\treason string\n}\n\nfunc (err MalformedRegistryURLErr) Error() string {\n\treturn \"tfr getter URL is malformed: \" + err.reason\n}\n\n// ServiceDiscoveryErr is returned if Terragrunt failed to identify the module API endpoint through the service\n// discovery protocol.\ntype ServiceDiscoveryErr struct {\n\treason string\n}\n\nfunc (err ServiceDiscoveryErr) Error() string {\n\treturn \"Error identifying module registry API location: \" + err.reason\n}\n\n// ModuleDownloadErr is returned if Terragrunt failed to download the module.\ntype ModuleDownloadErr struct {\n\tsourceURL string\n\tdetails   string\n}\n\nfunc (err ModuleDownloadErr) Error() string {\n\treturn fmt.Sprintf(\"Error downloading module from %s: %s\", err.sourceURL, err.details)\n}\n\n// RegistryAPIErr is returned if we get an unsuccessful HTTP return code from the registry.\ntype RegistryAPIErr struct {\n\turl        string\n\tstatusCode int\n}\n\nfunc (err RegistryAPIErr) Error() string {\n\treturn fmt.Sprintf(\"Failed to fetch url %s: status code %d\", err.url, err.statusCode)\n}\n"
  },
  {
    "path": "internal/tf/getproviders/constraints.go",
    "content": "package getproviders\n\nimport (\n\t\"fmt\"\n\t\"maps\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tfimpl\"\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/hashicorp/hcl/v2/hclsyntax\"\n\t\"github.com/zclconf/go-cty/cty\"\n)\n\n// ProviderConstraints maps provider addresses to their version constraints from required_providers blocks\ntype ProviderConstraints map[string]string\n\n// ParseProviderConstraints parses all .tf and .tofu files in the given directory and extracts required_providers constraints\nfunc ParseProviderConstraints(impl tfimpl.Type, workingDir string) (ProviderConstraints, error) {\n\tconstraints := make(ProviderConstraints)\n\n\tvar allFiles []string\n\n\ttfFiles, err := filepath.Glob(filepath.Join(workingDir, \"*.tf\"))\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tallFiles = append(allFiles, tfFiles...)\n\n\ttofuFiles, err := filepath.Glob(filepath.Join(workingDir, \"*.tofu\"))\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tallFiles = append(allFiles, tofuFiles...)\n\n\t// If no terraform files found, return empty constraints (not an error)\n\tif len(allFiles) == 0 {\n\t\treturn constraints, nil\n\t}\n\n\tfor _, file := range allFiles {\n\t\tfileConstraints, err := parseProviderConstraintsFromFile(impl, file)\n\t\tif err != nil {\n\t\t\t// Log parsing errors but continue processing other files\n\t\t\t// This allows partial success when some files have syntax errors\n\t\t\tcontinue\n\t\t}\n\n\t\t// Merge constraints from this file\n\t\tmaps.Copy(constraints, fileConstraints)\n\t}\n\n\treturn constraints, nil\n}\n\n// parseProviderConstraintsFromFile parses a single .tf file and extracts required_providers constraints\nfunc parseProviderConstraintsFromFile(impl tfimpl.Type, filename string) (ProviderConstraints, error) {\n\tconstraints := make(ProviderConstraints)\n\n\tcontent, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\t// Parse the HCL file\n\tfile, diags := hclsyntax.ParseConfig(content, filename, hcl.Pos{Line: 1, Column: 1})\n\tif diags.HasErrors() {\n\t\treturn nil, errors.New(diags)\n\t}\n\n\t// Walk through the file looking for terraform blocks with required_providers\n\tbody, ok := file.Body.(*hclsyntax.Body)\n\tif !ok {\n\t\treturn nil, errors.New(\"failed to parse HCL body\")\n\t}\n\n\tfor _, block := range body.Blocks {\n\t\tif block.Type != \"terraform\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Look for required_providers block within terraform block\n\t\tfor _, nestedBlock := range block.Body.Blocks {\n\t\t\tif nestedBlock.Type != \"required_providers\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Parse each provider in the required_providers block\n\t\t\tproviderConstraints := parseProvidersFromRequiredProvidersBlock(impl, nestedBlock)\n\n\t\t\t// Merge constraints from this required_providers block\n\t\t\tmaps.Copy(constraints, providerConstraints)\n\t\t}\n\t}\n\n\treturn constraints, nil\n}\n\n// parseProvidersFromRequiredProvidersBlock extracts provider constraints from a required_providers block\nfunc parseProvidersFromRequiredProvidersBlock(impl tfimpl.Type, block *hclsyntax.Block) ProviderConstraints {\n\tconstraints := make(ProviderConstraints)\n\n\t// Parse the attributes in the required_providers block\n\tfor name, attr := range block.Body.Attributes {\n\t\t// Skip if not an object expression (should be provider configuration)\n\t\tobjExpr, ok := attr.Expr.(*hclsyntax.ObjectConsExpr)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar source, version string\n\n\t\t// Extract source and version from the provider configuration\n\t\tfor _, item := range objExpr.Items {\n\t\t\tkeyExpr, ok := item.KeyExpr.(*hclsyntax.ObjectConsKeyExpr)\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Get the key name\n\t\t\tkeyName := \"\"\n\n\t\t\tif keyExpr.Wrapped != nil {\n\t\t\t\t// Try different types of key expressions\n\t\t\t\tswitch expr := keyExpr.Wrapped.(type) {\n\t\t\t\tcase *hclsyntax.TemplateExpr:\n\t\t\t\t\tif len(expr.Parts) == 1 {\n\t\t\t\t\t\tif literal, ok := expr.Parts[0].(*hclsyntax.LiteralValueExpr); ok {\n\t\t\t\t\t\t\tkeyName = literal.Val.AsString()\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\tcase *hclsyntax.ScopeTraversalExpr:\n\t\t\t\t\t// This handles bare identifiers like \"source\" or \"version\"\n\t\t\t\t\tif len(expr.Traversal) == 1 {\n\t\t\t\t\t\tif root, ok := expr.Traversal[0].(hcl.TraverseRoot); ok {\n\t\t\t\t\t\t\tkeyName = root.Name\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\tcase *hclsyntax.LiteralValueExpr:\n\t\t\t\t\t// Direct literal value\n\t\t\t\t\tif expr.Val.Type() == cty.String {\n\t\t\t\t\t\tkeyName = expr.Val.AsString()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Get the value\n\t\t\tvar value string\n\n\t\t\tif templateExpr, ok := item.ValueExpr.(*hclsyntax.TemplateExpr); ok {\n\t\t\t\tif len(templateExpr.Parts) == 1 {\n\t\t\t\t\tif literal, ok := templateExpr.Parts[0].(*hclsyntax.LiteralValueExpr); ok {\n\t\t\t\t\t\tif literal.Val.Type() == cty.String {\n\t\t\t\t\t\t\tvalue = literal.Val.AsString()\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\t// Store source and version attributes\n\t\t\tswitch keyName {\n\t\t\tcase \"source\":\n\t\t\t\tsource = value\n\t\t\tcase \"version\":\n\t\t\t\tversion = value\n\t\t\t}\n\t\t}\n\n\t\t// If we have both source and version, create the constraint mapping\n\t\tif source != \"\" && version != \"\" {\n\t\t\t// Normalize the source address to full registry format\n\t\t\tproviderAddr := normalizeProviderAddress(impl, source)\n\t\t\tconstraints[providerAddr] = normalizeVersionConstraint(version)\n\t\t} else if source == \"\" && version != \"\" {\n\t\t\t// If only version is specified, assume it's a hashicorp provider\n\t\t\tregistryDomain := tf.GetDefaultRegistryDomain(impl)\n\t\t\tproviderAddr := fmt.Sprintf(\"%s/hashicorp/%s\", registryDomain, name)\n\t\t\tconstraints[providerAddr] = normalizeVersionConstraint(version)\n\t\t}\n\t}\n\n\treturn constraints\n}\n\n// normalizeProviderAddress converts provider source to full registry format\nfunc normalizeProviderAddress(impl tfimpl.Type, source string) string {\n\tparts := strings.Split(source, \"/\")\n\tregistryDomain := tf.GetDefaultRegistryDomain(impl)\n\n\tconst (\n\t\tsinglePart    = 1\n\t\ttwoPartPath   = 2\n\t\tthreePartPath = 3\n\t)\n\n\tswitch len(parts) {\n\tcase singlePart:\n\t\t// \"aws\" -> \"registry.terraform.io/hashicorp/aws\" or \"registry.opentofu.org/hashicorp/aws\"\n\t\treturn fmt.Sprintf(\"%s/hashicorp/%s\", registryDomain, parts[0])\n\tcase twoPartPath:\n\t\t// \"hashicorp/aws\" -> \"registry.terraform.io/hashicorp/aws\" or \"registry.opentofu.org/hashicorp/aws\"\n\t\treturn fmt.Sprintf(\"%s/%s\", registryDomain, source)\n\tcase threePartPath:\n\t\t// \"registry.terraform.io/hashicorp/aws\" -> keep as is\n\t\treturn source\n\tdefault:\n\t\t// Fallback to original if format is unexpected\n\t\treturn source\n\t}\n}\n\n// normalizeVersionConstraint normalizes version constraints to the format expected by OpenTofu/Terraform lockfiles.\n//\n// This includes:\n// 1. Removing the \"=\" prefix if present\n// 2. Normalizing version numbers to full 3-part format (e.g., \"2.2\" becomes \"2.2.0\")\n// 3. Handling multi-part constraints (e.g., \">= 3.0, < 7.0\" becomes \">= 3.0.0, < 7.0.0\")\nfunc normalizeVersionConstraint(constraint string) string {\n\tconstraint = strings.TrimSpace(constraint)\n\n\tparts := strings.Split(constraint, \",\")\n\tnormalized := make([]string, 0, len(parts))\n\n\tfor _, part := range parts {\n\t\tnormalized = append(normalized, normalizeSingleConstraint(strings.TrimSpace(part)))\n\t}\n\n\treturn strings.Join(normalized, \", \")\n}\n\n// normalizeSingleConstraint normalizes a single version constraint (no commas).\nfunc normalizeSingleConstraint(constraint string) string {\n\tif after, ok := strings.CutPrefix(constraint, \"=\"); ok {\n\t\tconstraint = strings.TrimSpace(after)\n\t}\n\n\tfields := strings.Fields(constraint)\n\n\tconst justVersionParts = 1\n\tif len(fields) == justVersionParts {\n\t\tif v, err := version.NewVersion(fields[0]); err == nil {\n\t\t\treturn v.String()\n\t\t}\n\n\t\treturn constraint\n\t}\n\n\tconst operatorAndVersionParts = 2\n\tif len(fields) == operatorAndVersionParts {\n\t\toperator := fields[0]\n\t\tversionStr := fields[1]\n\n\t\tif v, err := version.NewVersion(versionStr); err == nil {\n\t\t\treturn fmt.Sprintf(\"%s %s\", operator, v.String())\n\t\t}\n\t}\n\n\treturn constraint\n}\n"
  },
  {
    "path": "internal/tf/getproviders/constraints_test.go",
    "content": "package getproviders_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/getproviders\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tfimpl\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParseProviderConstraints(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a temporary directory for testing\n\ttestDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create a test terraform file with required_providers block\n\tterraformContent := `\nterraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 5.0\"\n    }\n    cloudflare = {\n      source  = \"cloudflare/cloudflare\"\n      version = \"~> 4.0\"\n    }\n  }\n}\n`\n\n\terr := os.WriteFile(filepath.Join(testDir, \"main.tf\"), []byte(terraformContent), 0644)\n\trequire.NoError(t, err)\n\n\t// Test parsing with Terraform implementation\n\tconstraints, err := getproviders.ParseProviderConstraints(tfimpl.Terraform, testDir)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"~> 5.0.0\", constraints[\"registry.terraform.io/hashicorp/aws\"])\n\tassert.Equal(t, \"~> 4.0.0\", constraints[\"registry.terraform.io/cloudflare/cloudflare\"])\n\n\t// Test parsing with OpenTofu implementation\n\tconstraints, err = getproviders.ParseProviderConstraints(tfimpl.OpenTofu, testDir)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"~> 5.0.0\", constraints[\"registry.opentofu.org/hashicorp/aws\"])\n\tassert.Equal(t, \"~> 4.0.0\", constraints[\"registry.opentofu.org/cloudflare/cloudflare\"])\n}\n\nfunc TestParseProviderConstraintsWithImplicitProvider(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a temporary directory for testing\n\ttestDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create a test terraform file with implicit provider (no source specified)\n\tterraformContent := `\nterraform {\n  required_providers {\n    aws = {\n      version = \"~> 5.0\"\n    }\n  }\n}\n`\n\n\terr := os.WriteFile(filepath.Join(testDir, \"main.tf\"), []byte(terraformContent), 0644)\n\trequire.NoError(t, err)\n\n\t// Test parsing with Terraform implementation\n\tconstraints, err := getproviders.ParseProviderConstraints(tfimpl.Terraform, testDir)\n\trequire.NoError(t, err)\n\n\t// Verify the parsed constraints default to terraform registry and are normalized\n\tassert.Equal(t, \"~> 5.0.0\", constraints[\"registry.terraform.io/hashicorp/aws\"])\n\n\t// Test parsing with OpenTofu implementation\n\tconstraints, err = getproviders.ParseProviderConstraints(tfimpl.OpenTofu, testDir)\n\trequire.NoError(t, err)\n\n\t// Verify the parsed constraints default to OpenTofu registry and are normalized\n\tassert.Equal(t, \"~> 5.0.0\", constraints[\"registry.opentofu.org/hashicorp/aws\"])\n}\n\nfunc TestParseProviderConstraintsWithEnvironmentOverride(t *testing.T) {\n\t// Create a temporary directory for testing\n\ttestDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create a test terraform file with implicit provider (no source specified)\n\tterraformContent := `\nterraform {\n  required_providers {\n    aws = {\n      version = \"~> 5.0\"\n    }\n    custom = {\n      source  = \"example/custom\"\n      version = \"~> 1.0\"\n    }\n  }\n}\n`\n\n\terr := os.WriteFile(filepath.Join(testDir, \"main.tf\"), []byte(terraformContent), 0644)\n\trequire.NoError(t, err)\n\n\t// Set the environment variable to override the default registry\n\tcustomRegistry := \"custom.registry.example.com\"\n\tt.Setenv(\"TG_TF_DEFAULT_REGISTRY_HOST\", customRegistry)\n\n\t// Test parsing with Terraform implementation - should use custom registry\n\tconstraints, err := getproviders.ParseProviderConstraints(tfimpl.Terraform, testDir)\n\trequire.NoError(t, err)\n\n\t// Verify the parsed constraints use custom registry for implicit providers and are normalized\n\tassert.Equal(t, \"~> 5.0.0\", constraints[customRegistry+\"/hashicorp/aws\"])\n\t// Explicit source should use custom registry too and be normalized\n\tassert.Equal(t, \"~> 1.0.0\", constraints[customRegistry+\"/example/custom\"])\n\n\t// Test parsing with OpenTofu implementation - should also use custom registry (environment override takes precedence)\n\tconstraints, err = getproviders.ParseProviderConstraints(tfimpl.OpenTofu, testDir)\n\trequire.NoError(t, err)\n\n\t// Verify the parsed constraints use custom registry even with OpenTofu and are normalized\n\tassert.Equal(t, \"~> 5.0.0\", constraints[customRegistry+\"/hashicorp/aws\"])\n\tassert.Equal(t, \"~> 1.0.0\", constraints[customRegistry+\"/example/custom\"])\n}\n\nfunc TestParseProviderConstraintsWithTofuFiles(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a temporary directory for testing\n\ttestDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create a .tf file with one provider\n\ttfContent := `\nterraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"~> 5.0\"\n    }\n  }\n}\n`\n\terr := os.WriteFile(filepath.Join(testDir, \"main.tf\"), []byte(tfContent), 0644)\n\trequire.NoError(t, err)\n\n\t// Create a .tofu file with another provider\n\ttofuContent := `\nterraform {\n  required_providers {\n    azurerm = {\n      source  = \"hashicorp/azurerm\"\n      version = \"~> 3.0\"\n    }\n  }\n}\n`\n\terr = os.WriteFile(filepath.Join(testDir, \"providers.tofu\"), []byte(tofuContent), 0644)\n\trequire.NoError(t, err)\n\n\t// Test parsing with OpenTofu implementation\n\tconstraints, err := getproviders.ParseProviderConstraints(tfimpl.OpenTofu, testDir)\n\trequire.NoError(t, err)\n\n\t// Verify constraints from both .tf and .tofu files are parsed and normalized\n\tassert.Equal(t, \"~> 5.0.0\", constraints[\"registry.opentofu.org/hashicorp/aws\"])\n\tassert.Equal(t, \"~> 3.0.0\", constraints[\"registry.opentofu.org/hashicorp/azurerm\"])\n\n\t// Test parsing with Terraform implementation\n\tconstraints, err = getproviders.ParseProviderConstraints(tfimpl.Terraform, testDir)\n\trequire.NoError(t, err)\n\n\t// Verify constraints from both .tf and .tofu files are parsed with Terraform registry and normalized\n\tassert.Equal(t, \"~> 5.0.0\", constraints[\"registry.terraform.io/hashicorp/aws\"])\n\tassert.Equal(t, \"~> 3.0.0\", constraints[\"registry.terraform.io/hashicorp/azurerm\"])\n}\n\nfunc TestParseProviderConstraintsWithEqualsPrefix(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a temporary directory for testing\n\ttestDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create a test terraform file with \"=\" prefix in version constraints\n\tterraformContent := `\nterraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"= 5.100.0\"\n    }\n    cloudflare = {\n      source  = \"cloudflare/cloudflare\"\n      version = \"= 4.40.0\"\n    }\n    time = {\n      source  = \"hashicorp/time\"\n      version = \">= 0.10.0\"\n    }\n  }\n}\n`\n\n\terr := os.WriteFile(filepath.Join(testDir, \"main.tf\"), []byte(terraformContent), 0644)\n\trequire.NoError(t, err)\n\n\t// Test parsing with Terraform implementation\n\tconstraints, err := getproviders.ParseProviderConstraints(tfimpl.Terraform, testDir)\n\trequire.NoError(t, err)\n\n\t// Verify the parsed constraints are normalized (no \"=\" prefix)\n\tassert.Equal(t, \"5.100.0\", constraints[\"registry.terraform.io/hashicorp/aws\"])\n\tassert.Equal(t, \"4.40.0\", constraints[\"registry.terraform.io/cloudflare/cloudflare\"])\n\tassert.Equal(t, \">= 0.10.0\", constraints[\"registry.terraform.io/hashicorp/time\"])\n\n\t// Test parsing with OpenTofu implementation\n\tconstraints, err = getproviders.ParseProviderConstraints(tfimpl.OpenTofu, testDir)\n\trequire.NoError(t, err)\n\n\t// Verify the parsed constraints are normalized with OpenTofu registry\n\tassert.Equal(t, \"5.100.0\", constraints[\"registry.opentofu.org/hashicorp/aws\"])\n\tassert.Equal(t, \"4.40.0\", constraints[\"registry.opentofu.org/cloudflare/cloudflare\"])\n\tassert.Equal(t, \">= 0.10.0\", constraints[\"registry.opentofu.org/hashicorp/time\"])\n}\n\nfunc TestNormalizeVersionConstraint(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"normalize basic version constraint\",\n\t\t\tinput:    \">= 2.2\",\n\t\t\texpected: \">= 2.2.0\",\n\t\t},\n\t\t{\n\t\t\tname:     \"normalize pessimistic constraint\",\n\t\t\tinput:    \"~> 4.0\",\n\t\t\texpected: \"~> 4.0.0\",\n\t\t},\n\t\t{\n\t\t\tname:     \"already normalized constraint unchanged\",\n\t\t\tinput:    \">= 2.2.0\",\n\t\t\texpected: \">= 2.2.0\",\n\t\t},\n\t\t{\n\t\t\tname:     \"remove equals prefix\",\n\t\t\tinput:    \"= 1.0\",\n\t\t\texpected: \"1.0.0\",\n\t\t},\n\t\t{\n\t\t\tname:     \"complex constraint with patch version\",\n\t\t\tinput:    \"~> 3.14.15\",\n\t\t\texpected: \"~> 3.14.15\",\n\t\t},\n\t\t{\n\t\t\tname:     \"exact version constraint\",\n\t\t\tinput:    \"1.2\",\n\t\t\texpected: \"1.2.0\",\n\t\t},\n\t\t{\n\t\t\tname:     \"invalid constraint returned as-is\",\n\t\t\tinput:    \"invalid-constraint\",\n\t\t\texpected: \"invalid-constraint\",\n\t\t},\n\t\t{\n\t\t\tname:     \"whitespace handling\",\n\t\t\tinput:    \"  >= 1.0  \",\n\t\t\texpected: \">= 1.0.0\",\n\t\t},\n\t\t{\n\t\t\tname:     \"equals prefix with whitespace\",\n\t\t\tinput:    \"= 2.5\",\n\t\t\texpected: \"2.5.0\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multi-part constraint normalizes each part\",\n\t\t\tinput:    \">= 3.0, < 7.0\",\n\t\t\texpected: \">= 3.0.0, < 7.0.0\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multi-part constraint with three parts\",\n\t\t\tinput:    \">= 2.0, >= 3.0, < 7.0\",\n\t\t\texpected: \">= 2.0.0, >= 3.0.0, < 7.0.0\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multi-part already normalized\",\n\t\t\tinput:    \">= 3.0.0, < 7.0.0\",\n\t\t\texpected: \">= 3.0.0, < 7.0.0\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multi-part with mixed operators\",\n\t\t\tinput:    \"~> 5.0, != 5.3\",\n\t\t\texpected: \"~> 5.0.0, != 5.3.0\",\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.Parallel()\n\n\t\t\t// We need to call the unexported function through the public API\n\t\t\t// So we'll test it through the constraint parsing\n\t\t\ttestDir := helpers.TmpDirWOSymlinks(t)\n\t\t\tterraformContent := `terraform {\n  required_providers {\n    test = {\n      source  = \"example/test\"\n      version = \"` + tc.input + `\"\n    }\n  }\n}`\n\n\t\t\terr := os.WriteFile(filepath.Join(testDir, \"main.tf\"), []byte(terraformContent), 0644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tconstraints, err := getproviders.ParseProviderConstraints(tfimpl.Terraform, testDir)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tresult := constraints[\"registry.terraform.io/example/test\"]\n\t\t\tassert.Equal(t, tc.expected, result, \"Input: %s\", tc.input)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/tf/getproviders/hash.go",
    "content": "package getproviders\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"golang.org/x/mod/sumdb/dirhash\"\n)\n\n// Hash is a specially-formatted string representing a checksum of a package or the contents of the package.\ntype Hash string\n\nfunc (hash Hash) String() string {\n\treturn string(hash)\n}\n\n// HashScheme is an enumeration of schemes.\ntype HashScheme string\n\nconst (\n\t// HashSchemeZip is the scheme identifier for the legacy hash scheme that applies to distribution archives (.zip files) rather than package contents.\n\tHashSchemeZip HashScheme = HashScheme(\"zh:\")\n)\n\n// New creates a new Hash value with the receiver as its scheme and the given raw string as its value.\nfunc (scheme HashScheme) New(value string) Hash {\n\treturn Hash(string(scheme) + value)\n}\n\n// PackageHashLegacyZipSHA implements the old provider package hashing scheme of taking a SHA256 hash of the containing .zip archive itself, rather than of the contents of the archive.\nfunc PackageHashLegacyZipSHA(path string) (Hash, error) {\n\tarchivePath, err := filepath.EvalSymlinks(path)\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\tfile, err := os.Open(archivePath)\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\tdefer file.Close()\n\n\thash := sha256.New()\n\tif _, err = io.Copy(hash, file); err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\tgotHash := hash.Sum(nil)\n\n\treturn HashSchemeZip.New(hex.EncodeToString(gotHash)), nil\n}\n\n// HashLegacyZipSHAFromSHA is a convenience method to produce the schemed-string hash format from an already-calculated hash of a provider .zip archive.\nfunc HashLegacyZipSHAFromSHA(sum [sha256.Size]byte) Hash {\n\treturn HashSchemeZip.New(hex.EncodeToString(sum[:]))\n}\n\n// PackageHashV1 computes a hash of the contents of the package at the given location using hash algorithm 1. The resulting Hash is guaranteed to have the scheme HashScheme1.\nfunc PackageHashV1(path string) (Hash, error) {\n\t// We'll first dereference a possible symlink at our PackageDir location, as would be created if this package were linked in from another cache.\n\tpackageDir, err := filepath.EvalSymlinks(path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif fileInfo, err := os.Stat(packageDir); err != nil {\n\t\treturn \"\", errors.New(err)\n\t} else if !fileInfo.IsDir() {\n\t\treturn \"\", errors.Errorf(\"packageDir is not a directory %q\", packageDir)\n\t}\n\n\ts, err := dirhash.HashDir(packageDir, \"\", dirhash.Hash1)\n\n\treturn Hash(s), err\n}\n\nfunc DocumentHashes(doc []byte) []Hash {\n\tvar hashes []Hash\n\n\tsc := bufio.NewScanner(bytes.NewReader(doc))\n\tfor sc.Scan() {\n\t\tparts := bytes.Fields(sc.Bytes())\n\n\t\tcolumns := 2\n\t\tif len(parts) != columns {\n\t\t\t// Doesn't look like a valid sums file line, so we'll assume this whole thing isn't a checksums file.\n\t\t\tcontinue\n\t\t}\n\n\t\t// If this is a checksums file then the first part should be a hex-encoded SHA256 hash, so it should be 64 characters long and contain only hex digits.\n\t\thashStr := parts[0]\n\n\t\thashLen := 64\n\t\tif len(hashStr) != hashLen {\n\t\t\treturn nil // doesn't look like a checksums file\n\t\t}\n\n\t\tvar gotSHA256Sum [sha256.Size]byte\n\t\tif _, err := hex.Decode(gotSHA256Sum[:], hashStr); err != nil {\n\t\t\treturn nil // doesn't look like a checksums file\n\t\t}\n\n\t\thashes = append(hashes, HashLegacyZipSHAFromSHA(gotSHA256Sum))\n\t}\n\n\treturn hashes\n}\n"
  },
  {
    "path": "internal/tf/getproviders/hash_test.go",
    "content": "package getproviders_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/getproviders\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc createFakeZipArchive(t *testing.T, content []byte) string {\n\tt.Helper()\n\n\tfile, err := os.CreateTemp(helpers.TmpDirWOSymlinks(t), \"*\")\n\trequire.NoError(t, err)\n\n\tdefer file.Close()\n\n\t_, err = file.Write(content)\n\trequire.NoError(t, err)\n\n\treturn file.Name()\n}\n\nfunc TestPackageHashLegacyZipSHA(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tpath         string\n\t\texpectedHash getproviders.Hash\n\t}{\n\t\t{\n\t\t\tcreateFakeZipArchive(t, []byte(\"1234567890\")),\n\t\t\t\"zh:c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646\",\n\t\t},\n\t\t{\n\t\t\tcreateFakeZipArchive(t, []byte(\"0987654321\")),\n\t\t\t\"zh:17756315ebd47b7110359fc7b168179bf6f2df3646fcc888bc8aa05c78b38ac1\",\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\thash, err := getproviders.PackageHashLegacyZipSHA(tc.path)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.expectedHash, hash)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/tf/getproviders/lock.go",
    "content": "//go:generate mockgen -source=$GOFILE -destination=mocks/mock_$GOFILE -package=mocks\n\npackage getproviders\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/hashicorp/hcl/v2/hclsyntax\"\n\t\"github.com/hashicorp/hcl/v2/hclwrite\"\n\t\"github.com/zclconf/go-cty/cty\"\n)\n\n// UpdateLockfile updates the dependency lock file. If `.terraform.lock.hcl` does not exist, it will be created, otherwise it will be updated.\nfunc UpdateLockfile(ctx context.Context, workingDir string, providers []Provider) error {\n\tvar (\n\t\tfilename = filepath.Join(workingDir, tf.TerraformLockFile)\n\t\tfile     = hclwrite.NewFile()\n\t)\n\n\tif util.FileExists(filename) {\n\t\tcontent, err := os.ReadFile(filename)\n\t\tif err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\tvar diags hcl.Diagnostics\n\n\t\tfile, diags = hclwrite.ParseConfig(content, filename, hcl.Pos{Line: 1, Column: 1})\n\t\tif diags.HasErrors() {\n\t\t\treturn errors.New(diags)\n\t\t}\n\t}\n\n\tif err := updateLockfile(ctx, file, providers); err != nil {\n\t\treturn err\n\t}\n\n\tconst ownerWriteGlobalReadPerms = 0644\n\tif err := os.WriteFile(filename, file.Bytes(), ownerWriteGlobalReadPerms); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n\nfunc updateLockfile(ctx context.Context, file *hclwrite.File, providers []Provider) error {\n\tsort.Slice(providers, func(i, j int) bool {\n\t\treturn providers[i].Address() < providers[j].Address()\n\t})\n\n\tfor _, provider := range providers {\n\t\tproviderBlock := file.Body().FirstMatchingBlock(\"provider\", []string{provider.Address()})\n\t\tif providerBlock != nil {\n\t\t\t// update the existing provider block\n\t\t\tif err := updateProviderBlock(ctx, providerBlock, provider); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\t// create a new provider block\n\t\t\tfile.Body().AppendNewline()\n\t\t\tproviderBlock = file.Body().AppendNewBlock(\"provider\", []string{provider.Address()})\n\n\t\t\tif err := updateProviderBlock(ctx, providerBlock, provider); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// updateProviderBlock updates the provider block in the dependency lock file.\nfunc updateProviderBlock(ctx context.Context, providerBlock *hclwrite.Block, provider Provider) error {\n\thashes, err := getExistingHashes(providerBlock, provider)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tproviderBlock.Body().SetAttributeValue(\"version\", cty.StringVal(provider.Version()))\n\n\t// If version constraints exist in current lock file and match the new version, we keep them unchanged.\n\t// Otherwise, we specify the constraints attribute the same as the version.\n\tcurrentConstraintsAttr := providerBlock.Body().GetAttribute(\"constraints\")\n\n\tif shouldUpdateConstraints(currentConstraintsAttr, provider.Version()) {\n\t\t// Use module constraints if available, otherwise fall back to exact version\n\t\tconstraintsValue := provider.Constraints()\n\t\tif constraintsValue == \"\" {\n\t\t\tconstraintsValue = provider.Version()\n\t\t}\n\n\t\tproviderBlock.Body().SetAttributeValue(\"constraints\", cty.StringVal(constraintsValue))\n\t}\n\n\th1Hash, err := PackageHashV1(provider.PackageDir())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnewHashes := []Hash{h1Hash}\n\n\tdocumentSHA256Sums, err := provider.DocumentSHA256Sums(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif documentSHA256Sums != nil {\n\t\tzipHashes := DocumentHashes(documentSHA256Sums)\n\t\tnewHashes = append(newHashes, zipHashes...)\n\t}\n\n\t// merge with existing hashes\n\tfor _, newHashe := range newHashes {\n\t\tif !slices.Contains(hashes, newHashe) {\n\t\t\thashes = append(hashes, newHashe)\n\t\t}\n\t}\n\n\tslices.Sort(hashes)\n\n\tproviderBlock.Body().SetAttributeRaw(\"hashes\", tokensForListPerLine(hashes))\n\n\treturn nil\n}\n\nfunc getExistingHashes(providerBlock *hclwrite.Block, provider Provider) ([]Hash, error) {\n\tversionAttr := providerBlock.Body().GetAttribute(\"version\")\n\tif versionAttr == nil {\n\t\treturn nil, nil\n\t}\n\n\tvar hashes []Hash\n\n\t// a version attribute found\n\tversionVal := getAttributeValueAsUnquotedString(versionAttr)\n\n\tif versionVal == provider.Version() {\n\t\t// if version is equal, get already existing hashes from lock file to merge.\n\t\tif attr := providerBlock.Body().GetAttribute(\"hashes\"); attr != nil {\n\t\t\tvals, err := getAttributeValueAsSlice(attr)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tfor _, val := range vals {\n\t\t\t\thashes = append(hashes, Hash(val))\n\t\t\t}\n\t\t}\n\t}\n\n\treturn hashes, nil\n}\n\n// shouldUpdateConstraints returns true if the constraints attribute should be updated\n// based on the current lock file state and the new provider version.\nfunc shouldUpdateConstraints(currentConstraintsAttr *hclwrite.Attribute, providerVersion string) bool {\n\tif currentConstraintsAttr == nil {\n\t\treturn true\n\t}\n\n\tcurrentConstraintsValue := strings.ReplaceAll(\n\t\tstring(currentConstraintsAttr.Expr().BuildTokens(nil).Bytes()),\n\t\t`\"`,\n\t\t\"\",\n\t)\n\n\tcurrentConstraints, err := version.NewConstraint(currentConstraintsValue)\n\tif err != nil {\n\t\treturn true\n\t}\n\n\tnewVersion, err := version.NewVersion(providerVersion)\n\tif err != nil {\n\t\treturn true\n\t}\n\n\treturn !currentConstraints.Check(newVersion)\n}\n\n// getAttributeValueAsString returns a value of Attribute as string. There is no way to get value as string directly, so we parses tokens of Attribute and build string representation.\nfunc getAttributeValueAsUnquotedString(attr *hclwrite.Attribute) string {\n\t// find TokenEqual\n\texpr := attr.Expr()\n\texprTokens := expr.BuildTokens(nil)\n\n\t// TokenIdent records SpaceBefore, but we should ignore it here.\n\tquotedValue := strings.TrimSpace(string(exprTokens.Bytes()))\n\n\t// unquote\n\tvalue := strings.Trim(quotedValue, \"\\\"\")\n\n\treturn value\n}\n\n// getAttributeValueAsSlice returns a value of Attribute as slice.\nfunc getAttributeValueAsSlice(attr *hclwrite.Attribute) ([]string, error) {\n\texpr := attr.Expr()\n\texprTokens := expr.BuildTokens(nil)\n\n\tvalBytes := bytes.TrimFunc(exprTokens.Bytes(), func(r rune) bool {\n\t\tif unicode.IsSpace(r) || r == ']' || r == ',' {\n\t\t\treturn true\n\t\t}\n\n\t\treturn false\n\t})\n\tvalBytes = append(valBytes, ']')\n\n\tvar val []string\n\n\tif err := json.Unmarshal(valBytes, &val); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\treturn val, nil\n}\n\n// tokensForListPerLine builds a hclwrite.Tokens for a given hashes, but breaks the line for each element.\nfunc tokensForListPerLine(hashes []Hash) hclwrite.Tokens {\n\t// The original TokensForValue implementation does not break line by line for hashes, so we build a token sequence by ourselves.\n\ttokens := append(hclwrite.Tokens{},\n\t\t&hclwrite.Token{Type: hclsyntax.TokenOBrack, Bytes: []byte{'['}},\n\t\t&hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: []byte{'\\n'}})\n\n\tfor _, hash := range hashes {\n\t\tts := hclwrite.TokensForValue(cty.StringVal(hash.String()))\n\t\ttokens = append(tokens, ts...)\n\t\ttokens = append(tokens,\n\t\t\t&hclwrite.Token{Type: hclsyntax.TokenComma, Bytes: []byte{','}},\n\t\t\t&hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: []byte{'\\n'}})\n\t}\n\n\ttokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenCBrack, Bytes: []byte{']'}})\n\n\treturn tokens\n}\n\n// UpdateLockfileConstraints updates only the constraints in an existing lock file\n// This is used for upgrade scenarios where module constraints have changed\n// but no providers were newly downloaded\nfunc UpdateLockfileConstraints(ctx context.Context, workingDir string, constraints ProviderConstraints) error {\n\tfilename := filepath.Join(workingDir, tf.TerraformLockFile)\n\n\tif !util.FileExists(filename) {\n\t\treturn nil\n\t}\n\n\tcontent, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tfile, diags := hclwrite.ParseConfig(content, filename, hcl.Pos{Line: 1, Column: 1})\n\tif diags.HasErrors() {\n\t\treturn errors.New(diags)\n\t}\n\n\tupdated := false\n\n\t// Update constraints for each provider in the lock file\n\tfor providerAddr, newConstraint := range constraints {\n\t\tproviderBlock := file.Body().FirstMatchingBlock(\"provider\", []string{providerAddr})\n\t\tif providerBlock == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tcurrentConstraintsAttr := providerBlock.Body().GetAttribute(\"constraints\")\n\t\tif currentConstraintsAttr == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tcurrentConstraintsValue := strings.ReplaceAll(string(currentConstraintsAttr.Expr().BuildTokens(nil).Bytes()), `\"`, \"\")\n\n\t\tcurrentConstraints, err := version.NewConstraint(currentConstraintsValue)\n\t\tif err != nil {\n\t\t\tproviderBlock.Body().SetAttributeValue(\"constraints\", cty.StringVal(newConstraint))\n\n\t\t\tupdated = true\n\n\t\t\tcontinue\n\t\t}\n\n\t\tversionAttr := providerBlock.Body().GetAttribute(\"version\")\n\t\tif versionAttr != nil {\n\t\t\tversionVal := getAttributeValueAsUnquotedString(versionAttr)\n\t\t\tif v, err := version.NewVersion(versionVal); err == nil && currentConstraints.Check(v) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tproviderBlock.Body().SetAttributeValue(\"constraints\", cty.StringVal(newConstraint))\n\n\t\tupdated = true\n\t}\n\n\tif updated {\n\t\tconst ownerWriteGlobalReadPerms = 0644\n\t\tif err := os.WriteFile(filename, file.Bytes(), ownerWriteGlobalReadPerms); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/tf/getproviders/lock_test.go",
    "content": "//go:build mocks\n\npackage getproviders_test\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/getproviders\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/getproviders/mocks\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"go.uber.org/mock/gomock\"\n)\n\nfunc mockProviderWithConstraints(t *testing.T, ctrl *gomock.Controller, address, ver, constraints string) getproviders.Provider {\n\tt.Helper()\n\n\tpackageDir := helpers.TmpDirWOSymlinks(t)\n\tfile, err := os.Create(filepath.Join(packageDir, \"terraform-provider-v\"+ver))\n\trequire.NoError(t, err)\n\t_, err = fmt.Fprintf(file, \"mock-provider-content-%s-%s\", address, ver)\n\trequire.NoError(t, err)\n\terr = file.Close()\n\trequire.NoError(t, err)\n\n\tvar document string\n\n\tvar documentSB strings.Builder\n\n\tfor i := 0; i < 2; i++ {\n\t\tpackageName := fmt.Sprintf(\"%s-%s-%d\", address, ver, i)\n\t\thasher := sha256.New()\n\t\t_, err := hasher.Write([]byte(packageName))\n\t\trequire.NoError(t, err)\n\n\t\tsha := hex.EncodeToString(hasher.Sum(nil))\n\t\tdocumentSB.WriteString(fmt.Sprintf(\"%s %s\\n\", sha, packageName))\n\t}\n\n\tdocument += documentSB.String()\n\n\tprovider := mocks.NewMockProvider(ctrl)\n\tprovider.EXPECT().Address().Return(address).AnyTimes()\n\tprovider.EXPECT().Version().Return(ver).AnyTimes()\n\tprovider.EXPECT().Constraints().Return(constraints).AnyTimes()\n\tprovider.EXPECT().PackageDir().Return(packageDir).AnyTimes()\n\tprovider.EXPECT().Logger().Return(logger.CreateLogger()).AnyTimes()\n\tprovider.EXPECT().DocumentSHA256Sums(gomock.Any()).Return([]byte(document), nil).AnyTimes()\n\n\treturn provider\n}\n\nfunc mockProviderUpdateLock(t *testing.T, ctrl *gomock.Controller, address, version string) getproviders.Provider {\n\tt.Helper()\n\n\tpackageDir := helpers.TmpDirWOSymlinks(t)\n\tfile, err := os.Create(filepath.Join(packageDir, \"terraform-provider-v\"+version))\n\trequire.NoError(t, err)\n\t_, err = fmt.Fprintf(file, \"mock-provider-content-%s-%s\", address, version)\n\trequire.NoError(t, err)\n\terr = file.Close()\n\trequire.NoError(t, err)\n\n\tvar document string\n\n\tvar documentSB strings.Builder\n\n\tfor i := 0; i < 2; i++ {\n\t\tpackageName := fmt.Sprintf(\"%s-%s-%d\", address, version, i)\n\t\thasher := sha256.New()\n\t\t_, err := hasher.Write([]byte(packageName))\n\t\trequire.NoError(t, err)\n\n\t\tsha := hex.EncodeToString(hasher.Sum(nil))\n\t\tdocumentSB.WriteString(fmt.Sprintf(\"%s %s\\n\", sha, packageName))\n\t}\n\n\tdocument += documentSB.String()\n\n\tprovider := mocks.NewMockProvider(ctrl)\n\tprovider.EXPECT().Address().Return(address).AnyTimes()\n\tprovider.EXPECT().Version().Return(version).AnyTimes()\n\tprovider.EXPECT().Constraints().Return(\"\").AnyTimes()\n\tprovider.EXPECT().PackageDir().Return(packageDir).AnyTimes()\n\tprovider.EXPECT().Logger().Return(logger.CreateLogger()).AnyTimes()\n\tprovider.EXPECT().DocumentSHA256Sums(gomock.Any()).Return([]byte(document), nil).AnyTimes()\n\n\treturn provider\n}\n\nfunc TestMockUpdateLockfile(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tinitialLockfile  string\n\t\texpectedLockfile string\n\t\tproviders        []getproviders.Provider\n\t}{\n\t\t{\n\t\t\tproviders:       []getproviders.Provider{},\n\t\t\tinitialLockfile: ``,\n\t\t\texpectedLockfile: `\nprovider \"registry.terraform.io/hashicorp/aws\" {\n  version     = \"5.37.0\"\n  constraints = \"5.37.0\"\n  hashes = [\n    \"h1:SHOEBOHEif46z7Bb86YZ5evCtAeK5A4gtHdT8RU5OhA=\",\n    \"zh:7c810fb11d8b3ded0cb554a27c27a9d002cc644a7a57c29cae01eeea890f0398\",\n    \"zh:a3366f6b57b0f4b8bf8a5fecf42a834652709a97dd6db1b499c4ab186e33a41f\",\n  ]\n}\n`,\n\t\t},\n\t\t{\n\t\t\tproviders: []getproviders.Provider{},\n\t\t\tinitialLockfile: `\nprovider \"registry.terraform.io/hashicorp/aws\" {\n  version     = \"5.37.0\"\n  constraints = \"5.37.0\"\n  hashes = [\n    \"h1:SHOEBOHEif46z7Bb86YZ5evCtAeK5A4gtHdT8RU5OhA=\",\n    \"zh:7c810fb11d8b3ded0cb554a27c27a9d002cc644a7a57c29cae01eeea890f0398\",\n    \"zh:a3366f6b57b0f4b8bf8a5fecf42a834652709a97dd6db1b499c4ab186e33a41f\",\n  ]\n}\n\nprovider \"registry.terraform.io/hashicorp/azurerm\" {\n  version     = \"3.101.0\"\n  constraints = \"3.101.0\"\n  hashes = [\n    \"h1:Jrkhx+qKaf63sIV/WvE8sPR53QuC16pvTrBjxFVMPYM=\",\n    \"zh:38b02bce5cbe83f938a71716bbf9e8b07fed8b2c6b83c19b5e708eda7dee0f1d\",\n    \"zh:3ed094366ab35c4fcd632471a7e45a84ca6c72b00477cdf1276e541a0171b369\",\n  ]\n}\n`,\n\t\t\texpectedLockfile: `\nprovider \"registry.terraform.io/hashicorp/aws\" {\n  version     = \"5.36.0\"\n  constraints = \"5.36.0\"\n  hashes = [\n    \"h1:RpTjHdEAYqidB9hFPs68dIhkeIE1c/ZH9fEBdddf0Ik=\",\n    \"zh:8721239b83a06212fb2f474d2acddfa2659a224ef66c77e28e1efe2290a30fa7\",\n    \"zh:ed83a9620eab99e091b9f786f20f03fddb50cba030839fe0529bd518bfd67f8d\",\n  ]\n}\n\nprovider \"registry.terraform.io/hashicorp/azurerm\" {\n  version     = \"3.101.0\"\n  constraints = \"3.101.0\"\n  hashes = [\n    \"h1:Jrkhx+qKaf63sIV/WvE8sPR53QuC16pvTrBjxFVMPYM=\",\n    \"zh:38b02bce5cbe83f938a71716bbf9e8b07fed8b2c6b83c19b5e708eda7dee0f1d\",\n    \"zh:3ed094366ab35c4fcd632471a7e45a84ca6c72b00477cdf1276e541a0171b369\",\n  ]\n}\n\nprovider \"registry.terraform.io/hashicorp/template\" {\n  version     = \"2.2.0\"\n  constraints = \"2.2.0\"\n  hashes = [\n    \"h1:kvJsWhTmFya0WW8jAfY40fDtYhWQ6mOwPQC2ncDNjZs=\",\n    \"zh:02d170f0a0f453155686baf35c10b5a7a230ef20ca49f6e26de1c2691ac70a59\",\n    \"zh:d88ec10849d5a1d9d1db458847bbc62049f0282a2139e5176d645b75a0346992\",\n  ]\n}\n`,\n\t\t},\n\t\t{\n\t\t\tproviders: []getproviders.Provider{},\n\t\t\tinitialLockfile: `\nprovider \"registry.terraform.io/hashicorp/aws\" {\n  version     = \"5.36.0\"\n  constraints = \">= 5.36.0\"\n  hashes = [\n    \"h1:SHOEBOHEif46z7Bb86YZ5evCtAeK5A4gtHdT8RU5OhA=\",\n    \"zh:7c810fb11d8b3ded0cb554a27c27a9d002cc644a7a57c29cae01eeea890f0398\",\n    \"zh:a3366f6b57b0f4b8bf8a5fecf42a834652709a97dd6db1b499c4ab186e33a41f\",\n  ]\n}\n\nprovider \"registry.terraform.io/hashicorp/template\" {\n  version     = \"2.1.0\"\n  constraints = \"<= 2.1.0\"\n  hashes = [\n    \"h1:vxE/PD8SWl6Lmh5zRvIW1Y559xfUyuV2T/VeQLXi7f0=\",\n    \"zh:6fc271665ac28c3fee773b9dc2b8066280ba35b7e9a14a6148194a240c43f42a\",\n    \"zh:c19f719c9f7ce6d7449fe9c020100faed0705303c7f95beeef81dfd1e4a2004b\",\n  ]\n}\n`,\n\t\t\texpectedLockfile: `\nprovider \"registry.terraform.io/hashicorp/aws\" {\n  version     = \"5.37.0\"\n  constraints = \">= 5.36.0\"\n  hashes = [\n    \"h1:SHOEBOHEif46z7Bb86YZ5evCtAeK5A4gtHdT8RU5OhA=\",\n    \"zh:7c810fb11d8b3ded0cb554a27c27a9d002cc644a7a57c29cae01eeea890f0398\",\n    \"zh:a3366f6b57b0f4b8bf8a5fecf42a834652709a97dd6db1b499c4ab186e33a41f\",\n  ]\n}\n\nprovider \"registry.terraform.io/hashicorp/template\" {\n  version     = \"2.2.0\"\n  constraints = \"2.2.0\"\n  hashes = [\n    \"h1:kvJsWhTmFya0WW8jAfY40fDtYhWQ6mOwPQC2ncDNjZs=\",\n    \"zh:02d170f0a0f453155686baf35c10b5a7a230ef20ca49f6e26de1c2691ac70a59\",\n    \"zh:d88ec10849d5a1d9d1db458847bbc62049f0282a2139e5176d645b75a0346992\",\n  ]\n}\n`,\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tctrl := gomock.NewController(t)\n\t\t\tdefer ctrl.Finish()\n\n\t\t\tswitch i {\n\t\t\tcase 0:\n\t\t\t\ttc.providers = []getproviders.Provider{\n\t\t\t\t\tmockProviderUpdateLock(t, ctrl, \"registry.terraform.io/hashicorp/aws\", \"5.37.0\"),\n\t\t\t\t}\n\t\t\tcase 1:\n\t\t\t\ttc.providers = []getproviders.Provider{\n\t\t\t\t\tmockProviderUpdateLock(t, ctrl, \"registry.terraform.io/hashicorp/aws\", \"5.36.0\"),\n\t\t\t\t\tmockProviderUpdateLock(t, ctrl, \"registry.terraform.io/hashicorp/template\", \"2.2.0\"),\n\t\t\t\t}\n\t\t\tcase 2:\n\t\t\t\ttc.providers = []getproviders.Provider{\n\t\t\t\t\tmockProviderUpdateLock(t, ctrl, \"registry.terraform.io/hashicorp/aws\", \"5.37.0\"),\n\t\t\t\t\tmockProviderUpdateLock(t, ctrl, \"registry.terraform.io/hashicorp/template\", \"2.2.0\"),\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tworkingDir := helpers.TmpDirWOSymlinks(t)\n\t\t\tlockfilePath := filepath.Join(workingDir, \".terraform.lock.hcl\")\n\n\t\t\tif tc.initialLockfile != \"\" {\n\t\t\t\tfile, err := os.Create(lockfilePath)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\t_, err = file.WriteString(tc.initialLockfile)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\terr = file.Close()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\terr := getproviders.UpdateLockfile(t.Context(), workingDir, tc.providers)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tactualLockfile, err := os.ReadFile(lockfilePath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.expectedLockfile, string(actualLockfile))\n\t\t})\n\t}\n}\n\n// TestMockUpdateLockfilePreservesAggregatedConstraints verifies that when a lock file\n// already has valid aggregated constraints from the full dependency tree (e.g.\n// \">= 2.0.0, >= 3.0.0, >= 4.9.0, < 7.0.0\"), and the provider's module-only\n// constraints differ (e.g. \">= 3.0.0, < 7.0.0\"), the lock file constraints are\n// preserved as-is because the version still satisfies them.\n// This is the bug described in https://github.com/gruntwork-io/terragrunt/issues/5616\nfunc TestMockUpdateLockfilePreservesAggregatedConstraints(t *testing.T) {\n\tt.Parallel()\n\n\tctrl := gomock.NewController(t)\n\tdefer ctrl.Finish()\n\n\t// Create a provider whose module-level constraints differ from the lock file's\n\t// aggregated constraints, but whose version satisfies the lock file constraints.\n\tprovider := mockProviderWithConstraints(t, ctrl,\n\t\t\"registry.terraform.io/hashicorp/aws\", \"5.37.0\",\n\t\t\">= 3.0.0, < 7.0.0\", // module-only constraints (subset of lock file)\n\t)\n\n\tworkingDir := helpers.TmpDirWOSymlinks(t)\n\tlockfilePath := filepath.Join(workingDir, \".terraform.lock.hcl\")\n\n\t// Write a lock file with aggregated constraints from the full dependency tree.\n\tinitialLockfile := `\nprovider \"registry.terraform.io/hashicorp/aws\" {\n  version     = \"5.37.0\"\n  constraints = \">= 2.0.0, >= 3.0.0, >= 4.9.0, < 7.0.0\"\n  hashes = [\n    \"h1:existing-hash=\",\n  ]\n}\n`\n\terr := os.WriteFile(lockfilePath, []byte(initialLockfile), 0644)\n\trequire.NoError(t, err)\n\n\terr = getproviders.UpdateLockfile(t.Context(), workingDir, []getproviders.Provider{provider})\n\trequire.NoError(t, err)\n\n\tactualLockfile, err := os.ReadFile(lockfilePath)\n\trequire.NoError(t, err)\n\n\t// The constraints line must be preserved as the original aggregated constraints,\n\t// NOT overwritten with the module-only constraints.\n\tassert.Contains(t, string(actualLockfile), `constraints = \">= 2.0.0, >= 3.0.0, >= 4.9.0, < 7.0.0\"`,\n\t\t\"Lock file constraints should be preserved, not overwritten with module-only constraints\")\n\tassert.NotContains(t, string(actualLockfile), `constraints = \">= 3.0.0, < 7.0.0\"`,\n\t\t\"Module-only constraints should not replace the aggregated constraints\")\n}\n"
  },
  {
    "path": "internal/tf/getproviders/mocks/mock_lock.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: lock.go\n//\n// Generated by this command:\n//\n//\tmockgen -source=lock.go -destination=mocks/mock_lock.go -package=mocks\n//\n\n// Package mocks is a generated GoMock package.\npackage mocks\n"
  },
  {
    "path": "internal/tf/getproviders/mocks/mock_provider.go",
    "content": "// Code generated by MockGen. DO NOT EDIT.\n// Source: provider.go\n//\n// Generated by this command:\n//\n//\tmockgen -source=provider.go -destination=mocks/mock_provider.go -package=mocks\n//\n\n// Package mocks is a generated GoMock package.\npackage mocks\n\nimport (\n\tcontext \"context\"\n\treflect \"reflect\"\n\n\tlog \"github.com/gruntwork-io/terragrunt/pkg/log\"\n\tgomock \"go.uber.org/mock/gomock\"\n)\n\n// MockProvider is a mock of Provider interface.\ntype MockProvider struct {\n\tctrl     *gomock.Controller\n\trecorder *MockProviderMockRecorder\n\tisgomock struct{}\n}\n\n// MockProviderMockRecorder is the mock recorder for MockProvider.\ntype MockProviderMockRecorder struct {\n\tmock *MockProvider\n}\n\n// NewMockProvider creates a new mock instance.\nfunc NewMockProvider(ctrl *gomock.Controller) *MockProvider {\n\tmock := &MockProvider{ctrl: ctrl}\n\tmock.recorder = &MockProviderMockRecorder{mock}\n\treturn mock\n}\n\n// EXPECT returns an object that allows the caller to indicate expected use.\nfunc (m *MockProvider) EXPECT() *MockProviderMockRecorder {\n\treturn m.recorder\n}\n\n// Address mocks base method.\nfunc (m *MockProvider) Address() string {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Address\")\n\tret0, _ := ret[0].(string)\n\treturn ret0\n}\n\n// Address indicates an expected call of Address.\nfunc (mr *MockProviderMockRecorder) Address() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Address\", reflect.TypeOf((*MockProvider)(nil).Address))\n}\n\n// Constraints mocks base method.\nfunc (m *MockProvider) Constraints() string {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Constraints\")\n\tret0, _ := ret[0].(string)\n\treturn ret0\n}\n\n// Constraints indicates an expected call of Constraints.\nfunc (mr *MockProviderMockRecorder) Constraints() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Constraints\", reflect.TypeOf((*MockProvider)(nil).Constraints))\n}\n\n// DocumentSHA256Sums mocks base method.\nfunc (m *MockProvider) DocumentSHA256Sums(ctx context.Context) ([]byte, error) {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"DocumentSHA256Sums\", ctx)\n\tret0, _ := ret[0].([]byte)\n\tret1, _ := ret[1].(error)\n\treturn ret0, ret1\n}\n\n// DocumentSHA256Sums indicates an expected call of DocumentSHA256Sums.\nfunc (mr *MockProviderMockRecorder) DocumentSHA256Sums(ctx any) *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"DocumentSHA256Sums\", reflect.TypeOf((*MockProvider)(nil).DocumentSHA256Sums), ctx)\n}\n\n// Logger mocks base method.\nfunc (m *MockProvider) Logger() log.Logger {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Logger\")\n\tret0, _ := ret[0].(log.Logger)\n\treturn ret0\n}\n\n// Logger indicates an expected call of Logger.\nfunc (mr *MockProviderMockRecorder) Logger() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Logger\", reflect.TypeOf((*MockProvider)(nil).Logger))\n}\n\n// PackageDir mocks base method.\nfunc (m *MockProvider) PackageDir() string {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"PackageDir\")\n\tret0, _ := ret[0].(string)\n\treturn ret0\n}\n\n// PackageDir indicates an expected call of PackageDir.\nfunc (mr *MockProviderMockRecorder) PackageDir() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"PackageDir\", reflect.TypeOf((*MockProvider)(nil).PackageDir))\n}\n\n// Version mocks base method.\nfunc (m *MockProvider) Version() string {\n\tm.ctrl.T.Helper()\n\tret := m.ctrl.Call(m, \"Version\")\n\tret0, _ := ret[0].(string)\n\treturn ret0\n}\n\n// Version indicates an expected call of Version.\nfunc (mr *MockProviderMockRecorder) Version() *gomock.Call {\n\tmr.mock.ctrl.T.Helper()\n\treturn mr.mock.ctrl.RecordCallWithMethodType(mr.mock, \"Version\", reflect.TypeOf((*MockProvider)(nil).Version))\n}\n"
  },
  {
    "path": "internal/tf/getproviders/package_authentication.go",
    "content": "package getproviders\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\n\t\"github.com/ProtonMail/go-crypto/openpgp\"\n\topenpgpArmor \"github.com/ProtonMail/go-crypto/openpgp/armor\"\n\topenpgpErrors \"github.com/ProtonMail/go-crypto/openpgp/errors\"\n\t\"github.com/ProtonMail/go-crypto/openpgp/packet\"\n)\n\nconst (\n\tVerifiedChecksum PackageAuthenticationResult = iota\n\tOfficialProvider\n\tPartnerProvider\n\tCommunityProvider\n)\n\n// PackageAuthenticationResult is returned from a PackageAuthentication implementation which implements Stringer.\ntype PackageAuthenticationResult int\n\nfunc NewPackageAuthenticationResult(res PackageAuthenticationResult) *PackageAuthenticationResult {\n\treturn &res\n}\n\nfunc (result *PackageAuthenticationResult) String() string {\n\tif result == nil {\n\t\treturn \"unauthenticated\"\n\t}\n\n\treturn []string{\n\t\t\"verified checksum\",\n\t\t\"signed by HashiCorp\",\n\t\t\"signed by a HashiCorp partner\",\n\t\t\"self-signed\",\n\t}[*result]\n}\n\n// SignedByHashiCorp returns whether the package was authenticated as signed by HashiCorp.\nfunc (result PackageAuthenticationResult) SignedByHashiCorp() bool {\n\treturn result == OfficialProvider\n}\n\n// SignedByAnyParty returns whether the package was authenticated as signed by either HashiCorp or by a third-party.\nfunc (result PackageAuthenticationResult) SignedByAnyParty() bool {\n\treturn result == OfficialProvider || result == PartnerProvider || result == CommunityProvider\n}\n\n// ThirdPartySigned returns whether the package was authenticated as signed by a party other than HashiCorp.\nfunc (result PackageAuthenticationResult) ThirdPartySigned() bool {\n\treturn result == PartnerProvider || result == CommunityProvider\n}\n\n// PackageAuthentication implementation is responsible for authenticating that a package is what its distributor intended to distribute and that it has not been tampered with.\ntype PackageAuthentication interface {\n\t// Authenticate takes the path  of a package and returns a PackageAuthenticationResult, or an error if the authentication checks fail.\n\tAuthenticate(path string) (*PackageAuthenticationResult, error)\n}\n\n// PackageAuthenticationHashes is an optional interface implemented by PackageAuthentication implementations that are able to return a set of hashes they would consider valid\n// if a given path referred to a package that matched that hash string.\ntype PackageAuthenticationHashes interface {\n\tPackageAuthentication\n\n\t// AcceptableHashes returns a set of hashes that this authenticator considers to be valid for the current package or, where possible, equivalent packages on other platforms.\n\tAcceptableHashes() []Hash\n}\n\ntype packageAuthenticationAll []PackageAuthentication\n\n// PackageAuthenticationAll combines several authentications together into a single check value, which passes only if all of the given ones pass.\nfunc PackageAuthenticationAll(checks ...PackageAuthentication) PackageAuthentication {\n\treturn packageAuthenticationAll(checks)\n}\n\nfunc (checks packageAuthenticationAll) Authenticate(path string) (*PackageAuthenticationResult, error) {\n\tvar authResult *PackageAuthenticationResult\n\n\tfor _, check := range checks {\n\t\tvar err error\n\n\t\tauthResult, err = check.Authenticate(path)\n\t\tif err != nil {\n\t\t\treturn authResult, err\n\t\t}\n\t}\n\n\treturn authResult, nil\n}\n\nfunc (checks packageAuthenticationAll) AcceptableHashes() []Hash {\n\tfor i := len(checks) - 1; i >= 0; i-- {\n\t\tcheck, ok := checks[i].(PackageAuthenticationHashes)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tallHashes := check.AcceptableHashes()\n\t\tif len(allHashes) > 0 {\n\t\t\treturn allHashes\n\t\t}\n\t}\n\n\treturn nil\n}\n\ntype archiveHashAuthentication struct {\n\tWantSHA256Sum [sha256.Size]byte\n}\n\n// NewArchiveChecksumAuthentication returns a PackageAuthentication implementation that checks that the original distribution archive matches the given hash.\nfunc NewArchiveChecksumAuthentication(wantSHA256Sum [sha256.Size]byte) PackageAuthentication {\n\treturn archiveHashAuthentication{wantSHA256Sum}\n}\n\nfunc (auth archiveHashAuthentication) Authenticate(path string) (*PackageAuthenticationResult, error) {\n\tif fileInfo, err := os.Stat(path); err != nil {\n\t\treturn nil, errors.New(err)\n\t} else if fileInfo.IsDir() {\n\t\treturn nil, errors.Errorf(\"cannot check archive hash for non-archive location %s\", path)\n\t}\n\n\tgotHash, err := PackageHashLegacyZipSHA(path)\n\tif err != nil {\n\t\treturn nil, errors.Errorf(\"failed to compute checksum for %s: %s\", path, err)\n\t}\n\n\twantHash := HashLegacyZipSHAFromSHA(auth.WantSHA256Sum)\n\tif gotHash != wantHash {\n\t\treturn nil, errors.Errorf(\"archive has incorrect checksum %s (expected %s)\", gotHash, wantHash)\n\t}\n\n\treturn NewPackageAuthenticationResult(VerifiedChecksum), nil\n}\n\nfunc (auth archiveHashAuthentication) AcceptableHashes() []Hash {\n\treturn []Hash{HashLegacyZipSHAFromSHA(auth.WantSHA256Sum)}\n}\n\ntype matchingChecksumAuthentication struct {\n\tFilename      string\n\tDocument      []byte\n\tWantSHA256Sum [sha256.Size]byte\n}\n\n// NewMatchingChecksumAuthentication returns a PackageAuthentication implementation that scans a registry-provided SHA256SUMS document for a specified filename,\n// and compares the SHA256 hash against the expected hash\nfunc NewMatchingChecksumAuthentication(document []byte, filename string, wantSHA256Sum [sha256.Size]byte) PackageAuthentication {\n\treturn matchingChecksumAuthentication{\n\t\tDocument:      document,\n\t\tFilename:      filename,\n\t\tWantSHA256Sum: wantSHA256Sum,\n\t}\n}\n\nfunc (auth matchingChecksumAuthentication) Authenticate(location string) (*PackageAuthenticationResult, error) {\n\t// Find the checksum in the list with matching filename. The document is in the form \"0123456789abcdef filename.zip\".\n\tfilename := []byte(auth.Filename)\n\n\tchecksum := util.MatchSha256Checksum(auth.Document, filename)\n\tif checksum == nil {\n\t\treturn nil, errors.Errorf(\"checksum list has no SHA-256 hash for %q\", auth.Filename)\n\t}\n\n\t// Decode the ASCII checksum into a byte array for comparison.\n\tvar gotSHA256Sum [sha256.Size]byte\n\tif _, err := hex.Decode(gotSHA256Sum[:], checksum); err != nil {\n\t\treturn nil, errors.Errorf(\"checksum list has invalid SHA256 hash %q: %s\", string(checksum), err)\n\t}\n\n\t// If the checksums don't match, authentication fails.\n\tif !bytes.Equal(gotSHA256Sum[:], auth.WantSHA256Sum[:]) {\n\t\treturn nil, errors.Errorf(\"checksum list has unexpected SHA-256 hash %x (expected %x)\", gotSHA256Sum, auth.WantSHA256Sum[:])\n\t}\n\n\treturn nil, nil\n}\n\ntype signatureAuthentication struct {\n\tKeys      map[string]string\n\tDocument  []byte\n\tSignature []byte\n}\n\n// NewSignatureAuthentication returns a PackageAuthentication implementation that verifies the cryptographic signature for a package against any of the provided keys.\nfunc NewSignatureAuthentication(document, signature []byte, keys map[string]string) PackageAuthentication {\n\treturn signatureAuthentication{\n\t\tDocument:  document,\n\t\tSignature: signature,\n\t\tKeys:      keys,\n\t}\n}\n\nfunc (auth signatureAuthentication) Authenticate(location string) (*PackageAuthenticationResult, error) {\n\t// Find the key that signed the checksum file. This can fail if there is no valid signature for any of the provided keys.\n\tasciiArmor, trustSignature, err := auth.findSigningKey()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Verify the signature using the HashiCorp public key. If this succeeds, this is an official provider.\n\thashicorpKeyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(HashicorpPublicKey))\n\tif err != nil {\n\t\treturn nil, errors.Errorf(\"error creating HashiCorp keyring: %s\", err)\n\t}\n\n\tif err := auth.checkDetachedSignature(hashicorpKeyring, bytes.NewReader(auth.Document), bytes.NewReader(auth.Signature), nil); err == nil {\n\t\treturn NewPackageAuthenticationResult(OfficialProvider), nil\n\t}\n\n\t// If the signing key has a trust signature, attempt to verify it with the HashiCorp partners public key.\n\tif trustSignature != \"\" {\n\t\thashicorpPartnersKeyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(HashicorpPartnersKey))\n\t\tif err != nil {\n\t\t\treturn nil, errors.Errorf(\"error creating HashiCorp Partners keyring: %s\", err)\n\t\t}\n\n\t\tauthorKey, err := openpgpArmor.Decode(strings.NewReader(asciiArmor))\n\t\tif err != nil {\n\t\t\treturn nil, errors.Errorf(\"error decoding signing key: %s\", err)\n\t\t}\n\n\t\ttrustSignature, err := openpgpArmor.Decode(strings.NewReader(trustSignature))\n\t\tif err != nil {\n\t\t\treturn nil, errors.Errorf(\"error decoding trust signature: %s\", err)\n\t\t}\n\n\t\tif err := auth.checkDetachedSignature(hashicorpPartnersKeyring, authorKey.Body, trustSignature.Body, nil); err != nil {\n\t\t\treturn nil, errors.Errorf(\"error verifying trust signature: %s\", err)\n\t\t}\n\n\t\treturn NewPackageAuthenticationResult(PartnerProvider), nil\n\t}\n\n\t// We have a valid signature, but it's not from the HashiCorp key, and it also isn't a trusted partner. This is a community provider.\n\treturn NewPackageAuthenticationResult(CommunityProvider), nil\n}\n\nfunc (auth signatureAuthentication) checkDetachedSignature(keyring openpgp.KeyRing, signed, signature io.Reader, config *packet.Config) error {\n\tentity, err := openpgp.CheckDetachedSignature(keyring, signed, signature, config)\n\n\tif errors.Is(err, openpgpErrors.ErrKeyExpired) {\n\t\tfor id := range entity.Identities {\n\t\t\tlog.Warnf(\"expired openpgp key from %s\\n\", id)\n\t\t}\n\n\t\terr = nil\n\t}\n\n\treturn err\n}\n\nfunc (auth signatureAuthentication) AcceptableHashes() []Hash {\n\treturn DocumentHashes(auth.Document)\n}\n\n// findSigningKey attempts to verify the signature using each of the keys returned by the registry. If a valid signature is found, it returns the signing key.\nfunc (auth signatureAuthentication) findSigningKey() (string, string, error) {\n\tfor asciiArmor, trustSignature := range auth.Keys {\n\t\tkeyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(asciiArmor))\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", errors.Errorf(\"error decoding signing key: %s\", err)\n\t\t}\n\n\t\tif err := auth.checkDetachedSignature(keyring, bytes.NewReader(auth.Document), bytes.NewReader(auth.Signature), nil); err != nil {\n\t\t\t// If the signature issuer does not match the the key, keep trying the rest of the provided keys.\n\t\t\tif errors.Is(err, openpgpErrors.ErrUnknownIssuer) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Any other signature error is terminal.\n\t\t\treturn \"\", \"\", errors.Errorf(\"error checking signature: %s\", err)\n\t\t}\n\n\t\treturn asciiArmor, trustSignature, nil\n\t}\n\n\t// If none of the provided keys issued the signature, this package is unsigned. This is currently a terminal authentication error.\n\treturn \"\", \"\", errors.Errorf(\"authentication signature from unknown issuer\")\n}\n"
  },
  {
    "path": "internal/tf/getproviders/package_authentication_test.go",
    "content": "package getproviders_test\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/getproviders\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPackageAuthenticationResult(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tresult   *getproviders.PackageAuthenticationResult\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tnil,\n\t\t\t\"unauthenticated\",\n\t\t},\n\t\t{\n\t\t\tgetproviders.NewPackageAuthenticationResult(getproviders.VerifiedChecksum),\n\t\t\t\"verified checksum\",\n\t\t},\n\t\t{\n\t\t\tgetproviders.NewPackageAuthenticationResult(getproviders.OfficialProvider),\n\t\t\t\"signed by HashiCorp\",\n\t\t},\n\t\t{\n\t\t\tgetproviders.NewPackageAuthenticationResult(getproviders.PartnerProvider),\n\t\t\t\"signed by a HashiCorp partner\",\n\t\t},\n\t\t{\n\t\t\tgetproviders.NewPackageAuthenticationResult(getproviders.CommunityProvider),\n\t\t\t\"self-signed\",\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tassert.Equal(t, tc.expected, tc.result.String())\n\t\t})\n\t}\n}\n\nfunc TestArchiveChecksumAuthentication(t *testing.T) {\n\tt.Parallel()\n\n\t// Define platform-specific hashes for the test file\n\tlinuxHash := [sha256.Size]byte{\n\t\t0x4f, 0xb3, 0x98, 0x49, 0xf2, 0xe1, 0x38, 0xeb,\n\t\t0x16, 0xa1, 0x8b, 0xa0, 0xc6, 0x82, 0x63, 0x5d,\n\t\t0x78, 0x1c, 0xb8, 0xc3, 0xb2, 0x59, 0x01, 0xdd,\n\t\t0x5a, 0x79, 0x2a, 0xde, 0x97, 0x11, 0xf5, 0x01,\n\t}\n\n\t// Windows hash - this is the actual hash seen on Windows systems\n\twindowsHash := [sha256.Size]byte{\n\t\t0xa0, 0xd5, 0xc7, 0x0c, 0xb8, 0x17, 0xa8, 0xc2,\n\t\t0x00, 0x52, 0x73, 0x1d, 0x4c, 0xa3, 0x48, 0xe1,\n\t\t0xf2, 0xad, 0x95, 0x5d, 0xd3, 0xb1, 0x33, 0x72,\n\t\t0x96, 0x34, 0xb2, 0x78, 0xaa, 0x61, 0x03, 0xde,\n\t}\n\n\t// Choose hash based on platform\n\tvar expectedHash [sha256.Size]byte\n\tif runtime.GOOS == \"windows\" {\n\t\texpectedHash = windowsHash\n\t} else {\n\t\texpectedHash = linuxHash\n\t}\n\n\ttestCases := []struct {\n\t\texpectedErr    error\n\t\texpectedResult *getproviders.PackageAuthenticationResult\n\t\tpath           string\n\t\twantSHA256Sum  [sha256.Size]byte\n\t}{\n\t\t{\n\t\t\tpath:           \"testdata/filesystem-mirror/registry.terraform.io/hashicorp/null/terraform-provider-null_2.1.0_linux_amd64.zip\",\n\t\t\twantSHA256Sum:  expectedHash,\n\t\t\texpectedResult: getproviders.NewPackageAuthenticationResult(getproviders.VerifiedChecksum),\n\t\t},\n\t\t{\n\t\t\tpath:          \"testdata/filesystem-mirror/registry.terraform.io/hashicorp/null/terraform-provider-null_invalid.zip\",\n\t\t\twantSHA256Sum: expectedHash,\n\t\t\texpectedErr: func() error {\n\t\t\t\tif runtime.GOOS == \"windows\" {\n\t\t\t\t\treturn errors.New(\"archive has incorrect checksum zh:e8ad9768267f71ad74397f18c12fc073da9855d822817c5c4c2c25642e142e68 (expected zh:a0d5c70cb817a8c20052731d4ca348e1f2ad955dd3b133729634b278aa6103de)\")\n\t\t\t\t}\n\n\t\t\t\treturn errors.New(\"archive has incorrect checksum zh:8610a6d93c01e05a0d3920fe66c79b3c7c3b084f1f5c70715afd919fee1d978e (expected zh:4fb39849f2e138eb16a18ba0c682635d781cb8c3b25901dd5a792ade9711f501)\")\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tpath:          \"testdata/no-package-here.zip\",\n\t\t\twantSHA256Sum: [sha256.Size]byte{},\n\t\t\texpectedErr:   errors.New(\"file not found: testdata/no-package-here.zip\"),\n\t\t},\n\t\t{\n\t\t\tpath:          \"testdata/filesystem-mirror/registry.terraform.io/hashicorp/null/terraform-provider-null_2.1.0_linux_amd64.zip\",\n\t\t\twantSHA256Sum: [sha256.Size]byte{},\n\t\t\texpectedErr: func() error {\n\t\t\t\tif runtime.GOOS == \"windows\" {\n\t\t\t\t\treturn errors.New(\"archive has incorrect checksum zh:a0d5c70cb817a8c20052731d4ca348e1f2ad955dd3b133729634b278aa6103de (expected zh:0000000000000000000000000000000000000000000000000000000000000000)\")\n\t\t\t\t}\n\n\t\t\t\treturn errors.New(\"archive has incorrect checksum zh:4fb39849f2e138eb16a18ba0c682635d781cb8c3b25901dd5a792ade9711f501 (expected zh:0000000000000000000000000000000000000000000000000000000000000000)\")\n\t\t\t}(),\n\t\t},\n\t\t{\n\t\t\tpath:          \"testdata/filesystem-mirror/tfe.example.com/AwesomeCorp/happycloud/0.1.0-alpha.2/darwin_amd64\",\n\t\t\twantSHA256Sum: [sha256.Size]byte{},\n\t\t\texpectedErr:   errors.New(\"cannot check archive hash for non-archive location testdata/filesystem-mirror/tfe.example.com/AwesomeCorp/happycloud/0.1.0-alpha.2/darwin_amd64\"),\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tauth := getproviders.NewArchiveChecksumAuthentication(tc.wantSHA256Sum)\n\t\t\tactualResult, actualErr := auth.Authenticate(tc.path)\n\n\t\t\tif tc.expectedErr != nil {\n\t\t\t\tif actualErr == nil {\n\t\t\t\t\tt.Fatalf(\"expected error %v but got no error\", tc.expectedErr)\n\t\t\t\t}\n\t\t\t\t// For file not found errors, just check if it contains the expected text\n\t\t\t\tif strings.Contains(tc.expectedErr.Error(), \"file not found\") {\n\t\t\t\t\tif !strings.Contains(actualErr.Error(), \"no such file\") && !strings.Contains(actualErr.Error(), \"cannot find the file\") {\n\t\t\t\t\t\tt.Errorf(\"expected error containing 'file not found' but got: %v\", actualErr)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\trequire.EqualError(t, actualErr, tc.expectedErr.Error())\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, actualErr)\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectedResult, actualResult)\n\t\t})\n\t}\n}\n\nfunc TestNewMatchingChecksumAuthentication(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedErr   error\n\t\tpath          string\n\t\tfilename      string\n\t\tdocument      []byte\n\t\twantSHA256Sum [sha256.Size]byte\n\t}{\n\t\t{\n\t\t\tpath:          \"testdata/my-package.zip\",\n\t\t\tfilename:      \"my-package.zip\",\n\t\t\tdocument:      fmt.Appendf(nil, \"%x README.txt\\n%x my-package.zip\\n\", [sha256.Size]byte{0xc0, 0xff, 0xee}, [sha256.Size]byte{0xde, 0xca, 0xde}),\n\t\t\twantSHA256Sum: [sha256.Size]byte{0xde, 0xca, 0xde},\n\t\t},\n\n\t\t{\n\t\t\tpath:          \"testdata/my-package.zip\",\n\t\t\tfilename:      \"my-package.zip\",\n\t\t\tdocument:      fmt.Appendf(nil, \"%x README.txt\", [sha256.Size]byte{0xbe, 0xef}),\n\t\t\twantSHA256Sum: [sha256.Size]byte{0xde, 0xca, 0xde},\n\t\t\texpectedErr:   errors.New(`checksum list has no SHA-256 hash for \"my-package.zip\"`),\n\t\t},\n\t\t{\n\t\t\tpath:          \"testdata/my-package.zip\",\n\t\t\tfilename:      \"my-package.zip\",\n\t\t\tdocument:      fmt.Appendf(nil, \"%s README.txt\\n%s my-package.zip\", \"horses\", \"chickens\"),\n\t\t\twantSHA256Sum: [sha256.Size]byte{0xde, 0xca, 0xde},\n\t\t\texpectedErr:   errors.New(`checksum list has invalid SHA256 hash \"chickens\": encoding/hex: invalid byte: U+0068 'h'`),\n\t\t},\n\t\t{\n\t\t\tpath:          \"testdata/my-package.zip\",\n\t\t\tfilename:      \"my-package.zip\",\n\t\t\tdocument:      fmt.Appendf(nil, \"%x README.txt\\n%x my-package.zip\", [sha256.Size]byte{0xbe, 0xef}, [sha256.Size]byte{0xc0, 0xff, 0xee}),\n\t\t\twantSHA256Sum: [sha256.Size]byte{0xde, 0xca, 0xde},\n\t\t\texpectedErr:   errors.New(\"checksum list has unexpected SHA-256 hash c0ffee0000000000000000000000000000000000000000000000000000000000 (expected decade0000000000000000000000000000000000000000000000000000000000)\"),\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tauth := getproviders.NewMatchingChecksumAuthentication(tc.document, tc.filename, tc.wantSHA256Sum)\n\t\t\t_, actualErr := auth.Authenticate(tc.path)\n\n\t\t\tif tc.expectedErr != nil {\n\t\t\t\trequire.EqualError(t, actualErr, tc.expectedErr.Error())\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, actualErr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSignatureAuthentication(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tshasums        string\n\t\texpectedHashes []getproviders.Hash\n\t}{\n\t\t{\n\t\t\t`7d7e888fdd28abfe00894f9055209b9eec785153641de98e6852aa071008d4ee  terraform_0.14.0-alpha20200923_darwin_amd64.zip\nf8b6cf9ade087c17826d49d89cef21261cdc22bd27065bbc5b27d7dbf7fbbf6c  terraform_0.14.0-alpha20200923_freebsd_386.zip\na5ba9945606bb7bfb821ba303957eeb40dd9ee4e706ba8da1eaf7cbeb0356e63  terraform_0.14.0-alpha20200923_freebsd_amd64.zip\ndf3a5a8d6ffff7bacf19c92d10d0d500f98169ea17b3764b01a789f563d1aad7  terraform_0.14.0-alpha20200923_freebsd_arm.zip\n086119a26576d06b8281a97e8644380da89ce16197cd955f74ea5ee664e9358b  terraform_0.14.0-alpha20200923_linux_386.zip\n1e5f7a5f3ade7b8b1d1d59c5cea2e1a2f8d2f8c3f41962dbbe8647e222be8239  terraform_0.14.0-alpha20200923_linux_amd64.zip\n0e9fd0f3e2254b526a0e81e0cfdfc82583b0cd343778c53ead21aa7d52f776d7  terraform_0.14.0-alpha20200923_linux_arm.zip\n66a947e7de1c74caf9f584c3ed4e91d2cb1af6fe5ce8abaf1cf8f7ff626a09d1  terraform_0.14.0-alpha20200923_openbsd_386.zip\ndef1b73849bec0dc57a04405847921bf9206c75b52ae9de195476facb26bd85e  terraform_0.14.0-alpha20200923_openbsd_amd64.zip\n48f1826ec31d6f104e46cc2022b41f30cd1019ef48eaec9697654ef9ec37a879  terraform_0.14.0-alpha20200923_solaris_amd64.zip\n17e0b496022bc4e4137be15e96d2b051c8acd6e14cb48d9b13b262330464f6cc  terraform_0.14.0-alpha20200923_windows_386.zip\n2696c86228f491bc5425561c45904c9ce39b1c676b1e17734cb2ee6b578c4bcd  terraform_0.14.0-alpha20200923_windows_amd64.zip`,\n\t\t\t[]getproviders.Hash{\n\t\t\t\t\"zh:7d7e888fdd28abfe00894f9055209b9eec785153641de98e6852aa071008d4ee\",\n\t\t\t\t\"zh:f8b6cf9ade087c17826d49d89cef21261cdc22bd27065bbc5b27d7dbf7fbbf6c\",\n\t\t\t\t\"zh:a5ba9945606bb7bfb821ba303957eeb40dd9ee4e706ba8da1eaf7cbeb0356e63\",\n\t\t\t\t\"zh:df3a5a8d6ffff7bacf19c92d10d0d500f98169ea17b3764b01a789f563d1aad7\",\n\t\t\t\t\"zh:086119a26576d06b8281a97e8644380da89ce16197cd955f74ea5ee664e9358b\",\n\t\t\t\t\"zh:1e5f7a5f3ade7b8b1d1d59c5cea2e1a2f8d2f8c3f41962dbbe8647e222be8239\",\n\t\t\t\t\"zh:0e9fd0f3e2254b526a0e81e0cfdfc82583b0cd343778c53ead21aa7d52f776d7\",\n\t\t\t\t\"zh:66a947e7de1c74caf9f584c3ed4e91d2cb1af6fe5ce8abaf1cf8f7ff626a09d1\",\n\t\t\t\t\"zh:def1b73849bec0dc57a04405847921bf9206c75b52ae9de195476facb26bd85e\",\n\t\t\t\t\"zh:48f1826ec31d6f104e46cc2022b41f30cd1019ef48eaec9697654ef9ec37a879\",\n\t\t\t\t\"zh:17e0b496022bc4e4137be15e96d2b051c8acd6e14cb48d9b13b262330464f6cc\",\n\t\t\t\t\"zh:2696c86228f491bc5425561c45904c9ce39b1c676b1e17734cb2ee6b578c4bcd\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tauth := getproviders.NewSignatureAuthentication([]byte(tc.shasums), nil, nil)\n\t\t\tauthWithHashes, ok := auth.(getproviders.PackageAuthenticationHashes)\n\t\t\trequire.True(t, ok)\n\n\t\t\tactualHash := authWithHashes.AcceptableHashes()\n\t\t\tassert.Equal(t, tc.expectedHashes, actualHash)\n\t\t})\n\t}\n}\n\nfunc TestSignatureAuthenticate(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedErr    error\n\t\tkeys           map[string]string\n\t\texpectedResult *getproviders.PackageAuthenticationResult\n\t\tpath           string\n\t\tsignature      string\n\t\tdocument       []byte\n\t}{\n\t\t{\n\t\t\tpath:           \"testdata/my-package.zip\",\n\t\t\tdocument:       []byte(testProviderShaSums),\n\t\t\tsignature:      testHashicorpSignatureGoodBase64,\n\t\t\tkeys:           map[string]string{getproviders.HashicorpPublicKey: \"\"},\n\t\t\texpectedResult: getproviders.NewPackageAuthenticationResult(getproviders.OfficialProvider),\n\t\t},\n\t\t{\n\t\t\tpath:        \"testdata/my-package.zip\",\n\t\t\tdocument:    []byte(\"example shasums data\"),\n\t\t\tsignature:   testHashicorpSignatureGoodBase64,\n\t\t\tkeys:        map[string]string{\"invalid PGP armor value\": \"\"},\n\t\t\texpectedErr: errors.New(\"error decoding signing key: openpgp: invalid argument: no armored data found\"),\n\t\t},\n\t\t{\n\t\t\tpath:        \"testdata/my-package.zip\",\n\t\t\tdocument:    []byte(\"example shasums data\"),\n\t\t\tsignature:   testSignatureBadBase64,\n\t\t\tkeys:        map[string]string{testAuthorKeyArmor: \"\"},\n\t\t\texpectedErr: errors.New(\"error checking signature: openpgp: invalid data: signature subpacket truncated\"),\n\t\t},\n\t\t{\n\t\t\tpath:        \"testdata/my-package.zip\",\n\t\t\tdocument:    []byte(\"example shasums data\"),\n\t\t\tsignature:   testAuthorSignatureGoodBase64,\n\t\t\tkeys:        map[string]string{getproviders.HashicorpPublicKey: \"\"},\n\t\t\texpectedErr: errors.New(\"authentication signature from unknown issuer\"),\n\t\t},\n\t\t{\n\t\t\tpath:        \"testdata/my-package.zip\",\n\t\t\tdocument:    []byte(\"example shasums data\"),\n\t\t\tsignature:   testAuthorSignatureGoodBase64,\n\t\t\tkeys:        map[string]string{testAuthorKeyArmor: \"invalid PGP armor value\"},\n\t\t\texpectedErr: errors.New(\"error decoding trust signature: EOF\"),\n\t\t},\n\t\t{\n\t\t\tpath:        \"testdata/my-package.zip\",\n\t\t\tdocument:    []byte(\"example shasums data\"),\n\t\t\tsignature:   testAuthorSignatureGoodBase64,\n\t\t\tkeys:        map[string]string{testAuthorKeyArmor: testOtherKeyTrustSignatureArmor},\n\t\t\texpectedErr: errors.New(\"error verifying trust signature: openpgp: invalid signature: RSA verification failure\"),\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tsignature, err := base64.StdEncoding.DecodeString(tc.signature)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tauth := getproviders.NewSignatureAuthentication(tc.document, signature, tc.keys)\n\t\t\tactualResult, actualErr := auth.Authenticate(tc.path)\n\n\t\t\tif tc.expectedErr != nil {\n\t\t\t\trequire.EqualError(t, actualErr, tc.expectedErr.Error())\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, actualErr)\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectedResult, actualResult)\n\t\t})\n\t}\n}\n\n// testHashicorpSignatureGoodBase64 is a signature of testProviderShaSums signed with\n// HashicorpPublicKey, which represents the SHA256SUMS.sig file downloaded for\n// an official release.\nconst testHashicorpSignatureGoodBase64 = `wsFcBAABCAAQBQJgga+GCRCwtEEJdoW2dgAA` +\n\t`o0YQAAW911BGDr2WHLo5NwcZenwHyxL5DX9g+4BknKbc/WxRC1hD8Afi3eygZk1yR6eT4Gp2H` +\n\t`yNOwCjGL1PTONBumMfj9udIeuX8onrJMMvjFHh+bORGxBi4FKr4V3b2ZV1IYOjWMEyyTGRDvw` +\n\t`SCdxBkp3apH3s2xZLmRoAj84JZ4KaxGF7hlT0j4IkNyQKd2T5cCByN9DV80+x+HtzaOieFwJL` +\n\t`97iyGj6aznXfKfslK6S4oIrVTwyLTrQbxSxA0LsdUjRPHnJamL3sFOG77qUEUoXG3r61yi5vW` +\n\t`V4P5gCH/+C+VkfGHqaB1s0jHYLxoTEXtwthe66MydDBPe2Hd0J12u9ppOIeK3leeb4uiixWIi` +\n\t`rNdpWyjr/LU1KKWPxsDqMGYJ9TexyWkXjEpYmIEiY1Rxar8jrLh+FqVAhxRJajjgSRu5pZj50` +\n\t`CNeKmmbyolLhPCmICjYYU/xKPGXSyDFqonVVyMWCSpO+8F38OmwDQHIk5AWyc8hPOAZ+g5N95` +\n\t`cfUAzEqlvmNvVHQIU40Y6/Ip2HZzzFCLKQkMP1aDakYHq5w4ZO/ucjhKuoh1HDQMuMnZSu4eo` +\n\t`2nMTBzYZnUxwtROrJZF1t103avbmP2QE/GaPvLIQn7o5WMV3ZcPCJ+szzzby7H2e33WIynrY/` +\n\t`95ensBxh7mGFbcQ1C59b5o7viwIaaY2`\n\n// testSignatureBadBase64 is an invalid signature.\nconst testSignatureBadBase64 = `iQEzBAABCAAdFiEEW/7sQxfnRgCGIZcGN6arO88s` +\n\t`4qIKqL6DwddBF4Ju2svn2MeNMGfE358H31mxAl2k4PPrwBTR1sFUCUOzAXVA/g9Ov5Y9ni2G` +\n\t`rkTahBtV9yuUUd1D+oRTTTdP0bj3A+3xxXmKTBhRuvurydPTicKuWzeILIJkcwp7Kl5UbI2N` +\n\t`n1ayZdaCIw/r4w==`\n\n// testAuthorKeyArmor is test key ID 5BFEEC4317E746008621970637A6AB3BCF2C170A.\nconst testAuthorKeyArmor = `-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQENBF5vhgYBCAC40OcC2hEx3yGiLhHMbt7DAVEQ0nZwAWy6oL98niknLumBa1VO\nnMYshP+o/FKOFatBl8aXhmDo606P6pD9d4Pg/WNehqT7hGNHcAFlm+8qjQAvE5uX\nZ/na/Np7dmWasCiL5hYyHEnKU/XFpc9KyicbkS7n8igP1LEb8xDD1pMLULQsQHA4\n258asvtwjoYTZIij1I6bUE178bGFPNCfj+FzQM8nKzPpDVxZ7njN9c2sB9FEdJ1+\nS9mZQNK5PbJuEAOpD5Jp9BnGE16jsLUhDmvGHBjFZAXMBkNSloEMHhs2ty9lEzoF\neJmJx7XCGw+ds1SWp4MsHQPWzXxAlrfa4GMlABEBAAG0R1RlcnJhZm9ybSBUZXN0\naW5nIChwbHVnaW4vZGlzY292ZXJ5LykgPHRlcnJhZm9ybSt0ZXN0aW5nQGhhc2hp\nY29ycC5jb20+iQFOBBMBCAA4FiEEW/7sQxfnRgCGIZcGN6arO88sFwoFAl5vhgYC\nGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQN6arO88sFwpWvQf/apaMu4Bm\nea8AGjdl9acQhHBpWsyiHLIfZvN11xxN/f3+YITvPXIe2PMgveqNfXxu6PIeZGDb\n0DBvnBQy/vqmA+sCQ8t8+kIWdfZ1EeM2YcXdmAEtriooLvc85JFYjafLIKSj9N7o\nV/R/e1BCW/v1/7Je47c+6FSt3HHhwyT5AZ3BCq1zpw6PeCDSQ/gZr3Mvq4CjeLA/\nK+8TM3KyOF4qBGDvzGzp/t9umQSS2L0ozd90lxJtf5Q8ozqDaBiDo+f/osXT2EvN\nVwPP/xh/gABkXiNrPylFbeD+XPAC4N7NmYK5aPDzRYXXknP8e9PDMykoJKZ+bSdz\nF3IZ4q5RDHmmNbkBDQReb4YGAQgAt15e1F8TPQQm1jK8+scypHgfmPHbp7Qsulo1\nGTcUd8QmhbR4kayuLDEpJYzq6+IoTM4TPqsdVuq/1Nwey9oyK0wXk/SUR29nRIQh\n3GBg7JVg1YsObsfVTvEflYOdjk8T/Udqs4I6HnmSbtzsaohzybutpWXPUkW8OzFI\nATwfVTrrz70Yxs+ly0nSEH2Yf+kg2uYZvv5KsJ3MNENhXnHnlaTy2IfhsxAX0xOG\npa9fXV3NzdEbl0mYaEzMi77qRAyIQ9VrIL5F0yY/LlbpLSl6xk2+BB2v3a1Ey6SJ\nw4/le6AM0wlH2hKPCTlkvM0IvUWjlzrPzCkeu027iVc+fqdyiQARAQABiQE2BBgB\nCAAgFiEEW/7sQxfnRgCGIZcGN6arO88sFwoFAl5vhgYCGwwACgkQN6arO88sFwqz\nnAf/eF4oZG9F8sJX01mVdDm/L7Uthe4xjTdl7jwV4ygNX+pCyWrww3qc3qbd3QKg\nCFqIt/TAPE/OxHxCFuxalQefpOqfxjKzvcktxzWmpgxaWsvHaXiS4bKBPz78N/Ke\nMUtcjGHyLeSzYPUfjquqDzQxqXidRYhyHGSy9c0NKZ6wCElLZ6KcmCQb4sZxVwfu\nssjwAFbPMp1nr0f5SWCJfhTh7QF7lO2ldJaKMlcBM8aebmqFQ52P7ZWOFcgeerng\nG7Zdrci1KEd943HhzDCsUFz4gJwbvUyiAYb2ddndpUBkYwCB/XrHWPOSnGxHgZoo\n1gIqed9OV/+s5wKxZPjL0pCStQ==\n=mYqJ\n-----END PGP PUBLIC KEY BLOCK-----`\n\n// testAuthorSignatureGoodBase64 is a signature of testShaSums signed with\n// testAuthorKeyArmor, which represents the SHA256SUMS.sig file downloaded for\n// a release.\nconst testAuthorSignatureGoodBase64 = `iQEzBAABCAAdFiEEW/7sQxfnRgCGIZcGN6arO88s` +\n\t`FwoFAl5vh7gACgkQN6arO88sFwrAlQf6Al77qzjxNIj+NQNJfBGYUE5jHIgcuWOs1IPRTYUI` +\n\t`rHQIUU2RVrdHoAefKTKNzGde653JK/pYTflSV+6ini3/aZZnXlF6t001w3wswmakdwTr0hXx` +\n\t`Ez/hHYio72Gpn7+T/L+nl6dKkjeGqd/Kor5x2TY9uYB737ESmAe5T8ZlPaGMFHh0mYlNTeRq` +\n\t`4qIKqL6DwddBF4Ju2svn2MeNMGfE358H31mxAl2k4PPrwBTR1sFUCUOzAXVA/g9Ov5Y9ni2G` +\n\t`rkTahBtV9yuUUd1D+oRTTTdP0bj3A+3xxXmKTBhRuvurydPTicKuWzeILIJkcwp7Kl5UbI2N` +\n\t`n1ayZdaCIw/r4w==`\n\n// testOtherKeyTrustSignatureArmor is a trust signature of another key (not the\n// author key), signed with HashicorpPartnersKey.\nconst testOtherKeyTrustSignatureArmor = `-----BEGIN PGP SIGNATURE-----\n\niQIzBAABCAAdFiEEUYkGV8Ws20uCMIZWfXLUJo5GYPwFAl6POvsACgkQfXLUJo5G\nYPyGihAAomM1kGmrC5KRgWQ+V47r8wFoIkhsTgAYb9ENOzn/RVJt3SJSstcKxfA3\n7HW5R4kqAoXH1hcPYpUcOcdeAvtZxjGRQ9JgErV8NBg6sR11aQccCzAG4Hy0hWav\n/jB5NzTEX5JFEXH6WhpWI1avh0l2j6JxO1K1s+5+5PI3KbuO+XSqeZ3QmUz9FwGu\npr0J6oYcERupzrpnmgMb5fbkpHfzffR2/MOYdF9Hae4EvDS1b7tokuuKsStNnCm0\nge7PFdekwbj/OiQrQlqM1pOw2siPX3ouWCtW8oExm9tAxNw31Bn2g3oaNMkHMqJd\nhlVUZlqeJMyylUat3cY7GTQONfCnoyUHe/wv8exBUbV3v2glp9y2g9i2XmXkHOrV\nZ+pnNBc+jdp3a4O0Y8fXXZdjiIolZKY8BbvzheuMrQQIOmw4N3KrZbTpLKuqz8rb\nh8bqUbU42oWcJmBvzF4NZ4tQ+aFHs4CbOnjfDfS14baQr2Gqo9BqTfrzS5Pbs8lq\nAhY0r+zi71lQ1rBfgZfjd8zWlOzpDO//nwKhGCqYOWke/C/T6o0zxM0R4uR4zXwT\nKhvXK8/kK/L8Flaxqme0d5bzXLbsMe9I6I76DY5iNhkiFnnWt4+FhGoIDR03MTKS\nSnHodBLlpKLyUXi36DCDy/iKVsieqLsAdcYe0nQFuhoQcOme33A=\n=aHOG\n-----END PGP SIGNATURE-----`\n\nconst testProviderShaSums = `fea4227271ebf7d9e2b61b89ce2328c7262acd9fd190e1fd6d15a591abfa848e  terraform-provider-null_3.1.0_darwin_amd64.zip\n9ebf4d9704faba06b3ec7242c773c0fbfe12d62db7d00356d4f55385fc69bfb2  terraform-provider-null_3.1.0_darwin_arm64.zip\na6576c81adc70326e4e1c999c04ad9ca37113a6e925aefab4765e5a5198efa7e  terraform-provider-null_3.1.0_freebsd_386.zip\n5f9200bf708913621d0f6514179d89700e9aa3097c77dac730e8ba6e5901d521  terraform-provider-null_3.1.0_freebsd_amd64.zip\nfc39cc1fe71234a0b0369d5c5c7f876c71b956d23d7d6f518289737a001ba69b  terraform-provider-null_3.1.0_freebsd_arm.zip\nc797744d08a5307d50210e0454f91ca4d1c7621c68740441cf4579390452321d  terraform-provider-null_3.1.0_linux_386.zip\n53e30545ff8926a8e30ad30648991ca8b93b6fa496272cd23b26763c8ee84515  terraform-provider-null_3.1.0_linux_amd64.zip\ncecb6a304046df34c11229f20a80b24b1603960b794d68361a67c5efe58e62b8  terraform-provider-null_3.1.0_linux_arm64.zip\ne1371aa1e502000d9974cfaff5be4cfa02f47b17400005a16f14d2ef30dc2a70  terraform-provider-null_3.1.0_linux_arm.zip\na8a42d13346347aff6c63a37cda9b2c6aa5cc384a55b2fe6d6adfa390e609c53  terraform-provider-null_3.1.0_windows_386.zip\n02a1675fd8de126a00460942aaae242e65ca3380b5bb192e8773ef3da9073fd2  terraform-provider-null_3.1.0_windows_amd64.zip\n`\n"
  },
  {
    "path": "internal/tf/getproviders/provider.go",
    "content": "//go:generate mockgen -source=$GOFILE -destination=mocks/mock_$GOFILE -package=mocks\n\n// Package getproviders provides an interface for getting providers.\npackage getproviders\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\ntype Provider interface {\n\t// Address returns a source address of the provider. e.g.: registry.terraform.io/hashicorp/aws\n\tAddress() string\n\n\t// Version returns a version of the provider. e.g.: 5.36.0\n\tVersion() string\n\n\t// Constraints returns the version constraints from the module's required_providers block, or empty string if none.\n\tConstraints() string\n\n\t// DocumentSHA256Sums returns a document with providers hashes for different platforms.\n\tDocumentSHA256Sums(ctx context.Context) ([]byte, error)\n\n\t// PackageDir returns a directory with the unpacked provider.\n\tPackageDir() string\n\n\t// Logger returns logger\n\tLogger() log.Logger\n}\n"
  },
  {
    "path": "internal/tf/getproviders/public_keys.go",
    "content": "package getproviders\n\n// HashicorpPublicKey is the HashiCorp public key, also available at\n// https://www.hashicorp.com/security\nconst HashicorpPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBGB9+xkBEACabYZOWKmgZsHTdRDiyPJxhbuUiKX65GUWkyRMJKi/1dviVxOX\nPG6hBPtF48IFnVgxKpIb7G6NjBousAV+CuLlv5yqFKpOZEGC6sBV+Gx8Vu1CICpl\nZm+HpQPcIzwBpN+Ar4l/exCG/f/MZq/oxGgH+TyRF3XcYDjG8dbJCpHO5nQ5Cy9h\nQIp3/Bh09kET6lk+4QlofNgHKVT2epV8iK1cXlbQe2tZtfCUtxk+pxvU0UHXp+AB\n0xc3/gIhjZp/dePmCOyQyGPJbp5bpO4UeAJ6frqhexmNlaw9Z897ltZmRLGq1p4a\nRnWL8FPkBz9SCSKXS8uNyV5oMNVn4G1obCkc106iWuKBTibffYQzq5TG8FYVJKrh\nRwWB6piacEB8hl20IIWSxIM3J9tT7CPSnk5RYYCTRHgA5OOrqZhC7JefudrP8n+M\npxkDgNORDu7GCfAuisrf7dXYjLsxG4tu22DBJJC0c/IpRpXDnOuJN1Q5e/3VUKKW\nmypNumuQpP5lc1ZFG64TRzb1HR6oIdHfbrVQfdiQXpvdcFx+Fl57WuUraXRV6qfb\n4ZmKHX1JEwM/7tu21QE4F1dz0jroLSricZxfaCTHHWNfvGJoZ30/MZUrpSC0IfB3\niQutxbZrwIlTBt+fGLtm3vDtwMFNWM+Rb1lrOxEQd2eijdxhvBOHtlIcswARAQAB\ntERIYXNoaUNvcnAgU2VjdXJpdHkgKGhhc2hpY29ycC5jb20vc2VjdXJpdHkpIDxz\nZWN1cml0eUBoYXNoaWNvcnAuY29tPokCVAQTAQoAPhYhBMh0AR8KtAURDQIQVTQ2\nXZRy10aPBQJgffsZAhsDBQkJZgGABQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJ\nEDQ2XZRy10aPtpcP/0PhJKiHtC1zREpRTrjGizoyk4Sl2SXpBZYhkdrG++abo6zs\nbuaAG7kgWWChVXBo5E20L7dbstFK7OjVs7vAg/OLgO9dPD8n2M19rpqSbbvKYWvp\n0NSgvFTT7lbyDhtPj0/bzpkZEhmvQaDWGBsbDdb2dBHGitCXhGMpdP0BuuPWEix+\nQnUMaPwU51q9GM2guL45Tgks9EKNnpDR6ZdCeWcqo1IDmklloidxT8aKL21UOb8t\ncD+Bg8iPaAr73bW7Jh8TdcV6s6DBFub+xPJEB/0bVPmq3ZHs5B4NItroZ3r+h3ke\nVDoSOSIZLl6JtVooOJ2la9ZuMqxchO3mrXLlXxVCo6cGcSuOmOdQSz4OhQE5zBxx\nLuzA5ASIjASSeNZaRnffLIHmht17BPslgNPtm6ufyOk02P5XXwa69UCjA3RYrA2P\nQNNC+OWZ8qQLnzGldqE4MnRNAxRxV6cFNzv14ooKf7+k686LdZrP/3fQu2p3k5rY\n0xQUXKh1uwMUMtGR867ZBYaxYvwqDrg9XB7xi3N6aNyNQ+r7zI2lt65lzwG1v9hg\nFG2AHrDlBkQi/t3wiTS3JOo/GCT8BjN0nJh0lGaRFtQv2cXOQGVRW8+V/9IpqEJ1\nqQreftdBFWxvH7VJq2mSOXUJyRsoUrjkUuIivaA9Ocdipk2CkP8bpuGz7ZF4uQIN\nBGB9+xkBEACoklYsfvWRCjOwS8TOKBTfl8myuP9V9uBNbyHufzNETbhYeT33Cj0M\nGCNd9GdoaknzBQLbQVSQogA+spqVvQPz1MND18GIdtmr0BXENiZE7SRvu76jNqLp\nKxYALoK2Pc3yK0JGD30HcIIgx+lOofrVPA2dfVPTj1wXvm0rbSGA4Wd4Ng3d2AoR\nG/wZDAQ7sdZi1A9hhfugTFZwfqR3XAYCk+PUeoFrkJ0O7wngaon+6x2GJVedVPOs\n2x/XOR4l9ytFP3o+5ILhVnsK+ESVD9AQz2fhDEU6RhvzaqtHe+sQccR3oVLoGcat\nma5rbfzH0Fhj0JtkbP7WreQf9udYgXxVJKXLQFQgel34egEGG+NlbGSPG+qHOZtY\n4uWdlDSvmo+1P95P4VG/EBteqyBbDDGDGiMs6lAMg2cULrwOsbxWjsWka8y2IN3z\n1stlIJFvW2kggU+bKnQ+sNQnclq3wzCJjeDBfucR3a5WRojDtGoJP6Fc3luUtS7V\n5TAdOx4dhaMFU9+01OoH8ZdTRiHZ1K7RFeAIslSyd4iA/xkhOhHq89F4ECQf3Bt4\nZhGsXDTaA/VgHmf3AULbrC94O7HNqOvTWzwGiWHLfcxXQsr+ijIEQvh6rHKmJK8R\n9NMHqc3L18eMO6bqrzEHW0Xoiu9W8Yj+WuB3IKdhclT3w0pO4Pj8gQARAQABiQI8\nBBgBCgAmFiEEyHQBHwq0BRENAhBVNDZdlHLXRo8FAmB9+xkCGwwFCQlmAYAACgkQ\nNDZdlHLXRo9ZnA/7BmdpQLeTjEiXEJyW46efxlV1f6THn9U50GWcE9tebxCXgmQf\nu+Uju4hreltx6GDi/zbVVV3HCa0yaJ4JVvA4LBULJVe3ym6tXXSYaOfMdkiK6P1v\nJgfpBQ/b/mWB0yuWTUtWx18BQQwlNEQWcGe8n1lBbYsH9g7QkacRNb8tKUrUbWlQ\nQsU8wuFgly22m+Va1nO2N5C/eE/ZEHyN15jEQ+QwgQgPrK2wThcOMyNMQX/VNEr1\nY3bI2wHfZFjotmek3d7ZfP2VjyDudnmCPQ5xjezWpKbN1kvjO3as2yhcVKfnvQI5\nP5Frj19NgMIGAp7X6pF5Csr4FX/Vw316+AFJd9Ibhfud79HAylvFydpcYbvZpScl\n7zgtgaXMCVtthe3GsG4gO7IdxxEBZ/Fm4NLnmbzCIWOsPMx/FxH06a539xFq/1E2\n1nYFjiKg8a5JFmYU/4mV9MQs4bP/3ip9byi10V+fEIfp5cEEmfNeVeW5E7J8PqG9\nt4rLJ8FR4yJgQUa2gs2SNYsjWQuwS/MJvAv4fDKlkQjQmYRAOp1SszAnyaplvri4\nncmfDsf0r65/sd6S40g5lHH8LIbGxcOIN6kwthSTPWX89r42CbY8GzjTkaeejNKx\nv1aCrO58wAtursO1DiXCvBY7+NdafMRnoHwBk50iPqrVkNA8fv+auRyB2/G5Ag0E\nYH3+JQEQALivllTjMolxUW2OxrXb+a2Pt6vjCBsiJzrUj0Pa63U+lT9jldbCCfgP\nwDpcDuO1O05Q8k1MoYZ6HddjWnqKG7S3eqkV5c3ct3amAXp513QDKZUfIDylOmhU\nqvxjEgvGjdRjz6kECFGYr6Vnj/p6AwWv4/FBRFlrq7cnQgPynbIH4hrWvewp3Tqw\nGVgqm5RRofuAugi8iZQVlAiQZJo88yaztAQ/7VsXBiHTn61ugQ8bKdAsr8w/ZZU5\nHScHLqRolcYg0cKN91c0EbJq9k1LUC//CakPB9mhi5+aUVUGusIM8ECShUEgSTCi\nKQiJUPZ2CFbbPE9L5o9xoPCxjXoX+r7L/WyoCPTeoS3YRUMEnWKvc42Yxz3meRb+\nBmaqgbheNmzOah5nMwPupJYmHrjWPkX7oyyHxLSFw4dtoP2j6Z7GdRXKa2dUYdk2\nx3JYKocrDoPHh3Q0TAZujtpdjFi1BS8pbxYFb3hHmGSdvz7T7KcqP7ChC7k2RAKO\nGiG7QQe4NX3sSMgweYpl4OwvQOn73t5CVWYp/gIBNZGsU3Pto8g27vHeWyH9mKr4\ncSepDhw+/X8FGRNdxNfpLKm7Vc0Sm9Sof8TRFrBTqX+vIQupYHRi5QQCuYaV6OVr\nITeegNK3So4m39d6ajCR9QxRbmjnx9UcnSYYDmIB6fpBuwT0ogNtABEBAAGJBHIE\nGAEKACYCGwIWIQTIdAEfCrQFEQ0CEFU0Nl2UctdGjwUCYH4bgAUJAeFQ2wJAwXQg\nBBkBCgAdFiEEs2y6kaLAcwxDX8KAsLRBCXaFtnYFAmB9/iUACgkQsLRBCXaFtnYX\nBhAAlxejyFXoQwyGo9U+2g9N6LUb/tNtH29RHYxy4A3/ZUY7d/FMkArmh4+dfjf0\np9MJz98Zkps20kaYP+2YzYmaizO6OA6RIddcEXQDRCPHmLts3097mJ/skx9qLAf6\nrh9J7jWeSqWO6VW6Mlx8j9m7sm3Ae1OsjOx/m7lGZOhY4UYfY627+Jf7WQ5103Qs\nlgQ09es/vhTCx0g34SYEmMW15Tc3eCjQ21b1MeJD/V26npeakV8iCZ1kHZHawPq/\naCCuYEcCeQOOteTWvl7HXaHMhHIx7jjOd8XX9V+UxsGz2WCIxX/j7EEEc7CAxwAN\nnWp9jXeLfxYfjrUB7XQZsGCd4EHHzUyCf7iRJL7OJ3tz5Z+rOlNjSgci+ycHEccL\nYeFAEV+Fz+sj7q4cFAferkr7imY1XEI0Ji5P8p/uRYw/n8uUf7LrLw5TzHmZsTSC\nUaiL4llRzkDC6cVhYfqQWUXDd/r385OkE4oalNNE+n+txNRx92rpvXWZ5qFYfv7E\n95fltvpXc0iOugPMzyof3lwo3Xi4WZKc1CC/jEviKTQhfn3WZukuF5lbz3V1PQfI\nxFsYe9WYQmp25XGgezjXzp89C/OIcYsVB1KJAKihgbYdHyUN4fRCmOszmOUwEAKR\n3k5j4X8V5bk08sA69NVXPn2ofxyk3YYOMYWW8ouObnXoS8QJEDQ2XZRy10aPMpsQ\nAIbwX21erVqUDMPn1uONP6o4NBEq4MwG7d+fT85rc1U0RfeKBwjucAE/iStZDQoM\nZKWvGhFR+uoyg1LrXNKuSPB82unh2bpvj4zEnJsJadiwtShTKDsikhrfFEK3aCK8\nZuhpiu3jxMFDhpFzlxsSwaCcGJqcdwGhWUx0ZAVD2X71UCFoOXPjF9fNnpy80YNp\nflPjj2RnOZbJyBIM0sWIVMd8F44qkTASf8K5Qb47WFN5tSpePq7OCm7s8u+lYZGK\nwR18K7VliundR+5a8XAOyUXOL5UsDaQCK4Lj4lRaeFXunXl3DJ4E+7BKzZhReJL6\nEugV5eaGonA52TWtFdB8p+79wPUeI3KcdPmQ9Ll5Zi/jBemY4bzasmgKzNeMtwWP\nfk6WgrvBwptqohw71HDymGxFUnUP7XYYjic2sVKhv9AevMGycVgwWBiWroDCQ9Ja\nbtKfxHhI2p+g+rcywmBobWJbZsujTNjhtme+kNn1mhJsD3bKPjKQfAxaTskBLb0V\nwgV21891TS1Dq9kdPLwoS4XNpYg2LLB4p9hmeG3fu9+OmqwY5oKXsHiWc43dei9Y\nyxZ1AAUOIaIdPkq+YG/PhlGE4YcQZ4RPpltAr0HfGgZhmXWigbGS+66pUj+Ojysc\nj0K5tCVxVu0fhhFpOlHv0LWaxCbnkgkQH9jfMEJkAWMOuQINBGCAXCYBEADW6RNr\nZVGNXvHVBqSiOWaxl1XOiEoiHPt50Aijt25yXbG+0kHIFSoR+1g6Lh20JTCChgfQ\nkGGjzQvEuG1HTw07YhsvLc0pkjNMfu6gJqFox/ogc53mz69OxXauzUQ/TZ27GDVp\nUBu+EhDKt1s3OtA6Bjz/csop/Um7gT0+ivHyvJ/jGdnPEZv8tNuSE/Uo+hn/Q9hg\n8SbveZzo3C+U4KcabCESEFl8Gq6aRi9vAfa65oxD5jKaIz7cy+pwb0lizqlW7H9t\nQlr3dBfdIcdzgR55hTFC5/XrcwJ6/nHVH/xGskEasnfCQX8RYKMuy0UADJy72TkZ\nbYaCx+XXIcVB8GTOmJVoAhrTSSVLAZspfCnjwnSxisDn3ZzsYrq3cV6sU8b+QlIX\n7VAjurE+5cZiVlaxgCjyhKqlGgmonnReWOBacCgL/UvuwMmMp5TTLmiLXLT7uxeG\nojEyoCk4sMrqrU1jevHyGlDJH9Taux15GILDwnYFfAvPF9WCid4UZ4Ouwjcaxfys\n3LxNiZIlUsXNKwS3mhiMRL4TRsbs4k4QE+LIMOsauIvcvm8/frydvQ/kUwIhVTH8\n0XGOH909bYtJvY3fudK7ShIwm7ZFTduBJUG473E/Fn3VkhTmBX6+PjOC50HR/Hyb\nwaRCzfDruMe3TAcE/tSP5CUOb9C7+P+hPzQcDwARAQABiQRyBBgBCgAmFiEEyHQB\nHwq0BRENAhBVNDZdlHLXRo8FAmCAXCYCGwIFCQlmAYACQAkQNDZdlHLXRo/BdCAE\nGQEKAB0WIQQ3TsdbSFkTYEqDHMfIIMbVzSerhwUCYIBcJgAKCRDIIMbVzSerh0Xw\nD/9ghnUsoNCu1OulcoJdHboMazJvDt/znttdQSnULBVElgM5zk0Uyv87zFBzuCyQ\nJWL3bWesQ2uFx5fRWEPDEfWVdDrjpQGb1OCCQyz1QlNPV/1M1/xhKGS9EeXrL8Dw\nF6KTGkRwn1yXiP4BGgfeFIQHmJcKXEZ9HkrpNb8mcexkROv4aIPAwn+IaE+NHVtt\nIBnufMXLyfpkWJQtJa9elh9PMLlHHnuvnYLvuAoOkhuvs7fXDMpfFZ01C+QSv1dz\nHm52GSStERQzZ51w4c0rYDneYDniC/sQT1x3dP5Xf6wzO+EhRMabkvoTbMqPsTEP\nxyWr2pNtTBYp7pfQjsHxhJpQF0xjGN9C39z7f3gJG8IJhnPeulUqEZjhRFyVZQ6/\nsiUeq7vu4+dM/JQL+i7KKe7Lp9UMrG6NLMH+ltaoD3+lVm8fdTUxS5MNPoA/I8cK\n1OWTJHkrp7V/XaY7mUtvQn5V1yET5b4bogz4nME6WLiFMd+7x73gB+YJ6MGYNuO8\ne/NFK67MfHbk1/AiPTAJ6s5uHRQIkZcBPG7y5PpfcHpIlwPYCDGYlTajZXblyKrw\nBttVnYKvKsnlysv11glSg0DphGxQJbXzWpvBNyhMNH5dffcfvd3eXJAxnD81GD2z\nZAriMJ4Av2TfeqQ2nxd2ddn0jX4WVHtAvLXfCgLM2Gveho4jD/9sZ6PZz/rEeTvt\nh88t50qPcBa4bb25X0B5FO3TeK2LL3VKLuEp5lgdcHVonrcdqZFobN1CgGJua8TW\nSprIkh+8ATZ/FXQTi01NzLhHXT1IQzSpFaZw0gb2f5ruXwvTPpfXzQrs2omY+7s7\nfkCwGPesvpSXPKn9v8uhUwD7NGW/Dm+jUM+QtC/FqzX7+/Q+OuEPjClUh1cqopCZ\nEvAI3HjnavGrYuU6DgQdjyGT/UDbuwbCXqHxHojVVkISGzCTGpmBcQYQqhcFRedJ\nyJlu6PSXlA7+8Ajh52oiMJ3ez4xSssFgUQAyOB16432tm4erpGmCyakkoRmMUn3p\nwx+QIppxRlsHznhcCQKR3tcblUqH3vq5i4/ZAihusMCa0YrShtxfdSb13oKX+pFr\naZXvxyZlCa5qoQQBV1sowmPL1N2j3dR9TVpdTyCFQSv4KeiExmowtLIjeCppRBEK\neeYHJnlfkyKXPhxTVVO6H+dU4nVu0ASQZ07KiQjbI+zTpPKFLPp3/0sPRJM57r1+\naTS71iR7nZNZ1f8LZV2OvGE6fJVtgJ1J4Nu02K54uuIhU3tg1+7Xt+IqwRc9rbVr\npHH/hFCYBPW2D2dxB+k2pQlg5NI+TpsXj5Zun8kRw5RtVb+dLuiH/xmxArIee8Jq\nZF5q4h4I33PSGDdSvGXn9UMY5Isjpg==\n=7pIB\n-----END PGP PUBLIC KEY BLOCK-----`\n\n// HashicorpPartnersKey is a key created by HashiCorp, used to generate and\n// verify trust signatures for Partner tier providers.\nconst HashicorpPartnersKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQINBF5vdGkBEADKi3Nm83oqMcar+YSDFKBup7+/Ty7m+SldtDH4/RWT0vgVHuQ1\n0joA+TrjITR5/aBVQ1/i2pOiBiImnaWsykccjFw9f9AuJqHo520YrAbNCeA6LuGH\nGvz4u0ReL/Cjbb9xCb34tejmrVOX+tmyiYBQd+oTae3DiyffOI9HxF6v+IKhOFKz\nGrs3/R5MDwU1ZQIXTO2bdBOM67XBwvTUC+dy6Nem5UmmwuCI0Qz/JWTGndG8aGDC\nEO9+DJ59/IwzBYlbs11iqdfqiGALNr+4FXTwftsxZOGpyxhjyAK00U2PP+gQ/wOK\naeIOL7qpF94GdyVrZzDeMKVLUDmhXxDhyatG4UueRJVAoqNVvAFfEwavpYUrVpYl\nse/ZugCcTc9VeDodA4r4VI8yQQW805C+uZ/Q+Ym4r+xTsKcTyC4er4ogXgrMT73B\n9sgA2M1B4oGbMN5IuG/L2C9JZ1Tob0h0fX+UGMOvrpWeJkZEKTU8hm4mZwhxeRdL\nrrcqs6sewNPRnSiUlxz9ynJuf8vFNAD79Z6H9lULe6FnPuLImzH78FKH9QMQsoAW\nz1GlYDrxNs3rHDTkSmvglwmWKpsfCxUnfq4ecsYtroCDjAwhLsf2qO1WlXD8B53h\n6LU5DwPo7jJDpOv4B0YbjGuAJCf0oXmhXqdu9te6ybXb84ArtHlVO4EBRQARAQAB\ntFFIYXNoaUNvcnAgU2VjdXJpdHkgKFRlcnJhZm9ybSBQYXJ0bmVyIFNpZ25pbmcp\nIDxzZWN1cml0eSt0ZXJyYWZvcm1AaGFzaGljb3JwLmNvbT6JAk4EEwEIADgWIQRR\niQZXxazbS4IwhlZ9ctQmjkZg/AUCXm90aQIbAwULCQgHAgYVCgkICwIEFgIDAQIe\nAQIXgAAKCRB9ctQmjkZg/LxFEACACTHlqULv38VCteo8UR4sRFcaSK4kwzXyRLI2\noi3tnGdzc9AJ5Brp6/GwcERz0za3NU6LJ5kI7umHhuSb+FOjzQKLbttfKL+bTiNH\nHY9NyJPhr6wKJs4Mh8HJ7/FdU7Tsg0cpayNvO5ilU3Mf7H1zaWOVut8BFRYqXGKi\nK5/GGmw9C6QwaVSxR4i2kcZYUk4mnTikug53/4sQGnD3zScpDjipEqGTBMLk4r+E\n0792MZFRAYRIMmZ0NfaMoIGE7bnmtMrbqtNiw+VaPILk6EyDVK3XJxNDBY/4kwHW\n4pDa/qjD7nCL7LapP6NN8sDE++l2MSveorzjtR2yV+goqK1yV0VL2X8zwk1jANX7\nHatY6eKJwkx72BpL5N3ps915Od7kc/k7HdDgyoFQCOkuz9nHr7ix1ioltDcaEXwQ\nqTv33M21uG7muNlFsEav2yInPGmIRRqBaGg/5AjF8v1mnGOjzJKNMCIEXIpkYoPS\nfY9wud2s9DvHHvVuF+pT8YtmJDqKdGVAgv+VAH8z6zeIRaQXRRrbzFaCIozmz3qF\nRLPixaPhcw5EHB7MhWBVDnsPXJG811KjMxCrW57ldeBsbR+cEKydEpYFnSjwksGy\nFrCFPA4Vol/ks/ldotS7P9FDmYs7VfB0fco4fdyvwnxksRCfY1kg0dJA3Q0uj/uD\nMoBzF7kCDQReb3RpARAAr1uZ2iRuoFRTBiI2Ao9Mn2Nk0B+WEWT+4S6oDSuryf+6\nsKI9Z+wgSvp7DOKyNARoqv+hnjA5Z+t7y/2K7fZP4TYpqOKw8NRKIUoNH0U2/YED\nLN0FlXKuVdXtqfijoRZF/W/UyEMVRpub0yKwQDgsijoUDXIG1INVO/NSMGh5UJxE\nI+KoU+oIahNPSTgHPizqhJ5OEYkMMfvIr5eHErtB9uylqifVDlvojeHyzU46XmGw\nQLxYzufzLYoeBx9uZjZWIlxpxD2mVPmAYVJtDE0uKRZ29+fnlcxWzhx7Ow+wSVRp\nXLwDLxZh1YJseY/cGj6yzjA8NolG1fx94PRD1iF7VukHJ3LkukK3+Iw2o4JKmrFx\nFpVVcEoldb4bNRMnbY0KDOXn0/9LM+lhEnCRAo8y5zDO6kmjA56emy4iPHRBlngJ\nEgms8wnuKsgNkYG8uRaa6zC9FOY/4MbXtNPg8j3pPlWr5jQVdy053uB9UqGs7y3a\nC1z9bII58Otp8p4Hf5W97MNuXTxPgPDNmWXA6xu7k2+aut8dgvgz1msHTs31bTeG\nX4iRt23/XWlIy56Jar6NkV74rdiKevAbJRHp/sj9AIR4h0pm4yCjZSEKmMqELj7L\nnVSj0s9VSL0algqK5yXLoj6gYUWFfcuHcypnRGvjrpDzGgD9AKrDsmQ3pxFflZ8A\nEQEAAYkCNgQYAQgAIBYhBFGJBlfFrNtLgjCGVn1y1CaORmD8BQJeb3RpAhsMAAoJ\nEH1y1CaORmD89rUP/0gszqvnU3oXo1lMiwz44EfHDGWeY6sh1pJS0FfyjefIMEzE\nrAJvyWXbzRj+Dd2g7m7p5JUf/UEMO6EFdxe1l6IihHJBs+pC6hliFwlGosfJwVc2\nwtPg6okAfFI35RBedvrV3uzq01dqFlb+d85Gl24du6nOv6eBXiZ8Pr9F3zPDHLPw\nDTP/RtNDxnw8KOC0Z0TE9iQIY1rJCI2mekJ4btHRQ2q9eZQjGFp5HcHBXs/D2ZXC\nH/vwB0UskHrtduEUSeTgKkKuPuxbCU5rhE8RGprS41KLYozveD0r5BPa9kBx7qYZ\niOHgWfwlJ4yRjgjtoZl4E9/7aGioYycHNG26UZ+ZHgwTwtDrTU+LP89WrhzoOQmq\nH0oU4P/oMe2YKnG6FgCWt8h+31Q08G5VJeXNUoOn+RG02M7HOMHYGeP5wkzAy2HY\nI4iehn+A3Cwudv8Gh6WaRqPjLGbk9GWr5fAUG3KLUgJ8iEqnt0/waP7KD78TVId8\nDgHymHMvAU+tAxi5wUcC3iQYrBEc1X0vcsRcW6aAi2Cxc/KEkVCz+PJ+HmFVZakS\nV+fniKpSnhUlDkwlG5dMGhkGp/THU3u8oDb3rSydRPcRXVe1D0AReUFE2rDOeRoT\nVYF2OtVmpc4ntcRyrItyhSkR/m7BQeBFIT8GQvbTmrCDQgrZCsFsIwxd4Cb4\n=5/s+\n-----END PGP PUBLIC KEY BLOCK-----`\n"
  },
  {
    "path": "internal/tf/getproviders/testdata/filesystem-mirror/tfe.example.com/AwesomeCorp/happycloud/0.1.0-alpha.2/darwin_amd64/extra-data.txt",
    "content": "Provider plugin packages are allowed to include other files such as any static\ndata they need to operate, or possibly source files if the provider is written\nin an interpreted programming language.\n\nThis extra file is here just to make sure that extra files don't cause any\nmisbehavior during local discovery.\n"
  },
  {
    "path": "internal/tf/getproviders/testdata/filesystem-mirror/tfe.example.com/AwesomeCorp/happycloud/0.1.0-alpha.2/darwin_amd64/terraform-provider-happycloud",
    "content": "# This is just a placeholder file for discovery testing, not a real provider plugin.\n"
  },
  {
    "path": "internal/tf/getter.go",
    "content": "package tf\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tfimpl\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/hashicorp/go-cleanhttp\"\n\t\"github.com/hashicorp/go-getter\"\n\tsafetemp \"github.com/hashicorp/go-safetemp\"\n\tsvchost \"github.com/hashicorp/terraform-svchost\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf/cliconfig\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n)\n\n// httpClient is the default client to be used by HttpGetters.\nvar httpClient = cleanhttp.DefaultClient()\n\n// Constants relevant to the module registry\nconst (\n\tdefaultRegistryDomain   = \"registry.terraform.io\"\n\tdefaultOtRegistryDomain = \"registry.opentofu.org\"\n\tserviceDiscoveryPath    = \"/.well-known/terraform.json\"\n\tversionQueryKey         = \"version\"\n\tauthTokenEnvName        = \"TG_TF_REGISTRY_TOKEN\"\n\tdefaultRegistryEnvName  = \"TG_TF_DEFAULT_REGISTRY_HOST\"\n)\n\n// RegistryServicePath is a struct for extracting the modules service path in the Registry.\ntype RegistryServicePath struct {\n\tModulesPath string `json:\"modules.v1\"`\n}\n\n// RegistryGetter is a Getter (from go-getter) implementation that will download from the terraform module\n// registry. This supports getter URLs encoded in the following manner:\n//\n// tfr://REGISTRY_DOMAIN/MODULE_PATH?version=VERSION\n//\n// Where the REGISTRY_DOMAIN is the terraform registry endpoint (e.g., registry.terraform.io), MODULE_PATH is the\n// registry path for the module (e.g., terraform-aws-modules/vpc/aws), and VERSION is the specific version of the module\n// to download (e.g., 2.2.0).\n//\n// This protocol will use the Module Registry Protocol (documented at\n// https://www.terraform.io/docs/internals/module-registry-protocol.html) to lookup the module source URL and download\n// it.\n//\n// Authentication to private module registries is handled via environment variables. The authorization API token is\n// expected to be provided to Terragrunt via the TG_TF_REGISTRY_TOKEN environment variable. This token can be any\n// registry API token generated on Terraform Cloud / Enterprise.\n//\n// MAINTAINER'S NOTE: Ideally we implement the full credential system that terraform uses as part of `terraform login`,\n// but all the relevant packages are internal to the terraform repository, thus making it difficult to use as a\n// library. For now, we keep things simple by supporting providing tokens via env vars and in the future, we can\n// consider implementing functionality to load credentials from terraform.\n// GH issue: https://github.com/gruntwork-io/terragrunt/issues/1771\n//\n// MAINTAINER'S NOTE: Ideally we can support a shorthand notation that omits the tfr:// protocol to detect that it is\n// referring to a terraform registry, but this requires implementing a complex detector and ensuring it has precedence\n// over the file detector. We deferred the implementation for that to a future release.\n// GH issue: https://github.com/gruntwork-io/terragrunt/issues/1772\ntype RegistryGetter struct {\n\tclient             *getter.Client\n\tLogger             log.Logger\n\tTofuImplementation tfimpl.Type\n}\n\n// SetClient allows the getter to know what getter client (different from the underlying HTTP client) to use for\n// progress tracking.\nfunc (tfrGetter *RegistryGetter) SetClient(client *getter.Client) {\n\ttfrGetter.client = client\n}\n\n// Context returns the go context to use for the underlying fetch routines. This depends on what client is set.\nfunc (tfrGetter *RegistryGetter) Context() context.Context {\n\tif tfrGetter == nil || tfrGetter.client == nil {\n\t\treturn context.Background()\n\t}\n\n\treturn tfrGetter.client.Ctx\n}\n\n// registryDomain returns the default registry domain to use for the getter.\nfunc (tfrGetter *RegistryGetter) registryDomain() string {\n\treturn GetDefaultRegistryDomain(tfrGetter.TofuImplementation)\n}\n\n// GetDefaultRegistryDomain returns the appropriate registry domain based on the terraform implementation and environment variables.\n// This is the canonical function for determining which registry to use throughout Terragrunt.\nfunc GetDefaultRegistryDomain(impl tfimpl.Type) string {\n\t// if is set TG_TF_DEFAULT_REGISTRY env var, use it as default registry\n\tif defaultRegistry := os.Getenv(defaultRegistryEnvName); defaultRegistry != \"\" {\n\t\treturn defaultRegistry\n\t}\n\t// if binary is set to use OpenTofu registry, use OpenTofu as default registry\n\tif impl == tfimpl.OpenTofu {\n\t\treturn defaultOtRegistryDomain\n\t}\n\n\treturn defaultRegistryDomain\n}\n\n// ClientMode returns the download mode based on the given URL. Since this getter is designed around the Terraform\n// module registry, we always use Dir mode so that we can download the full Terraform module.\nfunc (tfrGetter *RegistryGetter) ClientMode(u *url.URL) (getter.ClientMode, error) {\n\treturn getter.ClientModeDir, nil\n}\n\n// Get is the main routine to fetch the module contents specified at the given URL and download it to the dstPath.\n// This routine assumes that the srcURL points to the Terraform registry URL, with the Path configured to the module\n// path encoded as `:namespace/:name/:system` as expected by the Terraform registry. Note that the URL query parameter\n// must have the `version` key to specify what version to download.\nfunc (tfrGetter *RegistryGetter) Get(dstPath string, srcURL *url.URL) error {\n\tctx := tfrGetter.Context()\n\n\tregistryDomain := srcURL.Host\n\tif registryDomain == \"\" {\n\t\tregistryDomain = tfrGetter.registryDomain()\n\t}\n\n\tqueryValues := srcURL.Query()\n\tmodulePath, moduleSubDir := getter.SourceDirSubdir(srcURL.Path)\n\n\tversionList, hasVersion := queryValues[versionQueryKey]\n\tif !hasVersion {\n\t\treturn errors.New(MalformedRegistryURLErr{reason: \"missing version query\"})\n\t}\n\n\tif len(versionList) != 1 {\n\t\treturn errors.New(MalformedRegistryURLErr{reason: \"more than one version query\"})\n\t}\n\n\tversion := versionList[0]\n\n\tmoduleRegistryBasePath, err := GetModuleRegistryURLBasePath(ctx, tfrGetter.Logger, registryDomain)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmoduleURL, err := BuildRequestURL(registryDomain, moduleRegistryBasePath, modulePath, version)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tterraformGet, err := GetTerraformGetHeader(ctx, tfrGetter.Logger, moduleURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdownloadURL, err := GetDownloadURLFromHeader(moduleURL, terraformGet)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// If there is a subdir component, then we download the root separately into a temporary directory, then copy over\n\t// the proper subdir. Note that we also have to take into account sub dirs in the original URL in addition to the\n\t// subdir component in the X-Terraform-Get download URL.\n\tsource, subDir := getter.SourceDirSubdir(downloadURL)\n\tif subDir == \"\" && moduleSubDir == \"\" {\n\t\tvar opts []getter.ClientOption\n\t\tif tfrGetter.client != nil {\n\t\t\topts = tfrGetter.client.Options\n\t\t}\n\n\t\treturn getter.Get(dstPath, source, opts...)\n\t}\n\n\t// We have a subdir, time to jump some hoops\n\treturn tfrGetter.getSubdir(ctx, tfrGetter.Logger, dstPath, source, path.Join(subDir, moduleSubDir))\n}\n\n// GetFile is not implemented for the Terraform module registry Getter since the terraform module registry doesn't serve\n// a single file.\nfunc (tfrGetter *RegistryGetter) GetFile(dst string, src *url.URL) error {\n\treturn errors.New(\"GetFile is not implemented for the Terraform Registry Getter\")\n}\n\n// getSubdir downloads the source into the destination, but with the proper subdir.\nfunc (tfrGetter *RegistryGetter) getSubdir(_ context.Context, l log.Logger, dstPath, sourceURL, subDir string) error {\n\t// Create a temporary directory to store the full source. This has to be a non-existent directory.\n\ttempdirPath, tempdirCloser, err := safetemp.Dir(\"\", \"getter\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func(tempdirCloser io.Closer) {\n\t\terr := tempdirCloser.Close()\n\t\tif err != nil {\n\t\t\tl.Warnf(\"Error closing temporary directory %s: %v\", tempdirPath, err)\n\t\t}\n\t}(tempdirCloser)\n\n\tvar opts []getter.ClientOption\n\tif tfrGetter.client != nil {\n\t\topts = tfrGetter.client.Options\n\t}\n\t// Download that into the given directory\n\tif err := getter.Get(tempdirPath, sourceURL, opts...); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t// Process any globbing\n\tsourcePath, err := getter.SubdirGlob(tempdirPath, subDir)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t// Make sure the subdir path actually exists\n\tif _, err := os.Stat(sourcePath); err != nil {\n\t\tdetails := fmt.Sprintf(\"could not stat download path %s (error: %s)\", sourcePath, err)\n\n\t\treturn errors.New(ModuleDownloadErr{sourceURL: sourceURL, details: details})\n\t}\n\n\t// Copy the subdirectory into our actual destination.\n\tif err := os.RemoveAll(dstPath); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t// Make the final destination\n\tconst ownerWriteGlobalReadExecutePerms = 0755\n\tif err := os.MkdirAll(dstPath, ownerWriteGlobalReadExecutePerms); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t// We use a temporary manifest file here that is deleted at the end of this routine since we don't intend to come\n\t// back to it.\n\tmanifestFname := \".tgmanifest\"\n\tmanifestPath := filepath.Join(dstPath, manifestFname)\n\n\tdefer func(name string) {\n\t\terr := os.Remove(name)\n\t\tif err != nil {\n\t\t\tl.Warnf(\"Error removing temporary directory %s: %v\", name, err)\n\t\t}\n\t}(manifestPath)\n\n\treturn util.CopyFolderContentsWithFilter(l, sourcePath, dstPath, manifestFname, func(path string) bool { return true })\n}\n\n// GetModuleRegistryURLBasePath uses the service discovery protocol\n// (https://www.terraform.io/docs/internals/remote-service-discovery.html)\n// to figure out where the modules are stored. This will return the base\n// path where the modules can be accessed\nfunc GetModuleRegistryURLBasePath(ctx context.Context, logger log.Logger, domain string) (string, error) {\n\tsdURL := url.URL{\n\t\tScheme: \"https\",\n\t\tHost:   domain,\n\t\tPath:   serviceDiscoveryPath,\n\t}\n\n\tbodyData, _, err := httpGETAndGetResponse(ctx, logger, &sdURL)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar respJSON RegistryServicePath\n\tif err := json.Unmarshal(bodyData, &respJSON); err != nil {\n\t\treason := fmt.Sprintf(\"Error parsing response body %s: %s\", string(bodyData), err)\n\n\t\treturn \"\", errors.New(ServiceDiscoveryErr{reason: reason})\n\t}\n\n\treturn respJSON.ModulesPath, nil\n}\n\n// GetTerraformGetHeader makes an http GET call to the given registry URL and return the contents of location json\n// body or the header X-Terraform-Get. This function will return an error if the response does not contain the header.\nfunc GetTerraformGetHeader(ctx context.Context, logger log.Logger, url *url.URL) (string, error) {\n\tbody, header, err := httpGETAndGetResponse(ctx, logger, url)\n\tif err != nil {\n\t\tdetails := \"error receiving HTTP data\"\n\n\t\treturn \"\", errors.New(ModuleDownloadErr{sourceURL: url.String(), details: details})\n\t}\n\n\tterraformGet := header.Get(\"X-Terraform-Get\")\n\tif terraformGet != \"\" {\n\t\treturn terraformGet, nil\n\t}\n\n\t// parse response from body as json\n\tvar responseJSON map[string]string\n\tif err := json.Unmarshal(body, &responseJSON); err != nil {\n\t\treason := fmt.Sprintf(\"Error parsing response body %s: %s\", string(body), err)\n\n\t\treturn \"\", errors.New(ModuleDownloadErr{sourceURL: url.String(), details: reason})\n\t}\n\t// get location value from responseJSON\n\tterraformGet = responseJSON[\"location\"]\n\tif terraformGet != \"\" {\n\t\treturn terraformGet, nil\n\t}\n\n\tif terraformGet == \"\" {\n\t\tdetails := \"no source URL was returned in header X-Terraform-Get and in location response from download URL\"\n\n\t\treturn \"\", errors.New(ModuleDownloadErr{sourceURL: url.String(), details: details})\n\t}\n\n\treturn terraformGet, nil\n}\n\n// GetDownloadURLFromHeader checks if the content of the X-Terraform-GET header contains the base url\n// and prepends it if not\nfunc GetDownloadURLFromHeader(moduleURL *url.URL, terraformGet string) (string, error) {\n\t// If url from X-Terrafrom-Get Header seems to be a relative url,\n\t// append scheme and host from url used for getting the download url\n\t// because third-party registry implementations may not \"know\" their own absolute URLs if\n\t// e.g. they are running behind a reverse proxy frontend, or such.\n\tif strings.HasPrefix(terraformGet, \"/\") || strings.HasPrefix(terraformGet, \"./\") || strings.HasPrefix(terraformGet, \"../\") {\n\t\trelativePathURL, err := url.Parse(terraformGet)\n\t\tif err != nil {\n\t\t\treturn \"\", errors.New(err)\n\t\t}\n\n\t\tterraformGetURL := moduleURL.ResolveReference(relativePathURL)\n\t\tterraformGet = terraformGetURL.String()\n\t}\n\n\treturn terraformGet, nil\n}\n\nfunc applyHostToken(req *http.Request) (*http.Request, error) {\n\tcliCfg, err := cliconfig.LoadUserConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif creds := cliCfg.CredentialsSource().ForHost(svchost.Hostname(req.URL.Hostname())); creds != nil {\n\t\tcreds.PrepareRequest(req)\n\t} else {\n\t\t// fall back to the TG_TF_REGISTRY_TOKEN\n\t\tauthToken := os.Getenv(authTokenEnvName)\n\t\tif authToken != \"\" {\n\t\t\treq.Header.Add(\"Authorization\", \"Bearer \"+authToken)\n\t\t}\n\t}\n\n\treturn req, nil\n}\n\n// httpGETAndGetResponse is a helper function to make a GET request to the given URL using the http client. This\n// function will then read the response and return the contents + the response header.\nfunc httpGETAndGetResponse(ctx context.Context, logger log.Logger, getURL *url.URL) ([]byte, *http.Header, error) {\n\tif getURL == nil {\n\t\treturn nil, nil, errors.New(\"httpGETAndGetResponse received nil getURL\")\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", getURL.String(), nil)\n\tif err != nil {\n\t\treturn nil, nil, errors.New(err)\n\t}\n\n\t// Handle authentication via env var. Authentication is done by providing the registry token as a bearer token in\n\t// the request header.\n\treq, err = applyHostToken(req)\n\tif err != nil {\n\t\treturn nil, nil, errors.New(err)\n\t}\n\n\tresp, err := httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, nil, errors.New(err)\n\t}\n\n\tdefer func() {\n\t\terr := resp.Body.Close()\n\t\tif err != nil {\n\t\t\tlogger.Warnf(\"Error closing response body: %v\", err)\n\t\t}\n\t}()\n\n\tif resp.StatusCode < 200 || resp.StatusCode >= 300 {\n\t\treturn nil, nil, errors.New(RegistryAPIErr{url: getURL.String(), statusCode: resp.StatusCode})\n\t}\n\n\tbodyData, err := io.ReadAll(resp.Body)\n\n\treturn bodyData, &resp.Header, errors.New(err)\n}\n\n// BuildRequestURL - create url to download module using moduleRegistryBasePath\nfunc BuildRequestURL(registryDomain string, moduleRegistryBasePath string, modulePath string, version string) (*url.URL, error) {\n\tmoduleRegistryBasePath = strings.TrimSuffix(moduleRegistryBasePath, \"/\")\n\tmodulePath = strings.TrimSuffix(modulePath, \"/\")\n\tmodulePath = strings.TrimPrefix(modulePath, \"/\")\n\n\tmoduleFullPath := fmt.Sprintf(\"%s/%s/%s/download\", moduleRegistryBasePath, modulePath, version)\n\n\tmoduleURL, err := url.Parse(moduleFullPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif moduleURL.Scheme != \"\" {\n\t\treturn moduleURL, nil\n\t}\n\n\treturn &url.URL{Scheme: \"https\", Host: registryDomain, Path: moduleFullPath}, nil\n}\n"
  },
  {
    "path": "internal/tf/getter_test.go",
    "content": "package tf_test\n\nimport (\n\t\"net/url\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tfimpl\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetModuleRegistryURLBasePath(t *testing.T) {\n\tt.Parallel()\n\n\tbasePath, err := tf.GetModuleRegistryURLBasePath(t.Context(), logger.CreateLogger(), \"registry.terraform.io\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"/v1/modules/\", basePath)\n}\n\nfunc TestGetTerraformHeader(t *testing.T) {\n\tt.Parallel()\n\n\ttestModuleURL := url.URL{\n\t\tScheme: \"https\",\n\t\tHost:   \"registry.terraform.io\",\n\t\tPath:   \"/v1/modules/terraform-aws-modules/vpc/aws/3.3.0/download\",\n\t}\n\tterraformGetHeader, err := tf.GetTerraformGetHeader(t.Context(), logger.CreateLogger(), &testModuleURL)\n\trequire.NoError(t, err)\n\tassert.Contains(t, terraformGetHeader, \"github.com/terraform-aws-modules/terraform-aws-vpc\")\n}\n\nfunc TestGetDownloadURLFromHeader(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname           string\n\t\tterraformGet   string\n\t\texpectedResult string\n\t\tmoduleURL      url.URL\n\t}{\n\t\t{\n\t\t\tname: \"BaseWithRoot\",\n\t\t\tmoduleURL: url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"registry.terraform.io\",\n\t\t\t},\n\t\t\tterraformGet:   \"/terraform-aws-modules/terraform-aws-vpc\",\n\t\t\texpectedResult: \"https://registry.terraform.io/terraform-aws-modules/terraform-aws-vpc\",\n\t\t},\n\t\t{\n\t\t\tname:           \"PrefixedURL\",\n\t\t\tmoduleURL:      url.URL{},\n\t\t\tterraformGet:   \"github.com/terraform-aws-modules/terraform-aws-vpc\",\n\t\t\texpectedResult: \"github.com/terraform-aws-modules/terraform-aws-vpc\",\n\t\t},\n\t\t{\n\t\t\tname: \"PathWithRoot\",\n\t\t\tmoduleURL: url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"registry.terraform.io\",\n\t\t\t\tPath:   \"modules/foo/bar\",\n\t\t\t},\n\t\t\tterraformGet:   \"/terraform-aws-modules/terraform-aws-vpc\",\n\t\t\texpectedResult: \"https://registry.terraform.io/terraform-aws-modules/terraform-aws-vpc\",\n\t\t},\n\t\t{\n\t\t\tname: \"PathWithRelativeRoot\",\n\t\t\tmoduleURL: url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"registry.terraform.io\",\n\t\t\t\tPath:   \"modules/foo/bar\",\n\t\t\t},\n\t\t\tterraformGet:   \"./terraform-aws-modules/terraform-aws-vpc\",\n\t\t\texpectedResult: \"https://registry.terraform.io/modules/foo/terraform-aws-modules/terraform-aws-vpc\",\n\t\t},\n\t\t{\n\t\t\tname: \"PathWithRelativeParent\",\n\t\t\tmoduleURL: url.URL{\n\t\t\t\tScheme: \"https\",\n\t\t\t\tHost:   \"registry.terraform.io\",\n\t\t\t\tPath:   \"modules/foo/bar\",\n\t\t\t},\n\t\t\tterraformGet:   \"../terraform-aws-modules/terraform-aws-vpc\",\n\t\t\texpectedResult: \"https://registry.terraform.io/modules/terraform-aws-modules/terraform-aws-vpc\",\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.Parallel()\n\n\t\t\tdownloadURL, err := tf.GetDownloadURLFromHeader(&tc.moduleURL, tc.terraformGet)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expectedResult, downloadURL)\n\t\t})\n\t}\n}\n\nfunc TestTFRGetterRootDir(t *testing.T) {\n\tt.Parallel()\n\n\ttestModuleURL, err := url.Parse(\"tfr://registry.terraform.io/terraform-aws-modules/vpc/aws?version=3.3.0\")\n\trequire.NoError(t, err)\n\n\tdstPath := helpers.TmpDirWOSymlinks(t)\n\n\t// The dest path must not exist for go getter to work\n\tmoduleDestPath := filepath.Join(dstPath, \"terraform-aws-vpc\")\n\tassert.False(t, util.FileExists(filepath.Join(moduleDestPath, \"main.tf\")))\n\n\ttfrGetter := new(tf.RegistryGetter)\n\ttfrGetter.TofuImplementation = tfimpl.Terraform\n\n\trequire.NoError(t, tfrGetter.Get(moduleDestPath, testModuleURL))\n\tassert.True(t, util.FileExists(filepath.Join(moduleDestPath, \"main.tf\")))\n}\n\nfunc TestTFRGetterSubModule(t *testing.T) {\n\tt.Parallel()\n\n\ttestModuleURL, err := url.Parse(\"tfr://registry.terraform.io/terraform-aws-modules/vpc/aws//modules/vpc-endpoints?version=3.3.0\")\n\trequire.NoError(t, err)\n\n\tdstPath := helpers.TmpDirWOSymlinks(t)\n\n\t// The dest path must not exist for go getter to work\n\tmoduleDestPath := filepath.Join(dstPath, \"terraform-aws-vpc\")\n\tassert.False(t, util.FileExists(filepath.Join(moduleDestPath, \"main.tf\")))\n\n\ttfrGetter := new(tf.RegistryGetter)\n\ttfrGetter.TofuImplementation = tfimpl.Terraform\n\n\trequire.NoError(t, tfrGetter.Get(moduleDestPath, testModuleURL))\n\tassert.True(t, util.FileExists(filepath.Join(moduleDestPath, \"main.tf\")))\n}\n\nfunc TestBuildRequestUrlFullPath(t *testing.T) {\n\tt.Parallel()\n\n\trequestURL, err := tf.BuildRequestURL(\"gruntwork.io\", \"https://gruntwork.io/registry/modules/v1/\", \"/tfr-project/terraform-aws-tfr\", \"6.6.6\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"https://gruntwork.io/registry/modules/v1/tfr-project/terraform-aws-tfr/6.6.6/download\", requestURL.String())\n}\n\nfunc TestBuildRequestUrlRelativePath(t *testing.T) {\n\tt.Parallel()\n\n\trequestURL, err := tf.BuildRequestURL(\"gruntwork.io\", \"/registry/modules/v1\", \"/tfr-project/terraform-aws-tfr\", \"6.6.6\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"https://gruntwork.io/registry/modules/v1/tfr-project/terraform-aws-tfr/6.6.6/download\", requestURL.String())\n}\n"
  },
  {
    "path": "internal/tf/log.go",
    "content": "package tf\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/writer\"\n)\n\nconst parseLogNumberOfValues = 4\n\nvar (\n\t// logTimestampFormat is TF_LOG timestamp formats.\n\tlogTimestampFormat = \"2006-01-02T15:04:05.000Z0700\"\n)\n\nvar (\n\t// tfLogTimeLevelMsgReg is a regular expression that matches TF_LOG output, example output:\n\t//\n\t// 2024-09-08T13:44:31.229+0300 [DEBUG] using github.com/zclconf/go-cty v1.14.3\n\t// 2024-09-08T13:44:31.229+0300 [INFO]  Go runtime version: go1.22.1\n\ttfLogTimeLevelMsgReg = regexp.MustCompile(`(?i)(^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}\\S*)\\s*\\[(trace|debug|warn|info|error)\\]\\s*(.+\\S)$`)\n)\n\n// ParseLogFunc wraps `ParseLog` to add msg prefix and bypasses the parse error if `returnError` is false,\n// since returning the error for `log/writer` will cause TG to fall with a `broken pipe` error.\nfunc ParseLogFunc(msgPrefix string, returnError bool) writer.WriterParseFunc {\n\treturn func(str string) (msg string, ptrTime *time.Time, ptrLevel *log.Level, err error) {\n\t\tif msg, ptrTime, ptrLevel, err = ParseLog(str); err != nil {\n\t\t\tif returnError {\n\t\t\t\treturn str, nil, nil, err\n\t\t\t}\n\n\t\t\treturn str, nil, nil, nil\n\t\t}\n\n\t\treturn msgPrefix + msg, ptrTime, ptrLevel, nil\n\t}\n}\n\nfunc ParseLog(str string) (msg string, ptrTime *time.Time, ptrLevel *log.Level, err error) {\n\tif !tfLogTimeLevelMsgReg.MatchString(str) {\n\t\treturn str, nil, nil, errors.Errorf(\"could not parse string %q: does not match a known format\", str)\n\t}\n\n\tmatch := tfLogTimeLevelMsgReg.FindStringSubmatch(str)\n\tif len(match) != parseLogNumberOfValues {\n\t\treturn str, nil, nil, errors.Errorf(\"could not parse string %q: does not match a known format\", str)\n\t}\n\n\ttimeStr, levelStr, msg := match[1], match[2], match[3]\n\n\tif levelStr != \"\" {\n\t\tlevel, err := log.ParseLevel(strings.ToLower(levelStr))\n\t\tif err != nil {\n\t\t\treturn str, nil, nil, errors.Errorf(\"could not parse level %q: %w\", levelStr, err)\n\t\t}\n\n\t\tptrLevel = &level\n\t}\n\n\tif timeStr != \"\" {\n\t\ttime, err := time.Parse(logTimestampFormat, timeStr)\n\t\tif err != nil {\n\t\t\treturn str, nil, nil, errors.Errorf(\"could not parse time %q: %w\", timeStr, err)\n\t\t}\n\n\t\tptrTime = &time\n\t}\n\n\treturn msg, ptrTime, ptrLevel, nil\n}\n"
  },
  {
    "path": "internal/tf/run_cmd.go",
    "content": "package tf\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"slices\"\n\n\t\"github.com/gruntwork-io/go-commons/collections\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tfimpl\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/internal/writer\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\"\n\tlogwriter \"github.com/gruntwork-io/terragrunt/pkg/log/writer\"\n\t\"github.com/mattn/go-isatty\"\n)\n\nconst (\n\t// tfLogMsgPrefix is a message prefix that is prepended to each TF_LOG output lines when the output is integrated in TG log, for example:\n\t//\n\t// TF_LOG: using github.com/zclconf/go-cty v1.14.3\n\t// TF_LOG: Go runtime version: go1.22.1\n\ttfLogMsgPrefix = \"TF_LOG: \"\n\n\tlogMsgSeparator = \"\\n\"\n\n\tdefaultWriterOptionsLen = 2\n)\n\n// Commands that implement a REPL need a pseudo TTY when run as a subprocess in order for the readline properties to be\n// preserved. This is a list of terraform commands that have this property, which is used to determine if terragrunt\n// should allocate a ptty when running that terraform command.\nvar commandsThatNeedPty = []string{\n\tCommandNameConsole,\n}\n\n// TFOptions contains the configuration needed to run TF commands.\ntype TFOptions struct {\n\tShellOptions         *shell.ShellOptions\n\tTerraformCliArgs     *iacargs.IacArgs\n\tTerragruntConfigPath string\n\tTofuImplementation   tfimpl.Type\n\n\tOriginalTerragruntConfigPath string\n\tJSONLogFormat                bool\n}\n\n// RunCommand runs the given Terraform command.\nfunc RunCommand(ctx context.Context, l log.Logger, runOpts *TFOptions, args ...string) error {\n\t_, err := RunCommandWithOutput(ctx, l, runOpts, args...)\n\n\treturn err\n}\n\n// RunCommandWithOutput runs the given Terraform command, writing its stdout/stderr to the terminal AND returning stdout/stderr to this\n// method's caller\nfunc RunCommandWithOutput(ctx context.Context, l log.Logger, runOpts *TFOptions, args ...string) (*util.CmdOutput, error) {\n\targs = clihelper.Args(args).Normalize(clihelper.SingleDashFlag)\n\n\tif fn := TerraformCommandHookFromContext(ctx); fn != nil {\n\t\treturn fn(ctx, l, runOpts, args)\n\t}\n\n\tneedsPTY, err := isCommandThatNeedsPty(args)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tshellOpts := runOpts.ShellOptions\n\tif !runOpts.ShellOptions.ForwardTFStdout {\n\t\t// Copy the shell opts to avoid mutating the caller's struct.\n\t\tshellOptsCopy := *shellOpts\n\t\tshellOpts = &shellOptsCopy\n\n\t\toutWriter, errWriter := logTFOutput(l, runOpts, args)\n\t\tshellOpts.Writers.Writer = outWriter\n\t\tshellOpts.Writers.ErrWriter = errWriter\n\t}\n\n\toutput, err := shell.RunCommandWithOutput(ctx, l, shellOpts, \"\", false, needsPTY, runOpts.ShellOptions.TFPath, args...)\n\n\thasDetailedExitCode := slices.Contains(args, FlagNameDetailedExitCode)\n\tif hasDetailedExitCode {\n\t\tcode := 0\n\n\t\tif err != nil {\n\t\t\tcode, _ = util.GetExitCode(err)\n\t\t}\n\n\t\tif exitCode := DetailedExitCodeFromContext(ctx); exitCode != nil {\n\t\t\texitCode.Set(filepath.Dir(runOpts.OriginalTerragruntConfigPath), code)\n\t\t}\n\n\t\tif code != 1 {\n\t\t\treturn output, nil\n\t\t}\n\t}\n\n\treturn output, err\n}\n\nfunc logTFOutput(l log.Logger, runOpts *TFOptions, args clihelper.Args) (io.Writer, io.Writer) {\n\tvar (\n\t\toriginalOutWriter           = writer.NewOriginalWriter(runOpts.ShellOptions.Writers.Writer)\n\t\toriginalErrWriter           = writer.NewOriginalWriter(runOpts.ShellOptions.Writers.ErrWriter)\n\t\toutWriter         io.Writer = originalOutWriter\n\t\terrWriter         io.Writer = originalErrWriter\n\t)\n\n\tlogger := l.\n\t\tWithField(placeholders.TFPathKeyName, filepath.Base(runOpts.ShellOptions.TFPath)).\n\t\tWithField(placeholders.TFCmdArgsKeyName, args.Slice()).\n\t\tWithField(placeholders.TFCmdKeyName, args.CommandName())\n\n\tif runOpts.JSONLogFormat && !args.Normalize(clihelper.SingleDashFlag).Contains(FlagNameJSON) {\n\t\twrappedOut := buildOutWriter(\n\t\t\tlogger,\n\t\t\trunOpts.ShellOptions.Headless,\n\t\t\toutWriter,\n\t\t\terrWriter,\n\t\t)\n\t\twrappedErr := buildErrWriter(\n\t\t\tlogger,\n\t\t\trunOpts.ShellOptions.Headless,\n\t\t\terrWriter,\n\t\t)\n\n\t\toutWriter = writer.NewWrappedWriter(wrappedOut, originalOutWriter)\n\t\terrWriter = writer.NewWrappedWriter(wrappedErr, originalErrWriter)\n\t} else if !shouldForceForwardTFStdout(args) {\n\t\twrappedOut := buildOutWriter(\n\t\t\tlogger,\n\t\t\trunOpts.ShellOptions.Headless,\n\t\t\toutWriter,\n\t\t\terrWriter,\n\t\t\tlogwriter.WithMsgSeparator(logMsgSeparator),\n\t\t)\n\t\twrappedErr := buildErrWriter(\n\t\t\tlogger,\n\t\t\trunOpts.ShellOptions.Headless,\n\t\t\terrWriter,\n\t\t\tlogwriter.WithMsgSeparator(logMsgSeparator),\n\t\t\tlogwriter.WithParseFunc(ParseLogFunc(tfLogMsgPrefix, false)),\n\t\t)\n\n\t\toutWriter = writer.NewWrappedWriter(wrappedOut, originalOutWriter)\n\t\terrWriter = writer.NewWrappedWriter(wrappedErr, originalErrWriter)\n\t}\n\n\treturn outWriter, errWriter\n}\n\n// isCommandThatNeedsPty returns true if the sub command of terraform we are running requires a pty.\nfunc isCommandThatNeedsPty(args []string) (bool, error) {\n\tif len(args) == 0 || !slices.Contains(commandsThatNeedPty, args[0]) {\n\t\treturn false, nil\n\t}\n\n\tfi, err := os.Stdin.Stat()\n\tif err != nil {\n\t\treturn false, errors.New(err)\n\t}\n\n\t// if there is data in the stdin, then the terraform console is used in non-interactive mode, for example `echo \"1 + 5\" | terragrunt console`.\n\tif fi.Size() > 0 {\n\t\treturn false, nil\n\t}\n\n\t// if the stdin is not a terminal, then the terraform console is used in non-interactive mode\n\tif !isatty.IsTerminal(os.Stdin.Fd()) {\n\t\treturn false, nil\n\t}\n\n\treturn true, nil\n}\n\n// shouldForceForwardTFStdout returns true if at least one of the conditions is met, args contains the `-json` flag or the `output` or `state` command.\nfunc shouldForceForwardTFStdout(args clihelper.Args) bool {\n\ttfCommands := []string{\n\t\tCommandNameOutput,\n\t\tCommandNameState,\n\t\tCommandNameVersion,\n\t\tCommandNameConsole,\n\t\tCommandNameGraph,\n\t}\n\n\ttfFlags := []string{\n\t\tFlagNameJSON,\n\t\tFlagNameVersion,\n\t\tFlagNameHelpLong,\n\t\tFlagNameHelpShort,\n\t}\n\n\tif slices.ContainsFunc(tfFlags, args.Normalize(clihelper.SingleDashFlag).Contains) {\n\t\treturn true\n\t}\n\n\treturn collections.ListContainsElement(tfCommands, args.CommandName())\n}\n\n// buildOutWriter returns the writer for the command's stdout.\n//\n// When Terragrunt is running in Headless mode, we want to forward\n// any stdout to the INFO log level, otherwise, we want to forward\n// stdout to the STDOUT log level.\n//\n// Also accepts any additional writer options desired.\nfunc buildOutWriter(l log.Logger, headless bool, outWriter, errWriter io.Writer, writerOptions ...logwriter.Option) io.Writer {\n\tlogLevel := log.StdoutLevel\n\n\tif headless {\n\t\tlogLevel = log.InfoLevel\n\t\toutWriter = errWriter\n\t}\n\n\topts := make([]logwriter.Option, 0, defaultWriterOptionsLen+len(writerOptions))\n\topts = append(opts,\n\t\tlogwriter.WithLogger(l.WithOptions(log.WithOutput(outWriter))),\n\t\tlogwriter.WithDefaultLevel(logLevel),\n\t)\n\topts = append(opts, writerOptions...)\n\n\treturn logwriter.New(opts...)\n}\n\n// buildErrWriter returns the writer for the command's stderr.\n//\n// When Terragrunt is running in Headless mode, we want to forward\n// any stderr to the ERROR log level, otherwise, we want to forward\n// stderr to the STDERR log level.\n//\n// Also accepts any additional writer options desired.\nfunc buildErrWriter(l log.Logger, headless bool, errWriter io.Writer, writerOptions ...logwriter.Option) io.Writer {\n\tlogLevel := log.StderrLevel\n\n\tif headless {\n\t\tlogLevel = log.ErrorLevel\n\t}\n\n\topts := make([]logwriter.Option, 0, defaultWriterOptionsLen+len(writerOptions))\n\topts = append(opts,\n\t\tlogwriter.WithLogger(l.WithOptions(log.WithOutput(errWriter))),\n\t\tlogwriter.WithDefaultLevel(logLevel),\n\t)\n\topts = append(opts, writerOptions...)\n\n\treturn logwriter.New(opts...)\n}\n"
  },
  {
    "path": "internal/tf/run_cmd_test.go",
    "content": "//go:build linux || darwin\n// +build linux darwin\n\npackage tf_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar (\n\tFullOutput = []string{\"stdout1\", \"stderr1\", \"stdout2\", \"stderr2\", \"stderr3\"}\n\tStdout     = []string{\"stdout1\", \"stdout2\"}\n\tStderr     = []string{\"stderr1\", \"stderr2\", \"stderr3\"}\n)\n\nfunc TestCommandOutputPrefix(t *testing.T) {\n\tt.Parallel()\n\n\tprefix := \".\"\n\tterraformPath := \"testdata/test_outputs.sh\"\n\n\tprefixedOutput := make([]string, 0, len(FullOutput))\n\tfor _, line := range FullOutput {\n\t\tprefixedOutput = append(prefixedOutput, fmt.Sprintf(\"prefix=%s tf-path=%s msg=%s\", prefix, filepath.Base(terraformPath), line))\n\t}\n\n\tlogFormatter := format.NewFormatter(format.NewKeyValueFormatPlaceholders())\n\n\ttestCommandOutput(t, func(terragruntOptions *options.TerragruntOptions) {\n\t\tterragruntOptions.TFPath = terraformPath\n\t}, func(l log.Logger) log.Logger {\n\t\tl.SetOptions(log.WithFormatter(logFormatter))\n\t\treturn l.WithField(placeholders.WorkDirKeyName, prefix)\n\t}, assertOutputs(t,\n\t\tprefixedOutput,\n\t\tStdout,\n\t\tStderr,\n\t))\n}\n\nfunc testCommandOutput(t *testing.T, withOptions func(*options.TerragruntOptions), withLogger func(log.Logger) log.Logger, assertResults func(string, *util.CmdOutput)) {\n\tt.Helper()\n\n\tterragruntOptions, err := options.NewTerragruntOptionsForTest(\"\")\n\trequire.NoError(t, err, \"Unexpected error creating NewTerragruntOptionsForTest: %v\", err)\n\n\t// Specify a single (locking) buffer for both as a way to check that the output is being written in the correct\n\t// order\n\tvar allOutputBuffer BufferWithLocking\n\n\tterragruntOptions.Writers.Writer = &allOutputBuffer\n\tterragruntOptions.Writers.ErrWriter = &allOutputBuffer\n\n\tterragruntOptions.TerraformCliArgs.AppendArgument(\"same\")\n\tterragruntOptions.TFPath = \"testdata/test_outputs.sh\"\n\n\twithOptions(terragruntOptions)\n\n\tl := logger.CreateLogger()\n\tl = withLogger(l)\n\n\tout, err := tf.RunCommandWithOutput(t.Context(), l, configbridge.TFRunOptsFromOpts(terragruntOptions), \"same\")\n\n\tassert.NotNil(t, out, \"Should get output\")\n\trequire.NoError(t, err, \"Should have no error\")\n\n\tassert.NotNil(t, out, \"Should get output\")\n\tassertResults(allOutputBuffer.String(), out)\n}\n\nfunc assertOutputs(\n\tt *testing.T,\n\texpectedAllOutputs []string,\n\texpectedStdOutputs []string,\n\texpectedStdErrs []string,\n) func(string, *util.CmdOutput) {\n\tt.Helper()\n\n\treturn func(allOutput string, out *util.CmdOutput) {\n\t\tallOutputs := strings.Split(strings.TrimSpace(allOutput), \"\\n\")\n\t\tassert.Len(t, allOutputs, len(expectedAllOutputs))\n\n\t\tfor i := range allOutputs {\n\t\t\tassert.Contains(t, allOutputs[i], expectedAllOutputs[i], allOutputs[i])\n\t\t}\n\n\t\tstdOutputs := strings.Split(strings.TrimSpace(out.Stdout.String()), \"\\n\")\n\t\tassert.Equal(t, expectedStdOutputs, stdOutputs)\n\n\t\tstdErrs := strings.Split(strings.TrimSpace(out.Stderr.String()), \"\\n\")\n\t\tassert.Equal(t, expectedStdErrs, stdErrs)\n\t}\n}\n\n// A goroutine-safe bytes.Buffer\ntype BufferWithLocking struct {\n\tbuffer bytes.Buffer\n\tmutex  sync.Mutex\n}\n\n// Write appends the contents of p to the buffer, growing the buffer as needed. It returns\n// the number of bytes written.\nfunc (s *BufferWithLocking) Write(p []byte) (n int, err error) {\n\ts.mutex.Lock()\n\tdefer s.mutex.Unlock()\n\n\treturn s.buffer.Write(p)\n}\n\n// String returns the contents of the unread portion of the buffer\n// as a string.  If the Buffer is a nil pointer, it returns \"<nil>\".\nfunc (s *BufferWithLocking) String() string {\n\ts.mutex.Lock()\n\tdefer s.mutex.Unlock()\n\n\treturn s.buffer.String()\n}\n"
  },
  {
    "path": "internal/tf/source.go",
    "content": "package tf\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/hashicorp/go-getter\"\n\turlhelper \"github.com/hashicorp/go-getter/helper/url\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n)\n\nvar (\n\tforcedRegexp     = regexp.MustCompile(`^([A-Za-z0-9]+)::(.+)$`)\n\thttpSchemeRegexp = regexp.MustCompile(`(?i)^https?://`)\n)\n\nconst matchCount = 2\n\n// Source represents information about Terraform source code that needs to be downloaded.\ntype Source struct {\n\t// A canonical version of RawSource, in URL format\n\tCanonicalSourceURL *url.URL\n\n\t// The folder where we should download the source to\n\tDownloadDir string\n\n\t// The folder in DownloadDir that should be used as the working directory for Terraform\n\tWorkingDir string\n\n\t// The path to a file in DownloadDir that stores the version number of the code\n\tVersionFile string\n\n\t// WalkDirWithSymlinks controls whether to walk symlinks in the downloaded source\n\tWalkDirWithSymlinks bool\n}\n\nfunc (src Source) String() string {\n\treturn fmt.Sprintf(\"Source{CanonicalSourceURL = %v, DownloadDir = %v, WorkingDir = %v, VersionFile = %v}\", src.CanonicalSourceURL, src.DownloadDir, src.WorkingDir, src.VersionFile)\n}\n\n// EncodeSourceVersion encodes a version number for the given source. When calculating a version number, we take the query\n// string of the source URL, calculate its sha1, and base 64 encode it. For remote URLs (e.g. Git URLs), this is\n// based on the assumption that the scheme/host/path of the URL (e.g. git::github.com/foo/bar) identifies the module\n// name and the query string (e.g. ?ref=v0.0.3) identifies the version. For local file paths, there is no query string,\n// so the same file path (/foo/bar) is always considered the same version. To detect changes the file path will be hashed\n// and returned as version. In case of hash error the default encoded source version will be returned.\n// See also the encodeSourceName and ProcessTerraformSource methods.\nfunc (src Source) EncodeSourceVersion(l log.Logger) (string, error) {\n\tif IsLocalSource(src.CanonicalSourceURL) {\n\t\tsourceHash := sha256.New()\n\t\tsourceDir := filepath.Clean(src.CanonicalSourceURL.Path)\n\n\t\tvar err error\n\n\t\twalkDir := filepath.WalkDir\n\t\tif src.WalkDirWithSymlinks {\n\t\t\twalkDir = util.WalkDirWithSymlinks\n\t\t}\n\n\t\terr = walkDir(sourceDir, func(path string, d fs.DirEntry, err error) error {\n\t\t\tif err != nil {\n\t\t\t\t// If we've encountered an error while walking the tree, give up\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif d.IsDir() {\n\t\t\t\t// We don't use any info from directories to calculate our hash\n\t\t\t\treturn util.SkipDirIfIgnorable(d.Name())\n\t\t\t}\n\t\t\t// avoid checking .terraform.lock.hcl file since contents is auto-generated\n\t\t\tif d.Name() == util.TerraformLockFile {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tinfo, err := d.Info()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfileModified := info.ModTime().UnixMicro()\n\n\t\t\thashContents := fmt.Sprintf(\"%s:%d\", path, fileModified)\n\t\t\tsourceHash.Write([]byte(hashContents))\n\n\t\t\treturn nil\n\t\t})\n\t\tif err == nil {\n\t\t\thash := hex.EncodeToString(sourceHash.Sum(nil))\n\n\t\t\treturn hash, nil\n\t\t}\n\n\t\tl.WithError(err).Warnf(\"Could not encode version for local source\")\n\n\t\treturn \"\", err\n\t}\n\n\treturn util.EncodeBase64Sha1(src.CanonicalSourceURL.Query().Encode()), nil\n}\n\n// WriteVersionFile writes a file into the DownloadDir that contains\n// the version number of this source code. The version number is\n// calculated using the EncodeSourceVersion method.\nfunc (src Source) WriteVersionFile(l log.Logger) error {\n\tversion, err := src.EncodeSourceVersion(l)\n\tif err != nil {\n\t\t// If we failed to calculate a SHA of the downloaded source, write a SHA of\n\t\t// some random data into the version file.\n\t\t//\n\t\t// This ensures we attempt to redownload the source next time.\n\t\tversion, err = util.GenerateRandomSha256()\n\t\tif err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\t}\n\n\tconst ownerReadWriteGroupReadPerms = 0640\n\n\treturn errors.New(os.WriteFile(src.VersionFile, []byte(version), ownerReadWriteGroupReadPerms))\n}\n\n// NewSource takes the given source path and create a Source struct from it, including the folder where the source should\n// be downloaded to. Our goal is to reuse the download folder for the same source URL between Terragrunt runs.\n// Otherwise, for every Terragrunt command, you'd have to wait for Terragrunt to download your Terraform code, download\n// that code's dependencies (terraform get), and configure remote state (terraform remote config), which is very slow.\n//\n// To maximize reuse, given a working directory w and a source URL s, we download code from S into the folder /T/W/H\n// where:\n//\n//  1. S is the part of s before the double-slash (//). This typically represents the root of the repo (e.g.\n//     github.com/foo/infrastructure-modules). We download the entire repo so that relative paths to other files in that\n//     repo resolve correctly. If no double-slash is specified, all of s is used.\n//  1. T is the OS temp dir (e.g. /tmp).\n//  2. W is the base 64 encoded sha1 hash of w. This ensures that if you are running Terragrunt concurrently in\n//     multiple folders (e.g. during automated tests), then even if those folders are using the same source URL s, they\n//     do not overwrite each other.\n//  3. H is the base 64 encoded sha1 of S without its query string. For remote source URLs (e.g. Git\n//     URLs), this is based on the assumption that the scheme/host/path of the URL (e.g. git::github.com/foo/bar)\n//     identifies the repo, and we always want to download the same repo into the same folder (see the encodeSourceName\n//     method). We also assume the version of the module is stored in the query string (e.g. ref=v0.0.3), so we store\n//     the base 64 encoded sha1 of the query string in a file called .terragrunt-source-version within /T/W/H.\n//\n// The downloadTerraformSourceIfNecessary decides when we should download the Terraform code and when not to. It uses\n// the following rules:\n//\n//  1. Always download source URLs pointing to local file paths.\n//  2. Only download source URLs pointing to remote paths if /T/W/H doesn't already exist or, if it does exist, if the\n//     version number in /T/W/H/.terragrunt-source-version doesn't match the current version.\nfunc NewSource(l log.Logger, source string, downloadDir string, workingDir string, walkDirWithSymlinks bool) (*Source, error) {\n\tcanonicalWorkingDir := filepath.Clean(workingDir)\n\n\tcanonicalSourceURL, err := ToSourceURL(source, canonicalWorkingDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trootSourceURL, modulePath, err := SplitSourceURL(l, canonicalSourceURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif IsLocalSource(rootSourceURL) {\n\t\t// Always use canonical file paths for local source folders, rather than relative paths, to ensure\n\t\t// that the same local folder always maps to the same download folder, no matter how the local folder\n\t\t// path is specified\n\t\trootSourceURL.Path = filepath.ToSlash(filepath.Clean(rootSourceURL.Path))\n\t}\n\n\trootPath, err := encodeSourceName(rootSourceURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tencodedWorkingDir := util.EncodeBase64Sha1(canonicalWorkingDir)\n\tupdatedDownloadDir := filepath.Join(downloadDir, encodedWorkingDir, rootPath)\n\tupdatedWorkingDir := filepath.Join(updatedDownloadDir, modulePath)\n\tversionFile := filepath.Join(updatedDownloadDir, \".terragrunt-source-version\")\n\n\treturn &Source{\n\t\tCanonicalSourceURL:  rootSourceURL,\n\t\tDownloadDir:         updatedDownloadDir,\n\t\tWorkingDir:          updatedWorkingDir,\n\t\tVersionFile:         versionFile,\n\t\tWalkDirWithSymlinks: walkDirWithSymlinks,\n\t}, nil\n}\n\n// ToSourceURL converts the given source into a URL struct.\n// This method should be able to handle all source URLs that the terraform\n// init command can handle, parsing local file paths, Git paths, and HTTP URLs correctly.\nfunc ToSourceURL(source string, workingDir string) (*url.URL, error) {\n\tsource, err := normalizeSourceURL(source, workingDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// The go-getter library is what Terraform's init command uses to download source URLs. Use that library to\n\t// parse the URL.\n\trawSourceURLWithGetter, err := getter.Detect(source, workingDir, getter.Detectors)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\treturn parseSourceURL(rawSourceURLWithGetter)\n}\n\n// We have to remove the http(s) scheme from the source URL to allow `getter.Detect` to add the source type, but only if the `getter` has a detector for that host.\nfunc normalizeSourceURL(source string, workingDir string) (string, error) {\n\tnewSource := httpSchemeRegexp.ReplaceAllString(source, \"\")\n\n\t// We can't use `the getter.Detectors` global variable because we need to exclude from checking:\n\t// * `getter.FileDetector` is not a host detector\n\t// * `getter.S3Detector` we should not remove `https` from s3 link since this is a public link, and if we remove `https` scheme, `getter.S3Detector` adds `s3::https` which in turn requires credentials.\n\tdetectors := []getter.Detector{\n\t\tnew(getter.GitHubDetector),\n\t\tnew(getter.GitLabDetector),\n\t\tnew(getter.GitDetector),\n\t\tnew(getter.BitBucketDetector),\n\t\tnew(getter.GCSDetector),\n\t}\n\n\tfor _, detector := range detectors {\n\t\t_, ok, err := detector.Detect(newSource, workingDir)\n\t\tif err != nil {\n\t\t\treturn source, errors.New(err)\n\t\t}\n\n\t\tif ok {\n\t\t\treturn newSource, nil\n\t\t}\n\t}\n\n\treturn source, nil\n}\n\n// Parse the given source URL into a URL struct. This method can handle source URLs that include go-getter's \"forced\n// getter\" prefixes, such as git::.\nfunc parseSourceURL(source string) (*url.URL, error) {\n\tforcedGetters := []string{}\n\t// Continuously strip the forced getters until there is no more. This is to handle complex URL schemes like the\n\t// git-remote-codecommit style URL.\n\tforcedGetter, rawSourceURL := getForcedGetter(source)\n\tfor forcedGetter != \"\" {\n\t\t// Prepend like a stack, so that we prepend to the URL scheme in the right order.\n\t\tforcedGetters = append([]string{forcedGetter}, forcedGetters...)\n\t\tforcedGetter, rawSourceURL = getForcedGetter(rawSourceURL)\n\t}\n\n\t// Parse the URL without the getter prefix\n\tcanonicalSourceURL, err := urlhelper.Parse(rawSourceURL)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\t// Reattach the \"getter\" prefix as part of the scheme\n\tfor _, forcedGetter := range forcedGetters {\n\t\tcanonicalSourceURL.Scheme = fmt.Sprintf(\"%s::%s\", forcedGetter, canonicalSourceURL.Scheme)\n\t}\n\n\treturn canonicalSourceURL, nil\n}\n\n// IsLocalSource returns true if the given URL refers to a path on the local file system\nfunc IsLocalSource(sourceURL *url.URL) bool {\n\treturn sourceURL.Scheme == \"file\"\n}\n\n// SplitSourceURL splits a source URL into the root repo and the path. The root repo is the part of the URL before the double-slash\n// (//), which typically represents the root of a modules repo (e.g. github.com/foo/infrastructure-modules) and the\n// path is everything after the double slash. If there is no double-slash in the URL, the root repo is the entire\n// sourceUrl and the path is an empty string.\nfunc SplitSourceURL(l log.Logger, sourceURL *url.URL) (*url.URL, string, error) {\n\tpathSplitOnDoubleSlash := strings.SplitN(sourceURL.Path, \"//\", 2) //nolint:mnd\n\n\tif len(pathSplitOnDoubleSlash) > 1 {\n\t\tsourceURLModifiedPath, err := parseSourceURL(sourceURL.String())\n\t\tif err != nil {\n\t\t\treturn nil, \"\", errors.New(err)\n\t\t}\n\n\t\tsourceURLModifiedPath.Path = pathSplitOnDoubleSlash[0]\n\n\t\treturn sourceURLModifiedPath, pathSplitOnDoubleSlash[1], nil\n\t}\n\t// check if path is remote URL\n\tif sourceURL.Scheme != \"\" {\n\t\treturn sourceURL, \"\", nil\n\t}\n\t// check if sourceUrl.Path is a local file path\n\t_, err := os.Stat(sourceURL.Path)\n\tif err != nil {\n\t\t// log warning message to notify user that sourceUrl.Path may not work\n\t\tl.Warnf(\"No double-slash (//) found in source URL %s. Relative paths in downloaded Terraform code may not work.\", sourceURL.Path)\n\t}\n\n\treturn sourceURL, \"\", nil\n}\n\n// Encode a the module name for the given source URL. When calculating a module name, we calculate the base 64 encoded\n// sha1 of the entire source URL without the query string. For remote URLs (e.g. Git URLs), this is based on the\n// assumption that the scheme/host/path of the URL (e.g. git::github.com/foo/bar) identifies the module name and the\n// query string (e.g. ?ref=v0.0.3) identifies the version. For local file paths, there is no query string, so the same\n// file path (/foo/bar) is always considered the same version. See also the EncodeSourceVersion and\n// ProcessTerraformSource methods.\nfunc encodeSourceName(sourceURL *url.URL) (string, error) {\n\tsourceURLNoQuery, err := parseSourceURL(sourceURL.String())\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\tsourceURLNoQuery.RawQuery = \"\"\n\n\treturn util.EncodeBase64Sha1(sourceURLNoQuery.String()), nil\n}\n\n// Terraform source URLs can contain a \"getter\" prefix that specifies the type of protocol to use to download that URL,\n// such as \"git::\", which means Git should be used to download the URL. This method returns the getter prefix and the\n// rest of the URL. This code is copied from the getForcedGetter method of go-getter/get.go, as that method is not\n// exported publicly.\nfunc getForcedGetter(sourceURL string) (string, string) {\n\tif matches := forcedRegexp.FindStringSubmatch(sourceURL); len(matches) > matchCount {\n\t\treturn matches[1], matches[2]\n\t}\n\n\treturn \"\", sourceURL\n}\n"
  },
  {
    "path": "internal/tf/source_test.go",
    "content": "package tf_test\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n)\n\nfunc TestSplitSourceUrl(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname               string\n\t\tsourceURL          string\n\t\texpectedSo         string\n\t\texpectedModulePath string\n\t}{\n\t\t{\"root-path-only-no-double-slash\", \"/foo\", \"/foo\", \"\"},\n\t\t{\"parent-path-one-child-no-double-slash\", \"/foo/bar\", \"/foo/bar\", \"\"},\n\t\t{\"parent-path-multiple-children-no-double-slash\", \"/foo/bar/baz/blah\", \"/foo/bar/baz/blah\", \"\"},\n\t\t{\"relative-path-no-children-no-double-slash\", \"../foo\", \"../foo\", \"\"},\n\t\t{\"relative-path-one-child-no-double-slash\", \"../foo/bar\", \"../foo/bar\", \"\"},\n\t\t{\"relative-path-multiple-children-no-double-slash\", \"../foo/bar/baz/blah\", \"../foo/bar/baz/blah\", \"\"},\n\t\t{\"root-path-only-with-double-slash\", \"/foo//\", \"/foo\", \"\"},\n\t\t{\"parent-path-one-child-with-double-slash\", \"/foo//bar\", \"/foo\", \"bar\"},\n\t\t{\"parent-path-multiple-children-with-double-slash\", \"/foo/bar//baz/blah\", \"/foo/bar\", \"baz/blah\"},\n\t\t{\"relative-path-no-children-with-double-slash\", \"..//foo\", \"..\", \"foo\"},\n\t\t{\"relative-path-one-child-with-double-slash\", \"../foo//bar\", \"../foo\", \"bar\"},\n\t\t{\"relative-path-multiple-children-with-double-slash\", \"../foo/bar//baz/blah\", \"../foo/bar\", \"baz/blah\"},\n\t\t{\"parent-url-one-child-no-double-slash\", \"ssh://git@github.com/foo/modules.git/foo\", \"ssh://git@github.com/foo/modules.git/foo\", \"\"},\n\t\t{\"parent-url-multiple-children-no-double-slash\", \"ssh://git@github.com/foo/modules.git/foo/bar/baz/blah\", \"ssh://git@github.com/foo/modules.git/foo/bar/baz/blah\", \"\"},\n\t\t{\"parent-url-one-child-with-double-slash\", \"ssh://git@github.com/foo/modules.git//foo\", \"ssh://git@github.com/foo/modules.git\", \"foo\"},\n\t\t{\"parent-url-multiple-children-with-double-slash\", \"ssh://git@github.com/foo/modules.git//foo/bar/baz/blah\", \"ssh://git@github.com/foo/modules.git\", \"foo/bar/baz/blah\"},\n\t\t{\"separate-ref-with-slash\", \"ssh://git@github.com/foo/modules.git//foo?ref=feature/modules\", \"ssh://git@github.com/foo/modules.git?ref=feature/modules\", \"foo\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tsourceURL, err := url.Parse(tc.sourceURL)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tl := logger.CreateLogger()\n\n\t\t\tactualRootRepo, actualModulePath, err := tf.SplitSourceURL(l, sourceURL)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.expectedSo, actualRootRepo.String())\n\t\t\tassert.Equal(t, tc.expectedModulePath, actualModulePath)\n\t\t})\n\t}\n}\n\nfunc TestToSourceUrl(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tsourceURL         string\n\t\texpectedSourceURL string\n\t}{\n\t\t{\"https://github.com/gruntwork-io/repo-name\", \"git::https://github.com/gruntwork-io/repo-name.git\"},\n\t\t{\"git::https://github.com/gruntwork-io/repo-name\", \"git::https://github.com/gruntwork-io/repo-name\"},\n\t\t{\"https://github.com/gruntwork-io/repo-name//modules/module-name\", \"git::https://github.com/gruntwork-io/repo-name.git//modules/module-name\"},\n\t\t{\"ssh://github.com/gruntwork-io/repo-name//modules/module-name\", \"ssh://github.com/gruntwork-io/repo-name//modules/module-name\"},\n\t\t{\"https://gitlab.com/catamphetamine/libphonenumber-js\", \"git::https://gitlab.com/catamphetamine/libphonenumber-js.git\"},\n\t\t{\"https://bitbucket.org/atlassian/aws-ecr-push-image\", \"git::https://bitbucket.org/atlassian/aws-ecr-push-image.git\"},\n\t\t{\"http://bitbucket.org/atlassian/aws-ecr-push-image\", \"git::https://bitbucket.org/atlassian/aws-ecr-push-image.git\"},\n\t\t{\"https://s3-eu-west-1.amazonaws.com/modules/vpc.zip\", \"https://s3-eu-west-1.amazonaws.com/modules/vpc.zip\"},\n\t\t{\"https://www.googleapis.com/storage/v1/modules/foomodule.zip\", \"gcs::https://www.googleapis.com/storage/v1/modules/foomodule.zip\"},\n\t\t{\"https://www.googleapis.com/storage/v1/modules/foomodule.zip\", \"gcs::https://www.googleapis.com/storage/v1/modules/foomodule.zip\"},\n\t\t{\"git::https://name@dev.azure.com/name/project-name/_git/repo-name\", \"git::https://name@dev.azure.com/name/project-name/_git/repo-name\"},\n\t\t{\"https://repository.rnd.net/artifactory/generic-production-iac/tf-auto-azr-iam.2.6.0.zip\", \"https://repository.rnd.net/artifactory/generic-production-iac/tf-auto-azr-iam.2.6.0.zip\"},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactualSourceURL, err := tf.ToSourceURL(tc.sourceURL, os.TempDir())\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expectedSourceURL, actualSourceURL.String())\n\t\t})\n\t}\n}\n\nfunc TestRegressionSupportForGitRemoteCodecommit(t *testing.T) {\n\tt.Parallel()\n\n\tsource := \"git::codecommit::ap-northeast-1://my_app_modules//my-app/modules/main-module\"\n\tsourceURL, err := tf.ToSourceURL(source, \".\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"git::codecommit::ap-northeast-1\", sourceURL.Scheme)\n\n\tl := logger.CreateLogger()\n\n\tactualRootRepo, actualModulePath, err := tf.SplitSourceURL(l, sourceURL)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, \"git::codecommit::ap-northeast-1://my_app_modules\", actualRootRepo.String())\n\trequire.Equal(t, \"my-app/modules/main-module\", actualModulePath)\n}\n"
  },
  {
    "path": "internal/tf/testdata/test_outputs.sh",
    "content": "#!/usr/bin/env bash\necho 'stdout1'\nsleep 1\n>&2 echo 'stderr1'\nsleep 1\necho 'stdout2'\nsleep 1\n>&2 echo 'stderr2'\nsleep 1\n>&2 echo 'stderr3'\n"
  },
  {
    "path": "internal/tf/tf.go",
    "content": "package tf\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/hashicorp/hcl/v2/hclparse\"\n)\n\nconst (\n\t// TF commands.\n\n\tCommandNameInit           = \"init\"\n\tCommandNameInitFromModule = \"init-from-module\"\n\tCommandNameImport         = \"import\"\n\tCommandNamePlan           = \"plan\"\n\tCommandNameApply          = \"apply\"\n\tCommandNameDestroy        = \"destroy\"\n\tCommandNameValidate       = \"validate\"\n\tCommandNameOutput         = \"output\"\n\tCommandNameProviders      = \"providers\"\n\tCommandNameState          = \"state\"\n\tCommandNameLock           = \"lock\"\n\tCommandNameGet            = \"get\"\n\tCommandNameGraph          = \"graph\"\n\tCommandNameTaint          = \"taint\"\n\tCommandNameUntaint        = \"untaint\"\n\tCommandNameConsole        = \"console\"\n\tCommandNameForceUnlock    = \"force-unlock\"\n\tCommandNameShow           = \"show\"\n\tCommandNameVersion        = \"version\"\n\tCommandNameFmt            = \"fmt\"\n\tCommandNameLogin          = \"login\"\n\tCommandNameLogout         = \"logout\"\n\tCommandNameMetadate       = \"metadata\"\n\tCommandNamePull           = \"pull\"\n\tCommandNamePush           = \"push\"\n\tCommandNameRefresh        = \"refresh\"\n\tCommandNameTest           = \"test\"\n\tCommandNameWorkspace      = \"workspace\"\n\tCommandNameQuery          = \"query\"\n\n\t// Deprecated TF commands.\n\n\tCommandNameEnv = \"env\"\n\n\t// TF flags.\n\n\tFlagNameDetailedExitCode = \"-detailed-exitcode\"\n\tFlagNameHelpLong         = \"-help\"\n\tFlagNameHelpShort        = \"-h\"\n\tFlagNameVersion          = \"-version\"\n\tFlagNameJSON             = \"-json\"\n\tFlagNameNoColor          = \"-no-color\"\n\t// `apply -destroy` is alias for `destroy`\n\tFlagNameDestroy = \"-destroy\"\n\n\t// `platform` is a flag used with the `providers lock` command.\n\tFlagNamePlatform = \"-platform\"\n\n\tEnvNameTFCLIConfigFile  = \"TF_CLI_CONFIG_FILE\"\n\tEnvNameTFPluginCacheDir = \"TF_PLUGIN_CACHE_DIR\"\n\tEnvNameTFTokenFmt       = \"TF_TOKEN_%s\"\n\tEnvNameTFVarFmt         = \"TF_VAR_%s\"\n\n\tDefaultTFDataDir  = \".terraform\"\n\tTerraformLockFile = \".terraform.lock.hcl\"\n\n\tTerraformPlanFile     = \"tfplan.tfplan\"\n\tTerraformPlanJSONFile = \"tfplan.json\"\n)\n\nvar (\n\tCommandNames = []string{\n\t\tCommandNameApply,\n\t\tCommandNameConsole,\n\t\tCommandNameDestroy,\n\t\tCommandNameEnv,\n\t\tCommandNameFmt,\n\t\tCommandNameGet,\n\t\tCommandNameGraph,\n\t\tCommandNameImport,\n\t\tCommandNameInit,\n\t\tCommandNameLogin,\n\t\tCommandNameLogout,\n\t\tCommandNameMetadate,\n\t\tCommandNameOutput,\n\t\tCommandNamePlan,\n\t\tCommandNameProviders,\n\t\tCommandNamePush,\n\t\tCommandNameRefresh,\n\t\tCommandNameShow,\n\t\tCommandNameTaint,\n\t\tCommandNameTest,\n\t\tCommandNameVersion,\n\t\tCommandNameValidate,\n\t\tCommandNameUntaint,\n\t\tCommandNameWorkspace,\n\t\tCommandNameForceUnlock,\n\t\tCommandNameState,\n\t\tCommandNameQuery,\n\t}\n\n\tCommandUsages = map[string]string{\n\t\tCommandNameApply:       \"Create or update infrastructure.\",\n\t\tCommandNameConsole:     \"Try OpenTofu/Terraform expressions at an interactive command prompt.\",\n\t\tCommandNameDestroy:     \"Destroy previously-created infrastructure.\",\n\t\tCommandNameFmt:         \"Reformat your configuration in the standard style.\",\n\t\tCommandNameGet:         \"Install or upgrade remote OpenTofu/Terraform modules.\",\n\t\tCommandNameGraph:       \"Generate a Graphviz graph of the steps in an operation.\",\n\t\tCommandNameImport:      \"Associate existing infrastructure with a OpenTofu/Terraform resource.\",\n\t\tCommandNameInit:        \"Prepare your working directory for other commands.\",\n\t\tCommandNameLogin:       \"Obtain and save credentials for a remote host.\",\n\t\tCommandNameLogout:      \"Remove locally-stored credentials for a remote host.\",\n\t\tCommandNameMetadate:    \"Metadata related commands.\",\n\t\tCommandNameOutput:      \"Show output values from your root module.\",\n\t\tCommandNamePlan:        \"Show changes required by the current configuration.\",\n\t\tCommandNameProviders:   \"Show the providers required for this configuration.\",\n\t\tCommandNameRefresh:     \"Update the state to match remote systems.\",\n\t\tCommandNameShow:        \"Show the current state or a saved plan.\",\n\t\tCommandNameTaint:       \"Mark a resource instance as not fully functional.\",\n\t\tCommandNameTest:        \"Execute integration tests for OpenTofu/Terraform modules.\",\n\t\tCommandNameVersion:     \"Show the current OpenTofu/Terraform version.\",\n\t\tCommandNameValidate:    \"Check whether the configuration is valid.\",\n\t\tCommandNameUntaint:     \"Remove the 'tainted' state from a resource instance.\",\n\t\tCommandNameWorkspace:   \"Workspace management.\",\n\t\tCommandNameForceUnlock: \"Release a stuck lock on the current workspace.\",\n\t\tCommandNameState:       \"Advanced state management.\",\n\t}\n)\n\n// ModuleVariables will return all the variables defined in the downloaded terraform modules, taking into\n// account all the generated sources. This function will return the required and optional variables separately.\nfunc ModuleVariables(modulePath string) ([]string, []string, error) {\n\tparser := hclparse.NewParser()\n\n\tfiles, err := os.ReadDir(modulePath)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\thclFiles := []*hcl.File{}\n\tallDiags := hcl.Diagnostics{}\n\n\tfor _, file := range files {\n\t\tif file.IsDir() {\n\t\t\tcontinue\n\t\t}\n\n\t\tparseFunc := parser.ParseHCLFile\n\n\t\tsuffix := filepath.Ext(file.Name())\n\n\t\tif suffix == \".json\" {\n\t\t\tparseFunc = parser.ParseJSONFile\n\t\t}\n\n\t\tif !(slices.Contains([]string{\".tf\", \".tofu\", \".json\"}, suffix)) {\n\t\t\tcontinue\n\t\t}\n\n\t\tfile, parseDiags := parseFunc(filepath.Join(modulePath, file.Name()))\n\n\t\thclFiles = append(hclFiles, file)\n\t\tallDiags = append(allDiags, parseDiags...)\n\t}\n\n\tbody := hcl.MergeFiles(hclFiles)\n\n\tvarsSchema := &hcl.BodySchema{\n\t\tBlocks: []hcl.BlockHeaderSchema{\n\t\t\t{\n\t\t\t\tType:       \"variable\",\n\t\t\t\tLabelNames: []string{\"name\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tvarsAttributesSchema := &hcl.BodySchema{\n\t\tAttributes: []hcl.AttributeSchema{\n\t\t\t{\n\t\t\t\tName:     \"default\",\n\t\t\t\tRequired: false,\n\t\t\t},\n\t\t},\n\t}\n\n\tvarsContent, _, contentDiags := body.PartialContent(varsSchema)\n\tallDiags = append(allDiags, contentDiags...)\n\toptional, required := []string{}, []string{}\n\n\tfor _, b := range varsContent.Blocks {\n\t\tname := b.Labels[0]\n\t\tvarBodyContent, _, attrDiags := b.Body.PartialContent(varsAttributesSchema)\n\n\t\tallDiags = append(allDiags, attrDiags...)\n\t\tif _, ok := varBodyContent.Attributes[\"default\"]; ok {\n\t\t\toptional = append(optional, name)\n\t\t} else {\n\t\t\trequired = append(required, name)\n\t\t}\n\t}\n\n\tif allDiags.HasErrors() {\n\t\treturn nil, nil, errors.New(allDiags)\n\t}\n\n\treturn required, optional, nil\n}\n"
  },
  {
    "path": "internal/tf/tf_test.go",
    "content": "package tf_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestModuleVariablesWithProviderFunctions verifies that ModuleVariables can parse\n// HCL files that use provider function syntax (provider::namespace::function).\n// This is a regression test for https://github.com/gruntwork-io/terragrunt/issues/3425\nfunc TestModuleVariablesWithProviderFunctions(t *testing.T) {\n\tt.Parallel()\n\n\tdir := t.TempDir()\n\n\thclContent := `\nterraform {\n  required_version = \"~> 1.8\"\n  required_providers {\n    assert = {\n      source  = \"hashicorp/assert\"\n      version = \"~> 0.13\"\n    }\n    azurerm = {\n      source  = \"hashicorp/azurerm\"\n      version = \"~> 3.8\"\n    }\n  }\n}\n\ndata \"azurerm_subnet\" \"main\" {\n  name                 = var.subnet.name\n  resource_group_name  = var.subnet.resource_group_name\n  virtual_network_name = var.subnet.virtual_network_name\n\n  lifecycle {\n    postcondition {\n      condition     = var.subnet.enable_ipv6 == false || anytrue([ for prefix in self.address_prefixes: provider::assert::cidrv6(prefix) ])\n      error_message = \"Subnet does not contain valid IPv6 CIDR. Either use a subnet that contains a valid IPv6 CIDR or disable IPv6 support.\"\n    }\n  }\n}\n\nvariable \"subnet\" {\n  type = object({\n    name                 = string\n    virtual_network_name = string\n    resource_group_name  = string\n    enable_ipv6          = optional(bool, false)\n  })\n}\n`\n\trequire.NoError(t, os.WriteFile(filepath.Join(dir, \"main.tf\"), []byte(hclContent), 0644))\n\n\trequired, optional, err := tf.ModuleVariables(dir)\n\trequire.NoError(t, err)\n\tassert.Equal(t, []string{\"subnet\"}, required)\n\tassert.Empty(t, optional)\n}\n"
  },
  {
    "path": "internal/tfimpl/tfimpl.go",
    "content": "// Package tfimpl defines the Terraform implementation type constants.\npackage tfimpl\n\n// Type represents which Terraform implementation is being used.\ntype Type string\n\nconst (\n\t// Terraform indicates the HashiCorp Terraform binary.\n\tTerraform Type = \"terraform\"\n\t// OpenTofu indicates the OpenTofu binary.\n\tOpenTofu Type = \"tofu\"\n\t// Unknown indicates an unrecognized implementation.\n\tUnknown Type = \"unknown\"\n)\n"
  },
  {
    "path": "internal/tflint/README.md",
    "content": "# tflint\n\nThis package allows us to embed [tflint](https://github.com/terraform-linters/tflint) in Terragrunt, enabling it to be natively executed from the before and after hooks without having to install `tflint` separately. Since `tflint` is licensed with MPL, we are required to let you know where you can find its source code: <https://github.com/terraform-linters/tflint>.\n"
  },
  {
    "path": "internal/tflint/tflint.go",
    "content": "// Package tflint embeds execution of tflint, which is under an MPL license, and you can\n// find its source code at https://github.com/terraform-linters/tflint\npackage tflint\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runcfg\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/internal/writer\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n)\n\n// TFLintOptions contains the subset of configuration needed by tflint execution.\ntype TFLintOptions struct {\n\tShellOptions         *shell.ShellOptions\n\tWriters              writer.Writers\n\tWorkingDir           string\n\tRootWorkingDir       string\n\tTerragruntConfigPath string\n\tMaxFoldersToCheck    int\n}\n\nconst (\n\t// tfVarPrefix Prefix to use for terraform variables set with environment variables.\n\ttfVarPrefix      = \"TF_VAR_\"\n\targVarPrefix     = \"-var=\"\n\targVarFilePrefix = \"-var-file=\"\n\ttfExternalTFLint = \"--terragrunt-external-tflint\"\n)\n\n// RunTflintWithOpts runs tflint with the given options and returns an error if there are any issues.\nfunc RunTflintWithOpts(ctx context.Context, l log.Logger, opts *TFLintOptions, cfg *runcfg.RunConfig, hook *runcfg.Hook) error {\n\thookExecute := slices.Clone(hook.Execute)\n\thookExecute = slices.DeleteFunc(hookExecute, func(arg string) bool {\n\t\treturn arg == tfExternalTFLint\n\t})\n\n\t// try to fetch configuration file from hook parameters\n\tconfigFile, err := tflintConfigFilePath(l, opts, hookExecute)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tl.Debugf(\"Using .tflint.hcl file in %s\", util.RelPathForLog(opts.RootWorkingDir, configFile, opts.Writers.LogShowAbsPaths))\n\n\tvariables, err := InputsToTflintVar(cfg.Inputs)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttfVariables, err := tfArgumentsToTflintVar(l, hook, &cfg.Terraform)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tl.Debugf(\n\t\t\"Initializing tflint in directory %s\",\n\t\tutil.RelPathForLog(opts.RootWorkingDir, opts.WorkingDir, opts.Writers.LogShowAbsPaths),\n\t)\n\n\ttflintArgs := hookExecute[1:]\n\n\tconfigFileRel := util.RelPathForLog(opts.WorkingDir, configFile, opts.Writers.LogShowAbsPaths)\n\tchdirRel := util.RelPathForLog(opts.RootWorkingDir, opts.WorkingDir, opts.Writers.LogShowAbsPaths)\n\n\t// tflint init\n\tinitArgs := []string{\"tflint\", \"--init\", \"--config\", configFileRel, \"--chdir\", chdirRel}\n\tl.Debugf(\"Running external tflint init with args %v\", initArgs)\n\n\t_, err = shell.RunCommandWithOutput(ctx, l, opts.ShellOptions, opts.RootWorkingDir, false, false,\n\t\tinitArgs[0], initArgs[1:]...)\n\tif err != nil {\n\t\treturn errors.New(ErrorRunningTflint{Args: initArgs, Err: err})\n\t}\n\n\t// tflint execution\n\targs := make([]string, 0, 5+len(variables)+len(tfVariables)+len(tflintArgs))\n\targs = append(args,\n\t\t\"tflint\",\n\t\t\"--config\", configFileRel,\n\t\t\"--chdir\", chdirRel,\n\t)\n\targs = append(args, variables...)\n\targs = append(args, tfVariables...)\n\targs = append(args, tflintArgs...)\n\n\tl.Debugf(\"Running external tflint with args %v\", args)\n\n\t_, err = shell.RunCommandWithOutput(ctx, l, opts.ShellOptions, opts.RootWorkingDir, false, false,\n\t\targs[0], args[1:]...)\n\tif err != nil {\n\t\treturn errors.New(ErrorRunningTflint{Args: args, Err: err})\n\t}\n\n\tl.Info(\"Tflint has run successfully. No issues found.\")\n\n\treturn nil\n}\n\n// InputsToTflintVar converts the inputs map to a list of tflint variables.\nfunc InputsToTflintVar(inputs map[string]any) ([]string, error) {\n\tvariables := make([]string, 0, len(inputs))\n\n\tfor key, value := range inputs {\n\t\tvarValue, err := util.AsTerraformEnvVarJSONValue(value)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tnewVar := fmt.Sprintf(\"--var=%s=%s\", key, varValue)\n\t\tvariables = append(variables, newVar)\n\t}\n\n\treturn variables, nil\n}\n\n// Custom error types\n\ntype ErrorRunningTflint struct {\n\tErr  error\n\tArgs []string\n}\n\nfunc (err ErrorRunningTflint) Error() string {\n\tif err.Err != nil {\n\t\treturn fmt.Sprintf(\"Error encountered while running tflint with args: '%v': %s\", err.Args, err.Err)\n\t}\n\n\treturn fmt.Sprintf(\"Error encountered while running tflint with args: '%v'\", err.Args)\n}\n\nfunc (err ErrorRunningTflint) Unwrap() error {\n\treturn err.Err\n}\n\ntype IssuesFound struct{}\n\nfunc (err IssuesFound) Error() string {\n\treturn \"Tflint found issues in the project. Check for the tflint logs.\"\n}\n\ntype UnknownError struct {\n\tstatusCode int\n}\n\nfunc (err UnknownError) Error() string {\n\treturn fmt.Sprintf(\"Unknown status code from tflint: %d\", err.statusCode)\n}\n\ntype ConfigNotFound struct {\n\tcause string\n}\n\nfunc (err ConfigNotFound) Error() string {\n\treturn \"Could not find .tflint.hcl config file in the parent folders: \" + err.cause\n}\n\n// tfArgumentsToTflintVar converts variables from the terraform config to a list of tflint variables.\nfunc tfArgumentsToTflintVar(l log.Logger, hook *runcfg.Hook,\n\ttfCfg *runcfg.TerraformConfig) ([]string, error) {\n\tvar variables []string\n\n\tfor i := range tfCfg.ExtraArgs {\n\t\targ := &tfCfg.ExtraArgs[i]\n\t\t// use extra args which will be used on same command as hook\n\t\tif !slices.ContainsFunc(arg.Commands, func(cmd string) bool {\n\t\t\treturn slices.Contains(hook.Commands, cmd)\n\t\t}) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif len(arg.EnvVars) > 0 {\n\t\t\t// extract env_vars\n\t\t\tfor name, value := range arg.EnvVars {\n\t\t\t\tif after, ok := strings.CutPrefix(name, tfVarPrefix); ok {\n\t\t\t\t\tvarName := after\n\n\t\t\t\t\tvarValue, err := util.AsTerraformEnvVarJSONValue(value)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\n\t\t\t\t\tnewVar := fmt.Sprintf(\"--var='%s=%s'\", varName, varValue)\n\t\t\t\t\tvariables = append(variables, newVar)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(arg.Arguments) > 0 {\n\t\t\t// extract variables and var files from arguments\n\t\t\tfor _, value := range arg.Arguments {\n\t\t\t\tif after, ok := strings.CutPrefix(value, argVarPrefix); ok {\n\t\t\t\t\tvarName := after\n\t\t\t\t\tnewVar := fmt.Sprintf(\"--var='%s'\", varName)\n\t\t\t\t\tvariables = append(variables, newVar)\n\t\t\t\t}\n\n\t\t\t\tif after, ok := strings.CutPrefix(value, argVarFilePrefix); ok {\n\t\t\t\t\tvarName := after\n\t\t\t\t\tnewVar := \"--var-file=\" + varName\n\t\t\t\t\tvariables = append(variables, newVar)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(arg.RequiredVarFiles) > 0 {\n\t\t\t// extract required variables\n\t\t\tfor _, file := range arg.RequiredVarFiles {\n\t\t\t\tnewVar := \"--var-file=\" + file\n\t\t\t\tvariables = append(variables, newVar)\n\t\t\t}\n\t\t}\n\n\t\tif len(arg.OptionalVarFiles) > 0 {\n\t\t\t// extract optional variables\n\t\t\tfor _, file := range util.RemoveDuplicatesKeepLast(arg.OptionalVarFiles) {\n\t\t\t\tif util.FileExists(file) {\n\t\t\t\t\tnewVar := \"--var-file=\" + file\n\t\t\t\t\tvariables = append(variables, newVar)\n\t\t\t\t} else {\n\t\t\t\t\tl.Debugf(\"Skipping tflint var-file %s as it does not exist\", file)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn variables, nil\n}\n\n// findTflintConfigInProject looks for a .tflint.hcl file in the current folder or it's parents.\n// When running from cache, we start searching from the original config directory to find config in the source directory.\nfunc findTflintConfigInProject(l log.Logger, opts *TFLintOptions) (string, error) {\n\tstartDir := opts.WorkingDir\n\tif opts.TerragruntConfigPath != \"\" {\n\t\tstartDir = filepath.Dir(opts.TerragruntConfigPath)\n\t}\n\n\tpreviousDir := startDir\n\n\t// To avoid getting into an accidental infinite loop (e.g. do to cyclical symlinks), set a max on the number of\n\t// parent folders we'll check\n\tfor range opts.MaxFoldersToCheck {\n\t\tcurrentDir := filepath.Dir(previousDir)\n\t\tl.Debugf(\"Finding .tflint.hcl file from %s and going to %s\",\n\t\t\tutil.RelPathForLog(opts.RootWorkingDir, previousDir, opts.Writers.LogShowAbsPaths),\n\t\t\tutil.RelPathForLog(opts.RootWorkingDir, currentDir, opts.Writers.LogShowAbsPaths))\n\n\t\tif currentDir == previousDir {\n\t\t\treturn \"\", errors.New(ConfigNotFound{cause: \"Traversed all the day to the root\"})\n\t\t}\n\n\t\tfileToFind := filepath.Join(previousDir, \".tflint.hcl\")\n\t\tif util.FileExists(fileToFind) {\n\t\t\tl.Debugf(\"Found .tflint.hcl in %s\", util.RelPathForLog(opts.RootWorkingDir, fileToFind, opts.Writers.LogShowAbsPaths))\n\t\t\treturn fileToFind, nil\n\t\t}\n\n\t\tpreviousDir = currentDir\n\t}\n\n\treturn \"\", errors.New(ConfigNotFound{\n\t\tcause: fmt.Sprintf(\"Exceeded maximum folders to check (%d)\", opts.MaxFoldersToCheck),\n\t})\n}\n\n// tflintConfigFilePath returns the configuration file specified in --config argument\nfunc tflintConfigFilePath(l log.Logger, opts *TFLintOptions, arguments []string) (string, error) {\n\tfor i, arg := range arguments {\n\t\tif arg == \"--config\" && len(arguments) > i+1 {\n\t\t\treturn arguments[i+1], nil\n\t\t}\n\t}\n\t// find .tflint.hcl configuration in project files if it is not provided in arguments\n\tprojectConfigFile, err := findTflintConfigInProject(l, opts)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn projectConfigFile, nil\n}\n"
  },
  {
    "path": "internal/tflint/tflint_test.go",
    "content": "package tflint_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tflint\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestInputsToTflintVar(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tinputs   map[string]any\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"strings\",\n\t\t\tmap[string]any{\"region\": \"eu-central-1\", \"instance_count\": 3},\n\t\t\t[]string{\"--var=region=eu-central-1\", \"--var=instance_count=3\"},\n\t\t},\n\t\t{\n\t\t\t\"strings and arrays\",\n\t\t\tmap[string]any{\"cidr_blocks\": []string{\"10.0.0.0/16\"}},\n\t\t\t[]string{\"--var=cidr_blocks=[\\\"10.0.0.0/16\\\"]\"},\n\t\t},\n\t\t{\n\t\t\t\"boolean\",\n\t\t\tmap[string]any{\"create_resource\": true},\n\t\t\t[]string{\"--var=create_resource=true\"},\n\t\t},\n\t\t{\n\t\t\t\"with white spaces\",\n\t\t\t// With white spaces, the string is still validated by tflint.\n\t\t\tmap[string]any{\"region\": \" eu-central-1 \"},\n\t\t\t[]string{\"--var=region= eu-central-1 \"},\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.Parallel()\n\n\t\t\tactual, err := tflint.InputsToTflintVar(tc.inputs)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.ElementsMatch(t, tc.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/tips/errors.go",
    "content": "package tips\n\nimport (\n\t\"strings\"\n)\n\n// InvalidTipNameError is an error that is returned when an invalid tip name is requested.\ntype InvalidTipNameError struct {\n\trequestedName string\n\tallowedNames  []string\n}\n\nfunc NewInvalidTipNameError(requestedName string, allowedNames []string) *InvalidTipNameError {\n\treturn &InvalidTipNameError{\n\t\trequestedName: requestedName,\n\t\tallowedNames:  allowedNames,\n\t}\n}\n\nfunc (err InvalidTipNameError) Error() string {\n\treturn \"invalid tip suppression requested for `--no-tip`: '\" + err.requestedName + \"'; valid tip(s) for suppression: \" + strings.Join(err.allowedNames, \", \")\n}\n\nfunc (err InvalidTipNameError) Is(target error) bool {\n\t_, ok := target.(*InvalidTipNameError)\n\treturn ok\n}\n"
  },
  {
    "path": "internal/tips/tip.go",
    "content": "// Package tips provides utilities for displaying helpful tips to users during specific workflows.\n// Tips are informational messages that can help users troubleshoot issues or learn about features.\n//\n// Tips can be disabled globally using --no-tips or individually using --no-tip <tip-name>.\npackage tips\n\nimport (\n\t\"slices\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// Tip represents a helpful tip displayed to users.\ntype Tip struct {\n\t// Name is a unique identifier for the tip\n\tName string\n\t// Message is the message to display when the tip is triggered\n\tMessage string\n\t// OnceShow is a sync.Once to ensure the tip is only shown once per session\n\tOnceShow sync.Once\n\t// disabled is an atomic boolean to ensure the tip is only disabled once per session\n\tdisabled atomic.Bool\n}\n\n// Tips is a collection of Tip pointers.\ntype Tips []*Tip\n\n// Evaluate displays the tip if not disabled and not already shown.\nfunc (tip *Tip) Evaluate(l log.Logger) {\n\tif tip == nil || tip.isDisabled() || l == nil {\n\t\treturn\n\t}\n\n\ttip.OnceShow.Do(func() {\n\t\tl.Info(tip.Message)\n\t})\n}\n\n// Disable disables this tip from being shown.\nfunc (tip *Tip) Disable() {\n\ttip.disabled.Store(true)\n}\n\nfunc (tip *Tip) isDisabled() bool {\n\treturn tip.disabled.Load()\n}\n\n// Names returns all tip names.\nfunc (t Tips) Names() []string {\n\tif len(t) == 0 {\n\t\treturn []string{}\n\t}\n\n\tnames := make([]string, 0, len(t))\n\n\tfor _, tip := range t {\n\t\tnames = append(names, tip.Name)\n\t}\n\n\tslices.Sort(names)\n\n\treturn names\n}\n\n// Find searches and returns the tip by the given `name`.\nfunc (t Tips) Find(name string) *Tip {\n\tif len(t) == 0 {\n\t\treturn nil\n\t}\n\n\tfor _, tip := range t {\n\t\tif tip.Name == name {\n\t\t\treturn tip\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// DisableAll disables all tips such that they aren't shown.\nfunc (t Tips) DisableAll() {\n\tif len(t) == 0 {\n\t\treturn\n\t}\n\n\tfor _, tip := range t {\n\t\ttip.Disable()\n\t}\n}\n\n// DisableTip validates that the specified tip name is valid and disables this tip.\nfunc (t Tips) DisableTip(name string) error {\n\tfound := t.Find(name)\n\tif found == nil {\n\t\treturn NewInvalidTipNameError(name, t.Names())\n\t}\n\n\tfound.Disable()\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/tips/tip_test.go",
    "content": "package tips_test\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tips\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestTipEvaluate(t *testing.T) {\n\tt.Parallel()\n\n\tlogger, output := newTestLogger()\n\n\ttip := &tips.Tip{\n\t\tName:    \"test-tip\",\n\t\tMessage: \"This is a test tip message\",\n\t}\n\n\ttip.Evaluate(logger)\n\n\tassert.Contains(\n\t\tt,\n\t\tstrings.TrimSpace(output.String()),\n\t\t\"This is a test tip message\",\n\t)\n}\n\nfunc TestTipEvaluateWithNilLogger(t *testing.T) {\n\tt.Parallel()\n\n\ttip := &tips.Tip{\n\t\tName:    \"test-tip\",\n\t\tMessage: \"This is a test tip message\",\n\t}\n\n\t// Should not panic\n\ttip.Evaluate(nil)\n}\n\nfunc TestTipEvaluateOnNilTip(t *testing.T) {\n\tt.Parallel()\n\n\tlogger, _ := newTestLogger()\n\n\tvar tip *tips.Tip\n\n\t// Should not panic\n\ttip.Evaluate(logger)\n}\n\nfunc TestTipDisable(t *testing.T) {\n\tt.Parallel()\n\n\tlogger, output := newTestLogger()\n\n\ttip := &tips.Tip{\n\t\tName:    \"test-tip\",\n\t\tMessage: \"This tip should not appear\",\n\t}\n\n\ttip.Disable()\n\ttip.Evaluate(logger)\n\n\tassert.Empty(t, output.String())\n}\n\nfunc TestTipOnceShowEnsuresTipShownOnlyOnce(t *testing.T) {\n\tt.Parallel()\n\n\tlogger, output := newTestLogger()\n\n\ttip := &tips.Tip{\n\t\tName:    \"test-tip\",\n\t\tMessage: \"Once message\",\n\t}\n\n\ttip.Evaluate(logger)\n\ttip.Evaluate(logger)\n\ttip.Evaluate(logger)\n\n\tcontent := output.String()\n\tcount := strings.Count(content, \"Once message\")\n\n\tassert.Equal(t, 1, count, \"Tip should only be shown once per session\")\n}\n\nfunc TestTipsDisableAll(t *testing.T) {\n\tt.Parallel()\n\n\tlogger, output := newTestLogger()\n\n\tallTips := tips.NewTips()\n\tallTips.DisableAll()\n\n\tfor _, tip := range allTips {\n\t\ttip.Evaluate(logger)\n\t}\n\n\tassert.Empty(t, output.String())\n}\n\nfunc TestTipsDisableTip(t *testing.T) {\n\tt.Parallel()\n\n\tlogger, output := newTestLogger()\n\n\tallTips := tips.NewTips()\n\n\terr := allTips.DisableTip(tips.DebuggingDocs)\n\trequire.NoError(t, err)\n\n\ttip := allTips.Find(tips.DebuggingDocs)\n\trequire.NotNil(t, tip)\n\n\ttip.Evaluate(logger)\n\n\tassert.Empty(t, output.String())\n}\n\nfunc TestTipsDisableTipInvalidName(t *testing.T) {\n\tt.Parallel()\n\n\tallTips := tips.NewTips()\n\n\terr := allTips.DisableTip(\"invalid-tip-name\")\n\n\trequire.Error(t, err)\n\n\tvar invalidErr *tips.InvalidTipNameError\n\trequire.ErrorAs(t, err, &invalidErr)\n\tassert.Contains(t, err.Error(), \"invalid tip suppression requested for `--no-tip`: 'invalid-tip-name'\")\n\tassert.Contains(t, err.Error(), \"valid tip(s) for suppression:\")\n\tassert.Contains(t, err.Error(), tips.DebuggingDocs)\n}\n\nfunc TestTipsFind(t *testing.T) {\n\tt.Parallel()\n\n\tallTips := tips.NewTips()\n\n\ttip := allTips.Find(tips.DebuggingDocs)\n\trequire.NotNil(t, tip)\n\tassert.Equal(t, tips.DebuggingDocs, tip.Name)\n}\n\nfunc TestTipsFindNonExistent(t *testing.T) {\n\tt.Parallel()\n\n\tallTips := tips.NewTips()\n\n\ttip := allTips.Find(\"non-existent\")\n\tassert.Nil(t, tip)\n}\n\nfunc TestTipsNames(t *testing.T) {\n\tt.Parallel()\n\n\tallTips := tips.NewTips()\n\n\tnames := allTips.Names()\n\n\tassert.Contains(t, names, tips.DebuggingDocs)\n}\n\nfunc TestNewTips(t *testing.T) {\n\tt.Parallel()\n\n\tallTips := tips.NewTips()\n\n\tassert.NotEmpty(t, allTips)\n\n\t// Verify the debugging-docs tip exists and has the expected message\n\ttip := allTips.Find(tips.DebuggingDocs)\n\trequire.NotNil(t, tip)\n\tassert.Contains(t, tip.Message, \"troubleshooting\")\n}\n\nfunc newTestLogger() (log.Logger, *bytes.Buffer) {\n\tformatter := format.NewFormatter(placeholders.Placeholders{placeholders.Message()})\n\toutput := new(bytes.Buffer)\n\tlogger := log.New(log.WithOutput(output), log.WithLevel(log.InfoLevel), log.WithFormatter(formatter))\n\n\treturn logger, output\n}\n"
  },
  {
    "path": "internal/tips/tips.go",
    "content": "package tips\n\nconst (\n\t// DebuggingDocs is the tip that points users to the debugging documentation.\n\tDebuggingDocs = \"debugging-docs\"\n)\n\n// NewTips returns a new Tips collection with all available tips.\n//\n// Never remove any of these tips, as removing them will cause a breaking change for users\n// using an invocation of `--no-tip` pointing to a non-existent tip.\n//\n// e.g. `terragrunt run --no-tip=debugging-docs`\n//\n// If you want to programmatically document that a tip should no longer be\n// used after removing it from the codebase, just set `disabled` to `1` here for that tip.\nfunc NewTips() Tips {\n\treturn Tips{\n\t\t{\n\t\t\tName:    DebuggingDocs,\n\t\t\tMessage: \"TIP (\" + DebuggingDocs + \"): For help troubleshooting errors, visit https://docs.terragrunt.com/troubleshooting/debugging\",\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/util/collections.go",
    "content": "package util\n\nimport (\n\t\"cmp\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n)\n\nfunc MatchesAny(regExps []string, s string) bool {\n\tfor _, item := range regExps {\n\t\tif matched, _ := regexp.MatchString(item, s); matched {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// ListContainsSublist returns true if an instance of the sublist can be found in the given list\nfunc ListContainsSublist[S ~[]E, E comparable](list, sublist S) bool {\n\t// A list cannot contain an empty sublist\n\tif len(sublist) == 0 {\n\t\treturn false\n\t}\n\n\tif len(sublist) > len(list) {\n\t\treturn false\n\t}\n\n\tfor i := 0; len(list[i:]) >= len(sublist); i++ {\n\t\tif slices.Equal(list[i:i+len(sublist)], sublist) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// ListHasPrefix returns true if list starts with the given prefix list\nfunc ListHasPrefix[S ~[]E, E comparable](list, prefix S) bool {\n\tif len(prefix) == 0 {\n\t\treturn false\n\t}\n\n\tif len(prefix) > len(list) {\n\t\treturn false\n\t}\n\n\treturn slices.Equal(list[:len(prefix)], prefix)\n}\n\n// RemoveDuplicates returns a new slice with duplicates removed.\n// Note: This function sorts the result, so original order is not preserved.\nfunc RemoveDuplicates[S ~[]E, E cmp.Ordered](list S) S {\n\tresult := slices.Clone(list)\n\tslices.Sort(result)\n\n\treturn slices.Compact(result)\n}\n\n// MergeSlices combines multiple slices and removes duplicates.\n// Note: This function sorts the result, so original order is not preserved.\nfunc MergeSlices[S ~[]E, E cmp.Ordered](slicesToMerge ...S) S {\n\tresult := slices.Concat(slicesToMerge...)\n\tif result == nil {\n\t\treturn S{}\n\t}\n\n\tslices.Sort(result)\n\n\treturn slices.Compact(result)\n}\n\n// RemoveDuplicatesKeepLast returns a new slice with duplicates removed, keeping the last occurrence.\n// Unlike RemoveDuplicates, this preserves the relative order of elements.\nfunc RemoveDuplicatesKeepLast[S ~[]E, E comparable](list S) S {\n\tseen := make(map[E]int, len(list))\n\tresult := make(S, 0, len(list))\n\n\tfor _, item := range list {\n\t\tif idx, exists := seen[item]; exists {\n\t\t\t// Remove the previous occurrence\n\t\t\tresult = slices.Delete(result, idx, idx+1)\n\t\t\t// Update indices for items that were shifted\n\t\t\tfor k, v := range seen {\n\t\t\t\tif v > idx {\n\t\t\t\t\tseen[k] = v - 1\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tseen[item] = len(result)\n\t\tresult = append(result, item)\n\t}\n\n\treturn result\n}\n\n// FirstNonEmpty returns the first non-empty/non-zero element from the slice, or the zero value if none found.\nfunc FirstNonEmpty[S ~[]E, E comparable](list S) E {\n\tvar empty E\n\tfor _, item := range list {\n\t\tif item != empty {\n\t\t\treturn item\n\t\t}\n\t}\n\n\treturn empty\n}\n\n// SplitUrls slices s into all substrings separated by sep and returns a slice of\n// the substrings between those separators.\n// Taking into account that the `=` sign can also be used as a git tag, e.g. `git@github.com/test.git?ref=feature`\nfunc SplitUrls(s, sep string) []string {\n\tmasks := map[string]string{\n\t\t\"?ref=\": \"<ref-place-holder>\",\n\t}\n\n\t// mask\n\tfor src, mask := range masks {\n\t\ts = strings.ReplaceAll(s, src, mask)\n\t}\n\n\turls := strings.Split(s, sep)\n\n\t// unmask\n\tfor i := range urls {\n\t\tfor src, mask := range masks {\n\t\t\turls[i] = strings.ReplaceAll(urls[i], mask, src)\n\t\t}\n\t}\n\n\treturn urls\n}\n"
  },
  {
    "path": "internal/util/collections_test.go",
    "content": "package util_test\n\nimport (\n\t\"slices\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestMatchesAny(t *testing.T) {\n\tt.Parallel()\n\n\trealWorldErrorMessages := []string{\n\t\t\"Failed to load state: RequestError: send request failed\\ncaused by: Get https://<BUCKET_NAME>.us-west-2.amazonaws.com/?prefix=env%3A%2F: dial tcp 54.231.176.160:443: i/o timeout\",\n\t\t\"aws_cloudwatch_metric_alarm.asg_high_memory_utilization: Creating metric alarm failed: ValidationError: A separate request to update this alarm is in progress. status code: 400, request id: 94309fbd-7e09-11e8-a5f8-1de9e697c6bf\",\n\t\t\"Error configuring the backend \\\"s3\\\": RequestError: send request failed\\ncaused by: Post https://sts.amazonaws.com/: net/http: TLS handshake timeout\",\n\t}\n\n\ttestCases := []struct {\n\t\telement  string\n\t\tlist     []string\n\t\texpected bool\n\t}{\n\t\t{list: nil, element: \"\", expected: false},\n\t\t{list: []string{}, element: \"\", expected: false},\n\t\t{list: []string{}, element: \"foo\", expected: false},\n\t\t{list: []string{\"foo\"}, element: \"kafoot\", expected: true},\n\t\t{list: []string{\"bar\", \"foo\", \".*Failed to load backend.*TLS handshake timeout.*\"}, element: \"Failed to load backend: Error...:...  TLS handshake timeout\", expected: true},\n\t\t{list: []string{\"bar\", \"foo\", \".*Failed to load backend.*TLS handshake timeout.*\"}, element: \"Failed to load backend: Error...:...  TLxS handshake timeout\", expected: false},\n\t\t{list: []string{\"(?s).*Failed to load state.*dial tcp.*timeout.*\"}, element: realWorldErrorMessages[0], expected: true},\n\t\t{list: []string{\"(?s).*Creating metric alarm failed.*request to update this alarm is in progress.*\"}, element: realWorldErrorMessages[1], expected: true},\n\t\t{list: []string{\"(?s).*Error configuring the backend.*TLS handshake timeout.*\"}, element: realWorldErrorMessages[2], expected: true},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := util.MatchesAny(tc.list, tc.element)\n\t\t\tassert.Equal(t, tc.expected, actual, \"For list %v and element %s\", tc.list, tc.element)\n\t\t})\n\t}\n}\n\nfunc TestListContainsElement(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\telement  string\n\t\tlist     []string\n\t\texpected bool\n\t}{\n\t\t{list: []string{}, element: \"\", expected: false},\n\t\t{list: []string{}, element: \"foo\", expected: false},\n\t\t{list: []string{\"foo\"}, element: \"foo\", expected: true},\n\t\t{list: []string{\"bar\", \"foo\", \"baz\"}, element: \"foo\", expected: true},\n\t\t{list: []string{\"bar\", \"foo\", \"baz\"}, element: \"nope\", expected: false},\n\t\t{list: []string{\"bar\", \"foo\", \"baz\"}, element: \"\", expected: false},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := slices.Contains(tc.list, tc.element)\n\t\t\tassert.Equal(t, tc.expected, actual, \"For list %v and element %s\", tc.list, tc.element)\n\t\t})\n\t}\n}\n\nfunc TestListEquals(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\ta        []string\n\t\tb        []string\n\t\texpected bool\n\t}{\n\t\t{[]string{\"\"}, []string{}, false},\n\t\t{[]string{\"foo\"}, []string{\"bar\"}, false},\n\t\t{[]string{\"foo\", \"bar\"}, []string{\"bar\"}, false},\n\t\t{[]string{\"foo\"}, []string{\"foo\", \"bar\"}, false},\n\t\t{[]string{\"foo\", \"bar\"}, []string{\"bar\", \"foo\"}, false},\n\n\t\t{[]string{}, []string{}, true},\n\t\t{[]string{\"\"}, []string{\"\"}, true},\n\t\t{[]string{\"foo\", \"bar\"}, []string{\"foo\", \"bar\"}, true},\n\t}\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := slices.Equal(tc.a, tc.b)\n\t\t\tassert.Equal(t, tc.expected, actual, \"For list %v and list %v\", tc.a, tc.b)\n\t\t})\n\t}\n}\n\nfunc TestListContainsSublist(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tlist     []string\n\t\tsublist  []string\n\t\texpected bool\n\t}{\n\t\t{[]string{}, []string{}, false},\n\t\t{[]string{}, []string{\"foo\"}, false},\n\t\t{[]string{\"foo\"}, []string{}, false},\n\t\t{[]string{\"foo\"}, []string{\"bar\"}, false},\n\t\t{[]string{\"foo\"}, []string{\"foo\", \"bar\"}, false},\n\t\t{[]string{\"bar\", \"foo\"}, []string{\"foo\", \"bar\"}, false},\n\t\t{[]string{\"bar\", \"foo\", \"gee\"}, []string{\"foo\", \"bar\"}, false},\n\t\t{[]string{\"foo\", \"foo\", \"gee\"}, []string{\"foo\", \"bar\"}, false},\n\t\t{[]string{\"zim\", \"gee\", \"foo\", \"foo\", \"foo\"}, []string{\"foo\", \"foo\", \"bar\", \"bar\"}, false},\n\n\t\t{[]string{\"\"}, []string{\"\"}, true},\n\t\t{[]string{\"foo\"}, []string{\"foo\"}, true},\n\t\t{[]string{\"foo\", \"bar\"}, []string{\"foo\"}, true},\n\t\t{[]string{\"bar\", \"foo\"}, []string{\"foo\"}, true},\n\t\t{[]string{\"foo\", \"bar\", \"gee\"}, []string{\"foo\", \"bar\"}, true},\n\t\t{[]string{\"zim\", \"foo\", \"bar\", \"gee\"}, []string{\"foo\", \"bar\"}, true},\n\t\t{[]string{\"foo\", \"foo\", \"bar\", \"gee\"}, []string{\"foo\", \"bar\"}, true},\n\t\t{[]string{\"zim\", \"gee\", \"foo\", \"bar\"}, []string{\"foo\", \"bar\"}, true},\n\t\t{[]string{\"foo\", \"foo\", \"foo\", \"bar\"}, []string{\"foo\", \"foo\"}, true},\n\t\t{[]string{\"bar\", \"foo\", \"foo\", \"foo\"}, []string{\"foo\", \"foo\"}, true},\n\t\t{[]string{\"zim\", \"gee\", \"foo\", \"bar\"}, []string{\"gee\", \"foo\", \"bar\"}, true},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := util.ListContainsSublist(tc.list, tc.sublist)\n\t\t\tassert.Equal(t, tc.expected, actual, \"For list %v and sublist %v\", tc.list, tc.sublist)\n\t\t})\n\t}\n}\n\nfunc TestListHasPrefix(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tlist     []string\n\t\tprefix   []string\n\t\texpected bool\n\t}{\n\t\t{[]string{}, []string{}, false},\n\t\t{[]string{\"\"}, []string{}, false},\n\t\t{[]string{\"foo\"}, []string{\"bar\"}, false},\n\t\t{[]string{\"foo\", \"bar\"}, []string{\"bar\"}, false},\n\t\t{[]string{\"foo\"}, []string{\"foo\", \"bar\"}, false},\n\t\t{[]string{\"foo\", \"bar\", \"foo\"}, []string{\"bar\", \"foo\"}, false},\n\n\t\t{[]string{\"\"}, []string{\"\"}, true},\n\t\t{[]string{\"\", \"foo\"}, []string{\"\"}, true},\n\t\t{[]string{\"foo\", \"bar\"}, []string{\"foo\"}, true},\n\t\t{[]string{\"foo\", \"bar\"}, []string{\"foo\", \"bar\"}, true},\n\t\t{[]string{\"foo\", \"bar\", \"biz\"}, []string{\"foo\", \"bar\"}, true},\n\t}\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := util.ListHasPrefix(tc.list, tc.prefix)\n\t\t\tassert.Equal(t, tc.expected, actual, \"For list %v and prefix %v\", tc.list, tc.prefix)\n\t\t})\n\t}\n}\n\nfunc TestRemoveDuplicates(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tlist     []string\n\t\texpected []string\n\t}{\n\t\t{[]string{}, []string{}},\n\t\t{[]string{\"foo\"}, []string{\"foo\"}},\n\t\t{[]string{\"foo\", \"bar\"}, []string{\"bar\", \"foo\"}}, // sorted\n\t\t{[]string{\"foo\", \"bar\", \"foobar\", \"bar\", \"foo\"}, []string{\"bar\", \"foo\", \"foobar\"}},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := util.RemoveDuplicates(tc.list)\n\t\t\tassert.Equal(t, tc.expected, actual, \"For list %v\", tc.list)\n\t\t})\n\t}\n}\n\nfunc TestRemoveDuplicatesKeepLast(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tlist     []string\n\t\texpected []string\n\t}{\n\t\t{[]string{}, []string{}},\n\t\t{[]string{\"foo\"}, []string{\"foo\"}},\n\t\t{[]string{\"foo\", \"bar\"}, []string{\"foo\", \"bar\"}},\n\t\t{[]string{\"foo\", \"bar\", \"foobar\", \"bar\", \"foo\"}, []string{\"foobar\", \"bar\", \"foo\"}},\n\t\t{[]string{\"foo\", \"bar\", \"foobar\", \"foo\", \"bar\"}, []string{\"foobar\", \"foo\", \"bar\"}},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := util.RemoveDuplicatesKeepLast(tc.list)\n\t\t\tassert.Equal(t, tc.expected, actual, \"For list %v\", tc.list)\n\t\t})\n\t}\n}\n\nfunc TestMergeSlices(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\ta        []string\n\t\tb        []string\n\t\texpected []string\n\t}{\n\t\t{[]string{}, []string{}, []string{}},\n\t\t{[]string{\"foo\"}, []string{}, []string{\"foo\"}},\n\t\t{[]string{}, []string{\"bar\"}, []string{\"bar\"}},\n\t\t{[]string{\"foo\"}, []string{\"bar\"}, []string{\"bar\", \"foo\"}}, // sorted\n\t\t{[]string{\"foo\", \"bar\"}, []string{\"bar\", \"baz\"}, []string{\"bar\", \"baz\", \"foo\"}},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := util.MergeSlices(tc.a, tc.b)\n\t\t\tassert.Equal(t, tc.expected, actual, \"For lists %v and %v\", tc.a, tc.b)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/util/datetime.go",
    "content": "//nolint:gocritic\npackage util\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n)\n\nfunc ParseTimestamp(ts string) (time.Time, error) {\n\tt, err := time.Parse(time.RFC3339, ts)\n\tif err != nil {\n\t\t// TODO: Remove this lint suppression\n\t\tswitch err := err.(type) { //nolint:errorlint\n\t\tcase *time.ParseError:\n\t\t\t// If err is a time.ParseError then its string representation is not\n\t\t\t// appropriate since it relies on details of Go's strange date format\n\t\t\t// representation, which a caller of our functions is not expected\n\t\t\t// to be familiar with.\n\t\t\t//\n\t\t\t// Therefore we do some light transformation to get a more suitable\n\t\t\t// error that should make more sense to our callers. These are\n\t\t\t// still not awesome error messages, but at least they refer to\n\t\t\t// the timestamp portions by name rather than by Go's example\n\t\t\t// values.\n\t\t\tif err.LayoutElem == \"\" && err.ValueElem == \"\" && err.Message != \"\" {\n\t\t\t\t// For some reason err.Message is populated with a \": \" prefix\n\t\t\t\t// by the time package.\n\t\t\t\treturn time.Time{}, fmt.Errorf(\"not a valid RFC3339 timestamp%s\", err.Message)\n\t\t\t}\n\n\t\t\tvar what string\n\n\t\t\tswitch err.LayoutElem {\n\t\t\tcase \"2006\":\n\t\t\t\twhat = \"year\"\n\t\t\tcase \"01\":\n\t\t\t\twhat = \"month\"\n\t\t\tcase \"02\":\n\t\t\t\twhat = \"day of month\"\n\t\t\tcase \"15\":\n\t\t\t\twhat = \"hour\"\n\t\t\tcase \"04\":\n\t\t\t\twhat = \"minute\"\n\t\t\tcase \"05\":\n\t\t\t\twhat = \"second\"\n\t\t\tcase \"Z07:00\":\n\t\t\t\twhat = \"UTC offset\"\n\t\t\tcase \"T\":\n\t\t\t\treturn time.Time{}, errors.New(\"not a valid RFC3339 timestamp: missing required time introducer 'T'\")\n\t\t\tcase \":\", \"-\":\n\t\t\t\tif err.ValueElem == \"\" {\n\t\t\t\t\treturn time.Time{}, fmt.Errorf(\"not a valid RFC3339 timestamp: end of string where %q is expected\", err.LayoutElem)\n\t\t\t\t} else {\n\t\t\t\t\treturn time.Time{}, fmt.Errorf(\"not a valid RFC3339 timestamp: found %q where %q is expected\", err.ValueElem, err.LayoutElem)\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\t// Should never get here, because time.RFC3339 includes only the\n\t\t\t\t// above portions, but since that might change in future we'll\n\t\t\t\t// be robust here.\n\t\t\t\twhat = \"timestamp segment\"\n\t\t\t}\n\n\t\t\tif err.ValueElem == \"\" {\n\t\t\t\treturn time.Time{}, fmt.Errorf(\"not a valid RFC3339 timestamp: end of string before %s\", what)\n\t\t\t} else {\n\t\t\t\treturn time.Time{}, fmt.Errorf(\"not a valid RFC3339 timestamp: cannot use %q as %s\", err.ValueElem, what)\n\t\t\t}\n\t\t}\n\n\t\treturn time.Time{}, err\n\t}\n\n\treturn t, nil\n}\n"
  },
  {
    "path": "internal/util/datetime_test.go",
    "content": "package util_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParseTimestamp(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\targ   string\n\t\tvalue time.Time\n\t\terr   string\n\t}{\n\t\t{\"2017-11-22T00:00:00Z\", time.Date(2017, time.Month(11), 22, 0, 0, 0, 0, time.UTC), \"\"},\n\t\t{\"2017-11-22T01:00:00+01:00\", time.Date(2017, time.Month(11), 22, 1, 0, 0, 0, time.FixedZone(\"\", 3600)), \"\"},\n\t\t{\"bloop\", time.Time{}, `not a valid RFC3339 timestamp: cannot use \"bloop\" as year`},\n\t\t{\"2017-11-22 00:00:00Z\", time.Time{}, `not a valid RFC3339 timestamp: missing required time introducer 'T'`},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"ParseTimestamp(%#v)\", tc.arg), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual, err := util.ParseTimestamp(tc.arg)\n\t\t\tif tc.err != \"\" {\n\t\t\t\trequire.EqualError(t, err, tc.err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.value, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/util/dirs.go",
    "content": "package util\n\nimport (\n\t\"path/filepath\"\n)\n\n// DefaultWorkingAndDownloadDirs gets the default working and download\n// directories for the given Terragrunt config path.\nfunc DefaultWorkingAndDownloadDirs(terragruntConfigPath string) (string, string) {\n\tworkingDir := filepath.Dir(terragruntConfigPath)\n\n\tdownloadDir := filepath.Clean(filepath.Join(workingDir, TerragruntCacheDir))\n\n\treturn workingDir, downloadDir\n}\n"
  },
  {
    "path": "internal/util/file.go",
    "content": "package util\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/sha256\"\n\t\"encoding/gob\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"syscall\"\n\n\turlhelper \"github.com/hashicorp/go-getter/helper/url\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/mattn/go-zglob\"\n\t\"github.com/mitchellh/go-homedir\"\n)\n\nconst (\n\tTerraformLockFile     = \".terraform.lock.hcl\"\n\tTerragruntCacheDir    = \".terragrunt-cache\"\n\tTerraformCacheDir     = \".terraform\"\n\tGitDir                = \".git\"\n\tDefaultBoilerplateDir = \".boilerplate\"\n\tChecksumReadBlock     = 8192\n)\n\n// FileOrData will read the contents of the data of the given arg if it is a file, and otherwise return the contents by\n// itself. This will return an error if the given path is a directory.\nfunc FileOrData(maybePath string) (string, error) {\n\t// We can blindly pass in maybePath to homedir.Expand, because homedir.Expand only does something if the first\n\t// character is ~, and if it is, there is a high chance of it being a path instead of data contents.\n\texpandedMaybePath, err := homedir.Expand(maybePath)\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\tif IsFile(expandedMaybePath) {\n\t\tcontents, err := os.ReadFile(expandedMaybePath)\n\t\tif err != nil {\n\t\t\treturn \"\", errors.New(err)\n\t\t}\n\n\t\treturn string(contents), nil\n\t} else if IsDir(expandedMaybePath) {\n\t\treturn \"\", errors.New(PathIsNotFile{path: expandedMaybePath})\n\t}\n\n\treturn expandedMaybePath, nil\n}\n\n// FileExists returns true if the given file exists.\nfunc FileExists(path string) bool {\n\t_, err := os.Stat(path)\n\treturn err == nil\n}\n\n// FileNotExists returns true if the given file does not exist.\nfunc FileNotExists(path string) bool {\n\t_, err := os.Stat(path)\n\treturn os.IsNotExist(err)\n}\n\n// EnsureDirectory creates a directory at this path if it does not exist, or error if the path exists and is a file.\nfunc EnsureDirectory(path string) error {\n\tif FileExists(path) && IsFile(path) {\n\t\treturn errors.New(PathIsNotDirectory{path})\n\t} else if !FileExists(path) {\n\t\tconst ownerReadWriteExecutePerms = 0700\n\n\t\treturn errors.New(os.MkdirAll(path, ownerReadWriteExecutePerms))\n\t}\n\n\treturn nil\n}\n\n// CanonicalPath returns the canonical version of the given path, relative to the given base path. That is, if the given\n// path is a relative path, assume it is relative to the given base path. A canonical path is an absolute path with all\n// relative components (e.g. \"../\") fully resolved, which makes it safe to compare paths as strings. If the path is\n// relative, basePath must be absolute or an error is returned.\nfunc CanonicalPath(path string, basePath string) (string, error) {\n\tif !filepath.IsAbs(path) {\n\t\tif !filepath.IsAbs(basePath) {\n\t\t\treturn \"\", fmt.Errorf(\"base path %q is not absolute\", basePath)\n\t\t}\n\n\t\tpath = filepath.Join(basePath, path)\n\t}\n\n\treturn filepath.Clean(path), nil\n}\n\n// Grep returns true if the given regex can be found in any of the files matched by the given glob.\nfunc Grep(regex *regexp.Regexp, glob string) (bool, error) {\n\t// Ideally, we'd use a builin Go library like filepath.Glob here, but per https://github.com/golang/go/issues/11862,\n\t// the current go implementation doesn't support treating ** as zero or more directories, just zero or one.\n\t// So we use a third-party library.\n\tmatches, err := zglob.Glob(glob)\n\tif err != nil {\n\t\treturn false, errors.New(err)\n\t}\n\n\tfor _, match := range matches {\n\t\tif IsDir(match) {\n\t\t\tcontinue\n\t\t}\n\n\t\tbytes, err := os.ReadFile(match)\n\t\tif err != nil {\n\t\t\treturn false, errors.New(err)\n\t\t}\n\n\t\tif regex.Match(bytes) {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\treturn false, nil\n}\n\n// FindTFFiles walks through the directory and returns all OpenTofu/Terraform files (.tf, .tofu, .tf.json, .tofu.json)\nfunc FindTFFiles(rootPath string) ([]string, error) {\n\tvar terraformFiles []string\n\n\terr := filepath.WalkDir(rootPath, func(path string, d fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif d.IsDir() {\n\t\t\treturn nil\n\t\t}\n\n\t\tif IsTFFile(path) {\n\t\t\tterraformFiles = append(terraformFiles, path)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\treturn terraformFiles, err\n}\n\n// RegexFoundInTFFiles walks through the directory and checks if any OpenTofu/Terraform files (.tf, .tofu, .tf.json, .tofu.json) contain the given regex pattern\nfunc RegexFoundInTFFiles(workingDir string, pattern *regexp.Regexp) (bool, error) {\n\tvar found bool\n\n\terr := filepath.WalkDir(workingDir, func(path string, d fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif d.IsDir() {\n\t\t\treturn nil\n\t\t}\n\n\t\tif !IsTFFile(path) {\n\t\t\treturn nil\n\t\t}\n\n\t\tcontent, err := os.ReadFile(path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif pattern.Match(content) {\n\t\t\tfound = true\n\t\t\treturn filepath.SkipAll\n\t\t}\n\n\t\treturn nil\n\t})\n\n\treturn found, err\n}\n\n// DirContainsTFFiles checks if the given directory contains any Terraform/OpenTofu files (.tf, .tofu, .tf.json, .tofu.json)\nfunc DirContainsTFFiles(dirPath string) (bool, error) {\n\tvar found bool\n\n\terr := filepath.WalkDir(dirPath, func(path string, d fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif d.IsDir() {\n\t\t\treturn nil\n\t\t}\n\n\t\tif IsTFFile(path) {\n\t\t\tfound = true\n\t\t\treturn filepath.SkipAll\n\t\t}\n\n\t\treturn nil\n\t})\n\n\treturn found, err\n}\n\n// IsTFFile checks if a given file is a Terraform/OpenTofu file (.tf, .tofu, .tf.json, .tofu.json)\nfunc IsTFFile(path string) bool {\n\tsuffixes := []string{\n\t\t\".tf\",\n\t\t\".tofu\",\n\t\t\".tf.json\",\n\t\t\".tofu.json\",\n\t}\n\n\tfor _, suffix := range suffixes {\n\t\tif strings.HasSuffix(path, suffix) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// IsDir returns true if the path points to a directory.\nfunc IsDir(path string) bool {\n\tfileInfo, err := os.Stat(path)\n\treturn err == nil && fileInfo.IsDir()\n}\n\n// IsFile returns true if the path points to a file.\nfunc IsFile(path string) bool {\n\tfileInfo, err := os.Stat(path)\n\treturn err == nil && !fileInfo.IsDir()\n}\n\n// GetPathRelativeTo returns the relative path you would have to take to get from basePath to path.\nfunc GetPathRelativeTo(path string, basePath string) (string, error) {\n\tif path == \"\" {\n\t\tpath = \".\"\n\t}\n\n\tif basePath == \"\" {\n\t\tbasePath = \".\"\n\t}\n\n\trelPath, err := filepath.Rel(basePath, path)\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\treturn relPath, nil\n}\n\n// ReadFileAsString returns the contents of the file at the given path as a string.\nfunc ReadFileAsString(path string) (string, error) {\n\tbytes, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn \"\", errors.Errorf(\"error reading file at path %s: %w\", path, err)\n\t}\n\n\treturn string(bytes), nil\n}\n\nfunc listContainsElementWithPrefix(list []string, elementPrefix string) bool {\n\tfor _, element := range list {\n\t\tif strings.HasPrefix(element, elementPrefix) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc pathContainsPrefix(path string, prefixes []string) bool {\n\tfor _, element := range prefixes {\n\t\tif strings.HasPrefix(path, element) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// Takes apbsolute glob path and returns an array of expanded relative paths\nfunc expandGlobPath(source, absoluteGlobPath string) ([]string, error) {\n\tincludeExpandedGlobs := []string{}\n\n\tabsoluteExpandGlob, err := zglob.Glob(absoluteGlobPath)\n\tif err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\t// we ignore not exist error as we only care about the globs that exist in the src dir\n\t\treturn nil, errors.New(err)\n\t}\n\n\tfor _, absoluteExpandGlobPath := range absoluteExpandGlob {\n\t\tif strings.Contains(absoluteExpandGlobPath, TerragruntCacheDir) {\n\t\t\tcontinue\n\t\t}\n\n\t\trelativeExpandGlobPath, err := GetPathRelativeTo(absoluteExpandGlobPath, source)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tincludeExpandedGlobs = append(includeExpandedGlobs, filepath.ToSlash(relativeExpandGlobPath))\n\n\t\tif IsDir(absoluteExpandGlobPath) {\n\t\t\tdirExpandGlob, err := expandGlobPath(source, absoluteExpandGlobPath+\"/*\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.New(err)\n\t\t\t}\n\n\t\t\tincludeExpandedGlobs = append(includeExpandedGlobs, dirExpandGlob...)\n\t\t}\n\t}\n\n\treturn includeExpandedGlobs, nil\n}\n\n// CopyFolderContents copies the files and folders within the source folder into the destination folder. Note that hidden files and folders\n// (those starting with a dot) will be skipped. Will create a specified manifest file that contains paths of all copied files.\nfunc CopyFolderContents(\n\tl log.Logger,\n\tsource,\n\tdestination,\n\tmanifestFile string,\n\tincludeInCopy []string,\n\texcludeFromCopy []string,\n) error {\n\t// We use filepath.ToSlash because we end up using globs here, and those expect forward slashes.\n\tsource = filepath.ToSlash(source)\n\tdestination = filepath.ToSlash(destination)\n\n\t// Expand all the includeInCopy glob paths, converting the globbed results to relative paths so that they work in\n\t// the copy filter.\n\tincludeExpandedGlobs := []string{}\n\n\tfor _, includeGlob := range includeInCopy {\n\t\tglobPath := filepath.Join(source, includeGlob)\n\n\t\texpandGlob, err := expandGlobPath(source, globPath)\n\t\tif err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\tincludeExpandedGlobs = append(includeExpandedGlobs, expandGlob...)\n\t}\n\n\texcludeExpandedGlobs := []string{}\n\n\tfor _, excludeGlob := range excludeFromCopy {\n\t\tglobPath := filepath.Join(source, excludeGlob)\n\n\t\texpandGlob, err := expandGlobPath(source, globPath)\n\t\tif err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\texcludeExpandedGlobs = append(excludeExpandedGlobs, expandGlob...)\n\t}\n\n\treturn CopyFolderContentsWithFilter(l, source, destination, manifestFile, func(absolutePath string) bool {\n\t\trelativePath, err := GetPathRelativeTo(absolutePath, source)\n\t\tif err != nil {\n\t\t\treturn false\n\t\t}\n\n\t\trelativePath = filepath.ToSlash(relativePath)\n\t\tpathHasPrefix := pathContainsPrefix(relativePath, excludeExpandedGlobs)\n\n\t\tlistHasElementWithPrefix := listContainsElementWithPrefix(includeExpandedGlobs, relativePath)\n\t\tif listHasElementWithPrefix && !pathHasPrefix {\n\t\t\treturn true\n\t\t}\n\n\t\tif pathHasPrefix {\n\t\t\treturn false\n\t\t}\n\n\t\treturn !TerragruntExcludes(filepath.FromSlash(relativePath))\n\t})\n}\n\n// CopyFolderContentsWithFilter copies the files and folders within the source folder into the destination folder.\nfunc CopyFolderContentsWithFilter(l log.Logger, source, destination, manifestFile string, filter func(absolutePath string) bool) error {\n\tconst ownerReadWriteExecutePerms = 0700\n\tif err := os.MkdirAll(destination, ownerReadWriteExecutePerms); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tmanifest := NewFileManifest(destination, manifestFile)\n\tif err := manifest.Clean(l); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tif err := manifest.Create(); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tdefer func(manifest *fileManifest) {\n\t\terr := manifest.Close()\n\t\tif err != nil {\n\t\t\tl.Warnf(\"Error closing manifest file: %v\", err)\n\t\t}\n\t}(manifest)\n\n\t// Why use filepath.Glob here? The original implementation used os.ReadDir, but that method calls lstat on all\n\t// the files/folders in the directory, including files/folders you may want to explicitly skip. The next attempt\n\t// was to use filepath.Walk, but that doesn't work because it ignores symlinks. So, now we turn to filepath.Glob.\n\tfiles, err := filepath.Glob(source + \"/*\")\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tfor _, file := range files {\n\t\tfileRelativePath, err := GetPathRelativeTo(file, source)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !filter(file) {\n\t\t\tcontinue\n\t\t}\n\n\t\tdest := filepath.Join(destination, fileRelativePath)\n\n\t\tif IsDir(file) {\n\t\t\tinfo, err := os.Lstat(file)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.New(err)\n\t\t\t}\n\n\t\t\tif err := os.MkdirAll(dest, info.Mode()); err != nil {\n\t\t\t\treturn errors.New(err)\n\t\t\t}\n\n\t\t\tif err := CopyFolderContentsWithFilter(l, file, dest, manifestFile, filter); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := manifest.AddDirectory(dest); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tparentDir := filepath.Dir(dest)\n\n\t\t\tconst ownerReadWriteExecutePerms = 0700\n\t\t\tif err := os.MkdirAll(parentDir, ownerReadWriteExecutePerms); err != nil {\n\t\t\t\treturn errors.New(err)\n\t\t\t}\n\n\t\t\tif err := CopyFile(file, dest); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := manifest.AddFile(dest); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// CopyFolderToTemp creates a temp directory with the given prefix, copies the\n// contents of the source folder into it using the provided filter, and returns\n// the path to the temp directory.\nfunc CopyFolderToTemp(source string, tempPrefix string, filter func(path string) bool) (string, error) {\n\tdest, err := os.MkdirTemp(\"\", tempPrefix)\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\tif err := CopyFolderContentsWithFilter(log.New(), source, dest, \".copymanifest\", filter); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn dest, nil\n}\n\n// IsSymLink returns true if the given file is a symbolic link\n// Per https://stackoverflow.com/a/18062079/2308858\nfunc IsSymLink(path string) bool {\n\tfileInfo, err := os.Lstat(path)\n\treturn err == nil && fileInfo.Mode()&os.ModeSymlink != 0\n}\n\nfunc TerragruntExcludes(path string) bool {\n\t// Do not exclude the terraform lock file (new feature added in terraform 0.14)\n\tif filepath.Base(path) == TerraformLockFile {\n\t\treturn false\n\t}\n\n\tpathParts := strings.SplitSeq(path, string(filepath.Separator))\n\tfor pathPart := range pathParts {\n\t\tif strings.HasPrefix(pathPart, \".\") && pathPart != \".\" && pathPart != \"..\" {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// CopyFile copies a file from source to destination.\nfunc CopyFile(source string, destination string) error {\n\tcontents, err := os.ReadFile(source)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn WriteFileWithSamePermissions(source, destination, contents)\n}\n\n// WriteFileWithSamePermissions writes a file to the given destination with the given contents\n// using the same permissions as the file at source.\nfunc WriteFileWithSamePermissions(source string, destination string, contents []byte) error {\n\tfileInfo, err := os.Stat(source)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\t// If destination exists, remove it first to avoid permission issues\n\t// This is especially important when CAS creates read-only files\n\tif FileExists(destination) {\n\t\tif err := os.Remove(destination); err != nil && !os.IsNotExist(err) {\n\t\t\treturn errors.New(err)\n\t\t}\n\t}\n\n\treturn os.WriteFile(destination, contents, fileInfo.Mode())\n}\n\n// ContainsPath returns true if path contains the given subpath\n// E.g. path=\"foo/bar/bee\", subpath=\"bar/bee\" -> true\n// E.g. path=\"foo/bar/bee\", subpath=\"bar/be\" -> false (because be is not a directory)\nfunc ContainsPath(path, subpath string) bool {\n\tsplitPath := strings.Split(filepath.Clean(path), string(filepath.Separator))\n\tsplitSubpath := strings.Split(filepath.Clean(subpath), string(filepath.Separator))\n\n\treturn ListContainsSublist(splitPath, splitSubpath)\n}\n\n// HasPathPrefix returns true if path starts with the given path prefix\n// E.g. path=\"/foo/bar/biz\", prefix=\"/foo/bar\" -> true\n// E.g. path=\"/foo/bar/biz\", prefix=\"/foo/ba\" -> false (because ba is not a directory\n// path)\nfunc HasPathPrefix(path, prefix string) bool {\n\tsplitPath := strings.Split(filepath.Clean(path), string(filepath.Separator))\n\tsplitPrefix := strings.Split(filepath.Clean(prefix), string(filepath.Separator))\n\n\treturn ListHasPrefix(splitPath, splitPrefix)\n}\n\n// JoinTerraformModulePath joins two paths together with a double-slash between them, as this is what\n// Terraform uses to identify where a \"repo\" ends and a path within the repo begins.\n// Note: The Terraform docs only mention two forward-slashes, so it's not clear\n// if on Windows those should be two back-slashes? https://www.terraform.io/docs/modules/sources.html\nfunc JoinTerraformModulePath(modulesFolder string, path string) string {\n\tcleanModulesFolder := strings.TrimRight(modulesFolder, `/\\`)\n\tcleanPath := strings.TrimLeft(path, `/\\`)\n\t// if source path contains \"?ref=\", reconstruct module dir using \"//\"\n\tif strings.Contains(cleanModulesFolder, \"?ref=\") && cleanPath != \"\" {\n\t\tcanonicalSourceURL, err := urlhelper.Parse(cleanModulesFolder)\n\t\tif err == nil {\n\t\t\t// append path\n\t\t\tif canonicalSourceURL.Opaque != \"\" {\n\t\t\t\tcanonicalSourceURL.Opaque = fmt.Sprintf(\"%s//%s\", strings.TrimRight(canonicalSourceURL.Opaque, `/\\`), cleanPath)\n\t\t\t} else {\n\t\t\t\tcanonicalSourceURL.Path = fmt.Sprintf(\"%s//%s\", strings.TrimRight(canonicalSourceURL.Path, `/\\`), cleanPath)\n\t\t\t}\n\n\t\t\treturn canonicalSourceURL.String()\n\t\t}\n\t}\n\n\t// fallback to old behavior if we can't parse the url\n\treturn fmt.Sprintf(\"%s//%s\", cleanModulesFolder, cleanPath)\n}\n\n// fileManifest represents a manifest with paths of all files copied by terragrunt.\n// This allows to clean those files on subsequent runs.\n// The problem is as follows: terragrunt copies the terraform source code first to \"working directory\" using go-getter,\n// and then copies all files from the working directory to the above dir.\n// It works fine on the first run, but if we delete a file from the current terragrunt directory, we want it\n// to be cleaned in the \"working directory\" as well. Since we don't really know what can get copied by go-getter,\n// we have to track all the files we touch in a manifest. This way we know exactly which files we need to clean on\n// subsequent runs.\ntype fileManifest struct {\n\tencoder        *gob.Encoder\n\tfileHandle     *os.File\n\tManifestFolder string\n\tManifestFile   string\n}\n\n// fileManifestEntry represents an entry in the fileManifest.\n// It uses a struct with IsDir flag so that we won't have to call Stat on every\n// file to determine if it's a directory or a file\ntype fileManifestEntry struct {\n\tPath  string\n\tIsDir bool\n}\n\n// Clean will recursively remove all files specified in the manifest\nfunc (manifest *fileManifest) Clean(l log.Logger) error {\n\treturn manifest.clean(l, filepath.Join(manifest.ManifestFolder, manifest.ManifestFile))\n}\n\n// clean cleans the files in the manifest. If it has a directory entry, then it recursively calls clean()\nfunc (manifest *fileManifest) clean(l log.Logger, manifestPath string) error {\n\t// if manifest file doesn't exist, just exit\n\tif !FileExists(manifestPath) {\n\t\treturn nil\n\t}\n\n\tfile, err := os.Open(manifestPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// cleaning manifest file\n\tdefer func(name string) {\n\t\tif err := file.Close(); err != nil {\n\t\t\tl.Warnf(\"Error closing file %s: %v\", name, err)\n\t\t}\n\n\t\tif err := os.Remove(name); err != nil {\n\t\t\tl.Warnf(\"Error removing manifest file %s: %v\", name, err)\n\t\t}\n\t}(manifestPath)\n\n\tdecoder := gob.NewDecoder(file)\n\t// decode paths one by one\n\tfor {\n\t\tvar manifestEntry fileManifestEntry\n\n\t\terr = decoder.Decode(&manifestEntry)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\tbreak\n\t\t\t} else {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif manifestEntry.IsDir {\n\t\t\t// join the directory entry path with the manifest file name and call clean()\n\t\t\tif err := manifest.clean(l, filepath.Join(manifestEntry.Path, manifest.ManifestFile)); err != nil {\n\t\t\t\treturn errors.New(err)\n\t\t\t}\n\t\t} else {\n\t\t\tif err := os.Remove(manifestEntry.Path); err != nil && !os.IsNotExist(err) {\n\t\t\t\treturn errors.New(err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Create will create the manifest file\nfunc (manifest *fileManifest) Create() error {\n\tconst ownerWriteGlobalReadPerms = 0644\n\n\tfileHandle, err := os.OpenFile(filepath.Join(manifest.ManifestFolder, manifest.ManifestFile), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, ownerWriteGlobalReadPerms)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmanifest.fileHandle = fileHandle\n\tmanifest.encoder = gob.NewEncoder(manifest.fileHandle)\n\n\treturn nil\n}\n\n// AddFile will add the file path to the manifest file. Please make sure to run Create() before using this\nfunc (manifest *fileManifest) AddFile(path string) error {\n\treturn manifest.encoder.Encode(fileManifestEntry{Path: path, IsDir: false})\n}\n\n// AddDirectory will add the directory path to the manifest file. Please make sure to run Create() before using this\nfunc (manifest *fileManifest) AddDirectory(path string) error {\n\treturn manifest.encoder.Encode(fileManifestEntry{Path: path, IsDir: true})\n}\n\n// Close closes the manifest file handle\nfunc (manifest *fileManifest) Close() error {\n\treturn manifest.fileHandle.Close()\n}\n\nfunc NewFileManifest(manifestFolder string, manifestFile string) *fileManifest {\n\treturn &fileManifest{ManifestFolder: manifestFolder, ManifestFile: manifestFile}\n}\n\n// Custom errors\n\n// PathIsNotDirectory is returned when the given path is unexpectedly not a directory.\ntype PathIsNotDirectory struct {\n\tpath string\n}\n\nfunc (err PathIsNotDirectory) Error() string {\n\treturn err.path + \" is not a directory\"\n}\n\n// PathIsNotFile is returned when the given path is unexpectedly not a file.\ntype PathIsNotFile struct {\n\tpath string\n}\n\nfunc (err PathIsNotFile) Error() string {\n\treturn err.path + \" is not a file\"\n}\n\n// ListTfFiles returns a list of all TF files in the specified directory.\nfunc ListTfFiles(directoryPath string, walkWithSymlinks bool) ([]string, error) {\n\tvar tfFiles []string\n\n\twalkFunc := filepath.WalkDir\n\tif walkWithSymlinks {\n\t\twalkFunc = WalkDirWithSymlinks\n\t}\n\n\terr := walkFunc(directoryPath, func(path string, d fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !d.IsDir() && IsTFFile(path) {\n\t\t\ttfFiles = append(tfFiles, path)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\treturn tfFiles, err\n}\n\n// IsDirectoryEmpty - returns true if the given path exists and is a empty directory.\nfunc IsDirectoryEmpty(dirPath string) (bool, error) {\n\tdir, err := os.Open(dirPath)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tdefer func() {\n\t\t_ = dir.Close()\n\t}()\n\n\t_, err = dir.Readdir(1)\n\tif err == nil {\n\t\treturn false, nil\n\t}\n\n\treturn true, nil\n}\n\n// GetCacheDir returns the global terragrunt cache directory for the current user.\nfunc GetCacheDir() (string, error) {\n\tcacheDir, err := os.UserCacheDir()\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\tcacheDir = filepath.Join(cacheDir, \"terragrunt\")\n\n\tif !FileExists(cacheDir) {\n\t\tif err := os.MkdirAll(cacheDir, os.ModePerm); err != nil {\n\t\t\treturn \"\", errors.New(err)\n\t\t}\n\t}\n\n\treturn cacheDir, nil\n}\n\n// GetTempDir returns the global terragrunt temp directory.\nfunc GetTempDir() (string, error) {\n\ttempDir := filepath.Join(os.TempDir(), \"terragrunt\")\n\n\tif !FileExists(tempDir) {\n\t\tif err := os.MkdirAll(tempDir, os.ModePerm); err != nil {\n\t\t\treturn \"\", errors.New(err)\n\t\t}\n\t}\n\n\treturn tempDir, nil\n}\n\n// ExcludeFiltersFromFile returns a list of filters from the given filename, where each filter starts on a new line.\n//\n// Note that this is a backwards compatibility implementation for the `--queue-excludes-file` flag, so it's going to\n// append the ! prefix to each filter to negate it.\nfunc ExcludeFiltersFromFile(baseDir, filename string) ([]string, error) {\n\tfilename, err := CanonicalPath(filename, baseDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !FileExists(filename) || !IsFile(filename) {\n\t\treturn nil, nil\n\t}\n\n\tcontent, err := ReadFileAsString(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar (\n\t\tlines   = strings.Split(strings.ReplaceAll(content, \"\\r\\n\", \"\\n\"), \"\\n\")\n\t\tfilters = make([]string, 0, len(lines))\n\t)\n\n\tfor _, dir := range lines {\n\t\tdir = strings.TrimSpace(dir)\n\t\tif dir == \"\" || strings.HasPrefix(dir, \"#\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tfilters = append(filters, \"!\"+dir)\n\t}\n\n\treturn filters, nil\n}\n\n// GetFiltersFromFile returns a list of filter queries from the given filename, where each filter query starts on a new line.\nfunc GetFiltersFromFile(baseDir, filename string) ([]string, error) {\n\tfilename, err := CanonicalPath(filename, baseDir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !FileExists(filename) || !IsFile(filename) {\n\t\treturn nil, nil\n\t}\n\n\tcontent, err := ReadFileAsString(filename)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar (\n\t\tlines   = strings.Split(strings.ReplaceAll(content, \"\\r\\n\", \"\\n\"), \"\\n\")\n\t\tfilters = make([]string, 0, len(lines))\n\t)\n\n\tfor _, filter := range lines {\n\t\tfilter = strings.TrimSpace(filter)\n\t\tif filter == \"\" || strings.HasPrefix(filter, \"#\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tfilters = append(filters, filter)\n\t}\n\n\treturn filters, nil\n}\n\n// MatchSha256Checksum returns the SHA256 checksum for the given file and filename.\nfunc MatchSha256Checksum(file, filename []byte) []byte {\n\tvar checksum []byte\n\n\tfor line := range bytes.SplitSeq(file, []byte(\"\\n\")) {\n\t\tparts := bytes.Fields(line)\n\t\tif len(parts) > 1 && bytes.Equal(parts[1], filename) {\n\t\t\tchecksum = parts[0]\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif checksum == nil {\n\t\treturn nil\n\t}\n\n\treturn checksum\n}\n\n// FileSHA256 calculates the SHA256 hash of the file at the given path.\nfunc FileSHA256(filePath string) ([]byte, error) {\n\tfile, err := os.Open(filePath)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\tdefer file.Close() //nolint:errcheck\n\n\thash := sha256.New()\n\tbuffer := make([]byte, ChecksumReadBlock)\n\n\tfor {\n\t\tn, err := file.Read(buffer)\n\t\tif err != nil && err != io.EOF {\n\t\t\treturn nil, errors.New(err)\n\t\t}\n\n\t\tif n == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tif _, err := hash.Write(buffer[:n]); err != nil {\n\t\t\treturn nil, errors.New(err)\n\t\t}\n\t}\n\n\treturn hash.Sum(nil), nil\n}\n\n// readerFunc is syntactic sugar for read interface.\ntype readerFunc func(data []byte) (int, error)\n\nfunc (rf readerFunc) Read(data []byte) (int, error) { return rf(data) }\n\n// writerFunc is syntactic sugar for write interface.\ntype writerFunc func(data []byte) (int, error)\n\nfunc (wf writerFunc) Write(data []byte) (int, error) { return wf(data) }\n\n// Copy is a io.Copy cancellable by context.\nfunc Copy(ctx context.Context, dst io.Writer, src io.Reader) (int64, error) {\n\tnum, err := io.Copy(\n\t\twriterFunc(func(data []byte) (int, error) {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\t// context has been canceled stop process and propagate \"context canceled\" error.\n\t\t\t\treturn 0, ctx.Err()\n\t\t\tdefault:\n\t\t\t\t// otherwise just run default io.Writer implementation.\n\t\t\t\treturn dst.Write(data)\n\t\t\t}\n\t\t}),\n\t\treaderFunc(func(data []byte) (int, error) {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\t// context has been canceled stop process and propagate \"context canceled\" error.\n\t\t\t\treturn 0, ctx.Err()\n\t\t\tdefault:\n\t\t\t\t// otherwise just run default io.Reader implementation.\n\t\t\t\treturn src.Read(data)\n\t\t\t}\n\t\t}),\n\t)\n\tif err != nil {\n\t\terr = errors.New(err)\n\t}\n\n\treturn num, err\n}\n\n// evalRealPathForWalkDir evaluates symlinks and returns the real path and whether it's a directory.\nfunc evalRealPathForWalkDir(currentPath string) (string, bool, error) {\n\trealPath, err := filepath.EvalSymlinks(currentPath)\n\tif err != nil {\n\t\treturn \"\", false, errors.Errorf(\"failed to evaluate symlinks for %s: %w\", currentPath, err)\n\t}\n\n\trealInfo, err := os.Stat(realPath)\n\tif err != nil {\n\t\treturn \"\", false, errors.Errorf(\"failed to describe file %s: %w\", realPath, err)\n\t}\n\n\treturn realPath, realInfo.IsDir(), nil\n}\n\n// WalkDirWithSymlinks traverses a directory tree using filepath.WalkDir, following symbolic links\n// and calling the provided function for each file or directory encountered. It handles both regular\n// symlinks and circular symlinks without getting into infinite loops.\n//\n//nolint:funlen\nfunc WalkDirWithSymlinks(root string, externalWalkFn fs.WalkDirFunc) error {\n\t// pathPair keeps track of both the physical (real) path on disk\n\t// and the logical path (how it appears in the walk)\n\ttype pathPair struct {\n\t\tphysical string\n\t\tlogical  string\n\t}\n\n\t// visited tracks symlink paths to prevent circular references\n\t// key is combination of realPath:symlinkPath\n\tvisited := make(map[string]bool)\n\n\t// visitedLogical tracks logical paths to prevent duplicates\n\t// when the same directory is reached through different symlinks\n\tvisitedLogical := make(map[string]bool)\n\n\tvar walkFn func(pathPair) error\n\n\twalkFn = func(pair pathPair) error {\n\t\treturn filepath.WalkDir(pair.physical, func(currentPath string, d fs.DirEntry, err error) error {\n\t\t\tif err != nil {\n\t\t\t\treturn externalWalkFn(currentPath, d, err)\n\t\t\t}\n\n\t\t\t// Convert the current physical path to a logical path relative to the walk root\n\t\t\trel, err := filepath.Rel(pair.physical, currentPath)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Errorf(\"failed to get relative path between %s and %s: %w\", pair.physical, currentPath, err)\n\t\t\t}\n\n\t\t\tlogicalPath := filepath.Join(pair.logical, rel)\n\n\t\t\t// Call the provided function only if we haven't seen this logical path before\n\t\t\tif !visitedLogical[logicalPath] {\n\t\t\t\tvisitedLogical[logicalPath] = true\n\n\t\t\t\tif err := externalWalkFn(logicalPath, d, nil); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// If we encounter a symlink, resolve and follow it\n\t\t\tif d.Type()&fs.ModeSymlink != 0 {\n\t\t\t\trealPath, isDir, evalErr := evalRealPathForWalkDir(currentPath)\n\t\t\t\tif evalErr != nil {\n\t\t\t\t\treturn evalErr\n\t\t\t\t}\n\n\t\t\t\t// Skip if we've seen this symlink->target combination before\n\t\t\t\t// This prevents infinite loops with circular symlinks\n\t\t\t\tif visited[realPath+\":\"+currentPath] {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tvisited[realPath+\":\"+currentPath] = true\n\n\t\t\t\t// If the target is a directory, recursively walk it\n\t\t\t\tif isDir {\n\t\t\t\t\treturn walkFn(pathPair{\n\t\t\t\t\t\tphysical: realPath,\n\t\t\t\t\t\tlogical:  logicalPath,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\trealRoot, err := filepath.EvalSymlinks(root)\n\tif err != nil {\n\t\treturn errors.Errorf(\"failed to evaluate symlinks for %s: %w\", root, err)\n\t}\n\n\t// Start the walk from the root directory\n\treturn walkFn(pathPair{\n\t\tphysical: realRoot,\n\t\tlogical:  realRoot,\n\t})\n}\n\n// SanitizePath resolves a file path within a base directory, returning the sanitized path or an error if it attempts\n// to access anything outside the base directory.\nfunc SanitizePath(baseDir string, file string) (string, error) {\n\tif baseDir == \"\" || file == \"\" {\n\t\treturn \"\", errors.New(\"baseDir and file must be provided\")\n\t}\n\n\tfile, err := url.QueryUnescape(file)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tbaseDir, err = url.QueryUnescape(baseDir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\troot, err := os.OpenRoot(baseDir)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer root.Close() //nolint:errcheck\n\n\tfileInfo, err := root.Stat(file)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfullPath := baseDir + string(os.PathSeparator) + fileInfo.Name()\n\n\treturn fullPath, nil\n}\n\n// RelPathForLog returns a relative path suitable for logging.\n// If the path cannot be made relative, it returns the original path.\n// Paths that don't start with \"..\" get a \"./\" prefix for clarity.\n// If showAbsPath is true, the original targetPath is returned unchanged.\nfunc RelPathForLog(basePath, targetPath string, showAbsPath bool) string {\n\tif showAbsPath {\n\t\treturn targetPath\n\t}\n\n\tif relPath, err := filepath.Rel(basePath, targetPath); err == nil {\n\t\tif relPath == \".\" {\n\t\t\treturn targetPath\n\t\t}\n\n\t\t// Add \"./\" prefix for paths within the base directory for clarity\n\t\tif !strings.HasPrefix(relPath, \"..\") {\n\t\t\treturn \".\" + string(filepath.Separator) + relPath\n\t\t}\n\n\t\treturn relPath\n\t}\n\n\treturn targetPath\n}\n\n// ResolvePath resolves symlinks in a path for consistent comparison across platforms.\n// On macOS, /var is a symlink to /private/var, so paths must be resolved.\n// Returns the original path if symlink resolution fails.\nfunc ResolvePath(path string) string {\n\tresolved, err := filepath.EvalSymlinks(path)\n\tif err != nil {\n\t\treturn path\n\t}\n\n\treturn resolved\n}\n\n// MoveFile attempts to rename a file from source to destination, if this fails\n// due to invalid cross-device link it falls back to copying the file contents\n// and deleting the original file.\nfunc MoveFile(source string, destination string) error {\n\tif renameErr := os.Rename(source, destination); renameErr != nil {\n\t\tvar sysErr syscall.Errno\n\t\tif errors.As(renameErr, &sysErr) && sysErr == syscall.EXDEV {\n\t\t\tif moveErr := CopyFile(source, destination); moveErr != nil {\n\t\t\t\treturn moveErr\n\t\t\t}\n\n\t\t\treturn os.Remove(source)\n\t\t}\n\n\t\treturn renameErr\n\t}\n\n\treturn nil\n}\n\n// SkipDirIfIgnorable checks if an entire directory should be skipped based on the fact that it's\n// in a directory that should never have components discovered in it.\nfunc SkipDirIfIgnorable(dir string) error {\n\tswitch dir {\n\tcase GitDir, TerraformCacheDir, TerragruntCacheDir:\n\t\treturn filepath.SkipDir\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/util/file_test.go",
    "content": "package util_test\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetPathRelativeTo(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tpath     string\n\t\tbasePath string\n\t\texpected string\n\t}{\n\t\t{\"\", \"\", \".\"},\n\t\t{helpers.RootFolder, helpers.RootFolder, \".\"},\n\t\t{helpers.RootFolder, helpers.RootFolder + \"child\", \"..\"},\n\t\t{helpers.RootFolder, helpers.RootFolder + \"child/sub-child/sub-sub-child\", \"../../..\"},\n\t\t{helpers.RootFolder + \"other-child\", helpers.RootFolder + \"child\", \"../other-child\"},\n\t\t{helpers.RootFolder + \"other-child/sub-child\", helpers.RootFolder + \"child/sub-child\", \"../../other-child/sub-child\"},\n\t\t{helpers.RootFolder + \"root\", helpers.RootFolder + \"other-root\", \"../root\"},\n\t\t{helpers.RootFolder + \"root\", helpers.RootFolder + \"other-root/sub-child/sub-sub-child\", \"../../../root\"},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual, err := util.GetPathRelativeTo(tc.path, tc.basePath)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, actual, \"For path %s and basePath %s\", tc.path, tc.basePath)\n\t\t})\n\t}\n}\n\nfunc TestCanonicalPath(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tpath     string\n\t\tbasePath string\n\t\texpected string\n\t}{\n\t\t{\"\", helpers.RootFolder + \"foo\", helpers.RootFolder + \"foo\"},\n\t\t{\".\", helpers.RootFolder + \"foo\", helpers.RootFolder + \"foo\"},\n\t\t{\"bar\", helpers.RootFolder + \"foo\", helpers.RootFolder + \"foo/bar\"},\n\t\t{\"bar/baz/blah\", helpers.RootFolder + \"foo\", helpers.RootFolder + \"foo/bar/baz/blah\"},\n\t\t{\"bar/../blah\", helpers.RootFolder + \"foo\", helpers.RootFolder + \"foo/blah\"},\n\t\t{\"bar/../..\", helpers.RootFolder + \"foo\", helpers.RootFolder},\n\t\t{\"bar/.././../baz\", helpers.RootFolder + \"foo\", helpers.RootFolder + \"baz\"},\n\t\t{\"bar\", helpers.RootFolder + \"foo/../baz\", helpers.RootFolder + \"baz/bar\"},\n\t\t{\"a/b/../c/d/..\", helpers.RootFolder + \"foo/../baz/.\", helpers.RootFolder + \"baz/a/c\"},\n\t\t{helpers.RootFolder + \"other\", helpers.RootFolder + \"foo\", helpers.RootFolder + \"other\"},\n\t\t{helpers.RootFolder + \"other/bar/blah\", helpers.RootFolder + \"foo\", helpers.RootFolder + \"other/bar/blah\"},\n\t\t{helpers.RootFolder + \"other/../blah\", helpers.RootFolder + \"foo\", helpers.RootFolder + \"blah\"},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual, err := util.CanonicalPath(tc.path, tc.basePath)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, actual, \"For path %s and basePath %s\", tc.path, tc.basePath)\n\t\t})\n\t}\n}\n\nfunc TestPathContainsHiddenFileOrFolder(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tpath     string\n\t\texpected bool\n\t}{\n\t\t{\"\", false},\n\t\t{\".\", false},\n\t\t{\".foo\", true},\n\t\t{\".foo/\", true},\n\t\t{\"foo/bar\", false},\n\t\t{\"/foo/bar\", false},\n\t\t{\".foo/bar\", true},\n\t\t{\"foo/.bar\", true},\n\t\t{\"/foo/.bar\", true},\n\t\t{\"/foo/./bar\", false},\n\t\t{\"/foo/../bar\", false},\n\t\t{\"/foo/.././bar\", false},\n\t\t{\"/foo/.././.bar\", true},\n\t\t{\"/foo/.././.bar/\", true},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.path, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tpath := filepath.FromSlash(tc.path)\n\t\t\tactual := util.TerragruntExcludes(path)\n\t\t\tassert.Equal(t, tc.expected, actual, \"For path %s\", path)\n\t\t})\n\t}\n}\n\nfunc TestJoinTerraformModulePath(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tmodulesFolder string\n\t\tpath          string\n\t\texpected      string\n\t}{\n\t\t{\"foo\", \"bar\", \"foo//bar\"},\n\t\t{\"foo/\", \"bar\", \"foo//bar\"},\n\t\t{\"foo\", \"/bar\", \"foo//bar\"},\n\t\t{\"foo/\", \"/bar\", \"foo//bar\"},\n\t\t{\"foo//\", \"/bar\", \"foo//bar\"},\n\t\t{\"foo//\", \"//bar\", \"foo//bar\"},\n\t\t{\"/foo/bar/baz\", \"/a/b/c\", \"/foo/bar/baz//a/b/c\"},\n\t\t{\"/foo/bar/baz/\", \"//a/b/c\", \"/foo/bar/baz//a/b/c\"},\n\t\t{\"/foo?ref=feature/1\", \"bar\", \"/foo//bar?ref=feature/1\"},\n\t\t{\"/foo?ref=feature/1\", \"/bar\", \"/foo//bar?ref=feature/1\"},\n\t\t{\"/foo//?ref=feature/1\", \"/bar\", \"/foo//bar?ref=feature/1\"},\n\t\t{\"/foo//?ref=feature/1\", \"//bar\", \"/foo//bar?ref=feature/1\"},\n\t\t{\"/foo/bar/baz?ref=feature/1\", \"/a/b/c\", \"/foo/bar/baz//a/b/c?ref=feature/1\"},\n\t\t{\"/foo/bar/baz/?ref=feature/1\", \"//a/b/c\", \"/foo/bar/baz//a/b/c?ref=feature/1\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"%s-%s\", tc.modulesFolder, tc.path), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := util.JoinTerraformModulePath(tc.modulesFolder, tc.path)\n\t\t\tassert.Equal(t, tc.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestFileManifest(t *testing.T) {\n\tt.Parallel()\n\n\tfiles := []string{\"file1\", \"file2\"}\n\n\tvar testfiles = make([]string, 0, len(files))\n\n\t// create temp dir\n\tdir := helpers.TmpDirWOSymlinks(t)\n\n\tfor _, file := range files {\n\t\t// create temp files in the dir\n\t\tf, err := os.CreateTemp(dir, file)\n\t\trequire.NoError(t, err)\n\t\t// Close the file handle immediately after creation\n\t\trequire.NoError(t, f.Close())\n\t\ttestfiles = append(testfiles, f.Name())\n\t}\n\t// will later test if the file already doesn't exist\n\ttestfiles = append(testfiles, path.Join(dir, \"ephemeral-file-that-doesnt-exist.txt\"))\n\n\t// create a manifest\n\tl := logger.CreateLogger()\n\tmanifest := util.NewFileManifest(dir, \".terragrunt-test-manifest\")\n\trequire.NoError(t, manifest.Create())\n\t// check the file manifest has been created\n\tassert.FileExists(t, filepath.Join(manifest.ManifestFolder, manifest.ManifestFile))\n\n\tfor _, file := range testfiles {\n\t\trequire.NoError(t, manifest.AddFile(file))\n\t}\n\t// check for a non-existent directory as well\n\tassert.NoError(t, manifest.AddDirectory(path.Join(dir, \"ephemeral-directory-that-doesnt-exist\")))\n\n\t// Close the manifest file handle before cleaning\n\trequire.NoError(t, manifest.Close())\n\n\tassert.NoError(t, manifest.Clean(l))\n\t// test if the files have been deleted\n\tfor _, file := range testfiles {\n\t\tassert.False(t, util.FileExists(file))\n\t}\n}\n\nfunc TestContainsPath(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tpath     string\n\t\tsubpath  string\n\t\texpected bool\n\t}{\n\t\t{\"\", \"\", true},\n\t\t{\"/\", \"/\", true},\n\t\t{\"foo/bar/.tf/tg.hcl\", \"foo/bar\", true},\n\t\t{\"/foo/bar/.tf/tg.hcl\", \"foo/bar\", true},\n\t\t{\"foo/bar/.tf/tg.hcl\", \"bar\", true},\n\t\t{\"foo/bar/.tf/tg.hcl\", \".tf/tg.hcl\", true},\n\t\t{\"foo/bar/.tf/tg.hcl\", \"tg.hcl\", true},\n\n\t\t{\"foo/bar/.tf/tg.hcl\", \"/bar\", false},\n\t\t{\"/foo/bar/.tf/tg.hcl\", \"/bar\", false},\n\t\t{\"foo/bar\", \"foo/bar/gee\", false},\n\t\t{\"foo/bar/.tf/tg.hcl\", \"foo/barf\", false},\n\t\t{\"foo/bar/.tf/tg.hcl\", \"foo/ba\", false},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := util.ContainsPath(tc.path, tc.subpath)\n\t\t\tassert.Equal(t, tc.expected, actual, \"For path %s and subpath %s\", tc.path, tc.subpath)\n\t\t})\n\t}\n}\n\nfunc TestHasPathPrefix(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tpath     string\n\t\tprefix   string\n\t\texpected bool\n\t}{\n\t\t{\"\", \"\", true},\n\t\t{\"/\", \"/\", true},\n\t\t{\"foo/bar/.tf/tg.hcl\", \"foo\", true},\n\t\t{\"/foo/bar/.tf/tg.hcl\", \"/foo\", true},\n\t\t{\"foo/bar/.tf/tg.hcl\", \"foo/bar\", true},\n\t\t{\"/foo/bar/.tf/tg.hcl\", \"/foo/bar\", true},\n\n\t\t{\"/\", \"\", false},\n\t\t{\"foo\", \"foo/bar/.tf/tg.hcl\", false},\n\t\t{\"/foo/bar/.tf/tg.hcl\", \"foo\", false},\n\t\t{\"/foo/bar/.tf/tg.hcl\", \"bar/.tf\", false},\n\t\t{\"/foo/bar/.tf/tg.hcl\", \"/foo/barf\", false},\n\t\t{\"/foo/bar/.tf/tg.hcl\", \"/foo/ba\", false},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := util.HasPathPrefix(tc.path, tc.prefix)\n\t\t\tassert.Equal(t, tc.expected, actual, \"For path %s and prefix %s\", tc.path, tc.prefix)\n\t\t})\n\t}\n}\n\nfunc TestIncludeInCopy(t *testing.T) {\n\tt.Parallel()\n\n\tincludeInCopy := []string{\"_module/.region2\", \"**/app2\", \"**/.include-me-too\"}\n\n\ttestCases := []struct {\n\t\tpath         string\n\t\tcopyExpected bool\n\t}{\n\t\t{\"/app/terragrunt.hcl\", true},\n\t\t{\"/_module/main.tf\", true},\n\t\t{\"/_module/.region1/info.txt\", false},\n\t\t{\"/_module/.region3/project3-1/f1-2-levels.txt\", false},\n\t\t{\"/_module/.region3/project3-1/app1/.include-me-too/file.txt\", true},\n\t\t{\"/_module/.region3/project3-2/.f0/f0-3-levels.txt\", false},\n\t\t{\"/_module/.region2/.project2-1/app2/f2-dot-f2.txt\", true},\n\t\t{\"/_module/.region2/.project2-1/readme.txt\", true},\n\t\t{\"/_module/.region2/project2-2/f2-dot-f0.txt\", true},\n\t}\n\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\tsource := filepath.Join(tempDir, \"source\")\n\tdestination := filepath.Join(tempDir, \"destination\")\n\n\tfileContent := []byte(\"source file\")\n\n\tfor _, tc := range testCases {\n\t\tpath := filepath.Join(source, tc.path)\n\t\tassert.NoError(t, os.MkdirAll(filepath.Dir(path), os.ModePerm))\n\t\tassert.NoError(t, os.WriteFile(path, fileContent, 0644))\n\t}\n\n\trequire.NoError(t, util.CopyFolderContents(logger.CreateLogger(), source, destination, \".terragrunt-test\", includeInCopy, nil))\n\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t_, err := os.Stat(filepath.Join(destination, tc.path))\n\t\t\tassert.True(t,\n\t\t\t\ttc.copyExpected && err == nil ||\n\t\t\t\t\t!tc.copyExpected && errors.Is(err, os.ErrNotExist),\n\t\t\t\t\"Unexpected copy result for file '%s' (should be copied: '%t') - got error: %s\", tc.path, tc.copyExpected, err)\n\t\t})\n\t}\n}\n\nfunc TestExcludeFromCopy(t *testing.T) {\n\tt.Parallel()\n\n\texcludeFromCopy := []string{\"module/region2\", \"**/exclude-me-here\", \"**/app1\"}\n\n\ttestCases := []struct {\n\t\tpath         string\n\t\tcopyExpected bool\n\t}{\n\t\t{\"/app/terragrunt.hcl\", true},\n\t\t{\"/module/main.tf\", true},\n\t\t{\"/module/region1/info.txt\", true},\n\t\t{\"/module/region1/project2-1/app1/f2-dot-f2.txt\", false},\n\t\t{\"/module/region3/project3-1/f1-2-levels.txt\", true},\n\t\t{\"/module/region3/project3-1/app1/exclude-me-here/file.txt\", false},\n\t\t{\"/module/region3/project3-2/f0/f0-3-levels.txt\", true},\n\t\t{\"/module/region2/project2-1/app2/f2-dot-f2.txt\", false},\n\t\t{\"/module/region2/project2-1/readme.txt\", false},\n\t\t{\"/module/region2/project2-2/f2-dot-f0.txt\", false},\n\t}\n\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\tsource := filepath.Join(tempDir, \"source\")\n\tdestination := filepath.Join(tempDir, \"destination\")\n\n\tfileContent := []byte(\"source file\")\n\n\tfor _, tc := range testCases {\n\t\tpath := filepath.Join(source, tc.path)\n\t\tassert.NoError(t, os.MkdirAll(filepath.Dir(path), os.ModePerm))\n\t\tassert.NoError(t, os.WriteFile(path, fileContent, 0644))\n\t}\n\n\trequire.NoError(t, util.CopyFolderContents(logger.CreateLogger(), source, destination, \".terragrunt-test\", nil, excludeFromCopy))\n\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t_, err := os.Stat(filepath.Join(destination, tc.path))\n\t\t\tassert.True(t,\n\t\t\t\ttc.copyExpected && err == nil ||\n\t\t\t\t\t!tc.copyExpected && errors.Is(err, os.ErrNotExist),\n\t\t\t\t\"Unexpected copy result for file '%s' (should be copied: '%t') - got error: %s\", tc.path, tc.copyExpected, err)\n\t\t})\n\t}\n}\n\nfunc TestExcludeIncludeBehaviourPriority(t *testing.T) {\n\tt.Parallel()\n\n\tincludeInCopy := []string{\"_module/.region2\", \"_module/.region3\"}\n\texcludeFromCopy := []string{\"**/.project2-2\", \"_module/.region3\"}\n\n\ttestCases := []struct {\n\t\tpath         string\n\t\tcopyExpected bool\n\t}{\n\t\t{\"/_module/.region2/.project2-1/app2/f2-dot-f2.txt\", true},\n\t\t{\"/_module/.region2/.project2-1/readme.txt\", true},\n\t\t{\"/_module/.region2/.project2-2/f2-dot-f0.txt\", false},\n\t\t{\"/_module/.region3/.project2-1/readme.txt\", false},\n\t}\n\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\tsource := filepath.Join(tempDir, \"source\")\n\tdestination := filepath.Join(tempDir, \"destination\")\n\n\tfileContent := []byte(\"source file\")\n\n\tfor _, tc := range testCases {\n\t\tpath := filepath.Join(source, tc.path)\n\t\tassert.NoError(t, os.MkdirAll(filepath.Dir(path), os.ModePerm))\n\t\tassert.NoError(t, os.WriteFile(path, fileContent, 0644))\n\t}\n\n\trequire.NoError(t, util.CopyFolderContents(logger.CreateLogger(), source, destination, \".terragrunt-test\", includeInCopy, excludeFromCopy))\n\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t_, err := os.Stat(filepath.Join(destination, tc.path))\n\t\t\tassert.True(t,\n\t\t\t\ttc.copyExpected && err == nil ||\n\t\t\t\t\t!tc.copyExpected && errors.Is(err, os.ErrNotExist),\n\t\t\t\t\"Unexpected copy result for file '%s' (should be copied: '%t') - got error: %s\", tc.path, tc.copyExpected, err)\n\t\t})\n\t}\n}\n\nfunc TestEmptyDir(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tpath        string\n\t\texpectEmpty bool\n\t}{\n\t\t{helpers.TmpDirWOSymlinks(t), true},\n\t\t{os.TempDir(), false},\n\t}\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\temptyValue, err := util.IsDirectoryEmpty(tc.path)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expectEmpty, emptyValue, \"For path %s\", tc.path)\n\t\t})\n\t}\n}\n\n//nolint:funlen\nfunc TestWalkWithSimpleSymlinks(t *testing.T) {\n\tt.Parallel()\n\t// Create a temporary test directory structure\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\ttempDir, err := filepath.EvalSymlinks(tempDir)\n\trequire.NoError(t, err)\n\n\t// Create directories\n\tdirs := []string{\"a\", \"d\"}\n\tfor _, dir := range dirs {\n\t\trequire.NoError(t, os.Mkdir(filepath.Join(tempDir, dir), 0755))\n\t}\n\n\t// Create test files\n\ttestFile := filepath.Join(tempDir, \"a\", \"test.txt\")\n\trequire.NoError(t, os.WriteFile(testFile, []byte(\"test\"), 0644))\n\n\t// Create symlinks\n\trequire.NoError(t, os.Symlink(filepath.Join(tempDir, \"a\"), filepath.Join(tempDir, \"b\")))\n\trequire.NoError(t, os.Symlink(filepath.Join(tempDir, \"a\"), filepath.Join(tempDir, \"c\")))\n\trequire.NoError(t, os.Symlink(filepath.Join(tempDir, \"a\"), filepath.Join(tempDir, \"d\", \"a\")))\n\n\tvar paths []string\n\n\terr = util.WalkDirWithSymlinks(tempDir, func(path string, _ fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\trelPath, err := filepath.Rel(tempDir, path)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tpaths = append(paths, relPath)\n\n\t\treturn nil\n\t})\n\trequire.NoError(t, err)\n\n\t// Sort paths for reliable comparison\n\tsort.Strings(paths)\n\n\t// Expected paths should include original and symlinked locations\n\texpectedPaths := []string{\n\t\t\".\",\n\t\t\"a\",\n\t\tfilepath.Join(\"a\", \"test.txt\"),\n\t\t\"b\",\n\t\tfilepath.Join(\"b\", \"test.txt\"),\n\t\t\"c\",\n\t\tfilepath.Join(\"c\", \"test.txt\"),\n\t\t\"d\",\n\t\tfilepath.Join(\"d\", \"a\"),\n\t\tfilepath.Join(\"d\", \"a\", \"test.txt\"),\n\t}\n\tsort.Strings(expectedPaths)\n\n\tif len(paths) != len(expectedPaths) {\n\t\tt.Errorf(\"Got %d paths, expected %d\", len(paths), len(expectedPaths))\n\t}\n\n\tfor expectedPath := range expectedPaths {\n\t\tif expectedPath >= len(paths) {\n\t\t\tt.Errorf(\"Missing expected path: %s\", expectedPaths[expectedPath])\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif paths[expectedPath] != expectedPaths[expectedPath] {\n\t\t\tt.Errorf(\"Path mismatch at index %d:\\ngot:  %s\\nwant: %s\", expectedPath, paths[expectedPath], expectedPaths[expectedPath])\n\t\t}\n\t}\n}\n\n//nolint:funlen\nfunc TestWalkWithCircularSymlinks(t *testing.T) {\n\tt.Parallel()\n\t// Create temporary test directory structure\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\ttempDir, err := filepath.EvalSymlinks(tempDir)\n\trequire.NoError(t, err)\n\n\t// Create directories\n\tdirs := []string{\"a\", \"b\", \"c\", \"d\"}\n\tfor _, dir := range dirs {\n\t\trequire.NoError(t, os.Mkdir(filepath.Join(tempDir, dir), 0755))\n\t}\n\n\t// Create test files\n\ttestFile := filepath.Join(tempDir, \"a\", \"test.txt\")\n\trequire.NoError(t, os.WriteFile(testFile, []byte(\"test\"), 0644))\n\n\t// Create symlinks\n\trequire.NoError(t, os.Symlink(filepath.Join(tempDir, \"a\"), filepath.Join(tempDir, \"b\", \"link-to-a\")))\n\trequire.NoError(t, os.Symlink(filepath.Join(tempDir, \"a\"), filepath.Join(tempDir, \"c\", \"another-link-to-a\")))\n\n\t// Create circular symlink\n\trequire.NoError(t, os.Symlink(filepath.Join(tempDir, \"d\"), filepath.Join(tempDir, \"a\", \"link-to-d\")))\n\trequire.NoError(t, os.Symlink(filepath.Join(tempDir, \"a\"), filepath.Join(tempDir, \"d\", \"link-to-a\")))\n\n\tvar paths []string\n\n\terr = util.WalkDirWithSymlinks(tempDir, func(path string, _ fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\trelPath, err := filepath.Rel(tempDir, path)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tpaths = append(paths, relPath)\n\n\t\treturn nil\n\t})\n\trequire.NoError(t, err)\n\n\t// Sort paths for reliable comparison\n\tsort.Strings(paths)\n\n\t// Expected paths should include original and symlinked locations\n\texpectedPaths := []string{\n\t\t\".\",\n\t\t\"a\",\n\t\tfilepath.Join(\"a\", \"link-to-d\"),\n\t\tfilepath.Join(\"a\", \"link-to-d\", \"link-to-a\"),\n\t\tfilepath.Join(\"a\", \"link-to-d\", \"link-to-a\", \"link-to-d\"),\n\t\tfilepath.Join(\"a\", \"link-to-d\", \"link-to-a\", \"test.txt\"),\n\t\tfilepath.Join(\"a\", \"test.txt\"),\n\t\t\"b\",\n\t\tfilepath.Join(\"b\", \"link-to-a\"),\n\t\tfilepath.Join(\"b\", \"link-to-a\", \"link-to-d\"),\n\t\tfilepath.Join(\"b\", \"link-to-a\", \"test.txt\"),\n\t\t\"c\",\n\t\tfilepath.Join(\"c\", \"another-link-to-a\"),\n\t\tfilepath.Join(\"c\", \"another-link-to-a\", \"link-to-d\"),\n\t\tfilepath.Join(\"c\", \"another-link-to-a\", \"test.txt\"),\n\t\t\"d\",\n\t\tfilepath.Join(\"d\", \"link-to-a\"),\n\t}\n\tsort.Strings(expectedPaths)\n\n\tif len(paths) != len(expectedPaths) {\n\t\tt.Errorf(\"Got %d paths, expected %d\", len(paths), len(expectedPaths))\n\t}\n\n\tfor expectedPath := range expectedPaths {\n\t\tif expectedPath >= len(paths) {\n\t\t\tt.Errorf(\"Missing expected path: %s\", expectedPaths[expectedPath])\n\n\t\t\tcontinue\n\t\t}\n\n\t\tif paths[expectedPath] != expectedPaths[expectedPath] {\n\t\t\tt.Errorf(\"Path mismatch at index %d:\\ngot:  %s\\nwant: %s\", expectedPath, paths[expectedPath], expectedPaths[expectedPath])\n\t\t}\n\t}\n}\n\nfunc TestWalkDirWithSymlinksErrors(t *testing.T) {\n\tt.Parallel()\n\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Test with non-existent directory\n\trequire.Error(t, util.WalkDirWithSymlinks(filepath.Join(tempDir, \"nonexistent\"), func(_ string, _ fs.DirEntry, err error) error {\n\t\treturn err\n\t}))\n\n\t// Test with broken symlink\n\tbrokenLink := filepath.Join(tempDir, \"broken\")\n\trequire.NoError(t, os.Symlink(filepath.Join(tempDir, \"nonexistent\"), brokenLink))\n\n\trequire.Error(t, util.WalkDirWithSymlinks(tempDir, func(_ string, _ fs.DirEntry, err error) error {\n\t\treturn err\n\t}))\n}\n\nfunc Test_sanitizePath(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname    string\n\t\tbaseDir string\n\t\tfile    string\n\t\twant    string\n\t\twantErr bool\n\t}{\n\t\t{\n\t\t\tname:    \"happy path\",\n\t\t\tbaseDir: \"./testdata/fixture-sanitize-path/env/unit\",\n\t\t\tfile:    \".terraform-version\",\n\t\t\twant:    \"./testdata/fixture-sanitize-path/env/unit/.terraform-version\",\n\t\t},\n\t\t{\n\t\t\tname:    \"base dir is empty\",\n\t\t\tbaseDir: \"\",\n\t\t\tfile:    \".terraform-version\",\n\t\t\twant:    \"\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"try to escape base dir\",\n\t\t\tbaseDir: \"./testdata/fixture-sanitize-path/env/unit\",\n\t\t\tfile:    \"../../../dev/random\",\n\t\t\twant:    \"\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"file is empty\",\n\t\t\tbaseDir: \"./testdata/fixture-sanitize-path/env/unit\",\n\t\t\tfile:    \"\",\n\t\t\twant:    \"\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"file is just a slash\",\n\t\t\tbaseDir: \"./testdata/fixture-sanitize-path/env/unit\",\n\t\t\tfile:    \"/\",\n\t\t\twant:    \"\",\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname:    \"file is just a dot\",\n\t\t\tbaseDir: \"./testdata/fixture-sanitize-path/env/unit\",\n\t\t\tfile:    \".\",\n\t\t\twant:    \"./testdata/fixture-sanitize-path/env/unit/.\",\n\t\t\twantErr: false,\n\t\t},\n\t\t{\n\t\t\tname:    \"encoded characters\",\n\t\t\tbaseDir: \"./testdata/fixture-sanitize-path/env/unit\",\n\t\t\tfile:    \"..%2F..%2Fetc%2Fpasswd\",\n\t\t\twant:    \"\",\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\tt.Parallel()\n\n\t\t\tgot, err := util.SanitizePath(tt.baseDir, tt.file)\n\n\t\t\tif tt.wantErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equalf(t, tt.want, got, \"sanitizePath(%v, %v)\", tt.baseDir, tt.file)\n\t\t})\n\t}\n}\n\nfunc TestMoveFile(t *testing.T) {\n\tt.Parallel()\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\n\tsrc := filepath.Join(tempDir, \"src.txt\")\n\tdst := filepath.Join(tempDir, \"dst.txt\")\n\n\trequire.NoError(t, os.WriteFile(src, []byte(\"test\"), 0644))\n\trequire.NoError(t, util.MoveFile(src, dst))\n\n\t// Verify the file was moved\n\t_, err := os.Stat(src)\n\trequire.True(t, os.IsNotExist(err))\n\tcontents, err := os.ReadFile(dst)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"test\", string(contents))\n}\n\nfunc TestRelPathForLog(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname        string\n\t\tbasePath    string\n\t\ttargetPath  string\n\t\texpected    string\n\t\tshowAbsPath bool\n\t}{\n\t\t{\n\t\t\tname:        \"showAbsPath true returns targetPath unchanged\",\n\t\t\tbasePath:    helpers.RootFolder + \"base\",\n\t\t\ttargetPath:  helpers.RootFolder + \"base/child/file.txt\",\n\t\t\tshowAbsPath: true,\n\t\t\texpected:    helpers.RootFolder + \"base/child/file.txt\",\n\t\t},\n\t\t{\n\t\t\tname:        \"same path returns targetPath\",\n\t\t\tbasePath:    helpers.RootFolder + \"base\",\n\t\t\ttargetPath:  helpers.RootFolder + \"base\",\n\t\t\tshowAbsPath: false,\n\t\t\texpected:    helpers.RootFolder + \"base\",\n\t\t},\n\t\t{\n\t\t\tname:        \"child path gets ./ prefix\",\n\t\t\tbasePath:    helpers.RootFolder + \"base\",\n\t\t\ttargetPath:  helpers.RootFolder + \"base/child\",\n\t\t\tshowAbsPath: false,\n\t\t\texpected:    \".\" + string(filepath.Separator) + \"child\",\n\t\t},\n\t\t{\n\t\t\tname:        \"nested child path gets ./ prefix\",\n\t\t\tbasePath:    helpers.RootFolder + \"base\",\n\t\t\ttargetPath:  helpers.RootFolder + \"base/child/subchild/file.txt\",\n\t\t\tshowAbsPath: false,\n\t\t\texpected:    \".\" + string(filepath.Separator) + filepath.Join(\"child\", \"subchild\", \"file.txt\"),\n\t\t},\n\t\t{\n\t\t\tname:        \"parent path returns relative path with ..\",\n\t\t\tbasePath:    helpers.RootFolder + \"base/child\",\n\t\t\ttargetPath:  helpers.RootFolder + \"base\",\n\t\t\tshowAbsPath: false,\n\t\t\texpected:    \"..\",\n\t\t},\n\t\t{\n\t\t\tname:        \"sibling path returns relative path with ..\",\n\t\t\tbasePath:    helpers.RootFolder + \"base/child1\",\n\t\t\ttargetPath:  helpers.RootFolder + \"base/child2\",\n\t\t\tshowAbsPath: false,\n\t\t\texpected:    \"..\" + string(filepath.Separator) + \"child2\",\n\t\t},\n\t\t{\n\t\t\tname:        \"deeply nested sibling path\",\n\t\t\tbasePath:    helpers.RootFolder + \"base/a/b/c\",\n\t\t\ttargetPath:  helpers.RootFolder + \"base/x/y/z\",\n\t\t\tshowAbsPath: false,\n\t\t\texpected:    \"..\" + string(filepath.Separator) + \"..\" + string(filepath.Separator) + \"..\" + string(filepath.Separator) + filepath.Join(\"x\", \"y\", \"z\"),\n\t\t},\n\t\t{\n\t\t\tname:        \"unrelated paths at different roots\",\n\t\t\tbasePath:    helpers.RootFolder + \"foo\",\n\t\t\ttargetPath:  helpers.RootFolder + \"bar/baz\",\n\t\t\tshowAbsPath: false,\n\t\t\texpected:    \"..\" + string(filepath.Separator) + filepath.Join(\"bar\", \"baz\"),\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.Parallel()\n\n\t\t\tactual := util.RelPathForLog(tc.basePath, tc.targetPath, tc.showAbsPath)\n\t\t\tassert.Equal(t, tc.expected, actual, \"For basePath %s and targetPath %s\", tc.basePath, tc.targetPath)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/util/file_tofu_test.go",
    "content": "package util_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar (\n\tbenchmarkBoolSink bool\n)\n\nfunc TestIsTFFile(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tdescription string\n\t\tpath        string\n\t\texpected    bool\n\t}{\n\t\t{\n\t\t\tdescription: \"Terraform .tf file\",\n\t\t\tpath:        \"main.tf\",\n\t\t\texpected:    true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"OpenTofu .tofu file\",\n\t\t\tpath:        \"main.tofu\",\n\t\t\texpected:    true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Terraform JSON .tf.json file\",\n\t\t\tpath:        \"main.tf.json\",\n\t\t\texpected:    true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"OpenTofu JSON .tofu.json file\",\n\t\t\tpath:        \"main.tofu.json\",\n\t\t\texpected:    true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Regular JSON file\",\n\t\t\tpath:        \"config.json\",\n\t\t\texpected:    false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Regular text file\",\n\t\t\tpath:        \"readme.txt\",\n\t\t\texpected:    false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"No extension\",\n\t\t\tpath:        \"Dockerfile\",\n\t\t\texpected:    false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"HCL file (not Terraform/OpenTofu)\",\n\t\t\tpath:        \"terragrunt.hcl\",\n\t\t\texpected:    false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Path with directories - .tf file\",\n\t\t\tpath:        \"/path/to/modules/main.tf\",\n\t\t\texpected:    true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Path with directories - .tofu file\",\n\t\t\tpath:        \"/path/to/modules/main.tofu\",\n\t\t\texpected:    true,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := util.IsTFFile(tc.path)\n\t\t\tassert.Equal(t, tc.expected, actual, \"For path %s\", tc.path)\n\t\t})\n\t}\n}\n\nfunc TestDirContainsTFFiles(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tdescription string\n\t\tfiles       []string\n\t\tdirectories []string\n\t\texpected    bool\n\t\texpectError bool\n\t}{\n\t\t{\n\t\t\tdescription: \"Directory with .tf file\",\n\t\t\tfiles:       []string{\"main.tf\"},\n\t\t\texpected:    true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with .tofu file\",\n\t\t\tfiles:       []string{\"main.tofu\"},\n\t\t\texpected:    true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with .tf.json file\",\n\t\t\tfiles:       []string{\"main.tf.json\"},\n\t\t\texpected:    true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with .tofu.json file\",\n\t\t\tfiles:       []string{\"main.tofu.json\"},\n\t\t\texpected:    true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with both .tf and .tofu files\",\n\t\t\tfiles:       []string{\"main.tf\", \"variables.tofu\"},\n\t\t\texpected:    true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with mixed file types including TF files\",\n\t\t\tfiles:       []string{\"main.tf\", \"readme.txt\", \"config.json\"},\n\t\t\texpected:    true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with no TF files\",\n\t\t\tfiles:       []string{\"readme.txt\", \"config.json\", \"script.sh\"},\n\t\t\texpected:    false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Empty directory\",\n\t\t\tfiles:       []string{},\n\t\t\texpected:    false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with subdirectories containing TF files\",\n\t\t\tfiles:       []string{\"modules/main.tf\", \"data/variables.tofu\"},\n\t\t\tdirectories: []string{\"modules\", \"data\"},\n\t\t\texpected:    true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Directory with only non-TF files in subdirectories\",\n\t\t\tfiles:       []string{\"modules/readme.txt\", \"data/config.json\"},\n\t\t\tdirectories: []string{\"modules\", \"data\"},\n\t\t\texpected:    false,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t// Create temporary directory\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\t// Create directories\n\t\t\tfor _, dir := range tc.directories {\n\t\t\t\tdirPath := filepath.Join(tmpDir, dir)\n\t\t\t\trequire.NoError(t, os.MkdirAll(dirPath, 0755))\n\t\t\t}\n\n\t\t\t// Create files\n\t\t\tfor _, file := range tc.files {\n\t\t\t\tfilePath := filepath.Join(tmpDir, file)\n\t\t\t\t// Ensure directory exists\n\t\t\t\trequire.NoError(t, os.MkdirAll(filepath.Dir(filePath), 0755))\n\t\t\t\trequire.NoError(t, os.WriteFile(filePath, []byte(\"# Test file content\"), 0644))\n\t\t\t}\n\n\t\t\tactual, err := util.DirContainsTFFiles(tmpDir)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.expected, actual, \"For test case: %s\", tc.description)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFindTFFiles(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tdescription   string\n\t\tfiles         []string\n\t\tdirectories   []string\n\t\texpectedFiles []string\n\t\texpectedCount int\n\t}{\n\t\t{\n\t\t\tdescription:   \"Directory with single .tf file\",\n\t\t\tfiles:         []string{\"main.tf\"},\n\t\t\texpectedCount: 1,\n\t\t\texpectedFiles: []string{\"main.tf\"},\n\t\t},\n\t\t{\n\t\t\tdescription:   \"Directory with single .tofu file\",\n\t\t\tfiles:         []string{\"main.tofu\"},\n\t\t\texpectedCount: 1,\n\t\t\texpectedFiles: []string{\"main.tofu\"},\n\t\t},\n\t\t{\n\t\t\tdescription:   \"Directory with mixed TF file types\",\n\t\t\tfiles:         []string{\"main.tf\", \"variables.tofu\", \"outputs.tf.json\", \"providers.tofu.json\"},\n\t\t\texpectedCount: 4,\n\t\t\texpectedFiles: []string{\"main.tf\", \"variables.tofu\", \"outputs.tf.json\", \"providers.tofu.json\"},\n\t\t},\n\t\t{\n\t\t\tdescription:   \"Directory with TF and non-TF files\",\n\t\t\tfiles:         []string{\"main.tf\", \"readme.txt\", \"variables.tofu\", \"config.json\"},\n\t\t\texpectedCount: 2,\n\t\t\texpectedFiles: []string{\"main.tf\", \"variables.tofu\"},\n\t\t},\n\t\t{\n\t\t\tdescription:   \"Empty directory\",\n\t\t\tfiles:         []string{},\n\t\t\texpectedCount: 0,\n\t\t\texpectedFiles: []string{},\n\t\t},\n\t\t{\n\t\t\tdescription:   \"Directory with only non-TF files\",\n\t\t\tfiles:         []string{\"readme.txt\", \"config.json\", \"script.sh\"},\n\t\t\texpectedCount: 0,\n\t\t\texpectedFiles: []string{},\n\t\t},\n\t\t{\n\t\t\tdescription:   \"Directory with nested TF files\",\n\t\t\tfiles:         []string{\"main.tf\", \"modules/vpc/main.tofu\", \"modules/security/variables.tf.json\"},\n\t\t\tdirectories:   []string{\"modules\", \"modules/vpc\", \"modules/security\"},\n\t\t\texpectedCount: 3,\n\t\t\texpectedFiles: []string{\"main.tf\", \"modules/vpc/main.tofu\", \"modules/security/variables.tf.json\"},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t// Create temporary directory\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\t// Create directories\n\t\t\tfor _, dir := range tc.directories {\n\t\t\t\tdirPath := filepath.Join(tmpDir, dir)\n\t\t\t\trequire.NoError(t, os.MkdirAll(dirPath, 0755))\n\t\t\t}\n\n\t\t\t// Create files\n\t\t\tfor _, file := range tc.files {\n\t\t\t\tfilePath := filepath.Join(tmpDir, file)\n\t\t\t\t// Ensure directory exists\n\t\t\t\trequire.NoError(t, os.MkdirAll(filepath.Dir(filePath), 0755))\n\t\t\t\trequire.NoError(t, os.WriteFile(filePath, []byte(\"# Test file content\"), 0644))\n\t\t\t}\n\n\t\t\tactual, err := util.FindTFFiles(tmpDir)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Check count\n\t\t\tassert.Len(t, actual, tc.expectedCount, \"Expected %d files, got %d\", tc.expectedCount, len(actual))\n\n\t\t\t// Check that all expected files are found (convert to relative paths for comparison)\n\t\t\texpectedRelativePaths := make([]string, len(tc.expectedFiles))\n\t\t\tfor i, expectedFile := range tc.expectedFiles {\n\t\t\t\texpectedRelativePaths[i] = filepath.Join(tmpDir, expectedFile)\n\t\t\t}\n\n\t\t\tfor _, expectedPath := range expectedRelativePaths {\n\t\t\t\tassert.Contains(t, actual, expectedPath, \"Expected file %s not found in results\", expectedPath)\n\t\t\t}\n\n\t\t\t// Check that all found files are actually TF files\n\t\t\tfor _, foundFile := range actual {\n\t\t\t\tassert.True(t, util.IsTFFile(foundFile), \"Non-TF file %s found in results\", foundFile)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRegexFoundInTFFiles(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tfiles       map[string]string\n\t\tdescription string\n\t\tpattern     string\n\t\tdirectories []string\n\t\texpected    bool\n\t}{\n\t\t{\n\t\t\tdescription: \"Pattern found in .tf file\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tf\": `\nterraform {\n  backend \"s3\" {\n    bucket = \"my-bucket\"\n  }\n}`,\n\t\t\t},\n\t\t\tpattern:  `backend[[:blank:]]+\"s3\"`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Pattern found in .tofu file\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tofu\": `\nterraform {\n  backend \"local\" {\n    path = \"terraform.tfstate\"\n  }\n}`,\n\t\t\t},\n\t\t\tpattern:  `backend[[:blank:]]+\"local\"`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Pattern found in .tf.json file\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tf.json\": `{\n  \"terraform\": {\n    \"backend\": {\n      \"remote\": {\n        \"organization\": \"my-org\"\n      }\n    }\n  }\n}`,\n\t\t\t},\n\t\t\tpattern:  `\"backend\":[[:space:]]*{[[:space:]]*\"remote\"`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Pattern found in .tofu.json file\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tofu.json\": `{\n  \"terraform\": {\n    \"backend\": {\n      \"gcs\": {\n        \"bucket\": \"my-bucket\"\n      }\n    }\n  }\n}`,\n\t\t\t},\n\t\t\tpattern:  `\"backend\":[[:space:]]*{[[:space:]]*\"gcs\"`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Pattern found in mixed file types\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tf\":      \"# No backend here\",\n\t\t\t\t\"backend.tofu\": `terraform { backend \"s3\" {} }`,\n\t\t\t\t\"readme.txt\":   \"This is not a TF file\",\n\t\t\t\t\"config.json\":  `{\"not\": \"terraform\"}`,\n\t\t\t},\n\t\t\tpattern:  `backend[[:blank:]]+\"s3\"`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Pattern not found in any TF files\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tf\":    \"resource \\\"aws_instance\\\" \\\"example\\\" {}\",\n\t\t\t\t\"vars.tofu\":  \"variable \\\"name\\\" { type = string }\",\n\t\t\t\t\"readme.txt\": \"This file contains backend configuration (but it's not a TF file)\",\n\t\t\t},\n\t\t\tpattern:  `backend[[:blank:]]+\"s3\"`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Pattern found in nested TF files\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tf\":                  \"# No backend\",\n\t\t\t\t\"modules/vpc/main.tofu\":    `terraform { backend \"s3\" {} }`,\n\t\t\t\t\"modules/security/vars.tf\": \"variable \\\"vpc_id\\\" {}\",\n\t\t\t},\n\t\t\tdirectories: []string{\"modules\", \"modules/vpc\", \"modules/security\"},\n\t\t\tpattern:     `backend[[:blank:]]+\"s3\"`,\n\t\t\texpected:    true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Module pattern found\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"main.tf\": `\nmodule \"vpc\" {\n  source = \"./modules/vpc\"\n}`,\n\t\t\t},\n\t\t\tpattern:  `module[[:blank:]]+\".+\"`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tdescription: \"Module pattern found in .tofu file\",\n\t\t\tfiles: map[string]string{\n\t\t\t\t\"infrastructure.tofu\": `\nmodule \"database\" {\n  source = \"git::https://github.com/example/modules.git//database\"\n}`,\n\t\t\t},\n\t\t\tpattern:  `module[[:blank:]]+\".+\"`,\n\t\t\texpected: true,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t// Create temporary directory\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\t// Create directories\n\t\t\tfor _, dir := range tc.directories {\n\t\t\t\tdirPath := filepath.Join(tmpDir, dir)\n\t\t\t\trequire.NoError(t, os.MkdirAll(dirPath, 0755))\n\t\t\t}\n\n\t\t\t// Create files with content\n\t\t\tfor filename, content := range tc.files {\n\t\t\t\tfilePath := filepath.Join(tmpDir, filename)\n\t\t\t\t// Ensure directory exists\n\t\t\t\trequire.NoError(t, os.MkdirAll(filepath.Dir(filePath), 0755))\n\t\t\t\trequire.NoError(t, os.WriteFile(filePath, []byte(content), 0644))\n\t\t\t}\n\n\t\t\t// Compile regex pattern\n\t\t\tregex, err := regexp.Compile(tc.pattern)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tactual, err := util.RegexFoundInTFFiles(tmpDir, regex)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, tc.expected, actual, \"For test case: %s\", tc.description)\n\t\t})\n\t}\n}\n\n// TestRegexFoundInTFFilesErrorHandling tests error conditions\nfunc TestRegexFoundInTFFilesErrorHandling(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"Non-existent directory\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tregex := regexp.MustCompile(\"test\")\n\t\t_, err := util.RegexFoundInTFFiles(\"/non/existent/directory\", regex)\n\t\tassert.Error(t, err)\n\t})\n\n\tt.Run(\"Permission denied file\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\t// Create a directory and file, then remove read permissions\n\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\t\ttestFile := filepath.Join(tmpDir, \"test.tf\")\n\t\trequire.NoError(t, os.WriteFile(testFile, []byte(\"content\"), 0644))\n\n\t\t// Remove read permissions (this test might not work on all systems/CI environments)\n\t\terr := os.Chmod(testFile, 0000)\n\t\tif err != nil {\n\t\t\tt.Skip(\"Cannot change file permissions on this system\")\n\t\t}\n\n\t\t// Restore permissions after test\n\t\tdefer func() {\n\t\t\t_ = os.Chmod(testFile, 0644)\n\t\t}()\n\n\t\tregex := regexp.MustCompile(\"test\")\n\t\t_, err = util.RegexFoundInTFFiles(tmpDir, regex)\n\t\t// We expect an error due to permission denied, but don't fail the test\n\t\t// if the OS doesn't enforce permission restrictions in the test environment\n\t\tif err == nil {\n\t\t\tt.Log(\"Permission restrictions not enforced in test environment\")\n\t\t}\n\t})\n}\n\nfunc TestListTfFiles(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tdescription   string\n\t\tfiles         []string\n\t\tdirectories   []string\n\t\texpectedFiles []string\n\t\texpectedCount int\n\t}{\n\t\t{\n\t\t\tdescription:   \"Directory with .tf files only\",\n\t\t\tfiles:         []string{\"main.tf\", \"variables.tf\"},\n\t\t\texpectedCount: 2,\n\t\t\texpectedFiles: []string{\"main.tf\", \"variables.tf\"},\n\t\t},\n\t\t{\n\t\t\tdescription:   \"Directory with .tofu files only\",\n\t\t\tfiles:         []string{\"main.tofu\", \"variables.tofu\"},\n\t\t\texpectedCount: 2,\n\t\t\texpectedFiles: []string{\"main.tofu\", \"variables.tofu\"},\n\t\t},\n\t\t{\n\t\t\tdescription:   \"Directory with mixed .tf and .tofu files\",\n\t\t\tfiles:         []string{\"main.tf\", \"variables.tofu\", \"outputs.tf.json\", \"providers.tofu.json\"},\n\t\t\texpectedCount: 4,\n\t\t\texpectedFiles: []string{\"main.tf\", \"variables.tofu\", \"outputs.tf.json\", \"providers.tofu.json\"},\n\t\t},\n\t\t{\n\t\t\tdescription:   \"Directory with TF and non-TF files\",\n\t\t\tfiles:         []string{\"main.tofu\", \"readme.txt\", \"config.json\"},\n\t\t\texpectedCount: 1,\n\t\t\texpectedFiles: []string{\"main.tofu\"},\n\t\t},\n\t\t{\n\t\t\tdescription:   \"Empty directory\",\n\t\t\tfiles:         []string{},\n\t\t\texpectedCount: 0,\n\t\t\texpectedFiles: []string{},\n\t\t},\n\t\t{\n\t\t\tdescription:   \"Directory with nested .tofu files\",\n\t\t\tfiles:         []string{\"main.tf\", \"modules/vpc/main.tofu\"},\n\t\t\tdirectories:   []string{\"modules\", \"modules/vpc\"},\n\t\t\texpectedCount: 2,\n\t\t\texpectedFiles: []string{\"main.tf\", \"modules/vpc/main.tofu\"},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\tfor _, dir := range tc.directories {\n\t\t\t\tdirPath := filepath.Join(tmpDir, dir)\n\t\t\t\trequire.NoError(t, os.MkdirAll(dirPath, 0755))\n\t\t\t}\n\n\t\t\tfor _, file := range tc.files {\n\t\t\t\tfilePath := filepath.Join(tmpDir, file)\n\t\t\t\trequire.NoError(t, os.MkdirAll(filepath.Dir(filePath), 0755))\n\t\t\t\trequire.NoError(t, os.WriteFile(filePath, []byte(\"# Test file content\"), 0644))\n\t\t\t}\n\n\t\t\tactual, err := util.ListTfFiles(tmpDir, false)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Len(t, actual, tc.expectedCount, \"Expected %d files, got %d\", tc.expectedCount, len(actual))\n\n\t\t\tfor _, expectedFile := range tc.expectedFiles {\n\t\t\t\texpectedPath := filepath.Join(tmpDir, expectedFile)\n\t\t\t\tassert.Contains(t, actual, expectedPath, \"Expected file %s not found in results\", expectedPath)\n\t\t\t}\n\n\t\t\tfor _, foundFile := range actual {\n\t\t\t\tassert.True(t, util.IsTFFile(foundFile), \"Non-TF file %s found in results\", foundFile)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestListTfFilesWithSymlinks(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create a real directory with .tofu files\n\trealDir := filepath.Join(tmpDir, \"real-module\")\n\trequire.NoError(t, os.MkdirAll(realDir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(realDir, \"main.tofu\"), []byte(\"# main\"), 0644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(realDir, \"variables.tf\"), []byte(\"# vars\"), 0644))\n\n\t// Create a symlink to the real directory\n\tlinkDir := filepath.Join(tmpDir, \"linked-module\")\n\trequire.NoError(t, os.Symlink(realDir, linkDir))\n\n\t// walkWithSymlinks=true should follow symlinks and find files\n\tactual, err := util.ListTfFiles(tmpDir, true)\n\trequire.NoError(t, err)\n\n\t// Should find files in both real dir and symlinked dir\n\tassert.Len(t, actual, 4)\n\n\tfor _, foundFile := range actual {\n\t\tassert.True(t, util.IsTFFile(foundFile), \"Non-TF file %s found in results\", foundFile)\n\t}\n}\n\n// Benchmark tests to ensure performance is reasonable\nfunc BenchmarkIsTFFile(b *testing.B) {\n\ttestPaths := []string{\n\t\t\"main.tf\",\n\t\t\"variables.tofu\",\n\t\t\"outputs.tf.json\",\n\t\t\"providers.tofu.json\",\n\t\t\"readme.txt\",\n\t\t\"config.json\",\n\t\t\"/very/long/path/to/terraform/modules/vpc/main.tf\",\n\t\t\"/very/long/path/to/opentofu/modules/database/variables.tofu\",\n\t}\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tfor b.Loop() {\n\t\tfor _, path := range testPaths {\n\t\t\tif util.IsTFFile(path) {\n\t\t\t\tbenchmarkBoolSink = !benchmarkBoolSink\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc BenchmarkDirContainsTFFiles(b *testing.B) {\n\t// Create a temporary directory with mixed files for benchmarking\n\ttmpDir := b.TempDir()\n\tfiles := []string{\n\t\t\"main.tf\",\n\t\t\"variables.tofu\",\n\t\t\"outputs.tf.json\",\n\t\t\"providers.tofu.json\",\n\t\t\"readme.txt\",\n\t\t\"config.json\",\n\t\t\"modules/vpc/main.tf\",\n\t\t\"modules/database/vars.tofu\",\n\t}\n\n\tfor _, file := range files {\n\t\tfilePath := filepath.Join(tmpDir, file)\n\t\trequire.NoError(b, os.MkdirAll(filepath.Dir(filePath), 0755))\n\t\trequire.NoError(b, os.WriteFile(filePath, []byte(\"# Test content\"), 0644))\n\t}\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\n\tfor b.Loop() {\n\t\tresult, err := util.DirContainsTFFiles(tmpDir)\n\t\trequire.NoError(b, err)\n\n\t\tbenchmarkBoolSink = benchmarkBoolSink != result\n\t}\n}\n"
  },
  {
    "path": "internal/util/hash.go",
    "content": "package util\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/sha1\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"fmt\"\n)\n\nconst (\n\tsha256InputSize = 32\n)\n\n// EncodeBase64Sha1 Returns the base 64 encoded sha1 hash of the given string\nfunc EncodeBase64Sha1(str string) string {\n\thash := sha1.Sum([]byte(str))\n\treturn base64.RawURLEncoding.EncodeToString(hash[:])\n}\n\nfunc GenerateRandomSha256() (string, error) {\n\trandomBytes := make([]byte, sha256InputSize)\n\n\t_, err := rand.Read(randomBytes)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn fmt.Sprintf(\"%x\", sha256.Sum256(randomBytes)), nil\n}\n"
  },
  {
    "path": "internal/util/jsons.go",
    "content": "package util\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n)\n\n// interpolationEscaper replaces unescaped HCL interpolation patterns (${...}) with\n// their escaped form ($${...}). Listing $${ first makes the replacement idempotent:\n// already-escaped $${...} is matched at that position before ${ is tried, so it is\n// emitted unchanged.\nvar interpolationEscaper = strings.NewReplacer(\"$${\", \"$${\", \"${\", \"$${\")\n\n// AsTerraformEnvVarJSONValue converts the given value to a JSON value that can be passed to\n// OpenTofu/Terraform as an environment variable. For the most part, this converts the value directly\n// to JSON using Go's built-in json.Marshal. However, we have special handling\n// for strings, which with normal JSON conversion would be wrapped in quotes, but when passing them to OpenTofu/Terraform via\n// env vars, we need to NOT wrap them in quotes, so this method adds special handling for that case.\n// For complex types (maps, lists, objects), string values containing ${...} patterns are escaped to $${...}\n// to prevent OpenTofu/Terraform's HCL parser from treating them as variable interpolations.\nfunc AsTerraformEnvVarJSONValue(value any) (string, error) {\n\tswitch val := value.(type) {\n\tcase string:\n\t\treturn val, nil\n\tdefault:\n\t\tescaped, err := escapeInterpolationPatternsInValue(val, 0)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tenvVarValue, err := json.Marshal(escaped)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\treturn string(envVarValue), nil\n\t}\n}\n\n// escapeInterpolationPatternsInValue recursively walks a value tree and escapes\n// HCL interpolation patterns (${...}) in string values to prevent OpenTofu/Terraform from\n// treating them as variable references when parsing complex type TF_VAR_* env vars.\n//\n// This unconditionally escapes ${...} in all string values within complex types.\n// This is intentional: OpenTofu/Terraform's HCL parser would error on unescaped ${...} in\n// complex TF_VAR values anyway (behaviour change: previously errored; now passes the\n// literal value through).\n//\n// Nil maps and slices are preserved as nil so json.Marshal serializes them as null\n// rather than {} or [], keeping OpenTofu/Terraform's null-vs-empty-collection semantics intact.\n//\n// Returns an error if the value tree is deeper than maxDepth (100), which prevents\n// infinite recursion on malformed inputs.\nfunc escapeInterpolationPatternsInValue(value any, depth int) (any, error) {\n\tconst maxDepth = 100\n\tif depth > maxDepth {\n\t\treturn nil, fmt.Errorf(\"escapeInterpolationPatternsInValue: input exceeds maximum nesting depth of %d\", maxDepth)\n\t}\n\n\tswitch v := value.(type) {\n\tcase string:\n\t\treturn EscapeInterpolationInString(v), nil\n\n\tcase map[string]any:\n\t\tif v == nil {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tresult := make(map[string]any, len(v))\n\n\t\tfor key, val := range v {\n\t\t\tescaped, err := escapeInterpolationPatternsInValue(val, depth+1)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tresult[key] = escaped\n\t\t}\n\n\t\treturn result, nil\n\n\tcase []any:\n\t\tif v == nil {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tresult := make([]any, 0, len(v))\n\n\t\tfor _, val := range v {\n\t\t\tescaped, err := escapeInterpolationPatternsInValue(val, depth+1)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tresult = append(result, escaped)\n\t\t}\n\n\t\treturn result, nil\n\n\tcase []string:\n\t\tif v == nil {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tresult := make([]any, 0, len(v))\n\n\t\tfor _, s := range v {\n\t\t\tresult = append(result, EscapeInterpolationInString(s))\n\t\t}\n\n\t\treturn result, nil\n\n\tcase map[string]string:\n\t\tif v == nil {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tresult := make(map[string]any, len(v))\n\n\t\tfor k, s := range v {\n\t\t\tresult[k] = EscapeInterpolationInString(s)\n\t\t}\n\n\t\treturn result, nil\n\n\tdefault:\n\t\treturn value, nil\n\t}\n}\n\n// EscapeInterpolationInString escapes HCL interpolation patterns (${...}) in a string\n// by doubling the dollar sign (${ → $${). This is idempotent: already-escaped $${...}\n// patterns are not double-escaped.\nfunc EscapeInterpolationInString(s string) string {\n\treturn interpolationEscaper.Replace(s)\n}\n"
  },
  {
    "path": "internal/util/jsons_test.go",
    "content": "package util_test\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAsTerraformEnvVarJsonValue(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tvalue    any\n\t\texpected string\n\t}{\n\t\t// plain strings: passed through unchanged (Terraform reads string vars as literals)\n\t\t{\"aws_region\", \"aws_region\"},\n\t\t{\"plain ${bar} string\", \"plain ${bar} string\"},\n\t\t// list: JSON serialized, strings within escaped\n\t\t{[]string{\"10.0.0.0/16\", \"10.0.0.10/16\"}, \"[\\\"10.0.0.0/16\\\",\\\"10.0.0.10/16\\\"]\"},\n\t\t// map: strings within escaped\n\t\t{map[string]any{\"foo\": \"test ${bar} test\"}, `{\"foo\":\"test $${bar} test\"}`},\n\t\t// idempotent: already-escaped $${...} not double-escaped\n\t\t{map[string]any{\"foo\": \"test $${bar} test\"}, `{\"foo\":\"test $${bar} test\"}`},\n\t\t// list with interpolation\n\t\t{[]any{\"${foo}\", \"bar\"}, `[\"$${foo}\",\"bar\"]`},\n\t\t// nested map\n\t\t{map[string]any{\"a\": map[string]any{\"b\": \"${nested}\"}}, `{\"a\":{\"b\":\"$${nested}\"}}`},\n\t\t// typed []string with interpolation\n\t\t{[]string{\"${foo}\", \"bar\"}, `[\"$${foo}\",\"bar\"]`},\n\t\t// typed map[string]string with interpolation\n\t\t{map[string]string{\"k\": \"${foo}\"}, `{\"k\":\"$${foo}\"}`},\n\t\t// nil containers must serialize as null, not {} or []\n\t\t{(map[string]any)(nil), \"null\"},\n\t\t{([]any)(nil), \"null\"},\n\t\t{([]string)(nil), \"null\"},\n\t\t{(map[string]string)(nil), \"null\"},\n\t\t// nil inside a complex type must also be null\n\t\t{map[string]any{\"list\": ([]any)(nil)}, `{\"list\":null}`},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual, err := util.AsTerraformEnvVarJSONValue(tc.value)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestAsTerraformEnvVarJsonValueDepthOverflow(t *testing.T) {\n\tt.Parallel()\n\n\t// Build a map nested 102 levels deep — exceeds the maxDepth of 100.\n\tdeep := buildNestedMap(102)\n\n\t_, err := util.AsTerraformEnvVarJSONValue(deep)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"maximum nesting depth\")\n}\n\n// buildNestedMap creates a map[string]any with the given nesting depth.\nfunc buildNestedMap(depth int) map[string]any {\n\tif depth == 0 {\n\t\treturn map[string]any{\"val\": \"leaf\"}\n\t}\n\n\treturn map[string]any{\"nested\": buildNestedMap(depth - 1)}\n}\n\nfunc TestEscapeInterpolationInString(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\"test ${bar} test\", \"test $${bar} test\"},\n\t\t// idempotent: already escaped\n\t\t{\"test $${bar} test\", \"test $${bar} test\"},\n\t\t// multiple interpolations\n\t\t{\"${a} and ${b}\", \"$${a} and $${b}\"},\n\t\t// no interpolation\n\t\t{\"no interpolation\", \"no interpolation\"},\n\t\t// dollar not followed by brace\n\t\t{\"$not_interpolation\", \"$not_interpolation\"},\n\t\t// empty string\n\t\t{\"\", \"\"},\n\t\t// just the pattern\n\t\t{\"${foo}\", \"$${foo}\"},\n\t\t// starts with already-escaped\n\t\t{\"$${foo} and ${bar}\", \"$${foo} and $${bar}\"},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tname := tc.input\n\t\tif name == \"\" {\n\t\t\tname = fmt.Sprintf(\"case-%d\", i)\n\t\t}\n\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := util.EscapeInterpolationInString(tc.input)\n\t\t\tassert.Equal(t, tc.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/util/lockfile.go",
    "content": "package util\n\nimport (\n\t\"os\"\n\n\t\"github.com/gofrs/flock\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\ntype Lockfile struct {\n\t*flock.Flock\n}\n\nfunc NewLockfile(filename string) *Lockfile {\n\treturn &Lockfile{\n\t\tflock.New(filename),\n\t}\n}\n\nfunc (lockfile *Lockfile) Unlock() error {\n\tif lockfile.Flock == nil {\n\t\treturn nil\n\t}\n\n\tif err := lockfile.Flock.Unlock(); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tif FileExists(lockfile.Path()) {\n\t\tif err := os.Remove(lockfile.Path()); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (lockfile *Lockfile) TryLock() error {\n\tif locked, err := lockfile.Flock.TryLock(); err != nil {\n\t\treturn errors.New(err)\n\t} else if !locked {\n\t\treturn errors.Errorf(\"unable to lock file %s\", lockfile.Path())\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/util/locks.go",
    "content": "package util\n\nimport (\n\t\"sync\"\n)\n\n// KeyLocks manages a map of locks, each associated with a string key.\ntype KeyLocks struct {\n\tlocks      map[string]*sync.Mutex\n\tmasterLock sync.Mutex\n}\n\n// NewKeyLocks creates a new instance of KeyLocks.\nfunc NewKeyLocks() *KeyLocks {\n\treturn &KeyLocks{\n\t\tlocks: make(map[string]*sync.Mutex),\n\t}\n}\n\n// getOrCreateLock retrieves the lock for the given key, creating it if it doesn't exist.\nfunc (kl *KeyLocks) getOrCreateLock(key string) *sync.Mutex {\n\tkl.masterLock.Lock()\n\tdefer kl.masterLock.Unlock()\n\n\tlock, ok := kl.locks[key]\n\tif !ok {\n\t\tlock = &sync.Mutex{}\n\t\tkl.locks[key] = lock\n\t}\n\n\treturn lock\n}\n\n// Lock acquires the lock for the given key.\nfunc (kl *KeyLocks) Lock(key string) {\n\tlock := kl.getOrCreateLock(key)\n\tlock.Lock()\n}\n\n// Unlock releases the lock for the given key.\nfunc (kl *KeyLocks) Unlock(key string) {\n\tkl.masterLock.Lock()\n\tdefer kl.masterLock.Unlock()\n\n\tif lock, ok := kl.locks[key]; ok {\n\t\tlock.Unlock()\n\t}\n}\n"
  },
  {
    "path": "internal/util/locks_test.go",
    "content": "package util_test\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestKeyLocksBasic verifies basic locking and unlocking behavior.\nfunc TestKeyLocksBasic(t *testing.T) {\n\tt.Parallel()\n\n\tkl := util.NewKeyLocks()\n\n\tvar counter int // Counter to track lock/unlock cycles\n\n\tkl.Lock(\"key1\")\n\n\tcounter++\n\n\tkl.Unlock(\"key1\")\n\n\tcounter++\n\n\trequire.Equal(t, 2, counter, \"Lock/unlock cycle should be completed\")\n}\n\n// TestKeyLocksConcurrentAccess ensures thread-safe access for multiple keys.\nfunc TestKeyLocksConcurrentAccess(t *testing.T) {\n\tt.Parallel()\n\n\tkl := util.NewKeyLocks()\n\n\tvar (\n\t\tcounters [10]int\n\t\twg       sync.WaitGroup\n\t)\n\n\tfor i := range 10 {\n\t\twg.Add(1)\n\n\t\tgo func(key string, idx int) {\n\t\t\tdefer wg.Done()\n\n\t\t\tkl.Lock(key)\n\t\t\tdefer kl.Unlock(key)\n\n\t\t\tcounters[idx]++\n\t\t\tcounters[idx]++\n\t\t}(\"test-key\", i)\n\t}\n\n\twg.Wait()\n\n\tfor i := range 10 {\n\t\trequire.Equal(t, 2, counters[i], \"Lock/unlock cycle for each key should be completed\")\n\t}\n}\n\n// TestKeyLocksUnlockWithoutLock checks for safe behavior when unlocking without locking.\nfunc TestKeyLocksUnlockWithoutLock(t *testing.T) {\n\tt.Parallel()\n\n\tkl := util.NewKeyLocks()\n\n\trequire.NotPanics(t, func() {\n\t\tkl.Unlock(\"nonexistent_key\")\n\t}, \"Unlocking without locking should not panic\")\n}\n\n// TestKeyLocksLockUnlockStressWithSharedKey tests a shared key under high concurrent load.\nfunc TestKeyLocksLockUnlockStressWithSharedKey(t *testing.T) {\n\tt.Parallel()\n\n\tkl := util.NewKeyLocks()\n\n\tconst (\n\t\tnumGoroutines = 100\n\t\tnumOperations = 1000\n\t)\n\n\tvar (\n\t\twg      sync.WaitGroup\n\t\tcounter int\n\t)\n\n\tfor range numGoroutines {\n\t\twg.Add(1)\n\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\tkl.Lock(\"shared_key\")\n\t\t\tdefer kl.Unlock(\"shared_key\")\n\n\t\t\tfor range numOperations {\n\t\t\t\tcounter++\n\t\t\t\tcounter++\n\t\t\t}\n\t\t}()\n\t}\n\n\twg.Wait()\n\n\trequire.Equal(t, numGoroutines*numOperations*2, counter, \"All lock/unlock cycles should be completed\")\n}\n"
  },
  {
    "path": "internal/util/random.go",
    "content": "package util\n\nimport (\n\t\"bytes\"\n\t\"math/rand\"\n\t\"time\"\n)\n\nconst (\n\tmSecond = 1000\n)\n\n// GetRandomTime gets a random time duration between the lower bound and upper bound.\n// This is useful because some of our automated tests\n// wound up flooding the AWS API all at once, leading to a \"Subscriber limit exceeded\" error.\n// TODO: Some of the more exotic test cases fail, but it's not worth catching them given the intended use of this function.\nfunc GetRandomTime(lowerBound, upperBound time.Duration) time.Duration {\n\tif lowerBound < 0 {\n\t\tlowerBound = -1 * lowerBound\n\t}\n\n\tif upperBound < 0 {\n\t\tupperBound = -1 * upperBound\n\t}\n\n\tif lowerBound > upperBound {\n\t\treturn upperBound\n\t}\n\n\tif lowerBound == upperBound {\n\t\treturn lowerBound\n\t}\n\n\tlowerBoundMs := lowerBound.Seconds() * mSecond\n\tupperBoundMs := upperBound.Seconds() * mSecond\n\n\tlowerBoundMsInt := int(lowerBoundMs)\n\tupperBoundMsInt := int(upperBoundMs)\n\n\trandTimeInt := random(lowerBoundMsInt, upperBoundMsInt)\n\n\treturn time.Duration(randTimeInt) * time.Millisecond\n}\n\n// Generate a random int between min and max, inclusive\nfunc random(min int, max int) int {\n\treturn rand.Intn(max-min) + min\n}\n\nconst Base62Chars = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\"\nconst UniqueIDLength = 6 // Should be good for 62^6 = 56+ billion combinations\n\n// UniqueID returns a unique (ish) id we can use to name resources so they don't conflict with each other.\n// Uses base 62 to generate a 6 character string that's unlikely to collide with the handful of\n// tests we run in parallel. Based on code here:\n//\n//\thttp://stackoverflow.com/a/9543797/483528\nfunc UniqueID() string {\n\tvar out bytes.Buffer\n\n\tfor range UniqueIDLength {\n\t\tout.WriteByte(Base62Chars[rand.Intn(len(Base62Chars))])\n\t}\n\n\treturn out.String()\n}\n"
  },
  {
    "path": "internal/util/random_test.go",
    "content": "package util_test\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n)\n\nfunc TestGetRandomTime(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tlowerBound time.Duration\n\t\tupperBound time.Duration\n\t}{\n\t\t{1 * time.Second, 10 * time.Second},\n\t\t{0, 0},\n\t\t{-1 * time.Second, -3 * time.Second},\n\t\t{1 * time.Second, 2000000001 * time.Nanosecond},\n\t\t{1 * time.Millisecond, 10 * time.Millisecond},\n\t\t// {1 * time.Second, 1000000001 * time.Nanosecond}, // This case fails\n\t}\n\n\t// Loop through each test case\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t// Try each test case 100 times to avoid fluke test results\n\t\t\tfor j := range 100 {\n\t\t\t\tt.Run(strconv.Itoa(j), func(t *testing.T) {\n\t\t\t\t\tt.Parallel()\n\n\t\t\t\t\tactual := util.GetRandomTime(tc.lowerBound, tc.upperBound)\n\n\t\t\t\t\tif tc.lowerBound > 0 && tc.upperBound > 0 {\n\t\t\t\t\t\tif actual < tc.lowerBound {\n\t\t\t\t\t\t\tt.Fatalf(\"Randomly computed time %v should not be less than lowerBound %v\", actual, tc.lowerBound)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif actual > tc.upperBound {\n\t\t\t\t\t\t\tt.Fatalf(\"Randomly computed time %v should not be greater than upperBound %v\", actual, tc.upperBound)\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"
  },
  {
    "path": "internal/util/reflect.go",
    "content": "package util\n\nimport (\n\t\"reflect\"\n\t\"strconv\"\n)\n\n// KindOf returns the kind of the type or Invalid if value is nil.\nfunc KindOf(value any) reflect.Kind {\n\tvalueType := reflect.TypeOf(value)\n\tif valueType == nil {\n\t\treturn reflect.Invalid\n\t}\n\n\treturn valueType.Kind()\n}\n\n// MustWalkTerraformOutput is a helper utility to deeply return a value from a terraform output.\n//\n//\tnil will be returned if the path is invalid\n//\n//\tUsing an example terraform output:\n//\t  a = {\n//\t    b = {\n//\t      c = \"foo\"\n//\t    }\n//\t    \"d\" = [\n//\t      1,\n//\t      2\n//\t    ]\n//\t  }\n//\n//\tpath [\"a\", \"b\", \"c\"] will return \"foo\"\n//\tpath [\"a\", \"d\", \"1\"] will return 2\n//\tpath [\"a\", \"foo\"] will return nil\nfunc MustWalkTerraformOutput(value any, path ...string) any {\n\tif value == nil {\n\t\treturn nil\n\t}\n\n\tfound := value\n\tfor _, p := range path {\n\t\tv := reflect.ValueOf(found)\n\n\t\tswitch reflect.TypeOf(found).Kind() { //nolint:exhaustive\n\t\tcase reflect.Map:\n\t\t\tif !v.MapIndex(reflect.ValueOf(p)).IsValid() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tfound = v.MapIndex(reflect.ValueOf(p)).Interface()\n\n\t\tcase reflect.Slice, reflect.Array:\n\t\t\ti, err := strconv.Atoi(p)\n\t\t\tif err != nil {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif v.Len()-1 < i {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tfound = v.Index(i).Interface()\n\n\t\tdefault:\n\t\t\treturn found\n\t\t}\n\t}\n\n\treturn found\n}\n"
  },
  {
    "path": "internal/util/reflect_test.go",
    "content": "package util_test\n\nimport (\n\t\"math\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestKindOf(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tvalue    any\n\t\texpected reflect.Kind\n\t}{\n\t\t{1, reflect.Int},\n\t\t{2.0, reflect.Float64},\n\t\t{'A', reflect.Int32},\n\t\t{math.Pi, reflect.Float64},\n\t\t{true, reflect.Bool},\n\t\t{nil, reflect.Invalid},\n\t\t{\"Hello World!\", reflect.String},\n\t\t{new(string), reflect.Ptr},\n\t\t{\"\", reflect.String},\n\t\t{any(false), reflect.Bool},\n\t}\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := util.KindOf(tc.value).String()\n\t\t\tassert.Equal(t, tc.expected.String(), actual, \"For value %v\", tc.value)\n\t\t\tt.Logf(\"%v passed\", tc.value)\n\t\t})\n\t}\n}\n\nfunc TestMustWalkTerraformOutput(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tvalue    any\n\t\texpected any\n\t\tpath     []string\n\t}{\n\t\t{\n\t\t\tvalue: map[string]map[string]string{\n\t\t\t\t\"a\": {\n\t\t\t\t\t\"b\": \"c\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tpath:     []string{\"a\", \"b\"},\n\t\t\texpected: \"c\",\n\t\t},\n\t\t{\n\t\t\tvalue: map[string]map[string]string{\n\t\t\t\t\"a\": {\n\t\t\t\t\t\"b\": \"c\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tpath:     []string{\"a\", \"d\"},\n\t\t\texpected: nil,\n\t\t},\n\t\t{\n\t\t\tvalue:    []string{\"a\", \"b\", \"c\"},\n\t\t\tpath:     []string{\"1\"},\n\t\t\texpected: \"b\",\n\t\t},\n\t\t{\n\t\t\tvalue:    []string{\"a\", \"b\", \"c\"},\n\t\t\tpath:     []string{\"10\"},\n\t\t\texpected: nil,\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(strconv.Itoa(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual := util.MustWalkTerraformOutput(tc.value, tc.path...)\n\t\t\tassert.Equal(t, tc.expected, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/util/retry.go",
    "content": "package util\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// DoWithRetry runs the specified action. If it returns a value, return that value. If it returns an error, sleep for\n// sleepBetweenRetries and try again, up to a maximum of maxRetries retries. If maxRetries is exceeded, return a\n// MaxRetriesExceeded error.\nfunc DoWithRetry(ctx context.Context, actionDescription string, maxRetries int, sleepBetweenRetries time.Duration, logger log.Logger, logLevel log.Level, action func(ctx context.Context) error) error {\n\tfor i := 0; i <= maxRetries; i++ {\n\t\tlogger.Logf(logLevel, \"%s\", actionDescription)\n\n\t\terr := action(ctx)\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tvar fatalErr FatalError\n\t\tif ok := errors.As(err, &fatalErr); ok {\n\t\t\treturn err\n\t\t}\n\n\t\tif ctx.Err() != nil {\n\t\t\tlogger.Debugf(\"%s returned an error: %s.\", actionDescription, err.Error())\n\n\t\t\treturn errors.New(ctx.Err())\n\t\t}\n\n\t\tlogger.Errorf(\"%s returned an error: %s. Retry %d of %d. Sleeping for %s and will try again.\", actionDescription, err.Error(), i, maxRetries, sleepBetweenRetries)\n\n\t\tselect {\n\t\tcase <-time.After(sleepBetweenRetries): // Try again\n\t\tcase <-ctx.Done():\n\t\t\treturn errors.New(ctx.Err())\n\t\t}\n\t}\n\n\treturn MaxRetriesExceeded{Description: actionDescription, MaxRetries: maxRetries}\n}\n\n// MaxRetriesExceeded is an error that occurs when the maximum amount of retries is exceeded.\ntype MaxRetriesExceeded struct {\n\tDescription string\n\tMaxRetries  int\n}\n\nfunc (err MaxRetriesExceeded) Error() string {\n\treturn fmt.Sprintf(\"'%s' unsuccessful after %d retries\", err.Description, err.MaxRetries)\n}\n\n// FatalError is error interface for cases that should not be retried.\ntype FatalError struct {\n\tUnderlying error\n}\n\nfunc (err FatalError) Error() string {\n\treturn err.Underlying.Error()\n}\n\nfunc (err FatalError) Unwrap() error {\n\treturn err.Underlying\n}\n"
  },
  {
    "path": "internal/util/shell.go",
    "content": "package util\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\n// IsCommandExecutable - returns true if a command can be executed without errors.\nfunc IsCommandExecutable(ctx context.Context, command string, args ...string) bool {\n\tcmd := exec.CommandContext(ctx, command, args...)\n\tcmd.Stdin = nil\n\tcmd.Stdout = nil\n\tcmd.Stderr = nil\n\n\tif err := cmd.Run(); err != nil {\n\t\tvar exitErr *exec.ExitError\n\t\tif ok := errors.As(err, &exitErr); ok {\n\t\t\treturn exitErr.ExitCode() == 0\n\t\t}\n\n\t\treturn false\n\t}\n\n\treturn true\n}\n\ntype CmdOutput struct {\n\tStdout bytes.Buffer\n\tStderr bytes.Buffer\n}\n\n// GetExitCode returns the exit code of a command. If the error does not\n// implement errorCode or is not an exec.ExitError\n// or *errors.MultiError type, the error is returned.\nfunc GetExitCode(err error) (int, error) {\n\tvar exitStatus interface {\n\t\tExitStatus() (int, error)\n\t}\n\tif errors.As(err, &exitStatus) {\n\t\treturn exitStatus.ExitStatus()\n\t}\n\n\tvar exitCoder clihelper.ExitCoder\n\tif errors.As(err, &exitCoder) {\n\t\treturn exitCoder.ExitCode(), nil\n\t}\n\n\tvar exiterr *exec.ExitError\n\tif ok := errors.As(err, &exiterr); ok {\n\t\tstatus := exiterr.Sys().(syscall.WaitStatus)\n\t\treturn status.ExitStatus(), nil\n\t}\n\n\tvar multiErr *errors.MultiError\n\tif ok := errors.As(err, &multiErr); ok {\n\t\tfor _, err := range multiErr.WrappedErrors() {\n\t\t\texitCode, exitCodeErr := GetExitCode(err)\n\t\t\tif exitCodeErr == nil {\n\t\t\t\treturn exitCode, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn 0, err\n}\n\n// ProcessExecutionError - error returned when a command fails, contains StdOut and StdErr\ntype ProcessExecutionError struct {\n\tErr             error\n\tWorkingDir      string\n\tRootWorkingDir  string\n\tCommand         string\n\tArgs            []string\n\tOutput          CmdOutput\n\tLogShowAbsPaths bool\n\tDisableSummary  bool\n}\n\nfunc (err ProcessExecutionError) Error() string { //nolint:gocritic\n\tcommandStr := strings.TrimSpace(\n\t\tstrings.Join(append([]string{err.Command}, err.Args...), \" \"),\n\t)\n\n\tworkingDirForLog := RelPathForLog(err.RootWorkingDir, err.WorkingDir, err.LogShowAbsPaths)\n\n\tif err.DisableSummary {\n\t\treturn fmt.Sprintf(\"Failed to execute \\\"%s\\\" in %s\",\n\t\t\tcommandStr,\n\t\t\tworkingDirForLog,\n\t\t)\n\t}\n\n\treturn fmt.Sprintf(\"Failed to execute \\\"%s\\\" in %s\\n%s\\n%v\",\n\t\tcommandStr,\n\t\tworkingDirForLog,\n\t\terr.Output.Stderr.String(),\n\t\terr.Err,\n\t)\n}\n\nfunc (err ProcessExecutionError) ExitStatus() (int, error) { //nolint:gocritic\n\treturn GetExitCode(err.Err)\n}\n\nfunc (err ProcessExecutionError) Unwrap() error { //nolint:gocritic\n\treturn err.Err\n}\n"
  },
  {
    "path": "internal/util/shell_test.go",
    "content": "package util_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestExistingCommand(t *testing.T) {\n\tt.Parallel()\n\n\tassert.True(t, util.IsCommandExecutable(t.Context(), \"pwd\"))\n}\n\nfunc TestNotExistingCommand(t *testing.T) {\n\tt.Parallel()\n\n\tassert.False(t, util.IsCommandExecutable(t.Context(), \"not-existing-command\", \"--version\"))\n}\n"
  },
  {
    "path": "internal/util/sync_writer.go",
    "content": "package util\n\nimport (\n\t\"io\"\n\t\"sync\"\n)\n\n// SyncWriter wraps an io.Writer with a mutex to make it safe for concurrent use.\n// This is necessary when multiple goroutines write to the same writer, such as\n// when running terraform commands in parallel during \"run --all\" operations.\ntype SyncWriter struct {\n\tw  io.Writer\n\tmu sync.Mutex\n}\n\n// NewSyncWriter returns a new SyncWriter that wraps the given writer.\nfunc NewSyncWriter(w io.Writer) *SyncWriter {\n\treturn &SyncWriter{w: w}\n}\n\n// Write implements the io.Writer interface with synchronization.\nfunc (sw *SyncWriter) Write(p []byte) (int, error) {\n\tsw.mu.Lock()\n\tdefer sw.mu.Unlock()\n\n\treturn sw.w.Write(p)\n}\n"
  },
  {
    "path": "internal/util/testdata/fixture-glob-canonical/module-a/terragrunt.hcl",
    "content": "terraform {\n  source = \"test\"\n}\n"
  },
  {
    "path": "internal/util/testdata/fixture-glob-canonical/module-b/module-b-child/main.tf",
    "content": ""
  },
  {
    "path": "internal/util/testdata/fixture-glob-canonical/module-b/module-b-child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "internal/util/testdata/fixture-glob-canonical/module-b/root.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket = \"bucket\"\n    key = \"${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\nterraform {\n  source = \"...\"\n}\n"
  },
  {
    "path": "internal/util/testdata/fixture-sanitize-path/env/unit/.terraform-version",
    "content": ""
  },
  {
    "path": "internal/util/trap_writer.go",
    "content": "package util\n\nimport (\n\t\"io\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\n// TrapWriter intercepts any messages received from the `writer` output.\n// Used when necessary to filter logs from terraform.\ntype TrapWriter struct {\n\twriter io.Writer\n\tmsgs   [][]byte\n}\n\n// NewTrapWriter returns a new TrapWriter instance.\nfunc NewTrapWriter(writer io.Writer) *TrapWriter {\n\treturn &TrapWriter{\n\t\twriter: writer,\n\t}\n}\n\n// Flush flushes intercepted messages to the writer.\nfunc (trap *TrapWriter) Flush() error {\n\tfor _, msg := range trap.msgs {\n\t\tif _, err := trap.writer.Write(msg); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Write implements `io.Writer` interface.\nfunc (trap *TrapWriter) Write(d []byte) (int, error) {\n\tmsg := make([]byte, len(d))\n\tcopy(msg, d)\n\n\ttrap.msgs = append(trap.msgs, msg)\n\n\treturn len(msg), nil\n}\n"
  },
  {
    "path": "internal/util/util.go",
    "content": "// Package util provides utility functions for Terragrunt.\npackage util\n"
  },
  {
    "path": "internal/util/writer_notifier.go",
    "content": "package util\n\nimport (\n\t\"io\"\n\t\"sync\"\n)\n\ntype writerNotifier struct {\n\tio.Writer\n\tnotifyFn func(p []byte)\n\tonce     sync.Once\n}\n\n// WriterNotifier fires `notifyFn` once when the first data comes at `Writer(p []byte)` and forwards data further to the specified `writer`.\nfunc WriterNotifier(writer io.Writer, notifyFn func(p []byte)) io.Writer {\n\treturn &writerNotifier{\n\t\tWriter:   writer,\n\t\tnotifyFn: notifyFn,\n\t}\n}\n\nfunc (notifier *writerNotifier) Write(p []byte) (int, error) {\n\tif len(p) > 0 {\n\t\tnotifier.once.Do(func() {\n\t\t\tnotifier.notifyFn(p)\n\t\t})\n\t}\n\n\treturn notifier.Writer.Write(p)\n}\n"
  },
  {
    "path": "internal/vfs/vfs.go",
    "content": "// Package vfs provides a virtual filesystem abstraction for testing and production use.\n// It wraps afero to provide a consistent interface for filesystem operations.\npackage vfs\n\nimport (\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/spf13/afero\"\n)\n\n// FS is the filesystem interface used throughout the codebase.\n// It provides an abstraction over real and in-memory filesystems.\ntype FS = afero.Fs\n\n// NewOSFS returns a filesystem backed by the real operating system filesystem.\nfunc NewOSFS() FS {\n\treturn afero.NewOsFs()\n}\n\n// NewMemMapFS returns an in-memory filesystem for testing purposes.\nfunc NewMemMapFS() FS {\n\treturn afero.NewMemMapFs()\n}\n\n// FileExists checks if a path exists using the given filesystem.\n// Returns (true, nil) if the file exists, (false, nil) if it does not exist,\n// and (false, error) for other errors (e.g., permission denied).\nfunc FileExists(fs FS, path string) (bool, error) {\n\t_, err := fs.Stat(path)\n\tif err == nil {\n\t\treturn true, nil\n\t}\n\n\tif os.IsNotExist(err) {\n\t\treturn false, nil\n\t}\n\n\treturn false, err\n}\n\n// WriteFile writes data to a file on the given filesystem.\nfunc WriteFile(fs FS, filename string, data []byte, perm os.FileMode) error {\n\treturn afero.WriteFile(fs, filename, data, perm)\n}\n\n// ReadFile reads the contents of a file from the given filesystem.\nfunc ReadFile(fs FS, filename string) ([]byte, error) {\n\treturn afero.ReadFile(fs, filename)\n}\n\n// Symlink creates a symbolic link. It uses afero's SymlinkIfPossible\n// which is supported by both OsFs and MemMapFs.\nfunc Symlink(fs FS, oldname, newname string) error {\n\tlinker, ok := fs.(afero.Linker)\n\tif !ok {\n\t\treturn &os.LinkError{Op: \"symlink\", Old: oldname, New: newname, Err: afero.ErrNoSymlink}\n\t}\n\n\treturn linker.SymlinkIfPossible(oldname, newname)\n}\n\n// ZipDecompressor handles zip archive extraction with configurable limits.\ntype ZipDecompressor struct {\n\t// FileSizeLimit limits total decompressed size in bytes. Zero means no limit.\n\tFileSizeLimit int64\n\t// FilesLimit limits the number of files. Zero means no limit.\n\tFilesLimit int\n}\n\n// ZipDecompressorOption is a functional option for configuring ZipDecompressor.\ntype ZipDecompressorOption func(*ZipDecompressor)\n\n// WithFileSizeLimit sets the maximum total decompressed size in bytes.\n// Zero means no limit.\nfunc WithFileSizeLimit(limit int64) ZipDecompressorOption {\n\treturn func(z *ZipDecompressor) {\n\t\tz.FileSizeLimit = limit\n\t}\n}\n\n// WithFilesLimit sets the maximum number of files that can be extracted.\n// Zero means no limit.\nfunc WithFilesLimit(limit int) ZipDecompressorOption {\n\treturn func(z *ZipDecompressor) {\n\t\tz.FilesLimit = limit\n\t}\n}\n\n// NewZipDecompressor creates a new ZipDecompressor with the given options.\nfunc NewZipDecompressor(opts ...ZipDecompressorOption) *ZipDecompressor {\n\tz := &ZipDecompressor{}\n\tfor _, opt := range opts {\n\t\topt(z)\n\t}\n\n\treturn z\n}\n\n// Unzip extracts a zip archive from src to dst directory on the given filesystem.\n// The umask parameter is applied to file permissions (use 0 to preserve original permissions).\nfunc (z *ZipDecompressor) Unzip(l log.Logger, fs FS, dst, src string, umask os.FileMode) error {\n\tfile, err := fs.Open(src)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open zip archive %q: %w\", src, err)\n\t}\n\n\tdefer func() {\n\t\tif closeErr := file.Close(); closeErr != nil {\n\t\t\tl.Warnf(\"Error closing zip archive %q: %v\", src, closeErr)\n\t\t}\n\t}()\n\n\tfileInfo, err := file.Stat()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to stat zip archive %q: %w\", src, err)\n\t}\n\n\tsize := fileInfo.Size()\n\n\tvar readerAt io.ReaderAt\n\tif ra, ok := file.(io.ReaderAt); ok {\n\t\treaderAt = ra\n\t} else {\n\t\tdata, err := io.ReadAll(file)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to read zip archive %q: %w\", src, err)\n\t\t}\n\n\t\treaderAt = bytes.NewReader(data)\n\t\tsize = int64(len(data))\n\t}\n\n\tzipReader, err := zip.NewReader(readerAt, size)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read zip archive %q: %w\", src, err)\n\t}\n\n\tif err := fs.MkdirAll(dst, os.ModePerm); err != nil {\n\t\treturn fmt.Errorf(\"failed to create directory %q: %w\", dst, err)\n\t}\n\n\tif z.FilesLimit > 0 && len(zipReader.File) > z.FilesLimit {\n\t\treturn fmt.Errorf(\n\t\t\t\"zip archive contains %d files, exceeds limit of %d\",\n\t\t\tlen(zipReader.File),\n\t\t\tz.FilesLimit,\n\t\t)\n\t}\n\n\tvar totalSize int64\n\n\tfor _, zipFile := range zipReader.File {\n\t\tif err := z.extractZipFile(l, fs, dst, zipFile, umask, &totalSize); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to extract file %q: %w\", zipFile.Name, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// containsDotDot checks if a path contains \"..\" as a path component.\n// This is more precise than strings.Contains(name, \"..\") which would\n// reject legitimate files like \"file..txt\".\nfunc containsDotDot(v string) bool {\n\tif !strings.Contains(v, \"..\") {\n\t\treturn false\n\t}\n\n\treturn slices.Contains(strings.FieldsFunc(v, func(r rune) bool {\n\t\treturn r == '/' || r == '\\\\'\n\t}), \"..\")\n}\n\n// sanitizeZipPath validates and sanitizes a zip entry path to prevent ZipSlip attacks.\nfunc sanitizeZipPath(dst, name string) (string, error) {\n\tif containsDotDot(name) {\n\t\treturn \"\", fmt.Errorf(\"illegal file path in zip: %s\", name)\n\t}\n\n\tdestPath := filepath.Join(dst, filepath.Clean(name))\n\n\tif !strings.HasPrefix(destPath, filepath.Clean(dst)+string(os.PathSeparator)) {\n\t\treturn \"\", fmt.Errorf(\"illegal destination path in zip: %s\", destPath)\n\t}\n\n\treturn destPath, nil\n}\n\n// extractZipFile extracts a single file from a zip archive.\nfunc (z *ZipDecompressor) extractZipFile(l log.Logger, fs FS, dst string, zipFile *zip.File, umask os.FileMode, totalSize *int64) error {\n\tdestPath, err := sanitizeZipPath(dst, zipFile.Name)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfileInfo := zipFile.FileInfo()\n\n\tif fileInfo.IsDir() {\n\t\tif err := fs.MkdirAll(destPath, applyUmask(fileInfo.Mode(), umask)); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create directory %q: %w\", destPath, err)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif fileInfo.Mode()&os.ModeSymlink != 0 {\n\t\treturn extractSymlink(l, fs, dst, destPath, zipFile)\n\t}\n\n\treturn z.extractRegularFile(l, fs, destPath, zipFile, umask, totalSize)\n}\n\n// validateSymlinkTarget validates that a symlink target doesn't escape the destination directory.\nfunc validateSymlinkTarget(dst, linkPath, target string) error {\n\t// Resolve the target relative to the link's directory\n\tabsTarget := target\n\tif !filepath.IsAbs(target) {\n\t\tabsTarget = filepath.Join(filepath.Dir(linkPath), target)\n\t}\n\n\tabsTarget = filepath.Clean(absTarget)\n\tcleanDst := filepath.Clean(dst)\n\n\t// Ensure it stays within dst\n\tif !strings.HasPrefix(absTarget, cleanDst+string(os.PathSeparator)) && absTarget != cleanDst {\n\t\treturn fmt.Errorf(\"symlink target escapes destination: %s -> %s\", linkPath, target)\n\t}\n\n\treturn nil\n}\n\n// extractSymlink extracts a symlink from a zip file.\nfunc extractSymlink(l log.Logger, fs FS, dst, destPath string, zipFile *zip.File) error {\n\trc, err := zipFile.Open()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open file %q: %w\", zipFile.Name, err)\n\t}\n\n\tdefer func() {\n\t\tif closeErr := rc.Close(); closeErr != nil {\n\t\t\tl.Warnf(\"Error closing file %q: %v\", zipFile.Name, closeErr)\n\t\t}\n\t}()\n\n\ttargetBytes, err := io.ReadAll(rc)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read file %q: %w\", zipFile.Name, err)\n\t}\n\n\ttarget := string(targetBytes)\n\n\t// Validate symlink target doesn't escape destination\n\tif err := validateSymlinkTarget(dst, destPath, target); err != nil {\n\t\treturn err\n\t}\n\n\tif err := fs.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {\n\t\treturn fmt.Errorf(\"failed to create directory %q: %w\", filepath.Dir(destPath), err)\n\t}\n\n\treturn Symlink(fs, target, destPath)\n}\n\n// extractRegularFile extracts a regular file from a zip file.\nfunc (z *ZipDecompressor) extractRegularFile(\n\tl log.Logger,\n\tfs FS,\n\tdestPath string,\n\tzipFile *zip.File,\n\tumask os.FileMode,\n\ttotalSize *int64,\n) error {\n\tif err := fs.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {\n\t\treturn fmt.Errorf(\"failed to create directory %q: %w\", filepath.Dir(destPath), err)\n\t}\n\n\trc, err := zipFile.Open()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open file %q: %w\", zipFile.Name, err)\n\t}\n\n\tdefer func() {\n\t\tif closeErr := rc.Close(); closeErr != nil {\n\t\t\tl.Warnf(\"Error closing file %q: %v\", zipFile.Name, closeErr)\n\t\t}\n\t}()\n\n\tmode := applyUmask(zipFile.FileInfo().Mode(), umask)\n\n\toutFile, err := fs.OpenFile(destPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create file %q: %w\", destPath, err)\n\t}\n\n\tvar reader io.Reader = rc\n\n\tif z.FileSizeLimit > 0 {\n\t\treader = &limitedReader{\n\t\t\treader:    rc,\n\t\t\tremaining: z.FileSizeLimit - *totalSize,\n\t\t}\n\t}\n\n\twritten, err := io.Copy(outFile, reader)\n\tif err != nil {\n\t\tif closeErr := outFile.Close(); closeErr != nil {\n\t\t\tl.Warnf(\"Error closing file %q: %v\", destPath, closeErr)\n\t\t}\n\n\t\tif removeErr := fs.Remove(destPath); removeErr != nil {\n\t\t\tl.Warnf(\"Error removing partial file %q: %v\", destPath, removeErr)\n\t\t}\n\n\t\treturn fmt.Errorf(\"failed to copy file %q: %w\", zipFile.Name, err)\n\t}\n\n\tif err := outFile.Close(); err != nil {\n\t\tl.Warnf(\"Error closing file %q: %v\", destPath, err)\n\t}\n\n\t// Update total size for limit tracking\n\tif z.FileSizeLimit > 0 {\n\t\t*totalSize += written\n\t}\n\n\treturn nil\n}\n\n// limitedReader wraps a reader and enforces a size limit.\ntype limitedReader struct {\n\treader    io.Reader\n\tremaining int64\n}\n\nfunc (r *limitedReader) Read(p []byte) (int, error) {\n\tif r.remaining <= 0 {\n\t\treturn 0, errors.New(\"decompressed size exceeds limit\")\n\t}\n\n\tif int64(len(p)) > r.remaining {\n\t\tp = p[:r.remaining]\n\t}\n\n\tn, err := r.reader.Read(p)\n\tr.remaining -= int64(n)\n\n\treturn n, err\n}\n\n// applyUmask applies a umask to a file mode.\nfunc applyUmask(mode, umask os.FileMode) os.FileMode {\n\treturn mode &^ umask\n}\n"
  },
  {
    "path": "internal/vfs/vfs_test.go",
    "content": "package vfs_test\n\nimport (\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/vfs\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/spf13/afero\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewOSFS(t *testing.T) {\n\tt.Parallel()\n\n\tfs := vfs.NewOSFS()\n\n\tassert.NotNil(t, fs)\n\t_, ok := fs.(*afero.OsFs)\n\tassert.True(t, ok, \"expected *afero.OsFs type\")\n}\n\nfunc TestNewMemMapFS(t *testing.T) {\n\tt.Parallel()\n\n\tfs := vfs.NewMemMapFS()\n\n\tassert.NotNil(t, fs)\n\t_, ok := fs.(*afero.MemMapFs)\n\tassert.True(t, ok, \"expected *afero.MemMapFs type\")\n}\n\nfunc TestFileExists(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tsetup    func(fs vfs.FS)\n\t\tpath     string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname: \"file exists\",\n\t\t\tsetup: func(fs vfs.FS) {\n\t\t\t\trequire.NoError(t, afero.WriteFile(fs, \"/test.txt\", []byte(\"content\"), 0644))\n\t\t\t},\n\t\t\tpath:     \"/test.txt\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"file does not exist\",\n\t\t\tsetup:    func(fs vfs.FS) {},\n\t\t\tpath:     \"/nonexistent.txt\",\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"directory exists\",\n\t\t\tsetup: func(fs vfs.FS) {\n\t\t\t\trequire.NoError(t, fs.MkdirAll(\"/testdir\", 0755))\n\t\t\t},\n\t\t\tpath:     \"/testdir\",\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"parent does not exist\",\n\t\t\tsetup:    func(fs vfs.FS) {},\n\t\t\tpath:     \"/nonexistent/file.txt\",\n\t\t\texpected: false,\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.Parallel()\n\n\t\t\tfs := vfs.NewMemMapFS()\n\t\t\ttc.setup(fs)\n\n\t\t\texists, err := vfs.FileExists(fs, tc.path)\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, exists)\n\t\t})\n\t}\n}\n\nfunc TestWriteFile(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tfilename string\n\t\tdata     []byte\n\t\tperm     os.FileMode\n\t}{\n\t\t{\n\t\t\tname:     \"write simple file\",\n\t\t\tfilename: \"/test.txt\",\n\t\t\tdata:     []byte(\"hello world\"),\n\t\t\tperm:     0644,\n\t\t},\n\t\t{\n\t\t\tname:     \"write with restricted permissions\",\n\t\t\tfilename: \"/restricted.txt\",\n\t\t\tdata:     []byte(\"secret\"),\n\t\t\tperm:     0600,\n\t\t},\n\t\t{\n\t\t\tname:     \"write to nested directory\",\n\t\t\tfilename: \"/nested/path/file.txt\",\n\t\t\tdata:     []byte(\"nested content\"),\n\t\t\tperm:     0644,\n\t\t},\n\t\t{\n\t\t\tname:     \"write empty file\",\n\t\t\tfilename: \"/empty.txt\",\n\t\t\tdata:     []byte{},\n\t\t\tperm:     0644,\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.Parallel()\n\n\t\t\tfs := vfs.NewMemMapFS()\n\n\t\t\terr := vfs.WriteFile(fs, tc.filename, tc.data, tc.perm)\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\texists, err := vfs.FileExists(fs, tc.filename)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.True(t, exists)\n\t\t})\n\t}\n}\n\nfunc TestReadFile(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"read existing file\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\texpected := []byte(\"test content\")\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/test.txt\", expected, 0644))\n\n\t\tdata, err := vfs.ReadFile(fs, \"/test.txt\")\n\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, expected, data)\n\t})\n\n\tt.Run(\"read non-existent file\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\n\t\t_, err := vfs.ReadFile(fs, \"/nonexistent.txt\")\n\n\t\trequire.Error(t, err)\n\t})\n}\n\nfunc TestSymlink(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"create valid symlink\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewOSFS()\n\t\ttempDir := t.TempDir()\n\t\ttargetPath := filepath.Join(tempDir, \"target.txt\")\n\t\tlinkPath := filepath.Join(tempDir, \"link.txt\")\n\n\t\trequire.NoError(t, vfs.WriteFile(fs, targetPath, []byte(\"target content\"), 0644))\n\n\t\terr := vfs.Symlink(fs, targetPath, linkPath)\n\n\t\trequire.NoError(t, err)\n\n\t\tdata, err := vfs.ReadFile(fs, linkPath)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []byte(\"target content\"), data)\n\t})\n\n\tt.Run(\"symlink to non-existent target\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewOSFS()\n\t\ttempDir := t.TempDir()\n\t\tlinkPath := filepath.Join(tempDir, \"dangling_link.txt\")\n\n\t\terr := vfs.Symlink(fs, \"/nonexistent/target\", linkPath)\n\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"filesystem without symlink support returns LinkError\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := afero.NewReadOnlyFs(vfs.NewMemMapFS())\n\n\t\terr := vfs.Symlink(fs, \"target\", \"link\")\n\n\t\trequire.Error(t, err)\n\n\t\tvar linkErr *os.LinkError\n\t\tassert.ErrorAs(t, err, &linkErr)\n\t})\n}\n\nfunc TestUnzip(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\tt.Run(\"extract single file\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\tzipData := createZipArchive(t, map[string][]byte{\n\t\t\t\"file.txt\": []byte(\"file content\"),\n\t\t})\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/archive.zip\", zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, \"/dst\", \"/archive.zip\", 0)\n\n\t\trequire.NoError(t, err)\n\n\t\tdata, err := vfs.ReadFile(fs, \"/dst/file.txt\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []byte(\"file content\"), data)\n\t})\n\n\tt.Run(\"extract archive with directories\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\tzipData := createZipArchiveWithDirs(t, map[string][]byte{\n\t\t\t\"dir/\":         nil,\n\t\t\t\"dir/file.txt\": []byte(\"nested file\"),\n\t\t})\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/archive.zip\", zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, \"/dst\", \"/archive.zip\", 0)\n\n\t\trequire.NoError(t, err)\n\n\t\texists, err := vfs.FileExists(fs, \"/dst/dir\")\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, exists)\n\n\t\tdata, err := vfs.ReadFile(fs, \"/dst/dir/file.txt\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []byte(\"nested file\"), data)\n\t})\n\n\tt.Run(\"extract archive with nested structure\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\tzipData := createZipArchive(t, map[string][]byte{\n\t\t\t\"a/b/c/deep.txt\": []byte(\"deep content\"),\n\t\t\t\"root.txt\":       []byte(\"root content\"),\n\t\t})\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/archive.zip\", zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, \"/dst\", \"/archive.zip\", 0)\n\n\t\trequire.NoError(t, err)\n\n\t\tdata, err := vfs.ReadFile(fs, \"/dst/a/b/c/deep.txt\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []byte(\"deep content\"), data)\n\n\t\tdata, err = vfs.ReadFile(fs, \"/dst/root.txt\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []byte(\"root content\"), data)\n\t})\n\n\tt.Run(\"extract archive with multiple files\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\tzipData := createZipArchive(t, map[string][]byte{\n\t\t\t\"file1.txt\": []byte(\"content1\"),\n\t\t\t\"file2.txt\": []byte(\"content2\"),\n\t\t\t\"file3.txt\": []byte(\"content3\"),\n\t\t})\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/archive.zip\", zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, \"/dst\", \"/archive.zip\", 0)\n\n\t\trequire.NoError(t, err)\n\n\t\tfor i := 1; i <= 3; i++ {\n\t\t\tdata, err := vfs.ReadFile(fs, \"/dst/file\"+string(rune('0'+i))+\".txt\")\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, []byte(\"content\"+string(rune('0'+i))), data)\n\t\t}\n\t})\n\n\tt.Run(\"zipslip prevention - path with dotdot\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\tzipData := createZipArchiveUnsafe(t, map[string][]byte{\n\t\t\t\"../escaped.txt\": []byte(\"malicious\"),\n\t\t})\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/archive.zip\", zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, \"/dst\", \"/archive.zip\", 0)\n\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"illegal file path\")\n\t})\n\n\tt.Run(\"zipslip prevention - nested dotdot\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\tzipData := createZipArchiveUnsafe(t, map[string][]byte{\n\t\t\t\"foo/../../escaped.txt\": []byte(\"malicious\"),\n\t\t})\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/archive.zip\", zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, \"/dst\", \"/archive.zip\", 0)\n\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"illegal file path\")\n\t})\n\n\tt.Run(\"permissions preserved with umask 0\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewOSFS()\n\t\ttempDir := t.TempDir()\n\t\tzipPath := filepath.Join(tempDir, \"archive.zip\")\n\t\tdstPath := filepath.Join(tempDir, \"dst\")\n\n\t\tzipData := createZipArchiveWithMode(t, \"executable.sh\", []byte(\"#!/bin/bash\"), 0755)\n\t\trequire.NoError(t, vfs.WriteFile(fs, zipPath, zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, dstPath, zipPath, 0)\n\n\t\trequire.NoError(t, err)\n\n\t\tinfo, err := fs.Stat(filepath.Join(dstPath, \"executable.sh\"))\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, os.FileMode(0755), info.Mode().Perm())\n\t})\n\n\tt.Run(\"permissions with umask applied\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewOSFS()\n\t\ttempDir := t.TempDir()\n\t\tzipPath := filepath.Join(tempDir, \"archive.zip\")\n\t\tdstPath := filepath.Join(tempDir, \"dst\")\n\n\t\tzipData := createZipArchiveWithMode(t, \"file.txt\", []byte(\"content\"), 0666)\n\t\trequire.NoError(t, vfs.WriteFile(fs, zipPath, zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, dstPath, zipPath, 0022)\n\n\t\trequire.NoError(t, err)\n\n\t\tinfo, err := fs.Stat(filepath.Join(dstPath, \"file.txt\"))\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, os.FileMode(0644), info.Mode().Perm())\n\t})\n\n\tt.Run(\"non-existent source file\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, \"/dst\", \"/nonexistent.zip\", 0)\n\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"failed to open zip archive\")\n\t})\n\n\tt.Run(\"invalid archive\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/invalid.zip\", []byte(\"not a zip file\"), 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, \"/dst\", \"/invalid.zip\", 0)\n\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"failed to read zip archive\")\n\t})\n\n\tt.Run(\"extract to existing directory\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\trequire.NoError(t, fs.MkdirAll(\"/dst\", 0755))\n\t\tzipData := createZipArchive(t, map[string][]byte{\n\t\t\t\"new.txt\": []byte(\"new content\"),\n\t\t})\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/archive.zip\", zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, \"/dst\", \"/archive.zip\", 0)\n\n\t\trequire.NoError(t, err)\n\n\t\tdata, err := vfs.ReadFile(fs, \"/dst/new.txt\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []byte(\"new content\"), data)\n\t})\n}\n\nfunc TestUnzipWithSymlinks(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\tfs := vfs.NewOSFS()\n\ttempDir := t.TempDir()\n\tzipPath := filepath.Join(tempDir, \"archive.zip\")\n\tdstPath := filepath.Join(tempDir, \"dst\")\n\n\tzipData := createZipArchiveWithSymlink(t, \"target.txt\", []byte(\"target content\"), \"link.txt\", \"target.txt\")\n\trequire.NoError(t, vfs.WriteFile(fs, zipPath, zipData, 0644))\n\n\terr := vfs.NewZipDecompressor().Unzip(l, fs, dstPath, zipPath, 0)\n\n\trequire.NoError(t, err)\n\n\ttargetData, err := vfs.ReadFile(fs, filepath.Join(dstPath, \"target.txt\"))\n\trequire.NoError(t, err)\n\tassert.Equal(t, []byte(\"target content\"), targetData)\n\n\tlinkData, err := vfs.ReadFile(fs, filepath.Join(dstPath, \"link.txt\"))\n\trequire.NoError(t, err)\n\tassert.Equal(t, []byte(\"target content\"), linkData)\n}\n\n// createZipArchive creates a zip archive in memory with the given files.\nfunc createZipArchive(t *testing.T, files map[string][]byte) []byte {\n\tt.Helper()\n\n\tvar buf bytes.Buffer\n\n\tw := zip.NewWriter(&buf)\n\n\tfor name, content := range files {\n\t\tf, err := w.Create(name)\n\t\trequire.NoError(t, err)\n\n\t\t_, err = f.Write(content)\n\t\trequire.NoError(t, err)\n\t}\n\n\trequire.NoError(t, w.Close())\n\n\treturn buf.Bytes()\n}\n\n// createZipArchiveWithDirs creates a zip archive that includes directory entries.\nfunc createZipArchiveWithDirs(t *testing.T, files map[string][]byte) []byte {\n\tt.Helper()\n\n\tvar buf bytes.Buffer\n\n\tw := zip.NewWriter(&buf)\n\n\tfor name, content := range files {\n\t\tif content == nil {\n\t\t\t_, err := w.Create(name)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tf, err := w.Create(name)\n\t\trequire.NoError(t, err)\n\n\t\t_, err = f.Write(content)\n\t\trequire.NoError(t, err)\n\t}\n\n\trequire.NoError(t, w.Close())\n\n\treturn buf.Bytes()\n}\n\n// createZipArchiveUnsafe creates a zip archive with potentially malicious paths (for testing ZipSlip).\nfunc createZipArchiveUnsafe(t *testing.T, files map[string][]byte) []byte {\n\tt.Helper()\n\n\tvar buf bytes.Buffer\n\n\tw := zip.NewWriter(&buf)\n\n\tfor name, content := range files {\n\t\theader := &zip.FileHeader{\n\t\t\tName:   name,\n\t\t\tMethod: zip.Deflate,\n\t\t}\n\n\t\tf, err := w.CreateHeader(header)\n\t\trequire.NoError(t, err)\n\n\t\t_, err = f.Write(content)\n\t\trequire.NoError(t, err)\n\t}\n\n\trequire.NoError(t, w.Close())\n\n\treturn buf.Bytes()\n}\n\n// createZipArchiveWithMode creates a zip archive with a single file with specific permissions.\nfunc createZipArchiveWithMode(t *testing.T, name string, content []byte, mode os.FileMode) []byte {\n\tt.Helper()\n\n\tvar buf bytes.Buffer\n\n\tw := zip.NewWriter(&buf)\n\n\theader := &zip.FileHeader{\n\t\tName:   name,\n\t\tMethod: zip.Deflate,\n\t}\n\theader.SetMode(mode)\n\n\tf, err := w.CreateHeader(header)\n\trequire.NoError(t, err)\n\n\t_, err = f.Write(content)\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, w.Close())\n\n\treturn buf.Bytes()\n}\n\n// createZipArchiveWithSymlink creates a zip archive with a regular file and a symlink to it.\nfunc createZipArchiveWithSymlink(t *testing.T, targetName string, targetContent []byte, linkName, linkTarget string) []byte {\n\tt.Helper()\n\n\tvar buf bytes.Buffer\n\n\tw := zip.NewWriter(&buf)\n\n\ttargetHeader := &zip.FileHeader{\n\t\tName:   targetName,\n\t\tMethod: zip.Deflate,\n\t}\n\ttargetHeader.SetMode(0644)\n\n\tf, err := w.CreateHeader(targetHeader)\n\trequire.NoError(t, err)\n\n\t_, err = f.Write(targetContent)\n\trequire.NoError(t, err)\n\n\tlinkHeader := &zip.FileHeader{\n\t\tName:   linkName,\n\t\tMethod: zip.Deflate,\n\t}\n\tlinkHeader.SetMode(os.ModeSymlink | 0777)\n\n\tlinkFile, err := w.CreateHeader(linkHeader)\n\trequire.NoError(t, err)\n\n\t_, err = linkFile.Write([]byte(linkTarget))\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, w.Close())\n\n\treturn buf.Bytes()\n}\n\nfunc TestContainsDotDot(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\tt.Run(\"allows file with double dots in name\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\tzipData := createZipArchive(t, map[string][]byte{\n\t\t\t\"file..txt\": []byte(\"content with dots\"),\n\t\t})\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/archive.zip\", zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, \"/dst\", \"/archive.zip\", 0)\n\n\t\trequire.NoError(t, err)\n\n\t\tdata, err := vfs.ReadFile(fs, \"/dst/file..txt\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []byte(\"content with dots\"), data)\n\t})\n\n\tt.Run(\"allows file with multiple dots\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\tzipData := createZipArchive(t, map[string][]byte{\n\t\t\t\"my..file..name.txt\": []byte(\"multiple dots\"),\n\t\t})\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/archive.zip\", zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, \"/dst\", \"/archive.zip\", 0)\n\n\t\trequire.NoError(t, err)\n\n\t\tdata, err := vfs.ReadFile(fs, \"/dst/my..file..name.txt\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []byte(\"multiple dots\"), data)\n\t})\n\n\tt.Run(\"blocks path with dotdot component\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\tzipData := createZipArchiveUnsafe(t, map[string][]byte{\n\t\t\t\"../evil.txt\": []byte(\"malicious\"),\n\t\t})\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/archive.zip\", zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, \"/dst\", \"/archive.zip\", 0)\n\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"illegal file path\")\n\t})\n\n\tt.Run(\"blocks nested dotdot path\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\tzipData := createZipArchiveUnsafe(t, map[string][]byte{\n\t\t\t\"subdir/../../../evil.txt\": []byte(\"malicious\"),\n\t\t})\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/archive.zip\", zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, \"/dst\", \"/archive.zip\", 0)\n\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"illegal file path\")\n\t})\n}\n\nfunc TestUnzipFilesLimit(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\tt.Run(\"allows extraction within file limit\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\tzipData := createZipArchive(t, map[string][]byte{\n\t\t\t\"file1.txt\": []byte(\"content1\"),\n\t\t\t\"file2.txt\": []byte(\"content2\"),\n\t\t})\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/archive.zip\", zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor(vfs.WithFilesLimit(5)).Unzip(l, fs, \"/dst\", \"/archive.zip\", 0)\n\n\t\trequire.NoError(t, err)\n\n\t\texists, err := vfs.FileExists(fs, \"/dst/file1.txt\")\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, exists)\n\t})\n\n\tt.Run(\"rejects extraction exceeding file limit\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\tzipData := createZipArchive(t, map[string][]byte{\n\t\t\t\"file1.txt\": []byte(\"content1\"),\n\t\t\t\"file2.txt\": []byte(\"content2\"),\n\t\t\t\"file3.txt\": []byte(\"content3\"),\n\t\t})\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/archive.zip\", zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor(vfs.WithFilesLimit(2)).Unzip(l, fs, \"/dst\", \"/archive.zip\", 0)\n\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"exceeds limit\")\n\t})\n\n\tt.Run(\"no limit when FilesLimit is zero\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\tzipData := createZipArchive(t, map[string][]byte{\n\t\t\t\"file1.txt\": []byte(\"content1\"),\n\t\t\t\"file2.txt\": []byte(\"content2\"),\n\t\t\t\"file3.txt\": []byte(\"content3\"),\n\t\t})\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/archive.zip\", zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, \"/dst\", \"/archive.zip\", 0)\n\n\t\trequire.NoError(t, err)\n\t})\n}\n\nfunc TestUnzipFileSizeLimit(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\tt.Run(\"allows extraction within size limit\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\tzipData := createZipArchive(t, map[string][]byte{\n\t\t\t\"small.txt\": []byte(\"small content\"),\n\t\t})\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/archive.zip\", zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor(vfs.WithFileSizeLimit(1000)).Unzip(l, fs, \"/dst\", \"/archive.zip\", 0)\n\n\t\trequire.NoError(t, err)\n\n\t\tdata, err := vfs.ReadFile(fs, \"/dst/small.txt\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []byte(\"small content\"), data)\n\t})\n\n\tt.Run(\"rejects extraction exceeding size limit\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\t// Create content that exceeds 10 bytes\n\t\tzipData := createZipArchive(t, map[string][]byte{\n\t\t\t\"large.txt\": []byte(\"this content is definitely more than 10 bytes\"),\n\t\t})\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/archive.zip\", zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor(vfs.WithFileSizeLimit(10)).Unzip(l, fs, \"/dst\", \"/archive.zip\", 0)\n\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"exceeds limit\")\n\t})\n\n\tt.Run(\"cumulative size limit across files\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\t// Each file is 10 bytes, total 30 bytes\n\t\tzipData := createZipArchive(t, map[string][]byte{\n\t\t\t\"file1.txt\": []byte(\"0123456789\"),\n\t\t\t\"file2.txt\": []byte(\"0123456789\"),\n\t\t\t\"file3.txt\": []byte(\"0123456789\"),\n\t\t})\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/archive.zip\", zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor(vfs.WithFileSizeLimit(25)).Unzip(l, fs, \"/dst\", \"/archive.zip\", 0)\n\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"exceeds limit\")\n\t})\n\n\tt.Run(\"no limit when FileSizeLimit is zero\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewMemMapFS()\n\t\tzipData := createZipArchive(t, map[string][]byte{\n\t\t\t\"file.txt\": []byte(\"content that would exceed any small limit\"),\n\t\t})\n\t\trequire.NoError(t, vfs.WriteFile(fs, \"/archive.zip\", zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, \"/dst\", \"/archive.zip\", 0)\n\n\t\trequire.NoError(t, err)\n\t})\n}\n\nfunc TestUnzipSymlinkEscape(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\tt.Run(\"allows symlink to file within destination\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewOSFS()\n\t\ttempDir := t.TempDir()\n\t\tzipPath := filepath.Join(tempDir, \"archive.zip\")\n\t\tdstPath := filepath.Join(tempDir, \"dst\")\n\n\t\tzipData := createZipArchiveWithSymlink(t, \"target.txt\", []byte(\"target content\"), \"link.txt\", \"target.txt\")\n\t\trequire.NoError(t, vfs.WriteFile(fs, zipPath, zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, dstPath, zipPath, 0)\n\n\t\trequire.NoError(t, err)\n\n\t\tlinkData, err := vfs.ReadFile(fs, filepath.Join(dstPath, \"link.txt\"))\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, []byte(\"target content\"), linkData)\n\t})\n\n\tt.Run(\"rejects symlink escaping destination with absolute path\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewOSFS()\n\t\ttempDir := t.TempDir()\n\t\tzipPath := filepath.Join(tempDir, \"archive.zip\")\n\t\tdstPath := filepath.Join(tempDir, \"dst\")\n\n\t\t// Create symlink pointing to absolute path outside destination\n\t\tzipData := createZipArchiveWithSymlink(t, \"target.txt\", []byte(\"target content\"), \"evil_link.txt\", \"/etc/passwd\")\n\t\trequire.NoError(t, vfs.WriteFile(fs, zipPath, zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, dstPath, zipPath, 0)\n\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"symlink target escapes destination\")\n\t})\n\n\tt.Run(\"rejects symlink escaping destination with relative path\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewOSFS()\n\t\ttempDir := t.TempDir()\n\t\tzipPath := filepath.Join(tempDir, \"archive.zip\")\n\t\tdstPath := filepath.Join(tempDir, \"dst\")\n\n\t\t// Create symlink pointing outside destination with ..\n\t\tzipData := createZipArchiveWithSymlink(t, \"target.txt\", []byte(\"target content\"), \"evil_link.txt\", \"../../../etc/passwd\")\n\t\trequire.NoError(t, vfs.WriteFile(fs, zipPath, zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, dstPath, zipPath, 0)\n\n\t\trequire.Error(t, err)\n\t\tassert.Contains(t, err.Error(), \"symlink target escapes destination\")\n\t})\n\n\tt.Run(\"allows symlink within nested directory\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tfs := vfs.NewOSFS()\n\t\ttempDir := t.TempDir()\n\t\tzipPath := filepath.Join(tempDir, \"archive.zip\")\n\t\tdstPath := filepath.Join(tempDir, \"dst\")\n\n\t\t// Create symlink in subdirectory pointing to file in same directory\n\t\tzipData := createZipArchiveWithNestedSymlink(t)\n\t\trequire.NoError(t, vfs.WriteFile(fs, zipPath, zipData, 0644))\n\n\t\terr := vfs.NewZipDecompressor().Unzip(l, fs, dstPath, zipPath, 0)\n\n\t\trequire.NoError(t, err)\n\t})\n}\n\n// createZipArchiveWithNestedSymlink creates a zip with a symlink in a subdirectory.\nfunc createZipArchiveWithNestedSymlink(t *testing.T) []byte {\n\tt.Helper()\n\n\tvar buf bytes.Buffer\n\n\tw := zip.NewWriter(&buf)\n\n\t// Create target file in subdir\n\ttargetHeader := &zip.FileHeader{\n\t\tName:   \"subdir/target.txt\",\n\t\tMethod: zip.Deflate,\n\t}\n\ttargetHeader.SetMode(0644)\n\n\tf, err := w.CreateHeader(targetHeader)\n\trequire.NoError(t, err)\n\n\t_, err = f.Write([]byte(\"target content\"))\n\trequire.NoError(t, err)\n\n\t// Create symlink in same subdir pointing to target\n\tlinkHeader := &zip.FileHeader{\n\t\tName:   \"subdir/link.txt\",\n\t\tMethod: zip.Deflate,\n\t}\n\tlinkHeader.SetMode(os.ModeSymlink | 0777)\n\n\tlinkFile, err := w.CreateHeader(linkHeader)\n\trequire.NoError(t, err)\n\n\t_, err = linkFile.Write([]byte(\"target.txt\"))\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, w.Close())\n\n\treturn buf.Bytes()\n}\n"
  },
  {
    "path": "internal/view/diagnostic/diagnostic.go",
    "content": "// Package diagnostic provides a way to represent diagnostics in a way\n// that can be easily marshalled to JSON.\npackage diagnostic\n\nimport (\n\t\"github.com/hashicorp/hcl/v2\"\n)\n\ntype Diagnostics []*Diagnostic\n\nfunc (diags *Diagnostics) Contains(find *Diagnostic) bool {\n\tfor _, diag := range *diags {\n\t\tif find.Range != nil && find.Range.String() == diag.Range.String() {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\ntype Diagnostic struct {\n\tRange    *Range             `json:\"range,omitempty\"`\n\tSnippet  *Snippet           `json:\"snippet,omitempty\"`\n\tSummary  string             `json:\"summary\"`\n\tDetail   string             `json:\"detail\"`\n\tSeverity DiagnosticSeverity `json:\"severity\"`\n}\n\nfunc NewDiagnostic(file *hcl.File, hclDiag *hcl.Diagnostic) *Diagnostic {\n\tdiag := &Diagnostic{\n\t\tSeverity: DiagnosticSeverity(hclDiag.Severity),\n\t\tSummary:  hclDiag.Summary,\n\t\tDetail:   hclDiag.Detail,\n\t}\n\n\tif hclDiag.Subject == nil {\n\t\treturn diag\n\t}\n\n\thighlightRange := *hclDiag.Subject\n\tif highlightRange.Empty() {\n\t\thighlightRange.End.Byte++\n\t\thighlightRange.End.Column++\n\t}\n\n\tdiag.Snippet = NewSnippet(file, hclDiag, highlightRange)\n\n\tdiag.Range = &Range{\n\t\tFilename: highlightRange.Filename,\n\t\tStart: Pos{\n\t\t\tLine:   highlightRange.Start.Line,\n\t\t\tColumn: highlightRange.Start.Column,\n\t\t\tByte:   highlightRange.Start.Byte,\n\t\t},\n\t\tEnd: Pos{\n\t\t\tLine:   highlightRange.End.Line,\n\t\t\tColumn: highlightRange.End.Column,\n\t\t\tByte:   highlightRange.End.Byte,\n\t\t},\n\t}\n\n\treturn diag\n}\n"
  },
  {
    "path": "internal/view/diagnostic/expression_value.go",
    "content": "package diagnostic\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/zclconf/go-cty/cty\"\n)\n\nconst (\n\t// Sensitive indicates that this value is marked as sensitive in the context of Terraform.\n\tSensitive = valueMark(\"Sensitive\")\n)\n\n// valueMarks allow creating strictly typed values for use as cty.Value marks.\ntype valueMark string\n\nfunc (m valueMark) GoString() string {\n\treturn \"marks.\" + string(m)\n}\n\n// ExpressionValue represents an HCL traversal string and a statement about its value while the expression was evaluated.\ntype ExpressionValue struct {\n\tTraversal string `json:\"traversal\"`\n\tStatement string `json:\"statement\"`\n}\n\nfunc DescribeExpressionValues(hclDiag *hcl.Diagnostic) []ExpressionValue {\n\tvar (\n\t\texpr = hclDiag.Expression\n\t\tctx  = hclDiag.EvalContext\n\n\t\tvars             = expr.Variables()\n\t\tvalues           = make([]ExpressionValue, 0, len(vars))\n\t\tseen             = make(map[string]struct{}, len(vars))\n\t\tincludeUnknown   = DiagnosticCausedByUnknown(hclDiag)\n\t\tincludeSensitive = DiagnosticCausedBySensitive(hclDiag)\n\t)\n\nTraversals:\n\tfor _, traversal := range vars {\n\t\tfor len(traversal) > 1 {\n\t\t\tval, diags := traversal.TraverseAbs(ctx)\n\t\t\tif diags.HasErrors() {\n\t\t\t\ttraversal = traversal[:len(traversal)-1]\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttraversalStr := traversalStr(traversal)\n\t\t\tif _, exists := seen[traversalStr]; exists {\n\t\t\t\tcontinue Traversals\n\t\t\t}\n\n\t\t\tvalue := ExpressionValue{\n\t\t\t\tTraversal: traversalStr,\n\t\t\t}\n\n\t\t\tswitch {\n\t\t\tcase val.HasMark(Sensitive):\n\t\t\t\tif !includeSensitive {\n\t\t\t\t\tcontinue Traversals\n\t\t\t\t}\n\n\t\t\t\tvalue.Statement = \"has a sensitive value\"\n\t\t\tcase !val.IsKnown():\n\t\t\t\tif ty := val.Type(); ty != cty.DynamicPseudoType {\n\t\t\t\t\tif includeUnknown {\n\t\t\t\t\t\tvalue.Statement = fmt.Sprintf(\"is a %s, known only after apply\", ty.FriendlyName())\n\t\t\t\t\t} else {\n\t\t\t\t\t\tvalue.Statement = \"is a \" + ty.FriendlyName()\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif !includeUnknown {\n\t\t\t\t\t\tcontinue Traversals\n\t\t\t\t\t}\n\n\t\t\t\t\tvalue.Statement = \"will be known only after apply\"\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tvalue.Statement = \"is \" + valueStr(val)\n\t\t\t}\n\n\t\t\tvalues = append(values, value)\n\t\t\tseen[traversalStr] = struct{}{}\n\t\t}\n\t}\n\n\tsort.Slice(values, func(i, j int) bool {\n\t\treturn values[i].Traversal < values[j].Traversal\n\t})\n\n\treturn values\n}\n\nfunc traversalStr(traversal hcl.Traversal) string {\n\tvar buf bytes.Buffer\n\n\tfor _, step := range traversal {\n\t\tswitch tStep := step.(type) {\n\t\tcase hcl.TraverseRoot:\n\t\t\tbuf.WriteString(tStep.Name)\n\t\tcase hcl.TraverseAttr:\n\t\t\tbuf.WriteByte('.')\n\t\t\tbuf.WriteString(tStep.Name)\n\t\tcase hcl.TraverseIndex:\n\t\t\tbuf.WriteByte('[')\n\n\t\t\tif keyTy := tStep.Key.Type(); keyTy.IsPrimitiveType() {\n\t\t\t\tbuf.WriteString(valueStr(tStep.Key))\n\t\t\t} else {\n\t\t\t\t// We'll just use a placeholder for more complex values, since otherwise our result could grow ridiculously long.\n\t\t\t\tbuf.WriteString(\"...\")\n\t\t\t}\n\n\t\t\tbuf.WriteByte(']')\n\t\t}\n\t}\n\n\treturn buf.String()\n}\n\nfunc valueStr(val cty.Value) string {\n\tif val.HasMark(Sensitive) {\n\t\treturn \"(sensitive value)\"\n\t}\n\n\tty := val.Type()\n\n\tswitch {\n\tcase val.IsNull():\n\t\treturn \"null\"\n\tcase !val.IsKnown():\n\t\treturn \"(not yet known)\"\n\tcase ty == cty.Bool:\n\t\tif val.True() {\n\t\t\treturn \"true\"\n\t\t}\n\n\t\treturn \"false\"\n\tcase ty == cty.Number:\n\t\tbf := val.AsBigFloat()\n\t\tprec := 10\n\n\t\treturn bf.Text('g', prec)\n\tcase ty == cty.String:\n\t\treturn fmt.Sprintf(\"%q\", val.AsString())\n\tcase ty.IsCollectionType() || ty.IsTupleType():\n\t\tl := val.LengthInt()\n\t\tswitch l {\n\t\tcase 0:\n\t\t\treturn \"empty \" + ty.FriendlyName()\n\t\tcase 1:\n\t\t\treturn ty.FriendlyName() + \" with 1 element\"\n\t\tdefault:\n\t\t\treturn fmt.Sprintf(\"%s with %d elements\", ty.FriendlyName(), l)\n\t\t}\n\tcase ty.IsObjectType():\n\t\tatys := ty.AttributeTypes()\n\t\tl := len(atys)\n\n\t\tswitch l {\n\t\tcase 0:\n\t\t\treturn \"object with no attributes\"\n\t\tcase 1:\n\t\t\tvar name string\n\t\t\tfor k := range atys {\n\t\t\t\tname = k\n\t\t\t}\n\n\t\t\treturn fmt.Sprintf(\"object with 1 attribute %q\", name)\n\t\tdefault:\n\t\t\treturn fmt.Sprintf(\"object with %d attributes\", l)\n\t\t}\n\tdefault:\n\t\treturn ty.FriendlyName()\n\t}\n}\n"
  },
  {
    "path": "internal/view/diagnostic/extra.go",
    "content": "package diagnostic\n\nimport \"github.com/hashicorp/hcl/v2\"\n\nfunc ExtraInfo[T any](diag *hcl.Diagnostic) T {\n\textra := diag.Extra\n\tif ret, ok := extra.(T); ok {\n\t\treturn ret\n\t}\n\n\t// If \"extra\" doesn't implement T directly then we'll delegate to our ExtraInfoNext helper to try iteratively unwrapping it.\n\treturn ExtraInfoNext[T](extra)\n}\n\n// ExtraInfoNext takes a value previously returned by ExtraInfo and attempts to find an implementation of interface T wrapped inside of it. The return value meaning is the same as for ExtraInfo.\nfunc ExtraInfoNext[T any](previous any) T {\n\t// As long as T is an interface type as documented, zero will always be a nil interface value for us to return in the non-matching case.\n\tvar zero T\n\n\tunwrapper, ok := previous.(DiagnosticExtraUnwrapper)\n\t// If the given value isn't unwrappable then it can't possibly have any other info nested inside of it.\n\tif !ok {\n\t\treturn zero\n\t}\n\n\textra := unwrapper.UnwrapDiagnosticExtra()\n\n\t// Keep unwrapping until we either find the interface to look for or we run out of layers of unwrapper.\n\tfor {\n\t\tif ret, ok := extra.(T); ok {\n\t\t\treturn ret\n\t\t}\n\n\t\tif unwrapper, ok := extra.(DiagnosticExtraUnwrapper); ok {\n\t\t\textra = unwrapper.UnwrapDiagnosticExtra()\n\t\t} else {\n\t\t\treturn zero\n\t\t}\n\t}\n}\n\n// DiagnosticExtraUnwrapper is an interface implemented by values in the Extra field of Diagnostic when they are wrapping another \"Extra\" value that was generated downstream.\ntype DiagnosticExtraUnwrapper interface {\n\tUnwrapDiagnosticExtra() any\n}\n\n// DiagnosticExtraBecauseUnknown is an interface implemented by values in the Extra field of Diagnostic when the diagnostic is potentially caused by the presence of unknown values in an expression evaluation.\ntype DiagnosticExtraBecauseUnknown interface {\n\tDiagnosticCausedByUnknown() bool\n}\n\n// DiagnosticCausedByUnknown returns true if the given diagnostic has an indication that it was caused by the presence of unknown values during an expression evaluation.\nfunc DiagnosticCausedByUnknown(diag *hcl.Diagnostic) bool {\n\tmaybe := ExtraInfo[DiagnosticExtraBecauseUnknown](diag)\n\tif maybe == nil {\n\t\treturn false\n\t}\n\n\treturn maybe.DiagnosticCausedByUnknown()\n}\n\n// DiagnosticExtraBecauseSensitive is an interface implemented by values in the Extra field of Diagnostic when the diagnostic is potentially caused by the presence of sensitive values in an expression evaluation.\ntype DiagnosticExtraBecauseSensitive interface {\n\tDiagnosticCausedBySensitive() bool\n}\n\n// DiagnosticCausedBySensitive returns true if the given diagnostic has an/ indication that it was caused by the presence of sensitive values during an expression evaluation.\nfunc DiagnosticCausedBySensitive(diag *hcl.Diagnostic) bool {\n\tmaybe := ExtraInfo[DiagnosticExtraBecauseSensitive](diag)\n\tif maybe == nil {\n\t\treturn false\n\t}\n\n\treturn maybe.DiagnosticCausedBySensitive()\n}\n"
  },
  {
    "path": "internal/view/diagnostic/function.go",
    "content": "package diagnostic\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/hashicorp/hcl/v2/hclsyntax\"\n\t\"github.com/zclconf/go-cty/cty\"\n\t\"github.com/zclconf/go-cty/cty/function\"\n)\n\n// FunctionParam represents a single parameter to a function, as represented by type Function.\ntype FunctionParam struct {\n\t// Name is a name for the function which is used primarily for documentation purposes.\n\tName string `json:\"name\"`\n\n\tDescription     string `json:\"description,omitempty\"`\n\tDescriptionKind string `json:\"description_kind,omitempty\"`\n\n\t// Type is a type constraint which is a static approximation of the possibly-dynamic type of the parameter\n\tType json.RawMessage `json:\"type\"`\n}\n\nfunc DescribeFunctionParam(p *function.Parameter) FunctionParam {\n\tret := FunctionParam{\n\t\tName: p.Name,\n\t}\n\n\tif raw, err := p.Type.MarshalJSON(); err != nil {\n\t\t// Treat any errors as if the function is dynamically typed because it would be weird to get here.\n\t\tret.Type = json.RawMessage(`\"dynamic\"`)\n\t} else {\n\t\tret.Type = raw\n\t}\n\n\treturn ret\n}\n\n// Function is a description of the JSON representation of the signature of a function callable from the Terraform language.\ntype Function struct {\n\tVariadicParam *FunctionParam `json:\"variadic_param,omitempty\"`\n\n\t// Name is the leaf name of the function, without any namespace prefix.\n\tName string `json:\"name\"`\n\n\tDescription     string          `json:\"description,omitempty\"`\n\tDescriptionKind string          `json:\"description_kind,omitempty\"`\n\tParams          []FunctionParam `json:\"params\"`\n\n\t// ReturnType is type constraint which is a static approximation of the possibly-dynamic return type of the function.\n\tReturnType json.RawMessage `json:\"return_type\"`\n}\n\n// DescribeFunction returns a description of the signature of the given cty function, as a pointer to this package's serializable type Function.\nfunc DescribeFunction(name string, f function.Function) *Function {\n\tret := &Function{\n\t\tName: name,\n\t}\n\n\tparams := f.Params()\n\tret.Params = make([]FunctionParam, len(params))\n\ttypeCheckArgs := make([]cty.Type, len(params), len(params)+1)\n\n\tfor i, param := range params {\n\t\tret.Params[i] = DescribeFunctionParam(&param)\n\t\ttypeCheckArgs[i] = param.Type\n\t}\n\n\tif varParam := f.VarParam(); varParam != nil {\n\t\tdescParam := DescribeFunctionParam(varParam)\n\t\tret.VariadicParam = &descParam\n\n\t\ttypeCheckArgs = append(typeCheckArgs, varParam.Type)\n\t}\n\n\tretType, err := f.ReturnType(typeCheckArgs)\n\tif err != nil {\n\t\tretType = cty.DynamicPseudoType\n\t}\n\n\tif raw, err := retType.MarshalJSON(); err != nil {\n\t\t// Treat any errors as if the function is dynamically typed because it would be weird to get here.\n\t\tret.ReturnType = json.RawMessage(`\"dynamic\"`)\n\t} else {\n\t\tret.ReturnType = raw\n\t}\n\n\treturn ret\n}\n\n// FunctionCall represents a function call whose information is being included as part of a diagnostic snippet.\ntype FunctionCall struct {\n\t// Signature is a description of the signature of the function that was/ called, if any.:\n\tSignature *Function `json:\"signature,omitempty\"`\n\n\t// CalledAs is the full name that was used to call this function, potentially including namespace prefixes if the function does not belong to the default function namespace.\n\tCalledAs string `json:\"called_as\"`\n}\n\nfunc DescribeFunctionCall(hclDiag *hcl.Diagnostic) *FunctionCall {\n\tcallInfo := ExtraInfo[hclsyntax.FunctionCallDiagExtra](hclDiag)\n\tif callInfo == nil || callInfo.CalledFunctionName() == \"\" {\n\t\treturn nil\n\t}\n\n\tcalledAs := callInfo.CalledFunctionName()\n\n\tbaseName := calledAs\n\tif idx := strings.LastIndex(baseName, \"::\"); idx >= 0 {\n\t\tbaseName = baseName[idx+2:]\n\t}\n\n\tvar signature *Function\n\n\tif f, ok := hclDiag.EvalContext.Functions[calledAs]; ok {\n\t\tsignature = DescribeFunction(baseName, f)\n\t}\n\n\treturn &FunctionCall{\n\t\tCalledAs:  calledAs,\n\t\tSignature: signature,\n\t}\n}\n"
  },
  {
    "path": "internal/view/diagnostic/range.go",
    "content": "package diagnostic\n\nimport \"fmt\"\n\n// Pos represents a position in the source code.\ntype Pos struct {\n\t// Line is a one-based count for the line in the indicated file.\n\tLine int `json:\"line\"`\n\n\t// Column is a one-based count of Unicode characters from the start of the line.\n\tColumn int `json:\"column\"`\n\n\t// Byte is a zero-based offset into the indicated file.\n\tByte int `json:\"byte\"`\n}\n\n// Range represents the filename and position of the diagnostic subject.\ntype Range struct {\n\tFilename string `json:\"filename\"`\n\tStart    Pos    `json:\"start\"`\n\tEnd      Pos    `json:\"end\"`\n}\n\nfunc (rng Range) String() string {\n\tif rng.Start.Line == rng.End.Line {\n\t\treturn fmt.Sprintf(\n\t\t\t\"%s:%d,%d-%d\",\n\t\t\trng.Filename,\n\t\t\trng.Start.Line, rng.Start.Column,\n\t\t\trng.End.Column,\n\t\t)\n\t} else {\n\t\treturn fmt.Sprintf(\n\t\t\t\"%s:%d,%d-%d,%d\",\n\t\t\trng.Filename,\n\t\t\trng.Start.Line, rng.Start.Column,\n\t\t\trng.End.Line, rng.End.Column,\n\t\t)\n\t}\n}\n"
  },
  {
    "path": "internal/view/diagnostic/servity.go",
    "content": "package diagnostic\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/hcl/v2\"\n)\n\nconst (\n\tDiagnosticSeverityUnknown = \"unknown\"\n\tDiagnosticSeverityError   = \"error\"\n\tDiagnosticSeverityWarning = \"warning\"\n)\n\ntype DiagnosticSeverity hcl.DiagnosticSeverity\n\nfunc (severity DiagnosticSeverity) String() string {\n\t// TODO: Remove lint suppression\n\tswitch hcl.DiagnosticSeverity(severity) { //nolint:exhaustive\n\tcase hcl.DiagError:\n\t\treturn DiagnosticSeverityError\n\tcase hcl.DiagWarning:\n\t\treturn DiagnosticSeverityWarning\n\tdefault:\n\t\treturn DiagnosticSeverityUnknown\n\t}\n}\n\nfunc (severity DiagnosticSeverity) MarshalJSON() ([]byte, error) {\n\treturn fmt.Appendf(nil, `\"%s\"`, severity.String()), nil\n}\n\nfunc (severity *DiagnosticSeverity) UnmarshalJSON(val []byte) error {\n\tswitch strings.Trim(string(val), `\"`) {\n\tcase DiagnosticSeverityError:\n\t\t*severity = DiagnosticSeverity(hcl.DiagError)\n\tcase DiagnosticSeverityWarning:\n\t\t*severity = DiagnosticSeverity(hcl.DiagWarning)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/view/diagnostic/snippet.go",
    "content": "package diagnostic\n\nimport (\n\t\"bufio\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/hashicorp/hcl/v2/hcled\"\n)\n\n// Snippet represents source code information about the diagnostic.\ntype Snippet struct {\n\t// FunctionCall is information about a function call whose failure is being reported by this diagnostic, if any.\n\tFunctionCall *FunctionCall `json:\"function_call,omitempty\"`\n\n\t// Context is derived from HCL's hcled.ContextString output. This gives a high-level summary of the root context of the diagnostic.\n\tContext string `json:\"context\"`\n\n\t// Code is a possibly-multi-line string of OpenTofu/Terraform configuration, which includes both the diagnostic source and any relevant context as defined by the diagnostic.\n\tCode string `json:\"code\"`\n\n\t// Values is a sorted slice of expression values which may be useful in understanding the source of an error in a complex expression.\n\tValues []ExpressionValue `json:\"values\"`\n\n\t// StartLine is the line number in the source file for the first line of the snippet code block.\n\tStartLine int `json:\"start_line\"`\n\n\t// HighlightStartOffset is the character offset into Code at which the diagnostic source range starts, which ought to be highlighted as such by the consumer of this data.\n\tHighlightStartOffset int `json:\"highlight_start_offset\"`\n\n\t// HighlightEndOffset is the character offset into Code at which the diagnostic source range ends.\n\tHighlightEndOffset int `json:\"highlight_end_offset\"`\n}\n\nfunc NewSnippet(file *hcl.File, hclDiag *hcl.Diagnostic, highlightRange hcl.Range) *Snippet {\n\tsnipRange := *hclDiag.Subject\n\tif hclDiag.Context != nil {\n\t\t// Show enough of the source code to include both the subject and context ranges, which overlap in all reasonable situations.\n\t\tsnipRange = hcl.RangeOver(snipRange, *hclDiag.Context)\n\t}\n\n\tif snipRange.Empty() {\n\t\tsnipRange.End.Byte++\n\t\tsnipRange.End.Column++\n\t}\n\n\tsnippet := &Snippet{\n\t\tStartLine: hclDiag.Subject.Start.Line,\n\t}\n\n\tif file != nil && file.Bytes != nil {\n\t\tsnippet.Context = hcled.ContextString(file, hclDiag.Subject.Start.Byte-1)\n\n\t\tvar (\n\t\t\tcodeStartByte int\n\t\t\tcode          strings.Builder\n\t\t)\n\n\t\tsc := hcl.NewRangeScanner(file.Bytes, hclDiag.Subject.Filename, bufio.ScanLines)\n\n\t\tfor sc.Scan() {\n\t\t\tlineRange := sc.Range()\n\t\t\tif lineRange.Overlaps(snipRange) {\n\t\t\t\tif codeStartByte == 0 && code.Len() == 0 {\n\t\t\t\t\tcodeStartByte = lineRange.Start.Byte\n\t\t\t\t}\n\n\t\t\t\tcode.Write(lineRange.SliceBytes(file.Bytes))\n\t\t\t\tcode.WriteRune('\\n')\n\t\t\t}\n\t\t}\n\n\t\tcodeStr := strings.TrimSuffix(code.String(), \"\\n\")\n\t\tsnippet.Code = codeStr\n\n\t\tstart := highlightRange.Start.Byte - codeStartByte\n\t\tend := start + (highlightRange.End.Byte - highlightRange.Start.Byte)\n\n\t\tif start < 0 {\n\t\t\tstart = 0\n\t\t} else if start > len(codeStr) {\n\t\t\tstart = len(codeStr)\n\t\t}\n\n\t\tif end < 0 {\n\t\t\tend = 0\n\t\t} else if end > len(codeStr) {\n\t\t\tend = len(codeStr)\n\t\t}\n\n\t\tsnippet.HighlightStartOffset = start\n\t\tsnippet.HighlightEndOffset = end\n\t}\n\n\tif hclDiag.Expression == nil || hclDiag.EvalContext == nil {\n\t\treturn snippet\n\t}\n\n\tsnippet.Values = DescribeExpressionValues(hclDiag)\n\tsnippet.FunctionCall = DescribeFunctionCall(hclDiag)\n\n\treturn snippet\n}\n"
  },
  {
    "path": "internal/view/human_render.go",
    "content": "package view\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/view/diagnostic\"\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/mitchellh/colorstring\"\n\t\"github.com/mitchellh/go-wordwrap\"\n\t\"golang.org/x/term\"\n)\n\nconst defaultWidth = 78\n\ntype HumanRender struct {\n\tcolorize *colorstring.Colorize\n\twidth    int\n}\n\nfunc NewHumanRender(disableColor bool) Render {\n\tdisableColor = disableColor || !term.IsTerminal(int(os.Stderr.Fd()))\n\n\twidth, _, err := term.GetSize(int(os.Stdout.Fd()))\n\tif err != nil {\n\t\twidth = defaultWidth\n\t}\n\n\treturn &HumanRender{\n\t\tcolorize: &colorstring.Colorize{\n\t\t\tColors:  colorstring.DefaultColors,\n\t\t\tDisable: disableColor,\n\t\t\tReset:   true,\n\t\t},\n\t\twidth: width,\n\t}\n}\n\nfunc (render *HumanRender) ShowConfigPath(filenames []string) (string, error) {\n\tvar buf bytes.Buffer\n\n\tfor _, filename := range filenames {\n\t\tbuf.WriteString(filename)\n\t\tbuf.WriteByte('\\n')\n\t}\n\n\treturn buf.String(), nil\n}\n\nfunc (render *HumanRender) Diagnostics(diags diagnostic.Diagnostics) (string, error) {\n\tvar buf bytes.Buffer\n\n\tfor _, diag := range diags {\n\t\tstr, err := render.Diagnostic(diag)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif str != \"\" {\n\t\t\tbuf.WriteString(str)\n\t\t\tbuf.WriteByte('\\n')\n\t\t}\n\t}\n\n\treturn buf.String(), nil\n}\n\n// Diagnostic formats a single diagnostic message.\nfunc (render *HumanRender) Diagnostic(diag *diagnostic.Diagnostic) (string, error) {\n\tvar buf bytes.Buffer\n\n\t// these leftRule* variables are markers for the beginning of the lines\n\t// containing the diagnostic that are intended to help sighted users\n\t// better understand the information hierarchy when diagnostics appear\n\t// alongside other information or alongside other diagnostics.\n\t//\n\t// Without this, it seems (based on folks sharing incomplete messages when\n\t// asking questions, or including extra content that's not part of the\n\t// diagnostic) that some readers have trouble easily identifying which\n\t// text belongs to the diagnostic and which does not.\n\tvar (\n\t\tleftRuleLine, leftRuleStart, leftRuleEnd string\n\t\tleftRuleWidth                            int // in visual character cells\n\t)\n\n\t// TODO: Remove lint suppression\n\tswitch hcl.DiagnosticSeverity(diag.Severity) { //nolint:exhaustive\n\tcase hcl.DiagError:\n\t\tbuf.WriteString(render.colorize.Color(\"[bold][red]Error: [reset]\"))\n\t\tleftRuleLine = render.colorize.Color(\"[red]│[reset] \")\n\t\tleftRuleStart = render.colorize.Color(\"[red]╷[reset]\")\n\t\tleftRuleEnd = render.colorize.Color(\"[red]╵[reset]\")\n\t\tleftRuleWidth = 2\n\tcase hcl.DiagWarning:\n\t\tbuf.WriteString(render.colorize.Color(\"[bold][yellow]Warning: [reset]\"))\n\t\tleftRuleLine = render.colorize.Color(\"[yellow]│[reset] \")\n\t\tleftRuleStart = render.colorize.Color(\"[yellow]╷[reset]\")\n\t\tleftRuleEnd = render.colorize.Color(\"[yellow]╵[reset]\")\n\t\tleftRuleWidth = 2\n\tdefault:\n\t\t// Clear out any coloring that might be applied by Terraform's UI helper,\n\t\t// so our result is not context-sensitive.\n\t\tbuf.WriteString(render.colorize.Color(\"\\n[reset]\"))\n\t}\n\n\t// We don't wrap the summary, since we expect it to be terse, and since\n\t// this is where we put the text of a native Go error it may not always\n\t// be pure text that lends itself well to word-wrapping.\n\tif _, err := fmt.Fprintf(&buf, render.colorize.Color(\"[bold]%s[reset]\\n\\n\"), diag.Summary); err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\tsourceSnippets, err := render.SourceSnippets(diag)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tbuf.WriteString(sourceSnippets)\n\n\tif diag.Detail != \"\" {\n\t\tparaWidth := render.width - leftRuleWidth - 1 // leave room for the left rule\n\t\tif paraWidth > 0 {\n\t\t\tlines := strings.SplitSeq(diag.Detail, \"\\n\")\n\t\t\tfor line := range lines {\n\t\t\t\tif !strings.HasPrefix(line, \" \") {\n\t\t\t\t\tline = wordwrap.WrapString(line, uint(paraWidth))\n\t\t\t\t}\n\n\t\t\t\tif _, err := fmt.Fprintf(&buf, \"%s\\n\", line); err != nil {\n\t\t\t\t\treturn \"\", errors.New(err)\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tif _, err := fmt.Fprintf(&buf, \"%s\\n\", diag.Detail); err != nil {\n\t\t\t\treturn \"\", errors.New(err)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Before we return, we'll finally add the left rule prefixes to each\n\t// line so that the overall message is visually delimited from what's\n\t// around it. We'll do that by scanning over what we already generated\n\t// and adding the prefix for each line.\n\tvar ruleBuf strings.Builder\n\n\tsc := bufio.NewScanner(&buf)\n\n\truleBuf.WriteString(leftRuleStart)\n\truleBuf.WriteByte('\\n')\n\n\tfor sc.Scan() {\n\t\tprefix := leftRuleLine\n\n\t\tline := sc.Text()\n\t\tif line == \"\" {\n\t\t\t// Don't print the space after the line if there would be nothing\n\t\t\t// after it anyway.\n\t\t\tprefix = strings.TrimSpace(prefix)\n\t\t}\n\n\t\truleBuf.WriteString(prefix)\n\t\truleBuf.WriteString(line)\n\t\truleBuf.WriteByte('\\n')\n\t}\n\n\truleBuf.WriteString(leftRuleEnd)\n\n\treturn ruleBuf.String(), nil\n}\n\nfunc (render *HumanRender) SourceSnippets(diag *diagnostic.Diagnostic) (string, error) {\n\tif diag.Range == nil || diag.Snippet == nil {\n\t\t// This should generally not happen, as long as sources are always\n\t\t// loaded through the main loader. We may load things in other\n\t\t// ways in weird cases, so we'll tolerate it at the expense of\n\t\t// a not-so-helpful error message.\n\t\treturn fmt.Sprintf(\"  on %s line %d:\\n  (source code not available)\\n\", diag.Range.Filename, diag.Range.Start.Line), nil\n\t}\n\n\tvar (\n\t\tbuf     = new(bytes.Buffer)\n\t\tsnippet = diag.Snippet\n\t\tcode    = snippet.Code\n\t)\n\n\tvar contextStr string\n\tif snippet.Context != \"\" {\n\t\tcontextStr = \", in \" + snippet.Context\n\t}\n\n\tif _, err := fmt.Fprintf(buf, \"  on %s line %d%s:\\n\", diag.Range.Filename, diag.Range.Start.Line, contextStr); err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\t// Split the snippet and render the highlighted section with underlines\n\tstart := snippet.HighlightStartOffset\n\tend := snippet.HighlightEndOffset\n\n\t// Only buggy diagnostics can have an end range before the start, but\n\t// we need to ensure we don't crash here if that happens.\n\tif end < start {\n\t\tend = min(start+1, len(code))\n\t}\n\n\t// If either start or end is out of range for the code buffer then\n\t// we'll cap them at the bounds just to avoid a panic, although\n\t// this would happen only if there's a bug in the code generating\n\t// the snippet objects.\n\tif start < 0 {\n\t\tstart = 0\n\t} else if start > len(code) {\n\t\tstart = len(code)\n\t}\n\n\tif end < 0 {\n\t\tend = 0\n\t} else if end > len(code) {\n\t\tend = len(code)\n\t}\n\n\tbefore, highlight, after := code[0:start], code[start:end], code[end:]\n\tcode = fmt.Sprintf(render.colorize.Color(\"%s[underline][white]%s[reset]%s\"), before, highlight, after)\n\n\t// Split the snippet into lines and render one at a time\n\tlines := strings.Split(code, \"\\n\")\n\tfor i, line := range lines {\n\t\tif _, err := fmt.Fprintf(\n\t\t\tbuf, \"%4d: %s\\n\",\n\t\t\tsnippet.StartLine+i,\n\t\t\tline,\n\t\t); err != nil {\n\t\t\treturn \"\", errors.New(err)\n\t\t}\n\t}\n\n\tif len(snippet.Values) > 0 || (snippet.FunctionCall != nil && snippet.FunctionCall.Signature != nil) {\n\t\t// The diagnostic may also have information about the dynamic\n\t\t// values of relevant variables at the point of evaluation.\n\t\t// This is particularly useful for expressions that get evaluated\n\t\t// multiple times with different values, such as blocks using\n\t\t// \"count\" and \"for_each\", or within \"for\" expressions.\n\t\tvalues := make([]diagnostic.ExpressionValue, len(snippet.Values))\n\t\tcopy(values, snippet.Values)\n\t\tsort.Slice(values, func(i, j int) bool {\n\t\t\treturn values[i].Traversal < values[j].Traversal\n\t\t})\n\n\t\tfmt.Fprint(buf, render.colorize.Color(\"    [dark_gray]├────────────────[reset]\\n\"))\n\n\t\tif callInfo := snippet.FunctionCall; callInfo != nil && callInfo.Signature != nil {\n\t\t\tif _, err := fmt.Fprintf(buf, render.colorize.Color(\"    [dark_gray]│[reset] while calling [bold]%s[reset](\"), callInfo.CalledAs); err != nil {\n\t\t\t\treturn \"\", errors.New(err)\n\t\t\t}\n\n\t\t\tfor i, param := range callInfo.Signature.Params {\n\t\t\t\tif i > 0 {\n\t\t\t\t\tbuf.WriteString(\", \")\n\t\t\t\t}\n\n\t\t\t\tbuf.WriteString(param.Name)\n\t\t\t}\n\n\t\t\tif param := callInfo.Signature.VariadicParam; param != nil {\n\t\t\t\tif len(callInfo.Signature.Params) > 0 {\n\t\t\t\t\tbuf.WriteString(\", \")\n\t\t\t\t}\n\n\t\t\t\tbuf.WriteString(param.Name)\n\t\t\t\tbuf.WriteString(\"...\")\n\t\t\t}\n\n\t\t\tbuf.WriteString(\")\\n\")\n\t\t}\n\n\t\tfor _, value := range values {\n\t\t\tif _, err := fmt.Fprintf(buf, render.colorize.Color(\"    [dark_gray]│[reset] [bold]%s[reset] %s\\n\"), value.Traversal, value.Statement); err != nil {\n\t\t\t\treturn \"\", errors.New(err)\n\t\t\t}\n\t\t}\n\t}\n\n\tbuf.WriteByte('\\n')\n\n\treturn buf.String(), nil\n}\n"
  },
  {
    "path": "internal/view/json_render.go",
    "content": "package view\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/view/diagnostic\"\n)\n\ntype JSONRender struct{}\n\nfunc NewJSONRender() Render {\n\treturn &JSONRender{}\n}\n\nfunc (render *JSONRender) Diagnostics(diags diagnostic.Diagnostics) (string, error) {\n\treturn render.toJSON(diags)\n}\n\nfunc (render *JSONRender) ShowConfigPath(filenames []string) (string, error) {\n\treturn render.toJSON(filenames)\n}\n\nfunc (render *JSONRender) toJSON(val any) (string, error) {\n\tjsonBytes, err := json.Marshal(val)\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\tif len(jsonBytes) == 0 {\n\t\treturn \"\", nil\n\t}\n\n\tjsonBytes = append(jsonBytes, '\\n')\n\n\treturn string(jsonBytes), nil\n}\n"
  },
  {
    "path": "internal/view/view.go",
    "content": "// Package view contains the rendering logic for terragrunt.\npackage view\n"
  },
  {
    "path": "internal/view/writer.go",
    "content": "package view\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"slices\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/view/diagnostic\"\n)\n\ntype Render interface {\n\t// Diagnostics renders early diagnostics, resulting from argument parsing.\n\tDiagnostics(diags diagnostic.Diagnostics) (string, error)\n\n\t// ShowConfigPath renders paths to configurations that contain errors.\n\tShowConfigPath(filenames []string) (string, error)\n}\n\n// Writer is the base layer for command views, encapsulating a set of I/O streams, a colorize implementation, and implementing a human friendly view for diagnostics.\ntype Writer struct {\n\tio.Writer\n\trender Render\n}\n\nfunc NewWriter(writer io.Writer, render Render) *Writer {\n\treturn &Writer{\n\t\tWriter: writer,\n\t\trender: render,\n\t}\n}\n\nfunc (writer *Writer) Diagnostics(diags diagnostic.Diagnostics) error {\n\toutput, err := writer.render.Diagnostics(diags)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn writer.output(output)\n}\n\nfunc (writer *Writer) ShowConfigPath(diags diagnostic.Diagnostics) error {\n\tvar filenames []string\n\n\tfor _, diag := range diags {\n\t\tif diag.Range != nil && diag.Range.Filename != \"\" && !slices.Contains(filenames, diag.Range.Filename) {\n\t\t\tfilenames = append(filenames, diag.Range.Filename)\n\t\t}\n\t}\n\n\toutput, err := writer.render.ShowConfigPath(filenames)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn writer.output(output)\n}\n\nfunc (writer *Writer) output(output string) error {\n\tif _, err := fmt.Fprint(writer, output); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/worker/worker.go",
    "content": "// Package worker provides a concurrent task execution system with a configurable number of workers.\n//\n// It allows for controlled parallel execution of tasks while managing resources efficiently through\n// a semaphore-based worker pool. Key features include:\n//\n// - Configurable maximum number of concurrent workers\n// - Non-blocking task submission\n// - Graceful shutdown capabilities\n// - Error collection and aggregation\n// - Thread-safe operations\n//\n// The Pool struct manages a pool of workers that can execute tasks concurrently while\n// limiting the number of goroutines running simultaneously. This prevents resource exhaustion\n// while maximizing throughput.\n//\n// This implementation is particularly useful for scenarios where you need to process many\n// independent tasks with controlled parallelism, such as in infrastructure management tools,\n// batch processing systems, or any application requiring concurrent execution with resource\n// constraints.\npackage worker\n\nimport (\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\n// Task represents a unit of work that can be executed\ntype Task func() error\n\n// Pool manages concurrent task execution with a configurable number of workers\ntype Pool struct {\n\tsemaphore   chan struct{}\n\tallErrors   *errors.MultiError\n\twg          sync.WaitGroup\n\tmaxWorkers  int\n\tmu          sync.RWMutex\n\tallErrorsMu sync.RWMutex\n\tisStopping  atomic.Bool\n\tisRunning   bool\n}\n\n// NewWorkerPool creates a new worker pool with the specified maximum number of concurrent workers\nfunc NewWorkerPool(maxWorkers int) *Pool {\n\tif maxWorkers <= 0 {\n\t\tmaxWorkers = 1\n\t}\n\n\treturn &Pool{\n\t\tmaxWorkers: maxWorkers,\n\t\tsemaphore:  make(chan struct{}, maxWorkers),\n\t\tisRunning:  false,\n\t\tallErrors:  &errors.MultiError{},\n\t}\n}\n\n// Start initializes the worker pool\nfunc (wp *Pool) Start() {\n\twp.mu.Lock()\n\n\tif wp.isRunning {\n\t\twp.mu.Unlock()\n\t\treturn\n\t}\n\n\twp.isRunning = true\n\twp.isStopping.Store(false)\n\n\twp.semaphore = make(chan struct{}, wp.maxWorkers)\n\n\t// Reset allErrors\n\twp.allErrorsMu.Lock()\n\twp.allErrors = &errors.MultiError{}\n\twp.allErrorsMu.Unlock()\n\n\twp.mu.Unlock()\n}\n\n// appendError safely appends an error to allErrors\nfunc (wp *Pool) appendError(err error) {\n\tif err == nil {\n\t\treturn\n\t}\n\n\twp.allErrorsMu.Lock()\n\twp.allErrors = wp.allErrors.Append(err)\n\twp.allErrorsMu.Unlock()\n}\n\n// Submit adds a new task and starts a goroutine to execute it when a worker is available\nfunc (wp *Pool) Submit(task Task) {\n\twp.mu.RLock()\n\tnotRunning := !wp.isRunning\n\twp.mu.RUnlock()\n\n\tif notRunning {\n\t\twp.Start()\n\t}\n\n\t// Don't submit new tasks if the pool is stopping\n\tif wp.isStopping.Load() {\n\t\treturn\n\t}\n\n\twp.wg.Add(1)\n\n\t// Start a new goroutine for each task, but limit concurrency with semaphore\n\tgo func() {\n\t\tdefer wp.wg.Done()\n\n\t\twp.semaphore <- struct{}{}\n\n\t\tdefer func() { <-wp.semaphore }()\n\n\t\terr := task()\n\t\tif err != nil {\n\t\t\twp.appendError(err)\n\t\t}\n\t}()\n}\n\n// Wait blocks until all tasks are completed and returns any errors\nfunc (wp *Pool) Wait() error {\n\t// Wait for all tasks to complete\n\twp.wg.Wait()\n\n\t// Get all collected errors\n\twp.allErrorsMu.RLock()\n\tresult := wp.allErrors.ErrorOrNil()\n\twp.allErrorsMu.RUnlock()\n\n\treturn result\n}\n\n// Stop shuts down the worker pool after current tasks are completed\nfunc (wp *Pool) Stop() {\n\twp.mu.Lock()\n\tdefer wp.mu.Unlock()\n\n\tif wp.isRunning {\n\t\t// Mark as stopping to prevent new task submissions\n\t\twp.isStopping.Store(true)\n\n\t\tgo func() {\n\t\t\twp.wg.Wait()\n\n\t\t\twp.mu.Lock()\n\t\t\twp.isRunning = false\n\t\t\twp.mu.Unlock()\n\t\t}()\n\t}\n}\n\n// GracefulStop waits for all tasks to complete before stopping the pool\nfunc (wp *Pool) GracefulStop() error {\n\t// Mark as stopping to prevent new task submissions\n\twp.isStopping.Store(true)\n\n\t// Wait for all tasks to complete and capture any errors\n\terr := wp.Wait()\n\n\t// Now fully stop the pool\n\twp.mu.Lock()\n\tdefer wp.mu.Unlock()\n\n\tif wp.isRunning {\n\t\twp.isRunning = false\n\t}\n\n\treturn err\n}\n\n// IsRunning returns whether the pool is currently running\nfunc (wp *Pool) IsRunning() bool {\n\twp.mu.RLock()\n\tdefer wp.mu.RUnlock()\n\n\treturn wp.isRunning\n}\n\n// IsStopping returns whether the pool is in the process of stopping\nfunc (wp *Pool) IsStopping() bool {\n\treturn wp.isStopping.Load()\n}\n"
  },
  {
    "path": "internal/worker/worker_test.go",
    "content": "package worker_test\n\nimport (\n\t\"sync/atomic\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/worker\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAllTasksCompleteWithoutErrors(t *testing.T) {\n\tt.Parallel()\n\n\twp := worker.NewWorkerPool(5)\n\tdefer wp.Stop()\n\n\tvar counter int32\n\n\t// Submit 10 tasks that increment a counter\n\tfor range 10 {\n\t\twp.Submit(func() error {\n\t\t\tatomic.AddInt32(&counter, 1)\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t// Wait for all tasks to complete\n\terrs := wp.Wait()\n\trequire.NoError(t, errs)\n\n\tif atomic.LoadInt32(&counter) != 10 {\n\t\tt.Errorf(\"expected counter to be 10, got %d\", counter)\n\t}\n}\n\nfunc TestSubmitLessAllTasksCompleteWithoutErrors(t *testing.T) {\n\tt.Parallel()\n\n\twp := worker.NewWorkerPool(10)\n\tdefer wp.Stop()\n\n\tvar counter int32\n\n\tfor range 5 {\n\t\twp.Submit(func() error {\n\t\t\tatomic.AddInt32(&counter, 1)\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t// Wait for all tasks to complete\n\terrs := wp.Wait()\n\trequire.NoError(t, errs)\n\n\tif atomic.LoadInt32(&counter) != 5 {\n\t\tt.Errorf(\"expected counter to be 5, got %d\", counter)\n\t}\n}\n\nfunc TestSomeTasksReturnErrors(t *testing.T) {\n\tt.Parallel()\n\n\twp := worker.NewWorkerPool(3)\n\tdefer wp.Stop()\n\n\tvar successCount int32\n\n\t// Submit tasks, half of which return an error\n\tfor i := range 10 {\n\t\twp.Submit(func() error {\n\t\t\tif i%2 == 0 {\n\t\t\t\treturn errors.New(\"mock error\")\n\t\t\t}\n\n\t\t\tatomic.AddInt32(&successCount, 1)\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\terrs := wp.Wait()\n\trequire.Error(t, errs)\n\n\tvar multiErr *errors.MultiError\n\trequire.True(t, errors.As(errs, &multiErr), \"expected *errors.MultiError, got %T\", errs)\n\trequire.Len(t, multiErr.WrappedErrors(), 5, \"expected exactly 5 errors, got %d\", len(multiErr.WrappedErrors()))\n\n\tif atomic.LoadInt32(&successCount) != 5 {\n\t\tt.Errorf(\"expected successCount to be 5, got %d\", successCount)\n\t}\n}\n\nfunc TestStopAndRestart(t *testing.T) {\n\tt.Parallel()\n\n\twp := worker.NewWorkerPool(2)\n\n\tvar counter int32\n\n\t// Submit some tasks\n\tfor range 5 {\n\t\twp.Submit(func() error {\n\t\t\tatomic.AddInt32(&counter, 1)\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t// Wait for all tasks to complete and stop the pool\n\terr := wp.Wait()\n\trequire.NoError(t, err)\n\twp.Stop()\n\n\tfinalCount := atomic.LoadInt32(&counter)\n\trequire.Equal(t, int32(5), finalCount, \"expected counter to be 5\")\n\n\t// Create a new worker pool instead of assuming restart\n\twp = worker.NewWorkerPool(2)\n\tdefer wp.Stop()\n\n\t// Submit new tasks\n\tfor range 3 {\n\t\twp.Submit(func() error {\n\t\t\tatomic.AddInt32(&counter, 1)\n\t\t\treturn nil\n\t\t})\n\t}\n\n\terrs := wp.Wait()\n\trequire.NoError(t, errs)\n\n\tfinalCountAfterRestart := atomic.LoadInt32(&counter)\n\trequire.Equal(t, int32(8), finalCountAfterRestart, \"expected counter to be 8\")\n}\nfunc TestParallelSubmitsAndWaits(t *testing.T) {\n\tt.Parallel()\n\n\twp := worker.NewWorkerPool(4)\n\n\tt.Cleanup(func() { wp.Stop() })\n\n\tvar totalCount int32\n\n\tt.Run(\"parallelTaskSubmit1\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tlocalWp := worker.NewWorkerPool(4) // Create a new worker pool per subtest\n\t\tdefer localWp.Stop()\n\n\t\tfor range 10 {\n\t\t\tlocalWp.Submit(func() error {\n\t\t\t\tatomic.AddInt32(&totalCount, 1)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}\n\n\t\terr := localWp.Wait()\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"parallelTaskSubmit2\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tlocalWp := worker.NewWorkerPool(4) // Create another fresh worker pool\n\t\tdefer localWp.Stop()\n\n\t\tfor range 15 {\n\t\t\tlocalWp.Submit(func() error {\n\t\t\t\tatomic.AddInt32(&totalCount, 1)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}\n\n\t\terr := localWp.Wait()\n\t\trequire.NoError(t, err)\n\t})\n}\n\nfunc TestValidateParallelSubmits(t *testing.T) {\n\tt.Parallel()\n\n\twp := worker.NewWorkerPool(1)\n\tdefer wp.Stop()\n\n\tvar totalCount int32\n\n\t// Submit 5 tasks\n\tfor range 5 {\n\t\twp.Submit(func() error {\n\t\t\tatomic.AddInt32(&totalCount, 1)\n\t\t\treturn nil\n\t\t})\n\t}\n\n\terrs := wp.Wait()\n\trequire.NoError(t, errs)\n\n\tif atomic.LoadInt32(&totalCount) != 5 {\n\t\tt.Errorf(\"expected totalCount to be 5, got %d\", totalCount)\n\t}\n}\n"
  },
  {
    "path": "internal/worktrees/worktrees.go",
    "content": "// Package worktrees provides functionality for creating and managing Git worktrees for operating across multiple\n// Git references.\npackage worktrees\n\nimport (\n\t\"context\"\n\t\"maps\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"golang.org/x/sync/errgroup\"\n)\n\n// Worktrees is a map of WorktreePairs, and the Git runner used to create and manage the worktrees.\n// The key is the string representation of the GitExpression that generated the worktree pair.\ntype Worktrees struct {\n\tWorktreePairs      map[string]WorktreePair\n\tgitRunner          *git.GitRunner\n\tOriginalWorkingDir string\n}\n\n// WorktreePair is a pair of worktrees, one for the from and one for the to reference, along with\n// the GitExpression that generated the diffs and the diff for that expression.\ntype WorktreePair struct {\n\tGitExpression *filter.GitExpression\n\tDiffs         *git.Diffs\n\tFromWorktree  Worktree\n\tToWorktree    Worktree\n}\n\n// Worktree is collects a Git reference and the path to the associated worktree.\ntype Worktree struct {\n\tRef  string\n\tPath string\n}\n\n// WorkingDir returns the path within a worktree that corresponds to the user's\n// original working directory. This is used for display purposes after discovery completes.\nfunc (w *Worktrees) WorkingDir(ctx context.Context, worktreePath string) string {\n\tif w.gitRunner == nil {\n\t\treturn worktreePath\n\t}\n\n\trepoRoot, err := w.gitRunner.GetRepoRoot(ctx)\n\tif err != nil {\n\t\treturn worktreePath\n\t}\n\n\trelPath, err := filepath.Rel(repoRoot, w.OriginalWorkingDir)\n\tif err != nil || relPath == \".\" {\n\t\treturn worktreePath\n\t}\n\n\treturn filepath.Join(worktreePath, relPath)\n}\n\n// DisplayPath translates a worktree path to the equivalent path in the original repository\n// for user-facing output. This is useful for logging and reporting where users expect to see\n// paths relative to their working directory, not temporary worktree paths.\n// If the path is not within a worktree, it returns the path unchanged.\nfunc (w *Worktrees) DisplayPath(worktreePath string) string {\n\tfor _, pair := range w.WorktreePairs {\n\t\tfor _, wt := range []Worktree{pair.FromWorktree, pair.ToWorktree} {\n\t\t\t// Use boundary-aware check to avoid false matches (e.g., \"/tmp/work\" vs \"/tmp/work-other\")\n\t\t\tif worktreePath == wt.Path || strings.HasPrefix(worktreePath, wt.Path+string(os.PathSeparator)) {\n\t\t\t\t// Get the relative path within the worktree\n\t\t\t\trelPath, err := filepath.Rel(wt.Path, worktreePath)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn worktreePath\n\t\t\t\t}\n\n\t\t\t\t// Join with original working dir\n\t\t\t\treturn filepath.Join(w.OriginalWorkingDir, relPath)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn worktreePath\n}\n\n// Cleanup removes all created Git worktrees and their temporary directories.\nfunc (w *Worktrees) Cleanup(ctx context.Context, l log.Logger) error {\n\t// Get repo remote for telemetry\n\tvar repoRemote string\n\tif w.gitRunner != nil {\n\t\trepoRemote = w.gitRunner.GetRemoteURL(ctx)\n\t}\n\n\treturn filter.TraceGitWorktreesCleanup(ctx, len(w.WorktreePairs), repoRemote, func(ctx context.Context) error {\n\t\tseen := make(map[string]struct{})\n\n\t\tfor _, pair := range w.WorktreePairs {\n\t\t\tfor _, worktree := range []Worktree{pair.FromWorktree, pair.ToWorktree} {\n\t\t\t\tif _, ok := seen[worktree.Path]; ok {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tseen[worktree.Path] = struct{}{}\n\n\t\t\t\t// Skip removal if the worktree path doesn't exist (may have been cleaned up already)\n\t\t\t\tif _, err := os.Stat(worktree.Path); os.IsNotExist(err) {\n\t\t\t\t\tl.Debugf(\"Worktree path %s already removed, skipping cleanup\", worktree.Path)\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\terr := filter.TraceGitWorktreeRemove(ctx, worktree.Ref, worktree.Path, func(ctx context.Context) error {\n\t\t\t\t\treturn w.gitRunner.RemoveWorktree(ctx, worktree.Path)\n\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\t// If the error is due to the worktree not existing, log and continue\n\t\t\t\t\t// This can happen during parallel test execution or if cleanup runs twice\n\t\t\t\t\terrStr := err.Error()\n\t\t\t\t\tif strings.Contains(errStr, \"No such file or directory\") ||\n\t\t\t\t\t\tstrings.Contains(errStr, \"does not exist\") ||\n\t\t\t\t\t\tstrings.Contains(errStr, \"not a valid directory\") {\n\t\t\t\t\t\tl.Debugf(\"Worktree for reference %s already cleaned up: %v\", worktree.Ref, err)\n\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\treturn errors.Errorf(\n\t\t\t\t\t\t\"failed to remove Git worktree for reference %s (%s): %w\",\n\t\t\t\t\t\tworktree.Ref,\n\t\t\t\t\t\tworktree.Path,\n\t\t\t\t\t\terr,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n}\n\ntype StackDiff struct {\n\tAdded   []*component.Stack\n\tRemoved []*component.Stack\n\tChanged []StackDiffChangedPair\n}\n\ntype StackDiffChangedPair struct {\n\tFromStack *component.Stack\n\tToStack   *component.Stack\n}\n\n// Stacks returns a slice of stacks that can be found in the diffs found in worktrees.\n//\n// This can be useful, as stacks need to be discovered in worktrees, generated, then diffed on-disk\n// to find changed units.\n//\n// They are returned as added, removed, and changed stacks, respectively.\nfunc (w *Worktrees) Stacks() StackDiff {\n\tstackDiff := StackDiff{\n\t\tAdded:   []*component.Stack{},\n\t\tRemoved: []*component.Stack{},\n\t\tChanged: []StackDiffChangedPair{},\n\t}\n\n\tfor _, pair := range w.WorktreePairs {\n\t\tfromWorktree := pair.FromWorktree.Path\n\t\ttoWorktree := pair.ToWorktree.Path\n\n\t\tfor _, added := range pair.Diffs.Added {\n\t\t\tif filepath.Base(added) != config.DefaultStackFile {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdir := filepath.Dir(added)\n\n\t\t\tstackDiff.Added = append(\n\t\t\t\tstackDiff.Added,\n\t\t\t\tcomponent.NewStack(filepath.Join(toWorktree, dir)).WithDiscoveryContext(\n\t\t\t\t\t&component.DiscoveryContext{\n\t\t\t\t\t\tWorkingDir: toWorktree,\n\t\t\t\t\t\tRef:        pair.ToWorktree.Ref,\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\n\t\tfor _, removed := range pair.Diffs.Removed {\n\t\t\tif filepath.Base(removed) != config.DefaultStackFile {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdir := filepath.Dir(removed)\n\n\t\t\tstackDiff.Removed = append(\n\t\t\t\tstackDiff.Removed,\n\t\t\t\tcomponent.NewStack(filepath.Join(fromWorktree, dir)).WithDiscoveryContext(\n\t\t\t\t\t&component.DiscoveryContext{\n\t\t\t\t\t\tWorkingDir: fromWorktree,\n\t\t\t\t\t\tRef:        pair.FromWorktree.Ref,\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\n\t\tfor _, changed := range pair.Diffs.Changed {\n\t\t\tif filepath.Base(changed) != config.DefaultStackFile {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdir := filepath.Dir(changed)\n\n\t\t\tstackDiff.Changed = append(\n\t\t\t\tstackDiff.Changed,\n\t\t\t\tStackDiffChangedPair{\n\t\t\t\t\tFromStack: component.NewStack(filepath.Join(fromWorktree, dir)).WithDiscoveryContext(\n\t\t\t\t\t\t&component.DiscoveryContext{\n\t\t\t\t\t\t\tWorkingDir: fromWorktree,\n\t\t\t\t\t\t\tRef:        pair.FromWorktree.Ref,\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t\tToStack: component.NewStack(filepath.Join(toWorktree, dir)).WithDiscoveryContext(\n\t\t\t\t\t\t&component.DiscoveryContext{\n\t\t\t\t\t\t\tWorkingDir: toWorktree,\n\t\t\t\t\t\t\tRef:        pair.ToWorktree.Ref,\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\treturn stackDiff\n}\n\n// Expand expands a worktree pair with an associated Git expression into the equivalent to and from filter\n// expressions based on the provided diffs for the worktree pair.\nfunc (wp *WorktreePair) Expand() (filter.Filters, filter.Filters, error) {\n\tdiffs := wp.Diffs\n\n\ttoPath := wp.ToWorktree.Path\n\n\tfromExpressions := make(filter.Expressions, 0, len(diffs.Removed))\n\ttoExpressions := make(filter.Expressions, 0, len(diffs.Added)+len(diffs.Changed))\n\n\t// Build simple expressions that can be determined simply from the diffs.\n\tif err := expandDiffPaths(diffs.Removed, toPath, &fromExpressions, &toExpressions); err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tif err := expandDiffPaths(diffs.Added, toPath, &toExpressions, &toExpressions); err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tfor _, path := range diffs.Changed {\n\t\tdir := filepath.Dir(path)\n\n\t\tswitch filepath.Base(path) {\n\t\tcase config.DefaultTerragruntConfigPath:\n\t\t\texpr, err := filter.NewPathFilter(dir)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, errors.Errorf(\"failed to create path filter for %s: %w\", dir, err)\n\t\t\t}\n\n\t\t\ttoExpressions = append(toExpressions, expr)\n\t\tdefault:\n\t\t\t// Check to see if the changed file is in the same directory as a unit in the to worktree.\n\t\t\t// If so, we'll consider the unit modified.\n\t\t\tif _, err := os.Stat(filepath.Join(toPath, dir, config.DefaultTerragruntConfigPath)); err == nil {\n\t\t\t\texpr, err := filter.NewPathFilter(dir)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, nil, errors.Errorf(\"failed to create path filter for %s: %w\", dir, err)\n\t\t\t\t}\n\n\t\t\t\ttoExpressions = append(toExpressions, expr)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Otherwise, we'll consider it a file that could potentially be read by other units, and needs to be\n\t\t\t// tracked using a reading filter.\n\t\t\texpr, err := filter.NewAttributeExpression(filter.AttributeReading, path)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, errors.Errorf(\"failed to create reading filter for %s: %w\", path, err)\n\t\t\t}\n\n\t\t\ttoExpressions = append(toExpressions, expr)\n\t\t}\n\t}\n\n\tfromFilters := make(filter.Filters, 0, len(fromExpressions))\n\tfor _, expression := range fromExpressions {\n\t\tfromFilters = append(\n\t\t\tfromFilters,\n\t\t\tfilter.NewFilter(expression, expression.String()),\n\t\t)\n\t}\n\n\ttoFilters := make(filter.Filters, 0, len(toExpressions))\n\tfor _, expression := range toExpressions {\n\t\ttoFilters = append(\n\t\t\ttoFilters,\n\t\t\tfilter.NewFilter(expression, expression.String()),\n\t\t)\n\t}\n\n\treturn fromFilters, toFilters, nil\n}\n\n// NewWorktrees creates a new Worktrees for a given set of Git filters.\n//\n// Note that it is the responsibility of the caller to call Cleanup on the Worktrees object when it is no longer needed.\nfunc NewWorktrees(\n\tctx context.Context,\n\tl log.Logger,\n\tworkingDir string,\n\tgitExpressions filter.GitExpressions,\n) (*Worktrees, error) {\n\tif len(gitExpressions) == 0 {\n\t\treturn &Worktrees{\n\t\t\tWorktreePairs:      make(map[string]WorktreePair),\n\t\t\tOriginalWorkingDir: workingDir,\n\t\t}, nil\n\t}\n\n\tgitRefs := gitExpressions.UniqueGitRefs()\n\n\tvar (\n\t\tworktrees *Worktrees\n\t\touterErr  error\n\t)\n\n\tgitRunner, err := git.NewGitRunner()\n\tif err != nil {\n\t\treturn nil, errors.Errorf(\"failed to create Git runner for worktree creation: %w\", err)\n\t}\n\n\tgitRunner = gitRunner.WithWorkDir(workingDir)\n\n\t// Get repo info for telemetry\n\trepoRemote := gitRunner.GetRemoteURL(ctx)\n\trepoBranch := gitRunner.GetCurrentBranch(ctx)\n\trepoCommit := gitRunner.GetHeadCommit(ctx)\n\n\t// Wrap entire worktree creation process with telemetry\n\ttraceErr := filter.TraceGitWorktreesCreate(ctx, workingDir, len(gitRefs), repoRemote, repoBranch, repoCommit, func(ctx context.Context) error {\n\t\tvar (\n\t\t\terrs []error\n\t\t\tmu   sync.Mutex\n\t\t)\n\n\t\texpressionsToDiffs := make(map[*filter.GitExpression]*git.Diffs, len(gitExpressions))\n\n\t\tgitCmdGroup, gitCmdCtx := errgroup.WithContext(ctx)\n\t\tgitCmdGroup.SetLimit(min(runtime.NumCPU(), len(gitRefs)))\n\n\t\trefsToPaths := make(map[string]string, len(gitRefs))\n\n\t\tif len(gitRefs) > 0 {\n\t\t\tgitCmdGroup.Go(func() error {\n\t\t\t\tpaths, err := createGitWorktrees(gitCmdCtx, l, gitRunner, gitRefs, repoRemote, repoBranch, repoCommit)\n\t\t\t\tif err != nil {\n\t\t\t\t\tmu.Lock()\n\n\t\t\t\t\terrs = append(errs, err)\n\n\t\t\t\t\tmu.Unlock()\n\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tmu.Lock()\n\n\t\t\t\tmaps.Copy(refsToPaths, paths)\n\n\t\t\t\tmu.Unlock()\n\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}\n\n\t\tfor _, gitExpression := range gitExpressions {\n\t\t\tgitCmdGroup.Go(func() error {\n\t\t\t\t// Wrap git diff with telemetry\n\t\t\t\tvar diffs *git.Diffs\n\n\t\t\t\tdiffErr := filter.TraceGitDiff(gitCmdCtx, gitExpression.FromRef, gitExpression.ToRef, repoRemote, func(ctx context.Context) error {\n\t\t\t\t\tvar err error\n\n\t\t\t\t\tdiffs, err = gitRunner.Diff(ctx, gitExpression.FromRef, gitExpression.ToRef)\n\n\t\t\t\t\treturn err\n\t\t\t\t})\n\t\t\t\tif diffErr != nil {\n\t\t\t\t\tmu.Lock()\n\n\t\t\t\t\terrs = append(errs, diffErr)\n\n\t\t\t\t\tmu.Unlock()\n\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tmu.Lock()\n\n\t\t\t\texpressionsToDiffs[gitExpression] = diffs\n\n\t\t\t\tmu.Unlock()\n\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}\n\n\t\tif err := gitCmdGroup.Wait(); err != nil {\n\t\t\tworktrees = &Worktrees{\n\t\t\t\tWorktreePairs:      make(map[string]WorktreePair),\n\t\t\t\tOriginalWorkingDir: workingDir,\n\t\t\t\tgitRunner:          gitRunner,\n\t\t\t}\n\t\t\touterErr = err\n\n\t\t\treturn err\n\t\t}\n\n\t\tworktreePairs := make(map[string]WorktreePair, len(gitExpressions))\n\t\tfor _, gitExpression := range gitExpressions {\n\t\t\tworktreePairs[gitExpression.String()] = WorktreePair{\n\t\t\t\tGitExpression: gitExpression,\n\t\t\t\tDiffs:         expressionsToDiffs[gitExpression],\n\t\t\t\tFromWorktree:  Worktree{Ref: gitExpression.FromRef, Path: refsToPaths[gitExpression.FromRef]},\n\t\t\t\tToWorktree:    Worktree{Ref: gitExpression.ToRef, Path: refsToPaths[gitExpression.ToRef]},\n\t\t\t}\n\n\t\t\t// Record telemetry for diff results\n\t\t\tif diffs := expressionsToDiffs[gitExpression]; diffs != nil {\n\t\t\t\trecordDiffTelemetry(ctx, diffs)\n\t\t\t}\n\t\t}\n\n\t\tworktrees = &Worktrees{\n\t\t\tWorktreePairs:      worktreePairs,\n\t\t\tOriginalWorkingDir: workingDir,\n\t\t\tgitRunner:          gitRunner,\n\t\t}\n\n\t\tif len(errs) > 0 {\n\t\t\touterErr = errors.Join(errs...)\n\t\t\treturn outerErr\n\t\t}\n\n\t\treturn nil\n\t})\n\n\tif traceErr != nil && outerErr == nil {\n\t\tl.Warnf(\"telemetry trace error during worktree creation: %v\", traceErr)\n\t}\n\n\t// cleanup worktrees\n\tif outerErr != nil && worktrees != nil {\n\t\tif cleanupErr := worktrees.Cleanup(ctx, l); cleanupErr != nil {\n\t\t\tl.Warnf(\"failed to cleanup worktrees: %v\", cleanupErr)\n\t\t}\n\t}\n\n\treturn worktrees, outerErr\n}\n\n// expandDiffPaths processes a list of changed paths from a worktree diff, creating path filter expressions\n// for discovered units and stacks. primaryExprs receives filters for config files (units/stacks),\n// while fallbackExprs receives filters for non-config files adjacent to units in the \"to\" worktree.\nfunc expandDiffPaths(paths []string, toPath string, primaryExprs, fallbackExprs *filter.Expressions) error {\n\tfor _, path := range paths {\n\t\tdir := filepath.Dir(path)\n\n\t\tswitch filepath.Base(path) {\n\t\tcase config.DefaultTerragruntConfigPath:\n\t\t\texpr, err := filter.NewPathFilter(dir)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Errorf(\"failed to create path filter for %s: %w\", dir, err)\n\t\t\t}\n\n\t\t\t*primaryExprs = append(*primaryExprs, expr)\n\t\tcase config.DefaultStackFile:\n\t\t\tdirExpr, err := filter.NewPathFilter(dir)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Errorf(\"failed to create path filter for %s: %w\", dir, err)\n\t\t\t}\n\n\t\t\tglobExpr, err := filter.NewPathFilter(filepath.Join(dir, \"**\"))\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Errorf(\"failed to create path filter for %s/**: %w\", dir, err)\n\t\t\t}\n\n\t\t\t*primaryExprs = append(*primaryExprs, dirExpr, globExpr)\n\t\tdefault:\n\t\t\tif _, err := os.Stat(filepath.Join(toPath, dir, config.DefaultTerragruntConfigPath)); err == nil {\n\t\t\t\texpr, err := filter.NewPathFilter(dir)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn errors.Errorf(\"failed to create path filter for %s: %w\", dir, err)\n\t\t\t\t}\n\n\t\t\t\t*fallbackExprs = append(*fallbackExprs, expr)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// recordDiffTelemetry records telemetry metrics for git diff results.\nfunc recordDiffTelemetry(ctx context.Context, diffs *git.Diffs) {\n\ttelemeter := telemetry.TelemeterFromContext(ctx)\n\tif telemeter == nil || telemeter.Meter == nil {\n\t\treturn\n\t}\n\n\ttelemeter.Count(ctx, \"git_diff_files_added\", int64(len(diffs.Added)))\n\ttelemeter.Count(ctx, \"git_diff_files_removed\", int64(len(diffs.Removed)))\n\ttelemeter.Count(ctx, \"git_diff_files_changed\", int64(len(diffs.Changed)))\n}\n\n// createGitWorktrees creates detached worktrees for each unique Git reference needed by filters.\n// The worktrees are created in temporary directories and tracked in refsToPaths.\n// Worktrees are created sequentially because git worktree operations on the same repository\n// are not thread-safe - concurrent calls to `git worktree add` can cause race conditions\n// accessing the `.git/worktrees/` directory.\nfunc createGitWorktrees(\n\tctx context.Context,\n\tl log.Logger,\n\tgitRunner *git.GitRunner,\n\tgitRefs []string,\n\trepoRemote, repoBranch, repoCommit string,\n) (map[string]string, error) {\n\tvar errs []error\n\n\trefsToPaths := make(map[string]string, len(gitRefs))\n\n\tfor _, ref := range gitRefs {\n\t\ttmpDir, err := os.MkdirTemp(\"\", \"terragrunt-worktree-\"+sanitizeRef(ref)+\"-*\")\n\t\tif err != nil {\n\t\t\terrs = append(errs, errors.Errorf(\"failed to create temporary directory for worktree: %w\", err))\n\n\t\t\tcontinue\n\t\t}\n\n\t\t// macOS will create the temporary directory with symlinks, so we need to evaluate them.\n\t\torigTmpDir := tmpDir\n\n\t\ttmpDir, err = filepath.EvalSymlinks(tmpDir)\n\t\tif err != nil {\n\t\t\tif cleanErr := os.RemoveAll(origTmpDir); cleanErr != nil {\n\t\t\t\tl.Warnf(\"failed to clean worktree directory %s: %v\", origTmpDir, cleanErr)\n\t\t\t}\n\n\t\t\terrs = append(errs, errors.Errorf(\"failed to evaluate symlinks for temporary directory: %w\", err))\n\n\t\t\tcontinue\n\t\t}\n\n\t\t// Wrap individual worktree creation with telemetry including repo info\n\t\terr = filter.TraceGitWorktreeCreate(ctx, ref, tmpDir, repoRemote, repoBranch, repoCommit, func(ctx context.Context) error {\n\t\t\treturn gitRunner.CreateDetachedWorktree(ctx, tmpDir, ref)\n\t\t})\n\t\tif err != nil {\n\t\t\tif cleanErr := os.RemoveAll(tmpDir); cleanErr != nil {\n\t\t\t\tl.Warnf(\"failed to clean worktree directory %s: %v\", tmpDir, cleanErr)\n\t\t\t}\n\n\t\t\terrs = append(errs, errors.Errorf(\"failed to create Git worktree for reference %s: %w\", ref, err))\n\n\t\t\tcontinue\n\t\t}\n\n\t\trefsToPaths[ref] = tmpDir\n\n\t\tl.Debugf(\"Created Git worktree for reference %s at %s\", ref, tmpDir)\n\t}\n\n\tif len(errs) > 0 {\n\t\treturn refsToPaths, errors.Join(errs...)\n\t}\n\n\treturn refsToPaths, nil\n}\n\n// sanitizeRef sanitizes a Git reference string for use in file paths.\n// It replaces invalid characters with underscores.\nfunc sanitizeRef(ref string) string {\n\tresult := strings.Builder{}\n\n\tfor _, r := range ref {\n\t\tif (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_' {\n\t\t\tresult.WriteRune(r)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tresult.WriteRune('_')\n\t}\n\n\treturn result.String()\n}\n"
  },
  {
    "path": "internal/worktrees/worktrees_test.go",
    "content": "package worktrees_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/internal/worktrees\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\tgogit \"github.com/go-git/go-git/v6\"\n\t\"github.com/go-git/go-git/v6/plumbing/object\"\n)\n\nfunc TestNewWorktrees(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpDir)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\terr = runner.GoOpenRepo()\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\terr = runner.GoCloseStorage()\n\t\tif err != nil {\n\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t}\n\t})\n\n\terr = runner.GoCommit(\"Initial commit\", &gogit.CommitOptions{\n\t\tAllowEmptyCommits: true,\n\t\tAuthor: &object.Signature{\n\t\t\tName:  \"Test User\",\n\t\t\tEmail: \"test@example.com\",\n\t\t\tWhen:  time.Now(),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\terr = runner.GoCommit(\"Second commit\", &gogit.CommitOptions{\n\t\tAllowEmptyCommits: true,\n\t\tAuthor: &object.Signature{\n\t\t\tName:  \"Test User\",\n\t\t\tEmail: \"test@example.com\",\n\t\t\tWhen:  time.Now(),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tfilters, err := filter.ParseFilterQueries(logger.CreateLogger(), []string{\"[HEAD~1...HEAD]\"})\n\trequire.NoError(t, err)\n\n\tw, err := worktrees.NewWorktrees(\n\t\tt.Context(),\n\t\tlogger.CreateLogger(),\n\t\ttmpDir,\n\t\tfilters.UniqueGitFilters(),\n\t)\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\tcleanupErr := w.Cleanup(context.Background(), logger.CreateLogger())\n\t\trequire.NoError(t, cleanupErr)\n\t})\n\n\trequire.NotEmpty(t, w.WorktreePairs)\n}\n\nfunc TestNewWorktreesWithInvalidReference(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Initialize Git repository\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpDir)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\terr = runner.GoOpenRepo()\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\terr = runner.GoCloseStorage()\n\t\tif err != nil {\n\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t}\n\t})\n\n\terr = runner.GoCommit(\"Initial commit\", &gogit.CommitOptions{\n\t\tAllowEmptyCommits: true,\n\t\tAuthor: &object.Signature{\n\t\t\tName:  \"Test User\",\n\t\t\tEmail: \"test@example.com\",\n\t\t\tWhen:  time.Now(),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\topts := options.NewTerragruntOptions()\n\topts.WorkingDir = tmpDir\n\topts.RootWorkingDir = tmpDir\n\n\t// Parse filter with invalid Git reference\n\tfilters, err := filter.ParseFilterQueries(logger.CreateLogger(), []string{\"[nonexistent-branch]\"})\n\trequire.NoError(t, err) // Parsing should succeed\n\n\t_, err = worktrees.NewWorktrees(\n\t\tt.Context(),\n\t\tlogger.CreateLogger(),\n\t\ttmpDir,\n\t\tfilters.UniqueGitFilters(),\n\t)\n\trequire.Error(t, err)\n}\n\nfunc TestExpressionExpansion(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tdiffs              *git.Diffs\n\t\tname               string\n\t\texpectedToPaths    []string\n\t\texpectedToReadings []string\n\t\texpectedFrom       int\n\t\texpectedTo         int\n\t}{\n\t\t{\n\t\t\tname: \"removed terragrunt.hcl files create from filters\",\n\t\t\tdiffs: &git.Diffs{\n\t\t\t\tRemoved: []string{\n\t\t\t\t\t\"app1/terragrunt.hcl\",\n\t\t\t\t\t\"app2/terragrunt.hcl\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFrom:       2,\n\t\t\texpectedTo:         0,\n\t\t\texpectedToPaths:    []string{},\n\t\t\texpectedToReadings: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"added terragrunt.hcl files create to filters\",\n\t\t\tdiffs: &git.Diffs{\n\t\t\t\tAdded: []string{\n\t\t\t\t\t\"app1/terragrunt.hcl\",\n\t\t\t\t\t\"app2/terragrunt.hcl\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFrom:       0,\n\t\t\texpectedTo:         2,\n\t\t\texpectedToPaths:    []string{\"app1\", \"app2\"},\n\t\t\texpectedToReadings: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"changed terragrunt.hcl files create to filters\",\n\t\t\tdiffs: &git.Diffs{\n\t\t\t\tChanged: []string{\n\t\t\t\t\t\"app1/terragrunt.hcl\",\n\t\t\t\t\t\"app2/terragrunt.hcl\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFrom:       0,\n\t\t\texpectedTo:         2,\n\t\t\texpectedToPaths:    []string{\"app1\", \"app2\"},\n\t\t\texpectedToReadings: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"changed non-terragrunt.hcl files create reading filters\",\n\t\t\tdiffs: &git.Diffs{\n\t\t\t\tChanged: []string{\n\t\t\t\t\t\"app1/main.tf\",\n\t\t\t\t\t\"app1/variables.tf\",\n\t\t\t\t\t\"app2/data.tf\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFrom:       0,\n\t\t\texpectedTo:         3,\n\t\t\texpectedToPaths:    []string{},\n\t\t\texpectedToReadings: []string{\"app1/main.tf\", \"app1/variables.tf\", \"app2/data.tf\"},\n\t\t},\n\t\t{\n\t\t\tname: \"changed stack files create reading filters\",\n\t\t\tdiffs: &git.Diffs{\n\t\t\t\tChanged: []string{\n\t\t\t\t\t\"stack/terragrunt.stack.hcl\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFrom:       0,\n\t\t\texpectedTo:         1,\n\t\t\texpectedToPaths:    []string{},\n\t\t\texpectedToReadings: []string{\"stack/terragrunt.stack.hcl\"},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed file types create appropriate filters\",\n\t\t\tdiffs: &git.Diffs{\n\t\t\t\tRemoved: []string{\n\t\t\t\t\t\"app-removed/terragrunt.hcl\",\n\t\t\t\t},\n\t\t\t\tAdded: []string{\n\t\t\t\t\t\"app-added/terragrunt.hcl\",\n\t\t\t\t},\n\t\t\t\tChanged: []string{\n\t\t\t\t\t\"app-modified/terragrunt.hcl\",\n\t\t\t\t\t\"app-modified/main.tf\",\n\t\t\t\t\t\"stack/terragrunt.stack.hcl\",\n\t\t\t\t\t\"other/file.hcl\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedFrom:       1,\n\t\t\texpectedTo:         5,\n\t\t\texpectedToPaths:    []string{\"app-added\", \"app-modified\"},\n\t\t\texpectedToReadings: []string{\"app-modified/main.tf\", \"stack/terragrunt.stack.hcl\", \"other/file.hcl\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\trunner, err := git.NewGitRunner()\n\t\t\trequire.NoError(t, err)\n\n\t\t\trunner = runner.WithWorkDir(tmpDir)\n\n\t\t\terr = runner.Init(t.Context())\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = runner.GoOpenRepo()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tt.Cleanup(func() {\n\t\t\t\terr = runner.GoCloseStorage()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\terr = runner.GoCommit(\"Initial commit\", &gogit.CommitOptions{\n\t\t\t\tAllowEmptyCommits: true,\n\t\t\t\tAuthor: &object.Signature{\n\t\t\t\t\tName:  \"Test User\",\n\t\t\t\t\tEmail: \"test@example.com\",\n\t\t\t\t\tWhen:  time.Now(),\n\t\t\t\t},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\twp := &worktrees.WorktreePair{\n\t\t\t\tDiffs: tt.diffs,\n\t\t\t}\n\n\t\t\tfromFilters, toFilters, err := wp.Expand()\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Verify from filters count\n\t\t\tassert.Len(t, fromFilters, tt.expectedFrom, \"From filters count should match\")\n\n\t\t\t// Verify to filters count\n\t\t\tassert.Len(t, toFilters, tt.expectedTo, \"To filters count should match\")\n\n\t\t\t// Verify from filters are path filters with correct paths\n\t\t\tfor i, f := range fromFilters {\n\t\t\t\tpathExpr, ok := f.Expression().(*filter.PathExpression)\n\t\t\t\trequire.True(t, ok, \"From filter %d should be a PathExpression\", i)\n\t\t\t\texpectedPath := filepath.Dir(tt.diffs.Removed[i])\n\t\t\t\tassert.Equal(t, expectedPath, pathExpr.Value, \"From filter %d should have correct path\", i)\n\t\t\t}\n\n\t\t\t// Verify to filters\n\t\t\ttoPaths := []string{}\n\t\t\ttoReadings := []string{}\n\n\t\t\tfor _, f := range toFilters {\n\t\t\t\tswitch expr := f.Expression().(type) {\n\t\t\t\tcase *filter.PathExpression:\n\t\t\t\t\ttoPaths = append(toPaths, expr.Value)\n\t\t\t\tcase *filter.AttributeExpression:\n\t\t\t\t\tif expr.Key == \"reading\" {\n\t\t\t\t\t\ttoReadings = append(toReadings, expr.Value)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Verify path filters\n\t\t\tassert.ElementsMatch(t, tt.expectedToPaths, toPaths, \"To path filters should match\")\n\n\t\t\t// Verify reading filters\n\t\t\tassert.ElementsMatch(t, tt.expectedToReadings, toReadings, \"To reading filters should match\")\n\t\t})\n\t}\n}\n\nfunc TestExpansionAttributeReadingFilters(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname             string\n\t\tdiffs            *git.Diffs\n\t\texpectedReadings []string\n\t}{\n\t\t{\n\t\t\tname: \"changed .tf file creates reading filter\",\n\t\t\tdiffs: &git.Diffs{\n\t\t\t\tChanged: []string{\n\t\t\t\t\t\"app/main.tf\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedReadings: []string{\"app/main.tf\"},\n\t\t},\n\t\t{\n\t\t\tname: \"changed .hcl file (not terragrunt.hcl) creates reading filter\",\n\t\t\tdiffs: &git.Diffs{\n\t\t\t\tChanged: []string{\n\t\t\t\t\t\"app/config.hcl\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedReadings: []string{\"app/config.hcl\"},\n\t\t},\n\t\t{\n\t\t\tname: \"changed file in subdirectory creates reading filter with correct path\",\n\t\t\tdiffs: &git.Diffs{\n\t\t\t\tChanged: []string{\n\t\t\t\t\t\"app/modules/database/main.tf\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedReadings: []string{\"app/modules/database/main.tf\"},\n\t\t},\n\t\t{\n\t\t\tname: \"multiple changed files create multiple reading filters\",\n\t\t\tdiffs: &git.Diffs{\n\t\t\t\tChanged: []string{\n\t\t\t\t\t\"app1/main.tf\",\n\t\t\t\t\t\"app1/variables.tf\",\n\t\t\t\t\t\"app2/data.tf\",\n\t\t\t\t\t\"app2/outputs.tf\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedReadings: []string{\n\t\t\t\t\"app1/main.tf\",\n\t\t\t\t\"app1/variables.tf\",\n\t\t\t\t\"app2/data.tf\",\n\t\t\t\t\"app2/outputs.tf\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"mixed terragrunt.hcl and other files\",\n\t\t\tdiffs: &git.Diffs{\n\t\t\t\tChanged: []string{\n\t\t\t\t\t\"app/terragrunt.hcl\",\n\t\t\t\t\t\"app/main.tf\",\n\t\t\t\t\t\"app/variables.tf\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedReadings: []string{\n\t\t\t\t\"app/main.tf\",\n\t\t\t\t\"app/variables.tf\",\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\tt.Parallel()\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\trunner, err := git.NewGitRunner()\n\t\t\trequire.NoError(t, err)\n\n\t\t\trunner = runner.WithWorkDir(tmpDir)\n\n\t\t\terr = runner.Init(t.Context())\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = runner.GoOpenRepo()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tt.Cleanup(func() {\n\t\t\t\terr = runner.GoCloseStorage()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\terr = runner.GoCommit(\"Initial commit\", &gogit.CommitOptions{\n\t\t\t\tAllowEmptyCommits: true,\n\t\t\t\tAuthor: &object.Signature{\n\t\t\t\t\tName:  \"Test User\",\n\t\t\t\t\tEmail: \"test@example.com\",\n\t\t\t\t\tWhen:  time.Now(),\n\t\t\t\t},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\twp := &worktrees.WorktreePair{\n\t\t\t\tDiffs: tt.diffs,\n\t\t\t}\n\n\t\t\t_, toFilters, err := wp.Expand()\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Extract reading filters\n\t\t\treadings := []string{}\n\n\t\t\tfor _, f := range toFilters {\n\t\t\t\tif attrExpr, ok := f.Expression().(*filter.AttributeExpression); ok {\n\t\t\t\t\tif attrExpr.Key == \"reading\" {\n\t\t\t\t\t\treadings = append(readings, attrExpr.Value)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Verify reading filters match expected\n\t\t\tassert.ElementsMatch(t, tt.expectedReadings, readings, \"Reading filters should match expected paths\")\n\n\t\t\t// Verify each reading filter is properly constructed\n\t\t\tfor _, expectedReading := range tt.expectedReadings {\n\t\t\t\tfound := false\n\n\t\t\t\tfor _, f := range toFilters {\n\t\t\t\t\tif attrExpr, ok := f.Expression().(*filter.AttributeExpression); ok {\n\t\t\t\t\t\tif attrExpr.Key == \"reading\" && attrExpr.Value == expectedReading {\n\t\t\t\t\t\t\tfound = true\n\n\t\t\t\t\t\t\tassert.Equal(t, \"reading\", attrExpr.Key, \"Filter should have reading key\")\n\t\t\t\t\t\t\tassert.Equal(t, expectedReading, attrExpr.Value, \"Filter should have correct file path\")\n\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tassert.True(t, found, \"Expected reading filter for %s should be present\", expectedReading)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestExpandWithUnitDirectoryDetection(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname               string\n\t\tsetupFilesystem    func(tmpDir string) error\n\t\tdiffs              *git.Diffs\n\t\texpectedToPaths    []string\n\t\texpectedToReadings []string\n\t\texpectedFrom       int\n\t}{\n\t\t{\n\t\t\tname: \"removed file in unit directory creates path filter\",\n\t\t\tsetupFilesystem: func(tmpDir string) error {\n\t\t\t\t// Create unit directory with terragrunt.hcl\n\t\t\t\tunitDir := filepath.Join(tmpDir, \"unit1\")\n\t\t\t\tif err := os.MkdirAll(unitDir, 0755); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tterragruntFile := filepath.Join(unitDir, \"terragrunt.hcl\")\n\n\t\t\t\treturn os.WriteFile(terragruntFile, []byte(\"# terragrunt config\"), 0644)\n\t\t\t},\n\t\t\tdiffs: &git.Diffs{\n\t\t\t\tRemoved: []string{\n\t\t\t\t\t\"unit1/main.tf\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedToPaths:    []string{\"unit1\"},\n\t\t\texpectedToReadings: []string{},\n\t\t\texpectedFrom:       0,\n\t\t},\n\t\t{\n\t\t\tname: \"removed file in non-unit directory creates no filter\",\n\t\t\tsetupFilesystem: func(tmpDir string) error {\n\t\t\t\t// Create non-unit directory (no terragrunt.hcl)\n\t\t\t\tnonUnitDir := filepath.Join(tmpDir, \"non-unit\")\n\t\t\t\treturn os.MkdirAll(nonUnitDir, 0755)\n\t\t\t},\n\t\t\tdiffs: &git.Diffs{\n\t\t\t\tRemoved: []string{\n\t\t\t\t\t\"non-unit/some-file.tf\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedToPaths:    []string{},\n\t\t\texpectedToReadings: []string{},\n\t\t\texpectedFrom:       0,\n\t\t},\n\t\t{\n\t\t\tname: \"added file in unit directory creates path filter\",\n\t\t\tsetupFilesystem: func(tmpDir string) error {\n\t\t\t\t// Create unit directory with terragrunt.hcl\n\t\t\t\tunitDir := filepath.Join(tmpDir, \"unit1\")\n\t\t\t\tif err := os.MkdirAll(unitDir, 0755); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tterragruntFile := filepath.Join(unitDir, \"terragrunt.hcl\")\n\n\t\t\t\treturn os.WriteFile(terragruntFile, []byte(\"# terragrunt config\"), 0644)\n\t\t\t},\n\t\t\tdiffs: &git.Diffs{\n\t\t\t\tAdded: []string{\n\t\t\t\t\t\"unit1/variables.tf\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedToPaths:    []string{\"unit1\"},\n\t\t\texpectedToReadings: []string{},\n\t\t\texpectedFrom:       0,\n\t\t},\n\t\t{\n\t\t\tname: \"added file in non-unit directory creates no filter\",\n\t\t\tsetupFilesystem: func(tmpDir string) error {\n\t\t\t\t// Create non-unit directory (no terragrunt.hcl)\n\t\t\t\tnonUnitDir := filepath.Join(tmpDir, \"non-unit\")\n\t\t\t\treturn os.MkdirAll(nonUnitDir, 0755)\n\t\t\t},\n\t\t\tdiffs: &git.Diffs{\n\t\t\t\tAdded: []string{\n\t\t\t\t\t\"non-unit/new-file.tf\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedToPaths:    []string{},\n\t\t\texpectedToReadings: []string{},\n\t\t\texpectedFrom:       0,\n\t\t},\n\t\t{\n\t\t\tname: \"changed file in unit directory creates path filter\",\n\t\t\tsetupFilesystem: func(tmpDir string) error {\n\t\t\t\t// Create unit directory with terragrunt.hcl\n\t\t\t\tunitDir := filepath.Join(tmpDir, \"unit1\")\n\t\t\t\tif err := os.MkdirAll(unitDir, 0755); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tterragruntFile := filepath.Join(unitDir, \"terragrunt.hcl\")\n\n\t\t\t\treturn os.WriteFile(terragruntFile, []byte(\"# terragrunt config\"), 0644)\n\t\t\t},\n\t\t\tdiffs: &git.Diffs{\n\t\t\t\tChanged: []string{\n\t\t\t\t\t\"unit1/main.tf\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedToPaths:    []string{\"unit1\"},\n\t\t\texpectedToReadings: []string{},\n\t\t\texpectedFrom:       0,\n\t\t},\n\t\t{\n\t\t\tname: \"changed file in non-unit directory creates reading filter\",\n\t\t\tsetupFilesystem: func(tmpDir string) error {\n\t\t\t\t// Create non-unit directory (no terragrunt.hcl)\n\t\t\t\tnonUnitDir := filepath.Join(tmpDir, \"non-unit\")\n\t\t\t\treturn os.MkdirAll(nonUnitDir, 0755)\n\t\t\t},\n\t\t\tdiffs: &git.Diffs{\n\t\t\t\tChanged: []string{\n\t\t\t\t\t\"non-unit/some-file.tf\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedToPaths:    []string{},\n\t\t\texpectedToReadings: []string{\"non-unit/some-file.tf\"},\n\t\t\texpectedFrom:       0,\n\t\t},\n\t\t{\n\t\t\tname: \"mixed scenarios with multiple units and non-units\",\n\t\t\tsetupFilesystem: func(tmpDir string) error {\n\t\t\t\t// Create unit1 directory\n\t\t\t\tunit1Dir := filepath.Join(tmpDir, \"unit1\")\n\t\t\t\tif err := os.MkdirAll(unit1Dir, 0755); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tterragruntFile1 := filepath.Join(unit1Dir, \"terragrunt.hcl\")\n\t\t\t\tif err := os.WriteFile(terragruntFile1, []byte(\"# terragrunt config\"), 0644); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\t// Create unit2 directory\n\t\t\t\tunit2Dir := filepath.Join(tmpDir, \"unit2\")\n\t\t\t\tif err := os.MkdirAll(unit2Dir, 0755); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tterragruntFile2 := filepath.Join(unit2Dir, \"terragrunt.hcl\")\n\t\t\t\tif err := os.WriteFile(terragruntFile2, []byte(\"# terragrunt config\"), 0644); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\t// Create non-unit directory\n\t\t\t\tnonUnitDir := filepath.Join(tmpDir, \"non-unit\")\n\n\t\t\t\treturn os.MkdirAll(nonUnitDir, 0755)\n\t\t\t},\n\t\t\tdiffs: &git.Diffs{\n\t\t\t\tRemoved: []string{\n\t\t\t\t\t\"unit1/old-file.tf\",\n\t\t\t\t},\n\t\t\t\tAdded: []string{\n\t\t\t\t\t\"unit2/new-file.tf\",\n\t\t\t\t},\n\t\t\t\tChanged: []string{\n\t\t\t\t\t\"unit1/modified.tf\",\n\t\t\t\t\t\"non-unit/shared.tf\",\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedToPaths:    []string{\"unit1\", \"unit2\"},\n\t\t\texpectedToReadings: []string{\"non-unit/shared.tf\"},\n\t\t\texpectedFrom:       0,\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\t// Setup filesystem structure\n\t\t\terr := tt.setupFilesystem(tmpDir)\n\t\t\trequire.NoError(t, err)\n\n\t\t\twp := &worktrees.WorktreePair{\n\t\t\t\tDiffs: tt.diffs,\n\t\t\t\tToWorktree: worktrees.Worktree{\n\t\t\t\t\tPath: tmpDir,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tfromFilters, toFilters, err := wp.Expand()\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Verify from filters count\n\t\t\tassert.Len(t, fromFilters, tt.expectedFrom, \"From filters count should match\")\n\n\t\t\t// Extract path and reading filters from toFilters\n\t\t\ttoPathsMap := make(map[string]bool)\n\t\t\ttoReadings := []string{}\n\n\t\t\tfor _, f := range toFilters {\n\t\t\t\tswitch expr := f.Expression().(type) {\n\t\t\t\tcase *filter.PathExpression:\n\t\t\t\t\ttoPathsMap[expr.Value] = true\n\t\t\t\tcase *filter.AttributeExpression:\n\t\t\t\t\tif expr.Key == \"reading\" {\n\t\t\t\t\t\ttoReadings = append(toReadings, expr.Value)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Convert map to slice for comparison (deduplicates)\n\t\t\ttoPaths := make([]string, 0, len(toPathsMap))\n\t\t\tfor path := range toPathsMap {\n\t\t\t\ttoPaths = append(toPaths, path)\n\t\t\t}\n\n\t\t\t// Verify path filters\n\t\t\tassert.ElementsMatch(t, tt.expectedToPaths, toPaths, \"To path filters should match\")\n\n\t\t\t// Verify reading filters\n\t\t\tassert.ElementsMatch(t, tt.expectedToReadings, toReadings, \"To reading filters should match\")\n\t\t})\n\t}\n}\n\n// TestWorktreeCleanup test worktree cleanup\nfunc TestWorktreeCleanup(t *testing.T) {\n\tt.Parallel()\n\ttmpDir := t.TempDir()\n\ttmpDir, err := filepath.EvalSymlinks(tmpDir)\n\trequire.NoError(t, err)\n\n\t// Initialize Git repository\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpDir)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\terr = runner.GoOpenRepo()\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\terr = runner.GoCloseStorage()\n\t\tif err != nil {\n\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t}\n\t})\n\n\tfor i := range 3 {\n\t\terr = runner.GoCommit(fmt.Sprintf(\"Commit %d\", i), &gogit.CommitOptions{\n\t\t\tAllowEmptyCommits: true,\n\t\t\tAuthor: &object.Signature{\n\t\t\t\tName:  \"Test User\",\n\t\t\t\tEmail: \"test@example.com\",\n\t\t\t\tWhen:  time.Now(),\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n\n\topts := options.NewTerragruntOptions()\n\topts.WorkingDir = tmpDir\n\topts.RootWorkingDir = tmpDir\n\n\tfilters, err := filter.ParseFilterQueries(logger.CreateLogger(), []string{\"[test-worktree-cleanup]\"})\n\trequire.NoError(t, err)\n\n\t_, err = worktrees.NewWorktrees(\n\t\tt.Context(),\n\t\tlogger.CreateLogger(),\n\t\ttmpDir,\n\t\tfilters.UniqueGitFilters(),\n\t)\n\trequire.Error(t, err)\n\n\ttempDir := os.TempDir()\n\n\tworktreeDirs, err := filepath.Glob(filepath.Join(tempDir, \"terragrunt-worktree-*\"))\n\trequire.NoError(t, err)\n\t// validate that test-worktree-cleanup worktree was deleted\n\tworktreeExists := false\n\n\tfor _, dir := range worktreeDirs {\n\t\tif strings.Contains(filepath.Base(dir), \"test-worktree-cleanup\") {\n\t\t\tworktreeExists = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tassert.False(t, worktreeExists, \"Worktree test-worktree-cleanup should be deleted\")\n}\n"
  },
  {
    "path": "internal/writer/writer.go",
    "content": "// Package writer provides writer types for Terragrunt I/O.\npackage writer\n\nimport \"io\"\n\n// Writers groups the writer-related fields that travel together across\n// TerragruntOptions, ParsingContext, shell.RunOptions and engine.ExecutionOptions.\ntype Writers struct {\n\t// Writer is the primary output writer (defaults to os.Stdout).\n\tWriter io.Writer\n\t// ErrWriter is the error output writer (defaults to os.Stderr).\n\tErrWriter io.Writer\n\t// LogShowAbsPaths disables replacing full paths in logs with short relative paths.\n\tLogShowAbsPaths bool\n\t// LogDisableErrorSummary is a flag to skip the error summary.\n\tLogDisableErrorSummary bool\n}\n\n// writerUnwrapper is any writer that can provide its underlying parent writer.\n// This interface allows extracting the original writer from wrapped writers.\ntype writerUnwrapper interface {\n\tUnwrap() io.Writer\n}\n\n// OriginalWriter wraps an io.Writer and implements writerUnwrapper to preserve\n// access to the original writer even after it's been wrapped by other writers.\n// This is used to maintain access to the original stdout/stderr writers after they\n// are wrapped by log writers in logTFOutput.\ntype OriginalWriter struct {\n\tw io.Writer\n}\n\n// NewOriginalWriter creates a new OriginalWriter that wraps the given writer.\nfunc NewOriginalWriter(w io.Writer) *OriginalWriter {\n\treturn &OriginalWriter{w: w}\n}\n\n// Write implements io.Writer by delegating to the wrapped writer.\nfunc (ow *OriginalWriter) Write(p []byte) (int, error) {\n\treturn ow.w.Write(p)\n}\n\n// Unwrap implements writerUnwrapper by returning the wrapped writer.\nfunc (ow *OriginalWriter) Unwrap() io.Writer {\n\treturn ow.w\n}\n\n// WrappedWriter wraps an io.Writer and implements writerUnwrapper to preserve\n// access to an underlying original writer. This is used to wrap the result of\n// buildOutWriter/buildErrWriter so the original writer can still be extracted.\ntype WrappedWriter struct {\n\twrapped  io.Writer\n\toriginal io.Writer\n}\n\n// NewWrappedWriter creates a new WrappedWriter that wraps the given writer\n// and preserves access to the original writer.\nfunc NewWrappedWriter(wrapped, original io.Writer) *WrappedWriter {\n\treturn &WrappedWriter{\n\t\twrapped:  wrapped,\n\t\toriginal: original,\n\t}\n}\n\n// Write implements io.Writer by delegating to the wrapped writer.\nfunc (ww *WrappedWriter) Write(p []byte) (int, error) {\n\treturn ww.wrapped.Write(p)\n}\n\n// Unwrap implements writerUnwrapper by returning the original writer.\nfunc (ww *WrappedWriter) Unwrap() io.Writer {\n\treturn ww.original\n}\n\n// ExtractOriginalWriter extracts the original writer from a potentially wrapped writer.\n// If the writer implements writerUnwrapper, it recursively extracts the parent.\n// Otherwise, it returns the writer as-is.\nfunc ExtractOriginalWriter(w io.Writer) io.Writer {\n\tif w == nil {\n\t\treturn nil\n\t}\n\n\tif u, ok := w.(writerUnwrapper); ok {\n\t\tparent := u.Unwrap()\n\n\t\treturn ExtractOriginalWriter(parent)\n\t}\n\n\treturn w\n}\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"os\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/global\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n)\n\n// The main entrypoint for Terragrunt\nfunc main() {\n\texitCode := tf.NewDetailedExitCodeMap()\n\n\topts := options.NewTerragruntOptions()\n\n\tl := log.New(\n\t\tlog.WithOutput(opts.Writers.ErrWriter),\n\t\tlog.WithLevel(options.DefaultLogLevel),\n\t\tlog.WithFormatter(format.NewFormatter(format.NewPrettyFormatPlaceholders())),\n\t)\n\n\t// Immediately parse the `TG_LOG_LEVEL` environment variable, e.g. to set the TRACE level.\n\tif err := global.NewLogLevelFlag(l, opts, nil).Parse(os.Args); err != nil {\n\t\tl.Error(err.Error())\n\t\tos.Exit(1)\n\t}\n\n\tdefer func() {\n\t\tif opts.TerraformCliArgs.Contains(tf.FlagNameDetailedExitCode) {\n\t\t\terrors.Recover(checkForErrorsAndExit(l, exitCode.GetFinalDetailedExitCode()))\n\t\t\treturn\n\t\t}\n\n\t\terrors.Recover(checkForErrorsAndExit(l, exitCode.GetFinalExitCode()))\n\t}()\n\n\tapp := cli.NewApp(l, opts)\n\n\tctx := setupContext(l, exitCode)\n\terr := app.RunContext(ctx, os.Args)\n\n\tif opts.TerraformCliArgs.Contains(tf.FlagNameDetailedExitCode) {\n\t\tcheckForErrorsAndExit(l, exitCode.GetFinalDetailedExitCode())(err)\n\n\t\treturn\n\t}\n\n\tcheckForErrorsAndExit(l, exitCode.GetFinalExitCode())(err)\n}\n\n// If there is an error, display it in the console and exit with a non-zero exit code. Otherwise, exit 0.\nfunc checkForErrorsAndExit(l log.Logger, exitCode int) func(error) {\n\treturn func(err error) {\n\t\tif err == nil {\n\t\t\tos.Exit(exitCode)\n\t\t}\n\n\t\tl.Error(err.Error())\n\n\t\tif errStack := errors.ErrorStack(err); errStack != \"\" {\n\t\t\tl.Trace(errStack)\n\t\t}\n\n\t\t// exit with the underlying error code\n\t\texitCoder, exitCodeErr := util.GetExitCode(err)\n\t\tif exitCodeErr != nil {\n\t\t\texitCoder = 1\n\t\t}\n\n\t\tif explain := shell.ExplainError(err); len(explain) > 0 {\n\t\t\tl.Errorf(\"Suggested fixes: \\n%s\", explain)\n\t\t}\n\n\t\tos.Exit(exitCoder)\n\t}\n}\n\nfunc setupContext(l log.Logger, exitCode *tf.DetailedExitCodeMap) context.Context {\n\tctx := context.Background()\n\tctx = tf.ContextWithDetailedExitCode(ctx, exitCode)\n\n\treturn log.ContextWithLogger(ctx, l)\n}\n"
  },
  {
    "path": "mise.cicd.toml",
    "content": "[tools]\ngo-junit-report = \"2.1.0\"\npre-commit = \"4.2.0\"\ngcloud = \"535.0.0\"\nawscli = \"2.28.15\"\n\"go:golang.org/x/tools/cmd/goimports\" = \"latest\"\n\"go:golang.org/x/tools/gopls\" = \"v0.20.0\"\ntflint = \"0.50.3\"\nterraform = \"1.14.4\"\npipx = { version = \"1.8.0\", os = [\"macos\", \"linux\"] }\n\"pipx:codespell\" = { version = \"2.4.1\", os = [\"macos\", \"linux\"] }\n"
  },
  {
    "path": "mise.toml",
    "content": "[tools]\ngo = \"1.26.0\"\nopentofu = \"1.11.4\"\ngolangci-lint = \"2.9.0\"\n\"go:github.com/goph/licensei/cmd/licensei\" = \"v0.9.0\"\n\"go:go.uber.org/mock/mockgen\" = \"v0.6.0\"\n\"go:golang.org/x/tools/gopls\" = \"v0.20.0\"\n\"go:golang.org/x/tools/cmd/goimports\" = \"0.38.0\"\n"
  },
  {
    "path": "pkg/config/cache_test.go",
    "content": "package config_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cache\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nconst testCacheName = \"TerragruntConfig\"\n\nfunc TestTerragruntConfigCacheCreation(t *testing.T) {\n\tt.Parallel()\n\n\tcache := cache.NewCache[config.TerragruntConfig](testCacheName)\n\n\tassert.NotNil(t, cache.Mutex)\n\tassert.NotNil(t, cache.Cache)\n\n\tassert.Empty(t, cache.Cache)\n}\n\nfunc TestTerragruntConfigCacheOperation(t *testing.T) {\n\tt.Parallel()\n\n\ttestCacheKey := \"super-safe-cache-key\"\n\n\tctx := t.Context()\n\tcache := cache.NewCache[config.TerragruntConfig](testCacheName)\n\n\tactualResult, found := cache.Get(ctx, testCacheKey)\n\n\tassert.False(t, found)\n\tassert.Empty(t, actualResult)\n\n\tstubTerragruntConfig := config.TerragruntConfig{\n\t\tIsPartial: true, // Any random property will be sufficient\n\t}\n\n\tcache.Put(ctx, testCacheKey, stubTerragruntConfig)\n\tactualResult, found = cache.Get(ctx, testCacheKey)\n\n\tassert.True(t, found)\n\tassert.NotEmpty(t, actualResult)\n\tassert.Equal(t, stubTerragruntConfig, actualResult)\n}\n"
  },
  {
    "path": "pkg/config/catalog.go",
    "content": "package config\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"regexp\"\n\n\t\"github.com/gruntwork-io/go-commons/files\"\n\t\"github.com/gruntwork-io/terragrunt/internal/ctyhelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/zclconf/go-cty/cty\"\n)\n\nconst (\n\trootConfigFmt = `\ninclude \"root\" {\n  path = find_in_parent_folders(\"%s\")\n}\n`\n\t// matches a block and ignores commented out config, where the block name is passed as the first argument to fmt, e.g.\n\t// `fmt.Sprintf(hclBlockRegExprFmt, \"include\")` returns a regexp expression matching the `include` block:\n\t//\n\t// ```hcl\n\t// include {\n\t//\n\t// }\n\t// ```\n\thclBlockRegExprFmt = `(?is)(?:^|^((?:[^/]|/[^*])*)(?:/\\*.*?\\*/)?((?:[^/]|/[^*])*)\\n)(%s[ {][^\\}]+)`\n)\n\nvar (\n\tincludeBlockReg = regexp.MustCompile(fmt.Sprintf(hclBlockRegExprFmt, MetadataInclude))\n\tcatalogBlockReg = regexp.MustCompile(fmt.Sprintf(hclBlockRegExprFmt, MetadataCatalog))\n)\n\ntype CatalogConfig struct {\n\tNoShell         *bool    `hcl:\"no_shell,optional\" cty:\"no_shell\"`\n\tNoHooks         *bool    `hcl:\"no_hooks,optional\" cty:\"no_hooks\"`\n\tDefaultTemplate string   `hcl:\"default_template,optional\" cty:\"default_template\"`\n\tURLs            []string `hcl:\"urls,attr\" cty:\"urls\"`\n}\n\nfunc (cfg *CatalogConfig) String() string {\n\treturn fmt.Sprintf(\"Catalog{URLs = %v, DefaultTemplate = %v, NoShell = %v, NoHooks = %v}\", cfg.URLs, cfg.DefaultTemplate, cfg.NoShell, cfg.NoHooks)\n}\n\nfunc (cfg *CatalogConfig) normalize(configPath string) {\n\tconfigDir := filepath.Dir(configPath)\n\n\t// transform relative paths to absolute ones\n\tfor i, url := range cfg.URLs {\n\t\turl := filepath.Join(configDir, url)\n\n\t\tif files.FileExists(url) {\n\t\t\tcfg.URLs[i] = url\n\t\t}\n\t}\n\n\tif cfg.DefaultTemplate != \"\" {\n\t\tpath := filepath.Join(configDir, cfg.DefaultTemplate)\n\t\tif files.FileExists(path) {\n\t\t\tcfg.DefaultTemplate = path\n\t\t}\n\t}\n}\n\n// ReadCatalogConfig reads the `catalog` block from the nearest `terragrunt.hcl` file in the parent directories.\n//\n// We want users to be able to browse to any folder in an `infra-live` repo, run `terragrunt catalog` (with no URL) arg.\n// ReadCatalogConfig looks for the \"nearest\" `terragrunt.hcl` in the parent directories if the given\n// `opts.TerragruntConfigPath` does not exist. Since our normal parsing `ParseConfig` does not always work,\n// as some `terragrunt.hcl` files are meant to be used from an `include` and/or they might use\n// `find_in_parent_folders` and they only work from certain child folders, it parses this file to see if the\n// config contains `include{...find_in_parent_folders()...}` block to determine if it is the root configuration.\n// If it finds `terragrunt.hcl` that already has `include`, then read that configuration as is,\n// otherwise generate a stub child `terragrunt.hcl` in memory with an `include` to pull in the one we found.\n// Unlike the \"ReadTerragruntConfig\" func, it ignores any configuration errors not related to the \"catalog\" block.\nfunc ReadCatalogConfig(parentCtx context.Context, l log.Logger, pctx *ParsingContext) (*CatalogConfig, error) {\n\tconfigPath, configString, err := findCatalogConfig(parentCtx, l, pctx)\n\tif err != nil || configPath == \"\" {\n\t\treturn nil, err\n\t}\n\n\tpctx = pctx.Clone()\n\tpctx.TerragruntConfigPath = configPath\n\tpctx.ParserOptions = append(pctx.ParserOptions, hclparse.WithHaltOnErrorOnlyForBlocks([]string{MetadataCatalog}))\n\tpctx.ConvertToTerragruntConfigFunc = convertToTerragruntCatalogConfig\n\n\tconfig, err := ParseConfigString(parentCtx, pctx, l, configPath, configString, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn config.Catalog, nil\n}\n\nfunc findCatalogConfig(ctx context.Context, l log.Logger, outerPctx *ParsingContext) (string, string, error) {\n\tvar (\n\t\tconfigPath        = filepath.Join(filepath.Dir(outerPctx.TerragruntConfigPath), outerPctx.ScaffoldRootFileName)\n\t\tconfigName        = outerPctx.ScaffoldRootFileName\n\t\tcatalogConfigPath string\n\t)\n\n\tfor {\n\t\t// This allows to stop the process by pressing Ctrl-C, in case the loop is endless,\n\t\t// it can happen if the functions of the `filepath` package do not work correctly under a certain operating system.\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn \"\", \"\", nil\n\t\tdefault: // continue\n\t\t}\n\n\t\tparseCtx, pctx := NewParsingContext(ctx, l, WithStrictControls(outerPctx.StrictControls))\n\t\tpctx.TerragruntConfigPath = filepath.Join(filepath.Dir(configPath), util.UniqueID(), configName)\n\t\tpctx.MaxFoldersToCheck = outerPctx.MaxFoldersToCheck\n\n\t\tnewConfigPath, err := FindInParentFolders(parseCtx, pctx, l, []string{configName})\n\t\tif err != nil {\n\t\t\tvar parentFileNotFoundError ParentFileNotFoundError\n\t\t\tif ok := errors.As(err, &parentFileNotFoundError); ok {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\treturn \"\", \"\", err\n\t\t}\n\n\t\tconfigString, err := util.ReadFileAsString(newConfigPath)\n\t\tif err != nil {\n\t\t\treturn \"\", \"\", err\n\t\t}\n\n\t\t// if the config contains `include` block (root config), read the config as is.\n\t\tif includeBlockReg.MatchString(configString) {\n\t\t\treturn newConfigPath, configString, nil\n\t\t}\n\n\t\t// if the config contains a `catalog` block, save the path in case the root config is not found\n\t\tif catalogBlockReg.MatchString(configString) {\n\t\t\tcatalogConfigPath = newConfigPath\n\t\t}\n\n\t\tconfigPath = filepath.Dir(newConfigPath)\n\t}\n\n\t// if the config with the `catalog` block is found, create the root config with `include{ find_in_parent_folders() }`\n\t// and the path one directory deeper in order for `find_in_parent_folders` can find the catalog configuration.\n\tif catalogConfigPath != \"\" {\n\t\tconfigString := fmt.Sprintf(rootConfigFmt, configName)\n\t\tconfigPath = filepath.Join(filepath.Dir(catalogConfigPath), util.UniqueID(), configName)\n\n\t\treturn configPath, configString, nil\n\t}\n\n\treturn \"\", \"\", nil\n}\n\nfunc convertToTerragruntCatalogConfig(ctx context.Context, pctx *ParsingContext, configPath string, terragruntConfigFromFile *terragruntConfigFile) (cfg *TerragruntConfig, err error) {\n\tvar (\n\t\tterragruntConfig = &TerragruntConfig{}\n\t\tdefaultMetadata  = map[string]any{FoundInFile: configPath}\n\t)\n\n\tif terragruntConfigFromFile.Catalog != nil {\n\t\tterragruntConfig.Catalog = terragruntConfigFromFile.Catalog\n\t\tterragruntConfig.Catalog.normalize(configPath)\n\t\tterragruntConfig.SetFieldMetadata(MetadataCatalog, defaultMetadata)\n\t}\n\n\tif terragruntConfigFromFile.Engine != nil {\n\t\tterragruntConfig.Engine = terragruntConfigFromFile.Engine\n\t\tterragruntConfig.SetFieldMetadata(MetadataEngine, defaultMetadata)\n\t}\n\n\tif terragruntConfigFromFile.Exclude != nil {\n\t\tterragruntConfig.Exclude = terragruntConfigFromFile.Exclude\n\t\tterragruntConfig.SetFieldMetadata(MetadataExclude, defaultMetadata)\n\t}\n\n\tif terragruntConfigFromFile.Errors != nil {\n\t\tterragruntConfig.Errors = terragruntConfigFromFile.Errors\n\t\tterragruntConfig.SetFieldMetadata(MetadataErrors, defaultMetadata)\n\t}\n\n\tif pctx.Locals != nil && *pctx.Locals != cty.NilVal {\n\t\t// we should ignore any errors from `parseCtyValueToMap` as some `locals` values might have been incorrectly evaluated, that results to `json.Unmarshal` error.\n\t\t// for example if the locals block looks like `{\"var1\":, \"var2\":\"value2\"}`, `parseCtyValueToMap` returns the map with \"var2\" value and an syntax error,\n\t\t// but since we consciously understand that not all variables can be evaluated correctly due to the fact that parsing may not start from the real root file, we can safely ignore this error.\n\t\tlocalsParsed, _ := ctyhelper.ParseCtyValueToMap(*pctx.Locals)\n\t\t// Only set Locals if there are actual values to avoid setting an empty map\n\t\tif len(localsParsed) > 0 {\n\t\t\tterragruntConfig.Locals = localsParsed\n\t\t\tterragruntConfig.SetFieldMetadataMap(MetadataLocals, localsParsed, defaultMetadata)\n\t\t}\n\t}\n\n\treturn terragruntConfig, nil\n}\n"
  },
  {
    "path": "pkg/config/catalog_test.go",
    "content": "package config_test\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCatalogParseConfigFile(t *testing.T) {\n\tt.Parallel()\n\n\tbasePath, err := filepath.Abs(filepath.Join(\"..\", \"..\", \"test\", \"fixtures\", \"catalog\"))\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\texpectedErr    error\n\t\texpectedConfig *config.CatalogConfig\n\t\tconfigPath     string\n\t}{\n\t\t{\n\t\t\tconfigPath: filepath.Join(basePath, \"config1.hcl\"),\n\t\t\texpectedConfig: &config.CatalogConfig{\n\t\t\t\tURLs: []string{\n\t\t\t\t\tfilepath.Join(basePath, \"terraform-aws-eks\"), // this path exists in the fixture directory and must be converted to the absolute path.\n\t\t\t\t\t\"/repo-copier\",\n\t\t\t\t\t\"./terraform-aws-service-catalog\",\n\t\t\t\t\t\"/project/terragrunt/test/terraform-aws-vpc\",\n\t\t\t\t\t\"github.com/gruntwork-io/terraform-aws-lambda\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tconfigPath: filepath.Join(basePath, \"config2.hcl\"),\n\t\t},\n\t\t{\n\t\t\tconfigPath:     filepath.Join(basePath, \"config3.hcl\"),\n\t\t\texpectedConfig: &config.CatalogConfig{},\n\t\t},\n\t\t{\n\t\t\tconfigPath: filepath.Join(basePath, \"complex-legacy-root/terragrunt.hcl\"),\n\t\t\texpectedConfig: &config.CatalogConfig{\n\t\t\t\tURLs: []string{\n\t\t\t\t\tfilepath.Join(basePath, \"complex-legacy-root/dev/us-west-1/modules/terraform-aws-eks\"),\n\t\t\t\t\t\"./terraform-aws-service-catalog\",\n\t\t\t\t\t\"https://github.com/gruntwork-io/terraform-aws-utilities\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tconfigPath: filepath.Join(basePath, \"complex/root.hcl\"),\n\t\t\texpectedConfig: &config.CatalogConfig{\n\t\t\t\tURLs: []string{\n\t\t\t\t\tfilepath.Join(basePath, \"complex/dev/us-west-1/modules/terraform-aws-eks\"),\n\t\t\t\t\t\"./terraform-aws-service-catalog\",\n\t\t\t\t\t\"https://github.com/gruntwork-io/terraform-aws-utilities\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tconfigPath: filepath.Join(basePath, \"complex-legacy-root/dev/terragrunt.hcl\"),\n\t\t\texpectedConfig: &config.CatalogConfig{\n\t\t\t\tURLs: []string{\n\t\t\t\t\tfilepath.Join(basePath, \"complex-legacy-root/dev/us-west-1/modules/terraform-aws-eks\"),\n\t\t\t\t\t\"./terraform-aws-service-catalog\",\n\t\t\t\t\t\"https://github.com/gruntwork-io/terraform-aws-utilities\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tconfigPath: filepath.Join(basePath, \"complex/dev/root.hcl\"),\n\t\t\texpectedConfig: &config.CatalogConfig{\n\t\t\t\tURLs: []string{\n\t\t\t\t\tfilepath.Join(basePath, \"complex/dev/us-west-1/modules/terraform-aws-eks\"),\n\t\t\t\t\t\"./terraform-aws-service-catalog\",\n\t\t\t\t\t\"https://github.com/gruntwork-io/terraform-aws-utilities\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tconfigPath: filepath.Join(basePath, \"complex/dev/us-west-1/terragrunt.hcl\"),\n\t\t\texpectedConfig: &config.CatalogConfig{\n\t\t\t\tURLs: []string{\n\t\t\t\t\tfilepath.Join(basePath, \"complex/dev/us-west-1/modules/terraform-aws-eks\"),\n\t\t\t\t\t\"./terraform-aws-service-catalog\",\n\t\t\t\t\t\"https://github.com/gruntwork-io/terraform-aws-utilities\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tconfigPath: filepath.Join(basePath, \"complex/dev/us-west-1/modules/terragrunt.hcl\"),\n\t\t\texpectedConfig: &config.CatalogConfig{\n\t\t\t\tURLs: []string{\n\t\t\t\t\tfilepath.Join(basePath, \"complex/dev/us-west-1/modules/terraform-aws-eks\"),\n\t\t\t\t\t\"./terraform-aws-service-catalog\",\n\t\t\t\t\t\"https://github.com/gruntwork-io/terraform-aws-utilities\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tconfigPath: filepath.Join(basePath, \"complex/prod/terragrunt.hcl\"),\n\t\t\texpectedConfig: &config.CatalogConfig{\n\t\t\t\tURLs: []string{\n\t\t\t\t\tfilepath.Join(basePath, \"complex/dev/us-west-1/modules/terraform-aws-eks\"),\n\t\t\t\t\t\"./terraform-aws-service-catalog\",\n\t\t\t\t\t\"https://github.com/gruntwork-io/terraform-aws-utilities\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tconfigPath: filepath.Join(basePath, \"config4.hcl\"),\n\t\t\texpectedConfig: &config.CatalogConfig{\n\t\t\t\tDefaultTemplate: \"/test/fixtures/scaffold/external-template\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i, tt := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tl := logger.CreateLogger()\n\t\t\t_, catalogPctx := newTestParsingContext(t, tt.configPath)\n\t\t\tcatalogPctx.ScaffoldRootFileName = filepath.Base(tt.configPath)\n\t\t\tconfig, err := config.ReadCatalogConfig(t.Context(), l, catalogPctx)\n\n\t\t\tif tt.expectedErr == nil {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tt.expectedConfig, config)\n\t\t\t} else {\n\t\t\t\tassert.EqualError(t, err, tt.expectedErr.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/config/config.go",
    "content": "// Package config provides functionality for parsing Terragrunt configuration files.\npackage config\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errorconfig\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/writer\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cache\"\n\t\"github.com/gruntwork-io/terragrunt/internal/ctyhelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iam\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n\n\t\"github.com/hashicorp/go-getter\"\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/hashicorp/hcl/v2/hclsyntax\"\n\t\"github.com/hashicorp/hcl/v2/hclwrite\"\n\t\"github.com/zclconf/go-cty/cty\"\n\n\t\"maps\"\n\n\t\"github.com/gruntwork-io/go-commons/files\"\n\t\"github.com/gruntwork-io/terragrunt/internal/codegen\"\n\t\"github.com/gruntwork-io/terragrunt/internal/engine\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n\t\"github.com/mitchellh/mapstructure\"\n)\n\nconst (\n\tDefaultTerragruntConfigPath     = \"terragrunt.hcl\"\n\tDefaultStackFile                = \"terragrunt.stack.hcl\"\n\tDefaultTerragruntJSONConfigPath = \"terragrunt.hcl.json\"\n\tRecommendedParentConfigName     = \"root.hcl\"\n\n\tFoundInFile = \"found_in_file\"\n\n\tiamRoleCacheName = \"iamRoleCache\"\n\n\tlogMsgSeparator = \"\\n\"\n\n\tDefaultEngineType                   = \"rpc\"\n\tMetadataTerraform                   = \"terraform\"\n\tMetadataTerraformBinary             = \"terraform_binary\"\n\tMetadataTerraformVersionConstraint  = \"terraform_version_constraint\"\n\tMetadataTerragruntVersionConstraint = \"terragrunt_version_constraint\"\n\tMetadataRemoteState                 = \"remote_state\"\n\tMetadataDependencies                = \"dependencies\"\n\tMetadataDependency                  = \"dependency\"\n\tMetadataDownloadDir                 = \"download_dir\"\n\tMetadataPreventDestroy              = \"prevent_destroy\"\n\tMetadataIamRole                     = \"iam_role\"\n\tMetadataIamAssumeRoleDuration       = \"iam_assume_role_duration\"\n\tMetadataIamAssumeRoleSessionName    = \"iam_assume_role_session_name\"\n\tMetadataIamWebIdentityToken         = \"iam_web_identity_token\"\n\tMetadataInputs                      = \"inputs\"\n\tMetadataLocals                      = \"locals\"\n\tMetadataLocal                       = \"local\"\n\tMetadataCatalog                     = \"catalog\"\n\tMetadataEngine                      = \"engine\"\n\tMetadataGenerateConfigs             = \"generate\"\n\tMetadataInclude                     = \"include\"\n\tMetadataFeatureFlag                 = \"feature\"\n\tMetadataExclude                     = \"exclude\"\n\tMetadataErrors                      = \"errors\"\n\tMetadataRetry                       = \"retry\"\n\tMetadataIgnore                      = \"ignore\"\n\tMetadataValues                      = \"values\"\n\tMetadataStack                       = \"stack\"\n\tMetadataUnit                        = \"unit\"\n)\n\nvar (\n\t// Order matters, for example if none of the files are found `GetDefaultConfigPath` func returns the last element.\n\tDefaultTerragruntConfigPaths = []string{\n\t\tDefaultTerragruntJSONConfigPath,\n\t\tDefaultTerragruntConfigPath,\n\t}\n\n\tDefaultParserOptions = func(l log.Logger, strictControls strict.Controls) []hclparse.Option {\n\t\twriter := writer.New(\n\t\t\twriter.WithLogger(l),\n\t\t\twriter.WithDefaultLevel(log.ErrorLevel),\n\t\t\twriter.WithMsgSeparator(logMsgSeparator),\n\t\t)\n\n\t\tparseOpts := make([]hclparse.Option, 0, 3) //nolint:mnd\n\t\tparseOpts = append(parseOpts,\n\t\t\thclparse.WithDiagnosticsWriter(writer, l.Formatter().DisabledColors()),\n\t\t\thclparse.WithLogger(l),\n\t\t)\n\n\t\tstrictControl := strictControls.Find(controls.BareInclude)\n\n\t\t// If we can't find the strict control, we're probably in a test\n\t\t// where the option is being hand written. In that case,\n\t\t// we'll assume we're not in strict mode.\n\t\tif strictControl != nil {\n\t\t\tstrictControl.SuppressWarning()\n\n\t\t\tif err := strictControl.Evaluate(context.Background()); err != nil {\n\t\t\t\treturn parseOpts\n\t\t\t}\n\t\t}\n\n\t\tparseOpts = append(parseOpts, hclparse.WithFileUpdate(updateBareIncludeBlock))\n\n\t\treturn parseOpts\n\t}\n\n\tDefaultGenerateBlockIfDisabledValueStr = codegen.DisabledSkipStr\n)\n\n// DecodedBaseBlocks decoded base blocks struct\ntype DecodedBaseBlocks struct {\n\tTrackInclude *TrackInclude\n\tLocals       *cty.Value\n\tFeatureFlags *cty.Value\n}\n\n// TerragruntConfig represents a parsed and expanded configuration\n// NOTE: if any attributes are added, make sure to update terragruntConfigAsCty in config_as_cty.go\ntype TerragruntConfig struct {\n\tLocals                      map[string]any\n\tProcessedIncludes           IncludeConfigsMap\n\tFieldsMetadata              map[string]map[string]any\n\tTerraform                   *TerraformConfig\n\tErrors                      *ErrorsConfig\n\tRemoteState                 *remotestate.RemoteState\n\tDependencies                *ModuleDependencies\n\tExclude                     *ExcludeConfig\n\tPreventDestroy              *bool\n\tGenerateConfigs             map[string]codegen.GenerateConfig\n\tIamAssumeRoleDuration       *int64\n\tInputs                      map[string]any\n\tEngine                      *EngineConfig\n\tCatalog                     *CatalogConfig\n\tIamWebIdentityToken         string\n\tIamAssumeRoleSessionName    string\n\tIamRole                     string\n\tDownloadDir                 string\n\tTerragruntVersionConstraint string\n\tTerraformVersionConstraint  string\n\tTerraformBinary             string\n\tTerragruntDependencies      Dependencies\n\tFeatureFlags                FeatureFlags\n\tIsPartial                   bool\n}\n\nfunc (cfg *TerragruntConfig) GetRemoteState(l log.Logger, pctx *ParsingContext) (*remotestate.RemoteState, error) {\n\tif cfg.RemoteState == nil {\n\t\tl.Debug(\"Did not find remote `remote_state` block in the config\")\n\n\t\treturn nil, nil\n\t}\n\n\tsourceURL, err := GetTerraformSourceURL(pctx.Source, pctx.SourceMap, pctx.OriginalTerragruntConfigPath, cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif sourceURL != \"\" {\n\t\twalkWithSymlinks := pctx.Experiments.Evaluate(experiment.Symlinks)\n\n\t\ttfSource, err := tf.NewSource(l, sourceURL, pctx.DownloadDir, pctx.WorkingDir, walkWithSymlinks)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tpctx.WorkingDir = tfSource.WorkingDir\n\t}\n\n\treturn cfg.RemoteState, nil\n}\n\nfunc (cfg *TerragruntConfig) String() string {\n\treturn fmt.Sprintf(\"TerragruntConfig{Terraform = %v, RemoteState = %v, Dependencies = %v, PreventDestroy = %v}\", cfg.Terraform, cfg.RemoteState, cfg.Dependencies, cfg.PreventDestroy)\n}\n\n// GetIAMRoleOptions is a helper function that converts the Terragrunt config IAM role attributes to\n// iam.RoleOptions struct.\nfunc (cfg *TerragruntConfig) GetIAMRoleOptions() iam.RoleOptions {\n\tconfigIAMRoleOptions := iam.RoleOptions{\n\t\tRoleARN:               cfg.IamRole,\n\t\tAssumeRoleSessionName: cfg.IamAssumeRoleSessionName,\n\t\tWebIdentityToken:      cfg.IamWebIdentityToken,\n\t}\n\tif cfg.IamAssumeRoleDuration != nil {\n\t\tconfigIAMRoleOptions.AssumeRoleDuration = *cfg.IamAssumeRoleDuration\n\t}\n\n\treturn configIAMRoleOptions\n}\n\n// WriteTo writes the terragrunt config to a writer\nfunc (cfg *TerragruntConfig) WriteTo(w io.Writer) (int64, error) {\n\tcfgAsCty, err := TerragruntConfigAsCty(cfg)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tf := hclwrite.NewFile()\n\trootBody := f.Body()\n\n\t// Handle blocks first\n\tif len(cfg.Locals) > 0 {\n\t\tlocalsBlock := hclwrite.NewBlock(\"locals\", nil)\n\t\tlocalsBody := localsBlock.Body()\n\n\t\tlocalsAsCty := cfgAsCty.GetAttr(\"locals\")\n\n\t\tfor k := range cfg.Locals {\n\t\t\tlocalsBody.SetAttributeValue(k, localsAsCty.GetAttr(k))\n\t\t}\n\n\t\trootBody.AppendBlock(localsBlock)\n\t}\n\n\tif cfg.Terraform != nil {\n\t\tterraformBlock := hclwrite.NewBlock(\"terraform\", nil)\n\t\tterraformBody := terraformBlock.Body()\n\t\tterraformAsCty := cfgAsCty.GetAttr(\"terraform\")\n\n\t\t// Handle source\n\t\tif cfg.Terraform.Source != nil {\n\t\t\tterraformBody.SetAttributeValue(\"source\", terraformAsCty.GetAttr(\"source\"))\n\t\t}\n\n\t\t// Handle extra_arguments blocks\n\t\tif len(cfg.Terraform.ExtraArgs) > 0 {\n\t\t\textraArgsAsCty := terraformAsCty.GetAttr(\"extra_arguments\").AsValueMap()\n\n\t\t\tfor _, arg := range cfg.Terraform.ExtraArgs {\n\t\t\t\textraArgBlock := hclwrite.NewBlock(\"extra_arguments\", []string{arg.Name})\n\t\t\t\textraArgBody := extraArgBlock.Body()\n\t\t\t\targCty := extraArgsAsCty[arg.Name]\n\n\t\t\t\tif arg.Commands != nil {\n\t\t\t\t\textraArgBody.SetAttributeValue(\"commands\", argCty.GetAttr(\"commands\"))\n\t\t\t\t}\n\n\t\t\t\tif arg.Arguments != nil {\n\t\t\t\t\textraArgBody.SetAttributeValue(\"arguments\", argCty.GetAttr(\"arguments\"))\n\t\t\t\t}\n\n\t\t\t\tif arg.RequiredVarFiles != nil {\n\t\t\t\t\textraArgBody.SetAttributeValue(\"required_var_files\", argCty.GetAttr(\"required_var_files\"))\n\t\t\t\t}\n\n\t\t\t\tif arg.OptionalVarFiles != nil {\n\t\t\t\t\textraArgBody.SetAttributeValue(\"optional_var_files\", argCty.GetAttr(\"optional_var_files\"))\n\t\t\t\t}\n\n\t\t\t\tif arg.EnvVars != nil {\n\t\t\t\t\textraArgBody.SetAttributeValue(\"env_vars\", argCty.GetAttr(\"env_vars\"))\n\t\t\t\t}\n\n\t\t\t\tterraformBody.AppendBlock(extraArgBlock)\n\t\t\t}\n\t\t}\n\n\t\t// Handle hooks\n\t\tfor _, beforeHook := range cfg.Terraform.BeforeHooks { //nolint:dupl\n\t\t\tbeforeHookBlock := hclwrite.NewBlock(\"before_hook\", []string{beforeHook.Name})\n\t\t\tbeforeHookBody := beforeHookBlock.Body()\n\n\t\t\tbeforeHookAsCty := terraformAsCty.GetAttr(\"before_hook\").AsValueMap()[beforeHook.Name]\n\n\t\t\tif beforeHook.If != nil {\n\t\t\t\tbeforeHookBody.SetAttributeValue(\"if\", beforeHookAsCty.GetAttr(\"if\"))\n\t\t\t}\n\n\t\t\tif beforeHook.RunOnError != nil {\n\t\t\t\tbeforeHookBody.SetAttributeValue(\"run_on_error\", beforeHookAsCty.GetAttr(\"run_on_error\"))\n\t\t\t}\n\n\t\t\tbeforeHookBody.SetAttributeValue(\"commands\", beforeHookAsCty.GetAttr(\"commands\"))\n\t\t\tbeforeHookBody.SetAttributeValue(\"execute\", beforeHookAsCty.GetAttr(\"execute\"))\n\n\t\t\tif beforeHook.WorkingDir != nil {\n\t\t\t\tbeforeHookBody.SetAttributeValue(\"working_dir\", beforeHookAsCty.GetAttr(\"working_dir\"))\n\t\t\t}\n\n\t\t\tterraformBody.AppendBlock(beforeHookBlock)\n\t\t}\n\n\t\tfor _, afterHook := range cfg.Terraform.AfterHooks { //nolint:dupl\n\t\t\tafterHookBlock := hclwrite.NewBlock(\"after_hook\", []string{afterHook.Name})\n\t\t\tafterHookBody := afterHookBlock.Body()\n\n\t\t\tafterHookAsCty := terraformAsCty.GetAttr(\"after_hook\").AsValueMap()[afterHook.Name]\n\n\t\t\tif afterHook.If != nil {\n\t\t\t\tafterHookBody.SetAttributeValue(\"if\", afterHookAsCty.GetAttr(\"if\"))\n\t\t\t}\n\n\t\t\tif afterHook.RunOnError != nil {\n\t\t\t\tafterHookBody.SetAttributeValue(\"run_on_error\", afterHookAsCty.GetAttr(\"run_on_error\"))\n\t\t\t}\n\n\t\t\tafterHookBody.SetAttributeValue(\"commands\", afterHookAsCty.GetAttr(\"commands\"))\n\t\t\tafterHookBody.SetAttributeValue(\"execute\", afterHookAsCty.GetAttr(\"execute\"))\n\n\t\t\tif afterHook.WorkingDir != nil {\n\t\t\t\tafterHookBody.SetAttributeValue(\"working_dir\", afterHookAsCty.GetAttr(\"working_dir\"))\n\t\t\t}\n\n\t\t\tterraformBody.AppendBlock(afterHookBlock)\n\t\t}\n\n\t\tfor _, errorHook := range cfg.Terraform.ErrorHooks {\n\t\t\terrorHookBlock := hclwrite.NewBlock(\"error_hook\", []string{errorHook.Name})\n\t\t\terrorHookBody := errorHookBlock.Body()\n\n\t\t\terrorHookAsCty := terraformAsCty.GetAttr(\"error_hook\").AsValueMap()[errorHook.Name]\n\n\t\t\terrorHookBody.SetAttributeValue(\"commands\", errorHookAsCty.GetAttr(\"commands\"))\n\t\t\terrorHookBody.SetAttributeValue(\"execute\", errorHookAsCty.GetAttr(\"execute\"))\n\t\t\terrorHookBody.SetAttributeValue(\"on_errors\", errorHookAsCty.GetAttr(\"on_errors\"))\n\n\t\t\tif errorHook.WorkingDir != nil {\n\t\t\t\terrorHookBody.SetAttributeValue(\"working_dir\", errorHookAsCty.GetAttr(\"working_dir\"))\n\t\t\t}\n\n\t\t\tterraformBody.AppendBlock(errorHookBlock)\n\t\t}\n\n\t\trootBody.AppendBlock(terraformBlock)\n\t}\n\n\tif cfg.RemoteState != nil {\n\t\tremoteStateBlock := hclwrite.NewBlock(\"remote_state\", nil)\n\t\tremoteStateBody := remoteStateBlock.Body()\n\t\tremoteStateAsCty := cfgAsCty.GetAttr(\"remote_state\")\n\n\t\tremoteStateBody.SetAttributeValue(\"backend\", remoteStateAsCty.GetAttr(\"backend\"))\n\n\t\tif cfg.RemoteState.DisableInit {\n\t\t\tremoteStateBody.SetAttributeValue(\"disable_init\", remoteStateAsCty.GetAttr(\"disable_init\"))\n\t\t}\n\n\t\tif cfg.RemoteState.DisableDependencyOptimization {\n\t\t\tremoteStateBody.SetAttributeValue(\"disable_dependency_optimization\", remoteStateAsCty.GetAttr(\"disable_dependency_optimization\"))\n\t\t}\n\n\t\tif cfg.RemoteState.BackendConfig != nil {\n\t\t\tremoteStateBody.SetAttributeValue(\"config\", remoteStateAsCty.GetAttr(\"config\"))\n\t\t}\n\n\t\trootBody.AppendBlock(remoteStateBlock)\n\t}\n\n\tif cfg.Dependencies != nil && len(cfg.Dependencies.Paths) > 0 {\n\t\tdependenciesBlock := hclwrite.NewBlock(\"dependencies\", nil)\n\t\tdependenciesBody := dependenciesBlock.Body()\n\n\t\tdependenciesAsCty := cfgAsCty.GetAttr(\"dependencies\")\n\n\t\tdependenciesBody.SetAttributeValue(\"paths\", dependenciesAsCty.GetAttr(\"paths\"))\n\t\trootBody.AppendBlock(dependenciesBlock)\n\t}\n\n\t// Handle dependency blocks\n\tfor _, dep := range cfg.TerragruntDependencies {\n\t\tdepBlock := hclwrite.NewBlock(\"dependency\", []string{dep.Name})\n\t\tdepBody := depBlock.Body()\n\t\tdepAsCty := cfgAsCty.GetAttr(\"dependency\").GetAttr(dep.Name)\n\t\tdepBody.SetAttributeValue(\"config_path\", depAsCty.GetAttr(\"config_path\"))\n\n\t\tif dep.Enabled != nil {\n\t\t\tdepBody.SetAttributeValue(\"enabled\", goboolToCty(*dep.Enabled))\n\t\t}\n\n\t\tif dep.SkipOutputs != nil {\n\t\t\tdepBody.SetAttributeValue(\"skip_outputs\", goboolToCty(*dep.SkipOutputs))\n\t\t}\n\n\t\tif dep.MockOutputs != nil {\n\t\t\tdepBody.SetAttributeValue(\"mock_outputs\", depAsCty.GetAttr(\"mock_outputs\"))\n\t\t}\n\n\t\tif dep.MockOutputsAllowedTerraformCommands != nil {\n\t\t\tdepBody.SetAttributeValue(\"mock_outputs_allowed_terraform_commands\", depAsCty.GetAttr(\"mock_outputs_allowed_terraform_commands\"))\n\t\t}\n\n\t\tif dep.MockOutputsMergeStrategyWithState != nil {\n\t\t\tdepBody.SetAttributeValue(\"mock_outputs_merge_strategy_with_state\", depAsCty.GetAttr(\"mock_outputs_merge_strategy_with_state\"))\n\t\t}\n\n\t\trootBody.AppendBlock(depBlock)\n\t}\n\n\t// Handle generate blocks\n\tfor name, gen := range cfg.GenerateConfigs {\n\t\tgenBlock := hclwrite.NewBlock(\"generate\", []string{name})\n\t\tgenBody := genBlock.Body()\n\t\tgenBody.SetAttributeValue(\"path\", gostringToCty(gen.Path))\n\t\tgenBody.SetAttributeValue(\"if_exists\", gostringToCty(gen.IfExistsStr))\n\t\tgenBody.SetAttributeValue(\"if_disabled\", gostringToCty(gen.IfDisabledStr))\n\t\tgenBody.SetAttributeValue(\"contents\", gostringToCty(gen.Contents))\n\n\t\tif gen.CommentPrefix != codegen.DefaultCommentPrefix {\n\t\t\tgenBody.SetAttributeValue(\"comment_prefix\", gostringToCty(gen.CommentPrefix))\n\t\t}\n\n\t\tif gen.DisableSignature {\n\t\t\tgenBody.SetAttributeValue(\"disable_signature\", goboolToCty(gen.DisableSignature))\n\t\t}\n\n\t\tif gen.Disable {\n\t\t\tgenBody.SetAttributeValue(\"disable\", goboolToCty(gen.Disable))\n\t\t}\n\n\t\trootBody.AppendBlock(genBlock)\n\t}\n\n\t// Handle feature flags\n\tfor _, flag := range cfg.FeatureFlags {\n\t\tflagBlock := hclwrite.NewBlock(\"feature\", []string{flag.Name})\n\t\tflagBody := flagBlock.Body()\n\t\tflagAsCty := cfgAsCty.GetAttr(\"feature\").GetAttr(flag.Name)\n\n\t\tif flag.Default != nil {\n\t\t\tflagBody.SetAttributeValue(\"default\", flagAsCty.GetAttr(\"default\"))\n\t\t}\n\n\t\trootBody.AppendBlock(flagBlock)\n\t}\n\n\t// Handle engine block\n\tif cfg.Engine != nil {\n\t\tengineBlock := hclwrite.NewBlock(\"engine\", nil)\n\t\tengineBody := engineBlock.Body()\n\t\tengineAsCty := cfgAsCty.GetAttr(\"engine\")\n\n\t\tif cfg.Engine.Source != \"\" {\n\t\t\tengineBody.SetAttributeValue(\"source\", engineAsCty.GetAttr(\"source\"))\n\t\t}\n\n\t\tif cfg.Engine.Version != nil {\n\t\t\tengineBody.SetAttributeValue(\"version\", engineAsCty.GetAttr(\"version\"))\n\t\t}\n\n\t\tif cfg.Engine.Type != nil {\n\t\t\tengineBody.SetAttributeValue(\"type\", engineAsCty.GetAttr(\"type\"))\n\t\t}\n\n\t\tif cfg.Engine.Meta != nil {\n\t\t\tengineBody.SetAttributeValue(\"meta\", engineAsCty.GetAttr(\"meta\"))\n\t\t}\n\n\t\trootBody.AppendBlock(engineBlock)\n\t}\n\n\t// Handle exclude block\n\tif cfg.Exclude != nil {\n\t\texcludeBlock := hclwrite.NewBlock(\"exclude\", nil)\n\t\texcludeBody := excludeBlock.Body()\n\t\texcludeAsCty := cfgAsCty.GetAttr(\"exclude\")\n\n\t\tif cfg.Exclude.ExcludeDependencies != nil {\n\t\t\texcludeBody.SetAttributeValue(\"exclude_dependencies\", excludeAsCty.GetAttr(\"exclude_dependencies\"))\n\t\t}\n\n\t\tif len(cfg.Exclude.Actions) > 0 {\n\t\t\texcludeBody.SetAttributeValue(\"actions\", excludeAsCty.GetAttr(\"actions\"))\n\t\t}\n\n\t\tif cfg.Exclude.NoRun != nil {\n\t\t\texcludeBody.SetAttributeValue(\"no_run\", excludeAsCty.GetAttr(\"no_run\"))\n\t\t}\n\n\t\texcludeBody.SetAttributeValue(\"if\", excludeAsCty.GetAttr(\"if\"))\n\n\t\trootBody.AppendBlock(excludeBlock)\n\t}\n\n\t// Handle errors block\n\tif cfg.Errors != nil {\n\t\terrorsBlock := hclwrite.NewBlock(\"errors\", nil)\n\t\terrorsBody := errorsBlock.Body()\n\n\t\t// Handle retry blocks\n\t\tif len(cfg.Errors.Retry) > 0 {\n\t\t\tfor _, retryConfig := range cfg.Errors.Retry {\n\t\t\t\tretryBlock := hclwrite.NewBlock(\"retry\", []string{retryConfig.Label})\n\t\t\t\tretryBody := retryBlock.Body()\n\n\t\t\t\tif retryConfig.MaxAttempts > 0 {\n\t\t\t\t\tretryBody.SetAttributeValue(\"max_attempts\", cty.NumberIntVal(int64(retryConfig.MaxAttempts)))\n\t\t\t\t}\n\n\t\t\t\tif retryConfig.SleepIntervalSec > 0 {\n\t\t\t\t\tretryBody.SetAttributeValue(\"sleep_interval_sec\", cty.NumberIntVal(int64(retryConfig.SleepIntervalSec)))\n\t\t\t\t}\n\n\t\t\t\tif len(retryConfig.RetryableErrors) > 0 {\n\t\t\t\t\tretryableErrors := make([]cty.Value, len(retryConfig.RetryableErrors))\n\n\t\t\t\t\tfor i, err := range retryConfig.RetryableErrors {\n\t\t\t\t\t\tretryableErrors[i] = cty.StringVal(err)\n\t\t\t\t\t}\n\n\t\t\t\t\tretryBody.SetAttributeValue(\"retryable_errors\", cty.ListVal(retryableErrors))\n\t\t\t\t}\n\n\t\t\t\terrorsBody.AppendBlock(retryBlock)\n\t\t\t}\n\t\t}\n\n\t\t// Handle ignore blocks\n\t\tif len(cfg.Errors.Ignore) > 0 {\n\t\t\tfor _, ignoreConfig := range cfg.Errors.Ignore {\n\t\t\t\tignoreBlock := hclwrite.NewBlock(\"ignore\", []string{ignoreConfig.Label})\n\t\t\t\tignoreBody := ignoreBlock.Body()\n\n\t\t\t\tif len(ignoreConfig.IgnorableErrors) > 0 {\n\t\t\t\t\tignorableErrors := make([]cty.Value, len(ignoreConfig.IgnorableErrors))\n\n\t\t\t\t\tfor i, err := range ignoreConfig.IgnorableErrors {\n\t\t\t\t\t\tignorableErrors[i] = cty.StringVal(err)\n\t\t\t\t\t}\n\n\t\t\t\t\tignoreBody.SetAttributeValue(\"ignorable_errors\", cty.ListVal(ignorableErrors))\n\t\t\t\t}\n\n\t\t\t\tif ignoreConfig.Message != \"\" {\n\t\t\t\t\tignoreBody.SetAttributeValue(\"message\", cty.StringVal(ignoreConfig.Message))\n\t\t\t\t}\n\n\t\t\t\tif ignoreConfig.Signals != nil {\n\t\t\t\t\tignoreBody.SetAttributeValue(\"signals\", cty.MapVal(ignoreConfig.Signals))\n\t\t\t\t}\n\n\t\t\t\terrorsBody.AppendBlock(ignoreBlock)\n\t\t\t}\n\t\t}\n\n\t\trootBody.AppendBlock(errorsBlock)\n\t}\n\n\t// Handle catalog block\n\tif cfg.Catalog != nil {\n\t\tcatalogBlock := hclwrite.NewBlock(\"catalog\", nil)\n\t\tcatalogBody := catalogBlock.Body()\n\t\tcatalogAsCty := cfgAsCty.GetAttr(\"catalog\")\n\n\t\tif cfg.Catalog.DefaultTemplate != \"\" {\n\t\t\tcatalogBody.SetAttributeValue(\"default_template\", catalogAsCty.GetAttr(\"default_template\"))\n\t\t}\n\n\t\tif len(cfg.Catalog.URLs) > 0 {\n\t\t\tcatalogBody.SetAttributeValue(\"urls\", catalogAsCty.GetAttr(\"urls\"))\n\t\t}\n\n\t\tif cfg.Catalog.NoShell != nil {\n\t\t\tcatalogBody.SetAttributeValue(\"no_shell\", catalogAsCty.GetAttr(\"no_shell\"))\n\t\t}\n\n\t\tif cfg.Catalog.NoHooks != nil {\n\t\t\tcatalogBody.SetAttributeValue(\"no_hooks\", catalogAsCty.GetAttr(\"no_hooks\"))\n\t\t}\n\n\t\trootBody.AppendBlock(catalogBlock)\n\t}\n\n\t// Handle attributes\n\tif cfg.TerraformBinary != \"\" {\n\t\trootBody.SetAttributeValue(\"terraform_binary\", cfgAsCty.GetAttr(\"terraform_binary\"))\n\t}\n\n\tif cfg.TerraformVersionConstraint != \"\" {\n\t\trootBody.SetAttributeValue(\"terraform_version_constraint\", cfgAsCty.GetAttr(\"terraform_version_constraint\"))\n\t}\n\n\tif cfg.TerragruntVersionConstraint != \"\" {\n\t\trootBody.SetAttributeValue(\"terragrunt_version_constraint\", cfgAsCty.GetAttr(\"terragrunt_version_constraint\"))\n\t}\n\n\tif cfg.DownloadDir != \"\" {\n\t\trootBody.SetAttributeValue(\"download_dir\", cfgAsCty.GetAttr(\"download_dir\"))\n\t}\n\n\tif cfg.PreventDestroy != nil {\n\t\trootBody.SetAttributeValue(\"prevent_destroy\", cfgAsCty.GetAttr(\"prevent_destroy\"))\n\t}\n\n\tif cfg.IamRole != \"\" {\n\t\trootBody.SetAttributeValue(\"iam_role\", cfgAsCty.GetAttr(\"iam_role\"))\n\t}\n\n\tif cfg.IamAssumeRoleDuration != nil {\n\t\trootBody.SetAttributeValue(\"iam_assume_role_duration\", cfgAsCty.GetAttr(\"iam_assume_role_duration\"))\n\t}\n\n\tif cfg.IamAssumeRoleSessionName != \"\" {\n\t\trootBody.SetAttributeValue(\"iam_assume_role_session_name\", cfgAsCty.GetAttr(\"iam_assume_role_session_name\"))\n\t}\n\n\tif len(cfg.Inputs) > 0 {\n\t\trootBody.SetAttributeValue(\"inputs\", cfgAsCty.GetAttr(\"inputs\"))\n\t}\n\n\treturn f.WriteTo(w)\n}\n\n// terragruntConfigFile represents the configuration supported in a Terragrunt configuration file (i.e.\n// terragrunt.hcl)\ntype terragruntConfigFile struct {\n\tCatalog                     *CatalogConfig   `hcl:\"catalog,block\"`\n\tEngine                      *EngineConfig    `hcl:\"engine,block\"`\n\tTerraform                   *TerraformConfig `hcl:\"terraform,block\"`\n\tTerraformBinary             *string          `hcl:\"terraform_binary,attr\"`\n\tTerraformVersionConstraint  *string          `hcl:\"terraform_version_constraint,attr\"`\n\tTerragruntVersionConstraint *string          `hcl:\"terragrunt_version_constraint,attr\"`\n\tInputs                      *cty.Value       `hcl:\"inputs,attr\"`\n\n\t// We allow users to configure remote state (backend) via blocks:\n\t//\n\t// remote_state {\n\t//   backend = \"s3\"\n\t//   config  = { ... }\n\t// }\n\t//\n\t// Or as attributes:\n\t//\n\t// remote_state = {\n\t//   backend = \"s3\"\n\t//   config  = { ... }\n\t// }\n\tRemoteState     *remotestate.ConfigFile `hcl:\"remote_state,block\"`\n\tRemoteStateAttr *cty.Value              `hcl:\"remote_state,optional\"`\n\n\tDependencies             *ModuleDependencies `hcl:\"dependencies,block\"`\n\tDownloadDir              *string             `hcl:\"download_dir,attr\"`\n\tPreventDestroy           *bool               `hcl:\"prevent_destroy,attr\"`\n\tIamRole                  *string             `hcl:\"iam_role,attr\"`\n\tIamAssumeRoleDuration    *int64              `hcl:\"iam_assume_role_duration,attr\"`\n\tIamAssumeRoleSessionName *string             `hcl:\"iam_assume_role_session_name,attr\"`\n\tIamWebIdentityToken      *string             `hcl:\"iam_web_identity_token,attr\"`\n\tTerragruntDependencies   []Dependency        `hcl:\"dependency,block\"`\n\tFeatureFlags             []*FeatureFlag      `hcl:\"feature,block\"`\n\tExclude                  *ExcludeConfig      `hcl:\"exclude,block\"`\n\tErrors                   *ErrorsConfig       `hcl:\"errors,block\"`\n\n\t// We allow users to configure code generation via blocks:\n\t//\n\t// generate \"example\" {\n\t//   path     = \"example.tf\"\n\t//   contents = \"example\"\n\t// }\n\t//\n\t// Or via attributes:\n\t//\n\t// generate = {\n\t//   example = {\n\t//     path     = \"example.tf\"\n\t//     contents = \"example\"\n\t//   }\n\t// }\n\tGenerateAttrs  *cty.Value                `hcl:\"generate,optional\"`\n\tGenerateBlocks []terragruntGenerateBlock `hcl:\"generate,block\"`\n\n\t// This struct is used for validating and parsing the entire terragrunt config. Since locals and include are\n\t// evaluated in a completely separate cycle, it should not be evaluated here. Otherwise, we can't support self\n\t// referencing other elements in the same block.\n\t// We don't want to use the special Remain keyword here, as that would cause the checker to support parsing config\n\t// that have extraneous, unsupported blocks and attributes.\n\tLocals  *terragruntLocal          `hcl:\"locals,block\"`\n\tInclude []terragruntIncludeIgnore `hcl:\"include,block\"`\n}\n\n// We use a struct designed to not parse the block, as locals and includes are parsed and decoded using a special\n// routine that allows references to the other locals in the same block.\ntype terragruntLocal struct {\n\tRemain hcl.Body `hcl:\",remain\"`\n}\n\ntype terragruntIncludeIgnore struct {\n\tRemain hcl.Body `hcl:\",remain\"`\n\tName   string   `hcl:\"name,label\"`\n}\n\n// Struct used to parse generate blocks. This will later be converted to GenerateConfig structs so that we can go\n// through the codegen routine.\ntype terragruntGenerateBlock struct {\n\tIfDisabled       *string `hcl:\"if_disabled,attr\" mapstructure:\"if_disabled\"`\n\tCommentPrefix    *string `hcl:\"comment_prefix,attr\" mapstructure:\"comment_prefix\"`\n\tDisableSignature *bool   `hcl:\"disable_signature,attr\" mapstructure:\"disable_signature\"`\n\tDisable          *bool   `hcl:\"disable,attr\" mapstructure:\"disable\"`\n\tName             string  `hcl:\",label\" mapstructure:\",omitempty\"`\n\tPath             string  `hcl:\"path,attr\" mapstructure:\"path\"`\n\tIfExists         string  `hcl:\"if_exists,attr\" mapstructure:\"if_exists\"`\n\tContents         string  `hcl:\"contents,attr\" mapstructure:\"contents\"`\n}\n\ntype IncludeConfigsMap map[string]IncludeConfig\n\n// ContainsPath returns true if the given path is contained in at least one configuration.\nfunc (cfgs IncludeConfigsMap) ContainsPath(path string) bool {\n\tfor _, cfg := range cfgs {\n\t\tif cfg.Path == path {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\ntype IncludeConfigs []IncludeConfig\n\n// IncludeConfig represents the configuration settings for a parent Terragrunt configuration file that you can\n// include into a child Terragrunt configuration file. You can have more than one include config.\ntype IncludeConfig struct {\n\tExpose        *bool   `hcl:\"expose,attr\"`\n\tMergeStrategy *string `hcl:\"merge_strategy,attr\"`\n\tName          string  `hcl:\"name,label\"`\n\tPath          string  `hcl:\"path,attr\"`\n}\n\nfunc (include *IncludeConfig) String() string {\n\tif include == nil {\n\t\treturn \"IncludeConfig{nil}\"\n\t}\n\n\texposeStr := \"nil\"\n\tif include.Expose != nil {\n\t\texposeStr = strconv.FormatBool(*include.Expose)\n\t}\n\n\tmergeStrategyStr := \"nil\"\n\tif include.MergeStrategy != nil {\n\t\tmergeStrategyStr = fmt.Sprintf(\"%v\", include.MergeStrategy)\n\t}\n\n\treturn fmt.Sprintf(\"IncludeConfig{Path = %v, Expose = %v, MergeStrategy = %v}\", include.Path, exposeStr, mergeStrategyStr)\n}\n\nfunc (include *IncludeConfig) GetExpose() bool {\n\tif include == nil || include.Expose == nil {\n\t\treturn false\n\t}\n\n\treturn *include.Expose\n}\n\nfunc (include *IncludeConfig) GetMergeStrategy() (MergeStrategyType, error) {\n\tif include.MergeStrategy == nil {\n\t\treturn ShallowMerge, nil\n\t}\n\n\tstrategy := *include.MergeStrategy\n\tswitch strategy {\n\tcase string(NoMerge):\n\t\treturn NoMerge, nil\n\tcase string(ShallowMerge):\n\t\treturn ShallowMerge, nil\n\tcase string(DeepMerge):\n\t\treturn DeepMerge, nil\n\tcase string(DeepMergeMapOnly):\n\t\treturn DeepMergeMapOnly, nil\n\tdefault:\n\t\treturn NoMerge, errors.New(InvalidMergeStrategyTypeError(strategy))\n\t}\n}\n\ntype MergeStrategyType string\n\nconst (\n\tNoMerge          MergeStrategyType = \"no_merge\"\n\tShallowMerge     MergeStrategyType = \"shallow\"\n\tDeepMerge        MergeStrategyType = \"deep\"\n\tDeepMergeMapOnly MergeStrategyType = \"deep_map_only\"\n)\n\n// ModuleDependencies represents the paths to other Terraform modules that must be applied before the current module\n// can be applied\ntype ModuleDependencies struct {\n\tPaths []string `hcl:\"paths,attr\" cty:\"paths\"`\n}\n\n// Merge appends the paths in the provided ModuleDependencies object into this ModuleDependencies object.\nfunc (deps *ModuleDependencies) Merge(source *ModuleDependencies) {\n\tif source == nil {\n\t\treturn\n\t}\n\n\tfor _, path := range source.Paths {\n\t\tif !slices.Contains(deps.Paths, path) {\n\t\t\tdeps.Paths = append(deps.Paths, path)\n\t\t}\n\t}\n}\n\nfunc (deps *ModuleDependencies) String() string {\n\treturn fmt.Sprintf(\"ModuleDependencies{Paths = %v}\", deps.Paths)\n}\n\n// Hook specifies terraform commands (apply/plan) and array of os commands to execute\ntype Hook struct {\n\tIf             *bool    `hcl:\"if,attr\" cty:\"if\"`\n\tRunOnError     *bool    `hcl:\"run_on_error,attr\" cty:\"run_on_error\"`\n\tSuppressStdout *bool    `hcl:\"suppress_stdout,attr\" cty:\"suppress_stdout\"`\n\tWorkingDir     *string  `hcl:\"working_dir,attr\" cty:\"working_dir\"`\n\tName           string   `hcl:\"name,label\" cty:\"name\"`\n\tCommands       []string `hcl:\"commands,attr\" cty:\"commands\"`\n\tExecute        []string `hcl:\"execute,attr\" cty:\"execute\"`\n}\n\ntype ErrorHook struct {\n\tSuppressStdout *bool    `hcl:\"suppress_stdout,attr\" cty:\"suppress_stdout\"`\n\tWorkingDir     *string  `hcl:\"working_dir,attr\" cty:\"working_dir\"`\n\tName           string   `hcl:\"name,label\" cty:\"name\"`\n\tCommands       []string `hcl:\"commands,attr\" cty:\"commands\"`\n\tExecute        []string `hcl:\"execute,attr\" cty:\"execute\"`\n\tOnErrors       []string `hcl:\"on_errors,attr\" cty:\"on_errors\"`\n}\n\nfunc (conf *Hook) String() string {\n\treturn fmt.Sprintf(\"Hook{Name = %s, Commands = %v}\", conf.Name, len(conf.Commands))\n}\n\nfunc (conf *ErrorHook) String() string {\n\treturn fmt.Sprintf(\"Hook{Name = %s, Commands = %v}\", conf.Name, len(conf.Commands))\n}\n\n// TerraformConfig specifies where to find the Terraform configuration files\n// NOTE: If any attributes or blocks are added here, be sure to add it to ctyTerraformConfig in config_as_cty.go as\n// well.\ntype TerraformConfig struct {\n\tSource *string `hcl:\"source,attr\"`\n\n\t// Ideally we can avoid the pointer to list slice, but if it is not a pointer, Terraform requires the attribute to\n\t// be defined and we want to make this optional.\n\tIncludeInCopy   *[]string `hcl:\"include_in_copy,attr\"`\n\tExcludeFromCopy *[]string `hcl:\"exclude_from_copy,attr\"`\n\n\tCopyTerraformLockFile *bool                     `hcl:\"copy_terraform_lock_file,attr\"`\n\tExtraArgs             []TerraformExtraArguments `hcl:\"extra_arguments,block\"`\n\tBeforeHooks           []Hook                    `hcl:\"before_hook,block\"`\n\tAfterHooks            []Hook                    `hcl:\"after_hook,block\"`\n\tErrorHooks            []ErrorHook               `hcl:\"error_hook,block\"`\n}\n\nfunc (cfg *TerraformConfig) String() string {\n\treturn fmt.Sprintf(\"TerraformConfig{Source = %v}\", cfg.Source)\n}\n\nfunc (cfg *TerraformConfig) GetBeforeHooks() []Hook {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\treturn cfg.BeforeHooks\n}\n\nfunc (cfg *TerraformConfig) GetAfterHooks() []Hook {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\treturn cfg.AfterHooks\n}\n\nfunc (cfg *TerraformConfig) GetErrorHooks() []ErrorHook {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\treturn cfg.ErrorHooks\n}\n\nfunc (cfg *TerraformConfig) ValidateHooks() error {\n\tbeforeAndAfterHooks := append(cfg.GetBeforeHooks(), cfg.GetAfterHooks()...)\n\n\tfor _, curHook := range beforeAndAfterHooks {\n\t\tif len(curHook.Execute) < 1 || curHook.Execute[0] == \"\" {\n\t\t\treturn InvalidArgError(fmt.Sprintf(\"Error with hook %s. Need at least one non-empty argument in 'execute'.\", curHook.Name))\n\t\t}\n\t}\n\n\tfor _, curHook := range cfg.GetErrorHooks() {\n\t\tif len(curHook.Execute) < 1 || curHook.Execute[0] == \"\" {\n\t\t\treturn InvalidArgError(fmt.Sprintf(\"Error with hook %s. Need at least one non-empty argument in 'execute'.\", curHook.Name))\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// TerraformExtraArguments sets a list of arguments to pass to Terraform if command fits any in the `Commands` list\ntype TerraformExtraArguments struct {\n\tArguments        *[]string          `hcl:\"arguments,attr\" cty:\"arguments\"`\n\tRequiredVarFiles *[]string          `hcl:\"required_var_files,attr\" cty:\"required_var_files\"`\n\tOptionalVarFiles *[]string          `hcl:\"optional_var_files,attr\" cty:\"optional_var_files\"`\n\tEnvVars          *map[string]string `hcl:\"env_vars,attr\" cty:\"env_vars\"`\n\tName             string             `hcl:\"name,label\" cty:\"name\"`\n\tCommands         []string           `hcl:\"commands,attr\" cty:\"commands\"`\n}\n\nfunc (args *TerraformExtraArguments) String() string {\n\treturn fmt.Sprintf(\n\t\t\"TerraformArguments{Name = %s, Arguments = %v, Commands = %v, EnvVars = %v}\",\n\t\targs.Name,\n\t\targs.Arguments,\n\t\targs.Commands,\n\t\targs.EnvVars)\n}\n\nfunc (args *TerraformExtraArguments) GetVarFiles(l log.Logger) []string {\n\tvar varFiles []string\n\n\t// Include all specified RequiredVarFiles.\n\tif args.RequiredVarFiles != nil {\n\t\tvarFiles = append(varFiles, util.RemoveDuplicatesKeepLast(*args.RequiredVarFiles)...)\n\t}\n\n\t// If OptionalVarFiles is specified, check for each file if it exists and if so, include in the var\n\t// files list. Note that it is possible that many files resolve to the same path, so we remove\n\t// duplicates.\n\tif args.OptionalVarFiles != nil {\n\t\tfor _, file := range util.RemoveDuplicatesKeepLast(*args.OptionalVarFiles) {\n\t\t\tif util.FileExists(file) {\n\t\t\t\tvarFiles = append(varFiles, file)\n\t\t\t} else {\n\t\t\t\tl.Debugf(\"Skipping var-file %s as it does not exist\", file)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn varFiles\n}\n\n// GetTerraformSourceURL returns the source URL for OpenTofu/Terraform configuration.\n//\n// There are two ways a user can tell Terragrunt that it needs to download Terraform configurations from a specific\n// URL: via a command-line option or via an entry in the Terragrunt configuration. If the user used one of these, this\n// method returns the source URL. If neither is specified, returns \".\" to indicate the current directory should be\n// used as the source, ensuring a .terragrunt-cache directory is always created for consistency.\nfunc GetTerraformSourceURL(source string, sourceMap map[string]string, originalConfigPath string, terragruntConfig *TerragruntConfig) (string, error) {\n\tswitch {\n\tcase source != \"\":\n\t\treturn source, nil\n\tcase terragruntConfig.Terraform != nil && terragruntConfig.Terraform.Source != nil:\n\t\treturn adjustSourceWithMap(sourceMap, *terragruntConfig.Terraform.Source, originalConfigPath)\n\tdefault:\n\t\treturn \".\", nil\n\t}\n}\n\n// adjustSourceWithMap implements the --terragrunt-source-map feature. This function will check if the URL portion of a\n// terraform source matches any entry in the provided source map and if it does, replace it with the configured source\n// in the map. Note that this only performs literal matches with the URL portion.\n//\n// Example:\n// Suppose terragrunt is called with:\n//\n//\t--terragrunt-source-map git::ssh://git@github.com/gruntwork-io/i-dont-exist.git=/path/to/local-modules\n//\n// and the terraform source is:\n//\n//\tgit::ssh://git@github.com/gruntwork-io/i-dont-exist.git//fixtures/source-map/modules/app?ref=master\n//\n// This function will take that source and transform it to:\n//\n//\t/path/to/local-modules/source-map/modules/app\nfunc adjustSourceWithMap(sourceMap map[string]string, source string, modulePath string) (string, error) {\n\t// Skip logic if source map is not configured\n\tif len(sourceMap) == 0 {\n\t\treturn source, nil\n\t}\n\n\t// use go-getter to split the module source string into a valid URL and subdirectory (if // is present)\n\tmoduleURL, moduleSubdir := getter.SourceDirSubdir(source)\n\n\t// if both URL and subdir are missing, something went terribly wrong\n\tif moduleURL == \"\" && moduleSubdir == \"\" {\n\t\treturn \"\", errors.New(InvalidSourceURLWithMapError{ModulePath: modulePath, ModuleSourceURL: source})\n\t}\n\n\t// If module URL is missing, return the source as is as it will not match anything in the map.\n\tif moduleURL == \"\" {\n\t\treturn source, nil\n\t}\n\n\t// Before looking up in sourceMap, make sure to drop any query parameters.\n\tmoduleURLParsed, err := url.Parse(moduleURL)\n\tif err != nil {\n\t\treturn source, err\n\t}\n\n\tmoduleURLParsed.RawQuery = \"\"\n\tmoduleURLQuery := moduleURLParsed.String()\n\n\t// Check if there is an entry to replace the URL portion in the map. Return the source as is if there is no entry in\n\t// the map.\n\tsourcePath, hasKey := sourceMap[moduleURLQuery]\n\tif !hasKey {\n\t\treturn source, nil\n\t}\n\n\t// Since there is a source mapping, replace the module URL portion with the entry in the map, and join with the\n\t// subdir.\n\t// If subdir is missing, check if we can obtain a valid module name from the URL portion.\n\tif moduleSubdir == \"\" {\n\t\tmoduleSubdirFromURL, err := getModulePathFromSourceURL(moduleURL)\n\t\tif err != nil {\n\t\t\treturn moduleSubdirFromURL, err\n\t\t}\n\n\t\tmoduleSubdir = moduleSubdirFromURL\n\t}\n\n\treturn util.JoinTerraformModulePath(sourcePath, moduleSubdir), nil\n}\n\n// GetDefaultConfigPath returns the default path to use for the Terragrunt configuration\n// that exists within the path giving preference to `terragrunt.hcl`\nfunc GetDefaultConfigPath(workingDir string) string {\n\t// check if a configuration file was passed as `workingDir`.\n\tif !files.IsDir(workingDir) && files.FileExists(workingDir) {\n\t\treturn workingDir\n\t}\n\n\tvar configPath string\n\n\tfor _, configPath = range DefaultTerragruntConfigPaths {\n\t\tif !filepath.IsAbs(configPath) {\n\t\t\tconfigPath = filepath.Join(workingDir, configPath)\n\t\t}\n\n\t\tif files.FileExists(configPath) {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn configPath\n}\n\n// FindConfigFilesInPath returns a list of all Terragrunt config files in the given path or any subfolder of the path.\n//\n// Parameters:\n//   - rootPath: the root directory to search\n//   - experiments: experiment flags (for symlink support)\n//   - configPath: the terragrunt config path (to detect non-default config filenames)\n//   - env: environment variables (to resolve TF_DATA_DIR)\n//   - downloadDir: the terragrunt download directory to skip\nfunc FindConfigFilesInPath(\n\trootPath string,\n\texperiments experiment.Experiments,\n\tconfigPath string,\n\tenv map[string]string,\n\tdownloadDir string,\n) ([]string, error) {\n\tconfigFiles := []string{}\n\n\twalkFunc := filepath.WalkDir\n\n\tif experiments.Evaluate(experiment.Symlinks) {\n\t\twalkFunc = util.WalkDirWithSymlinks\n\t}\n\n\ttfDataDir := tf.DefaultTFDataDir\n\tif d, ok := env[\"TF_DATA_DIR\"]; ok {\n\t\ttfDataDir = d\n\t}\n\n\terr := walkFunc(rootPath, func(path string, d fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !d.IsDir() {\n\t\t\treturn nil\n\t\t}\n\n\t\tif !isTerragruntModuleDir(path, tfDataDir, downloadDir) {\n\t\t\treturn filepath.SkipDir\n\t\t}\n\n\t\tfor _, configFile := range append(DefaultTerragruntConfigPaths, filepath.Base(configPath)) {\n\t\t\tif !filepath.IsAbs(configFile) {\n\t\t\t\tconfigFile = filepath.Join(path, configFile)\n\t\t\t}\n\n\t\t\tif !util.IsDir(configFile) && util.FileExists(configFile) {\n\t\t\t\tconfigFiles = append(configFiles, configFile)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\n\treturn configFiles, err\n}\n\n// isTerragruntModuleDir returns true if the given path contains a Terragrunt module and false otherwise. The path\n// can not contain a cache, data, or download dir.\nfunc isTerragruntModuleDir(path string, tfDataDir string, downloadDir string) bool {\n\t// Skip the Terragrunt cache dir\n\tif util.ContainsPath(path, util.TerragruntCacheDir) {\n\t\treturn false\n\t}\n\n\t// Skip the Terraform data dir\n\tif filepath.IsAbs(tfDataDir) {\n\t\tif util.HasPathPrefix(path, tfDataDir) {\n\t\t\treturn false\n\t\t}\n\t} else {\n\t\tif util.ContainsPath(path, tfDataDir) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\t// Skip any custom download dir specified by the user\n\tif strings.Contains(filepath.Clean(path), filepath.Clean(downloadDir)) {\n\t\treturn false\n\t}\n\n\treturn true\n}\n\n// ReadTerragruntConfig reads the Terragrunt config file from its default location.\n// The caller provides a fully populated ParsingContext (typically via configbridge.NewParsingContext).\nfunc ReadTerragruntConfig(ctx context.Context,\n\tl log.Logger,\n\tpctx *ParsingContext,\n\tparserOptions []hclparse.Option,\n) (*TerragruntConfig, error) {\n\tl.Debugf(\"Reading Terragrunt config file at %s\", util.RelPathForLog(pctx.RootWorkingDir, pctx.TerragruntConfigPath, pctx.Writers.LogShowAbsPaths))\n\n\tpctx = pctx.WithParseOption(parserOptions)\n\n\treturn ParseConfigFile(ctx, pctx, l, pctx.TerragruntConfigPath, nil)\n}\n\n// ParseConfigFile parses the Terragrunt config file at the given path. If the include parameter is not nil, then treat this as a config\n// included in some other config file when resolving relative paths.\nfunc ParseConfigFile(\n\tctx context.Context,\n\tpctx *ParsingContext,\n\tl log.Logger,\n\tconfigPath string,\n\tincludeFromChild *IncludeConfig,\n) (*TerragruntConfig, error) {\n\tvar err error\n\n\tpctx, err = pctx.WithIncrementedDepth()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar config *TerragruntConfig\n\n\thclCache := cache.ContextCache[*hclparse.File](ctx, HclCacheContextKey)\n\n\t// Build cache key components before tracing to determine cache hit status\n\tchildKey := \"nil\"\n\tif includeFromChild != nil {\n\t\tchildKey = includeFromChild.String()\n\t}\n\n\tdecodeListKey := \"nil\"\n\tif pctx.PartialParseDecodeList != nil {\n\t\tdecodeListKey = fmt.Sprintf(\"%v\", pctx.PartialParseDecodeList)\n\t}\n\n\tfileInfo, err := os.Stat(configPath)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil, TerragruntConfigNotFoundError{Path: configPath}\n\t\t}\n\n\t\treturn nil, errors.Errorf(\"failed to get file info: %w\", err)\n\t}\n\n\tcacheKey := fmt.Sprintf(\"%v-%v-%v-%v-%v\",\n\t\tconfigPath,\n\t\tpctx.WorkingDir,\n\t\tchildKey,\n\t\tdecodeListKey,\n\t\tfileInfo.ModTime().UnixMicro(),\n\t)\n\n\t// Check cache hit status before tracing\n\t_, cacheHit := hclCache.Get(ctx, cacheKey)\n\n\tisPartial := len(pctx.PartialParseDecodeList) > 0\n\n\terr = TraceParseConfigFile(\n\t\tctx,\n\t\tconfigPath,\n\t\tpctx.WorkingDir,\n\t\tisPartial,\n\t\tpctx.PartialParseDecodeList,\n\t\tincludeFromChild,\n\t\tcacheHit,\n\t\tfunc(childCtx context.Context) error {\n\t\t\tvar file *hclparse.File\n\n\t\t\tif cacheConfig, found := hclCache.Get(childCtx, cacheKey); found {\n\t\t\t\tfile = cacheConfig\n\t\t\t} else {\n\t\t\t\t// Parse the HCL file into an AST body that can be decoded multiple times later without having to re-parse\n\t\t\t\tvar parseErr error\n\n\t\t\t\tfile, parseErr = hclparse.NewParser(pctx.ParserOptions...).ParseFromFile(configPath)\n\t\t\t\tif parseErr != nil {\n\t\t\t\t\treturn parseErr\n\t\t\t\t}\n\n\t\t\t\thclCache.Put(childCtx, cacheKey, file)\n\t\t\t}\n\n\t\t\tvar parseErr error\n\n\t\t\tconfig, parseErr = ParseConfig(childCtx, pctx, l, file, includeFromChild)\n\t\t\tif parseErr != nil {\n\t\t\t\treturn parseErr\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\tif err != nil {\n\t\treturn config, err\n\t}\n\n\treturn config, nil\n}\n\nfunc ParseConfigString(ctx context.Context, pctx *ParsingContext, l log.Logger, configPath string, configString string, includeFromChild *IncludeConfig) (*TerragruntConfig, error) {\n\t// Parse the HCL file into an AST body that can be decoded multiple times later without having to re-parse\n\tfile, err := hclparse.NewParser(pctx.ParserOptions...).ParseFromString(configString, configPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconfig, err := ParseConfig(ctx, pctx, l, file, includeFromChild)\n\tif err != nil {\n\t\treturn config, err\n\t}\n\n\treturn config, nil\n}\n\n// ParseConfig parses the Terragrunt config contained in the given hcl file and merge it with the given include config (if any). Note\n// that the config parsing consists of multiple stages so as to allow referencing of data resulting from parsing\n// previous config. The parsing order is:\n//  1. Parse include. Include is parsed first and is used to import another config. All the config in the include block is\n//     then merged into the current TerragruntConfig, except for locals (by design). Note that since the include block is\n//     parsed first, you cannot reference locals in the include block config.\n//  2. Parse locals. Since locals are parsed next, you can only reference other locals in the locals block. Although it\n//     is possible to merge locals from a config imported with an include block, we do not do that here to avoid\n//     complicated referencing issues. Please refer to the globals proposal for an alternative that allows merging from\n//     included config: https://github.com/gruntwork-io/terragrunt/issues/814\n//     Allowed References:\n//     - locals\n//  3. Parse dependency blocks. This includes running `terragrunt output` to fetch the output data from another\n//     terragrunt config, so that it is accessible within the config. See PartialParseConfigString for a way to parse the\n//     blocks but avoid decoding.\n//     Note that this step is skipped if we already retrieved all the dependencies (which is the case when parsing\n//     included config files). This is determined by the dependencyOutputs input parameter.\n//     Allowed References:\n//     - locals\n//  4. Parse everything else. At this point, all the necessary building blocks for parsing the rest of the config are\n//     available, so parse the rest of the config.\n//     Allowed References:\n//     - locals\n//     - dependency\n//  5. Merge the included config with the parsed config. Note that all the config data is mergeable except for `locals`\n//     blocks, which are only scoped to be available within the defining config.\nfunc ParseConfig(\n\tctx context.Context,\n\tpctx *ParsingContext,\n\tl log.Logger,\n\tfile *hclparse.File,\n\tincludeFromChild *IncludeConfig,\n) (*TerragruntConfig, error) {\n\terrs := &errors.MultiError{}\n\n\tif err := DetectDeprecatedConfigurations(ctx, pctx, l, file); err != nil {\n\t\treturn nil, err\n\t}\n\n\tpctx = pctx.WithTrackInclude(nil)\n\n\t// Initial evaluation of configuration to load flags like IamRole which will be used for final parsing\n\t// https://github.com/gruntwork-io/terragrunt/issues/667\n\tif err := setIAMRole(ctx, pctx, l, file, includeFromChild); err != nil {\n\t\terrs = errs.Append(err)\n\t}\n\n\t// read unit files and add to context\n\tunitValues, err := ReadValues(ctx, pctx, l, filepath.Dir(file.ConfigPath))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpctx = pctx.WithValues(unitValues)\n\n\t// Decode just the Base blocks. See the function docs for DecodeBaseBlocks for more info on what base blocks are.\n\tvar baseBlocks *DecodedBaseBlocks\n\n\tbaseBlocks, err = TraceParseBaseBlocks(ctx, l, file.ConfigPath, func(childCtx context.Context) (*DecodedBaseBlocks, error) {\n\t\treturn DecodeBaseBlocks(childCtx, pctx, l, file, includeFromChild)\n\t})\n\tif err != nil {\n\t\terrs = errs.Append(err)\n\t}\n\n\tif baseBlocks != nil {\n\t\tpctx = pctx.WithTrackInclude(baseBlocks.TrackInclude)\n\t\tpctx = pctx.WithFeatures(baseBlocks.FeatureFlags)\n\t\tpctx = pctx.WithLocals(baseBlocks.Locals)\n\t}\n\n\t// Emit additional trace with comprehensive base blocks details\n\tif baseBlocks != nil {\n\t\tTraceParseBaseBlocksResult(ctx, file.ConfigPath, baseBlocks)\n\t}\n\n\tif !pctx.SkipOutputsResolution && pctx.DecodedDependencies == nil {\n\t\t// Decode just the `dependency` blocks, retrieving the outputs from the target terragrunt config in the\n\t\t// process.\n\t\tretrievedOutputs, err := decodeAndRetrieveOutputs(ctx, pctx, l, file)\n\t\tif err != nil {\n\t\t\terrs = errs.Append(err)\n\t\t}\n\n\t\tpctx.DecodedDependencies = retrievedOutputs\n\t}\n\n\tevalContext, err := createTerragruntEvalContext(ctx, pctx, l, file.ConfigPath)\n\tif err != nil {\n\t\terrs = errs.Append(err)\n\t}\n\n\t// Decode the rest of the config, passing in this config's `include` block or the child's `include` block, whichever\n\t// is appropriate\n\tvar terragruntConfigFile *terragruntConfigFile\n\n\terr = TraceParseConfigDecode(ctx, file.ConfigPath, func(childCtx context.Context) error {\n\t\tvar decodeErr error\n\n\t\tterragruntConfigFile, decodeErr = decodeAsTerragruntConfigFile(pctx, l, file, evalContext)\n\n\t\treturn decodeErr\n\t})\n\tif err != nil {\n\t\terrs = errs.Append(err)\n\t}\n\n\tif terragruntConfigFile == nil {\n\t\treturn nil, errors.New(CouldNotResolveTerragruntConfigInFileError(file.ConfigPath))\n\t}\n\n\tconfig, err := convertToTerragruntConfig(ctx, pctx, file.ConfigPath, terragruntConfigFile)\n\tif err != nil {\n\t\terrs = errs.Append(err)\n\t}\n\n\t// If this file includes another, parse and merge it. Otherwise, just return this config.\n\t// If there have been errors during this parse, don't attempt to parse the included config.\n\tif pctx.TrackInclude != nil {\n\t\t// Extract include paths for telemetry\n\t\tincludeCount := len(pctx.TrackInclude.CurrentList)\n\t\tincludePaths := make([]string, 0, includeCount)\n\n\t\tfor _, inc := range pctx.TrackInclude.CurrentList {\n\t\t\tif inc.Path != \"\" {\n\t\t\t\tincludePaths = append(includePaths, inc.Path)\n\t\t\t}\n\t\t}\n\n\t\tvar mergedConfig *TerragruntConfig\n\n\t\terr = TraceParseIncludeMerge(ctx, file.ConfigPath, includeCount, includePaths, func(childCtx context.Context) error {\n\t\t\tvar mergeErr error\n\n\t\t\t// Use the child context for trace propagation so include parsing is a child span\n\t\t\tmergedConfig, mergeErr = handleInclude(childCtx, pctx, l, config, false)\n\n\t\t\treturn mergeErr\n\t\t})\n\t\tif err != nil {\n\t\t\terrs = errs.Append(err)\n\t\t\treturn config, errs.ErrorOrNil()\n\t\t}\n\n\t\t// We should never get a nil config here, so if we do, return the config we've been able to parse so far\n\t\t// and return any errors that have occurred so far to avoid a nil pointer dereference below.\n\t\tif mergedConfig == nil {\n\t\t\treturn config, errs.ErrorOrNil()\n\t\t}\n\n\t\t// Saving processed includes into configuration, direct assignment since nested includes aren't supported\n\t\tmergedConfig.ProcessedIncludes = pctx.TrackInclude.CurrentMap\n\t\t// Make sure the top level information that is not automatically merged in is captured on the merged config to\n\t\t// ensure the proper representation of the config is captured.\n\t\t// - Locals are deliberately not merged in so that they remain local in scope. Here, we directly set it to the\n\t\t//   original locals for the current config being handled, as that is the locals list that is in scope for this\n\t\t//   config.\n\t\tmergedConfig.Locals = config.Locals\n\t\tmergedConfig.Exclude = config.Exclude\n\n\t\treturn mergedConfig, errs.ErrorOrNil()\n\t}\n\n\treturn config, errs.ErrorOrNil()\n}\n\n// DetectDeprecatedConfigurations detects if deprecated configurations are used in the given HCL file.\nfunc DetectDeprecatedConfigurations(ctx context.Context, pctx *ParsingContext, l log.Logger, file *hclparse.File) error {\n\tif DetectInputsCtyUsage(file) {\n\t\t// Dependency inputs (dependency.foo.inputs.bar) are now blocked by default for performance.\n\t\t// This deprecated feature causes significant performance overhead due to recursive parsing.\n\t\treturn errors.New(\"Reading inputs from dependencies is no longer supported. To acquire values from dependencies, use outputs (dependency.foo.outputs.bar) instead.\")\n\t}\n\n\tif detectBareIncludeUsage(file) {\n\t\tallControls := pctx.StrictControls\n\n\t\tbareInclude := allControls.Find(controls.BareInclude)\n\t\tif bareInclude == nil {\n\t\t\treturn errors.New(\"failed to find control \" + controls.BareInclude)\n\t\t}\n\n\t\tevalCtx := log.ContextWithLogger(ctx, l)\n\t\tif err := bareInclude.Evaluate(evalCtx); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// DetectInputsCtyUsage detects if an identifier matching dependency.foo.inputs.bar is used in the given HCL file.\n//\n// This is deprecated functionality, so we look for this to determine if we should throw an error or warning.\nfunc DetectInputsCtyUsage(file *hclparse.File) bool {\n\tbody, ok := file.Body.(*hclsyntax.Body)\n\tif !ok {\n\t\treturn false\n\t}\n\n\tfor _, attr := range body.Attributes {\n\t\tfor _, traversal := range attr.Expr.Variables() {\n\t\t\tconst dependencyInputsIdentifierMinParts = 3\n\n\t\t\tif len(traversal) < dependencyInputsIdentifierMinParts {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\troot, ok := traversal[0].(hcl.TraverseRoot)\n\t\t\tif !ok || root.Name != MetadataDependency {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tattrTraversal, ok := traversal[2].(hcl.TraverseAttr)\n\t\t\tif !ok || attrTraversal.Name != MetadataInputs {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// detectBareIncludeUsage detects if an identifier matching include.foo is used in the given HCL file.\n//\n// This is deprecated functionality, so we look for this to determine if we should throw an error or warning.\nfunc detectBareIncludeUsage(file *hclparse.File) bool {\n\tswitch filepath.Ext(file.ConfigPath) {\n\tcase \".json\":\n\t\tvar data map[string]any\n\t\tif err := json.Unmarshal(file.Bytes, &data); err != nil {\n\t\t\t// If JSON is invalid, it can't be a valid bare include structure.\n\t\t\t// The main parser will handle the invalid JSON error.\n\t\t\treturn false\n\t\t}\n\n\t\tincludeBlockUntyped, exists := data[MetadataInclude]\n\t\tif !exists {\n\t\t\treturn false\n\t\t}\n\n\t\tswitch includeBlockTyped := includeBlockUntyped.(type) {\n\t\tcase map[string]any:\n\t\t\t// Delegate to the logic from include.go, which checks if the map\n\t\t\t// represents a bare include block (e.g., only known include attributes).\n\t\t\treturn jsonIsIncludeBlock(includeBlockTyped)\n\t\tcase []any:\n\t\t\t// A bare include in JSON array form must have exactly one element,\n\t\t\t// and that element must be an include block.\n\t\t\tif len(includeBlockTyped) == 1 {\n\t\t\t\tif firstElement, ok := includeBlockTyped[0].(map[string]any); ok {\n\t\t\t\t\treturn jsonIsIncludeBlock(firstElement)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn false\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\tdefault:\n\t\tbody, ok := file.Body.(*hclsyntax.Body)\n\t\tif !ok {\n\t\t\treturn false\n\t\t}\n\n\t\tfor _, block := range body.Blocks {\n\t\t\tif block.Type == MetadataInclude && len(block.Labels) == 0 {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\n\t\treturn false\n\t}\n}\n\n// iamRoleCache - store for cached values of IAM roles\nvar iamRoleCache = cache.NewCache[iam.RoleOptions](iamRoleCacheName)\n\n// setIAMRole - extract IAM role details from Terragrunt flags block\nfunc setIAMRole(ctx context.Context, pctx *ParsingContext, l log.Logger, file *hclparse.File, includeFromChild *IncludeConfig) error {\n\t// Prefer the IAM Role CLI args if they were passed otherwise lazily evaluate the IamRoleOptions using the config.\n\tif pctx.OriginalIAMRoleOptions.RoleARN != \"\" {\n\t\tpctx.IAMRoleOptions = pctx.OriginalIAMRoleOptions\n\t} else {\n\t\t// as key is considered HCL code and include configuration\n\t\tvar (\n\t\t\tkey           = fmt.Sprintf(\"%v-%v\", file.Content(), includeFromChild)\n\t\t\tconfig, found = iamRoleCache.Get(ctx, key)\n\t\t)\n\n\t\tif !found {\n\t\t\tiamConfig, err := TerragruntConfigFromPartialConfig(ctx, pctx.WithDecodeList(TerragruntFlags), l, file, includeFromChild)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tconfig = iamConfig.GetIAMRoleOptions()\n\t\t\tiamRoleCache.Put(ctx, key, config)\n\t\t}\n\t\t// We merge the OriginalIAMRoleOptions into the one from the config, because the CLI passed IAMRoleOptions has\n\t\t// precedence.\n\t\tmerged := iam.MergeRoleOptions(\n\t\t\tconfig,\n\t\t\tpctx.OriginalIAMRoleOptions,\n\t\t)\n\t\tpctx.IAMRoleOptions = merged\n\t}\n\n\treturn nil\n}\n\nfunc decodeAsTerragruntConfigFile(pctx *ParsingContext, l log.Logger, file *hclparse.File, evalContext *hcl.EvalContext) (*terragruntConfigFile, error) {\n\tterragruntConfig := terragruntConfigFile{}\n\n\tif err := file.Decode(&terragruntConfig, evalContext); err != nil {\n\t\tvar diagErr hcl.Diagnostics\n\n\t\tok := errors.As(err, &diagErr)\n\n\t\t// in case of render-json command and inputs reference error, we update the inputs with default value\n\t\tif (!ok || !isRenderJSONCommand(pctx) || !isAttributeAccessError(diagErr)) &&\n\t\t\t(!ok || !isRenderCommand(pctx) || !isAttributeAccessError(diagErr)) {\n\t\t\treturn &terragruntConfig, err\n\t\t}\n\n\t\tl.Warnf(\"Failed to decode inputs %v\", diagErr)\n\t}\n\n\tif terragruntConfig.Inputs != nil {\n\t\tinputs, err := ctyhelper.UpdateUnknownCtyValValues(*terragruntConfig.Inputs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tterragruntConfig.Inputs = &inputs\n\t}\n\n\treturn &terragruntConfig, nil\n}\n\n// Returns the index of the Hook with the given name,\n// or -1 if no Hook have the given name.\nfunc getIndexOfHookWithName(hooks []Hook, name string) int {\n\tfor i, hook := range hooks {\n\t\tif hook.Name == name {\n\t\t\treturn i\n\t\t}\n\t}\n\n\treturn -1\n}\n\n// isAttributeAccessError returns true if the given diagnostics indicate an error accessing an attribute\nfunc isAttributeAccessError(diagnostics hcl.Diagnostics) bool {\n\tfor _, diagnostic := range diagnostics {\n\t\tif diagnostic.Severity == hcl.DiagError && strings.Contains(diagnostic.Summary, \"Unsupported attribute\") {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// Returns the index of the ErrorHook with the given name,\n// or -1 if no Hook have the given name.\n// TODO: Figure out more DRY way to do this\nfunc getIndexOfErrorHookWithName(hooks []ErrorHook, name string) int {\n\tfor i, hook := range hooks {\n\t\tif hook.Name == name {\n\t\t\treturn i\n\t\t}\n\t}\n\n\treturn -1\n}\n\n// Returns the index of the extraArgs with the given name,\n// or -1 if no extraArgs have the given name.\nfunc getIndexOfExtraArgsWithName(extraArgs []TerraformExtraArguments, name string) int {\n\tfor i, extra := range extraArgs {\n\t\tif extra.Name == name {\n\t\t\treturn i\n\t\t}\n\t}\n\n\treturn -1\n}\n\n// Convert the contents of a fully resolved Terragrunt configuration to a TerragruntConfig object\nfunc convertToTerragruntConfig(ctx context.Context, pctx *ParsingContext, configPath string, terragruntConfigFromFile *terragruntConfigFile) (cfg *TerragruntConfig, err error) {\n\terrs := &errors.MultiError{}\n\n\tif pctx.ConvertToTerragruntConfigFunc != nil {\n\t\treturn pctx.ConvertToTerragruntConfigFunc(ctx, pctx, configPath, terragruntConfigFromFile)\n\t}\n\n\tterragruntConfig := &TerragruntConfig{\n\t\tIsPartial: false,\n\t\t// Initialize GenerateConfigs so we can append to it\n\t\tGenerateConfigs: map[string]codegen.GenerateConfig{},\n\t}\n\n\tdefaultMetadata := map[string]any{FoundInFile: configPath}\n\n\tif terragruntConfigFromFile.RemoteState != nil {\n\t\tconfig, err := terragruntConfigFromFile.RemoteState.Config()\n\t\tif err != nil {\n\t\t\terrs = errs.Append(err)\n\t\t}\n\n\t\tterragruntConfig.RemoteState = remotestate.New(config)\n\t\tterragruntConfig.SetFieldMetadata(MetadataRemoteState, defaultMetadata)\n\t}\n\n\tif terragruntConfigFromFile.RemoteStateAttr != nil {\n\t\tremoteStateMap, err := ctyhelper.ParseCtyValueToMap(*terragruntConfigFromFile.RemoteStateAttr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tvar config *remotestate.Config\n\t\tif err := mapstructure.WeakDecode(remoteStateMap, &config); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tterragruntConfig.RemoteState = remotestate.New(config)\n\t\tterragruntConfig.SetFieldMetadata(MetadataRemoteState, defaultMetadata)\n\t}\n\n\tif err := terragruntConfigFromFile.Terraform.ValidateHooks(); err != nil {\n\t\terrs = errs.Append(err)\n\t}\n\n\tterragruntConfig.Terraform = terragruntConfigFromFile.Terraform\n\tif terragruntConfig.Terraform != nil { // since Terraform is nil each time avoid saving metadata when it is nil\n\t\tterragruntConfig.SetFieldMetadata(MetadataTerraform, defaultMetadata)\n\t}\n\n\tif err := validateDependencies(pctx, terragruntConfigFromFile.Dependencies); err != nil {\n\t\terrs = errs.Append(err)\n\t}\n\n\tterragruntConfig.Dependencies = terragruntConfigFromFile.Dependencies\n\tif terragruntConfig.Dependencies != nil {\n\t\tfor _, item := range terragruntConfig.Dependencies.Paths {\n\t\t\tterragruntConfig.SetFieldMetadataWithType(MetadataDependencies, item, defaultMetadata)\n\t\t}\n\t}\n\n\tterragruntConfig.TerragruntDependencies = terragruntConfigFromFile.TerragruntDependencies\n\tfor _, dep := range terragruntConfig.TerragruntDependencies {\n\t\tterragruntConfig.SetFieldMetadataWithType(MetadataDependency, dep.Name, defaultMetadata)\n\t}\n\n\tif terragruntConfigFromFile.TerraformBinary != nil {\n\t\tterragruntConfig.TerraformBinary = *terragruntConfigFromFile.TerraformBinary\n\t\tterragruntConfig.SetFieldMetadata(MetadataTerraformBinary, defaultMetadata)\n\t}\n\n\tif terragruntConfigFromFile.DownloadDir != nil {\n\t\tterragruntConfig.DownloadDir = *terragruntConfigFromFile.DownloadDir\n\t\tterragruntConfig.SetFieldMetadata(MetadataDownloadDir, defaultMetadata)\n\t}\n\n\tif terragruntConfigFromFile.TerraformVersionConstraint != nil {\n\t\tterragruntConfig.TerraformVersionConstraint = *terragruntConfigFromFile.TerraformVersionConstraint\n\t\tterragruntConfig.SetFieldMetadata(MetadataTerraformVersionConstraint, defaultMetadata)\n\t}\n\n\tif terragruntConfigFromFile.TerragruntVersionConstraint != nil {\n\t\tterragruntConfig.TerragruntVersionConstraint = *terragruntConfigFromFile.TerragruntVersionConstraint\n\t\tterragruntConfig.SetFieldMetadata(MetadataTerragruntVersionConstraint, defaultMetadata)\n\t}\n\n\tif terragruntConfigFromFile.PreventDestroy != nil {\n\t\tterragruntConfig.PreventDestroy = terragruntConfigFromFile.PreventDestroy\n\t\tterragruntConfig.SetFieldMetadata(MetadataPreventDestroy, defaultMetadata)\n\t}\n\n\tif terragruntConfigFromFile.IamRole != nil {\n\t\tterragruntConfig.IamRole = *terragruntConfigFromFile.IamRole\n\t\tterragruntConfig.SetFieldMetadata(MetadataIamRole, defaultMetadata)\n\t}\n\n\tif terragruntConfigFromFile.IamAssumeRoleDuration != nil {\n\t\tterragruntConfig.IamAssumeRoleDuration = terragruntConfigFromFile.IamAssumeRoleDuration\n\t\tterragruntConfig.SetFieldMetadata(MetadataIamAssumeRoleDuration, defaultMetadata)\n\t}\n\n\tif terragruntConfigFromFile.IamAssumeRoleSessionName != nil {\n\t\tterragruntConfig.IamAssumeRoleSessionName = *terragruntConfigFromFile.IamAssumeRoleSessionName\n\t\tterragruntConfig.SetFieldMetadata(MetadataIamAssumeRoleSessionName, defaultMetadata)\n\t}\n\n\tif terragruntConfigFromFile.IamWebIdentityToken != nil {\n\t\tterragruntConfig.IamWebIdentityToken = *terragruntConfigFromFile.IamWebIdentityToken\n\t\tterragruntConfig.SetFieldMetadata(MetadataIamWebIdentityToken, defaultMetadata)\n\t}\n\n\tif terragruntConfigFromFile.Engine != nil {\n\t\tterragruntConfig.Engine = terragruntConfigFromFile.Engine\n\t\tterragruntConfig.SetFieldMetadata(MetadataEngine, defaultMetadata)\n\t}\n\n\tif terragruntConfigFromFile.FeatureFlags != nil {\n\t\tterragruntConfig.FeatureFlags = terragruntConfigFromFile.FeatureFlags\n\t\tfor _, flag := range terragruntConfig.FeatureFlags {\n\t\t\tterragruntConfig.SetFieldMetadataWithType(MetadataFeatureFlag, flag.Name, defaultMetadata)\n\t\t}\n\t}\n\n\tif terragruntConfigFromFile.Exclude != nil {\n\t\tterragruntConfig.Exclude = terragruntConfigFromFile.Exclude\n\t\tterragruntConfig.SetFieldMetadata(MetadataExclude, defaultMetadata)\n\t}\n\n\tif terragruntConfigFromFile.Errors != nil {\n\t\tterragruntConfig.Errors = terragruntConfigFromFile.Errors\n\t\tterragruntConfig.SetFieldMetadata(MetadataErrors, defaultMetadata)\n\t}\n\n\tgenerateBlocks := []terragruntGenerateBlock{}\n\tgenerateBlocks = append(generateBlocks, terragruntConfigFromFile.GenerateBlocks...)\n\n\tif terragruntConfigFromFile.GenerateAttrs != nil {\n\t\tgenerateMap, err := ctyhelper.ParseCtyValueToMap(*terragruntConfigFromFile.GenerateAttrs)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfor name, block := range generateMap {\n\t\t\tvar generateBlock terragruntGenerateBlock\n\t\t\tif err := mapstructure.WeakDecode(block, &generateBlock); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tgenerateBlock.Name = name\n\t\t\tgenerateBlocks = append(generateBlocks, generateBlock)\n\t\t}\n\t}\n\n\tif err := validateGenerateBlocks(&generateBlocks); err != nil {\n\t\terrs = errs.Append(err)\n\t}\n\n\tfor _, block := range generateBlocks {\n\t\t// Validate that if_exists is provided (required attribute)\n\t\tif block.IfExists == \"\" {\n\t\t\terrs = errs.Append(errors.Errorf(\"generate block %q is missing required attribute \\\"if_exists\\\"\", block.Name))\n\t\t\tcontinue\n\t\t}\n\n\t\tifExists, err := codegen.GenerateConfigExistsFromString(block.IfExists)\n\t\tif err != nil {\n\t\t\terrs = errs.Append(errors.Errorf(\"generate block %q: %w\", block.Name, err))\n\t\t\tcontinue\n\t\t}\n\n\t\tif block.IfDisabled == nil {\n\t\t\tblock.IfDisabled = &DefaultGenerateBlockIfDisabledValueStr\n\t\t}\n\n\t\tifDisabled, err := codegen.GenerateConfigDisabledFromString(*block.IfDisabled)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tgenConfig := codegen.GenerateConfig{\n\t\t\tPath:          block.Path,\n\t\t\tIfExists:      ifExists,\n\t\t\tIfExistsStr:   block.IfExists,\n\t\t\tIfDisabled:    ifDisabled,\n\t\t\tIfDisabledStr: *block.IfDisabled,\n\t\t\tContents:      block.Contents,\n\t\t}\n\t\tif block.CommentPrefix == nil {\n\t\t\tgenConfig.CommentPrefix = codegen.DefaultCommentPrefix\n\t\t} else {\n\t\t\tgenConfig.CommentPrefix = *block.CommentPrefix\n\t\t}\n\n\t\tif block.DisableSignature == nil {\n\t\t\tgenConfig.DisableSignature = false\n\t\t} else {\n\t\t\tgenConfig.DisableSignature = *block.DisableSignature\n\t\t}\n\n\t\tif block.Disable == nil {\n\t\t\tgenConfig.Disable = false\n\t\t} else {\n\t\t\tgenConfig.Disable = *block.Disable\n\t\t}\n\n\t\tterragruntConfig.GenerateConfigs[block.Name] = genConfig\n\t\tterragruntConfig.SetFieldMetadataWithType(MetadataGenerateConfigs, block.Name, defaultMetadata)\n\t}\n\n\tif terragruntConfigFromFile.Inputs != nil {\n\t\tinputs, err := ctyhelper.ParseCtyValueToMap(*terragruntConfigFromFile.Inputs)\n\t\tif err != nil {\n\t\t\terrs = errs.Append(err)\n\t\t}\n\n\t\tterragruntConfig.Inputs = inputs\n\t\tterragruntConfig.SetFieldMetadataMap(MetadataInputs, terragruntConfig.Inputs, defaultMetadata)\n\t}\n\n\tif pctx.Locals != nil && *pctx.Locals != cty.NilVal {\n\t\tlocalsParsed, err := ctyhelper.ParseCtyValueToMap(*pctx.Locals)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// Only set Locals if there are actual values to avoid setting an empty map\n\t\tif len(localsParsed) > 0 {\n\t\t\tterragruntConfig.Locals = localsParsed\n\t\t\tterragruntConfig.SetFieldMetadataMap(MetadataLocals, localsParsed, defaultMetadata)\n\t\t}\n\t}\n\n\treturn terragruntConfig, errs.ErrorOrNil()\n}\n\n// Iterate over dependencies paths and check if directories exists, return error with all missing dependencies\nfunc validateDependencies(ctx *ParsingContext, dependencies *ModuleDependencies) error {\n\tvar missingDependencies []string\n\n\tif dependencies == nil {\n\t\treturn nil\n\t}\n\n\tfor _, dependencyPath := range dependencies.Paths {\n\t\tfullPath := filepath.FromSlash(dependencyPath)\n\t\tif !filepath.IsAbs(fullPath) {\n\t\t\tfullPath = path.Join(ctx.WorkingDir, fullPath)\n\t\t}\n\n\t\tif !util.IsDir(fullPath) {\n\t\t\tmissingDependencies = append(missingDependencies, fmt.Sprintf(\"%s (%s)\", dependencyPath, fullPath))\n\t\t}\n\t}\n\n\tif len(missingDependencies) > 0 {\n\t\treturn DependencyDirNotFoundError{missingDependencies}\n\t}\n\n\treturn nil\n}\n\n// Iterate over generate blocks and detect duplicate names, return error with list of duplicated names\nfunc validateGenerateBlocks(blocks *[]terragruntGenerateBlock) error {\n\tvar (\n\t\tblockNames                   = map[string]bool{}\n\t\tduplicatedGenerateBlockNames []string\n\t)\n\n\tfor _, block := range *blocks {\n\t\t_, found := blockNames[block.Name]\n\t\tif found {\n\t\t\tduplicatedGenerateBlockNames = append(duplicatedGenerateBlockNames, block.Name)\n\t\t\tcontinue\n\t\t}\n\n\t\tblockNames[block.Name] = true\n\t}\n\n\tif len(duplicatedGenerateBlockNames) != 0 {\n\t\treturn DuplicatedGenerateBlocksError{duplicatedGenerateBlockNames}\n\t}\n\n\treturn nil\n}\n\n// configFileHasDependencyBlock statically checks the terrragrunt config file at the given path and checks if it has any\n// dependency or dependencies blocks defined. Note that this does not do any decoding of the blocks, as it is only meant\n// to check for block presence.\nfunc configFileHasDependencyBlock(configPath string) (bool, error) {\n\tconfigBytes, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn false, DependencyFileNotFoundError{Path: configPath}\n\t\t}\n\n\t\treturn false, errors.New(err)\n\t}\n\n\t// We use hclwrite to parse the config instead of the normal parser because the normal parser doesn't give us an AST\n\t// that we can walk and scan, and requires structured data to map against. This makes the parsing strict, so to\n\t// avoid weird parsing errors due to missing dependency data, we do a structural scan here.\n\thclFile, diags := hclwrite.ParseConfig(configBytes, configPath, hcl.InitialPos)\n\tif diags.HasErrors() {\n\t\treturn false, errors.New(diags)\n\t}\n\n\tfor _, block := range hclFile.Body().Blocks() {\n\t\tif block.Type() == \"dependency\" || block.Type() == \"dependencies\" {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\treturn false, nil\n}\n\n// SetFieldMetadataWithType set metadata on the given field name grouped by type.\n// Example usage - setting metadata on different dependencies, locals, inputs.\nfunc (cfg *TerragruntConfig) SetFieldMetadataWithType(fieldType, fieldName string, m map[string]any) {\n\tif cfg.FieldsMetadata == nil {\n\t\tcfg.FieldsMetadata = map[string]map[string]any{}\n\t}\n\n\tfield := fmt.Sprintf(\"%s-%s\", fieldType, fieldName)\n\n\tmetadata, found := cfg.FieldsMetadata[field]\n\tif !found {\n\t\tmetadata = make(map[string]any)\n\t}\n\n\tmaps.Copy(metadata, m)\n\n\tcfg.FieldsMetadata[field] = metadata\n}\n\n// SetFieldMetadata set metadata on the given field name.\nfunc (cfg *TerragruntConfig) SetFieldMetadata(fieldName string, m map[string]any) {\n\tcfg.SetFieldMetadataWithType(fieldName, fieldName, m)\n}\n\n// SetFieldMetadataMap set metadata on fields from map keys.\n// Example usage - setting metadata on all variables from inputs.\nfunc (cfg *TerragruntConfig) SetFieldMetadataMap(field string, data map[string]any, metadata map[string]any) {\n\tfor name := range data {\n\t\tcfg.SetFieldMetadataWithType(field, name, metadata)\n\t}\n}\n\n// GetFieldMetadata return field metadata by field name.\nfunc (cfg *TerragruntConfig) GetFieldMetadata(fieldName string) (map[string]string, bool) {\n\treturn cfg.GetMapFieldMetadata(fieldName, fieldName)\n}\n\n// GetMapFieldMetadata return field metadata by field type and name.\nfunc (cfg *TerragruntConfig) GetMapFieldMetadata(fieldType, fieldName string) (map[string]string, bool) {\n\tif cfg.FieldsMetadata == nil {\n\t\treturn nil, false\n\t}\n\n\tfield := fmt.Sprintf(\"%s-%s\", fieldType, fieldName)\n\n\tvalue, found := cfg.FieldsMetadata[field]\n\tif !found {\n\t\treturn nil, false\n\t}\n\n\tresult := make(map[string]string)\n\tfor key, value := range value {\n\t\tresult[key] = fmt.Sprintf(\"%v\", value)\n\t}\n\n\treturn result, found\n}\n\n// EngineOptions fetch engine options\nfunc (cfg *TerragruntConfig) EngineOptions() (*engine.EngineConfig, error) {\n\tif cfg.Engine == nil {\n\t\treturn nil, nil\n\t}\n\t// in case of Meta is null, set empty meta\n\tmeta := map[string]any{}\n\n\tif cfg.Engine.Meta != nil {\n\t\tparsedMeta, err := ctyhelper.ParseCtyValueToMap(*cfg.Engine.Meta)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tmeta = parsedMeta\n\t}\n\n\tvar version, engineType string\n\tif cfg.Engine.Version != nil {\n\t\tversion = *cfg.Engine.Version\n\t}\n\n\tif cfg.Engine.Type != nil {\n\t\tengineType = *cfg.Engine.Type\n\t}\n\t// if type is null of empty, set to \"rpc\"\n\tif len(engineType) == 0 {\n\t\tengineType = DefaultEngineType\n\t}\n\n\treturn &engine.EngineConfig{\n\t\tSource:  cfg.Engine.Source,\n\t\tVersion: version,\n\t\tType:    engineType,\n\t\tMeta:    meta,\n\t}, nil\n}\n\n// ErrorsConfig fetch errors configuration for options package\nfunc (cfg *TerragruntConfig) ErrorsConfig() (*errorconfig.Config, error) {\n\tif cfg.Errors == nil {\n\t\treturn nil, nil\n\t}\n\n\tresult := &errorconfig.Config{\n\t\tRetry:  make(map[string]*errorconfig.RetryConfig),\n\t\tIgnore: make(map[string]*errorconfig.IgnoreConfig),\n\t}\n\n\tfor _, retryBlock := range cfg.Errors.Retry {\n\t\tif retryBlock == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Validate retry settings\n\t\tif retryBlock.MaxAttempts < 1 {\n\t\t\treturn nil, errors.Errorf(\"cannot have less than 1 max retry in errors.retry %q, but you specified %d\", retryBlock.Label, retryBlock.MaxAttempts)\n\t\t}\n\n\t\tif retryBlock.SleepIntervalSec < 0 {\n\t\t\treturn nil, errors.Errorf(\"cannot sleep for less than 0 seconds in errors.retry %q, but you specified %d\", retryBlock.Label, retryBlock.SleepIntervalSec)\n\t\t}\n\n\t\tcompiledPatterns := make([]*errorconfig.Pattern, 0, len(retryBlock.RetryableErrors))\n\n\t\tfor _, pattern := range retryBlock.RetryableErrors {\n\t\t\tvalue, err := errorsPattern(pattern)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Errorf(\"invalid retry pattern %q in block %q: %w\",\n\t\t\t\t\tpattern, retryBlock.Label, err)\n\t\t\t}\n\n\t\t\tcompiledPatterns = append(compiledPatterns, value)\n\t\t}\n\n\t\tresult.Retry[retryBlock.Label] = &errorconfig.RetryConfig{\n\t\t\tName:             retryBlock.Label,\n\t\t\tRetryableErrors:  compiledPatterns,\n\t\t\tMaxAttempts:      retryBlock.MaxAttempts,\n\t\t\tSleepIntervalSec: retryBlock.SleepIntervalSec,\n\t\t}\n\t}\n\n\tfor _, ignoreBlock := range cfg.Errors.Ignore {\n\t\tif ignoreBlock == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar signals map[string]any\n\n\t\tif ignoreBlock.Signals != nil {\n\t\t\tvalue, err := ConvertValuesMapToCtyVal(ignoreBlock.Signals)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tsignals, err = ctyhelper.ParseCtyValueToMap(value)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tcompiledPatterns := make([]*errorconfig.Pattern, 0, len(ignoreBlock.IgnorableErrors))\n\n\t\tfor _, pattern := range ignoreBlock.IgnorableErrors {\n\t\t\tvalue, err := errorsPattern(pattern)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.Errorf(\"invalid ignore pattern %q in block %q: %w\",\n\t\t\t\t\tpattern, ignoreBlock.Label, err)\n\t\t\t}\n\n\t\t\tcompiledPatterns = append(compiledPatterns, value)\n\t\t}\n\n\t\tresult.Ignore[ignoreBlock.Label] = &errorconfig.IgnoreConfig{\n\t\t\tName:            ignoreBlock.Label,\n\t\t\tIgnorableErrors: compiledPatterns,\n\t\t\tMessage:         ignoreBlock.Message,\n\t\t\tSignals:         signals,\n\t\t}\n\t}\n\n\treturn result, nil\n}\n\n// Build ErrorsPattern from string\nfunc errorsPattern(pattern string) (*errorconfig.Pattern, error) {\n\tisNegative := false\n\tp := pattern\n\n\tif len(p) > 0 && p[0] == '!' {\n\t\tisNegative = true\n\t\tp = p[1:]\n\t}\n\n\tcompiled, err := regexp.Compile(p)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &errorconfig.Pattern{\n\t\tPattern:  compiled,\n\t\tNegative: isNegative,\n\t}, nil\n}\n\n// ParseRemoteState reads the Terragrunt config file from its default location\n// and parses and returns the `remote_state` block.\n// The caller provides a fully populated ParsingContext (typically via configbridge.NewParsingContext).\nfunc ParseRemoteState(ctx context.Context, l log.Logger, pctx *ParsingContext) (*remotestate.RemoteState, error) {\n\tcfg, err := ReadTerragruntConfig(ctx, l, pctx, pctx.ParserOptions)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn cfg.GetRemoteState(l, pctx)\n}\n"
  },
  {
    "path": "pkg/config/config_as_cty.go",
    "content": "package config\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/zclconf/go-cty/cty\"\n\t\"github.com/zclconf/go-cty/cty/gocty\"\n\tctyjson \"github.com/zclconf/go-cty/cty/json\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate\"\n)\n\n// TerragruntConfigAsCty serializes TerragruntConfig struct to a cty Value that can be used to reference the attributes in other config. Note\n// that we can't straight up convert the struct using cty tags due to differences in the desired representation.\n// Specifically, we want to reference blocks by named attributes, but blocks are rendered to lists in the\n// TerragruntConfig struct, so we need to do some massaging of the data to convert the list of blocks in to a map going\n// from the block name label to the block value.\nfunc TerragruntConfigAsCty(config *TerragruntConfig) (cty.Value, error) {\n\toutput := map[string]cty.Value{}\n\n\t// Convert attributes that are primitive types\n\toutput[MetadataTerraformBinary] = gostringToCty(config.TerraformBinary)\n\toutput[MetadataTerraformVersionConstraint] = gostringToCty(config.TerraformVersionConstraint)\n\toutput[MetadataTerragruntVersionConstraint] = gostringToCty(config.TerragruntVersionConstraint)\n\toutput[MetadataDownloadDir] = gostringToCty(config.DownloadDir)\n\toutput[MetadataIamRole] = gostringToCty(config.IamRole)\n\toutput[MetadataIamAssumeRoleSessionName] = gostringToCty(config.IamAssumeRoleSessionName)\n\toutput[MetadataIamWebIdentityToken] = gostringToCty(config.IamWebIdentityToken)\n\n\tcatalogConfigCty, err := catalogConfigAsCty(config.Catalog)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif catalogConfigCty != cty.NilVal {\n\t\toutput[MetadataCatalog] = catalogConfigCty\n\t}\n\n\tengineConfigCty, err := engineConfigAsCty(config.Engine)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif engineConfigCty != cty.NilVal {\n\t\toutput[MetadataEngine] = engineConfigCty\n\t}\n\n\texcludeConfigCty, err := excludeConfigAsCty(config.Exclude)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif excludeConfigCty != cty.NilVal {\n\t\toutput[MetadataExclude] = excludeConfigCty\n\t}\n\n\terrorsConfigCty, err := errorsConfigAsCty(config.Errors)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif errorsConfigCty != cty.NilVal {\n\t\toutput[MetadataErrors] = errorsConfigCty\n\t}\n\n\tterraformConfigCty, err := terraformConfigAsCty(config.Terraform)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif terraformConfigCty != cty.NilVal {\n\t\toutput[MetadataTerraform] = terraformConfigCty\n\t}\n\n\tremoteStateCty, err := RemoteStateAsCty(config.RemoteState)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif remoteStateCty != cty.NilVal {\n\t\toutput[MetadataRemoteState] = remoteStateCty\n\t}\n\n\tdependenciesCty, err := GoTypeToCty(config.Dependencies)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif dependenciesCty != cty.NilVal {\n\t\toutput[MetadataDependencies] = dependenciesCty\n\t}\n\n\tif config.PreventDestroy != nil {\n\t\toutput[MetadataPreventDestroy] = goboolToCty(*config.PreventDestroy)\n\t}\n\n\tdependencyCty, err := dependencyBlocksAsCty(config.TerragruntDependencies)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif dependencyCty != cty.NilVal {\n\t\toutput[MetadataDependency] = dependencyCty\n\t}\n\n\tgenerateCty, err := GoTypeToCty(config.GenerateConfigs)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif generateCty != cty.NilVal {\n\t\toutput[MetadataGenerateConfigs] = generateCty\n\t}\n\n\tiamAssumeRoleDurationCty, err := GoTypeToCty(config.IamAssumeRoleDuration)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif iamAssumeRoleDurationCty != cty.NilVal {\n\t\toutput[MetadataIamAssumeRoleDuration] = iamAssumeRoleDurationCty\n\t}\n\n\tinputsCty, err := convertToCtyWithJSON(config.Inputs)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif inputsCty != cty.NilVal {\n\t\toutput[MetadataInputs] = inputsCty\n\t}\n\n\tlocalsCty, err := convertToCtyWithJSON(config.Locals)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif localsCty != cty.NilVal {\n\t\toutput[MetadataLocals] = localsCty\n\t}\n\n\tfeatureFlagsCty, err := featureFlagsBlocksAsCty(config.FeatureFlags)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif featureFlagsCty != cty.NilVal {\n\t\toutput[MetadataFeatureFlag] = featureFlagsCty\n\t}\n\n\treturn ConvertValuesMapToCtyVal(output)\n}\n\nfunc TerragruntConfigAsCtyWithMetadata(config *TerragruntConfig) (cty.Value, error) {\n\toutput := map[string]cty.Value{}\n\n\t// Convert attributes that are primitive types\n\tif err := wrapWithMetadata(config, config.TerraformBinary, MetadataTerraformBinary, &output); err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif err := wrapWithMetadata(config, config.TerraformVersionConstraint, MetadataTerraformVersionConstraint, &output); err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif err := wrapWithMetadata(config, config.TerragruntVersionConstraint, MetadataTerragruntVersionConstraint, &output); err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif err := wrapWithMetadata(config, config.DownloadDir, MetadataDownloadDir, &output); err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif err := wrapWithMetadata(config, config.IamRole, MetadataIamRole, &output); err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif err := wrapWithMetadata(config, config.IamAssumeRoleSessionName, MetadataIamAssumeRoleSessionName, &output); err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif config.PreventDestroy != nil {\n\t\tif err := wrapWithMetadata(config, *config.PreventDestroy, MetadataPreventDestroy, &output); err != nil {\n\t\t\treturn cty.NilVal, err\n\t\t}\n\t}\n\n\tif err := wrapWithMetadata(config, config.IamAssumeRoleDuration, MetadataIamAssumeRoleDuration, &output); err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tterraformConfigCty, err := terraformConfigAsCty(config.Terraform)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif terraformConfigCty != cty.NilVal {\n\t\tif err := wrapWithMetadata(config, terraformConfigCty, MetadataTerraform, &output); err != nil {\n\t\t\treturn cty.NilVal, err\n\t\t}\n\t}\n\n\t// Remote state\n\tremoteStateCty, err := RemoteStateAsCty(config.RemoteState)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif remoteStateCty != cty.NilVal {\n\t\tif err := wrapWithMetadata(config, remoteStateCty, MetadataRemoteState, &output); err != nil {\n\t\t\treturn cty.NilVal, err\n\t\t}\n\t}\n\n\tif err := wrapCtyMapWithMetadata(config, &config.Inputs, MetadataInputs, &output); err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif err := wrapCtyMapWithMetadata(config, &config.Locals, MetadataLocals, &output); err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\t// remder dependencies as list of maps with \"value\" and \"metadata\"\n\tif config.Dependencies != nil {\n\t\tvar dependencyWithMetadata = make([]ValueWithMetadata, 0, len(config.Dependencies.Paths))\n\n\t\tfor _, dependency := range config.Dependencies.Paths {\n\t\t\tvar content = ValueWithMetadata{}\n\n\t\t\tcontent.Value = gostringToCty(dependency)\n\n\t\t\tmetadata, found := config.GetMapFieldMetadata(MetadataDependencies, dependency)\n\t\t\tif found {\n\t\t\t\tcontent.Metadata = metadata\n\t\t\t}\n\n\t\t\tdependencyWithMetadata = append(dependencyWithMetadata, content)\n\t\t}\n\n\t\tdependenciesCty, err := GoTypeToCty(dependencyWithMetadata)\n\t\tif err != nil {\n\t\t\treturn cty.NilVal, err\n\t\t}\n\n\t\toutput[MetadataDependencies] = dependenciesCty\n\t}\n\n\tif config.TerragruntDependencies != nil {\n\t\tvar dependenciesMap = map[string]cty.Value{}\n\n\t\tfor _, block := range config.TerragruntDependencies {\n\t\t\tctyValue, err := GoTypeToCty(block)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif ctyValue == cty.NilVal {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar content = ValueWithMetadata{}\n\n\t\t\tcontent.Value = ctyValue\n\n\t\t\tmetadata, found := config.GetMapFieldMetadata(MetadataDependency, block.Name)\n\t\t\tif found {\n\t\t\t\tcontent.Metadata = metadata\n\t\t\t}\n\n\t\t\tvalue, err := GoTypeToCty(content)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdependenciesMap[block.Name] = value\n\t\t}\n\n\t\tif len(dependenciesMap) > 0 {\n\t\t\tdependenciesCty, err := ConvertValuesMapToCtyVal(dependenciesMap)\n\t\t\tif err != nil {\n\t\t\t\treturn cty.NilVal, err\n\t\t\t}\n\n\t\t\toutput[MetadataDependency] = dependenciesCty\n\t\t}\n\t}\n\n\tif config.GenerateConfigs != nil {\n\t\tvar generateConfigsWithMetadata = map[string]cty.Value{}\n\n\t\tfor key, value := range config.GenerateConfigs {\n\t\t\tctyValue, err := GoTypeToCty(value)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif ctyValue == cty.NilVal {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar content = ValueWithMetadata{}\n\n\t\t\tcontent.Value = ctyValue\n\n\t\t\tmetadata, found := config.GetMapFieldMetadata(MetadataGenerateConfigs, key)\n\t\t\tif found {\n\t\t\t\tcontent.Metadata = metadata\n\t\t\t}\n\n\t\t\tv, err := GoTypeToCty(content)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tgenerateConfigsWithMetadata[key] = v\n\t\t}\n\n\t\tif len(generateConfigsWithMetadata) > 0 {\n\t\t\tdependenciesCty, err := ConvertValuesMapToCtyVal(generateConfigsWithMetadata)\n\t\t\tif err != nil {\n\t\t\t\treturn cty.NilVal, err\n\t\t\t}\n\n\t\t\toutput[MetadataGenerateConfigs] = dependenciesCty\n\t\t}\n\t}\n\n\treturn ConvertValuesMapToCtyVal(output)\n}\n\nfunc wrapCtyMapWithMetadata(config *TerragruntConfig, data *map[string]any, fieldType string, output *map[string]cty.Value) error {\n\tvar valueWithMetadata = map[string]cty.Value{}\n\n\tfor key, value := range *data {\n\t\tvar content = ValueWithMetadata{}\n\n\t\tctyValue, err := convertToCtyWithJSON(value)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcontent.Value = ctyValue\n\n\t\tmetadata, found := config.GetMapFieldMetadata(fieldType, key)\n\t\tif found {\n\t\t\tcontent.Metadata = metadata\n\t\t}\n\n\t\tv, err := GoTypeToCty(content)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tvalueWithMetadata[key] = v\n\t}\n\n\tif len(valueWithMetadata) > 0 {\n\t\tlocalsCty, err := ConvertValuesMapToCtyVal(valueWithMetadata)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t(*output)[fieldType] = localsCty\n\t}\n\n\treturn nil\n}\n\nfunc wrapWithMetadata(config *TerragruntConfig, value any, metadataName string, output *map[string]cty.Value) error {\n\tif value == nil {\n\t\treturn nil\n\t}\n\n\tvar valueWithMetadata = ValueWithMetadata{}\n\n\tctyValue, err := GoTypeToCty(value)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvalueWithMetadata.Value = ctyValue\n\n\tmetadata, found := config.GetFieldMetadata(metadataName)\n\tif found {\n\t\tvalueWithMetadata.Metadata = metadata\n\t}\n\n\tctyJSON, err := GoTypeToCty(valueWithMetadata)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif ctyJSON != cty.NilVal {\n\t\t(*output)[metadataName] = ctyJSON\n\t}\n\n\treturn nil\n}\n\n// ValueWithMetadata stores value and metadata used in render-json with metadata.\ntype ValueWithMetadata struct {\n\tValue    cty.Value         `json:\"value\" cty:\"value\"`\n\tMetadata map[string]string `json:\"metadata\" cty:\"metadata\"`\n}\n\n// ctyCatalogConfig is an alternate representation of CatalogConfig that converts internal blocks into a map that\n// maps the name to the underlying struct, as opposed to a list representation.\ntype ctyCatalogConfig struct {\n\tURLs []string `cty:\"urls\"`\n}\n\n// ctyEngineConfig is an alternate representation of EngineConfig that converts internal blocks into a map that\n// maps the name to the underlying struct, as opposed to a list representation.\ntype ctyEngineConfig struct {\n\tMeta    cty.Value `cty:\"meta\"`\n\tSource  string    `cty:\"source\"`\n\tVersion string    `cty:\"version\"`\n\tType    string    `cty:\"type\"`\n}\n\n// ctyExclude exclude representation for cty.\ntype ctyExclude struct {\n\tActions             []string `cty:\"actions\"`\n\tIf                  bool     `cty:\"if\"`\n\tExcludeDependencies bool     `cty:\"exclude_dependencies\"`\n}\n\n// Serialize CatalogConfig to a cty Value, but with maps instead of lists for the blocks.\nfunc catalogConfigAsCty(config *CatalogConfig) (cty.Value, error) {\n\tif config == nil {\n\t\treturn cty.NilVal, nil\n\t}\n\n\tconfigCty := ctyCatalogConfig{\n\t\tURLs: config.URLs,\n\t}\n\n\treturn GoTypeToCty(configCty)\n}\n\n// Serialize engineConfigAsCty to a cty Value, but with maps instead of lists for the blocks.\nfunc engineConfigAsCty(config *EngineConfig) (cty.Value, error) {\n\tif config == nil {\n\t\treturn cty.NilVal, nil\n\t}\n\n\tvar v, t string\n\tif config.Version != nil {\n\t\tv = *config.Version\n\t}\n\n\tif config.Type != nil {\n\t\tt = *config.Type\n\t}\n\n\tconfigCty := ctyEngineConfig{\n\t\tSource:  config.Source,\n\t\tVersion: v,\n\t\tType:    t,\n\t}\n\n\tif config.Meta != nil {\n\t\tconfigCty.Meta = *config.Meta\n\t}\n\n\treturn GoTypeToCty(configCty)\n}\n\n// excludeConfigAsCty serialize exclude configuration to a cty Value.\nfunc excludeConfigAsCty(config *ExcludeConfig) (cty.Value, error) {\n\tif config == nil {\n\t\treturn cty.NilVal, nil\n\t}\n\n\texcludeDependencies := false\n\tif config.ExcludeDependencies != nil {\n\t\texcludeDependencies = *config.ExcludeDependencies\n\t}\n\n\tconfigCty := ctyExclude{\n\t\tIf:                  config.If,\n\t\tActions:             config.Actions,\n\t\tExcludeDependencies: excludeDependencies,\n\t}\n\n\treturn GoTypeToCty(configCty)\n}\n\n// CtyTerraformConfig is an alternate representation of TerraformConfig that converts internal blocks into a map that\n// maps the name to the underlying struct, as opposed to a list representation.\ntype CtyTerraformConfig struct {\n\tExtraArgs             map[string]TerraformExtraArguments `cty:\"extra_arguments\"`\n\tSource                *string                            `cty:\"source\"`\n\tIncludeInCopy         *[]string                          `cty:\"include_in_copy\"`\n\tExcludeFromCopy       *[]string                          `cty:\"exclude_from_copy\"`\n\tCopyTerraformLockFile *bool                              `cty:\"copy_terraform_lock_file\"`\n\tBeforeHooks           map[string]Hook                    `cty:\"before_hook\"`\n\tAfterHooks            map[string]Hook                    `cty:\"after_hook\"`\n\tErrorHooks            map[string]ErrorHook               `cty:\"error_hook\"`\n}\n\n// Serialize TerraformConfig to a cty Value, but with maps instead of lists for the blocks.\nfunc terraformConfigAsCty(config *TerraformConfig) (cty.Value, error) {\n\tif config == nil {\n\t\treturn cty.NilVal, nil\n\t}\n\n\tconfigCty := CtyTerraformConfig{\n\t\tSource:                config.Source,\n\t\tIncludeInCopy:         config.IncludeInCopy,\n\t\tExcludeFromCopy:       config.ExcludeFromCopy,\n\t\tCopyTerraformLockFile: config.CopyTerraformLockFile,\n\t\tExtraArgs:             map[string]TerraformExtraArguments{},\n\t\tBeforeHooks:           map[string]Hook{},\n\t\tAfterHooks:            map[string]Hook{},\n\t\tErrorHooks:            map[string]ErrorHook{},\n\t}\n\n\tfor _, arg := range config.ExtraArgs {\n\t\tconfigCty.ExtraArgs[arg.Name] = arg\n\t}\n\n\tfor _, hook := range config.BeforeHooks {\n\t\tconfigCty.BeforeHooks[hook.Name] = hook\n\t}\n\n\tfor _, hook := range config.AfterHooks {\n\t\tconfigCty.AfterHooks[hook.Name] = hook\n\t}\n\n\tfor _, errorHook := range config.ErrorHooks {\n\t\tconfigCty.ErrorHooks[errorHook.Name] = errorHook\n\t}\n\n\treturn GoTypeToCty(configCty)\n}\n\n// RemoteStateAsCty serializes RemoteState to a cty Value. We can't directly\n// serialize the struct because `config` and `encryption` are arbitrary\n// interfaces whose type we do not know, so we have to do a hack to go through json.\nfunc RemoteStateAsCty(remote *remotestate.RemoteState) (cty.Value, error) {\n\tif remote == nil || remote.Config == nil {\n\t\treturn cty.NilVal, nil\n\t}\n\n\tconfig := remote.Config\n\n\toutput := map[string]cty.Value{}\n\toutput[\"backend\"] = gostringToCty(config.BackendName)\n\toutput[\"disable_init\"] = goboolToCty(config.DisableInit)\n\toutput[\"disable_dependency_optimization\"] = goboolToCty(config.DisableDependencyOptimization)\n\n\tgenerateCty, err := GoTypeToCty(config.Generate)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\toutput[\"generate\"] = generateCty\n\n\tctyJSONVal, err := convertToCtyWithJSON(config.BackendConfig)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\toutput[\"config\"] = ctyJSONVal\n\n\tctyJSONVal, err = convertToCtyWithJSON(config.Encryption)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\toutput[\"encryption\"] = ctyJSONVal\n\n\treturn ConvertValuesMapToCtyVal(output)\n}\n\n// Serialize the list of dependency blocks to a cty Value as a map that maps the block names to the cty representation.\nfunc dependencyBlocksAsCty(dependencyBlocks Dependencies) (cty.Value, error) {\n\tout := map[string]cty.Value{}\n\n\tfor _, block := range dependencyBlocks {\n\t\tblockCty, err := GoTypeToCty(block)\n\t\tif err != nil {\n\t\t\treturn cty.NilVal, err\n\t\t}\n\n\t\tout[block.Name] = blockCty\n\t}\n\n\treturn ConvertValuesMapToCtyVal(out)\n}\n\n// Serialize the list of feature flags to a cty Value as a map that maps the feature names to the cty representation.\nfunc featureFlagsBlocksAsCty(featureFlagBlocks FeatureFlags) (cty.Value, error) {\n\tout := map[string]cty.Value{}\n\n\tfor _, feature := range featureFlagBlocks {\n\t\tfeatureCty, err := GoTypeToCty(feature)\n\t\tif err != nil {\n\t\t\treturn cty.NilVal, err\n\t\t}\n\n\t\tout[feature.Name] = featureCty\n\t}\n\n\treturn ConvertValuesMapToCtyVal(out)\n}\n\n// Serialize errors configuration as cty.Value.\nfunc errorsConfigAsCty(config *ErrorsConfig) (cty.Value, error) {\n\tif config == nil {\n\t\treturn cty.NilVal, nil\n\t}\n\n\toutput := map[string]cty.Value{}\n\n\tretryCty, err := GoTypeToCty(config.Retry)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif retryCty != cty.NilVal {\n\t\toutput[MetadataRetry] = retryCty\n\t}\n\n\tignoreCty, err := GoTypeToCty(config.Ignore)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tif ignoreCty != cty.NilVal {\n\t\toutput[MetadataIgnore] = ignoreCty\n\t}\n\n\treturn ConvertValuesMapToCtyVal(output)\n}\n\n// stackConfigAsCty converts a StackConfig into a cty Value so its attributes can be used in other configs.\nfunc stackConfigAsCty(stackConfig *StackConfig) (cty.Value, error) {\n\tif stackConfig == nil {\n\t\treturn cty.NilVal, nil\n\t}\n\n\toutput := map[string]cty.Value{}\n\n\tif stackConfig.Locals != nil {\n\t\tlocalsCty, err := convertToCtyWithJSON(stackConfig.Locals)\n\t\tif err != nil {\n\t\t\treturn cty.NilVal, err\n\t\t}\n\n\t\tif localsCty != cty.NilVal {\n\t\t\toutput[MetadataLocal] = localsCty\n\t\t}\n\t}\n\n\t// Process stacks as a map from stack name to stack config\n\tif len(stackConfig.Stacks) > 0 {\n\t\tstacksMap := make(map[string]cty.Value, len(stackConfig.Stacks))\n\n\t\tfor _, stack := range stackConfig.Stacks {\n\t\t\tstackCty, err := stackToCty(stack)\n\t\t\tif err != nil {\n\t\t\t\treturn cty.NilVal, err\n\t\t\t}\n\n\t\t\tif stackCty != cty.NilVal {\n\t\t\t\tstacksMap[stack.Name] = stackCty\n\t\t\t}\n\t\t}\n\n\t\tif len(stacksMap) > 0 {\n\t\t\tstacksCty, err := ConvertValuesMapToCtyVal(stacksMap)\n\t\t\tif err != nil {\n\t\t\t\treturn cty.NilVal, err\n\t\t\t}\n\n\t\t\toutput[MetadataStack] = stacksCty\n\t\t}\n\t}\n\n\t// Process units as a map from unit name to unit config\n\tif len(stackConfig.Units) > 0 {\n\t\tunitsMap := make(map[string]cty.Value, len(stackConfig.Units))\n\n\t\tfor _, unit := range stackConfig.Units {\n\t\t\tunitCty, err := unitToCty(unit)\n\t\t\tif err != nil {\n\t\t\t\treturn cty.NilVal, err\n\t\t\t}\n\n\t\t\tif unitCty != cty.NilVal {\n\t\t\t\tunitsMap[unit.Name] = unitCty\n\t\t\t}\n\t\t}\n\n\t\tif len(unitsMap) > 0 {\n\t\t\tunitsCty, err := ConvertValuesMapToCtyVal(unitsMap)\n\t\t\tif err != nil {\n\t\t\t\treturn cty.NilVal, err\n\t\t\t}\n\n\t\t\toutput[MetadataUnit] = unitsCty\n\t\t}\n\t}\n\n\treturn ConvertValuesMapToCtyVal(output)\n}\n\n// stackToCty converts a Stack struct to a cty Value\nfunc stackToCty(stack *Stack) (cty.Value, error) {\n\tif stack == nil {\n\t\treturn cty.NilVal, nil\n\t}\n\n\toutput := map[string]cty.Value{\n\t\t\"name\":   gostringToCty(stack.Name),\n\t\t\"source\": gostringToCty(stack.Source),\n\t\t\"path\":   gostringToCty(stack.Path),\n\t}\n\n\t// Handle Values if available\n\tif stack.Values != nil {\n\t\toutput[\"values\"] = *stack.Values\n\t}\n\n\t// Handle NoStack if available\n\tif stack.NoStack != nil {\n\t\toutput[\"no_dot_terragrunt_stack\"] = goboolToCty(*stack.NoStack)\n\t}\n\n\tif stack.NoValidation != nil {\n\t\toutput[\"no_validation\"] = goboolToCty(*stack.NoValidation)\n\t}\n\n\treturn ConvertValuesMapToCtyVal(output)\n}\n\n// unitToCty converts a Unit struct to a cty Value\nfunc unitToCty(unit *Unit) (cty.Value, error) {\n\tif unit == nil {\n\t\treturn cty.NilVal, nil\n\t}\n\n\toutput := map[string]cty.Value{\n\t\t\"name\":   gostringToCty(unit.Name),\n\t\t\"source\": gostringToCty(unit.Source),\n\t\t\"path\":   gostringToCty(unit.Path),\n\t}\n\n\t// Handle Values if available\n\tif unit.Values != nil {\n\t\toutput[\"values\"] = *unit.Values\n\t}\n\n\t// Handle NoStack if available\n\tif unit.NoStack != nil {\n\t\toutput[\"no_dot_terragrunt_stack\"] = goboolToCty(*unit.NoStack)\n\t}\n\n\tif unit.NoValidation != nil {\n\t\toutput[\"no_validation\"] = goboolToCty(*unit.NoValidation)\n\t}\n\n\treturn ConvertValuesMapToCtyVal(output)\n}\n\n// Converts arbitrary go types that are json serializable to a cty Value by using json as an intermediary\n// representation. This avoids the strict type nature of cty, where you need to know the output type beforehand to\n// serialize to cty.\nfunc convertToCtyWithJSON(val any) (cty.Value, error) {\n\tjsonBytes, err := json.Marshal(val)\n\tif err != nil {\n\t\treturn cty.NilVal, errors.New(err)\n\t}\n\n\tvar ctyJSONVal ctyjson.SimpleJSONValue\n\tif err := ctyJSONVal.UnmarshalJSON(jsonBytes); err != nil {\n\t\treturn cty.NilVal, errors.New(err)\n\t}\n\n\treturn ctyJSONVal.Value, nil\n}\n\n// GoTypeToCty converts arbitrary go type (struct that has cty tags, slice, map with string keys, string, bool, int\n// uint, float, cty.Value) to a cty Value\nfunc GoTypeToCty(val any) (cty.Value, error) {\n\t// Check if the value is a map\n\tif m, ok := val.(map[string]any); ok {\n\t\tconvertedMap := make(map[string]cty.Value)\n\n\t\tfor k, v := range m {\n\t\t\tconvertedValue, err := GoTypeToCty(v)\n\t\t\tif err != nil {\n\t\t\t\treturn cty.NilVal, err\n\t\t\t}\n\n\t\t\tconvertedMap[k] = convertedValue\n\t\t}\n\n\t\treturn cty.ObjectVal(convertedMap), nil\n\t}\n\n\t// Use the existing logic for other types\n\tctyType, err := gocty.ImpliedType(val)\n\tif err != nil {\n\t\treturn cty.NilVal, errors.New(err)\n\t}\n\n\tctyOut, err := gocty.ToCtyValue(val, ctyType)\n\tif err != nil {\n\t\treturn cty.NilVal, errors.New(err)\n\t}\n\n\treturn ctyOut, nil\n}\n\n// Converts primitive go strings to a cty Value.\nfunc gostringToCty(val string) cty.Value {\n\tctyOut, err := gocty.ToCtyValue(val, cty.String)\n\tif err != nil {\n\t\t// Since we are converting primitive strings, we should never get an error in this conversion.\n\t\tpanic(err)\n\t}\n\n\treturn ctyOut\n}\n\n// Converts primitive go bools to a cty Value.\nfunc goboolToCty(val bool) cty.Value {\n\tctyOut, err := gocty.ToCtyValue(val, cty.Bool)\n\tif err != nil {\n\t\t// Since we are converting primitive bools, we should never get an error in this conversion.\n\t\tpanic(err)\n\t}\n\n\treturn ctyOut\n}\n\n// FormatValue converts a primitive value to its string representation.\nfunc FormatValue(value cty.Value) (string, error) {\n\tif value.Type() == cty.String {\n\t\treturn value.AsString(), nil\n\t}\n\n\treturn GetValueString(value)\n}\n"
  },
  {
    "path": "pkg/config/config_as_cty_test.go",
    "content": "package config_test\n\nimport (\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/fatih/structs\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/zclconf/go-cty/cty\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/codegen\"\n\t\"github.com/gruntwork-io/terragrunt/internal/ctyhelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n)\n\n// This test makes sure that all the fields from the TerragruntConfig struct are accounted for in the conversion to\n// cty.Value.\nfunc TestTerragruntConfigAsCtyDrift(t *testing.T) {\n\tt.Parallel()\n\n\ttestSource := \"./foo\"\n\ttestTrue := true\n\ttestFalse := false\n\tmockOutputs := cty.Zero\n\tmockOutputsAllowedTerraformCommands := []string{\"init\"}\n\tmetaVal := cty.MapVal(map[string]cty.Value{\n\t\t\"foo\": cty.StringVal(\"bar\"),\n\t})\n\ttestConfig := config.TerragruntConfig{\n\t\tEngine: &config.EngineConfig{\n\t\t\tSource: \"github.com/acme/terragrunt-plugin-custom-opentofu\",\n\t\t\tMeta:   &metaVal,\n\t\t},\n\t\tCatalog: &config.CatalogConfig{\n\t\t\tURLs: []string{\n\t\t\t\t\"repo/path\",\n\t\t\t},\n\t\t},\n\t\tTerraform: &config.TerraformConfig{\n\t\t\tSource: &testSource,\n\t\t\tExtraArgs: []config.TerraformExtraArguments{\n\t\t\t\t{\n\t\t\t\t\tName:     \"init\",\n\t\t\t\t\tCommands: []string{\"init\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tBeforeHooks: []config.Hook{\n\t\t\t\t{\n\t\t\t\t\tName:     \"init\",\n\t\t\t\t\tCommands: []string{\"init\"},\n\t\t\t\t\tExecute:  []string{\"true\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tAfterHooks: []config.Hook{\n\t\t\t\t{\n\t\t\t\t\tName:     \"init\",\n\t\t\t\t\tCommands: []string{\"init\"},\n\t\t\t\t\tExecute:  []string{\"true\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tErrorHooks: []config.ErrorHook{\n\t\t\t\t{\n\t\t\t\t\tName:     \"init\",\n\t\t\t\t\tCommands: []string{\"init\"},\n\t\t\t\t\tExecute:  []string{\"true\"},\n\t\t\t\t\tOnErrors: []string{\".*\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tTerraformBinary:             \"terraform\",\n\t\tTerraformVersionConstraint:  \"= 0.12.20\",\n\t\tTerragruntVersionConstraint: \"= 0.23.18\",\n\t\tRemoteState: remotestate.New(&remotestate.Config{\n\t\t\tBackendName:                   \"foo\",\n\t\t\tDisableInit:                   true,\n\t\t\tDisableDependencyOptimization: true,\n\t\t\tBackendConfig: map[string]any{\n\t\t\t\t\"bar\": \"baz\",\n\t\t\t},\n\t\t}),\n\t\tDependencies: &config.ModuleDependencies{\n\t\t\tPaths: []string{\"foo\"},\n\t\t},\n\t\tDownloadDir:    \".terragrunt-cache\",\n\t\tPreventDestroy: &testTrue,\n\t\tIamRole:        \"terragruntRole\",\n\t\tInputs: map[string]any{\n\t\t\t\"aws_region\": \"us-east-1\",\n\t\t},\n\t\tLocals: map[string]any{\n\t\t\t\"quote\": \"the answer is 42\",\n\t\t},\n\t\tTerragruntDependencies: config.Dependencies{\n\t\t\tconfig.Dependency{\n\t\t\t\tName:                                \"foo\",\n\t\t\t\tConfigPath:                          cty.StringVal(\"foo\"),\n\t\t\t\tSkipOutputs:                         &testTrue,\n\t\t\t\tMockOutputs:                         &mockOutputs,\n\t\t\t\tMockOutputsAllowedTerraformCommands: &mockOutputsAllowedTerraformCommands,\n\t\t\t\tMockOutputsMergeWithState:           &testFalse,\n\t\t\t\tRenderedOutputs:                     &mockOutputs,\n\t\t\t},\n\t\t},\n\t\tFeatureFlags: config.FeatureFlags{\n\t\t\t&config.FeatureFlag{\n\t\t\t\tName:    \"test\",\n\t\t\t\tDefault: &cty.Zero,\n\t\t\t},\n\t\t},\n\t\tErrors: &config.ErrorsConfig{\n\t\t\tRetry: []*config.RetryBlock{\n\t\t\t\t{\n\t\t\t\t\tLabel:            \"test\",\n\t\t\t\t\tRetryableErrors:  []string{\"test\"},\n\t\t\t\t\tMaxAttempts:      0,\n\t\t\t\t\tSleepIntervalSec: 0,\n\t\t\t\t},\n\t\t\t},\n\t\t\tIgnore: []*config.IgnoreBlock{\n\t\t\t\t{\n\t\t\t\t\tLabel:           \"test\",\n\t\t\t\t\tIgnorableErrors: nil,\n\t\t\t\t\tMessage:         \"\",\n\t\t\t\t\tSignals:         nil,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tGenerateConfigs: map[string]codegen.GenerateConfig{\n\t\t\t\"provider\": {\n\t\t\t\tPath:          \"foo\",\n\t\t\t\tIfExists:      codegen.ExistsOverwriteTerragrunt,\n\t\t\t\tIfExistsStr:   \"overwrite_terragrunt\",\n\t\t\t\tCommentPrefix: \"# \",\n\t\t\t\tContents: `terraform {\n  backend \"s3\" {}\n}`,\n\t\t\t},\n\t\t},\n\t\tExclude: &config.ExcludeConfig{},\n\t}\n\tctyVal, err := config.TerragruntConfigAsCty(&testConfig)\n\trequire.NoError(t, err)\n\n\tctyMap, err := ctyhelper.ParseCtyValueToMap(ctyVal)\n\trequire.NoError(t, err)\n\n\t// Test the root properties\n\ttestConfigStructInfo := structs.New(testConfig)\n\ttestConfigFields := testConfigStructInfo.Names()\n\tchecked := map[string]bool{} // used to track which fields of the ctyMap were seen\n\n\tfor _, field := range testConfigFields {\n\t\tmapKey, isConverted := terragruntConfigStructFieldToMapKey(t, field)\n\t\tif isConverted {\n\t\t\t_, hasKey := ctyMap[mapKey]\n\t\t\tassert.Truef(t, hasKey, \"Struct field %s (convert of map key %s) did not convert to cty val\", field, mapKey)\n\t\t\tchecked[mapKey] = true\n\t\t}\n\t}\n\n\tfor key := range ctyMap {\n\t\t_, hasKey := checked[key]\n\t\tassert.Truef(t, hasKey, \"cty value key %s is not accounted for from struct field\", key)\n\t}\n}\n\n// This test makes sure that all the fields in RemoteState are converted to cty\nfunc TestRemoteStateAsCtyDrift(t *testing.T) {\n\tt.Parallel()\n\n\ttestConfig := remotestate.Config{\n\t\tBackendName:                   \"foo\",\n\t\tDisableInit:                   true,\n\t\tDisableDependencyOptimization: true,\n\t\tGenerate: &remotestate.ConfigGenerate{\n\t\t\tPath:     \"foo\",\n\t\t\tIfExists: \"overwrite_terragrunt\",\n\t\t},\n\t\tBackendConfig: map[string]any{\n\t\t\t\"bar\": \"baz\",\n\t\t},\n\t\tEncryption: map[string]any{\n\t\t\t\"bar\": \"baz\",\n\t\t},\n\t}\n\n\tctyVal, err := config.RemoteStateAsCty(remotestate.New(&testConfig))\n\trequire.NoError(t, err)\n\n\tctyMap, err := ctyhelper.ParseCtyValueToMap(ctyVal)\n\trequire.NoError(t, err)\n\n\t// Test the root properties\n\ttestConfigStructInfo := structs.New(testConfig)\n\ttestConfigFields := testConfigStructInfo.Names()\n\tchecked := map[string]bool{} // used to track which fields of the ctyMap were seen\n\n\tfor _, field := range testConfigFields {\n\t\tmapKey, isConverted := remoteStateStructFieldToMapKey(t, field)\n\t\tif isConverted {\n\t\t\t_, hasKey := ctyMap[mapKey]\n\t\t\tassert.Truef(t, hasKey, \"Struct field %s (convert of map key %s) did not convert to cty val\", field, mapKey)\n\t\t\tchecked[mapKey] = true\n\t\t}\n\t}\n\n\tfor key := range ctyMap {\n\t\t_, hasKey := checked[key]\n\t\tassert.Truef(t, hasKey, \"cty value key %s is not accounted for from struct field\", key)\n\t}\n}\n\n// This test makes sure that all the fields in TerraformConfig exist in ctyTerraformConfig.\nfunc TestTerraformConfigAsCtyDrift(t *testing.T) {\n\tt.Parallel()\n\n\tterraformConfigStructInfo := structs.New(config.TerraformConfig{})\n\tterraformConfigFields := terraformConfigStructInfo.Names()\n\tsort.Strings(terraformConfigFields)\n\n\tctyTerraformConfigStructInfo := structs.New(config.CtyTerraformConfig{})\n\tctyTerraformConfigFields := ctyTerraformConfigStructInfo.Names()\n\tsort.Strings(ctyTerraformConfigFields)\n\tassert.Equal(t, terraformConfigFields, ctyTerraformConfigFields)\n}\n\nfunc TestStackUnitCtyReading(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\ttgConfigCty, err := config.ParseTerragruntConfig(ctx, pctx, l, \"../../test/fixtures/stacks/basic/live/terragrunt.stack.hcl\", nil)\n\trequire.NoError(t, err)\n\tstackMap, err := ctyhelper.ParseCtyValueToMap(tgConfigCty)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, stackMap)\n\t// validate parsed unit\n\tunit := stackMap[\"unit\"].(map[string]any)\n\tassert.NotNil(t, unit)\n\tassert.NotNil(t, unit[\"mother\"])\n\tassert.NotNil(t, unit[\"father\"])\n\tassert.NotNil(t, unit[\"chick_1\"])\n\tassert.NotNil(t, unit[\"chick_2\"])\n}\n\nfunc TestStackLocalsCtyReading(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\ttgConfigCty, err := config.ParseTerragruntConfig(ctx, pctx, l, \"../../test/fixtures/stacks/locals/live/terragrunt.stack.hcl\", nil)\n\trequire.NoError(t, err)\n\tstackMap, err := ctyhelper.ParseCtyValueToMap(tgConfigCty)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, stackMap)\n\tlocals := stackMap[\"local\"].(map[string]any)\n\tassert.NotNil(t, locals)\n}\n\nfunc terragruntConfigStructFieldToMapKey(t *testing.T, fieldName string) (string, bool) {\n\tt.Helper()\n\n\tswitch fieldName {\n\tcase \"Catalog\":\n\t\treturn \"catalog\", true\n\tcase \"Terraform\":\n\t\treturn \"terraform\", true\n\tcase \"TerraformBinary\":\n\t\treturn \"terraform_binary\", true\n\tcase \"TerraformVersionConstraint\":\n\t\treturn \"terraform_version_constraint\", true\n\tcase \"TerragruntVersionConstraint\":\n\t\treturn \"terragrunt_version_constraint\", true\n\tcase \"RemoteState\":\n\t\treturn \"remote_state\", true\n\tcase \"Dependencies\":\n\t\treturn \"dependencies\", true\n\tcase \"DownloadDir\":\n\t\treturn \"download_dir\", true\n\tcase \"PreventDestroy\":\n\t\treturn \"prevent_destroy\", true\n\tcase \"IamRole\":\n\t\treturn \"iam_role\", true\n\tcase \"IamAssumeRoleDuration\":\n\t\treturn \"iam_assume_role_duration\", true\n\tcase \"IamAssumeRoleSessionName\":\n\t\treturn \"iam_assume_role_session_name\", true\n\tcase \"IamWebIdentityToken\":\n\t\treturn \"iam_web_identity_token\", true\n\tcase \"Inputs\":\n\t\treturn \"inputs\", true\n\tcase \"Locals\":\n\t\treturn \"locals\", true\n\tcase \"TerragruntDependencies\":\n\t\treturn \"dependency\", true\n\tcase \"GenerateConfigs\":\n\t\treturn \"generate\", true\n\tcase \"IsPartial\":\n\t\treturn \"\", false\n\tcase \"ProcessedIncludes\":\n\t\treturn \"\", false\n\tcase \"FieldsMetadata\":\n\t\treturn \"\", false\n\tcase \"Engine\":\n\t\treturn \"engine\", true\n\tcase \"FeatureFlags\":\n\t\treturn \"feature\", true\n\tcase \"Exclude\":\n\t\treturn \"exclude\", true\n\tcase \"Errors\":\n\t\treturn \"errors\", true\n\tdefault:\n\t\tt.Fatalf(\"Unknown struct property: %s\", fieldName)\n\t\t// This should not execute\n\t\treturn \"\", false\n\t}\n}\n\nfunc remoteStateStructFieldToMapKey(t *testing.T, fieldName string) (string, bool) {\n\tt.Helper()\n\n\tswitch fieldName {\n\tcase \"BackendName\":\n\t\treturn \"backend\", true\n\tcase \"DisableInit\":\n\t\treturn \"disable_init\", true\n\tcase \"DisableDependencyOptimization\":\n\t\treturn \"disable_dependency_optimization\", true\n\tcase \"Generate\":\n\t\treturn \"generate\", true\n\tcase \"BackendConfig\":\n\t\treturn \"config\", true\n\tcase \"Encryption\":\n\t\treturn \"encryption\", true\n\tdefault:\n\t\tt.Fatalf(\"Unknown struct property: %s\", fieldName)\n\t\t// This should not execute\n\t\treturn \"\", false\n\t}\n}\n"
  },
  {
    "path": "pkg/config/config_helpers.go",
    "content": "package config\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"unicode/utf8\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/getsops/sops/v3/cmd/sops/formats\"\n\t\"github.com/getsops/sops/v3/decrypt\"\n\t\"github.com/hashicorp/go-getter\"\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/hashicorp/hcl/v2\"\n\ttflang \"github.com/hashicorp/terraform/lang\"\n\t\"github.com/zclconf/go-cty/cty\"\n\t\"github.com/zclconf/go-cty/cty/function\"\n\t\"github.com/zclconf/go-cty/cty/gocty\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/awshelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cache\"\n\t\"github.com/gruntwork-io/terragrunt/internal/clihelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/ctyhelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/locks\"\n\t\"github.com/gruntwork-io/terragrunt/internal/retry\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\nconst (\n\tnoMatchedPats = 1\n\tmatchedPats   = 2\n)\n\n// RunCmdCacheEntry stores run_cmd results including output for replay.\n// This allows the output to be replayed on cache hits, which is necessary\n// when the command was first executed during discovery phase (with io.Discard writers)\n// but needs to show output during the execution phase (with real writers).\ntype RunCmdCacheEntry struct {\n\t// Stdout is the raw stdout of the command.\n\tStdout string\n\t// Stderr is the raw stderr of the command.\n\tStderr string\n\t// replayOnce ensures output is replayed exactly once to a real (non-Discard) writer.\n\treplayOnce sync.Once\n}\n\n// Value returns the whitespace-trimmed stdout, which is the return value of run_cmd.\nfunc (e *RunCmdCacheEntry) Value() string {\n\treturn strings.TrimSuffix(e.Stdout, \"\\n\")\n}\n\nconst (\n\tFuncNameFindInParentFolders                     = \"find_in_parent_folders\"\n\tFuncNamePathRelativeToInclude                   = \"path_relative_to_include\"\n\tFuncNamePathRelativeFromInclude                 = \"path_relative_from_include\"\n\tFuncNameGetEnv                                  = \"get_env\"\n\tFuncNameRunCmd                                  = \"run_cmd\"\n\tFuncNameReadTerragruntConfig                    = \"read_terragrunt_config\"\n\tFuncNameGetPlatform                             = \"get_platform\"\n\tFuncNameGetRepoRoot                             = \"get_repo_root\"\n\tFuncNameGetPathFromRepoRoot                     = \"get_path_from_repo_root\"\n\tFuncNameGetPathToRepoRoot                       = \"get_path_to_repo_root\"\n\tFuncNameGetTerragruntDir                        = \"get_terragrunt_dir\"\n\tFuncNameGetOriginalTerragruntDir                = \"get_original_terragrunt_dir\"\n\tFuncNameGetTerraformCommand                     = \"get_terraform_command\"\n\tFuncNameGetTerraformCLIArgs                     = \"get_terraform_cli_args\"\n\tFuncNameGetParentTerragruntDir                  = \"get_parent_terragrunt_dir\"\n\tFuncNameGetAWSAccountAlias                      = \"get_aws_account_alias\"\n\tFuncNameGetAWSAccountID                         = \"get_aws_account_id\"\n\tFuncNameGetAWSCallerIdentityArn                 = \"get_aws_caller_identity_arn\"\n\tFuncNameGetAWSCallerIdentityUserID              = \"get_aws_caller_identity_user_id\"\n\tFuncNameGetTerraformCommandsThatNeedVars        = \"get_terraform_commands_that_need_vars\"\n\tFuncNameGetTerraformCommandsThatNeedLocking     = \"get_terraform_commands_that_need_locking\"\n\tFuncNameGetTerraformCommandsThatNeedInput       = \"get_terraform_commands_that_need_input\"\n\tFuncNameGetTerraformCommandsThatNeedParallelism = \"get_terraform_commands_that_need_parallelism\"\n\tFuncNameSopsDecryptFile                         = \"sops_decrypt_file\"\n\tFuncNameGetTerragruntSourceCLIFlag              = \"get_terragrunt_source_cli_flag\"\n\tFuncNameGetDefaultRetryableErrors               = \"get_default_retryable_errors\"\n\tFuncNameReadTfvarsFile                          = \"read_tfvars_file\"\n\tFuncNameGetWorkingDir                           = \"get_working_dir\"\n\tFuncNameStartsWith                              = \"startswith\"\n\tFuncNameEndsWith                                = \"endswith\"\n\tFuncNameStrContains                             = \"strcontains\"\n\tFuncNameTimeCmp                                 = \"timecmp\"\n\tFuncNameMarkAsRead                              = \"mark_as_read\"\n\tFuncNameConstraintCheck                         = \"constraint_check\"\n)\n\n// TerraformCommandsNeedLocking is a list of terraform commands that accept -lock-timeout\nvar TerraformCommandsNeedLocking = []string{\n\t\"apply\",\n\t\"destroy\",\n\t\"import\",\n\t\"plan\",\n\t\"refresh\",\n\t\"taint\",\n\t\"untaint\",\n}\n\n// TerraformCommandsNeedVars is a list of terraform commands that accept -var or -var-file\nvar TerraformCommandsNeedVars = []string{\n\t\"apply\",\n\t\"console\",\n\t\"destroy\",\n\t\"import\",\n\t\"plan\",\n\t\"push\",\n\t\"refresh\",\n}\n\n// TerraformCommandsNeedInput is list of terraform commands that accept -input=\nvar TerraformCommandsNeedInput = []string{\n\t\"apply\",\n\t\"import\",\n\t\"init\",\n\t\"plan\",\n\t\"refresh\",\n}\n\n// TerraformCommandsNeedParallelism is a list of terraform commands that accept -parallelism=\nvar TerraformCommandsNeedParallelism = []string{\n\t\"apply\",\n\t\"plan\",\n\t\"destroy\",\n}\n\ntype EnvVar struct {\n\tName         string\n\tDefaultValue string\n\tIsRequired   bool\n}\n\n// TrackInclude is used to differentiate between an included config in the current parsing ctx, and an included\n// config that was passed through from a previous parsing ctx.\ntype TrackInclude struct {\n\t// CurrentMap is the map version of CurrentList that maps the block labels to the included config.\n\tCurrentMap map[string]IncludeConfig\n\t// Original is used to track the original included config, and is used for resolving the include related\n\t// functions.\n\tOriginal *IncludeConfig\n\t// CurrentList is used to track the list of configs that should be imported and merged before the final\n\t// TerragruntConfig is returned. This preserves the order of the blocks as they appear in the config, so that we can\n\t// merge the included config in the right order.\n\tCurrentList IncludeConfigs\n}\n\n// Create an EvalContext for the HCL2 parser. We can define functions and variables in this ctx that the HCL2 parser\n// will make available to the Terragrunt configuration during parsing.\nfunc createTerragruntEvalContext(ctx context.Context, pctx *ParsingContext, l log.Logger, configPath string) (*hcl.EvalContext, error) {\n\ttfscope := tflang.Scope{\n\t\tBaseDir: filepath.Dir(configPath),\n\t}\n\n\tterragruntFunctions := map[string]function.Function{\n\t\tFuncNameFindInParentFolders:                     wrapStringSliceToStringAsFuncImpl(ctx, pctx, l, FindInParentFolders),\n\t\tFuncNamePathRelativeToInclude:                   wrapStringSliceToStringAsFuncImpl(ctx, pctx, l, PathRelativeToInclude),\n\t\tFuncNamePathRelativeFromInclude:                 wrapStringSliceToStringAsFuncImpl(ctx, pctx, l, PathRelativeFromInclude),\n\t\tFuncNameGetEnv:                                  wrapStringSliceToStringAsFuncImpl(ctx, pctx, l, getEnvironmentVariable),\n\t\tFuncNameRunCmd:                                  wrapStringSliceToStringAsFuncImpl(ctx, pctx, l, RunCommand),\n\t\tFuncNameReadTerragruntConfig:                    readTerragruntConfigAsFuncImpl(ctx, pctx, l),\n\t\tFuncNameGetPlatform:                             wrapVoidToStringAsFuncImpl(ctx, pctx, l, getPlatform),\n\t\tFuncNameGetRepoRoot:                             wrapVoidToStringAsFuncImpl(ctx, pctx, l, getRepoRoot),\n\t\tFuncNameGetPathFromRepoRoot:                     wrapVoidToStringAsFuncImpl(ctx, pctx, l, getPathFromRepoRoot),\n\t\tFuncNameGetPathToRepoRoot:                       wrapVoidToStringAsFuncImpl(ctx, pctx, l, getPathToRepoRoot),\n\t\tFuncNameGetTerragruntDir:                        wrapVoidToStringAsFuncImpl(ctx, pctx, l, GetTerragruntDir),\n\t\tFuncNameGetOriginalTerragruntDir:                wrapVoidToStringAsFuncImpl(ctx, pctx, l, getOriginalTerragruntDir),\n\t\tFuncNameGetTerraformCommand:                     wrapVoidToStringAsFuncImpl(ctx, pctx, l, getTerraformCommand),\n\t\tFuncNameGetTerraformCLIArgs:                     wrapVoidToStringSliceAsFuncImpl(ctx, pctx, l, getTerraformCliArgs),\n\t\tFuncNameGetParentTerragruntDir:                  wrapStringSliceToStringAsFuncImpl(ctx, pctx, l, GetParentTerragruntDir),\n\t\tFuncNameGetAWSAccountAlias:                      wrapVoidToStringAsFuncImpl(ctx, pctx, l, getAWSAccountAlias),\n\t\tFuncNameGetAWSAccountID:                         wrapVoidToStringAsFuncImpl(ctx, pctx, l, getAWSAccountID),\n\t\tFuncNameGetAWSCallerIdentityArn:                 wrapVoidToStringAsFuncImpl(ctx, pctx, l, getAWSCallerIdentityARN),\n\t\tFuncNameGetAWSCallerIdentityUserID:              wrapVoidToStringAsFuncImpl(ctx, pctx, l, getAWSCallerIdentityUserID),\n\t\tFuncNameGetTerraformCommandsThatNeedVars:        wrapStaticValueToStringSliceAsFuncImpl(TerraformCommandsNeedVars),\n\t\tFuncNameGetTerraformCommandsThatNeedLocking:     wrapStaticValueToStringSliceAsFuncImpl(TerraformCommandsNeedLocking),\n\t\tFuncNameGetTerraformCommandsThatNeedInput:       wrapStaticValueToStringSliceAsFuncImpl(TerraformCommandsNeedInput),\n\t\tFuncNameGetTerraformCommandsThatNeedParallelism: wrapStaticValueToStringSliceAsFuncImpl(TerraformCommandsNeedParallelism),\n\t\tFuncNameSopsDecryptFile:                         wrapStringSliceToStringAsFuncImpl(ctx, pctx, l, sopsDecryptFile),\n\t\tFuncNameGetTerragruntSourceCLIFlag:              wrapVoidToStringAsFuncImpl(ctx, pctx, l, getTerragruntSourceCliFlag),\n\t\tFuncNameGetDefaultRetryableErrors:               wrapVoidToStringSliceAsFuncImpl(ctx, pctx, l, getDefaultRetryableErrors),\n\t\tFuncNameReadTfvarsFile:                          wrapStringSliceToStringAsFuncImpl(ctx, pctx, l, readTFVarsFile),\n\t\tFuncNameGetWorkingDir:                           wrapVoidToStringAsFuncImpl(ctx, pctx, l, getWorkingDir),\n\t\tFuncNameMarkAsRead:                              wrapStringSliceToStringAsFuncImpl(ctx, pctx, l, markAsRead),\n\t\tFuncNameConstraintCheck:                         wrapStringSliceToBoolAsFuncImpl(ctx, pctx, ConstraintCheck),\n\n\t\t// Map with HCL functions introduced in Terraform after v0.15.3, since upgrade to a later version is not supported\n\t\t// https://github.com/gruntwork-io/terragrunt/blob/master/go.mod#L22\n\t\tFuncNameStartsWith:  wrapStringSliceToBoolAsFuncImpl(ctx, pctx, StartsWith),\n\t\tFuncNameEndsWith:    wrapStringSliceToBoolAsFuncImpl(ctx, pctx, EndsWith),\n\t\tFuncNameStrContains: wrapStringSliceToBoolAsFuncImpl(ctx, pctx, StrContains),\n\t\tFuncNameTimeCmp:     wrapStringSliceToNumberAsFuncImpl(ctx, pctx, l, TimeCmp),\n\t}\n\n\tfunctions := map[string]function.Function{}\n\n\tmaps.Copy(functions, tfscope.Functions())\n\tmaps.Copy(functions, terragruntFunctions)\n\tmaps.Copy(functions, pctx.PredefinedFunctions)\n\n\tevalCtx := &hcl.EvalContext{\n\t\tFunctions: functions,\n\t}\n\n\tevalCtx.Variables = map[string]cty.Value{}\n\tif pctx.Locals != nil {\n\t\tevalCtx.Variables[MetadataLocal] = *pctx.Locals\n\t}\n\n\tif pctx.Features != nil {\n\t\tevalCtx.Variables[MetadataFeatureFlag] = *pctx.Features\n\t}\n\n\tif pctx.Values != nil {\n\t\tevalCtx.Variables[MetadataValues] = *pctx.Values\n\t}\n\n\tif pctx.DecodedDependencies != nil {\n\t\tevalCtx.Variables[MetadataDependency] = *pctx.DecodedDependencies\n\t}\n\n\tif pctx.TrackInclude != nil && len(pctx.TrackInclude.CurrentList) > 0 {\n\t\t// For each include block, check if we want to expose the included config, and if so, add under the include\n\t\t// variable.\n\t\texposedInclude, err := includeMapAsCtyVal(ctx, pctx, l)\n\t\tif err != nil {\n\t\t\treturn evalCtx, err\n\t\t}\n\n\t\tevalCtx.Variables[MetadataInclude] = exposedInclude\n\t}\n\n\treturn evalCtx, nil\n}\n\n// Return the OS platform\nfunc getPlatform(ctx context.Context, pctx *ParsingContext, l log.Logger) (string, error) {\n\treturn runtime.GOOS, nil\n}\n\n// Return the repository root as an absolute path\nfunc getRepoRoot(ctx context.Context, pctx *ParsingContext, l log.Logger) (string, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\n\tvar result string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_get_repo_root\", attrs, func(childCtx context.Context) error {\n\t\tvar innerErr error\n\n\t\tresult, innerErr = shell.GitTopLevelDir(childCtx, l, pctx.Env, pctx.WorkingDir)\n\n\t\treturn innerErr\n\t})\n\n\treturn result, err\n}\n\n// Return the path from the repository root\nfunc getPathFromRepoRoot(ctx context.Context, pctx *ParsingContext, l log.Logger) (string, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\n\tvar result string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_get_path_from_repo_root\", attrs, func(childCtx context.Context) error {\n\t\trepoAbsPath, innerErr := shell.GitTopLevelDir(childCtx, l, pctx.Env, pctx.WorkingDir)\n\t\tif innerErr != nil {\n\t\t\treturn errors.New(innerErr)\n\t\t}\n\n\t\trepoRelPath, innerErr := filepath.Rel(repoAbsPath, pctx.WorkingDir)\n\t\tif innerErr != nil {\n\t\t\treturn errors.New(innerErr)\n\t\t}\n\n\t\tresult = repoRelPath\n\n\t\treturn nil\n\t})\n\n\treturn result, err\n}\n\n// Return the path to the repository root\nfunc getPathToRepoRoot(ctx context.Context, pctx *ParsingContext, l log.Logger) (string, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\n\tvar result string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_get_path_to_repo_root\", attrs, func(childCtx context.Context) error {\n\t\trepoAbsPath, innerErr := shell.GitTopLevelDir(childCtx, l, pctx.Env, pctx.WorkingDir)\n\t\tif innerErr != nil {\n\t\t\treturn errors.New(innerErr)\n\t\t}\n\n\t\trepoRootPathAbs, innerErr := filepath.Rel(pctx.WorkingDir, repoAbsPath)\n\t\tif innerErr != nil {\n\t\t\treturn errors.New(innerErr)\n\t\t}\n\n\t\tresult = strings.TrimSpace(repoRootPathAbs)\n\n\t\treturn nil\n\t})\n\n\treturn result, err\n}\n\n// GetTerragruntDir returns the directory where the Terragrunt configuration file lives.\nfunc GetTerragruntDir(ctx context.Context, pctx *ParsingContext, l log.Logger) (string, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\n\tvar result string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_get_terragrunt_dir\", attrs, func(childCtx context.Context) error {\n\t\tresult = filepath.Dir(pctx.TerragruntConfigPath)\n\n\t\treturn nil\n\t})\n\n\treturn result, err\n}\n\n// Return the directory where the original Terragrunt configuration file lives. This is primarily useful when one\n// Terragrunt config is being read from another e.g., if /terraform-code/terragrunt.hcl\n// calls read_terragrunt_config(\"/foo/bar.hcl\"), and within bar.hcl, you call get_original_terragrunt_dir(), you'll\n// get back /terraform-code.\nfunc getOriginalTerragruntDir(ctx context.Context, pctx *ParsingContext, l log.Logger) (string, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\n\tvar result string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_get_original_terragrunt_dir\", attrs, func(childCtx context.Context) error {\n\t\tresult = filepath.Dir(pctx.OriginalTerragruntConfigPath)\n\n\t\treturn nil\n\t})\n\n\treturn result, err\n}\n\n// GetParentTerragruntDir returns the parent directory where the Terragrunt configuration file lives.\nfunc GetParentTerragruntDir(ctx context.Context, pctx *ParsingContext, l log.Logger, params []string) (string, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\tif len(params) > 0 {\n\t\tattrs[\"include_label\"] = params[0]\n\t}\n\n\tvar result string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_get_parent_terragrunt_dir\", attrs, func(childCtx context.Context) error {\n\t\tparentPath, innerErr := PathRelativeFromInclude(childCtx, pctx, l, params)\n\t\tif innerErr != nil {\n\t\t\treturn errors.New(innerErr)\n\t\t}\n\n\t\tcurrentPath := filepath.Dir(pctx.TerragruntConfigPath)\n\n\t\tparentPath = filepath.Clean(filepath.Join(currentPath, parentPath))\n\n\t\tresult = parentPath\n\n\t\treturn nil\n\t})\n\n\treturn result, err\n}\n\nfunc parseGetEnvParameters(parameters []string) (EnvVar, error) {\n\tenvVariable := EnvVar{}\n\n\tswitch len(parameters) {\n\tcase noMatchedPats:\n\t\tenvVariable.IsRequired = true\n\t\tenvVariable.Name = parameters[0]\n\tcase matchedPats:\n\t\tenvVariable.Name = parameters[0]\n\t\tenvVariable.DefaultValue = parameters[1]\n\tdefault:\n\t\treturn envVariable, errors.New(InvalidGetEnvParamsError{ActualNumParams: len(parameters), Example: `getEnv(\"<NAME>\", \"[DEFAULT]\")`})\n\t}\n\n\tif envVariable.Name == \"\" {\n\t\treturn envVariable, errors.New(InvalidEnvParamNameError{EnvName: parameters[0]})\n\t}\n\n\treturn envVariable, nil\n}\n\n// RunCommand is a helper function that runs a command and returns the stdout as the interpolation\n// for each `run_cmd` in locals section, function is called twice\n// result\nfunc RunCommand(ctx context.Context, pctx *ParsingContext, l log.Logger, args []string) (string, error) {\n\t// Capture original args for telemetry before any modifications\n\toriginalArgs := make([]string, len(args))\n\tcopy(originalArgs, args)\n\n\t// Parse flags for telemetry attributes\n\tsuppressOutput := false\n\tdisableCache := false\n\tuseGlobalCache := false\n\n\tfor _, arg := range args {\n\t\tswitch arg {\n\t\tcase \"--terragrunt-quiet\":\n\t\t\tsuppressOutput = true\n\t\tcase \"--terragrunt-global-cache\":\n\t\t\tuseGlobalCache = true\n\t\tcase \"--terragrunt-no-cache\":\n\t\t\tdisableCache = true\n\t\t}\n\t}\n\n\t// Extract command name (first non-flag argument)\n\tvar command string\n\n\tfor _, arg := range args {\n\t\tif !strings.HasPrefix(arg, \"--terragrunt-\") {\n\t\t\tcommand = arg\n\t\t\tbreak\n\t\t}\n\t}\n\n\tvar result string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_run_cmd\", map[string]any{\n\t\t\"config_path\":     pctx.TerragruntConfigPath,\n\t\t\"working_dir\":     pctx.WorkingDir,\n\t\t\"command\":         command,\n\t\t\"args\":            fmt.Sprintf(\"%v\", originalArgs),\n\t\t\"suppress_output\": suppressOutput,\n\t\t\"global_cache\":    useGlobalCache,\n\t\t\"no_cache\":        disableCache,\n\t}, func(childCtx context.Context) error {\n\t\tvar innerErr error\n\n\t\tresult, innerErr = runCommandImpl(childCtx, pctx, l, args)\n\n\t\treturn innerErr\n\t})\n\n\treturn result, err\n}\n\n// runCommandImpl contains the actual implementation of RunCommand\nfunc runCommandImpl(ctx context.Context, pctx *ParsingContext, l log.Logger, args []string) (string, error) {\n\t// runCommandCache - cache of evaluated `run_cmd` invocations\n\t// see: https://github.com/gruntwork-io/terragrunt/issues/1427\n\trunCommandCache := cache.ContextCache[*RunCmdCacheEntry](ctx, RunCmdCacheContextKey)\n\n\tif len(args) == 0 {\n\t\treturn \"\", errors.New(EmptyStringNotAllowedError(\"parameter to the run_cmd function\"))\n\t}\n\n\tsuppressOutput := false\n\tdisableCache := false\n\tuseGlobalCache := false\n\tcurrentPath := filepath.Dir(pctx.TerragruntConfigPath)\n\tcachePath := currentPath\n\n\tcheckOptions := true\n\tfor checkOptions && len(args) > 0 {\n\t\tswitch args[0] {\n\t\tcase \"--terragrunt-quiet\":\n\t\t\tsuppressOutput = true\n\n\t\t\targs = slices.Delete(args, 0, 1)\n\t\tcase \"--terragrunt-global-cache\":\n\t\t\tif disableCache {\n\t\t\t\treturn \"\", errors.New(ConflictingRunCmdCacheOptionsError{})\n\t\t\t}\n\n\t\t\tuseGlobalCache = true\n\t\t\tcachePath = \"_global_\"\n\n\t\t\targs = slices.Delete(args, 0, 1)\n\t\tcase \"--terragrunt-no-cache\":\n\t\t\tif useGlobalCache {\n\t\t\t\treturn \"\", errors.New(ConflictingRunCmdCacheOptionsError{})\n\t\t\t}\n\n\t\t\tdisableCache = true\n\n\t\t\targs = slices.Delete(args, 0, 1)\n\t\tdefault:\n\t\t\tcheckOptions = false\n\t\t}\n\t}\n\n\t// To avoid re-run of the same run_cmd command, is used in memory cache for command results, with caching key path + arguments\n\t// see: https://github.com/gruntwork-io/terragrunt/issues/1427\n\tcacheKey := fmt.Sprintf(\"%v-%v\", cachePath, args)\n\n\t// Skip cache lookup if --terragrunt-no-cache is set\n\tif !disableCache {\n\t\tcachedEntry, foundInCache := runCommandCache.Get(ctx, cacheKey)\n\t\tif foundInCache {\n\t\t\t// Replay stdout/stderr to current writers once when we have a real (non-Discard) writer.\n\t\t\t// This is needed because the command may have first run during discovery phase\n\t\t\t// with io.Discard writers, so we need to replay the output during execution phase.\n\t\t\t// We only call Do() when we have a real writer, so it won't fire during discovery.\n\t\t\tif pctx.Writers.Writer != io.Discard {\n\t\t\t\tcachedEntry.replayOnce.Do(func() {\n\t\t\t\t\tif !suppressOutput && cachedEntry.Stdout != \"\" {\n\t\t\t\t\t\t_, _ = pctx.Writers.Writer.Write([]byte(cachedEntry.Stdout))\n\t\t\t\t\t}\n\n\t\t\t\t\tif cachedEntry.Stderr != \"\" {\n\t\t\t\t\t\t_, _ = pctx.Writers.ErrWriter.Write([]byte(cachedEntry.Stderr))\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif suppressOutput {\n\t\t\t\tl.Debugf(\"run_cmd, cached output: [REDACTED]\")\n\t\t\t} else {\n\t\t\t\tl.Debugf(\"run_cmd, cached output: [%s]\", cachedEntry.Value())\n\t\t\t}\n\n\t\t\treturn cachedEntry.Value(), nil\n\t\t}\n\t}\n\n\tcmdOutput, err := shell.RunCommandWithOutput(\n\t\tctx,\n\t\tl,\n\t\tshellRunOptsFromPctx(pctx),\n\t\tcurrentPath,\n\t\ttrue,\n\t\tfalse,\n\t\targs[0],\n\t\targs[1:]...,\n\t)\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\tvalue := strings.TrimSuffix(cmdOutput.Stdout.String(), \"\\n\")\n\n\tif suppressOutput {\n\t\tl.Debugf(\"run_cmd output: [REDACTED]\")\n\t} else {\n\t\tl.Debugf(\"run_cmd output: [%s]\", value)\n\t}\n\n\tentry := &RunCmdCacheEntry{\n\t\tStdout: cmdOutput.Stdout.String(),\n\t\tStderr: cmdOutput.Stderr.String(),\n\t}\n\n\tif pctx.Writers.Writer != io.Discard {\n\t\tentry.replayOnce.Do(func() {\n\t\t\tif !suppressOutput && entry.Stdout != \"\" {\n\t\t\t\t_, _ = pctx.Writers.Writer.Write([]byte(entry.Stdout))\n\t\t\t}\n\n\t\t\tif entry.Stderr != \"\" {\n\t\t\t\t_, _ = pctx.Writers.ErrWriter.Write([]byte(entry.Stderr))\n\t\t\t}\n\t\t})\n\t}\n\n\tif !disableCache {\n\t\trunCommandCache.Put(ctx, cacheKey, entry)\n\t}\n\n\treturn value, nil\n}\n\nfunc getEnvironmentVariable(ctx context.Context, pctx *ParsingContext, l log.Logger, parameters []string) (string, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\n\tif len(parameters) > 0 {\n\t\tattrs[\"env_name\"] = parameters[0]\n\t}\n\n\tif len(parameters) > 1 {\n\t\tattrs[\"default_value\"] = parameters[1]\n\t}\n\n\tvar result string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_get_env\", attrs, func(childCtx context.Context) error {\n\t\tparameterMap, innerErr := parseGetEnvParameters(parameters)\n\t\tif innerErr != nil {\n\t\t\treturn errors.New(innerErr)\n\t\t}\n\n\t\tenvValue, exists := pctx.Env[parameterMap.Name]\n\n\t\tif !exists {\n\t\t\tif parameterMap.IsRequired {\n\t\t\t\treturn errors.New(EnvVarNotFoundError{EnvVar: parameterMap.Name})\n\t\t\t}\n\n\t\t\tenvValue = parameterMap.DefaultValue\n\t\t}\n\n\t\tresult = envValue\n\n\t\treturn nil\n\t})\n\n\treturn result, err\n}\n\n// FindInParentFolders fings a parent Terragrunt configuration file in the parent\n// folders above the current Terragrunt configuration file and return its path.\nfunc FindInParentFolders(\n\tctx context.Context,\n\tpctx *ParsingContext,\n\tl log.Logger,\n\tparams []string,\n) (string, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\tif len(params) > 0 {\n\t\tattrs[\"file_to_find\"] = params[0]\n\t}\n\n\tif len(params) > 1 {\n\t\tattrs[\"fallback\"] = params[1]\n\t}\n\n\tvar result string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_find_in_parent_folders\", attrs, func(childCtx context.Context) error {\n\t\tvar innerErr error\n\n\t\tresult, innerErr = findInParentFoldersImpl(childCtx, pctx, l, params)\n\n\t\treturn innerErr\n\t})\n\n\treturn result, err\n}\n\n// findInParentFoldersImpl contains the actual implementation of FindInParentFolders\nfunc findInParentFoldersImpl(ctx context.Context, pctx *ParsingContext, l log.Logger, params []string) (string, error) {\n\tnumParams := len(params)\n\n\tvar (\n\t\tfileToFindParam string\n\t\tfallbackParam   string\n\t)\n\n\tif numParams > 0 {\n\t\tfileToFindParam = params[0]\n\t}\n\n\tif numParams > 1 {\n\t\tfallbackParam = params[1]\n\t}\n\n\tif numParams > matchedPats {\n\t\treturn \"\", errors.New(WrongNumberOfParamsError{Func: \"find_in_parent_folders\", Expected: \"0, 1, or 2\", Actual: numParams})\n\t}\n\n\tpreviousDir := filepath.Dir(pctx.TerragruntConfigPath)\n\n\tif fileToFindParam == \"\" || fileToFindParam == DefaultTerragruntConfigPath {\n\t\tallControls := pctx.StrictControls\n\t\trootTGHCLControl := allControls.FilterByNames(controls.RootTerragruntHCL)\n\t\tlogger := log.ContextWithLogger(ctx, l)\n\n\t\tif err := rootTGHCLControl.Evaluate(logger); err != nil {\n\t\t\treturn \"\", clihelper.NewExitError(err, clihelper.ExitCodeGeneralError)\n\t\t}\n\t}\n\n\t// The strict control above will make this function return an error when no parameter is passed.\n\t// When this becomes a breaking change, we can remove the strict control and\n\t// do some validation here to ensure that users aren't using \"terragrunt.hcl\" as the root of their Terragrunt\n\t// configurations.\n\tfileToFindStr := DefaultTerragruntConfigPath\n\tif fileToFindParam != \"\" {\n\t\tfileToFindStr = fileToFindParam\n\t}\n\n\t// To avoid getting into an accidental infinite loop (e.g. do to cyclical symlinks), set a max on the number of\n\t// parent folders we'll check\n\tfor range pctx.MaxFoldersToCheck {\n\t\tcurrentDir := filepath.Dir(previousDir)\n\t\tif currentDir == previousDir {\n\t\t\tif numParams == matchedPats {\n\t\t\t\treturn fallbackParam, nil\n\t\t\t}\n\n\t\t\treturn \"\", errors.New(ParentFileNotFoundError{\n\t\t\t\tPath:  pctx.TerragruntConfigPath,\n\t\t\t\tFile:  fileToFindStr,\n\t\t\t\tCause: \"Traversed all the way to the root\",\n\t\t\t})\n\t\t}\n\n\t\tfileToFind := GetDefaultConfigPath(currentDir)\n\t\tif fileToFindParam != \"\" {\n\t\t\tfileToFind = filepath.Join(currentDir, fileToFindParam)\n\t\t}\n\n\t\tif util.FileExists(fileToFind) {\n\t\t\treturn fileToFind, nil\n\t\t}\n\n\t\tpreviousDir = currentDir\n\t}\n\n\treturn \"\", errors.New(ParentFileNotFoundError{\n\t\tPath:  pctx.TerragruntConfigPath,\n\t\tFile:  fileToFindStr,\n\t\tCause: fmt.Sprintf(\"Exceeded maximum folders to check (%d)\", pctx.MaxFoldersToCheck),\n\t})\n}\n\n// PathRelativeToInclude returns the relative path between the included Terragrunt configuration file\n// and the current Terragrunt configuration file. Name param is required and used to lookup the\n// relevant import block when called in a child config with multiple import blocks.\nfunc PathRelativeToInclude(ctx context.Context, pctx *ParsingContext, l log.Logger, params []string) (string, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\tif len(params) > 0 {\n\t\tattrs[\"include_label\"] = params[0]\n\t}\n\n\tvar result string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_path_relative_to_include\", attrs, func(childCtx context.Context) error {\n\t\tif pctx.TrackInclude == nil {\n\t\t\tresult = \".\"\n\t\t\treturn nil\n\t\t}\n\n\t\tvar included IncludeConfig\n\n\t\tswitch {\n\t\tcase pctx.TrackInclude.Original != nil:\n\t\t\tincluded = *pctx.TrackInclude.Original\n\t\tcase len(pctx.TrackInclude.CurrentList) > 0:\n\t\t\t// Called in child ctx, so we need to select the right include file.\n\t\t\tselected, innerErr := getSelectedIncludeBlock(*pctx.TrackInclude, params)\n\t\t\tif innerErr != nil {\n\t\t\t\treturn innerErr\n\t\t\t}\n\n\t\t\tincluded = *selected\n\t\tdefault:\n\t\t\tresult = \".\"\n\t\t\treturn nil\n\t\t}\n\n\t\tcurrentPath := filepath.Dir(pctx.TerragruntConfigPath)\n\t\tincludePath := filepath.Dir(included.Path)\n\n\t\tif !filepath.IsAbs(includePath) {\n\t\t\tincludePath = filepath.Join(currentPath, includePath)\n\t\t}\n\n\t\tvar innerErr error\n\n\t\tresult, innerErr = util.GetPathRelativeTo(currentPath, includePath)\n\n\t\treturn innerErr\n\t})\n\n\treturn result, err\n}\n\n// PathRelativeFromInclude returns the relative path from the current Terragrunt configuration to the included Terragrunt configuration file\nfunc PathRelativeFromInclude(ctx context.Context, pctx *ParsingContext, l log.Logger, params []string) (string, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\tif len(params) > 0 {\n\t\tattrs[\"include_label\"] = params[0]\n\t}\n\n\tvar result string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_path_relative_from_include\", attrs, func(childCtx context.Context) error {\n\t\tif pctx.TrackInclude == nil {\n\t\t\tresult = \".\"\n\t\t\treturn nil\n\t\t}\n\n\t\tincluded, innerErr := getSelectedIncludeBlock(*pctx.TrackInclude, params)\n\t\tif innerErr != nil {\n\t\t\treturn innerErr\n\t\t} else if included == nil {\n\t\t\tresult = \".\"\n\t\t\treturn nil\n\t\t}\n\n\t\tincludePath := filepath.Dir(included.Path)\n\t\tcurrentPath := filepath.Dir(pctx.TerragruntConfigPath)\n\n\t\tif !filepath.IsAbs(includePath) {\n\t\t\tincludePath = filepath.Join(currentPath, includePath)\n\t\t}\n\n\t\tresult, innerErr = util.GetPathRelativeTo(includePath, currentPath)\n\n\t\treturn innerErr\n\t})\n\n\treturn result, err\n}\n\n// getTerraformCommand returns the current terraform command in execution\nfunc getTerraformCommand(ctx context.Context, pctx *ParsingContext, l log.Logger) (string, error) {\n\treturn pctx.TerraformCommand, nil\n}\n\n// getWorkingDir returns the current working dir\nfunc getWorkingDir(ctx context.Context, pctx *ParsingContext, l log.Logger) (string, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\n\tvar result string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_get_working_dir\", attrs, func(childCtx context.Context) error {\n\t\tvar innerErr error\n\n\t\tresult, innerErr = getWorkingDirImpl(childCtx, pctx, l)\n\n\t\treturn innerErr\n\t})\n\n\treturn result, err\n}\n\n// getWorkingDirImpl contains the actual implementation of getWorkingDir\nfunc getWorkingDirImpl(ctx context.Context, pctx *ParsingContext, l log.Logger) (string, error) {\n\tl.Debugf(\"Start processing get_working_dir built-in function\")\n\tdefer l.Debugf(\"Complete processing get_working_dir built-in function\")\n\n\t// Initialize evaluation ctx extensions from base blocks.\n\tpctx.PredefinedFunctions = map[string]function.Function{\n\t\tFuncNameGetWorkingDir: wrapVoidToEmptyStringAsFuncImpl(),\n\t}\n\n\tterragruntConfig, err := ParseConfigFile(ctx, pctx, l, pctx.TerragruntConfigPath, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tsourceURL, err := GetTerraformSourceURL(pctx.Source, pctx.SourceMap, pctx.OriginalTerragruntConfigPath, terragruntConfig)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// sourceURL will always be at least \".\" (current directory) to ensure cache is always used\n\twalkWithSymlinks := pctx.Experiments.Evaluate(experiment.Symlinks)\n\n\tsource, err := tf.NewSource(l, sourceURL, pctx.DownloadDir, pctx.WorkingDir, walkWithSymlinks)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn source.WorkingDir, nil\n}\n\n// getTerraformCliArgs returns cli args for terraform\nfunc getTerraformCliArgs(ctx context.Context, pctx *ParsingContext, l log.Logger) ([]string, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\n\tvar result []string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_get_terraform_cli_args\", attrs, func(childCtx context.Context) error {\n\t\tif pctx.TerraformCliArgs != nil {\n\t\t\tresult = pctx.TerraformCliArgs.Slice()\n\t\t}\n\n\t\treturn nil\n\t})\n\n\treturn result, err\n}\n\n// getDefaultRetryableErrors returns default retryable errors for use in errors.retry blocks\nfunc getDefaultRetryableErrors(ctx context.Context, pctx *ParsingContext, l log.Logger) ([]string, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\n\tvar result []string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_get_default_retryable_errors\", attrs, func(childCtx context.Context) error {\n\t\tresult = retry.DefaultRetryableErrors\n\t\treturn nil\n\t})\n\n\treturn result, err\n}\n\n// getAWSField is a common helper for fetching a single AWS field with telemetry.\n// It builds an AWS config from the parsing context, then calls fetchFn to get the value.\nfunc getAWSField(ctx context.Context, pctx *ParsingContext, l log.Logger, telemetryName string, fetchFn func(context.Context, *aws.Config) (string, error)) (string, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\n\tvar result string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, telemetryName, attrs, func(childCtx context.Context) error {\n\t\tawsConfig, err := awshelper.NewAWSConfigBuilder().\n\t\t\tWithEnv(pctx.Env).\n\t\t\tWithIAMRoleOptions(pctx.IAMRoleOptions).\n\t\t\tBuild(childCtx, l)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tval, err := fetchFn(childCtx, &awsConfig)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tresult = val\n\n\t\treturn nil\n\t})\n\n\treturn result, err\n}\n\nfunc getAWSAccountAlias(ctx context.Context, pctx *ParsingContext, l log.Logger) (string, error) {\n\treturn getAWSField(ctx, pctx, l, \"hcl_fn_get_aws_account_alias\", awshelper.GetAWSAccountAlias)\n}\n\nfunc getAWSAccountID(ctx context.Context, pctx *ParsingContext, l log.Logger) (string, error) {\n\treturn getAWSField(ctx, pctx, l, \"hcl_fn_get_aws_account_id\", awshelper.GetAWSAccountID)\n}\n\nfunc getAWSCallerIdentityARN(ctx context.Context, pctx *ParsingContext, l log.Logger) (string, error) {\n\treturn getAWSField(ctx, pctx, l, \"hcl_fn_get_aws_caller_identity_arn\", awshelper.GetAWSIdentityArn)\n}\n\nfunc getAWSCallerIdentityUserID(ctx context.Context, pctx *ParsingContext, l log.Logger) (string, error) {\n\treturn getAWSField(ctx, pctx, l, \"hcl_fn_get_aws_caller_identity_user_id\", awshelper.GetAWSUserID)\n}\n\n// ParseTerragruntConfig parses the terragrunt config and return a\n// representation that can be used as a reference. If given a default value,\n// this will return the default if the terragrunt config file does not exist.\nfunc ParseTerragruntConfig(ctx context.Context, pctx *ParsingContext, l log.Logger, configPath string, defaultVal *cty.Value) (cty.Value, error) {\n\t// target config check: make sure the target config exists. If the file does not exist, and there is no default val,\n\t// return an error. If the file does not exist but there is a default val, return the default val. Otherwise,\n\t// proceed to parse the file as a terragrunt config file.\n\ttargetConfig := getCleanedTargetConfigPath(configPath, pctx.TerragruntConfigPath)\n\n\ttargetConfigFileExists := util.FileExists(targetConfig)\n\n\tif !targetConfigFileExists && defaultVal == nil {\n\t\treturn cty.NilVal, errors.New(TerragruntConfigNotFoundError{Path: targetConfig})\n\t}\n\n\tif !targetConfigFileExists {\n\t\treturn *defaultVal, nil\n\t}\n\n\tpath := targetConfig\n\n\tif !filepath.IsAbs(path) {\n\t\tpath = filepath.Join(pctx.WorkingDir, path)\n\t\tpath = filepath.Clean(path)\n\t}\n\n\t// Track that this file was read during parsing\n\ttrackFileRead(pctx.FilesRead, path)\n\n\t// We update the ctx of terragruntOptions to the config being read in.\n\tl, pctx, err := pctx.WithConfigPath(l, targetConfig)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tpctx = pctx.WithDiagnosticsSuppressed(l)\n\n\t// check if file is stack file, decode as stack file\n\tif filepath.Base(targetConfig) == DefaultStackFile {\n\t\tstackSourceDir := filepath.Dir(targetConfig)\n\n\t\tvalues, readErr := ReadValues(ctx, pctx, l, stackSourceDir)\n\t\tif readErr != nil {\n\t\t\treturn cty.NilVal, errors.Errorf(\"failed to read values from directory %s: %v\", stackSourceDir, readErr)\n\t\t}\n\n\t\tstackFile, readErr := ReadStackConfigFile(ctx, l, pctx, targetConfig, values)\n\t\tif readErr != nil {\n\t\t\treturn cty.NilVal, errors.New(readErr)\n\t\t}\n\n\t\treturn stackConfigAsCty(stackFile)\n\t}\n\n\t// check if file is a values file, decode as values file\n\tif strings.HasSuffix(targetConfig, valuesFile) {\n\t\tunitValues, readErr := ReadValues(ctx, pctx, l, filepath.Dir(targetConfig))\n\t\tif readErr != nil {\n\t\t\treturn cty.NilVal, errors.New(readErr)\n\t\t}\n\n\t\treturn *unitValues, nil\n\t}\n\n\tconfig, err := ParseConfigFile(ctx, pctx, l, targetConfig, nil)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\t// We have to set the rendered outputs here because ParseConfigFile will not do so on the TerragruntConfig. The\n\t// outputs are stored in a special map that is used only for rendering and thus is not available when we try to\n\t// serialize the config for consumption.\n\t// NOTE: this will not call terragrunt output, since all the values are cached from the ParseConfigFile call\n\t// NOTE: we don't use range here because range will copy the slice, thereby undoing the set attribute.\n\tfor i := range len(config.TerragruntDependencies) {\n\t\terr := config.TerragruntDependencies[i].setRenderedOutputs(ctx, pctx, l)\n\t\tif err != nil {\n\t\t\treturn cty.NilVal, errors.New(err)\n\t\t}\n\t}\n\n\treturn TerragruntConfigAsCty(config)\n}\n\n// Create a cty Function that can be used to for calling read_terragrunt_config.\nfunc readTerragruntConfigAsFuncImpl(ctx context.Context, pctx *ParsingContext, l log.Logger) function.Function {\n\treturn function.New(&function.Spec{\n\t\t// Takes one required string param\n\t\tParams: []function.Parameter{{Type: cty.String}},\n\t\t// And optional param that takes anything\n\t\tVarParam: &function.Parameter{Type: cty.DynamicPseudoType},\n\t\t// We don't know the return type until we parse the terragrunt config, so we use a dynamic type\n\t\tType: function.StaticReturnType(cty.DynamicPseudoType),\n\t\tImpl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {\n\t\t\tnumParams := len(args)\n\n\t\t\tif numParams == 0 || numParams > 2 {\n\t\t\t\treturn cty.NilVal, errors.New(WrongNumberOfParamsError{Func: \"read_terragrunt_config\", Expected: \"1 or 2\", Actual: numParams})\n\t\t\t}\n\n\t\t\tstrArgs, err := ctySliceToStringSlice(args[:1])\n\t\t\tif err != nil {\n\t\t\t\treturn cty.NilVal, err\n\t\t\t}\n\n\t\t\tvar defaultVal *cty.Value = nil\n\t\t\tif numParams == matchedPats {\n\t\t\t\tdefaultVal = &args[1]\n\t\t\t}\n\n\t\t\ttargetConfigPath := strArgs[0]\n\n\t\t\tattrs := map[string]any{\n\t\t\t\t\"config_path\":        pctx.TerragruntConfigPath,\n\t\t\t\t\"working_dir\":        pctx.WorkingDir,\n\t\t\t\t\"target_config_path\": targetConfigPath,\n\t\t\t\t\"has_default\":        defaultVal != nil,\n\t\t\t}\n\n\t\t\tvar result cty.Value\n\n\t\t\terr = telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_read_terragrunt_config\", attrs, func(childCtx context.Context) error {\n\t\t\t\tvar innerErr error\n\n\t\t\t\tresult, innerErr = ParseTerragruntConfig(childCtx, pctx, l, targetConfigPath, defaultVal)\n\n\t\t\t\treturn innerErr\n\t\t\t})\n\n\t\t\treturn result, err\n\t\t},\n\t})\n}\n\n// Returns a cleaned path to the target config (the `terragrunt.hcl` or `terragrunt.hcl.json` file), handling relative\n// paths correctly. This will automatically append `terragrunt.hcl` or `terragrunt.hcl.json` to the path if the target\n// path is a directory.\nfunc getCleanedTargetConfigPath(configPath string, workingPath string) string {\n\tcwd := filepath.Dir(workingPath)\n\n\ttargetConfig := configPath\n\tif !filepath.IsAbs(targetConfig) {\n\t\ttargetConfig = filepath.Join(cwd, targetConfig)\n\t}\n\n\tif util.IsDir(targetConfig) {\n\t\ttargetConfig = GetDefaultConfigPath(targetConfig)\n\t}\n\n\treturn filepath.Clean(targetConfig)\n}\n\n// GetTerragruntSourceForModule returns the source path for a module based on the source path of the parent module and the\n// source path specified in the module's terragrunt.hcl file.\n//\n// If one of the xxx-all commands is called with the --source parameter, then for each module, we need to\n// build its own --source parameter by doing the following:\n//\n// 1. Read the source URL from the Terragrunt configuration of each module\n// 2. Extract the path from that URL (the part after a double-slash)\n// 3. Append the path to the --source parameter\n//\n// Example:\n//\n// --source: /source/infrastructure-modules\n// source param in module's terragrunt.hcl: git::git@github.com:acme/infrastructure-modules.git//networking/vpc?ref=v0.0.1\n//\n// This method will return: /source/infrastructure-modules//networking/vpc\nfunc GetTerragruntSourceForModule(sourcePath string, modulePath string, moduleTerragruntConfig *TerragruntConfig) (string, error) {\n\tif sourcePath == \"\" || moduleTerragruntConfig.Terraform == nil || moduleTerragruntConfig.Terraform.Source == nil || *moduleTerragruntConfig.Terraform.Source == \"\" {\n\t\treturn \"\", nil\n\t}\n\n\t// use go-getter to split the module source string into a valid URL and subdirectory (if // is present)\n\tmoduleURL, moduleSubdir := getter.SourceDirSubdir(*moduleTerragruntConfig.Terraform.Source)\n\n\t// if both URL and subdir are missing, something went terribly wrong\n\tif moduleURL == \"\" && moduleSubdir == \"\" {\n\t\treturn \"\", errors.New(InvalidSourceURLError{\n\t\t\tModulePath:       modulePath,\n\t\t\tModuleSourceURL:  *moduleTerragruntConfig.Terraform.Source,\n\t\t\tTerragruntSource: sourcePath,\n\t\t})\n\t}\n\n\t// if only subdir is missing, check if we can obtain a valid module name from the URL portion\n\tif moduleURL != \"\" && moduleSubdir == \"\" {\n\t\tmoduleSubdirFromURL, err := getModulePathFromSourceURL(moduleURL)\n\t\tif err != nil {\n\t\t\treturn moduleSubdirFromURL, err\n\t\t}\n\n\t\treturn util.JoinTerraformModulePath(sourcePath, moduleSubdirFromURL), nil\n\t}\n\n\treturn util.JoinTerraformModulePath(sourcePath, moduleSubdir), nil\n}\n\n// Parse sourceUrl not containing '//', and attempt to obtain a module path.\n// Example:\n//\n// sourceUrl = \"git::ssh://git@ghe.ourcorp.com/OurOrg/module-name.git\"\n// will return \"module-name\".\nfunc getModulePathFromSourceURL(sourceURL string) (string, error) {\n\t// Regexp for module name extraction. It assumes that the query string has already been stripped off.\n\t// Then we simply capture anything after the last slash, and before `.` or end of string.\n\tvar moduleNameRegexp = regexp.MustCompile(`(?:.+/)(.+?)(?:\\.|$)`)\n\n\t// strip off the query string if present\n\tsourceURL = strings.Split(sourceURL, \"?\")[0]\n\n\tmatches := moduleNameRegexp.FindStringSubmatch(sourceURL)\n\n\t// if regexp returns less/more than the full match + 1 capture group, then something went wrong with regex (invalid source string)\n\tif len(matches) != matchedPats {\n\t\treturn \"\", errors.New(ParsingModulePathError{ModuleSourceURL: sourceURL})\n\t}\n\n\treturn matches[1], nil\n}\n\n// decrypts and returns sops encrypted utf-8 yaml or json data as a string\nfunc sopsDecryptFile(ctx context.Context, pctx *ParsingContext, l log.Logger, params []string) (string, error) {\n\tvar sourceFile string\n\tif len(params) > 0 {\n\t\tsourceFile = params[0]\n\t}\n\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t\t\"file_path\":   sourceFile,\n\t}\n\n\tvar result string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_sops_decrypt_file\", attrs, func(childCtx context.Context) error {\n\t\tif len(params) != 1 {\n\t\t\treturn errors.New(WrongNumberOfParamsError{Func: \"sops_decrypt_file\", Expected: \"1\", Actual: len(params)})\n\t\t}\n\n\t\tformat, err := getSopsFileFormat(sourceFile)\n\t\tif err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\n\t\tpath := sourceFile\n\n\t\tif !filepath.IsAbs(path) {\n\t\t\tpath = filepath.Join(pctx.WorkingDir, path)\n\t\t\tpath = filepath.Clean(path)\n\t\t}\n\n\t\ttrackFileRead(pctx.FilesRead, path)\n\n\t\tvar innerErr error\n\n\t\tresult, innerErr = sopsDecryptFileImpl(childCtx, pctx, l, path, format, decrypt.File)\n\n\t\treturn innerErr\n\t})\n\n\treturn result, err\n}\n\n// sopsDecryptFileImpl contains the actual implementation of sopsDecryptFile\nfunc sopsDecryptFileImpl(ctx context.Context, pctx *ParsingContext, l log.Logger, path string, format string, decryptFn func(string, string) ([]byte, error)) (string, error) {\n\tsopsCache := cache.ContextCache[string](ctx, SopsCacheContextKey)\n\n\t// Fast path: check cache before acquiring lock.\n\t// Cache has its own sync.RWMutex, safe for concurrent reads.\n\tif val, ok := sopsCache.Get(ctx, path); ok {\n\t\tl.Debugf(\"sops decrypt: cache hit for %s (len=%d)\", path, len(val))\n\n\t\treturn val, nil\n\t}\n\n\t// Cache miss: acquire lock for env mutation + decrypt.\n\t// The lock serializes os.Setenv/os.Unsetenv to prevent race conditions\n\t// when multiple units decrypt concurrently with different auth credentials.\n\t// See https://github.com/gruntwork-io/terragrunt/issues/5515\n\tl.Debugf(\"sops decrypt: cache miss, acquiring lock for %s (format=%s)\", path, format)\n\n\tlocks.EnvLock.Lock()\n\tdefer locks.EnvLock.Unlock()\n\n\t// Double-check: another goroutine may have populated cache while we waited for the lock.\n\tif val, ok := sopsCache.Get(ctx, path); ok {\n\t\tl.Debugf(\"sops decrypt: cache hit after lock for %s (len=%d)\", path, len(val))\n\n\t\treturn val, nil\n\t}\n\n\t// Set env vars from opts.Env that are missing from process env.\n\t// Auth-provider credentials (e.g., AWS_SESSION_TOKEN) may not exist\n\t// in process env yet — SOPS needs them for KMS auth.\n\t// Existing process env vars are preserved to avoid overriding real\n\t// credentials with empty auth-provider values.\n\tenv := pctx.Env\n\n\tsetKeys := make([]string, 0, len(env))\n\n\tfor k, v := range env {\n\t\tif _, exists := os.LookupEnv(k); exists {\n\t\t\tcontinue\n\t\t}\n\n\t\tos.Setenv(k, v) //nolint:errcheck\n\n\t\tsetKeys = append(setKeys, k)\n\t}\n\n\tdefer func() {\n\t\tfor _, k := range setKeys {\n\t\t\tos.Unsetenv(k) //nolint:errcheck\n\t\t}\n\t}()\n\n\tl.Debugf(\"sops decrypt: decrypting %s\", path)\n\n\trawData, err := decryptFn(path, format)\n\tif err != nil {\n\t\treturn \"\", errors.New(extractSopsErrors(err))\n\t}\n\n\tif utf8.Valid(rawData) {\n\t\tvalue := string(rawData)\n\t\tsopsCache.Put(ctx, path, value)\n\n\t\treturn value, nil\n\t}\n\n\treturn \"\", errors.New(InvalidSopsFormatError{SourceFilePath: path})\n}\n\n// Mapping of SOPS format to string\nvar sopsFormatToString = map[formats.Format]string{\n\tformats.Binary: \"binary\",\n\tformats.Dotenv: \"dotenv\",\n\tformats.Ini:    \"ini\",\n\tformats.Json:   \"json\",\n\tformats.Yaml:   \"yaml\",\n}\n\n// getSopsFileFormat - Return file format for SOPS library\nfunc getSopsFileFormat(sourceFile string) (string, error) {\n\tfileFormat := formats.FormatForPath(sourceFile)\n\n\tformat, found := sopsFormatToString[fileFormat]\n\tif !found {\n\t\treturn \"\", InvalidSopsFormatError{SourceFilePath: sourceFile}\n\t}\n\n\treturn format, nil\n}\n\n// Return the location of the Terraform files provided via --source\nfunc getTerragruntSourceCliFlag(ctx context.Context, pctx *ParsingContext, l log.Logger) (string, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\n\tvar result string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_get_terragrunt_source_cli_flag\", attrs, func(childCtx context.Context) error {\n\t\tresult = pctx.Source\n\t\treturn nil\n\t})\n\n\treturn result, err\n}\n\n// Return the selected include block based on a label passed in as a function param. Note that the assumption is that:\n//   - If the Original attribute is set, we are in the parent ctx so return that.\n//   - If there are no include blocks, no param is required and nil is returned.\n//   - If there is only one include block, no param is required and that is automatically returned.\n//   - If there is more than one include block, 1 param is required to use as the label name to lookup the include block\n//     to use.\nfunc getSelectedIncludeBlock(trackInclude TrackInclude, params []string) (*IncludeConfig, error) {\n\timportMap := trackInclude.CurrentMap\n\n\tif trackInclude.Original != nil {\n\t\treturn trackInclude.Original, nil\n\t}\n\n\tif len(importMap) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tif len(importMap) == 1 {\n\t\tfor _, val := range importMap {\n\t\t\treturn &val, nil\n\t\t}\n\t}\n\n\tnumParams := len(params)\n\tif numParams != 1 {\n\t\treturn nil, errors.New(WrongNumberOfParamsError{Func: \"path_relative_from_include\", Expected: \"1\", Actual: numParams})\n\t}\n\n\timportName := params[0]\n\n\timported, hasKey := importMap[importName]\n\tif !hasKey {\n\t\treturn nil, errors.New(InvalidIncludeKeyError{name: importName})\n\t}\n\n\treturn &imported, nil\n}\n\n// StartsWith Implementation of Terraform's StartsWith function\n//\n//nolint:dupl\nfunc StartsWith(ctx context.Context, pctx *ParsingContext, args []string) (bool, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\tif len(args) > 0 {\n\t\tattrs[\"str\"] = args[0]\n\t}\n\n\tif len(args) > 1 {\n\t\tattrs[\"prefix\"] = args[1]\n\t}\n\n\tvar result bool\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_startswith\", attrs, func(childCtx context.Context) error {\n\t\tif len(args) == 0 {\n\t\t\treturn errors.New(EmptyStringNotAllowedError(\"parameter to the startswith function\"))\n\t\t}\n\n\t\tstr := args[0]\n\t\tprefix := args[1]\n\n\t\tresult = strings.HasPrefix(str, prefix)\n\n\t\treturn nil\n\t})\n\n\treturn result, err\n}\n\n// EndsWith Implementation of Terraform's EndsWith function\n//\n//nolint:dupl\nfunc EndsWith(ctx context.Context, pctx *ParsingContext, args []string) (bool, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\tif len(args) > 0 {\n\t\tattrs[\"str\"] = args[0]\n\t}\n\n\tif len(args) > 1 {\n\t\tattrs[\"suffix\"] = args[1]\n\t}\n\n\tvar result bool\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_endswith\", attrs, func(childCtx context.Context) error {\n\t\tif len(args) == 0 {\n\t\t\treturn errors.New(EmptyStringNotAllowedError(\"parameter to the endswith function\"))\n\t\t}\n\n\t\tstr := args[0]\n\t\tsuffix := args[1]\n\n\t\tresult = strings.HasSuffix(str, suffix)\n\n\t\treturn nil\n\t})\n\n\treturn result, err\n}\n\n// TimeCmp implements Terraform's `timecmp` function that compares two timestamps.\nfunc TimeCmp(ctx context.Context, pctx *ParsingContext, l log.Logger, args []string) (int64, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\tif len(args) > 0 {\n\t\tattrs[\"timestamp_a\"] = args[0]\n\t}\n\n\tif len(args) > 1 {\n\t\tattrs[\"timestamp_b\"] = args[1]\n\t}\n\n\tvar result int64\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_timecmp\", attrs, func(childCtx context.Context) error {\n\t\tif len(args) != matchedPats {\n\t\t\treturn errors.New(errors.New(\"function can take only two parameters: timestamp_a and timestamp_b\"))\n\t\t}\n\n\t\ttsA, innerErr := util.ParseTimestamp(args[0])\n\t\tif innerErr != nil {\n\t\t\treturn errors.New(fmt.Errorf(\"could not parse first parameter %q: %w\", args[0], innerErr))\n\t\t}\n\n\t\ttsB, innerErr := util.ParseTimestamp(args[1])\n\t\tif innerErr != nil {\n\t\t\treturn errors.New(fmt.Errorf(\"could not parse second parameter %q: %w\", args[1], innerErr))\n\t\t}\n\n\t\tswitch {\n\t\tcase tsA.Equal(tsB):\n\t\t\tresult = 0\n\t\tcase tsA.Before(tsB):\n\t\t\tresult = -1\n\t\tdefault:\n\t\t\t// By elimination, tsA must be after tsB.\n\t\t\tresult = 1\n\t\t}\n\n\t\treturn nil\n\t})\n\n\treturn result, err\n}\n\n// StrContains Implementation of Terraform's StrContains function\n//\n//nolint:dupl\nfunc StrContains(ctx context.Context, pctx *ParsingContext, args []string) (bool, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\tif len(args) > 0 {\n\t\tattrs[\"str\"] = args[0]\n\t}\n\n\tif len(args) > 1 {\n\t\tattrs[\"substr\"] = args[1]\n\t}\n\n\tvar result bool\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_strcontains\", attrs, func(childCtx context.Context) error {\n\t\tif len(args) == 0 {\n\t\t\treturn errors.New(EmptyStringNotAllowedError(\"parameter to the strcontains function\"))\n\t\t}\n\n\t\tstr := args[0]\n\t\tsubstr := args[1]\n\n\t\tresult = strings.Contains(str, substr)\n\n\t\treturn nil\n\t})\n\n\treturn result, err\n}\n\n// readTFVarsFile reads a *.tfvars or *.tfvars.json file and returns the contents as a JSON encoded string\nfunc readTFVarsFile(ctx context.Context, pctx *ParsingContext, l log.Logger, args []string) (string, error) {\n\tvar filePath string\n\tif len(args) > 0 {\n\t\tfilePath = args[0]\n\t}\n\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t\t\"file_path\":   filePath,\n\t}\n\n\tvar result string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_read_tfvars_file\", attrs, func(childCtx context.Context) error {\n\t\tvar innerErr error\n\n\t\tresult, innerErr = readTFVarsFileImpl(pctx, l, args)\n\n\t\treturn innerErr\n\t})\n\n\treturn result, err\n}\n\n// readTFVarsFileImpl contains the actual implementation of readTFVarsFile\nfunc readTFVarsFileImpl(pctx *ParsingContext, l log.Logger, args []string) (string, error) {\n\tif len(args) != 1 {\n\t\treturn \"\", errors.New(WrongNumberOfParamsError{Func: \"read_tfvars_file\", Expected: \"1\", Actual: len(args)})\n\t}\n\n\tvarFile := args[0]\n\n\tif !filepath.IsAbs(varFile) {\n\t\tvarFile = filepath.Join(pctx.WorkingDir, varFile)\n\t\tvarFile = filepath.Clean(varFile)\n\t}\n\n\tif !util.FileExists(varFile) {\n\t\treturn \"\", errors.New(TFVarFileNotFoundError{File: varFile})\n\t}\n\n\t// Track that this file was read during parsing\n\ttrackFileRead(pctx.FilesRead, varFile)\n\n\tfileContents, err := os.ReadFile(varFile)\n\tif err != nil {\n\t\treturn \"\", errors.New(fmt.Errorf(\"could not read file %q: %w\", varFile, err))\n\t}\n\n\tif strings.HasSuffix(varFile, \"json\") {\n\t\tvar variables map[string]any\n\t\t// just want to be sure that the file is valid json\n\t\tif err := json.Unmarshal(fileContents, &variables); err != nil {\n\t\t\treturn \"\", errors.New(fmt.Errorf(\"could not unmarshal json body of tfvar file: %w\", err))\n\t\t}\n\n\t\treturn string(fileContents), nil\n\t}\n\n\tvar variables map[string]any\n\tif err := ParseAndDecodeVarFile(l, varFile, fileContents, &variables); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdata, err := json.Marshal(variables)\n\tif err != nil {\n\t\treturn \"\", errors.New(fmt.Errorf(\"could not marshal json body of tfvar file: %w\", err))\n\t}\n\n\treturn string(data), nil\n}\n\n// markAsRead marks a file as explicitly read. This is useful for detection via TerragruntUnitsReading flag.\nfunc markAsRead(ctx context.Context, pctx *ParsingContext, l log.Logger, args []string) (string, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\tif len(args) > 0 {\n\t\tattrs[\"file_path\"] = args[0]\n\t}\n\n\tvar result string\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_mark_as_read\", attrs, func(childCtx context.Context) error {\n\t\tif len(args) != 1 {\n\t\t\treturn errors.New(WrongNumberOfParamsError{Func: \"mark_as_read\", Expected: \"1\", Actual: len(args)})\n\t\t}\n\n\t\tfile := args[0]\n\n\t\t// Copy the file path to avoid modifying the original.\n\t\t// This is necessary so that the HCL function doesn't\n\t\t// return a different value than the original file path.\n\t\tpath := file\n\n\t\tif !filepath.IsAbs(path) {\n\t\t\tpath = filepath.Join(pctx.WorkingDir, path)\n\t\t\tpath = filepath.Clean(path)\n\t\t}\n\n\t\t// Track that this file was read during parsing\n\t\ttrackFileRead(pctx.FilesRead, path)\n\n\t\tresult = file\n\n\t\treturn nil\n\t})\n\n\treturn result, err\n}\n\n// warnWhenFileNotMarkedAsRead warns when a file is not being marked as read, even though a user might expect it to be.\n// Situations where this is the case include:\n// - A user specifies a file in the UnitsReading flag and that file is being read while parsing the inputs attribute.\n//\n// When the file is not marked as read, the function will return true, otherwise false.\n\n// ParseAndDecodeVarFile uses the HCL2 file to parse the given varfile string into an HCL file body, and then decode it\n// into the provided output.\nfunc ParseAndDecodeVarFile(l log.Logger, varFile string, fileContents []byte, out any) error {\n\tparser := hclparse.NewParser(hclparse.WithLogger(l))\n\n\tfile, err := parser.ParseFromBytes(fileContents, varFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tattrs, err := file.JustAttributes()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvalMap := map[string]cty.Value{}\n\n\tfor _, attr := range attrs {\n\t\tval, err := attr.Value(nil) // nil because no function calls or variable references are allowed here\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvalMap[attr.Name] = val\n\t}\n\n\tctyVal, err := ConvertValuesMapToCtyVal(valMap)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif ctyVal.IsNull() {\n\t\t// If the file is empty, doesn't make sense to do conversion\n\t\treturn nil\n\t}\n\n\ttypedOut, hasType := out.(*map[string]any)\n\tif hasType {\n\t\tgenericMap, err := ctyhelper.ParseCtyValueToMap(ctyVal)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t*typedOut = genericMap\n\n\t\treturn nil\n\t}\n\n\treturn gocty.FromCtyValue(ctyVal, out)\n}\n\n// extractSopsErrors extracts the original errors from the sops library and returns them as a errors.MultiError.\nfunc extractSopsErrors(err error) *errors.MultiError {\n\tvar errs = &errors.MultiError{}\n\n\t// workaround to extract original errors from sops library\n\t// using reflection extract GroupResults from getDataKeyError\n\t// may not be compatible with future versions\n\terrValue := reflect.ValueOf(err)\n\tif errValue.Kind() == reflect.Pointer {\n\t\terrValue = errValue.Elem()\n\t}\n\n\tif errValue.Type().Name() == \"getDataKeyError\" {\n\t\tgroupResultsField := errValue.FieldByName(\"GroupResults\")\n\t\tif groupResultsField.IsValid() && groupResultsField.Kind() == reflect.Slice {\n\t\t\tfor i := range groupResultsField.Len() {\n\t\t\t\tgroupErr := groupResultsField.Index(i)\n\t\t\t\tif groupErr.CanInterface() {\n\t\t\t\t\tif err, ok := groupErr.Interface().(error); ok {\n\t\t\t\t\t\terrs = errs.Append(err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// append the original error if no group results were found\n\tif errs.Len() == 0 {\n\t\terrs = errs.Append(err)\n\t}\n\n\treturn errs\n}\n\n// ConstraintCheck Implementation of Terraform's StartsWith function\nfunc ConstraintCheck(ctx context.Context, pctx *ParsingContext, args []string) (bool, error) {\n\tattrs := map[string]any{\n\t\t\"config_path\": pctx.TerragruntConfigPath,\n\t\t\"working_dir\": pctx.WorkingDir,\n\t}\n\tif len(args) > 0 {\n\t\tattrs[\"version\"] = args[0]\n\t}\n\n\tif len(args) > 1 {\n\t\tattrs[\"constraint\"] = args[1]\n\t}\n\n\tvar result bool\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, \"hcl_fn_constraint_check\", attrs, func(childCtx context.Context) error {\n\t\tif len(args) != matchedPats {\n\t\t\treturn errors.New(WrongNumberOfParamsError{Func: FuncNameConstraintCheck, Expected: \"2\", Actual: len(args)})\n\t\t}\n\n\t\tv, innerErr := version.NewSemver(args[0])\n\t\tif innerErr != nil {\n\t\t\treturn errors.Errorf(\"invalid version %s: %w\", args[0], innerErr)\n\t\t}\n\n\t\tc, innerErr := version.NewConstraint(args[1])\n\t\tif innerErr != nil {\n\t\t\treturn errors.Errorf(\"invalid constraint %s: %w\", args[1], innerErr)\n\t\t}\n\n\t\tresult = c.Check(v)\n\n\t\treturn nil\n\t})\n\n\treturn result, err\n}\n\n// trackFileRead adds a file path to the FilesRead slice if it's not already present.\n// This prevents duplicate entries when the same file is read multiple times during parsing.\nfunc trackFileRead(filesRead *[]string, path string) {\n\tif filesRead == nil {\n\t\treturn\n\t}\n\n\tif slices.Contains(*filesRead, path) {\n\t\treturn\n\t}\n\n\t*filesRead = append(*filesRead, path)\n}\n"
  },
  {
    "path": "pkg/config/config_helpers_test.go",
    "content": "package config_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/ctyhelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/engine\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tfimpl\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/puzpuzpuz/xsync/v3\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/zclconf/go-cty/cty\"\n)\n\n// assertErrorType checks that the error chain contains an error of the same type as expectedErr.\nfunc assertErrorType(t *testing.T, expectedErr, actualErr error) bool {\n\tt.Helper()\n\n\texpectedType := reflect.TypeOf(expectedErr)\n\n\tfor err := actualErr; err != nil; err = errors.Unwrap(err) {\n\t\tif reflect.TypeOf(err) == expectedType {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn assert.Fail(t, \"error type mismatch\", \"expected error of type %T in chain, but got %T\", expectedErr, actualErr)\n}\n\nfunc TestPathRelativeToInclude(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tinclude      map[string]config.IncludeConfig\n\t\tconfigPath   string\n\t\texpectedPath string\n\t\tparams       []string\n\t}{\n\t\t{\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: \".\",\n\t\t},\n\t\t{\n\t\t\tinclude:      map[string]config.IncludeConfig{\"\": {Path: filepath.Join(\"..\", config.DefaultTerragruntConfigPath)}},\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: \"child\",\n\t\t},\n\t\t{\n\t\t\tinclude:      map[string]config.IncludeConfig{\"\": {Path: filepath.Join(helpers.RootFolder, config.DefaultTerragruntConfigPath)}},\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: \"child\",\n\t\t},\n\t\t{\n\t\t\tinclude:      map[string]config.IncludeConfig{\"\": {Path: filepath.Join(\"../../..\", config.DefaultTerragruntConfigPath)}},\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", \"sub-child\", \"sub-sub-child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: \"child/sub-child/sub-sub-child\",\n\t\t},\n\t\t{\n\t\t\tinclude:      map[string]config.IncludeConfig{\"\": {Path: filepath.Join(helpers.RootFolder, config.DefaultTerragruntConfigPath)}},\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", \"sub-child\", \"sub-sub-child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: \"child/sub-child/sub-sub-child\",\n\t\t},\n\t\t{\n\t\t\tinclude:      map[string]config.IncludeConfig{\"\": {Path: filepath.Join(\"../..\", \"other-child\", config.DefaultTerragruntConfigPath)}},\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", \"sub-child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: \"../child/sub-child\",\n\t\t},\n\t\t{\n\t\t\tinclude:      map[string]config.IncludeConfig{\"\": {Path: filepath.Join(\"../..\", config.DefaultTerragruntConfigPath)}},\n\t\t\tconfigPath:   filepath.Join(\"..\", \"child\", \"sub-child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: \"child/sub-child\",\n\t\t},\n\t\t{\n\t\t\tinclude: map[string]config.IncludeConfig{\n\t\t\t\t\"root\":  {Path: filepath.Join(\"../..\", config.DefaultTerragruntConfigPath)},\n\t\t\t\t\"child\": {Path: filepath.Join(\"../..\", \"other-child\", config.DefaultTerragruntConfigPath)},\n\t\t\t},\n\t\t\tparams:       []string{\"child\"},\n\t\t\tconfigPath:   filepath.Join(\"..\", \"child\", \"sub-child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: \"../child/sub-child\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\ttrackInclude := getTrackIncludeFromTestData(tc.include, tc.params)\n\t\tl := logger.CreateLogger()\n\t\tctx, pctx := newTestParsingContext(t, tc.configPath)\n\t\tpctx = pctx.WithTrackInclude(trackInclude)\n\t\tactualPath, actualErr := config.PathRelativeToInclude(ctx, pctx, l, tc.params)\n\t\trequire.NoError(t, actualErr, \"For include %v and configPath %v, unexpected error: %v\", tc.include, tc.configPath, actualErr)\n\t\tassert.Equal(t, tc.expectedPath, actualPath, \"For include %v and configPath %v\", tc.include, tc.configPath)\n\t}\n}\n\nfunc TestPathRelativeFromInclude(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tinclude      map[string]config.IncludeConfig\n\t\tconfigPath   string\n\t\texpectedPath string\n\t\tparams       []string\n\t}{\n\t\t{\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: \".\",\n\t\t},\n\t\t{\n\t\t\tinclude:      map[string]config.IncludeConfig{\"\": {Path: filepath.Join(\"..\", config.DefaultTerragruntConfigPath)}},\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: \"..\",\n\t\t},\n\t\t{\n\t\t\tinclude:      map[string]config.IncludeConfig{\"\": {Path: filepath.Join(helpers.RootFolder, config.DefaultTerragruntConfigPath)}},\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: \"..\",\n\t\t},\n\t\t{\n\t\t\tinclude:      map[string]config.IncludeConfig{\"\": {Path: filepath.Join(\"../../..\", config.DefaultTerragruntConfigPath)}},\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", \"sub-child\", \"sub-sub-child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: \"../../..\",\n\t\t},\n\t\t{\n\t\t\tinclude:      map[string]config.IncludeConfig{\"\": {Path: filepath.Join(helpers.RootFolder, config.DefaultTerragruntConfigPath)}},\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", \"sub-child\", \"sub-sub-child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: \"../../..\",\n\t\t},\n\t\t{\n\t\t\tinclude:      map[string]config.IncludeConfig{\"\": {Path: filepath.Join(\"../..\", \"other-child\", config.DefaultTerragruntConfigPath)}},\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", \"sub-child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: \"../../other-child\",\n\t\t},\n\t\t{\n\t\t\tinclude:      map[string]config.IncludeConfig{\"\": {Path: filepath.Join(\"../..\", config.DefaultTerragruntConfigPath)}},\n\t\t\tconfigPath:   filepath.Join(\"..\", \"child\", \"sub-child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: \"../..\",\n\t\t},\n\t\t{\n\t\t\tinclude: map[string]config.IncludeConfig{\n\t\t\t\t\"root\":  {Path: filepath.Join(\"../..\", config.DefaultTerragruntConfigPath)},\n\t\t\t\t\"child\": {Path: filepath.Join(\"../..\", \"other-child\", config.DefaultTerragruntConfigPath)},\n\t\t\t},\n\t\t\tparams:       []string{\"child\"},\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", \"sub-child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: \"../../other-child\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\ttrackInclude := getTrackIncludeFromTestData(tc.include, tc.params)\n\t\tl := logger.CreateLogger()\n\t\tctx, pctx := newTestParsingContext(t, tc.configPath)\n\t\tpctx = pctx.WithTrackInclude(trackInclude)\n\t\tactualPath, actualErr := config.PathRelativeFromInclude(ctx, pctx, l, tc.params)\n\t\trequire.NoError(t, actualErr, \"For include %v and configPath %v, unexpected error: %v\", tc.include, tc.configPath, actualErr)\n\t\tassert.Equal(t, tc.expectedPath, actualPath, \"For include %v and configPath %v\", tc.include, tc.configPath)\n\t}\n}\n\nfunc TestRunCommand(t *testing.T) {\n\tt.Parallel()\n\n\tif runtime.GOOS == \"windows\" {\n\t\tt.Skip(\"Skipping test on Windows because it doesn't support bash\")\n\t}\n\n\thomeDir := os.Getenv(\"HOME\")\n\n\ttestCases := []struct {\n\t\texpectedErr    error\n\t\tconfigPath     string\n\t\texpectedOutput string\n\t\tparams         []string\n\t}{\n\t\t{\n\t\t\tparams:         []string{\"/bin/bash\", \"-c\", \"echo -n foo\"},\n\t\t\tconfigPath:     homeDir,\n\t\t\texpectedOutput: \"foo\",\n\t\t},\n\t\t{\n\t\t\tparams:         []string{\"/bin/bash\", \"-c\", \"echo foo\"},\n\t\t\tconfigPath:     homeDir,\n\t\t\texpectedOutput: \"foo\",\n\t\t},\n\t\t{\n\t\t\tparams:         []string{\"--terragrunt-quiet\", \"/bin/bash\", \"-c\", \"echo -n foo\"},\n\t\t\tconfigPath:     homeDir,\n\t\t\texpectedOutput: \"foo\",\n\t\t},\n\t\t{\n\t\t\tparams:         []string{\"--terragrunt-quiet\", \"/bin/bash\", \"-c\", \"echo foo\"},\n\t\t\tconfigPath:     homeDir,\n\t\t\texpectedOutput: \"foo\",\n\t\t},\n\t\t{\n\t\t\tparams:         []string{\"--terragrunt-global-cache\", \"/bin/bash\", \"-c\", \"echo foo\"},\n\t\t\tconfigPath:     homeDir,\n\t\t\texpectedOutput: \"foo\",\n\t\t},\n\t\t{\n\t\t\tparams:         []string{\"--terragrunt-global-cache\", \"--terragrunt-quiet\", \"/bin/bash\", \"-c\", \"echo foo\"},\n\t\t\tconfigPath:     homeDir,\n\t\t\texpectedOutput: \"foo\",\n\t\t},\n\t\t{\n\t\t\tparams:         []string{\"--terragrunt-quiet\", \"--terragrunt-global-cache\", \"/bin/bash\", \"-c\", \"echo foo\"},\n\t\t\tconfigPath:     homeDir,\n\t\t\texpectedOutput: \"foo\",\n\t\t},\n\t\t{\n\t\t\tparams:         []string{\"--terragrunt-no-cache\", \"/bin/bash\", \"-c\", \"echo foo\"},\n\t\t\tconfigPath:     homeDir,\n\t\t\texpectedOutput: \"foo\",\n\t\t},\n\t\t{\n\t\t\tparams:         []string{\"--terragrunt-no-cache\", \"--terragrunt-quiet\", \"/bin/bash\", \"-c\", \"echo foo\"},\n\t\t\tconfigPath:     homeDir,\n\t\t\texpectedOutput: \"foo\",\n\t\t},\n\t\t{\n\t\t\tparams:         []string{\"--terragrunt-quiet\", \"--terragrunt-no-cache\", \"/bin/bash\", \"-c\", \"echo foo\"},\n\t\t\tconfigPath:     homeDir,\n\t\t\texpectedOutput: \"foo\",\n\t\t},\n\t\t{\n\t\t\tparams:      []string{\"--terragrunt-no-cache\", \"--terragrunt-global-cache\", \"--terragrunt-quiet\", \"/bin/bash\", \"-c\", \"echo foo\"},\n\t\t\tconfigPath:  homeDir,\n\t\t\texpectedErr: config.ConflictingRunCmdCacheOptionsError{},\n\t\t},\n\t\t{\n\t\t\tparams:      []string{\"--terragrunt-global-cache\", \"--terragrunt-no-cache\", \"--terragrunt-quiet\", \"/bin/bash\", \"-c\", \"echo foo\"},\n\t\t\tconfigPath:  homeDir,\n\t\t\texpectedErr: config.ConflictingRunCmdCacheOptionsError{},\n\t\t},\n\t\t{\n\t\t\tconfigPath:  homeDir,\n\t\t\texpectedErr: config.EmptyStringNotAllowedError(\"{run_cmd()}\"),\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.configPath, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tl := logger.CreateLogger()\n\t\t\tctx, pctx := newTestParsingContext(t, tc.configPath)\n\n\t\t\tactualOutput, actualErr := config.RunCommand(ctx, pctx, l, tc.params)\n\t\t\tif tc.expectedErr != nil {\n\t\t\t\tif assert.Error(t, actualErr) {\n\t\t\t\t\tassertErrorType(t, tc.expectedErr, actualErr)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, actualErr)\n\t\t\t\tassert.Equal(t, tc.expectedOutput, actualOutput)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc absPath(t *testing.T, path string) string {\n\tt.Helper()\n\n\tif filepath.IsAbs(path) {\n\t\treturn filepath.Clean(path)\n\t}\n\n\tabsPath, err := filepath.Abs(path)\n\trequire.NoError(t, err)\n\n\treturn filepath.Clean(absPath)\n}\n\nfunc TestFindInParentFolders(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedErr       error\n\t\tconfigPath        string\n\t\tname              string\n\t\texpectedPath      string\n\t\tparams            []string\n\t\tmaxFoldersToCheck int\n\t}{\n\t\t{\n\t\t\tname:         \"simple-lookup\",\n\t\t\tparams:       []string{\"root.hcl\"},\n\t\t\tconfigPath:   absPath(t, filepath.Join(\"../..\", \"test\", \"fixtures\", \"parent-folders\", \"terragrunt-in-root\", \"child\", config.DefaultTerragruntConfigPath)),\n\t\t\texpectedPath: absPath(t, \"../../test/fixtures/parent-folders/terragrunt-in-root/root.hcl\"),\n\t\t},\n\t\t{\n\t\t\tname:         \"nested-lookup\",\n\t\t\tparams:       []string{\"root.hcl\"},\n\t\t\tconfigPath:   absPath(t, filepath.Join(\"../..\", \"test\", \"fixtures\", \"parent-folders\", \"terragrunt-in-root\", \"child\", \"sub-child\", \"sub-sub-child\", config.DefaultTerragruntConfigPath)),\n\t\t\texpectedPath: absPath(t, \"../../test/fixtures/parent-folders/terragrunt-in-root/root.hcl\"),\n\t\t},\n\t\t{\n\t\t\tname:              \"lookup-with-max-folders\",\n\t\t\tparams:            []string{\"root.hcl\"},\n\t\t\tconfigPath:        absPath(t, filepath.Join(\"../..\", \"test\", \"fixtures\", \"parent-folders\", \"no-terragrunt-in-root\", \"child\", \"sub-child\", config.DefaultTerragruntConfigPath)),\n\t\t\tmaxFoldersToCheck: 3,\n\t\t\texpectedErr:       config.ParentFileNotFoundError{},\n\t\t},\n\t\t{\n\t\t\tname:         \"multiple-terragrunt-in-parents\",\n\t\t\tparams:       []string{\"root.hcl\"},\n\t\t\tconfigPath:   absPath(t, filepath.Join(\"../..\", \"test\", \"fixtures\", \"parent-folders\", \"multiple-terragrunt-in-parents\", \"child\", config.DefaultTerragruntConfigPath)),\n\t\t\texpectedPath: absPath(t, \"../../test/fixtures/parent-folders/multiple-terragrunt-in-parents/root.hcl\"),\n\t\t},\n\t\t{\n\t\t\tname:         \"multiple-terragrunt-in-parents-under-child\",\n\t\t\tparams:       []string{\"root.hcl\"},\n\t\t\tconfigPath:   absPath(t, filepath.Join(\"../..\", \"test\", \"fixtures\", \"parent-folders\", \"multiple-terragrunt-in-parents\", \"child\", \"sub-child\", config.DefaultTerragruntConfigPath)),\n\t\t\texpectedPath: absPath(t, \"../../test/fixtures/parent-folders/multiple-terragrunt-in-parents/child/root.hcl\"),\n\t\t},\n\t\t{\n\t\t\tname:         \"multiple-terragrunt-in-parents-under-sub-child\",\n\t\t\tparams:       []string{\"root.hcl\"},\n\t\t\tconfigPath:   absPath(t, filepath.Join(\"../..\", \"test\", \"fixtures\", \"parent-folders\", \"multiple-terragrunt-in-parents\", \"child\", \"sub-child\", \"sub-sub-child\", config.DefaultTerragruntConfigPath)),\n\t\t\texpectedPath: absPath(t, \"../../test/fixtures/parent-folders/multiple-terragrunt-in-parents/child/sub-child/root.hcl\"),\n\t\t},\n\t\t{\n\t\t\tname:         \"parent-file-that-isnt-terragrunt\",\n\t\t\tparams:       []string{\"foo.txt\"},\n\t\t\tconfigPath:   absPath(t, filepath.Join(\"../..\", \"test\", \"fixtures\", \"parent-folders\", \"other-file-names\", \"child\", config.DefaultTerragruntConfigPath)),\n\t\t\texpectedPath: absPath(t, \"../../test/fixtures/parent-folders/other-file-names/foo.txt\"),\n\t\t},\n\t\t{\n\t\t\tname:         \"parent-file-that-isnt-terragrunt-in-another-subfolder\",\n\t\t\tparams:       []string{\"common/foo.txt\"},\n\t\t\tconfigPath:   absPath(t, filepath.Join(\"../..\", \"test\", \"fixtures\", \"parent-folders\", \"in-another-subfolder\", \"live\", config.DefaultTerragruntConfigPath)),\n\t\t\texpectedPath: absPath(t, \"../../test/fixtures/parent-folders/in-another-subfolder/common/foo.txt\"),\n\t\t},\n\t\t{\n\t\t\tname:         \"parent-file-that-isnt-terragrunt-in-another-subfolder-with-params\",\n\t\t\tparams:       []string{\"tfwork\"},\n\t\t\tconfigPath:   absPath(t, filepath.Join(\"../..\", \"test\", \"fixtures\", \"parent-folders\", \"with-params\", \"tfwork\", \"tg\", config.DefaultTerragruntConfigPath)),\n\t\t\texpectedPath: absPath(t, \"../../test/fixtures/parent-folders/with-params/tfwork\"),\n\t\t},\n\t\t{\n\t\t\tname:        \"not-found\",\n\t\t\tconfigPath:  \"/\",\n\t\t\texpectedErr: config.ParentFileNotFoundError{},\n\t\t},\n\t\t{\n\t\t\tname:        \"not-found-with-path\",\n\t\t\tconfigPath:  \"/fake/path\",\n\t\t\texpectedErr: config.ParentFileNotFoundError{},\n\t\t},\n\t\t{\n\t\t\tname:         \"fallback\",\n\t\t\tparams:       []string{\"foo.txt\", \"fallback.txt\"},\n\t\t\tconfigPath:   \"/fake/path\",\n\t\t\texpectedPath: \"fallback.txt\",\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.Parallel()\n\n\t\t\tl := logger.CreateLogger()\n\t\t\tctx, pctx := newTestParsingContext(t, tc.configPath)\n\n\t\t\tif tc.maxFoldersToCheck != 0 {\n\t\t\t\tpctx.MaxFoldersToCheck = tc.maxFoldersToCheck\n\t\t\t}\n\n\t\t\tactualPath, actualErr := config.FindInParentFolders(ctx, pctx, l, tc.params)\n\t\t\tif tc.expectedErr != nil {\n\t\t\t\tif assert.Error(t, actualErr) {\n\t\t\t\t\tassertErrorType(t, tc.expectedErr, actualErr)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, actualErr)\n\t\t\t\tassert.Equal(t, tc.expectedPath, actualPath)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFindInParentFoldersWithStackFile(t *testing.T) {\n\tt.Parallel()\n\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\n\tregionHclPath := filepath.Join(tempDir, \"region.hcl\")\n\tregionHclContent := `locals {\n  aws_region = \"us-east-1\"\n}`\n\terr := os.WriteFile(regionHclPath, []byte(regionHclContent), 0644)\n\trequire.NoError(t, err)\n\n\tstackDir := filepath.Join(tempDir, \"stack\")\n\terr = os.MkdirAll(stackDir, 0755)\n\trequire.NoError(t, err)\n\n\tstackHclPath := filepath.Join(stackDir, \"terragrunt.stack.hcl\")\n\tstackHclContent := `locals {\n  regions_vars = read_terragrunt_config(find_in_parent_folders(\"region.hcl\"))\n  region       = local.regions_vars.locals.aws_region\n}\n\nunit \"test\" {\n  source = \".\"\n  path   = \"test\"\n}`\n\terr = os.WriteFile(stackHclPath, []byte(stackHclContent), 0644)\n\trequire.NoError(t, err)\n\n\tl := logger.CreateLogger()\n\t_, pctx := newTestParsingContext(t, stackHclPath)\n\tpctx.WorkingDir = tempDir\n\n\tstackConfig, err := config.ReadStackConfigFile(t.Context(), l, pctx, stackHclPath, nil)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, stackConfig)\n\n\tregion, exists := stackConfig.Locals[\"region\"]\n\trequire.True(t, exists, \"Expected 'region' local to be parsed\")\n\trequire.Equal(t, \"us-east-1\", region)\n}\n\nfunc TestResolveTerragruntInterpolation(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tinclude           *config.IncludeConfig\n\t\tstr               string\n\t\tconfigPath        string\n\t\texpectedOut       string\n\t\texpectedErr       string\n\t\tmaxFoldersToCheck int\n\t}{\n\t\t{\n\t\t\tstr:         \"terraform { source = path_relative_to_include() }\",\n\t\t\tconfigPath:  filepath.Join(\"/root\", \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedOut: \".\",\n\t\t},\n\t\t{\n\t\t\tstr:         \"terraform { source = path_relative_to_include() }\",\n\t\t\tinclude:     &config.IncludeConfig{Path: filepath.Join(\"..\", config.DefaultTerragruntConfigPath)},\n\t\t\tconfigPath:  filepath.Join(\"/root\", \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedOut: \"child\",\n\t\t},\n\t\t{\n\t\t\tstr:         \"terraform { source = find_in_parent_folders(\\\"root.hcl\\\") }\",\n\t\t\tconfigPath:  absPath(t, filepath.Join(\"../..\", \"test\", \"fixtures\", \"parent-folders\", \"terragrunt-in-root\", \"child\", \"sub-child\", config.DefaultTerragruntConfigPath)),\n\t\t\texpectedOut: absPath(t, \"../../test/fixtures/parent-folders/terragrunt-in-root/root.hcl\"),\n\t\t},\n\t\t{\n\t\t\tstr:               \"terraform { source = find_in_parent_folders(\\\"root.hcl\\\") }\",\n\t\t\tconfigPath:        absPath(t, filepath.Join(\"../..\", \"test\", \"fixtures\", \"parent-folders\", \"terragrunt-in-root\", \"child\", \"sub-child\", config.DefaultTerragruntConfigPath)),\n\t\t\texpectedErr:       \"ParentFileNotFoundError\",\n\t\t\tmaxFoldersToCheck: 1,\n\t\t},\n\t\t{\n\t\t\tstr:               \"terraform { source = find_in_parent_folders(\\\"root.hcl\\\") }\",\n\t\t\tconfigPath:        absPath(t, filepath.Join(\"../..\", \"test\", \"fixtures\", \"parent-folders\", \"no-terragrunt-in-root\", \"child\", \"sub-child\", config.DefaultTerragruntConfigPath)),\n\t\t\texpectedErr:       \"ParentFileNotFoundError\",\n\t\t\tmaxFoldersToCheck: 3,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\t// The following is necessary to make sure tc's values don't\n\t\t// get updated due to concurrency within the scope of t.Run(..) below\n\t\tt.Run(fmt.Sprintf(\"%s--%s\", tc.str, tc.configPath), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tl := logger.CreateLogger()\n\t\t\tctx, pctx := newTestParsingContext(t, tc.configPath)\n\n\t\t\tif tc.maxFoldersToCheck != 0 {\n\t\t\t\tpctx.MaxFoldersToCheck = tc.maxFoldersToCheck\n\t\t\t}\n\n\t\t\tactualOut, actualErr := config.ParseConfigString(ctx, pctx, l, \"mock-path-for-test.hcl\", tc.str, tc.include)\n\t\t\tif tc.expectedErr != \"\" {\n\t\t\t\trequire.Error(t, actualErr)\n\t\t\t\tassert.Contains(t, actualErr.Error(), tc.expectedErr)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, actualErr)\n\t\t\t\tassert.NotNil(t, actualOut)\n\t\t\t\tassert.NotNil(t, actualOut.Terraform)\n\t\t\t\tassert.NotNil(t, actualOut.Terraform.Source)\n\t\t\t\tassert.Equal(t, tc.expectedOut, *actualOut.Terraform.Source)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestResolveEnvInterpolationConfigString(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tinclude     *config.IncludeConfig\n\t\tenv         map[string]string\n\t\tstr         string\n\t\tconfigPath  string\n\t\texpectedOut string\n\t\texpectedErr string\n\t}{\n\t\t{\n\t\t\tstr:         `iam_role = \"foo/${get_env()}/bar\"`,\n\t\t\tconfigPath:  filepath.Join(\"/root\", \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedErr: \"InvalidGetEnvParamsError\",\n\t\t},\n\t\t{\n\t\t\tstr:         `iam_role = \"foo/${get_env(\"\",\"\")}/bar\"`,\n\t\t\tconfigPath:  filepath.Join(\"/root\", \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedErr: \"InvalidEnvParamNameError\",\n\t\t},\n\t\t{\n\t\t\tstr:         `iam_role = get_env()`,\n\t\t\tconfigPath:  filepath.Join(\"/root\", \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedErr: \"InvalidGetEnvParamsError\",\n\t\t},\n\t\t{\n\t\t\tstr:         `iam_role = get_env(\"TEST_VAR_1\", \"TEST_VAR_2\", \"TEST_VAR_3\")`,\n\t\t\tconfigPath:  filepath.Join(\"/root\", \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedErr: \"InvalidGetEnvParamsError\",\n\t\t},\n\t\t{\n\t\t\tstr:         `iam_role = get_env(\"TEST_ENV_TERRAGRUNT_VAR\")`,\n\t\t\tconfigPath:  filepath.Join(\"/root\", \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedOut: \"SOMETHING\",\n\t\t\tenv:         map[string]string{\"TEST_ENV_TERRAGRUNT_VAR\": \"SOMETHING\"},\n\t\t},\n\t\t{\n\t\t\tstr:         `iam_role = get_env(\"SOME_VAR\", \"SOME_VALUE\")`,\n\t\t\tconfigPath:  filepath.Join(\"/root\", \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedOut: \"SOME_VALUE\",\n\t\t},\n\t\t{\n\t\t\tstr:         `iam_role = \"foo/${get_env(\"TEST_ENV_TERRAGRUNT_HIT\",\"\")}/bar\"`,\n\t\t\tconfigPath:  filepath.Join(\"/root\", \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedOut: \"foo//bar\",\n\t\t\tenv:         map[string]string{\"TEST_ENV_TERRAGRUNT_OTHER\": \"SOMETHING\"},\n\t\t},\n\t\t{\n\t\t\tstr:         `iam_role = \"foo/${get_env(\"TEST_ENV_TERRAGRUNT_HIT\",\"DEFAULT\")}/bar\"`,\n\t\t\tconfigPath:  filepath.Join(\"/root\", \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedOut: \"foo/DEFAULT/bar\",\n\t\t\tenv:         map[string]string{\"TEST_ENV_TERRAGRUNT_OTHER\": \"SOMETHING\"},\n\t\t},\n\t\t{\n\t\t\tstr:         `iam_role = \"foo/${get_env(\"TEST_ENV_TERRAGRUNT_VAR\")}/bar\"`,\n\t\t\tconfigPath:  filepath.Join(\"/root\", \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedOut: \"foo/SOMETHING/bar\",\n\t\t\tenv:         map[string]string{\"TEST_ENV_TERRAGRUNT_VAR\": \"SOMETHING\"},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\t// The following is necessary to make sure tc's values don't\n\t\t// get updated due to concurrency within the scope of t.Run(..) below\n\t\tt.Run(tc.str, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tl := logger.CreateLogger()\n\t\t\tctx, pctx := newTestParsingContext(t, tc.configPath)\n\n\t\t\tif tc.env != nil {\n\t\t\t\tpctx.Env = tc.env\n\t\t\t}\n\n\t\t\tactualOut, actualErr := config.ParseConfigString(ctx, pctx, l, \"mock-path-for-test.hcl\", tc.str, tc.include)\n\t\t\tif tc.expectedErr != \"\" {\n\t\t\t\trequire.Error(t, actualErr)\n\t\t\t\tassert.Contains(t, actualErr.Error(), tc.expectedErr)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, actualErr)\n\t\t\t\tassert.Equal(t, tc.expectedOut, actualOut.IamRole)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestResolveCommandsInterpolationConfigString(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tinclude          *config.IncludeConfig\n\t\tstr              string\n\t\tconfigPath       string\n\t\texpectedFooInput []string\n\t}{\n\t\t{\n\t\t\tstr:              \"inputs = { foo = get_terraform_commands_that_need_locking() }\",\n\t\t\tconfigPath:       config.DefaultTerragruntConfigPath,\n\t\t\texpectedFooInput: config.TerraformCommandsNeedLocking,\n\t\t},\n\t\t{\n\t\t\tstr:              `inputs = { foo = get_terraform_commands_that_need_vars() }`,\n\t\t\tconfigPath:       config.DefaultTerragruntConfigPath,\n\t\t\texpectedFooInput: config.TerraformCommandsNeedVars,\n\t\t},\n\t\t{\n\t\t\tstr:              \"inputs = { foo = get_terraform_commands_that_need_parallelism() }\",\n\t\t\tconfigPath:       config.DefaultTerragruntConfigPath,\n\t\t\texpectedFooInput: config.TerraformCommandsNeedParallelism,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\t// The following is necessary to make sure tc's values don't\n\t\t// get updated due to concurrency within the scope of t.Run(..) below\n\t\tt.Run(tc.str, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tl := logger.CreateLogger()\n\t\t\tctx, pctx := newTestParsingContext(t, tc.configPath)\n\t\t\tactualOut, actualErr := config.ParseConfigString(ctx, pctx, l, \"mock-path-for-test.hcl\", tc.str, tc.include)\n\t\t\trequire.NoError(t, actualErr, \"For string '%s' include %v and configPath %v, unexpected error: %v\", tc.str, tc.include, tc.configPath, actualErr)\n\n\t\t\tassert.NotNil(t, actualOut)\n\n\t\t\tinputs := actualOut.Inputs\n\t\t\tassert.NotNil(t, inputs)\n\n\t\t\tfoo, containsFoo := inputs[\"foo\"]\n\t\t\tassert.True(t, containsFoo)\n\n\t\t\tfooSlice := toStringSlice(t, foo)\n\n\t\t\tassert.Equal(t, tc.expectedFooInput, fooSlice, \"For string '%s' include %v and configPath %v\", tc.str, tc.include, tc.configPath)\n\t\t})\n\t}\n}\n\nfunc TestResolveCliArgsInterpolationConfigString(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, cliArgs := range [][]string{nil, {}, {\"apply\"}, {\"plan\", \"-out=planfile\"}} {\n\t\texpectedFooInput := cliArgs\n\t\t// Expecting nil to be returned for get_terraform_cli_args() call for\n\t\t// either nil or empty array of input args\n\t\tif len(cliArgs) == 0 {\n\t\t\texpectedFooInput = nil\n\t\t}\n\n\t\tstr := \"inputs = { foo = get_terraform_cli_args() }\"\n\t\tt.Run(str, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tl := logger.CreateLogger()\n\t\t\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\t\t\tpctx.TerraformCliArgs = iacargs.New(cliArgs...)\n\n\t\t\tactualOut, actualErr := config.ParseConfigString(ctx, pctx, l, \"mock-path-for-test.hcl\", str, nil)\n\t\t\trequire.NoError(t, actualErr, \"For string '%s', unexpected error: %v\", str, actualErr)\n\n\t\t\tassert.NotNil(t, actualOut)\n\n\t\t\tinputs := actualOut.Inputs\n\t\t\tassert.NotNil(t, inputs)\n\n\t\t\tfoo, containsFoo := inputs[\"foo\"]\n\t\t\tassert.True(t, containsFoo)\n\n\t\t\tfooSlice := toStringSlice(t, foo)\n\t\t\tassert.Equal(t, expectedFooInput, fooSlice, \"For string '%s'\", str)\n\t\t})\n\t}\n}\n\nfunc toStringSlice(t *testing.T, value any) []string {\n\tt.Helper()\n\n\tif value == nil {\n\t\treturn nil\n\t}\n\n\tasInterfaceSlice, isInterfaceSlice := value.([]any)\n\tassert.True(t, isInterfaceSlice)\n\n\t// TODO: See if this logic is desired\n\tif len(asInterfaceSlice) == 0 {\n\t\treturn nil\n\t}\n\n\tvar out = make([]string, 0, len(asInterfaceSlice))\n\tfor _, item := range asInterfaceSlice {\n\t\tasStr, isStr := item.(string)\n\t\tassert.True(t, isStr)\n\n\t\tout = append(out, asStr)\n\t}\n\n\treturn out\n}\n\nfunc TestGetTerragruntDirAbsPath(t *testing.T) {\n\tt.Parallel()\n\n\tworkingDir, err := os.Getwd()\n\trequire.NoError(t, err, \"Could not get current working dir: %v\", err)\n\ttestGetTerragruntDir(t, \"/foo/bar/terragrunt.hcl\", filepath.VolumeName(workingDir)+\"/foo/bar\")\n}\n\nfunc TestGetTerragruntDirRelPath(t *testing.T) {\n\tt.Parallel()\n\n\ttestGetTerragruntDir(t, \"foo/bar/terragrunt.hcl\", filepath.Join(\"foo\", \"bar\"))\n}\n\nfunc testGetTerragruntDir(t *testing.T, configPath string, expectedPath string) {\n\tt.Helper()\n\n\tl := logger.CreateLogger()\n\tctx, pctx := newTestParsingContext(t, configPath)\n\tactualPath, err := config.GetTerragruntDir(ctx, pctx, l)\n\n\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n\tassert.Equal(t, expectedPath, actualPath)\n}\n\n// newTestParsingContext creates a ParsingContext with sensible test defaults.\n// Replicates NewTerragruntOptionsForTest + configbridge.populateFromOpts.\nfunc newTestParsingContext(tb testing.TB, configPath string) (context.Context, *config.ParsingContext) {\n\ttb.Helper()\n\n\tl := logger.CreateLogger()\n\tctx, pctx := config.NewParsingContext(tb.Context(), l, config.WithStrictControls(controls.New()))\n\n\tworkingDir, downloadDir := util.DefaultWorkingAndDownloadDirs(configPath)\n\n\tpctx.TerragruntConfigPath = configPath\n\tpctx.WorkingDir = workingDir\n\tpctx.RootWorkingDir = workingDir\n\tpctx.DownloadDir = downloadDir\n\tpctx.TFPath = \"tofu\"\n\tpctx.AutoInit = true\n\tpctx.Env = map[string]string{}\n\tpctx.SourceMap = map[string]string{}\n\tpctx.TerraformCliArgs = iacargs.New()\n\tpctx.Writers.Writer = os.Stdout\n\tpctx.Writers.ErrWriter = os.Stderr\n\tpctx.MaxFoldersToCheck = 100\n\tpctx.TofuImplementation = tfimpl.Unknown\n\tpctx.Experiments = experiment.NewExperiments()\n\tpctx.Telemetry = new(telemetry.Options)\n\tpctx.EngineOptions = new(engine.EngineOptions)\n\tpctx.FeatureFlags = xsync.NewMapOf[string, string]()\n\n\treturn ctx, pctx\n}\n\nfunc TestGetParentTerragruntDir(t *testing.T) {\n\tt.Parallel()\n\n\tcurrentDir, err := os.Getwd()\n\trequire.NoError(t, err, \"Could not get current working dir: %v\", err)\n\n\tparentDir := filepath.Dir(currentDir)\n\n\ttestCases := []struct {\n\t\tinclude      map[string]config.IncludeConfig\n\t\tconfigPath   string\n\t\texpectedPath string\n\t\tparams       []string\n\t}{\n\t\t{\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: filepath.Join(helpers.RootFolder, \"child\"),\n\t\t},\n\t\t{\n\t\t\tinclude:      map[string]config.IncludeConfig{\"\": {Path: filepath.Join(\"..\", config.DefaultTerragruntConfigPath)}},\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: helpers.RootFolder,\n\t\t},\n\t\t{\n\t\t\tinclude:      map[string]config.IncludeConfig{\"\": {Path: filepath.Join(helpers.RootFolder, config.DefaultTerragruntConfigPath)}},\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: helpers.RootFolder,\n\t\t},\n\t\t{\n\t\t\tinclude:      map[string]config.IncludeConfig{\"\": {Path: filepath.Join(\"../../..\", config.DefaultTerragruntConfigPath)}},\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", \"sub-child\", \"sub-sub-child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: helpers.RootFolder,\n\t\t},\n\t\t{\n\t\t\tinclude:      map[string]config.IncludeConfig{\"\": {Path: filepath.Join(helpers.RootFolder, config.DefaultTerragruntConfigPath)}},\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", \"sub-child\", \"sub-sub-child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: helpers.RootFolder,\n\t\t},\n\t\t{\n\t\t\tinclude:      map[string]config.IncludeConfig{\"\": {Path: filepath.Join(\"../..\", \"other-child\", config.DefaultTerragruntConfigPath)}},\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", \"sub-child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: filepath.VolumeName(parentDir) + \"/other-child\",\n\t\t},\n\t\t{\n\t\t\tinclude:      map[string]config.IncludeConfig{\"\": {Path: filepath.Join(\"../..\", config.DefaultTerragruntConfigPath)}},\n\t\t\tconfigPath:   filepath.Join(\"..\", \"child\", \"sub-child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: \"..\",\n\t\t},\n\t\t{\n\t\t\tinclude: map[string]config.IncludeConfig{\n\t\t\t\t\"root\":  {Path: filepath.Join(\"../..\", config.DefaultTerragruntConfigPath)},\n\t\t\t\t\"child\": {Path: filepath.Join(\"../..\", \"other-child\", config.DefaultTerragruntConfigPath)},\n\t\t\t},\n\t\t\tparams:       []string{\"child\"},\n\t\t\tconfigPath:   filepath.Join(helpers.RootFolder, \"child\", \"sub-child\", config.DefaultTerragruntConfigPath),\n\t\t\texpectedPath: filepath.VolumeName(parentDir) + \"/other-child\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\ttrackInclude := getTrackIncludeFromTestData(tc.include, tc.params)\n\t\tl := logger.CreateLogger()\n\t\tctx, pctx := newTestParsingContext(t, tc.configPath)\n\t\tpctx = pctx.WithTrackInclude(trackInclude)\n\t\tactualPath, actualErr := config.GetParentTerragruntDir(ctx, pctx, l, tc.params)\n\t\trequire.NoError(t, actualErr, \"For include %v and configPath %v, unexpected error: %v\", tc.include, tc.configPath, actualErr)\n\t\tassert.Equal(t, tc.expectedPath, actualPath, \"For include %v and configPath %v\", tc.include, tc.configPath)\n\t}\n}\n\nfunc TestTerraformBuiltInFunctions(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpected any\n\t\tinput    string\n\t}{\n\t\t{\n\t\t\tinput:    \"abs(-1)\",\n\t\t\texpected: 1.,\n\t\t},\n\t\t{\n\t\t\tinput:    `element([\"one\", \"two\", \"three\"], 1)`,\n\t\t\texpected: \"two\",\n\t\t},\n\t\t{\n\t\t\tinput:    `chomp(file(\"other-file.txt\"))`,\n\t\t\texpected: \"This is a test file\",\n\t\t},\n\t\t{\n\t\t\tinput:    `sha1(\"input\")`,\n\t\t\texpected: \"140f86aae51ab9e1cda9b4254fe98a74eb54c1a1\",\n\t\t},\n\t\t{\n\t\t\tinput:    `split(\"|\", \"one|two|three\")`,\n\t\t\texpected: []any{\"one\", \"two\", \"three\"},\n\t\t},\n\t\t{\n\t\t\tinput:    `!tobool(\"false\")`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tinput:    `trimspace(\"     content     \")`,\n\t\t\texpected: \"content\",\n\t\t},\n\t\t{\n\t\t\tinput:    `zipmap([\"one\", \"two\", \"three\"], [1, 2, 3])`,\n\t\t\texpected: map[string]any{\"one\": 1., \"two\": 2., \"three\": 3.},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.input, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tcfgPath := filepath.Join(\"../..\", \"test\", \"fixtures\", \"config-terraform-functions\", config.DefaultTerragruntConfigPath)\n\t\t\tconfigString := fmt.Sprintf(\"inputs = { test = %s }\", tc.input)\n\t\t\tl := logger.CreateLogger()\n\t\t\tctx, pctx := newTestParsingContext(t, cfgPath)\n\t\t\tactual, err := config.ParseConfigString(ctx, pctx, l, cfgPath, configString, nil)\n\t\t\trequire.NoError(t, err, \"For hcl '%s', unexpected error: %v\", tc.input, err)\n\n\t\t\tassert.NotNil(t, actual)\n\n\t\t\tinputs := actual.Inputs\n\t\t\tassert.NotNil(t, inputs)\n\n\t\t\ttest, containsTest := inputs[\"test\"]\n\t\t\tassert.True(t, containsTest)\n\n\t\t\tassert.Equal(t, tc.expected, test, \"For hcl '%s'\", tc.input)\n\t\t})\n\t}\n}\n\nfunc TestTerraformOutputJsonToCtyValueMap(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpected map[string]cty.Value\n\t\tinput    string\n\t}{\n\t\t{\n\t\t\tinput:    `{\"bool\": {\"sensitive\": false, \"type\": \"bool\", \"value\": true}}`,\n\t\t\texpected: map[string]cty.Value{\"bool\": cty.True},\n\t\t},\n\t\t{\n\t\t\tinput:    `{\"number\": {\"sensitive\": false, \"type\": \"number\", \"value\": 42}}`,\n\t\t\texpected: map[string]cty.Value{\"number\": cty.NumberIntVal(42)},\n\t\t},\n\t\t{\n\t\t\tinput:    `{\"list_string\": {\"sensitive\": false, \"type\": [\"list\", \"string\"], \"value\": [\"4\", \"2\"]}}`,\n\t\t\texpected: map[string]cty.Value{\"list_string\": cty.ListVal([]cty.Value{cty.StringVal(\"4\"), cty.StringVal(\"2\")})},\n\t\t},\n\t\t{\n\t\t\tinput:    `{\"map_string\": {\"sensitive\": false, \"type\": [\"map\", \"string\"], \"value\": {\"x\": \"foo\", \"y\": \"bar\"}}}`,\n\t\t\texpected: map[string]cty.Value{\"map_string\": cty.MapVal(map[string]cty.Value{\"x\": cty.StringVal(\"foo\"), \"y\": cty.StringVal(\"bar\")})},\n\t\t},\n\t\t{\n\t\t\tinput: `{\"map_list_number\": {\"sensitive\": false, \"type\": [\"map\", [\"list\", \"number\"]], \"value\": {\"x\": [4, 2]}}}`,\n\t\t\texpected: map[string]cty.Value{\n\t\t\t\t\"map_list_number\": cty.MapVal(\n\t\t\t\t\tmap[string]cty.Value{\n\t\t\t\t\t\t\"x\": cty.ListVal([]cty.Value{cty.NumberIntVal(4), cty.NumberIntVal(2)}),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tinput: `{\"object\": {\"sensitive\": false, \"type\": [\"object\", {\"x\": \"number\", \"y\": \"string\", \"lst\": [\"list\", \"string\"]}], \"value\": {\"x\": 42, \"y\": \"the truth\", \"lst\": [\"foo\", \"bar\"]}}}`,\n\t\t\texpected: map[string]cty.Value{\n\t\t\t\t\"object\": cty.ObjectVal(\n\t\t\t\t\tmap[string]cty.Value{\n\t\t\t\t\t\t\"x\":   cty.NumberIntVal(42),\n\t\t\t\t\t\t\"y\":   cty.StringVal(\"the truth\"),\n\t\t\t\t\t\t\"lst\": cty.ListVal([]cty.Value{cty.StringVal(\"foo\"), cty.StringVal(\"bar\")}),\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tinput: `{\"out1\": {\"sensitive\": false, \"type\": \"number\", \"value\": 42}, \"out2\": {\"sensitive\": false, \"type\": \"string\", \"value\": \"foo bar\"}}`,\n\t\t\texpected: map[string]cty.Value{\n\t\t\t\t\"out1\": cty.NumberIntVal(42),\n\t\t\t\t\"out2\": cty.StringVal(\"foo bar\"),\n\t\t\t},\n\t\t},\n\t}\n\n\tmockTargetConfig := config.DefaultTerragruntConfigPath\n\tfor _, tc := range testCases {\n\t\tconverted, err := config.TerraformOutputJSONToCtyValueMap(mockTargetConfig, []byte(tc.input))\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, getKeys(converted), getKeys(tc.expected))\n\n\t\tfor k, v := range converted {\n\t\t\tassert.True(t, v.Equals(tc.expected[k]).True())\n\t\t}\n\t}\n}\n\nfunc TestReadTerragruntConfigInputs(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\ttgConfigCty, err := config.ParseTerragruntConfig(ctx, pctx, l, \"../../test/fixtures/inputs/terragrunt.hcl\", nil)\n\trequire.NoError(t, err)\n\n\ttgConfigMap, err := ctyhelper.ParseCtyValueToMap(tgConfigCty)\n\trequire.NoError(t, err)\n\n\tinputsMap := tgConfigMap[\"inputs\"].(map[string]any)\n\n\tassert.Equal(t, \"string\", inputsMap[\"string\"].(string))\n\tassert.InEpsilon(t, float64(42), inputsMap[\"number\"].(float64), 0.0000000001)\n\tassert.True(t, inputsMap[\"bool\"].(bool))\n\tassert.Equal(t, []any{\"a\", \"b\", \"c\"}, inputsMap[\"list_string\"].([]any))\n\tassert.Equal(t, []any{float64(1), float64(2), float64(3)}, inputsMap[\"list_number\"].([]any))\n\tassert.Equal(t, []any{true, false}, inputsMap[\"list_bool\"].([]any))\n\tassert.Equal(t, map[string]any{\"foo\": \"bar\"}, inputsMap[\"map_string\"].(map[string]any))\n\tassert.Equal(t, map[string]any{\"foo\": float64(42), \"bar\": float64(12345)}, inputsMap[\"map_number\"].(map[string]any))\n\tassert.Equal(t, map[string]any{\"foo\": true, \"bar\": false, \"baz\": true}, inputsMap[\"map_bool\"].(map[string]any))\n\n\tassert.Equal(\n\t\tt,\n\t\tmap[string]any{\n\t\t\t\"str\":  \"string\",\n\t\t\t\"num\":  float64(42),\n\t\t\t\"list\": []any{float64(1), float64(2), float64(3)},\n\t\t\t\"map\":  map[string]any{\"foo\": \"bar\"},\n\t\t},\n\t\tinputsMap[\"object\"].(map[string]any),\n\t)\n\n\tassert.Equal(t, \"default\", inputsMap[\"from_env\"].(string))\n}\n\nfunc TestReadTerragruntConfigRemoteState(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\ttgConfigCty, err := config.ParseTerragruntConfig(ctx, pctx, l, \"../../test/fixtures/terragrunt/terragrunt.hcl\", nil)\n\trequire.NoError(t, err)\n\n\ttgConfigMap, err := ctyhelper.ParseCtyValueToMap(tgConfigCty)\n\trequire.NoError(t, err)\n\n\tremoteStateMap := tgConfigMap[\"remote_state\"].(map[string]any)\n\tassert.Equal(t, \"s3\", remoteStateMap[\"backend\"].(string))\n\tconfigMap := remoteStateMap[\"config\"].(map[string]any)\n\tassert.True(t, configMap[\"encrypt\"].(bool))\n\tassert.Equal(t, \"terraform.tfstate\", configMap[\"key\"].(string))\n\tassert.Equal(\n\t\tt,\n\t\tmap[string]any{\"owner\": \"terragrunt integration test\", \"name\": \"Terraform state storage\"},\n\t\tconfigMap[\"s3_bucket_tags\"].(map[string]any),\n\t)\n\tassert.Equal(\n\t\tt,\n\t\tmap[string]any{\"owner\": \"terragrunt integration test\", \"name\": \"Terraform lock table\"},\n\t\tconfigMap[\"dynamodb_table_tags\"].(map[string]any),\n\t)\n\tassert.Equal(\n\t\tt,\n\t\tmap[string]any{\"owner\": \"terragrunt integration test\", \"name\": \"Terraform access log storage\"},\n\t\tconfigMap[\"accesslogging_bucket_tags\"].(map[string]any),\n\t)\n}\n\nfunc TestReadTerragruntConfigHooks(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\ttgConfigCty, err := config.ParseTerragruntConfig(ctx, pctx, l, \"../../test/fixtures/hooks/before-after-and-on-error/terragrunt.hcl\", nil)\n\trequire.NoError(t, err)\n\n\ttgConfigMap, err := ctyhelper.ParseCtyValueToMap(tgConfigCty)\n\trequire.NoError(t, err)\n\n\tterraformMap := tgConfigMap[\"terraform\"].(map[string]any)\n\tbeforeHooksMap := terraformMap[\"before_hook\"].(map[string]any)\n\tassert.Equal(\n\t\tt,\n\t\t[]any{\"touch\", \"before.out\"},\n\t\tbeforeHooksMap[\"before_hook_1\"].(map[string]any)[\"execute\"].([]any),\n\t)\n\tassert.Equal(\n\t\tt,\n\t\t[]any{\"echo\", \"BEFORE_TERRAGRUNT_READ_CONFIG\"},\n\t\tbeforeHooksMap[\"before_hook_2\"].(map[string]any)[\"execute\"].([]any),\n\t)\n\n\tafterHooksMap := terraformMap[\"after_hook\"].(map[string]any)\n\tassert.Equal(\n\t\tt,\n\t\t[]any{\"touch\", \"after.out\"},\n\t\tafterHooksMap[\"after_hook_1\"].(map[string]any)[\"execute\"].([]any),\n\t)\n\tassert.Equal(\n\t\tt,\n\t\t[]any{\"echo\", \"AFTER_TERRAGRUNT_READ_CONFIG\"},\n\t\tafterHooksMap[\"after_hook_2\"].(map[string]any)[\"execute\"].([]any),\n\t)\n\n\terrorHooksMap := terraformMap[\"error_hook\"].(map[string]any)\n\tassert.Equal(\n\t\tt,\n\t\t[]any{\"echo\", \"ON_APPLY_ERROR\"},\n\t\terrorHooksMap[\"error_hook_1\"].(map[string]any)[\"execute\"].([]any),\n\t)\n}\n\nfunc TestReadTerragruntConfigLocals(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\ttgConfigCty, err := config.ParseTerragruntConfig(ctx, pctx, l, \"../../test/fixtures/locals/canonical/terragrunt.hcl\", nil)\n\trequire.NoError(t, err)\n\n\ttgConfigMap, err := ctyhelper.ParseCtyValueToMap(tgConfigCty)\n\trequire.NoError(t, err)\n\n\tlocalsMap := tgConfigMap[\"locals\"].(map[string]any)\n\tassert.InEpsilon(t, float64(2), localsMap[\"x\"].(float64), 0.0000000001)\n\tassert.Equal(t, \"Hello world\", strings.TrimSpace(localsMap[\"file_contents\"].(string)))\n\tassert.InEpsilon(t, float64(42), localsMap[\"number_expression\"].(float64), 0.0000000001)\n}\n\nfunc TestGetTerragruntSourceForModuleHappyPath(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tconfig   *config.TerragruntConfig\n\t\tsource   string\n\t\texpected string\n\t}{\n\t\t{config: mockConfigWithSource(\"\"), source: \"\", expected: \"\"},\n\t\t{config: mockConfigWithSource(\"\"), source: \"/source/modules\", expected: \"\"},\n\t\t{config: mockConfigWithSource(\"git::git@github.com:acme/modules.git//foo/bar\"), source: \"/source/modules\", expected: \"/source/modules//foo/bar\"},\n\t\t{config: mockConfigWithSource(\"git::git@github.com:acme/modules.git//foo/bar?ref=v0.0.1\"), source: \"/source/modules\", expected: \"/source/modules//foo/bar\"},\n\t\t{config: mockConfigWithSource(\"git::git@github.com:acme/emr_cluster.git?ref=feature/fix_bugs\"), source: \"/source/modules\", expected: \"/source/modules//emr_cluster\"},\n\t\t{config: mockConfigWithSource(\"git::ssh://git@ghe.ourcorp.com/OurOrg/some-module.git\"), source: \"/source/modules\", expected: \"/source/modules//some-module\"},\n\t\t{config: mockConfigWithSource(\"github.com/hashicorp/example\"), source: \"/source/modules\", expected: \"/source/modules//example\"},\n\t\t{config: mockConfigWithSource(\"github.com/hashicorp/example//subdir\"), source: \"/source/modules\", expected: \"/source/modules//subdir\"},\n\t\t{config: mockConfigWithSource(\"git@github.com:hashicorp/example.git//subdir\"), source: \"/source/modules\", expected: \"/source/modules//subdir\"},\n\t\t{config: mockConfigWithSource(\"./some/path//to/modulename\"), source: \"/source/modules\", expected: \"/source/modules//to/modulename\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"%v-%s\", *tc.config.Terraform.Source, tc.source), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tactual, err := config.GetTerragruntSourceForModule(tc.source, \"mock-for-test\", tc.config)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestStartsWith(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\targs     []string\n\t\texpected bool\n\t}{\n\t\t{args: []string{\"hello world\", \"hello\"}, expected: true},\n\t\t{args: []string{\"hello world\", \"world\"}, expected: false},\n\t\t{args: []string{\"hello world\", \"\"}, expected: true},\n\t\t{args: []string{\"hello world\", \" \"}, expected: false},\n\t\t{args: []string{\"\", \"\"}, expected: true},\n\t\t{args: []string{\"\", \" \"}, expected: false},\n\t\t{args: []string{\" \", \"\"}, expected: true},\n\t\t{args: []string{\"\", \"hello\"}, expected: false},\n\t\t{args: []string{\" \", \"hello\"}, expected: false},\n\t}\n\n\tfor id, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"%v %v\", id, tc.args), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tctx, pctx := newTestParsingContext(t, \"\")\n\t\t\tactual, err := config.StartsWith(ctx, pctx, tc.args)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestEndsWith(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\targs     []string\n\t\texpected bool\n\t}{\n\t\t{args: []string{\"hello world\", \"world\"}, expected: true},\n\t\t{args: []string{\"hello world\", \"hello\"}, expected: false},\n\t\t{args: []string{\"hello world\", \"\"}, expected: true},\n\t\t{args: []string{\"hello world\", \" \"}, expected: false},\n\t\t{args: []string{\"\", \"\"}, expected: true},\n\t\t{args: []string{\"\", \" \"}, expected: false},\n\t\t{args: []string{\" \", \"\"}, expected: true},\n\t\t{args: []string{\"\", \"hello\"}, expected: false},\n\t\t{args: []string{\" \", \"hello\"}, expected: false},\n\t}\n\n\tfor id, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"%v %v\", id, tc.args), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tctx, pctx := newTestParsingContext(t, \"\")\n\t\t\tactual, err := config.EndsWith(ctx, pctx, tc.args)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, tc.expected, actual)\n\t\t})\n\t}\n}\n\nfunc TestTimeCmp(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\terr   string\n\t\targs  []string\n\t\tvalue int64\n\t}{\n\t\t{\n\t\t\targs: []string{\"2017-11-22T00:00:00Z\", \"2017-11-22T00:00:00Z\"},\n\t\t},\n\t\t{\n\t\t\targs: []string{\"2017-11-22T00:00:00Z\", \"2017-11-22T01:00:00+01:00\"},\n\t\t},\n\t\t{\n\t\t\targs:  []string{\"2017-11-22T00:00:01Z\", \"2017-11-22T01:00:00+01:00\"},\n\t\t\tvalue: 1,\n\t\t},\n\t\t{\n\t\t\targs:  []string{\"2017-11-22T01:00:00Z\", \"2017-11-22T00:59:00-01:00\"},\n\t\t\tvalue: -1,\n\t\t},\n\t\t{\n\t\t\targs:  []string{\"2017-11-22T01:00:00+01:00\", \"2017-11-22T01:00:00-01:00\"},\n\t\t\tvalue: -1,\n\t\t},\n\t\t{\n\t\t\targs:  []string{\"2017-11-22T01:00:00-01:00\", \"2017-11-22T01:00:00+01:00\"},\n\t\t\tvalue: 1,\n\t\t},\n\t\t{\n\t\t\targs: []string{\"2017-11-22T00:00:00Z\", \"bloop\"},\n\t\t\terr:  `could not parse second parameter \"bloop\": not a valid RFC3339 timestamp: cannot use \"bloop\" as year`,\n\t\t},\n\t\t{\n\t\t\targs: []string{\"2017-11-22 00:00:00Z\", \"2017-11-22T00:00:00Z\"},\n\t\t\terr:  `could not parse first parameter \"2017-11-22 00:00:00Z\": not a valid RFC3339 timestamp: missing required time introducer 'T'`,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"TimeCmp(%#v, %#v)\", tc.args[0], tc.args[1]), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tl := logger.CreateLogger()\n\t\t\tctx, pctx := newTestParsingContext(t, \"\")\n\n\t\t\tactual, err := config.TimeCmp(ctx, pctx, l, tc.args)\n\t\t\tif tc.err != \"\" {\n\t\t\t\trequire.EqualError(t, err, tc.err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.value, actual)\n\t\t})\n\t}\n}\n\nfunc TestStrContains(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\terr   string\n\t\targs  []string\n\t\tvalue bool\n\t}{\n\t\t{\n\t\t\targs:  []string{\"hello world\", \"hello\"},\n\t\t\tvalue: true,\n\t\t},\n\t\t{\n\t\t\targs:  []string{\"hello world\", \"world\"},\n\t\t\tvalue: true,\n\t\t},\n\t\t{\n\t\t\targs:  []string{\"hello world0\", \"0\"},\n\t\t\tvalue: true,\n\t\t},\n\t\t{\n\t\t\targs: []string{\"hello world\", \"test\"},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"StrContains %v\", tc.args), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tctx, pctx := newTestParsingContext(t, \"\")\n\n\t\t\tactual, err := config.StrContains(ctx, pctx, tc.args)\n\t\t\tif tc.err != \"\" {\n\t\t\t\trequire.EqualError(t, err, tc.err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.value, actual)\n\t\t})\n\t}\n}\n\nfunc TestReadTFVarsFiles(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\ttgConfigCty, err := config.ParseTerragruntConfig(ctx, pctx, l, \"../../test/fixtures/read-tf-vars/terragrunt.hcl\", nil)\n\trequire.NoError(t, err)\n\n\ttgConfigMap, err := ctyhelper.ParseCtyValueToMap(tgConfigCty)\n\trequire.NoError(t, err)\n\n\tlocals := tgConfigMap[\"locals\"].(map[string]any)\n\n\tassert.Equal(t, \"string\", locals[\"string_var\"].(string))\n\tassert.InEpsilon(t, float64(42), locals[\"number_var\"].(float64), 0.0000000001)\n\tassert.True(t, locals[\"bool_var\"].(bool))\n\tassert.Equal(t, []any{\"hello\", \"world\"}, locals[\"list_var\"].([]any))\n\n\tassert.InEpsilon(t, float64(24), locals[\"json_number_var\"].(float64), 0.0000000001)\n\tassert.Equal(t, \"another string\", locals[\"json_string_var\"].(string))\n\tassert.False(t, locals[\"json_bool_var\"].(bool))\n}\n\nfunc mockConfigWithSource(sourceURL string) *config.TerragruntConfig {\n\tcfg := config.TerragruntConfig{IsPartial: true}\n\tcfg.Terraform = &config.TerraformConfig{Source: &sourceURL}\n\n\treturn &cfg\n}\n\n// Return keys as a map so it is treated like a set, and order doesn't matter when comparing equivalence\nfunc getKeys(valueMap map[string]cty.Value) map[string]bool {\n\tkeys := map[string]bool{}\n\tfor k := range valueMap {\n\t\tkeys[k] = true\n\t}\n\n\treturn keys\n}\n\nfunc getTrackIncludeFromTestData(includeMap map[string]config.IncludeConfig, params []string) *config.TrackInclude {\n\tif len(includeMap) == 0 {\n\t\treturn nil\n\t}\n\n\tcurrentList := make([]config.IncludeConfig, len(includeMap))\n\n\ti := 0\n\tfor _, val := range includeMap {\n\t\tcurrentList[i] = val\n\t\ti++\n\t}\n\n\ttrackInclude := &config.TrackInclude{\n\t\tCurrentList: currentList,\n\t\tCurrentMap:  includeMap,\n\t}\n\tif len(params) == 0 {\n\t\ttrackInclude.Original = &currentList[0]\n\t}\n\n\treturn trackInclude\n}\n\nfunc TestConstraintCheck(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\terr   string\n\t\targs  []string\n\t\tvalue bool\n\t}{\n\t\t{\n\t\t\targs:  []string{\"1.2\", \">= 1.0, < 1.4\"},\n\t\t\tvalue: true,\n\t\t},\n\t\t{\n\t\t\targs:  []string{\"1.0\", \">= 1.0, < 1.4\"},\n\t\t\tvalue: true,\n\t\t},\n\t\t{\n\t\t\targs:  []string{\"1.4\", \">= 1.0, < 1.4\"},\n\t\t\tvalue: false,\n\t\t},\n\t\t{\n\t\t\targs:  []string{\"1.E\", \">= 1.0, < 1.4\"},\n\t\t\tvalue: false,\n\t\t\terr:   \"invalid version 1.E: malformed version: 1.E\",\n\t\t},\n\t\t{\n\t\t\targs:  []string{\"1.4\", \">== 1.0, < 1.4\"},\n\t\t\tvalue: false,\n\t\t\terr:   \"invalid constraint >== 1.0, < 1.4: malformed constraint: >== 1.0\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"constraint_check(%#v, %#v)\", tc.args[0], tc.args[1]), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tctx, pctx := newTestParsingContext(t, \"\")\n\n\t\t\tactual, err := config.ConstraintCheck(ctx, pctx, tc.args)\n\t\t\tif tc.err != \"\" {\n\t\t\t\trequire.EqualError(t, err, tc.err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.value, actual)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/config/config_partial.go",
    "content": "package config\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/huandu/go-clone\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cache\"\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/zclconf/go-cty/cty\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n)\n\n// PartialDecodeSectionType is an enum that is used to list out which blocks/sections of the terragrunt config should be\n// decoded in a partial decode.\ntype PartialDecodeSectionType int\n\nconst (\n\tDependenciesBlock PartialDecodeSectionType = iota\n\tDependencyBlock\n\tTerraformBlock\n\tTerraformSource\n\tTerragruntFlags\n\tTerragruntVersionConstraints\n\tRemoteStateBlock\n\tFeatureFlagsBlock\n\tEngineBlock\n\tExcludeBlock\n\tErrorsBlock\n)\n\n// terragruntIncludeMultiple is a struct that can be used to only decode the include block with labels.\ntype terragruntIncludeMultiple struct {\n\tRemain  hcl.Body       `hcl:\",remain\"`\n\tInclude IncludeConfigs `hcl:\"include,block\"`\n}\n\n// terragruntDependencies is a struct that can be used to only decode the dependencies block.\ntype terragruntDependencies struct {\n\tDependencies *ModuleDependencies `hcl:\"dependencies,block\"`\n\tRemain       hcl.Body            `hcl:\",remain\"`\n}\n\n// terragruntFeatureFlags is a struct that can be used to store decoded feature flags.\ntype terragruntFeatureFlags struct {\n\tRemain       hcl.Body     `hcl:\",remain\"`\n\tFeatureFlags FeatureFlags `hcl:\"feature,block\"`\n}\n\n// terragruntErrors struct to decode errors block\ntype terragruntErrors struct {\n\tErrors *ErrorsConfig `hcl:\"errors,block\"`\n\tRemain hcl.Body      `hcl:\",remain\"`\n}\n\n// terragruntTerraform is a struct that can be used to only decode the terraform block.\ntype terragruntTerraform struct {\n\tTerraform *TerraformConfig `hcl:\"terraform,block\"`\n\tRemain    hcl.Body         `hcl:\",remain\"`\n}\n\n// terragruntTerraformSource is a struct that can be used to only decode the terraform block, and only the source\n// attribute.\ntype terragruntTerraformSource struct {\n\tTerraform *terraformConfigSourceOnly `hcl:\"terraform,block\"`\n\tRemain    hcl.Body                   `hcl:\",remain\"`\n}\n\n// terraformConfigSourceOnly is a struct that can be used to decode only the source attribute of the terraform block.\ntype terraformConfigSourceOnly struct {\n\tSource *string  `hcl:\"source,attr\"`\n\tRemain hcl.Body `hcl:\",remain\"`\n}\n\n// terragruntFlags is a struct that can be used to only decode the flag attributes (prevent_destroy)\ntype terragruntFlags struct {\n\tIamRole             *string  `hcl:\"iam_role,attr\"`\n\tIamWebIdentityToken *string  `hcl:\"iam_web_identity_token,attr\"`\n\tPreventDestroy      *bool    `hcl:\"prevent_destroy,attr\"`\n\tRemain              hcl.Body `hcl:\",remain\"`\n}\n\n// terragruntVersionConstraints is a struct that can be used to only decode the attributes related to constraining the\n// versions of terragrunt and terraform.\ntype terragruntVersionConstraints struct {\n\tTerragruntVersionConstraint *string  `hcl:\"terragrunt_version_constraint,attr\"`\n\tTerraformVersionConstraint  *string  `hcl:\"terraform_version_constraint,attr\"`\n\tTerraformBinary             *string  `hcl:\"terraform_binary,attr\"`\n\tRemain                      hcl.Body `hcl:\",remain\"`\n}\n\n// TerragruntDependency is a struct that can be used to only decode the dependency blocks in the terragrunt config\ntype TerragruntDependency struct {\n\tRemain       hcl.Body     `hcl:\",remain\"`\n\tDependencies Dependencies `hcl:\"dependency,block\"`\n}\n\n// terragruntRemoteState is a struct that can be used to only decode the remote_state blocks in the terragrunt config\ntype terragruntRemoteState struct {\n\tRemoteState *remotestate.ConfigFile `hcl:\"remote_state,block\"`\n\tRemain      hcl.Body                `hcl:\",remain\"`\n}\n\n// terragruntEngine is a struct that can only be used to decode the engine block.\ntype terragruntEngine struct {\n\tEngine *EngineConfig `hcl:\"engine,block\"`\n\tRemain hcl.Body      `hcl:\",remain\"`\n}\n\n// DecodeBaseBlocks takes in a parsed HCL2 file and decodes the base blocks. Base blocks are blocks that should always\n// be decoded even in partial decoding, because they provide bindings that are necessary for parsing any block in the\n// file. Currently base blocks are:\n// - locals\n// - features\n// - include\nfunc DecodeBaseBlocks(ctx context.Context, pctx *ParsingContext, l log.Logger, file *hclparse.File, includeFromChild *IncludeConfig) (*DecodedBaseBlocks, error) {\n\terrs := &errors.MultiError{}\n\n\tevalParsingContext, err := createTerragruntEvalContext(ctx, pctx, l, file.ConfigPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Decode just the `include` and `import` blocks, and verify that it's allowed here\n\tterragruntIncludeList, err := decodeAsTerragruntInclude(\n\t\tfile,\n\t\tevalParsingContext,\n\t)\n\tif err != nil {\n\t\terrs = errs.Append(err)\n\t}\n\n\ttrackInclude, err := getTrackInclude(pctx, terragruntIncludeList, includeFromChild)\n\tif err != nil {\n\t\terrs = errs.Append(err)\n\t}\n\n\t// set feature flags\n\ttgFlags := terragruntFeatureFlags{}\n\t// load default feature flags\n\tif err := file.Decode(&tgFlags, evalParsingContext); err != nil {\n\t\treturn nil, err\n\t}\n\t// validate flags to have default value, collect errors\n\tflagErrs := &errors.MultiError{}\n\n\tfor _, flag := range tgFlags.FeatureFlags {\n\t\tif flag.Default == nil {\n\t\t\tflagErr := fmt.Errorf(\"feature flag %s does not have a default value in %s\", flag.Name, file.ConfigPath)\n\t\t\tflagErrs = flagErrs.Append(flagErr)\n\t\t}\n\t}\n\n\tif flagErrs.ErrorOrNil() != nil {\n\t\terrs = errs.Append(flagErrs)\n\t}\n\n\tflagsAsCtyVal, err := flagsAsCty(pctx, tgFlags.FeatureFlags)\n\tif err != nil {\n\t\terrs = errs.Append(err)\n\t}\n\n\t// Evaluate all the expressions in the locals block separately and generate the variables list to use in the\n\t// evaluation ctx.\n\tlocals, err := EvaluateLocalsBlock(ctx, pctx.WithTrackInclude(trackInclude).WithFeatures(&flagsAsCtyVal), l, file)\n\tif err != nil {\n\t\terrs = errs.Append(err)\n\t}\n\n\tlocalsAsCtyVal, err := ConvertValuesMapToCtyVal(locals)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &DecodedBaseBlocks{\n\t\tTrackInclude: trackInclude,\n\t\tLocals:       &localsAsCtyVal,\n\t\tFeatureFlags: &flagsAsCtyVal,\n\t}, errs.ErrorOrNil()\n}\n\nfunc flagsAsCty(ctx *ParsingContext, tgFlags FeatureFlags) (cty.Value, error) {\n\t// extract all flags in map by name\n\tflagByName := map[string]*FeatureFlag{}\n\tfor _, flag := range tgFlags {\n\t\tflagByName[flag.Name] = flag\n\t}\n\n\tevaluatedFlags, err := cliFlagsToCty(ctx, flagByName)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\terrs := &errors.MultiError{}\n\n\tfor _, flag := range tgFlags {\n\t\tif _, exists := evaluatedFlags[flag.Name]; !exists {\n\t\t\tif flag.Default == nil {\n\t\t\t\terrs = errs.Append(fmt.Errorf(\"feature flag %s does not have a default value in %s\", flag.Name, ctx.TerragruntConfigPath))\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcontextFlag, err := flagToCtyValue(flag.Name, *flag.Default)\n\t\t\tif err != nil {\n\t\t\t\treturn cty.NilVal, err\n\t\t\t}\n\n\t\t\tevaluatedFlags[flag.Name] = contextFlag\n\t\t}\n\t}\n\n\tflagsAsCtyVal, err := ConvertValuesMapToCtyVal(evaluatedFlags)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\treturn flagsAsCtyVal, errs.ErrorOrNil()\n}\n\n// cliFlagsToCty converts CLI feature flags to Cty values. It returns a map of flag names\n// to their corresponding Cty values and any error encountered during conversion.\nfunc cliFlagsToCty(ctx *ParsingContext, flagByName map[string]*FeatureFlag) (map[string]cty.Value, error) {\n\tif ctx.FeatureFlags == nil {\n\t\treturn make(map[string]cty.Value), nil\n\t}\n\n\tevaluatedFlags := make(map[string]cty.Value)\n\n\tvar conversionErr error\n\n\tctx.FeatureFlags.Range(func(name, value string) bool {\n\t\tvar flag cty.Value\n\n\t\tvar err error\n\n\t\tif existingFlag, ok := flagByName[name]; ok {\n\t\t\tflag, err = flagToTypedCtyValue(name, existingFlag.Default.Type(), value)\n\t\t} else {\n\t\t\tflag, err = flagToCtyValue(name, value)\n\t\t}\n\n\t\tif err != nil {\n\t\t\tconversionErr = err\n\n\t\t\treturn false\n\t\t}\n\n\t\tevaluatedFlags[name] = flag\n\n\t\treturn true\n\t})\n\n\tif conversionErr != nil {\n\t\treturn nil, conversionErr\n\t}\n\n\treturn evaluatedFlags, nil\n}\n\nfunc PartialParseConfigFile(ctx context.Context, pctx *ParsingContext, l log.Logger, configPath string, include *IncludeConfig) (*TerragruntConfig, error) {\n\thclCache := cache.ContextCache[*hclparse.File](ctx, HclCacheContextKey)\n\n\tfileInfo, err := os.Stat(configPath)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil, TerragruntConfigNotFoundError{Path: configPath}\n\t\t}\n\n\t\treturn nil, errors.New(err)\n\t}\n\n\tcacheKey := fmt.Sprintf(\"configPath-%v-modTime-%v\", configPath, fileInfo.ModTime().UnixMicro())\n\n\t// Check cache hit status before tracing\n\t_, cacheHit := hclCache.Get(ctx, cacheKey)\n\n\tvar config *TerragruntConfig\n\n\terr = TraceParseConfigFile(\n\t\tctx,\n\t\tconfigPath,\n\t\tpctx.WorkingDir,\n\t\ttrue, // isPartial\n\t\tpctx.PartialParseDecodeList,\n\t\tinclude,\n\t\tcacheHit,\n\t\tfunc(ctx context.Context) error {\n\t\t\tvar file *hclparse.File\n\n\t\t\tif cacheConfig, found := hclCache.Get(ctx, cacheKey); found {\n\t\t\t\tfile = cacheConfig\n\t\t\t} else {\n\t\t\t\tvar parseErr error\n\n\t\t\t\tfile, parseErr = hclparse.NewParser(pctx.ParserOptions...).ParseFromFile(configPath)\n\t\t\t\tif parseErr != nil {\n\t\t\t\t\treturn parseErr\n\t\t\t\t}\n\n\t\t\t\thclCache.Put(ctx, cacheKey, file)\n\t\t\t}\n\n\t\t\tvar parseErr error\n\n\t\t\tconfig, parseErr = TerragruntConfigFromPartialConfig(ctx, pctx, l, file, include)\n\n\t\t\treturn parseErr\n\t\t})\n\n\treturn config, err\n}\n\n// TerragruntConfigFromPartialConfig is a wrapper of PartialParseConfigString which checks for cached configs.\n// filename, configString, includeFromChild and decodeList are used for the cache key,\n// by getting the default value (%#v) through fmt.\nfunc TerragruntConfigFromPartialConfig(ctx context.Context, pctx *ParsingContext, l log.Logger, file *hclparse.File, includeFromChild *IncludeConfig) (*TerragruntConfig, error) {\n\tvar cacheKey = fmt.Sprintf(\"%#v-%#v-%#v-%#v-%#v\", file.ConfigPath, file.Content(), includeFromChild, pctx.PartialParseDecodeList, pctx.TerragruntConfigPath)\n\n\tterragruntConfigCache := cache.ContextCache[*TerragruntConfig](ctx, TerragruntConfigCacheContextKey)\n\tif pctx.UsePartialParseConfigCache {\n\t\tif config, found := terragruntConfigCache.Get(ctx, cacheKey); found {\n\t\t\tl.Debugf(\"Cache hit for '%s' (partial parsing), decodeList: '%v'.\", pctx.TerragruntConfigPath, pctx.PartialParseDecodeList)\n\n\t\t\tdeepCopy := clone.Clone(config).(*TerragruntConfig)\n\n\t\t\treturn deepCopy, nil\n\t\t}\n\n\t\tl.Debugf(\"Cache miss for '%s' (partial parsing), decodeList: '%v'.\", pctx.TerragruntConfigPath, pctx.PartialParseDecodeList)\n\t}\n\n\tconfig, err := PartialParseConfig(ctx, pctx, l, file, includeFromChild)\n\tif err != nil {\n\t\treturn config, err\n\t}\n\n\tif pctx.UsePartialParseConfigCache {\n\t\tputConfig := clone.Clone(config).(*TerragruntConfig)\n\t\tterragruntConfigCache.Put(ctx, cacheKey, putConfig)\n\t}\n\n\treturn config, nil\n}\n\n// PartialParseConfigString partially parses and decodes the provided string. Which blocks/attributes to decode is\n// controlled by the function parameter decodeList. These blocks/attributes are parsed and set on the output\n// TerragruntConfig. Valid values are:\n//   - DependenciesBlock: Parses the `dependencies` block in the config\n//   - DependencyBlock: Parses the `dependency` block in the config\n//   - TerraformBlock: Parses the `terraform` block in the config\n//   - TerragruntFlags: Parses the boolean flags `prevent_destroy` and `skip` in the config\n//   - TerragruntVersionConstraints: Parses the attributes related to constraining terragrunt and terraform versions in\n//     the config.\n//   - RemoteStateBlock: Parses the `remote_state` block in the config\n//   - FeatureFlagsBlock: Parses the `feature` block in the config\n//   - EngineBlock: Parses the `engine` block in the config\n//   - ExcludeBlock : Parses the `exclude` block in the config\n//\n// Note that the following blocks are always decoded:\n// - locals\n// - include\n// Note also that the following blocks are never decoded in a partial parse:\n// - inputs\nfunc PartialParseConfigString(ctx context.Context, pctx *ParsingContext, l log.Logger, configPath, configString string, include *IncludeConfig) (*TerragruntConfig, error) {\n\tfile, err := hclparse.NewParser(pctx.ParserOptions...).ParseFromString(configString, configPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn PartialParseConfig(ctx, pctx, l, file, include)\n}\n\nfunc PartialParseConfig(ctx context.Context, pctx *ParsingContext, l log.Logger, file *hclparse.File, includeFromChild *IncludeConfig) (*TerragruntConfig, error) {\n\terrs := &errors.MultiError{}\n\n\t// Detect and block deprecated configurations early, before attempting to parse.\n\t// This ensures included configs with deprecated syntax get clear error messages\n\t// instead of cryptic \"Could not find Terragrunt configuration settings\" errors.\n\t// See: https://github.com/gruntwork-io/terragrunt/issues/4983\n\tif err := DetectDeprecatedConfigurations(ctx, pctx, l, file); err != nil {\n\t\treturn nil, err\n\t}\n\n\tpctx = pctx.WithTrackInclude(nil)\n\n\t// read unit files and add to context\n\tunitValues, err := ReadValues(ctx, pctx, l, filepath.Dir(file.ConfigPath))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpctx = pctx.WithValues(unitValues)\n\n\t// Decode just the Base blocks. See the function docs for DecodeBaseBlocks for more info on what base blocks are.\n\t// Initialize evaluation ctx extensions from base blocks.\n\tbaseBlocks, err := DecodeBaseBlocks(ctx, pctx, l, file, includeFromChild)\n\tif err != nil {\n\t\terrs = errs.Append(err)\n\t}\n\n\tif baseBlocks != nil {\n\t\tpctx = pctx.WithTrackInclude(baseBlocks.TrackInclude)\n\t\tpctx = pctx.WithFeatures(baseBlocks.FeatureFlags)\n\t\tpctx = pctx.WithLocals(baseBlocks.Locals)\n\t}\n\n\t// Set parsed Locals on the parsed config\n\toutput, err := convertToTerragruntConfig(ctx, pctx, file.ConfigPath, &terragruntConfigFile{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\toutput.IsPartial = true\n\n\tevalParsingContext, err := createTerragruntEvalContext(ctx, pctx, l, file.ConfigPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Now loop through each requested block / component to decode from the terragrunt config, decode them, and merge\n\t// them into the output TerragruntConfig struct.\n\thasExcludeBlock := false\n\n\tfor _, decode := range pctx.PartialParseDecodeList {\n\t\tswitch decode {\n\t\tcase DependenciesBlock:\n\t\t\tdecoded := terragruntDependencies{}\n\n\t\t\terr := file.Decode(&decoded, evalParsingContext)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\t// If we already decoded some dependencies, merge them in. Otherwise, set as the new list.\n\t\t\tif output.Dependencies != nil {\n\t\t\t\toutput.Dependencies.Merge(decoded.Dependencies)\n\t\t\t} else {\n\t\t\t\toutput.Dependencies = decoded.Dependencies\n\t\t\t}\n\n\t\tcase TerraformBlock:\n\t\t\tdecoded := terragruntTerraform{}\n\n\t\t\terr := file.Decode(&decoded, evalParsingContext)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\toutput.Terraform = decoded.Terraform\n\n\t\tcase TerraformSource:\n\t\t\tdecoded := terragruntTerraformSource{}\n\n\t\t\terr := file.Decode(&decoded, evalParsingContext)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tif decoded.Terraform != nil {\n\t\t\t\toutput.Terraform = &TerraformConfig{Source: decoded.Terraform.Source}\n\t\t\t}\n\n\t\tcase DependencyBlock:\n\t\t\tdecoded := TerragruntDependency{}\n\n\t\t\terr := file.Decode(&decoded, evalParsingContext)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\t// In normal operation, if a dependency block does not have a `config_path` attribute, decoding returns an error since this attribute is required, but the `hclvalidate` command suppresses decoding errors and this causes a cycle between modules, so we need to filter out dependencies without a defined `config_path`.\n\t\t\tdecoded.Dependencies = decoded.Dependencies.FilteredWithoutConfigPath()\n\n\t\t\toutput.TerragruntDependencies = decoded.Dependencies\n\t\t\t// Convert dependency blocks into module dependency lists. If we already decoded some dependencies,\n\t\t\t// merge them in. Otherwise, set as the new list.\n\t\t\tdependencies := dependencyBlocksToModuleDependencies(l, decoded.Dependencies)\n\t\t\tif output.Dependencies != nil {\n\t\t\t\toutput.Dependencies.Merge(dependencies)\n\t\t\t} else {\n\t\t\t\toutput.Dependencies = dependencies\n\t\t\t}\n\n\t\tcase EngineBlock:\n\t\t\tdecoded := terragruntEngine{}\n\n\t\t\terr := file.Decode(&decoded, evalParsingContext)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\toutput.Engine = decoded.Engine\n\n\t\tcase TerragruntFlags:\n\t\t\tdecoded := terragruntFlags{}\n\n\t\t\terr := file.Decode(&decoded, evalParsingContext)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tif decoded.PreventDestroy != nil {\n\t\t\t\toutput.PreventDestroy = decoded.PreventDestroy\n\t\t\t}\n\n\t\t\tif decoded.IamRole != nil {\n\t\t\t\toutput.IamRole = *decoded.IamRole\n\t\t\t}\n\n\t\t\tif decoded.IamWebIdentityToken != nil {\n\t\t\t\toutput.IamWebIdentityToken = *decoded.IamWebIdentityToken\n\t\t\t}\n\t\tcase TerragruntVersionConstraints:\n\t\t\tdecoded := terragruntVersionConstraints{}\n\n\t\t\terr := file.Decode(&decoded, evalParsingContext)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tif decoded.TerragruntVersionConstraint != nil {\n\t\t\t\toutput.TerragruntVersionConstraint = *decoded.TerragruntVersionConstraint\n\t\t\t}\n\n\t\t\tif decoded.TerraformVersionConstraint != nil {\n\t\t\t\toutput.TerraformVersionConstraint = *decoded.TerraformVersionConstraint\n\t\t\t}\n\n\t\t\t// If the TFPath is not explicitly set, use the TFPath from the config if it is set.\n\t\t\tif !pctx.TFPathExplicitlySet && decoded.TerraformBinary != nil {\n\t\t\t\toutput.TerraformBinary = *decoded.TerraformBinary\n\t\t\t}\n\n\t\tcase RemoteStateBlock:\n\t\t\tdecoded := terragruntRemoteState{}\n\n\t\t\terr := file.Decode(&decoded, evalParsingContext)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tif decoded.RemoteState != nil {\n\t\t\t\tconfig, err := decoded.RemoteState.Config()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\toutput.RemoteState = remotestate.New(config)\n\t\t\t}\n\t\tcase FeatureFlagsBlock:\n\t\t\tdecoded := terragruntFeatureFlags{}\n\n\t\t\terr := file.Decode(&decoded, evalParsingContext)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tif output.FeatureFlags != nil {\n\t\t\t\tflags, err := deepMergeFeatureBlocks(output.FeatureFlags, decoded.FeatureFlags)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\toutput.FeatureFlags = flags\n\t\t\t} else {\n\t\t\t\toutput.FeatureFlags = decoded.FeatureFlags\n\t\t\t}\n\n\t\tcase ExcludeBlock:\n\t\t\thasExcludeBlock = true\n\n\t\tcase ErrorsBlock:\n\t\t\tdecoded := terragruntErrors{}\n\n\t\t\terr := file.Decode(&decoded, evalParsingContext)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tif output.Errors != nil {\n\t\t\t\toutput.Errors.Merge(decoded.Errors)\n\t\t\t} else {\n\t\t\t\toutput.Errors = decoded.Errors\n\t\t\t}\n\n\t\tdefault:\n\t\t\treturn nil, InvalidPartialBlockName{decode}\n\t\t}\n\t}\n\n\terrsContainsIncludeErr := false\n\n\tfor _, err := range errs.WrappedErrors() {\n\t\tif errors.As(err, &TooManyLevelsOfInheritanceError{}) {\n\t\t\terrsContainsIncludeErr = true\n\t\t}\n\t}\n\n\t// If this file includes another, parse and merge the partial blocks. Otherwise, just return this config.\n\t// If there have been errors during this parse, don't attempt to parse the included config.\n\tif len(pctx.TrackInclude.CurrentList) > 0 && !errsContainsIncludeErr {\n\t\tincludeCount := len(pctx.TrackInclude.CurrentList)\n\t\tincludePaths := make([]string, 0, includeCount)\n\n\t\tfor _, inc := range pctx.TrackInclude.CurrentList {\n\t\t\tif inc.Path != \"\" {\n\t\t\t\tincludePaths = append(includePaths, inc.Path)\n\t\t\t}\n\t\t}\n\n\t\tvar config *TerragruntConfig\n\n\t\terr := TraceParseIncludeMerge(ctx, file.ConfigPath, includeCount, includePaths, func(ctx context.Context) error {\n\t\t\tvar mergeErr error\n\n\t\t\tconfig, mergeErr = handleInclude(ctx, pctx, l, output, true)\n\n\t\t\treturn mergeErr\n\t\t})\n\t\tif err != nil {\n\t\t\terrs = errs.Append(err)\n\t\t}\n\n\t\t// Saving processed includes into configuration, direct assignment since nested includes aren't supported\n\t\tconfig.ProcessedIncludes = pctx.TrackInclude.CurrentMap\n\n\t\toutput = config\n\t}\n\n\tif errs.ErrorOrNil() != nil {\n\t\treturn output, errs.ErrorOrNil()\n\t}\n\n\tif hasExcludeBlock {\n\t\treturn processExcludes(ctx, pctx, l, output, file)\n\t}\n\n\treturn output, nil\n}\n\n// processExcludes evaluate exclude blocks and merge them into the config.\nfunc processExcludes(ctx context.Context, pctx *ParsingContext, l log.Logger, config *TerragruntConfig, file *hclparse.File) (*TerragruntConfig, error) {\n\tflagsAsCtyVal, err := flagsAsCty(pctx, config.FeatureFlags)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\texcludeConfig, err := evaluateExcludeBlocks(ctx, pctx.WithFeatures(&flagsAsCtyVal), l, file)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif excludeConfig == nil {\n\t\treturn config, nil\n\t}\n\n\tif config.Exclude != nil {\n\t\tconfig.Exclude.Merge(excludeConfig)\n\t} else {\n\t\tconfig.Exclude = excludeConfig\n\t}\n\n\treturn config, nil\n}\n\nfunc partialParseIncludedConfig(ctx context.Context, pctx *ParsingContext, l log.Logger, includedConfig *IncludeConfig) (*TerragruntConfig, error) {\n\tif includedConfig.Path == \"\" {\n\t\treturn nil, errors.New(IncludedConfigMissingPathError(pctx.TerragruntConfigPath))\n\t}\n\n\tincludePath := includedConfig.Path\n\n\tif !filepath.IsAbs(includePath) {\n\t\tincludePath = filepath.Join(filepath.Dir(pctx.TerragruntConfigPath), includePath)\n\t}\n\n\tconfig, err := PartialParseConfigFile(\n\t\tctx,\n\t\tpctx,\n\t\tl,\n\t\tincludePath,\n\t\tincludedConfig,\n\t)\n\tif err != nil {\n\t\t// Convert generic config not found error to include-specific error\n\t\tvar configNotFoundError TerragruntConfigNotFoundError\n\t\tif errors.As(err, &configNotFoundError) {\n\t\t\treturn nil, IncludeConfigNotFoundError{IncludePath: includePath, SourcePath: pctx.TerragruntConfigPath}\n\t\t}\n\n\t\treturn nil, err\n\t}\n\n\treturn config, nil\n}\n\n// This decodes only the `include` blocks of a terragrunt config, so its value can be used while decoding the rest of\n// the config.\n// For consistency, `include` in the call to `file.Decode` is always assumed to be nil. Either it really is nil (parsing\n// the child config), or it shouldn't be used anyway (the parent config shouldn't have an include block).\nfunc decodeAsTerragruntInclude(file *hclparse.File, evalParsingContext *hcl.EvalContext) (IncludeConfigs, error) {\n\ttgInc := terragruntIncludeMultiple{}\n\tif err := file.Decode(&tgInc, evalParsingContext); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn tgInc.Include, nil\n}\n\n// Custom error types\n\ntype InvalidPartialBlockName struct {\n\tsectionCode PartialDecodeSectionType\n}\n\nfunc (err InvalidPartialBlockName) Error() string {\n\treturn fmt.Sprintf(\"Unrecognized partial block code %d. This is most likely an error in terragrunt. Please file a bug report on the project repository.\", err.sectionCode)\n}\n"
  },
  {
    "path": "pkg/config/config_partial_test.go",
    "content": "package config_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cache\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/zclconf/go-cty/cty\"\n)\n\nfunc TestPartialParseResolvesLocals(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nlocals {\n  app1 = \"../app1\"\n}\n\ndependencies {\n  paths = [local.app1]\n}\n`\n\n\tl := logger.CreateLogger()\n\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tpctx = pctx.WithDecodeList(config.DependenciesBlock)\n\tterragruntConfig, err := config.PartialParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\tassert.True(t, terragruntConfig.IsPartial)\n\n\tassert.NotNil(t, terragruntConfig.Dependencies)\n\tassert.Len(t, terragruntConfig.Dependencies.Paths, 1)\n\tassert.Equal(t, \"../app1\", terragruntConfig.Dependencies.Paths[0])\n\tassert.Equal(t, map[string]any{\"app1\": \"../app1\"}, terragruntConfig.Locals)\n\n\tassert.Nil(t, terragruntConfig.PreventDestroy)\n\tassert.Nil(t, terragruntConfig.Terraform)\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Inputs)\n}\n\nfunc TestPartialParseDoesNotResolveIgnoredBlock(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ndependencies {\n  # This function call will fail when attempting to decode\n  paths = [file(\"i-am-a-file-that-does-not-exist\")]\n}\n\nprevent_destroy = false\n`\n\n\tl := logger.CreateLogger()\n\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\t_, err := config.PartialParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\n\tpctx = pctx.WithDecodeList(config.DependenciesBlock)\n\t_, err = config.PartialParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\tassert.Error(t, err)\n}\n\nfunc TestPartialParseMultipleItems(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ndependencies {\n  paths = [\"../app1\"]\n}\n\nprevent_destroy = true\nskip = true\n`\n\n\tl := logger.CreateLogger()\n\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tpctx = pctx.WithDecodeList(config.DependenciesBlock, config.TerragruntFlags)\n\tterragruntConfig, err := config.PartialParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\tassert.True(t, terragruntConfig.IsPartial)\n\n\tassert.NotNil(t, terragruntConfig.Dependencies)\n\tassert.Len(t, terragruntConfig.Dependencies.Paths, 1)\n\tassert.Equal(t, \"../app1\", terragruntConfig.Dependencies.Paths[0])\n\n\tassert.True(t, *terragruntConfig.PreventDestroy)\n\n\tassert.Nil(t, terragruntConfig.Terraform)\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Inputs)\n\tassert.Nil(t, terragruntConfig.Locals)\n}\n\nfunc TestPartialParseOmittedItems(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tpctx = pctx.WithDecodeList(config.DependenciesBlock, config.TerragruntFlags)\n\tterragruntConfig, err := config.PartialParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, \"\", nil)\n\n\trequire.NoError(t, err)\n\tassert.True(t, terragruntConfig.IsPartial)\n\tassert.Nil(t, terragruntConfig.Dependencies)\n\tassert.Nil(t, terragruntConfig.PreventDestroy)\n\tassert.Nil(t, terragruntConfig.Terraform)\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Inputs)\n\tassert.Nil(t, terragruntConfig.Locals)\n}\n\nfunc TestPartialParseDoesNotResolveIgnoredBlockEvenInParent(t *testing.T) {\n\tt.Parallel()\n\n\tconfigPath, err := filepath.Abs(filepath.Join(\"../..\", \"test\", \"fixtures\", \"partial-parse\", \"ignore-bad-block-in-parent\", \"child\", config.DefaultTerragruntConfigPath))\n\trequire.NoError(t, err)\n\n\tl := logger.CreateLogger()\n\n\tctx, pctx := newTestParsingContext(t, configPath)\n\tpctx = pctx.WithDecodeList(config.TerragruntFlags)\n\t_, err = config.PartialParseConfigFile(ctx, pctx, l, configPath, nil)\n\trequire.NoError(t, err)\n\n\tpctx = pctx.WithDecodeList(config.DependenciesBlock)\n\t_, err = config.PartialParseConfigFile(ctx, pctx, l, configPath, nil)\n\tassert.Error(t, err)\n}\n\nfunc TestPartialParseOnlyInheritsSelectedBlocksFlags(t *testing.T) {\n\tt.Parallel()\n\n\tconfigPath, err := filepath.Abs(filepath.Join(\"../..\", \"test\", \"fixtures\", \"partial-parse\", \"partial-inheritance\", \"child\", config.DefaultTerragruntConfigPath))\n\trequire.NoError(t, err)\n\n\tl := logger.CreateLogger()\n\n\tctx, pctx := newTestParsingContext(t, configPath)\n\tpctx = pctx.WithDecodeList(config.TerragruntFlags)\n\tterragruntConfig, err := config.PartialParseConfigFile(ctx, pctx, l, configPath, nil)\n\trequire.NoError(t, err)\n\n\tassert.True(t, terragruntConfig.IsPartial)\n\tassert.Nil(t, terragruntConfig.Dependencies)\n\tassert.True(t, *terragruntConfig.PreventDestroy)\n\tassert.Nil(t, terragruntConfig.Terraform)\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Inputs)\n\tassert.Nil(t, terragruntConfig.Locals)\n}\n\nfunc TestPartialParseOnlyInheritsSelectedBlocksDependencies(t *testing.T) {\n\tt.Parallel()\n\n\tconfigPath, err := filepath.Abs(filepath.Join(\"../..\", \"test\", \"fixtures\", \"partial-parse\", \"partial-inheritance\", \"child\", config.DefaultTerragruntConfigPath))\n\trequire.NoError(t, err)\n\n\tl := logger.CreateLogger()\n\n\tctx, pctx := newTestParsingContext(t, configPath)\n\tpctx = pctx.WithDecodeList(config.DependenciesBlock)\n\tterragruntConfig, err := config.PartialParseConfigFile(ctx, pctx, l, configPath, nil)\n\trequire.NoError(t, err)\n\n\tassert.True(t, terragruntConfig.IsPartial)\n\n\tassert.NotNil(t, terragruntConfig.Dependencies)\n\tassert.Len(t, terragruntConfig.Dependencies.Paths, 1)\n\tassert.Equal(t, \"../app1\", terragruntConfig.Dependencies.Paths[0])\n\n\tassert.Nil(t, terragruntConfig.PreventDestroy)\n\tassert.Nil(t, terragruntConfig.Terraform)\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Inputs)\n\tassert.Nil(t, terragruntConfig.Locals)\n}\n\nfunc TestPartialParseDependencyBlockSetsTerragruntDependencies(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ndependency \"vpc\" {\n  config_path = \"../app1\"\n}\n`\n\n\tl := logger.CreateLogger()\n\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tpctx = pctx.WithDecodeList(config.DependencyBlock)\n\tterragruntConfig, err := config.PartialParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\tassert.True(t, terragruntConfig.IsPartial)\n\n\tassert.NotNil(t, terragruntConfig.TerragruntDependencies)\n\tassert.Len(t, terragruntConfig.TerragruntDependencies, 1)\n\tassert.Equal(t, \"vpc\", terragruntConfig.TerragruntDependencies[0].Name)\n\tassert.Equal(t, cty.StringVal(\"../app1\"), terragruntConfig.TerragruntDependencies[0].ConfigPath)\n}\n\nfunc TestPartialParseMultipleDependencyBlockSetsTerragruntDependencies(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ndependency \"vpc\" {\n  config_path = \"../app1\"\n}\n\ndependency \"sql\" {\n  config_path = \"../db1\"\n}\n`\n\n\tl := logger.CreateLogger()\n\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tpctx = pctx.WithDecodeList(config.DependencyBlock)\n\tterragruntConfig, err := config.PartialParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\tassert.True(t, terragruntConfig.IsPartial)\n\n\tassert.NotNil(t, terragruntConfig.TerragruntDependencies)\n\tassert.Len(t, terragruntConfig.TerragruntDependencies, 2)\n\tassert.Equal(t, \"vpc\", terragruntConfig.TerragruntDependencies[0].Name)\n\tassert.Equal(t, cty.StringVal(\"../app1\"), terragruntConfig.TerragruntDependencies[0].ConfigPath)\n\tassert.Equal(t, \"sql\", terragruntConfig.TerragruntDependencies[1].Name)\n\tassert.Equal(t, cty.StringVal(\"../db1\"), terragruntConfig.TerragruntDependencies[1].ConfigPath)\n}\n\nfunc TestPartialParseDependencyBlockSetsDependencies(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ndependency \"vpc\" {\n  config_path = \"../app1\"\n}\n\ndependency \"sql\" {\n  config_path = \"../db1\"\n}\n`\n\n\tl := logger.CreateLogger()\n\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tpctx = pctx.WithDecodeList(config.DependencyBlock)\n\tterragruntConfig, err := config.PartialParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\tassert.True(t, terragruntConfig.IsPartial)\n\n\tassert.NotNil(t, terragruntConfig.Dependencies)\n\tassert.Len(t, terragruntConfig.Dependencies.Paths, 2)\n\tassert.Equal(t, []string{\"../app1\", \"../db1\"}, terragruntConfig.Dependencies.Paths)\n}\n\nfunc TestPartialParseDependencyBlockMergesDependencies(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ndependency \"vpc\" {\n  config_path = \"../app1\"\n}\n\ndependencies {\n  paths = [\"../vpc\"]\n}\n\ndependency \"sql\" {\n  config_path = \"../db1\"\n}\n`\n\n\tl := logger.CreateLogger()\n\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tpctx = pctx.WithDecodeList(config.DependenciesBlock, config.DependencyBlock)\n\tterragruntConfig, err := config.PartialParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\tassert.True(t, terragruntConfig.IsPartial)\n\n\tassert.NotNil(t, terragruntConfig.Dependencies)\n\tassert.Len(t, terragruntConfig.Dependencies.Paths, 3)\n\tassert.Equal(t, []string{\"../vpc\", \"../app1\", \"../db1\"}, terragruntConfig.Dependencies.Paths)\n}\n\nfunc TestPartialParseDependencyBlockMergesDependenciesOrdering(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ndependency \"vpc\" {\n  config_path = \"../app1\"\n}\n\ndependencies {\n  paths = [\"../vpc\"]\n}\n\ndependency \"sql\" {\n  config_path = \"../db1\"\n}\n`\n\n\tl := logger.CreateLogger()\n\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tpctx = pctx.WithDecodeList(config.DependencyBlock, config.DependenciesBlock)\n\tterragruntConfig, err := config.PartialParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\tassert.True(t, terragruntConfig.IsPartial)\n\n\tassert.NotNil(t, terragruntConfig.Dependencies)\n\tassert.Len(t, terragruntConfig.Dependencies.Paths, 3)\n\tassert.Equal(t, []string{\"../app1\", \"../db1\", \"../vpc\"}, terragruntConfig.Dependencies.Paths)\n}\n\nfunc TestPartialParseDependencyBlockMergesDependenciesDedup(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ndependency \"vpc\" {\n  config_path = \"../app1\"\n}\n\ndependencies {\n  paths = [\"../app1\"]\n}\n\ndependency \"sql\" {\n  config_path = \"../db1\"\n}\n`\n\n\tl := logger.CreateLogger()\n\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tpctx = pctx.WithDecodeList(config.DependencyBlock, config.DependenciesBlock)\n\tterragruntConfig, err := config.PartialParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\tassert.True(t, terragruntConfig.IsPartial)\n\n\tassert.NotNil(t, terragruntConfig.Dependencies)\n\tassert.Len(t, terragruntConfig.Dependencies.Paths, 2)\n\tassert.Equal(t, []string{\"../app1\", \"../db1\"}, terragruntConfig.Dependencies.Paths)\n}\n\nfunc TestPartialParseOnlyParsesTerraformSource(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n\nterraform {\n  source = \"../../modules/app\"\n  before_hook \"before\" {\n    commands = [\"apply\"]\n\texecute  = [\"echo\", dependency.vpc.outputs.vpc_id]\n  }\n}\n`\n\n\tl := logger.CreateLogger()\n\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tpctx = pctx.WithDecodeList(config.TerraformSource)\n\tterragruntConfig, err := config.PartialParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\tassert.True(t, terragruntConfig.IsPartial)\n\n\tassert.NotNil(t, terragruntConfig.Terraform)\n\tassert.NotNil(t, terragruntConfig.Terraform.Source)\n\tassert.Equal(t, \"../../modules/app\", *terragruntConfig.Terraform.Source)\n}\n\nfunc TestOptionalDependenciesAreSkipped(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\ndependency \"ec2\" {\n  config_path = \"../ec2\"\n  enabled    = false\n}\n`\n\n\tl := logger.CreateLogger()\n\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tpctx = pctx.WithDecodeList(config.DependencyBlock)\n\tterragruntConfig, err := config.PartialParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\tassert.Len(t, terragruntConfig.Dependencies.Paths, 1)\n}\n\nfunc TestPartialParseSavesToHclCache(t *testing.T) {\n\tt.Parallel()\n\n\t// Setup test environment\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\tconfigPath := filepath.Join(tmpDir, \"terragrunt.hcl\")\n\tconfigContent := `dependencies { paths = [\"../app1\"] }` //nolint:goconst\n\trequire.NoError(t, os.WriteFile(configPath, []byte(configContent), 0644))\n\n\t// Get file metadata for cache key generation\n\tfileInfo, err := os.Stat(configPath)\n\trequire.NoError(t, err)\n\n\texpectedCacheKey := fmt.Sprintf(\"configPath-%v-modTime-%v\", configPath, fileInfo.ModTime().UnixMicro())\n\n\t// Setup cache and context\n\thclCache := cache.NewCache[*hclparse.File](\"test-hcl-cache\")\n\tl := logger.CreateLogger()\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tctx = context.WithValue(ctx, config.HclCacheContextKey, hclCache)\n\tpctx = pctx.WithDecodeList(config.DependenciesBlock)\n\n\t// Verify cache is empty initially\n\t_, found := hclCache.Get(ctx, expectedCacheKey)\n\trequire.False(t, found, \"cache should be empty before parsing\")\n\n\t// Parse config file (should populate cache)\n\t_, err = config.PartialParseConfigFile(ctx, pctx, l, configPath, nil)\n\trequire.NoError(t, err)\n\n\t// Verify file was cached\n\tcachedFile, found := hclCache.Get(ctx, expectedCacheKey)\n\trequire.True(t, found, \"expected file to be in cache after first parse\")\n\trequire.NotNil(t, cachedFile, \"cached file should not be nil\")\n\n\t// Verify cached content matches the original\n\tassert.Equal(t, configPath, cachedFile.ConfigPath)\n\tassert.Contains(t, cachedFile.Content(), \"dependencies\")\n}\n\nfunc TestPartialParseCacheHitOnSecondParse(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\tconfigPath := filepath.Join(tmpDir, \"terragrunt.hcl\")\n\tconfigContent := `dependencies { paths = [\"../app1\"] }`\n\trequire.NoError(t, os.WriteFile(configPath, []byte(configContent), 0644))\n\n\tfileInfo, err := os.Stat(configPath)\n\trequire.NoError(t, err)\n\n\tcacheKey := fmt.Sprintf(\"configPath-%v-modTime-%v\", configPath, fileInfo.ModTime().UnixMicro())\n\n\thclCache := cache.NewCache[*hclparse.File](\"test-hcl-cache\")\n\tl := logger.CreateLogger()\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tctx = context.WithValue(ctx, config.HclCacheContextKey, hclCache)\n\tpctx = pctx.WithDecodeList(config.DependenciesBlock)\n\n\t// First parse - should be cache miss\n\t_, err = config.PartialParseConfigFile(ctx, pctx, l, configPath, nil)\n\trequire.NoError(t, err)\n\n\t// Verify cache hit on second parse\n\t_, err = config.PartialParseConfigFile(ctx, pctx, l, configPath, nil)\n\trequire.NoError(t, err)\n\n\t// Verify same file object is returned from cache\n\tcachedFile, found := hclCache.Get(ctx, cacheKey)\n\trequire.True(t, found)\n\trequire.NotNil(t, cachedFile)\n}\n\nfunc TestPartialParseCacheInvalidationOnFileModification(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\tconfigPath := filepath.Join(tmpDir, \"terragrunt.hcl\")\n\toriginalContent := `dependencies { paths = [\"../app1\"] }`\n\tmodifiedContent := `dependencies { paths = [\"../app1\", \"../app2\"] }`\n\n\trequire.NoError(t, os.WriteFile(configPath, []byte(originalContent), 0644))\n\n\tfileInfo, err := os.Stat(configPath)\n\trequire.NoError(t, err)\n\n\toriginalCacheKey := fmt.Sprintf(\"configPath-%v-modTime-%v\", configPath, fileInfo.ModTime().UnixMicro())\n\n\thclCache := cache.NewCache[*hclparse.File](\"test-hcl-cache\")\n\tl := logger.CreateLogger()\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tctx = context.WithValue(ctx, config.HclCacheContextKey, hclCache)\n\tpctx = pctx.WithDecodeList(config.DependenciesBlock)\n\n\t// Parse original file\n\t_, err = config.PartialParseConfigFile(ctx, pctx, l, configPath, nil)\n\trequire.NoError(t, err)\n\n\t// Verify original file is cached\n\t_, found := hclCache.Get(ctx, originalCacheKey)\n\trequire.True(t, found, \"original file should be cached\")\n\n\t// Modify file (this changes mod time)\n\trequire.NoError(t, os.WriteFile(configPath, []byte(modifiedContent), 0644))\n\tforceModTimeChange(t, configPath, fileInfo.ModTime())\n\n\t// Parse modified file - should create new cache entry\n\t_, err = config.PartialParseConfigFile(ctx, pctx, l, configPath, nil)\n\trequire.NoError(t, err)\n\n\t// Verify old cache entry is still there but new one exists\n\t_, found = hclCache.Get(ctx, originalCacheKey)\n\trequire.True(t, found, \"original cache entry should still exist\")\n\n\t// Get new cache key\n\tfileInfo, err = os.Stat(configPath)\n\trequire.NoError(t, err)\n\n\tnewCacheKey := fmt.Sprintf(\"configPath-%v-modTime-%v\", configPath, fileInfo.ModTime().UnixMicro())\n\n\t// Verify new file is cached with different content\n\tnewCachedFile, found := hclCache.Get(ctx, newCacheKey)\n\trequire.True(t, found, \"modified file should be cached\")\n\trequire.NotNil(t, newCachedFile)\n\tassert.Contains(t, newCachedFile.Content(), \"../app2\")\n}\n\nfunc TestPartialParseCacheWithInvalidFile(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\tconfigPath := filepath.Join(tmpDir, \"terragrunt.hcl\")\n\tinvalidContent := `invalid hcl syntax {`\n\trequire.NoError(t, os.WriteFile(configPath, []byte(invalidContent), 0644))\n\n\thclCache := cache.NewCache[*hclparse.File](\"test-hcl-cache\")\n\tl := logger.CreateLogger()\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tctx = context.WithValue(ctx, config.HclCacheContextKey, hclCache)\n\tpctx = pctx.WithDecodeList(config.DependenciesBlock)\n\n\t// Parse should fail and not cache an invalid file\n\t_, err := config.PartialParseConfigFile(ctx, pctx, l, configPath, nil)\n\trequire.Error(t, err, \"parsing invalid HCL should fail\")\n\n\t// Verify nothing was cached\n\tfileInfo, err := os.Stat(configPath)\n\trequire.NoError(t, err)\n\n\tcacheKey := fmt.Sprintf(\"configPath-%v-modTime-%v\", configPath, fileInfo.ModTime().UnixMicro())\n\n\t_, found := hclCache.Get(ctx, cacheKey)\n\trequire.False(t, found, \"invalid file should not be cached\")\n}\n\nfunc TestPartialParseCacheKeyFormat(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\tconfigPath := filepath.Join(tmpDir, \"terragrunt.hcl\")\n\tconfigContent := `dependencies { paths = [\"../app1\"] }`\n\trequire.NoError(t, os.WriteFile(configPath, []byte(configContent), 0644))\n\n\tfileInfo, err := os.Stat(configPath)\n\trequire.NoError(t, err)\n\n\texpectedCacheKey := fmt.Sprintf(\"configPath-%v-modTime-%v\", configPath, fileInfo.ModTime().UnixMicro())\n\n\thclCache := cache.NewCache[*hclparse.File](\"test-hcl-cache\")\n\tl := logger.CreateLogger()\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tctx = context.WithValue(ctx, config.HclCacheContextKey, hclCache)\n\tpctx = pctx.WithDecodeList(config.DependenciesBlock)\n\n\t_, err = config.PartialParseConfigFile(ctx, pctx, l, configPath, nil)\n\trequire.NoError(t, err)\n\n\t// Verify cache key format matches the expected pattern\n\tassert.Regexp(t, `^configPath-.*-modTime-\\d+$`, expectedCacheKey, \"cache key should match expected format\")\n\tassert.Contains(t, expectedCacheKey, configPath, \"cache key should contain config path\")\n\tassert.Contains(t, expectedCacheKey, strconv.FormatInt(fileInfo.ModTime().UnixMicro(), 10), \"cache key should contain mod time\")\n\n\t// Verify we can retrieve using the expected key\n\t_, found := hclCache.Get(ctx, expectedCacheKey)\n\trequire.True(t, found, \"should be able to retrieve using expected cache key format\")\n}\n\n// forceModTimeChange ensures the file at path has a modification time strictly after prev.\nfunc forceModTimeChange(t *testing.T, path string, prev time.Time) {\n\tt.Helper()\n\n\tdeadline := time.Now().Add(5 * time.Second)\n\tfor time.Now().Before(deadline) {\n\t\terr := os.Chtimes(path, time.Now(), time.Now())\n\n\t\trequire.NoError(t, err)\n\n\t\tif fileInfo, err := os.Stat(path); err == nil && fileInfo.ModTime().After(prev) {\n\t\t\treturn\n\t\t}\n\n\t\ttime.Sleep(1 * time.Millisecond)\n\t}\n\n\tt.Fatalf(\"Failed to change modification time of %s within 5 seconds\", path)\n}\n\n// TestPartialParseConfigCacheDifferentCallers verifies that the partial parse config cache\n// creates separate entries for different calling modules parsing the same file.\n// This prevents cross-environment dependency bugs where context-sensitive functions\n// (e.g. path_relative_to_include) return wrong values from a cached result.\nfunc TestPartialParseConfigCacheDifferentCallers(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a shared config file that both modules will parse.\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\tsharedConfigPath := filepath.Join(tmpDir, \"shared.hcl\")\n\tsharedContent := `dependencies { paths = [\"../app1\"] }`\n\trequire.NoError(t, os.WriteFile(sharedConfigPath, []byte(sharedContent), 0644))\n\n\t// Create two different module directories with distinct config paths.\n\tmoduleADir := filepath.Join(tmpDir, \"moduleA\")\n\tmoduleBDir := filepath.Join(tmpDir, \"moduleB\")\n\n\trequire.NoError(t, os.MkdirAll(moduleADir, 0755))\n\trequire.NoError(t, os.MkdirAll(moduleBDir, 0755))\n\n\tmoduleAConfigPath := filepath.Join(moduleADir, \"terragrunt.hcl\")\n\tmoduleBConfigPath := filepath.Join(moduleBDir, \"terragrunt.hcl\")\n\n\trequire.NoError(t, os.WriteFile(moduleAConfigPath, []byte(\"\"), 0644))\n\trequire.NoError(t, os.WriteFile(moduleBConfigPath, []byte(\"\"), 0644))\n\n\t// Setup shared caches in context so both modules use the same config cache.\n\thclCache := cache.NewCache[*hclparse.File](\"test-hcl-cache\")\n\tconfigCache := cache.NewCache[*config.TerragruntConfig](\"test-config-cache\")\n\tl := logger.CreateLogger()\n\n\t// Parse shared config from module A's context.\n\tctxA, pctxA := newTestParsingContext(t, moduleAConfigPath)\n\tctxA = context.WithValue(ctxA, config.HclCacheContextKey, hclCache)\n\tctxA = context.WithValue(ctxA, config.TerragruntConfigCacheContextKey, configCache)\n\tpctxA.UsePartialParseConfigCache = true\n\tpctxA = pctxA.WithDecodeList(config.DependenciesBlock)\n\n\tconfigA, err := config.PartialParseConfigFile(ctxA, pctxA, l, sharedConfigPath, nil)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, configA)\n\n\t// Parse shared config from module B's context (different TerragruntConfigPath).\n\tctxB, pctxB := newTestParsingContext(t, moduleBConfigPath)\n\tctxB = context.WithValue(ctxB, config.HclCacheContextKey, hclCache)\n\tctxB = context.WithValue(ctxB, config.TerragruntConfigCacheContextKey, configCache)\n\tpctxB.UsePartialParseConfigCache = true\n\tpctxB = pctxB.WithDecodeList(config.DependenciesBlock)\n\n\tconfigB, err := config.PartialParseConfigFile(ctxB, pctxB, l, sharedConfigPath, nil)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, configB)\n\n\t// Verify that two separate cache entries were created (one per caller),\n\t// not a single shared entry. This ensures context-sensitive functions like\n\t// path_relative_to_include() would evaluate correctly for each caller.\n\tconfigCache.Mutex.RLock()\n\tcacheLen := len(configCache.Cache)\n\tconfigCache.Mutex.RUnlock()\n\tassert.Equal(t, 2, cacheLen, \"config cache should have 2 entries (one per calling module), not 1\")\n\n\t// Both should return valid results.\n\tassert.Equal(t, []string{\"../app1\"}, configA.Dependencies.Paths)\n\tassert.Equal(t, []string{\"../app1\"}, configB.Dependencies.Paths)\n}\n"
  },
  {
    "path": "pkg/config/config_test.go",
    "content": "package config_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/codegen\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate/backend/s3\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/zclconf/go-cty/cty\"\n)\n\nfunc createLogger() log.Logger {\n\tformatter := format.NewFormatter(format.NewKeyValueFormatPlaceholders())\n\tformatter.SetDisabledColors(true)\n\n\treturn log.New(log.WithLevel(log.DebugLevel), log.WithFormatter(formatter))\n}\n\nfunc TestParseTerragruntConfigRemoteStateMinimalConfig(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nremote_state {\n  backend \t  = \"s3\"\n  config  \t  = {}\n  encryption  = {}\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\n\tassert.Nil(t, terragruntConfig.Terraform)\n\n\tassert.Empty(t, terragruntConfig.IamRole)\n\n\tif assert.NotNil(t, terragruntConfig.RemoteState) {\n\t\tassert.Equal(t, \"s3\", terragruntConfig.RemoteState.BackendName)\n\t\tassert.Empty(t, terragruntConfig.RemoteState.BackendConfig)\n\t\tassert.Empty(t, terragruntConfig.RemoteState.Encryption)\n\t}\n}\n\nfunc TestParseTerragruntConfigRemoteStateAttrMinimalConfig(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nremote_state = {\n  backend = \"s3\"\n  config  = {}\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\n\tassert.Nil(t, terragruntConfig.Terraform)\n\n\tassert.Empty(t, terragruntConfig.IamRole)\n\n\tif assert.NotNil(t, terragruntConfig.RemoteState) {\n\t\tassert.Equal(t, \"s3\", terragruntConfig.RemoteState.BackendName)\n\t\tassert.Empty(t, terragruntConfig.RemoteState.BackendConfig)\n\t}\n}\n\nfunc TestParseTerragruntConfigRemoteStateAttrStringBoolCoercion(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nlocals {\n  enable_flags = true\n}\n\nremote_state = {\n  backend = \"s3\"\n  disable_init = local.enable_flags ? \"true\" : \"false\"\n  disable_dependency_optimization = local.enable_flags ? \"false\" : \"true\"\n  config = {\n    bucket = \"my-bucket\"\n    key = \"terraform.tfstate\"\n    region = \"us-east-1\"\n  }\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\n\tif assert.NotNil(t, terragruntConfig.RemoteState) {\n\t\tassert.True(t, terragruntConfig.RemoteState.DisableInit)\n\t\tassert.False(t, terragruntConfig.RemoteState.DisableDependencyOptimization)\n\t}\n}\n\n// TestParseTerragruntConfigRemoteStateTernaryUseLockfile reproduces issue #5646:\n// when remote_state.config is produced from a local resolved with a ternary operator,\n// HCL type unification converts bool values (like use_lockfile) to strings.\n// The S3 backend must normalize these back to native bools before codegen.\nfunc TestParseTerragruntConfigRemoteStateTernaryUseLockfile(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nlocals {\n  is_prod = true\n  remote_state_config = local.is_prod ? {\n    bucket       = \"prod-bucket\"\n    key          = \"terraform.tfstate\"\n    region       = \"us-east-1\"\n    encrypt      = true\n    use_lockfile = true\n  } : {\n    bucket       = \"dev-bucket\"\n    key          = \"terraform.tfstate\"\n    region       = \"us-west-2\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n\nremote_state {\n  backend = \"s3\"\n  config  = local.remote_state_config\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\n\tif assert.NotNil(t, terragruntConfig.RemoteState) {\n\t\tassert.Equal(t, \"s3\", terragruntConfig.RemoteState.BackendName)\n\t\tassert.Equal(t, \"prod-bucket\", terragruntConfig.RemoteState.BackendConfig[\"bucket\"])\n\n\t\t// After parsing, verify S3 backend normalizes string bools in GetTFInitArgs\n\t\ts3Backend := s3.NewBackend()\n\t\tinitArgs := s3Backend.GetTFInitArgs(terragruntConfig.RemoteState.BackendConfig)\n\t\tassert.IsType(t, true, initArgs[\"use_lockfile\"])\n\t}\n}\n\nfunc TestParseTerragruntConfigRemoteStateAttrInvalidStringBool(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nremote_state = {\n  backend = \"s3\"\n  disable_init = \"maybe\"\n  config = {}\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\t_, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.Error(t, err)\n}\n\nfunc TestParseTerragruntConfigGenerateAttrStringBoolCoercion(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nlocals {\n  enable_flags = true\n}\n\ngenerate = {\n  provider = {\n    path = \"provider.tf\"\n    if_exists = \"overwrite\"\n    contents = \"provider \\\"aws\\\" {}\"\n    disable_signature = local.enable_flags ? \"true\" : \"false\"\n    disable = local.enable_flags ? \"false\" : \"true\"\n  }\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\n\tproviderGenerateConfig, ok := terragruntConfig.GenerateConfigs[\"provider\"]\n\trequire.True(t, ok)\n\tassert.True(t, providerGenerateConfig.DisableSignature)\n\tassert.False(t, providerGenerateConfig.Disable)\n}\n\nfunc TestParseTerragruntConfigGenerateAttrInvalidStringBool(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ngenerate = {\n  provider = {\n    path = \"provider.tf\"\n    if_exists = \"overwrite\"\n    contents = \"provider \\\"aws\\\" {}\"\n    disable_signature = \"maybe\"\n  }\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\t_, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.Error(t, err)\n}\n\nfunc TestParseTerragruntJsonConfigRemoteStateMinimalConfig(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\n{\n\t\"remote_state\": {\n\t\t\"backend\": \"s3\",\n\t\t\"config\": {}\n\t}\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntJSONConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\n\tassert.Nil(t, terragruntConfig.Terraform)\n\tassert.Empty(t, terragruntConfig.IamRole)\n\n\tif assert.NotNil(t, terragruntConfig.RemoteState) {\n\t\tassert.Equal(t, \"s3\", terragruntConfig.RemoteState.BackendName)\n\t\tassert.Empty(t, terragruntConfig.RemoteState.BackendConfig)\n\t}\n}\n\nfunc TestParseTerragruntHclConfigRemoteStateMissingBackend(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nremote_state {}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\t_, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"Missing required argument; The argument \\\"backend\\\" is required\")\n}\n\nfunc TestParseTerragruntHclConfigRemoteStateFullConfig(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nremote_state {\n\tbackend = \"s3\"\n\tconfig = {\n  \t\tencrypt = true\n  \t\tbucket = \"my-bucket\"\n  \t\tkey = \"terraform.tfstate\"\n  \t\tregion = \"us-east-1\"\n\t}\n\tencryption = {\n\t\tkey_provider = \"pbkdf2\"\n\t\tpassphrase = \"correct-horse-battery-staple\"\n\t}\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.Nil(t, terragruntConfig.Terraform)\n\tassert.Empty(t, terragruntConfig.IamRole)\n\n\tif assert.NotNil(t, terragruntConfig.RemoteState) {\n\t\tassert.Equal(t, \"s3\", terragruntConfig.RemoteState.BackendName)\n\t\tassert.NotEmpty(t, terragruntConfig.RemoteState.BackendConfig)\n\t\tassert.Equal(t, true, terragruntConfig.RemoteState.BackendConfig[\"encrypt\"])\n\t\tassert.Equal(t, \"my-bucket\", terragruntConfig.RemoteState.BackendConfig[\"bucket\"])\n\t\tassert.Equal(t, \"terraform.tfstate\", terragruntConfig.RemoteState.BackendConfig[\"key\"])\n\t\tassert.Equal(t, \"us-east-1\", terragruntConfig.RemoteState.BackendConfig[\"region\"])\n\t\tassert.Equal(t, \"pbkdf2\", terragruntConfig.RemoteState.Encryption[\"key_provider\"])\n\t\tassert.Equal(t, \"correct-horse-battery-staple\", terragruntConfig.RemoteState.Encryption[\"passphrase\"])\n\t}\n}\n\nfunc TestParseTerragruntJsonConfigRemoteStateFullConfig(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\n{\n\t\"remote_state\":{\n\t\t\"backend\":\"s3\",\n\t\t\"config\":{\n\t\t\t\"encrypt\": true,\n\t\t\t\"bucket\": \"my-bucket\",\n\t\t\t\"key\": \"terraform.tfstate\",\n\t\t\t\"region\":\"us-east-1\"\n\t\t},\n\t\t\"encryption\":{\n\t\t\t\"key_provider\": \"pbkdf2\",\n\t\t\t\"passphrase\": \"correct-horse-battery-staple\"\n\t\t}\n\t}\n}\n`\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntJSONConfigPath, cfg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.Nil(t, terragruntConfig.Terraform)\n\tassert.Empty(t, terragruntConfig.IamRole)\n\n\tif assert.NotNil(t, terragruntConfig.RemoteState) {\n\t\tassert.Equal(t, \"s3\", terragruntConfig.RemoteState.BackendName)\n\t\tassert.NotEmpty(t, terragruntConfig.RemoteState.BackendConfig)\n\t\tassert.Equal(t, true, terragruntConfig.RemoteState.BackendConfig[\"encrypt\"])\n\t\tassert.Equal(t, \"my-bucket\", terragruntConfig.RemoteState.BackendConfig[\"bucket\"])\n\t\tassert.Equal(t, \"terraform.tfstate\", terragruntConfig.RemoteState.BackendConfig[\"key\"])\n\t\tassert.Equal(t, \"us-east-1\", terragruntConfig.RemoteState.BackendConfig[\"region\"])\n\t\tassert.Equal(t, \"pbkdf2\", terragruntConfig.RemoteState.Encryption[\"key_provider\"])\n\t\tassert.Equal(t, \"correct-horse-battery-staple\", terragruntConfig.RemoteState.Encryption[\"passphrase\"])\n\t}\n}\n\nfunc TestParseTerragruntHclConfigRetryConfiguration(t *testing.T) {\n\tt.Parallel()\n\n\t// All three legacy retry attributes should be rejected\n\tcfg := `\nretryable_errors = [\".*Error.*\"]\n`\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\t_, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"retryable_errors\")\n\tassert.Contains(t, err.Error(), \"Unsupported argument\")\n}\n\nfunc TestParseTerragruntJsonConfigRetryConfiguration(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\n{\n\t\"retryable_errors\": [\".*Error.*\"]\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\t_, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntJSONConfigPath, cfg, nil)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"retryable_errors\")\n\t// JSON config gives a slightly different error message\n\tassert.True(t, strings.Contains(err.Error(), \"Unsupported argument\") || strings.Contains(err.Error(), \"No argument or block type\"))\n}\n\nfunc TestParseIamRole(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `iam_role = \"terragrunt-iam-role\"`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Terraform)\n\tassert.Nil(t, terragruntConfig.Dependencies)\n\n\tassert.Equal(t, \"terragrunt-iam-role\", terragruntConfig.IamRole)\n}\n\nfunc TestParseIamAssumeRoleDuration(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `iam_assume_role_duration = 36000`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Terraform)\n\tassert.Nil(t, terragruntConfig.Dependencies)\n\n\tassert.Equal(t, int64(36000), *terragruntConfig.IamAssumeRoleDuration)\n}\n\nfunc TestParseIamAssumeRoleSessionName(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `iam_assume_role_session_name = \"terragrunt-iam-assume-role-session-name\"`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Terraform)\n\tassert.Nil(t, terragruntConfig.Dependencies)\n\n\tassert.Equal(t, \"terragrunt-iam-assume-role-session-name\", terragruntConfig.IamAssumeRoleSessionName)\n}\n\nfunc TestParseIamWebIdentity(t *testing.T) {\n\tt.Parallel()\n\n\ttoken := \"test-token\"\n\n\tcfg := fmt.Sprintf(`iam_web_identity_token = \"%s\"`, token)\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Terraform)\n\tassert.Nil(t, terragruntConfig.Dependencies)\n\tassert.Empty(t, terragruntConfig.IamRole)\n\tassert.Equal(t, token, terragruntConfig.IamWebIdentityToken)\n}\n\nfunc TestParseTerragruntConfigDependenciesOnePath(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ndependencies {\n\tpaths = [\"../../test/fixtures/parent-folders/multiple-terragrunt-in-parents\"]\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Terraform)\n\n\tassert.Empty(t, terragruntConfig.IamRole)\n\n\tif assert.NotNil(t, terragruntConfig.Dependencies) {\n\t\tassert.Equal(t, []string{\"../../test/fixtures/parent-folders/multiple-terragrunt-in-parents\"}, terragruntConfig.Dependencies.Paths)\n\t}\n}\n\nfunc TestParseTerragruntConfigDependenciesMultiplePaths(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ndependencies {\n\tpaths = [\"../../test/fixtures/terragrunt\", \"../../test/fixtures/dirs\", \"../../test/fixtures/inputs\"]\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Terraform)\n\tassert.Empty(t, terragruntConfig.IamRole)\n\n\tif assert.NotNil(t, terragruntConfig.Dependencies) {\n\t\tassert.Equal(t, []string{\"../../test/fixtures/terragrunt\", \"../../test/fixtures/dirs\", \"../../test/fixtures/inputs\"}, terragruntConfig.Dependencies.Paths)\n\t}\n}\n\nfunc TestParseTerragruntConfigRemoteStateDynamoDbTerraformConfigAndDependenciesFullConfig(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nterraform {\n\tsource = \"foo\"\n}\n\nremote_state {\n\tbackend = \"s3\"\n\tconfig = {\n\t\tencrypt = true\n\t\tbucket = \"my-bucket\"\n\t\tkey = \"terraform.tfstate\"\n\t\tregion = \"us-east-1\"\n\t}\n}\n\ndependencies {\n\tpaths = [\"../../test/fixtures/terragrunt\", \"../../test/fixtures/dirs\", \"../../test/fixtures/inputs\"]\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.NotNil(t, terragruntConfig.Terraform)\n\tassert.NotNil(t, terragruntConfig.Terraform.Source)\n\tassert.Equal(t, \"foo\", *terragruntConfig.Terraform.Source)\n\tassert.Empty(t, terragruntConfig.IamRole)\n\n\tif assert.NotNil(t, terragruntConfig.RemoteState) {\n\t\tassert.Equal(t, \"s3\", terragruntConfig.RemoteState.BackendName)\n\t\tassert.NotEmpty(t, terragruntConfig.RemoteState.BackendConfig)\n\t\tassert.Equal(t, true, terragruntConfig.RemoteState.BackendConfig[\"encrypt\"])\n\t\tassert.Equal(t, \"my-bucket\", terragruntConfig.RemoteState.BackendConfig[\"bucket\"])\n\t\tassert.Equal(t, \"terraform.tfstate\", terragruntConfig.RemoteState.BackendConfig[\"key\"])\n\t\tassert.Equal(t, \"us-east-1\", terragruntConfig.RemoteState.BackendConfig[\"region\"])\n\t}\n\n\tif assert.NotNil(t, terragruntConfig.Dependencies) {\n\t\tassert.Equal(t, []string{\"../../test/fixtures/terragrunt\", \"../../test/fixtures/dirs\", \"../../test/fixtures/inputs\"}, terragruntConfig.Dependencies.Paths)\n\t}\n}\n\nfunc TestParseTerragruntJsonConfigRemoteStateDynamoDbTerraformConfigAndDependenciesFullConfig(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\n{\n\t\"terraform\": {\n\t\t\"source\": \"foo\"\n\t},\n\t\"remote_state\": {\n\t\t\"backend\": \"s3\",\n\t\t\"config\": {\n\t\t\t\"encrypt\": true,\n\t\t\t\"bucket\": \"my-bucket\",\n\t\t\t\"key\": \"terraform.tfstate\",\n\t\t\t\"region\": \"us-east-1\"\n\t\t}\n\t},\n\t\"dependencies\":{\n\t\t\"paths\": [\"../../test/fixtures/terragrunt\", \"../../test/fixtures/dirs\", \"../../test/fixtures/inputs\"]\n\t}\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntJSONConfigPath, cfg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.NotNil(t, terragruntConfig.Terraform)\n\tassert.NotNil(t, terragruntConfig.Terraform.Source)\n\tassert.Equal(t, \"foo\", *terragruntConfig.Terraform.Source)\n\tassert.Empty(t, terragruntConfig.IamRole)\n\n\tif assert.NotNil(t, terragruntConfig.RemoteState) {\n\t\tassert.Equal(t, \"s3\", terragruntConfig.RemoteState.BackendName)\n\t\tassert.NotEmpty(t, terragruntConfig.RemoteState.BackendConfig)\n\t\tassert.Equal(t, true, terragruntConfig.RemoteState.BackendConfig[\"encrypt\"])\n\t\tassert.Equal(t, \"my-bucket\", terragruntConfig.RemoteState.BackendConfig[\"bucket\"])\n\t\tassert.Equal(t, \"terraform.tfstate\", terragruntConfig.RemoteState.BackendConfig[\"key\"])\n\t\tassert.Equal(t, \"us-east-1\", terragruntConfig.RemoteState.BackendConfig[\"region\"])\n\t}\n\n\tif assert.NotNil(t, terragruntConfig.Dependencies) {\n\t\tassert.Equal(t, []string{\"../../test/fixtures/terragrunt\", \"../../test/fixtures/dirs\", \"../../test/fixtures/inputs\"}, terragruntConfig.Dependencies.Paths)\n\t}\n}\n\nfunc TestParseTerragruntConfigInclude(t *testing.T) {\n\tt.Parallel()\n\n\tcfg :=\n\t\tfmt.Sprintf(`\ninclude {\n\tpath = \"../../../%s\"\n}\n`, \"root.hcl\")\n\n\tcfgPath := \"../../test/fixtures/parent-folders/terragrunt-in-root/child/sub-child/sub-sub-child/\" + config.DefaultTerragruntConfigPath\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, cfgPath)\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, cfgPath, cfg, nil)\n\tif assert.NoError(t, err, \"Unexpected error: %v\", errors.New(err)) {\n\t\tassert.Nil(t, terragruntConfig.Terraform)\n\n\t\tif assert.NotNil(t, terragruntConfig.RemoteState) {\n\t\t\tassert.Equal(t, \"s3\", terragruntConfig.RemoteState.BackendName)\n\t\t\tassert.NotEmpty(t, terragruntConfig.RemoteState.BackendConfig)\n\t\t\tassert.Equal(t, true, terragruntConfig.RemoteState.BackendConfig[\"encrypt\"])\n\t\t\tassert.Equal(t, \"my-bucket\", terragruntConfig.RemoteState.BackendConfig[\"bucket\"])\n\t\t\tassert.Equal(t, \"child/sub-child/sub-sub-child/terraform.tfstate\", terragruntConfig.RemoteState.BackendConfig[\"key\"])\n\t\t\tassert.Equal(t, \"us-east-1\", terragruntConfig.RemoteState.BackendConfig[\"region\"])\n\t\t}\n\t}\n}\n\nfunc TestParseTerragruntConfigIncludeWithFindInParentFolders(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ninclude {\n\tpath = find_in_parent_folders(\"root.hcl\")\n}\n`\n\n\tcfgPath, err := filepath.Abs(filepath.Join(\"../..\", \"test\", \"fixtures\", \"parent-folders\", \"terragrunt-in-root\", \"child\", \"sub-child\", \"sub-sub-child\", config.DefaultTerragruntConfigPath))\n\trequire.NoError(t, err)\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, cfgPath)\n\n\tterragruntConfig, parseErr := config.ParseConfigString(ctx, pctx, l, cfgPath, cfg, nil)\n\tif assert.NoError(t, parseErr, \"Unexpected error: %v\", errors.New(parseErr)) {\n\t\tassert.Nil(t, terragruntConfig.Terraform)\n\n\t\tif assert.NotNil(t, terragruntConfig.RemoteState) {\n\t\t\tassert.Equal(t, \"s3\", terragruntConfig.RemoteState.BackendName)\n\t\t\tassert.NotEmpty(t, terragruntConfig.RemoteState.BackendConfig)\n\t\t\tassert.Equal(t, true, terragruntConfig.RemoteState.BackendConfig[\"encrypt\"])\n\t\t\tassert.Equal(t, \"my-bucket\", terragruntConfig.RemoteState.BackendConfig[\"bucket\"])\n\t\t\tassert.Equal(t, \"child/sub-child/sub-sub-child/terraform.tfstate\", terragruntConfig.RemoteState.BackendConfig[\"key\"])\n\t\t\tassert.Equal(t, \"us-east-1\", terragruntConfig.RemoteState.BackendConfig[\"region\"])\n\t\t}\n\t}\n}\n\nfunc TestParseTerragruntConfigIncludeOverrideRemote(t *testing.T) {\n\tt.Parallel()\n\n\tcfg :=\n\t\tfmt.Sprintf(`\ninclude {\n\tpath = \"../../../%s\"\n}\n\n# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n\tbackend = \"s3\"\n\tconfig = {\n\t\tencrypt = false\n\t\tbucket = \"override\"\n\t\tkey = \"override\"\n\t\tregion = \"override\"\n\t}\n}\n`, \"root.hcl\")\n\n\tcfgPath, err := filepath.Abs(filepath.Join(\"../..\", \"test\", \"fixtures\", \"parent-folders\", \"terragrunt-in-root\", \"child\", \"sub-child\", \"sub-sub-child\", config.DefaultTerragruntConfigPath))\n\trequire.NoError(t, err)\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, cfgPath)\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, cfgPath, cfg, nil)\n\tif assert.NoError(t, err, \"Unexpected error: %v\", errors.New(err)) {\n\t\tassert.Nil(t, terragruntConfig.Terraform)\n\n\t\tif assert.NotNil(t, terragruntConfig.RemoteState) {\n\t\t\tassert.Equal(t, \"s3\", terragruntConfig.RemoteState.BackendName)\n\t\t\tassert.NotEmpty(t, terragruntConfig.RemoteState.BackendConfig)\n\t\t\tassert.Equal(t, false, terragruntConfig.RemoteState.BackendConfig[\"encrypt\"])\n\t\t\tassert.Equal(t, \"override\", terragruntConfig.RemoteState.BackendConfig[\"bucket\"])\n\t\t\tassert.Equal(t, \"override\", terragruntConfig.RemoteState.BackendConfig[\"key\"])\n\t\t\tassert.Equal(t, \"override\", terragruntConfig.RemoteState.BackendConfig[\"region\"])\n\t\t}\n\t}\n}\n\nfunc TestParseTerragruntConfigIncludeOverrideAll(t *testing.T) {\n\tt.Parallel()\n\n\tcfg :=\n\t\tfmt.Sprintf(`\ninclude {\n\tpath = \"../../../%s\"\n}\n\nterraform {\n\tsource = \"foo\"\n}\n\n# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n\tbackend = \"s3\"\n\tconfig = {\n\t\tencrypt = false\n\t\tbucket = \"override\"\n\t\tkey = \"override\"\n\t\tregion = \"override\"\n\t}\n}\n\ndependencies {\n\tpaths = [\"override\"]\n}\n`, \"root.hcl\")\n\n\tconfigPath := \"../../test/fixtures/parent-folders/terragrunt-in-root/child/sub-child/sub-sub-child/\" + config.DefaultTerragruntConfigPath\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, configPath)\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, configPath, cfg, nil)\n\trequire.NoError(t, err, \"Unexpected error: %v\", errors.New(err))\n\n\tassert.NotNil(t, terragruntConfig.Terraform)\n\tassert.NotNil(t, terragruntConfig.Terraform.Source)\n\tassert.Equal(t, \"foo\", *terragruntConfig.Terraform.Source)\n\n\tif assert.NotNil(t, terragruntConfig.RemoteState) {\n\t\tassert.Equal(t, \"s3\", terragruntConfig.RemoteState.BackendName)\n\t\tassert.NotEmpty(t, terragruntConfig.RemoteState.BackendConfig)\n\t\tassert.Equal(t, false, terragruntConfig.RemoteState.BackendConfig[\"encrypt\"])\n\t\tassert.Equal(t, \"override\", terragruntConfig.RemoteState.BackendConfig[\"bucket\"])\n\t\tassert.Equal(t, \"override\", terragruntConfig.RemoteState.BackendConfig[\"key\"])\n\t\tassert.Equal(t, \"override\", terragruntConfig.RemoteState.BackendConfig[\"region\"])\n\t}\n\n\tassert.Equal(t, []string{\"override\"}, terragruntConfig.Dependencies.Paths)\n}\n\nfunc TestParseTerragruntJsonConfigIncludeOverrideAll(t *testing.T) {\n\tt.Parallel()\n\n\tcfg :=\n\t\tfmt.Sprintf(`\n{\n\t\"include\":{\n\t\t\"path\": \"../../../%s\"\n\t},\n\t\"terraform\":{\n\t\t\"source\": \"foo\"\n\t},\n\t\"remote_state\":{\n\t\t\"backend\": \"s3\",\n\t\t\"config\":{\n\t\t\t\"encrypt\": false,\n\t\t\t\"bucket\": \"override\",\n\t\t\t\"key\": \"override\",\n\t\t\t\"region\": \"override\"\n\t\t}\n\t},\n\t\"dependencies\":{\n\t\t\"paths\": [\"override\"]\n\t}\n}\n`, \"root.hcl\")\n\n\tcfgPath := \"../../test/fixtures/parent-folders/terragrunt-in-root/child/sub-child/sub-sub-child/\" + config.DefaultTerragruntJSONConfigPath\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, cfgPath)\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, cfgPath, cfg, nil)\n\trequire.NoError(t, err, \"Unexpected error: %v\", errors.New(err))\n\n\tassert.NotNil(t, terragruntConfig.Terraform)\n\tassert.NotNil(t, terragruntConfig.Terraform.Source)\n\tassert.Equal(t, \"foo\", *terragruntConfig.Terraform.Source)\n\n\tif assert.NotNil(t, terragruntConfig.RemoteState) {\n\t\tassert.Equal(t, \"s3\", terragruntConfig.RemoteState.BackendName)\n\t\tassert.NotEmpty(t, terragruntConfig.RemoteState.BackendConfig)\n\t\tassert.Equal(t, false, terragruntConfig.RemoteState.BackendConfig[\"encrypt\"])\n\t\tassert.Equal(t, \"override\", terragruntConfig.RemoteState.BackendConfig[\"bucket\"])\n\t\tassert.Equal(t, \"override\", terragruntConfig.RemoteState.BackendConfig[\"key\"])\n\t\tassert.Equal(t, \"override\", terragruntConfig.RemoteState.BackendConfig[\"region\"])\n\t}\n\n\tassert.Equal(t, []string{\"override\"}, terragruntConfig.Dependencies.Paths)\n}\n\nfunc TestParseTerragruntConfigTwoLevels(t *testing.T) {\n\tt.Parallel()\n\n\tconfigPathRel := \"../../test/fixtures/parent-folders/multiple-terragrunt-in-parents/child/sub-child/\" + config.RecommendedParentConfigName\n\tconfigPath := absPath(t, configPathRel)\n\n\tcfg, err := util.ReadFileAsString(configPathRel)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, configPath)\n\n\t_, actualErr := config.ParseConfigString(ctx, pctx, l, configPath, cfg, nil)\n\n\terrStr := actualErr.Error()\n\n\texpectedErrPath := absPath(t, \"../../test/fixtures/parent-folders/multiple-terragrunt-in-parents/child/\"+config.RecommendedParentConfigName)\n\texpectedErrStr := fmt.Sprintf(\"%s includes %s, which itself includes %s. Only one level of includes is allowed.\",\n\t\tconfigPath, expectedErrPath, expectedErrPath)\n\n\tassert.Contains(t, errStr, expectedErrStr)\n}\n\nfunc TestParseTerragruntConfigThreeLevels(t *testing.T) {\n\tt.Parallel()\n\n\tconfigPathRel := \"../../test/fixtures/parent-folders/multiple-terragrunt-in-parents/child/sub-child/sub-sub-child/\" + config.DefaultTerragruntConfigPath\n\tconfigPath := absPath(t, configPathRel)\n\n\tcfg, err := util.ReadFileAsString(configPathRel)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, configPath)\n\n\t_, actualErr := config.ParseConfigString(ctx, pctx, l, configPath, cfg, nil)\n\n\terrStr := actualErr.Error()\n\n\t// Build expected error string\n\texpectedErrPath1 := absPath(t, \"../../test/fixtures/parent-folders/multiple-terragrunt-in-parents/child/sub-child/\"+config.RecommendedParentConfigName)\n\texpectedErrPath2 := absPath(t, \"../../test/fixtures/parent-folders/multiple-terragrunt-in-parents/child/sub-child/\"+config.RecommendedParentConfigName)\n\texpectedErrStr := fmt.Sprintf(\"%s includes %s, which itself includes %s. Only one level of includes is allowed.\",\n\t\tconfigPath, expectedErrPath1, expectedErrPath2)\n\n\tassert.Contains(t, errStr, expectedErrStr)\n}\n\nfunc TestParseTerragruntConfigEmptyConfig(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := ``\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\n\tassert.Nil(t, terragruntConfig.Terraform)\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Dependencies)\n\tassert.Nil(t, terragruntConfig.PreventDestroy)\n\tassert.Empty(t, terragruntConfig.IamRole)\n\tassert.Empty(t, terragruntConfig.IamWebIdentityToken)\n}\n\nfunc TestParseTerragruntConfigEmptyConfigOldConfig(t *testing.T) {\n\tt.Parallel()\n\n\tcfgString := ``\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\tcfg, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfgString, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.Nil(t, cfg.RemoteState)\n}\n\nfunc TestParseTerragruntConfigTerraformNoSource(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nterraform {}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Dependencies)\n\n\tassert.NotNil(t, terragruntConfig.Terraform)\n\tassert.Nil(t, terragruntConfig.Terraform.Source)\n}\n\nfunc TestParseTerragruntConfigTerraformWithSource(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nterraform {\n\tsource = \"foo\"\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Dependencies)\n\n\tassert.NotNil(t, terragruntConfig.Terraform)\n\tassert.NotNil(t, terragruntConfig.Terraform.Source)\n\tassert.Equal(t, \"foo\", *terragruntConfig.Terraform.Source)\n}\n\nfunc TestParseTerragruntConfigTerraformWithExtraArguments(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nterraform {\n\textra_arguments \"secrets\" {\n\t\targuments = [\n\t\t\t\"-var-file=terraform.tfvars\",\n\t\t\t\"-var-file=terraform-secret.tfvars\"\n\t\t]\n\t\tcommands = get_terraform_commands_that_need_vars()\n\t\tenv_vars = {\n\t\t\tTEST_VAR = \"value\"\n\t\t}\n\t}\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Dependencies)\n\n\tif assert.NotNil(t, terragruntConfig.Terraform) {\n\t\tassert.Equal(t, \"secrets\", terragruntConfig.Terraform.ExtraArgs[0].Name)\n\t\tassert.Equal(t,\n\t\t\t&[]string{\n\t\t\t\t\"-var-file=terraform.tfvars\",\n\t\t\t\t\"-var-file=terraform-secret.tfvars\",\n\t\t\t},\n\t\t\tterragruntConfig.Terraform.ExtraArgs[0].Arguments)\n\t\tassert.Equal(t,\n\t\t\tconfig.TerraformCommandsNeedVars,\n\t\t\tterragruntConfig.Terraform.ExtraArgs[0].Commands)\n\n\t\tassert.Equal(t,\n\t\t\t&map[string]string{\"TEST_VAR\": \"value\"},\n\t\t\tterragruntConfig.Terraform.ExtraArgs[0].EnvVars)\n\t}\n}\n\nfunc TestParseTerragruntConfigTerraformWithMultipleExtraArguments(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nterraform {\n\textra_arguments \"json_output\" {\n\t\targuments = [\"-json\"]\n\t\tcommands = [\"output\"]\n\t}\n\n\textra_arguments \"fmt_diff\" {\n\t\targuments = [\"-diff=true\"]\n\t\tcommands = [\"fmt\"]\n\t}\n\n\textra_arguments \"required_tfvars\" {\n\t\trequired_var_files = [\n\t\t\t\"file1.tfvars\",\n\t\t\t\"file2.tfvars\"\n\t\t]\n\t\tcommands = get_terraform_commands_that_need_vars()\n\t}\n\n\textra_arguments \"optional_tfvars\" {\n\t\toptional_var_files = [\n\t\t\t\"opt1.tfvars\",\n\t\t\t\"opt2.tfvars\"\n\t\t]\n\t\tcommands = get_terraform_commands_that_need_vars()\n\t}\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Dependencies)\n\n\tif assert.NotNil(t, terragruntConfig.Terraform) {\n\t\tassert.Equal(t, \"json_output\", terragruntConfig.Terraform.ExtraArgs[0].Name)\n\t\tassert.Equal(t, &[]string{\"-json\"}, terragruntConfig.Terraform.ExtraArgs[0].Arguments)\n\t\tassert.Equal(t, []string{\"output\"}, terragruntConfig.Terraform.ExtraArgs[0].Commands)\n\t\tassert.Equal(t, \"fmt_diff\", terragruntConfig.Terraform.ExtraArgs[1].Name)\n\t\tassert.Equal(t, &[]string{\"-diff=true\"}, terragruntConfig.Terraform.ExtraArgs[1].Arguments)\n\t\tassert.Equal(t, []string{\"fmt\"}, terragruntConfig.Terraform.ExtraArgs[1].Commands)\n\t\tassert.Equal(t, \"required_tfvars\", terragruntConfig.Terraform.ExtraArgs[2].Name)\n\t\tassert.Equal(t, &[]string{\"file1.tfvars\", \"file2.tfvars\"}, terragruntConfig.Terraform.ExtraArgs[2].RequiredVarFiles)\n\t\tassert.Equal(t, config.TerraformCommandsNeedVars, terragruntConfig.Terraform.ExtraArgs[2].Commands)\n\t\tassert.Equal(t, \"optional_tfvars\", terragruntConfig.Terraform.ExtraArgs[3].Name)\n\t\tassert.Equal(t, &[]string{\"opt1.tfvars\", \"opt2.tfvars\"}, terragruntConfig.Terraform.ExtraArgs[3].OptionalVarFiles)\n\t\tassert.Equal(t, config.TerraformCommandsNeedVars, terragruntConfig.Terraform.ExtraArgs[3].Commands)\n\t}\n}\n\nfunc TestParseTerragruntJsonConfigTerraformWithMultipleExtraArguments(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\n{\n\t\"terraform\":{\n\t\t\"extra_arguments\":{\n\t\t\t\"json_output\":{\n\t\t\t\t\"arguments\": [\"-json\"],\n\t\t\t\t\"commands\": [\"output\"]\n\t\t\t},\n\t\t\t\"fmt_diff\":{\n\t\t\t\t\"arguments\": [\"-diff=true\"],\n\t\t\t\t\"commands\": [\"fmt\"]\n\t\t\t},\n\t\t\t\"required_tfvars\":{\n\t\t\t\t\"required_var_files\":[\n\t\t\t\t\t\"file1.tfvars\",\n\t\t\t\t\t\"file2.tfvars\"\n\t\t\t\t],\n\t\t\t\t\"commands\": \"${get_terraform_commands_that_need_vars()}\"\n\t\t\t},\n\t\t\t\"optional_tfvars\":{\n\t\t\t\t\"optional_var_files\":[\n\t\t\t\t\t\"opt1.tfvars\",\n\t\t\t\t\t\"opt2.tfvars\"\n\t\t\t\t],\n\t\t\t\t\"commands\": \"${get_terraform_commands_that_need_vars()}\"\n\t\t\t}\n\t\t}\n\t}\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntJSONConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Dependencies)\n\n\tif assert.NotNil(t, terragruntConfig.Terraform) {\n\t\tassert.Equal(t, \"json_output\", terragruntConfig.Terraform.ExtraArgs[0].Name)\n\t\tassert.Equal(t, &[]string{\"-json\"}, terragruntConfig.Terraform.ExtraArgs[0].Arguments)\n\t\tassert.Equal(t, []string{\"output\"}, terragruntConfig.Terraform.ExtraArgs[0].Commands)\n\t\tassert.Equal(t, \"fmt_diff\", terragruntConfig.Terraform.ExtraArgs[1].Name)\n\t\tassert.Equal(t, &[]string{\"-diff=true\"}, terragruntConfig.Terraform.ExtraArgs[1].Arguments)\n\t\tassert.Equal(t, []string{\"fmt\"}, terragruntConfig.Terraform.ExtraArgs[1].Commands)\n\t\tassert.Equal(t, \"required_tfvars\", terragruntConfig.Terraform.ExtraArgs[2].Name)\n\t\tassert.Equal(t, &[]string{\"file1.tfvars\", \"file2.tfvars\"}, terragruntConfig.Terraform.ExtraArgs[2].RequiredVarFiles)\n\t\tassert.Equal(t, config.TerraformCommandsNeedVars, terragruntConfig.Terraform.ExtraArgs[2].Commands)\n\t\tassert.Equal(t, \"optional_tfvars\", terragruntConfig.Terraform.ExtraArgs[3].Name)\n\t\tassert.Equal(t, &[]string{\"opt1.tfvars\", \"opt2.tfvars\"}, terragruntConfig.Terraform.ExtraArgs[3].OptionalVarFiles)\n\t\tassert.Equal(t, config.TerraformCommandsNeedVars, terragruntConfig.Terraform.ExtraArgs[3].Commands)\n\t}\n}\n\nfunc testDownloadDir(tb testing.TB, configPath string) string {\n\ttb.Helper()\n\n\t_, downloadDir := util.DefaultWorkingAndDownloadDirs(configPath)\n\n\treturn downloadDir\n}\n\nfunc TestFindConfigFilesInPathNone(t *testing.T) {\n\tt.Parallel()\n\n\texpected := []string{}\n\tactual, err := config.FindConfigFilesInPath(\"../../test/fixtures/config-files/none\", experiment.NewExperiments(), \"test\", map[string]string{}, testDownloadDir(t, \"test\"))\n\n\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestFindConfigFilesInPathOneConfig(t *testing.T) {\n\tt.Parallel()\n\n\texpected := []string{\"../../test/fixtures/config-files/one-config/subdir/terragrunt.hcl\"}\n\tactual, err := config.FindConfigFilesInPath(\"../../test/fixtures/config-files/one-config\", experiment.NewExperiments(), \"test\", map[string]string{}, testDownloadDir(t, \"test\"))\n\n\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestFindConfigFilesInPathOneJsonConfig(t *testing.T) {\n\tt.Parallel()\n\n\texpected := []string{\"../../test/fixtures/config-files/one-json-config/subdir/terragrunt.hcl.json\"}\n\tactual, err := config.FindConfigFilesInPath(\"../../test/fixtures/config-files/one-json-config\", experiment.NewExperiments(), \"test\", map[string]string{}, testDownloadDir(t, \"test\"))\n\n\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestFindConfigFilesInPathMultipleConfigs(t *testing.T) {\n\tt.Parallel()\n\n\texpected := []string{\n\t\t\"../../test/fixtures/config-files/multiple-configs/terragrunt.hcl\",\n\t\t\"../../test/fixtures/config-files/multiple-configs/subdir-2/subdir/terragrunt.hcl\",\n\t\t\"../../test/fixtures/config-files/multiple-configs/subdir-3/terragrunt.hcl\",\n\t}\n\tactual, err := config.FindConfigFilesInPath(\"../../test/fixtures/config-files/multiple-configs\", experiment.NewExperiments(), \"test\", map[string]string{}, testDownloadDir(t, \"test\"))\n\n\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n\tassert.ElementsMatch(t, expected, actual)\n}\n\nfunc TestFindConfigFilesInPathMultipleJsonConfigs(t *testing.T) {\n\tt.Parallel()\n\n\texpected := []string{\n\t\t\"../../test/fixtures/config-files/multiple-json-configs/terragrunt.hcl.json\",\n\t\t\"../../test/fixtures/config-files/multiple-json-configs/subdir-2/subdir/terragrunt.hcl.json\",\n\t\t\"../../test/fixtures/config-files/multiple-json-configs/subdir-3/terragrunt.hcl.json\",\n\t}\n\tactual, err := config.FindConfigFilesInPath(\"../../test/fixtures/config-files/multiple-json-configs\", experiment.NewExperiments(), \"test\", map[string]string{}, testDownloadDir(t, \"test\"))\n\n\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n\tassert.ElementsMatch(t, expected, actual)\n}\n\nfunc TestFindConfigFilesInPathMultipleMixedConfigs(t *testing.T) {\n\tt.Parallel()\n\n\texpected := []string{\n\t\t\"../../test/fixtures/config-files/multiple-mixed-configs/terragrunt.hcl.json\",\n\t\t\"../../test/fixtures/config-files/multiple-mixed-configs/subdir-2/subdir/terragrunt.hcl\",\n\t\t\"../../test/fixtures/config-files/multiple-mixed-configs/subdir-3/terragrunt.hcl.json\",\n\t}\n\tactual, err := config.FindConfigFilesInPath(\"../../test/fixtures/config-files/multiple-mixed-configs\", experiment.NewExperiments(), \"test\", map[string]string{}, testDownloadDir(t, \"test\"))\n\n\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n\tassert.ElementsMatch(t, expected, actual)\n}\n\nfunc TestFindConfigFilesIgnoresTerragruntCache(t *testing.T) {\n\tt.Parallel()\n\n\texpected := []string{\n\t\t\"../../test/fixtures/config-files/ignore-cached-config/terragrunt.hcl\",\n\t}\n\tactual, err := config.FindConfigFilesInPath(\"../../test/fixtures/config-files/ignore-cached-config\", experiment.NewExperiments(), \"test\", map[string]string{}, testDownloadDir(t, \"test\"))\n\n\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n\tassert.Equal(t, expected, actual)\n}\n\nfunc TestFindConfigFilesIgnoresTerraformDataDir(t *testing.T) {\n\tt.Parallel()\n\n\texpected := []string{\n\t\t\"../../test/fixtures/config-files/ignore-terraform-data-dir/.tf_data/modules/mod/terragrunt.hcl\",\n\t\t\"../../test/fixtures/config-files/ignore-terraform-data-dir/subdir/terragrunt.hcl\",\n\t\t\"../../test/fixtures/config-files/ignore-terraform-data-dir/subdir/.tf_data/modules/mod/terragrunt.hcl\",\n\t}\n\tactual, err := config.FindConfigFilesInPath(\"../../test/fixtures/config-files/ignore-terraform-data-dir\", experiment.NewExperiments(), \"test\", map[string]string{}, testDownloadDir(t, \"test\"))\n\n\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n\tassert.ElementsMatch(t, expected, actual)\n}\n\nfunc TestFindConfigFilesIgnoresTerraformDataDirEnv(t *testing.T) {\n\tt.Parallel()\n\n\texpected := []string{\n\t\t\"../../test/fixtures/config-files/ignore-terraform-data-dir/subdir/terragrunt.hcl\",\n\t\t\"../../test/fixtures/config-files/ignore-terraform-data-dir/subdir/.terraform/modules/mod/terragrunt.hcl\",\n\t}\n\tactual, err := config.FindConfigFilesInPath(\"../../test/fixtures/config-files/ignore-terraform-data-dir\", experiment.NewExperiments(), \"test\", map[string]string{\"TF_DATA_DIR\": \".tf_data\"}, testDownloadDir(t, \"test\"))\n\n\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n\tassert.ElementsMatch(t, expected, actual)\n}\n\nfunc TestFindConfigFilesIgnoresTerraformDataDirEnvPath(t *testing.T) {\n\tt.Parallel()\n\n\texpected := []string{\n\t\t\"../../test/fixtures/config-files/ignore-terraform-data-dir/.tf_data/modules/mod/terragrunt.hcl\",\n\t\t\"../../test/fixtures/config-files/ignore-terraform-data-dir/subdir/terragrunt.hcl\",\n\t\t\"../../test/fixtures/config-files/ignore-terraform-data-dir/subdir/.terraform/modules/mod/terragrunt.hcl\",\n\t}\n\tactual, err := config.FindConfigFilesInPath(\"../../test/fixtures/config-files/ignore-terraform-data-dir\", experiment.NewExperiments(), \"test\", map[string]string{\"TF_DATA_DIR\": \"subdir/.tf_data\"}, testDownloadDir(t, \"test\"))\n\n\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n\tassert.ElementsMatch(t, expected, actual)\n}\n\nfunc TestFindConfigFilesIgnoresTerraformDataDirEnvRoot(t *testing.T) {\n\tt.Parallel()\n\n\tworkingDir, err := filepath.Abs(filepath.Join(\"..\", \"..\", \"test\", \"fixtures\", \"config-files\", \"ignore-terraform-data-dir\"))\n\trequire.NoError(t, err)\n\n\tactual, err := config.FindConfigFilesInPath(workingDir, experiment.NewExperiments(), workingDir, map[string]string{\"TF_DATA_DIR\": filepath.Join(workingDir, \".tf_data\")}, testDownloadDir(t, workingDir))\n\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n\n\t// Create expected paths using filepath.Join for cross-platform compatibility\n\texpected := []string{\n\t\tfilepath.Join(workingDir, \"subdir\", \"terragrunt.hcl\"),\n\t\tfilepath.Join(workingDir, \"subdir\", \".terraform\", \"modules\", \"mod\", \"terragrunt.hcl\"),\n\t\tfilepath.Join(workingDir, \"subdir\", \".tf_data\", \"modules\", \"mod\", \"terragrunt.hcl\"),\n\t}\n\n\t// Sort both slices to ensure consistent order for comparison\n\tsort.Strings(actual)\n\tsort.Strings(expected)\n\n\t// Compare the paths using filepath.Clean to normalize them\n\tnormalizedActual := make([]string, len(actual))\n\tnormalizedExpected := make([]string, len(expected))\n\n\tfor i, path := range actual {\n\t\tnormalizedActual[i] = filepath.Clean(path)\n\t}\n\n\tfor i, path := range expected {\n\t\tnormalizedExpected[i] = filepath.Clean(path)\n\t}\n\n\tassert.Equal(t, normalizedExpected, normalizedActual)\n}\n\nfunc TestFindConfigFilesIgnoresDownloadDir(t *testing.T) {\n\tt.Parallel()\n\n\texpected := []string{\n\t\t\"../../test/fixtures/config-files/multiple-configs/terragrunt.hcl\",\n\t\t\"../../test/fixtures/config-files/multiple-configs/subdir-3/terragrunt.hcl\",\n\t}\n\tactual, err := config.FindConfigFilesInPath(\"../../test/fixtures/config-files/multiple-configs\", experiment.NewExperiments(), \"test\", map[string]string{}, \"../../test/fixtures/config-files/multiple-configs/subdir-2\")\n\n\trequire.NoError(t, err, \"Unexpected error: %v\", err)\n\tassert.ElementsMatch(t, expected, actual)\n}\n\nfunc TestParseTerragruntConfigPreventDestroyTrue(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nprevent_destroy = true\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.Nil(t, terragruntConfig.Terraform)\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Dependencies)\n\tassert.True(t, *terragruntConfig.PreventDestroy)\n}\n\nfunc TestParseTerragruntConfigPreventDestroyFalse(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nprevent_destroy = false\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.Nil(t, terragruntConfig.Terraform)\n\tassert.Nil(t, terragruntConfig.RemoteState)\n\tassert.Nil(t, terragruntConfig.Dependencies)\n\tassert.False(t, *terragruntConfig.PreventDestroy)\n}\n\nfunc TestParseTerragruntConfigSkipTrue(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nskip = true\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\t_, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"skip\")\n\tassert.Contains(t, err.Error(), \"Unsupported argument\")\n}\n\nfunc TestParseTerragruntConfigSkipFalse(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nskip = false\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\t_, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"skip\")\n\tassert.Contains(t, err.Error(), \"Unsupported argument\")\n}\n\nfunc TestIncludeFunctionsWorkInChildConfig(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ninclude {\n\tpath = find_in_parent_folders(\"root.hcl\")\n}\nterraform {\n\tsource = path_relative_to_include()\n}\n`\n\tl := createLogger()\n\n\tabsConfigPath, err := filepath.Abs(filepath.Join(\"../..\", \"test\", \"fixtures\", \"parent-folders\", \"terragrunt-in-root\", \"child\", config.DefaultTerragruntConfigPath))\n\trequire.NoError(t, err)\n\n\tctx, pctx := newTestParsingContext(t, absConfigPath)\n\tpctx.MaxFoldersToCheck = 5\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, pctx.TerragruntConfigPath, cfg, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tassert.Equal(t, \"child\", *terragruntConfig.Terraform.Source)\n}\n\nfunc TestModuleDependenciesMerge(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname     string\n\t\ttarget   []string\n\t\tsource   []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\t\"MergeNil\",\n\t\t\t[]string{\"../vpc\", \"../sql\"},\n\t\t\tnil,\n\t\t\t[]string{\"../vpc\", \"../sql\"},\n\t\t},\n\t\t{\n\t\t\t\"MergeOne\",\n\t\t\t[]string{\"../vpc\", \"../sql\"},\n\t\t\t[]string{\"../services\"},\n\t\t\t[]string{\"../vpc\", \"../sql\", \"../services\"},\n\t\t},\n\t\t{\n\t\t\t\"MergeMany\",\n\t\t\t[]string{\"../vpc\", \"../sql\"},\n\t\t\t[]string{\"../services\", \"../groups\"},\n\t\t\t[]string{\"../vpc\", \"../sql\", \"../services\", \"../groups\"},\n\t\t},\n\t\t{\n\t\t\t\"MergeEmpty\",\n\t\t\t[]string{\"../vpc\", \"../sql\"},\n\t\t\t[]string{},\n\t\t\t[]string{\"../vpc\", \"../sql\"},\n\t\t},\n\t\t{\n\t\t\t\"MergeOneExisting\",\n\t\t\t[]string{\"../vpc\", \"../sql\"},\n\t\t\t[]string{\"../vpc\"},\n\t\t\t[]string{\"../vpc\", \"../sql\"},\n\t\t},\n\t\t{\n\t\t\t\"MergeAllExisting\",\n\t\t\t[]string{\"../vpc\", \"../sql\"},\n\t\t\t[]string{\"../vpc\", \"../sql\"},\n\t\t\t[]string{\"../vpc\", \"../sql\"},\n\t\t},\n\t\t{\n\t\t\t\"MergeSomeExisting\",\n\t\t\t[]string{\"../vpc\", \"../sql\"},\n\t\t\t[]string{\"../vpc\", \"../services\"},\n\t\t\t[]string{\"../vpc\", \"../sql\", \"../services\"},\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.Parallel()\n\n\t\t\ttarget := &config.ModuleDependencies{Paths: tc.target}\n\n\t\t\tvar source *config.ModuleDependencies = nil\n\t\t\tif tc.source != nil {\n\t\t\t\tsource = &config.ModuleDependencies{Paths: tc.source}\n\t\t\t}\n\n\t\t\ttarget.Merge(source)\n\t\t\tassert.Equal(t, tc.expected, target.Paths)\n\t\t})\n\t}\n}\n\nfunc ptr(str string) *string {\n\treturn &str\n}\n\n// Run a benchmark on ReadTerragruntConfig for all fixtures possible.\n// This should reveal regressions on execution time due to new, changed or removed features.\nfunc BenchmarkReadTerragruntConfig(b *testing.B) {\n\t// Setup\n\tb.StopTimer()\n\n\ttestDir := \"../test\"\n\n\tfixtureDirs := []struct {\n\t\tdescription          string\n\t\tworkingDir           string\n\t\tusePartialParseCache bool\n\t}{\n\t\t{\"PartialParseBenchmarkRegressionCaching\", \"regressions/benchmark-parsing/production/deployment-group-1/webserver/terragrunt.hcl\", true},\n\t\t{\"PartialParseBenchmarkRegressionNoCache\", \"regressions/benchmark-parsing/production/deployment-group-1/webserver/terragrunt.hcl\", false},\n\t\t{\"PartialParseBenchmarkRegressionIncludesCaching\", \"regressions/benchmark-parsing-includes/production/deployment-group-1/webserver/terragrunt.hcl\", true},\n\t\t{\"PartialParseBenchmarkRegressionIncludesNoCache\", \"regressions/benchmark-parsing-includes/production/deployment-group-1/webserver/terragrunt.hcl\", false},\n\t}\n\n\t// Run benchmarks\n\tfor _, fixture := range fixtureDirs {\n\t\tb.Run(fixture.description, func(b *testing.B) {\n\t\t\tworkingDir, err := filepath.Abs(filepath.Join(testDir, fixture.workingDir))\n\t\t\trequire.NoError(b, err)\n\n\t\t\trequire.NoError(b, err)\n\n\t\t\tl := createLogger()\n\t\t\t_, pctx := newTestParsingContext(b, workingDir)\n\t\t\tpctx.UsePartialParseConfigCache = fixture.usePartialParseCache\n\n\t\t\tb.ResetTimer()\n\t\t\tb.StartTimer()\n\t\t\tactual, err := config.ReadTerragruntConfig(b.Context(), l, pctx, config.DefaultParserOptions(l, pctx.StrictControls))\n\t\t\tb.StopTimer()\n\t\t\trequire.NoError(b, err)\n\t\t\tassert.NotNil(b, actual)\n\t\t})\n\t}\n}\n\nfunc TestBestEffortParseConfigString(t *testing.T) {\n\tt.Parallel()\n\n\ttc := []struct {\n\t\texpectedConfig *config.TerragruntConfig\n\t\tname           string\n\t\tcfg            string\n\t\texpectError    bool\n\t}{\n\t\t{\n\t\t\tname: \"Simple\",\n\t\t\tcfg: `locals {\n\tsimple        = \"value\"\n\trequires_auth = run_cmd(\"bash\", \"-c\", \"exit 1\") // intentional error\n}\n`,\n\t\t\texpectError: true,\n\t\t\texpectedConfig: &config.TerragruntConfig{\n\t\t\t\tLocals: map[string]any{\n\t\t\t\t\t\"simple\": \"value\",\n\t\t\t\t},\n\t\t\t\tGenerateConfigs:   map[string]codegen.GenerateConfig{},\n\t\t\t\tProcessedIncludes: config.IncludeConfigsMap{},\n\t\t\t\tFieldsMetadata: map[string]map[string]any{\n\t\t\t\t\t\"locals-simple\": {\n\t\t\t\t\t\t\"found_in_file\": \"terragrunt.hcl\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Locals referencing each other\",\n\t\t\tcfg: `locals {\n\treference = local.simple\n\tsimple    = \"value\"\n}\n`,\n\t\t\texpectError: false,\n\t\t\texpectedConfig: &config.TerragruntConfig{\n\t\t\t\tLocals: map[string]any{\n\t\t\t\t\t\"reference\": \"value\",\n\t\t\t\t\t\"simple\":    \"value\",\n\t\t\t\t},\n\t\t\t\tGenerateConfigs:   map[string]codegen.GenerateConfig{},\n\t\t\t\tProcessedIncludes: config.IncludeConfigsMap{},\n\t\t\t\tFieldsMetadata: map[string]map[string]any{\n\t\t\t\t\t\"locals-reference\": {\n\t\t\t\t\t\t\"found_in_file\": \"terragrunt.hcl\",\n\t\t\t\t\t},\n\t\t\t\t\t\"locals-simple\": {\n\t\t\t\t\t\t\"found_in_file\": \"terragrunt.hcl\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tc {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tl := createLogger()\n\n\t\t\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\t\t\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, tt.cfg, nil)\n\t\t\tif tt.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tassert.Equal(t, tt.expectedConfig, terragruntConfig)\n\t\t})\n\t}\n}\n\nfunc TestParseConfigWithMissingIfExists(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `generate \"test\" {\n  path     = \"test.tf\"\n  contents = \"foo\"\n}`\n\n\tl := createLogger()\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.Error(t, err)\n\n\terrStr := err.Error()\n\thasIfExistsError := strings.Contains(errStr, \"if_exists\")\n\thasGenerateError := strings.Contains(errStr, \"generate\") || strings.Contains(errStr, \"Missing required argument\")\n\tassert.True(t, hasIfExistsError || hasGenerateError, \"Error message should mention missing if_exists attribute or generate block. Got: %s\", errStr)\n\tassert.NotNil(t, terragruntConfig)\n}\n\nfunc TestBestEffortParseConfigStringWDependency(t *testing.T) {\n\tt.Parallel()\n\n\tdepCfg := `locals {\n\tsimple = \"value\"\n\tfail   = run_cmd(\"bash\", \"-c\", \"exit 1\") // intentional error\n}`\n\n\tcfg := `locals {\n\tsimple = \"value\"\n\tfail   = run_cmd(\"bash\", \"-c\", \"exit 1\") // intentional error\n}\n\ndependency \"dep\" {\n\tconfig_path = \"../dep\"\n}`\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tdepPath := filepath.Join(tmpDir, \"dep\")\n\trequire.NoError(t, os.MkdirAll(depPath, 0755))\n\n\tdepCfgPath := filepath.Join(depPath, config.DefaultTerragruntConfigPath)\n\trequire.NoError(t, os.WriteFile(depCfgPath, []byte(depCfg), 0644))\n\n\tunitPath := filepath.Join(tmpDir, \"unit\")\n\trequire.NoError(t, os.MkdirAll(unitPath, 0755))\n\n\tunitCfgPath := filepath.Join(unitPath, config.DefaultTerragruntConfigPath)\n\trequire.NoError(t, os.WriteFile(unitCfgPath, []byte(cfg), 0644))\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\n\tpctx.WorkingDir = unitPath\n\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.Error(t, err)\n\n\tassert.Equal(t, &config.TerragruntConfig{\n\t\tLocals: map[string]any{\n\t\t\t\"simple\": \"value\",\n\t\t},\n\t\tGenerateConfigs:   map[string]codegen.GenerateConfig{},\n\t\tProcessedIncludes: config.IncludeConfigsMap{},\n\t\tFieldsMetadata: map[string]map[string]any{\n\t\t\t\"dependency-dep\": {\n\t\t\t\t\"found_in_file\": \"terragrunt.hcl\",\n\t\t\t},\n\t\t\t\"locals-simple\": {\n\t\t\t\t\"found_in_file\": \"terragrunt.hcl\",\n\t\t\t},\n\t\t},\n\t\tTerragruntDependencies: config.Dependencies{\n\t\t\tconfig.Dependency{\n\t\t\t\tName:       \"dep\",\n\t\t\t\tConfigPath: cty.StringVal(\"../dep\"),\n\t\t\t},\n\t\t},\n\t}, terragruntConfig)\n}\n\nfunc TestWriteTo(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nlocals {\n\tstring = \"value\"\n\tbool   = true\n\tnumber = 123\n\tlist   = [\"a\", \"b\", \"c\"]\n}\n\nterraform {\n\tsource = \"git::git@github.com:org/repo.git//modules/test?ref=v0.1.0\"\n\n\textra_arguments \"secrets\" {\n\t\tcommands = [\"plan\", \"apply\"]\n\t\targuments = [\"-var-file=secrets.tfvars\"]\n\t\trequired_var_files = [\"common.tfvars\"]\n\t\toptional_var_files = [\"optional.tfvars\"]\n\t\tenv_vars = {\n\t\t\tTEST_VAR = \"value\"\n\t\t}\n\t}\n\n\tbefore_hook \"before\" {\n\t\tcommands = [\"plan\", \"apply\"]\n\t\texecute  = [\"echo\", \"before\"]\n\t\tworking_dir = \"before_dir\"\n\t}\n\n\tafter_hook \"after\" {\n\t\tcommands = [\"plan\", \"apply\"]\n\t\texecute  = [\"echo\", \"after\"]\n\t\tworking_dir = \"after_dir\"\n\t}\n\n\terror_hook \"error\" {\n\t\tcommands = [\"plan\", \"apply\"]\n\t\texecute  = [\"echo\", \"error\"]\n\t\ton_errors = [\n\t\t\t\".*Error.*\",\n\t\t\t\".*Exception.*\"\n\t\t]\n\t\tworking_dir = \"error_dir\"\n\t}\n}\n\nengine {\n\tsource = \"github.com/gruntwork-io/terragrunt\"\n\tversion = \"v0.1.0\"\n\ttype = \"rpc\"\n\tmeta = {\n\t\tkey = \"value\"\n\t}\n}\n\nexclude {\n\texclude_dependencies = true\n\tactions = [\"init\", \"plan\"]\n\tif = true\n}\n\nerrors {\n\tretry \"test_retry\" {\n\t\tmax_attempts = 3\n\t\tsleep_interval_sec = 5\n\t\tretryable_errors = [\n\t\t\t\".*Error.*\",\n\t\t\t\".*Exception.*\"\n\t\t]\n\t}\n\n\tignore \"test_ignore\" {\n\t\tignorable_errors = [\n\t\t\t\".*Warning.*\",\n\t\t\t\".*Deprecated.*\"\n\t\t]\n\t\tmessage = \"Ignoring warning messages\"\n\t\tsignals = {\n\t\t\tkey = \"value\"\n\t\t}\n\t}\n}\n\n// The catalog block won't actually show up when using\n// ParseConfigString. It probably should, but that's not\n// a problem for this test.\n//\n// catalog {\n// \tdefault_template = \"default.hcl\"\n// \turls = [\n// \t\t\"github.com/org/repo//templates/template1.hcl\",\n// \t\t\"github.com/org/repo//templates/template2.hcl\"\n// \t]\n// }\n\nremote_state {\n\tbackend = \"s3\"\n\tdisable_init = true\n\tdisable_dependency_optimization = true\n\tconfig = {\n\t\tbucket = \"my-bucket\"\n\t\tkey    = \"terraform.tfstate\"\n\t\tregion = \"us-east-1\"\n\t}\n}\n\n// These aren't worth testing because they require filesystem operations\n// as currently implemented, and we don't want to do that in this test.\n//\n// dependencies {\n// \tpaths = [\"../vpc\", \"../database\"]\n// }\n\n// dependency \"vpc\" {\n// \tconfig_path = \"../vpc\"\n// \tskip_outputs = true\n// \tmock_outputs = {\n// \t\tvpc_id = \"mock-vpc-id\"\n// \t}\n// }\n\ngenerate \"provider\" {\n\tpath = \"provider.tf\"\n\tif_exists = \"overwrite\"\n\tcontents = <<EOF\nprovider \"aws\" {\n\tregion = \"us-east-1\"\n}\nEOF\n\tcomment_prefix = \"//\"\n\tdisable_signature = true\n\tdisable = false\n}\n\nfeature \"test_feature\" {\n\tdefault = true\n}\n\nterraform_binary = \"terraform\"\nterraform_version_constraint = \">= 1.0.0\"\nterragrunt_version_constraint = \">= 0.36.0\"\ndownload_dir = \".terragrunt-cache\"\nprevent_destroy = true\niam_role = \"arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME\"\niam_assume_role_duration = 3600\niam_assume_role_session_name = \"terragrunt\"\n\ninputs = {\n\tstring = \"value\"\n\tbool   = true\n\tnumber = 123\n\tlist   = [\"a\", \"b\", \"c\"]\n\tmap    = {\n\t\tkey = \"value\"\n\t}\n}\n`\n\n\tl := createLogger()\n\n\tctx, pctx := newTestParsingContext(t, \"test-time-mock\")\n\tterragruntConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\n\t// Write the config to a buffer\n\tbuf := &bytes.Buffer{}\n\tn, err := terragruntConfig.WriteTo(buf)\n\trequire.NoError(t, err)\n\tassert.Positive(t, n)\n\n\t// Parse the written config back\n\trereadConfig, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, buf.String(), nil)\n\trequire.NoError(t, err)\n\n\t// Verify the configs match\n\tassert.Equal(t, terragruntConfig.Locals, rereadConfig.Locals)\n\tassert.Equal(t, terragruntConfig.Terraform.Source, rereadConfig.Terraform.Source)\n\tassert.Equal(t, terragruntConfig.Terraform.ExtraArgs, rereadConfig.Terraform.ExtraArgs)\n\tassert.Equal(t, terragruntConfig.Terraform.BeforeHooks, rereadConfig.Terraform.BeforeHooks)\n\tassert.Equal(t, terragruntConfig.Terraform.AfterHooks, rereadConfig.Terraform.AfterHooks)\n\tassert.Equal(t, terragruntConfig.Terraform.ErrorHooks, rereadConfig.Terraform.ErrorHooks)\n\n\t// Test engine block\n\tassert.Equal(t, terragruntConfig.Engine.Source, rereadConfig.Engine.Source)\n\tassert.Equal(t, terragruntConfig.Engine.Version, rereadConfig.Engine.Version)\n\tassert.Equal(t, terragruntConfig.Engine.Type, rereadConfig.Engine.Type)\n\tassert.Equal(t, terragruntConfig.Engine.Meta, rereadConfig.Engine.Meta)\n\n\t// Test exclude block\n\tassert.Equal(t, terragruntConfig.Exclude.ExcludeDependencies, rereadConfig.Exclude.ExcludeDependencies)\n\tassert.Equal(t, terragruntConfig.Exclude.Actions, rereadConfig.Exclude.Actions)\n\tassert.Equal(t, terragruntConfig.Exclude.If, rereadConfig.Exclude.If)\n\n\t// Test errors block\n\tassert.Len(t, terragruntConfig.Errors.Retry, len(rereadConfig.Errors.Retry))\n\n\tif len(terragruntConfig.Errors.Retry) > 0 {\n\t\tassert.Equal(t, terragruntConfig.Errors.Retry[0].Label, rereadConfig.Errors.Retry[0].Label)\n\t\tassert.Equal(t, terragruntConfig.Errors.Retry[0].MaxAttempts, rereadConfig.Errors.Retry[0].MaxAttempts)\n\t\tassert.Equal(t, terragruntConfig.Errors.Retry[0].SleepIntervalSec, rereadConfig.Errors.Retry[0].SleepIntervalSec)\n\t\tassert.Equal(t, terragruntConfig.Errors.Retry[0].RetryableErrors, rereadConfig.Errors.Retry[0].RetryableErrors)\n\t}\n\n\tassert.Len(t, terragruntConfig.Errors.Ignore, len(rereadConfig.Errors.Ignore))\n\n\tif len(terragruntConfig.Errors.Ignore) > 0 {\n\t\tassert.Equal(t, terragruntConfig.Errors.Ignore[0].Label, rereadConfig.Errors.Ignore[0].Label)\n\t\tassert.Equal(t, terragruntConfig.Errors.Ignore[0].IgnorableErrors, rereadConfig.Errors.Ignore[0].IgnorableErrors)\n\t\tassert.Equal(t, terragruntConfig.Errors.Ignore[0].Message, rereadConfig.Errors.Ignore[0].Message)\n\t\tassert.Equal(t, terragruntConfig.Errors.Ignore[0].Signals, rereadConfig.Errors.Ignore[0].Signals)\n\t}\n\n\t// The catalog block won't actually show up when using\n\t// ParseConfigString. It probably should, but that's not\n\t// a problem for this test.\n\t//\n\t// assert.Equal(t, terragruntConfig.Catalog.DefaultTemplate, rereadConfig.Catalog.DefaultTemplate)\n\t// assert.Equal(t, terragruntConfig.Catalog.URLs, rereadConfig.Catalog.URLs)\n\n\tassert.Equal(t, terragruntConfig.RemoteState.BackendName, rereadConfig.RemoteState.BackendName)\n\tassert.Equal(t, terragruntConfig.RemoteState.DisableInit, rereadConfig.RemoteState.DisableInit)\n\tassert.Equal(t, terragruntConfig.RemoteState.DisableDependencyOptimization, rereadConfig.RemoteState.DisableDependencyOptimization)\n\tassert.Equal(t, terragruntConfig.RemoteState.BackendConfig, rereadConfig.RemoteState.BackendConfig)\n\n\t// We don't test dependencies here because they require filesystem operations.\n\t// assert.Equal(t, terragruntConfig.Dependencies.Paths, rereadConfig.Dependencies.Paths)\n\t// assert.Equal(t, terragruntConfig.TerragruntDependencies, rereadConfig.TerragruntDependencies)\n\n\tassert.Equal(t, terragruntConfig.GenerateConfigs, rereadConfig.GenerateConfigs)\n\tassert.Equal(t, terragruntConfig.FeatureFlags, rereadConfig.FeatureFlags)\n\tassert.Equal(t, terragruntConfig.TerraformBinary, rereadConfig.TerraformBinary)\n\tassert.Equal(t, terragruntConfig.TerraformVersionConstraint, rereadConfig.TerraformVersionConstraint)\n\tassert.Equal(t, terragruntConfig.TerragruntVersionConstraint, rereadConfig.TerragruntVersionConstraint)\n\tassert.Equal(t, terragruntConfig.DownloadDir, rereadConfig.DownloadDir)\n\tassert.Equal(t, terragruntConfig.PreventDestroy, rereadConfig.PreventDestroy)\n\tassert.Equal(t, terragruntConfig.IamRole, rereadConfig.IamRole)\n\tassert.Equal(t, terragruntConfig.IamAssumeRoleDuration, rereadConfig.IamAssumeRoleDuration)\n\tassert.Equal(t, terragruntConfig.IamAssumeRoleSessionName, rereadConfig.IamAssumeRoleSessionName)\n\tassert.Equal(t, terragruntConfig.Inputs, rereadConfig.Inputs)\n}\n"
  },
  {
    "path": "pkg/config/context.go",
    "content": "package config\n\nimport (\n\t\"context\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cache\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n)\n\ntype configKey byte\n\nconst (\n\tHclCacheContextKey              configKey = iota\n\tTerragruntConfigCacheContextKey configKey = iota\n\tRunCmdCacheContextKey           configKey = iota\n\tDependencyOutputCacheContextKey configKey = iota\n\tJSONOutputCacheContextKey       configKey = iota\n\tOutputLocksContextKey           configKey = iota\n\tSopsCacheContextKey             configKey = iota\n\n\thclCacheName              = \"hclCache\"\n\tconfigCacheName           = \"configCache\"\n\trunCmdCacheName           = \"runCmdCache\"\n\tdependencyOutputCacheName = \"dependencyOutputCache\"\n\tjsonOutputCacheName       = \"jsonOutputCache\"\n\tsopsCacheName             = \"sopsCache\"\n)\n\n// WithConfigValues add to context default values for configuration.\nfunc WithConfigValues(ctx context.Context) context.Context {\n\tctx = context.WithValue(ctx, HclCacheContextKey, cache.NewCache[*hclparse.File](hclCacheName))\n\tctx = context.WithValue(ctx, TerragruntConfigCacheContextKey, cache.NewCache[*TerragruntConfig](configCacheName))\n\tctx = context.WithValue(ctx, RunCmdCacheContextKey, cache.NewCache[*RunCmdCacheEntry](runCmdCacheName))\n\tctx = context.WithValue(ctx, DependencyOutputCacheContextKey, cache.NewCache[*dependencyOutputCache](dependencyOutputCacheName))\n\tctx = context.WithValue(ctx, JSONOutputCacheContextKey, cache.NewCache[[]byte](jsonOutputCacheName))\n\tctx = context.WithValue(ctx, OutputLocksContextKey, util.NewKeyLocks())\n\tctx = context.WithValue(ctx, SopsCacheContextKey, cache.NewCache[string](sopsCacheName))\n\n\treturn ctx\n}\n"
  },
  {
    "path": "pkg/config/cty_helpers.go",
    "content": "//nolint:dupl\npackage config\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\n\t\"dario.cat/mergo\"\n\t\"github.com/zclconf/go-cty/cty\"\n\t\"github.com/zclconf/go-cty/cty/function\"\n\tctyjson \"github.com/zclconf/go-cty/cty/json\"\n\n\t\"maps\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/ctyhelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\n// Create a cty Function that takes as input parameters a slice of strings (var args, so this slice could be of any\n// length) and returns as output a string. The implementation of the function calls the given toWrap function, passing\n// it the input parameters string slice as well as the given include and terragruntOptions.\nfunc wrapStringSliceToStringAsFuncImpl(\n\tctx context.Context,\n\tpctx *ParsingContext,\n\tl log.Logger,\n\ttoWrap func(ctx context.Context, pctx *ParsingContext, l log.Logger, params []string) (string, error),\n) function.Function {\n\treturn function.New(&function.Spec{\n\t\tVarParam: &function.Parameter{Type: cty.String},\n\t\tType:     function.StaticReturnType(cty.String),\n\t\tImpl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {\n\t\t\tparams, err := ctySliceToStringSlice(args)\n\t\t\tif err != nil {\n\t\t\t\treturn cty.StringVal(\"\"), err\n\t\t\t}\n\n\t\t\tout, err := toWrap(ctx, pctx, l, params)\n\t\t\tif err != nil {\n\t\t\t\treturn cty.StringVal(\"\"), err\n\t\t\t}\n\n\t\t\treturn cty.StringVal(out), nil\n\t\t},\n\t})\n}\n\nfunc wrapStringSliceToNumberAsFuncImpl(\n\tctx context.Context,\n\tpctx *ParsingContext,\n\tl log.Logger,\n\ttoWrap func(ctx context.Context, pctx *ParsingContext, l log.Logger, params []string) (int64, error),\n) function.Function {\n\treturn function.New(&function.Spec{\n\t\tVarParam: &function.Parameter{Type: cty.String},\n\t\tType:     function.StaticReturnType(cty.Number),\n\t\tImpl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {\n\t\t\tparams, err := ctySliceToStringSlice(args)\n\t\t\tif err != nil {\n\t\t\t\treturn cty.NumberIntVal(0), err\n\t\t\t}\n\n\t\t\tout, err := toWrap(ctx, pctx, l, params)\n\t\t\tif err != nil {\n\t\t\t\treturn cty.NumberIntVal(0), err\n\t\t\t}\n\n\t\t\treturn cty.NumberIntVal(out), nil\n\t\t},\n\t})\n}\n\nfunc wrapStringSliceToBoolAsFuncImpl(\n\tctx context.Context,\n\tpctx *ParsingContext,\n\ttoWrap func(ctx context.Context, pctx *ParsingContext, params []string) (bool, error),\n) function.Function {\n\treturn function.New(&function.Spec{\n\t\tVarParam: &function.Parameter{Type: cty.String},\n\t\tType:     function.StaticReturnType(cty.Bool),\n\t\tImpl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {\n\t\t\tparams, err := ctySliceToStringSlice(args)\n\t\t\tif err != nil {\n\t\t\t\treturn cty.BoolVal(false), err\n\t\t\t}\n\n\t\t\tout, err := toWrap(ctx, pctx, params)\n\t\t\tif err != nil {\n\t\t\t\treturn cty.BoolVal(false), err\n\t\t\t}\n\n\t\t\treturn cty.BoolVal(out), nil\n\t\t},\n\t})\n}\n\n// Create a cty Function that takes no input parameters and returns as output a string. The implementation of the\n// function calls the given toWrap function, passing it the given include and terragruntOptions.\nfunc wrapVoidToStringAsFuncImpl(\n\tctx context.Context,\n\tpctx *ParsingContext,\n\tl log.Logger,\n\ttoWrap func(ctx context.Context, pctx *ParsingContext, l log.Logger) (string, error),\n) function.Function {\n\treturn function.New(&function.Spec{\n\t\tType: function.StaticReturnType(cty.String),\n\t\tImpl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {\n\t\t\tout, err := toWrap(ctx, pctx, l)\n\t\t\tif err != nil {\n\t\t\t\treturn cty.StringVal(\"\"), err\n\t\t\t}\n\n\t\t\treturn cty.StringVal(out), nil\n\t\t},\n\t})\n}\n\n// Create a cty Function that takes no input parameters and returns as output an empty string.\nfunc wrapVoidToEmptyStringAsFuncImpl() function.Function {\n\treturn function.New(&function.Spec{\n\t\tType: function.StaticReturnType(cty.String),\n\t\tImpl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {\n\t\t\treturn cty.StringVal(\"\"), nil\n\t\t},\n\t})\n}\n\n// Create a cty Function that takes no input parameters and returns as output a string slice. The implementation of the\n// function calls the given toWrap function, passing it the given include and terragruntOptions.\nfunc wrapVoidToStringSliceAsFuncImpl(\n\tctx context.Context,\n\tpctx *ParsingContext,\n\tl log.Logger,\n\ttoWrap func(ctx context.Context, pctx *ParsingContext, l log.Logger) ([]string, error),\n) function.Function {\n\treturn function.New(&function.Spec{\n\t\tType: function.StaticReturnType(cty.List(cty.String)),\n\t\tImpl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {\n\t\t\toutVals, err := toWrap(ctx, pctx, l)\n\t\t\tif err != nil || len(outVals) == 0 {\n\t\t\t\treturn cty.ListValEmpty(cty.String), err\n\t\t\t}\n\n\t\t\toutCtyVals := []cty.Value{}\n\t\t\tfor _, val := range outVals {\n\t\t\t\toutCtyVals = append(outCtyVals, cty.StringVal(val))\n\t\t\t}\n\n\t\t\treturn cty.ListVal(outCtyVals), nil\n\t\t},\n\t})\n}\n\n// Create a cty Function that takes no input parameters and returns as output a string slice. The implementation of the\n// function returns the given string slice.\nfunc wrapStaticValueToStringSliceAsFuncImpl(out []string) function.Function {\n\treturn function.New(&function.Spec{\n\t\tType: function.StaticReturnType(cty.List(cty.String)),\n\t\tImpl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {\n\t\t\toutVals := make([]cty.Value, 0, len(out))\n\t\t\tfor _, val := range out {\n\t\t\t\toutVals = append(outVals, cty.StringVal(val))\n\t\t\t}\n\n\t\t\treturn cty.ListVal(outVals), nil\n\t\t},\n\t})\n}\n\n// Convert the slice of cty values to a slice of strings. If any of the values in the given slice is not a string,\n// return an error.\nfunc ctySliceToStringSlice(args []cty.Value) ([]string, error) {\n\tvar out = make([]string, 0, len(args))\n\n\tfor _, arg := range args {\n\t\tif arg.Type() != cty.String {\n\t\t\treturn nil, errors.New(InvalidParameterTypeError{Expected: \"string\", Actual: arg.Type().FriendlyName()})\n\t\t}\n\n\t\tout = append(out, arg.AsString())\n\t}\n\n\treturn out, nil\n}\n\n// shallowMergeCtyMaps performs a shallow merge of two cty value objects.\nfunc shallowMergeCtyMaps(target cty.Value, source cty.Value) (*cty.Value, error) {\n\toutMap, err := ctyhelper.ParseCtyValueToMap(target)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tSourceMap, err := ctyhelper.ParseCtyValueToMap(source)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor key, sourceValue := range SourceMap {\n\t\tif _, ok := outMap[key]; !ok {\n\t\t\toutMap[key] = sourceValue\n\t\t}\n\t}\n\n\toutCty, err := convertToCtyWithJSON(outMap)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &outCty, nil\n}\n\nfunc deepMergeCtyMaps(target cty.Value, source cty.Value) (*cty.Value, error) {\n\treturn deepMergeCtyMapsMapOnly(target, source, mergo.WithAppendSlice)\n}\n\n// deepMergeCtyMapsMapOnly implements a deep merge of two cty value objects. We can't directly merge two cty.Value objects, so\n// we cheat by using map[string]any as an intermediary. Note that this assumes the provided cty value objects\n// are already maps or objects in HCL land.\nfunc deepMergeCtyMapsMapOnly(target cty.Value, source cty.Value, opts ...func(*mergo.Config)) (*cty.Value, error) {\n\toutMap := make(map[string]any)\n\n\ttargetMap, err := ctyhelper.ParseCtyValueToMap(target)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsourceMap, err := ctyhelper.ParseCtyValueToMap(source)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmaps.Copy(outMap, targetMap)\n\n\tif err := mergo.Merge(&outMap, sourceMap, append(opts, mergo.WithOverride)...); err != nil {\n\t\treturn nil, err\n\t}\n\n\toutCty, err := convertToCtyWithJSON(outMap)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &outCty, nil\n}\n\n// ConvertValuesMapToCtyVal takes a map of name - cty.Value pairs and converts to a single cty.Value object.\nfunc ConvertValuesMapToCtyVal(valMap map[string]cty.Value) (cty.Value, error) {\n\tif len(valMap) == 0 {\n\t\t// Return an empty object instead of NilVal for empty maps.\n\t\treturn cty.EmptyObjectVal, nil\n\t}\n\n\t// Use cty.ObjectVal directly instead of gocty.ToCtyValue to preserve marks (like sensitive())\n\treturn cty.ObjectVal(valMap), nil\n}\n\n// generateTypeFromValuesMap takes a values map and returns an object type that has the same number of fields, but\n// bound to each type of the underlying evaluated expression. This is the only way the HCL decoder will be happy, as\n// object type is the only map type that allows different types for each attribute (cty.Map requires all attributes to\n// have the same type.\nfunc generateTypeFromValuesMap(valMap map[string]cty.Value) cty.Type {\n\toutType := map[string]cty.Type{}\n\tfor k, v := range valMap {\n\t\toutType[k] = v.Type()\n\t}\n\n\treturn cty.Object(outType)\n}\n\n// includeMapAsCtyVal converts the include map into a cty.Value struct that can be exposed to the child config. For\n// backward compatibility, this function will return the included config object if the config only defines a single bare\n// include block that is exposed.\n// NOTE: When evaluated in a partial parse ctx, only the partially parsed ctx is available in the expose. This\n// ensures that we can parse the child config without having access to dependencies when constructing the dependency\n// graph.\nfunc includeMapAsCtyVal(ctx context.Context, pctx *ParsingContext, l log.Logger) (cty.Value, error) {\n\tbareInclude, hasBareInclude := pctx.TrackInclude.CurrentMap[bareIncludeKey]\n\tif len(pctx.TrackInclude.CurrentMap) == 1 && hasBareInclude {\n\t\tl.Debug(\"Detected single bare include block - exposing as top level\")\n\t\treturn includeConfigAsCtyVal(ctx, pctx, l, bareInclude)\n\t}\n\n\texposedIncludeMap := map[string]cty.Value{}\n\n\tfor key, included := range pctx.TrackInclude.CurrentMap {\n\t\tparsedIncludedCty, err := includeConfigAsCtyVal(ctx, pctx, l, included)\n\t\tif err != nil {\n\t\t\treturn cty.NilVal, err\n\t\t}\n\n\t\tif parsedIncludedCty != cty.NilVal {\n\t\t\tl.Debugf(\"Exposing include block '%s'\", key)\n\n\t\t\texposedIncludeMap[key] = parsedIncludedCty\n\t\t}\n\t}\n\n\treturn ConvertValuesMapToCtyVal(exposedIncludeMap)\n}\n\n// includeConfigAsCtyVal returns the parsed include block as a cty.Value object if expose is true. Otherwise, return\n// the nil representation of cty.Value.\nfunc includeConfigAsCtyVal(ctx context.Context, pctx *ParsingContext, l log.Logger, includeConfig IncludeConfig) (cty.Value, error) {\n\tpctx = pctx.WithTrackInclude(nil)\n\n\tif includeConfig.GetExpose() {\n\t\tparsedIncluded, err := parseIncludedConfig(ctx, pctx, l, &includeConfig)\n\t\tif err != nil {\n\t\t\treturn cty.NilVal, err\n\t\t}\n\n\t\tparsedIncludedCty, err := TerragruntConfigAsCty(parsedIncluded)\n\t\tif err != nil {\n\t\t\treturn cty.NilVal, err\n\t\t}\n\n\t\treturn parsedIncludedCty, nil\n\t}\n\n\treturn cty.NilVal, nil\n}\n\n// CtyToStruct converts a cty.Value to a go struct.\nfunc CtyToStruct(ctyValue cty.Value, target any) error {\n\tjsonBytes, err := ctyjson.Marshal(ctyValue, ctyValue.Type())\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tif err := json.Unmarshal(jsonBytes, target); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n\n// CtyValueAsString converts a cty.Value to a string.\nfunc CtyValueAsString(val cty.Value) (string, error) {\n\tjsonBytes, err := ctyjson.Marshal(val, val.Type())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(jsonBytes), nil\n}\n\n// GetValueString returns the string representation of a cty.Value.\n// If the value is of type cty.String, it returns the raw string value directly.\n// Otherwise, it falls back to converting the value to a JSON-formatted string\n// using the CtyValueAsString helper function.\n//\n// Returns an error if the conversion fails.\nfunc GetValueString(value cty.Value) (string, error) {\n\tif value.Type() == cty.String {\n\t\treturn value.AsString(), nil\n\t}\n\n\treturn CtyValueAsString(value)\n}\n\n// IsComplexType checks if a value is a complex data type that can't be used with raw output.\nfunc IsComplexType(value cty.Value) bool {\n\treturn value.Type().IsObjectType() || value.Type().IsMapType() ||\n\t\tvalue.Type().IsListType() || value.Type().IsTupleType() ||\n\t\tvalue.Type().IsSetType()\n}\n\n// GetFirstKey returns the first key from a map.\n// This is a helper for maps that are known to have exactly one element.\nfunc GetFirstKey(m map[string]cty.Value) string {\n\tfor k := range m {\n\t\treturn k\n\t}\n\n\treturn \"\"\n}\n"
  },
  {
    "path": "pkg/config/dependency.go",
    "content": "package config\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"github.com/gruntwork-io/terragrunt/internal/awshelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cache\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\n\ts3backend \"github.com/gruntwork-io/terragrunt/internal/remotestate/backend/s3\"\n\n\t\"github.com/hashicorp/go-getter\"\n\t\"github.com/zclconf/go-cty/cty\"\n\t\"github.com/zclconf/go-cty/cty/gocty\"\n\tctyjson \"github.com/zclconf/go-cty/cty/json\"\n\t\"golang.org/x/sync/errgroup\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/codegen\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iam\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds/providers/amazonsts\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds/providers/externalcmd\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n)\n\nconst (\n\trenderJSONCommand = \"render-json\"\n\trenderCommand     = \"render\"\n)\n\ntype Dependencies []Dependency\n\n// Struct to hold the decoded dependency blocks.\ntype dependencyOutputCache struct {\n\tEnabled *bool\n\tInputs  cty.Value\n}\n\ntype Dependency struct {\n\tConfigPath                          cty.Value  `hcl:\"config_path,attr\" cty:\"config_path\"`\n\tEnabled                             *bool      `hcl:\"enabled,attr\" cty:\"enabled\"`\n\tSkipOutputs                         *bool      `hcl:\"skip_outputs,attr\" cty:\"skip\"`\n\tMockOutputs                         *cty.Value `hcl:\"mock_outputs,attr\" cty:\"mock_outputs\"`\n\tMockOutputsAllowedTerraformCommands *[]string  `hcl:\"mock_outputs_allowed_terraform_commands,attr\" cty:\"mock_outputs_allowed_terraform_commands\"`\n\n\t// MockOutputsMergeWithState is deprecated. Use MockOutputsMergeStrategyWithState\n\tMockOutputsMergeWithState *bool `hcl:\"mock_outputs_merge_with_state,attr\" cty:\"mock_outputs_merge_with_state\"`\n\n\tMockOutputsMergeStrategyWithState *MergeStrategyType `hcl:\"mock_outputs_merge_strategy_with_state\" cty:\"mock_outputs_merge_strategy_with_state\"`\n\n\t// Used to store the rendered outputs for use when the config is imported or read with `read_terragrunt_config`\n\tRenderedOutputs *cty.Value `cty:\"outputs\"`\n\n\tInputs *cty.Value `cty:\"inputs\"`\n\tName   string     `hcl:\",label\" cty:\"name\"`\n}\n\n// DeepMerge will deep merge two Dependency configs, updating the target. Deep merge for Dependency configs is defined\n// as follows:\n//   - For simple attributes (bools and strings), the source will override the target.\n//   - For MockOutputs, the two maps will be deeply merged together. This means that maps are recursively merged, while\n//     lists are concatenated together.\n//   - For MockOutputsAllowedTerraformCommands, the source will be concatenated to the target.\n//\n// Note that RenderedOutputs is ignored in the deep merge operation.\nfunc (dep *Dependency) DeepMerge(sourceDepConfig *Dependency) error {\n\tif sourceDepConfig.ConfigPath.AsString() != \"\" {\n\t\tdep.ConfigPath = sourceDepConfig.ConfigPath\n\t}\n\n\tif sourceDepConfig.Enabled != nil {\n\t\tdep.Enabled = sourceDepConfig.Enabled\n\t}\n\n\tif sourceDepConfig.SkipOutputs != nil {\n\t\tdep.SkipOutputs = sourceDepConfig.SkipOutputs\n\t}\n\n\tif sourceDepConfig.MockOutputs != nil {\n\t\tif dep.MockOutputs == nil {\n\t\t\tdep.MockOutputs = sourceDepConfig.MockOutputs\n\t\t} else {\n\t\t\tnewMockOutputs, err := deepMergeCtyMaps(*dep.MockOutputs, *sourceDepConfig.MockOutputs)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tdep.MockOutputs = newMockOutputs\n\t\t}\n\t}\n\n\tif sourceDepConfig.MockOutputsAllowedTerraformCommands != nil {\n\t\tif dep.MockOutputsAllowedTerraformCommands == nil {\n\t\t\tdep.MockOutputsAllowedTerraformCommands = sourceDepConfig.MockOutputsAllowedTerraformCommands\n\t\t} else {\n\t\t\tmergedCmds := append(*dep.MockOutputsAllowedTerraformCommands, *sourceDepConfig.MockOutputsAllowedTerraformCommands...)\n\t\t\tdep.MockOutputsAllowedTerraformCommands = &mergedCmds\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// getMockOutputsMergeStrategy returns the MergeStrategyType following the deprecation of mock_outputs_merge_with_state\n// - If mock_outputs_merge_strategy_with_state is not null. The value of mock_outputs_merge_strategy_with_state will be returned\n// - If mock_outputs_merge_strategy_with_state is null and mock_outputs_merge_with_state is not null:\n//   - mock_outputs_merge_with_state being true returns ShallowMerge\n//   - mock_outputs_merge_with_state being false returns NoMerge\nfunc (dep *Dependency) getMockOutputsMergeStrategy() MergeStrategyType {\n\tif dep.MockOutputsMergeStrategyWithState == nil {\n\t\tif dep.MockOutputsMergeWithState != nil && (*dep.MockOutputsMergeWithState) {\n\t\t\treturn ShallowMerge\n\t\t} else {\n\t\t\treturn NoMerge\n\t\t}\n\t}\n\n\treturn *dep.MockOutputsMergeStrategyWithState\n}\n\n// Given a dependency config, we should only attempt to get the outputs if SkipOutputs is nil or false\nfunc (dep *Dependency) shouldGetOutputs(ctx *ParsingContext) bool {\n\treturn !ctx.SkipOutput && dep.isEnabled() && (dep.SkipOutputs == nil || !*dep.SkipOutputs)\n}\n\n// isEnabled returns true if the dependency is enabled\nfunc (dep *Dependency) isEnabled() bool {\n\tif dep.Enabled == nil {\n\t\treturn true\n\t}\n\n\treturn *dep.Enabled\n}\n\n// isDisabled returns true if the dependency is disabled\nfunc (dep *Dependency) isDisabled() bool {\n\treturn !dep.isEnabled()\n}\n\n// Given a dependency config, we should only attempt to merge mocks outputs with the outputs if MockOutputsMergeWithState is not nil or true\nfunc (dep *Dependency) shouldMergeMockOutputsWithState(ctx *ParsingContext) bool {\n\tallowedCommand :=\n\t\tdep.MockOutputsAllowedTerraformCommands == nil ||\n\t\t\tlen(*dep.MockOutputsAllowedTerraformCommands) == 0 ||\n\t\t\tslices.Contains(*dep.MockOutputsAllowedTerraformCommands, ctx.OriginalTerraformCommand)\n\n\treturn allowedCommand && dep.getMockOutputsMergeStrategy() != NoMerge\n}\n\nfunc (dep *Dependency) setRenderedOutputs(ctx context.Context, pctx *ParsingContext, l log.Logger) error {\n\tif dep == nil {\n\t\treturn nil\n\t}\n\n\tif dep.shouldGetOutputs(pctx) || dep.shouldReturnMockOutputs(pctx) {\n\t\toutputVal, err := getTerragruntOutputIfAppliedElseConfiguredDefault(ctx, pctx, l, dep)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdep.RenderedOutputs = outputVal\n\t}\n\n\treturn nil\n}\n\n// outputLocksFromContext retrieves the KeyLocks from the context for synchronizing output retrieval.\n// If not present in context, returns a new KeyLocks instance.\nfunc outputLocksFromContext(ctx context.Context) *util.KeyLocks {\n\tif val, ok := ctx.Value(OutputLocksContextKey).(*util.KeyLocks); ok && val != nil {\n\t\treturn val\n\t}\n\n\treturn util.NewKeyLocks()\n}\n\n// Decode the dependency blocks from the file, and then retrieve all the outputs from the remote state. Then encode the\n// resulting map as a cty.Value object.\n// TODO: In the future, consider allowing importing dependency blocks from included config\n// NOTE FOR MAINTAINER: When implementing importation of other config blocks (e.g referencing inputs), carefully\n//\n//\tconsider whether or not the implementation of the cyclic dependency detection still makes sense.\nfunc decodeAndRetrieveOutputs(ctx context.Context, pctx *ParsingContext, l log.Logger, file *hclparse.File) (*cty.Value, error) {\n\tevalParsingContext, err := createTerragruntEvalContext(ctx, pctx, l, file.ConfigPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdecodedDependency := TerragruntDependency{}\n\tif err := file.Decode(&decodedDependency, evalParsingContext); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// In normal operation, if a dependency block does not have a `config_path` attribute, decoding returns an error since this attribute is required, but the `hclvalidate` command suppresses decoding errors and this causes a cycle between modules, so we need to filter out dependencies without a defined `config_path`.\n\tdecodedDependency.Dependencies = decodedDependency.Dependencies.FilteredWithoutConfigPath()\n\n\t// Validate that dependency config_path is not an empty string.\n\t// Skip null/unknown values and non-strings (which can appear during partial decode or hclvalidate).\n\tfor _, dep := range decodedDependency.Dependencies {\n\t\tif dep.isDisabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !IsValidConfigPath(dep.ConfigPath) {\n\t\t\treturn nil, errors.New(DependencyInvalidConfigPathError{DependencyName: dep.Name})\n\t\t}\n\t}\n\n\tif err := checkForDependencyBlockCycles(ctx, pctx, l, pctx.TerragruntConfigPath, decodedDependency); err != nil {\n\t\treturn nil, err\n\t}\n\n\tupdatedDependencies, err := decodeDependencies(ctx, pctx, l, decodedDependency)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdecodedDependency = *updatedDependencies\n\n\t// Merge in included dependencies\n\tif pctx.TrackInclude != nil {\n\t\tmergedDecodedDependency, err := handleIncludeForDependency(ctx, pctx, l, decodedDependency)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tdecodedDependency = *mergedDecodedDependency\n\t}\n\n\t// Extract dependency names for tracing\n\tdependencyNames := make([]string, 0, len(decodedDependency.Dependencies))\n\tfor _, dep := range decodedDependency.Dependencies {\n\t\tdependencyNames = append(dependencyNames, dep.Name)\n\t}\n\n\tvar result *cty.Value\n\n\terr = TraceParseDependencies(ctx, file.ConfigPath, pctx.SkipOutputsResolution, len(decodedDependency.Dependencies), dependencyNames, func(ctx context.Context) error {\n\t\tvar depErr error\n\n\t\tresult, depErr = dependencyBlocksToCtyValue(ctx, pctx, l, file.ConfigPath, decodedDependency.Dependencies)\n\n\t\treturn depErr\n\t})\n\n\treturn result, err\n}\n\n// decodeDependencies decode dependencies and fetch inputs\nfunc decodeDependencies(ctx context.Context, pctx *ParsingContext, l log.Logger, decodedDependency TerragruntDependency) (*TerragruntDependency, error) {\n\tupdatedDependencies := TerragruntDependency{}\n\tdepCache := cache.ContextCache[*dependencyOutputCache](ctx, DependencyOutputCacheContextKey)\n\n\tfor _, dep := range decodedDependency.Dependencies {\n\t\tif !dep.isEnabled() {\n\t\t\tupdatedDependencies.Dependencies = append(updatedDependencies.Dependencies, dep)\n\t\t\tcontinue\n\t\t}\n\n\t\tif !IsValidConfigPath(dep.ConfigPath) {\n\t\t\treturn &updatedDependencies, errors.New(DependencyInvalidConfigPathError{DependencyName: dep.Name})\n\t\t}\n\n\t\tdepPath := getCleanedTargetConfigPath(dep.ConfigPath.AsString(), pctx.TerragruntConfigPath)\n\n\t\tif !util.FileExists(depPath) {\n\t\t\tupdatedDependencies.Dependencies = append(updatedDependencies.Dependencies, dep)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tcacheKey := filepath.Join(pctx.WorkingDir, depPath)\n\n\t\t// Cache hit - reuse cached values\n\t\tif cachedDependency, found := depCache.Get(ctx, cacheKey); found {\n\t\t\tdep.Enabled = cachedDependency.Enabled\n\t\t\tdep.Inputs = &cachedDependency.Inputs\n\t\t\tupdatedDependencies.Dependencies = append(updatedDependencies.Dependencies, dep)\n\n\t\t\tcontinue\n\t\t}\n\n\t\t// Cache miss - parse and cache\n\n\t\tif !pctx.SkipOutputsResolution {\n\t\t\tl.Debugf(\"Reading Terragrunt config file at %s\", util.RelPathForLog(pctx.RootWorkingDir, depPath, pctx.Writers.LogShowAbsPaths))\n\t\t}\n\n\t\t_, depCtx, err := pctx.WithConfigPath(l, depPath)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tdepCtx.DownloadDir = filepath.Join(filepath.Dir(depPath), util.TerragruntCacheDir)\n\n\t\tif depCtx.IAMRoleOptions != depCtx.OriginalIAMRoleOptions {\n\t\t\tdepCtx.IAMRoleOptions = iam.RoleOptions{}\n\t\t}\n\n\t\tdepCtx = depCtx.WithDecodeList(TerragruntFlags).WithDiagnosticsSuppressed(l)\n\n\t\tdepConfig, err := PartialParseConfigFile(ctx, depCtx, l, depPath, nil)\n\t\tif err != nil {\n\t\t\tl.Warnf(\"Error reading partial config for dependency %s at %s: %v\", dep.Name, depPath, err)\n\t\t\tupdatedDependencies.Dependencies = append(updatedDependencies.Dependencies, dep)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tinputsCty, err := convertToCtyWithJSON(depConfig.Inputs)\n\t\tif err != nil {\n\t\t\treturn nil, errors.Errorf(\"failed to convert inputs for dependency %q: %w\", dep.Name, err)\n\t\t}\n\n\t\tcachedValue := dependencyOutputCache{\n\t\t\tEnabled: dep.Enabled,\n\t\t\tInputs:  inputsCty,\n\t\t}\n\t\tdepCache.Put(ctx, cacheKey, &cachedValue)\n\n\t\tdep.Inputs = &inputsCty\n\n\t\tupdatedDependencies.Dependencies = append(updatedDependencies.Dependencies, dep)\n\t}\n\n\treturn &updatedDependencies, nil\n}\n\n// Convert the list of parsed Dependency blocks into a list of module dependencies. Each output block should\n// become a dependency of the current config, since that module has to be applied before we can read the output.\nfunc dependencyBlocksToModuleDependencies(l log.Logger, decodedDependencyBlocks []Dependency) *ModuleDependencies {\n\tif len(decodedDependencyBlocks) == 0 {\n\t\treturn nil\n\t}\n\n\tpaths := []string{}\n\n\tfor _, decodedDependencyBlock := range decodedDependencyBlocks {\n\t\t// skip dependency if is not enabled\n\t\tif !decodedDependencyBlock.isEnabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\t// Skip if ConfigPath is not a known string value (can happen during discovery phase)\n\t\tif decodedDependencyBlock.ConfigPath.IsNull() ||\n\t\t\t!decodedDependencyBlock.ConfigPath.IsWhollyKnown() ||\n\t\t\t!decodedDependencyBlock.ConfigPath.Type().Equals(cty.String) {\n\t\t\tl.Debugf(\"Skipping dependency %q: ConfigPath is not a valid known string value\", decodedDependencyBlock.Name)\n\t\t\tcontinue\n\t\t}\n\n\t\tpaths = append(paths, decodedDependencyBlock.ConfigPath.AsString())\n\t}\n\n\treturn &ModuleDependencies{Paths: paths}\n}\n\n// Check for cyclic dependency blocks to avoid infinite `terragrunt output` loops. To avoid reparsing the config, we\n// kickstart the initial loop using what we already decoded.\nfunc checkForDependencyBlockCycles(ctx context.Context, pctx *ParsingContext, l log.Logger, configPath string, decodedDependency TerragruntDependency) error {\n\tvisitedPaths := []string{}\n\tcurrentTraversalPaths := []string{configPath}\n\n\tfor _, dependency := range decodedDependency.Dependencies {\n\t\tif dependency.isDisabled() {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !IsValidConfigPath(dependency.ConfigPath) {\n\t\t\treturn errors.New(DependencyInvalidConfigPathError{DependencyName: dependency.Name})\n\t\t}\n\n\t\tdependencyPath := getCleanedTargetConfigPath(dependency.ConfigPath.AsString(), configPath)\n\n\t\tl, dependencyContext, err := pctx.WithConfigPath(l, dependencyPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := checkForDependencyBlockCyclesUsingDFS(ctx, dependencyContext, l, dependencyPath, &visitedPaths, &currentTraversalPaths); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Helper function for checkForDependencyBlockCycles.\n//\n// Same implementation as configstack/graph.go:checkForCyclesUsingDepthFirstSearch, except walks the graph of\n// dependencies by `dependency` blocks (which make explicit `terragrunt output` calls) instead of explicit dependencies.\nfunc checkForDependencyBlockCyclesUsingDFS(\n\tctx context.Context,\n\tpctx *ParsingContext,\n\tl log.Logger,\n\tdependencyPath string,\n\tvisitedPaths *[]string,\n\tcurrentTraversalPaths *[]string,\n) error {\n\tif slices.Contains(*visitedPaths, dependencyPath) {\n\t\treturn nil\n\t}\n\n\tif slices.Contains(*currentTraversalPaths, dependencyPath) {\n\t\treturn errors.New(DependencyCycleError(append(*currentTraversalPaths, dependencyPath)))\n\t}\n\n\t*currentTraversalPaths = append(*currentTraversalPaths, dependencyPath)\n\n\tdependencyPaths, err := getDependencyBlockConfigPathsByFilepath(ctx, pctx, l, dependencyPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, dependency := range dependencyPaths {\n\t\tdependencyPath := getCleanedTargetConfigPath(dependency, dependencyPath)\n\n\t\tl, dependencyContext, err := pctx.WithConfigPath(l, dependencyPath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := checkForDependencyBlockCyclesUsingDFS(ctx, dependencyContext, l, dependencyPath, visitedPaths, currentTraversalPaths); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t*visitedPaths = append(*visitedPaths, dependencyPath)\n\t*currentTraversalPaths = slices.DeleteFunc(*currentTraversalPaths, func(path string) bool { return path == dependencyPath })\n\n\treturn nil\n}\n\n// Given the config path, return the list of config paths that are specified as dependency blocks in the config\nfunc getDependencyBlockConfigPathsByFilepath(ctx context.Context, pctx *ParsingContext, l log.Logger, configPath string) ([]string, error) {\n\t// This will automatically parse everything needed to parse the dependency block configs, and load them as\n\t// TerragruntConfig.Dependencies. Note that since we aren't passing in `DependenciesBlock` to the\n\t// PartialDecodeSectionType list, the Dependencies attribute will not include any dependencies specified via the\n\t// dependencies block.\n\ttgConfig, err := PartialParseConfigFile(ctx, pctx.WithDecodeList(DependencyBlock).WithDiagnosticsSuppressed(l), l, configPath, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif tgConfig.Dependencies == nil {\n\t\treturn []string{}, nil\n\t}\n\n\treturn tgConfig.Dependencies.Paths, nil\n}\n\n// Encode the list of dependency blocks into a single cty.Value object that maps the dependency block name to the\n// encoded dependency mapping. The encoded dependency mapping should have the attributes:\n//   - outputs: The map of outputs of the corresponding terraform module that lives at the target config of the\n//     dependency.\n//\n// This routine will go through the process of obtaining the outputs using `terragrunt output` from the target config.\n// The traceCtx parameter is the trace context from the parent span (parse_dependencies) to establish parent-child\n// relationship for individual dependency traces.\nfunc dependencyBlocksToCtyValue(traceCtx context.Context, pctx *ParsingContext, l log.Logger, parentConfigPath string, dependencyConfigs []Dependency) (*cty.Value, error) {\n\tpaths := []string{}\n\n\t// dependencyMap is the top level map that maps dependency block names to the encoded version, which includes\n\t// various attributes for accessing information about the target config (including the module outputs).\n\tdependencyMap := map[string]cty.Value{}\n\tlock := sync.Mutex{}\n\tdependencyErrGroup, _ := errgroup.WithContext(traceCtx)\n\n\tfor _, dependencyConfig := range dependencyConfigs {\n\t\tdependencyErrGroup.Go(func() error {\n\t\t\t// Get dependency path for tracing (handle invalid/unknown paths gracefully)\n\t\t\t// Use getCleanedTargetConfigPath to get the absolute path\n\t\t\tdepPath := \"\"\n\t\t\tif IsValidConfigPath(dependencyConfig.ConfigPath) {\n\t\t\t\tdepPath = getCleanedTargetConfigPath(dependencyConfig.ConfigPath.AsString(), parentConfigPath)\n\t\t\t}\n\n\t\t\t// Use traceCtx to make this a child span of parse_dependencies\n\t\t\treturn TraceParseDependency(traceCtx, dependencyConfig.Name, depPath, func(ctx context.Context) error {\n\t\t\t\t// Loose struct to hold the attributes of the dependency. This includes:\n\t\t\t\t// - outputs: The module outputs of the target config\n\t\t\t\tdependencyEncodingMap := map[string]cty.Value{}\n\n\t\t\t\t// Encode the outputs and nest under `outputs` attribute if we should get the outputs or the `mock_outputs`\n\t\t\t\tif err := dependencyConfig.setRenderedOutputs(ctx, pctx, l); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif dependencyConfig.RenderedOutputs != nil {\n\t\t\t\t\tlock.Lock()\n\n\t\t\t\t\tpaths = append(paths, dependencyConfig.ConfigPath.AsString())\n\n\t\t\t\t\tlock.Unlock()\n\n\t\t\t\t\tdependencyEncodingMap[\"outputs\"] = *dependencyConfig.RenderedOutputs\n\t\t\t\t}\n\n\t\t\t\tif dependencyConfig.Inputs != nil {\n\t\t\t\t\tdependencyEncodingMap[\"inputs\"] = *dependencyConfig.Inputs\n\t\t\t\t}\n\n\t\t\t\t// Once the dependency is encoded into a map, we need to convert to a cty.Value again so that it can be fed to\n\t\t\t\t// the higher order dependency map.\n\t\t\t\tdependencyEncodingMapEncoded, err := gocty.ToCtyValue(dependencyEncodingMap, generateTypeFromValuesMap(dependencyEncodingMap))\n\t\t\t\tif err != nil {\n\t\t\t\t\terr = TerragruntOutputListEncodingError{Paths: paths, Err: err}\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\t// Lock the map as only one goroutine should be writing to the map at a time\n\t\t\t\tlock.Lock()\n\t\t\t\tdefer lock.Unlock()\n\n\t\t\t\t// Finally, feed the encoded dependency into the higher order map under the block name\n\t\t\t\tdependencyMap[dependencyConfig.Name] = dependencyEncodingMapEncoded\n\n\t\t\t\treturn nil\n\t\t\t})\n\t\t})\n\t}\n\n\tif err := dependencyErrGroup.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// We need to convert the value map to a single cty.Value at the end so that it can be used in the execution ctx\n\tconvertedOutput, err := gocty.ToCtyValue(dependencyMap, generateTypeFromValuesMap(dependencyMap))\n\tif err != nil {\n\t\terr = TerragruntOutputListEncodingError{Paths: paths, Err: err}\n\t}\n\n\treturn &convertedOutput, errors.New(err)\n}\n\n// This will attempt to get the outputs from the target terragrunt config if it is applied. If it is not applied, the\n// behavior is different depending on the configuration of the dependency:\n//   - If the dependency block indicates a mock_outputs attribute, this will return that.\n//     If the dependency block indicates a mock_outputs_merge_strategy_with_state attribute, mock_outputs and state outputs will be merged following the merge strategy\n//   - If the dependency block does NOT indicate a mock_outputs attribute, this will return an error.\nfunc getTerragruntOutputIfAppliedElseConfiguredDefault(\n\tctx context.Context,\n\tpctx *ParsingContext,\n\tl log.Logger,\n\tdependencyConfig *Dependency,\n) (*cty.Value, error) {\n\tif dependencyConfig.isDisabled() {\n\t\tl.Debugf(\"Skipping outputs reading for disabled dependency %s\", dependencyConfig.Name)\n\t\treturn dependencyConfig.MockOutputs, nil\n\t}\n\n\tif dependencyConfig.shouldGetOutputs(pctx) {\n\t\toutputVal, isEmpty, err := getTerragruntOutput(ctx, pctx, l, dependencyConfig)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif !isEmpty && dependencyConfig.shouldMergeMockOutputsWithState(pctx) && dependencyConfig.MockOutputs != nil {\n\t\t\tmockMergeStrategy := dependencyConfig.getMockOutputsMergeStrategy()\n\n\t\t\tswitch mockMergeStrategy { // nolint:exhaustive\n\t\t\tcase NoMerge:\n\t\t\t\treturn outputVal, nil\n\t\t\tcase ShallowMerge:\n\t\t\t\treturn shallowMergeCtyMaps(*outputVal, *dependencyConfig.MockOutputs)\n\t\t\tcase DeepMergeMapOnly:\n\t\t\t\treturn deepMergeCtyMapsMapOnly(*dependencyConfig.MockOutputs, *outputVal)\n\t\t\tdefault:\n\t\t\t\treturn nil, errors.New(InvalidMergeStrategyTypeError(mockMergeStrategy))\n\t\t\t}\n\t\t} else if !isEmpty {\n\t\t\treturn outputVal, err\n\t\t}\n\t}\n\n\t// When we get no output, it can be an indication that either the module has no outputs or the module is not\n\t// applied. In either case, check if there are default output values to return. If yes, return that. Else,\n\t// return error.\n\ttargetConfig := getCleanedTargetConfigPath(dependencyConfig.ConfigPath.AsString(), pctx.TerragruntConfigPath)\n\n\tif dependencyConfig.shouldReturnMockOutputs(pctx) {\n\t\tl.Warnf(\"Config %s is a dependency of %s that has no outputs, but mock outputs provided and returning those in dependency output.\",\n\t\t\ttargetConfig,\n\t\t\tpctx.TerragruntConfigPath,\n\t\t)\n\n\t\treturn dependencyConfig.MockOutputs, nil\n\t}\n\n\t// At this point, we expect outputs to exist because there is a `dependency` block without skip_outputs = true, and\n\t// returning mocks is not allowed. So return a useful error message indicating that we expected outputs, but they\n\t// did not exist.\n\terr := TerragruntOutputTargetNoOutputs{\n\t\ttargetName:    dependencyConfig.Name,\n\t\ttargetPath:    dependencyConfig.ConfigPath.AsString(),\n\t\ttargetConfig:  targetConfig,\n\t\tcurrentConfig: pctx.TerragruntConfigPath,\n\t}\n\n\treturn nil, err\n}\n\n// We should only return default outputs if the mock_outputs attribute is set, and if we are running one of the\n// allowed commands when `mock_outputs_allowed_terraform_commands` is set as well.\nfunc (dep *Dependency) shouldReturnMockOutputs(pctx *ParsingContext) bool {\n\tif dep.isDisabled() {\n\t\treturn true\n\t}\n\n\tdefaultOutputsSet := dep.MockOutputs != nil\n\n\tallowedCommand :=\n\t\tdep.MockOutputsAllowedTerraformCommands == nil ||\n\t\t\tlen(*dep.MockOutputsAllowedTerraformCommands) == 0 ||\n\t\t\tslices.Contains(*dep.MockOutputsAllowedTerraformCommands, pctx.OriginalTerraformCommand)\n\n\treturn defaultOutputsSet && allowedCommand || isRenderJSONCommand(pctx) || isRenderCommand(pctx)\n}\n\n// Return the output from the state of another module, managed by terragrunt. This function will parse the provided\n// terragrunt config and extract the desired output from the remote state. Note that this will error if the targeted\n// module hasn't been applied yet.\nfunc getTerragruntOutput(\n\tctx context.Context,\n\tpctx *ParsingContext,\n\tl log.Logger,\n\tdependencyConfig *Dependency,\n) (*cty.Value, bool, error) {\n\t// target config check: make sure the target config exists\n\ttargetConfigPath := getCleanedTargetConfigPath(\n\t\tdependencyConfig.ConfigPath.AsString(),\n\t\tpctx.TerragruntConfigPath,\n\t)\n\n\tif !util.FileExists(targetConfigPath) {\n\t\treturn nil, true, errors.New(DependencyConfigNotFound{Path: targetConfigPath})\n\t}\n\n\tjsonBytes, err := getOutputJSONWithCaching(ctx, pctx, l, targetConfigPath)\n\tif err != nil {\n\t\tif !isRenderJSONCommand(pctx) && !isRenderCommand(pctx) && !isAwsS3NoSuchKey(err) {\n\t\t\treturn nil, true, err\n\t\t}\n\n\t\tl.Warnf(\n\t\t\t\"Failed to read outputs from %s referenced in %s as %s, fallback to mock outputs. Error: %v\",\n\t\t\ttargetConfigPath,\n\t\t\tpctx.TerragruntConfigPath,\n\t\t\tdependencyConfig.Name,\n\t\t\terr,\n\t\t)\n\n\t\tjsonBytes, err = json.Marshal(dependencyConfig.MockOutputs)\n\t\tif err != nil {\n\t\t\treturn nil, true, err\n\t\t}\n\t}\n\n\tisEmpty := string(jsonBytes) == \"{}\"\n\n\toutputMap, err := TerraformOutputJSONToCtyValueMap(targetConfigPath, jsonBytes)\n\tif err != nil {\n\t\treturn nil, isEmpty, err\n\t}\n\n\t// We need to convert the value map to a single cty.Value at the end for use in the terragrunt config.\n\tconvertedOutput, err := gocty.ToCtyValue(outputMap, generateTypeFromValuesMap(outputMap))\n\tif err != nil {\n\t\terr = TerragruntOutputEncodingError{Path: targetConfigPath, Err: err}\n\t}\n\n\treturn &convertedOutput, isEmpty, errors.New(err)\n}\n\nfunc isAwsS3NoSuchKey(err error) bool {\n\tif err != nil {\n\t\terrStr := err.Error()\n\t\treturn strings.Contains(errStr, \"NoSuchKey\") || strings.Contains(errStr, \"NotFound\")\n\t}\n\n\treturn false\n}\n\n// isRenderJSONCommand This function will true if terragrunt was invoked with render-json\nfunc isRenderJSONCommand(pctx *ParsingContext) bool {\n\tif pctx.TerraformCliArgs == nil {\n\t\treturn false\n\t}\n\n\treturn pctx.TerraformCliArgs.Contains(renderJSONCommand)\n}\n\n// isRenderCommand will return true if terragrunt was invoked with render\nfunc isRenderCommand(pctx *ParsingContext) bool {\n\tif pctx.TerraformCliArgs == nil {\n\t\treturn false\n\t}\n\n\treturn pctx.TerraformCliArgs.Contains(renderCommand)\n}\n\n// getOutputJSONWithCaching will run terragrunt output on the target config if it is not already cached.\nfunc getOutputJSONWithCaching(ctx context.Context, pctx *ParsingContext, l log.Logger, targetConfig string) ([]byte, error) {\n\tlocks := outputLocksFromContext(ctx)\n\n\tlocks.Lock(targetConfig)\n\tdefer locks.Unlock(targetConfig)\n\n\tl.Debugf(\"Getting output of dependency %s for config %s\", util.RelPathForLog(pctx.RootWorkingDir, targetConfig, pctx.Writers.LogShowAbsPaths), util.RelPathForLog(pctx.RootWorkingDir, pctx.TerragruntConfigPath, pctx.Writers.LogShowAbsPaths))\n\n\tjsonCache := cache.ContextCache[[]byte](ctx, JSONOutputCacheContextKey)\n\tif jsonBytes, found := jsonCache.Get(ctx, targetConfig); found {\n\t\tl.Debugf(\"%s was run before. Using cached output.\", targetConfig)\n\t\treturn jsonBytes, nil\n\t}\n\n\tnewJSONBytes, err := getTerragruntOutputJSON(ctx, pctx, l, targetConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// When AWS Client Side Monitoring (CSM) is enabled the aws-sdk-go displays log as a plaintext \"Enabling CSM\" to stdout, even if the `output -json` flag is specified. The final output looks like this: \"2023/05/04 20:22:43 Enabling CSM{...omitted json string...}\", and and prevents proper json parsing. Since there is no way to disable this log, the only way out is to filter.\n\t// Related AWS code: https://github.com/aws/aws-sdk-go/blob/81d1cbbc6a2028023aff7bcab0fe1be320cd39f7/aws/session/session.go#L444\n\t// Related issues: https://github.com/gruntwork-io/terragrunt/issues/2233 https://github.com/hashicorp/terraform-provider-aws/issues/23620\n\tif index := bytes.IndexByte(newJSONBytes, byte('{')); index > 0 {\n\t\tnewJSONBytes = newJSONBytes[index:]\n\t}\n\n\tjsonCache.Put(ctx, targetConfig, newJSONBytes)\n\n\treturn newJSONBytes, nil\n}\n\n// Retrieve the outputs from the terraform state in the target configuration. This attempts to optimize the output\n// retrieval if the following conditions are true:\n// - State backends are managed with a `remote_state` block.\n// - The `remote_state` block does not depend on any `dependency` outputs.\n// If these conditions are met, terragrunt can optimize the retrieval to avoid recursively retrieving dependency outputs\n// by directly pulling down the state file. Otherwise, terragrunt will fallback to running `terragrunt output` on the\n// target module.\nfunc getTerragruntOutputJSON(ctx context.Context, pctx *ParsingContext, l log.Logger, targetConfig string) ([]byte, error) {\n\t// Create dependency context using WithConfigPath\n\tl, pctx, err := pctx.WithConfigPath(l, targetConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Set dependency-specific fields\n\tpctx.OriginalTerragruntConfigPath = targetConfig\n\tpctx.ForwardTFStdout = false\n\tpctx.CheckDependentUnits = false\n\tpctx.TerraformCommand = \"output\"\n\tpctx.TerraformCliArgs = iacargs.New().SetCommand(\"output\").AppendFlag(\"-json\")\n\n\t// DownloadDir needs to be the dependency's default download directory\n\t_, downloadDir := util.DefaultWorkingAndDownloadDirs(targetConfig)\n\n\tpctx.DownloadDir = downloadDir\n\n\t// Clear IAM if changed from original\n\tif pctx.IAMRoleOptions != pctx.OriginalIAMRoleOptions {\n\t\tpctx.IAMRoleOptions = iam.RoleOptions{}\n\t}\n\n\t// Validate and use TerragruntVersionConstraints.TerraformBinary for dependency\n\tpartialTerragruntConfig, err := PartialParseConfigFile(\n\t\tctx,\n\t\tpctx.WithDecodeList(DependencyBlock).WithDiagnosticsSuppressed(l),\n\t\tl,\n\t\ttargetConfig,\n\t\tnil,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Only override TFPath if it was not explicitly set by the user via CLI or environment variable\n\tif !pctx.TFPathExplicitlySet && partialTerragruntConfig.TerraformBinary != \"\" {\n\t\tpctx.TFPath = partialTerragruntConfig.TerraformBinary\n\t}\n\n\t// If the Source is set, then we need to recompute it in the ctx of the target config.\n\tif pctx.Source != \"\" {\n\t\tpartialParseIncludedConfig, err := PartialParseConfigFile(\n\t\t\tctx,\n\t\t\tpctx.WithDecodeList(TerraformBlock).WithDiagnosticsSuppressed(l),\n\t\t\tl,\n\t\t\ttargetConfig,\n\t\t\tnil,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t// Update the source value to be everything before \"//\" so that it can be recomputed\n\t\tmoduleURL, _ := getter.SourceDirSubdir(pctx.Source)\n\n\t\t// Finally, update the source to be the combined path between the terraform source in the target config, and the\n\t\t// value before \"//\" in the original terragrunt options.\n\t\ttargetSource, err := GetTerragruntSourceForModule(moduleURL, filepath.Dir(targetConfig), partialParseIncludedConfig)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tpctx.Source = targetSource\n\t}\n\n\t// First attempt to parse the `remote_state` blocks without parsing/getting dependency outputs. If this is possible,\n\t// proceed to routine that fetches remote state directly. Otherwise, fallback to calling `terragrunt output`\n\t// directly.\n\n\t// we need to suspend logging diagnostic errors on this attempt\n\tparseOptions := slices.Concat(pctx.ParserOptions, []hclparse.Option{hclparse.WithDiagnosticsWriter(io.Discard, true)})\n\n\tremoteStateTGConfig, err := PartialParseConfigFile(\n\t\tctx,\n\t\tpctx.WithParseOption(parseOptions).WithDecodeList(\n\t\t\tRemoteStateBlock,\n\t\t\tTerragruntFlags,\n\t\t\tEngineBlock,\n\t\t),\n\t\tl,\n\t\ttargetConfig,\n\t\tnil,\n\t)\n\tcanGet := canGetRemoteState(remoteStateTGConfig.RemoteState)\n\n\tif err != nil || !canGet {\n\t\tl.Debugf(\"Could not parse remote_state block from target config %s\", pctx.TerragruntConfigPath)\n\t\tl.Debugf(\"Falling back to terragrunt output.\")\n\n\t\treturn runTerragruntOutputJSON(ctx, pctx, l, targetConfig)\n\t}\n\n\t// In optimization mode, see if there is already an init-ed folder that terragrunt can use, and if so, run\n\t// `terraform output` in the working directory.\n\tisInit, workingDir, err := terragruntAlreadyInit(ctx, l, pctx, targetConfig)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Fetch engine options so they can be passed to the dependency functions\n\tengineOpts, err := remoteStateTGConfig.EngineOptions()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpctx.EngineConfig = engineOpts\n\n\tshouldFetchFromState := pctx.Experiments.Evaluate(experiment.DependencyFetchOutputFromState) &&\n\t\t!pctx.NoDependencyFetchOutputFromState &&\n\t\tremoteStateTGConfig.RemoteState.BackendName == s3backend.BackendName\n\n\tif shouldFetchFromState {\n\t\treturn getTerragruntOutputJSONFromRemoteState(\n\t\t\tctx,\n\t\t\tpctx,\n\t\t\tl,\n\t\t\ttargetConfig,\n\t\t\tremoteStateTGConfig.RemoteState,\n\t\t\tremoteStateTGConfig.GetIAMRoleOptions(),\n\t\t)\n\t}\n\n\tif isInit {\n\t\tcredsGetter := creds.NewGetter()\n\t\tif err = credsGetter.ObtainAndUpdateEnvIfNecessary(\n\t\t\tctx,\n\t\t\tl,\n\t\t\tpctx.Env,\n\t\t\texternalcmd.NewProvider(l, pctx.AuthProviderCmd, shellRunOptsFromPctx(pctx)),\n\t\t); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn getTerragruntOutputJSONFromInitFolder(\n\t\t\tctx,\n\t\t\tpctx,\n\t\t\tl,\n\t\t\tworkingDir,\n\t\t\tremoteStateTGConfig.GetIAMRoleOptions(),\n\t\t\tcredsGetter,\n\t\t)\n\t}\n\n\treturn getTerragruntOutputJSONFromRemoteState(\n\t\tctx,\n\t\tpctx,\n\t\tl,\n\t\ttargetConfig,\n\t\tremoteStateTGConfig.RemoteState,\n\t\tremoteStateTGConfig.GetIAMRoleOptions(),\n\t)\n}\n\n// canGetRemoteState returns true if the remote state block is not nil and dependency optimization is not disabled\nfunc canGetRemoteState(remoteState *remotestate.RemoteState) bool {\n\treturn remoteState != nil && !remoteState.DisableDependencyOptimization\n}\n\n// terragruntAlreadyInit returns true if it detects that the module specified by the given terragrunt configuration is\n// already initialized with the terraform source. This will also return the working directory where you can run\n// terraform.\nfunc terragruntAlreadyInit(ctx context.Context, l log.Logger, pctx *ParsingContext, configPath string) (bool, string, error) {\n\t// We need to first determine the working directory where the terraform source should be located. This is dependent\n\t// on the source field of the terraform block in the config.\n\tterraformBlockTGConfig, err := PartialParseConfigFile(ctx, pctx.WithDecodeList(TerraformSource), l, configPath, nil)\n\tif err != nil {\n\t\treturn false, \"\", err\n\t}\n\n\tsourceURL, err := GetTerraformSourceURL(pctx.Source, pctx.SourceMap, pctx.OriginalTerragruntConfigPath, terraformBlockTGConfig)\n\tif err != nil {\n\t\treturn false, \"\", err\n\t}\n\n\t// sourceURL will always be at least \".\" (current directory) to ensure cache is always used.\n\t// Always compute the cache working directory using NewSource.\n\twalkWithSymlinks := pctx.Experiments.Evaluate(experiment.Symlinks)\n\n\tterraformSource, err := tf.NewSource(l, sourceURL, pctx.DownloadDir, pctx.WorkingDir, walkWithSymlinks)\n\tif err != nil {\n\t\treturn false, \"\", err\n\t}\n\t// We're only interested in the computed working dir.\n\tworkingDir := terraformSource.WorkingDir\n\t// Terragrunt is already init-ed if the terraform state dir (.terraform) exists in the working dir.\n\t// NOTE: if the ref changes, the workingDir would be different as the download dir includes a base64 encoded hash of\n\t// the source URL with ref. This would ensure that this routine would not return true if the new ref is not already\n\t// init-ed.\n\treturn util.FileExists(filepath.Join(workingDir, \".terraform\")), workingDir, nil\n}\n\n// getTerragruntOutputJSONFromInitFolder will retrieve the outputs directly from the module's working directory without\n// running init.\nfunc getTerragruntOutputJSONFromInitFolder(\n\tctx context.Context,\n\tpctx *ParsingContext,\n\tl log.Logger,\n\tterraformWorkingDir string,\n\tiamRoleOpts iam.RoleOptions,\n\tcredsGetter *creds.Getter,\n) ([]byte, error) {\n\ttargetConfigPath := pctx.TerragruntConfigPath\n\n\ttfRunOpts, err := setupTFRunOptsForBareTerraform(\n\t\tctx,\n\t\tpctx,\n\t\tl,\n\t\tterraformWorkingDir,\n\t\tiamRoleOpts,\n\t\tcredsGetter,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tl.Debugf(\n\t\t\"Unit '%s' is already init-ed. \"+\n\t\t\t\"Retrieving outputs directly from working directory.\",\n\t\tutil.RelPathForLog(\n\t\t\tpctx.RootWorkingDir,\n\t\t\tfilepath.Dir(targetConfigPath),\n\t\t\tpctx.Writers.LogShowAbsPaths,\n\t\t),\n\t)\n\n\tbareCtx := tf.ContextWithTerraformCommandHook(ctx, nil)\n\n\tout, err := tf.RunCommandWithOutput(bareCtx, l, tfRunOpts, tf.CommandNameOutput, \"-json\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjsonString := strings.TrimSpace(out.Stdout.String())\n\tjsonBytes := []byte(jsonString)\n\n\tl.Debugf(\n\t\t\"Retrieved output from %s as json: %s\",\n\t\tutil.RelPathForLog(\n\t\t\tpctx.RootWorkingDir,\n\t\t\ttargetConfigPath,\n\t\t\tpctx.Writers.LogShowAbsPaths,\n\t\t),\n\t\tjsonString,\n\t)\n\n\treturn jsonBytes, nil\n}\n\n// getTerragruntOutputJSONFromRemoteState will retrieve the outputs directly by using just the remote state block. This\n// uses terraform's feature where `output` and `init` can work without the real source, as long as you have the\n// `backend` configured.\n// To do this, this function will:\n// - Create a temporary folder\n// - Generate the backend.tf file with the backend configuration from the remote_state block\n// - Copy the provider lock file, if there is one in the dependency's working directory\n// - Run terraform init and terraform output\n// - Clean up folder once json file is generated\n// NOTE: terragruntOptions should be in the ctx of the targetConfig already.\nfunc getTerragruntOutputJSONFromRemoteState(\n\tctx context.Context,\n\tpctx *ParsingContext,\n\tl log.Logger,\n\ttargetConfigPath string,\n\tremoteState *remotestate.RemoteState,\n\tiamRoleOpts iam.RoleOptions,\n) ([]byte, error) {\n\tl.Debugf(\"Detected remote state block with generate config. Resolving dependency by pulling remote state.\")\n\t// Create working directory where we will run terraform in. We will create the temporary directory in the download\n\t// directory for consistency with other file generation capabilities of terragrunt. Make sure it is cleaned up\n\t// before the function returns.\n\tif err := util.EnsureDirectory(pctx.DownloadDir); err != nil {\n\t\treturn nil, err\n\t}\n\n\ttempWorkDir, err := os.MkdirTemp(pctx.DownloadDir, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer func(path string) {\n\t\terr := os.RemoveAll(path)\n\t\tif err != nil {\n\t\t\tl.Warnf(\"Failed to remove %s: %v\", path, err)\n\t\t}\n\t}(tempWorkDir)\n\n\tl.Debugf(\"Setting dependency working directory to %s\", tempWorkDir)\n\n\tcredsGetter := creds.NewGetter()\n\tif err = credsGetter.ObtainAndUpdateEnvIfNecessary(\n\t\tctx,\n\t\tl,\n\t\tpctx.Env,\n\t\texternalcmd.NewProvider(l, pctx.AuthProviderCmd, shellRunOptsFromPctx(pctx)),\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\n\ttfRunOpts, err := setupTFRunOptsForBareTerraform(\n\t\tctx,\n\t\tpctx,\n\t\tl,\n\t\ttempWorkDir,\n\t\tiamRoleOpts,\n\t\tcredsGetter,\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// To speed up dependencies processing it is possible to retrieve its output directly from the backend without init dependencies\n\tif pctx.Experiments.Evaluate(experiment.DependencyFetchOutputFromState) && !pctx.NoDependencyFetchOutputFromState {\n\t\tswitch backend := remoteState.BackendName; backend {\n\t\tcase s3backend.BackendName:\n\t\t\tjsonBytes, s3GetErr := getTerragruntOutputJSONFromRemoteStateS3(\n\t\t\t\tctx,\n\t\t\t\tl,\n\t\t\t\tpctx,\n\t\t\t\tremoteState,\n\t\t\t)\n\t\t\tif s3GetErr != nil {\n\t\t\t\treturn nil, s3GetErr\n\t\t\t}\n\n\t\t\tl.Debugf(\"Retrieved output from %s as json: %s using s3 bucket\", pctx.TerragruntConfigPath, jsonBytes)\n\n\t\t\treturn jsonBytes, nil\n\t\tdefault:\n\t\t\tl.Debugf(\"dependency-fetch-output-from-state experiment is not supported for backend %s, falling back to default output retrieval\", backend)\n\t\t}\n\t}\n\n\t// Generate the backend configuration in the working dir. If no generate config is set on the remote state block,\n\t// set a temporary generate config so we can generate the backend code.\n\tif remoteState.Generate == nil {\n\t\tremoteState.Generate = &remotestate.ConfigGenerate{\n\t\t\tPath:     \"backend.tf\",\n\t\t\tIfExists: codegen.ExistsOverwriteTerragruntStr,\n\t\t}\n\t}\n\n\tif err := remoteState.GenerateOpenTofuCode(l, tempWorkDir); err != nil {\n\t\treturn nil, err\n\t}\n\n\tl.Debugf(\"Generated remote state configuration in working dir %s\", tempWorkDir)\n\n\t// Check for a provider lock file and copy it to the working dir if it exists.\n\tterragruntDir := filepath.Dir(pctx.TerragruntConfigPath)\n\tif err := CopyLockFile(l, pctx.RootWorkingDir, pctx.Writers.LogShowAbsPaths, terragruntDir, tempWorkDir); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// The working directory is now set up to interact with the state, so pull it down to get the json output.\n\n\t// Clone pctx and discard init stdout so it doesn't leak into the caller's output buffer.\n\tinitPctx := pctx.Clone()\n\tinitPctx.Writers.Writer = io.Discard\n\n\t// First run init to setup the backend configuration so that we can run output.\n\trunTerraformInitForDependencyOutput(ctx, initPctx, l, tempWorkDir)\n\n\t// Now that the backend is initialized, run terraform output to get the data and return it.\n\tbareCtx := tf.ContextWithTerraformCommandHook(ctx, nil)\n\n\tout, err := tf.RunCommandWithOutput(bareCtx, l, tfRunOpts, tf.CommandNameOutput, \"-json\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjsonString := strings.TrimSpace(out.Stdout.String())\n\tjsonBytes := []byte(jsonString)\n\tl.Debugf(\"Retrieved output from %s as json: %s\", targetConfigPath, jsonString)\n\n\treturn jsonBytes, nil\n}\n\n// getTerragruntOutputJSONFromRemoteStateS3 pulls the output directly from an S3 bucket without calling Terraform\nfunc getTerragruntOutputJSONFromRemoteStateS3(ctx context.Context, l log.Logger, pctx *ParsingContext, remoteState *remotestate.RemoteState) ([]byte, error) {\n\tl.Debugf(\"Fetching outputs directly from s3://%s/%s\", remoteState.BackendConfig[\"bucket\"], remoteState.BackendConfig[\"key\"])\n\n\ts3ConfigExtended, err := s3backend.Config(remoteState.BackendConfig).ParseExtendedS3Config()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsessionConfig := s3ConfigExtended.GetAwsSessionConfig()\n\n\ts3Client, err := awshelper.NewAWSConfigBuilder().\n\t\tWithSessionConfig(sessionConfig).\n\t\tWithEnv(pctx.Env).\n\t\tWithIAMRoleOptions(pctx.IAMRoleOptions).\n\t\tBuildS3Client(ctx, l)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tresult, err := s3Client.GetObject(ctx, &s3.GetObjectInput{\n\t\tBucket: aws.String(fmt.Sprintf(\"%s\", remoteState.BackendConfig[\"bucket\"])),\n\t\tKey:    aws.String(fmt.Sprintf(\"%s\", remoteState.BackendConfig[\"key\"])),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer func(Body io.ReadCloser) {\n\t\terr := Body.Close()\n\t\tif err != nil {\n\t\t\tl.Warnf(\"Failed to close remote state response %v\", err)\n\t\t}\n\t}(result.Body)\n\n\tsteateBody, err := io.ReadAll(result.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjsonState := string(steateBody)\n\tjsonMap := make(map[string]any)\n\n\terr = json.Unmarshal([]byte(jsonState), &jsonMap)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tjsonOutputs, err := json.Marshal(jsonMap[\"outputs\"])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn jsonOutputs, nil\n}\n\n// setupTFRunOptsForBareTerraform builds a *tf.RunOptions that can be used to run terraform\n// without going through the full RunTerragrunt operation. It merges IAM roles and obtains\n// credentials inline.\nfunc setupTFRunOptsForBareTerraform(\n\tctx context.Context,\n\tpctx *ParsingContext,\n\tl log.Logger,\n\tworkingDir string,\n\tiamRoleOpts iam.RoleOptions,\n\tcredsGetter *creds.Getter,\n) (*tf.TFOptions, error) {\n\t// Merge IAM options\n\tmergedIAM := iam.MergeRoleOptions(iamRoleOpts, pctx.OriginalIAMRoleOptions)\n\n\t// Build shell.RunOptions for this specific working dir with io.Discard as writer\n\tshellOpts := shellRunOptsFromPctx(pctx)\n\tshellOpts.WorkingDir = workingDir\n\tshellOpts.Writers.Writer = io.Discard\n\n\t// Make sure to assume any roles set by TG_IAM_ROLE\n\tif err := credsGetter.ObtainAndUpdateEnvIfNecessary(ctx, l, pctx.Env,\n\t\texternalcmd.NewProvider(l, pctx.AuthProviderCmd, shellOpts),\n\t\tamazonsts.NewProvider(l, mergedIAM, pctx.Env),\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &tf.TFOptions{\n\t\tJSONLogFormat:                pctx.JSONLogFormat,\n\t\tOriginalTerragruntConfigPath: pctx.OriginalTerragruntConfigPath,\n\t\tShellOptions:                 shellOpts,\n\t}, nil\n}\n\n// runTerragruntOutputJSON uses terragrunt running functions to extract the json output from the target config.\nfunc runTerragruntOutputJSON(ctx context.Context, pctx *ParsingContext, l log.Logger, targetConfig string) ([]byte, error) {\n\t// Update the stdout buffer so we can capture the output\n\tvar stdoutBuffer bytes.Buffer\n\n\tstdoutBufferWriter := bufio.NewWriter(&stdoutBuffer)\n\n\t// Override pctx for this specific operation\n\tpctx = pctx.Clone()\n\tpctx.ForwardTFStdout = false\n\tpctx.JSONLogFormat = false\n\tpctx.Writers.Writer = stdoutBufferWriter\n\n\tcfg, err := ParseConfigFile(ctx, pctx, l, pctx.TerragruntConfigPath, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trunCfg := cfg.ToRunConfig(l)\n\n\tcredsGetter := creds.NewGetter()\n\tif err = credsGetter.ObtainAndUpdateEnvIfNecessary(\n\t\tctx,\n\t\tl,\n\t\tpctx.Env,\n\t\texternalcmd.NewProvider(l, pctx.AuthProviderCmd, shellRunOptsFromPctx(pctx)),\n\t); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Build run.Options directly from ParsingContext fields.\n\t// Override Writers.Writer to capture stdout, and force ForwardTFStdout/JSONLogFormat off.\n\trunWriters := pctx.Writers\n\trunWriters.Writer = stdoutBufferWriter\n\n\trunOpts := &run.Options{\n\t\tWriters:                      runWriters,\n\t\tTerragruntConfigPath:         pctx.TerragruntConfigPath,\n\t\tOriginalTerragruntConfigPath: pctx.OriginalTerragruntConfigPath,\n\t\tWorkingDir:                   pctx.WorkingDir,\n\t\tRootWorkingDir:               pctx.RootWorkingDir,\n\t\tDownloadDir:                  pctx.DownloadDir,\n\t\tSource:                       pctx.Source,\n\t\tSourceMap:                    pctx.SourceMap,\n\t\tTerraformCommand:             pctx.TerraformCommand,\n\t\tOriginalTerraformCommand:     pctx.OriginalTerraformCommand,\n\t\tTerraformCliArgs:             pctx.TerraformCliArgs,\n\t\tEnv:                          pctx.Env,\n\t\tIAMRoleOptions:               pctx.IAMRoleOptions,\n\t\tOriginalIAMRoleOptions:       pctx.OriginalIAMRoleOptions,\n\t\tExperiments:                  pctx.Experiments,\n\t\tStrictControls:               pctx.StrictControls,\n\t\tFeatureFlags:                 pctx.FeatureFlags,\n\t\tEngineConfig:                 pctx.EngineConfig,\n\t\tEngineOptions:                pctx.EngineOptions,\n\t\tTFPath:                       pctx.TFPath,\n\t\tTofuImplementation:           pctx.TofuImplementation,\n\t\tForwardTFStdout:              false,\n\t\tJSONLogFormat:                false,\n\t\tHeadless:                     pctx.Headless,\n\t\tDebug:                        pctx.Debug,\n\t\tAutoInit:                     pctx.AutoInit,\n\t\tBackendBootstrap:             pctx.BackendBootstrap,\n\t\tTelemetry:                    pctx.Telemetry,\n\t\tAuthProviderCmd:              pctx.AuthProviderCmd,\n\t}\n\n\terr = run.Run(ctx, l, runOpts, report.NewReport(), runCfg, credsGetter)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\terr = stdoutBufferWriter.Flush()\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tjsonString := strings.TrimSpace(stdoutBuffer.String())\n\tjsonBytes := []byte(jsonString)\n\n\tl.Debugf(\"Retrieved output from %s as json: %s\", targetConfig, jsonString)\n\n\treturn jsonBytes, nil\n}\n\n// shellRunOptsFromPctx builds a *shell.RunOptions from ParsingContext flat fields.\nfunc shellRunOptsFromPctx(pctx *ParsingContext) *shell.ShellOptions {\n\treturn &shell.ShellOptions{\n\t\tWriters:         pctx.Writers,\n\t\tEngineOptions:   pctx.EngineOptions,\n\t\tWorkingDir:      pctx.WorkingDir,\n\t\tEnv:             pctx.Env,\n\t\tTFPath:          pctx.TFPath,\n\t\tEngineConfig:    pctx.EngineConfig,\n\t\tExperiments:     pctx.Experiments,\n\t\tTelemetry:       pctx.Telemetry,\n\t\tRootWorkingDir:  pctx.RootWorkingDir,\n\t\tHeadless:        pctx.Headless,\n\t\tForwardTFStdout: pctx.ForwardTFStdout,\n\t}\n}\n\n// tfRunOptsFromPctx builds a *tf.RunOptions from ParsingContext flat fields.\nfunc tfRunOptsFromPctx(pctx *ParsingContext) *tf.TFOptions {\n\treturn &tf.TFOptions{\n\t\tJSONLogFormat:                pctx.JSONLogFormat,\n\t\tOriginalTerragruntConfigPath: pctx.OriginalTerragruntConfigPath,\n\t\tShellOptions:                 shellRunOptsFromPctx(pctx),\n\t}\n}\n\n// TerraformOutputJSONToCtyValueMap takes the terraform output json and converts to a mapping between output keys to the\n// parsed cty.Value encoding of the json objects.\nfunc TerraformOutputJSONToCtyValueMap(targetConfigPath string, jsonBytes []byte) (map[string]cty.Value, error) {\n\t// When getting all outputs, terraform returns a json with the data containing metadata about the types, so we\n\t// can't quite return the data directly. Instead, we will need further processing to get the output we want.\n\t// To do so, we first Unmarshal the json into a simple go map to a OutputMeta struct.\n\ttype OutputMeta struct {\n\t\tType      json.RawMessage `json:\"type\"`\n\t\tValue     json.RawMessage `json:\"value\"`\n\t\tSensitive bool            `json:\"sensitive\"`\n\t}\n\n\tvar outputs map[string]OutputMeta\n\n\terr := json.Unmarshal(jsonBytes, &outputs)\n\tif err != nil {\n\t\treturn nil, errors.New(TerragruntOutputParsingError{Path: targetConfigPath, Err: err})\n\t}\n\n\tflattenedOutput := map[string]cty.Value{}\n\n\tfor k, v := range outputs {\n\t\toutputType, err := ctyjson.UnmarshalType(v.Type)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(TerragruntOutputParsingError{Path: targetConfigPath, Err: err})\n\t\t}\n\n\t\toutputVal, err := ctyjson.Unmarshal(v.Value, outputType)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(TerragruntOutputParsingError{Path: targetConfigPath, Err: err})\n\t\t}\n\n\t\tflattenedOutput[k] = outputVal\n\t}\n\n\treturn flattenedOutput, nil\n}\n\n// runTerraformInitForDependencyOutput will run terraform init in a mode that doesn't pull down plugins or modules. Note\n// that this will cause the command to fail for most modules as terraform init does a validation check to make sure the\n// plugins are available, even though we don't need it for our purposes (terraform output does not depend on any of the\n// plugins being available). As such this command will ignore errors in the init command.\n// To help with debuggability, the errors will be printed to the console when TG_LOG=debug is set.\nfunc runTerraformInitForDependencyOutput(ctx context.Context, pctx *ParsingContext, l log.Logger, workingDir string) {\n\tstderr := bytes.Buffer{}\n\n\tinitRunOpts := tfRunOptsFromPctx(pctx)\n\tinitRunOpts.ShellOptions.WorkingDir = workingDir\n\tinitRunOpts.ShellOptions.Writers.ErrWriter = &stderr\n\n\tbareCtx := tf.ContextWithTerraformCommandHook(ctx, nil)\n\n\tif err := tf.RunCommand(bareCtx, l, initRunOpts, tf.CommandNameInit, \"-get=false\"); err != nil {\n\t\tl.Debugf(\"Ignoring expected error from dependency init call\")\n\t\tl.Debugf(\"Init call stderr:\")\n\t\tl.Debugf(\"%s\", stderr.String())\n\t}\n}\n\nfunc (deps Dependencies) FilteredWithoutConfigPath() Dependencies {\n\tvar filteredDeps Dependencies\n\n\tfor _, dep := range deps {\n\t\tif !dep.ConfigPath.IsNull() {\n\t\t\tfilteredDeps = append(filteredDeps, dep)\n\t\t}\n\t}\n\n\treturn filteredDeps\n}\n\n// IsValidConfigPath checks if a cty.Value is a valid, usable config path.\nfunc IsValidConfigPath(v cty.Value) bool {\n\tif v.IsNull() || !v.IsWhollyKnown() || !v.Type().Equals(cty.String) {\n\t\treturn false\n\t}\n\n\t// Empty string is not a valid config path\n\tif v.AsString() == \"\" {\n\t\treturn false\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "pkg/config/dependency_inputs_test.go",
    "content": "package config_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDependencyInputsBlockedByDefault(t *testing.T) {\n\tt.Parallel()\n\n\t// Test that dependency.foo.inputs syntax is now blocked by default\n\tconfigWithDependencyInputs := `\ndependency \"dep\" {\n  config_path = \"../dep\"\n}\n\ninputs = {\n  value = dependency.dep.inputs.some_value\n}\n`\n\n\tparser := hclparse.NewParser()\n\tfile, err := parser.ParseFromString(configWithDependencyInputs, \"terragrunt.hcl\")\n\trequire.NoError(t, err)\n\n\t// Create a parsing context with strict controls\n\tpctx := &config.ParsingContext{\n\t\tStrictControls: controls.New(),\n\t}\n\n\tlogger := log.New()\n\n\t// Test that the deprecated configuration is detected and blocked\n\terr = config.DetectDeprecatedConfigurations(t.Context(), pctx, logger, file)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"Reading inputs from dependencies is no longer supported\")\n\tassert.Contains(t, err.Error(), \"use outputs\")\n}\n\nfunc TestDependencyOutputsStillAllowed(t *testing.T) {\n\tt.Parallel()\n\n\t// Test that dependency.foo.outputs syntax still works fine\n\tconfigWithDependencyOutputs := `\ndependency \"dep\" {\n  config_path = \"../dep\"\n}\n\ninputs = {\n  value = dependency.dep.outputs.some_value\n}\n`\n\n\tparser := hclparse.NewParser()\n\tfile, err := parser.ParseFromString(configWithDependencyOutputs, \"terragrunt.hcl\")\n\trequire.NoError(t, err)\n\n\t// Create a parsing context with strict controls\n\tpctx := &config.ParsingContext{\n\t\tStrictControls: controls.New(),\n\t}\n\n\tlogger := log.New()\n\n\t// Test that the dependency outputs are allowed (no error)\n\terr = config.DetectDeprecatedConfigurations(t.Context(), pctx, logger, file)\n\trequire.NoError(t, err)\n}\n\nfunc TestDetectInputsCtyUsageFunction(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tconfig   string\n\t\texpected bool\n\t}{\n\t\t{\n\t\t\tname: \"dependency inputs detected\",\n\t\t\tconfig: `\ninputs = {\n  value = dependency.dep.inputs.some_value\n}\n`,\n\t\t\texpected: true,\n\t\t},\n\t\t{\n\t\t\tname: \"dependency outputs not detected\",\n\t\t\tconfig: `\ninputs = {\n  value = dependency.dep.outputs.some_value\n}\n`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"no dependency references\",\n\t\t\tconfig: `\ninputs = {\n  value = \"static_value\"\n}\n`,\n\t\t\texpected: false,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple dependency inputs detected\",\n\t\t\tconfig: `\ninputs = {\n  value1 = dependency.dep1.inputs.val1\n  value2 = dependency.dep2.inputs.val2\n}\n`,\n\t\t\texpected: true,\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.Parallel()\n\n\t\t\tparser := hclparse.NewParser()\n\t\t\tfile, err := parser.ParseFromString(tc.config, \"terragrunt.hcl\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tresult := config.DetectInputsCtyUsage(file)\n\t\t\tassert.Equal(t, tc.expected, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/config/dependency_test.go",
    "content": "package config_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\n\t\"github.com/gruntwork-io/go-commons/env\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/zclconf/go-cty/cty\"\n\t\"github.com/zclconf/go-cty/cty/gocty\"\n)\n\nfunc TestDecodeDependencyBlockMultiple(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n\ndependency \"sql\" {\n  config_path = \"../sql\"\n}\n`\n\tfilename := config.DefaultTerragruntConfigPath\n\tfile, err := hclparse.NewParser().ParseFromString(cfg, filename)\n\trequire.NoError(t, err)\n\n\tdecoded := config.TerragruntDependency{}\n\trequire.NoError(t, file.Decode(&decoded, &hcl.EvalContext{}))\n\n\tassert.Len(t, decoded.Dependencies, 2)\n\tassert.Equal(t, \"vpc\", decoded.Dependencies[0].Name)\n\tassert.Equal(t, cty.StringVal(\"../vpc\"), decoded.Dependencies[0].ConfigPath)\n\tassert.Equal(t, \"sql\", decoded.Dependencies[1].Name)\n\tassert.Equal(t, cty.StringVal(\"../sql\"), decoded.Dependencies[1].ConfigPath)\n}\n\nfunc TestDecodeNoDependencyBlock(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nlocals {\n  path = \"../vpc\"\n}\n`\n\tfilename := config.DefaultTerragruntConfigPath\n\tfile, err := hclparse.NewParser().ParseFromString(cfg, filename)\n\trequire.NoError(t, err)\n\n\tdecoded := config.TerragruntDependency{}\n\trequire.NoError(t, file.Decode(&decoded, &hcl.EvalContext{}))\n\tassert.Empty(t, decoded.Dependencies)\n}\n\nfunc TestDecodeDependencyNoLabelIsError(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ndependency {\n  config_path = \"../vpc\"\n}\n`\n\tfilename := config.DefaultTerragruntConfigPath\n\tfile, err := hclparse.NewParser().ParseFromString(cfg, filename)\n\trequire.NoError(t, err)\n\n\tdecoded := config.TerragruntDependency{}\n\trequire.Error(t, file.Decode(&decoded, &hcl.EvalContext{}))\n}\n\nfunc TestDecodeDependencyMockOutputs(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ndependency \"hitchhiker\" {\n  config_path = \"../answers\"\n  mock_outputs = {\n    the_answer = 42\n  }\n  mock_outputs_allowed_terraform_commands = [\"validate\", \"apply\"]\n}\n`\n\tfilename := config.DefaultTerragruntConfigPath\n\tfile, err := hclparse.NewParser().ParseFromString(cfg, filename)\n\trequire.NoError(t, err)\n\n\tdecoded := config.TerragruntDependency{}\n\trequire.NoError(t, file.Decode(&decoded, &hcl.EvalContext{}))\n\n\tassert.Len(t, decoded.Dependencies, 1)\n\tdependency := decoded.Dependencies[0]\n\tassert.Equal(t, \"hitchhiker\", dependency.Name)\n\tassert.Equal(t, cty.StringVal(\"../answers\"), dependency.ConfigPath)\n\n\tctyValueDefault := dependency.MockOutputs\n\tassert.NotNil(t, ctyValueDefault)\n\n\tvar actualDefault struct {\n\t\tTheAnswer int `cty:\"the_answer\"`\n\t}\n\trequire.NoError(t, gocty.FromCtyValue(*ctyValueDefault, &actualDefault))\n\tassert.Equal(t, 42, actualDefault.TheAnswer)\n\n\tdefaultAllowedCommands := dependency.MockOutputsAllowedTerraformCommands\n\tassert.NotNil(t, defaultAllowedCommands)\n\tassert.Equal(t, []string{\"validate\", \"apply\"}, *defaultAllowedCommands)\n}\nfunc TestParseDependencyBlockMultiple(t *testing.T) {\n\tt.Parallel()\n\n\tfilename, err := filepath.Abs(filepath.Join(\"../..\", \"test\", \"fixtures\", \"regressions\", \"multiple-dependency-load-sync\", \"main\", \"terragrunt.hcl\"))\n\trequire.NoError(t, err)\n\n\tctx, pctx := newTestParsingContext(t, filename)\n\terr = pctx.Experiments.EnableExperiment(experiment.DependencyFetchOutputFromState)\n\trequire.NoError(t, err)\n\n\tpctx.Env = env.Parse(os.Environ())\n\ttfConfig, err := config.ParseConfigFile(ctx, pctx, logger.CreateLogger(), filename, nil)\n\trequire.NoError(t, err)\n\tassert.Len(t, tfConfig.TerragruntDependencies, 2)\n\tassert.Equal(t, \"dependency_1\", tfConfig.TerragruntDependencies[0].Name)\n\tassert.Equal(t, \"dependency_2\", tfConfig.TerragruntDependencies[1].Name)\n}\n\nfunc TestDisabledDependency(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ndependency \"ec2\" {\n  config_path = \"../ec2\"\n  enabled    = false\n}\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n`\n\tfilename := config.DefaultTerragruntConfigPath\n\tfile, err := hclparse.NewParser().ParseFromString(cfg, filename)\n\trequire.NoError(t, err)\n\n\tdecoded := config.TerragruntDependency{}\n\trequire.NoError(t, file.Decode(&decoded, &hcl.EvalContext{}))\n\tassert.Len(t, decoded.Dependencies, 2)\n}\n\n// TestDisabledDependencyWithNullConfigPath verifies that disabled dependencies\n// with null config_path don't panic during parsing (they bypass validation).\nfunc TestDisabledDependencyWithNullConfigPath(t *testing.T) {\n\tt.Parallel()\n\n\t// This config has a disabled dependency with config_path that would fail\n\t// validation if it were enabled (uses a local that resolves to null)\n\tcfg := `\nlocals {\n  disabled_path = null\n}\n\ndependency \"disabled\" {\n  config_path = local.disabled_path\n  enabled     = false\n}\n\ndependency \"enabled\" {\n  config_path = \"../vpc\"\n}\n`\n\tl := logger.CreateLogger()\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tpctx = pctx.WithDecodeList(config.DependencyBlock)\n\n\t// Should not panic - disabled deps bypass config_path validation\n\tterragruntConfig, err := config.PartialParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\n\t// Only enabled dependency should be in the paths\n\tassert.Len(t, terragruntConfig.Dependencies.Paths, 1)\n}\n\n// TestDisabledDependencyWithEmptyConfigPath verifies that disabled dependencies\n// with empty config_path don't cause errors.\nfunc TestDisabledDependencyWithEmptyConfigPath(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\ndependency \"disabled\" {\n  config_path = \"\"\n  enabled     = false\n}\n\ndependency \"enabled\" {\n  config_path = \"../vpc\"\n}\n`\n\tl := logger.CreateLogger()\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tpctx = pctx.WithDecodeList(config.DependencyBlock)\n\n\t// Should not error - disabled deps bypass config_path validation\n\tterragruntConfig, err := config.PartialParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, cfg, nil)\n\trequire.NoError(t, err)\n\n\t// Only enabled dependency should be in the paths\n\tassert.Len(t, terragruntConfig.Dependencies.Paths, 1)\n}\n"
  },
  {
    "path": "pkg/config/engine.go",
    "content": "package config\n\nimport (\n\t\"github.com/zclconf/go-cty/cty\"\n)\n\n// EngineConfig represents the structure of the HCL data\ntype EngineConfig struct {\n\tVersion *string    `hcl:\"version,attr\" cty:\"version\"`\n\tType    *string    `hcl:\"type,attr\" cty:\"type\"`\n\tMeta    *cty.Value `hcl:\"meta,attr\" cty:\"meta\"`\n\tSource  string     `hcl:\"source,attr\" cty:\"source\"`\n}\n\n// Clone returns a copy of the EngineConfig used in deep copy\nfunc (c *EngineConfig) Clone() *EngineConfig {\n\treturn &EngineConfig{\n\t\tSource:  c.Source,\n\t\tVersion: c.Version,\n\t\tType:    c.Type,\n\t\tMeta:    c.Meta,\n\t}\n}\n\n// Merge merges the EngineConfig with another EngineConfig\nfunc (c *EngineConfig) Merge(engine *EngineConfig) {\n\tif engine.Source != \"\" {\n\t\tc.Source = engine.Source\n\t}\n\n\tif engine.Version != nil {\n\t\tc.Version = engine.Version\n\t}\n\n\tif engine.Type != nil {\n\t\tc.Type = engine.Type\n\t}\n\n\tif engine.Meta != nil {\n\t\tc.Meta = engine.Meta\n\t}\n}\n"
  },
  {
    "path": "pkg/config/errors.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// Custom error types\n\ntype InvalidArgError string\n\nfunc (e InvalidArgError) Error() string {\n\treturn string(e)\n}\n\ntype IncludedConfigMissingPathError string\n\nfunc (err IncludedConfigMissingPathError) Error() string {\n\treturn fmt.Sprintf(\"The include configuration in %s must specify a 'path' parameter\", string(err))\n}\n\ntype IncludeConfigNotFoundError struct {\n\tIncludePath string\n\tSourcePath  string\n}\n\nfunc (err IncludeConfigNotFoundError) Error() string {\n\treturn fmt.Sprintf(\"Include configuration not found: %s (referenced from: %s)\", err.IncludePath, err.SourcePath)\n}\n\ntype TooManyLevelsOfInheritanceError struct {\n\tConfigPath             string\n\tFirstLevelIncludePath  string\n\tSecondLevelIncludePath string\n}\n\nfunc (err TooManyLevelsOfInheritanceError) Error() string {\n\treturn fmt.Sprintf(\"%s includes %s, which itself includes %s. Only one level of includes is allowed.\", err.ConfigPath, err.FirstLevelIncludePath, err.SecondLevelIncludePath)\n}\n\ntype CouldNotResolveTerragruntConfigInFileError string\n\nfunc (err CouldNotResolveTerragruntConfigInFileError) Error() string {\n\treturn \"Could not find Terragrunt configuration settings in \" + string(err)\n}\n\ntype InvalidMergeStrategyTypeError string\n\nfunc (err InvalidMergeStrategyTypeError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"Include merge strategy %s is unknown. Valid strategies are: %s, %s, %s, %s\",\n\t\tstring(err),\n\t\tNoMerge,\n\t\tShallowMerge,\n\t\tDeepMerge,\n\t\tDeepMergeMapOnly,\n\t)\n}\n\ntype DependencyDirNotFoundError struct {\n\tDir []string\n}\n\nfunc (err DependencyDirNotFoundError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"Found paths in the 'dependencies' block that do not exist: %v\", err.Dir,\n\t)\n}\n\ntype DuplicatedGenerateBlocksError struct {\n\tBlockName []string\n}\n\nfunc (err DuplicatedGenerateBlocksError) Error() string {\n\treturn fmt.Sprintf(\n\t\t\"Detected generate blocks with the same name: %v\", err.BlockName,\n\t)\n}\n\ntype TFVarFileNotFoundError struct {\n\tFile  string\n\tCause string\n}\n\nfunc (err TFVarFileNotFoundError) Error() string {\n\treturn fmt.Sprintf(\"TFVarFileNotFound: Could not find a %s. Cause: %s.\", err.File, err.Cause)\n}\n\ntype WrongNumberOfParamsError struct {\n\tFunc     string\n\tExpected string\n\tActual   int\n}\n\nfunc (err WrongNumberOfParamsError) Error() string {\n\treturn fmt.Sprintf(\"Expected %s params for function %s, but got %d\", err.Expected, err.Func, err.Actual)\n}\n\ntype InvalidParameterTypeError struct {\n\tExpected string\n\tActual   string\n}\n\nfunc (err InvalidParameterTypeError) Error() string {\n\treturn fmt.Sprintf(\"Expected param of type %s but got %s\", err.Expected, err.Actual)\n}\n\ntype ParentFileNotFoundError struct {\n\tPath  string\n\tFile  string\n\tCause string\n}\n\nfunc (err ParentFileNotFoundError) Error() string {\n\treturn fmt.Sprintf(\"ParentFileNotFoundError: Could not find a %s in any of the parent folders of %s. Cause: %s.\", err.File, err.Path, err.Cause)\n}\n\ntype InvalidGetEnvParamsError struct {\n\tExample         string\n\tActualNumParams int\n}\n\nfunc (err InvalidGetEnvParamsError) Error() string {\n\treturn fmt.Sprintf(\"InvalidGetEnvParamsError: Expected one or two parameters (%s) for get_env but got %d.\", err.Example, err.ActualNumParams)\n}\n\ntype EnvVarNotFoundError struct {\n\tEnvVar string\n}\n\nfunc (err EnvVarNotFoundError) Error() string {\n\treturn fmt.Sprintf(\"EnvVarNotFoundError: Required environment variable %s - not found\", err.EnvVar)\n}\n\ntype InvalidEnvParamNameError struct {\n\tEnvName string\n}\n\nfunc (err InvalidEnvParamNameError) Error() string {\n\treturn fmt.Sprintf(\"InvalidEnvParamNameError: Invalid environment variable name - (%s) \", err.EnvName)\n}\n\ntype EmptyStringNotAllowedError string\n\nfunc (err EmptyStringNotAllowedError) Error() string {\n\treturn \"Empty string value is not allowed for \" + string(err)\n}\n\ntype ConflictingRunCmdCacheOptionsError struct{}\n\nfunc (err ConflictingRunCmdCacheOptionsError) Error() string {\n\treturn \"The --terragrunt-global-cache and --terragrunt-no-cache options cannot be used together. Choose one caching option for run_cmd.\"\n}\n\ntype TerragruntConfigNotFoundError struct {\n\tPath string\n}\n\nfunc (err TerragruntConfigNotFoundError) Error() string {\n\treturn fmt.Sprintf(\"You attempted to run terragrunt in a folder that does not contain a terragrunt.hcl file. Please add a terragrunt.hcl file and try again.\\n\\nPath: %q\", err.Path)\n}\n\ntype InvalidSourceURLError struct {\n\tModulePath       string\n\tModuleSourceURL  string\n\tTerragruntSource string\n}\n\nfunc (err InvalidSourceURLError) Error() string {\n\treturn fmt.Sprintf(\"The --source parameter is set to '%s', but the source URL in the module at '%s' is invalid: '%s'. Note that the module URL must have a double-slash to separate the repo URL from the path within the repo!\", err.TerragruntSource, err.ModulePath, err.ModuleSourceURL)\n}\n\ntype InvalidSourceURLWithMapError struct {\n\tModulePath      string\n\tModuleSourceURL string\n}\n\nfunc (err InvalidSourceURLWithMapError) Error() string {\n\treturn fmt.Sprintf(\"The --source-map parameter was passed in, but the source URL in the module at '%s' is invalid: '%s'. Note that the module URL must have a double-slash to separate the repo URL from the path within the repo!\", err.ModulePath, err.ModuleSourceURL)\n}\n\ntype ParsingModulePathError struct {\n\tModuleSourceURL string\n}\n\nfunc (err ParsingModulePathError) Error() string {\n\treturn fmt.Sprintf(\"Unable to obtain the module path from the source URL '%s'. Ensure that the URL is in a supported format.\", err.ModuleSourceURL)\n}\n\ntype InvalidSopsFormatError struct {\n\tSourceFilePath string\n}\n\nfunc (err InvalidSopsFormatError) Error() string {\n\treturn fmt.Sprintf(\"File %s is not a valid format or encoding. Terragrunt will only decrypt yaml or json files in UTF-8 encoding.\", err.SourceFilePath)\n}\n\ntype InvalidIncludeKeyError struct {\n\tname string\n}\n\nfunc (err InvalidIncludeKeyError) Error() string {\n\treturn fmt.Sprintf(\"There is no include block in the current config with the label '%s'\", err.name)\n}\n\ntype DependencyFileNotFoundError struct {\n\tPath string\n}\n\nfunc (err DependencyFileNotFoundError) Error() string {\n\treturn \"Dependency file not found: \" + err.Path\n}\n\n// Dependency Custom error types\n\ntype DependencyConfigNotFound struct {\n\tPath string\n}\n\nfunc (err DependencyConfigNotFound) Error() string {\n\treturn err.Path + \" does not exist\"\n}\n\ntype TerragruntOutputParsingError struct {\n\tErr  error\n\tPath string\n}\n\nfunc (err TerragruntOutputParsingError) Error() string {\n\treturn fmt.Sprintf(\"Could not parse output from terragrunt config %s. Underlying error: %s\", err.Path, err.Err)\n}\n\ntype TerragruntOutputEncodingError struct {\n\tErr  error\n\tPath string\n}\n\nfunc (err TerragruntOutputEncodingError) Error() string {\n\treturn fmt.Sprintf(\"Could not encode output from terragrunt config %s. Underlying error: %s\", err.Path, err.Err)\n}\n\ntype TerragruntOutputListEncodingError struct {\n\tErr   error\n\tPaths []string\n}\n\nfunc (err TerragruntOutputListEncodingError) Error() string {\n\treturn fmt.Sprintf(\"Could not encode output from list of terragrunt configs %v. Underlying error: %s\", err.Paths, err.Err)\n}\n\ntype TerragruntOutputTargetNoOutputs struct {\n\ttargetName    string\n\ttargetPath    string\n\ttargetConfig  string\n\tcurrentConfig string\n}\n\nfunc (err TerragruntOutputTargetNoOutputs) ExitCode() int {\n\treturn 1\n}\n\nfunc (err TerragruntOutputTargetNoOutputs) Unwrap() error {\n\treturn nil\n}\n\nfunc (err TerragruntOutputTargetNoOutputs) Error() string {\n\tmsg := `\nIf this dependency is accessed before the outputs are ready (which can happen during the planning phase of an unapplied stack), consider using mock_outputs:\n\ndependency \"` + err.targetName + `\" {\n    config_path = \"` + err.targetPath + `\"\n\n    mock_outputs = {\n        ` + err.targetName + `_output = \"mock-` + err.targetName + `-output\"\n    }\n}\n\nFor more info, see:\nhttps://docs.terragrunt.com/features/stacks/#unapplied-dependency-and-mock-outputs\n\nIf you do not require outputs from your dependency, consider using the dependencies block instead:\nhttps://docs.terragrunt.com/reference/config-blocks-and-attributes/#dependencies\n`\n\n\treturn fmt.Sprintf(\n\t\t\"%s is a dependency of %s but detected no outputs. Either the target module has not been applied yet, or the module has no outputs.\\n%s\",\n\t\terr.targetConfig,\n\t\terr.currentConfig,\n\t\tmsg,\n\t)\n}\n\ntype DependencyCycleError []string\n\nfunc (err DependencyCycleError) Error() string {\n\treturn \"Found a dependency cycle between modules: \" + strings.Join([]string(err), \" -> \")\n}\n\ntype DependencyInvalidConfigPathError struct {\n\tDependencyName string\n}\n\nfunc (err DependencyInvalidConfigPathError) Error() string {\n\treturn fmt.Sprintf(\"dependency %q has invalid config_path\", err.DependencyName)\n}\n\n// MaxParseDepthError is returned when config parsing exceeds the maximum allowed depth.\ntype MaxParseDepthError struct {\n\tDepth int\n\tMax   int\n}\n\nfunc (err MaxParseDepthError) Error() string {\n\treturn fmt.Sprintf(\"maximum parse depth of %d exceeded (current depth: %d). This usually indicates circular includes or extremely deep config nesting.\", err.Max, err.Depth)\n}\n"
  },
  {
    "path": "pkg/config/errors_block.go",
    "content": "package config\n\nimport (\n\t\"maps\"\n\t\"slices\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/zclconf/go-cty/cty\"\n)\n\n// ErrorsConfig represents the top-level errors configuration\ntype ErrorsConfig struct {\n\tRetry  []*RetryBlock  `cty:\"retry\" hcl:\"retry,block\"`\n\tIgnore []*IgnoreBlock `cty:\"ignore\" hcl:\"ignore,block\"`\n}\n\n// RetryBlock represents a labeled retry block\ntype RetryBlock struct {\n\tLabel            string   `cty:\"name\" hcl:\"name,label\"`\n\tRetryableErrors  []string `cty:\"retryable_errors\" hcl:\"retryable_errors\"`\n\tMaxAttempts      int      `cty:\"max_attempts\" hcl:\"max_attempts\"`\n\tSleepIntervalSec int      `cty:\"sleep_interval_sec\" hcl:\"sleep_interval_sec\"`\n}\n\n// IgnoreBlock represents a labeled ignore block\ntype IgnoreBlock struct {\n\tSignals         map[string]cty.Value `cty:\"signals\" hcl:\"signals,optional\"`\n\tLabel           string               `cty:\"name\" hcl:\"name,label\"`\n\tMessage         string               `cty:\"message\" hcl:\"message,optional\"`\n\tIgnorableErrors []string             `cty:\"ignorable_errors\" hcl:\"ignorable_errors\"`\n}\n\n// Clone returns a deep copy of ErrorsConfig\nfunc (c *ErrorsConfig) Clone() *ErrorsConfig {\n\tif c == nil {\n\t\treturn nil\n\t}\n\n\treturn &ErrorsConfig{\n\t\tRetry:  cloneRetryBlocks(c.Retry),\n\t\tIgnore: cloneIgnoreBlocks(c.Ignore),\n\t}\n}\n\n// Merge combines the current ErrorsConfig with another one, prioritizing the other config\nfunc (c *ErrorsConfig) Merge(other *ErrorsConfig) {\n\tif c == nil || other == nil {\n\t\treturn\n\t}\n\n\tc.Retry = mergeRetryBlocks(c.Retry, other.Retry)\n\tc.Ignore = mergeIgnoreBlocks(c.Ignore, other.Ignore)\n}\n\n// Clone returns a deep copy of a RetryBlock\nfunc (r *RetryBlock) Clone() *RetryBlock {\n\tif r == nil {\n\t\treturn nil\n\t}\n\n\treturn &RetryBlock{\n\t\tLabel:            r.Label,\n\t\tRetryableErrors:  cloneStringSlice(r.RetryableErrors),\n\t\tMaxAttempts:      r.MaxAttempts,\n\t\tSleepIntervalSec: r.SleepIntervalSec,\n\t}\n}\n\n// Clone returns a deep copy of an IgnoreBlock\nfunc (i *IgnoreBlock) Clone() *IgnoreBlock {\n\tif i == nil {\n\t\treturn nil\n\t}\n\n\treturn &IgnoreBlock{\n\t\tLabel:           i.Label,\n\t\tIgnorableErrors: cloneStringSlice(i.IgnorableErrors),\n\t\tMessage:         i.Message,\n\t\tSignals:         cloneSignalsMap(i.Signals),\n\t}\n}\n\n// Helper function to deep copy a slice of RetryBlock\nfunc cloneRetryBlocks(blocks []*RetryBlock) []*RetryBlock {\n\tif blocks == nil {\n\t\treturn nil\n\t}\n\n\tcloned := make([]*RetryBlock, len(blocks))\n\tfor i, block := range blocks {\n\t\tcloned[i] = block.Clone()\n\t}\n\n\treturn cloned\n}\n\n// Helper function to deep copy a slice of IgnoreBlock\nfunc cloneIgnoreBlocks(blocks []*IgnoreBlock) []*IgnoreBlock {\n\tif blocks == nil {\n\t\treturn nil\n\t}\n\n\tcloned := make([]*IgnoreBlock, len(blocks))\n\tfor i, block := range blocks {\n\t\tcloned[i] = block.Clone()\n\t}\n\n\treturn cloned\n}\n\n// Helper function to deep copy a slice of strings\nfunc cloneStringSlice(slice []string) []string {\n\tif slice == nil {\n\t\treturn nil\n\t}\n\n\tcloned := make([]string, len(slice))\n\tcopy(cloned, slice)\n\n\treturn cloned\n}\n\n// Helper function to deep copy a map of signals\nfunc cloneSignalsMap(signals map[string]cty.Value) map[string]cty.Value {\n\tif signals == nil {\n\t\treturn nil\n\t}\n\n\tcloned := make(map[string]cty.Value, len(signals))\n\tmaps.Copy(cloned, signals)\n\n\treturn cloned\n}\n\n// Merges two slices of RetryBlock, prioritizing the second slice\nfunc mergeRetryBlocks(existing, other []*RetryBlock) []*RetryBlock {\n\tretryMap := make(map[string]*RetryBlock, len(existing)+len(other))\n\n\t// Add existing retry blocks\n\tfor _, block := range existing {\n\t\tretryMap[block.Label] = block\n\t}\n\n\t// Merge retry blocks from 'other'\n\tfor _, otherBlock := range other {\n\t\tif existingBlock, found := retryMap[otherBlock.Label]; found {\n\t\t\texistingBlock.RetryableErrors = util.MergeSlices(existingBlock.RetryableErrors, otherBlock.RetryableErrors)\n\n\t\t\tif otherBlock.MaxAttempts > 0 {\n\t\t\t\texistingBlock.MaxAttempts = otherBlock.MaxAttempts\n\t\t\t}\n\n\t\t\tif otherBlock.SleepIntervalSec > 0 {\n\t\t\t\texistingBlock.SleepIntervalSec = otherBlock.SleepIntervalSec\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\tretryMap[otherBlock.Label] = otherBlock\n\t}\n\n\treturn slices.Collect(maps.Values(retryMap))\n}\n\n// Merges two slices of IgnoreBlock, prioritizing the second slice\nfunc mergeIgnoreBlocks(existing, other []*IgnoreBlock) []*IgnoreBlock {\n\tignoreMap := make(map[string]*IgnoreBlock, len(existing)+len(other))\n\n\t// Add existing ignore blocks\n\tfor _, block := range existing {\n\t\tignoreMap[block.Label] = block\n\t}\n\n\t// Merge ignore blocks from 'other'\n\tfor _, otherBlock := range other {\n\t\tif existingBlock, found := ignoreMap[otherBlock.Label]; found {\n\t\t\texistingBlock.IgnorableErrors = util.MergeSlices(existingBlock.IgnorableErrors, otherBlock.IgnorableErrors)\n\n\t\t\tif otherBlock.Message != \"\" {\n\t\t\t\texistingBlock.Message = otherBlock.Message\n\t\t\t}\n\n\t\t\tif otherBlock.Signals != nil {\n\t\t\t\tif existingBlock.Signals == nil {\n\t\t\t\t\texistingBlock.Signals = make(map[string]cty.Value, len(otherBlock.Signals))\n\t\t\t\t}\n\n\t\t\t\tmaps.Copy(existingBlock.Signals, otherBlock.Signals)\n\t\t\t}\n\t\t} else {\n\t\t\tignoreMap[otherBlock.Label] = otherBlock\n\t\t}\n\t}\n\n\t// Convert map back to slice\n\treturn slices.Collect(maps.Values(ignoreMap))\n}\n"
  },
  {
    "path": "pkg/config/exclude.go",
    "content": "package config\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runcfg\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/zclconf/go-cty/cty\"\n)\n\n// bool values to be used as booleans.\nvar boolFlagValues = []string{\"if\", \"exclude_dependencies\", \"no_run\"}\n\n// ExcludeConfig configurations for hcl files.\ntype ExcludeConfig struct {\n\tExcludeDependencies *bool    `cty:\"exclude_dependencies\" hcl:\"exclude_dependencies,attr\" json:\"exclude_dependencies\"`\n\tNoRun               *bool    `cty:\"no_run\" hcl:\"no_run,attr\" json:\"no_run\"`\n\tActions             []string `cty:\"actions\" hcl:\"actions,attr\" json:\"actions\"`\n\tIf                  bool     `cty:\"if\" hcl:\"if,attr\" json:\"if\"`\n}\n\n// IsActionListed checks if the action is listed in the exclude block.\nfunc (e *ExcludeConfig) IsActionListed(action string) bool {\n\treturn runcfg.IsActionListedInExclude(e.Actions, action)\n}\n\n// ShouldPreventRun checks if the unit should be prevented from running based on the no_run attribute and current action.\nfunc (e *ExcludeConfig) ShouldPreventRun(action string) bool {\n\treturn runcfg.ShouldPreventRunBasedOnExclude(e.Actions, e.NoRun, e.If, action)\n}\n\n// Clone returns a new instance of ExcludeConfig with the same values as the original.\nfunc (e *ExcludeConfig) Clone() *ExcludeConfig {\n\treturn &ExcludeConfig{\n\t\tIf:                  e.If,\n\t\tActions:             e.Actions,\n\t\tExcludeDependencies: e.ExcludeDependencies,\n\t\tNoRun:               e.NoRun,\n\t}\n}\n\n// Merge merges the values of the provided ExcludeConfig into the original.\nfunc (e *ExcludeConfig) Merge(exclude *ExcludeConfig) {\n\t// copy not empty fields\n\te.If = exclude.If\n\tif len(exclude.Actions) > 0 {\n\t\te.Actions = exclude.Actions\n\t}\n\n\te.ExcludeDependencies = exclude.ExcludeDependencies\n\te.NoRun = exclude.NoRun\n}\n\n// evaluateExcludeBlocks evaluates the exclude block in the parsed file.\nfunc evaluateExcludeBlocks(ctx context.Context, pctx *ParsingContext, l log.Logger, file *hclparse.File) (*ExcludeConfig, error) {\n\texcludeBlock, err := file.Blocks(MetadataExclude, false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(excludeBlock) == 0 {\n\t\treturn nil, nil\n\t}\n\n\tif len(excludeBlock) > 1 {\n\t\t// only one block allowed\n\t\treturn nil, errors.Errorf(\"Only one %s block is allowed found multiple in %s\", MetadataExclude, file.ConfigPath)\n\t}\n\n\tattrs, err := excludeBlock[0].JustAttributes()\n\tif err != nil {\n\t\tl.Debugf(\"Encountered error while decoding exclude block.\")\n\t\treturn nil, err\n\t}\n\n\tevalCtx, err := createTerragruntEvalContext(ctx, pctx, l, file.ConfigPath)\n\tif err != nil {\n\t\tl.Errorf(\"Failed to create eval context %s\", file.ConfigPath)\n\t\treturn nil, err\n\t}\n\n\tevaluatedAttrs := map[string]cty.Value{}\n\n\tfor _, attr := range attrs {\n\t\tvalue, err := attr.Value(evalCtx)\n\t\tif err != nil {\n\t\t\tl.Debugf(\"Encountered error while evaluating exclude block in file %s\", file.ConfigPath)\n\n\t\t\treturn nil, err\n\t\t}\n\n\t\tevaluatedAttrs[attr.Name] = value\n\t}\n\n\tfor _, boolFlag := range boolFlagValues {\n\t\tif value, ok := evaluatedAttrs[boolFlag]; ok {\n\t\t\tif value.Type() == cty.String { // handle bool flag value\n\t\t\t\tval, err := strconv.ParseBool(value.AsString())\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, errors.New(err)\n\t\t\t\t}\n\n\t\t\t\tevaluatedAttrs[boolFlag] = cty.BoolVal(val)\n\t\t\t}\n\t\t}\n\t}\n\n\texcludeAsCtyVal, err := ConvertValuesMapToCtyVal(evaluatedAttrs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// convert cty map to ExcludeConfig\n\texcludeConfig := &ExcludeConfig{}\n\tif err := CtyToStruct(excludeAsCtyVal, excludeConfig); err != nil {\n\t\treturn nil, errors.Unwrap(err)\n\t}\n\n\treturn excludeConfig, nil\n}\n"
  },
  {
    "path": "pkg/config/external_test.go",
    "content": "// This file validates that the pkg/config package is usable by external consumers\n// as a public API. All tests here use only the external (black-box) package name\n// `config_test` and import only public packages — no `internal/` imports are allowed.\n\npackage config_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/zclconf/go-cty/cty\"\n)\n\nfunc createExternalLogger() log.Logger {\n\tformatter := format.NewFormatter(format.NewKeyValueFormatPlaceholders())\n\tformatter.SetDisabledColors(true)\n\n\treturn log.New(log.WithLevel(log.DebugLevel), log.WithFormatter(formatter))\n}\n\nfunc TestExternalConstants(t *testing.T) {\n\tt.Parallel()\n\n\tassert.Equal(t, \"terragrunt.hcl\", config.DefaultTerragruntConfigPath)\n\tassert.Equal(t, \"terragrunt.stack.hcl\", config.DefaultStackFile)\n\tassert.Equal(t, \".terragrunt-stack\", config.StackDir)\n\tassert.Equal(t, \"terragrunt.hcl.json\", config.DefaultTerragruntJSONConfigPath)\n\tassert.Equal(t, \"root.hcl\", config.RecommendedParentConfigName)\n\tassert.Equal(t, \"found_in_file\", config.FoundInFile)\n\tassert.NotEmpty(t, config.DefaultTerragruntConfigPaths)\n}\n\nfunc TestExternalTerragruntConfigStruct(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := &config.TerragruntConfig{\n\t\tTerraformBinary:             \"tofu\",\n\t\tTerragruntVersionConstraint: \">= 0.50.0\",\n\t\tTerraformVersionConstraint:  \">= 1.5.0\",\n\t\tDownloadDir:                 \"/tmp/download\",\n\t\tIamRole:                     \"arn:aws:iam::123456789012:role/test\",\n\t\tIamAssumeRoleSessionName:    \"test-session\",\n\t\tIamWebIdentityToken:         \"token-value\",\n\t\tInputs:                      map[string]any{\"key\": \"value\"},\n\t\tLocals:                      map[string]any{\"local_key\": \"local_value\"},\n\t\tIsPartial:                   false,\n\t}\n\n\tassert.Equal(t, \"tofu\", cfg.TerraformBinary)\n\tassert.Equal(t, \">= 0.50.0\", cfg.TerragruntVersionConstraint)\n\tassert.Equal(t, \">= 1.5.0\", cfg.TerraformVersionConstraint)\n\tassert.Equal(t, \"/tmp/download\", cfg.DownloadDir)\n\tassert.Equal(t, \"arn:aws:iam::123456789012:role/test\", cfg.IamRole)\n\tassert.Equal(t, \"test-session\", cfg.IamAssumeRoleSessionName)\n\tassert.Equal(t, \"token-value\", cfg.IamWebIdentityToken)\n\tassert.Equal(t, map[string]any{\"key\": \"value\"}, cfg.Inputs)\n\tassert.Equal(t, map[string]any{\"local_key\": \"local_value\"}, cfg.Locals)\n\tassert.False(t, cfg.IsPartial)\n\tassert.Nil(t, cfg.Terraform)\n\tassert.Nil(t, cfg.RemoteState)\n\tassert.Nil(t, cfg.Dependencies)\n\tassert.Nil(t, cfg.Engine)\n\tassert.Nil(t, cfg.PreventDestroy)\n}\n\nfunc TestExternalTerragruntConfigAsCty(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := &config.TerragruntConfig{\n\t\tTerraformBinary: \"terraform\",\n\t\tInputs:          map[string]any{\"env\": \"dev\"},\n\t}\n\n\tctyVal, err := config.TerragruntConfigAsCty(cfg)\n\trequire.NoError(t, err)\n\tassert.True(t, ctyVal.IsKnown())\n\tassert.True(t, ctyVal.Type().IsObjectType())\n}\n\nfunc TestExternalGetTerraformSourceURL(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"explicit source overrides config\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tresult, err := config.GetTerraformSourceURL(\"explicit-source\", nil, \"config.hcl\", &config.TerragruntConfig{})\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"explicit-source\", result)\n\t})\n\n\tt.Run(\"no source returns dot\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tresult, err := config.GetTerraformSourceURL(\"\", nil, \"config.hcl\", &config.TerragruntConfig{})\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \".\", result)\n\t})\n}\n\nfunc TestExternalEngineConfig(t *testing.T) {\n\tt.Parallel()\n\n\tversion := \"1.0.0\"\n\tengineType := \"rpc\"\n\n\tengine := &config.EngineConfig{\n\t\tSource:  \"github.com/example/engine\",\n\t\tVersion: &version,\n\t\tType:    &engineType,\n\t}\n\n\tt.Run(\"clone\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tcloned := engine.Clone()\n\t\tassert.Equal(t, engine.Source, cloned.Source)\n\t\tassert.Equal(t, *engine.Version, *cloned.Version)\n\t\tassert.Equal(t, *engine.Type, *cloned.Type)\n\t})\n\n\tt.Run(\"merge\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tbase := &config.EngineConfig{\n\t\t\tSource: \"original-source\",\n\t\t}\n\t\tnewVersion := \"2.0.0\"\n\t\toverride := &config.EngineConfig{\n\t\t\tSource:  \"new-source\",\n\t\t\tVersion: &newVersion,\n\t\t}\n\t\tbase.Merge(override)\n\t\tassert.Equal(t, \"new-source\", base.Source)\n\t\tassert.Equal(t, \"2.0.0\", *base.Version)\n\t})\n}\n\nfunc TestExternalCtyHelpers(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"GetValueString with string\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tresult, err := config.GetValueString(cty.StringVal(\"hello\"))\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, \"hello\", result)\n\t})\n\n\tt.Run(\"GetValueString with number\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tresult, err := config.GetValueString(cty.NumberIntVal(42))\n\t\trequire.NoError(t, err)\n\t\tassert.NotEmpty(t, result)\n\t})\n\n\tt.Run(\"GetFirstKey\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tm := map[string]cty.Value{\"only_key\": cty.StringVal(\"val\")}\n\t\tassert.Equal(t, \"only_key\", config.GetFirstKey(m))\n\t})\n\n\tt.Run(\"GetFirstKey empty map\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tassert.Empty(t, config.GetFirstKey(map[string]cty.Value{}))\n\t})\n\n\tt.Run(\"IsComplexType\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tassert.False(t, config.IsComplexType(cty.StringVal(\"simple\")))\n\t\tassert.False(t, config.IsComplexType(cty.NumberIntVal(1)))\n\t\tassert.True(t, config.IsComplexType(cty.ObjectVal(map[string]cty.Value{\"k\": cty.StringVal(\"v\")})))\n\t\tassert.True(t, config.IsComplexType(cty.ListVal([]cty.Value{cty.StringVal(\"a\")})))\n\t})\n\n\tt.Run(\"ConvertValuesMapToCtyVal\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvalMap := map[string]cty.Value{\n\t\t\t\"str\": cty.StringVal(\"value\"),\n\t\t\t\"num\": cty.NumberIntVal(10),\n\t\t}\n\t\tresult, err := config.ConvertValuesMapToCtyVal(valMap)\n\t\trequire.NoError(t, err)\n\t\tassert.True(t, result.Type().IsObjectType())\n\t})\n\n\tt.Run(\"ConvertValuesMapToCtyVal empty\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tresult, err := config.ConvertValuesMapToCtyVal(map[string]cty.Value{})\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, cty.EmptyObjectVal, result)\n\t})\n}\n\nfunc TestExternalTerraformOutputJSONToCtyValueMap(t *testing.T) {\n\tt.Parallel()\n\n\tjsonOutput := []byte(`{\n\t\t\"vpc_id\": {\n\t\t\t\"sensitive\": false,\n\t\t\t\"type\": \"string\",\n\t\t\t\"value\": \"vpc-abc123\"\n\t\t},\n\t\t\"instance_count\": {\n\t\t\t\"sensitive\": false,\n\t\t\t\"type\": \"number\",\n\t\t\t\"value\": 3\n\t\t}\n\t}`)\n\n\tresult, err := config.TerraformOutputJSONToCtyValueMap(\"test-config\", jsonOutput)\n\trequire.NoError(t, err)\n\tassert.Len(t, result, 2)\n\n\tvpcID := result[\"vpc_id\"]\n\tassert.Equal(t, cty.String, vpcID.Type())\n\tassert.Equal(t, \"vpc-abc123\", vpcID.AsString())\n}\n\nfunc TestExternalGetUnitDir(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"with stack dir\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tunit := &config.Unit{\n\t\t\tName:   \"app\",\n\t\t\tSource: \"./modules/app\",\n\t\t\tPath:   \"app\",\n\t\t}\n\t\tdir := config.GetUnitDir(\"/project\", unit)\n\t\tassert.Equal(t, filepath.Join(\"/project\", config.StackDir, \"app\"), dir)\n\t})\n\n\tt.Run(\"no stack dir\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tnoStack := true\n\t\tunit := &config.Unit{\n\t\t\tName:    \"app\",\n\t\t\tSource:  \"./modules/app\",\n\t\t\tPath:    \"app\",\n\t\t\tNoStack: &noStack,\n\t\t}\n\t\tdir := config.GetUnitDir(\"/project\", unit)\n\t\tassert.Equal(t, filepath.Join(\"/project\", \"app\"), dir)\n\t})\n}\n\nfunc TestExternalStackTypes(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"StackConfig\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tsc := &config.StackConfig{\n\t\t\tUnits: []*config.Unit{\n\t\t\t\t{Name: \"web\", Source: \"./web\", Path: \"web\"},\n\t\t\t},\n\t\t\tStacks: []*config.Stack{\n\t\t\t\t{Name: \"infra\", Source: \"./infra\", Path: \"infra\"},\n\t\t\t},\n\t\t}\n\t\tassert.Len(t, sc.Units, 1)\n\t\tassert.Len(t, sc.Stacks, 1)\n\t\tassert.Equal(t, \"web\", sc.Units[0].Name)\n\t\tassert.Equal(t, \"infra\", sc.Stacks[0].Name)\n\t})\n\n\tt.Run(\"Unit fields\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tnoValidation := true\n\t\tunit := &config.Unit{\n\t\t\tName:         \"db\",\n\t\t\tPath:         \"database\",\n\t\t\tNoValidation: &noValidation,\n\t\t}\n\t\tassert.Equal(t, \"db\", unit.Name)\n\t\tassert.Equal(t, \"database\", unit.Path)\n\t\tassert.True(t, *unit.NoValidation)\n\t})\n\n\tt.Run(\"Stack fields\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tnoStack := false\n\t\tstack := &config.Stack{\n\t\t\tName:    \"networking\",\n\t\t\tPath:    \"net\",\n\t\t\tNoStack: &noStack,\n\t\t}\n\t\tassert.Equal(t, \"networking\", stack.Name)\n\t\tassert.Equal(t, \"net\", stack.Path)\n\t\tassert.False(t, *stack.NoStack)\n\t})\n}\n\nfunc TestExternalHclparse(t *testing.T) {\n\tt.Parallel()\n\n\tl := createExternalLogger()\n\tparser := hclparse.NewParser(hclparse.WithLogger(l))\n\n\thclContent := `\n\t\tname = \"test\"\n\t\tcount = 42\n\t`\n\n\tfile, err := parser.ParseFromString(hclContent, \"test.hcl\")\n\trequire.NoError(t, err)\n\trequire.NotNil(t, file)\n\n\tattrs, err := file.JustAttributes()\n\trequire.NoError(t, err)\n\n\tattrNames := make([]string, 0, len(attrs))\n\tfor _, attr := range attrs {\n\t\tattrNames = append(attrNames, attr.Name)\n\t}\n\n\tassert.Contains(t, attrNames, \"name\")\n\tassert.Contains(t, attrNames, \"count\")\n}\n\nfunc TestExternalModuleDependencies(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"create and read\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tdeps := &config.ModuleDependencies{\n\t\t\tPaths: []string{\"../vpc\", \"../rds\"},\n\t\t}\n\t\tassert.Len(t, deps.Paths, 2)\n\t\tassert.Equal(t, \"../vpc\", deps.Paths[0])\n\t})\n\n\tt.Run(\"merge\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tdeps := &config.ModuleDependencies{\n\t\t\tPaths: []string{\"../vpc\"},\n\t\t}\n\t\tother := &config.ModuleDependencies{\n\t\t\tPaths: []string{\"../rds\", \"../vpc\"},\n\t\t}\n\t\tdeps.Merge(other)\n\t\tassert.Len(t, deps.Paths, 2)\n\t\tassert.Contains(t, deps.Paths, \"../vpc\")\n\t\tassert.Contains(t, deps.Paths, \"../rds\")\n\t})\n\n\tt.Run(\"merge nil\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tdeps := &config.ModuleDependencies{\n\t\t\tPaths: []string{\"../vpc\"},\n\t\t}\n\t\tdeps.Merge(nil)\n\t\tassert.Len(t, deps.Paths, 1)\n\t})\n}\n\nfunc TestExternalGetDefaultConfigPath(t *testing.T) {\n\tt.Parallel()\n\n\t// When given a non-existent directory, GetDefaultConfigPath returns a path\n\t// ending with the default config file name.\n\tresult := config.GetDefaultConfigPath(\"/some/nonexistent/path\")\n\tassert.Contains(t, result, \"terragrunt.hcl\")\n}\n\nfunc TestExternalParseAndDecodeVarFile(t *testing.T) {\n\tt.Parallel()\n\n\tl := createExternalLogger()\n\n\tvarFileContent := []byte(`\n\t\tregion = \"us-east-1\"\n\t\tenabled = true\n\t`)\n\n\tvar out map[string]any\n\n\terr := config.ParseAndDecodeVarFile(l, \"test.hcl\", varFileContent, &out)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, out, \"region\")\n\tassert.Contains(t, out, \"enabled\")\n\tassert.Equal(t, \"us-east-1\", out[\"region\"])\n\tassert.Equal(t, true, out[\"enabled\"])\n}\n\n// TestExternalParseConfigStringNoCommand validates that an external consumer can\n// parse a Terragrunt config using NewParsingContext with zero options (no\n// internal/ imports required). This previously caused a nil pointer dereference\n// when TerraformCliArgs was nil.\nfunc TestExternalParseConfigStringNoCommand(t *testing.T) {\n\tt.Parallel()\n\n\tl := createExternalLogger()\n\n\thclConfig := `\ninputs = {\n  env = \"dev\"\n}\n`\n\n\tctx := t.Context()\n\tctx, pctx := config.NewParsingContext(ctx, l)\n\tcfg, err := config.ParseConfigString(ctx, pctx, l, config.DefaultTerragruntConfigPath, hclConfig, nil)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, cfg)\n\tassert.Equal(t, \"dev\", cfg.Inputs[\"env\"])\n}\n\n// TestExternalParseStackConfigString validates that an external consumer can\n// parse a terragrunt.stack.hcl config using NewParsingContext with zero options\n// and no internal/ imports.\nfunc TestExternalParseStackConfigString(t *testing.T) {\n\tt.Parallel()\n\n\tl := createExternalLogger()\n\n\tstackHCL := `\nunit \"app\" {\n  source = \"./modules/app\"\n  path   = \"app\"\n}\n\nunit \"db\" {\n  source = \"./modules/db\"\n  path   = \"database\"\n}\n`\n\n\tctx := t.Context()\n\tctx, pctx := config.NewParsingContext(ctx, l)\n\n\tv := cty.ObjectVal(map[string]cty.Value{})\n\n\tsc, err := config.ReadStackConfigString(\n\t\tctx,\n\t\tl,\n\t\tpctx,\n\t\tconfig.DefaultStackFile,\n\t\tstackHCL,\n\t\t&v,\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, sc)\n\trequire.Len(t, sc.Units, 2)\n\tassert.Equal(t, \"app\", sc.Units[0].Name)\n\tassert.Equal(t, \"./modules/app\", sc.Units[0].Source)\n\tassert.Equal(t, \"app\", sc.Units[0].Path)\n\tassert.Equal(t, \"db\", sc.Units[1].Name)\n\tassert.Equal(t, \"database\", sc.Units[1].Path)\n}\n\n// TestExternalParseStackConfigStringNilValues validates that an external consumer can\n// parse a terragrunt.stack.hcl config using NewParsingContext with nil for values.\nfunc TestExternalParseStackConfigStringNilValues(t *testing.T) {\n\tt.Parallel()\n\n\tl := createExternalLogger()\n\n\tstackHCL := `\nunit \"app\" {\n  source = \"./modules/app\"\n  path   = \"app\"\n}\n\nunit \"db\" {\n  source = \"./modules/db\"\n  path   = \"database\"\n}\n`\n\n\tctx := t.Context()\n\tctx, pctx := config.NewParsingContext(ctx, l)\n\n\tsc, err := config.ReadStackConfigString(\n\t\tctx,\n\t\tl,\n\t\tpctx,\n\t\tconfig.DefaultStackFile,\n\t\tstackHCL,\n\t\tnil,\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, sc)\n\trequire.Len(t, sc.Units, 2)\n\tassert.Equal(t, \"app\", sc.Units[0].Name)\n\tassert.Equal(t, \"./modules/app\", sc.Units[0].Source)\n\tassert.Equal(t, \"app\", sc.Units[0].Path)\n\tassert.Equal(t, \"db\", sc.Units[1].Name)\n\tassert.Equal(t, \"database\", sc.Units[1].Path)\n}\n\n// TestExternalParseStackConfigValidValues validates that an external consumer can\n// parse a terragrunt.stack.hcl config using NewParsingContext with valid values.\nfunc TestExternalParseStackConfigStringValidValues(t *testing.T) {\n\tt.Parallel()\n\n\tl := createExternalLogger()\n\n\tstackHCL := `\nunit \"app\" {\n  source = \"./modules/app\"\n  path   = values.app_path\n}\n\nunit \"db\" {\n  source = \"./modules/db\"\n  path   = \"database\"\n}\n`\n\n\tctx := t.Context()\n\tctx, pctx := config.NewParsingContext(ctx, l)\n\n\tv := cty.ObjectVal(map[string]cty.Value{\n\t\t\"app_path\": cty.StringVal(\"foo\"),\n\t})\n\n\tsc, err := config.ReadStackConfigString(\n\t\tctx,\n\t\tl,\n\t\tpctx,\n\t\tconfig.DefaultStackFile,\n\t\tstackHCL,\n\t\t&v,\n\t)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, sc)\n\trequire.Len(t, sc.Units, 2)\n\tassert.Equal(t, \"app\", sc.Units[0].Name)\n\tassert.Equal(t, \"./modules/app\", sc.Units[0].Source)\n\tassert.Equal(t, \"foo\", sc.Units[0].Path)\n\tassert.Equal(t, \"db\", sc.Units[1].Name)\n\tassert.Equal(t, \"database\", sc.Units[1].Path)\n}\n\n// TestExternalReadValuesAndParseStackConfig validates that an external consumer\n// can read a terragrunt.values.hcl file from disk using ReadValues and feed the\n// result into ReadStackConfigString — no internal/ imports required.\nfunc TestExternalReadValuesAndParseStackConfig(t *testing.T) {\n\tt.Parallel()\n\n\tl := createExternalLogger()\n\n\t// Write a terragrunt.values.hcl file to a temp directory.\n\tdir := t.TempDir()\n\tvaluesContent := []byte(`\napp_path = \"my-app\"\nregion   = \"us-west-2\"\n`)\n\trequire.NoError(t, os.WriteFile(filepath.Join(dir, \"terragrunt.values.hcl\"), valuesContent, 0644))\n\n\tctx := t.Context()\n\tctx, pctx := config.NewParsingContext(ctx, l)\n\n\t// Read values from the file on disk.\n\tvalues, err := config.ReadValues(ctx, pctx, l, dir)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, values)\n\n\t// Parse a stack config that references the values.\n\tstackHCL := `\nunit \"app\" {\n  source = \"./modules/app\"\n  path   = values.app_path\n}\n`\n\tsc, err := config.ReadStackConfigString(ctx, l, pctx, config.DefaultStackFile, stackHCL, values)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, sc)\n\trequire.Len(t, sc.Units, 1)\n\tassert.Equal(t, \"app\", sc.Units[0].Name)\n\tassert.Equal(t, \"my-app\", sc.Units[0].Path)\n}\n\n// TestExternalReadValuesAndParseConfig validates that an external consumer can\n// parse a regular terragrunt.hcl that references values.* when a\n// terragrunt.values.hcl file sits next to it — no internal/ imports required.\n//\n// ParseConfig automatically calls ReadValues from the config file's directory,\n// so the configPath argument must point into the directory containing the\n// values file.\nfunc TestExternalReadValuesAndParseConfig(t *testing.T) {\n\tt.Parallel()\n\n\tl := createExternalLogger()\n\n\t// Write a terragrunt.values.hcl file to a temp directory.\n\tdir := t.TempDir()\n\tvaluesContent := []byte(`\nenv    = \"staging\"\nregion = \"eu-west-1\"\n`)\n\trequire.NoError(t, os.WriteFile(filepath.Join(dir, \"terragrunt.values.hcl\"), valuesContent, 0644))\n\n\tctx := t.Context()\n\tctx, pctx := config.NewParsingContext(ctx, l)\n\n\t// Use a configPath inside the temp dir so ParseConfig discovers the\n\t// adjacent terragrunt.values.hcl automatically.\n\tconfigPath := filepath.Join(dir, config.DefaultTerragruntConfigPath)\n\n\thclConfig := `\ninputs = {\n  env    = values.env\n  region = values.region\n}\n`\n\tcfg, err := config.ParseConfigString(ctx, pctx, l, configPath, hclConfig, nil)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, cfg)\n\tassert.Equal(t, \"staging\", cfg.Inputs[\"env\"])\n\tassert.Equal(t, \"eu-west-1\", cfg.Inputs[\"region\"])\n}\n"
  },
  {
    "path": "pkg/config/feature_flag.go",
    "content": "package config\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/zclconf/go-cty/cty\"\n)\n\n// FeatureFlags represents a list of feature flags.\ntype FeatureFlags []*FeatureFlag\n\n// FeatureFlag feature flags struct.\ntype FeatureFlag struct {\n\tDefault *cty.Value `cty:\"default\" hcl:\"default,attr\"`\n\tName    string     `cty:\"name\"    hcl:\",label\"`\n}\n\n// ctyFeatureFlag struct used to pass FeatureFlag to cty.Value.\ntype ctyFeatureFlag struct {\n\tValue cty.Value `cty:\"value\"`\n\tName  string    `cty:\"name\"`\n}\n\n// DeepMerge merges the source FeatureFlag into the target FeatureFlag.\nfunc (feature *FeatureFlag) DeepMerge(source *FeatureFlag) error {\n\tif source.Name != \"\" {\n\t\tfeature.Name = source.Name\n\t}\n\n\tif source.Default == nil {\n\t\tfeature.Default = source.Default\n\t} else {\n\t\tupdatedDefaults, err := deepMergeCtyMaps(*feature.Default, *source.Default)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfeature.Default = updatedDefaults\n\t}\n\n\treturn nil\n}\n\n// DeepMerge feature flags.\nfunc deepMergeFeatureBlocks(targetFeatureFlags []*FeatureFlag, sourceFeatureFlags []*FeatureFlag) ([]*FeatureFlag, error) {\n\tif sourceFeatureFlags == nil && targetFeatureFlags == nil {\n\t\treturn nil, nil\n\t}\n\n\tkeys := make([]string, 0, len(targetFeatureFlags))\n\n\tfeatureBlocks := make(map[string]*FeatureFlag)\n\n\tfor _, flag := range targetFeatureFlags {\n\t\tfeatureBlocks[flag.Name] = flag\n\t\tkeys = append(keys, flag.Name)\n\t}\n\n\tfor _, flag := range sourceFeatureFlags {\n\t\tsameKeyDep, hasSameKey := featureBlocks[flag.Name]\n\t\tif hasSameKey {\n\t\t\tsameKeyFlagPtr := sameKeyDep\n\t\t\tif err := sameKeyFlagPtr.DeepMerge(flag); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tfeatureBlocks[flag.Name] = sameKeyFlagPtr\n\t\t} else {\n\t\t\tfeatureBlocks[flag.Name] = flag\n\t\t\tkeys = append(keys, flag.Name)\n\t\t}\n\t}\n\n\tcombinedFlags := make([]*FeatureFlag, 0, len(keys))\n\tfor _, key := range keys {\n\t\tcombinedFlags = append(combinedFlags, featureBlocks[key])\n\t}\n\n\treturn combinedFlags, nil\n}\n\n// DefaultAsString returns the default value of the feature flag as a string.\nfunc (feature *FeatureFlag) DefaultAsString() (string, error) {\n\tif feature.Default == nil {\n\t\treturn \"\", nil\n\t}\n\n\tif feature.Default.Type() == cty.String {\n\t\treturn feature.Default.AsString(), nil\n\t}\n\n\treturn CtyValueAsString(*feature.Default)\n}\n\n// Convert generic flag value to cty.Value.\nfunc flagToCtyValue(name string, value any) (cty.Value, error) {\n\tctyValue, err := GoTypeToCty(value)\n\tif err != nil {\n\t\treturn cty.NilVal, err\n\t}\n\n\tctyFlag := ctyFeatureFlag{\n\t\tName:  name,\n\t\tValue: ctyValue,\n\t}\n\n\treturn GoTypeToCty(ctyFlag)\n}\n\n// Convert a flag to a cty.Value using the provided cty.Type.\nfunc flagToTypedCtyValue(name string, ctyType cty.Type, value any) (cty.Value, error) {\n\tvar flagValue = value\n\tif ctyType == cty.Bool {\n\t\t// convert value to boolean even if it is string\n\t\tparsedValue, err := strconv.ParseBool(fmt.Sprintf(\"%v\", flagValue))\n\t\tif err != nil {\n\t\t\treturn cty.NilVal, errors.WithStack(err)\n\t\t}\n\n\t\tflagValue = parsedValue\n\t}\n\n\tctyOut, err := GoTypeToCty(flagValue)\n\tif err != nil {\n\t\treturn cty.NilVal, errors.WithStack(err)\n\t}\n\n\tctyFlag := ctyFeatureFlag{\n\t\tName:  name,\n\t\tValue: ctyOut,\n\t}\n\n\treturn GoTypeToCty(ctyFlag)\n}\n"
  },
  {
    "path": "pkg/config/hclparse/attributes.go",
    "content": "package hclparse\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/hashicorp/hcl/v2/hclsyntax\"\n\t\"github.com/zclconf/go-cty/cty\"\n)\n\ntype Attributes []*Attribute\n\nfunc NewAttributes(file *File, hclAttrs hcl.Attributes) Attributes {\n\tvar attrs = make(Attributes, 0, len(hclAttrs))\n\n\tfor _, hclAttr := range hclAttrs {\n\t\tattrs = append(attrs, &Attribute{\n\t\t\tFile:      file,\n\t\t\tAttribute: hclAttr,\n\t\t})\n\t}\n\n\treturn attrs\n}\n\nfunc (attrs Attributes) ValidateIdentifier() error {\n\tfor _, attr := range attrs {\n\t\tif err := attr.ValidateIdentifier(); err != nil {\n\t\t\t// TODO: Remove lint suppression\n\t\t\treturn nil //nolint:nilerr\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (attrs Attributes) Range() hcl.Range {\n\tvar rng hcl.Range\n\n\tfor _, attr := range attrs {\n\t\trng.Filename = attr.Range.Filename\n\n\t\tif rng.Start.Line > attr.Range.Start.Line || rng.Start.Column > attr.Range.Start.Column {\n\t\t\trng.Start = attr.Range.Start\n\t\t}\n\n\t\tif rng.End.Line < attr.Range.End.Line || rng.End.Column < attr.Range.End.Column {\n\t\t\trng.End = attr.Range.End\n\t\t}\n\t}\n\n\treturn rng\n}\n\ntype Attribute struct {\n\t*File\n\t*hcl.Attribute\n}\n\nfunc (attr *Attribute) ValidateIdentifier() error {\n\tif !hclsyntax.ValidIdentifier(attr.Name) {\n\t\tdiags := hcl.Diagnostics{{\n\t\t\tSeverity: hcl.DiagError,\n\t\t\tSummary:  \"Invalid value name\",\n\t\t\tDetail:   badIdentifierDetail,\n\t\t\tSubject:  &attr.NameRange,\n\t\t}}\n\n\t\tif err := attr.HandleDiagnostics(diags); err != nil {\n\t\t\treturn errors.New(err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (attr *Attribute) Value(evalCtx *hcl.EvalContext) (cty.Value, error) {\n\tevaluatedVal, diags := attr.Expr.Value(evalCtx)\n\n\tif err := attr.HandleDiagnostics(diags); err != nil {\n\t\treturn evaluatedVal, errors.New(err)\n\t}\n\n\treturn evaluatedVal, nil\n}\n"
  },
  {
    "path": "pkg/config/hclparse/block.go",
    "content": "package hclparse\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/hashicorp/hcl/v2\"\n)\n\n// Detailed error messages in diagnostics returned by parsing locals\nconst (\n\t// A consistent detail message for all \"not a valid identifier\" diagnostics. This is exactly the same as that returned\n\t// by terraform.\n\tbadIdentifierDetail = \"A name must start with a letter and may contain only letters, digits, underscores, and dashes.\"\n)\n\ntype Block struct {\n\t*File\n\t*hcl.Block\n}\n\n// JustAttributes loads the block into name expression pairs to assist with evaluation of the attrs prior to\n// evaluating the whole config. Note that this is exactly the same as\n// terraform/configs/named_values.go:decodeLocalsBlock\nfunc (block *Block) JustAttributes() (Attributes, error) {\n\thclAttrs, diags := block.Body.JustAttributes()\n\n\tif err := block.HandleDiagnostics(diags); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tattrs := NewAttributes(block.File, hclAttrs)\n\n\tif err := attrs.ValidateIdentifier(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn attrs, nil\n}\n"
  },
  {
    "path": "pkg/config/hclparse/errors.go",
    "content": "package hclparse\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n)\n\ntype PanicWhileParsingConfigError struct {\n\tRecoveredValue any\n\tConfigFile     string\n}\n\nfunc (err PanicWhileParsingConfigError) Error() string {\n\treturn fmt.Sprintf(\"Recovering panic while parsing '%s'. Got error of type '%v': %v\", err.ConfigFile, reflect.TypeOf(err.RecoveredValue), err.RecoveredValue)\n}\n"
  },
  {
    "path": "pkg/config/hclparse/file.go",
    "content": "package hclparse\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\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\nconst (\n\t// A consistent error message for multiple catalog block in terragrunt config (which is currently not supported)\n\tmultipleBlockDetailFmt = \"Terragrunt currently does not support multiple %[1]s blocks in a single config. Consolidate to a single %[1]s block.\"\n)\n\ntype File struct {\n\t*Parser\n\t*hcl.File\n\tConfigPath string\n}\n\nfunc (file *File) Content() string {\n\treturn string(file.Bytes)\n}\n\n// Update reparses the file with the new `content`.\nfunc (file *File) Update(content []byte) error {\n\t// Since `hclparse.Parser` has a cache, we need to recreate(clone) the Parser instance without current file\n\t// to be able to parse the configuration with the same `configPath`.\n\tparser := hclparse.NewParser()\n\n\tfor configPath, copyfile := range file.Files() {\n\t\tif configPath != file.ConfigPath {\n\t\t\tparser.AddFile(configPath, copyfile)\n\t\t}\n\t}\n\n\tfile.Parser.Parser = parser\n\n\t// we need to reparse the new updated contents. This is necessarily because the blocks\n\t// returned by hclparse does not support editing, and so we have to go through hclwrite, which leads to a\n\t// different AST representation.\n\tupdatedFile, err := file.ParseFromBytes(content, file.ConfigPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfile.File = updatedFile.File\n\n\treturn nil\n}\n\n// Decode uses the HCL2 parser to decode the parsed HCL into the struct specified by out.\n//\n// Note that we take a two pass approach to support parsing include blocks without a label. Ideally we can parse include\n// blocks with and without labels in a single pass, but the HCL parser is fairly restrictive when it comes to parsing\n// blocks with labels, requiring the exact number of expected labels in the parsing step.  To handle this restriction,\n// we first see if there are any include blocks without any labels, and if there is, we modify it in the file object to\n// inject the label as \"\".\nfunc (file *File) Decode(out any, evalContext *hcl.EvalContext) (err error) {\n\tif file.fileUpdateHandlerFunc != nil {\n\t\tif err := file.fileUpdateHandlerFunc(file); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tdiags := gohcl.DecodeBody(file.Body, evalContext, out)\n\tif err := file.HandleDiagnostics(diags); err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\treturn nil\n}\n\n// Blocks takes a parsed HCL file and extracts a reference to the `name` block, if there are defined.\nfunc (file *File) Blocks(name string, isMultipleAllowed bool) ([]*Block, error) {\n\tcatalogSchema := &hcl.BodySchema{\n\t\tBlocks: []hcl.BlockHeaderSchema{\n\t\t\t{Type: name},\n\t\t},\n\t}\n\t// We use PartialContent here, because we are only interested in parsing out the catalog block.\n\tparsed, _, diags := file.Body.PartialContent(catalogSchema)\n\tif err := file.HandleDiagnostics(diags); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\textractedBlocks := []*Block{}\n\n\tfor _, block := range parsed.Blocks {\n\t\tif block.Type == name {\n\t\t\textractedBlocks = append(extractedBlocks, &Block{\n\t\t\t\tFile:  file,\n\t\t\t\tBlock: block,\n\t\t\t})\n\t\t}\n\t}\n\n\tif len(extractedBlocks) > 1 && !isMultipleAllowed {\n\t\treturn nil, errors.New(\n\t\t\t&hcl.Diagnostic{\n\t\t\t\tSeverity: hcl.DiagError,\n\t\t\t\tSummary:  fmt.Sprintf(\"Multiple %s block\", name),\n\t\t\t\tDetail:   fmt.Sprintf(multipleBlockDetailFmt, name),\n\t\t\t},\n\t\t)\n\t}\n\n\treturn extractedBlocks, nil\n}\n\nfunc (file *File) JustAttributes() (Attributes, error) {\n\thclAttrs, diags := file.Body.JustAttributes()\n\n\tif err := file.HandleDiagnostics(diags); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tattrs := NewAttributes(file, hclAttrs)\n\n\tif err := attrs.ValidateIdentifier(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn attrs, nil\n}\n\nfunc (file *File) HandleDiagnostics(diags hcl.Diagnostics) error {\n\treturn file.handleDiagnostics(file, diags)\n}\n"
  },
  {
    "path": "pkg/config/hclparse/options.go",
    "content": "package hclparse\n\nimport (\n\t\"io\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/hashicorp/hcl/v2\"\n)\n\ntype Option func(*Parser) *Parser\n\nfunc WithLogger(logger log.Logger) Option {\n\treturn func(parser *Parser) *Parser {\n\t\tparser.logger = logger\n\n\t\treturn parser\n\t}\n}\n\nfunc WithDiagnosticsWriter(writer io.Writer, disableColor bool) Option {\n\treturn func(parser *Parser) *Parser {\n\t\tdiagsWriter := parser.GetDiagnosticsWriter(writer, disableColor)\n\n\t\tparser.diagsWriterFunc = func(diags hcl.Diagnostics) error {\n\t\t\tif !diags.HasErrors() {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif err := diagsWriter.WriteDiagnostics(diags); err != nil {\n\t\t\t\treturn errors.New(err)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\n\t\treturn parser\n\t}\n}\n\n// WithFileUpdate sets the `fileUpdateHandlerFunc` func which is run before each file decoding.\nfunc WithFileUpdate(fn func(*File) error) Option {\n\treturn func(parser *Parser) *Parser {\n\t\tparser.fileUpdateHandlerFunc = fn\n\t\treturn parser\n\t}\n}\n\n// WithHaltOnErrorOnlyForBlocks configures a diagnostic error handler that runs when diagnostic errors occur.\n// If errors occur in the given `blockNames` blocks, parser returns the error to its caller, otherwise it skips the error.\nfunc WithHaltOnErrorOnlyForBlocks(blockNames []string) Option {\n\treturn func(parser *Parser) *Parser {\n\t\tparser.handleDiagnosticsFunc = appendHandleDiagnosticsFunc(parser.handleDiagnosticsFunc, func(file *File, diags hcl.Diagnostics) (hcl.Diagnostics, error) {\n\t\t\tif file == nil || !diags.HasErrors() {\n\t\t\t\treturn diags, nil\n\t\t\t}\n\n\t\t\tfor _, blockName := range blockNames {\n\t\t\t\tblocks, err := file.Blocks(blockName, true)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tfor _, block := range blocks {\n\t\t\t\t\tblockAttrs, _ := block.Body.JustAttributes()\n\n\t\t\t\t\tfor _, blokcAttr := range blockAttrs {\n\t\t\t\t\t\tfor _, diag := range diags {\n\t\t\t\t\t\t\tif diag.Context != nil && blokcAttr.Range.Overlaps(*diag.Context) {\n\t\t\t\t\t\t\t\treturn diags, 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\n\t\t\treturn nil, nil\n\t\t})\n\n\t\treturn parser\n\t}\n}\n\nfunc WithDiagnosticsHandler(fn func(file *hcl.File, diags hcl.Diagnostics) (hcl.Diagnostics, error)) Option {\n\treturn func(parser *Parser) *Parser {\n\t\tparser.handleDiagnosticsFunc = appendHandleDiagnosticsFunc(parser.handleDiagnosticsFunc, func(file *File, diags hcl.Diagnostics) (hcl.Diagnostics, error) {\n\t\t\treturn fn(file.File, diags)\n\t\t})\n\n\t\treturn parser\n\t}\n}\n\nfunc appendHandleDiagnosticsFunc(prev, next func(*File, hcl.Diagnostics) (hcl.Diagnostics, error)) func(*File, hcl.Diagnostics) (hcl.Diagnostics, error) {\n\treturn func(file *File, diags hcl.Diagnostics) (hcl.Diagnostics, error) {\n\t\tvar err error\n\n\t\tif prev != nil {\n\t\t\tif diags, err = prev(file, diags); err != nil {\n\t\t\t\treturn diags, err\n\t\t\t}\n\t\t}\n\n\t\treturn next(file, diags)\n\t}\n}\n"
  },
  {
    "path": "pkg/config/hclparse/parser.go",
    "content": "// Package hclparse provides a wrapper around the HCL2 parser to handle diagnostics and errors in a more user-friendly way.\n//\n// The package wraps `hclparse.Parser` to be able to handle diagnostic errors from one place, see `handleDiagnostics(diags hcl.Diagnostics) error` func.\n// This allows us to halt the process only when certain errors occur, such as skipping all errors not related to the `catalog` block.\npackage hclparse\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/hashicorp/hcl/v2/hclparse\"\n\t\"golang.org/x/term\"\n)\n\ntype Parser struct {\n\t*hclparse.Parser\n\tdiagsWriterFunc       func(hcl.Diagnostics) error\n\thandleDiagnosticsFunc func(*File, hcl.Diagnostics) (hcl.Diagnostics, error)\n\tfileUpdateHandlerFunc func(*File) error\n\tlogger                log.Logger\n}\n\nfunc NewParser(opts ...Option) *Parser {\n\treturn (&Parser{\n\t\tParser: hclparse.NewParser(),\n\t\tlogger: log.Default(),\n\t}).withOptions(opts...)\n}\n\nfunc (parser *Parser) withOptions(opts ...Option) *Parser {\n\tfor _, opt := range opts {\n\t\tparser = opt(parser)\n\t}\n\n\treturn parser\n}\n\nfunc (parser *Parser) ParseFromFile(configPath string) (*File, error) {\n\tcontent, err := os.ReadFile(configPath)\n\tif err != nil {\n\t\tparser.logger.Warnf(\"Error reading file %s: %v\", configPath, err)\n\n\t\treturn nil, errors.New(err)\n\t}\n\n\treturn parser.ParseFromBytes(content, configPath)\n}\n\n// ParseFromString uses the HCL2 parser to parse the given string into an HCL file body.\nfunc (parser *Parser) ParseFromString(content, configPath string) (file *File, err error) {\n\treturn parser.ParseFromBytes([]byte(content), configPath)\n}\n\nfunc (parser *Parser) ParseFromBytes(content []byte, configPath string) (file *File, err error) {\n\t// The HCL2 parser and especially cty conversions will panic in many types of errors, so we have to recover from\n\t// those panics here and convert them to normal errors\n\tdefer func() {\n\t\tif recovered := recover(); recovered != nil {\n\t\t\terr = errors.New(PanicWhileParsingConfigError{RecoveredValue: recovered, ConfigFile: configPath})\n\t\t}\n\t}()\n\n\tvar (\n\t\tdiags   hcl.Diagnostics\n\t\thclFile *hcl.File\n\t)\n\n\tswitch filepath.Ext(configPath) {\n\tcase \".json\":\n\t\thclFile, diags = parser.ParseJSON(content, configPath)\n\tdefault:\n\t\thclFile, diags = parser.ParseHCL(content, configPath)\n\t}\n\n\tfile = &File{\n\t\tParser:     parser,\n\t\tFile:       hclFile,\n\t\tConfigPath: configPath,\n\t}\n\n\tif err := parser.handleDiagnostics(file, diags); err != nil {\n\t\tparser.logger.Warnf(\"Failed to parse HCL in file %s: %v\", configPath, diags)\n\n\t\treturn nil, errors.New(diags)\n\t}\n\n\treturn file, nil\n}\n\n// GetDiagnosticsWriter returns a hcl2 parsing diagnostics emitter for the current terminal.\nfunc (parser *Parser) GetDiagnosticsWriter(writer io.Writer, disableColor bool) hcl.DiagnosticWriter {\n\ttermColor := !disableColor && term.IsTerminal(int(os.Stderr.Fd()))\n\n\ttermWidth, _, err := term.GetSize(int(os.Stdout.Fd()))\n\tif err != nil {\n\t\t// When not connected to a terminal (e.g., in CI, tests, or piped output),\n\t\t// use width 0 to disable word-wrapping. This prevents error messages from\n\t\t// being split at unpredictable positions based on path lengths, which can\n\t\t// cause issues when parsing or testing error output.\n\t\ttermWidth = 0\n\t}\n\n\treturn hcl.NewDiagnosticTextWriter(writer, parser.Files(), uint(termWidth), termColor)\n}\n\nfunc (parser *Parser) handleDiagnostics(file *File, diags hcl.Diagnostics) error {\n\tif len(diags) == 0 {\n\t\treturn nil\n\t}\n\n\tif fn := parser.handleDiagnosticsFunc; fn != nil {\n\t\tvar err error\n\t\tif diags, err = fn(file, diags); err != nil || diags == nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif fn := parser.diagsWriterFunc; fn != nil {\n\t\tif err := fn(diags); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn diags\n}\n"
  },
  {
    "path": "pkg/config/include.go",
    "content": "package config\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/codegen\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\n\t\"dario.cat/mergo\"\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/hashicorp/hcl/v2/hclwrite\"\n\n\t\"maps\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n)\n\nconst bareIncludeKey = \"\"\n\nvar fieldsCopyLocks = util.NewKeyLocks()\n\n// Parse the config of the given include, if one is specified\nfunc parseIncludedConfig(ctx context.Context, pctx *ParsingContext, l log.Logger, includedConfig *IncludeConfig) (*TerragruntConfig, error) {\n\tif includedConfig.Path == \"\" {\n\t\treturn nil, errors.New(IncludedConfigMissingPathError(pctx.TerragruntConfigPath))\n\t}\n\n\tincludePath := includedConfig.Path\n\n\tif !filepath.IsAbs(includePath) {\n\t\tincludePath = filepath.Join(filepath.Dir(pctx.TerragruntConfigPath), includePath)\n\t}\n\n\t// These condition are here to specifically handle the `run --all` command. During any `run --all` call, terragrunt\n\t// needs to first build up the dependency graph to know what order to process the modules in. We want to limit users\n\t// from creating a dependency between the dependency path for graph generation, and a module output. This is because\n\t// the outputs may not be available yet during the graph generation. E.g., consider a completely new deployment and\n\t// `terragrunt run --all apply` is called. In this case, the outputs are expected to be materialized while terragrunt\n\t// is running `apply` through the graph, but NOT when the dependency graph is first being formulated.\n\t//\n\t// To support this, we implement the following conditions for when terragrunt can fully parse the included config\n\t// (only one needs to be true):\n\t// - Included config does NOT have a dependency block.\n\t// - Terragrunt is NOT performing a partial parse (which indicates whether or not Terragrunt is building a module\n\t//   graph).\n\t//\n\t// These conditions are sufficient to avoid a situation where dependency block parsing relies on output fetching.\n\t// Note that the user does not have to have a dynamic dependency path that directly depends on dependency outputs to\n\t// cause this! For example, suppose the user has a dependency path that depends on an included input:\n\t//\n\t// include \"root\" {\n\t//   path = find_in_parent_folders(\"root.hcl\")\n\t//   expose = true\n\t// }\n\t// dependency \"dep\" {\n\t//   config_path = include.root.inputs.vpc_dir\n\t// }\n\t//\n\t// In this example, the user the vpc_dir input may not directly depend on a dependency. However, what if the root\n\t// config had other inputs that depended on a dependency? E.g.:\n\t//\n\t// inputs = {\n\t//   vpc_dir = \"../vpc\"\n\t//   vpc_id  = dependency.vpc.outputs.id\n\t// }\n\t//\n\t// In this situation, terragrunt can not parse the included inputs attribute unless it fetches the `vpc` dependency\n\t// outputs. Since the block parsing is transitive, it leads to a situation where terragrunt cannot parse the `dep`\n\t// dependency block unless the `vpc` dependency has outputs (since we can't partially parse the `inputs` attribute).\n\t// OTOH, if we know the included config has no `dependency` defined, then no matter what attribute is pulled in, we\n\t// know that the `dependency` block path will never depend on dependency outputs. Hence, we perform a full\n\t// parse of the included config in the graph generation stage only if the included config does NOT have a dependency\n\t// block, but resort to a partial parse otherwise.\n\t//\n\t// NOTE: To make the logic easier to implement, we implement the inverse here, where we check whether the included\n\t// config has a dependency block, and if we are in the middle of a partial parse, we perform a partial parse of the\n\t// included config.\n\thasDependency, err := configFileHasDependencyBlock(includePath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif hasDependency && len(pctx.PartialParseDecodeList) > 0 {\n\t\tl.Debugf(\n\t\t\t\"Included config %s can only be partially parsed during dependency graph formation for run --all command as it has a dependency block.\",\n\t\t\tincludePath,\n\t\t)\n\n\t\treturn PartialParseConfigFile(ctx, pctx, l, includePath, includedConfig)\n\t}\n\n\t// When included config has dependencies, suppress diagnostics during parsing.\n\tparseCtx := pctx\n\tif hasDependency {\n\t\tparseCtx = pctx.WithDiagnosticsSuppressed(l)\n\t}\n\n\tconfig, err := ParseConfigFile(ctx, parseCtx, l, includePath, includedConfig)\n\tif err != nil {\n\t\tvar configNotFoundError TerragruntConfigNotFoundError\n\t\tif errors.As(err, &configNotFoundError) {\n\t\t\treturn nil, IncludeConfigNotFoundError{\n\t\t\t\tIncludePath: includePath,\n\t\t\t\tSourcePath:  pctx.TerragruntConfigPath,\n\t\t\t}\n\t\t}\n\n\t\treturn nil, err\n\t}\n\n\treturn config, nil\n}\n\n// handleInclude merges the included config into the current config depending on the merge strategy specified by the\n// user.\nfunc handleInclude(ctx context.Context, pctx *ParsingContext, l log.Logger, config *TerragruntConfig, isPartial bool) (*TerragruntConfig, error) {\n\tif pctx.TrackInclude == nil {\n\t\treturn nil, errors.New(\"you reached an impossible condition. This is most likely a bug in terragrunt. Please open an issue at github.com/gruntwork-io/terragrunt with this error message.Code: HANDLE_INCLUDE_NIL_INCLUDE_CONFIG\")\n\t}\n\n\t// We merge in the include blocks in reverse order here. The expectation is that the bottom most elements override\n\t// those in earlier includes, so we need to merge bottom up instead of top down to ensure this.\n\tincludeList := pctx.TrackInclude.CurrentList\n\tbaseConfig := config\n\n\tfor i := len(includeList) - 1; i >= 0; i-- {\n\t\tincludeConfig := includeList[i]\n\n\t\tmergeStrategy, err := includeConfig.GetMergeStrategy()\n\t\tif err != nil {\n\t\t\treturn config, err\n\t\t}\n\n\t\tvar (\n\t\t\tparsedIncludeConfig *TerragruntConfig\n\t\t\tlogPrefix           string\n\t\t)\n\n\t\ttrackedIncludePath := includeConfig.Path\n\t\tif !filepath.IsAbs(trackedIncludePath) {\n\t\t\ttrackedIncludePath = filepath.Clean(filepath.Join(filepath.Dir(pctx.TerragruntConfigPath), trackedIncludePath))\n\t\t}\n\n\t\ttrackFileRead(pctx.FilesRead, trackedIncludePath)\n\n\t\tif isPartial {\n\t\t\tparsedIncludeConfig, err = partialParseIncludedConfig(ctx, pctx, l, &includeConfig)\n\t\t\tlogPrefix = \"[Partial] \"\n\t\t} else {\n\t\t\tparsedIncludeConfig, err = parseIncludedConfig(ctx, pctx, l, &includeConfig)\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn baseConfig, err\n\t\t}\n\n\t\t// TODO: Remove lint suppression\n\t\tswitch mergeStrategy { //nolint:exhaustive\n\t\tcase NoMerge:\n\t\t\tl.Debugf(\"%sIncluded config %s has strategy no merge: not merging config in.\", logPrefix, includeConfig.Path)\n\t\tcase ShallowMerge:\n\t\t\tl.Debugf(\"%sIncluded config %s has strategy shallow merge: merging config in (shallow).\", logPrefix, includeConfig.Path)\n\n\t\t\tif err := parsedIncludeConfig.Merge(l, baseConfig); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tbaseConfig = parsedIncludeConfig\n\t\tcase DeepMerge:\n\t\t\tl.Debugf(\"%sIncluded config %s has strategy deep merge: merging config in (deep).\", logPrefix, includeConfig.Path)\n\n\t\t\tif err := parsedIncludeConfig.DeepMerge(l, baseConfig); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tbaseConfig = parsedIncludeConfig\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"you reached an impossible condition. This is most likely a bug in terragrunt. Please open an issue at github.com/gruntwork-io/terragrunt with this error message. Code: UNKNOWN_MERGE_STRATEGY_%s\", mergeStrategy)\n\t\t}\n\t}\n\n\treturn baseConfig, nil\n}\n\n// handleIncludeForDependency is a partial merge of the included config to handle dependencies. This only merges the\n// dependency block configurations between the included config and the child config. This allows us to merge the two\n// dependencies prior to retrieving the outputs, allowing you to have partial configuration that is overridden by a\n// child.\nfunc handleIncludeForDependency(ctx context.Context, pctx *ParsingContext, l log.Logger, childDecodedDependency TerragruntDependency) (*TerragruntDependency, error) {\n\tif pctx.TrackInclude == nil {\n\t\treturn nil, errors.New(\"you reached an impossible condition. This is most likely a bug in terragrunt. Please open an issue at github.com/gruntwork-io/terragrunt with this error message. Code: HANDLE_INCLUDE_DEPENDENCY_NIL_INCLUDE_CONFIG\")\n\t}\n\t// We merge in the include blocks in reverse order here. The expectation is that the bottom most elements override\n\t// those in earlier includes, so we need to merge bottom up instead of top down to ensure this.\n\tincludeList := pctx.TrackInclude.CurrentList\n\tbaseDependencyBlock := childDecodedDependency.Dependencies\n\n\tfor i := len(includeList) - 1; i >= 0; i-- {\n\t\tincludeConfig := includeList[i]\n\n\t\tmergeStrategy, err := includeConfig.GetMergeStrategy()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tincludedPartialParse, err := partialParseIncludedConfig(\n\t\t\tctx, pctx.WithDecodeList(DependencyBlock, FeatureFlagsBlock, ExcludeBlock, ErrorsBlock), l, &includeConfig)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// TODO: Remove lint suppression\n\t\tswitch mergeStrategy { //nolint:exhaustive\n\t\tcase NoMerge:\n\t\t\tl.Debugf(\n\t\t\t\t\"Included config %s has strategy no merge: not merging config in for dependency.\",\n\t\t\t\tutil.RelPathForLog(\n\t\t\t\t\tpctx.RootWorkingDir,\n\t\t\t\t\tincludeConfig.Path,\n\t\t\t\t\tpctx.Writers.LogShowAbsPaths,\n\t\t\t\t),\n\t\t\t)\n\t\tcase ShallowMerge:\n\t\t\tl.Debugf(\n\t\t\t\t\"Included config %s has strategy shallow merge: merging config in (shallow) for dependency.\",\n\t\t\t\tutil.RelPathForLog(\n\t\t\t\t\tpctx.RootWorkingDir,\n\t\t\t\t\tincludeConfig.Path,\n\t\t\t\t\tpctx.Writers.LogShowAbsPaths,\n\t\t\t\t),\n\t\t\t)\n\n\t\t\tmergedDependencyBlock := mergeDependencyBlocks(includedPartialParse.TerragruntDependencies, baseDependencyBlock)\n\t\t\tbaseDependencyBlock = mergedDependencyBlock\n\t\tcase DeepMerge:\n\t\t\tl.Debugf(\n\t\t\t\t\"Included config %s has strategy deep merge: merging config in (deep) for dependency.\",\n\t\t\t\tutil.RelPathForLog(\n\t\t\t\t\tpctx.RootWorkingDir,\n\t\t\t\t\tincludeConfig.Path,\n\t\t\t\t\tpctx.Writers.LogShowAbsPaths,\n\t\t\t\t),\n\t\t\t)\n\n\t\t\tmergedDependencyBlock, err := deepMergeDependencyBlocks(includedPartialParse.TerragruntDependencies, baseDependencyBlock)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tbaseDependencyBlock = mergedDependencyBlock\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\n\t\t\t\t\"you reached an impossible condition. This is most likely a bug in terragrunt. \"+\n\t\t\t\t\t\"Please open an issue at github.com/gruntwork-io/terragrunt with this error message. \"+\n\t\t\t\t\t\"Code: UNKNOWN_MERGE_STRATEGY_%s_DEPENDENCY\",\n\t\t\t\tmergeStrategy,\n\t\t\t)\n\t\t}\n\t}\n\n\treturn &TerragruntDependency{Dependencies: baseDependencyBlock}, nil\n}\n\n// Merge performs a shallow merge of the given sourceConfig into the targetConfig. sourceConfig will override common\n// attributes defined in the targetConfig. Note that this will modify the targetConfig.\n// NOTE: the following attributes are deliberately omitted from the merge operation, as they are handled differently in\n// the parser:\n//   - locals [These blocks are not merged by design]\n//\n// NOTE: dependencies block is a special case and is merged deeply. This is necessary to ensure the configstack system\n// works correctly, as it uses the `Dependencies` list to track the dependencies of modules for graph building purposes.\n// This list includes the dependencies added from dependency blocks, which is handled in a different stage.\nfunc (cfg *TerragruntConfig) Merge(l log.Logger, sourceConfig *TerragruntConfig) error {\n\t// Merge simple attributes first\n\tif sourceConfig.DownloadDir != \"\" {\n\t\tcfg.DownloadDir = sourceConfig.DownloadDir\n\t}\n\n\tif sourceConfig.IamRole != \"\" {\n\t\tcfg.IamRole = sourceConfig.IamRole\n\t}\n\n\tif sourceConfig.IamAssumeRoleDuration != nil {\n\t\tcfg.IamAssumeRoleDuration = sourceConfig.IamAssumeRoleDuration\n\t}\n\n\tif sourceConfig.IamAssumeRoleSessionName != \"\" {\n\t\tcfg.IamAssumeRoleSessionName = sourceConfig.IamAssumeRoleSessionName\n\t}\n\n\tif sourceConfig.IamWebIdentityToken != \"\" {\n\t\tcfg.IamWebIdentityToken = sourceConfig.IamWebIdentityToken\n\t}\n\n\tif sourceConfig.TerraformVersionConstraint != \"\" {\n\t\tcfg.TerraformVersionConstraint = sourceConfig.TerraformVersionConstraint\n\t}\n\n\tif sourceConfig.TerraformBinary != \"\" {\n\t\tcfg.TerraformBinary = sourceConfig.TerraformBinary\n\t}\n\n\tif sourceConfig.PreventDestroy != nil {\n\t\tcfg.PreventDestroy = sourceConfig.PreventDestroy\n\t}\n\n\tif sourceConfig.TerragruntVersionConstraint != \"\" {\n\t\tcfg.TerragruntVersionConstraint = sourceConfig.TerragruntVersionConstraint\n\t}\n\n\tif sourceConfig.Engine != nil {\n\t\tcfg.Engine = sourceConfig.Engine.Clone()\n\t}\n\n\tif sourceConfig.Exclude != nil {\n\t\tcfg.Exclude = sourceConfig.Exclude.Clone()\n\t}\n\n\tif sourceConfig.Errors != nil {\n\t\tcfg.Errors = sourceConfig.Errors.Clone()\n\t}\n\n\tif sourceConfig.RemoteState != nil {\n\t\tcfg.RemoteState = sourceConfig.RemoteState\n\t}\n\n\tif sourceConfig.Terraform != nil {\n\t\tif cfg.Terraform == nil {\n\t\t\tcfg.Terraform = sourceConfig.Terraform\n\t\t} else {\n\t\t\tif sourceConfig.Terraform.Source != nil {\n\t\t\t\tcfg.Terraform.Source = sourceConfig.Terraform.Source\n\t\t\t}\n\n\t\t\tif sourceConfig.Terraform.CopyTerraformLockFile != nil {\n\t\t\t\tcfg.Terraform.CopyTerraformLockFile = sourceConfig.Terraform.CopyTerraformLockFile\n\t\t\t}\n\n\t\t\tmergeExtraArgs(l, sourceConfig.Terraform.ExtraArgs, &cfg.Terraform.ExtraArgs)\n\n\t\t\tmergeHooks(l, sourceConfig.Terraform.BeforeHooks, &cfg.Terraform.BeforeHooks)\n\t\t\tmergeHooks(l, sourceConfig.Terraform.AfterHooks, &cfg.Terraform.AfterHooks)\n\t\t\tmergeErrorHooks(l, sourceConfig.Terraform.ErrorHooks, &cfg.Terraform.ErrorHooks)\n\t\t}\n\t}\n\n\t// Dependency blocks are shallow merged by name\n\tcfg.TerragruntDependencies = mergeDependencyBlocks(cfg.TerragruntDependencies, sourceConfig.TerragruntDependencies)\n\n\tcfg.FeatureFlags = mergeFeatureFlags(cfg.FeatureFlags, sourceConfig.FeatureFlags)\n\n\t// Deep merge the dependencies list. This is different from dependency blocks, and refers to the deprecated\n\t// dependencies block!\n\tif sourceConfig.Dependencies != nil {\n\t\tif cfg.Dependencies == nil {\n\t\t\tcfg.Dependencies = sourceConfig.Dependencies\n\t\t} else {\n\t\t\tcfg.Dependencies.Merge(sourceConfig.Dependencies)\n\t\t}\n\t}\n\n\t// Merge the generate configs. This is a shallow merge. Meaning, if the child has the same name generate block, then the\n\t// child's generate block will override the parent's block.\n\n\terr := validateGenerateConfigs(&sourceConfig.GenerateConfigs, &cfg.GenerateConfigs)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmaps.Copy(cfg.GenerateConfigs, sourceConfig.GenerateConfigs)\n\n\tif sourceConfig.Inputs != nil {\n\t\tcfg.Inputs = mergeInputs(sourceConfig.Inputs, cfg.Inputs)\n\t}\n\n\tCopyFieldsMetadata(sourceConfig, cfg)\n\n\treturn nil\n}\n\n// DeepMerge performs a deep merge of the given sourceConfig into the targetConfig. Deep merge is defined as follows:\n//   - For simple types, the source overrides the target.\n//   - For lists, the two attribute lists are combined together in concatenation.\n//   - For maps, the two maps are combined together recursively. That is, if the map keys overlap, then a deep merge is\n//     performed on the map value.\n//   - Note that some structs are not deep mergeable due to an implementation detail. This will change in the future. The\n//     following structs have this limitation:\n//   - remote_state\n//   - generate\n//   - Note that the following attributes are deliberately omitted from the merge operation, as they are handled\n//     differently in the parser:\n//   - dependency blocks (TerragruntDependencies) [These blocks need to retrieve outputs, so we need to merge during\n//     the parsing step, not after the full config is decoded]\n//   - locals [These blocks are not merged by design]\nfunc (cfg *TerragruntConfig) DeepMerge(l log.Logger, sourceConfig *TerragruntConfig) error {\n\t// Merge simple attributes first\n\tif sourceConfig.DownloadDir != \"\" {\n\t\tcfg.DownloadDir = sourceConfig.DownloadDir\n\t}\n\n\tif sourceConfig.IamRole != \"\" {\n\t\tcfg.IamRole = sourceConfig.IamRole\n\t}\n\n\tif sourceConfig.IamAssumeRoleDuration != nil {\n\t\tcfg.IamAssumeRoleDuration = sourceConfig.IamAssumeRoleDuration\n\t}\n\n\tif sourceConfig.IamAssumeRoleSessionName != \"\" {\n\t\tcfg.IamAssumeRoleSessionName = sourceConfig.IamAssumeRoleSessionName\n\t}\n\n\tif sourceConfig.IamWebIdentityToken != \"\" {\n\t\tcfg.IamWebIdentityToken = sourceConfig.IamWebIdentityToken\n\t}\n\n\tif sourceConfig.TerraformVersionConstraint != \"\" {\n\t\tcfg.TerraformVersionConstraint = sourceConfig.TerraformVersionConstraint\n\t}\n\n\tif sourceConfig.TerraformBinary != \"\" {\n\t\tcfg.TerraformBinary = sourceConfig.TerraformBinary\n\t}\n\n\tif sourceConfig.PreventDestroy != nil {\n\t\tcfg.PreventDestroy = sourceConfig.PreventDestroy\n\t}\n\n\tif sourceConfig.TerragruntVersionConstraint != \"\" {\n\t\tcfg.TerragruntVersionConstraint = sourceConfig.TerragruntVersionConstraint\n\t}\n\n\tif sourceConfig.Engine != nil {\n\t\tif cfg.Engine == nil {\n\t\t\tcfg.Engine = &EngineConfig{}\n\t\t}\n\n\t\tcfg.Engine.Merge(sourceConfig.Engine)\n\t}\n\n\tif sourceConfig.Exclude != nil {\n\t\tif cfg.Exclude == nil {\n\t\t\tcfg.Exclude = &ExcludeConfig{}\n\t\t}\n\n\t\tcfg.Exclude.Merge(sourceConfig.Exclude)\n\t}\n\n\tif sourceConfig.Errors != nil {\n\t\tif cfg.Errors == nil {\n\t\t\tcfg.Errors = &ErrorsConfig{}\n\t\t}\n\n\t\tcfg.Errors.Merge(sourceConfig.Errors)\n\t}\n\n\t// Copy only dependencies which doesn't exist in source\n\tif sourceConfig.Dependencies != nil {\n\t\tresultModuleDependencies := &ModuleDependencies{}\n\n\t\tif cfg.Dependencies != nil {\n\t\t\t// take in result dependencies only paths which aren't defined in source\n\t\t\t// Fix for issue: https://github.com/gruntwork-io/terragrunt/issues/1900\n\t\t\ttargetPathMap := fetchDependencyPaths(cfg)\n\t\t\tsourcePathMap := fetchDependencyPaths(sourceConfig)\n\n\t\t\tfor key, value := range targetPathMap {\n\t\t\t\t_, found := sourcePathMap[key]\n\t\t\t\tif !found {\n\t\t\t\t\tresultModuleDependencies.Paths = append(resultModuleDependencies.Paths, value)\n\t\t\t\t}\n\t\t\t}\n\t\t\t// copy target paths which are defined only in Dependencies and not in TerragruntDependencies\n\t\t\t// if TerragruntDependencies will be empty, all targetConfig.Dependencies.Paths will be copied to resultModuleDependencies.Paths\n\t\t\tfor _, dependencyPath := range cfg.Dependencies.Paths {\n\t\t\t\tvar addPath = true\n\n\t\t\t\tfor _, targetPath := range targetPathMap {\n\t\t\t\t\tif dependencyPath == targetPath { // path already defined in TerragruntDependencies, skip adding\n\t\t\t\t\t\taddPath = false\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif addPath {\n\t\t\t\t\tresultModuleDependencies.Paths = append(resultModuleDependencies.Paths, dependencyPath)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tresultModuleDependencies.Paths = append(resultModuleDependencies.Paths, sourceConfig.Dependencies.Paths...)\n\t\tcfg.Dependencies = resultModuleDependencies\n\t}\n\n\t// Dependency blocks are deep merged by name\n\tmergedDeps, err := deepMergeDependencyBlocks(cfg.TerragruntDependencies, sourceConfig.TerragruntDependencies)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcfg.TerragruntDependencies = mergedDeps\n\n\tmergedFlags, err := deepMergeFeatureBlocks(cfg.FeatureFlags, sourceConfig.FeatureFlags)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcfg.FeatureFlags = mergedFlags\n\n\t// Handle complex structs by recursively merging the structs together\n\tif sourceConfig.Terraform != nil {\n\t\tif cfg.Terraform == nil {\n\t\t\tcfg.Terraform = sourceConfig.Terraform\n\t\t} else {\n\t\t\tif sourceConfig.Terraform.Source != nil {\n\t\t\t\tcfg.Terraform.Source = sourceConfig.Terraform.Source\n\t\t\t}\n\n\t\t\tif sourceConfig.Terraform.CopyTerraformLockFile != nil {\n\t\t\t\tcfg.Terraform.CopyTerraformLockFile = sourceConfig.Terraform.CopyTerraformLockFile\n\t\t\t}\n\n\t\t\tif sourceConfig.Terraform.IncludeInCopy != nil {\n\t\t\t\tsrcList := *sourceConfig.Terraform.IncludeInCopy\n\n\t\t\t\tif cfg.Terraform.IncludeInCopy != nil {\n\t\t\t\t\ttargetList := *cfg.Terraform.IncludeInCopy\n\t\t\t\t\tcombinedList := slices.Concat(srcList, targetList)\n\t\t\t\t\tcfg.Terraform.IncludeInCopy = &combinedList\n\t\t\t\t} else {\n\t\t\t\t\tcfg.Terraform.IncludeInCopy = &srcList\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif sourceConfig.Terraform.ExcludeFromCopy != nil {\n\t\t\t\tsrcList := *sourceConfig.Terraform.ExcludeFromCopy\n\n\t\t\t\tif cfg.Terraform.ExcludeFromCopy != nil {\n\t\t\t\t\ttargetList := *cfg.Terraform.ExcludeFromCopy\n\t\t\t\t\tcombinedList := slices.Concat(srcList, targetList)\n\t\t\t\t\tcfg.Terraform.ExcludeFromCopy = &combinedList\n\t\t\t\t} else {\n\t\t\t\t\tcfg.Terraform.ExcludeFromCopy = &srcList\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmergeExtraArgs(l, sourceConfig.Terraform.ExtraArgs, &cfg.Terraform.ExtraArgs)\n\n\t\t\tmergeHooks(l, sourceConfig.Terraform.BeforeHooks, &cfg.Terraform.BeforeHooks)\n\t\t\tmergeHooks(l, sourceConfig.Terraform.AfterHooks, &cfg.Terraform.AfterHooks)\n\t\t\tmergeErrorHooks(l, sourceConfig.Terraform.ErrorHooks, &cfg.Terraform.ErrorHooks)\n\t\t}\n\t}\n\n\tif sourceConfig.Inputs != nil {\n\t\tmergedInputs, err := deepMergeInputs(sourceConfig.Inputs, cfg.Inputs)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcfg.Inputs = mergedInputs\n\t}\n\n\t// MAINTAINER'S NOTE: The following structs cannot be deep merged due to an implementation detail (they do not\n\t// support nil attributes, so we can't determine if an attribute was intentionally set, or was defaulted from\n\t// unspecified - this is especially problematic for bool attributes).\n\tif sourceConfig.RemoteState != nil {\n\t\tcfg.RemoteState = sourceConfig.RemoteState\n\t}\n\n\tmaps.Copy(cfg.GenerateConfigs, sourceConfig.GenerateConfigs)\n\n\tCopyFieldsMetadata(sourceConfig, cfg)\n\n\treturn nil\n}\n\n// fetchDependencyPaths - return from configuration map with dependency_name: path\nfunc fetchDependencyPaths(config *TerragruntConfig) map[string]string {\n\tvar m = make(map[string]string)\n\tif config == nil {\n\t\treturn m\n\t}\n\n\tfor _, dependency := range config.TerragruntDependencies {\n\t\tm[dependency.Name] = dependency.ConfigPath.AsString()\n\t}\n\n\treturn m\n}\n\n// merge feature flags by name.\nfunc mergeFeatureFlags(targetFlags []*FeatureFlag, sourceFlags []*FeatureFlag) []*FeatureFlag {\n\tif sourceFlags == nil && targetFlags == nil {\n\t\treturn nil\n\t}\n\n\tkeys := make([]string, 0, len(targetFlags))\n\n\tflagBlocks := make(map[string]*FeatureFlag)\n\tfor _, flags := range targetFlags {\n\t\tflagBlocks[flags.Name] = flags\n\t\tkeys = append(keys, flags.Name)\n\t}\n\n\tfor _, dep := range sourceFlags {\n\t\t_, hasSameKey := flagBlocks[dep.Name]\n\t\tif !hasSameKey {\n\t\t\tkeys = append(keys, dep.Name)\n\t\t}\n\n\t\tflagBlocks[dep.Name] = dep\n\t}\n\n\tcombinedFlags := make([]*FeatureFlag, 0, len(keys))\n\n\tfor _, key := range keys {\n\t\tcombinedFlags = append(combinedFlags, flagBlocks[key])\n\t}\n\n\treturn combinedFlags\n}\n\n// Merge dependency blocks shallowly. If the source list has the same name as the target, it will override the\n// dependency block in the target. Otherwise, the blocks are appended.\nfunc mergeDependencyBlocks(targetDependencies []Dependency, sourceDependencies []Dependency) []Dependency {\n\t// We track the keys so that the dependencies are added in order, with those in target prepending those in\n\t// source. This is not strictly necessary, but it makes testing easier by making the output list more\n\t// predictable.\n\tkeys := []string{}\n\n\tdependencyBlocks := make(map[string]Dependency)\n\tfor _, dep := range targetDependencies {\n\t\tdependencyBlocks[dep.Name] = dep\n\t\tkeys = append(keys, dep.Name)\n\t}\n\n\tfor _, dep := range sourceDependencies {\n\t\t_, hasSameKey := dependencyBlocks[dep.Name]\n\t\tif !hasSameKey {\n\t\t\tkeys = append(keys, dep.Name)\n\t\t}\n\t\t// Regardless of what is in dependencyBlocks, we will always override the key with source\n\t\tdependencyBlocks[dep.Name] = dep\n\t}\n\t// Now convert the map to list and set target\n\tcombinedDeps := make([]Dependency, 0, len(keys))\n\tfor _, key := range keys {\n\t\tcombinedDeps = append(combinedDeps, dependencyBlocks[key])\n\t}\n\n\treturn combinedDeps\n}\n\n// Merge dependency blocks deeply. This works almost the same as mergeDependencyBlocks, except it will recursively merge\n// attributes of the dependency struct if they share the same name.\nfunc deepMergeDependencyBlocks(targetDependencies []Dependency, sourceDependencies []Dependency) ([]Dependency, error) {\n\t// We track the keys so that the dependencies are added in order, with those in target prepending those in\n\t// source. This is not strictly necessary, but it makes testing easier by making the output list more\n\t// predictable.\n\tkeys := []string{}\n\n\tdependencyBlocks := make(map[string]Dependency)\n\tfor _, dep := range targetDependencies {\n\t\tdependencyBlocks[dep.Name] = dep\n\t\tkeys = append(keys, dep.Name)\n\t}\n\n\tfor _, dep := range sourceDependencies {\n\t\tsameKeyDep, hasSameKey := dependencyBlocks[dep.Name]\n\t\tif hasSameKey {\n\t\t\tsameKeyDepPtr := &sameKeyDep\n\t\t\tif err := sameKeyDepPtr.DeepMerge(&dep); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tdependencyBlocks[dep.Name] = *sameKeyDepPtr\n\t\t} else {\n\t\t\tdependencyBlocks[dep.Name] = dep\n\t\t\tkeys = append(keys, dep.Name)\n\t\t}\n\t}\n\n\t// Now convert the map to list and set target\n\tcombinedDeps := make([]Dependency, 0, len(keys))\n\tfor _, key := range keys {\n\t\tcombinedDeps = append(combinedDeps, dependencyBlocks[key])\n\t}\n\n\treturn combinedDeps, nil\n}\n\n// Merge the extra arguments.\n//\n// If a child's extra_arguments has the same name a parent's extra_arguments,\n// then the child's extra_arguments will be selected (and the parent's ignored)\n// If a child's extra_arguments has a different name from all of the parent's extra_arguments,\n// then the child's extra_arguments will be added to the end  of the parents.\n// Therefore, terragrunt will put the child extra_arguments after the parent's\n// extra_arguments on the terraform cli.\n// Therefore, if .tfvar files from both the parent and child contain a variable\n// with the same name, the value from the child will win.\nfunc mergeExtraArgs(l log.Logger, childExtraArgs []TerraformExtraArguments, parentExtraArgs *[]TerraformExtraArguments) {\n\tresult := *parentExtraArgs\n\tfor _, child := range childExtraArgs {\n\t\tparentExtraArgsWithSameName := getIndexOfExtraArgsWithName(result, child.Name)\n\t\tif parentExtraArgsWithSameName != -1 {\n\t\t\t// If the parent contains an extra_arguments with the same name as the child,\n\t\t\t// then override the parent's extra_arguments with the child's.\n\t\t\tl.Debugf(\"extra_arguments '%v' from child overriding parent\", child.Name)\n\t\t\tresult[parentExtraArgsWithSameName] = child\n\t\t} else {\n\t\t\t// If the parent does not contain an extra_arguments with the same name as the child\n\t\t\t// then add the child to the end.\n\t\t\t// This ensures the child extra_arguments are added to the command line after the parent extra_arguments.\n\t\t\tresult = append(result, child)\n\t\t}\n\t}\n\n\t*parentExtraArgs = result\n}\n\nfunc mergeInputs(childInputs map[string]any, parentInputs map[string]any) map[string]any {\n\tout := map[string]any{}\n\n\tmaps.Copy(out, parentInputs)\n\n\tmaps.Copy(out, childInputs)\n\n\treturn out\n}\n\nfunc deepMergeInputs(childInputs map[string]any, parentInputs map[string]any) (map[string]any, error) {\n\tout := map[string]any{}\n\tmaps.Copy(out, parentInputs)\n\n\terr := mergo.Merge(&out, childInputs, mergo.WithAppendSlice, mergo.WithOverride)\n\n\treturn out, errors.New(err)\n}\n\n// Merge the hooks (before_hook and after_hook).\n//\n// If a child's hook (before_hook or after_hook) has the same name a parent's hook,\n// then the child's hook will be selected (and the parent's ignored)\n// If a child's hook has a different name from all of the parent's hooks,\n// then the child's hook will be added to the end of the parent's.\n// Therefore, the child with the same name overrides the parent\nfunc mergeHooks(l log.Logger, childHooks []Hook, parentHooks *[]Hook) {\n\tresult := *parentHooks\n\tfor _, child := range childHooks {\n\t\tparentHookWithSameName := getIndexOfHookWithName(result, child.Name)\n\t\tif parentHookWithSameName != -1 {\n\t\t\t// If the parent contains a hook with the same name as the child,\n\t\t\t// then override the parent's hook with the child's.\n\t\t\tl.Debugf(\"hook '%v' from child overriding parent\", child.Name)\n\t\t\tresult[parentHookWithSameName] = child\n\t\t} else {\n\t\t\t// If the parent does not contain a hook with the same name as the child\n\t\t\t// then add the child to the end.\n\t\t\tresult = append(result, child)\n\t\t}\n\t}\n\n\t*parentHooks = result\n}\n\n// Merge the error hooks (error_hook).\n// Does the same thing as mergeHooks but for error hooks\n// TODO: Figure out more DRY way to do this\nfunc mergeErrorHooks(l log.Logger, childHooks []ErrorHook, parentHooks *[]ErrorHook) {\n\tresult := *parentHooks\n\tfor _, child := range childHooks {\n\t\tparentHookWithSameName := getIndexOfErrorHookWithName(result, child.Name)\n\t\tif parentHookWithSameName != -1 {\n\t\t\t// If the parent contains a hook with the same name as the child,\n\t\t\t// then override the parent's hook with the child's.\n\t\t\tl.Debugf(\"hook '%v' from child overriding parent\", child.Name)\n\t\t\tresult[parentHookWithSameName] = child\n\t\t} else {\n\t\t\t// If the parent does not contain a hook with the same name as the child\n\t\t\t// then add the child to the end.\n\t\t\tresult = append(result, child)\n\t\t}\n\t}\n\n\t*parentHooks = result\n}\n\n// getTrackInclude converts the terragrunt include blocks into TrackInclude structs that differentiate between an\n// included config in the current parsing ctx, and an included config that was passed through from a previous\n// parsing ctx.\nfunc getTrackInclude(ctx *ParsingContext, terragruntIncludeList IncludeConfigs, includeFromChild *IncludeConfig) (*TrackInclude, error) {\n\tincludedPaths := make([]string, 0, len(terragruntIncludeList))\n\tterragruntIncludeMap := make(map[string]IncludeConfig, len(terragruntIncludeList))\n\n\tfor _, tgInc := range terragruntIncludeList {\n\t\tincludedPaths = append(includedPaths, tgInc.Path)\n\t\tterragruntIncludeMap[tgInc.Name] = tgInc\n\t}\n\n\thasInclude := len(terragruntIncludeList) > 0\n\n\ttrackInc := TrackInclude{\n\t\tCurrentList: terragruntIncludeList,\n\t\tCurrentMap:  terragruntIncludeMap,\n\t}\n\n\tswitch {\n\tcase hasInclude && includeFromChild != nil:\n\t\t// tgInc appears in a parent that is already included, which means a nested include block. This is not\n\t\t// something we currently support.\n\t\terr := errors.New(TooManyLevelsOfInheritanceError{\n\t\t\tConfigPath:             ctx.TerragruntConfigPath,\n\t\t\tFirstLevelIncludePath:  includeFromChild.Path,\n\t\t\tSecondLevelIncludePath: strings.Join(includedPaths, \",\"),\n\t\t})\n\n\t\treturn &TrackInclude{}, err\n\tcase hasInclude && includeFromChild == nil:\n\t\t// Current parsing ctx where there is no included config already loaded.\n\tcase !hasInclude:\n\t\t// Parsing ctx where there is an included config already loaded.\n\t\ttrackInc.Original = includeFromChild\n\t}\n\n\treturn &trackInc, nil\n}\n\n// updateBareIncludeBlock searches the parsed terragrunt contents for a bare include block (include without a label),\n// and convert it to one with empty string as the label. This is necessary because the hcl parser is strictly enforces\n// label counts when parsing out labels with a go struct.\n//\n// Returns the updated contents, a boolean indicated whether anything changed, and an error (if any).\nfunc updateBareIncludeBlock(file *hclparse.File) error {\n\t// To save us from doing a lot of extra work, first going to check to see if the file has a naked include, first.\n\t// If it doesn't, we aren't going to bother fully parsing the file.\n\tif !detectBareIncludeUsage(file) {\n\t\treturn nil\n\t}\n\n\tvar (\n\t\tcodeWasUpdated bool\n\t\tcontent        []byte\n\t\terr            error\n\t)\n\n\tswitch filepath.Ext(file.ConfigPath) {\n\tcase \".json\":\n\t\tcontent, codeWasUpdated, err = updateBareIncludeBlockJSON(file.Bytes)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\tdefault:\n\t\thclFile, diags := hclwrite.ParseConfig(file.Bytes, file.ConfigPath, hcl.InitialPos)\n\t\tif diags.HasErrors() {\n\t\t\treturn errors.New(diags)\n\t\t}\n\n\t\tfor _, block := range hclFile.Body().Blocks() {\n\t\t\tif block.Type() == MetadataInclude && len(block.Labels()) == 0 {\n\t\t\t\tif codeWasUpdated {\n\t\t\t\t\treturn errors.New(MultipleBareIncludeBlocksErr{})\n\t\t\t\t}\n\n\t\t\t\tblock.SetLabels([]string{bareIncludeKey})\n\n\t\t\t\tcodeWasUpdated = true\n\t\t\t}\n\t\t}\n\n\t\tcontent = hclFile.Bytes()\n\t}\n\n\tif !codeWasUpdated {\n\t\treturn nil\n\t}\n\n\treturn file.Update(content)\n}\n\n// updateBareIncludeBlockJSON implements the logic for updateBareIncludeBlock when the terragrunt.hcl configuration is\n// encoded in json. The json version of this function is fairly complex due to the flexibility in how the blocks are\n// encoded. That is, all of the following are valid encodings of a terragrunt.hcl.json file that has a bare include\n// block:\n//\n// Case 1: a single include block as top level:\n//\n//\t{\n//\t  \"include\": {\n//\t    \"path\": \"foo\"\n//\t  }\n//\t}\n//\n// Case 2: a single include block in list:\n//\n//\t{\n//\t  \"include\": [\n//\t    {\"path\": \"foo\"}\n//\t  ]\n//\t}\n//\n// Case 3: mixed bare and labeled include block as list:\n//\n//\t{\n//\t  \"include\": [\n//\t    {\"path\": \"foo\"},\n//\t    {\n//\t      \"labeled\": {\"path\": \"bar\"}\n//\t    }\n//\t  ]\n//\t}\n//\n// For simplicity of implementation, we focus on handling Case 1 and 2, and ignore Case 3. If we see Case 3, we will\n// error out. Instead, the user should handle this case explicitly using the object encoding instead of list encoding:\n//\n//\t{\n//\t  \"include\": {\n//\t    \"\": {\"path\": \"foo\"},\n//\t    \"labeled\": {\"path\": \"bar\"}\n//\t  }\n//\t}\n//\n// If the multiple include blocks are encoded in this way in the json configuration, nothing needs to be done by this\n// function.\nfunc updateBareIncludeBlockJSON(fileBytes []byte) ([]byte, bool, error) {\n\tvar parsed map[string]any\n\tif err := json.Unmarshal(fileBytes, &parsed); err != nil {\n\t\treturn nil, false, errors.New(err)\n\t}\n\n\tincludeBlock, hasKey := parsed[MetadataInclude]\n\tif !hasKey {\n\t\t// No include block, so don't do anything\n\t\treturn fileBytes, false, nil\n\t}\n\n\tswitch typed := includeBlock.(type) {\n\tcase []any:\n\t\tif len(typed) == 0 {\n\t\t\t// No include block, so don't do anything\n\t\t\treturn nil, false, nil\n\t\t} else if len(typed) > 1 {\n\t\t\t// Could be multiple bare includes, or Case 3. We simplify the handling of this case by erroring out,\n\t\t\t// ignoring the possibility of Case 3, which, while valid HCL encoding, is too complex to detect and handle\n\t\t\t// here. Instead we will recommend users use the object encoding.\n\t\t\treturn nil, false, errors.New(MultipleBareIncludeBlocksErr{})\n\t\t}\n\n\t\t// Make sure this is Case 2, and not Case 3 with a single labeled block. If Case 2, update to inject the labeled\n\t\t// version. Otherwise, return without modifying.\n\t\tsingleBlock := typed[0]\n\t\tif jsonIsIncludeBlock(singleBlock) {\n\t\t\treturn updateSingleBareIncludeInParsedJSON(parsed, singleBlock)\n\t\t}\n\n\t\treturn nil, false, nil\n\tcase map[string]any:\n\t\tif len(typed) == 0 {\n\t\t\t// No include block, so don't do anything\n\t\t\treturn nil, false, nil\n\t\t}\n\n\t\t// We will only update the include block if we detect the object to represent an include block. Otherwise, the\n\t\t// blocks are labeled so we can pass forward to the tg parser step.\n\t\tif jsonIsIncludeBlock(typed) {\n\t\t\treturn updateSingleBareIncludeInParsedJSON(parsed, typed)\n\t\t}\n\n\t\treturn nil, false, nil\n\t}\n\n\treturn nil, false, errors.New(IncludeIsNotABlockErr{parsed: includeBlock})\n}\n\n// updateSingleBareIncludeInParsedJSON replaces the include attribute into a block with the label \"\" in the json. Note that we\n// can directly assign to the map with the single \"\" key without worrying about the possibility of other include blocks\n// since we will only call this function if there is only one include block, and that is a bare block with no labels.\nfunc updateSingleBareIncludeInParsedJSON(parsed map[string]any, newVal any) ([]byte, bool, error) {\n\tparsed[MetadataInclude] = map[string]any{bareIncludeKey: newVal}\n\tupdatedBytes, err := json.Marshal(parsed)\n\n\treturn updatedBytes, true, errors.New(err)\n}\n\n// jsonIsIncludeBlock checks if the arbitrary json data is the include block. The data is determined to be an include\n// block if:\n// - It is an object\n// - Has the 'path' attribute\n// - The 'path' attribute is a string\nfunc jsonIsIncludeBlock(jsonData any) bool {\n\ttyped, isMap := jsonData.(map[string]any)\n\tif isMap {\n\t\tpathAttr, hasPath := typed[\"path\"]\n\t\tif hasPath {\n\t\t\t_, pathIsString := pathAttr.(string)\n\t\t\treturn pathIsString\n\t\t}\n\t}\n\n\treturn false\n}\n\n// CopyFieldsMetadata Copy fields metadata between TerragruntConfig instances.\nfunc CopyFieldsMetadata(sourceConfig *TerragruntConfig, targetConfig *TerragruntConfig) {\n\tfieldsCopyLocks.Lock(targetConfig.DownloadDir)\n\tdefer fieldsCopyLocks.Unlock(targetConfig.DownloadDir)\n\n\tif sourceConfig.FieldsMetadata != nil {\n\t\tif targetConfig.FieldsMetadata == nil {\n\t\t\ttargetConfig.FieldsMetadata = map[string]map[string]any{}\n\t\t}\n\n\t\tmaps.Copy(targetConfig.FieldsMetadata, sourceConfig.FieldsMetadata)\n\t}\n}\n\n// validateGenerateConfigs Validate if exists duplicate generate configs.\nfunc validateGenerateConfigs(sourceConfig *map[string]codegen.GenerateConfig, targetConfig *map[string]codegen.GenerateConfig) error {\n\tvar duplicatedNames []string\n\n\tfor key := range *targetConfig {\n\t\tif _, found := (*sourceConfig)[key]; found {\n\t\t\tduplicatedNames = append(duplicatedNames, key)\n\t\t}\n\t}\n\n\tif len(duplicatedNames) != 0 {\n\t\treturn DuplicatedGenerateBlocksError{duplicatedNames}\n\t}\n\n\treturn nil\n}\n\n// Custom error types\n\ntype MultipleBareIncludeBlocksErr struct{}\n\nfunc (err MultipleBareIncludeBlocksErr) Error() string {\n\treturn \"Multiple bare include blocks (include blocks without label) is not supported.\"\n}\n\ntype IncludeIsNotABlockErr struct {\n\tparsed any\n}\n\nfunc (err IncludeIsNotABlockErr) Error() string {\n\treturn fmt.Sprintf(\"Parsed include is not a block: %v\", err.parsed)\n}\n"
  },
  {
    "path": "pkg/config/include_test.go",
    "content": "package config_test\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/zclconf/go-cty/cty\"\n)\n\nfunc TestMergeConfigIntoIncludedConfig(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tconfig         *config.TerragruntConfig\n\t\tincludedConfig *config.TerragruntConfig\n\t\texpected       *config.TerragruntConfig\n\t}{\n\t\t{\n\t\t\t&config.TerragruntConfig{},\n\t\t\t&config.TerragruntConfig{},\n\t\t\t&config.TerragruntConfig{},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{Source: ptr(\"foo\")}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{Source: ptr(\"foo\")}},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{},\n\t\t\t&config.TerragruntConfig{RemoteState: remotestate.New(&remotestate.Config{BackendName: \"bar\"}), Terraform: &config.TerraformConfig{Source: ptr(\"foo\")}},\n\t\t\t&config.TerragruntConfig{RemoteState: remotestate.New(&remotestate.Config{BackendName: \"bar\"}), Terraform: &config.TerraformConfig{Source: ptr(\"foo\")}},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{RemoteState: remotestate.New(&remotestate.Config{BackendName: \"foo\"}), Terraform: &config.TerraformConfig{Source: ptr(\"foo\")}},\n\t\t\t&config.TerragruntConfig{RemoteState: remotestate.New(&remotestate.Config{BackendName: \"bar\"}), Terraform: &config.TerraformConfig{Source: ptr(\"foo\")}},\n\t\t\t&config.TerragruntConfig{RemoteState: remotestate.New(&remotestate.Config{BackendName: \"foo\"}), Terraform: &config.TerraformConfig{Source: ptr(\"foo\")}},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{Source: ptr(\"foo\")}},\n\t\t\t&config.TerragruntConfig{RemoteState: remotestate.New(&remotestate.Config{BackendName: \"bar\"}), Terraform: &config.TerraformConfig{Source: ptr(\"foo\")}},\n\t\t\t&config.TerragruntConfig{RemoteState: remotestate.New(&remotestate.Config{BackendName: \"bar\"}), Terraform: &config.TerraformConfig{Source: ptr(\"foo\")}},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{ExtraArgs: []config.TerraformExtraArguments{{Name: \"childArgs\"}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{ExtraArgs: []config.TerraformExtraArguments{{Name: \"childArgs\"}}}},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{ExtraArgs: []config.TerraformExtraArguments{{Name: \"childArgs\"}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{ExtraArgs: []config.TerraformExtraArguments{{Name: \"parentArgs\"}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{ExtraArgs: []config.TerraformExtraArguments{{Name: \"parentArgs\"}, {Name: \"childArgs\"}}}},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{ExtraArgs: []config.TerraformExtraArguments{{Name: \"overrideArgs\", Arguments: &[]string{\"-child\"}}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{ExtraArgs: []config.TerraformExtraArguments{{Name: \"overrideArgs\", Arguments: &[]string{\"-parent\"}}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{ExtraArgs: []config.TerraformExtraArguments{{Name: \"overrideArgs\", Arguments: &[]string{\"-child\"}}}}},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{BeforeHooks: []config.Hook{{Name: \"childHooks\"}}}},\n\t\t\t&config.TerragruntConfig{Terraform: nil},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{BeforeHooks: []config.Hook{{Name: \"childHooks\"}}}},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{Terraform: nil},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{BeforeHooks: []config.Hook{{Name: \"parentHooks\"}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{BeforeHooks: []config.Hook{{Name: \"parentHooks\"}}}},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{BeforeHooks: []config.Hook{{Name: \"childHooks\"}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{BeforeHooks: []config.Hook{{Name: \"childHooks\"}}}},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{BeforeHooks: []config.Hook{{Name: \"childHooks\"}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{BeforeHooks: []config.Hook{{Name: \"parentHooks\"}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{BeforeHooks: []config.Hook{{Name: \"parentHooks\"}, {Name: \"childHooks\"}}}},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{BeforeHooks: []config.Hook{{Name: \"overrideHooks\", Commands: []string{\"child-apply\"}}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{BeforeHooks: []config.Hook{{Name: \"overrideHooks\", Commands: []string{\"parent-apply\"}}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{BeforeHooks: []config.Hook{{Name: \"overrideHooks\", Commands: []string{\"child-apply\"}}}}},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{AfterHooks: []config.Hook{{Name: \"childHooks\"}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{AfterHooks: []config.Hook{{Name: \"childHooks\"}}}},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{AfterHooks: []config.Hook{{Name: \"childHooks\"}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{AfterHooks: []config.Hook{{Name: \"parentHooks\"}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{AfterHooks: []config.Hook{{Name: \"parentHooks\"}, {Name: \"childHooks\"}}}},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{AfterHooks: []config.Hook{{Name: \"overrideHooks\", Commands: []string{\"child-apply\"}}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{AfterHooks: []config.Hook{{Name: \"overrideHooks\", Commands: []string{\"parent-apply\"}}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{AfterHooks: []config.Hook{{Name: \"overrideHooks\", Commands: []string{\"child-apply\"}}}}},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{AfterHooks: []config.Hook{{Name: \"overrideHooksPlusMore\", Commands: []string{\"child-apply\"}}, {Name: \"childHooks\"}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{AfterHooks: []config.Hook{{Name: \"overrideHooksPlusMore\", Commands: []string{\"parent-apply\"}}, {Name: \"parentHooks\"}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{AfterHooks: []config.Hook{{Name: \"overrideHooksPlusMore\", Commands: []string{\"child-apply\"}}, {Name: \"parentHooks\"}, {Name: \"childHooks\"}}}},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{AfterHooks: []config.Hook{{Name: \"overrideWithEmptyHooks\"}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{AfterHooks: []config.Hook{{Name: \"overrideWithEmptyHooks\", Commands: []string{\"parent-apply\"}}}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{AfterHooks: []config.Hook{{Name: \"overrideWithEmptyHooks\"}}}},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{IamRole: \"role2\"},\n\t\t\t&config.TerragruntConfig{IamRole: \"role1\"},\n\t\t\t&config.TerragruntConfig{IamRole: \"role2\"},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{IamWebIdentityToken: \"token\"},\n\t\t\t&config.TerragruntConfig{IamWebIdentityToken: \"token\"},\n\t\t\t&config.TerragruntConfig{IamWebIdentityToken: \"token\"},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{IamWebIdentityToken: \"token\"},\n\t\t\t&config.TerragruntConfig{IamWebIdentityToken: \"token2\"},\n\t\t\t&config.TerragruntConfig{IamWebIdentityToken: \"token\"},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{},\n\t\t\t&config.TerragruntConfig{IamWebIdentityToken: \"token\"},\n\t\t\t&config.TerragruntConfig{IamWebIdentityToken: \"token\"},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{IamAssumeRoleSessionName: \"session\"},\n\t\t\t&config.TerragruntConfig{IamAssumeRoleSessionName: \"session2\"},\n\t\t\t&config.TerragruntConfig{IamAssumeRoleSessionName: \"session\"},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{},\n\t\t\t&config.TerragruntConfig{IamAssumeRoleSessionName: \"session\"},\n\t\t\t&config.TerragruntConfig{IamAssumeRoleSessionName: \"session\"},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{CopyTerraformLockFile: &[]bool{false}[0]}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{IncludeInCopy: &[]string{\"abc\"}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{CopyTerraformLockFile: &[]bool{false}[0], IncludeInCopy: &[]string{\"abc\"}}},\n\t\t},\n\t\t{\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{CopyTerraformLockFile: &[]bool{false}[0]}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{ExcludeFromCopy: &[]string{\"abc\"}}},\n\t\t\t&config.TerragruntConfig{Terraform: &config.TerraformConfig{CopyTerraformLockFile: &[]bool{false}[0], ExcludeFromCopy: &[]string{\"abc\"}}},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\t// if nil, initialize to empty dependency list\n\t\tif tc.expected.TerragruntDependencies == nil {\n\t\t\ttc.expected.TerragruntDependencies = config.Dependencies{}\n\t\t}\n\n\t\terr := tc.includedConfig.Merge(logger.CreateLogger(), tc.config)\n\t\trequire.NoError(t, err)\n\t\tassert.EqualExportedValues(t, tc.expected, tc.includedConfig)\n\t}\n}\n\nfunc TestDeepMergeConfigIntoIncludedConfig(t *testing.T) {\n\tt.Parallel()\n\n\t// The following maps are convenience vars for setting up deep merge map tests\n\toverrideMap := map[string]any{\n\t\t\"simple_string_override\": \"hello, mock\",\n\t\t\"simple_string_append\":   \"new val\",\n\t\t\"list_attr\":              []string{\"mock\"},\n\t\t\"map_attr\": map[string]any{\n\t\t\t\"simple_string_override\": \"hello, mock\",\n\t\t\t\"simple_string_append\":   \"new val\",\n\t\t\t\"list_attr\":              []string{\"mock\"},\n\t\t\t\"map_attr\": map[string]any{\n\t\t\t\t\"simple_string_override\": \"hello, mock\",\n\t\t\t\t\"simple_string_append\":   \"new val\",\n\t\t\t\t\"list_attr\":              []string{\"mock\"},\n\t\t\t},\n\t\t},\n\t}\n\toriginalMap := map[string]any{\n\t\t\"simple_string_override\": \"hello, world\",\n\t\t\"original_string\":        \"original val\",\n\t\t\"list_attr\":              []string{\"hello\"},\n\t\t\"map_attr\": map[string]any{\n\t\t\t\"simple_string_override\": \"hello, world\",\n\t\t\t\"original_string\":        \"original val\",\n\t\t\t\"list_attr\":              []string{\"hello\"},\n\t\t\t\"map_attr\": map[string]any{\n\t\t\t\t\"simple_string_override\": \"hello, world\",\n\t\t\t\t\"original_string\":        \"original val\",\n\t\t\t\t\"list_attr\":              []string{\"hello\"},\n\t\t\t},\n\t\t},\n\t}\n\tmergedMap := map[string]any{\n\t\t\"simple_string_override\": \"hello, mock\",\n\t\t\"original_string\":        \"original val\",\n\t\t\"simple_string_append\":   \"new val\",\n\t\t\"list_attr\":              []string{\"hello\", \"mock\"},\n\t\t\"map_attr\": map[string]any{\n\t\t\t\"simple_string_override\": \"hello, mock\",\n\t\t\t\"original_string\":        \"original val\",\n\t\t\t\"simple_string_append\":   \"new val\",\n\t\t\t\"list_attr\":              []string{\"hello\", \"mock\"},\n\t\t\t\"map_attr\": map[string]any{\n\t\t\t\t\"simple_string_override\": \"hello, mock\",\n\t\t\t\t\"original_string\":        \"original val\",\n\t\t\t\t\"simple_string_append\":   \"new val\",\n\t\t\t\t\"list_attr\":              []string{\"hello\", \"mock\"},\n\t\t\t},\n\t\t},\n\t}\n\n\ttestCases := []struct {\n\t\tsource   *config.TerragruntConfig\n\t\ttarget   *config.TerragruntConfig\n\t\texpected *config.TerragruntConfig\n\t\tname     string\n\t}{\n\t\t// Base case: empty config\n\t\t{\n\t\t\tname:     \"base case\",\n\t\t\tsource:   &config.TerragruntConfig{},\n\t\t\ttarget:   &config.TerragruntConfig{},\n\t\t\texpected: &config.TerragruntConfig{},\n\t\t},\n\t\t// Simple attribute in target\n\t\t{\n\t\t\tname:     \"simple in target\",\n\t\t\tsource:   &config.TerragruntConfig{},\n\t\t\ttarget:   &config.TerragruntConfig{IamRole: \"foo\"},\n\t\t\texpected: &config.TerragruntConfig{IamRole: \"foo\"},\n\t\t},\n\t\t// Simple attribute in source\n\t\t{\n\t\t\tname:     \"simple in source\",\n\t\t\tsource:   &config.TerragruntConfig{IamRole: \"foo\"},\n\t\t\ttarget:   &config.TerragruntConfig{},\n\t\t\texpected: &config.TerragruntConfig{IamRole: \"foo\"},\n\t\t},\n\t\t// Simple attribute in both\n\t\t{\n\t\t\tname:     \"simple in both\",\n\t\t\tsource:   &config.TerragruntConfig{IamRole: \"foo\"},\n\t\t\ttarget:   &config.TerragruntConfig{IamRole: \"bar\"},\n\t\t\texpected: &config.TerragruntConfig{IamRole: \"foo\"},\n\t\t},\n\t\t// skip related tests\n\t\t// Deep merge dependencies\n\t\t{\n\t\t\tname: \"dependencies\",\n\t\t\tsource: &config.TerragruntConfig{Dependencies: &config.ModuleDependencies{Paths: []string{\"../vpc\"}},\n\t\t\t\tTerragruntDependencies: config.Dependencies{\n\t\t\t\t\tconfig.Dependency{\n\t\t\t\t\t\tName:       \"vpc\",\n\t\t\t\t\t\tConfigPath: cty.StringVal(\"../vpc\"),\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\ttarget: &config.TerragruntConfig{Dependencies: &config.ModuleDependencies{Paths: []string{\"../mysql\"}},\n\t\t\t\tTerragruntDependencies: config.Dependencies{\n\t\t\t\t\tconfig.Dependency{\n\t\t\t\t\t\tName:       \"mysql\",\n\t\t\t\t\t\tConfigPath: cty.StringVal(\"../mysql\"),\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\texpected: &config.TerragruntConfig{Dependencies: &config.ModuleDependencies{Paths: []string{\"../mysql\", \"../vpc\"}},\n\t\t\t\tTerragruntDependencies: config.Dependencies{\n\t\t\t\t\tconfig.Dependency{\n\t\t\t\t\t\tName:       \"mysql\",\n\t\t\t\t\t\tConfigPath: cty.StringVal(\"../mysql\"),\n\t\t\t\t\t},\n\t\t\t\t\tconfig.Dependency{\n\t\t\t\t\t\tName:       \"vpc\",\n\t\t\t\t\t\tConfigPath: cty.StringVal(\"../vpc\"),\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t},\n\t\t// Deep merge retryable errors\n\t\t// Deep merge inputs\n\t\t{\n\t\t\tname:     \"inputs\",\n\t\t\tsource:   &config.TerragruntConfig{Inputs: overrideMap},\n\t\t\ttarget:   &config.TerragruntConfig{Inputs: originalMap},\n\t\t\texpected: &config.TerragruntConfig{Inputs: mergedMap},\n\t\t},\n\t\t{\n\t\t\tname:     \"terraform copy_terraform_lock_file\",\n\t\t\tsource:   &config.TerragruntConfig{Terraform: &config.TerraformConfig{CopyTerraformLockFile: &[]bool{false}[0]}},\n\t\t\ttarget:   &config.TerragruntConfig{Terraform: &config.TerraformConfig{IncludeInCopy: &[]string{\"abc\"}}},\n\t\t\texpected: &config.TerragruntConfig{Terraform: &config.TerraformConfig{CopyTerraformLockFile: &[]bool{false}[0], IncludeInCopy: &[]string{\"abc\"}}},\n\t\t},\n\t\t{\n\t\t\tname:     \"terraform copy_terraform_lock_file\",\n\t\t\tsource:   &config.TerragruntConfig{Terraform: &config.TerraformConfig{CopyTerraformLockFile: &[]bool{false}[0]}},\n\t\t\ttarget:   &config.TerragruntConfig{Terraform: &config.TerraformConfig{ExcludeFromCopy: &[]string{\"abc\"}}},\n\t\t\texpected: &config.TerragruntConfig{Terraform: &config.TerraformConfig{CopyTerraformLockFile: &[]bool{false}[0], ExcludeFromCopy: &[]string{\"abc\"}}},\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.Parallel()\n\n\t\t\terr := tc.target.DeepMerge(logger.CreateLogger(), tc.source)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// if nil, initialize to empty dependency list\n\t\t\tif tc.expected.TerragruntDependencies == nil {\n\t\t\t\ttc.expected.TerragruntDependencies = config.Dependencies{}\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expected, tc.target)\n\t\t})\n\t}\n}\n\nfunc TestConcurrentCopyFieldsMetadata(t *testing.T) {\n\tt.Parallel()\n\n\tsourceConfig := &config.TerragruntConfig{\n\t\tFieldsMetadata: map[string]map[string]any{\n\t\t\t\"field1\": {\"key1\": \"value1\", \"key2\": \"value2\"},\n\t\t\t\"field2\": {\"key3\": \"value3\", \"key4\": \"value4\"},\n\t\t},\n\t}\n\n\ttargetConfig := &config.TerragruntConfig{}\n\n\tvar wg sync.WaitGroup\n\n\tnumGoroutines := 666\n\n\twg.Add(numGoroutines)\n\n\tfor range numGoroutines {\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\tconfig.CopyFieldsMetadata(sourceConfig, targetConfig)\n\t\t}()\n\t}\n\n\twg.Wait()\n\n\t// Optionally, here you can add assertions to check the integrity of the targetConfig\n\t// For example, checking if all keys and values have been copied correctly\n\texpectedFields := len(sourceConfig.FieldsMetadata)\n\tif len(targetConfig.FieldsMetadata) != expectedFields {\n\t\tt.Errorf(\"Expected %d fields, got %d\", expectedFields, len(targetConfig.FieldsMetadata))\n\t}\n}\n\nfunc TestDependencyFileNotFoundError(t *testing.T) {\n\tt.Parallel()\n\n\t// Test that DependencyFileNotFoundError is properly defined and formatted\n\terr := config.DependencyFileNotFoundError{Path: \"/test/path/terragrunt.hcl\"}\n\n\tassert.Equal(t, \"/test/path/terragrunt.hcl\", err.Path)\n\tassert.Contains(t, err.Error(), \"Dependency file not found: /test/path/terragrunt.hcl\")\n\n\t// Test with a different path\n\terr2 := config.DependencyFileNotFoundError{Path: \"/another/path/config.hcl\"}\n\tassert.Equal(t, \"/another/path/config.hcl\", err2.Path)\n\tassert.Contains(t, err2.Error(), \"Dependency file not found: /another/path/config.hcl\")\n}\n\nfunc TestIncludeConfigNotFoundError(t *testing.T) {\n\tt.Parallel()\n\n\t// Test that IncludeConfigNotFoundError is properly defined and formatted\n\terr := config.IncludeConfigNotFoundError{IncludePath: \"/test/path/terragrunt.hcl\", SourcePath: \"/source/config.hcl\"}\n\n\tassert.Equal(t, \"/test/path/terragrunt.hcl\", err.IncludePath)\n\tassert.Equal(t, \"/source/config.hcl\", err.SourcePath)\n\tassert.Contains(t, err.Error(), \"Include configuration not found: /test/path/terragrunt.hcl (referenced from: /source/config.hcl)\")\n\n\t// Test with a different path\n\terr2 := config.IncludeConfigNotFoundError{IncludePath: \"/another/path/config.hcl\", SourcePath: \"/different/source.hcl\"}\n\tassert.Equal(t, \"/another/path/config.hcl\", err2.IncludePath)\n\tassert.Equal(t, \"/different/source.hcl\", err2.SourcePath)\n\tassert.Contains(t, err2.Error(), \"Include configuration not found: /another/path/config.hcl (referenced from: /different/source.hcl)\")\n}\n"
  },
  {
    "path": "pkg/config/locals.go",
    "content": "package config\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/zclconf/go-cty/cty\"\n\n\t\"maps\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// MaxIter is the maximum number of depth we support in recursively evaluating locals.\nconst MaxIter = 1000\n\n// EvaluateLocalsBlock is a routine to evaluate the locals block in a way to allow references to other locals. This\n// will:\n//   - Extract a reference to the locals block from the parsed file\n//   - Continuously evaluate the block until all references are evaluated, deferring evaluation of anything that references\n//     other locals until those references are evaluated.\n//\n// This returns a map of the local names to the evaluated expressions (represented as `cty.Value` objects). This will\n// error if there are remaining unevaluated locals after all references that can be evaluated has been evaluated.\nfunc EvaluateLocalsBlock(ctx context.Context, pctx *ParsingContext, l log.Logger, file *hclparse.File) (map[string]cty.Value, error) {\n\tlocalsBlock, err := file.Blocks(MetadataLocals, false)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(localsBlock) == 0 {\n\t\t// No locals block referenced in the file\n\t\tl.Debugf(\"Did not find any locals block: skipping evaluation.\")\n\t\treturn nil, nil\n\t}\n\n\tl.Debugf(\"Found locals block: evaluating the expressions.\")\n\n\tattrs, err := localsBlock[0].JustAttributes()\n\tif err != nil {\n\t\tl.Debugf(\"Encountered error while decoding locals block into name expression pairs.\")\n\t\treturn nil, err\n\t}\n\n\t// Continuously attempt to evaluate the locals until there are no more locals to evaluate, or we can't evaluate\n\t// further.\n\tevaluatedLocals := map[string]cty.Value{}\n\tevaluated := true\n\n\tfor iterations := 0; len(attrs) > 0 && evaluated; iterations++ {\n\t\tif iterations > MaxIter {\n\t\t\t// Reached maximum supported iterations, which is most likely an infinite loop bug so cut the iteration\n\t\t\t// short an return an error.\n\t\t\treturn nil, errors.New(MaxIterError{})\n\t\t}\n\n\t\tvar err error\n\n\t\tattrs, evaluatedLocals, evaluated, err = attemptEvaluateLocals(\n\t\t\tctx,\n\t\t\tpctx,\n\t\t\tl,\n\t\t\tfile,\n\t\t\tattrs,\n\t\t\tevaluatedLocals,\n\t\t)\n\t\tif err != nil {\n\t\t\tl.Debugf(\"Encountered error while evaluating locals in file %s\", util.RelPathForLog(pctx.RootWorkingDir, pctx.TerragruntConfigPath, pctx.Writers.LogShowAbsPaths))\n\t\t\treturn evaluatedLocals, err\n\t\t}\n\t}\n\n\tif len(attrs) > 0 {\n\t\t// This is an error because we couldn't evaluate all locals\n\t\tl.Debugf(\"Not all locals could be evaluated:\")\n\n\t\tvar errs *errors.MultiError\n\n\t\tfor _, attr := range attrs {\n\t\t\tdiags := canEvaluateLocals(attr.Expr, evaluatedLocals)\n\t\t\tif err := file.HandleDiagnostics(diags); err != nil {\n\t\t\t\terrs = errs.Append(err)\n\t\t\t}\n\t\t}\n\n\t\tif err := errs.ErrorOrNil(); err != nil {\n\t\t\treturn nil, errors.New(CouldNotEvaluateAllLocalsError{Err: err})\n\t\t}\n\t}\n\n\treturn evaluatedLocals, nil\n}\n\n// attemptEvaluateLocals attempts to evaluate the locals block given the map of already evaluated locals, replacing\n// references to locals with the previously evaluated values. This will return:\n// - the list of remaining locals that were unevaluated in this attempt\n// - the updated map of evaluated locals after this attempt\n// - whether or not any locals were evaluated in this attempt\n// - any errors from the evaluation\nfunc attemptEvaluateLocals(\n\tctx context.Context,\n\tpctx *ParsingContext,\n\tl log.Logger,\n\tfile *hclparse.File,\n\tattrs hclparse.Attributes,\n\tevaluatedLocals map[string]cty.Value,\n) (unevaluatedAttrs hclparse.Attributes, newEvaluatedLocals map[string]cty.Value, evaluated bool, err error) {\n\tlocalsAsCtyVal, err := ConvertValuesMapToCtyVal(evaluatedLocals)\n\tif err != nil {\n\t\tl.Errorf(\"Could not convert evaluated locals to the execution ctx to evaluate additional locals in file %s\", file.ConfigPath)\n\t\treturn nil, evaluatedLocals, false, err\n\t}\n\n\tpctx.Locals = &localsAsCtyVal\n\n\tevalCtx, err := createTerragruntEvalContext(ctx, pctx, l, file.ConfigPath)\n\tif err != nil {\n\t\tl.Errorf(\"Could not convert include to the execution ctx to evaluate additional locals in file %s\", file.ConfigPath)\n\t\treturn nil, evaluatedLocals, false, err\n\t}\n\n\t// Track the locals that were evaluated for logging purposes\n\tnewlyEvaluatedLocalNames := []string{}\n\n\tunevaluatedAttrs = hclparse.Attributes{}\n\tevaluated = false\n\n\tnewEvaluatedLocals = make(map[string]cty.Value, len(evaluatedLocals))\n\tmaps.Copy(newEvaluatedLocals, evaluatedLocals)\n\n\terrs := &errors.MultiError{}\n\n\tfor _, attr := range attrs {\n\t\tif diags := canEvaluateLocals(attr.Expr, evaluatedLocals); !diags.HasErrors() {\n\t\t\tevaluatedVal, err := attr.Value(evalCtx)\n\t\t\tif err != nil {\n\t\t\t\terrs = errs.Append(err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tnewEvaluatedLocals[attr.Name] = evaluatedVal\n\n\t\t\tnewlyEvaluatedLocalNames = append(newlyEvaluatedLocalNames, attr.Name)\n\t\t\tevaluated = true\n\t\t} else {\n\t\t\tunevaluatedAttrs = append(unevaluatedAttrs, attr)\n\t\t}\n\t}\n\n\tl.Debugf(\n\t\t\"Evaluated %d locals (remaining %d): %s\",\n\t\tlen(newlyEvaluatedLocalNames),\n\t\tlen(unevaluatedAttrs),\n\t\tstrings.Join(newlyEvaluatedLocalNames, \", \"),\n\t)\n\n\treturn unevaluatedAttrs, newEvaluatedLocals, evaluated, errs.ErrorOrNil()\n}\n\n// canEvaluateLocals determines if the local expression can be evaluated. An expression can be evaluated if one of the\n// following is true:\n// - It has no references to other locals.\n// - It has references to other locals that have already been evaluated.\n// Note that the second return value is a human friendly reason for why the expression can not be evaluated, and is\n// useful for error reporting.\nfunc canEvaluateLocals(expression hcl.Expression, evaluatedLocals map[string]cty.Value) hcl.Diagnostics {\n\tvar diags hcl.Diagnostics\n\n\tlocalVars := expression.Variables()\n\n\tfor _, localVar := range localVars {\n\t\tvar (\n\t\t\trootName        = localVar.RootName()\n\t\t\tlocalName       = getLocalName(localVar)\n\t\t\t_, hasEvaluated = evaluatedLocals[localName]\n\t\t\tdetail          string\n\t\t)\n\n\t\tswitch {\n\t\tcase localVar.IsRelative():\n\t\t\t// This should never happen, but if it does, we can't evaluate this expression.\n\t\t\tdetail = \"This caused an impossible condition, tnis is almost certainly a bug in Terragrunt. Please open an issue at github.com/gruntwork-io/terragrunt with this message and the contents of your terragrunt.hcl file that caused this.\"\n\n\t\tcase rootName == MetadataInclude:\n\t\t\t// If the variable is `include`, then we can evaluate it now\n\n\t\tcase rootName == MetadataFeatureFlag:\n\t\t\t// If the variable is `feature`\n\n\t\tcase rootName == MetadataValues:\n\t\t\t// If the variable is `values`\n\n\t\tcase rootName != \"local\":\n\t\t\t// We can't evaluate any variable other than `local`\n\t\t\tdetail = fmt.Sprintf(\"You can only reference to other local variables here, but it looks like you're referencing something else (%q is not defined)\", rootName)\n\n\t\tcase localName == \"\":\n\t\t\t// If we can't get any local name, we can't evaluate it.\n\t\t\tdetail = \"This local var name can not be determined.\"\n\n\t\tcase !hasEvaluated:\n\t\t\t// If the referenced local isn't evaluated, we can't evaluate this expression.\n\t\t\tdetail = fmt.Sprintf(\"The local reference '%s' is not evaluated. Either it is not ready yet in the current pass, or there was an error evaluating it in an earlier stage.\", localName)\n\t\t}\n\n\t\tif detail != \"\" {\n\t\t\tdiags = diags.Append(&hcl.Diagnostic{\n\t\t\t\tSeverity: hcl.DiagError,\n\t\t\t\tSummary:  \"Can't evaluate expression\",\n\t\t\t\tDetail:   detail,\n\t\t\t\tSubject:  expression.Range().Ptr(),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn diags\n}\n\n// getLocalName takes a variable reference encoded as a HCL tree traversal that is rooted at the name `local` and\n// returns the underlying variable lookup on the local map. If it is not a local name lookup, this will return empty\n// string.\nfunc getLocalName(traversal hcl.Traversal) string {\n\tif traversal.IsRelative() {\n\t\treturn \"\"\n\t}\n\n\tif traversal.RootName() != \"local\" {\n\t\treturn \"\"\n\t}\n\n\tsplit := traversal.SimpleSplit()\n\tfor _, relRaw := range split.Rel {\n\t\tswitch rel := relRaw.(type) {\n\t\tcase hcl.TraverseAttr:\n\t\t\treturn rel.Name\n\t\tdefault:\n\t\t\t// This means that it is either an operation directly on the locals block, or is an unsupported action (e.g\n\t\t\t// a splat or lookup). Either way, there is no local name.\n\t\t\tcontinue\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\n// ------------------------------------------------\n// Custom Errors Returned by Functions in this Code\n// ------------------------------------------------\n\ntype CouldNotEvaluateAllLocalsError struct {\n\tErr error\n}\n\nfunc (err CouldNotEvaluateAllLocalsError) Error() string {\n\treturn \"Could not evaluate all locals in block.\"\n}\n\nfunc (err CouldNotEvaluateAllLocalsError) Unwrap() error {\n\treturn err.Err\n}\n\ntype MaxIterError struct{}\n\nfunc (err MaxIterError) Error() string {\n\treturn \"Maximum iterations reached in attempting to evaluate locals. This is most likely a bug in Terragrunt. Please file an issue on the project: https://github.com/gruntwork-io/terragrunt/issues\"\n}\n"
  },
  {
    "path": "pkg/config/locals_test.go",
    "content": "package config_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/zclconf/go-cty/cty/gocty\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n)\n\nfunc TestEvaluateLocalsBlock(t *testing.T) {\n\tt.Parallel()\n\n\tfile, err := hclparse.NewParser().ParseFromString(LocalsTestConfig, config.DefaultTerragruntConfigPath)\n\trequire.NoError(t, err)\n\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tevaluatedLocals, err := config.EvaluateLocalsBlock(ctx, pctx, logger.CreateLogger(), file)\n\trequire.NoError(t, err)\n\n\tvar actualRegion string\n\trequire.NoError(t, gocty.FromCtyValue(evaluatedLocals[\"region\"], &actualRegion))\n\tassert.Equal(t, \"us-east-1\", actualRegion)\n\n\tvar actualS3Url string\n\trequire.NoError(t, gocty.FromCtyValue(evaluatedLocals[\"s3_url\"], &actualS3Url))\n\tassert.Equal(t, \"com.amazonaws.us-east-1.s3\", actualS3Url)\n\n\tvar actualX float64\n\trequire.NoError(t, gocty.FromCtyValue(evaluatedLocals[\"x\"], &actualX))\n\tassert.InEpsilon(t, float64(1), actualX, 0.0000001)\n\n\tvar actualY float64                                                    //codespell:ignore\n\trequire.NoError(t, gocty.FromCtyValue(evaluatedLocals[\"y\"], &actualY)) //codespell:ignore\n\tassert.InEpsilon(t, float64(2), actualY, 0.0000001)                    //codespell:ignore\n\n\tvar actualZ float64\n\trequire.NoError(t, gocty.FromCtyValue(evaluatedLocals[\"z\"], &actualZ))\n\tassert.InEpsilon(t, float64(3), actualZ, 0.0000001)\n\n\tvar actualFoo struct{ First Foo }\n\trequire.NoError(t, gocty.FromCtyValue(evaluatedLocals[\"foo\"], &actualFoo))\n\tassert.Equal(t, Foo{\n\t\tRegion: \"us-east-1\",\n\t\tFoo:    \"bar\",\n\t}, actualFoo.First)\n\n\tvar actualBar string\n\trequire.NoError(t, gocty.FromCtyValue(evaluatedLocals[\"bar\"], &actualBar))\n\tassert.Equal(t, \"us-east-1\", actualBar)\n}\n\nfunc TestEvaluateLocalsBlockMultiDeepReference(t *testing.T) {\n\tt.Parallel()\n\n\tfile, err := hclparse.NewParser().ParseFromString(LocalsTestMultiDeepReferenceConfig, config.DefaultTerragruntConfigPath)\n\trequire.NoError(t, err)\n\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tevaluatedLocals, err := config.EvaluateLocalsBlock(ctx, pctx, logger.CreateLogger(), file)\n\trequire.NoError(t, err)\n\n\texpected := \"a\"\n\n\tvar actualA string\n\trequire.NoError(t, gocty.FromCtyValue(evaluatedLocals[\"a\"], &actualA))\n\tassert.Equal(t, expected, actualA)\n\n\ttestCases := []string{\n\t\t\"b\",\n\t\t\"c\",\n\t\t\"d\",\n\t\t\"e\",\n\t\t\"f\",\n\t\t\"g\",\n\t\t\"h\",\n\t\t\"i\",\n\t\t\"j\",\n\t}\n\tfor _, tc := range testCases {\n\t\texpected = fmt.Sprintf(\"%s/%s\", expected, tc)\n\n\t\tvar actual string\n\t\trequire.NoError(t, gocty.FromCtyValue(evaluatedLocals[tc], &actual))\n\t\tassert.Equal(t, expected, actual)\n\t}\n}\n\nfunc TestEvaluateLocalsBlockImpossibleWillFail(t *testing.T) {\n\tt.Parallel()\n\n\tfile, err := hclparse.NewParser().ParseFromString(LocalsTestImpossibleConfig, config.DefaultTerragruntConfigPath)\n\trequire.NoError(t, err)\n\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\t_, err = config.EvaluateLocalsBlock(ctx, pctx, logger.CreateLogger(), file)\n\trequire.Error(t, err)\n\n\tswitch errors.Unwrap(err).(type) { //nolint:errorlint\n\tcase config.CouldNotEvaluateAllLocalsError:\n\tdefault:\n\t\tt.Fatalf(\"Did not get expected error: %s\", err)\n\t}\n}\n\nfunc TestEvaluateLocalsBlockMultipleLocalsBlocksWillFail(t *testing.T) {\n\tt.Parallel()\n\n\tfile, err := hclparse.NewParser().ParseFromString(MultipleLocalsBlockConfig, config.DefaultTerragruntConfigPath)\n\trequire.NoError(t, err)\n\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\t_, err = config.EvaluateLocalsBlock(ctx, pctx, logger.CreateLogger(), file)\n\trequire.Error(t, err)\n}\n\ntype Foo struct {\n\tRegion string `cty:\"region\"`\n\tFoo    string `cty:\"foo\"`\n}\n\nconst LocalsTestConfig = `\nlocals {\n  region = \"us-east-1\"\n\n  // Simple reference\n  s3_url = \"com.amazonaws.${local.region}.s3\"\n\n  // Nested reference\n  foo = [\n    merge(\n      {region = local.region},\n\t  {foo = \"bar\"},\n\t)\n  ]\n  bar = local.foo[0][\"region\"]\n\n  // Multiple references\n  x = 1\n  y = 2\n  z = local.x + local.y\n}\n`\n\nconst LocalsTestMultiDeepReferenceConfig = `\n# 10 chains deep\nlocals {\n  a = \"a\"\n  b = \"${local.a}/b\"\n  c = \"${local.b}/c\"\n  d = \"${local.c}/d\"\n  e = \"${local.d}/e\"\n  f = \"${local.e}/f\"\n  g = \"${local.f}/g\"\n  h = \"${local.g}/h\"\n  i = \"${local.h}/i\"\n  j = \"${local.i}/j\"\n}\n`\n\nconst LocalsTestImpossibleConfig = `\nlocals {\n  a = local.b\n  b = local.a\n}\n`\n\nconst MultipleLocalsBlockConfig = `\nlocals {\n  a = \"a\"\n}\n\nlocals {\n  b = \"b\"\n}\n`\n"
  },
  {
    "path": "pkg/config/options.go",
    "content": "package config\n\nimport \"github.com/gruntwork-io/terragrunt/internal/strict\"\n\n// Option is a functional option for NewParsingContext.\ntype Option func(*ParsingContext)\n\n// WithStrictControls sets the strict controls for the parsing context.\nfunc WithStrictControls(controls strict.Controls) Option {\n\treturn func(pctx *ParsingContext) {\n\t\tpctx.StrictControls = controls\n\t}\n}\n"
  },
  {
    "path": "pkg/config/parsing_context.go",
    "content": "package config\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"maps\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\n\t\"github.com/puzpuzpuz/xsync/v3\"\n\t\"github.com/zclconf/go-cty/cty\"\n\t\"github.com/zclconf/go-cty/cty/function\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/engine\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iam\"\n\tpcoptions \"github.com/gruntwork-io/terragrunt/internal/providercache/options\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict\"\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tfimpl\"\n\t\"github.com/gruntwork-io/terragrunt/internal/writer\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\"\n)\n\nconst (\n\t// MaxParseDepth limits nested parsing to prevent stack overflow\n\t// from deeply recursive config structures (includes, dependencies, etc.).\n\tMaxParseDepth = 1000\n)\n\n// ParsingContext provides various variables that are used throughout all funcs and passed from function to function.\n// Using `ParsingContext` makes the code more readable.\n// Note: context.Context should be passed explicitly as the first parameter to functions, not embedded in this struct.\ntype ParsingContext struct {\n\tWriters writer.Writers\n\n\tTerraformCliArgs *iacargs.IacArgs\n\tTrackInclude     *TrackInclude\n\tEngineConfig     *engine.EngineConfig\n\tEngineOptions    *engine.EngineOptions\n\tFeatureFlags     *xsync.MapOf[string, string]\n\tFilesRead        *[]string\n\tTelemetry        *telemetry.Options\n\n\tDecodedDependencies *cty.Value\n\tValues              *cty.Value\n\tFeatures            *cty.Value\n\tLocals              *cty.Value\n\n\tEnv                 map[string]string\n\tSourceMap           map[string]string\n\tPredefinedFunctions map[string]function.Function\n\n\tConvertToTerragruntConfigFunc func(ctx context.Context, pctx *ParsingContext, configPath string, terragruntConfigFromFile *terragruntConfigFile) (cfg *TerragruntConfig, err error)\n\n\tTerragruntConfigPath         string\n\tOriginalTerragruntConfigPath string\n\tWorkingDir                   string\n\tRootWorkingDir               string\n\tDownloadDir                  string\n\tSource                       string\n\tTerraformCommand             string\n\tOriginalTerraformCommand     string\n\tAuthProviderCmd              string\n\tTFPath                       string\n\tScaffoldRootFileName         string\n\tTerragruntStackConfigPath    string\n\tTofuImplementation           tfimpl.Type\n\n\tIAMRoleOptions         iam.RoleOptions\n\tOriginalIAMRoleOptions iam.RoleOptions\n\n\tExperiments            experiment.Experiments\n\tStrictControls         strict.Controls\n\tPartialParseDecodeList []PartialDecodeSectionType\n\tParserOptions          []hclparse.Option\n\n\tProviderCacheOptions pcoptions.ProviderCacheOptions\n\n\tMaxFoldersToCheck int\n\tParseDepth        int\n\n\tTFPathExplicitlySet bool\n\tSkipOutput          bool\n\tForwardTFStdout     bool\n\tJSONLogFormat       bool\n\tDebug               bool\n\tAutoInit            bool\n\tHeadless            bool\n\tBackendBootstrap    bool\n\tCheckDependentUnits bool\n\n\tNoDependencyFetchOutputFromState bool\n\tUsePartialParseConfigCache       bool\n\tSkipOutputsResolution            bool\n\tNoStackValidate                  bool\n}\n\nfunc NewParsingContext(ctx context.Context, l log.Logger, opts ...Option) (context.Context, *ParsingContext) {\n\tfilesRead := make([]string, 0)\n\n\tpctx := &ParsingContext{\n\t\tTerraformCliArgs: iacargs.New(),\n\t\tFilesRead:        &filesRead,\n\t}\n\n\tfor _, opt := range opts {\n\t\topt(pctx)\n\t}\n\n\tpctx.ParserOptions = DefaultParserOptions(l, pctx.StrictControls)\n\n\treturn ctx, pctx\n}\n\n// Clone returns a copy of the ParsingContext.\n// Maps are deep-copied so that mutations (e.g. credential injection into Env)\n// on a clone do not affect the original or other clones.\nfunc (ctx *ParsingContext) Clone() *ParsingContext {\n\tclone := *ctx\n\n\tif ctx.Env != nil {\n\t\tclone.Env = maps.Clone(ctx.Env)\n\t}\n\n\tif ctx.SourceMap != nil {\n\t\tclone.SourceMap = maps.Clone(ctx.SourceMap)\n\t}\n\n\tif ctx.EngineOptions != nil {\n\t\teo := *ctx.EngineOptions\n\t\tclone.EngineOptions = &eo\n\t}\n\n\tclone.ProviderCacheOptions.RegistryNames = slices.Clone(ctx.ProviderCacheOptions.RegistryNames)\n\n\treturn &clone\n}\n\nfunc (ctx *ParsingContext) WithDecodeList(decodeList ...PartialDecodeSectionType) *ParsingContext {\n\tc := ctx.Clone()\n\tc.PartialParseDecodeList = decodeList\n\n\treturn c\n}\n\nfunc (ctx *ParsingContext) WithLocals(locals *cty.Value) *ParsingContext {\n\tc := ctx.Clone()\n\tc.Locals = locals\n\n\treturn c\n}\n\nfunc (ctx *ParsingContext) WithValues(values *cty.Value) *ParsingContext {\n\tc := ctx.Clone()\n\tc.Values = values\n\n\treturn c\n}\n\n// WithFeatures sets the feature flags to be used in evaluation context.\nfunc (ctx *ParsingContext) WithFeatures(features *cty.Value) *ParsingContext {\n\tc := ctx.Clone()\n\tc.Features = features\n\n\treturn c\n}\n\nfunc (ctx *ParsingContext) WithTrackInclude(trackInclude *TrackInclude) *ParsingContext {\n\tc := ctx.Clone()\n\tc.TrackInclude = trackInclude\n\n\treturn c\n}\n\nfunc (ctx *ParsingContext) WithParseOption(parserOptions []hclparse.Option) *ParsingContext {\n\tc := ctx.Clone()\n\tc.ParserOptions = parserOptions\n\n\treturn c\n}\n\n// WithDiagnosticsSuppressed returns a new ParsingContext with diagnostics suppressed.\n// Diagnostics are written to stderr in debug mode for troubleshooting, otherwise discarded.\n// This avoids false positive \"There is no variable named dependency\" errors during parsing\n// when dependency outputs haven't been resolved yet.\nfunc (ctx *ParsingContext) WithDiagnosticsSuppressed(l log.Logger) *ParsingContext {\n\tvar diagWriter = io.Discard\n\tif l.Level() >= log.DebugLevel {\n\t\tdiagWriter = os.Stderr\n\t}\n\n\tc := ctx.Clone()\n\tc.ParserOptions = slices.Concat(ctx.ParserOptions, []hclparse.Option{hclparse.WithDiagnosticsWriter(diagWriter, true)})\n\n\treturn c\n}\n\nfunc (ctx *ParsingContext) WithSkipOutputsResolution() *ParsingContext {\n\tc := ctx.Clone()\n\tc.SkipOutputsResolution = true\n\n\treturn c\n}\n\n// WithIncrementedDepth returns a new ParsingContext with incremented parse depth.\n// Returns an error if the maximum depth would be exceeded.\nfunc (ctx *ParsingContext) WithIncrementedDepth() (*ParsingContext, error) {\n\tif ctx.ParseDepth > MaxParseDepth {\n\t\treturn nil, errors.New(MaxParseDepthError{\n\t\t\tDepth: ctx.ParseDepth,\n\t\t\tMax:   MaxParseDepth,\n\t\t})\n\t}\n\n\tc := ctx.Clone()\n\tc.ParseDepth = ctx.ParseDepth + 1\n\n\treturn c, nil\n}\n\n// WithConfigPath returns a new ParsingContext with the config path updated.\n// It normalizes the path to an absolute path, updates WorkingDir to the directory\n// containing the config, and adjusts the logger's working directory field if it changed.\nfunc (ctx *ParsingContext) WithConfigPath(l log.Logger, configPath string) (log.Logger, *ParsingContext, error) {\n\tconfigPath = filepath.Clean(configPath)\n\tif !filepath.IsAbs(configPath) {\n\t\tconfigPath = filepath.Clean(filepath.Join(ctx.WorkingDir, configPath))\n\t}\n\n\tworkingDir := filepath.Dir(configPath)\n\n\tif workingDir != ctx.WorkingDir {\n\t\tl = l.WithField(placeholders.WorkDirKeyName, workingDir)\n\t}\n\n\tc := ctx.Clone()\n\tc.TerragruntConfigPath = configPath\n\tc.WorkingDir = workingDir\n\n\treturn l, c, nil\n}\n"
  },
  {
    "path": "pkg/config/sops_race_test.go",
    "content": "package config //nolint:testpackage // needs access to sopsDecryptFileImpl\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestSOPSDecryptConcurrencyWithRacing is a regression test for\n// https://github.com/gruntwork-io/terragrunt/issues/5515\n//\n// Run with -race to detect data races in env var handling during concurrent\n// SOPS decryption. The CI \"Race\" job runs tests matching .*WithRacing with -race.\n//\n// Multiple goroutines call sopsDecryptFileImpl concurrently with different\n// opts.Env credentials. Without proper locking, the race detector catches\n// concurrent os.Setenv/os.Getenv/os.Unsetenv calls.\nfunc TestSOPSDecryptConcurrencyWithRacing(t *testing.T) {\n\tt.Parallel()\n\n\tconst (\n\t\tauthKey       = \"SOPS_RACE_TEST_TOKEN\"\n\t\tnumGoroutines = 10\n\t)\n\n\tdir := t.TempDir()\n\n\tvar files []string\n\n\tfor i := 1; i <= numGoroutines; i++ {\n\t\tunitDir := filepath.Join(dir, fmt.Sprintf(\"unit-%02d\", i))\n\t\trequire.NoError(t, os.MkdirAll(unitDir, 0755))\n\n\t\tsecretFile := filepath.Join(unitDir, \"secret.enc.json\")\n\t\trequire.NoError(t, os.WriteFile(secretFile,\n\t\t\t[]byte(fmt.Sprintf(`{\"value\":\"secret-from-unit-%02d\"}`, i)), 0644))\n\n\t\tfiles = append(files, secretFile)\n\t}\n\n\t// Mock decrypt that reads the env var (creating a read that races with\n\t// concurrent Setenv/Unsetenv if locking is broken).\n\tmockDecryptFn := func(path string, _ string) ([]byte, error) {\n\t\t_ = os.Getenv(authKey) // read that would race without lock\n\n\t\treturn os.ReadFile(path)\n\t}\n\n\tvar (\n\t\twg      sync.WaitGroup\n\t\tbarrier = make(chan struct{})\n\t)\n\n\tctx := WithConfigValues(t.Context())\n\n\tfor i, f := range files {\n\t\twg.Add(1)\n\n\t\tgo func(idx int, filePath string) {\n\t\t\tdefer wg.Done()\n\n\t\t\t<-barrier\n\n\t\t\tl := logger.CreateLogger()\n\t\t\t_, pctx := NewParsingContext(ctx, l, WithStrictControls(controls.New()))\n\t\t\tpctx.WorkingDir = filepath.Dir(filePath)\n\t\t\tpctx.Env = map[string]string{authKey: fmt.Sprintf(\"token-%d\", idx)}\n\n\t\t\tresult, err := sopsDecryptFileImpl(ctx, pctx, l, filePath, \"json\", mockDecryptFn)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.Contains(t, result, `\"value\":\"secret-from-unit-`)\n\t\t}(i, f)\n\t}\n\n\tclose(barrier)\n\twg.Wait()\n\n\t// Verify env is clean after all goroutines complete.\n\t_, exists := os.LookupEnv(authKey)\n\trequire.False(t, exists, \"env var must be cleaned up after concurrent decrypts\")\n}\n"
  },
  {
    "path": "pkg/config/sops_test.go",
    "content": "//go:build sops\n\npackage config //nolint:testpackage // needs access to sopsDecryptFileImpl\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// generateTestSecretFiles creates plain JSON files in a temp directory.\n// No SOPS encryption needed — the test injects a mock decryptFn to read raw files.\nfunc generateTestSecretFiles(t *testing.T, count int) []string {\n\tt.Helper()\n\n\tdir := t.TempDir()\n\n\tvar files []string\n\n\tfor i := 1; i <= count; i++ {\n\t\tunitDir := filepath.Join(dir, fmt.Sprintf(\"unit-%02d\", i))\n\t\trequire.NoError(t, os.MkdirAll(unitDir, 0755))\n\n\t\tsecretFile := filepath.Join(unitDir, \"secret.enc.json\")\n\t\trequire.NoError(t, os.WriteFile(secretFile,\n\t\t\t[]byte(fmt.Sprintf(`{\"value\":\"secret-from-unit-%02d\"}`, i)), 0644))\n\n\t\tfiles = append(files, secretFile)\n\t}\n\n\treturn files\n}\n\n// TestSOPSDecryptEnvPropagation is a deterministic regression test for\n// https://github.com/gruntwork-io/terragrunt/issues/5515\n//\n// The original customer-reported bug: sops_decrypt_file() during HCL evaluation\n// couldn't authenticate to KMS because auth-provider credentials were not yet\n// loaded into opts.Env. This caused SOPS to return empty/wrong secrets.\n//\n// This test verifies the env propagation contract of sopsDecryptFileImpl:\n//   - Existing process env vars are preserved (not overridden by opts.Env)\n//   - Missing env vars from opts.Env are set during decrypt and unset after\n//   - Without credentials, decrypt fails (reproduces the original bug)\n//   - Concurrent goroutines with different credentials are properly isolated\nfunc TestSOPSDecryptEnvPropagation(t *testing.T) { //nolint:paralleltest // mutates process env vars\n\tconst authKey = \"SOPS_TEST_AUTH_CRED\"\n\n\tt.Cleanup(func() {\n\t\tos.Unsetenv(authKey) //nolint:errcheck\n\t})\n\n\tsecretFiles := generateTestSecretFiles(t, 1)\n\tsecretFile := secretFiles[0]\n\n\t// Mock decryptFn that requires authKey to be set — simulates KMS auth.\n\tauthRequiringDecryptFn := func(path string, _ string) ([]byte, error) {\n\t\ttoken := os.Getenv(authKey)\n\t\tif token == \"\" {\n\t\t\treturn nil, errors.New(\"KMS auth failed: no credential set\")\n\t\t}\n\n\t\treturn os.ReadFile(path)\n\t}\n\n\t// Subtest 1: Existing process env vars are preserved (not overridden).\n\t// Models: CI runner has real AWS_SESSION_TOKEN, auth-provider returns empty token.\n\t// sopsDecryptFileImpl must NOT override the real token with empty — SOPS uses process env.\n\tt.Run(\"existing_process_env_preserved\", func(t *testing.T) { //nolint:paralleltest // mutates process env\n\t\tt.Setenv(authKey, \"real-ci-token\")\n\n\t\tl := logger.CreateLogger()\n\t\tctx := WithConfigValues(t.Context())\n\t\t_, pctx := NewParsingContext(ctx, l, WithStrictControls(controls.New()))\n\t\tpctx.WorkingDir = filepath.Dir(secretFile)\n\t\t// pctx.Env has empty value for authKey (like auth-provider returning empty session token)\n\t\tpctx.Env = map[string]string{authKey: \"\"}\n\n\t\tresult, err := sopsDecryptFileImpl(ctx, pctx, l, secretFile, \"json\", authRequiringDecryptFn)\n\t\trequire.NoError(t, err, \"decrypt must succeed using existing process env credentials\")\n\t\tassert.Contains(t, result, `\"value\":\"secret-from-unit-01\"`)\n\n\t\t// Process env must still have the real token — not overridden\n\t\tassert.Equal(t, \"real-ci-token\", os.Getenv(authKey),\n\t\t\t\"existing process env var must not be overridden\")\n\t})\n\n\t// Subtest 2: Credentials injected when absent from process env.\n\t// Models: first run, auth-provider loaded creds into opts.Env, process env was empty.\n\tt.Run(\"new_creds_set_when_absent_from_process_env\", func(t *testing.T) { //nolint:paralleltest // mutates process env\n\t\tos.Unsetenv(authKey) //nolint:errcheck\n\n\t\tl := logger.CreateLogger()\n\t\tctx := WithConfigValues(t.Context())\n\t\t_, pctx := NewParsingContext(ctx, l, WithStrictControls(controls.New()))\n\t\tpctx.WorkingDir = filepath.Dir(secretFile)\n\t\tpctx.Env = map[string]string{authKey: \"fresh-token\"}\n\n\t\tresult, err := sopsDecryptFileImpl(ctx, pctx, l, secretFile, \"json\", authRequiringDecryptFn)\n\t\trequire.NoError(t, err, \"decrypt must succeed with fresh credentials from opts.Env\")\n\t\tassert.Contains(t, result, `\"value\":\"secret-from-unit-01\"`)\n\n\t\t// Process env must be unset (not empty string) after decrypt\n\t\t_, exists := os.LookupEnv(authKey)\n\t\tassert.False(t, exists,\n\t\t\t\"env var must be unset after decrypt, not set to empty string\")\n\t})\n\n\t// Subtest 3: Missing credentials cause decrypt failure.\n\t// Reproduces the ORIGINAL bug: auth-provider hasn't run yet, opts.Env has no\n\t// auth token, process env has no auth token → SOPS can't authenticate to KMS.\n\tt.Run(\"missing_creds_fails_decrypt\", func(t *testing.T) { //nolint:paralleltest // mutates process env\n\t\tos.Unsetenv(authKey) //nolint:errcheck\n\n\t\tl := logger.CreateLogger()\n\t\tctx := WithConfigValues(t.Context())\n\t\t_, pctx := NewParsingContext(ctx, l, WithStrictControls(controls.New()))\n\t\tpctx.WorkingDir = filepath.Dir(secretFile)\n\t\t// Empty env — simulates auth-provider NOT having run (the original bug)\n\t\tpctx.Env = map[string]string{}\n\n\t\t_, err := sopsDecryptFileImpl(ctx, pctx, l, secretFile, \"json\", authRequiringDecryptFn)\n\t\trequire.Error(t, err,\n\t\t\t\"decrypt must fail without auth credentials — reproduces original issue #5515\")\n\t})\n\n\t// Subtest 4: Concurrent goroutines with DIFFERENT auth tokens are isolated.\n\t// Models production: multiple units decrypt in parallel, each with different\n\t// auth-provider credentials. The lock must ensure each sees its OWN token.\n\tt.Run(\"concurrent_different_creds_isolated\", func(t *testing.T) { //nolint:paralleltest // mutates process env\n\t\tconst numGoroutines = 5\n\n\t\tos.Unsetenv(authKey) //nolint:errcheck\n\n\t\tfiles := generateTestSecretFiles(t, numGoroutines)\n\n\t\tvar wg sync.WaitGroup\n\n\t\tbarrier := make(chan struct{})\n\n\t\tvar failures atomic.Int32\n\n\t\tctx := WithConfigValues(t.Context())\n\n\t\tfor i, f := range files {\n\t\t\twg.Add(1)\n\n\t\t\tgo func(idx int, filePath string) {\n\t\t\t\tdefer wg.Done()\n\n\t\t\t\t<-barrier\n\n\t\t\t\texpectedToken := fmt.Sprintf(\"token-%d\", idx)\n\n\t\t\t\t// Each goroutine's decryptFn verifies it sees ITS OWN token\n\t\t\t\ttokenCheckFn := func(path string, _ string) ([]byte, error) {\n\t\t\t\t\tactual := os.Getenv(authKey)\n\t\t\t\t\tif actual != expectedToken {\n\t\t\t\t\t\treturn nil, fmt.Errorf(\"goroutine %d: expected %q, got %q\", idx, expectedToken, actual)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn os.ReadFile(path)\n\t\t\t\t}\n\n\t\t\t\tl := logger.CreateLogger()\n\t\t\t\t_, pctx := NewParsingContext(ctx, l, WithStrictControls(controls.New()))\n\t\t\t\tpctx.WorkingDir = filepath.Dir(filePath)\n\t\t\t\tpctx.Env = map[string]string{authKey: expectedToken}\n\n\t\t\t\tresult, decryptErr := sopsDecryptFileImpl(ctx, pctx, l, filePath, \"json\", tokenCheckFn)\n\t\t\t\tif decryptErr != nil {\n\t\t\t\t\tt.Logf(\"goroutine %d failed: %v\", idx, decryptErr)\n\t\t\t\t\tfailures.Add(1)\n\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\texpectedPrefix := `{\"value\":\"secret-from-unit-`\n\t\t\t\tif len(result) < len(expectedPrefix) || result[:len(expectedPrefix)] != expectedPrefix {\n\t\t\t\t\tt.Logf(\"goroutine %d: wrong content: %s\", idx, result)\n\t\t\t\t\tfailures.Add(1)\n\t\t\t\t}\n\t\t\t}(i, f)\n\t\t}\n\n\t\tclose(barrier)\n\t\twg.Wait()\n\n\t\trequire.Zero(t, failures.Load(),\n\t\t\t\"all goroutines must see their own auth token during decrypt — env isolation failed\")\n\n\t\tassert.Empty(t, os.Getenv(authKey),\n\t\t\t\"process env must be clean after all concurrent decrypts\")\n\t})\n}\n"
  },
  {
    "path": "pkg/config/stack.go",
    "content": "package config\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/ctyhelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/worker\"\n\n\t\"github.com/hashicorp/go-getter/v2\"\n\n\t\"github.com/hashicorp/hcl/v2/hclsyntax\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/hashicorp/hcl/v2/hclwrite\"\n\n\t\"github.com/zclconf/go-cty/cty\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n)\n\nconst (\n\tStackDir      = \".terragrunt-stack\"\n\tvaluesFile    = \"terragrunt.values.hcl\"\n\tmanifestName  = \".terragrunt-stack-manifest\"\n\tunitDirPerm   = 0755\n\tvalueFilePerm = 0644\n)\n\n// StackConfigFile represents the structure of terragrunt.stack.hcl stack file.\ntype StackConfigFile struct {\n\tLocals *terragruntLocal `hcl:\"locals,block\"`\n\tStacks []*Stack         `hcl:\"stack,block\"`\n\tUnits  []*Unit          `hcl:\"unit,block\"`\n}\n\n// StackConfig represents the structure of terragrunt.stack.hcl stack file.\ntype StackConfig struct {\n\tLocals map[string]any\n\tStacks []*Stack\n\tUnits  []*Unit\n}\n\n// Unit represents unit from a stack file.\ntype Unit struct {\n\tNoStack      *bool      `hcl:\"no_dot_terragrunt_stack,attr\"`\n\tNoValidation *bool      `hcl:\"no_validation,attr\"`\n\tValues       *cty.Value `hcl:\"values,attr\"`\n\tName         string     `hcl:\",label\"`\n\tSource       string     `hcl:\"source,attr\"`\n\tPath         string     `hcl:\"path,attr\"`\n}\n\n// Stack represents the stack block in the configuration.\ntype Stack struct {\n\tNoStack      *bool      `hcl:\"no_dot_terragrunt_stack,attr\"`\n\tNoValidation *bool      `hcl:\"no_validation,attr\"`\n\tValues       *cty.Value `hcl:\"values,attr\"`\n\tName         string     `hcl:\",label\"`\n\tSource       string     `hcl:\"source,attr\"`\n\tPath         string     `hcl:\"path,attr\"`\n}\n\n// GenerateStackFile generates the Terragrunt stack configuration from the given stackFilePath,\n// reads necessary values, and generates units and stacks in the target directory.\n// It handles the creation of required directories and returns any errors encountered.\nfunc GenerateStackFile(ctx context.Context, l log.Logger, pctx *ParsingContext, pool *worker.Pool, stackFilePath string) error {\n\tstackSourceDir := filepath.Dir(stackFilePath)\n\n\tvalues, err := ReadValues(ctx, pctx, l, stackSourceDir)\n\tif err != nil {\n\t\treturn errors.Errorf(\"failed to read values from directory %s: %w\", stackSourceDir, err)\n\t}\n\n\tstackFile, err := ReadStackConfigFile(ctx, l, pctx, stackFilePath, values)\n\tif err != nil {\n\t\treturn errors.Errorf(\"Failed to read stack file %s in %s %w\", stackFilePath, stackSourceDir, err)\n\t}\n\n\tstackTargetDir := filepath.Join(stackSourceDir, StackDir)\n\n\tgenOpts := generateOpts{\n\t\trootWorkingDir:  pctx.RootWorkingDir,\n\t\tlogShowAbsPaths: pctx.Writers.LogShowAbsPaths,\n\t\tsourceMap:       pctx.SourceMap,\n\t\tnoStackValidate: pctx.NoStackValidate,\n\t\tstackConfigPath: pctx.TerragruntStackConfigPath,\n\t}\n\n\tif err := generateUnits(ctx, l, genOpts, pool, stackFilePath, stackSourceDir, stackTargetDir, stackFile.Units); err != nil {\n\t\treturn err\n\t}\n\n\tif err := generateStacks(ctx, l, genOpts, pool, stackFilePath, stackSourceDir, stackTargetDir, stackFile.Stacks); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// generateOpts holds the subset of options needed for stack/unit generation.\ntype generateOpts struct {\n\tsourceMap       map[string]string\n\trootWorkingDir  string\n\tstackConfigPath string\n\tlogShowAbsPaths bool\n\tnoStackValidate bool\n}\n\n// generateUnits iterates through a slice of Unit objects, generating each one by copying\n// source files to their destination paths and writing unit-specific values.\n// It logs the generating progress and returns any errors encountered during the operation.\nfunc generateUnits(ctx context.Context, l log.Logger, opts generateOpts, pool *worker.Pool, sourceFile, sourceDir, targetDir string, units []*Unit) error {\n\tfor _, unit := range units {\n\t\tpool.Submit(func() error {\n\t\t\titem := componentToGenerate{\n\t\t\t\tsourceDir:    sourceDir,\n\t\t\t\ttargetDir:    targetDir,\n\t\t\t\tname:         unit.Name,\n\t\t\t\tpath:         unit.Path,\n\t\t\t\tsource:       unit.Source,\n\t\t\t\tvalues:       unit.Values,\n\t\t\t\tnoStack:      unit.NoStack != nil && *unit.NoStack,\n\t\t\t\tnoValidation: unit.NoValidation != nil && *unit.NoValidation,\n\t\t\t\tkind:         unitKind,\n\t\t\t}\n\n\t\t\tl.Infof(\"Generating unit %s from %s\", unit.Name, util.RelPathForLog(opts.rootWorkingDir, sourceFile, opts.logShowAbsPaths))\n\n\t\t\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, \"stack_generate_unit\", map[string]any{\n\t\t\t\t\"stack_file\":  sourceFile,\n\t\t\t\t\"unit_name\":   unit.Name,\n\t\t\t\t\"unit_source\": unit.Source,\n\t\t\t\t\"unit_path\":   unit.Path,\n\t\t\t}, func(ctx context.Context) error {\n\t\t\t\treturn generateComponent(ctx, l, opts, &item)\n\t\t\t})\n\t\t})\n\t}\n\n\treturn nil\n}\n\n// generateStacks generates each stack by resolving its destination path and copying files from the source.\n// It logs each operation and returns early if any error is encountered.\nfunc generateStacks(ctx context.Context, l log.Logger, opts generateOpts, pool *worker.Pool, sourceFile, sourceDir, targetDir string, stacks []*Stack) error {\n\tfor _, stack := range stacks {\n\t\tpool.Submit(func() error {\n\t\t\titem := componentToGenerate{\n\t\t\t\tsourceDir:    sourceDir,\n\t\t\t\ttargetDir:    targetDir,\n\t\t\t\tname:         stack.Name,\n\t\t\t\tpath:         stack.Path,\n\t\t\t\tsource:       stack.Source,\n\t\t\t\tnoStack:      stack.NoStack != nil && *stack.NoStack,\n\t\t\t\tnoValidation: stack.NoValidation != nil && *stack.NoValidation,\n\t\t\t\tvalues:       stack.Values,\n\t\t\t\tkind:         stackKind,\n\t\t\t}\n\n\t\t\tl.Infof(\"Generating stack %s from %s\", stack.Name, util.RelPathForLog(opts.rootWorkingDir, sourceFile, opts.logShowAbsPaths))\n\n\t\t\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, \"stack_generate_stack\", map[string]any{\n\t\t\t\t\"stack_file\":   sourceFile,\n\t\t\t\t\"stack_name\":   stack.Name,\n\t\t\t\t\"stack_source\": stack.Source,\n\t\t\t\t\"stack_path\":   stack.Path,\n\t\t\t}, func(ctx context.Context) error {\n\t\t\t\treturn generateComponent(ctx, l, opts, &item)\n\t\t\t})\n\t\t})\n\t}\n\n\treturn nil\n}\n\ntype componentKind int\n\nconst (\n\tunitKind componentKind = iota\n\tstackKind\n)\n\n// componentToGenerate represents an item of work for generating a stack or unit.\n// It contains information about the source and target directories, the name and path of the item, the source URL or path,\n// and any associated values that need to be generated.\ntype componentToGenerate struct {\n\tvalues       *cty.Value\n\tsourceDir    string\n\ttargetDir    string\n\tname         string\n\tpath         string\n\tsource       string\n\tnoStack      bool\n\tnoValidation bool\n\tkind         componentKind\n}\n\n// generateComponent copies files from the source directory to the target destination and generates a corresponding values file.\nfunc generateComponent(ctx context.Context, l log.Logger, opts generateOpts, cmp *componentToGenerate) error {\n\tsource := cmp.source\n\t// Adjust source path using the provided source mapping configuration if available\n\tsource, err := adjustSourceWithMap(opts.sourceMap, source, opts.stackConfigPath)\n\tif err != nil {\n\t\treturn errors.Errorf(\"failed to adjust source %s: %w\", cmp.source, err)\n\t}\n\n\tif filepath.IsAbs(cmp.path) {\n\t\treturn errors.Errorf(\"path %s must be relative\", cmp.path)\n\t}\n\n\tkindStr := \"unit\"\n\tif cmp.kind == stackKind {\n\t\tkindStr = \"stack\"\n\t}\n\n\t// building destination path based on target directory\n\tdest := filepath.Join(cmp.targetDir, cmp.path)\n\n\t// validate destination path is within the stack directory\n\tabsDest := filepath.Clean(dest)\n\tabsStackDir := filepath.Clean(cmp.targetDir)\n\n\t// validate that the destination path is within the stack directory\n\tif !strings.HasPrefix(absDest, absStackDir) {\n\t\treturn errors.Errorf(\"%s destination path '%s' is outside of the stack directory '%s'\", cmp.name, absDest, absStackDir)\n\t}\n\n\tif cmp.noStack {\n\t\t// for noStack components, we copy the files to the base directory of the target directory\n\t\tdest = filepath.Join(filepath.Dir(cmp.targetDir), cmp.path)\n\t}\n\n\tl.Debugf(\"Generating: %s (%s) to %s\", cmp.name, source, dest)\n\n\tif err := copyFiles(ctx, l, cmp.name, cmp.sourceDir, source, dest); err != nil {\n\t\treturn errors.Errorf(\n\t\t\t\"Failed to fetch %s %s\\n\"+\n\t\t\t\t\"  Source:      %s\\n\"+\n\t\t\t\t\"  Destination: %s\\n\\n\"+\n\t\t\t\t\"Troubleshooting:\\n\"+\n\t\t\t\t\"  1. Check if your source path is correct relative to the stack file location\\n\"+\n\t\t\t\t\"  2. Verify the units or stacks directory exists at the expected location\\n\"+\n\t\t\t\t\"  3. Ensure you have proper permissions to read from source and write to destination\\n\\n\"+\n\t\t\t\t\"Original error: %w\",\n\t\t\tkindStr,\n\t\t\tcmp.name,\n\t\t\tsource,\n\t\t\tdest,\n\t\t\terr,\n\t\t)\n\t}\n\n\tskipValidation := false\n\n\tif cmp.noStack {\n\t\tl.Debugf(\"Skipping validation for %s %s due to no_stack flag\", kindStr, cmp.name)\n\n\t\tskipValidation = true\n\t}\n\n\tif cmp.noValidation {\n\t\tl.Debugf(\"Skipping validation for %s %s due to no_validation flag\", kindStr, cmp.name)\n\n\t\tskipValidation = true\n\t}\n\n\tif !skipValidation {\n\t\t// validate what was copied to the destination, don't do validation for special noStack components\n\t\texpectedFile := DefaultTerragruntConfigPath\n\n\t\tif cmp.kind == stackKind {\n\t\t\texpectedFile = DefaultStackFile\n\t\t}\n\n\t\tif err := validateTargetDir(kindStr, cmp.name, dest, expectedFile); err != nil {\n\t\t\tif opts.noStackValidate {\n\t\t\t\t// print warning if validation is skipped\n\t\t\t\tl.Warnf(\"Suppressing validation error for %s %s at path %s: expected %s to generate with %s file at root of generated directory.\", kindStr, cmp.name, cmp.targetDir, kindStr, expectedFile)\n\t\t\t} else {\n\t\t\t\treturn errors.Errorf(\"Validation failed for %s %s at path %s: expected %s to generate with %s file at root of generated directory.\", kindStr, cmp.name, cmp.targetDir, kindStr, expectedFile)\n\t\t\t}\n\t\t}\n\t}\n\n\t// generate values file\n\tif err := writeValues(l, cmp.values, dest); err != nil {\n\t\treturn errors.Errorf(\"failed to write values %v %w\", cmp.name, err)\n\t}\n\n\treturn nil\n}\n\n// copyFiles copies files or directories from a source to a destination path.\n//\n// The function checks if the source is local or remote. If local, it copies the\n// contents of the source directory to the destination. If remote, it fetches the\n// source and stores it in the destination directory.\nfunc copyFiles(ctx context.Context, l log.Logger, identifier, sourceDir, src, dest string) error {\n\tif isLocal(l, sourceDir, src) {\n\t\t// check if src is absolute path, if not, join with sourceDir\n\t\tvar localSrc string\n\n\t\tif filepath.IsAbs(src) {\n\t\t\tlocalSrc = src\n\t\t} else {\n\t\t\tlocalSrc = filepath.Join(sourceDir, src)\n\t\t}\n\n\t\tlocalSrc = filepath.Clean(localSrc)\n\n\t\tif err := util.CopyFolderContentsWithFilter(l, localSrc, dest, manifestName, func(absolutePath string) bool {\n\t\t\treturn true\n\t\t}); err != nil {\n\t\t\treturn errors.Errorf(\"Failed to copy %s to %s %w\", localSrc, dest, err)\n\t\t}\n\t} else {\n\t\tif err := os.MkdirAll(dest, os.ModePerm); err != nil {\n\t\t\treturn errors.Errorf(\"Failed to create directory %s for %s %w\", dest, identifier, err)\n\t\t}\n\n\t\tif _, err := getter.GetAny(ctx, dest, src); err != nil {\n\t\t\treturn errors.Errorf(\"Failed to fetch %s %s for %s %w\", src, dest, identifier, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// isLocal determines if a given source path is local or remote.\n//\n// It checks if the provided source file exists locally. If not, it checks if\n// the path is relative to the working directory. If that also fails, the function\n// attempts to detect the source's getter type and recognizes if it is a file URL.\nfunc isLocal(l log.Logger, workingDir, src string) bool {\n\t// check initially if the source is a local file\n\tif util.FileExists(src) {\n\t\treturn true\n\t}\n\n\tsrc = filepath.Join(workingDir, src)\n\tif util.FileExists(src) {\n\t\treturn true\n\t}\n\t// check path through getters\n\treq := &getter.Request{\n\t\tSrc: src,\n\t}\n\tfor _, g := range getter.Getters {\n\t\trecognized, err := getter.Detect(req, g)\n\t\tif err != nil {\n\t\t\tl.Debugf(\"Error detecting getter for %s: %v\", src, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tif recognized {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn strings.HasPrefix(req.Src, \"file://\")\n}\n\n// ReadOutputs retrieves the OpenTofu/Terraform output JSON for this unit, converts it into a map of cty.Values,\n// and logs the operation for debugging. It returns early in case of any errors during retrieval or conversion.\nfunc (u *Unit) ReadOutputs(ctx context.Context, l log.Logger, pctx *ParsingContext, unitDir string) (map[string]cty.Value, error) {\n\tconfigPath := filepath.Join(unitDir, DefaultTerragruntConfigPath)\n\tl.Debugf(\"Getting output from unit %s in %s\", u.Name, unitDir)\n\n\tjsonBytes, err := getOutputJSONWithCaching(ctx, pctx, l, configPath)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\toutputMap, err := TerraformOutputJSONToCtyValueMap(configPath, jsonBytes)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\treturn outputMap, nil\n}\n\n// ReadStackConfigFile reads and parses a Terragrunt stack configuration file from the given path.\n// It creates a parsing context, processes locals, and decodes the file into a StackConfig struct.\n// Validation is performed on the resulting config, and any encountered errors cause an early return.\nfunc ReadStackConfigFile(ctx context.Context, l log.Logger, pctx *ParsingContext, filePath string, values *cty.Value) (*StackConfig, error) {\n\tl.Debugf(\"Reading Terragrunt stack config file at %s\", filePath)\n\n\tstackPctx := pctx.Clone()\n\tstackPctx.TerragruntConfigPath = filePath\n\tstackPctx.OriginalTerragruntConfigPath = filePath\n\n\tfile, err := hclparse.NewParser(stackPctx.ParserOptions...).ParseFromFile(filePath)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\treturn ParseStackConfig(ctx, l, stackPctx, file, values)\n}\n\n// ReadStackConfigString reads and parses a Terragrunt stack configuration from a string.\nfunc ReadStackConfigString(\n\tctx context.Context,\n\tl log.Logger,\n\tpctx *ParsingContext,\n\tconfigPath string,\n\tconfigString string,\n\tvalues *cty.Value,\n) (*StackConfig, error) {\n\tif values != nil {\n\t\tpctx = pctx.WithValues(values)\n\t}\n\n\thclFile, err := hclparse.NewParser(pctx.ParserOptions...).ParseFromString(configString, configPath)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\treturn ParseStackConfig(ctx, l, pctx, hclFile, values)\n}\n\n// ParseStackConfig parses the stack configuration from the given file and values.\nfunc ParseStackConfig(ctx context.Context, l log.Logger, parser *ParsingContext, file *hclparse.File, values *cty.Value) (*StackConfig, error) {\n\tif values != nil {\n\t\tparser = parser.WithValues(values)\n\t}\n\n\tif err := processLocals(ctx, l, parser, file); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tevalParsingContext, err := createTerragruntEvalContext(ctx, parser, l, file.ConfigPath)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tconfig := &StackConfigFile{}\n\tif decodeErr := file.Decode(config, evalParsingContext); decodeErr != nil {\n\t\treturn nil, errors.New(decodeErr)\n\t}\n\n\tlocalsParsed := map[string]any{}\n\tif parser.Locals != nil {\n\t\tlocalsParsed, err = ctyhelper.ParseCtyValueToMap(*parser.Locals)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(err)\n\t\t}\n\t}\n\n\tstackConfig := &StackConfig{\n\t\tLocals: localsParsed,\n\t\tStacks: config.Stacks,\n\t\tUnits:  config.Units,\n\t}\n\n\tif err := ValidateStackConfig(config); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\treturn stackConfig, nil\n}\n\n// writeValues generates and writes values to a terragrunt.values.hcl file in the specified directory.\nfunc writeValues(l log.Logger, values *cty.Value, directory string) error {\n\tif values == nil {\n\t\tl.Debugf(\"No values to write in %s\", directory)\n\t\treturn nil\n\t}\n\t// Avoid panics if the provided values are in unsupported format\n\tif values.IsNull() {\n\t\tl.Debugf(\"Skipping writing values in %s: values is null\", directory)\n\t\treturn nil\n\t}\n\n\tif !values.IsWhollyKnown() {\n\t\tl.Debugf(\"Skipping writing values in %s: values are not fully known\", directory)\n\t\treturn nil\n\t}\n\n\tvalType := values.Type()\n\n\tif !valType.IsObjectType() && !valType.IsMapType() {\n\t\treturn errors.Errorf(\"writeValues: expected object or map, got %s\", valType.FriendlyName())\n\t}\n\n\tif directory == \"\" {\n\t\treturn errors.New(\"writeValues: unit directory path cannot be empty\")\n\t}\n\n\tif err := os.MkdirAll(directory, unitDirPerm); err != nil {\n\t\treturn errors.Errorf(\"failed to create directory %s: %w\", directory, err)\n\t}\n\n\tl.Debugf(\"Writing values file in %s\", directory)\n\tfilePath := filepath.Join(directory, valuesFile)\n\n\tfile := hclwrite.NewEmptyFile()\n\tbody := file.Body()\n\tbody.AppendUnstructuredTokens([]*hclwrite.Token{\n\t\t{\n\t\t\tType:  hclsyntax.TokenComment,\n\t\t\tBytes: []byte(\"# Auto-generated by the terragrunt.stack.hcl file by Terragrunt. Do not edit manually\\n\"),\n\t\t},\n\t})\n\n\t// Sort keys for deterministic output\n\tvalueMap := values.AsValueMap()\n\n\tkeys := make([]string, 0, len(valueMap))\n\tfor key := range valueMap {\n\t\tkeys = append(keys, key)\n\t}\n\n\t// Sort keys alphabetically\n\tsort.Strings(keys)\n\n\tfor _, key := range keys {\n\t\tbody.SetAttributeValue(key, valueMap[key])\n\t}\n\n\tif err := os.WriteFile(filePath, file.Bytes(), valueFilePerm); err != nil {\n\t\treturn errors.Errorf(\"failed to write values file %s: %w\", filePath, err)\n\t}\n\n\treturn nil\n}\n\n// ReadValues reads values from the terragrunt.values.hcl file in the specified directory.\nfunc ReadValues(ctx context.Context, pctx *ParsingContext, l log.Logger, directory string) (*cty.Value, error) {\n\tif directory == \"\" {\n\t\treturn nil, errors.New(\"ReadValues: directory path cannot be empty\")\n\t}\n\n\tfilePath := filepath.Join(directory, valuesFile)\n\n\tif util.FileNotExists(filePath) {\n\t\treturn nil, nil\n\t}\n\n\tl.Debugf(\"Reading Terragrunt stack values file at %s\", filePath)\n\n\tfile, err := hclparse.NewParser(pctx.ParserOptions...).ParseFromFile(filePath)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tevalParsingContext, err := createTerragruntEvalContext(ctx, pctx, l, file.ConfigPath)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tvalues := map[string]cty.Value{}\n\n\tif err := file.Decode(&values, evalParsingContext); err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tresult := cty.ObjectVal(values)\n\n\treturn &result, nil\n}\n\n// processLocals processes the locals block in the stack file.\nfunc processLocals(ctx context.Context, l log.Logger, parser *ParsingContext, file *hclparse.File) error {\n\tlocalsBlock, err := file.Blocks(MetadataLocals, false)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tif len(localsBlock) == 0 {\n\t\treturn nil\n\t}\n\n\tif len(localsBlock) > 1 {\n\t\treturn errors.New(fmt.Sprintf(\"up to one locals block is allowed per stack file, but found %d in %s\", len(localsBlock), file.ConfigPath))\n\t}\n\n\tattrs, err := localsBlock[0].JustAttributes()\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tevaluatedLocals := map[string]cty.Value{}\n\tevaluated := true\n\n\tfor iterations := 0; len(attrs) > 0 && evaluated; iterations++ {\n\t\tif iterations > MaxIter {\n\t\t\t// Reached maximum supported iterations, which is most likely an infinite loop bug so cut the iteration\n\t\t\t// short and return an error.\n\t\t\treturn errors.New(MaxIterError{})\n\t\t}\n\n\t\tvar evalErr error\n\n\t\tattrs, evaluatedLocals, evaluated, evalErr = attemptEvaluateLocals(\n\t\t\tctx,\n\t\t\tparser,\n\t\t\tl,\n\t\t\tfile,\n\t\t\tattrs,\n\t\t\tevaluatedLocals,\n\t\t)\n\t\tif evalErr != nil {\n\t\t\tl.Debugf(\"Encountered error while evaluating locals in file %s\", util.RelPathForLog(parser.RootWorkingDir, file.ConfigPath, parser.Writers.LogShowAbsPaths))\n\n\t\t\treturn errors.New(evalErr)\n\t\t}\n\t}\n\n\tlocalsAsCtyVal, err := ConvertValuesMapToCtyVal(evaluatedLocals)\n\tif err != nil {\n\t\treturn errors.New(err)\n\t}\n\n\tparser.Locals = &localsAsCtyVal\n\n\treturn nil\n}\n\n// validateTargetDir target destination directory.\nfunc validateTargetDir(kind, name, destDir, expectedFile string) error {\n\texpectedPath := filepath.Join(destDir, expectedFile)\n\n\tinfo, err := os.Stat(expectedPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"%s '%s': expected file '%s' not found in target directory '%s': %w\", kind, name, expectedFile, destDir, err)\n\t}\n\n\tif info.IsDir() {\n\t\treturn fmt.Errorf(\"%s '%s': expected file '%s' is a directory, not a file\", kind, name, expectedFile)\n\t}\n\n\treturn nil\n}\n\n// GetUnitDir returns the directory path for a unit based on its no_dot_terragrunt_stack setting.\nfunc GetUnitDir(dir string, unit *Unit) string {\n\tif unit.NoStack != nil && *unit.NoStack {\n\t\treturn filepath.Join(dir, unit.Path)\n\t}\n\n\treturn filepath.Join(dir, StackDir, unit.Path)\n}\n"
  },
  {
    "path": "pkg/config/stack_test.go",
    "content": "package config_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParseTerragruntStackConfig(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nlocals {\n\tproject = \"my-project\"\n}\n\nunit \"unit1\" {\n\tsource = \"units/app1\"\n\tpath   = \"unit1\"\n}\n\nstack \"projects\" {\n\tsource = \"../projects\"\n\tpath = \"projects\"\n}\n\n`\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tterragruntStackConfig, err := config.ReadStackConfigString(ctx, logger.CreateLogger(), pctx, config.DefaultStackFile, cfg, nil)\n\trequire.NoError(t, err)\n\n\tassert.NotNil(t, terragruntStackConfig)\n\n\tassert.NotNil(t, terragruntStackConfig.Locals)\n\tassert.Len(t, terragruntStackConfig.Locals, 1)\n\tassert.Equal(t, \"my-project\", terragruntStackConfig.Locals[\"project\"])\n\n\tassert.NotNil(t, terragruntStackConfig.Units)\n\tassert.Len(t, terragruntStackConfig.Units, 1)\n\n\tunit := terragruntStackConfig.Units[0]\n\tassert.Equal(t, \"unit1\", unit.Name)\n\tassert.Equal(t, \"units/app1\", unit.Source)\n\tassert.Equal(t, \"unit1\", unit.Path)\n\tassert.Nil(t, unit.NoStack)\n\n\tassert.NotNil(t, terragruntStackConfig.Stacks)\n\tassert.Len(t, terragruntStackConfig.Stacks, 1)\n\n\tstack := terragruntStackConfig.Stacks[0]\n\tassert.Equal(t, \"projects\", stack.Name)\n\tassert.Equal(t, \"../projects\", stack.Source)\n\tassert.Equal(t, \"projects\", stack.Path)\n\tassert.Nil(t, stack.NoStack)\n}\n\nfunc TestParseTerragruntStackConfigComplex(t *testing.T) {\n\tt.Parallel()\n\n\tcfg := `\nlocals {\n    project = \"my-project\"\n    env     = \"dev\"\n}\n\nunit \"unit1\" {\n    source = \"units/app1\"\n    path   = \"unit1\"\n    no_dot_terragrunt_stack = true\n    values = {\n        name = \"app1\"\n        port = 8080\n    }\n}\n\nunit \"unit2\" {\n    source = \"units/app2\"\n    path   = \"unit2\"\n    no_dot_terragrunt_stack = false\n    values = {\n        name = \"app2\"\n        port = 9090\n    }\n}\n\nstack \"projects\" {\n    source = \"../projects\"\n    path = \"projects\"\n    values = {\n        region = \"us-west-2\"\n    }\n}\n\nstack \"network\" {\n    source = \"../network\"\n    path = \"network\"\n    no_dot_terragrunt_stack = true\n}\n`\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\tterragruntStackConfig, err := config.ReadStackConfigString(ctx, logger.CreateLogger(), pctx, config.DefaultStackFile, cfg, nil)\n\trequire.NoError(t, err)\n\n\t// Check that config is not nil\n\tassert.NotNil(t, terragruntStackConfig)\n\n\tassert.NotNil(t, terragruntStackConfig.Locals)\n\tassert.Len(t, terragruntStackConfig.Locals, 2)\n\tassert.Equal(t, \"my-project\", terragruntStackConfig.Locals[\"project\"])\n\tassert.Equal(t, \"dev\", terragruntStackConfig.Locals[\"env\"])\n\n\tassert.NotNil(t, terragruntStackConfig.Units)\n\tassert.Len(t, terragruntStackConfig.Units, 2)\n\n\tunit1 := terragruntStackConfig.Units[0]\n\tassert.Equal(t, \"unit1\", unit1.Name)\n\tassert.Equal(t, \"units/app1\", unit1.Source)\n\tassert.Equal(t, \"unit1\", unit1.Path)\n\tassert.NotNil(t, unit1.NoStack)\n\tassert.True(t, *unit1.NoStack)\n\tassert.NotNil(t, unit1.Values)\n\n\tunit2 := terragruntStackConfig.Units[1]\n\tassert.Equal(t, \"unit2\", unit2.Name)\n\tassert.Equal(t, \"units/app2\", unit2.Source)\n\tassert.Equal(t, \"unit2\", unit2.Path)\n\tassert.NotNil(t, unit2.NoStack)\n\tassert.False(t, *unit2.NoStack)\n\tassert.NotNil(t, unit2.Values)\n\n\tassert.NotNil(t, terragruntStackConfig.Stacks)\n\tassert.Len(t, terragruntStackConfig.Stacks, 2)\n\n\tstack1 := terragruntStackConfig.Stacks[0]\n\tassert.Equal(t, \"projects\", stack1.Name)\n\tassert.Equal(t, \"../projects\", stack1.Source)\n\tassert.Equal(t, \"projects\", stack1.Path)\n\tassert.Nil(t, stack1.NoStack)\n\tassert.NotNil(t, stack1.Values)\n\n\tstack2 := terragruntStackConfig.Stacks[1]\n\tassert.Equal(t, \"network\", stack2.Name)\n\tassert.Equal(t, \"../network\", stack2.Source)\n\tassert.Equal(t, \"network\", stack2.Path)\n\tassert.NotNil(t, stack2.NoStack)\n\tassert.True(t, *stack2.NoStack)\n}\n\nfunc TestParseTerragruntStackConfigInvalidSyntax(t *testing.T) {\n\tt.Parallel()\n\n\tinvalidCfg := `\nlocals {\n\tproject = \"my-project\n}\n`\n\tctx, pctx := newTestParsingContext(t, config.DefaultTerragruntConfigPath)\n\t_, err := config.ReadStackConfigString(ctx, logger.CreateLogger(), pctx, config.DefaultStackFile, invalidCfg, nil)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"Invalid multi-line string\")\n}\n\nfunc TestWriteValuesSortsKeys(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\tvaluesFilePath := setupTestFiles(t, tmpDir)\n\n\t// Helper function to read and return the values file content\n\treadValuesFile := func() string {\n\t\tcontent, err := os.ReadFile(valuesFilePath)\n\t\trequire.NoError(t, err)\n\n\t\treturn string(content)\n\t}\n\n\t// Run multiple generations to test for deterministic behavior\n\tconst numIterations = 5\n\n\tgenerationContents := make([]string, 0, numIterations)\n\n\tfor iteration := range numIterations {\n\t\t// Clean up any existing stack directory\n\t\tstackDir := filepath.Join(tmpDir, \".terragrunt-stack\")\n\t\tos.RemoveAll(stackDir)\n\n\t\t// Generate the stack\n\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+tmpDir)\n\t\trequire.NoError(t, err)\n\t\trequire.FileExists(t, valuesFilePath)\n\n\t\tcontent := readValuesFile()\n\t\tgenerationContents = append(generationContents, content)\n\n\t\tt.Logf(\"Generation %d content:\\n%s\\n\", iteration+1, content)\n\t}\n\n\t// Extract only the complex verification logic to reduce cyclomatic complexity\n\tverifyDeterministicSortedOutput(t, generationContents)\n}\n\n// setupTestFiles creates the test environment and returns the values file path.\nfunc setupTestFiles(t *testing.T, tmpDir string) string {\n\tt.Helper()\n\n\t// Create a test stack configuration with more values in non-alphabetical order\n\tstackConfig := `\nunit \"test_unit\" {\n\tsource = \"./unit\"\n\tpath   = \"test_unit\"\n\tvalues = {\n\t\tzzz_last    = \"should be last\"\n\t\taaa_first   = \"should be first\"\n\t\tmmm_middle  = \"should be middle\"\n\t\tbeta        = 42\n\t\tgamma       = true\n\t\tdelta       = [\"a\", \"b\"]\n\t\tzebra       = \"animal\"\n\t\talpha       = \"letter\"\n\t\tomega       = \"end\"\n\t\tcharlie     = \"nato\"\n\t}\n}\n`\n\n\t// Create the stack file\n\tstackFilePath := filepath.Join(tmpDir, config.DefaultStackFile)\n\terr := os.WriteFile(stackFilePath, []byte(stackConfig), 0644)\n\trequire.NoError(t, err)\n\n\t// Create a simple unit directory with minimal terragrunt config\n\tunitDir := filepath.Join(tmpDir, \"unit\")\n\terr = os.MkdirAll(unitDir, 0755)\n\trequire.NoError(t, err)\n\n\tunitConfig := `\nterraform {\n\tsource = \".\"\n}\n`\n\tunitConfigPath := filepath.Join(unitDir, config.DefaultTerragruntConfigPath)\n\terr = os.WriteFile(unitConfigPath, []byte(unitConfig), 0644)\n\trequire.NoError(t, err)\n\n\t// Create a minimal main.tf in the unit\n\tmainTf := `\nresource \"local_file\" \"test\" {\n\tcontent  = \"test\"\n\tfilename = \"test.txt\"\n}\n`\n\tmainTfPath := filepath.Join(unitDir, \"main.tf\")\n\terr = os.WriteFile(mainTfPath, []byte(mainTf), 0644)\n\trequire.NoError(t, err)\n\n\treturn filepath.Join(tmpDir, \".terragrunt-stack\", \"test_unit\", \"terragrunt.values.hcl\")\n}\n\n// verifyDeterministicSortedOutput checks that all generations are identical and sorted.\nfunc verifyDeterministicSortedOutput(t *testing.T, generationContents []string) {\n\tt.Helper()\n\n\t// Check if all generations produced identical output\n\tallIdentical := true\n\n\tfor i := 1; i < len(generationContents); i++ {\n\t\tif generationContents[i] != generationContents[0] {\n\t\t\tallIdentical = false\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif !allIdentical {\n\t\tt.Logf(\"Non-deterministic behavior detected! Generations produced different output:\")\n\n\t\tfor i, content := range generationContents {\n\t\t\tt.Logf(\"Generation %d:\\n%s\\n\", i+1, content)\n\t\t}\n\n\t\tassert.True(t, allIdentical, \"Stack generation should be deterministic - all runs should produce identical values files\")\n\n\t\treturn\n\t}\n\n\tt.Logf(\"All generations produced identical output - checking if it's sorted...\")\n\n\t// Now test the actual content and ordering using the first generation\n\tcontentStr := generationContents[0]\n\n\t// Check if the keys appear in alphabetical order\n\tkeys := []string{\"aaa_first\", \"alpha\", \"beta\", \"charlie\", \"delta\", \"gamma\", \"mmm_middle\", \"omega\", \"zebra\", \"zzz_last\"}\n\n\tpositions := make([]int, len(keys))\n\tfor i, key := range keys {\n\t\tpositions[i] = strings.Index(contentStr, key)\n\t\tif positions[i] == -1 {\n\t\t\tt.Fatalf(\"Key %s not found in generated content\", key)\n\t\t}\n\t}\n\n\t// Check if positions are in ascending order (alphabetical)\n\tkeysInOrder := true\n\n\tfor i := 1; i < len(positions); i++ {\n\t\tif positions[i] < positions[i-1] {\n\t\t\tkeysInOrder = false\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\tt.Logf(\"Key positions: %v\", positions)\n\tt.Logf(\"Keys in alphabetical order: %v\", keysInOrder)\n\n\tif !keysInOrder {\n\t\tassert.True(t, keysInOrder, \"Keys should appear in alphabetical order for deterministic output\")\n\t} else {\n\t\tt.Logf(\"Keys are in alphabetical order - sorting implementation is working!\")\n\t}\n}\n\nfunc TestWriteValuesSkipsWhenNilOrNull(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create two units: one without values, one with explicit null values\n\tstackConfig := `\nunit \"u1\" {\n  source = \"./unit1\"\n  path   = \"u1\"\n}\n\nunit \"u2\" {\n  source = \"./unit2\"\n  path   = \"u2\"\n  values = null\n}\n`\n\n\tstackFilePath := filepath.Join(tmpDir, config.DefaultStackFile)\n\trequire.NoError(t, os.WriteFile(stackFilePath, []byte(stackConfig), 0644))\n\n\t// Unit 1\n\tunit1Dir := filepath.Join(tmpDir, \"unit1\")\n\trequire.NoError(t, os.MkdirAll(unit1Dir, 0755))\n\n\tunit1Config := `\nterraform {\n  source = \".\"\n}\n`\n\tunit1ConfigPath := filepath.Join(unit1Dir, config.DefaultTerragruntConfigPath)\n\trequire.NoError(t, os.WriteFile(unit1ConfigPath, []byte(unit1Config), 0644))\n\n\tunit1MainTf := \"\"\n\trequire.NoError(t, os.WriteFile(filepath.Join(unit1Dir, \"main.tf\"), []byte(unit1MainTf), 0644))\n\n\t// Unit 2\n\tunit2Dir := filepath.Join(tmpDir, \"unit2\")\n\trequire.NoError(t, os.MkdirAll(unit2Dir, 0755))\n\n\tunit2Config := unit1Config\n\tunit2ConfigPath := filepath.Join(unit2Dir, config.DefaultTerragruntConfigPath)\n\trequire.NoError(t, os.WriteFile(unit2ConfigPath, []byte(unit2Config), 0644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(unit2Dir, \"main.tf\"), []byte(unit1MainTf), 0644))\n\n\t// Generate the stack\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+tmpDir)\n\trequire.NoError(t, err)\n\n\t// Ensure values files are not created for both units\n\tvaluesU1 := filepath.Join(tmpDir, \".terragrunt-stack\", \"u1\", \"terragrunt.values.hcl\")\n\tvaluesU2 := filepath.Join(tmpDir, \".terragrunt-stack\", \"u2\", \"terragrunt.values.hcl\")\n\n\tassert.NoFileExists(t, valuesU1)\n\tassert.NoFileExists(t, valuesU2)\n}\n\nfunc TestWriteValuesRejectsNonObjectValues(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tstackConfig := `\nunit \"bad\" {\n  source = \"./unit\"\n  path   = \"bad\"\n  values = 666\n}\n`\n\n\tstackFilePath := filepath.Join(tmpDir, config.DefaultStackFile)\n\trequire.NoError(t, os.WriteFile(stackFilePath, []byte(stackConfig), 0644))\n\n\tunitDir := filepath.Join(tmpDir, \"unit\")\n\trequire.NoError(t, os.MkdirAll(unitDir, 0755))\n\n\tunitConfig := `\nterraform {\n  source = \".\"\n}\n`\n\tunitConfigPath := filepath.Join(unitDir, config.DefaultTerragruntConfigPath)\n\trequire.NoError(t, os.WriteFile(unitConfigPath, []byte(unitConfig), 0644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(unitDir, \"main.tf\"), []byte(\"\"), 0644))\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+tmpDir)\n\tif err == nil {\n\t\t// If no error, that's a failure for this test\n\t\tt.Fatalf(\"expected error when values is non-object, got none. stdout=%s stderr=%s\", stdout, stderr)\n\t}\n\n\tcombined := stdout + \"\\n\" + stderr + \"\\n\" + err.Error()\n\tassert.Contains(t, combined, \"expected object or map\")\n}\n"
  },
  {
    "path": "pkg/config/stack_validation.go",
    "content": "package config\n\nimport (\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\n// ValidateStackConfig validates a StackConfigFile instance according to the rules:\n// - Unit name, source, and path shouldn't be empty\n// - Unit names should be unique\n// - Units shouldn't have duplicate paths\n// - Stack name, source, and path shouldn't be empty\n// - Stack names should be unique\n// - Stack shouldn't have duplicate paths\nfunc ValidateStackConfig(config *StackConfigFile) error {\n\tif config == nil {\n\t\treturn errors.New(\"stack config cannot be nil\")\n\t}\n\n\t// Check if we have any units or stacks\n\tif len(config.Units) == 0 && len(config.Stacks) == 0 {\n\t\treturn errors.New(\"stack config must contain at least one unit or stack\")\n\t}\n\n\tvalidationErrors := &errors.MultiError{}\n\n\tif err := validateUnits(config.Units); err != nil {\n\t\tvalidationErrors = validationErrors.Append(err)\n\t}\n\n\tif err := validateStacks(config.Stacks); err != nil {\n\t\tvalidationErrors = validationErrors.Append(err)\n\t}\n\n\treturn validationErrors.ErrorOrNil()\n}\n\n// validateUnits validates all units in the configuration\nfunc validateUnits(units []*Unit) error {\n\treturn validateConfigElementsGeneric(units, \"unit\", func(element any, i int) (string, string, string) {\n\t\tunit := element.(*Unit)\n\t\treturn unit.Name, unit.Path, unit.Source\n\t})\n}\n\n// validateStacks validates all stacks in the configuration\nfunc validateStacks(stacks []*Stack) error {\n\treturn validateConfigElementsGeneric(stacks, \"stack\", func(element any, i int) (string, string, string) {\n\t\tstack := element.(*Stack)\n\t\treturn stack.Name, stack.Path, stack.Source\n\t})\n}\n\n// validateConfigElementsGeneric is a generic function to validate configuration elements\n// It takes a slice of elements, the element type name, and a function to extract name, path, and source from an element\nfunc validateConfigElementsGeneric(elements any, elementType string, getValues func(element any, index int) (name, path, source string)) error {\n\tvalidationErrors := &errors.MultiError{}\n\n\tvar slice []any\n\n\t// Convert the slice to a slice of interface{}\n\tswitch v := elements.(type) {\n\tcase []*Unit:\n\t\tslice = make([]any, len(v))\n\t\tfor i, unit := range v {\n\t\t\tslice[i] = unit\n\t\t}\n\tcase []*Stack:\n\t\tslice = make([]any, len(v))\n\t\tfor i, stack := range v {\n\t\t\tslice[i] = stack\n\t\t}\n\tdefault:\n\t\treturn errors.New(\"unknown element type\")\n\t}\n\n\tnames := make(map[string]bool, len(slice))\n\tpaths := make(map[string]bool, len(slice))\n\n\tfor i, element := range slice {\n\t\tif element == nil {\n\t\t\tvalidationErrors = validationErrors.Append(errors.Errorf(\"%s at index %d is nil\", elementType, i))\n\t\t\tcontinue\n\t\t}\n\n\t\tname, path, source := getValues(element, i)\n\t\tname = strings.TrimSpace(name)\n\t\tpath = strings.TrimSpace(path)\n\t\tsource = strings.TrimSpace(source)\n\n\t\t// Validate name, source, and path\n\t\tif name == \"\" {\n\t\t\tvalidationErrors = validationErrors.Append(errors.Errorf(\"%s at index %d has empty name\", elementType, i))\n\t\t}\n\n\t\tif source == \"\" {\n\t\t\tvalidationErrors = validationErrors.Append(errors.Errorf(\"%s '%s' has empty source\", elementType, name))\n\t\t}\n\n\t\tif path == \"\" {\n\t\t\tvalidationErrors = validationErrors.Append(errors.Errorf(\"%s '%s' has empty path\", elementType, name))\n\t\t}\n\n\t\t// Check for duplicates\n\t\tif names[name] {\n\t\t\tvalidationErrors = validationErrors.Append(errors.Errorf(\"duplicate %s name found: '%s'\", elementType, name))\n\t\t}\n\n\t\tif paths[path] {\n\t\t\tvalidationErrors = validationErrors.Append(errors.Errorf(\"duplicate %s path found: '%s'\", elementType, path))\n\t\t}\n\n\t\t// Save non-empty values for uniqueness check\n\t\tif name != \"\" {\n\t\t\tnames[name] = true\n\t\t}\n\n\t\tif path != \"\" {\n\t\t\tpaths[path] = true\n\t\t}\n\t}\n\n\treturn validationErrors.ErrorOrNil()\n}\n"
  },
  {
    "path": "pkg/config/stack_validation_test.go",
    "content": "package config_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestValidateStackConfig(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname    string\n\t\tconfig  *config.StackConfigFile\n\t\twantErr string\n\t}{\n\t\t{\n\t\t\tname: \"valid config\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tUnits: []*config.Unit{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"unit1\",\n\t\t\t\t\t\tSource: \"source1\",\n\t\t\t\t\t\tPath:   \"path1\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"unit2\",\n\t\t\t\t\t\tSource: \"source2\",\n\t\t\t\t\t\tPath:   \"path2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"empty config\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tUnits: []*config.Unit{},\n\t\t\t},\n\t\t\twantErr: \"stack config must contain at least one unit\",\n\t\t},\n\t\t{\n\t\t\tname: \"empty unit name\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tUnits: []*config.Unit{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"\",\n\t\t\t\t\t\tSource: \"source1\",\n\t\t\t\t\t\tPath:   \"path1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"unit at index 0 has empty name\",\n\t\t},\n\t\t{\n\t\t\tname: \"whitespace unit name\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tUnits: []*config.Unit{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"  \",\n\t\t\t\t\t\tSource: \"source1\",\n\t\t\t\t\t\tPath:   \"path1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"unit at index 0 has empty name\",\n\t\t},\n\t\t{\n\t\t\tname: \"empty unit source\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tUnits: []*config.Unit{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"unit1\",\n\t\t\t\t\t\tSource: \"\",\n\t\t\t\t\t\tPath:   \"path1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"unit 'unit1' has empty source\",\n\t\t},\n\t\t{\n\t\t\tname: \"whitespace unit source\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tUnits: []*config.Unit{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"unit1\",\n\t\t\t\t\t\tSource: \"   \",\n\t\t\t\t\t\tPath:   \"path1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"unit 'unit1' has empty source\",\n\t\t},\n\t\t{\n\t\t\tname: \"empty unit path\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tUnits: []*config.Unit{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"unit1\",\n\t\t\t\t\t\tSource: \"source1\",\n\t\t\t\t\t\tPath:   \"\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"unit 'unit1' has empty path\",\n\t\t},\n\t\t{\n\t\t\tname: \"whitespace unit path\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tUnits: []*config.Unit{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"unit1\",\n\t\t\t\t\t\tSource: \"source1\",\n\t\t\t\t\t\tPath:   \"  \",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"unit 'unit1' has empty path\",\n\t\t},\n\t\t{\n\t\t\tname: \"duplicate unit names\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tUnits: []*config.Unit{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"unit1\",\n\t\t\t\t\t\tSource: \"source1\",\n\t\t\t\t\t\tPath:   \"path1\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"unit1\",\n\t\t\t\t\t\tSource: \"source2\",\n\t\t\t\t\t\tPath:   \"path2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"duplicate unit name found: 'unit1'\",\n\t\t},\n\t\t{\n\t\t\tname: \"duplicate unit paths\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tUnits: []*config.Unit{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"unit1\",\n\t\t\t\t\t\tSource: \"source1\",\n\t\t\t\t\t\tPath:   \"path1\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"unit2\",\n\t\t\t\t\t\tSource: \"source2\",\n\t\t\t\t\t\tPath:   \"path1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"duplicate unit path found: 'path1'\",\n\t\t},\n\n\t\t{\n\t\t\tname: \"valid config with stacks\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tStacks: []*config.Stack{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"stack1\",\n\t\t\t\t\t\tSource: \"source1\",\n\t\t\t\t\t\tPath:   \"path1\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"stack2\",\n\t\t\t\t\t\tSource: \"source2\",\n\t\t\t\t\t\tPath:   \"path2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"empty stack name\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tStacks: []*config.Stack{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"\",\n\t\t\t\t\t\tSource: \"source1\",\n\t\t\t\t\t\tPath:   \"path1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"stack at index 0 has empty name\",\n\t\t},\n\t\t{\n\t\t\tname: \"whitespace stack name\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tStacks: []*config.Stack{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"  \",\n\t\t\t\t\t\tSource: \"source1\",\n\t\t\t\t\t\tPath:   \"path1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"stack at index 0 has empty name\",\n\t\t},\n\t\t{\n\t\t\tname: \"empty stack source\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tStacks: []*config.Stack{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"stack1\",\n\t\t\t\t\t\tSource: \"\",\n\t\t\t\t\t\tPath:   \"path1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"stack 'stack1' has empty source\",\n\t\t},\n\t\t{\n\t\t\tname: \"whitespace stack source\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tStacks: []*config.Stack{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"stack1\",\n\t\t\t\t\t\tSource: \"   \",\n\t\t\t\t\t\tPath:   \"path1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"stack 'stack1' has empty source\",\n\t\t},\n\t\t{\n\t\t\tname: \"empty stack path\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tStacks: []*config.Stack{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"stack1\",\n\t\t\t\t\t\tSource: \"source1\",\n\t\t\t\t\t\tPath:   \"\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"stack 'stack1' has empty path\",\n\t\t},\n\t\t{\n\t\t\tname: \"whitespace stack path\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tStacks: []*config.Stack{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"stack1\",\n\t\t\t\t\t\tSource: \"source1\",\n\t\t\t\t\t\tPath:   \"  \",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"stack 'stack1' has empty path\",\n\t\t},\n\t\t{\n\t\t\tname: \"duplicate stack names\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tStacks: []*config.Stack{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"stack1\",\n\t\t\t\t\t\tSource: \"source1\",\n\t\t\t\t\t\tPath:   \"path1\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"stack1\",\n\t\t\t\t\t\tSource: \"source2\",\n\t\t\t\t\t\tPath:   \"path2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"duplicate stack name found: 'stack1'\",\n\t\t},\n\t\t{\n\t\t\tname: \"duplicate stack paths\",\n\t\t\tconfig: &config.StackConfigFile{\n\t\t\t\tStacks: []*config.Stack{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"stack1\",\n\t\t\t\t\t\tSource: \"source1\",\n\t\t\t\t\t\tPath:   \"path1\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName:   \"stack2\",\n\t\t\t\t\t\tSource: \"source2\",\n\t\t\t\t\t\tPath:   \"path1\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"duplicate stack path found: 'path1'\",\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\terr := config.ValidateStackConfig(tt.config)\n\t\t\tif tt.wantErr != \"\" {\n\t\t\t\tassert.Contains(t, err.Error(), tt.wantErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/config/telemetry.go",
    "content": "// Package config provides telemetry support for configuration parsing operations.\npackage config\n\nimport (\n\t\"context\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"go.opentelemetry.io/otel/attribute\"\n\t\"go.opentelemetry.io/otel/trace\"\n)\n\n// Telemetry operation names for config parsing operations.\nconst (\n\tTelemetryOpParseConfigFile   = \"parse_config_file\"\n\tTelemetryOpParseBaseBlocks   = \"parse_base_blocks\"\n\tTelemetryOpParseDependencies = \"parse_dependencies\"\n\tTelemetryOpParseDependency   = \"parse_dependency\"\n\tTelemetryOpParseConfigDecode = \"parse_config_decode\"\n\tTelemetryOpParseIncludeMerge = \"parse_include_merge\"\n)\n\n// Telemetry attribute keys for config parsing operations.\nconst (\n\tAttrConfigPath       = \"config_path\"\n\tAttrWorkingDir       = \"working_dir\"\n\tAttrIsPartial        = \"is_partial\"\n\tAttrDecodeList       = \"decode_list\"\n\tAttrCacheHit         = \"cache_hit\"\n\tAttrIncludeFromChild = \"include_from_child\"\n\tAttrIncludeChildPath = \"include_child_path\"\n\tAttrHasIncludes      = \"has_includes\"\n\tAttrIncludeCount     = \"include_count\"\n\tAttrIncludePaths     = \"include_paths\"\n\tAttrDependencyCount  = \"dependency_count\"\n\tAttrDependencyNames  = \"dependency_names\"\n\tAttrDependencyName   = \"dependency_name\"\n\tAttrDependencyPath   = \"dependency_path\"\n\tAttrLocalsCount      = \"locals_count\"\n\tAttrLocalsNames      = \"locals_names\"\n\tAttrFeatureFlagCount = \"feature_flag_count\"\n\tAttrFeatureFlagNames = \"feature_flag_names\"\n\tAttrSkipOutputs      = \"skip_outputs_resolution\"\n)\n\n// TraceParseConfigFile wraps a config file parsing operation with telemetry.\nfunc TraceParseConfigFile(\n\tctx context.Context,\n\tconfigPath string,\n\tworkingDir string,\n\tisPartial bool,\n\tdecodeList []PartialDecodeSectionType,\n\tincludeFromChild *IncludeConfig,\n\tcacheHit bool,\n\tfn func(ctx context.Context) error,\n) error {\n\tattrs := map[string]any{\n\t\tAttrConfigPath:       configPath,\n\t\tAttrWorkingDir:       workingDir,\n\t\tAttrIsPartial:        isPartial,\n\t\tAttrCacheHit:         cacheHit,\n\t\tAttrIncludeFromChild: includeFromChild != nil,\n\t}\n\n\tif len(decodeList) > 0 {\n\t\tattrs[AttrDecodeList] = formatDecodeList(decodeList)\n\t}\n\n\tif includeFromChild != nil {\n\t\tattrs[AttrIncludeChildPath] = includeFromChild.Path\n\t}\n\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpParseConfigFile, attrs, fn)\n}\n\n// TraceParseBaseBlocks wraps base blocks parsing with telemetry.\nfunc TraceParseBaseBlocks(\n\tctx context.Context,\n\tl log.Logger,\n\tconfigPath string,\n\tfn func(ctx context.Context) (*DecodedBaseBlocks, error),\n) (*DecodedBaseBlocks, error) {\n\tvar (\n\t\tresult    *DecodedBaseBlocks\n\t\tresultErr error\n\t)\n\n\terr := telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpParseBaseBlocks, map[string]any{\n\t\tAttrConfigPath: configPath,\n\t}, func(childCtx context.Context) error {\n\t\tresult, resultErr = fn(childCtx)\n\t\treturn resultErr\n\t})\n\tif err != nil {\n\t\tl.Warnf(\"Telemetry error during base blocks parsing: %v\", err)\n\t}\n\n\treturn result, resultErr\n}\n\n// TraceParseBaseBlocksResult adds result attributes to the current span from context.\nfunc TraceParseBaseBlocksResult(\n\tctx context.Context,\n\tconfigPath string,\n\tbaseBlocks *DecodedBaseBlocks,\n) {\n\tspan := trace.SpanFromContext(ctx)\n\tif span == nil || !span.IsRecording() {\n\t\treturn\n\t}\n\n\tattrs := []attribute.KeyValue{\n\t\tattribute.String(AttrConfigPath, configPath),\n\t}\n\n\tif baseBlocks != nil {\n\t\t// Include information\n\t\tif baseBlocks.TrackInclude != nil && len(baseBlocks.TrackInclude.CurrentList) > 0 {\n\t\t\tattrs = append(attrs,\n\t\t\t\tattribute.Bool(AttrHasIncludes, true),\n\t\t\t\tattribute.Int(AttrIncludeCount, len(baseBlocks.TrackInclude.CurrentList)),\n\t\t\t\tattribute.String(AttrIncludePaths, formatIncludePaths(baseBlocks.TrackInclude.CurrentList)),\n\t\t\t)\n\t\t} else {\n\t\t\tattrs = append(attrs,\n\t\t\t\tattribute.Bool(AttrHasIncludes, false),\n\t\t\t\tattribute.Int(AttrIncludeCount, 0),\n\t\t\t)\n\t\t}\n\n\t\t// Locals information\n\t\tif baseBlocks.Locals != nil && !baseBlocks.Locals.IsNull() {\n\t\t\tlocalsMap := baseBlocks.Locals.AsValueMap()\n\t\t\tattrs = append(attrs,\n\t\t\t\tattribute.Int(AttrLocalsCount, len(localsMap)),\n\t\t\t\tattribute.String(AttrLocalsNames, formatMapKeys(localsMap)),\n\t\t\t)\n\t\t} else {\n\t\t\tattrs = append(attrs, attribute.Int(AttrLocalsCount, 0))\n\t\t}\n\n\t\t// Feature flags information\n\t\tif baseBlocks.FeatureFlags != nil && !baseBlocks.FeatureFlags.IsNull() {\n\t\t\tflagsMap := baseBlocks.FeatureFlags.AsValueMap()\n\t\t\tattrs = append(attrs,\n\t\t\t\tattribute.Int(AttrFeatureFlagCount, len(flagsMap)),\n\t\t\t\tattribute.String(AttrFeatureFlagNames, formatMapKeys(flagsMap)),\n\t\t\t)\n\t\t} else {\n\t\t\tattrs = append(attrs, attribute.Int(AttrFeatureFlagCount, 0))\n\t\t}\n\t}\n\n\tspan.SetAttributes(attrs...)\n}\n\n// TraceParseDependencies wraps dependency parsing with telemetry.\nfunc TraceParseDependencies(\n\tctx context.Context,\n\tconfigPath string,\n\tskipOutputsResolution bool,\n\tdependencyCount int,\n\tdependencyNames []string,\n\tfn func(ctx context.Context) error,\n) error {\n\tattrs := map[string]any{\n\t\tAttrConfigPath:      configPath,\n\t\tAttrSkipOutputs:     skipOutputsResolution,\n\t\tAttrDependencyCount: dependencyCount,\n\t}\n\n\tif len(dependencyNames) > 0 {\n\t\tattrs[AttrDependencyNames] = strings.Join(dependencyNames, \",\")\n\t}\n\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpParseDependencies, attrs, fn)\n}\n\n// TraceParseDependency wraps individual dependency output resolution with telemetry.\nfunc TraceParseDependency(\n\tctx context.Context,\n\tdependencyName string,\n\tdependencyPath string,\n\tfn func(ctx context.Context) error,\n) error {\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpParseDependency, map[string]any{\n\t\tAttrDependencyName: dependencyName,\n\t\tAttrDependencyPath: dependencyPath,\n\t}, fn)\n}\n\n// TraceParseConfigDecode wraps config decoding with telemetry.\nfunc TraceParseConfigDecode(\n\tctx context.Context,\n\tconfigPath string,\n\tfn func(ctx context.Context) error,\n) error {\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpParseConfigDecode, map[string]any{\n\t\tAttrConfigPath: configPath,\n\t}, fn)\n}\n\n// TraceParseIncludeMerge wraps include merging with telemetry.\nfunc TraceParseIncludeMerge(\n\tctx context.Context,\n\tconfigPath string,\n\tincludeCount int,\n\tincludePaths []string,\n\tfn func(ctx context.Context) error,\n) error {\n\tattrs := map[string]any{\n\t\tAttrConfigPath:   configPath,\n\t\tAttrIncludeCount: includeCount,\n\t}\n\n\tif len(includePaths) > 0 {\n\t\tattrs[AttrIncludePaths] = strings.Join(includePaths, \",\")\n\t}\n\n\treturn telemetry.TelemeterFromContext(ctx).Collect(ctx, TelemetryOpParseIncludeMerge, attrs, fn)\n}\n\n// formatDecodeList converts a slice of PartialDecodeSectionType to a comma-separated string.\nfunc formatDecodeList(decodeList []PartialDecodeSectionType) string {\n\tnames := make([]string, 0, len(decodeList))\n\tfor _, section := range decodeList {\n\t\tnames = append(names, partialDecodeSectionName(section))\n\t}\n\n\treturn strings.Join(names, \",\")\n}\n\n// partialDecodeSectionName returns a human-readable name for a PartialDecodeSectionType.\nfunc partialDecodeSectionName(section PartialDecodeSectionType) string {\n\tswitch section {\n\tcase DependenciesBlock:\n\t\treturn \"dependencies\"\n\tcase DependencyBlock:\n\t\treturn \"dependency\"\n\tcase TerraformBlock:\n\t\treturn \"terraform\"\n\tcase TerraformSource:\n\t\treturn \"terraform_source\"\n\tcase TerragruntFlags:\n\t\treturn \"terragrunt_flags\"\n\tcase TerragruntVersionConstraints:\n\t\treturn \"version_constraints\"\n\tcase RemoteStateBlock:\n\t\treturn \"remote_state\"\n\tcase FeatureFlagsBlock:\n\t\treturn \"feature_flags\"\n\tcase EngineBlock:\n\t\treturn \"engine\"\n\tcase ExcludeBlock:\n\t\treturn \"exclude\"\n\tcase ErrorsBlock:\n\t\treturn \"errors\"\n\tdefault:\n\t\treturn \"unknown\"\n\t}\n}\n\n// formatIncludePaths extracts and formats include paths from a list of IncludeConfigs.\nfunc formatIncludePaths(includes IncludeConfigs) string {\n\tpaths := make([]string, 0, len(includes))\n\tfor _, inc := range includes {\n\t\tif inc.Path != \"\" {\n\t\t\tpaths = append(paths, inc.Path)\n\t\t}\n\t}\n\n\treturn strings.Join(paths, \",\")\n}\n\n// formatMapKeys extracts keys from a map and returns them as a comma-separated string.\nfunc formatMapKeys[V any](m map[string]V) string {\n\tkeys := make([]string, 0, len(m))\n\tfor k := range m {\n\t\tkeys = append(keys, k)\n\t}\n\n\treturn strings.Join(keys, \",\")\n}\n"
  },
  {
    "path": "pkg/config/translate.go",
    "content": "package config\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/codegen\"\n\t\"github.com/gruntwork-io/terragrunt/internal/remotestate\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runcfg\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// ToRunConfig translates a TerragruntConfig to a runcfg.RunConfig.\n// This is the primary method for converting config types to runner types.\nfunc (cfg *TerragruntConfig) ToRunConfig(l log.Logger) *runcfg.RunConfig {\n\tif cfg == nil {\n\t\treturn nil\n\t}\n\n\trunCfg := &runcfg.RunConfig{\n\t\tTerraform:                   translateTerraformConfig(cfg.Terraform, l),\n\t\tRemoteState:                 translateRemoteState(cfg.RemoteState),\n\t\tExclude:                     translateExcludeConfig(cfg.Exclude),\n\t\tGenerateConfigs:             translateGenerateConfigs(cfg.GenerateConfigs),\n\t\tInputs:                      translateInputs(cfg.Inputs),\n\t\tIAMRole:                     cfg.GetIAMRoleOptions(),\n\t\tDownloadDir:                 cfg.DownloadDir,\n\t\tTerraformBinary:             cfg.TerraformBinary,\n\t\tTerraformVersionConstraint:  cfg.TerraformVersionConstraint,\n\t\tTerragruntVersionConstraint: cfg.TerragruntVersionConstraint,\n\t\tPreventDestroy:              translatePreventDestroy(cfg.PreventDestroy),\n\t\tProcessedIncludes:           translateProcessedIncludes(cfg.ProcessedIncludes),\n\t\tDependencies:                translateModuleDependencies(cfg.Dependencies),\n\t\tEngine:                      translateEngineConfig(cfg.Engine),\n\t\tErrors:                      translateErrorsConfig(cfg.Errors),\n\t}\n\n\treturn runCfg\n}\n\n// translateTerraformConfig converts config.TerraformConfig to runcfg.TerraformConfig.\nfunc translateTerraformConfig(tf *TerraformConfig, l log.Logger) runcfg.TerraformConfig {\n\tif tf == nil {\n\t\treturn runcfg.TerraformConfig{}\n\t}\n\n\tvar source string\n\tif tf.Source != nil {\n\t\tsource = *tf.Source\n\t}\n\n\tvar includeInCopy []string\n\tif tf.IncludeInCopy != nil {\n\t\tincludeInCopy = *tf.IncludeInCopy\n\t}\n\n\tvar excludeFromCopy []string\n\tif tf.ExcludeFromCopy != nil {\n\t\texcludeFromCopy = *tf.ExcludeFromCopy\n\t}\n\n\tnoCopyTerraformLockFile := false\n\tif tf.CopyTerraformLockFile != nil {\n\t\tnoCopyTerraformLockFile = !*tf.CopyTerraformLockFile\n\t}\n\n\treturn runcfg.TerraformConfig{\n\t\tSource:                  source,\n\t\tIncludeInCopy:           includeInCopy,\n\t\tExcludeFromCopy:         excludeFromCopy,\n\t\tNoCopyTerraformLockFile: noCopyTerraformLockFile,\n\t\tExtraArgs:               translateExtraArgs(tf.ExtraArgs, l),\n\t\tBeforeHooks:             translateHooks(tf.BeforeHooks),\n\t\tAfterHooks:              translateHooks(tf.AfterHooks),\n\t\tErrorHooks:              translateErrorHooks(tf.ErrorHooks),\n\t}\n}\n\n// translateExtraArgs converts []TerraformExtraArguments to []runcfg.TerraformExtraArguments.\nfunc translateExtraArgs(args []TerraformExtraArguments, l log.Logger) []runcfg.TerraformExtraArguments {\n\tif args == nil {\n\t\treturn nil\n\t}\n\n\tresult := make([]runcfg.TerraformExtraArguments, len(args))\n\tfor i, arg := range args {\n\t\tvarFiles := computeVarFiles(arg.RequiredVarFiles, arg.OptionalVarFiles, l)\n\n\t\tvar arguments []string\n\t\tif arg.Arguments != nil {\n\t\t\targuments = *arg.Arguments\n\t\t}\n\n\t\tvar requiredVarFiles []string\n\t\tif arg.RequiredVarFiles != nil {\n\t\t\trequiredVarFiles = *arg.RequiredVarFiles\n\t\t}\n\n\t\tvar optionalVarFiles []string\n\t\tif arg.OptionalVarFiles != nil {\n\t\t\toptionalVarFiles = *arg.OptionalVarFiles\n\t\t}\n\n\t\tvar envVars map[string]string\n\t\tif arg.EnvVars != nil {\n\t\t\tenvVars = *arg.EnvVars\n\t\t}\n\n\t\tresult[i] = runcfg.TerraformExtraArguments{\n\t\t\tName:             arg.Name,\n\t\t\tCommands:         arg.Commands,\n\t\t\tArguments:        arguments,\n\t\t\tRequiredVarFiles: requiredVarFiles,\n\t\t\tOptionalVarFiles: optionalVarFiles,\n\t\t\tVarFiles:         varFiles,\n\t\t\tEnvVars:          envVars,\n\t\t}\n\t}\n\n\treturn result\n}\n\n// computeVarFiles returns a list of variable files, including required and optional files.\nfunc computeVarFiles(requiredVarFiles *[]string, optionalVarFiles *[]string, l log.Logger) []string {\n\tvar varFiles []string\n\n\t// Include all specified RequiredVarFiles.\n\tif requiredVarFiles != nil {\n\t\tvarFiles = append(varFiles, util.RemoveDuplicatesKeepLast(*requiredVarFiles)...)\n\t}\n\n\t// If OptionalVarFiles is specified, check for each file if it exists and if so, include in the var\n\t// files list. Note that it is possible that many files resolve to the same path, so we remove\n\t// duplicates.\n\tif optionalVarFiles != nil {\n\t\tfor _, file := range util.RemoveDuplicatesKeepLast(*optionalVarFiles) {\n\t\t\tif util.FileExists(file) {\n\t\t\t\tvarFiles = append(varFiles, file)\n\t\t\t} else {\n\t\t\t\tl.Debugf(\"Skipping var-file %s as it does not exist\", file)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn varFiles\n}\n\n// translateHooks converts []Hook to []runcfg.Hook.\nfunc translateHooks(hooks []Hook) []runcfg.Hook {\n\tif hooks == nil {\n\t\treturn nil\n\t}\n\n\tresult := make([]runcfg.Hook, len(hooks))\n\tfor i, hook := range hooks {\n\t\tvar workingDir string\n\t\tif hook.WorkingDir != nil {\n\t\t\tworkingDir = *hook.WorkingDir\n\t\t}\n\n\t\tvar runOnError bool\n\t\tif hook.RunOnError != nil {\n\t\t\trunOnError = *hook.RunOnError\n\t\t}\n\n\t\tifCondition := true\n\t\tif hook.If != nil {\n\t\t\tifCondition = *hook.If\n\t\t}\n\n\t\tvar suppressStdout bool\n\t\tif hook.SuppressStdout != nil {\n\t\t\tsuppressStdout = *hook.SuppressStdout\n\t\t}\n\n\t\tresult[i] = runcfg.Hook{\n\t\t\tName:           hook.Name,\n\t\t\tCommands:       hook.Commands,\n\t\t\tExecute:        hook.Execute,\n\t\t\tWorkingDir:     workingDir,\n\t\t\tRunOnError:     runOnError,\n\t\t\tIf:             ifCondition,\n\t\t\tSuppressStdout: suppressStdout,\n\t\t}\n\t}\n\n\treturn result\n}\n\n// translateErrorHooks converts []ErrorHook to []runcfg.ErrorHook.\nfunc translateErrorHooks(hooks []ErrorHook) []runcfg.ErrorHook {\n\tif hooks == nil {\n\t\treturn nil\n\t}\n\n\tresult := make([]runcfg.ErrorHook, len(hooks))\n\tfor i, hook := range hooks {\n\t\tvar workingDir string\n\t\tif hook.WorkingDir != nil {\n\t\t\tworkingDir = *hook.WorkingDir\n\t\t}\n\n\t\tvar suppressStdout bool\n\t\tif hook.SuppressStdout != nil {\n\t\t\tsuppressStdout = *hook.SuppressStdout\n\t\t}\n\n\t\tresult[i] = runcfg.ErrorHook{\n\t\t\tName:           hook.Name,\n\t\t\tCommands:       hook.Commands,\n\t\t\tExecute:        hook.Execute,\n\t\t\tOnErrors:       hook.OnErrors,\n\t\t\tWorkingDir:     workingDir,\n\t\t\tSuppressStdout: suppressStdout,\n\t\t}\n\t}\n\n\treturn result\n}\n\n// translateGenerateConfigs converts map[string]codegen.GenerateConfig to map[string]codegen.GenerateConfig.\n// Returns an empty map if the input is nil.\nfunc translateGenerateConfigs(generateConfigs map[string]codegen.GenerateConfig) map[string]codegen.GenerateConfig {\n\tif generateConfigs == nil {\n\t\treturn make(map[string]codegen.GenerateConfig)\n\t}\n\n\treturn generateConfigs\n}\n\n// translateInputs converts map[string]any to map[string]any.\n// Returns an empty map if the input is nil.\nfunc translateInputs(inputs map[string]any) map[string]any {\n\tif inputs == nil {\n\t\treturn make(map[string]any)\n\t}\n\n\treturn inputs\n}\n\n// translatePreventDestroy converts *bool to bool.\nfunc translatePreventDestroy(preventDestroy *bool) bool {\n\tif preventDestroy == nil {\n\t\treturn false\n\t}\n\n\treturn *preventDestroy\n}\n\n// translateRemoteState converts *remotestate.RemoteState to remotestate.RemoteState.\nfunc translateRemoteState(remoteState *remotestate.RemoteState) remotestate.RemoteState {\n\tif remoteState == nil {\n\t\treturn remotestate.RemoteState{}\n\t}\n\n\treturn *remoteState\n}\n\n// translateExcludeConfig converts *ExcludeConfig to runcfg.ExcludeConfig.\nfunc translateExcludeConfig(exclude *ExcludeConfig) runcfg.ExcludeConfig {\n\tif exclude == nil {\n\t\treturn runcfg.ExcludeConfig{}\n\t}\n\n\tvar excludeDependencies bool\n\tif exclude.ExcludeDependencies != nil {\n\t\texcludeDependencies = *exclude.ExcludeDependencies\n\t}\n\n\tvar noRun bool\n\tif exclude.NoRun != nil {\n\t\tnoRun = *exclude.NoRun\n\t}\n\n\treturn runcfg.ExcludeConfig{\n\t\tIf:                  exclude.If,\n\t\tActions:             exclude.Actions,\n\t\tExcludeDependencies: excludeDependencies,\n\t\tNoRun:               noRun,\n\t}\n}\n\n// translateIncludeConfigs converts IncludeConfigsMap to map[string]runcfg.IncludeConfig.\nfunc translateIncludeConfigs(includes IncludeConfigsMap) map[string]runcfg.IncludeConfig {\n\tif includes == nil {\n\t\treturn nil\n\t}\n\n\tresult := make(map[string]runcfg.IncludeConfig, len(includes))\n\tfor name, inc := range includes {\n\t\tvar expose bool\n\t\tif inc.Expose != nil {\n\t\t\texpose = *inc.Expose\n\t\t}\n\n\t\tvar mergeStrategy string\n\t\tif inc.MergeStrategy != nil {\n\t\t\tmergeStrategy = *inc.MergeStrategy\n\t\t}\n\n\t\tresult[name] = runcfg.IncludeConfig{\n\t\t\tName:          inc.Name,\n\t\t\tPath:          inc.Path,\n\t\t\tExpose:        expose,\n\t\t\tMergeStrategy: mergeStrategy,\n\t\t}\n\t}\n\n\treturn result\n}\n\n// translateProcessedIncludes converts IncludeConfigsMap to map[string]runcfg.IncludeConfig.\n// Returns an empty map if the input is nil.\nfunc translateProcessedIncludes(includes IncludeConfigsMap) map[string]runcfg.IncludeConfig {\n\tresult := translateIncludeConfigs(includes)\n\tif result == nil {\n\t\treturn make(map[string]runcfg.IncludeConfig)\n\t}\n\n\treturn result\n}\n\n// translateModuleDependencies converts *ModuleDependencies to runcfg.ModuleDependencies.\nfunc translateModuleDependencies(deps *ModuleDependencies) runcfg.ModuleDependencies {\n\tif deps == nil {\n\t\treturn runcfg.ModuleDependencies{}\n\t}\n\n\treturn runcfg.ModuleDependencies{\n\t\tPaths: deps.Paths,\n\t}\n}\n\n// translateEngineConfig converts *EngineConfig to runcfg.EngineConfig.\nfunc translateEngineConfig(engine *EngineConfig) runcfg.EngineConfig {\n\tif engine == nil {\n\t\treturn runcfg.EngineConfig{}\n\t}\n\n\tvar version string\n\tif engine.Version != nil {\n\t\tversion = *engine.Version\n\t}\n\n\tvar engineType string\n\tif engine.Type != nil {\n\t\tengineType = *engine.Type\n\t}\n\n\treturn runcfg.EngineConfig{\n\t\tEnable:  true,\n\t\tSource:  engine.Source,\n\t\tVersion: version,\n\t\tType:    engineType,\n\t\tMeta:    engine.Meta,\n\t}\n}\n\n// translateErrorsConfig converts *ErrorsConfig to runcfg.ErrorsConfig.\nfunc translateErrorsConfig(errors *ErrorsConfig) runcfg.ErrorsConfig {\n\tif errors == nil {\n\t\treturn runcfg.ErrorsConfig{}\n\t}\n\n\treturn runcfg.ErrorsConfig{\n\t\tRetry:  translateRetryBlocks(errors.Retry),\n\t\tIgnore: translateIgnoreBlocks(errors.Ignore),\n\t}\n}\n\n// translateRetryBlocks converts []*RetryBlock to []*runcfg.RetryBlock.\nfunc translateRetryBlocks(blocks []*RetryBlock) []*runcfg.RetryBlock {\n\tif blocks == nil {\n\t\treturn nil\n\t}\n\n\tresult := make([]*runcfg.RetryBlock, len(blocks))\n\tfor i, block := range blocks {\n\t\tif block == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tresult[i] = &runcfg.RetryBlock{\n\t\t\tLabel:            block.Label,\n\t\t\tRetryableErrors:  block.RetryableErrors,\n\t\t\tMaxAttempts:      block.MaxAttempts,\n\t\t\tSleepIntervalSec: block.SleepIntervalSec,\n\t\t}\n\t}\n\n\treturn result\n}\n\n// translateIgnoreBlocks converts []*IgnoreBlock to []*runcfg.IgnoreBlock.\nfunc translateIgnoreBlocks(blocks []*IgnoreBlock) []*runcfg.IgnoreBlock {\n\tif blocks == nil {\n\t\treturn nil\n\t}\n\n\tresult := make([]*runcfg.IgnoreBlock, len(blocks))\n\tfor i, block := range blocks {\n\t\tif block == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tresult[i] = &runcfg.IgnoreBlock{\n\t\t\tLabel:           block.Label,\n\t\t\tIgnorableErrors: block.IgnorableErrors,\n\t\t\tMessage:         block.Message,\n\t\t\tSignals:         block.Signals,\n\t\t}\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "pkg/config/util.go",
    "content": "package config\n\nimport (\n\t\"path/filepath\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// CopyLockFile copies the lock file from the source folder to the destination folder.\n//\n// Terraform 0.14 now generates a lock file when you run `terraform init`.\n// If any such file exists, this function will copy the lock file to the destination folder\nfunc CopyLockFile(l log.Logger, rootWorkingDir string, logShowAbsPaths bool, sourceFolder, destinationFolder string) error {\n\tsourceLockFilePath := filepath.Join(sourceFolder, tf.TerraformLockFile)\n\tdestinationLockFilePath := filepath.Join(destinationFolder, tf.TerraformLockFile)\n\n\tif util.FileExists(sourceLockFilePath) {\n\t\tl.Debugf(\n\t\t\t\"Copying lock file from %s to %s\",\n\t\t\tutil.RelPathForLog(\n\t\t\t\trootWorkingDir,\n\t\t\t\tsourceLockFilePath,\n\t\t\t\tlogShowAbsPaths,\n\t\t\t),\n\t\t\tutil.RelPathForLog(\n\t\t\t\trootWorkingDir,\n\t\t\t\tdestinationLockFilePath,\n\t\t\t\tlogShowAbsPaths,\n\t\t\t),\n\t\t)\n\n\t\treturn util.CopyFile(sourceLockFilePath, destinationLockFilePath)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/config/variable.go",
    "content": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/hashicorp/hcl/v2/hclsyntax\"\n\t\"github.com/zclconf/go-cty/cty\"\n\tctyjson \"github.com/zclconf/go-cty/cty/json\"\n)\n\n// ParsedVariable structure with input name, default value and description.\ntype ParsedVariable struct {\n\tName                    string\n\tDescription             string\n\tType                    string\n\tDefaultValue            string\n\tDefaultValuePlaceholder string\n}\n\n// ParseVariables - parse variables from tf files.\nfunc ParseVariables(l log.Logger, experiments experiment.Experiments, strictControls strict.Controls, directoryPath string) ([]*ParsedVariable, error) {\n\twalkWithSymlinks := experiments.Evaluate(experiment.Symlinks)\n\n\t// list all tf files\n\ttfFiles, err := util.ListTfFiles(directoryPath, walkWithSymlinks)\n\tif err != nil {\n\t\treturn nil, errors.New(err)\n\t}\n\n\tparser := hclparse.NewParser(DefaultParserOptions(l, strictControls)...)\n\n\t// iterate over files and parse variables.\n\tvar parsedInputs []*ParsedVariable\n\n\tfor _, tfFile := range tfFiles {\n\t\tif _, err := parser.ParseFromFile(tfFile); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tfor _, file := range parser.Files() {\n\t\tctx := &hcl.EvalContext{}\n\n\t\tif body, ok := file.Body.(*hclsyntax.Body); ok {\n\t\t\tfor _, block := range body.Blocks {\n\t\t\t\tif block.Type == \"variable\" {\n\t\t\t\t\tif len(block.Labels[0]) > 0 {\n\t\t\t\t\t\t// extract variable attributes\n\t\t\t\t\t\tname := block.Labels[0]\n\n\t\t\t\t\t\tvar descriptionAttrText string\n\n\t\t\t\t\t\tdescriptionAttr, err := readBlockAttribute(ctx, block, \"description\")\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tl.Warnf(\"Failed to read descriptionAttr for %s %v\", name, err)\n\n\t\t\t\t\t\t\tdescriptionAttr = nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif descriptionAttr != nil {\n\t\t\t\t\t\t\tdescriptionAttrText = descriptionAttr.AsString()\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tdescriptionAttrText = fmt.Sprintf(\"(variable %s did not define a description)\", name)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tvar typeAttrText string\n\n\t\t\t\t\t\ttypeAttr, err := readBlockAttribute(ctx, block, \"type\")\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tl.Warnf(\"Failed to read type attribute for %s %v\", name, err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif typeAttr != nil {\n\t\t\t\t\t\t\ttypeAttrText = typeAttr.AsString()\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\ttypeAttrText = fmt.Sprintf(\"(variable %s does not define a type)\", name)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tdefaultValue, err := readBlockAttribute(ctx, block, \"default\")\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tl.Warnf(\"Failed to read default value for %s %v\", name, err)\n\n\t\t\t\t\t\t\tdefaultValue = nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tdefaultValueText := \"\"\n\n\t\t\t\t\t\tif defaultValue != nil {\n\t\t\t\t\t\t\tjsonBytes, err := ctyjson.Marshal(*defaultValue, cty.DynamicPseudoType)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn nil, errors.New(err)\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tvar ctyJSONOutput ctyJSONValue\n\t\t\t\t\t\t\tif err := json.Unmarshal(jsonBytes, &ctyJSONOutput); err != nil {\n\t\t\t\t\t\t\t\treturn nil, errors.New(err)\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tjsonBytes, err = json.Marshal(ctyJSONOutput.Value)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn nil, errors.New(err)\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tdefaultValueText = string(jsonBytes)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tinput := &ParsedVariable{\n\t\t\t\t\t\t\tName:                    name,\n\t\t\t\t\t\t\tType:                    typeAttrText,\n\t\t\t\t\t\t\tDescription:             descriptionAttrText,\n\t\t\t\t\t\t\tDefaultValue:            defaultValueText,\n\t\t\t\t\t\t\tDefaultValuePlaceholder: generateDefaultValue(typeAttrText),\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tparsedInputs = append(parsedInputs, input)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn parsedInputs, nil\n}\n\n// generateDefaultValue - generate hcl default value\n// HCL type of variable https://developer.hashicorp.com/packer/docs/templates/hcl_templates/variables#type-constraints\nfunc generateDefaultValue(variableType string) string {\n\tswitch variableType {\n\tcase \"number\":\n\t\treturn \"0\"\n\tcase \"bool\":\n\t\treturn \"false\"\n\tcase \"list\":\n\t\treturn \"[]\"\n\tcase \"map\":\n\t\treturn \"{}\"\n\tcase \"object\":\n\t\treturn \"{}\"\n\t}\n\t// fallback to empty value\n\treturn \"\\\"\\\"\"\n}\n\ntype ctyJSONValue struct {\n\tValue any `json:\"Value\"`\n\tType  any `json:\"Type\"`\n}\n\n// readBlockAttribute - hcl block attribute.\nfunc readBlockAttribute(ctx *hcl.EvalContext, block *hclsyntax.Block, name string) (*cty.Value, error) {\n\tif attr, ok := block.Body.Attributes[name]; ok {\n\t\tif attr.Expr != nil {\n\t\t\tif call, ok := attr.Expr.(*hclsyntax.FunctionCallExpr); ok {\n\t\t\t\tresult := cty.StringVal(call.Name)\n\t\t\t\treturn &result, nil\n\t\t\t}\n\t\t\t// check if first var is traversal\n\t\t\tif len(attr.Expr.Variables()) > 0 {\n\t\t\t\tv := attr.Expr.Variables()[0]\n\t\t\t\t// check if variable is traversal\n\t\t\t\tif varTr, ok := v[0].(hcl.TraverseRoot); ok {\n\t\t\t\t\tresult := cty.StringVal(varTr.Name)\n\t\t\t\t\treturn &result, nil\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvalue, err := attr.Expr.Value(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn &value, nil\n\t\t}\n\t}\n\n\treturn nil, nil\n}\n"
  },
  {
    "path": "pkg/config/variable_test.go",
    "content": "package config_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestScanVariables(t *testing.T) {\n\tt.Parallel()\n\n\tinputs, err := config.ParseVariables(logger.CreateLogger(), experiment.NewExperiments(), controls.New(), \"../../test/fixtures/inputs\")\n\trequire.NoError(t, err)\n\tassert.Len(t, inputs, 11)\n\n\tvarByName := map[string]*config.ParsedVariable{}\n\tfor _, input := range inputs {\n\t\tvarByName[input.Name] = input\n\t}\n\n\tassert.Equal(t, \"string\", varByName[\"string\"].Type)\n\tassert.Equal(t, \"\\\"\\\"\", varByName[\"string\"].DefaultValuePlaceholder)\n\n\tassert.Equal(t, \"bool\", varByName[\"bool\"].Type)\n\tassert.Equal(t, \"false\", varByName[\"bool\"].DefaultValuePlaceholder)\n\n\tassert.Equal(t, \"number\", varByName[\"number\"].Type)\n\tassert.Equal(t, \"0\", varByName[\"number\"].DefaultValuePlaceholder)\n\n\tassert.Equal(t, \"object\", varByName[\"object\"].Type)\n\tassert.Equal(t, \"{}\", varByName[\"object\"].DefaultValuePlaceholder)\n\n\tassert.Equal(t, \"map\", varByName[\"map_bool\"].Type)\n\tassert.Equal(t, \"{}\", varByName[\"map_bool\"].DefaultValuePlaceholder)\n\n\tassert.Equal(t, \"list\", varByName[\"list_bool\"].Type)\n\tassert.Equal(t, \"[]\", varByName[\"list_bool\"].DefaultValuePlaceholder)\n}\n\nfunc TestScanDefaultVariables(t *testing.T) {\n\tt.Parallel()\n\n\tinputs, err := config.ParseVariables(logger.CreateLogger(), experiment.NewExperiments(), controls.New(), \"../../test/fixtures/inputs-defaults\")\n\trequire.NoError(t, err)\n\tassert.Len(t, inputs, 11)\n\n\tvarByName := map[string]*config.ParsedVariable{}\n\tfor _, input := range inputs {\n\t\tvarByName[input.Name] = input\n\t}\n\n\tassert.Equal(t, \"string\", varByName[\"project_name\"].Type)\n\tassert.Equal(t, \"Project name\", varByName[\"project_name\"].Description)\n\tassert.Equal(t, \"\\\"\\\"\", varByName[\"project_name\"].DefaultValuePlaceholder)\n\n\tassert.Equal(t, \"(variable no_type_value_var does not define a type)\", varByName[\"no_type_value_var\"].Type)\n\tassert.Equal(t, \"(variable no_type_value_var did not define a description)\", varByName[\"no_type_value_var\"].Description)\n\tassert.Equal(t, \"\\\"\\\"\", varByName[\"no_type_value_var\"].DefaultValuePlaceholder)\n\n\tassert.Equal(t, \"number\", varByName[\"number_default\"].Type)\n\tassert.Equal(t, \"number variable with default\", varByName[\"number_default\"].Description)\n\tassert.Equal(t, \"42\", varByName[\"number_default\"].DefaultValue)\n\tassert.Equal(t, \"0\", varByName[\"number_default\"].DefaultValuePlaceholder)\n\n\tassert.Equal(t, \"object\", varByName[\"object_var\"].Type)\n\tassert.JSONEq(t, \"{\\\"num\\\":42,\\\"str\\\":\\\"default\\\"}\", varByName[\"object_var\"].DefaultValue)\n\n\tassert.Equal(t, \"map\", varByName[\"map_var\"].Type)\n\tassert.JSONEq(t, \"{\\\"key\\\":\\\"value42\\\"}\", varByName[\"map_var\"].DefaultValue)\n\n\tassert.Equal(t, \"bool\", varByName[\"enabled\"].Type)\n\tassert.Equal(t, \"true\", varByName[\"enabled\"].DefaultValue)\n\tassert.Equal(t, \"Enable or disable the module\", varByName[\"enabled\"].Description)\n\n\tassert.Equal(t, \"string\", varByName[\"vpc\"].Type)\n\tassert.Equal(t, \"\\\"default-vpc\\\"\", varByName[\"vpc\"].DefaultValue)\n\tassert.Equal(t, \"VPC to be used\", varByName[\"vpc\"].Description)\n}\n"
  },
  {
    "path": "pkg/log/context.go",
    "content": "package log\n\nimport \"context\"\n\nconst (\n\tloggerContextKey ctxKey = iota\n)\n\ntype ctxKey byte\n\nfunc ContextWithLogger(ctx context.Context, logger Logger) context.Context {\n\treturn context.WithValue(ctx, loggerContextKey, logger)\n}\n\nfunc LoggerFromContext(ctx context.Context) Logger {\n\tif val := ctx.Value(loggerContextKey); val != nil {\n\t\tif val, ok := val.(Logger); ok {\n\t\t\treturn val\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/log/context_test.go",
    "content": "package log_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestContextWithLogger(t *testing.T) {\n\tt.Parallel()\n\n\tlogger := log.New()\n\tctx := log.ContextWithLogger(t.Context(), logger)\n\n\tretrieved := log.LoggerFromContext(ctx)\n\trequire.NotNil(t, retrieved)\n\tassert.Equal(t, logger, retrieved)\n}\n\nfunc TestLoggerFromContextEmpty(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\tretrieved := log.LoggerFromContext(ctx)\n\tassert.Nil(t, retrieved)\n}\n"
  },
  {
    "path": "pkg/log/external_test.go",
    "content": "// Tests in this file validate that the pkg/log package can be fully utilized as\n// an external dependency. Every scenario here imports only public packages\n// (pkg/log, pkg/log/format, pkg/log/format/placeholders, pkg/log/writer) and\n// never reaches into internal/ packages. If any of these tests fail to compile,\n// it signals a broken public API contract.\npackage log_test\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/writer\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestExternalLoggerLifecycle exercises the core create → configure → log →\n// clone workflow that an external consumer would follow.\nfunc TestExternalLoggerLifecycle(t *testing.T) {\n\tt.Parallel()\n\n\tbuf := new(bytes.Buffer)\n\tlogger := log.New(\n\t\tlog.WithLevel(log.DebugLevel),\n\t\tlog.WithOutput(buf),\n\t)\n\n\t// Basic logging methods.\n\tlogger.Info(\"info message\")\n\tassert.Contains(t, buf.String(), \"info message\")\n\n\tbuf.Reset()\n\n\tlogger.Debugf(\"count=%d\", 42)\n\tassert.Contains(t, buf.String(), \"count=42\")\n\n\tbuf.Reset()\n\n\t// Clone and change level independently.\n\tchild := logger.Clone()\n\tchild.SetOptions(log.WithOutput(buf))\n\n\trequire.NoError(t, child.SetLevel(\"trace\"))\n\tchild.Trace(\"trace message\")\n\tassert.Contains(t, buf.String(), \"trace message\")\n}\n\n// TestExternalLevelRoundTrip confirms that an external consumer can parse a\n// level string, inspect its various name forms, and marshal/unmarshal it.\nfunc TestExternalLevelRoundTrip(t *testing.T) {\n\tt.Parallel()\n\n\tlevel, err := log.ParseLevel(\"warn\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, log.WarnLevel, level)\n\tassert.Equal(t, \"warn\", level.String())\n\tassert.Equal(t, \"wrn\", level.ShortName())\n\tassert.Equal(t, \"w\", level.TinyName())\n\n\tdata, err := level.MarshalText()\n\trequire.NoError(t, err)\n\n\tvar restored log.Level\n\n\trequire.NoError(t, restored.UnmarshalText(data))\n\tassert.Equal(t, level, restored)\n}\n\n// TestExternalAllLevelsEnumeration verifies that AllLevels is accessible and\n// that every level can be stringified.\nfunc TestExternalAllLevelsEnumeration(t *testing.T) {\n\tt.Parallel()\n\n\tassert.Len(t, log.AllLevels, 7)\n\n\tfor _, lvl := range log.AllLevels {\n\t\tassert.NotEmpty(t, lvl.String())\n\t\tassert.True(t, log.AllLevels.Contains(lvl))\n\t}\n\n\tassert.False(t, log.AllLevels.Contains(log.Level(99)))\n}\n\n// TestExternalFieldsAndErrors confirms that WithField, WithFields, and\n// WithError return enriched loggers whose output contains the added metadata.\nfunc TestExternalFieldsAndErrors(t *testing.T) {\n\tt.Parallel()\n\n\tbuf := new(bytes.Buffer)\n\tlogger := log.New(log.WithLevel(log.InfoLevel), log.WithOutput(buf))\n\n\tlogger.WithField(\"component\", \"auth\").Info(\"field check\")\n\tassert.Contains(t, buf.String(), \"component\")\n\n\tbuf.Reset()\n\n\tlogger.WithFields(log.Fields{\"a\": 1, \"b\": 2}).Info(\"fields check\")\n\n\toutput := buf.String()\n\tassert.Contains(t, output, \"a\")\n\tassert.Contains(t, output, \"b\")\n\n\tbuf.Reset()\n\n\tlogger.WithError(assert.AnError).Info(\"error check\")\n\tassert.Contains(t, buf.String(), assert.AnError.Error())\n}\n\n// TestExternalContextPropagation stores a logger in a context and retrieves it,\n// the way middleware or request handlers would.\nfunc TestExternalContextPropagation(t *testing.T) {\n\tt.Parallel()\n\n\tlogger := log.New(log.WithLevel(log.InfoLevel))\n\tctx := log.ContextWithLogger(t.Context(), logger)\n\n\tretrieved := log.LoggerFromContext(ctx)\n\trequire.NotNil(t, retrieved)\n\tassert.Equal(t, log.InfoLevel, retrieved.Level())\n\n\tassert.Nil(t, log.LoggerFromContext(t.Context()))\n}\n\n// TestExternalFormatterIntegration creates a Formatter from the format\n// subpackage, wires it into a logger, and verifies formatted output.\nfunc TestExternalFormatterIntegration(t *testing.T) {\n\tt.Parallel()\n\n\tfmtr := format.NewFormatter(format.NewBareFormatPlaceholders())\n\tfmtr.SetDisabledColors(true)\n\n\tbuf := new(bytes.Buffer)\n\tlogger := log.New(\n\t\tlog.WithLevel(log.InfoLevel),\n\t\tlog.WithOutput(buf),\n\t\tlog.WithFormatter(fmtr),\n\t)\n\n\tlogger.Info(\"formatted output\")\n\tassert.Contains(t, buf.String(), \"formatted output\")\n}\n\n// TestExternalParseFormatPresets confirms all four named format presets are\n// accessible via ParseFormat.\nfunc TestExternalParseFormatPresets(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, name := range []string{\n\t\tformat.BareFormatName,\n\t\tformat.PrettyFormatName,\n\t\tformat.JSONFormatName,\n\t\tformat.KeyValueFormatName,\n\t} {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tphs, err := format.ParseFormat(name)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.NotEmpty(t, phs)\n\t\t})\n\t}\n}\n\n// TestExternalCustomFormatParsing parses a user-supplied format string through\n// the placeholders subpackage and formats an entry with it.\nfunc TestExternalCustomFormatParsing(t *testing.T) {\n\tt.Parallel()\n\n\tfmtr := format.NewFormatter(nil)\n\tfmtr.SetDisabledColors(true)\n\n\trequire.NoError(t, fmtr.SetCustomFormat(\"%level %msg\"))\n\n\tbuf := new(bytes.Buffer)\n\tlogger := log.New(\n\t\tlog.WithLevel(log.InfoLevel),\n\t\tlog.WithOutput(buf),\n\t\tlog.WithFormatter(fmtr),\n\t)\n\n\tlogger.Info(\"custom format test\")\n\n\toutput := buf.String()\n\tassert.Contains(t, output, \"info\")\n\tassert.Contains(t, output, \"custom format test\")\n}\n\n// TestExternalPlaceholderRegistry ensures the placeholder register and its\n// Parse function are accessible to external callers.\nfunc TestExternalPlaceholderRegistry(t *testing.T) {\n\tt.Parallel()\n\n\treg := placeholders.NewPlaceholderRegister()\n\tassert.NotNil(t, reg.Get(\"level\"))\n\tassert.NotNil(t, reg.Get(\"msg\"))\n\tassert.NotNil(t, reg.Get(\"time\"))\n\tassert.NotNil(t, reg.Get(\"interval\"))\n\tassert.Nil(t, reg.Get(\"nonexistent\"))\n\n\tphs, err := placeholders.Parse(\"%level %msg\")\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, phs)\n}\n\n// TestExternalWriterAdapter verifies that the writer subpackage can be used to\n// bridge an io.Writer into the logging system.\nfunc TestExternalWriterAdapter(t *testing.T) {\n\tt.Parallel()\n\n\tbuf := new(bytes.Buffer)\n\tlogger := log.New(log.WithLevel(log.InfoLevel), log.WithOutput(buf))\n\n\tw := writer.New(\n\t\twriter.WithLogger(logger),\n\t\twriter.WithDefaultLevel(log.InfoLevel),\n\t\twriter.WithMsgSeparator(\"\\n\"),\n\t)\n\n\tn, err := w.Write([]byte(\"line1\\nline2\"))\n\trequire.NoError(t, err)\n\tassert.Len(t, \"line1\\nline2\", n)\n\tassert.Contains(t, buf.String(), \"line1\")\n\tassert.Contains(t, buf.String(), \"line2\")\n}\n\n// TestExternalWriterParseFunc confirms that a custom parse function can be\n// supplied to the writer adapter.\nfunc TestExternalWriterParseFunc(t *testing.T) {\n\tt.Parallel()\n\n\tbuf := new(bytes.Buffer)\n\tlogger := log.New(log.WithLevel(log.TraceLevel), log.WithOutput(buf))\n\twarnLevel := log.WarnLevel\n\n\tw := writer.New(\n\t\twriter.WithLogger(logger),\n\t\twriter.WithParseFunc(func(str string) (string, *time.Time, *log.Level, error) {\n\t\t\treturn \"wrapped: \" + str, nil, &warnLevel, nil\n\t\t}),\n\t)\n\n\t_, err := w.Write([]byte(\"hello\"))\n\trequire.NoError(t, err)\n\tassert.Contains(t, buf.String(), \"wrapped: hello\")\n}\n\n// TestExternalANSIUtilities exercises the ANSI helper functions that external\n// consumers might use when processing coloured log output.\nfunc TestExternalANSIUtilities(t *testing.T) {\n\tt.Parallel()\n\n\tcoloured := \"\\033[31mred text\\033[0m\"\n\tassert.Equal(t, \"red text\", log.RemoveAllASCISeq(coloured))\n\n\tpartial := \"\\033[32mgreen\"\n\tassert.Contains(t, log.ResetASCISeq(partial), \"\\033[0m\")\n\n\tassert.Equal(t, \"plain\", log.RemoveAllASCISeq(\"plain\"))\n\tassert.Equal(t, \"plain\", log.ResetASCISeq(\"plain\"))\n}\n\n// TestExternalPackageLevelFunctions confirms the package-level convenience\n// functions work without creating an explicit logger.\nfunc TestExternalPackageLevelFunctions(t *testing.T) {\n\tt.Parallel()\n\n\tlogger := log.Default()\n\trequire.NotNil(t, logger)\n\n\t// WithOptions returns a new logger without mutating the default.\n\tcustom := log.WithOptions(log.WithLevel(log.TraceLevel))\n\tassert.Equal(t, log.TraceLevel, custom.Level())\n\n\t// WithField / WithFields / WithError return enriched loggers.\n\tassert.NotNil(t, log.WithField(\"k\", \"v\"))\n\tassert.NotNil(t, log.WithFields(log.Fields{\"a\": 1}))\n\tassert.NotNil(t, log.WithError(assert.AnError))\n}\n\n// TestExternalForceLogLevelHook verifies that the force-level hook can be\n// created and wired in via WithHooks.\nfunc TestExternalForceLogLevelHook(t *testing.T) {\n\tt.Parallel()\n\n\thook := log.NewForceLogLevelHook(log.WarnLevel)\n\tassert.Len(t, hook.Levels(), 7)\n\n\tbuf := new(bytes.Buffer)\n\n\t// The hook can be attached through the public WithHooks option.\n\tlogger := log.New(\n\t\tlog.WithLevel(log.TraceLevel),\n\t\tlog.WithOutput(buf),\n\t\tlog.WithHooks(hook),\n\t)\n\n\tlogger.Info(\"hooked message\")\n\t// The message should still appear (hook changes level, dropper formatter\n\t// controls visibility based on logger level which is Trace — most permissive).\n\tassert.Contains(t, buf.String(), \"hooked message\")\n}\n\n// TestExternalConstants ensures exported constants from the package are\n// accessible.\nfunc TestExternalConstants(t *testing.T) {\n\tt.Parallel()\n\n\tassert.Equal(t, \".\", log.CurDir)\n\tassert.NotEmpty(t, log.CurDirWithSeparator)\n\n\tassert.Equal(t, \"bare\", format.BareFormatName)\n\tassert.Equal(t, \"pretty\", format.PrettyFormatName)\n\tassert.Equal(t, \"json\", format.JSONFormatName)\n\tassert.Equal(t, \"key-value\", format.KeyValueFormatName)\n\n\tassert.Equal(t, \"prefix\", placeholders.WorkDirKeyName)\n\tassert.Equal(t, \"tf-path\", placeholders.TFPathKeyName)\n\tassert.Equal(t, \"tf-command-args\", placeholders.TFCmdArgsKeyName)\n\tassert.Equal(t, \"tf-command\", placeholders.TFCmdKeyName)\n}\n"
  },
  {
    "path": "pkg/log/fields.go",
    "content": "package log\n\n// Fields is the type used to pass arguments to `WithFields`.\ntype Fields map[string]any\n"
  },
  {
    "path": "pkg/log/force_level_hook.go",
    "content": "package log\n\nimport \"github.com/sirupsen/logrus\"\n\n// ForceLogLevelHook is a log hook which can change log level for messages which contains specific substrings\ntype ForceLogLevelHook struct {\n\ttriggerLevels []logrus.Level\n\tforcedLevel   logrus.Level\n}\n\n// NewForceLogLevelHook creates default log reduction hook\nfunc NewForceLogLevelHook(forcedLevel Level) *ForceLogLevelHook {\n\treturn &ForceLogLevelHook{\n\t\tforcedLevel:   forcedLevel.ToLogrusLevel(),\n\t\ttriggerLevels: AllLevels.ToLogrusLevels(),\n\t}\n}\n\n// Levels implements logrus.Hook.Levels()\nfunc (hook *ForceLogLevelHook) Levels() []logrus.Level {\n\treturn hook.triggerLevels\n}\n\n// Fire implements logrus.Hook.Fire()\nfunc (hook *ForceLogLevelHook) Fire(entry *logrus.Entry) error {\n\tentry.Level = hook.forcedLevel\n\t// special formatter to skip printing of log entries since after hook evaluation, entries are printed directly\n\tformatter := LogEntriesDropperFormatter{originalFormatter: entry.Logger.Formatter}\n\tentry.Logger.Formatter = &formatter\n\n\treturn nil\n}\n\n// LogEntriesDropperFormatter is a custom formatter which will ignore log entries which has lower level than preconfigured in logger\ntype LogEntriesDropperFormatter struct {\n\toriginalFormatter logrus.Formatter\n}\n\n// Format implements logrus.Formatter\nfunc (formatter *LogEntriesDropperFormatter) Format(entry *logrus.Entry) ([]byte, error) {\n\tif entry.Logger.Level >= entry.Level {\n\t\treturn formatter.originalFormatter.Format(entry)\n\t}\n\n\treturn []byte(\"\"), nil\n}\n"
  },
  {
    "path": "pkg/log/force_level_hook_test.go",
    "content": "package log_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestForceLogLevelHookLevels(t *testing.T) {\n\tt.Parallel()\n\n\thook := log.NewForceLogLevelHook(log.WarnLevel)\n\tlevels := hook.Levels()\n\tassert.Len(t, levels, 7)\n}\n\nfunc TestForceLogLevelHookFire(t *testing.T) {\n\tt.Parallel()\n\n\thook := log.NewForceLogLevelHook(log.WarnLevel)\n\tlogger := logrus.New()\n\tlogger.SetLevel(logrus.TraceLevel)\n\n\tentry := logrus.NewEntry(logger)\n\tentry.Level = logrus.InfoLevel\n\n\terr := hook.Fire(entry)\n\trequire.NoError(t, err)\n\n\t// Entry level should be changed to the forced level (WarnLevel = 3, + shift 2 = logrus.Level(5))\n\tassert.Equal(t, log.WarnLevel.ToLogrusLevel(), entry.Level)\n}\n\nfunc TestLogEntriesDropperFormatter(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname        string\n\t\tloggerLevel log.Level\n\t\tforcedLevel log.Level\n\t\texpectEmpty bool\n\t}{\n\t\t{\n\t\t\tname:        \"entry_at_logger_level_produces_output\",\n\t\t\tloggerLevel: log.InfoLevel,\n\t\t\tforcedLevel: log.InfoLevel,\n\t\t\texpectEmpty: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"entry_above_logger_level_produces_output\",\n\t\t\tloggerLevel: log.TraceLevel,\n\t\t\tforcedLevel: log.InfoLevel,\n\t\t\texpectEmpty: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"entry_below_logger_level_produces_empty\",\n\t\t\tloggerLevel: log.ErrorLevel,\n\t\t\tforcedLevel: log.InfoLevel,\n\t\t\texpectEmpty: true,\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tlogger := logrus.New()\n\t\t\tlogger.SetLevel(tc.loggerLevel.ToLogrusLevel())\n\n\t\t\thook := log.NewForceLogLevelHook(tc.forcedLevel)\n\n\t\t\tentry := logrus.NewEntry(logger)\n\t\t\tentry.Level = logrus.InfoLevel\n\t\t\tentry.Message = \"test message\"\n\n\t\t\terr := hook.Fire(entry)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// After Fire, the logger's formatter is a LogEntriesDropperFormatter.\n\t\t\t// The dropper checks entry.Logger.Level >= entry.Level.\n\t\t\toutput, err := logger.Formatter.Format(entry)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif tc.expectEmpty {\n\t\t\t\tassert.Empty(t, string(output))\n\t\t\t} else {\n\t\t\t\tassert.NotEmpty(t, string(output))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/log/format/format.go",
    "content": "// Package format implements a custom format logs\npackage format\n\nimport (\n\t\"fmt\"\n\t\"maps\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t. \"github.com/gruntwork-io/terragrunt/pkg/log/format/options\"      //nolint:revive\n\t. \"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\" //nolint:revive\n)\n\nconst (\n\tBareFormatName     = \"bare\"\n\tPrettyFormatName   = \"pretty\"\n\tJSONFormatName     = \"json\"\n\tKeyValueFormatName = \"key-value\"\n)\n\nfunc NewBareFormatPlaceholders() Placeholders {\n\treturn Placeholders{\n\t\tLevel(\n\t\t\tWidth(4), //nolint:mnd\n\t\t\tCase(UpperCase),\n\t\t),\n\t\tInterval(\n\t\t\tPrefix(\"[\"),\n\t\t\tSuffix(\"]\"),\n\t\t),\n\t\tPlainText(\" \"),\n\t\tMessage(),\n\t\tField(WorkDirKeyName,\n\t\t\tPathFormat(ShortPath),\n\t\t\tPrefix(\"\\t prefix=[\"),\n\t\t\tSuffix(\"] \"),\n\t\t),\n\t}\n}\n\nfunc NewPrettyFormatPlaceholders() Placeholders {\n\treturn Placeholders{\n\t\tTime(\n\t\t\tTimeFormat(fmt.Sprintf(\"%s:%s:%s%s\", Hour24Zero, MinZero, SecZero, MilliSec)),\n\t\t\tColor(LightBlackColor),\n\t\t),\n\t\tPlainText(\" \"),\n\t\tLevel(\n\t\t\tWidth(6), //nolint:mnd\n\t\t\tCase(UpperCase),\n\t\t\tColor(PresetColor),\n\t\t),\n\t\tPlainText(\" \"),\n\t\tField(WorkDirKeyName,\n\t\t\tPathFormat(ShortRelativePath),\n\t\t\tPrefix(\"[\"),\n\t\t\tSuffix(\"] \"),\n\t\t\tColor(GradientColor),\n\t\t),\n\t\tField(TFPathKeyName,\n\t\t\tPathFormat(FilenamePath),\n\t\t\tSuffix(\": \"),\n\t\t\tColor(CyanColor),\n\t\t),\n\t\tMessage(),\n\t\tField(CacheServerURLKeyName,\n\t\t\tPrefix(\" \"+CacheServerURLKeyName+\"=\"),\n\t\t),\n\t\tField(CacheServerStatusKeyName,\n\t\t\tPrefix(\" \"+CacheServerStatusKeyName+\"=\"),\n\t\t),\n\t}\n}\n\nfunc NewJSONFormatPlaceholders() Placeholders {\n\treturn Placeholders{\n\t\tPlainText(`{`),\n\t\tTime(\n\t\t\tPrefix(`\"time\":\"`),\n\t\t\tSuffix(`\"`),\n\t\t\tTimeFormat(RFC3339),\n\t\t\tEscape(JSONEscape),\n\t\t),\n\t\tLevel(\n\t\t\tPrefix(`, \"level\":\"`),\n\t\t\tSuffix(`\"`),\n\t\t\tEscape(JSONEscape),\n\t\t),\n\t\tField(WorkDirKeyName,\n\t\t\tPrefix(`, \"working-dir\":\"`),\n\t\t\tSuffix(`\"`),\n\t\t\tEscape(JSONEscape),\n\t\t),\n\t\tField(TFPathKeyName,\n\t\t\tPrefix(`, \"tf-path\":\"`),\n\t\t\tSuffix(`\"`),\n\t\t\tPathFormat(FilenamePath),\n\t\t\tEscape(JSONEscape),\n\t\t),\n\t\tField(TFCmdArgsKeyName,\n\t\t\tPrefix(`, \"tf-command-args\":[`),\n\t\t\tSuffix(`]`),\n\t\t\tEscape(JSONEscape),\n\t\t),\n\t\tMessage(\n\t\t\tPrefix(`, \"msg\":\"`),\n\t\t\tSuffix(`\"`),\n\t\t\tColor(DisableColor),\n\t\t\tEscape(JSONEscape),\n\t\t),\n\t\tPlainText(`}`),\n\t}\n}\n\nfunc NewKeyValueFormatPlaceholders() Placeholders {\n\treturn Placeholders{\n\t\tTime(\n\t\t\tPrefix(\"time=\"),\n\t\t\tTimeFormat(RFC3339),\n\t\t),\n\t\tLevel(\n\t\t\tPrefix(\" level=\"),\n\t\t),\n\t\tField(WorkDirKeyName,\n\t\t\tPrefix(\" prefix=\"),\n\t\t\tPathFormat(ShortRelativePath),\n\t\t),\n\t\tField(TFPathKeyName,\n\t\t\tPrefix(\" tf-path=\"),\n\t\t\tPathFormat(FilenamePath),\n\t\t),\n\t\tMessage(\n\t\t\tPrefix(\" msg=\"),\n\t\t\tColor(DisableColor),\n\t\t),\n\t}\n}\n\nfunc ParseFormat(str string) (Placeholders, error) {\n\tvar presets = map[string]func() Placeholders{\n\t\tBareFormatName:     NewBareFormatPlaceholders,\n\t\tPrettyFormatName:   NewPrettyFormatPlaceholders,\n\t\tJSONFormatName:     NewJSONFormatPlaceholders,\n\t\tKeyValueFormatName: NewKeyValueFormatPlaceholders,\n\t}\n\n\tfor name, formatFn := range presets {\n\t\tif name == str {\n\t\t\treturn formatFn(), nil\n\t\t}\n\t}\n\n\treturn nil, errors.Errorf(\"available values: %s\", strings.Join(slices.Collect(maps.Keys(presets)), \",\"))\n}\n"
  },
  {
    "path": "pkg/log/format/format_test.go",
    "content": "package format_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/options\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParseFormat(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname      string\n\t\tinput     string\n\t\texpectErr bool\n\t}{\n\t\t{name: \"bare\", input: \"bare\"},\n\t\t{name: \"pretty\", input: \"pretty\"},\n\t\t{name: \"json\", input: \"json\"},\n\t\t{name: \"key-value\", input: \"key-value\"},\n\t\t{name: \"nonexistent\", input: \"nonexistent\", expectErr: true},\n\t\t{name: \"empty\", input: \"\", expectErr: true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tphs, err := format.ParseFormat(tc.input)\n\t\t\tif tc.expectErr {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Nil(t, phs)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.NotNil(t, phs)\n\t\t\t\tassert.NotEmpty(t, phs)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFormatterFormat(t *testing.T) {\n\tt.Parallel()\n\n\tphs := format.NewBareFormatPlaceholders()\n\tfmtr := format.NewFormatter(phs)\n\tfmtr.SetDisabledColors(true)\n\n\tlogrusLogger := logrus.New()\n\tlogrusEntry := logrus.NewEntry(logrusLogger)\n\tlogrusEntry.Level = log.InfoLevel.ToLogrusLevel()\n\tlogrusEntry.Message = \"hello formatter\"\n\n\tentry := &log.Entry{\n\t\tEntry: logrusEntry,\n\t\tLevel: log.InfoLevel,\n\t}\n\n\toutput, err := fmtr.Format(entry)\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, output)\n\tassert.Contains(t, string(output), \"hello formatter\")\n}\n\nfunc TestFormatterDisabledOutput(t *testing.T) {\n\tt.Parallel()\n\n\tphs := format.NewBareFormatPlaceholders()\n\tfmtr := format.NewFormatter(phs)\n\tfmtr.SetDisabledOutput(true)\n\n\tlogrusLogger := logrus.New()\n\tlogrusEntry := logrus.NewEntry(logrusLogger)\n\tlogrusEntry.Level = log.InfoLevel.ToLogrusLevel()\n\tlogrusEntry.Message = \"should not appear\"\n\n\tentry := &log.Entry{\n\t\tEntry: logrusEntry,\n\t\tLevel: log.InfoLevel,\n\t}\n\n\toutput, err := fmtr.Format(entry)\n\trequire.NoError(t, err)\n\tassert.Nil(t, output)\n}\n\nfunc TestFormatterSetFormat(t *testing.T) {\n\tt.Parallel()\n\n\tfmtr := format.NewFormatter(nil)\n\n\terr := fmtr.SetFormat(\"bare\")\n\trequire.NoError(t, err)\n\n\tlogrusLogger := logrus.New()\n\tlogrusEntry := logrus.NewEntry(logrusLogger)\n\tlogrusEntry.Level = log.InfoLevel.ToLogrusLevel()\n\tlogrusEntry.Message = \"bare format\"\n\n\tentry := &log.Entry{\n\t\tEntry: logrusEntry,\n\t\tLevel: log.InfoLevel,\n\t}\n\n\toutput, err := fmtr.Format(entry)\n\trequire.NoError(t, err)\n\tassert.Contains(t, string(output), \"bare format\")\n}\n\nfunc TestFormatterSetCustomFormat(t *testing.T) {\n\tt.Parallel()\n\n\tfmtr := format.NewFormatter(nil)\n\tfmtr.SetDisabledColors(true)\n\n\terr := fmtr.SetCustomFormat(\"%level %msg\")\n\trequire.NoError(t, err)\n\n\tlogrusLogger := logrus.New()\n\tlogrusEntry := logrus.NewEntry(logrusLogger)\n\tlogrusEntry.Level = log.InfoLevel.ToLogrusLevel()\n\tlogrusEntry.Message = \"custom msg\"\n\n\tentry := &log.Entry{\n\t\tEntry:  logrusEntry,\n\t\tLevel:  log.InfoLevel,\n\t\tFields: log.Fields{},\n\t}\n\n\toutput, err := fmtr.Format(entry)\n\trequire.NoError(t, err)\n\n\toutputStr := string(output)\n\tassert.Contains(t, outputStr, \"info\")\n\tassert.Contains(t, outputStr, \"custom msg\")\n}\n\nfunc TestFormatterNilPlaceholders(t *testing.T) {\n\tt.Parallel()\n\n\tfmtr := format.NewFormatter(nil)\n\n\tlogrusLogger := logrus.New()\n\tlogrusEntry := logrus.NewEntry(logrusLogger)\n\tlogrusEntry.Level = log.InfoLevel.ToLogrusLevel()\n\tlogrusEntry.Message = \"nil placeholders\"\n\n\tentry := &log.Entry{\n\t\tEntry: logrusEntry,\n\t\tLevel: log.InfoLevel,\n\t}\n\n\toutput, err := fmtr.Format(entry)\n\trequire.NoError(t, err)\n\tassert.Nil(t, output)\n}\n\nfunc TestPlaceholderFormatsAccessible(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"bare_format\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tphs := format.NewBareFormatPlaceholders()\n\t\tassert.NotEmpty(t, phs)\n\n\t\t// Format should work with minimal data\n\t\tlogrusLogger := logrus.New()\n\t\tlogrusEntry := logrus.NewEntry(logrusLogger)\n\t\tlogrusEntry.Level = log.InfoLevel.ToLogrusLevel()\n\t\tlogrusEntry.Message = \"test\"\n\n\t\tdata := &options.Data{\n\t\t\tEntry: &log.Entry{\n\t\t\t\tEntry: logrusEntry,\n\t\t\t\tLevel: log.InfoLevel,\n\t\t\t},\n\t\t}\n\n\t\tresult, err := phs.Format(data)\n\t\trequire.NoError(t, err)\n\t\tassert.NotEmpty(t, result)\n\t})\n\n\tt.Run(\"pretty_format\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tphs := format.NewPrettyFormatPlaceholders()\n\t\tassert.NotEmpty(t, phs)\n\t})\n\n\tt.Run(\"json_format\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tphs := format.NewJSONFormatPlaceholders()\n\t\tassert.NotEmpty(t, phs)\n\t})\n\n\tt.Run(\"key_value_format\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tphs := format.NewKeyValueFormatPlaceholders()\n\t\tassert.NotEmpty(t, phs)\n\t})\n}\n\nfunc TestFormatterSetFormatInvalid(t *testing.T) {\n\tt.Parallel()\n\n\tfmtr := format.NewFormatter(nil)\n\terr := fmtr.SetFormat(\"nonexistent\")\n\tassert.Error(t, err)\n}\n\nfunc TestFormatterSetCustomFormatInvalid(t *testing.T) {\n\tt.Parallel()\n\n\tfmtr := format.NewFormatter(nil)\n\terr := fmtr.SetCustomFormat(\"%banana\")\n\tassert.Error(t, err)\n}\n\nfunc TestFormatterPlaceholderRegisterNames(t *testing.T) {\n\tt.Parallel()\n\n\tphs := placeholders.NewPlaceholderRegister()\n\tnames := phs.Names()\n\tassert.NotEmpty(t, names)\n\tassert.Contains(t, names, \"level\")\n\tassert.Contains(t, names, \"msg\")\n\tassert.Contains(t, names, \"time\")\n\tassert.Contains(t, names, \"interval\")\n}\n"
  },
  {
    "path": "pkg/log/format/formatter.go",
    "content": "package format\n\nimport (\n\t\"bytes\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/options\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\"\n)\n\n// Formatter implements logrus.Formatter.\nvar _ log.Formatter = new(Formatter)\n\ntype Formatter struct {\n\trelativePather *options.RelativePather\n\tbaseDir        string\n\tplaceholders   placeholders.Placeholders\n\tmu             sync.Mutex\n\tdisabledColors bool\n\tdisabledOutput bool\n}\n\n// NewFormatter returns a new Formatter instance with default values.\nfunc NewFormatter(phs placeholders.Placeholders) *Formatter {\n\treturn &Formatter{\n\t\tplaceholders: phs,\n\t}\n}\n\n// Format implements logrus.Format.\nfunc (formatter *Formatter) Format(entry *log.Entry) ([]byte, error) {\n\tif formatter.placeholders == nil || formatter.disabledOutput {\n\t\treturn nil, nil\n\t}\n\n\tbuf := entry.Buffer\n\tif buf == nil {\n\t\tbuf = new(bytes.Buffer)\n\t}\n\n\tstr, err := formatter.placeholders.Format(&options.Data{\n\t\tEntry:          entry,\n\t\tBaseDir:        formatter.baseDir,\n\t\tDisabledColors: formatter.disabledColors,\n\t\tRelativePather: formatter.relativePather,\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tformatter.mu.Lock()\n\tdefer formatter.mu.Unlock()\n\n\tif str != \"\" {\n\t\tif _, err := buf.WriteString(str); err != nil {\n\t\t\treturn nil, errors.New(err)\n\t\t}\n\n\t\tif err := buf.WriteByte('\\n'); err != nil {\n\t\t\treturn nil, errors.New(err)\n\t\t}\n\t}\n\n\treturn buf.Bytes(), nil\n}\n\n// SetBaseDir creates a set of relative paths that are used to convert full paths to relative ones.\nfunc (formatter *Formatter) SetBaseDir(baseDir string) error {\n\tpather, err := options.NewRelativePather(baseDir)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tformatter.relativePather = pather\n\tformatter.baseDir = baseDir\n\n\treturn nil\n}\n\n// DisableRelativePaths disables the conversion of absolute paths to relative ones.\nfunc (formatter *Formatter) DisableRelativePaths() {\n\tformatter.relativePather = nil\n}\n\n// SetFormat parses and sets log format.\nfunc (formatter *Formatter) SetFormat(str string) error {\n\tphs, err := ParseFormat(str)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tformatter.placeholders = phs\n\n\treturn nil\n}\n\n// SetCustomFormat parses and sets custom log format.\nfunc (formatter *Formatter) SetCustomFormat(str string) error {\n\tphs, err := placeholders.Parse(str)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tformatter.placeholders = phs\n\n\treturn nil\n}\n\n// SetDisabledColors enables/disables log colors.\nfunc (formatter *Formatter) SetDisabledColors(val bool) {\n\tformatter.disabledColors = val\n}\n\n// DisabledColors returns true if log colors are disabled.\nfunc (formatter *Formatter) DisabledColors() bool {\n\treturn formatter.disabledColors\n}\n\n// SetDisabledOutput enables/disables log output.\nfunc (formatter *Formatter) SetDisabledOutput(val bool) {\n\tformatter.disabledOutput = val\n}\n\n// DisabledOutput returns true if log output is disabled.\nfunc (formatter *Formatter) DisabledOutput() bool {\n\treturn formatter.disabledOutput\n}\n"
  },
  {
    "path": "pkg/log/format/options/align.go",
    "content": "package options\n\nimport (\n\t\"strings\"\n)\n\n// AlignOptionName is the option name.\nconst AlignOptionName = \"align\"\n\nconst (\n\tNoneAlign AlignValue = iota\n\tLeftAlign\n\tCenterAlign\n\tRightAlign\n)\n\nvar alignList = NewMapValue(map[AlignValue]string{ //nolint:gochecknoglobals\n\tLeftAlign:   \"left\",\n\tCenterAlign: \"center\",\n\tRightAlign:  \"right\",\n})\n\ntype AlignValue byte\n\ntype AlignOption struct {\n\t*CommonOption[AlignValue]\n}\n\n// Format implements `Option` interface.\nfunc (option *AlignOption) Format(_ *Data, val any) (any, error) {\n\tstr := toString(val)\n\n\twithoutSpaces := strings.TrimSpace(str)\n\tspaces := len(str) - len(withoutSpaces)\n\n\tswitch option.value.Get() {\n\tcase LeftAlign:\n\t\treturn withoutSpaces + strings.Repeat(\" \", spaces), nil\n\tcase RightAlign:\n\t\treturn strings.Repeat(\" \", spaces) + withoutSpaces, nil\n\tcase CenterAlign:\n\t\ttwoSides := 2\n\t\trightSpaces := (spaces - spaces%2) / twoSides\n\t\tleftSpaces := spaces - rightSpaces\n\n\t\treturn strings.Repeat(\" \", leftSpaces) + strings.TrimSpace(str) + strings.Repeat(\" \", rightSpaces), nil\n\tcase NoneAlign:\n\t}\n\n\treturn str, nil\n}\n\n// Align creates the option to align text relative to the edges.\nfunc Align(value AlignValue) Option {\n\treturn &AlignOption{\n\t\tCommonOption: NewCommonOption(AlignOptionName, alignList.Set(value)),\n\t}\n}\n"
  },
  {
    "path": "pkg/log/format/options/case.go",
    "content": "package options\n\nimport (\n\t\"strings\"\n\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n)\n\n// CaseOptionName is the option name.\nconst CaseOptionName = \"case\"\n\nconst (\n\tNoneCase CaseValue = iota\n\tUpperCase\n\tLowerCase\n\tCapitalizeCase\n)\n\nvar caseList = NewMapValue(map[CaseValue]string{ //nolint:gochecknoglobals\n\tUpperCase:      \"upper\",\n\tLowerCase:      \"lower\",\n\tCapitalizeCase: \"capitalize\",\n})\n\ntype CaseValue byte\n\ntype CaseOption struct {\n\t*CommonOption[CaseValue]\n}\n\n// Format implements `Option` interface.\nfunc (option *CaseOption) Format(_ *Data, val any) (any, error) {\n\tstr := toString(val)\n\n\tswitch option.value.Get() {\n\tcase UpperCase:\n\t\treturn strings.ToUpper(str), nil\n\tcase LowerCase:\n\t\treturn strings.ToLower(str), nil\n\tcase CapitalizeCase:\n\t\treturn cases.Title(language.English, cases.Compact).String(str), nil\n\tcase NoneCase:\n\t}\n\n\treturn str, nil\n}\n\n// Case creates the option to change the case of text.\nfunc Case(value CaseValue) Option {\n\treturn &CaseOption{\n\t\tCommonOption: NewCommonOption(CaseOptionName, caseList.Set(value)),\n\t}\n}\n"
  },
  {
    "path": "pkg/log/format/options/color.go",
    "content": "package options\n\nimport (\n\t\"maps\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/mgutz/ansi\"\n\t\"github.com/puzpuzpuz/xsync/v3\"\n)\n\n// ColorOptionName is the option name.\nconst ColorOptionName = \"color\"\n\nconst (\n\tNoneColor ColorValue = iota + 255\n\tDisableColor\n\tGradientColor\n\tPresetColor\n\n\tBlackColor\n\tRedColor\n\tWhiteColor\n\tYellowColor\n\tGreenColor\n\tBlueColor\n\tCyanColor\n\tMagentaColor\n\n\tLightBlueColor\n\tLightBlackColor\n\tLightRedColor\n\tLightGreenColor\n\tLightYellowColor\n\tLightMagentaColor\n\tLightCyanColor\n\tLightWhiteColor\n)\n\nvar (\n\tcolorList = NewColorList(map[ColorValue]string{ //nolint:gochecknoglobals\n\t\tPresetColor:   \"preset\",\n\t\tGradientColor: \"gradient\",\n\t\tDisableColor:  \"disable\",\n\n\t\tBlackColor:   \"black\",\n\t\tRedColor:     \"red\",\n\t\tWhiteColor:   \"white\",\n\t\tYellowColor:  \"yellow\",\n\t\tGreenColor:   \"green\",\n\t\tCyanColor:    \"cyan\",\n\t\tMagentaColor: \"magenta\",\n\t\tBlueColor:    \"blue\",\n\n\t\tLightBlueColor:    \"light-blue\",\n\t\tLightBlackColor:   \"light-black\",\n\t\tLightRedColor:     \"light-red\",\n\t\tLightGreenColor:   \"light-green\",\n\t\tLightYellowColor:  \"light-yellow\",\n\t\tLightMagentaColor: \"light-magenta\",\n\t\tLightCyanColor:    \"light-cyan\",\n\t\tLightWhiteColor:   \"light-white\",\n\t})\n\n\tcolorScheme = ColorScheme{ //nolint:gochecknoglobals\n\t\tBlackColor:        \"black\",\n\t\tRedColor:          \"red\",\n\t\tWhiteColor:        \"white\",\n\t\tYellowColor:       \"yellow\",\n\t\tGreenColor:        \"green\",\n\t\tCyanColor:         \"cyan\",\n\t\tBlueColor:         \"blue\",\n\t\tMagentaColor:      \"magenta\",\n\t\tLightBlueColor:    \"blue+h\",\n\t\tLightBlackColor:   \"black+h\",\n\t\tLightRedColor:     \"red+h\",\n\t\tLightGreenColor:   \"green+h\",\n\t\tLightYellowColor:  \"yellow+h\",\n\t\tLightMagentaColor: \"magenta+h\",\n\t\tLightCyanColor:    \"cyan+h\",\n\t\tLightWhiteColor:   \"white+h\",\n\t}\n)\n\ntype ColorList struct {\n\tMapValue[ColorValue]\n}\n\nfunc NewColorList(list map[ColorValue]string) ColorList {\n\treturn ColorList{\n\t\tMapValue: NewMapValue(list),\n\t}\n}\n\nfunc (val *ColorList) Set(v ColorValue) *ColorList {\n\treturn &ColorList{MapValue: *val.MapValue.Set(v)}\n}\n\nfunc (val *ColorList) Parse(str string) error {\n\tif num, err := strconv.Atoi(str); err == nil && num >= 0 && num <= 255 {\n\t\tval.value = ColorValue(byte(num))\n\n\t\treturn nil\n\t}\n\n\tif err := val.MapValue.Parse(str); err != nil {\n\t\treturn errors.Errorf(\"available values: 0..255,%s\", strings.Join(slices.Collect(maps.Values(val.list)), \",\"))\n\t}\n\n\treturn nil\n}\n\ntype ColorScheme map[ColorValue]ColorStyle\n\nfunc (scheme ColorScheme) Compile() compiledColorScheme {\n\tcompiled := make(compiledColorScheme, len(scheme))\n\n\tfor name, val := range scheme {\n\t\tcompiled[name] = val.ColorFunc()\n\t}\n\n\tfor i := range 255 {\n\t\ts := strconv.Itoa(i)\n\n\t\tcompiled[ColorValue(i)] = ColorStyle(s).ColorFunc()\n\t}\n\n\treturn compiled\n}\n\ntype ColorStyle string\n\nfunc (val ColorStyle) ColorFunc() ColorFunc {\n\treturn ansi.ColorFunc(string(val))\n}\n\ntype ColorFunc func(string) string\n\ntype ColorValue int\n\ntype compiledColorScheme map[ColorValue]ColorFunc\n\ntype ColorOption struct {\n\t*CommonOption[ColorValue]\n\tcompiledColors compiledColorScheme\n\tgradientColor  *gradientColor\n}\n\n// Format implements `Option` interface.\nfunc (color *ColorOption) Format(data *Data, val any) (any, error) {\n\tvar (\n\t\tstr   = toString(val)\n\t\tvalue = color.value.Get()\n\t)\n\n\tif value == NoneColor {\n\t\treturn str, nil\n\t}\n\n\tif value == DisableColor || data.DisabledColors {\n\t\treturn log.RemoveAllASCISeq(str), nil\n\t}\n\n\tif value == PresetColor && data.PresetColorFn != nil {\n\t\tvalue = data.PresetColorFn()\n\t}\n\n\tif value == GradientColor && color.gradientColor != nil {\n\t\tvalue = color.gradientColor.Value(str)\n\t}\n\n\tif colorFn, ok := color.compiledColors[value]; ok {\n\t\tstr = colorFn(str)\n\t}\n\n\treturn str, nil\n}\n\n// Color creates the option to change the color of text.\nfunc Color(val ColorValue) Option {\n\treturn &ColorOption{\n\t\tCommonOption:   NewCommonOption(ColorOptionName, colorList.Set(val)),\n\t\tcompiledColors: colorScheme.Compile(),\n\t\tgradientColor:  newGradientColor(),\n\t}\n}\n\nvar (\n\t// defaultAutoColorValues contains ANSI color codes that are assigned sequentially to each unique text in a rotating order\n\t// https://user-images.githubusercontent.com/995050/47952855-ecb12480-df75-11e8-89d4-ac26c50e80b9.png\n\t// https://www.hackitu.de/termcolor256/\n\tdefaultAutoColorValues = []ColorValue{ //nolint:gochecknoglobals\n\t\t66,\n\t\t67,\n\t\t95,\n\t\t96,\n\t\t102,\n\t\t103,\n\t\t108,\n\t\t109,\n\t\t138,\n\t\t139,\n\t\t144,\n\t\t145,\n\t}\n)\n\ntype gradientColor struct {\n\t// cache stores unique text with their color code.\n\t// We use [xsync.MapOf](https://github.com/puzpuzpuz/xsync?tab=readme-ov-file#map) instead of standard `sync.Map` since it's faster and has generic types.\n\tcache  *xsync.MapOf[string, ColorValue]\n\tvalues []ColorValue\n\tmu     sync.Mutex\n\n\t// nextStyleIndex is used to get the next style from the `codes` list for a newly discovered text.\n\tnextStyleIndex int\n}\n\nfunc newGradientColor() *gradientColor {\n\treturn &gradientColor{\n\t\tcache:  xsync.NewMapOf[string, ColorValue](),\n\t\tvalues: defaultAutoColorValues,\n\t}\n}\n\nfunc (color *gradientColor) Value(text string) ColorValue {\n\tcolor.mu.Lock()\n\tdefer color.mu.Unlock()\n\n\tif colorCode, ok := color.cache.Load(text); ok {\n\t\treturn colorCode\n\t}\n\n\tif color.nextStyleIndex >= len(color.values) {\n\t\tcolor.nextStyleIndex = 0\n\t}\n\n\tcolorCode := color.values[color.nextStyleIndex]\n\n\tcolor.cache.Store(text, colorCode)\n\n\tcolor.nextStyleIndex++\n\n\treturn colorCode\n}\n"
  },
  {
    "path": "pkg/log/format/options/common.go",
    "content": "package options\n\nimport (\n\t\"fmt\"\n\t\"maps\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\ntype CommonOption[T comparable] struct {\n\tvalue OptionValue[T]\n\tname  string\n}\n\n// NewCommonOption creates a new Common option.\nfunc NewCommonOption[T comparable](name string, value OptionValue[T]) *CommonOption[T] {\n\treturn &CommonOption[T]{\n\t\tname:  name,\n\t\tvalue: value,\n\t}\n}\n\n// String implements `fmt.Stringer` interface.\nfunc (option *CommonOption[T]) String() string {\n\treturn fmt.Sprintf(\"%v\", option.value.Get())\n}\n\n// Name implements `Option` interface.\nfunc (option *CommonOption[T]) Name() string {\n\treturn option.name\n}\n\n// Format implements `Option` interface.\nfunc (option *CommonOption[T]) Format(_ *Data, str string) (string, error) {\n\treturn str, nil\n}\n\n// ParseValue implements `Option` interface.\nfunc (option *CommonOption[T]) ParseValue(str string) error {\n\treturn option.value.Parse(str)\n}\n\ntype StringValue string\n\nfunc NewStringValue(val string) *StringValue {\n\tv := StringValue(val)\n\treturn &v\n}\n\nfunc (val *StringValue) Parse(str string) error {\n\t*val = StringValue(str)\n\n\treturn nil\n}\n\nfunc (val *StringValue) Get() string {\n\treturn string(*val)\n}\n\ntype IntValue int\n\nfunc NewIntValue(val int) *IntValue {\n\tv := IntValue(val)\n\treturn &v\n}\n\nfunc (val *IntValue) Parse(str string) error {\n\tv, err := strconv.Atoi(str)\n\tif err != nil {\n\t\treturn errors.Errorf(\"incorrect option value: %s\", str)\n\t}\n\n\t*val = IntValue(v)\n\n\treturn nil\n}\n\nfunc (val *IntValue) Get() int {\n\treturn int(*val)\n}\n\ntype MapValue[T comparable] struct {\n\tlist  map[T]string\n\tvalue T\n}\n\nfunc NewMapValue[T comparable](list map[T]string) MapValue[T] {\n\treturn MapValue[T]{\n\t\tlist: list,\n\t}\n}\n\nfunc (val *MapValue[T]) Get() T {\n\treturn val.value\n}\n\nfunc (val MapValue[T]) Set(v T) *MapValue[T] {\n\tval.value = v\n\n\treturn &val\n}\n\nfunc (val *MapValue[T]) Parse(str string) error {\n\tfor v, name := range val.list {\n\t\tif name == str {\n\t\t\tval.value = v\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tlist := slices.Sorted(maps.Values(val.list))\n\n\treturn errors.Errorf(\"available values: %s\", strings.Join(list, \",\"))\n}\n\nfunc (val *MapValue[T]) Filter(vals ...T) MapValue[T] {\n\tnewVal := MapValue[T]{\n\t\tlist: make(map[T]string, len(vals)),\n\t}\n\n\tfor _, v := range vals {\n\t\tif name, ok := val.list[v]; ok {\n\t\t\tnewVal.list[v] = name\n\t\t}\n\t}\n\n\treturn newVal\n}\n"
  },
  {
    "path": "pkg/log/format/options/content.go",
    "content": "package options\n\n// ContentOptionName is the option name.\nconst ContentOptionName = \"content\"\n\ntype ContentOption struct {\n\t*CommonOption[string]\n}\n\n// Format implements `Option` interface.\nfunc (option *ContentOption) Format(_ *Data, val any) (any, error) {\n\tif val := option.value.Get(); val != \"\" {\n\t\treturn val, nil\n\t}\n\n\treturn val, nil\n}\n\n// Content creates the option that sets the content.\nfunc Content(val string) Option {\n\treturn &ContentOption{\n\t\tCommonOption: NewCommonOption(ContentOptionName, NewStringValue(val)),\n\t}\n}\n"
  },
  {
    "path": "pkg/log/format/options/errors.go",
    "content": "package options\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// InvalidOptionError is an invalid `option` syntax error.\ntype InvalidOptionError struct {\n\tstr string\n}\n\n// NewInvalidOptionError returns a new `InvalidOptionError` instance.\nfunc NewInvalidOptionError(str string) *InvalidOptionError {\n\treturn &InvalidOptionError{\n\t\tstr: str,\n\t}\n}\n\nfunc (err InvalidOptionError) Error() string {\n\treturn fmt.Sprintf(\"invalid option syntax %q\", err.str)\n}\n\n// EmptyOptionNameError is an empty `option` name error.\ntype EmptyOptionNameError struct {\n\tstr string\n}\n\n// NewEmptyOptionNameError returns a new `EmptyOptionNameError` instance.\nfunc NewEmptyOptionNameError(str string) *EmptyOptionNameError {\n\treturn &EmptyOptionNameError{\n\t\tstr: str,\n\t}\n}\n\nfunc (err EmptyOptionNameError) Error() string {\n\treturn fmt.Sprintf(\"empty option name %q\", err.str)\n}\n\n// InvalidOptionNameError is an invalid `option` name error.\ntype InvalidOptionNameError struct {\n\tname string\n\topts Options\n}\n\n// NewInvalidOptionNameError returns a new `InvalidOptionNameError` instance.\nfunc NewInvalidOptionNameError(name string, opts Options) *InvalidOptionNameError {\n\treturn &InvalidOptionNameError{\n\t\tname: name,\n\t\topts: opts,\n\t}\n}\n\nfunc (err InvalidOptionNameError) Error() string {\n\treturn fmt.Sprintf(\"invalid option name %q, available names: %s\", err.name, strings.Join(err.opts.Names(), \",\"))\n}\n\n// InvalidOptionValueError is an invalid `option` value error.\ntype InvalidOptionValueError struct {\n\topt Option\n\terr error\n\tval string\n}\n\n// NewInvalidOptionValueError returns a new `InvalidOptionValueError` instance.\nfunc NewInvalidOptionValueError(opt Option, val string, err error) *InvalidOptionValueError {\n\treturn &InvalidOptionValueError{\n\t\tval: val,\n\t\topt: opt,\n\t\terr: err,\n\t}\n}\n\nfunc (err InvalidOptionValueError) Error() string {\n\treturn fmt.Sprintf(\"option %q, invalid value %q, %v\", err.opt.Name(), err.val, err.err)\n}\n\nfunc (err InvalidOptionValueError) Unwrap() error {\n\treturn err.err\n}\n"
  },
  {
    "path": "pkg/log/format/options/escape.go",
    "content": "package options\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n)\n\n// EscapeOptionName is the option name.\nconst EscapeOptionName = \"escape\"\n\nconst (\n\tNoneEscape EscapeValue = iota\n\tJSONEscape\n)\n\nvar escapeList = NewMapValue(map[EscapeValue]string{ //nolint:gochecknoglobals\n\tJSONEscape: \"json\",\n})\n\ntype EscapeValue byte\n\ntype EscapeOption struct {\n\t*CommonOption[EscapeValue]\n}\n\n// Format implements `Option` interface.\nfunc (option *EscapeOption) Format(_ *Data, val any) (any, error) {\n\tif option.value.Get() != JSONEscape {\n\t\treturn val, nil\n\t}\n\n\tjsonStr, err := json.Marshal(val)\n\tif err != nil {\n\t\treturn \"\", errors.New(err)\n\t}\n\n\t// Trim the beginning and trailing \" character.\n\treturn string(jsonStr[1 : len(jsonStr)-1]), nil\n}\n\n// Escape creates the option to escape text.\nfunc Escape(val EscapeValue) Option {\n\treturn &EscapeOption{\n\t\tCommonOption: NewCommonOption(EscapeOptionName, escapeList.Set(val)),\n\t}\n}\n"
  },
  {
    "path": "pkg/log/format/options/level_format.go",
    "content": "package options\n\n// LevelFormatOptionName is the option name.\nconst LevelFormatOptionName = \"format\"\n\nconst (\n\tLevelFormatFull LevelFormatValue = iota\n\tLevelFormatShort\n\tLevelFormatTiny\n)\n\nvar levelFormatList = NewMapValue(map[LevelFormatValue]string{ //nolint:gochecknoglobals\n\tLevelFormatTiny:  \"tiny\",\n\tLevelFormatShort: \"short\",\n\tLevelFormatFull:  \"full\",\n})\n\ntype LevelFormatValue byte\n\ntype LevelFormatOption struct {\n\t*CommonOption[LevelFormatValue]\n}\n\n// Format implements `Option` interface.\nfunc (format *LevelFormatOption) Format(data *Data, _ any) (any, error) {\n\tswitch format.value.Get() {\n\tcase LevelFormatTiny:\n\t\treturn data.Level.TinyName(), nil\n\tcase LevelFormatShort:\n\t\treturn data.Level.ShortName(), nil\n\tcase LevelFormatFull:\n\t}\n\n\treturn data.Level.FullName(), nil\n}\n\n// LevelFormat creates the option to format level name.\nfunc LevelFormat(val LevelFormatValue) Option {\n\treturn &LevelFormatOption{\n\t\tCommonOption: NewCommonOption(LevelFormatOptionName, levelFormatList.Set(val)),\n\t}\n}\n"
  },
  {
    "path": "pkg/log/format/options/option.go",
    "content": "// Package options represents a set of placeholders options.\npackage options\n\nimport (\n\t\"reflect\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"slices\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// Constants for parsing options.\nconst (\n\tOptNameValueSep = \"=\"\n\tOptSep          = \",\"\n\tOptStartSign    = \"(\"\n\tOptEndSign      = \")\"\n)\n\nconst splitIntoNameAndValue = 2\n\n// OptionValue contains the value of the option.\ntype OptionValue[T any] interface {\n\t// Configure parses and sets the value of the option.\n\tParse(str string) error\n\t// Get returns the value of the option.\n\tGet() T\n}\n\n// Option represents a value modifier of placeholders.\ntype Option interface {\n\t// Name returns the name of the option.\n\tName() string\n\t// Format formats the given string.\n\tFormat(data *Data, str any) (any, error)\n\t// ParseValue parses and sets the value of the option.\n\tParseValue(str string) error\n}\n\n// Data is a log entry data.\ntype Data struct {\n\t*log.Entry\n\tRelativePather *RelativePather\n\tPresetColorFn  func() ColorValue\n\tBaseDir        string\n\tDisabledColors bool\n}\n\n// Options is a set of Options.\ntype Options []Option\n\n// Get returns the option with the given name.\nfunc (opts Options) Get(name string) Option {\n\tfor _, opt := range opts {\n\t\tif opt.Name() == name {\n\t\t\treturn opt\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Names returns names of the options.\nfunc (opts Options) Names() []string {\n\tvar names = make([]string, len(opts))\n\n\tfor i, opt := range opts {\n\t\tnames[i] = opt.Name()\n\t}\n\n\treturn names\n}\n\n// Merge replaces options with the same name and adds new ones to the end.\nfunc (opts Options) Merge(withOpts ...Option) Options {\n\tfor i := range opts {\n\t\tfor t := range withOpts {\n\t\t\tif reflect.TypeOf(opts[i]) == reflect.TypeOf(withOpts[t]) {\n\t\t\t\topts[i] = withOpts[t]\n\t\t\t\twithOpts = slices.Delete(withOpts, t, t+1)\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn append(opts, withOpts...)\n}\n\n// Format returns the formatted value.\nfunc (opts Options) Format(data *Data, str any) (string, error) {\n\tvar err error\n\n\tfor _, opt := range opts {\n\t\tstr, err = opt.Format(data, str)\n\t\tif str == \"\" || err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\treturn toString(str), nil\n}\n\n// Configure parsers the given `str` to configure the `opts` and returns the rest of the given `str`.\n//\n// e.g. (color=green, case=upper) some-text\" sets `color` option to `green`, `case` option to `upper` and returns \" some-text\".\nfunc (opts Options) Configure(str string) (string, error) {\n\tif len(str) == 0 || !strings.HasPrefix(str, OptStartSign) {\n\t\treturn str, nil\n\t}\n\n\tstr = str[1:]\n\n\tfor {\n\t\tvar (\n\t\t\tok  bool\n\t\t\terr error\n\t\t)\n\n\t\tif str, ok = nextOption(str); !ok {\n\t\t\treturn str, nil\n\t\t}\n\n\t\tparts := strings.SplitN(str, OptNameValueSep, splitIntoNameAndValue)\n\t\tif len(parts) != splitIntoNameAndValue {\n\t\t\treturn \"\", errors.New(NewInvalidOptionError(str))\n\t\t}\n\n\t\tname := strings.TrimSpace(parts[0])\n\n\t\tif name == \"\" {\n\t\t\treturn \"\", errors.New(NewEmptyOptionNameError(str))\n\t\t}\n\n\t\topt := opts.Get(name)\n\t\tif opt == nil {\n\t\t\treturn \"\", errors.New(NewInvalidOptionNameError(name, opts))\n\t\t}\n\n\t\tif str, err = setOptionValue(opt, parts[1]); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n}\n\n// setOptionValue parses the given `str` and sets the value for the given `opt` and returns the rest of the given `str`.\n//\n// e.g. \"green, case=upper) some-text\" sets \"green\" to the option and returns \", case=upper) some-text\".\n// e.g. \"' quoted value ') some-text\" sets \" quoted value \" to the option and returns \") some-text\".\nfunc setOptionValue(opt Option, str string) (string, error) {\n\tvar quoteChar byte\n\n\tfor index := range str {\n\t\tif quoteOpened(str[:index], &quoteChar) {\n\t\t\tcontinue\n\t\t}\n\n\t\tlastSign := str[index : index+1]\n\t\tif !strings.HasSuffix(lastSign, OptSep) && !strings.HasSuffix(lastSign, OptEndSign) {\n\t\t\tcontinue\n\t\t}\n\n\t\tval := strings.TrimSpace(str[:index])\n\t\tval = strings.Trim(val, \"'\")\n\t\tval = strings.Trim(val, \"\\\"\")\n\n\t\tif err := opt.ParseValue(val); err != nil {\n\t\t\treturn \"\", errors.New(NewInvalidOptionValueError(opt, val, err))\n\t\t}\n\n\t\treturn str[index:], nil\n\t}\n\n\treturn str, nil\n}\n\n// nextOption returns true if the given `str` contains one more option\n// and returns the given `str` without separator sign \",\" or \")\".\n//\n// e.g. \",color=green) some-text\" returns \"color=green) some-text\" and `true`.\n// e.g. \"(color=green) some-text\" returns \"color=green) some-text\" and `true`.\n// e.g. \") some-text\"  returns \" some-text\" and `false`.\nfunc nextOption(str string) (string, bool) {\n\tstr = strings.TrimLeftFunc(str, unicode.IsSpace)\n\n\tswitch {\n\tcase strings.HasPrefix(str, OptEndSign):\n\t\treturn str[1:], false\n\tcase strings.HasPrefix(str, OptSep):\n\t\treturn str[1:], true\n\t}\n\n\treturn str, true\n}\n\n// quoteOpened returns true if the given `str` contains an unclosed quote.\n//\n// e.g. \"%(content=' level\" return `true`.\n// e.g. \"%(content=' level '\" return `false`.\n// e.g. \"%(content=\\\" level\" return `true`.\nfunc quoteOpened(str string, quoteChar *byte) bool {\n\tstrlen := len(str)\n\n\tif strlen == 0 {\n\t\treturn false\n\t}\n\n\tchar := str[strlen-1]\n\n\tif char == '\"' || char == '\\'' {\n\t\tif *quoteChar == 0 {\n\t\t\t*quoteChar = char\n\t\t} else if *quoteChar == char && (strlen < 2 || str[strlen-2] != '\\\\') {\n\t\t\t*quoteChar = 0\n\t\t}\n\t}\n\n\treturn *quoteChar != 0\n}\n"
  },
  {
    "path": "pkg/log/format/options/path_format.go",
    "content": "package options\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// PathFormatOptionName is the option name.\nconst PathFormatOptionName = \"path\"\n\nconst (\n\tNonePath PathFormatValue = iota\n\tRelativePath\n\tShortRelativePath\n\tShortPath\n\tFilenamePath\n\tDirectoryPath\n)\n\nvar pathFormatList = NewMapValue(map[PathFormatValue]string{ //nolint:gochecknoglobals\n\tRelativePath:      \"relative\",\n\tShortRelativePath: \"short-relative\",\n\tShortPath:         \"short\",\n\tFilenamePath:      \"filename\",\n\tDirectoryPath:     \"dir\",\n})\n\ntype PathFormatValue byte\n\ntype PathFormatOption struct {\n\t*CommonOption[PathFormatValue]\n}\n\n// Format implements `Option` interface.\nfunc (option *PathFormatOption) Format(data *Data, val any) (any, error) {\n\tstr := toString(val)\n\n\tswitch option.value.Get() {\n\tcase RelativePath:\n\t\tif data.RelativePather == nil {\n\t\t\tbreak\n\t\t}\n\n\t\treturn data.RelativePather.ReplaceAbsPaths(str), nil\n\tcase ShortRelativePath:\n\t\tif data.RelativePather == nil {\n\t\t\tbreak\n\t\t}\n\n\t\treturn option.shortRelativePath(data, str), nil\n\tcase ShortPath:\n\t\tif str == data.BaseDir {\n\t\t\treturn \"\", nil\n\t\t}\n\n\t\treturn str, nil\n\tcase FilenamePath:\n\t\treturn filepath.Base(str), nil\n\tcase DirectoryPath:\n\t\treturn filepath.Dir(str), nil\n\tcase NonePath:\n\t}\n\n\treturn val, nil\n}\n\nfunc (option *PathFormatOption) shortRelativePath(data *Data, str string) string {\n\tif str == data.BaseDir {\n\t\treturn \"\"\n\t}\n\n\tstr = data.RelativePather.ReplaceAbsPaths(str)\n\n\tif strings.HasPrefix(str, log.CurDirWithSeparator) {\n\t\treturn str[len(log.CurDirWithSeparator):]\n\t}\n\n\treturn str\n}\n\n// PathFormat creates the option to format the paths.\nfunc PathFormat(val PathFormatValue, allowed ...PathFormatValue) Option {\n\tlist := pathFormatList\n\tif len(allowed) > 0 {\n\t\tlist = list.Filter(allowed...)\n\t}\n\n\treturn &PathFormatOption{\n\t\tCommonOption: NewCommonOption(PathFormatOptionName, list.Set(val)),\n\t}\n}\n\n// RelativePather replaces absolute paths with relative ones,\n// For better performance, during instance creation, we creating a cache of relative paths for each subdirectory of baseDir.\n//\n// Example of cache:\n// /path/to/dir ./\n// /path/to     ../\n// /path        ../..\ntype RelativePather struct {\n\trelPaths    []string\n\tabsPathsReg []*regexp.Regexp\n}\n\n// NewRelativePather returns a new RelativePather instance.\n// It returns an error if the cache of relative paths could not be created for the given `baseDir`.\nfunc NewRelativePather(baseDir string) (*RelativePather, error) {\n\tbaseDir = filepath.Clean(baseDir)\n\n\tpathSeparator := string(os.PathSeparator)\n\tdirs := strings.Split(baseDir, pathSeparator)\n\tabsPath := dirs[0]\n\tdirs = dirs[1:]\n\n\trelPaths := make([]string, len(dirs))\n\tabsPathsReg := make([]*regexp.Regexp, len(dirs))\n\treversIndex := len(dirs)\n\n\tfor _, dir := range dirs {\n\t\tabsPath = filepath.Join(absPath, pathSeparator, dir)\n\n\t\trelPath, err := filepath.Rel(baseDir, absPath)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(err)\n\t\t}\n\n\t\treversIndex--\n\t\trelPaths[reversIndex] = relPath\n\n\t\tregStr := fmt.Sprintf(`(^|[^%[1]s\\w])%[2]s([%[1]s\"'\\s]|$)`, regexp.QuoteMeta(pathSeparator), regexp.QuoteMeta(absPath))\n\t\tabsPathsReg[reversIndex] = regexp.MustCompile(regStr)\n\t}\n\n\treturn &RelativePather{\n\t\tabsPathsReg: absPathsReg,\n\t\trelPaths:    relPaths,\n\t}, nil\n}\n\nfunc (hook *RelativePather) ReplaceAbsPaths(str string) string {\n\tfor i, absPath := range hook.absPathsReg {\n\t\tstr = absPath.ReplaceAllString(str, \"$1\"+hook.relPaths[i]+\"$2\")\n\t}\n\n\treturn str\n}\n"
  },
  {
    "path": "pkg/log/format/options/prefix.go",
    "content": "package options\n\n// PrefixOptionName is the option name.\nconst PrefixOptionName = \"prefix\"\n\ntype PrefixOption struct {\n\t*CommonOption[string]\n}\n\n// Format implements `Option` interface.\nfunc (option *PrefixOption) Format(_ *Data, val any) (any, error) {\n\treturn option.value.Get() + toString(val), nil\n}\n\n// Prefix creates the option to add a prefix to the text.\nfunc Prefix(val string) Option {\n\treturn &PrefixOption{\n\t\tCommonOption: NewCommonOption(PrefixOptionName, NewStringValue(val)),\n\t}\n}\n"
  },
  {
    "path": "pkg/log/format/options/suffix.go",
    "content": "package options\n\n// SuffixOptionName is the option name.\nconst SuffixOptionName = \"suffix\"\n\ntype SuffixOption struct {\n\t*CommonOption[string]\n}\n\n// Format implements `Option` interface.\nfunc (option *SuffixOption) Format(_ *Data, val any) (any, error) {\n\treturn toString(val) + option.value.Get(), nil\n}\n\n// Suffix creates the option to add a suffix to the text.\nfunc Suffix(val string) Option {\n\treturn &SuffixOption{\n\t\tCommonOption: NewCommonOption(SuffixOptionName, NewStringValue(val)),\n\t}\n}\n"
  },
  {
    "path": "pkg/log/format/options/time_format.go",
    "content": "package options\n\nimport (\n\t\"maps\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n)\n\n// TimeFormatOptionName is the option name.\nconst TimeFormatOptionName = \"format\"\n\nconst (\n\tDateTime    = \"date-time\"\n\tDateOnly    = \"date-only\"\n\tTimeOnly    = \"time-only\"\n\tRFC3339     = \"rfc3339\"\n\tRFC3339Nano = \"rfc3339-nano\"\n\n\tHour24Zero     = \"H\"\n\tHour12Zero     = \"h\"\n\tHour12         = \"g\"\n\tMinZero        = \"i\"\n\tSecZero        = \"s\"\n\tMilliSec       = \"v\"\n\tMicroSec       = \"u\"\n\tYearFull       = \"Y\"\n\tYear           = \"y\"\n\tMonthNumZero   = \"m\"\n\tMonthNum       = \"n\"\n\tMonthText      = \"M\"\n\tDayZero        = \"d\"\n\tDay            = \"j\"\n\tDayText        = \"D\"\n\tPMUpper        = \"A\"\n\tPMLower        = \"a\"\n\tTZText         = \"T\"\n\tTZNumWithColon = \"P\"\n\tTZNum          = \"O\"\n)\n\nvar (\n\ttimeFormatList = NewTimeFormatValue(map[string]string{ //nolint:gochecknoglobals\n\t\tYearFull:       \"2006\",\n\t\tYear:           \"06\",\n\t\tMonthNumZero:   \"01\",\n\t\tMonthNum:       \"1\",\n\t\tMonthText:      \"Jan\",\n\t\tDay:            \"2\",\n\t\tDayZero:        \"02\",\n\t\tDayText:        \"Mon\",\n\t\tPMUpper:        \"PM\",\n\t\tPMLower:        \"pm\",\n\t\tHour24Zero:     \"15\",\n\t\tHour12Zero:     \"03\",\n\t\tHour12:         \"3\",\n\t\tMinZero:        \"04\",\n\t\tSecZero:        \"05\",\n\t\tMicroSec:       \".000000\",\n\t\tMilliSec:       \".000\",\n\t\tTZText:         \"MST\",\n\t\tTZNum:          \"-0700\",\n\t\tTZNumWithColon: \"-07:00\",\n\t\tRFC3339:        time.RFC3339,\n\t\tRFC3339Nano:    time.RFC3339Nano,\n\t\tDateTime:       time.DateTime,\n\t\tDateOnly:       time.DateOnly,\n\t\tTimeOnly:       time.TimeOnly,\n\t})\n)\n\ntype TimeFormatValue struct {\n\tMapValue[string]\n}\n\nfunc NewTimeFormatValue(list map[string]string) *TimeFormatValue {\n\treturn &TimeFormatValue{\n\t\tMapValue: NewMapValue(list),\n\t}\n}\n\nfunc (val TimeFormatValue) SortedKeys() []string {\n\tkeys := maps.Keys(val.list)\n\n\treturn slices.SortedFunc(keys, func(a, b string) int {\n\t\treturn strings.Compare(val.list[a], val.list[b])\n\t})\n}\n\nfunc (val TimeFormatValue) Set(v string) *TimeFormatValue {\n\tval.value = timeFormatList.Value(v)\n\n\treturn &val\n}\n\nfunc (val TimeFormatValue) Value(str string) string {\n\tfor _, key := range val.SortedKeys() {\n\t\tstr = strings.ReplaceAll(str, key, val.list[key])\n\t}\n\n\treturn str\n}\n\nfunc (val *TimeFormatValue) Parse(str string) error {\n\tval.value = timeFormatList.Value(str)\n\n\treturn nil\n}\n\ntype TimeFormatOption struct {\n\t*CommonOption[string]\n}\n\n// Format implements `Option` interface.\nfunc (option *TimeFormatOption) Format(data *Data, _ any) (any, error) {\n\treturn data.Time.Format(option.value.Get()), nil\n}\n\nfunc TimeFormat(val string) Option {\n\treturn &TimeFormatOption{\n\t\tCommonOption: NewCommonOption(TimeFormatOptionName, timeFormatList.Set(val)),\n\t}\n}\n"
  },
  {
    "path": "pkg/log/format/options/util.go",
    "content": "package options\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\nfunc toString(val any) string {\n\tswitch val := val.(type) {\n\tcase string:\n\t\treturn val\n\tcase []string:\n\t\treturn strings.Join(val, \" \")\n\t}\n\n\treturn fmt.Sprintf(\"%v\", val)\n}\n"
  },
  {
    "path": "pkg/log/format/options/width.go",
    "content": "package options\n\nimport (\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// WidthOptionName is the option name.\nconst WidthOptionName = \"width\"\n\ntype WidthOption struct {\n\t*CommonOption[int]\n}\n\n// Format implements `Option` interface.\nfunc (option *WidthOption) Format(_ *Data, val any) (any, error) {\n\tstr := toString(val)\n\n\twidth := option.value.Get()\n\tif width == 0 {\n\t\treturn str, nil\n\t}\n\n\tstrLen := len(log.RemoveAllASCISeq(str))\n\n\tif width < strLen {\n\t\treturn str[:width], nil\n\t}\n\n\treturn str + strings.Repeat(\" \", width-strLen), nil\n}\n\n// Width creates the option to set the column width.\nfunc Width(val int) Option {\n\treturn &WidthOption{\n\t\tCommonOption: NewCommonOption(WidthOptionName, NewIntValue(val)),\n\t}\n}\n"
  },
  {
    "path": "pkg/log/format/placeholders/common.go",
    "content": "package placeholders\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/options\"\n)\n\n// WithCommonOptions is a set of common options that are used in all placeholders.\nfunc WithCommonOptions(opts ...options.Option) options.Options {\n\treturn options.Options(append(opts,\n\t\toptions.Content(\"\"),\n\t\toptions.Escape(options.NoneEscape),\n\t\toptions.Case(options.NoneCase),\n\t\toptions.Width(0),\n\t\toptions.Align(options.NoneAlign),\n\t\toptions.Prefix(\"\"),\n\t\toptions.Suffix(\"\"),\n\t\toptions.Color(options.NoneColor),\n\t))\n}\n\ntype CommonPlaceholder struct {\n\tname string\n\topts options.Options\n}\n\n// NewCommonPlaceholder creates a new Common placeholder.\nfunc NewCommonPlaceholder(name string, opts ...options.Option) *CommonPlaceholder {\n\treturn &CommonPlaceholder{\n\t\tname: name,\n\t\topts: opts,\n\t}\n}\n\n// Name implements `Placeholder` interface.\nfunc (common *CommonPlaceholder) Name() string {\n\treturn common.name\n}\n\n// Options implements `Placeholder` interface.\nfunc (common *CommonPlaceholder) Options() options.Options {\n\treturn common.opts\n}\n\n// Format implements `Placeholder` interface.\nfunc (common *CommonPlaceholder) Format(data *options.Data) (string, error) {\n\treturn common.opts.Format(data, \"\")\n}\n"
  },
  {
    "path": "pkg/log/format/placeholders/errors.go",
    "content": "package placeholders\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\n// InvalidPlaceholderNameError is an invalid `placeholder` name error.\ntype InvalidPlaceholderNameError struct {\n\tstr  string\n\topts Placeholders\n}\n\n// NewInvalidPlaceholderNameError returns a new `InvalidPlaceholderNameError` instance.\nfunc NewInvalidPlaceholderNameError(str string, opts Placeholders) *InvalidPlaceholderNameError {\n\treturn &InvalidPlaceholderNameError{\n\t\tstr:  str,\n\t\topts: opts,\n\t}\n}\n\nfunc (err InvalidPlaceholderNameError) Error() string {\n\tvar name string\n\n\tfor index := range len(err.str) {\n\t\tif !isPlaceholderNameCharacter(err.str[index]) {\n\t\t\tbreak\n\t\t}\n\n\t\tname = err.str[:index+1]\n\t}\n\n\treturn fmt.Sprintf(\"invalid placeholder name %q, available names: %s\", name, strings.Join(err.opts.Names(), \",\"))\n}\n\n// InvalidPlaceholderOptionError is an invalid `placeholder` option error.\ntype InvalidPlaceholderOptionError struct {\n\tph  Placeholder\n\terr error\n}\n\n// NewInvalidPlaceholderOptionError returns a new `InvalidPlaceholderOptionError` instance.\nfunc NewInvalidPlaceholderOptionError(ph Placeholder, err error) *InvalidPlaceholderOptionError {\n\treturn &InvalidPlaceholderOptionError{\n\t\tph:  ph,\n\t\terr: err,\n\t}\n}\n\nfunc (err InvalidPlaceholderOptionError) Error() string {\n\treturn fmt.Sprintf(\"placeholder %q, %v\", err.ph.Name(), err.err)\n}\n\nfunc (err InvalidPlaceholderOptionError) Unwrap() error {\n\treturn err.err\n}\n"
  },
  {
    "path": "pkg/log/format/placeholders/field.go",
    "content": "package placeholders\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/options\"\n)\n\nconst (\n\tWorkDirKeyName   = \"prefix\"\n\tTFPathKeyName    = \"tf-path\"\n\tTFCmdArgsKeyName = \"tf-command-args\"\n\tTFCmdKeyName     = \"tf-command\"\n\n\t// Terragrunt Provider Cache Server fields.\n\tCacheServerURLKeyName    = \"url\"\n\tCacheServerStatusKeyName = \"status\"\n)\n\ntype fieldPlaceholder struct {\n\t*CommonPlaceholder\n}\n\n// Format implements `Placeholder` interface.\nfunc (field *fieldPlaceholder) Format(data *options.Data) (string, error) {\n\tif val, ok := data.Fields[field.Name()]; ok {\n\t\treturn field.opts.Format(data, val)\n\t}\n\n\treturn \"\", nil\n}\n\n// Field creates a placeholder that displays log field value.\nfunc Field(fieldName string, opts ...options.Option) Placeholder {\n\topts = WithCommonOptions(\n\t\toptions.PathFormat(options.NonePath),\n\t).Merge(opts...)\n\n\treturn &fieldPlaceholder{\n\t\tCommonPlaceholder: NewCommonPlaceholder(fieldName, opts...),\n\t}\n}\n"
  },
  {
    "path": "pkg/log/format/placeholders/interval.go",
    "content": "package placeholders\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/options\"\n)\n\n// IntervalPlaceholderName is the placeholder name.\nconst IntervalPlaceholderName = \"interval\"\n\ntype intervalPlaceholder struct {\n\tbaseTime time.Time\n\t*CommonPlaceholder\n}\n\n// Format implements `Placeholder` interface.\nfunc (t *intervalPlaceholder) Format(data *options.Data) (string, error) {\n\treturn t.opts.Format(data, fmt.Sprintf(\"%04d\", time.Since(t.baseTime)/time.Second))\n}\n\n// Interval creates a placeholder that displays seconds that have passed since app started.\nfunc Interval(opts ...options.Option) Placeholder {\n\topts = WithCommonOptions().Merge(opts...)\n\n\treturn &intervalPlaceholder{\n\t\tbaseTime:          time.Now(),\n\t\tCommonPlaceholder: NewCommonPlaceholder(IntervalPlaceholderName, opts...),\n\t}\n}\n"
  },
  {
    "path": "pkg/log/format/placeholders/level.go",
    "content": "package placeholders\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/options\"\n)\n\n// LevelPlaceholderName is the placeholder name.\nconst LevelPlaceholderName = \"level\"\n\nvar levlAutoColorFunc = func(level log.Level) options.ColorValue {\n\tswitch level {\n\tcase log.TraceLevel:\n\t\treturn options.WhiteColor\n\tcase log.DebugLevel:\n\t\treturn options.LightBlueColor\n\tcase log.InfoLevel:\n\t\treturn options.GreenColor\n\tcase log.WarnLevel:\n\t\treturn options.YellowColor\n\tcase log.ErrorLevel:\n\t\treturn options.RedColor\n\tcase log.StdoutLevel:\n\t\treturn options.WhiteColor\n\tcase log.StderrLevel:\n\t\treturn options.RedColor\n\tdefault:\n\t\treturn options.NoneColor\n\t}\n}\n\ntype level struct {\n\t*CommonPlaceholder\n}\n\n// Format implements `Placeholder` interface.\nfunc (level *level) Format(data *options.Data) (string, error) {\n\tnewData := *data\n\tnewData.PresetColorFn = func() options.ColorValue {\n\t\treturn levlAutoColorFunc(data.Level)\n\t}\n\n\treturn level.opts.Format(&newData, data.Level.String())\n}\n\n// Level creates a placeholder that displays log level name.\nfunc Level(opts ...options.Option) Placeholder {\n\topts = WithCommonOptions(\n\t\toptions.LevelFormat(options.LevelFormatFull),\n\t).Merge(opts...)\n\n\treturn &level{\n\t\tCommonPlaceholder: NewCommonPlaceholder(LevelPlaceholderName, opts...),\n\t}\n}\n"
  },
  {
    "path": "pkg/log/format/placeholders/message.go",
    "content": "package placeholders\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/options\"\n)\n\n// MessagePlaceholderName is the placeholder name.\nconst MessagePlaceholderName = \"msg\"\n\ntype message struct {\n\t*CommonPlaceholder\n}\n\n// Format implements `Placeholder` interface.\nfunc (msg *message) Format(data *options.Data) (string, error) {\n\treturn msg.opts.Format(data, data.Message)\n}\n\n// Message creates a placeholder that displays log message.\nfunc Message(opts ...options.Option) Placeholder {\n\topts = WithCommonOptions(\n\t\toptions.PathFormat(options.NonePath, options.RelativePath),\n\t).Merge(opts...)\n\n\treturn &message{\n\t\tCommonPlaceholder: NewCommonPlaceholder(MessagePlaceholderName, opts...),\n\t}\n}\n"
  },
  {
    "path": "pkg/log/format/placeholders/placeholder.go",
    "content": "// Package placeholders represents a set of placeholders for formatting various log values.\npackage placeholders\n\nimport (\n\t\"strings\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/options\"\n)\n\nconst (\n\tplaceholderSign             = \"%\"\n\tsplitIntoTextAndPlaceholder = 2\n)\n\n// Placeholder is part of the log message, used to format different log values.\ntype Placeholder interface {\n\t// Name returns a placeholder name.\n\tName() string\n\t// Options returns the placeholder options.\n\tOptions() options.Options\n\t// Format returns the formatted value.\n\tFormat(data *options.Data) (string, error)\n}\n\n// Placeholders are a set of Placeholders.\ntype Placeholders []Placeholder\n\n// NewPlaceholderRegister returns a new `Placeholder` collection instance available for use in a custom format string.\nfunc NewPlaceholderRegister() Placeholders {\n\treturn Placeholders{\n\t\tInterval(),\n\t\tTime(),\n\t\tLevel(),\n\t\tMessage(),\n\t\tField(WorkDirKeyName, options.PathFormat(options.NonePath, options.RelativePath, options.ShortRelativePath, options.ShortPath)),\n\t\tField(TFPathKeyName, options.PathFormat(options.NonePath, options.FilenamePath, options.DirectoryPath)),\n\t\tField(TFCmdArgsKeyName),\n\t\tField(TFCmdKeyName),\n\t}\n}\n\n// Get returns the placeholder by its name.\nfunc (phs Placeholders) Get(name string) Placeholder {\n\tfor _, ph := range phs {\n\t\tif ph.Name() == name {\n\t\t\treturn ph\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Names returns the names of the placeholders.\nfunc (phs Placeholders) Names() []string {\n\tvar names = make([]string, len(phs))\n\n\tfor i, ph := range phs {\n\t\tnames[i] = ph.Name()\n\t}\n\n\treturn names\n}\n\n// Format returns a formatted string that is the concatenation of the formatted placeholder values.\nfunc (phs Placeholders) Format(data *options.Data) (string, error) {\n\tvar str string\n\n\tvar strSb69 strings.Builder\n\n\tfor _, ph := range phs {\n\t\ts, err := ph.Format(data)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tstrSb69.WriteString(s)\n\t}\n\n\tstr += strSb69.String()\n\n\treturn str, nil\n}\n\n// findPlaceholder parses the given `str` to find a placeholder name present in the `phs` collection,\n// returns that placeholder, and the rest of the given `str`.\n//\n// e.g. \"level(color=green, case=upper) some-text\" returns the instance of the `level` placeholder\n// and \"(color=green, case=upper) some-text\" string.\nfunc (phs Placeholders) findPlaceholder(str string) (Placeholder, string) { //nolint:ireturn\n\tvar (\n\t\tplaceholder Placeholder\n\t\toptIndex    int\n\t)\n\n\t// We don't stop at the first one we find, we look for the longest name.\n\t// Of these two `%tf-command` `%tf-command-args` we need to find the second one.\n\tfor index := range len(str) {\n\t\tif !isPlaceholderNameCharacter(str[index]) {\n\t\t\tbreak\n\t\t}\n\n\t\tname := str[:index+1]\n\n\t\tif pl := phs.Get(name); pl != nil {\n\t\t\tplaceholder = pl\n\t\t\toptIndex = index + 1\n\t\t}\n\t}\n\n\tif placeholder != nil {\n\t\treturn placeholder, str[optIndex:]\n\t}\n\n\treturn findPlaintextPlaceholder(str)\n}\n\n// Parse parses the given `str` and returns a set of placeholders that are then used to format log data.\nfunc Parse(str string) (Placeholders, error) {\n\tvar (\n\t\tplaceholders Placeholders\n\t\tplaceholder  Placeholder\n\t\terr          error\n\t)\n\n\tfor {\n\t\t// We need to create a new placeholders collection to avoid overriding options\n\t\t// if the custom format string contains two or more same placeholders.\n\t\t// e.g. \"%level(format=full) some-text %level(format=tiny)\"\n\t\tplaceholderRegister := NewPlaceholderRegister()\n\n\t\tparts := strings.SplitN(str, placeholderSign, splitIntoTextAndPlaceholder)\n\n\t\tif plaintext := parts[0]; plaintext != \"\" {\n\t\t\tplaceholders = append(placeholders, PlainText(plaintext))\n\t\t}\n\n\t\tif len(parts) == 1 {\n\t\t\treturn placeholders, nil\n\t\t}\n\n\t\tstr = parts[1]\n\n\t\tplaceholder, str = placeholderRegister.findPlaceholder(str)\n\t\tif placeholder == nil {\n\t\t\treturn nil, errors.New(NewInvalidPlaceholderNameError(str, placeholderRegister))\n\t\t}\n\n\t\tstr, err = placeholder.Options().Configure(str)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(NewInvalidPlaceholderOptionError(placeholder, err))\n\t\t}\n\n\t\tplaceholders = append(placeholders, placeholder)\n\t}\n}\n\nfunc findPlaintextPlaceholder(str string) (Placeholder, string) { //nolint:ireturn\n\tif len(str) == 0 {\n\t\treturn nil, str\n\t}\n\n\tswitch str[0:1] {\n\tcase options.OptStartSign:\n\t\t// Unnamed placeholder, format `%(content='...')`.\n\t\treturn PlainText(\"\"), str\n\tcase \" \":\n\t\t// Single `%` character, format `% `.\n\t\treturn PlainText(placeholderSign), str\n\tcase placeholderSign:\n\t\t// Escaped `%`, format `%%`.\n\t\treturn PlainText(placeholderSign), str[1:]\n\tcase \"t\":\n\t\t// Indent, format `%t`.\n\t\treturn PlainText(\"\\t\"), str[1:]\n\tcase \"n\":\n\t\t// Newline, format `%n`.\n\t\treturn PlainText(\"\\n\"), str[1:]\n\t}\n\n\treturn nil, str\n}\n\n// isPlaceholderNameCharacter returns true if the given character `c` does not contain any restricted characters for placeholder names.\n//\n// e.g. \"time\" return `true`.\n// e.g. \"time \" return `false`.\n// e.g. \"time(\" return `false`.\nfunc isPlaceholderNameCharacter(c byte) bool {\n\t// Check if the byte value falls within the range of alphanumeric characters\n\treturn c == '-' || c == '_' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')\n}\n"
  },
  {
    "path": "pkg/log/format/placeholders/placeholder_test.go",
    "content": "package placeholders_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/options\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParse(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname         string\n\t\tinput        string\n\t\texpectOutput string // if non-empty, format and check output contains this\n\t\texpectCount  int    // expected number of placeholders, -1 to skip\n\t\texpectErr    bool\n\t}{\n\t\t{\n\t\t\tname:        \"single_level\",\n\t\t\tinput:       \"%level\",\n\t\t\texpectCount: 1,\n\t\t},\n\t\t{\n\t\t\tname:        \"single_msg\",\n\t\t\tinput:       \"%msg\",\n\t\t\texpectCount: 1,\n\t\t},\n\t\t{\n\t\t\tname:        \"single_time\",\n\t\t\tinput:       \"%time\",\n\t\t\texpectCount: 1,\n\t\t},\n\t\t{\n\t\t\tname:        \"single_interval\",\n\t\t\tinput:       \"%interval\",\n\t\t\texpectCount: 1,\n\t\t},\n\t\t{\n\t\t\tname:         \"plaintext_only\",\n\t\t\tinput:        \"just plain text\",\n\t\t\texpectCount:  1,\n\t\t\texpectOutput: \"just plain text\",\n\t\t},\n\t\t{\n\t\t\tname:        \"mixed_text_and_placeholder\",\n\t\t\tinput:       \"level=%level msg=%msg\",\n\t\t\texpectCount: 4, // \"level=\" + level + \" msg=\" + msg\n\t\t},\n\t\t{\n\t\t\tname:        \"with_options\",\n\t\t\tinput:       \"%level(format=short)\",\n\t\t\texpectCount: 1,\n\t\t},\n\t\t{\n\t\t\tname:         \"escaped_percent\",\n\t\t\tinput:        \"100%%\",\n\t\t\texpectCount:  2, // \"100\" + \"%\" (from %%)\n\t\t\texpectOutput: \"100%\",\n\t\t},\n\t\t{\n\t\t\tname:         \"tab\",\n\t\t\tinput:        \"%t\",\n\t\t\texpectCount:  1,\n\t\t\texpectOutput: \"\\t\",\n\t\t},\n\t\t{\n\t\t\tname:         \"newline\",\n\t\t\tinput:        \"%n\",\n\t\t\texpectCount:  1,\n\t\t\texpectOutput: \"\\n\",\n\t\t},\n\t\t{\n\t\t\tname:        \"field_prefix\",\n\t\t\tinput:       \"%prefix\",\n\t\t\texpectCount: 1,\n\t\t},\n\t\t{\n\t\t\tname:        \"field_tf_path\",\n\t\t\tinput:       \"%tf-path\",\n\t\t\texpectCount: 1,\n\t\t},\n\t\t{\n\t\t\tname:        \"field_tf_command_args\",\n\t\t\tinput:       \"%tf-command-args\",\n\t\t\texpectCount: 1,\n\t\t},\n\t\t{\n\t\t\tname:      \"invalid_name_banana\",\n\t\t\tinput:     \"%banana\",\n\t\t\texpectErr: true,\n\t\t},\n\t\t{\n\t\t\tname:        \"complex_multi_placeholder\",\n\t\t\tinput:       \"%level %msg [%interval]\",\n\t\t\texpectCount: 6, // level + \" \" + msg + \" [\" + interval + \"]\"\n\t\t},\n\t\t{\n\t\t\tname:         \"empty_string\",\n\t\t\tinput:        \"\",\n\t\t\texpectCount:  0,\n\t\t\texpectOutput: \"\",\n\t\t},\n\t\t{\n\t\t\tname:        \"unnamed_placeholder\",\n\t\t\tinput:       \"%(content='hello')\",\n\t\t\texpectCount: 1,\n\t\t},\n\t\t{\n\t\t\tname:        \"duplicate_placeholders_different_options\",\n\t\t\tinput:       \"%level(format=full) %level(format=short)\",\n\t\t\texpectCount: 3, // level(full) + \" \" + level(short)\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tphs, err := placeholders.Parse(tc.input)\n\t\t\tif tc.expectErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif tc.expectCount >= 0 {\n\t\t\t\tassert.Len(t, phs, tc.expectCount)\n\t\t\t}\n\n\t\t\tif tc.expectOutput != \"\" {\n\t\t\t\tdata := newMinimalData(\"test\", log.InfoLevel)\n\t\t\t\toutput, err := phs.Format(data)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Contains(t, output, tc.expectOutput)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPlaceholderRegisterNames(t *testing.T) {\n\tt.Parallel()\n\n\tphs := placeholders.NewPlaceholderRegister()\n\tnames := phs.Names()\n\tassert.NotEmpty(t, names)\n\n\texpectedNames := []string{\"interval\", \"time\", \"level\", \"msg\"}\n\tfor _, name := range expectedNames {\n\t\tassert.Contains(t, names, name)\n\t}\n}\n\nfunc TestPlaceholdersGet(t *testing.T) {\n\tt.Parallel()\n\n\tphs := placeholders.NewPlaceholderRegister()\n\n\tt.Run(\"existing_level\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tph := phs.Get(\"level\")\n\t\tassert.NotNil(t, ph)\n\t\tassert.Equal(t, \"level\", ph.Name())\n\t})\n\n\tt.Run(\"existing_msg\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tph := phs.Get(\"msg\")\n\t\tassert.NotNil(t, ph)\n\t})\n\n\tt.Run(\"nonexistent\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tph := phs.Get(\"nonexistent\")\n\t\tassert.Nil(t, ph)\n\t})\n}\n\nfunc TestPlaceholdersFormat(t *testing.T) {\n\tt.Parallel()\n\n\tphs := placeholders.Placeholders{\n\t\tplaceholders.PlainText(\"hello\"),\n\t\tplaceholders.PlainText(\" world\"),\n\t}\n\n\tdata := newMinimalData(\"\", log.InfoLevel)\n\toutput, err := phs.Format(data)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"hello world\", output)\n}\n\nfunc TestLevelPlaceholderFormats(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tformat   string\n\t\tcontains string\n\t\tlevel    log.Level\n\t}{\n\t\t{name: \"full_info\", format: \"%level\", contains: \"info\", level: log.InfoLevel},\n\t\t{name: \"full_error\", format: \"%level\", contains: \"error\", level: log.ErrorLevel},\n\t\t{name: \"short_info\", format: \"%level(format=short)\", contains: \"inf\", level: log.InfoLevel},\n\t\t{name: \"tiny_info\", format: \"%level(format=tiny)\", contains: \"i\", level: log.InfoLevel},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tphs, err := placeholders.Parse(tc.format)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tdata := newMinimalData(\"msg\", tc.level)\n\t\t\toutput, err := phs.Format(data)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Contains(t, output, tc.contains)\n\t\t})\n\t}\n}\n\nfunc TestMessagePlaceholder(t *testing.T) {\n\tt.Parallel()\n\n\tphs, err := placeholders.Parse(\"%msg\")\n\trequire.NoError(t, err)\n\n\tdata := newMinimalData(\"hello world\", log.InfoLevel)\n\toutput, err := phs.Format(data)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"hello world\", output)\n}\n\nfunc FuzzParse(f *testing.F) {\n\tseeds := []string{\n\t\t\"%level\", \"%msg\", \"%time\", \"%interval\",\n\t\t\"%level(format=short)\", \"%level(format=tiny)\",\n\t\t\"plain text\", \"%level %msg\",\n\t\t\"%%\", \"%t\", \"%n\",\n\t\t\"%(content='hello')\",\n\t\t\"%prefix\", \"%tf-path\", \"%tf-command-args\",\n\t\t\"\", \"%\", \"%()\", \"%(content='unclosed\",\n\t\t\"%banana\",\n\t\t\"%level(format=full) some-text %level(format=tiny)\",\n\t}\n\n\tfor _, s := range seeds {\n\t\tf.Add(s)\n\t}\n\n\tf.Fuzz(func(t *testing.T, input string) {\n\t\tphs, err := placeholders.Parse(input)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\t// If parsing succeeded, formatting should not panic\n\t\tdata := &options.Data{\n\t\t\tEntry: &log.Entry{\n\t\t\t\tEntry: logrus.NewEntry(logrus.New()),\n\t\t\t\tLevel: log.InfoLevel,\n\t\t\t},\n\t\t\tDisabledColors: true,\n\t\t}\n\n\t\t_, _ = phs.Format(data)\n\t})\n}\n\nfunc newMinimalData(msg string, level log.Level) *options.Data {\n\tlogrusLogger := logrus.New()\n\tlogrusEntry := logrus.NewEntry(logrusLogger)\n\tlogrusEntry.Level = level.ToLogrusLevel()\n\tlogrusEntry.Message = msg\n\n\treturn &options.Data{\n\t\tEntry: &log.Entry{\n\t\t\tEntry:  logrusEntry,\n\t\t\tLevel:  level,\n\t\t\tFields: log.Fields{},\n\t\t},\n\t\tDisabledColors: true,\n\t}\n}\n"
  },
  {
    "path": "pkg/log/format/placeholders/plaintext.go",
    "content": "package placeholders\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/options\"\n)\n\n// PlainTextPlaceholderName is the placeholder name.\nconst PlainTextPlaceholderName = \"\"\n\ntype plainText struct {\n\t*CommonPlaceholder\n}\n\n// PlainText creates a placeholder that displays plaintext.\n// Although plaintext can be used as is without placeholder, this allows you to format the content,\n// for example set a color: `%(content='just text',color=green)`.\nfunc PlainText(value string, opts ...options.Option) Placeholder {\n\topts = WithCommonOptions(\n\t\toptions.Content(value),\n\t).Merge(opts...)\n\n\treturn &plainText{\n\t\tCommonPlaceholder: NewCommonPlaceholder(PlainTextPlaceholderName, opts...),\n\t}\n}\n"
  },
  {
    "path": "pkg/log/format/placeholders/time.go",
    "content": "package placeholders\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/options\"\n)\n\n// TimePlaceholderName is the placeholder name. Example `%time()`.\nconst TimePlaceholderName = \"time\"\n\ntype timePlaceholder struct {\n\t*CommonPlaceholder\n}\n\n// Format implements `Placeholder` interface.\nfunc (t *timePlaceholder) Format(data *options.Data) (string, error) {\n\treturn t.opts.Format(data, data.Time.String())\n}\n\n// Time creates a placeholder that displays log time.\nfunc Time(opts ...options.Option) Placeholder {\n\topts = WithCommonOptions(\n\t\toptions.TimeFormat(fmt.Sprintf(\"%s:%s:%s%s\", options.Hour24Zero, options.MinZero, options.SecZero, options.MilliSec)),\n\t).Merge(opts...)\n\n\treturn &timePlaceholder{\n\t\tCommonPlaceholder: NewCommonPlaceholder(TimePlaceholderName, opts...),\n\t}\n}\n"
  },
  {
    "path": "pkg/log/formatter.go",
    "content": "package log\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/sirupsen/logrus\"\n)\n\n// Formatter is used to implement a custom Formatter.\ntype Formatter interface {\n\t// SetDisabledColors enables/disables log colors.\n\tSetDisabledColors(val bool)\n\t// DisabledColors returns true if log colors are disabled.\n\tDisabledColors() bool\n\t// SetDisabledOutput  enables/disables log output.\n\tSetDisabledOutput(val bool)\n\t// DisabledOutput returns true if log output is disabled.\n\tDisabledOutput() bool\n\t// SetBaseDir creates a set of relative paths that are used to convert full paths to relative ones.\n\tSetBaseDir(baseDir string) error\n\t// DisableRelativePaths disables the conversion of absolute paths to relative ones.\n\tDisableRelativePaths()\n\t// SetFormat parses and sets log format.\n\tSetFormat(str string) error\n\t// SetCustomFormat parses and sets custom log format.\n\tSetCustomFormat(str string) error\n\n\t// Format takes an `Entry`. It exposes all the fields, including the default ones:\n\t//\n\t// * `entry.Data[\"msg\"]`. The message passed from Info, Warn, Error ..\n\t// * `entry.Data[\"time\"]`. The timestamp.\n\t// * `entry.Data[\"level\"]. The level the entry was logged at.\n\t//\n\t// Any additional fields added with `WithField` or `WithFields` are also in\n\t// `entry.Data`. Format is expected to return an array of bytes which are then\n\t// logged to `logger.Out`.\n\tFormat(entry *Entry) ([]byte, error)\n}\n\n// Entry is the final logging entry.\ntype Entry struct {\n\t*logrus.Entry\n\tFields Fields\n\tLevel  Level\n}\n\n// fromLogrusFormatter converts call from logrus.Formatter interface to our long.Formatter interface.\ntype fromLogrusFormatter struct {\n\tFormatter\n}\n\nfunc (f *fromLogrusFormatter) Format(parent *logrus.Entry) ([]byte, error) {\n\tif parent == nil {\n\t\treturn nil, errors.Errorf(\"nil entry provided\")\n\t}\n\n\tentry := &Entry{\n\t\tEntry:  parent,\n\t\tLevel:  FromLogrusLevel(parent.Level),\n\t\tFields: Fields(parent.Data),\n\t}\n\n\treturn f.Formatter.Format(entry)\n}\n"
  },
  {
    "path": "pkg/log/level.go",
    "content": "package log\n\nimport (\n\t\"strings\"\n\n\t\"slices\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/sirupsen/logrus\"\n)\n\n// These are the different logging levels.\nconst (\n\t// StderrLevel level. Used to log error messages that we get from OpenTofu/Terraform stderr.\n\tStderrLevel Level = iota\n\t// StdoutLevel level. Used to log messages that we get from OpenTofu/Terraform stdout.\n\tStdoutLevel\n\t// ErrorLevel level. Logs. Used for errors that should definitely be noted.\n\tErrorLevel\n\t// WarnLevel level. Non-critical entries that deserve eyes.\n\tWarnLevel\n\t// InfoLevel level. General operational entries about what's going on inside the application.\n\tInfoLevel\n\t// DebugLevel level. Usually only enabled when debugging. Very verbose logging.\n\tDebugLevel\n\t// TraceLevel level. Designates finer-grained informational events than the Debug.\n\tTraceLevel\n)\n\n// Since the first two logrus levels are Panic and Fatal, which cause an exit or panic when called, we need to shift all of our levels by two bytes.\nconst shiftLogrusLevel = 2\n\nvar logrusLevels = map[Level]logrus.Level{\n\tStderrLevel: logrus.Level(StderrLevel + shiftLogrusLevel),\n\tStdoutLevel: logrus.Level(StdoutLevel + shiftLogrusLevel),\n\tErrorLevel:  logrus.Level(ErrorLevel + shiftLogrusLevel),\n\tWarnLevel:   logrus.Level(WarnLevel + shiftLogrusLevel),\n\tInfoLevel:   logrus.Level(InfoLevel + shiftLogrusLevel),\n\tDebugLevel:  logrus.Level(DebugLevel + shiftLogrusLevel),\n\tTraceLevel:  logrus.Level(TraceLevel + shiftLogrusLevel),\n}\n\n// AllLevels exposes all logging levels\nvar AllLevels = Levels{\n\tStderrLevel,\n\tStdoutLevel,\n\tErrorLevel,\n\tWarnLevel,\n\tInfoLevel,\n\tDebugLevel,\n\tTraceLevel,\n}\n\nvar levelNames = map[Level]string{\n\tStderrLevel: \"stderr\",\n\tStdoutLevel: \"stdout\",\n\tErrorLevel:  \"error\",\n\tWarnLevel:   \"warn\",\n\tInfoLevel:   \"info\",\n\tDebugLevel:  \"debug\",\n\tTraceLevel:  \"trace\",\n}\n\nvar levelShortNames = map[Level]string{\n\tStderrLevel: \"std\",\n\tStdoutLevel: \"std\",\n\tErrorLevel:  \"err\",\n\tWarnLevel:   \"wrn\",\n\tInfoLevel:   \"inf\",\n\tDebugLevel:  \"deb\",\n\tTraceLevel:  \"trc\",\n}\n\nvar levelTinyNames = map[Level]string{\n\tStderrLevel: \"s\",\n\tStdoutLevel: \"s\",\n\tErrorLevel:  \"e\",\n\tWarnLevel:   \"w\",\n\tInfoLevel:   \"i\",\n\tDebugLevel:  \"d\",\n\tTraceLevel:  \"t\",\n}\n\n// Level type\ntype Level uint32\n\n// ParseLevel takes a string and returns the Level constant.\nfunc ParseLevel(str string) (Level, error) {\n\tfor level, name := range levelNames {\n\t\tif strings.EqualFold(name, str) {\n\t\t\treturn level, nil\n\t\t}\n\t}\n\n\treturn Level(0), errors.Errorf(\"invalid level %q, supported levels: %s\", str, AllLevels)\n}\n\n// String implements fmt.Stringer.\nfunc (level Level) String() string {\n\treturn level.FullName()\n}\n\n// FullName returns the full level name.\nfunc (level Level) FullName() string {\n\tif name, ok := levelNames[level]; ok {\n\t\treturn name\n\t}\n\n\treturn \"\"\n}\n\n// TinyName returns the level name in one character.\nfunc (level Level) TinyName() string {\n\tif name, ok := levelTinyNames[level]; ok {\n\t\treturn name\n\t}\n\n\treturn \"\"\n}\n\n// ShortName returns the level name in third characters.\nfunc (level Level) ShortName() string {\n\tif name, ok := levelShortNames[level]; ok {\n\t\treturn name\n\t}\n\n\treturn \"\"\n}\n\n// UnmarshalText implements encoding.TextUnmarshaler.\nfunc (level *Level) UnmarshalText(text []byte) error {\n\tlvl, err := ParseLevel(string(text))\n\tif err != nil {\n\t\treturn errors.Errorf(\"invalid: %q\", string(text))\n\t}\n\n\t*level = lvl\n\n\treturn nil\n}\n\n// MarshalText implements encoding.MarshalText.\nfunc (level Level) MarshalText() ([]byte, error) {\n\tif name := level.String(); name != \"\" {\n\t\treturn []byte(name), nil\n\t}\n\n\treturn nil, errors.Errorf(\"invalid: %q\", level)\n}\n\n// ToLogrusLevel converts our `Level` to `logrus.Level`.\nfunc (level Level) ToLogrusLevel() logrus.Level {\n\tif logrusLevel, ok := logrusLevels[level]; ok {\n\t\treturn logrusLevel\n\t}\n\n\treturn logrus.Level(0)\n}\n\n// Levels is a slice of `Level` type.\ntype Levels []Level\n\n// Contains returns true if the `Levels` list contains the given search `Level`.\nfunc (levels Levels) Contains(search Level) bool {\n\treturn slices.Contains(levels, search)\n}\n\n// ToLogrusLevels converts our `Levels` to `logrus.Levels`.\nfunc (levels Levels) ToLogrusLevels() []logrus.Level {\n\tlogrusLevels := make([]logrus.Level, len(levels))\n\n\tfor i, level := range levels {\n\t\tlogrusLevels[i] = level.ToLogrusLevel()\n\t}\n\n\treturn logrusLevels\n}\n\n// Names returns a list of full level names.\nfunc (levels Levels) Names() []string {\n\tstrs := make([]string, len(levels))\n\n\tfor i, level := range levels {\n\t\tstrs[i] = level.String()\n\t}\n\n\treturn strs\n}\n\n// String implements the `fmt.Stringer` interface.\nfunc (levels Levels) String() string {\n\treturn strings.Join(levels.Names(), \", \")\n}\n\n// FromLogrusLevel converts `logrus.Level` to our `Level`.\nfunc FromLogrusLevel(lvl logrus.Level) Level {\n\tfor level, logrusLevel := range logrusLevels {\n\t\tif logrusLevel == lvl {\n\t\t\treturn level\n\t\t}\n\t}\n\n\treturn Level(0)\n}\n"
  },
  {
    "path": "pkg/log/level_test.go",
    "content": "package log_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParseLevel(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname      string\n\t\tinput     string\n\t\texpected  log.Level\n\t\texpectErr bool\n\t}{\n\t\t{name: \"stderr\", input: \"stderr\", expected: log.StderrLevel},\n\t\t{name: \"stdout\", input: \"stdout\", expected: log.StdoutLevel},\n\t\t{name: \"error\", input: \"error\", expected: log.ErrorLevel},\n\t\t{name: \"warn\", input: \"warn\", expected: log.WarnLevel},\n\t\t{name: \"info\", input: \"info\", expected: log.InfoLevel},\n\t\t{name: \"debug\", input: \"debug\", expected: log.DebugLevel},\n\t\t{name: \"trace\", input: \"trace\", expected: log.TraceLevel},\n\t\t{name: \"upper_INFO\", input: \"INFO\", expected: log.InfoLevel},\n\t\t{name: \"mixed_Debug\", input: \"Debug\", expected: log.DebugLevel},\n\t\t{name: \"upper_WARN\", input: \"WARN\", expected: log.WarnLevel},\n\t\t{name: \"upper_ERROR\", input: \"ERROR\", expected: log.ErrorLevel},\n\t\t{name: \"upper_TRACE\", input: \"TRACE\", expected: log.TraceLevel},\n\t\t{name: \"empty\", input: \"\", expectErr: true},\n\t\t{name: \"invalid_banana\", input: \"banana\", expectErr: true},\n\t\t{name: \"invalid_inf\", input: \"inf\", expectErr: true},\n\t\t{name: \"padded_info\", input: \" info \", expectErr: true},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tlevel, err := log.ParseLevel(tc.input)\n\t\t\tif tc.expectErr {\n\t\t\t\tassert.Error(t, err)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, tc.expected, level)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLevelString(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected string\n\t\tlevel    log.Level\n\t}{\n\t\t{expected: \"stderr\", level: log.StderrLevel},\n\t\t{expected: \"stdout\", level: log.StdoutLevel},\n\t\t{expected: \"error\", level: log.ErrorLevel},\n\t\t{expected: \"warn\", level: log.WarnLevel},\n\t\t{expected: \"info\", level: log.InfoLevel},\n\t\t{expected: \"debug\", level: log.DebugLevel},\n\t\t{expected: \"trace\", level: log.TraceLevel},\n\t\t{expected: \"\", level: log.Level(99)},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.expected, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, tc.expected, tc.level.String())\n\t\t})\n\t}\n}\n\nfunc TestLevelShortName(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected string\n\t\tlevel    log.Level\n\t}{\n\t\t{expected: \"std\", level: log.StderrLevel},\n\t\t{expected: \"std\", level: log.StdoutLevel},\n\t\t{expected: \"err\", level: log.ErrorLevel},\n\t\t{expected: \"wrn\", level: log.WarnLevel},\n\t\t{expected: \"inf\", level: log.InfoLevel},\n\t\t{expected: \"deb\", level: log.DebugLevel},\n\t\t{expected: \"trc\", level: log.TraceLevel},\n\t\t{expected: \"\", level: log.Level(99)},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.expected, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, tc.expected, tc.level.ShortName())\n\t\t})\n\t}\n}\n\nfunc TestLevelTinyName(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\texpected string\n\t\tlevel    log.Level\n\t}{\n\t\t{expected: \"s\", level: log.StderrLevel},\n\t\t{expected: \"s\", level: log.StdoutLevel},\n\t\t{expected: \"e\", level: log.ErrorLevel},\n\t\t{expected: \"w\", level: log.WarnLevel},\n\t\t{expected: \"i\", level: log.InfoLevel},\n\t\t{expected: \"d\", level: log.DebugLevel},\n\t\t{expected: \"t\", level: log.TraceLevel},\n\t\t{expected: \"\", level: log.Level(99)},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.expected, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, tc.expected, tc.level.TinyName())\n\t\t})\n\t}\n}\n\nfunc TestMarshalUnmarshalRoundTrip(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, level := range log.AllLevels {\n\t\tt.Run(level.String(), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tdata, err := level.MarshalText()\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.NotEmpty(t, data)\n\n\t\t\tvar unmarshaled log.Level\n\n\t\t\terr = unmarshaled.UnmarshalText(data)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, level, unmarshaled)\n\t\t})\n\t}\n\n\tt.Run(\"marshal_unknown_level\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tunknown := log.Level(99)\n\t\t_, err := unknown.MarshalText()\n\t\tassert.Error(t, err)\n\t})\n\n\tt.Run(\"unmarshal_invalid_text\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tvar level log.Level\n\n\t\terr := level.UnmarshalText([]byte(\"banana\"))\n\t\tassert.Error(t, err)\n\t})\n}\n\nfunc TestToLogrusLevel(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tlevel         log.Level\n\t\texpectedShift uint32\n\t}{\n\t\t{level: log.StderrLevel, expectedShift: 2},\n\t\t{level: log.StdoutLevel, expectedShift: 3},\n\t\t{level: log.ErrorLevel, expectedShift: 4},\n\t\t{level: log.WarnLevel, expectedShift: 5},\n\t\t{level: log.InfoLevel, expectedShift: 6},\n\t\t{level: log.DebugLevel, expectedShift: 7},\n\t\t{level: log.TraceLevel, expectedShift: 8},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.level.String(), func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, logrus.Level(tc.expectedShift), tc.level.ToLogrusLevel())\n\t\t})\n\t}\n\n\tt.Run(\"unknown_level\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tassert.Equal(t, logrus.Level(0), log.Level(99).ToLogrusLevel())\n\t})\n}\n\nfunc TestFromLogrusLevel(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, level := range log.AllLevels {\n\t\tt.Run(level.String(), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tlogrusLevel := level.ToLogrusLevel()\n\t\t\troundTripped := log.FromLogrusLevel(logrusLevel)\n\t\t\tassert.Equal(t, level, roundTripped)\n\t\t})\n\t}\n\n\tt.Run(\"unknown_logrus_level\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tassert.Equal(t, log.Level(0), log.FromLogrusLevel(logrus.Level(99)))\n\t})\n}\n\nfunc TestLevelsContains(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"contains_known_level\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tassert.True(t, log.AllLevels.Contains(log.InfoLevel))\n\t})\n\n\tt.Run(\"does_not_contain_unknown_level\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tassert.False(t, log.AllLevels.Contains(log.Level(99)))\n\t})\n}\n\nfunc TestLevelsNames(t *testing.T) {\n\tt.Parallel()\n\n\tnames := log.AllLevels.Names()\n\tassert.Len(t, names, 7)\n\n\tfor _, name := range names {\n\t\tassert.NotEmpty(t, name)\n\t}\n}\n\nfunc TestLevelsString(t *testing.T) {\n\tt.Parallel()\n\n\tstr := log.AllLevels.String()\n\tassert.Contains(t, str, \",\")\n\n\tfor _, level := range log.AllLevels {\n\t\tassert.Contains(t, str, level.String())\n\t}\n}\n\nfunc TestLevelsToLogrusLevels(t *testing.T) {\n\tt.Parallel()\n\n\tlogrusLevels := log.AllLevels.ToLogrusLevels()\n\tassert.Len(t, logrusLevels, 7)\n\n\tfor i, logrusLevel := range logrusLevels {\n\t\t// Each level should be shifted by 2 from its index\n\t\tassert.Equal(t, logrus.Level(uint32(i)+2), logrusLevel)\n\t}\n}\n\nfunc FuzzParseLevel(f *testing.F) {\n\t// Seed with valid names, mixed case, and garbage\n\tseeds := []string{\n\t\t\"stderr\", \"stdout\", \"error\", \"warn\", \"info\", \"debug\", \"trace\",\n\t\t\"INFO\", \"Debug\", \"WARN\", \"ERROR\", \"TRACE\",\n\t\t\"\", \"banana\", \"inf\", \" info \", \"12345\",\n\t}\n\n\tfor _, s := range seeds {\n\t\tf.Add(s)\n\t}\n\n\tf.Fuzz(func(t *testing.T, input string) {\n\t\tlevel, err := log.ParseLevel(input)\n\t\tif err == nil {\n\t\t\t// Valid level: round-trip must produce the same level\n\t\t\treparsed, err := log.ParseLevel(level.String())\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, level, reparsed)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "pkg/log/log.go",
    "content": "// Package log provides a leveled logger with structured logging support.\npackage log\n\nvar (\n\t// std is the name of the default logger.\n\tstd = New()\n)\n\n// Default returns the standard logger used by the package-level output functions.\n// Typically used as the default logger for various packages.\n// It is highly recommended not to use it to avoid conflicts in tests.\nfunc Default() Logger {\n\treturn std\n}\n\n// Debug logs a message at level Debug on the standard logger.\nfunc Debug(args ...any) {\n\tstd.Debug(args...)\n}\n\n// Trace logs a message at level Trace on the standard logger.\nfunc Trace(args ...any) {\n\tstd.Trace(args...)\n}\n\n// Info logs a message at level Info on the standard logger.\nfunc Info(args ...any) {\n\tstd.Info(args...)\n}\n\n// Print logs a message at level Info on the standard logger.\nfunc Print(args ...any) {\n\tstd.Print(args...)\n}\n\n// Warn logs a message at level Warn on the standard logger.\nfunc Warn(args ...any) {\n\tstd.Warn(args...)\n}\n\n// Error logs a message at level Error on the standard logger.\nfunc Error(args ...any) {\n\tstd.Error(args...)\n}\n\n// Debugln logs a message at level Debug on the standard logger.\nfunc Debugln(args ...any) {\n\tstd.Debugln(args...)\n}\n\n// Infoln logs a message at level Info on the standard logger.\nfunc Infoln(args ...any) {\n\tstd.Infoln(args...)\n}\n\n// Println logs a message at level Info on the standard logger.\nfunc Println(args ...any) {\n\tstd.Println(args...)\n}\n\n// Warnln logs a message at level Warn on the standard logger.\nfunc Warnln(args ...any) {\n\tstd.Warnln(args...)\n}\n\n// Errorln logs a message at level Error on the standard logger.\nfunc Errorln(args ...any) {\n\tstd.Errorln(args...)\n}\n\n// Debugf logs a message at level Debug on the standard logger.\nfunc Debugf(format string, args ...any) {\n\tstd.Debugf(format, args...)\n}\n\n// Tracef logs a message at level Trace on the standard logger.\nfunc Tracef(format string, args ...any) {\n\tstd.Tracef(format, args...)\n}\n\n// Infof logs a message at level Info on the standard logger.\nfunc Infof(format string, args ...any) {\n\tstd.Infof(format, args...)\n}\n\n// Printf logs a message at level Info on the standard logger.\nfunc Printf(args ...any) {\n\tstd.Print(args...)\n}\n\n// Warnf logs a message at level Warn on the standard logger.\nfunc Warnf(format string, args ...any) {\n\tstd.Warnf(format, args...)\n}\n\n// Errorf logs a message at level Error on the standard logger.\nfunc Errorf(format string, args ...any) {\n\tstd.Errorf(format, args...)\n}\n\n// WithField allocates a new entry and adds a field to it.\nfunc WithField(key string, value any) Logger {\n\treturn std.WithField(key, value)\n}\n\n// WithFields adds a struct of fields to the logger. All it does is call `WithField` for each `Field`.\nfunc WithFields(fields Fields) Logger {\n\treturn std.WithFields(fields)\n}\n\n// WithError adds an error to log entry, using the value defined in ErrorKey as key.\nfunc WithError(err error) Logger {\n\treturn std.WithError(err)\n}\n\n// WithOptions returns a new logger with the given options.\nfunc WithOptions(opts ...Option) Logger {\n\treturn std.WithOptions(opts...)\n}\n\n// SetOptions sets the options for the standard logger.\nfunc SetOptions(opts ...Option) {\n\tstd.SetOptions(opts...)\n}\n"
  },
  {
    "path": "pkg/log/logger.go",
    "content": "package log\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\n// Logger wraps the logrus package to have full control over implementing the required functionality,\n// such as adding or removing log levels etc. This also provides developers with an easier way to clone and set parameters.\ntype Logger interface {\n\t// Clone creates a new Logger instance with a copy of the fields from the current one.\n\tClone() Logger\n\n\t// SetOptions sets the given options to the instance.\n\tSetOptions(opts ...Option)\n\n\t// Level returns log level.\n\tLevel() Level\n\n\t// SetLevel parses and sets log level.\n\tSetLevel(str string) error\n\n\t// SetFormatter sets the logger formatter.\n\tSetFormatter(formatter Formatter)\n\n\t// Formatter returns the logger formatter.\n\tFormatter() Formatter\n\n\t// WithOptions clones and sets the given options for the new instance.\n\t// In other words, it is a combination of two methods, `log.Clone().SetOptions(...)`, but\n\t// unlike `SetOptions(...)`, it returns the instance, which is convenient for further actions.\n\tWithOptions(opts ...Option) Logger\n\n\t// WithField adds a single field to the Logger and returns partly cloning instance, the `Entry` structure.\n\t// This way the field is added to the returned instance only.\n\tWithField(key string, value any) Logger\n\n\t// WithFields adds a struct of fields to the Logger. All it does is call `WithField` for each `Field`.\n\tWithFields(fields Fields) Logger\n\n\t// WithError adds an error as single field to the Logger. The error is added to the returned instance only.\n\tWithError(err error) Logger\n\n\t// WithContext adds a context to the Logger. The context is added to the returned instance only.\n\tWithContext(ctx context.Context) Logger\n\n\t// WithTime overrides the time of the Logger. This only affects the returned instance.\n\tWithTime(t time.Time) Logger\n\n\t// Writer returns an io.Writer that writes to the Logger at the info log level.\n\tWriter() *io.PipeWriter\n\n\t// WriterLevel returns an io.Writer that writes to the Logger at the given log level.\n\tWriterLevel(level Level) *io.PipeWriter\n\n\t// Logf logs a message at the level given as parameter on the Logger.\n\tLogf(level Level, format string, args ...any)\n\n\t// Tracef logs a message at level Trace on the Logger.\n\tTracef(format string, args ...any)\n\n\t// Debugf logs a message at level Debug on the Logger.\n\tDebugf(format string, args ...any)\n\n\t// Infof logs a message at level Info on the Logger.\n\tInfof(format string, args ...any)\n\n\t// Printf logs a message at level Info on the Logger.\n\tPrintf(format string, args ...any)\n\n\t// Warnf logs a message at level Warn on the Logger.\n\tWarnf(format string, args ...any)\n\n\t// Errorf logs a message at level Error on the Logger.\n\tErrorf(format string, args ...any)\n\n\t// Log logs a message at the level given as parameter on the Logger.\n\tLog(level Level, args ...any)\n\n\t// Trace logs a message at level Trace on the Logger.\n\tTrace(args ...any)\n\n\t// Debug logs a message at level Debug on the Logger.\n\tDebug(args ...any)\n\n\t// Info logs a message at level Info on the Logger.\n\tInfo(args ...any)\n\n\t// Print logs a message at level Info on the Logger.\n\tPrint(args ...any)\n\n\t// Warn logs a message at level Warn on the Logger.\n\tWarn(args ...any)\n\n\t// Error logs a message at level Error on the Logger.\n\tError(args ...any)\n\n\t// Logln logs a message at the level given as parameter on the Logger.\n\tLogln(level Level, args ...any)\n\n\t// Traceln logs a message at level Trace on the Logger.\n\tTraceln(args ...any)\n\n\t// Debugln logs a message at level Debug on the Logger.\n\tDebugln(args ...any)\n\n\t// Infoln logs a message at level Info on the Logger.\n\tInfoln(args ...any)\n\n\t// Println logs a message at level Info on the Logger.\n\tPrintln(args ...any)\n\n\t// Warnln logs a message at level Warn on the Logger.\n\tWarnln(args ...any)\n\n\t// Errorln logs a message at level Error on the Logger.\n\tErrorln(args ...any)\n}\n\ntype logger struct {\n\t*logrus.Entry\n\tformatter Formatter\n}\n\n// New returns a new Logger instance.\nfunc New(opts ...Option) Logger {\n\tlogger := &logger{\n\t\tEntry: logrus.NewEntry(logrus.New()),\n\t}\n\tlogger.SetOptions(opts...)\n\n\treturn logger\n}\n\n// Clone implements the Logger interface method.\nfunc (logger *logger) Clone() Logger {\n\treturn logger.clone()\n}\n\n// SetOptions implements the Logger interface method.\nfunc (logger *logger) SetOptions(opts ...Option) {\n\tif len(opts) == 0 {\n\t\treturn\n\t}\n\n\tfor _, opt := range opts {\n\t\topt(logger)\n\t}\n}\n\n// SetFormatter sets the logger formatter.\nfunc (logger *logger) SetFormatter(formatter Formatter) {\n\tlogger.formatter = formatter\n\tlogger.Logger.SetFormatter(&fromLogrusFormatter{Formatter: formatter})\n}\n\n// SetFormatter returns the logger formatter.\nfunc (logger *logger) Formatter() Formatter {\n\treturn logger.formatter\n}\n\n// WithOptions implements the Logger interface method.\nfunc (logger *logger) WithOptions(opts ...Option) Logger {\n\tif len(opts) == 0 {\n\t\treturn logger\n\t}\n\n\tlogger = logger.clone()\n\tlogger.SetOptions(opts...)\n\n\treturn logger\n}\n\n// Level returns log level.\nfunc (logger *logger) Level() Level {\n\treturn FromLogrusLevel(logger.Logger.Level)\n}\n\n// SetLevel parses and sets log level.\nfunc (logger *logger) SetLevel(str string) error {\n\tlevel, err := ParseLevel(str)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlogger.Logger.SetLevel(level.ToLogrusLevel())\n\n\treturn nil\n}\n\n// WriterLevel implements the Logger interface method.\nfunc (logger *logger) WriterLevel(level Level) *io.PipeWriter {\n\treturn logger.Logger.WriterLevel(level.ToLogrusLevel())\n}\n\n// // WithField implements the Logger interface method.\nfunc (logger *logger) WithField(key string, value any) Logger {\n\treturn logger.WithFields(Fields{key: value})\n}\n\n// WithFields implements the Logger interface method.\nfunc (logger *logger) WithFields(fields Fields) Logger {\n\treturn logger.setEntry(logger.Entry.WithFields(logrus.Fields(fields)))\n}\n\n// WithError implements the Logger interface method.\nfunc (logger *logger) WithError(err error) Logger {\n\treturn logger.setEntry(logger.Entry.WithError(err))\n}\n\n// WithContext implements the Logger interface method.\nfunc (logger *logger) WithContext(ctx context.Context) Logger {\n\treturn logger.setEntry(logger.Entry.WithContext(ctx))\n}\n\n// WithTime implements the Logger interface method.\nfunc (logger *logger) WithTime(t time.Time) Logger {\n\treturn logger.setEntry(logger.Entry.WithTime(t))\n}\n\n// Logf implements the Logger interface method.\nfunc (logger *logger) Logf(level Level, format string, args ...any) {\n\tlogger.Entry.Logf(level.ToLogrusLevel(), format, args...)\n}\n\n// Log implements the Logger interface method.\nfunc (logger *logger) Log(level Level, args ...any) {\n\tlogger.Entry.Log(level.ToLogrusLevel(), args...)\n}\n\n// Logln implements the Logger interface method.\nfunc (logger *logger) Logln(level Level, args ...any) {\n\tlogger.Entry.Logln(level.ToLogrusLevel(), args...)\n}\n\n// Trace implements the Logger interface method.\nfunc (logger *logger) Trace(args ...any) {\n\tlogger.Log(TraceLevel, args...)\n}\n\n// Debug implements the Logger interface method.\nfunc (logger *logger) Debug(args ...any) {\n\tlogger.Log(DebugLevel, args...)\n}\n\n// Print implements the Logger interface method.\nfunc (logger *logger) Print(args ...any) {\n\tlogger.Info(args...)\n}\n\n// Info implements the Logger interface method.\nfunc (logger *logger) Info(args ...any) {\n\tlogger.Log(InfoLevel, args...)\n}\n\n// Warn implements the Logger interface method.\nfunc (logger *logger) Warn(args ...any) {\n\tlogger.Log(WarnLevel, args...)\n}\n\n// Error implements the Logger interface method.\nfunc (logger *logger) Error(args ...any) {\n\tlogger.Log(ErrorLevel, args...)\n}\n\n// Entry Printf family functions.\n\n// Tracef implements the Logger interface method.\nfunc (logger *logger) Tracef(format string, args ...any) {\n\tlogger.Logf(TraceLevel, format, args...)\n}\n\n// Debugf implements the Logger interface method.\nfunc (logger *logger) Debugf(format string, args ...any) {\n\tlogger.Logf(DebugLevel, format, args...)\n}\n\n// Infof implements the Logger interface method.\nfunc (logger *logger) Infof(format string, args ...any) {\n\tlogger.Logf(InfoLevel, format, args...)\n}\n\n// Printf implements the Logger interface method.\nfunc (logger *logger) Printf(format string, args ...any) {\n\tlogger.Infof(format, args...)\n}\n\n// Warnf implements the Logger interface method.\nfunc (logger *logger) Warnf(format string, args ...any) {\n\tlogger.Logf(WarnLevel, format, args...)\n}\n\n// Errorf implements the Logger interface method.\nfunc (logger *logger) Errorf(format string, args ...any) {\n\tlogger.Logf(ErrorLevel, format, args...)\n}\n\n// Entry Println family functions\n\n// Traceln implements the Logger interface method.\nfunc (logger *logger) Traceln(args ...any) {\n\tlogger.Logln(TraceLevel, args...)\n}\n\n// Debugln implements the Logger interface method.\nfunc (logger *logger) Debugln(args ...any) {\n\tlogger.Logln(DebugLevel, args...)\n}\n\n// Infoln implements the Logger interface method.\nfunc (logger *logger) Infoln(args ...any) {\n\tlogger.Logln(InfoLevel, args...)\n}\n\n// Println implements the Logger interface method.\nfunc (logger *logger) Println(args ...any) {\n\tlogger.Infoln(args...)\n}\n\n// Warnln implements the Logger interface method.\nfunc (logger *logger) Warnln(args ...any) {\n\tlogger.Logln(WarnLevel, args...)\n}\n\n// Errorln implements the Logger interface method.\nfunc (logger *logger) Errorln(args ...any) {\n\tlogger.Logln(ErrorLevel, args...)\n}\n\nfunc (logger *logger) setEntry(entry *logrus.Entry) *logger {\n\tnewLogger := *logger\n\tnewLogger.Entry = entry\n\n\treturn &newLogger\n}\n\nfunc (logger *logger) clone() *logger {\n\tnewLogger := *logger\n\n\tparentLogger := newLogger.Logger\n\n\tnewLogger.Logger = logrus.New()\n\tnewLogger.Logger.SetOutput(parentLogger.Out)\n\tnewLogger.Logger.SetLevel(parentLogger.Level)\n\tnewLogger.Logger.SetFormatter(parentLogger.Formatter)\n\tnewLogger.Logger.ReplaceHooks(parentLogger.Hooks)\n\tnewLogger.Entry = newLogger.Dup()\n\n\treturn &newLogger\n}\n"
  },
  {
    "path": "pkg/log/logger_test.go",
    "content": "package log_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNew(t *testing.T) {\n\tt.Parallel()\n\n\tlogger := log.New()\n\tassert.NotNil(t, logger)\n}\n\nfunc TestLoggerLevelFiltering(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname        string\n\t\tloggerLevel log.Level\n\t\tmsgLevel    log.Level\n\t\texpectEmpty bool\n\t}{\n\t\t{name: \"info_at_info_visible\", loggerLevel: log.InfoLevel, msgLevel: log.InfoLevel},\n\t\t{name: \"debug_at_info_hidden\", loggerLevel: log.InfoLevel, msgLevel: log.DebugLevel, expectEmpty: true},\n\t\t{name: \"error_at_info_visible\", loggerLevel: log.InfoLevel, msgLevel: log.ErrorLevel},\n\t\t{name: \"trace_at_trace_visible\", loggerLevel: log.TraceLevel, msgLevel: log.TraceLevel},\n\t\t{name: \"warn_at_error_hidden\", loggerLevel: log.ErrorLevel, msgLevel: log.WarnLevel, expectEmpty: true},\n\t\t{name: \"stderr_at_info_visible\", loggerLevel: log.InfoLevel, msgLevel: log.StderrLevel},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tlogger, buf := newTestLogger(tc.loggerLevel)\n\t\t\tlogger.Log(tc.msgLevel, \"test message\")\n\n\t\t\tif tc.expectEmpty {\n\t\t\t\tassert.Empty(t, buf.String())\n\t\t\t} else {\n\t\t\t\tassert.Contains(t, buf.String(), \"test message\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLoggerClone(t *testing.T) {\n\tt.Parallel()\n\n\toriginal := log.New(log.WithLevel(log.InfoLevel))\n\tclone := original.Clone()\n\n\tassert.NotNil(t, clone)\n\tassert.Equal(t, log.InfoLevel, clone.Level())\n\n\t// Clone preserves output independence: writing to clone doesn't affect a separate buffer\n\tbuf := new(bytes.Buffer)\n\tcloneWithBuf := clone.WithOptions(log.WithLevel(log.DebugLevel), log.WithOutput(buf))\n\tcloneWithBuf.Debug(\"clone message\")\n\tassert.Contains(t, buf.String(), \"clone message\")\n}\n\nfunc TestLoggerWithOptions(t *testing.T) {\n\tt.Parallel()\n\n\toriginal := log.New(log.WithLevel(log.InfoLevel))\n\tmodified := original.WithOptions(log.WithLevel(log.DebugLevel))\n\n\tassert.NotNil(t, modified)\n\tassert.Equal(t, log.DebugLevel, modified.Level())\n}\n\nfunc TestLoggerSetLevel(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"valid_level\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tlogger := log.New(log.WithLevel(log.InfoLevel))\n\t\terr := logger.SetLevel(\"debug\")\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, log.DebugLevel, logger.Level())\n\t})\n\n\tt.Run(\"invalid_level\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tlogger := log.New(log.WithLevel(log.InfoLevel))\n\t\terr := logger.SetLevel(\"banana\")\n\t\trequire.Error(t, err)\n\t\tassert.Equal(t, log.InfoLevel, logger.Level())\n\t})\n}\n\nfunc TestLoggerWithField(t *testing.T) {\n\tt.Parallel()\n\n\tlogger, buf := newTestLogger(log.InfoLevel)\n\tloggerWithField := logger.WithField(\"key\", \"value\")\n\tloggerWithField.Info(\"field test\")\n\n\toutput := buf.String()\n\tassert.Contains(t, output, \"field test\")\n\tassert.Contains(t, output, \"key\")\n}\n\nfunc TestLoggerWithFields(t *testing.T) {\n\tt.Parallel()\n\n\tlogger, buf := newTestLogger(log.InfoLevel)\n\tloggerWithFields := logger.WithFields(log.Fields{\"k1\": \"v1\", \"k2\": \"v2\"})\n\tloggerWithFields.Info(\"fields test\")\n\n\toutput := buf.String()\n\tassert.Contains(t, output, \"fields test\")\n\tassert.Contains(t, output, \"k1\")\n\tassert.Contains(t, output, \"k2\")\n}\n\nfunc TestLoggerWithError(t *testing.T) {\n\tt.Parallel()\n\n\tlogger, buf := newTestLogger(log.InfoLevel)\n\tloggerWithErr := logger.WithError(errors.New(\"test error\"))\n\tloggerWithErr.Info(\"error test\")\n\n\toutput := buf.String()\n\tassert.Contains(t, output, \"error test\")\n\tassert.Contains(t, output, \"test error\")\n}\n\nfunc TestLoggerFormattedOutput(t *testing.T) {\n\tt.Parallel()\n\n\tlogger, buf := newTestLogger(log.InfoLevel)\n\tlogger.Infof(\"hello %s\", \"world\")\n\n\tassert.Contains(t, buf.String(), \"hello world\")\n}\n\n// newTestLogger creates a logger that writes to a buffer using the default logrus text formatter.\nfunc newTestLogger(level log.Level) (log.Logger, *bytes.Buffer) {\n\tbuf := new(bytes.Buffer)\n\tlogger := log.New(\n\t\tlog.WithLevel(level),\n\t\tlog.WithOutput(buf),\n\t)\n\n\treturn logger, buf\n}\n"
  },
  {
    "path": "pkg/log/options.go",
    "content": "package log\n\nimport (\n\t\"io\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\n// Option is a function to set options for logger.\ntype Option func(logger *logger)\n\n// WithLevel sets the logger level.\nfunc WithLevel(level Level) Option {\n\treturn func(logger *logger) {\n\t\tlogger.Logger.SetLevel(level.ToLogrusLevel())\n\t}\n}\n\n// WithOutput sets the logger output.\nfunc WithOutput(output io.Writer) Option {\n\treturn func(logger *logger) {\n\t\tlogger.Logger.SetOutput(output)\n\t}\n}\n\n// WithFormatter sets the logger formatter.\nfunc WithFormatter(formatter Formatter) Option {\n\treturn func(logger *logger) {\n\t\tlogger.SetFormatter(formatter)\n\t}\n}\n\n// WithHooks adds hooks to the logger hooks.\nfunc WithHooks(hooks ...logrus.Hook) Option {\n\treturn func(logger *logger) {\n\t\tfor _, hook := range hooks {\n\t\t\tlogger.Logger.AddHook(hook)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/log/util.go",
    "content": "package log\n\nimport (\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nconst (\n\tCurDir              = \".\"\n\tCurDirWithSeparator = CurDir + string(os.PathSeparator)\n\n\t// startASNISeq is the ANSI start escape sequence\n\tstartASNISeq = \"\\033[\"\n\t// resetANSISeq is the ANSI reset escape sequence\n\tresetANSISeq = \"\\033[0m\"\n\n\tansiSeq = \"[\\u001B\\u009B][[\\\\]()#;?]*(?:(?:(?:[a-zA-Z\\\\d]*(?:;[a-zA-Z\\\\d]*)*)?\\u0007)|(?:(?:\\\\d{1,4}(?:;\\\\d{0,4})*)?[\\\\dA-PRZcf-ntqry=><~]))\"\n)\n\nvar (\n\t// regexp matches ansi characters getting from a shell output, used for colors etc.\n\tansiReg = regexp.MustCompile(ansiSeq)\n)\n\n// RemoveAllASCISeq returns a string with all ASCII color characters removed.\nfunc RemoveAllASCISeq(str string) string {\n\tif strings.Contains(str, startASNISeq) {\n\t\tstr = ansiReg.ReplaceAllString(str, \"\")\n\t}\n\n\treturn str\n}\n\n// ResetASCISeq returns a string with the ASCI color reset to the default one.\nfunc ResetASCISeq(str string) string {\n\tif strings.Contains(str, startASNISeq) {\n\t\tstr += resetANSISeq\n\t}\n\n\treturn str\n}\n"
  },
  {
    "path": "pkg/log/util_test.go",
    "content": "package log_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRemoveAllASCISeq(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"no_ansi\",\n\t\t\tinput:    \"hello world\",\n\t\t\texpected: \"hello world\",\n\t\t},\n\t\t{\n\t\t\tname:     \"single_color_code\",\n\t\t\tinput:    \"\\033[31mhello\\033[0m\",\n\t\t\texpected: \"hello\",\n\t\t},\n\t\t{\n\t\t\tname:     \"bold\",\n\t\t\tinput:    \"\\033[1mbold text\\033[0m\",\n\t\t\texpected: \"bold text\",\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple_sequences\",\n\t\t\tinput:    \"\\033[31mred\\033[0m and \\033[32mgreen\\033[0m\",\n\t\t\texpected: \"red and green\",\n\t\t},\n\t\t{\n\t\t\tname:     \"empty_string\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"embedded_mid_string\",\n\t\t\tinput:    \"before\\033[33mmiddle\\033[0mafter\",\n\t\t\texpected: \"beforemiddleafter\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, tc.expected, log.RemoveAllASCISeq(tc.input))\n\t\t})\n\t}\n}\n\nfunc TestResetASCISeq(t *testing.T) {\n\tt.Parallel()\n\n\ttests := []struct {\n\t\tname     string\n\t\tinput    string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"no_ansi_unchanged\",\n\t\t\tinput:    \"hello world\",\n\t\t\texpected: \"hello world\",\n\t\t},\n\t\t{\n\t\t\tname:     \"with_ansi_appends_reset\",\n\t\t\tinput:    \"\\033[31mhello\",\n\t\t\texpected: \"\\033[31mhello\\033[0m\",\n\t\t},\n\t\t{\n\t\t\tname:     \"empty_unchanged\",\n\t\t\tinput:    \"\",\n\t\t\texpected: \"\",\n\t\t},\n\t\t{\n\t\t\tname:     \"already_has_reset\",\n\t\t\tinput:    \"\\033[31mhello\\033[0m\",\n\t\t\texpected: \"\\033[31mhello\\033[0m\\033[0m\",\n\t\t},\n\t}\n\n\tfor _, tc := range tests {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, tc.expected, log.ResetASCISeq(tc.input))\n\t\t})\n\t}\n}\n\nfunc FuzzRemoveAllASCISeq(f *testing.F) {\n\tf.Add(\"\")\n\tf.Add(\"hello\")\n\tf.Add(\"\\033[31mred\\033[0m\")\n\tf.Add(\"\\033[1;32mbold green\\033[0m\")\n\tf.Add(\"\\033[\") // bare/incomplete sequence\n\n\tf.Fuzz(func(t *testing.T, input string) {\n\t\t// Must never panic\n\t\tresult := log.RemoveAllASCISeq(input)\n\t\t// If input had no ANSI start sequence, output equals input\n\t\tif !strings.Contains(input, \"\\033[\") {\n\t\t\tassert.Equal(t, input, result)\n\t\t}\n\t})\n}\n\nfunc FuzzResetASCISeq(f *testing.F) {\n\tf.Add(\"\")\n\tf.Add(\"hello\")\n\tf.Add(\"\\033[31mred\")\n\tf.Add(\"\\033[1;32mbold green\\033[0m\")\n\n\tf.Fuzz(func(t *testing.T, input string) {\n\t\tresult := log.ResetASCISeq(input)\n\t\t// If input contained ANSI escape, output must end with reset sequence\n\t\tif strings.Contains(input, \"\\033[\") {\n\t\t\tassert.True(t, strings.HasSuffix(result, \"\\033[0m\"), \"output with ANSI sequences should end with reset\")\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "pkg/log/writer/options.go",
    "content": "package writer\n\nimport \"github.com/gruntwork-io/terragrunt/pkg/log\"\n\n// Option is a function to set options for Writer.\ntype Option func(writer *Writer)\n\n// WithLogger sets Logger to the Writer.\nfunc WithLogger(logger log.Logger) Option {\n\treturn func(writer *Writer) {\n\t\twriter.logger = logger\n\t}\n}\n\n// WithDefaultLevel sets the default log level for Writer in case the log level cannot be extracted from the message.\nfunc WithDefaultLevel(level log.Level) Option {\n\treturn func(writer *Writer) {\n\t\twriter.defaultLevel = level\n\t}\n}\n\n// WithMsgSeparator configures Writer to split the received text into string and log them as separate records.\nfunc WithMsgSeparator(sep string) Option {\n\treturn func(writer *Writer) {\n\t\twriter.msgSeparator = sep\n\t}\n}\n\n// WithParseFunc sets the parser func.\nfunc WithParseFunc(fn WriterParseFunc) Option {\n\treturn func(writer *Writer) {\n\t\twriter.parseFunc = fn\n\t}\n}\n"
  },
  {
    "path": "pkg/log/writer/writer.go",
    "content": "// Package writer provides a writer that redirects Write requests to configured logger and level.\npackage writer\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n)\n\n// WriterParseFunc is a function used to parse records to extract the time and level from them.\ntype WriterParseFunc func(str string) (msg string, time *time.Time, level *log.Level, err error)\n\n// Writer redirects Write requests to configured logger and level\ntype Writer struct {\n\tlogger       log.Logger\n\tparseFunc    WriterParseFunc\n\tmsgSeparator string\n\tdefaultLevel log.Level\n}\n\n// New returns a new Writer instance with fields assigned to default values.\nfunc New(opts ...Option) *Writer {\n\twriter := &Writer{\n\t\tlogger:       log.Default(),\n\t\tdefaultLevel: log.InfoLevel,\n\t\tparseFunc:    func(str string) (msg string, time *time.Time, level *log.Level, err error) { return str, nil, nil, nil },\n\t}\n\twriter.SetOption(opts...)\n\n\treturn writer\n}\n\n// SetOption sets options to the `Writer`.\nfunc (writer *Writer) SetOption(opts ...Option) {\n\tfor _, opt := range opts {\n\t\topt(writer)\n\t}\n}\n\n// Write implements `io.Writer` interface.\nfunc (writer *Writer) Write(p []byte) (n int, err error) {\n\tvar (\n\t\tstr  = string(p)\n\t\tstrs = []string{str}\n\t)\n\n\tif writer.msgSeparator != \"\" {\n\t\tstrs = strings.Split(str, writer.msgSeparator)\n\t}\n\n\tfor _, str := range strs {\n\t\tif len(str) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tmsg, time, level, err := writer.parseFunc(str)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\t// Reset ANSI styles at the end of a line so that the new line does not inherit them\n\t\tmsg = log.ResetASCISeq(msg)\n\n\t\tlogger := writer.logger\n\n\t\tif time != nil {\n\t\t\tlogger = logger.WithTime(*time)\n\t\t}\n\n\t\tif level == nil {\n\t\t\tlevel = &writer.defaultLevel\n\t\t}\n\n\t\tlogger.Log(*level, msg)\n\t}\n\n\treturn len(p), nil\n}\n"
  },
  {
    "path": "pkg/log/writer/writer_test.go",
    "content": "package writer_test\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/writer\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestWriterWrite(t *testing.T) {\n\tt.Parallel()\n\n\tlogger, buf := newTestLogger(log.InfoLevel)\n\tw := writer.New(\n\t\twriter.WithLogger(logger),\n\t)\n\n\tn, err := w.Write([]byte(\"hello writer\"))\n\trequire.NoError(t, err)\n\tassert.Len(t, \"hello writer\", n)\n\tassert.Contains(t, buf.String(), \"hello writer\")\n}\n\nfunc TestWriterWithMsgSeparator(t *testing.T) {\n\tt.Parallel()\n\n\tlogger, buf := newTestLogger(log.InfoLevel)\n\tw := writer.New(\n\t\twriter.WithLogger(logger),\n\t\twriter.WithMsgSeparator(\"\\n\"),\n\t)\n\n\t_, err := w.Write([]byte(\"line1\\nline2\\nline3\"))\n\trequire.NoError(t, err)\n\n\toutput := buf.String()\n\tassert.Contains(t, output, \"line1\")\n\tassert.Contains(t, output, \"line2\")\n\tassert.Contains(t, output, \"line3\")\n}\n\nfunc TestWriterWithDefaultLevel(t *testing.T) {\n\tt.Parallel()\n\n\tlogger, buf := newTestLogger(log.TraceLevel)\n\tw := writer.New(\n\t\twriter.WithLogger(logger),\n\t\twriter.WithDefaultLevel(log.DebugLevel),\n\t)\n\n\t_, err := w.Write([]byte(\"debug message\"))\n\trequire.NoError(t, err)\n\tassert.Contains(t, buf.String(), \"debug message\")\n}\n\nfunc TestWriterWithParseFunc(t *testing.T) {\n\tt.Parallel()\n\n\tlogger, buf := newTestLogger(log.TraceLevel)\n\twarnLevel := log.WarnLevel\n\n\tw := writer.New(\n\t\twriter.WithLogger(logger),\n\t\twriter.WithParseFunc(func(str string) (string, *time.Time, *log.Level, error) {\n\t\t\treturn \"parsed: \" + str, nil, &warnLevel, nil\n\t\t}),\n\t)\n\n\t_, err := w.Write([]byte(\"raw message\"))\n\trequire.NoError(t, err)\n\tassert.Contains(t, buf.String(), \"parsed: raw message\")\n}\n\nfunc TestWriterEmptyInput(t *testing.T) {\n\tt.Parallel()\n\n\tlogger, buf := newTestLogger(log.InfoLevel)\n\tw := writer.New(\n\t\twriter.WithLogger(logger),\n\t\twriter.WithMsgSeparator(\"\\n\"),\n\t)\n\n\tn, err := w.Write([]byte(\"\"))\n\trequire.NoError(t, err)\n\tassert.Equal(t, 0, n)\n\tassert.Empty(t, buf.String())\n}\n\nfunc newTestLogger(level log.Level) (log.Logger, *bytes.Buffer) {\n\tbuf := new(bytes.Buffer)\n\tlogger := log.New(\n\t\tlog.WithLevel(level),\n\t\tlog.WithOutput(buf),\n\t)\n\n\treturn logger, buf\n}\n"
  },
  {
    "path": "pkg/options/auto_retry_options.go",
    "content": "package options\n\nimport (\n\t\"regexp\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errorconfig\"\n\t\"github.com/gruntwork-io/terragrunt/internal/retry\"\n)\n\n// defaultErrorsConfig builds a default errorconfig.Config using retry.DefaultRetryableErrors\n// and default retry timings. Intended as a fallback when no errors{retry} blocks\n// are defined in configuration.\nfunc defaultErrorsConfig() *errorconfig.Config {\n\tcompiled := make([]*errorconfig.Pattern, 0, len(retry.DefaultRetryableErrors))\n\n\tfor _, pat := range retry.DefaultRetryableErrors {\n\t\tre, err := regexp.Compile(pat)\n\t\tif err != nil {\n\t\t\t// Should not happen, as patterns are hardcoded and tested\n\t\t\tpanic(err)\n\t\t}\n\n\t\tcompiled = append(compiled, &errorconfig.Pattern{Pattern: re})\n\t}\n\n\tcfg := &errorconfig.Config{\n\t\tRetry:  map[string]*errorconfig.RetryConfig{},\n\t\tIgnore: map[string]*errorconfig.IgnoreConfig{},\n\t}\n\n\tif len(compiled) == 0 {\n\t\treturn cfg\n\t}\n\n\tcfg.Retry[\"default\"] = &errorconfig.RetryConfig{\n\t\tName:             \"default\",\n\t\tRetryableErrors:  compiled,\n\t\tMaxAttempts:      retry.DefaultMaxAttempts,\n\t\tSleepIntervalSec: int(retry.DefaultSleepInterval / time.Second),\n\t}\n\n\treturn cfg\n}\n"
  },
  {
    "path": "pkg/options/options.go",
    "content": "// Package options provides a set of options that configure the behavior of the Terragrunt program.\npackage options\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cloner\"\n\t\"github.com/gruntwork-io/terragrunt/internal/engine\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errorconfig\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/experiment\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\t\"github.com/gruntwork-io/terragrunt/internal/iam\"\n\tpcoptions \"github.com/gruntwork-io/terragrunt/internal/providercache/options\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict\"\n\t\"github.com/gruntwork-io/terragrunt/internal/strict/controls\"\n\t\"github.com/gruntwork-io/terragrunt/internal/telemetry\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tfimpl\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tips\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/internal/writer\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\"\n\t\"github.com/hashicorp/go-version\"\n\t\"github.com/puzpuzpuz/xsync/v3\"\n)\n\nconst ContextKey ctxKey = iota\n\nconst (\n\tDefaultMaxFoldersToCheck = 100\n\n\t// no limits on parallelism by default (limited by GOPROCS)\n\tDefaultParallelism = math.MaxInt32\n\n\t// TofuDefaultPath command to run tofu\n\tTofuDefaultPath = \"tofu\"\n\n\t// TerraformDefaultPath just takes terraform from the path\n\tTerraformDefaultPath = \"terraform\"\n\n\t// Default to naming it `terragrunt_rendered.json` in the terragrunt config directory.\n\tDefaultJSONOutName = \"terragrunt_rendered.json\"\n\n\tDefaultSignalsFile = \"error-signals.json\"\n\n\tDefaultTFDataDir = \".terraform\"\n\n\tdefaultExcludesFile = \".terragrunt-excludes\"\n\tdefaultFiltersFile  = \".terragrunt-filters\"\n\n\tDefaultLogLevel = log.InfoLevel\n)\n\nvar (\n\tDefaultWrappedPath = identifyDefaultWrappedExecutable(context.Background())\n\n\tdefaultVersionManagerFileName = []string{\n\t\t\".terraform-version\",\n\t\t\".tool-versions\",\n\t\t\"mise.toml\",\n\t\t\".mise.toml\",\n\t}\n)\n\ntype ctxKey byte\n\n// TerragruntOptions represents options that configure the behavior of the Terragrunt program\ntype TerragruntOptions struct {\n\tWriters writer.Writers\n\t// Version of terragrunt\n\tTerragruntVersion *version.Version `clone:\"shadowcopy\"`\n\t// FeatureFlags is a map of feature flags to enable.\n\tFeatureFlags *xsync.MapOf[string, string] `clone:\"shadowcopy\"`\n\t// EngineConfig holds the resolved engine configuration from HCL.\n\tEngineConfig *engine.EngineConfig\n\t// EngineOptions groups CLI-supplied engine options.\n\tEngineOptions *engine.EngineOptions\n\t// Telemetry are telemetry options.\n\tTelemetry *telemetry.Options\n\t// Attributes to override in AWS provider nested within modules as part of the aws-provider-patch command.\n\tAwsProviderPatchOverrides map[string]string\n\t// Version of terraform (obtained by running 'terraform version')\n\tTerraformVersion *version.Version `clone:\"shadowcopy\"`\n\t// Errors is a configuration for error handling.\n\tErrors *errorconfig.Config\n\t// Map to replace terraform source locations.\n\tSourceMap map[string]string\n\t// Environment variables at runtime\n\tEnv map[string]string\n\t// StackAction is the action that should be performed on the stack.\n\tStackAction string\n\t// IAM Role options that should be used when authenticating to AWS.\n\tIAMRoleOptions iam.RoleOptions\n\t// IAM Role options set from command line.\n\tOriginalIAMRoleOptions iam.RoleOptions\n\t// Current Terraform command being executed by Terragrunt\n\tTerraformCommand string\n\t// StackOutputFormat format how the stack output is rendered.\n\tStackOutputFormat         string\n\tTerragruntStackConfigPath string\n\t// Location of the original Terragrunt config file.\n\tOriginalTerragruntConfigPath string\n\t// Unlike `WorkingDir`, this path is the same for all dependencies and points to the root working directory specified in the CLI.\n\tRootWorkingDir string\n\t// Download Terraform configurations from the specified source location into a temporary folder\n\tSource string\n\t// The working directory in which to run Terraform\n\tWorkingDir string\n\t// Location (or name) of the OpenTofu/Terraform binary\n\tTFPath string\n\t// Download Terraform configurations specified in the Source parameter into this folder\n\tDownloadDir string\n\t// Original Terraform command being executed by Terragrunt.\n\tOriginalTerraformCommand string\n\t// Terraform implementation tool (e.g. terraform, tofu) that terragrunt is wrapping\n\tTofuImplementation tfimpl.Type\n\t// The file path that terragrunt should use when rendering the terragrunt.hcl config as json.\n\tJSONOut string\n\t// The command and arguments that can be used to fetch authentication configurations.\n\tAuthProviderCmd string\n\t// Folder to store JSON representation of output files.\n\tJSONOutputFolder string\n\t// Folder to store output files.\n\tOutputFolder string\n\t// The file which hclfmt should be specifically run on\n\tHclFile string\n\t// Location of the Terragrunt config file\n\tTerragruntConfigPath string\n\t// Name of the root Terragrunt configuration file, if used.\n\tScaffoldRootFileName string\n\t// Path to a file with a list of directories that need to be excluded when running *-all commands.\n\tExcludesFile string\n\t// Path to folder of scaffold output\n\tScaffoldOutputFolder string\n\t// Root directory for graph command.\n\tGraphRoot string\n\t// Path to the report file.\n\tReportFile string\n\t// Path to a file containing filter queries, one per line. Default is .terragrunt-filters.\n\tFiltersFile string\n\t// Report format.\n\tReportFormat report.Format\n\t// Path to the report schema file.\n\tReportSchemaFile string\n\t// CLI args that are intended for Terraform (i.e. all the CLI args except the --terragrunt ones)\n\tTerraformCliArgs *iacargs.IacArgs\n\t// Files with variables to be used in modules scaffolding.\n\tScaffoldVarFiles []string\n\t// If set hclfmt will skip files in given directories.\n\tHclExclude []string\n\t// Variables for usage in scaffolding.\n\tScaffoldVars []string\n\t// StrictControls is a slice of strict controls.\n\tStrictControls strict.Controls `clone:\"shadowcopy\"`\n\t// Filters contains parsed filter objects for component selection.\n\tFilters filter.Filters `clone:\"shadowcopy\"`\n\t// When set, it will be used to compute the cache key for `-version` checks.\n\tVersionManagerFileName []string\n\t// Experiments is a map of experiments, and their status.\n\tExperiments experiment.Experiments `clone:\"shadowcopy\"`\n\t// Tips is a collection of tips that can be shown to users.\n\tTips tips.Tips `clone:\"shadowcopy\"`\n\t// ProviderCacheOptions groups all provider-cache-specific configuration.\n\tProviderCacheOptions pcoptions.ProviderCacheOptions\n\t// Parallelism limits the number of commands to run concurrently during *-all commands\n\tParallelism int\n\t// When searching the directory tree, this is the max folders to check before exiting with an error.\n\tMaxFoldersToCheck int\n\t// Output Terragrunt logs in JSON format\n\tJSONLogFormat bool\n\t// True if terragrunt should run in debug mode\n\tDebug bool\n\t// Disable TF output formatting\n\tForwardTFStdout bool\n\t// Fail execution if is required to create S3 bucket\n\tFailIfBucketCreationRequired bool\n\t// FilterAllowDestroy allows destroy runs when using Git-based filters\n\tFilterAllowDestroy bool\n\t// Controls if s3 bucket should be updated or skipped\n\tDisableBucketUpdate bool\n\t// Disables validation terraform command\n\tDisableCommandValidation bool\n\t// If True then HCL from StdIn must should be formatted.\n\tHclFromStdin bool\n\t// Show diff, by default it's disabled.\n\tDiff bool\n\t// Do not include root unit in scaffolding.\n\tScaffoldNoIncludeRoot bool\n\t// Enable check mode, by default it's disabled.\n\tCheck bool\n\t// Enables caching of includes during partial parsing operations.\n\tUsePartialParseConfigCache bool\n\t// True if is required to show dependent units and confirm action\n\tCheckDependentUnits bool\n\t// True if is required to check for dependent modules during destroy operations\n\tDestroyDependenciesCheck bool\n\t// Include fields metadata in render-json\n\tRenderJSONWithMetadata bool\n\t// Whether we should automatically retry errored Terraform commands\n\tAutoRetry bool\n\t// Whether we should automatically run terraform init if necessary when executing other commands\n\tAutoInit bool\n\t// Allows to skip the output of all dependencies.\n\tSkipOutput bool\n\t// Whether we should prompt the user for confirmation or always assume \"yes\"\n\tNonInteractive bool\n\t// If set to true, ignore the dependency order when running *-all command.\n\tIgnoreDependencyOrder bool\n\t// If set to true, continue running *-all commands even if a dependency has errors.\n\tIgnoreDependencyErrors bool\n\t// Whether we should automatically run terraform with -auto-apply in run --all mode.\n\tRunAllAutoApprove bool\n\t// If set to true, delete the contents of the temporary folder before downloading Terraform source code into it\n\tSourceUpdate bool\n\t// HCLValidateStrict is a strict mode for HCL validation files. When it's set to false the command will only return an error if required inputs are missing from all input sources (env vars, var files, etc). When it's set to true, an error will be returned if required inputs are missing or if unused variables are passed to Terragrunt.\",\n\tHCLValidateStrict bool\n\t// HCLValidateInputs checks if the terragrunt configured inputs align with the terraform defined variables.\n\tHCLValidateInputs bool\n\t// HCLValidateShowConfigPath shows the paths of the hcl invalid configs.\n\tHCLValidateShowConfigPath bool\n\t// HCLValidateJSONOutput outputs the hcl validate result as a JSON string.\n\tHCLValidateJSONOutput bool\n\t// If true, logs will be displayed in formatter key/value, by default logs are formatted in human-readable formatter.\n\tDisableLogFormatting bool\n\t// Headless is set when Terragrunt is running in headless mode.\n\tHeadless bool\n\t// NoStackGenerate disable stack generation.\n\tNoStackGenerate bool\n\t// NoStackValidate disable generated stack validation.\n\tNoStackValidate bool\n\t// RunAll runs the provided OpenTofu/Terraform command against a stack.\n\tRunAll bool\n\t// Graph runs the provided OpenTofu/Terraform against the graph of dependencies for the unit in the current working directory.\n\tGraph bool\n\t// BackendBootstrap automatically bootstraps backend infrastructure before attempting to use it.\n\tBackendBootstrap bool\n\t// DeleteBucket determines whether to delete entire bucket.\n\tDeleteBucket bool\n\t// ForceBackendDelete forces the backend to be deleted, even if the bucket is not versioned.\n\tForceBackendDelete bool\n\t// ForceBackendMigrate forces the backend to be migrated, even if the bucket is not versioned.\n\tForceBackendMigrate bool\n\t// SummaryDisable disables the summary output at the end of a run.\n\tSummaryDisable bool\n\t// SummaryPerUnit enables showing duration information for each unit in the summary.\n\tSummaryPerUnit bool\n\t// NoAutoProviderCacheDir disables the auto-provider-cache-dir feature even when the experiment is enabled.\n\tNoAutoProviderCacheDir bool\n\t// NoDependencyFetchOutputFromState disables the dependency-fetch-output-from-state feature even when the experiment is enabled.\n\tNoDependencyFetchOutputFromState bool\n\t// TFPathExplicitlySet is set to true if the user has explicitly set the TFPath via the --tf-path flag.\n\tTFPathExplicitlySet bool\n\t// FailFast is a flag to stop execution on the first error in apply of units.\n\tFailFast bool\n\t// NoDependencyPrompt disables prompt requiring confirmation for base and leaf file dependencies when using scaffolding.\n\tNoDependencyPrompt bool\n\t// NoShell disables shell commands when using boilerplate templates in catalog and scaffold commands.\n\tNoShell bool\n\t// NoHooks disables hooks when using boilerplate templates in catalog and scaffold commands.\n\tNoHooks bool\n\t// If set, disable automatic reading of .terragrunt-filters file.\n\tNoFiltersFile bool\n}\n\n// TerragruntOptionsFunc is a functional option type used to pass options in certain integration tests\ntype TerragruntOptionsFunc func(*TerragruntOptions)\n\n// WithIAMRoleARN adds the provided role ARN to IamRoleOptions\nfunc WithIAMRoleARN(arn string) TerragruntOptionsFunc {\n\treturn func(t *TerragruntOptions) {\n\t\tt.IAMRoleOptions.RoleARN = arn\n\t}\n}\n\n// WithIAMWebIdentityToken adds the provided WebIdentity token to IamRoleOptions\nfunc WithIAMWebIdentityToken(token string) TerragruntOptionsFunc {\n\treturn func(t *TerragruntOptions) {\n\t\tt.IAMRoleOptions.WebIdentityToken = token\n\t}\n}\n\n// NewTerragruntOptions creates a new TerragruntOptions object with\n// reasonable defaults for real usage\nfunc NewTerragruntOptions() *TerragruntOptions {\n\treturn NewTerragruntOptionsWithWriters(os.Stdout, os.Stderr)\n}\n\nfunc NewTerragruntOptionsWithWriters(stdout, stderr io.Writer) *TerragruntOptions {\n\treturn &TerragruntOptions{\n\t\tWriters:                writer.Writers{Writer: stdout, ErrWriter: stderr},\n\t\tTFPath:                 DefaultWrappedPath,\n\t\tExcludesFile:           defaultExcludesFile,\n\t\tFiltersFile:            defaultFiltersFile,\n\t\tAutoInit:               true,\n\t\tRunAllAutoApprove:      true,\n\t\tEnv:                    map[string]string{},\n\t\tSourceMap:              map[string]string{},\n\t\tTerraformCliArgs:       iacargs.New(),\n\t\tMaxFoldersToCheck:      DefaultMaxFoldersToCheck,\n\t\tAutoRetry:              true,\n\t\tParallelism:            DefaultParallelism,\n\t\tJSONOut:                DefaultJSONOutName,\n\t\tTofuImplementation:     tfimpl.Unknown,\n\t\tProviderCacheOptions:   pcoptions.ProviderCacheOptions{RegistryNames: pcoptions.DefaultRegistryNames},\n\t\tFeatureFlags:           xsync.NewMapOf[string, string](),\n\t\tErrors:                 defaultErrorsConfig(),\n\t\tStrictControls:         controls.New(),\n\t\tExperiments:            experiment.NewExperiments(),\n\t\tTips:                   tips.NewTips(),\n\t\tTelemetry:              new(telemetry.Options),\n\t\tEngineOptions:          new(engine.EngineOptions),\n\t\tVersionManagerFileName: defaultVersionManagerFileName,\n\t}\n}\n\nfunc NewTerragruntOptionsWithConfigPath(terragruntConfigPath string) (*TerragruntOptions, error) {\n\topts := NewTerragruntOptions()\n\n\t// Ensure config path is absolute so downstream code can rely on it.\n\t// Skip resolution for empty paths (sentinel meaning \"not set\").\n\tif terragruntConfigPath != \"\" {\n\t\tif !filepath.IsAbs(terragruntConfigPath) {\n\t\t\tabsPath, err := filepath.Abs(terragruntConfigPath)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, errors.New(err)\n\t\t\t}\n\n\t\t\tterragruntConfigPath = absPath\n\t\t}\n\n\t\tterragruntConfigPath = filepath.Clean(terragruntConfigPath)\n\t}\n\n\topts.TerragruntConfigPath = terragruntConfigPath\n\n\tworkingDir, downloadDir := util.DefaultWorkingAndDownloadDirs(terragruntConfigPath)\n\n\topts.WorkingDir = workingDir\n\topts.RootWorkingDir = workingDir\n\topts.DownloadDir = downloadDir\n\n\treturn opts, nil\n}\n\n// GetDefaultIAMAssumeRoleSessionName gets the default IAM assume role session name.\nfunc GetDefaultIAMAssumeRoleSessionName() string {\n\treturn fmt.Sprintf(\"terragrunt-%d\", time.Now().UTC().UnixNano())\n}\n\n// NewTerragruntOptionsForTest creates a new TerragruntOptions object with reasonable defaults for test usage.\nfunc NewTerragruntOptionsForTest(terragruntConfigPath string, options ...TerragruntOptionsFunc) (*TerragruntOptions, error) {\n\tformatter := format.NewFormatter(format.NewKeyValueFormatPlaceholders())\n\tformatter.SetDisabledColors(true)\n\n\topts, err := NewTerragruntOptionsWithConfigPath(terragruntConfigPath)\n\tif err != nil {\n\t\tlog.WithOptions(log.WithLevel(log.DebugLevel), log.WithFormatter(formatter)).Errorf(\"%v\\n\", errors.New(err))\n\n\t\treturn nil, err\n\t}\n\n\topts.NonInteractive = true\n\n\tfor _, opt := range options {\n\t\topt(opts)\n\t}\n\n\treturn opts, nil\n}\n\n// OptionsFromContext tries to retrieve options from context, otherwise, returns its own instance.\nfunc (opts *TerragruntOptions) OptionsFromContext(ctx context.Context) *TerragruntOptions {\n\tif val := ctx.Value(ContextKey); val != nil {\n\t\tif opts, ok := val.(*TerragruntOptions); ok {\n\t\t\treturn opts\n\t\t}\n\t}\n\n\treturn opts\n}\n\n// Clone performs a deep copy of `opts` with shadow copies of: interfaces, and funcs.\n// Fields with \"clone\" tags can override this behavior.\nfunc (opts *TerragruntOptions) Clone() *TerragruntOptions {\n\tnewOpts := cloner.Clone(opts)\n\n\treturn newOpts\n}\n\n// CloneWithConfigPath creates a copy of this TerragruntOptions, but with different values for the given variables. This is useful for\n// creating a TerragruntOptions that behaves the same way, but is used for a Terraform module in a different folder.\n//\n// It also adjusts the given logger, as each cloned option has to use a working directory specific logger to enrich\n// log output correctly.\nfunc (opts *TerragruntOptions) CloneWithConfigPath(l log.Logger, configPath string) (log.Logger, *TerragruntOptions, error) {\n\tnewOpts := opts.Clone()\n\n\t// Ensure configPath is absolute and normalized for consistent path handling\n\tconfigPath = filepath.Clean(configPath)\n\tif !filepath.IsAbs(configPath) {\n\t\tconfigPath = filepath.Clean(filepath.Join(opts.WorkingDir, configPath))\n\t}\n\n\tworkingDir := filepath.Dir(configPath)\n\n\t// Only update logger field if the working directory actually changed\n\t// This preserves any custom display path (e.g., relative path) set on the logger\n\tif workingDir != opts.WorkingDir {\n\t\tl = l.WithField(placeholders.WorkDirKeyName, workingDir)\n\t}\n\n\tnewOpts.TerragruntConfigPath = configPath\n\tnewOpts.WorkingDir = workingDir\n\n\treturn l, newOpts, nil\n}\n\n// InsertTerraformCliArgs inserts the given argsToInsert after the terraform command argument, but before the remaining args.\n// Uses IacArgs parsing to properly distinguish flags from arguments.\nfunc (opts *TerragruntOptions) InsertTerraformCliArgs(argsToInsert ...string) {\n\t// Ensure TerraformCliArgs is initialized. This allows callers to use\n\t// Insert/AppendTerraformCliArgs without pre-initializing the struct,\n\t// which is common when building options incrementally or in tests.\n\tif opts.TerraformCliArgs == nil {\n\t\topts.TerraformCliArgs = iacargs.New()\n\t}\n\n\t// Parse args using IacArgs to properly separate flags from arguments\n\tparsed := iacargs.New(argsToInsert...)\n\n\t// Insert flags at beginning\n\topts.TerraformCliArgs.InsertFlag(0, parsed.Flags...)\n\n\t// Merge command and subcommands from parsed args\n\topts.mergeCommandAndSubCommand(parsed)\n\n\t// Arguments: insert at the beginning\n\topts.TerraformCliArgs.InsertArguments(0, parsed.Arguments...)\n}\n\n// mergeCommandAndSubCommand handles command and subcommand merging during arg insertion.\n// Command rules:\n//   - If opts has no command, use parsed.Command\n//   - If parsed.Command matches opts command, do nothing\n//   - If parsed.Command is a known subcommand, add to SubCommand\n//   - Otherwise treat parsed.Command as positional argument\n//\n// SubCommand rules:\n//   - If parsed has explicit subcommands, use them (last writer wins)\n//   - Otherwise keep any subcommand set during command merging\nfunc (opts *TerragruntOptions) mergeCommandAndSubCommand(parsed *iacargs.IacArgs) {\n\t// Handle command field\n\tswitch {\n\tcase opts.TerraformCliArgs.Command == \"\":\n\t\topts.TerraformCliArgs.Command = parsed.Command\n\tcase parsed.Command == \"\" || parsed.Command == opts.TerraformCliArgs.Command:\n\t\t// no-op\n\tcase iacargs.IsKnownSubCommand(parsed.Command):\n\t\topts.TerraformCliArgs.SubCommand = []string{parsed.Command}\n\tdefault:\n\t\topts.TerraformCliArgs.InsertArguments(0, parsed.Command)\n\t}\n\n\t// Explicit subcommands in parsed take precedence\n\tif len(parsed.SubCommand) > 0 {\n\t\topts.TerraformCliArgs.SubCommand = parsed.SubCommand\n\t}\n}\n\n// AppendTerraformCliArgs appends the given argsToAppend after the current TerraformCliArgs.\n// Uses IacArgs parsing to properly distinguish flags from arguments.\nfunc (opts *TerragruntOptions) AppendTerraformCliArgs(argsToAppend ...string) {\n\t// Ensure TerraformCliArgs is initialized. This allows callers to use\n\t// Insert/AppendTerraformCliArgs without pre-initializing the struct,\n\t// which is common when building options incrementally or in tests.\n\tif opts.TerraformCliArgs == nil {\n\t\topts.TerraformCliArgs = iacargs.New()\n\t}\n\n\t// Parse args using IacArgs to properly separate flags from arguments\n\tparsed := iacargs.New(argsToAppend...)\n\n\topts.TerraformCliArgs.AppendFlag(parsed.Flags...)\n\n\t// Handle parsed.Command as an argument (extra_arguments don't have a command)\n\tif parsed.Command != \"\" {\n\t\topts.TerraformCliArgs.AppendArgument(parsed.Command)\n\t}\n\n\topts.TerraformCliArgs.AppendArgument(parsed.Arguments...)\n\n\t// Replace subcommand if provided\n\tif len(parsed.SubCommand) > 0 {\n\t\topts.TerraformCliArgs.SubCommand = parsed.SubCommand\n\t}\n}\n\n// TerraformDataDir returns Terraform data directory (.terraform by default, overridden by $TF_DATA_DIR envvar)\nfunc (opts *TerragruntOptions) TerraformDataDir() string {\n\tif tfDataDir, ok := opts.Env[\"TF_DATA_DIR\"]; ok {\n\t\treturn tfDataDir\n\t}\n\n\treturn DefaultTFDataDir\n}\n\n// DataDir returns the Terraform data directory prepended with the working directory path,\n// or just the Terraform data directory if it is an absolute path.\nfunc (opts *TerragruntOptions) DataDir() string {\n\ttfDataDir := opts.TerraformDataDir()\n\tif filepath.IsAbs(tfDataDir) {\n\t\treturn tfDataDir\n\t}\n\n\treturn filepath.Join(opts.WorkingDir, tfDataDir)\n}\n\n// identifyDefaultWrappedExecutable returns default path used for wrapped executable.\nfunc identifyDefaultWrappedExecutable(ctx context.Context) string {\n\tif util.IsCommandExecutable(ctx, TofuDefaultPath, \"-version\") {\n\t\treturn TofuDefaultPath\n\t}\n\t// fallback to Terraform if tofu is not available\n\treturn TerraformDefaultPath\n}\n\n// RunWithErrorHandling runs the given operation and handles any errors according to the configuration.\nfunc (opts *TerragruntOptions) RunWithErrorHandling(\n\tctx context.Context,\n\tl log.Logger,\n\tr *report.Report,\n\toperation func() error,\n) error {\n\tif opts.Errors == nil {\n\t\treturn operation()\n\t}\n\n\tcurrentAttempt := 1\n\n\t// Convert working dir to a clean, absolute path for reporting.\n\t// Use directory of original config path (pre-cache location) to ensure\n\t// report runs match those created by the runner pool.\n\treportWorkingDir := opts.WorkingDir\n\tif opts.OriginalTerragruntConfigPath != \"\" {\n\t\treportWorkingDir = filepath.Dir(opts.OriginalTerragruntConfigPath)\n\t}\n\n\treportDir := filepath.Clean(reportWorkingDir)\n\n\tfor {\n\t\terr := operation()\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\t// Process the error through our error handling configuration\n\t\taction, recoveryErr := opts.Errors.AttemptErrorRecovery(l, err, currentAttempt)\n\t\tif recoveryErr != nil {\n\t\t\tvar maxAttemptsReachedError *errorconfig.MaxAttemptsReachedError\n\t\t\tif errors.As(recoveryErr, &maxAttemptsReachedError) {\n\t\t\t\treturn maxAttemptsReachedError\n\t\t\t}\n\n\t\t\treturn fmt.Errorf(\"encountered error while attempting error recovery: %w\", recoveryErr)\n\t\t}\n\n\t\tif action == nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif action.ShouldIgnore {\n\t\t\tl.Warnf(\"Ignoring error, reason: %s\", action.IgnoreMessage)\n\n\t\t\t// Handle ignore signals if any are configured\n\t\t\tif len(action.IgnoreSignals) > 0 {\n\t\t\t\tif err := opts.handleIgnoreSignals(l, action.IgnoreSignals); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\n\t\t\trun, err := r.EnsureRun(l, reportDir)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := r.EndRun(\n\t\t\t\tl,\n\t\t\t\trun.Path,\n\t\t\t\treport.WithResult(report.ResultSucceeded),\n\t\t\t\treport.WithReason(report.ReasonErrorIgnored),\n\t\t\t\treport.WithCauseIgnoreBlock(action.IgnoreBlockName),\n\t\t\t); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\n\t\tif action.ShouldRetry {\n\t\t\t// Respect --no-auto-retry flag\n\t\t\tif !opts.AutoRetry {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tl.Warnf(\n\t\t\t\t\"Encountered retryable error: %s\\nAttempt %d of %d. Waiting %d second(s) before retrying...\",\n\t\t\t\taction.RetryBlockName,\n\t\t\t\tcurrentAttempt,\n\t\t\t\taction.RetryAttempts,\n\t\t\t\taction.RetrySleepSecs,\n\t\t\t)\n\n\t\t\t// Record that a retry will be attempted without prematurely marking success.\n\t\t\trun, err := r.EnsureRun(l, reportDir)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err := r.EndRun(\n\t\t\t\tl,\n\t\t\t\trun.Path,\n\t\t\t\treport.WithResult(report.ResultSucceeded),\n\t\t\t\treport.WithReason(report.ReasonRetrySucceeded),\n\t\t\t\treport.WithCauseRetryBlock(action.RetryBlockName),\n\t\t\t); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// Sleep before retry\n\t\t\tselect {\n\t\t\tcase <-time.After(time.Duration(action.RetrySleepSecs) * time.Second):\n\t\t\t\t// try again\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn errors.New(ctx.Err())\n\t\t\t}\n\n\t\t\tcurrentAttempt++\n\n\t\t\tcontinue\n\t\t}\n\n\t\treturn err\n\t}\n}\n\nfunc (opts *TerragruntOptions) handleIgnoreSignals(l log.Logger, signals map[string]any) error {\n\tworkingDir := opts.WorkingDir\n\tsignalsFile := filepath.Join(workingDir, DefaultSignalsFile)\n\n\tsignalsJSON, err := json.MarshalIndent(signals, \"\", \"  \")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tconst ownerPerms = 0644\n\n\tl.Warnf(\"Writing error signals to %s\", signalsFile)\n\n\tif err := os.WriteFile(signalsFile, signalsJSON, ownerPerms); err != nil {\n\t\treturn fmt.Errorf(\"failed to write signals file %s: %w\", signalsFile, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/options/options_test.go",
    "content": "package options_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/iacargs\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestInsertTerraformCliArgsSubcommandReplacement(t *testing.T) {\n\tt.Parallel()\n\n\ttc := []struct {\n\t\tname     string\n\t\tinitial  []string\n\t\tinsert   []string\n\t\texpected []string\n\t}{\n\t\t{\n\t\t\tname:     \"replace_lock_with_mirror\",\n\t\t\tinitial:  []string{\"providers\", \"lock\", \"-platform=linux_amd64\"},\n\t\t\tinsert:   []string{\"providers\", \"mirror\"},\n\t\t\texpected: []string{\"providers\", \"mirror\", \"-platform=linux_amd64\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"no_replacement_if_no_subcommand\",\n\t\t\tinitial:  []string{\"apply\", \"-auto-approve\"},\n\t\t\tinsert:   []string{\"-var\", \"foo=bar\"},\n\t\t\texpected: []string{\"apply\", \"-var\", \"foo=bar\", \"-auto-approve\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"append_new_subcommand\",\n\t\t\tinitial:  []string{\"state\"},\n\t\t\tinsert:   []string{\"list\"},\n\t\t\texpected: []string{\"state\", \"list\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"same_command_no_change\",\n\t\t\tinitial:  []string{\"apply\", \"-auto-approve\"},\n\t\t\tinsert:   []string{\"apply\"},\n\t\t\texpected: []string{\"apply\", \"-auto-approve\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"unknown_command_becomes_argument\",\n\t\t\tinitial:  []string{\"apply\", \"-auto-approve\"},\n\t\t\tinsert:   []string{\"myplan.tfplan\"},\n\t\t\texpected: []string{\"apply\", \"-auto-approve\", \"myplan.tfplan\"},\n\t\t},\n\t\t{\n\t\t\tname:     \"empty_insert_no_change\",\n\t\t\tinitial:  []string{\"plan\", \"-out=plan.tfplan\"},\n\t\t\tinsert:   []string{},\n\t\t\texpected: []string{\"plan\", \"-out=plan.tfplan\"},\n\t\t},\n\t}\n\n\tfor _, tt := range tc {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\topts := &options.TerragruntOptions{\n\t\t\t\tTerraformCliArgs: iacargs.New(tt.initial...),\n\t\t\t}\n\t\t\topts.InsertTerraformCliArgs(tt.insert...)\n\t\t\tassert.Equal(t, tt.expected, opts.TerraformCliArgs.Slice())\n\t\t})\n\t}\n}\n\nfunc TestInsertTerraformCliArgsNilGuard(t *testing.T) {\n\tt.Parallel()\n\n\topts := &options.TerragruntOptions{}\n\t// Should not panic\n\topts.InsertTerraformCliArgs(\"plan\")\n\tassert.Equal(t, []string{\"plan\"}, opts.TerraformCliArgs.Slice())\n}\n"
  },
  {
    "path": "pkg/pkg.go",
    "content": "// Package pkg is a collection of common libraries that are used across the application.\n//\n// The purpose of this package `/pkg` is a developing a new common libraries which can be moved to `go-commons` in the future.\n// This approach helps us test new functionality well on a single application first and after that to safely merged with rest common libraries in `go-commons`\npackage pkg\n"
  },
  {
    "path": "test/benchmarks/.gitignore",
    "content": "*.test\n"
  },
  {
    "path": "test/benchmarks/helpers/helpers.go",
    "content": "// Package helpers provides helper functions for the integration benchmarks.\npackage helpers\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\t// DefaultDirPermissions specifies the default file mode for creating directories.\n\t// rwxr-xr-x (owner can read, write, execute; group and others can read, execute)\n\tDefaultDirPermissions = 0755\n\t// DefaultFilePermissions specifies the default file mode for creating files.\n\t// rw-r--r-- (owner can read, write; group and others can read)\n\tDefaultFilePermissions = 0644\n)\n\n// RunTerragruntCommand runs a Terragrunt command and logs the output to io.Discard.\nfunc RunTerragruntCommand(b *testing.B, args ...string) {\n\tb.Helper()\n\n\twriter := io.Discard\n\terrwriter := io.Discard\n\n\topts := options.NewTerragruntOptionsWithWriters(writer, errwriter)\n\n\tl := logger.CreateLogger().WithOptions(log.WithOutput(io.Discard))\n\n\tapp := cli.NewApp(l, opts)\n\n\tctx := log.ContextWithLogger(b.Context(), l)\n\n\terr := app.RunContext(ctx, args)\n\trequire.NoError(b, err)\n}\n\n// GenerateNUnits generates n units in the given temporary directory.\nfunc GenerateNUnits(b *testing.B, dir string, n int, tgConfig string, tfConfig string) {\n\tb.Helper()\n\n\tfor i := range n {\n\t\tunitDir := filepath.Join(dir, \"unit-\"+strconv.Itoa(i))\n\t\trequire.NoError(b, os.MkdirAll(unitDir, DefaultDirPermissions))\n\n\t\t// Create an empty `terragrunt.hcl` file\n\t\tunitTerragruntConfigPath := filepath.Join(unitDir, \"terragrunt.hcl\")\n\t\trequire.NoError(b, os.WriteFile(unitTerragruntConfigPath, []byte(tgConfig), DefaultFilePermissions))\n\n\t\t// Create an empty `main.tf` file\n\t\tunitMainTfPath := filepath.Join(unitDir, \"main.tf\")\n\t\trequire.NoError(b, os.WriteFile(unitMainTfPath, []byte(tfConfig), DefaultFilePermissions))\n\t}\n}\n\n// GenerateEmptyUnits generates n empty units in the given temporary directory.\nfunc GenerateEmptyUnits(b *testing.B, dir string, n int) {\n\tb.Helper()\n\n\temptyRootConfig := ``\n\tincludeRootConfig := `include \"root\" {\n\t\tpath = find_in_parent_folders(\"root.hcl\")\n}\n`\n\temptyMainTf := ``\n\n\trootTerragruntConfigPath := filepath.Join(dir, \"root.hcl\")\n\n\t// Create an empty `root.hcl` file\n\trequire.NoError(b, os.WriteFile(rootTerragruntConfigPath, []byte(emptyRootConfig), DefaultFilePermissions))\n\n\t// Generate n units\n\tGenerateNUnits(b, dir, n, includeRootConfig, emptyMainTf)\n}\n\nfunc Init(b *testing.B, dir string) {\n\tb.Helper()\n\n\t// Measure plan time\n\tplanStart := time.Now()\n\n\tRunTerragruntCommand(b, \"terragrunt\", \"run\", \"--all\", \"init\", \"--non-interactive\", \"--working-dir\", dir)\n\n\tplanDuration := time.Since(planStart)\n\n\tb.ReportMetric(float64(planDuration.Seconds()), \"init_s/op\")\n}\n\nfunc Plan(b *testing.B, dir string) {\n\tb.Helper()\n\n\t// Measure plan time\n\tplanStart := time.Now()\n\n\tRunTerragruntCommand(b, \"terragrunt\", \"run\", \"--all\", \"plan\", \"--non-interactive\", \"--working-dir\", dir)\n\n\tplanDuration := time.Since(planStart)\n\n\tb.ReportMetric(float64(planDuration.Seconds()), \"plan_s/op\")\n}\n\nfunc Apply(b *testing.B, dir string) {\n\tb.Helper()\n\n\t// Track apply time\n\tapplyStart := time.Now()\n\n\tRunTerragruntCommand(b, \"terragrunt\", \"run\", \"--all\", \"apply\", \"--non-interactive\", \"--working-dir\", dir)\n\n\tapplyDuration := time.Since(applyStart)\n\n\tb.ReportMetric(float64(applyDuration.Seconds()), \"apply_s/op\")\n}\n\nfunc ApplyWithRunnerPool(b *testing.B, dir string) {\n\tb.Helper()\n\n\t// Track apply time\n\tapplyStart := time.Now()\n\n\tRunTerragruntCommand(b, \"terragrunt\", \"run\", \"--all\", \"apply\", \"--non-interactive\", \"--experiment\", \"runner-pool\", \"--working-dir\", dir)\n\n\tapplyDuration := time.Since(applyStart)\n\n\tb.ReportMetric(float64(applyDuration.Seconds()), \"apply_s/op\")\n}\n"
  },
  {
    "path": "test/benchmarks/integration_auto_provider_cache_dir_bench_test.go",
    "content": "package test_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/benchmarks/helpers\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// BenchmarkAutoProviderCacheDirInit benchmarks Terragrunt init with and without auto provider cache dir enabled\nfunc BenchmarkAutoProviderCacheDirInit(b *testing.B) {\n\tsetup := func(tmpDir string) {\n\t\tfixtureSource := filepath.Join(\"..\", \"fixtures\", \"auto-provider-cache-dir\", \"heavy\", \"unit\")\n\t\tterragruntConfigPath := filepath.Join(tmpDir, \"terragrunt.hcl\")\n\t\tmainTfPath := filepath.Join(tmpDir, \"main.tf\")\n\n\t\toriginalTerragruntConfig, err := os.ReadFile(filepath.Join(fixtureSource, \"terragrunt.hcl\"))\n\t\trequire.NoError(b, err)\n\n\t\toriginalMainTf, err := os.ReadFile(filepath.Join(fixtureSource, \"main.tf\"))\n\t\trequire.NoError(b, err)\n\n\t\trequire.NoError(b, os.WriteFile(terragruntConfigPath, originalTerragruntConfig, helpers.DefaultFilePermissions))\n\t\trequire.NoError(b, os.WriteFile(mainTfPath, originalMainTf, helpers.DefaultFilePermissions))\n\n\t\thelpers.RunTerragruntCommand(\n\t\t\tb,\n\t\t\t\"terragrunt\",\n\t\t\t\"init\",\n\t\t\t\"--non-interactive\",\n\t\t\t\"--working-dir\",\n\t\t\ttmpDir,\n\t\t)\n\t}\n\n\tb.Run(\"init without auto provider cache dir\", func(b *testing.B) {\n\t\ttmpDir := b.TempDir()\n\n\t\tsetup(tmpDir)\n\n\t\tb.ResetTimer()\n\n\t\tfor b.Loop() {\n\t\t\thelpers.RunTerragruntCommand(\n\t\t\t\tb,\n\t\t\t\t\"terragrunt\",\n\t\t\t\t\"init\",\n\t\t\t\t\"--source-update\",\n\t\t\t\t\"--non-interactive\",\n\t\t\t\t\"--working-dir\", tmpDir)\n\t\t}\n\n\t\tb.StopTimer()\n\t})\n\n\tb.Run(\"init with auto provider cache dir\", func(b *testing.B) {\n\t\ttmpDir := b.TempDir()\n\n\t\tsetup(tmpDir)\n\n\t\tb.ResetTimer()\n\n\t\tfor b.Loop() {\n\t\t\thelpers.RunTerragruntCommand(\n\t\t\t\tb,\n\t\t\t\t\"terragrunt\",\n\t\t\t\t\"init\",\n\t\t\t\t\"--experiment\", \"auto-provider-cache-dir\",\n\t\t\t\t\"--source-update\",\n\t\t\t\t\"--non-interactive\",\n\t\t\t\t\"--working-dir\",\n\t\t\t\ttmpDir)\n\t\t}\n\n\t\tb.StopTimer()\n\t})\n}\n\n// BenchmarkProviderCachingComparison benchmarks Terragrunt init with many units\n// comparing no caching, provider cache server, and auto provider cache dir experiment.\nfunc BenchmarkProviderCachingComparison(b *testing.B) {\n\tsetup := func(tmpDir string, count int) {\n\t\tfixtureSource := filepath.Join(\"..\", \"fixtures\", \"auto-provider-cache-dir\", \"heavy\", \"unit\")\n\t\toriginalTerragruntConfig, err := os.ReadFile(filepath.Join(fixtureSource, \"terragrunt.hcl\"))\n\t\trequire.NoError(b, err)\n\t\toriginalMainTf, err := os.ReadFile(filepath.Join(fixtureSource, \"main.tf\"))\n\t\trequire.NoError(b, err)\n\n\t\t// Generate units with the provider configuration\n\t\tfor i := range count {\n\t\t\tunitDir := filepath.Join(tmpDir, \"unit-\"+strconv.Itoa(i))\n\t\t\trequire.NoError(b, os.MkdirAll(unitDir, helpers.DefaultDirPermissions))\n\n\t\t\tunitTerragruntConfigPath := filepath.Join(unitDir, \"terragrunt.hcl\")\n\t\t\tunitMainTfPath := filepath.Join(unitDir, \"main.tf\")\n\n\t\t\trequire.NoError(b, os.WriteFile(unitTerragruntConfigPath, originalTerragruntConfig, helpers.DefaultFilePermissions))\n\t\t\trequire.NoError(b, os.WriteFile(unitMainTfPath, originalMainTf, helpers.DefaultFilePermissions))\n\t\t}\n\n\t\t// Run initial init to avoid noise from the first iteration being slower\n\t\thelpers.RunTerragruntCommand(\n\t\t\tb,\n\t\t\t\"terragrunt\",\n\t\t\t\"run\",\n\t\t\t\"--all\",\n\t\t\t\"init\",\n\t\t\t\"--non-interactive\",\n\t\t\t\"--working-dir\",\n\t\t\ttmpDir,\n\t\t)\n\t}\n\n\tcounts := []int{\n\t\t1,\n\t\t2,\n\t\t4,\n\t\t8,\n\t\t16,\n\t}\n\n\tcacheTypes := []struct {\n\t\tname string\n\t\targs []string\n\t}{\n\t\t{\n\t\t\tname: \"no provider caching\",\n\t\t\targs: []string{},\n\t\t},\n\t\t{\n\t\t\tname: \"with provider cache server\",\n\t\t\targs: []string{\"--provider-cache\"},\n\t\t},\n\t\t{\n\t\t\tname: \"with auto provider cache dir\",\n\t\t\targs: []string{\"--experiment\", \"auto-provider-cache-dir\"},\n\t\t},\n\t}\n\n\tfor _, count := range counts {\n\t\tfor _, cacheType := range cacheTypes {\n\t\t\tname := strconv.Itoa(count) + \" units \" + cacheType.name\n\n\t\t\tb.Run(name, func(b *testing.B) {\n\t\t\t\ttmpDir := b.TempDir()\n\n\t\t\t\tsetup(tmpDir, count)\n\n\t\t\t\targs := make([]string, 0, 8+len(cacheType.args))\n\t\t\t\targs = append(args,\n\t\t\t\t\t\"terragrunt\",\n\t\t\t\t\t\"run\",\n\t\t\t\t\t\"--all\",\n\t\t\t\t\t\"init\",\n\t\t\t\t\t\"--source-update\",\n\t\t\t\t\t\"--non-interactive\",\n\t\t\t\t\t\"--working-dir\",\n\t\t\t\t\ttmpDir,\n\t\t\t\t)\n\n\t\t\t\targs = append(args, cacheType.args...)\n\n\t\t\t\tb.ResetTimer()\n\n\t\t\t\tfor b.Loop() {\n\t\t\t\t\thelpers.RunTerragruntCommand(\n\t\t\t\t\t\tb,\n\t\t\t\t\t\targs...,\n\t\t\t\t\t)\n\t\t\t\t}\n\n\t\t\t\tb.StopTimer()\n\t\t\t})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "test/benchmarks/integration_bench_test.go",
    "content": "package test_test\n\nimport (\n\t\"fmt\"\n\t\"math/rand\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/benchmarks/helpers\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc BenchmarkEmptyTerragruntInit(b *testing.B) {\n\temptyMainTf := ``\n\n\temptyRootConfig := ``\n\tincludeRootConfig := `include \"root\" {\n\t\tpath = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n\tsource = \".\"\n}\n`\n\n\t// Create a temporary directory for the test\n\ttmpDir := b.TempDir()\n\trootTerragruntConfigPath := filepath.Join(tmpDir, \"root.hcl\")\n\t// Create an empty `root.hcl` file\n\trequire.NoError(b, os.WriteFile(rootTerragruntConfigPath, []byte(emptyRootConfig), helpers.DefaultFilePermissions))\n\n\t// Create 1 units\n\thelpers.GenerateNUnits(b, tmpDir, 1, includeRootConfig, emptyMainTf)\n\n\t// Do an initial init to avoid noise from the first iteration being slower\n\thelpers.Init(b, tmpDir)\n\n\tb.Run(\"1 units\", func(b *testing.B) {\n\t\tfor b.Loop() {\n\t\t\thelpers.Init(b, tmpDir)\n\t\t}\n\t})\n}\n\nfunc BenchmarkTwoEmptyTerragruntInits(b *testing.B) {\n\temptyMainTf := ``\n\n\temptyRootConfig := ``\n\tincludeRootConfig := `include \"root\" {\n\t\tpath = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n\tsource = \".\"\n}\n`\n\n\ttmpDir := b.TempDir()\n\n\trootTerragruntConfigPath := filepath.Join(tmpDir, \"root.hcl\")\n\trequire.NoError(b, os.WriteFile(rootTerragruntConfigPath, []byte(emptyRootConfig), helpers.DefaultFilePermissions))\n\n\thelpers.GenerateNUnits(b, tmpDir, 2, includeRootConfig, emptyMainTf)\n\n\t// Do an initial init to avoid noise from the first iteration being slower\n\thelpers.Init(b, tmpDir)\n\n\tb.Run(\"2 units\", func(b *testing.B) {\n\t\tfor b.Loop() {\n\t\t\thelpers.Init(b, tmpDir)\n\t\t}\n\t})\n}\n\nfunc BenchmarkManyEmptyTerragruntInits(b *testing.B) {\n\temptyMainTf := ``\n\n\temptyRootConfig := ``\n\tincludeRootConfig := `include \"root\" {\n\t\tpath = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n\tsource = \".\"\n}\n`\n\n\ttmpDir := b.TempDir()\n\n\trootTerragruntConfigPath := filepath.Join(tmpDir, \"root.hcl\")\n\trequire.NoError(b, os.WriteFile(rootTerragruntConfigPath, []byte(emptyRootConfig), helpers.DefaultFilePermissions))\n\n\thelpers.GenerateNUnits(b, tmpDir, 1000, includeRootConfig, emptyMainTf)\n\n\t// Do an initial init to avoid noise from the first iteration being slower\n\thelpers.Init(b, tmpDir)\n\n\tb.Run(\"1000 units\", func(b *testing.B) {\n\t\tfor b.Loop() {\n\t\t\thelpers.Init(b, tmpDir)\n\t\t}\n\t})\n}\n\nfunc BenchmarkEmptyTerragruntPlan(b *testing.B) {\n\temptyMainTf := ``\n\n\temptyRootConfig := ``\n\tincludeRootConfig := `include \"root\" {\n\t\tpath = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n\tsource = \".\"\n}\n`\n\n\t// Create a temporary directory for the test\n\ttmpDir := b.TempDir()\n\trootTerragruntConfigPath := filepath.Join(tmpDir, \"root.hcl\")\n\t// Create an empty `root.hcl` file\n\trequire.NoError(b, os.WriteFile(rootTerragruntConfigPath, []byte(emptyRootConfig), helpers.DefaultFilePermissions))\n\n\t// Create 1 units\n\thelpers.GenerateNUnits(b, tmpDir, 1, includeRootConfig, emptyMainTf)\n\n\thelpers.Init(b, tmpDir)\n\n\tb.Run(\"1 units\", func(b *testing.B) {\n\t\tfor b.Loop() {\n\t\t\thelpers.Plan(b, tmpDir)\n\t\t}\n\t})\n}\n\nfunc BenchmarkTwoEmptyTerragruntPlans(b *testing.B) {\n\temptyMainTf := ``\n\n\temptyRootConfig := ``\n\tincludeRootConfig := `include \"root\" {\n\t\tpath = find_in_parent_folders(\"root.hcl\")\n\t}\n\n\tterraform {\n\t\tsource = \".\"\n\t}\n`\n\n\ttmpDir := b.TempDir()\n\n\trootTerragruntConfigPath := filepath.Join(tmpDir, \"root.hcl\")\n\trequire.NoError(b, os.WriteFile(rootTerragruntConfigPath, []byte(emptyRootConfig), helpers.DefaultFilePermissions))\n\n\thelpers.GenerateNUnits(b, tmpDir, 2, includeRootConfig, emptyMainTf)\n\n\thelpers.Init(b, tmpDir)\n\n\tb.Run(\"2 units\", func(b *testing.B) {\n\t\tfor b.Loop() {\n\t\t\thelpers.Plan(b, tmpDir)\n\t\t}\n\t})\n}\n\nfunc BenchmarkManyEmptyTerragruntPlans(b *testing.B) {\n\temptyMainTf := ``\n\n\temptyRootConfig := ``\n\tincludeRootConfig := `include \"root\" {\n\t\tpath = find_in_parent_folders(\"root.hcl\")\n\t}\n\n\tterraform {\n\t\tsource = \".\"\n\t}\n`\n\n\ttmpDir := b.TempDir()\n\trootTerragruntConfigPath := filepath.Join(tmpDir, \"root.hcl\")\n\trequire.NoError(b, os.WriteFile(rootTerragruntConfigPath, []byte(emptyRootConfig), helpers.DefaultFilePermissions))\n\n\thelpers.GenerateNUnits(b, tmpDir, 1000, includeRootConfig, emptyMainTf)\n\n\thelpers.Init(b, tmpDir)\n\n\tb.Run(\"1000 units\", func(b *testing.B) {\n\t\tfor b.Loop() {\n\t\t\thelpers.Plan(b, tmpDir)\n\t\t}\n\t})\n}\n\nfunc BenchmarkUnitsNoDependencies(b *testing.B) {\n\tbaseMainTf := `resource \"null_resource\" \"test\" {\n  triggers = {\n    timestamp = timestamp()\n  }\n}`\n\n\temptyRootConfig := ``\n\tincludeRootConfig := `include \"root\" {\n        path = find_in_parent_folders(\"root.hcl\")\n}\nterraform {\n    source = \".\"\n}\n`\n\n\ttmpDir := b.TempDir()\n\trootTerragruntConfigPath := filepath.Join(tmpDir, \"root.hcl\")\n\trequire.NoError(b, os.WriteFile(rootTerragruntConfigPath, []byte(emptyRootConfig), helpers.DefaultFilePermissions))\n\n\thelpers.GenerateNUnits(b, tmpDir, 10, includeRootConfig, baseMainTf)\n\n\thelpers.Init(b, tmpDir)\n\n\tb.Run(\"default_runner\", func(b *testing.B) {\n\t\t// Warmups (not measured)\n\t\twarmupApplies(b, tmpDir, false, 2)\n\t\tb.ResetTimer()\n\n\t\tfor b.Loop() {\n\t\t\thelpers.Apply(b, tmpDir)\n\t\t}\n\t})\n\n\tb.Run(\"runner_pool\", func(b *testing.B) {\n\t\t// Warmups (not measured)\n\t\twarmupApplies(b, tmpDir, true, 2)\n\t\tb.ResetTimer()\n\n\t\tfor b.Loop() {\n\t\t\thelpers.ApplyWithRunnerPool(b, tmpDir)\n\t\t}\n\t})\n}\n\nfunc BenchmarkUnitsNoDependenciesRandomWait(b *testing.B) {\n\temptyRootConfig := ``\n\tincludeRootConfig := `include \"root\" {\n        path = find_in_parent_folders(\"root.hcl\")\n}\nterraform {\n    source = \".\"\n}\n`\n\n\ttmpDir := b.TempDir()\n\trootTerragruntConfigPath := filepath.Join(tmpDir, \"root.hcl\")\n\trequire.NoError(b, os.WriteFile(rootTerragruntConfigPath, []byte(emptyRootConfig), helpers.DefaultFilePermissions))\n\n\t// Generate independent units with random 100-300ms waits\n\tfor i := 0; i < 10; i++ {\n\t\tunitDir := filepath.Join(tmpDir, fmt.Sprintf(\"unit-%d\", i))\n\t\trequire.NoError(b, os.MkdirAll(unitDir, helpers.DefaultDirPermissions))\n\n\t\ttgPath := filepath.Join(unitDir, \"terragrunt.hcl\")\n\t\trequire.NoError(b, os.WriteFile(tgPath, []byte(includeRootConfig), helpers.DefaultFilePermissions))\n\n\t\tms := 100 + rand.Intn(201) // 100..300 ms\n\t\tsecs := float64(ms) / 1000.0\n\t\tmainTf := fmt.Sprintf(`resource \"null_resource\" \"wait\" {\n  provisioner \"local-exec\" {\n    command = \"bash -c 'sleep %.3f'\"\n  }\n  triggers = {\n    timestamp = timestamp()\n  }\n}\n`, secs)\n\t\ttfPath := filepath.Join(unitDir, \"main.tf\")\n\t\trequire.NoError(b, os.WriteFile(tfPath, []byte(mainTf), helpers.DefaultFilePermissions))\n\t}\n\n\thelpers.Init(b, tmpDir)\n\n\tb.Run(\"default_runner\", func(b *testing.B) {\n\t\t// Warmups (not measured)\n\t\twarmupApplies(b, tmpDir, false, 2)\n\t\tb.ResetTimer()\n\n\t\tfor b.Loop() {\n\t\t\thelpers.Apply(b, tmpDir)\n\t\t}\n\t})\n\n\tb.Run(\"runner_pool\", func(b *testing.B) {\n\t\t// Warmups (not measured)\n\t\twarmupApplies(b, tmpDir, true, 2)\n\t\tb.ResetTimer()\n\n\t\tfor b.Loop() {\n\t\t\thelpers.ApplyWithRunnerPool(b, tmpDir)\n\t\t}\n\t})\n}\n\nfunc BenchmarkUnitsOneDependencyWithWait(b *testing.B) {\n\tbaseMainTf := `resource \"null_resource\" \"test\" {\n  triggers = {\n    timestamp = timestamp()\n  }\n}`\n\n\temptyRootConfig := ``\n\tincludeRootConfig := `include \"root\" {\n        path = find_in_parent_folders(\"root.hcl\")\n}\nterraform {\n    source = \".\"\n}\n`\n\n\ttmpDir := b.TempDir()\n\trootTerragruntConfigPath := filepath.Join(tmpDir, \"root.hcl\")\n\trequire.NoError(b, os.WriteFile(rootTerragruntConfigPath, []byte(emptyRootConfig), helpers.DefaultFilePermissions))\n\n\t// Create units\n\tfor i := 0; i < 10; i++ {\n\t\tunitDir := filepath.Join(tmpDir, fmt.Sprintf(\"unit-%d\", i))\n\t\trequire.NoError(b, os.MkdirAll(unitDir, helpers.DefaultDirPermissions))\n\n\t\t// terragrunt.hcl\n\t\tvar tgConfig string\n\t\tif i == 2 {\n\t\t\t// unit-2 depends on unit-1\n\t\t\ttgConfig = `include \"root\" {\n        path = find_in_parent_folders(\"root.hcl\")\n}\nterraform {\n    source = \".\"\n}\ndependencies {\n    paths = [\"../unit-1\"]\n}`\n\t\t} else {\n\t\t\ttgConfig = includeRootConfig\n\t\t}\n\n\t\ttgPath := filepath.Join(unitDir, \"terragrunt.hcl\")\n\t\trequire.NoError(b, os.WriteFile(tgPath, []byte(tgConfig), helpers.DefaultFilePermissions))\n\n\t\t// main.tf\n\t\tvar tfConfig string\n\t\tif i == 1 {\n\t\t\t// unit-1 has 400ms wait\n\t\t\ttfConfig = `resource \"null_resource\" \"wait\" {\n  provisioner \"local-exec\" {\n    command = \"bash -c 'sleep 0.4'\"\n  }\n  triggers = {\n    timestamp = timestamp()\n  }\n}`\n\t\t} else {\n\t\t\ttfConfig = baseMainTf\n\t\t}\n\n\t\ttfPath := filepath.Join(unitDir, \"main.tf\")\n\t\trequire.NoError(b, os.WriteFile(tfPath, []byte(tfConfig), helpers.DefaultFilePermissions))\n\t}\n\n\thelpers.Init(b, tmpDir)\n\n\tb.Run(\"default_runner\", func(b *testing.B) {\n\t\t// Warmups (not measured)\n\t\twarmupApplies(b, tmpDir, false, 2)\n\t\tb.ResetTimer()\n\n\t\tfor b.Loop() {\n\t\t\thelpers.Apply(b, tmpDir)\n\t\t}\n\t})\n\n\tb.Run(\"runner_pool\", func(b *testing.B) {\n\t\t// Warmups (not measured)\n\t\twarmupApplies(b, tmpDir, true, 2)\n\t\tb.ResetTimer()\n\n\t\tfor b.Loop() {\n\t\t\thelpers.ApplyWithRunnerPool(b, tmpDir)\n\t\t}\n\t})\n}\n\n// BenchmarkDependencyPairwiseOddDependsOnPrevEvenRandomWait generates N units (50, 100) where:\n// - Every odd-indexed unit depends on the previous even-indexed unit (e.g., 1->0, 3->2, ...)\n// - Even-indexed units perform a random sleep via local-exec to simulate workload (50..100ms)\n// - Odd-indexed units are no-ops but depend on their paired even unit\n// It measures apply times for both the default runner (configstack) and the runner pool on the SAME stack.\nfunc BenchmarkDependencyPairwiseOddDependsOnPrevEvenRandomWait(b *testing.B) {\n\t// Sizes for parameterized benchmark (2,4,8,...,128)\n\tsizes := []int{2, 4, 8, 16, 32, 64, 128}\n\n\temptyRootConfig := ``\n\tincludeRootConfig := `include \"root\" {\n\t\tpath = find_in_parent_folders(\"root.hcl\")\n}\nterraform {\n\tsource = \".\"\n}\n`\n\n\tfor _, n := range sizes {\n\t\tb.Run(fmt.Sprintf(\"%d_units\", n), func(b *testing.B) {\n\t\t\t// Generate a single stack used by both runners\n\t\t\tdir := b.TempDir()\n\n\t\t\t// Write root.hcl\n\t\t\trequire.NoError(b, os.WriteFile(filepath.Join(dir, \"root.hcl\"), []byte(emptyRootConfig), helpers.DefaultFilePermissions))\n\n\t\t\t// Seed random generator deterministically within this sub-benchmark\n\t\t\trnd := rand.New(rand.NewSource(int64(n)))\n\n\t\t\t// Generate units where every odd depends on every even\n\t\t\tfor i := 0; i < n; i++ {\n\t\t\t\tunitDir := filepath.Join(dir, fmt.Sprintf(\"unit-%d\", i))\n\t\t\t\trequire.NoError(b, os.MkdirAll(unitDir, helpers.DefaultDirPermissions))\n\n\t\t\t\t// terragrunt.hcl: odd units depend only on the previous even unit (i-1)\n\t\t\t\tvar tgConfig string\n\n\t\t\t\tif i%2 == 1 {\n\t\t\t\t\tprev := i - 1\n\t\t\t\t\tif prev >= 0 {\n\t\t\t\t\t\tdepBlock := fmt.Sprintf(\"dependency \\\"unit_%d\\\" {\\n  config_path = \\\"../unit-%d\\\"\\n}\\n\\n\", prev, prev)\n\t\t\t\t\t\ttgConfig = includeRootConfig + depBlock\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttgConfig = includeRootConfig\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\ttgConfig = includeRootConfig\n\t\t\t\t}\n\n\t\t\t\ttgPath := filepath.Join(unitDir, \"terragrunt.hcl\")\n\t\t\t\trequire.NoError(b, os.WriteFile(tgPath, []byte(tgConfig), helpers.DefaultFilePermissions))\n\n\t\t\t\t// main.tf: even units wait 50..100ms; odd units are no-ops\n\t\t\t\tvar mainTf string\n\n\t\t\t\tif i%2 == 0 { // even: random sleep\n\t\t\t\t\tms := 50 + rnd.Intn(51) // 50..100ms\n\t\t\t\t\tsecs := float64(ms) / 1000.0\n\t\t\t\t\tmainTf = fmt.Sprintf(`resource \"null_resource\" \"wait\" {\n  provisioner \"local-exec\" {\n    command = \"bash -c 'sleep %.3f'\"\n  }\n  triggers = {\n    timestamp = timestamp()\n  }\n}\n`, secs)\n\t\t\t\t} else { // odd: noop\n\t\t\t\t\tmainTf = `resource \"null_resource\" \"noop\" {\n  triggers = {\n    timestamp = timestamp()\n  }\n}\n`\n\t\t\t\t}\n\n\t\t\t\ttfPath := filepath.Join(unitDir, \"main.tf\")\n\t\t\t\trequire.NoError(b, os.WriteFile(tfPath, []byte(mainTf), helpers.DefaultFilePermissions))\n\t\t\t}\n\n\t\t\t// Init once to prepare\n\t\t\thelpers.Init(b, dir)\n\n\t\t\tb.Run(\"configstack\", func(b *testing.B) {\n\t\t\t\t// Warmups (not measured)\n\t\t\t\twarmupApplies(b, dir, false, 2)\n\t\t\t\tb.ResetTimer()\n\n\t\t\t\tfor b.Loop() {\n\t\t\t\t\thelpers.Apply(b, dir)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tb.Run(\"runner_pool\", func(b *testing.B) {\n\t\t\t\t// Warmups (not measured)\n\t\t\t\twarmupApplies(b, dir, true, 2)\n\t\t\t\tb.ResetTimer()\n\n\t\t\t\tfor b.Loop() {\n\t\t\t\t\thelpers.ApplyWithRunnerPool(b, dir)\n\t\t\t\t}\n\t\t\t})\n\t\t})\n\t}\n}\n\n// warmupApplies performs a number of unmeasured apply runs to warm caches and workers.\nfunc warmupApplies(b *testing.B, tmpDir string, useRunnerPool bool, count int) {\n\tb.Helper()\n\n\tfor range make([]struct{}, count) {\n\t\tif useRunnerPool {\n\t\t\thelpers.ApplyWithRunnerPool(b, tmpDir)\n\t\t} else {\n\t\t\thelpers.Apply(b, tmpDir)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "test/benchmarks/integration_cas_bench_test.go",
    "content": "package test_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/benchmarks/helpers\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// BenchmarkCASInit benchmarks Terragrunt init with remote source with and without CAS enabled\nfunc BenchmarkCASInit(b *testing.B) {\n\tsetup := func(tmpDir string) {\n\t\t// Copy the remote fixture content to our test directory\n\t\tremoteFixtureSource := filepath.Join(\"..\", \"fixtures\", \"download\", \"remote\")\n\t\tremoteTerragruntConfigPath := filepath.Join(tmpDir, \"terragrunt.hcl\")\n\n\t\t// Read the original terragrunt.hcl from the remote fixture\n\t\toriginalConfig, err := os.ReadFile(filepath.Join(remoteFixtureSource, \"terragrunt.hcl\"))\n\t\trequire.NoError(b, err)\n\n\t\t// Write the config to our test directory\n\t\trequire.NoError(b, os.WriteFile(remoteTerragruntConfigPath, originalConfig, helpers.DefaultFilePermissions))\n\n\t\t// Run initial init to avoid noise from the first iteration being slower\n\t\thelpers.RunTerragruntCommand(\n\t\t\tb,\n\t\t\t\"terragrunt\",\n\t\t\t\"init\",\n\t\t\t\"--experiment\", \"cas\",\n\t\t\t\"--non-interactive\",\n\t\t\t\"--provider-cache\",\n\t\t\t\"--working-dir\",\n\t\t\ttmpDir,\n\t\t)\n\t}\n\n\tb.Run(\"remote init without CAS\", func(b *testing.B) {\n\t\ttmpDir := b.TempDir()\n\n\t\tsetup(tmpDir)\n\n\t\tb.ResetTimer()\n\n\t\tfor b.Loop() {\n\t\t\thelpers.RunTerragruntCommand(\n\t\t\t\tb,\n\t\t\t\t\"terragrunt\",\n\t\t\t\t\"init\",\n\t\t\t\t\"--non-interactive\",\n\t\t\t\t\"--provider-cache\",\n\t\t\t\t\"--source-update\",\n\t\t\t\t\"--working-dir\", tmpDir)\n\t\t}\n\n\t\tb.StopTimer()\n\t})\n\n\tb.Run(\"remote init with CAS\", func(b *testing.B) {\n\t\ttmpDir := b.TempDir()\n\n\t\tsetup(tmpDir)\n\n\t\tb.ResetTimer()\n\n\t\tfor b.Loop() {\n\t\t\thelpers.RunTerragruntCommand(\n\t\t\t\tb,\n\t\t\t\t\"terragrunt\",\n\t\t\t\t\"init\",\n\t\t\t\t\"--experiment\", \"cas\",\n\t\t\t\t\"--non-interactive\",\n\t\t\t\t\"--provider-cache\",\n\t\t\t\t\"--source-update\",\n\t\t\t\t\"--working-dir\",\n\t\t\t\ttmpDir)\n\t\t}\n\n\t\tb.StopTimer()\n\t})\n}\n\n// BenchmarkCASWithManyUnits benchmarks Terragrunt init with many remote units with and without CAS enabled\nfunc BenchmarkCASWithManyUnits(b *testing.B) {\n\tsetup := func(tmpDir string, count int) {\n\t\tremoteFixtureSource := filepath.Join(\"..\", \"fixtures\", \"download\", \"remote\")\n\t\toriginalConfig, err := os.ReadFile(filepath.Join(remoteFixtureSource, \"terragrunt.hcl\"))\n\t\trequire.NoError(b, err)\n\n\t\t// Generate units with the remote configuration\n\t\tfor i := range count {\n\t\t\tunitDir := filepath.Join(tmpDir, \"unit-\"+strconv.Itoa(i))\n\t\t\trequire.NoError(b, os.MkdirAll(unitDir, helpers.DefaultDirPermissions))\n\n\t\t\tunitTerragruntConfigPath := filepath.Join(unitDir, \"terragrunt.hcl\")\n\t\t\trequire.NoError(b, os.WriteFile(unitTerragruntConfigPath, originalConfig, helpers.DefaultFilePermissions))\n\t\t}\n\n\t\t// Run initial init to avoid noise from the first iteration being slower\n\t\thelpers.RunTerragruntCommand(\n\t\t\tb,\n\t\t\t\"terragrunt\",\n\t\t\t\"run\",\n\t\t\t\"--all\",\n\t\t\t\"init\",\n\t\t\t\"--non-interactive\",\n\t\t\t\"--provider-cache\",\n\t\t\t\"--source-update\",\n\t\t\t\"--working-dir\",\n\t\t\ttmpDir,\n\t\t)\n\t}\n\n\tcounts := []int{\n\t\t1,\n\t\t2,\n\t\t4,\n\t\t8,\n\t\t16,\n\t\t32,\n\t\t64,\n\t\t128,\n\t}\n\n\tfor _, count := range counts {\n\t\tfor _, cas := range []bool{false, true} {\n\t\t\tname := strconv.Itoa(count) + \" remote units \" + (func() string {\n\t\t\t\tif cas {\n\t\t\t\t\treturn \"with CAS\"\n\t\t\t\t}\n\n\t\t\t\treturn \"without CAS\"\n\t\t\t})()\n\n\t\t\tb.Run(name, func(b *testing.B) {\n\t\t\t\ttmpDir := b.TempDir()\n\n\t\t\t\tsetup(tmpDir, count)\n\n\t\t\t\targs := []string{\n\t\t\t\t\t\"terragrunt\",\n\t\t\t\t\t\"run\",\n\t\t\t\t\t\"--all\",\n\t\t\t\t\t\"init\",\n\t\t\t\t\t\"--non-interactive\",\n\t\t\t\t\t\"--provider-cache\",\n\t\t\t\t\t\"--source-update\",\n\t\t\t\t\t\"--working-dir\",\n\t\t\t\t\ttmpDir,\n\t\t\t\t}\n\n\t\t\t\tif cas {\n\t\t\t\t\targs = append(args, \"--experiment\", \"cas\")\n\t\t\t\t}\n\n\t\t\t\tb.ResetTimer()\n\n\t\t\t\tfor b.Loop() {\n\t\t\t\t\thelpers.RunTerragruntCommand(\n\t\t\t\t\t\tb,\n\t\t\t\t\t\targs...,\n\t\t\t\t\t)\n\t\t\t\t}\n\n\t\t\t\tb.StopTimer()\n\t\t\t})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "test/cliconfig.go",
    "content": "// Package test provides integration tests for Terragrunt.\npackage test\n\nimport (\n\t\"html/template\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar testCLIConfigTemplate = `\n{{ if or (gt (len .FilesystemMirrorMethods) 0) (gt (len .NetworkMirrorMethods) 0) (gt (len .DirectMethods) 0) }}\nprovider_installation {\n{{ if gt (len .FilesystemMirrorMethods) 0 }}{{ range $method := .FilesystemMirrorMethods }}\n  filesystem_mirror {\n    path    = \"{{ $method.Path }}\"\n{{ if gt (len $method.Include) 0 }}\n    include = [{{ range $index, $include := $method.Include }}{{ if $index }},{{ end }}\"{{ $include }}\"{{ end }}]\n{{ end }}{{ if gt (len $method.Exclude) 0 }}\n    exclude = [{{ range $index, $exclude := $method.Exclude }}{{ if $index }},{{ end }}\"{{ $exclude }}\"{{ end }}]\n{{ end }}\n  }\n{{ end }}{{ end }}\n{{ if gt (len .NetworkMirrorMethods) 0 }}{{ range $method := .NetworkMirrorMethods }}\n  network_mirror {\n    url    = \"{{ $method.URL }}\"\n{{ if gt (len $method.Include) 0 }}\n    include = [{{ range $index, $include := $method.Include }}{{ if $index }},{{ end }}\"{{ $include }}\"{{ end }}]\n{{ end }}{{ if gt (len $method.Exclude) 0 }}\n    exclude = [{{ range $index, $exclude := $method.Exclude }}{{ if $index }},{{ end }}\"{{ $exclude }}\"{{ end }}]\n{{ end }}\n  }\n{{ end }}{{ end }}\n{{ if gt (len .DirectMethods) 0 }}{{ range $method := .DirectMethods }}\n  direct {\n{{ if gt (len $method.Include) 0 }}\n    include = [{{ range $index, $include := $method.Include }}{{ if $index }},{{ end }}\"{{ $include }}\"{{ end }}]\n{{ end }}{{ if gt (len $method.Exclude) 0 }}\n    exclude = [{{ range $index, $exclude := $method.Exclude }}{{ if $index }},{{ end }}\"{{ $exclude }}\"{{ end }}]\n{{ end }}\n  }\n{{ end }}{{ end }}\n}\n{{ end }}\n`\n\ntype CLIConfigProviderInstallationFilesystemMirror struct {\n\tPath             string\n\tInclude, Exclude []string\n}\n\ntype CLIConfigProviderInstallationNetworkMirror struct {\n\tURL              string\n\tInclude, Exclude []string\n}\n\ntype CLIConfigProviderInstallationDirect struct {\n\tInclude, Exclude []string\n}\n\ntype CLIConfigSettings struct {\n\tFilesystemMirrorMethods []CLIConfigProviderInstallationFilesystemMirror\n\tNetworkMirrorMethods    []CLIConfigProviderInstallationNetworkMirror\n\tDirectMethods           []CLIConfigProviderInstallationDirect\n}\n\nfunc CreateCLIConfig(t *testing.T, file *os.File, settings *CLIConfigSettings) {\n\tt.Helper()\n\n\ttmp, err := template.New(\"cliconfig\").Parse(testCLIConfigTemplate)\n\trequire.NoError(t, err)\n\n\terr = tmp.Execute(file, settings)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "test/fixtures/assume-role/duration/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/assume-role/duration/main.tf",
    "content": "resource \"local_file\" \"test_file\" {\n  content  = \"test_file\"\n  filename = \"${path.module}/test_file.txt\"\n}\n\n"
  },
  {
    "path": "test/fixtures/assume-role/duration/terragrunt.hcl",
    "content": "remote_state {\n  backend  = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n  config = {\n    bucket         = \"__FILL_IN_BUCKET_NAME__\"\n    key            = \"${path_relative_to_include()}/terraform.tfstate\"\n    region         = \"us-west-2\"\n    encrypt        = true\n    assume_role    = {\n      role_arn     = \"__FILL_IN_ASSUME_ROLE__\"\n      duration     = \"1h\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/assume-role/external-id/terragrunt.hcl",
    "content": "remote_state {\n  backend  = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n  config = {\n    bucket         = \"__FILL_IN_BUCKET_NAME__\"\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n    key            = \"${path_relative_to_include()}/terraform.tfstate\"\n    region         = \"us-west-2\"\n    encrypt        = true\n    assume_role    = {\n      role_arn     = get_aws_caller_identity_arn()\n      external_id  = \"external_id_123\"\n      session_name = \"session_name_example\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/assume-role/external-id-with-comma/terragrunt.hcl",
    "content": "remote_state {\n  backend  = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n  config = {\n    bucket         = \"__FILL_IN_BUCKET_NAME__\"\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n    key            = \"${path_relative_to_include()}/terraform.tfstate\"\n    region         = \"us-west-2\"\n    encrypt        = true\n    assume_role    = {\n      role_arn     = get_aws_caller_identity_arn()\n      external_id  = \"external_id_123,external_id_456\"\n      session_name = \"session_name_example\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/assume-role-web-identity/file-path/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/assume-role-web-identity/file-path/main.tf",
    "content": "resource \"local_file\" \"test_file\" {\n  content  = \"test_file\"\n  filename = \"${path.module}/test_file.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/assume-role-web-identity/file-path/terragrunt.hcl",
    "content": "iam_role               = \"__FILL_IN_ASSUME_ROLE__\"\niam_web_identity_token = \"__FILL_IN_IDENTITY_TOKEN_FILE_PATH__\"\n\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n  config = {\n    bucket  = \"__FILL_IN_BUCKET_NAME__\"\n    key     = \"${path_relative_to_include()}/terraform.tfstate\"\n    region  = \"__FILL_IN_REGION__\"\n    encrypt = true\n  }\n}\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/creds-for-dependency/dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/creds-for-dependency/dependency/creds.config",
    "content": "access_key_id=__FILL_AWS_ACCESS_KEY_ID__\nsecret_access_key=__FILL_AWS_SECRET_ACCESS_KEY__\nsession_token=\ntf_var_foo=\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/creds-for-dependency/dependency/main.tf",
    "content": "output \"foo\" {\n  value = \"foo\"\n}\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/creds-for-dependency/dependency/terragrunt.hcl",
    "content": "locals {\n    aws_account_id = get_aws_account_id()\n}\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/creds-for-dependency/dependent/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/creds-for-dependency/dependent/creds.config",
    "content": "access_key_id=__FILL_AWS_ACCESS_KEY_ID__\nsecret_access_key=__FILL_AWS_SECRET_ACCESS_KEY__\nsession_token=\ntf_var_foo=\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/creds-for-dependency/dependent/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/auth-provider-cmd/creds-for-dependency/dependent/terragrunt.hcl",
    "content": "dependency \"dependency\" {\n  config_path  = \"../dependency\"\n}\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/mock-auth-cmd.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n. \"${PWD}/creds.config\"\n\njson_string=$(jq -n \\\n\t--arg access_key_id \"$access_key_id\" \\\n\t--arg secret_access_key \"$secret_access_key\" \\\n\t--arg session_token \"$session_token\" \\\n\t--arg tf_var_foo \"$tf_var_foo\" \\\n\t'{awsCredentials: {ACCESS_KEY_ID: $access_key_id, SECRET_ACCESS_KEY: $secret_access_key, SESSION_TOKEN: $session_token}, envs: {TF_VAR_foo: $tf_var_foo}}')\n\nprintf '%s\\n' \"$json_string\"\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/multiple-apps/app1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/multiple-apps/app1/creds.config",
    "content": "access_key_id=app1_access_key_id\nsecret_access_key=app1_secret_access_key\nsession_token=app1_session_token\ntf_var_foo=app1-bar\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/multiple-apps/app1/main.tf",
    "content": "variable \"foo\" {}\n\nvariable \"foo-app2\" {}\n\nvariable \"foo-app3\" {}\n\noutput \"foo-app1\" {\n  value = var.foo\n}\n\noutput \"foo-app2\" {\n  value = var.foo-app2\n}\n\noutput \"foo-app3\" {\n  value = var.foo-app3\n}\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/multiple-apps/app1/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"app2\" {\n  config_path  = \"../app2\"\n}\n\ninputs = {\n  foo-app2 = dependency.app2.outputs.foo-app2\n  foo-app3 = dependency.app2.outputs.foo-app3\n}\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/multiple-apps/app2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/multiple-apps/app2/creds.config",
    "content": "access_key_id=app2_access_key_id\nsecret_access_key=app2_secret_access_key\nsession_token=app2_session_token\ntf_var_foo=app2-bar\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/multiple-apps/app2/main.tf",
    "content": "variable \"foo\" {}\n\nvariable \"foo-app3\" {}\n\noutput \"foo-app2\" {\n  value = var.foo\n}\n\noutput \"foo-app3\" {\n  value = var.foo-app3\n}\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/multiple-apps/app2/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"app3\" {\n  config_path  = \"../app3\"\n}\n\ninputs = {\n  foo-app3 = dependency.app3.outputs.foo-app3\n}\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/multiple-apps/app3/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/multiple-apps/app3/creds.config",
    "content": "access_key_id=app3_access_key_id\nsecret_access_key=app3_secret_access_key\nsession_token=app3_session_token\ntf_var_foo=app3-bar\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/multiple-apps/app3/main.tf",
    "content": "variable \"foo\" {}\n\noutput \"foo-app3\" {\n  value = var.foo\n}\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/multiple-apps/app3/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/multiple-apps/creds.config",
    "content": "access_key_id=app1_access_key_id\nsecret_access_key=app1_secret_access_key\nsession_token=app1_session_token\ntf_var_foo=top-level\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/multiple-apps/root.hcl",
    "content": "terraform {\n  before_hook \"before_hook\" {\n    commands = [\"init\"]\n    execute  = [\"./test-creds.sh\", get_terragrunt_dir()]\n    working_dir = dirname(find_in_parent_folders(\"root.hcl\"))\n  }\n}\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/multiple-apps/test-creds.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n# Use argument if provided, otherwise fallback to PWD\nCONFIG_DIR=\"${1:-${PWD}}\"\n\n. \"${CONFIG_DIR}/creds.config\"\n\nif [[ \"$access_key_id\" != \"$AWS_ACCESS_KEY_ID\" ]]; then\n    exit 1\nfi\n\nif [[ \"$secret_access_key\" != \"$AWS_SECRET_ACCESS_KEY\" ]]; then\n    exit 1\nfi\n\nif [[ \"$session_token\" != \"$AWS_SESSION_TOKEN\" ]]; then\n    exit 1\nfi\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/oidc/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/oidc/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/auth-provider-cmd/oidc/mock-auth-cmd.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n: \"${AWS_TEST_OIDC_ROLE_ARN:?The AWS_TEST_OIDC_ROLE_ARN environment variable must be set.}\"\n: \"${OIDC_TOKEN:?The OIDC_TOKEN environment variable must be set.}\"\n\njq -n \\\n\t--arg role \"$AWS_TEST_OIDC_ROLE_ARN\" \\\n\t--arg token \"$OIDC_TOKEN\" \\\n\t'{awsRole: {roleARN: $role, webIdentityToken: $token}}'\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/oidc/terragrunt.hcl",
    "content": "locals {\n    account_id = get_aws_account_id()\n}\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/remote-state/creds.config",
    "content": "access_key_id=__FILL_AWS_ACCESS_KEY_ID__\nsecret_access_key=__FILL_AWS_SECRET_ACCESS_KEY__\nsession_token=\ntf_var_foo=\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/remote-state/terragrunt.hcl",
    "content": "locals {\n  aws_account_id = \"${get_aws_account_id()}\"\n}\n\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend-${get_aws_account_id()}.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n  config = {\n    bucket  = \"__FILL_IN_BUCKET_NAME__\"\n    key     = \"${path_relative_to_include()}/terraform.tfstate\"\n    region  = \"__FILL_IN_REGION__\"\n    encrypt = true\n  }\n}\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/remote-state-w-oidc/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/remote-state-w-oidc/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/auth-provider-cmd/remote-state-w-oidc/mock-auth-cmd.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\n: \"${AWS_TEST_OIDC_ROLE_ARN:?The AWS_TEST_OIDC_ROLE_ARN environment variable must be set.}\"\n: \"${OIDC_TOKEN:?The OIDC_TOKEN environment variable must be set.}\"\n\njq -n \\\n\t--arg role \"$AWS_TEST_OIDC_ROLE_ARN\" \\\n\t--arg token \"$OIDC_TOKEN\" \\\n\t'{awsRole: {roleARN: $role, webIdentityToken: $token}}'\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/remote-state-w-oidc/terragrunt.hcl",
    "content": "remote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend-${get_aws_account_id()}.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n  config = {\n    bucket  = \"__FILL_IN_BUCKET_NAME__\"\n    key     = \"${path_relative_to_include()}/tofu.tfstate\"\n    region  = \"__FILL_IN_REGION__\"\n    encrypt = true\n  }\n}\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/sops/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/sops/creds.config",
    "content": "access_key_id=__FILL_AWS_ACCESS_KEY_ID__\nsecret_access_key=__FILL_AWS_SECRET_ACCESS_KEY__\nsession_token=\ntf_var_foo=\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/sops/main.tf",
    "content": "variable \"hello\" {}\n\noutput \"hello\" {\n  value = var.hello\n}\n\n"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/sops/secrets.json",
    "content": "{\n\t\"data\": \"ENC[AES256_GCM,data:RPZ0W610wwmpQ//ETxX8aTJG2Uyfv0ArESrP3+TsqIBwKZyyZhzEg0Ae3cVIrb2OmmKIUxI5Kg7wZy3Gxw1/dzgbgkOX08eVHTZp2heVewWIOf5l0ehdMJgNAFCVM16PaK/qaUIgWeDf6F3YZZVQziVvGOaDwK+my7F6BthhbOYYBHj5cz+RxkGUeLg2sayQMrD1QGIxXFAAuZbdISBhgOI0NbEI+h1kXJPnaMHjqWVOnsD7PWJ5qb58K9YjXXtX1LecX+1oULTkDqC9/KDeg8VFYYO5lsuiKScx0PHtoYDClAuDpC4nVw==,iv:REDUyVWQZSjRXhOLEzvApMa6prEFp2G+EWNXBVLTqpo=,tag:yWitF/oplh3y4H+SJA+iBQ==,type:str]\",\n\t\"sops\": {\n\t\t\"kms\": [\n\t\t\t{\n\t\t\t\t\"arn\": \"arn:aws:kms:us-east-1:654954254241:key/7a8b0c4e-ff3c-49d0-93ba-15e3ca3488fb\",\n\t\t\t\t\"created_at\": \"2025-07-24T15:58:53Z\",\n\t\t\t\t\"enc\": \"AQICAHhHPy++UbYHYaSeo34uZaMsxPhT+PDk7Hd1dYS/NZi4YQGYBq1EZVemmWINY2hMZL16AAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMoEWoiy4snYZ9BlEwAgEQgDt6QJiGDC3S+xdLQ5O4AetAD16vQrHfMqamy7mdmh1aFrJJkyC1U7wph/bbnaFFkGDi4VNYcjyd+yMpzg==\",\n\t\t\t\t\"aws_profile\": \"\"\n\t\t\t}\n\t\t],\n\t\t\"gcp_kms\": null,\n\t\t\"azure_kv\": null,\n\t\t\"hc_vault\": null,\n\t\t\"age\": null,\n\t\t\"lastmodified\": \"2025-07-24T15:58:53Z\",\n\t\t\"mac\": \"ENC[AES256_GCM,data:Qotj1yITGEy3tdMXB5dlAvZAW84X3WgVQKkfg58NgSvolqcdM75w362fr3fIBMNLBZO0Su/OZNII2AYxs005qdPp8/uF+OUmxk7S//N9n/UDtpS/YrSQBMvkfsdUa3qbt8RtxBmqpdTxBSssj1kmSYy9bUS/DsCEH3FzACuDhVs=,iv:Od6dO7M93D0ERh4uqVqVFPqNvWd1M6PYM2gEPFMQCfs=,tag:MbujA94Qr+P/LMUdWJlmuw==,type:str]\",\n\t\t\"pgp\": null,\n\t\t\"unencrypted_suffix\": \"_unencrypted\",\n\t\t\"version\": \"3.9.0\"\n\t}\n}"
  },
  {
    "path": "test/fixtures/auth-provider-cmd/sops/terragrunt.hcl",
    "content": "locals {\n  data = jsondecode(jsondecode(sops_decrypt_file(\"secrets.json\")).data)\n}\n\ninputs = {\n  hello = local.data.hello\n}\n\n"
  },
  {
    "path": "test/fixtures/auth-provider-parallel/auth-provider.sh",
    "content": "#!/usr/bin/env bash\n# Mock auth provider that uses file-based coordination to verify parallel execution\n\nset -e\n\n# Get the directory where this script is located\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nLOCK_DIR=\"${SCRIPT_DIR}/.auth-locks\"\nmkdir -p \"$LOCK_DIR\"\n\n# Get a unique ID for this invocation\n# Use POSIX-compatible timestamp (seconds) + PID + RANDOM to ensure uniqueness\n# This works on Linux, macOS, and BSD without requiring nanosecond precision\nINVOCATION_ID=\"auth-$$-$(date +%s)-$RANDOM\"\n\n# Create a lock file to indicate we've started\n# This acts as a synchronization point - by the time the file is created,\n# the parent process should be ready to capture our stderr output.\ntouch \"${LOCK_DIR}/start-${INVOCATION_ID}\"\n\n# Log to stderr so it shows up in terragrunt output\n# Note: Output after lock file creation to avoid macOS stderr buffering race condition\necho \"Auth start ${INVOCATION_ID}\" >&2\n\n# Wait for other auth commands to also start (up to 500ms)\n# This ensures we test the parallel execution scenario\nWAIT_COUNT=0\nMAX_WAIT=50  # 50 * 10ms = 500ms max wait\n\nwhile [[ $WAIT_COUNT -lt $MAX_WAIT ]]; do\n    # Count how many auth commands have started\n    STARTED=$(ls -1 \"${LOCK_DIR}\"/start-* 2>/dev/null | wc -l | tr -d ' \\t')\n\n    # If we see at least 2 others started (3 total), we know it's parallel\n    if [[ \"$STARTED\" -ge 2 ]]; then\n        echo \"Auth concurrent ${INVOCATION_ID} detected=$STARTED\" >&2\n        break\n    fi\n\n    # Sleep a bit and check again\n    sleep 0.01\n    WAIT_COUNT=$((WAIT_COUNT + 1))\ndone\n\n# Simulate some auth work\nsleep 0.1\n\n# Return fake credentials as JSON\ncat <<EOF\n{\n  \"envs\": {\n    \"AWS_ACCESS_KEY_ID\": \"fake-access-key\",\n    \"AWS_SECRET_ACCESS_KEY\": \"fake-secret-key\",\n    \"AWS_SESSION_TOKEN\": \"fake-session-token\"\n  }\n}\nEOF\n\n# Create completion marker\ntouch \"${LOCK_DIR}/end-${INVOCATION_ID}\"\n\necho \"Auth end ${INVOCATION_ID}\" >&2\n"
  },
  {
    "path": "test/fixtures/auth-provider-parallel/unit-a/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/auth-provider-parallel/unit-a/main.tf",
    "content": "output \"unit_name\" {\n  value = \"unit-a\"\n}\n"
  },
  {
    "path": "test/fixtures/auth-provider-parallel/unit-a/terragrunt.hcl",
    "content": "# Unit A - no dependencies\n"
  },
  {
    "path": "test/fixtures/auth-provider-parallel/unit-b/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/auth-provider-parallel/unit-b/main.tf",
    "content": "output \"unit_name\" {\n  value = \"unit-b\"\n}\n"
  },
  {
    "path": "test/fixtures/auth-provider-parallel/unit-b/terragrunt.hcl",
    "content": "# Unit B - no dependencies\n"
  },
  {
    "path": "test/fixtures/auth-provider-parallel/unit-c/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/auth-provider-parallel/unit-c/main.tf",
    "content": "output \"unit_name\" {\n  value = \"unit-c\"\n}\n"
  },
  {
    "path": "test/fixtures/auth-provider-parallel/unit-c/terragrunt.hcl",
    "content": "# Unit C - no dependencies\n"
  },
  {
    "path": "test/fixtures/auto-provider-cache-dir/basic/unit/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.0\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/auto-provider-cache-dir/basic/unit/main.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/auto-provider-cache-dir/basic/unit/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/auto-provider-cache-dir/heavy/unit/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/datadog/datadog\" {\n  version     = \"3.85.0\"\n  constraints = \"~> 3.0\"\n  hashes = [\n    \"h1:9+yctZTXhDrCeUUARit7sxuQHliz4fngPPcIxQaRZQ4=\",\n    \"h1:hP5Jjzzn1v8eYEm8UAiCOqbzMzpnwuyHu9fUKIRdMCI=\",\n    \"h1:syzI/3cBghgn0ymfH4iKn6sEJz6pzJL2M/ZFhV+AvmU=\",\n    \"zh:05812358f73ffa6a598814eef2b2ceacdba68e7ec02bfa1b8c865882cb61d713\",\n    \"zh:06c1bd121347969a891e6d5ab5940601f296fff97ec0231cc96dba3b547e6159\",\n    \"zh:10a3fd82798a54295662aedd20653ba4fc98c24bdf1cc89cd8428b103e384091\",\n    \"zh:1ef821bd12dedd0b3f107f21c4b2f41061508a9e8262ef9b0985f1f84b3b60a9\",\n    \"zh:2af26dfeccf8ca23d999dffb208601bd8afefea7a085ac9ee51021ec2df9ef20\",\n    \"zh:2c05e48ab1676f1b70f0e5fc58c1e14c85b845040839be2c96fc56a25d7ede48\",\n    \"zh:38d032954ac922d1e8dbbc86597c9ca8acadb4c7354f3a4be98c181985cfc51c\",\n    \"zh:6d6114c9a9146dc5da0bedf9b6c7b68e90d38f3b0c545486dbc910fcc02117c1\",\n    \"zh:90b3fffb13f9bc96d11cce17b5224055367696eca09dc4a4fa7ff142208809b8\",\n    \"zh:a2f114e7c896ae853df98182e6d338ce7eaaba21dc2d08b9a2c65b24c41d2888\",\n    \"zh:a8e9585a12ca7be57065f817e39d0c001b005f2c1752edd1e20e297c17b5f6d4\",\n    \"zh:da690c516eec9cd3a50c71d1bec572e2bb47bc0d39331e68f296872701330d00\",\n    \"zh:dc54a703c759dec57e42fa9bdc1510a1f81f04b99101a05649a549f457f08111\",\n    \"zh:e8ac4b03359f91b0247847389fb9e2dfffdc033f16c465d4afa86e71a523d703\",\n  ]\n}\n\nprovider \"registry.opentofu.org/hashicorp/aws\" {\n  version     = \"6.28.0\"\n  constraints = \"~> 6.0\"\n  hashes = [\n    \"h1:i0G7vt2sNy0oz84IiuG4gplonNVyOLRdprKLurU8pe4=\",\n    \"h1:tcau98fkhZ2RhbPHo8LdiiUk2RGpZUgT/t06sdMLids=\",\n    \"h1:wek8vEEZpTPulbLi9xCf2wnxvc97JXAN4qcOhduSg7k=\",\n    \"zh:38d58305206953783c150fb96d5c4f3ea5fe0b9e0987d927c884a6b0f2adf7a9\",\n    \"zh:43fd483251165f98b7a44360b41b437d309b007ef2bfff818eedcf3730e3f5cb\",\n    \"zh:4753decc5a718cb74b08244a02d00c150f0ddd6ebf2e1227f6a985c647c03ce9\",\n    \"zh:5956525650554bd3fbc4b695eb5250193f0ebf94c45862a7730457ab6a315069\",\n    \"zh:76d98fa1146750c01f607bae4421952ee9cd14ed3a4a59deb7136749adb9e0ae\",\n    \"zh:792c29e5ec91356baddb6219ac7f6f1df09c251cbe4ab6e089fc25d64270b22a\",\n    \"zh:856424380caa7c1536dc00515d12beac2693db1a8425da654eed5530abeb17d9\",\n    \"zh:e8982ec2bc692efa7236e3565e7094a09f52c5b71d8860a570a36fb31a40f27f\",\n    \"zh:f5e7ff825dc3f7356fb80936bfe7bb1b54a728ccf429cb753cfe590932f0403b\",\n  ]\n}\n\nprovider \"registry.opentofu.org/hashicorp/azurerm\" {\n  version     = \"4.58.0\"\n  constraints = \"~> 4.0\"\n  hashes = [\n    \"h1:BkBNt+rpKjPIr+DNswIe9eplWRDNso0ycLuG+lbIe+A=\",\n    \"h1:DXEaadoG38C45w4xShoMGY8zGnGrh8YGVZFWf8B27fA=\",\n    \"h1:IYvqMRiiYlBvwelEeOo6zKE0bo7jja9fbj620JdbU3s=\",\n    \"zh:05e9a538cd705e94c20623c67825fff5f521f8489878fbe03b316ece1f716296\",\n    \"zh:240bba4c06058dcf452b2a193c6b38479d8c1b20267336a9f7247ca4951ed9aa\",\n    \"zh:2521de66f43433536139ba7c3d1127ac122baa2160ae839e3ebc7b99dfe82f16\",\n    \"zh:2f9cc3dc5489df24042324f9b2a1b6e9e5be4df13228e78f71e3199264db02b5\",\n    \"zh:44eca2e0697bcfed2c9fa2af7ff3027d3d3c50812f8eebf2fb6606f9e8b6263c\",\n    \"zh:552375812c2b04dab1f6e550ed31cecfa133c590ac24bbf4606d49799b4f4a15\",\n    \"zh:90781e35bd558ef994233731551e967c94251a1eb04b4a06f7994218d3e04dc9\",\n    \"zh:968ffd53dbc16dde853fd88b92dca5cf3ac1951bb7a8ceb10bc2b6dcbc2fbe0e\",\n    \"zh:d427ed21518fba72dd41d33e3a0c936985a6b796a27e5a3bee733e856a5d7171\",\n  ]\n}\n\nprovider \"registry.opentofu.org/hashicorp/consul\" {\n  version     = \"2.22.1\"\n  constraints = \"~> 2.0\"\n  hashes = [\n    \"h1:hC2//FKKZnkssQcQ1Su3OIiUcpPU7p71oFp2WIBr8DM=\",\n    \"h1:iKErHXJI0g84ne56Ih/Kax560Y+GYk3wT40nBwEZRAw=\",\n    \"h1:qkhEG3O7Z0wijc8CjlzL5fgTOlBuw4Brqihxr+Oyzgc=\",\n    \"zh:04fe2ab9370335e24e19955db5b85977c706cdc1916edd63e9b63438b4ff1990\",\n    \"zh:170c6125a71dd5920194f23dbfc411a9d24116f8c776049e5499af9b187df61a\",\n    \"zh:417ad55c114bc79da09359207974aa69fba08040d114f37235e00a946326a550\",\n    \"zh:5fc629534d6c5bdab364de1530a7efd7eb90952af36f7ad4f9a674b2e7428ed4\",\n    \"zh:6240587dbbe3d61d57c76e55725ff7d6150b02921f53df9560e577c4e421f94a\",\n    \"zh:731d3ebee059595069becd3c8cfa46ac1ebd1fba9364d6f5c67e5b50a5f60ee6\",\n    \"zh:967a3e6550ea44a76c18627a83dbf02c5050498ad9e7f309ffefa746e8646349\",\n    \"zh:a9df0ba4c7eb4dc7d621d23ae282eeb92c7b27fcbd9720d40636399d2c590a44\",\n    \"zh:bb2ac6d4d5a600b4871025c97efd07e67de43c7bae42025bbf43e31667f86e6e\",\n  ]\n}\n\nprovider \"registry.opentofu.org/hashicorp/google\" {\n  version     = \"6.50.0\"\n  constraints = \"~> 6.0\"\n  hashes = [\n    \"h1:0qkP2yFo87EamHXoV0cK2w6hADP2grd+ZfzAixUPDSw=\",\n    \"h1:IH3uigEekXZECc3XgxC771MS1u32uWq5RHmZtVBsau8=\",\n    \"h1:MAAe4zFFdqS9M5rpmJK/vKgdb6ZMD/s/0Xd97yTDipA=\",\n    \"zh:1d4695f807d998f11fcdcfa174766287b82a8093513af857bcdad2d81c642480\",\n    \"zh:3173ac5df0294624d113812e49e2a55714aff7db617488168cecdf4168df9e29\",\n    \"zh:34d2b3d44c23bd6354fc4ab5917b302872ea1ab8de107034567f955b1717fa5b\",\n    \"zh:3a77f3cc2f3664cd5aaeeef4d044e6ec1695a079588fffec3ca03953664e5f04\",\n    \"zh:6b444e4b629ea8dc8cb112a39dde098dc5584d26d6de4177558f556a9a226696\",\n    \"zh:96545c8cd4d3a57069c5d1799eab5aedd887e16d98b5559a195f6d2c2d9bc674\",\n    \"zh:ba464caafde95ee16671d6b5ec90f053ed77a9d06c567456db6efd9160fa3165\",\n    \"zh:d876938e5b0d3f57a984d9be72467995f87fef6569968623415dc51d9f54d30b\",\n    \"zh:dfd908d873e314ab807d0abc9cfd42d2611cd06dc1b9ec719ebdbb738e8e68d6\",\n    \"zh:f9f16819a7738d564afd45fd169ba61004ec4e4e7089d2a4950cb8895be1fe1f\",\n  ]\n}\n\nprovider \"registry.opentofu.org/hashicorp/helm\" {\n  version     = \"3.1.1\"\n  constraints = \"~> 3.0\"\n  hashes = [\n    \"h1:1TvLWj0VSBgoIQy2qo0yvDTvb/7tk1t/7iQRZ9cv0CA=\",\n    \"h1:8SOQHxpTUK0rYBsCoxqrvDRc75KZl9hBt1m7QLrs+QM=\",\n    \"h1:brfn5YltnzexsfqpWKw+5gS9U/m77e0An3hZQamlEZk=\",\n    \"zh:09b38905e234c2e0b185332819614224660050b7e4b25e9e858b593ab01adafe\",\n    \"zh:09fed1b19b8bcded169fb76304e06c5b1216d5ceba92948c23384f34ddbf1fac\",\n    \"zh:2e0af220f3fe79048d82f6de91752ba9929c215819d3de4f82ccb473bcd9e5df\",\n    \"zh:5fe8657cbf6aca769b9565a4fb4605d7b441c2c558d915b067c0adf6f77c58d4\",\n    \"zh:713943f797be3a4c6fc6bb5f1306c4f74762bfaa663f98fd8b4c49d28ee54ecf\",\n    \"zh:b426458c0bbad64f9000c11af7e74a24ce9e0adb3037c05dadf80c0c3e757931\",\n    \"zh:c0664866280a42156484a48f6c461d0ddb2d212da9b6e930c721ef577ab75270\",\n    \"zh:e4f9d0ebb70d63d8ac3ccee00a4d8cdb15b97aaa390f95ed65921e9d0f65bfa0\",\n    \"zh:f6fe7ecfafc344f4e6aecacf5ae12ac73b94389b9679dcd0f04fc5ff45bdc066\",\n  ]\n}\n\nprovider \"registry.opentofu.org/hashicorp/kubernetes\" {\n  version     = \"2.38.0\"\n  constraints = \"~> 2.0\"\n  hashes = [\n    \"h1:HGkB9bCmUqMRcR5/bAUOSqPBsx6DAIEnbT1fZ8vzI78=\",\n    \"h1:ems+O2dA7atxMWpbtqIrsH7Oa+u+ERWSfpMaFnZPbh0=\",\n    \"h1:nY7J9jFXcsRINog0KYagiWZw1GVYF9D2JmtIB7Wnrao=\",\n    \"zh:1096b41c4e5b2ee6c1980916fb9a8579bc1892071396f7a9432be058aabf3cbc\",\n    \"zh:2959fde9ae3d1deb5e317df0d7b02ea4977951ee6b9c4beb083c148ca8f3681c\",\n    \"zh:5082f98fcb3389c73339365f7df39fc6912bf2bd1a46d5f97778f441a67fd337\",\n    \"zh:620fd5d0fbc2d7a24ac6b420a4922e6093020358162a62fa8cbd37b2bac1d22e\",\n    \"zh:7f47c2de179bba35d759147c53082cad6c3449d19b0ec0c5a4ca8db5b06393e1\",\n    \"zh:89c3aa2a87e29febf100fd21cead34f9a4c0e6e7ae5f383b5cef815c677eb52a\",\n    \"zh:96eecc9f94938a0bc35b8a63d2c4a5f972395e44206620db06760b730d0471fc\",\n    \"zh:e15567c1095f898af173c281b66bffdc4f3068afdd9f84bb5b5b5521d9f29584\",\n    \"zh:ecc6b912629734a9a41a7cf1c4c73fb13b4b510afc9e7b2e0011d290bcd6d77f\",\n  ]\n}\n\nprovider \"registry.opentofu.org/hashicorp/nomad\" {\n  version     = \"2.5.2\"\n  constraints = \"~> 2.0\"\n  hashes = [\n    \"h1:51iNOCGImmeqQQCI/6OvbFLCZvkTvDF/VcewKhBKXpg=\",\n    \"h1:GLMMAUCTUmOliU8CkdgYcFr3w8TYjMneJgrP3S7QAE8=\",\n    \"h1:ri3dE41H43PPzJ2drl6LVVS0mlJ1tALpxME0xxWQ34c=\",\n    \"zh:2ef8139181e8855c318be2e0f1bc3180f52a675a158ea1ddb3939f9ef79202c3\",\n    \"zh:33c7350e986756b0c4a2fcc5dad417823f1c4535699dad2fcb736e8838709c14\",\n    \"zh:3a4fa226ceded41ca4513cd9d261eb396cb3ed3549c9708eddd2d6eb8a3c4204\",\n    \"zh:3bc2956a3e25e617e2b65ee28d7f10fd330ec400a8dec8c0e50c4643b2283464\",\n    \"zh:534d2101c5d349e1b4b0614e57a47a56e0f8bf126c798f93dbc8ecf7943984e6\",\n    \"zh:57652799408405acb4476a2ab3b8dd022aa9d726516d3b7927aa2a097cbe9286\",\n    \"zh:6b61f2b3c898979c7fd939e45509370df1292c7d377df928996b16f4768289b1\",\n    \"zh:78870b227f9a90884bb47e352abdbf52bac0364ac9a5f584636a8637ab4a95f8\",\n    \"zh:ceda24edada8ed297bdf88a312b3476a080396636e6a115734adc279aa009f35\",\n  ]\n}\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.0\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n\nprovider \"registry.opentofu.org/hashicorp/random\" {\n  version     = \"3.8.0\"\n  constraints = \"~> 3.0\"\n  hashes = [\n    \"h1:aEaTEHutDdKNaztKFmInhfzmZK0/OaVL8uxmncM9YF8=\",\n    \"h1:ey4eBIHiuAC5xsblxtXghXE3nWwUvGqTT6KAsggiAwo=\",\n    \"h1:nRPdhXsZpGPMppuUgBe/ZcAtD73NaCLGROYHXv41qz8=\",\n    \"zh:2d5e0bbfac7f15595739fe54a9ab8b8eea92fd6d879706139dad7ecaa5c01c19\",\n    \"zh:349e637066625d97aaa84db1b1418c86d6457cf9c5a62f6dcc3f55cbd535112c\",\n    \"zh:5f4456d53f5256ccfdb87dd35d3bf34578d01bd9b71cffaf507f0692805eac8a\",\n    \"zh:6c1ecfacc5f7079a068d7f8eb8924485d4ec8183f36e6318a6e748d35921ddac\",\n    \"zh:6d86641edeb8c394f121f7b0a691d72f89cf9b938b987a01fc32aad396a50555\",\n    \"zh:76947bd7bc7033b33980538da149c94e386f9b0abb2ce63733f25a57517e4742\",\n    \"zh:79c07f4c8b3a63d9f89e25e4348b462c57e179bca66ba533710851c485e282db\",\n    \"zh:ac1c2b941d994728a3a93aba093fd2202f9311d099ff85f66678897c792161ba\",\n    \"zh:cbb2aa867fd828fcb4125239e00862b9a3bc2f280e945c760224276b476f4c49\",\n  ]\n}\n\nprovider \"registry.opentofu.org/hashicorp/tls\" {\n  version     = \"4.1.0\"\n  constraints = \"~> 4.0\"\n  hashes = [\n    \"h1:MByilNnYPdjPTlb/qcNgR0DErA6550hI6wd8OJYB1vw=\",\n    \"h1:RBhHxjVu41XdAnM4WxxGTz2nYaccHNLalqx4031L8rE=\",\n    \"h1:yNZuPWUgw6Ik2huf9lhsuCGONWo2rsY1MfeceT0BQpw=\",\n    \"zh:187a99f0d236fd92da224e2f026c4ca8f1dcbf2b5cddc8e6896801bacfab0d73\",\n    \"zh:61a32a01cc46f382014dcf7aff5bcac61fe97bd69d3ccb51c801e9437ecdb9ce\",\n    \"zh:683ba18baa2cc336ff83f061b5e4569e2cd7c4a097b53a2d80bb0a26be2fc59a\",\n    \"zh:85c7640ea13dcf5ae5f7f3abbf2f21e4b93ce7f333ffee5b4a6acd6b5fe71223\",\n    \"zh:882f2c5214fd6d280a500acfd560925a71030ef70e10d11fa2b94815b58ae9b6\",\n    \"zh:97cb5e0b81b8687870a6b8a16e9a9cfe546e2fdb7534bdd8302eda0d66393f78\",\n    \"zh:c0a0110b15ce45140036fe5bf5a44cb822c2f55b30ff2770faf37d7c3cae3b5e\",\n    \"zh:d98c1c63fd0c76704fd7be38c316c305a2c95f3215330f2fb1e6b0b7081bf8e9\",\n    \"zh:e703a7adf220ac436f8ebfd06529de865b965fcfc461c7ef7b71afa0de04c8e9\",\n    \"zh:e93e241150cd438a0708679cb4aa7976742fde02f4c1725cfdefc405c4eeca1a\",\n  ]\n}\n\nprovider \"registry.opentofu.org/hashicorp/vault\" {\n  version     = \"5.6.0\"\n  constraints = \"~> 5.0\"\n  hashes = [\n    \"h1:6rFaHCFAy0DoICv8MQjy+UyMVbxOVyanKLpjZNR2ZPg=\",\n    \"h1:Jdm4FcgcTCtbuQNYZtwu/4aooPfyshBxMQJ3sls4udQ=\",\n    \"h1:afl/41BzHl93OACbk2EnGHF/xVgHtzmEREcdejASJcM=\",\n    \"zh:437c9f3920cc29d7a453aa5342270d55c618ca4979718c34641a49af1437d5a8\",\n    \"zh:5aeec1c3ed1710d5594277250ae4202c97bf9d8462a3c672c689e96bec3d7c8a\",\n    \"zh:71a61b7667e1016b5ed524b5842c65be1ab1661f258faf7a7cec9db4ea44abd8\",\n    \"zh:769a946cc4d99752dce8db0c4e509a201c0063f266dd387164b239854392d1b7\",\n    \"zh:82b0173dc9f65035b4ae6faaea497239455e5562d5d8c24a1f882e4451234dcf\",\n    \"zh:8f3373ef3dd1e026424c194d46842c0451ee79aacc67d1a1087d8d1ef8e55e95\",\n    \"zh:984d15ebd2fdaa55608efff7d98d59e414a1cb8c7b899b883fbbfbeb5849b70d\",\n    \"zh:9b4194ab7cf28d22098c51436dbfea878e413c418d6dbcfd3689aeb580993e21\",\n    \"zh:ef30a4958328b9fa3d58845ddebe8bd574a533170a28dc7b73121d52f565563b\",\n  ]\n}\n\nprovider \"registry.opentofu.org/integrations/github\" {\n  version     = \"6.10.2\"\n  constraints = \"~> 6.0\"\n  hashes = [\n    \"h1:0fG4EGf4A2gbz1OWMdYsdyq7+RGzlfFUlLsZmSgocNw=\",\n    \"h1:FBpodFPW47rtbjMx2olf4qwXHwh42BX0Wwy7nfXdnQ8=\",\n    \"h1:xuphBoJqSkQT4s20Y6afnkfVbbRfGdbPEdM8frN/41I=\",\n    \"zh:0276720213c19abb83faf6774697518dc1040fd37bc83eef86634f85d4781cae\",\n    \"zh:19c6a9736a3d0264c9e0064ee66f7a957f2e35af2d2b7d4a2936d6d02ae122c4\",\n    \"zh:3212c3c92b2ab16feb6d99c1d2b252fef255aeb709303a2428ea3af0677c30a1\",\n    \"zh:472b40f129c4e7ad2308870972c584874e018723fc190dfebe2416c5a6e6580f\",\n    \"zh:721d21615de5565b5ab0b1a6cf79f3bff2c95b26289c9175fcc75947a216774d\",\n    \"zh:80e9128e4da85e7d146025425aefaf20e85b6187e37c91182a19814bbc949684\",\n    \"zh:83a0573eceb8f211a4f8cb9d2a98ad47c04878369046a6fcb18636cb3cbc78f3\",\n    \"zh:9df2be04ec201a0d3c9251776c7eab1a8773b9ae758ebb996000e6ec7a3d72aa\",\n    \"zh:a4028674e02099b2c63c73bbb51c8bebf437a5cc4537f56107852abdc2d442e7\",\n    \"zh:a55dfa5f6665c63daa72bc3b50a90f2c11113c05014b2a8ae7d7f29a9ad87251\",\n    \"zh:aa9af998f01ec75876fcfc9a0b4390c8f9d41dc8365e015506e7b227a0b2f042\",\n    \"zh:f35e64bf0448856e21a9ecc3e551d11afdae11d44a408f124c7283354d504209\",\n    \"zh:f6079518e101494fe6e62588393c739c5448e6b8b3f277da3d9b04f2088b14fe\",\n    \"zh:fbd1fee2c9df3aa19cf8851ce134dea6e45ea01cb85695c1726670c285797e25\",\n    \"zh:fe5835aa672c7ff876c3d327b1e1641b8d69da2f61322bd8ef4a226e40947e0f\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/auto-provider-cache-dir/heavy/unit/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"registry.opentofu.org/hashicorp/aws\"\n      version = \"~> 6.0\"\n    }\n    google = {\n      source  = \"registry.opentofu.org/hashicorp/google\"\n      version = \"~> 6.0\"\n    }\n    azurerm = {\n      source  = \"registry.opentofu.org/hashicorp/azurerm\"\n      version = \"~> 4.0\"\n    }\n    kubernetes = {\n      source  = \"registry.opentofu.org/hashicorp/kubernetes\"\n      version = \"~> 2.0\"\n    }\n    helm = {\n      source  = \"registry.opentofu.org/hashicorp/helm\"\n      version = \"~> 3.0\"\n    }\n    vault = {\n      source  = \"registry.opentofu.org/hashicorp/vault\"\n      version = \"~> 5.0\"\n    }\n    consul = {\n      source  = \"registry.opentofu.org/hashicorp/consul\"\n      version = \"~> 2.0\"\n    }\n    nomad = {\n      source  = \"registry.opentofu.org/hashicorp/nomad\"\n      version = \"~> 2.0\"\n    }\n    datadog = {\n      source  = \"registry.opentofu.org/DataDog/datadog\"\n      version = \"~> 3.0\"\n    }\n    github = {\n      source  = \"registry.opentofu.org/integrations/github\"\n      version = \"~> 6.0\"\n    }\n    tls = {\n      source  = \"registry.opentofu.org/hashicorp/tls\"\n      version = \"~> 4.0\"\n    }\n    random = {\n      source  = \"registry.opentofu.org/hashicorp/random\"\n      version = \"~> 3.0\"\n    }\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/auto-provider-cache-dir/heavy/unit/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/aws-provider-patch/example-module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/aws\" {\n  version = \"6.28.0\"\n  hashes = [\n    \"h1:i0G7vt2sNy0oz84IiuG4gplonNVyOLRdprKLurU8pe4=\",\n    \"h1:tcau98fkhZ2RhbPHo8LdiiUk2RGpZUgT/t06sdMLids=\",\n    \"h1:wek8vEEZpTPulbLi9xCf2wnxvc97JXAN4qcOhduSg7k=\",\n    \"zh:38d58305206953783c150fb96d5c4f3ea5fe0b9e0987d927c884a6b0f2adf7a9\",\n    \"zh:43fd483251165f98b7a44360b41b437d309b007ef2bfff818eedcf3730e3f5cb\",\n    \"zh:4753decc5a718cb74b08244a02d00c150f0ddd6ebf2e1227f6a985c647c03ce9\",\n    \"zh:5956525650554bd3fbc4b695eb5250193f0ebf94c45862a7730457ab6a315069\",\n    \"zh:76d98fa1146750c01f607bae4421952ee9cd14ed3a4a59deb7136749adb9e0ae\",\n    \"zh:792c29e5ec91356baddb6219ac7f6f1df09c251cbe4ab6e089fc25d64270b22a\",\n    \"zh:856424380caa7c1536dc00515d12beac2693db1a8425da654eed5530abeb17d9\",\n    \"zh:e8982ec2bc692efa7236e3565e7094a09f52c5b71d8860a570a36fb31a40f27f\",\n    \"zh:f5e7ff825dc3f7356fb80936bfe7bb1b54a728ccf429cb753cfe590932f0403b\",\n  ]\n}\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/aws-provider-patch/example-module/main.tf",
    "content": "# We intentionally have an AWS provider block nested within this module so that we can have an integration test that\n# checks if the aws-provider-patch command helps to work around https://github.com/hashicorp/terraform/issues/13018.\nprovider \"aws\" {\n  region              = var.secondary_aws_region\n  allowed_account_ids = var.allowed_account_ids\n  alias               = \"secondary\"\n}\n\nvariable \"secondary_aws_region\" {\n  description = \"The AWS region to deploy the S3 bucket into\"\n  type        = string\n}\n\nvariable \"bucket_name\" {\n  description = \"The name to use for the S3 bucket\"\n  type        = string\n}\n\nvariable \"allowed_account_ids\" {\n  description = \"The list of IDs of AWS accounts that are allowed to run this module.\"\n  type        = list(string)\n}\n\nresource \"aws_s3_bucket\" \"example\" {\n  bucket = var.bucket_name\n\n  provider = aws.secondary\n\n  # Set to true to make testing easier\n  force_destroy = true\n}\n\nresource \"null_resource\" \"complex_expression\" {\n  triggers = (\n    var.secondary_aws_region == \"us-east-1\"\n    ? { default = \"True\" }\n    : {}\n  )\n}\n"
  },
  {
    "path": "test/fixtures/aws-provider-patch/main.tf",
    "content": "provider \"aws\" {\n  region              = var.primary_aws_region\n  allowed_account_ids = var.allowed_account_ids\n}\n\nmodule \"example_module\" {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/aws-provider-patch/example-module?ref=__BRANCH_NAME__\"\n\n  allowed_account_ids  = var.allowed_account_ids\n  secondary_aws_region = var.secondary_aws_region\n  bucket_name          = var.bucket_name\n}\n\nvariable \"primary_aws_region\" {\n  description = \"The primary AWS region for this module\"\n  type        = string\n}\n\nvariable \"secondary_aws_region\" {\n  description = \"The secondary AWS region to deploy the S3 bucket from the module into\"\n  type        = string\n}\n\nvariable \"allowed_account_ids\" {\n  description = \"The list of IDs of AWS accounts that are allowed to run this module.\"\n  type        = list(string)\n}\n\nvariable \"bucket_name\" {\n  description = \"The name to use for the S3 bucket\"\n  type        = string\n}\n"
  },
  {
    "path": "test/fixtures/aws-provider-patch/terragrunt.hcl",
    "content": "# Intentionally empty. This is merely a placeholder so that Terragrunt treats this folder as a Terragrunt module."
  },
  {
    "path": "test/fixtures/broken-dependency/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/broken-dependency/app/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/broken-dependency/app/terragrunt.hcl",
    "content": "dependency \"dependency\" {\n  config_path = \"../dependency\"\n\n  mock_outputs = {\n    test = \"value\"\n  }\n\n}\n"
  },
  {
    "path": "test/fixtures/broken-dependency/dependency/main.tf",
    "content": "module \"example_module\" {\n  source = \"/tmp/not-existing-path\"\n}\n"
  },
  {
    "path": "test/fixtures/broken-dependency/dependency/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/broken-locals/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/broken-locals/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/broken-locals/terragrunt.hcl",
    "content": "locals {\n  file = yamldecode(sops_decrypt_file(\"not-existing-file-that-will-fail-locals-evaluating.yaml\"))\n}\n"
  },
  {
    "path": "test/fixtures/buffer-module-output/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/buffer-module-output/app/main.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nprovider \"null\" {}\n\n# Create a large string by repeating a smaller string multiple times\nresource \"null_resource\" \"large_json\" {\n  count = 1\n\n  triggers = {\n    large_data = join(\"\", [\n      for i in range(0, 1024) : \"ThisIsAVeryLongStringRepeatedManyTimesToCreateLargeDataBlock_\"\n    ])\n  }\n}\n\nresource \"null_resource\" \"large_json_2\" {\n  count = 1\n\n  triggers = {\n    large_data = join(\"\", [\n      for i in range(0, 1024) : \"ThisIsAVeryLongStringRepeatedManyTimesToCreateLargeDataBlock_1024\"\n    ])\n  }\n}\n\n\noutput \"large_json_output\" {\n  value = null_resource.large_json[0].triggers.large_data\n}\n\n\noutput \"large_json_output_2\" {\n  value = null_resource.large_json_2[0].triggers.large_data\n}\n"
  },
  {
    "path": "test/fixtures/buffer-module-output/app/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/buffer-module-output/app2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/buffer-module-output/app2/main.tf",
    "content": "provider \"null\" {}\n\nterraform {\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\n\n# Create a large string by repeating a smaller string multiple times\nresource \"null_resource\" \"large_json\" {\n  count = 1\n\n  triggers = {\n    large_data = join(\"\", [\n      for i in range(0, 1024) : \"ThisIsAVeryLongStringRepeatedManyTimesToCreateLargeDataBlock_\"\n    ])\n  }\n}\n\nresource \"null_resource\" \"large_json_2\" {\n  count = 1\n\n  triggers = {\n    large_data = join(\"\", [\n      for i in range(0, 1024) : \"ThisIsAVeryLongStringRepeatedManyTimesToCreateLargeDataBlock_1024\"\n    ])\n  }\n}\n\n\noutput \"large_json_output\" {\n  value = null_resource.large_json[0].triggers.large_data\n}\n\n\noutput \"large_json_output_2\" {\n  value = null_resource.large_json_2[0].triggers.large_data\n}\n"
  },
  {
    "path": "test/fixtures/buffer-module-output/app2/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/buffer-module-output/app3/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/buffer-module-output/app3/main.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nprovider \"null\" {}\n\n# Create a large string by repeating a smaller string multiple times\nresource \"null_resource\" \"large_json\" {\n  count = 1\n\n  triggers = {\n    large_data = join(\"\", [\n      for i in range(0, 1024) : \"ThisIsAVeryLongStringRepeatedManyTimesToCreateLargeDataBlock_\"\n    ])\n  }\n}\n\nresource \"null_resource\" \"large_json_2\" {\n  count = 1\n\n  triggers = {\n    large_data = join(\"\", [\n      for i in range(0, 1024) : \"ThisIsAVeryLongStringRepeatedManyTimesToCreateLargeDataBlock_1024\"\n    ])\n  }\n}\n\n\noutput \"large_json_output\" {\n  value = null_resource.large_json[0].triggers.large_data\n}\n\n\noutput \"large_json_output_2\" {\n  value = null_resource.large_json_2[0].triggers.large_data\n}\n"
  },
  {
    "path": "test/fixtures/buffer-module-output/app3/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/catalog/complex/common.hcl",
    "content": "locals {\n  github_org = \"gruntwork-io\"\n}\n"
  },
  {
    "path": "test/fixtures/catalog/complex/dev/account.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/catalog/complex/dev/us-west-1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/catalog/complex/dev/us-west-1/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/catalog/complex/dev/us-west-1/modules/terraform-aws-eks/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/catalog/complex/dev/us-west-1/modules/terraform-aws-eks/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/catalog/complex/dev/us-west-1/region.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/catalog/complex/dev/us-west-1/terragrunt.hcl",
    "content": "/*\ndfsdfsdf\n\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\n*/\n\n/*\ndfsdfsdf\n\n*/\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/catalog/complex/prod/account.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/catalog/complex/prod/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/catalog/complex/root.hcl",
    "content": "locals {\n  # Automatically load catalog variables shared across all accounts\n  catalog_vars = read_terragrunt_config(find_in_parent_folders(\"common.hcl\"))\n\n  # Automatically load inputs variables shared across all accounts\n  inputs_vars = read_terragrunt_config(find_in_parent_folders(\"no-exist.hcl\"))\n\n  # Automatically load account-level variables\n  account_vars = read_terragrunt_config(find_in_parent_folders(\"account.hcl\"))\n\n  # Automatically load region-level variables\n  region_vars = read_terragrunt_config(\"region.hcl\")\n}\n\n\ncatalog {\n  urls = [\n    \"dev/us-west-1/modules/terraform-aws-eks\",\n    \"./terraform-aws-service-catalog\",\n    \"https://github.com/${local.catalog_vars.locals.github_org}/terraform-aws-utilities\",\n  ]\n}\n\ninputs = {\n  github_org = local.inputs_vars.locals.github_org\n}\n"
  },
  {
    "path": "test/fixtures/catalog/complex/stage/account.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/catalog/complex/stage/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/catalog/complex-legacy-root/common.hcl",
    "content": "locals {\n  github_org = \"gruntwork-io\"\n}\n"
  },
  {
    "path": "test/fixtures/catalog/complex-legacy-root/dev/account.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/catalog/complex-legacy-root/dev/us-west-1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/catalog/complex-legacy-root/dev/us-west-1/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/catalog/complex-legacy-root/dev/us-west-1/modules/terraform-aws-eks/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/catalog/complex-legacy-root/dev/us-west-1/modules/terraform-aws-eks/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/catalog/complex-legacy-root/dev/us-west-1/region.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/catalog/complex-legacy-root/dev/us-west-1/terragrunt.hcl",
    "content": "/*\ndfsdfsdf\n\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\n*/\n\n/*\ndfsdfsdf\n\n*/\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/catalog/complex-legacy-root/prod/account.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/catalog/complex-legacy-root/prod/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/catalog/complex-legacy-root/stage/account.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/catalog/complex-legacy-root/stage/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/catalog/complex-legacy-root/terragrunt.hcl",
    "content": "locals {\n  # Automatically load catalog variables shared across all accounts\n  catalog_vars = read_terragrunt_config(find_in_parent_folders(\"common.hcl\"))\n\n  # Automatically load inputs variables shared across all accounts\n  inputs_vars = read_terragrunt_config(find_in_parent_folders(\"no-exist.hcl\"))\n\n  # Automatically load account-level variables\n  account_vars = read_terragrunt_config(find_in_parent_folders(\"account.hcl\"))\n\n  # Automatically load region-level variables\n  region_vars = read_terragrunt_config(\"region.hcl\")\n}\n\n\ncatalog {\n  urls = [\n    \"dev/us-west-1/modules/terraform-aws-eks\",\n    \"./terraform-aws-service-catalog\",\n    \"https://github.com/${local.catalog_vars.locals.github_org}/terraform-aws-utilities\",\n  ]\n}\n\ninputs = {\n  github_org = local.inputs_vars.locals.github_org\n}\n"
  },
  {
    "path": "test/fixtures/catalog/config1.hcl",
    "content": "locals {\n  baseRepo = \"github.com/gruntwork-io\"\n}\n\ncatalog {\n  urls = [\n    \"terraform-aws-eks\",\n    \"/repo-copier\",\n    \"./terraform-aws-service-catalog\",\n    \"/project/terragrunt/test/terraform-aws-vpc\",\n    \"${local.baseRepo}/terraform-aws-lambda\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/catalog/config2.hcl",
    "content": "locals {\n  baseRepo = \"github.com/gruntwork-io\"\n}\n"
  },
  {
    "path": "test/fixtures/catalog/config3.hcl",
    "content": "catalog {\n}\n"
  },
  {
    "path": "test/fixtures/catalog/config4.hcl",
    "content": "locals {\n  baseRepo = \"github.com/gruntwork-io\"\n}\n\ncatalog {\n  default_template = \"/test/fixtures/scaffold/external-template\"\n}\n"
  },
  {
    "path": "test/fixtures/catalog/local-template/.boilerplate/boilerplate.yml",
    "content": "variables:\n  - name: EnableRootInclude\n    description: Should include root module\n    type: bool\n    default: true\n  - name: RootFileName\n    description: Name of the root Terragrunt configuration file\n    type: string\n"
  },
  {
    "path": "test/fixtures/catalog/local-template/.boilerplate/custom-template.txt",
    "content": "This file proves that the local template was used for scaffolding.\n"
  },
  {
    "path": "test/fixtures/catalog/local-template/.boilerplate/terragrunt.hcl",
    "content": "# Custom local template\nterraform {\n  source = \"{{ .sourceUrl }}\"\n}\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ninputs = {\n  # This is a custom template from a local directory\n  template_type = \"local\"\n\n  # Required variables would be listed here if any exist\n  {{- if .requiredVariables }}\n  {{- range .requiredVariables }}\n  # {{ if .Description }}{{ .Description }}{{ else }}Variable: {{ .Name }}{{ end }}\n  # Type: {{ .Type }}\n  {{ .Name }} = {{ .DefaultValuePlaceholder }}  # TODO: fill in value\n  {{- end }}\n  {{- else }}\n  # No required variables found for this module\n  {{- end }}\n}\n"
  },
  {
    "path": "test/fixtures/catalog/local-template/app/.gitkeep",
    "content": "# This file ensures the app directory exists\n"
  },
  {
    "path": "test/fixtures/catalog/local-template/root.hcl",
    "content": "catalog {\n  urls             = [\".\"]\n  default_template = \"${get_parent_terragrunt_dir()}/.boilerplate\"\n}\n"
  },
  {
    "path": "test/fixtures/catalog/terraform-aws-eks/README.md",
    "content": ""
  },
  {
    "path": "test/fixtures/cli-flag-hints/terragrunt.hcl",
    "content": "terraform {\n  source = \"tfr://registry.terraform.io/yorinasub17/terragrunt-registry-test/null//modules/one?version=0.0.2\"\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-attr/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/codegen/generate-attr/main.tf",
    "content": "variable \"text\" {}"
  },
  {
    "path": "test/fixtures/codegen/generate-attr/terragrunt.hcl",
    "content": "generate = {\n  test = {\n    path      = \"test.tf\"\n    if_exists = \"overwrite\"\n    contents  = <<EOF\noutput \"text\" {\n  value = var.text\n}\nEOF\n  }\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-attr/test.tf",
    "content": "# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa\noutput \"text\" {\n  value = var.text\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/disable/.gitignore",
    "content": "data.txt\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/disable/terragrunt.hcl",
    "content": "generate \"backend\" {\n  path = \"data.txt\"\n  contents = \"test data1\"\n  if_exists = \"error\"\n  disable = true\n}\n\nterraform {\n  source = \"../../module\"\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/disable-signature/.gitignore",
    "content": "data.json\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/disable-signature/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/disable-signature/main.tf",
    "content": "output \"text\" {\n  value = jsondecode(file(\"${path.module}/data.json\"))[\"text\"]\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/disable-signature/terragrunt.hcl",
    "content": "generate \"json\" {\n  path              = \"data.json\"\n  if_exists         = \"overwrite\"\n  disable_signature = true\n  contents          = <<EOF\n{\n  \"text\": \"Hello, World!\"\n}\nEOF\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/enable/.gitignore",
    "content": "data.txt\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/enable/terragrunt.hcl",
    "content": "generate \"backend\" {\n  path = \"data.txt\"\n  contents = \"test data1\"\n  if_exists = \"error\"\n  disable = false\n}\n\nterraform {\n  source = \"../../module\"\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/nested/.gitignore",
    "content": "random_file.txt\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/nested/child_inherit/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/nested/child_inherit/backend.tf",
    "content": "terraform {\n  backend \"local\" {\n    path = \"bar.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/nested/child_inherit/terragrunt.hcl",
    "content": "include {\n  path = \"${get_terragrunt_dir()}/../root.hcl\"\n}\n\ngenerate \"random_file\" {\n  path      = \"random_file.txt\"\n  if_exists = \"overwrite\"\n  contents  = \"Hello world\"\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/nested/child_overwrite/terragrunt.hcl",
    "content": "include {\n  path = \"${get_terragrunt_dir()}/../root.hcl\"\n  merge_strategy = \"deep\"\n}\n\ngenerate \"backend\" {\n  path      = \"backend.tf\"\n  if_exists = \"overwrite\"\n  contents  = <<EOF\nterraform {\n  backend \"local\" {\n    path = \"bar.tfstate\"\n  }\n}\nEOF\n}\n\ngenerate \"random_file\" {\n  path = \"random_file.txt\"\n  if_exists = \"overwrite\"\n  contents = \"Hello world\"\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/nested/root.hcl",
    "content": "generate \"backend\" {\n  path      = \"backend.tf\"\n  if_exists = \"overwrite\"\n  contents  = <<EOF\nterraform {\n  backend \"local\" {\n    path = \"foo.tfstate\"\n  }\n}\nEOF\n}\n\nterraform {\n  source = \"${get_parent_terragrunt_dir()}/../../module\"\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/overwrite/.gitignore",
    "content": "foo.tfstate\nbar.tfstate\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/overwrite/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/overwrite/backend.tf",
    "content": "terraform {\n  backend \"local\" {\n    path = \"bar.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/overwrite/terragrunt.hcl",
    "content": "generate \"backend\" {\n  path      = \"backend.tf\"\n  if_exists = \"overwrite\"\n  contents  = <<EOF\nterraform {\n  backend \"local\" {\n    path = \"foo.tfstate\"\n  }\n}\nEOF\n}\n\nterraform {\n  source = \"../../module\"\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/overwrite_terragrunt/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/overwrite_terragrunt/backend.tf",
    "content": "// Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa\nterraform {\n  backend \"local\" {\n    path = \"bar.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/overwrite_terragrunt/terragrunt.hcl",
    "content": "# No error and overwrite file because the existing file has terragrunt signature.\ngenerate \"backend\" {\n  path      = \"backend.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents  = <<EOF\nterraform {\n  backend \"local\" {\n    path = \"foo.tfstate\"\n  }\n}\nEOF\n}\n\nterraform {\n  source = \"../../module\"\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/overwrite_terragrunt_error/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/overwrite_terragrunt_error/backend.tf",
    "content": "terraform {\n  backend \"local\" {\n    path = \"bar.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/overwrite_terragrunt_error/terragrunt.hcl",
    "content": "# Error because the existing file does not have terragrunt signature.\ngenerate \"backend\" {\n  path      = \"backend.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents  = <<EOF\nterraform {\n  backend \"local\" {\n    path = \"foo.tfstate\"\n  }\n}\nEOF\n}\n\nterraform {\n  source = \"../../module\"\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/same_name_error/terragrunt.hcl",
    "content": "generate \"backend\" {\n  path = \"data.txt\"\n  if_exists = \"overwrite\"\n  contents = \"test data1\"\n}\n\ngenerate \"backend\" {\n  path = \"data.txt\"\n  if_exists = \"overwrite\"\n  contents = \"test data2\"\n}\n\n\nterraform {\n  source = \"../../module\"\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/same_name_includes_error/app1.hcl",
    "content": "generate \"backend\" {\n  path = \"backend.txt\"\n  contents = \"backend 2\"\n  if_exists = \"overwrite_terragrunt\"\n}\n\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/same_name_includes_error/app2.hcl",
    "content": "generate \"backend\" {\n  path = \"backend.txt\"\n  contents = \"other_module\"\n  if_exists = \"overwrite_terragrunt\"\n}\n\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/same_name_includes_error/terragrunt.hcl",
    "content": "include \"app1\" {\n  path = \"app1.hcl\"\n}\n\ninclude \"app2\" {\n  path = \"app2.hcl\"\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/same_name_pair_error/terragrunt.hcl",
    "content": "generate \"backend\" {\n  path = \"data.txt\"\n  if_exists = \"overwrite\"\n  contents = \"test data1\"\n}\n\ngenerate \"backend2\" {\n  path = \"data2.txt\"\n  if_exists = \"overwrite\"\n  contents = \"test data2\"\n}\n\ngenerate \"backend\" {\n  path = \"data.txt\"\n  if_exists = \"overwrite\"\n  contents = \"test data1\"\n}\n\ngenerate \"backend2\" {\n  path = \"data2.txt\"\n  if_exists = \"overwrite\"\n  contents = \"test data2\"\n}\n\n\nterraform {\n  source = \"../../module\"\n}\n"
  },
  {
    "path": "test/fixtures/codegen/generate-block/skip/terragrunt.hcl",
    "content": "generate \"backend\" {\n  path      = \"main.tf\"\n  if_exists = \"skip\"\n  contents  = <<EOF\nterraform {\n  backend \"local\" {\n    path = \"foo.tfstate\"\n  }\n}\nEOF\n}\n\nterraform {\n  source = \"../../module\"\n}\n"
  },
  {
    "path": "test/fixtures/codegen/module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/random\" {\n  version = \"3.8.0\"\n  hashes = [\n    \"h1:aEaTEHutDdKNaztKFmInhfzmZK0/OaVL8uxmncM9YF8=\",\n    \"h1:ey4eBIHiuAC5xsblxtXghXE3nWwUvGqTT6KAsggiAwo=\",\n    \"h1:nRPdhXsZpGPMppuUgBe/ZcAtD73NaCLGROYHXv41qz8=\",\n    \"zh:2d5e0bbfac7f15595739fe54a9ab8b8eea92fd6d879706139dad7ecaa5c01c19\",\n    \"zh:349e637066625d97aaa84db1b1418c86d6457cf9c5a62f6dcc3f55cbd535112c\",\n    \"zh:5f4456d53f5256ccfdb87dd35d3bf34578d01bd9b71cffaf507f0692805eac8a\",\n    \"zh:6c1ecfacc5f7079a068d7f8eb8924485d4ec8183f36e6318a6e748d35921ddac\",\n    \"zh:6d86641edeb8c394f121f7b0a691d72f89cf9b938b987a01fc32aad396a50555\",\n    \"zh:76947bd7bc7033b33980538da149c94e386f9b0abb2ce63733f25a57517e4742\",\n    \"zh:79c07f4c8b3a63d9f89e25e4348b462c57e179bca66ba533710851c485e282db\",\n    \"zh:ac1c2b941d994728a3a93aba093fd2202f9311d099ff85f66678897c792161ba\",\n    \"zh:cbb2aa867fd828fcb4125239e00862b9a3bc2f280e945c760224276b476f4c49\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/codegen/module/main.tf",
    "content": "resource \"random_string\" \"random\" {\n  length = 16\n}\n\noutput \"random_string\" {\n  value = random_string.random.result\n}\n"
  },
  {
    "path": "test/fixtures/codegen/remote-state/base/.gitignore",
    "content": "foo.tfstate\nbackend.tf\n"
  },
  {
    "path": "test/fixtures/codegen/remote-state/base/terragrunt.hcl",
    "content": "remote_state {\n  backend = \"local\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    path = \"foo.tfstate\"\n  }\n}\n\nterraform {\n  source = \"../../module\"\n}\n"
  },
  {
    "path": "test/fixtures/codegen/remote-state/error/terragrunt.hcl",
    "content": "remote_state {\n  backend = \"local\"\n  generate = {\n    # Intentionally named main.tf so that it conflicts\n    path      = \"main.tf\"\n    if_exists = \"error\"\n  }\n  config = {\n    path = \"foo.tfstate\"\n  }\n}\n\nterraform {\n  source = \"../../module\"\n}\n"
  },
  {
    "path": "test/fixtures/codegen/remote-state/overwrite/.gitignore",
    "content": "foo.tfstate\n"
  },
  {
    "path": "test/fixtures/codegen/remote-state/overwrite/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/codegen/remote-state/overwrite/backend.tf",
    "content": "terraform {\n  backend \"local\" {\n    path = \"bar.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/codegen/remote-state/overwrite/terragrunt.hcl",
    "content": "remote_state {\n  backend = \"local\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    path = \"foo.tfstate\"\n  }\n}\n\nterraform {\n  source = \"../../module\"\n}\n"
  },
  {
    "path": "test/fixtures/codegen/remote-state/s3/.gitignore",
    "content": "backend.tf\n"
  },
  {
    "path": "test/fixtures/codegen/remote-state/s3/terragrunt.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket, but use the `generate` feature to configure\n# the remote state\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    encrypt                        = true\n    bucket                         = \"__FILL_IN_BUCKET_NAME__\"\n    key                            = \"terraform.tfstate\"\n    region                         = \"us-west-2\"\n    dynamodb_table                 = \"__FILL_IN_LOCK_TABLE_NAME__\"\n    enable_lock_table_ssencryption = true\n\n    s3_bucket_tags = {\n      owner = \"terragrunt integration test\"\n      name  = \"Terraform state storage\"\n    }\n\n    dynamodb_table_tags = {\n      owner = \"terragrunt integration test\"\n      name  = \"Terraform lock table\"\n    }\n\n    accesslogging_bucket_tags = {\n      owner = \"terragrunt integration test\"\n      name  = \"Terraform access log storage\"\n    }\n  }\n}\n\nterraform {\n  source = \"../../module\"\n}\n"
  },
  {
    "path": "test/fixtures/codegen/remote-state/skip/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/codegen/remote-state/skip/backend.tf",
    "content": "terraform {\n  backend \"local\" {}\n}\n"
  },
  {
    "path": "test/fixtures/codegen/remote-state/skip/terragrunt.hcl",
    "content": "remote_state {\n  backend = \"local\"\n  generate = {\n    # Intentionally named main.tf so that it conflicts\n    path      = \"main.tf\"\n    if_exists = \"skip\"\n  }\n  config = {\n    path = \"foo.tfstate\"\n  }\n}\n\nterraform {\n  source = \"../../module\"\n}\n"
  },
  {
    "path": "test/fixtures/codegen/remove-file/remove/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/codegen/remove-file/remove/backend.tf",
    "content": "terraform {\n  backend \"local\" {\n    path = \"bar.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/codegen/remove-file/remove/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/codegen/remove-file/remove/terragrunt.hcl",
    "content": "generate \"backend\" {\n  path        = \"backend.tf\"\n  disable     = true\n  if_disabled = \"remove\"\n  if_exists   = \"overwrite\"\n  contents    = <<EOF\nterraform {\n  backend \"local\" {\n    path = \"foo.tfstate\"\n  }\n}\nEOF\n}\n"
  },
  {
    "path": "test/fixtures/codegen/remove-file/remove_terragrunt/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/codegen/remove-file/remove_terragrunt/backend.tf",
    "content": "// Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa\nterraform {\n  backend \"local\" {\n    path = \"bar.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/codegen/remove-file/remove_terragrunt/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/codegen/remove-file/remove_terragrunt/terragrunt.hcl",
    "content": "# No error and remove file because the existing file has terragrunt signature.\ngenerate \"backend\" {\n  path        = \"backend.tf\"\n  disable     = true\n  if_disabled = \"remove_terragrunt\"\n  if_exists   = \"overwrite_terragrunt\"\n  contents    = <<EOF\nterraform {\n  backend \"local\" {\n    path = \"foo.tfstate\"\n  }\n}\nEOF\n}\n"
  },
  {
    "path": "test/fixtures/codegen/remove-file/remove_terragrunt_error/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/codegen/remove-file/remove_terragrunt_error/backend.tf",
    "content": "terraform {\n  backend \"local\" {\n    path = \"bar.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/codegen/remove-file/remove_terragrunt_error/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/codegen/remove-file/remove_terragrunt_error/terragrunt.hcl",
    "content": "# Error because the existing file does not have terragrunt signature.\ngenerate \"backend\" {\n  path        = \"backend.tf\"\n  disable     = true\n  if_disabled = \"remove_terragrunt\"\n  if_exists   = \"overwrite_terragrunt\"\n  contents    = <<EOF\nterraform {\n  backend \"local\" {\n    path = \"foo.tfstate\"\n  }\n}\nEOF\n}\n"
  },
  {
    "path": "test/fixtures/codegen/remove-file/skip/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/codegen/remove-file/skip/backend.tf",
    "content": "terraform {\n  backend \"local\" {\n    path = \"bar.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/codegen/remove-file/skip/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/codegen/remove-file/skip/terragrunt.hcl",
    "content": "generate \"backend\" {\n  path        = \"backend.tf\"\n  disable     = true\n  if_disabled = \"skip\"\n  if_exists   = \"skip\"\n  contents    = <<EOF\nterraform {\n  backend \"local\" {\n    path = \"foo.tfstate\"\n  }\n}\nEOF\n}\n"
  },
  {
    "path": "test/fixtures/commands-that-need-input/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/commands-that-need-input/main.tf",
    "content": "\n"
  },
  {
    "path": "test/fixtures/commands-that-need-input/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n  extra_arguments \"disable_input\" {\n    commands  = get_terraform_commands_that_need_input()\n    arguments = [\"-input=false\"]\n  }\n}"
  },
  {
    "path": "test/fixtures/config-files/ignore-cached-config/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/config-files/ignore-terraform-data-dir/.tf_data/modules/mod/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/config-files/ignore-terraform-data-dir/.tf_data/modules/mod/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/config-files/ignore-terraform-data-dir/root.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/config-files/ignore-terraform-data-dir/subdir/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/config-files/ignore-terraform-data-dir/subdir/.tf_data/modules/mod/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/config-files/ignore-terraform-data-dir/subdir/.tf_data/modules/mod/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/config-files/ignore-terraform-data-dir/subdir/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/config-files/ignore-terraform-data-dir/subdir/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/config-files/multiple-configs/subdir-1/empty.txt",
    "content": ""
  },
  {
    "path": "test/fixtures/config-files/multiple-configs/subdir-2/subdir/terragrunt.hcl",
    "content": "# Intentionally empty"
  },
  {
    "path": "test/fixtures/config-files/multiple-configs/subdir-3/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/config-files/multiple-configs/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/config-files/multiple-json-configs/subdir-1/empty.txt",
    "content": ""
  },
  {
    "path": "test/fixtures/config-files/multiple-json-configs/subdir-2/subdir/terragrunt.hcl.json",
    "content": "{}"
  },
  {
    "path": "test/fixtures/config-files/multiple-json-configs/subdir-3/terragrunt.hcl.json",
    "content": "{}"
  },
  {
    "path": "test/fixtures/config-files/multiple-json-configs/terragrunt.hcl.json",
    "content": "{}"
  },
  {
    "path": "test/fixtures/config-files/multiple-mixed-configs/subdir-1/empty.txt",
    "content": ""
  },
  {
    "path": "test/fixtures/config-files/multiple-mixed-configs/subdir-2/subdir/terragrunt.hcl",
    "content": "# Intentionally empty"
  },
  {
    "path": "test/fixtures/config-files/multiple-mixed-configs/subdir-3/terragrunt.hcl.json",
    "content": "{}"
  },
  {
    "path": "test/fixtures/config-files/multiple-mixed-configs/terragrunt.hcl.json",
    "content": "{}"
  },
  {
    "path": "test/fixtures/config-files/none/empty.txt",
    "content": ""
  },
  {
    "path": "test/fixtures/config-files/none/subdir/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/config-files/none/subdir/main.tf",
    "content": "# Intentionally empty"
  },
  {
    "path": "test/fixtures/config-files/one-config/empty.txt",
    "content": ""
  },
  {
    "path": "test/fixtures/config-files/one-config/subdir/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/config-files/one-json-config/empty.txt",
    "content": ""
  },
  {
    "path": "test/fixtures/config-files/one-json-config/subdir/terragrunt.hcl.json",
    "content": "{}"
  },
  {
    "path": "test/fixtures/config-files/single-json-config/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/config-files/single-json-config/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/config-files/single-json-config/terragrunt.hcl.json",
    "content": "{}"
  },
  {
    "path": "test/fixtures/config-files/with-non-default-names/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/config-files/with-non-default-names/app/main.hcl",
    "content": "include \"parent\" {\n  path = \"../parent.hcl\"\n}"
  },
  {
    "path": "test/fixtures/config-files/with-non-default-names/app/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/config-files/with-non-default-names/common.hcl",
    "content": "locals {\n  common = run_cmd(\"echo\", \"common_hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/config-files/with-non-default-names/dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/config-files/with-non-default-names/dependency/another-name.hcl",
    "content": "locals {\n  parent_var = run_cmd(\"echo\", \"dependency_hcl\")\n}\n\ninclude \"common\" {\n  path = \"../common.hcl\"\n}\n"
  },
  {
    "path": "test/fixtures/config-files/with-non-default-names/dependency/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/config-files/with-non-default-names/parent.hcl",
    "content": "locals {\n  parent_var = run_cmd(\"echo\", \"parent_hcl_file\")\n}\n\ndependency \"dependency\" {\n  config_path = \"../dependency/another-name.hcl\"\n\n  mock_outputs = {\n    mock_key = \"mock_value\"\n  }\n\n}\n"
  },
  {
    "path": "test/fixtures/config-terraform-functions/other-file.txt",
    "content": "This is a test file\n"
  },
  {
    "path": "test/fixtures/config-terraform-functions/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/dag-graph/region-1/unit-a/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/dag-graph/region-1/unit-a/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/dag-graph/region-1/unit-a/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders()\n}\n\ndependency \"b\" {\n  config_path = \"../../region-2/unit-b/\"\n}\n"
  },
  {
    "path": "test/fixtures/dag-graph/region-2/unit-b/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/dag-graph/region-2/unit-b/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/dag-graph/region-2/unit-b/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders()\n}\n"
  },
  {
    "path": "test/fixtures/dag-graph/root.hcl.hcl",
    "content": "errors {\n  retry \"common_errors\" {\n    retryable_errors = [\n      \"Error acquiring the state lock\",\n      \"Plugin did not respond\",\n      \"Request cancelled\",\n      \"request was cancelled\",\n      \"InvalidIdentityToken\"\n    ]\n    max_attempts = 3\n    sleep_interval_sec = 5\n  }\n}\n"
  },
  {
    "path": "test/fixtures/dependency-optimisation/module-a/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/dependency-optimisation/module-a/main.tf",
    "content": "output \"test_output\" {\n  value = \"hello from module-a\"\n}\n"
  },
  {
    "path": "test/fixtures/dependency-optimisation/module-a/terragrunt.hcl",
    "content": "terraform {\n  source = \".//\"\n}\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/dependency-optimisation/module-b/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/dependency-optimisation/module-b/main.tf",
    "content": "variable \"test_var\" {\n  type = string\n}\n\noutput \"test_output\" {\n  value = var.test_var\n}\n"
  },
  {
    "path": "test/fixtures/dependency-optimisation/module-b/terragrunt.hcl",
    "content": "terraform {\n  source = \".//\"\n}\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"module_a\" {\n  config_path = \"../module-a\"\n}\n\ninputs = {\n  test_var = dependency.module_a.outputs.test_output\n}\n"
  },
  {
    "path": "test/fixtures/dependency-optimisation/module-c/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/dependency-optimisation/module-c/main.tf",
    "content": "variable \"test_var\" {\n  type = string\n}\n\noutput \"result\" {\n  value = var.test_var\n}\n"
  },
  {
    "path": "test/fixtures/dependency-optimisation/module-c/terragrunt.hcl",
    "content": "terraform {\n  source = \".//\"\n}\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"module_b\" {\n  config_path = \"../module-b\"\n}\n\ninputs = {\n  test_var = dependency.module_b.outputs.test_output\n}\n"
  },
  {
    "path": "test/fixtures/dependency-optimisation/root.hcl",
    "content": "remote_state {\n  backend = \"local\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n  config = {\n    path = \"terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/dependency-output/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/dependency-output/app/main.tf",
    "content": "variable \"input_value\" {}\n\noutput \"output_value\" {\n  value = var.input_value\n}"
  },
  {
    "path": "test/fixtures/dependency-output/app/terragrunt.hcl",
    "content": "\ndependency \"dependency\" {\n  config_path = \"../dependency\"\n}\n\ninputs = {\n  input_value = dependency.dependency.outputs.result\n}"
  },
  {
    "path": "test/fixtures/dependency-output/dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/dependency-output/dependency/main.tf",
    "content": "output \"result\" {\n\n  value = \"42\"\n}"
  },
  {
    "path": "test/fixtures/dependency-output/dependency/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/destroy-dependent-module/a/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/destroy-dependent-module/a/main.tf",
    "content": "resource \"local_file\" \"file\" {\n  content         = \"module-a\"\n  filename        = \"module-a.txt\"\n  file_permission = \"0644\"\n}\n\noutput \"value\" {\n  value = local_file.file.filename\n}\n"
  },
  {
    "path": "test/fixtures/destroy-dependent-module/a/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/destroy-dependent-module/b/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/destroy-dependent-module/b/main.tf",
    "content": "resource \"local_file\" \"file\" {\n  content         = \"module-b\"\n  filename        = \"module-b.txt\"\n  file_permission = \"0644\"\n}\n\noutput \"value\" {\n  value = local_file.file.filename\n}\n"
  },
  {
    "path": "test/fixtures/destroy-dependent-module/b/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/destroy-dependent-module/c/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/destroy-dependent-module/c/main.tf",
    "content": "resource \"local_file\" \"file\" {\n  content         = \"module-c\"\n  filename        = \"module-c.txt\"\n  file_permission = \"0644\"\n}\n\noutput \"value\" {\n  value = local_file.file.filename\n}\n"
  },
  {
    "path": "test/fixtures/destroy-dependent-module/c/terragrunt.hcl",
    "content": "dependency \"a\" {\n  config_path = \"../a\"\n}\n\ndependency \"b\" {\n  config_path = \"../b\"\n}\n\ninputs = {\n  test_a_arn = dependency.a.outputs.value\n  test_b_arn = dependency.b.outputs.value\n}\n\n"
  },
  {
    "path": "test/fixtures/destroy-dependent-module-errors/dev/app1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/destroy-dependent-module-errors/dev/app1/main.tf",
    "content": "variable \"data\" {}\n\nresource \"local_file\" \"file\" {\n  content         = \"local file\"\n  filename        = \"module-a-${var.data}.txt\"\n  file_permission = \"0644\"\n}\n\noutput \"value\" {\n  value = local_file.file.filename\n}\n"
  },
  {
    "path": "test/fixtures/destroy-dependent-module-errors/dev/app1/terragrunt.hcl",
    "content": "locals {\n    env_config  = read_terragrunt_config(find_in_parent_folders(\"env.hcl\"))\n}\n\ninputs = {\n    data  = local.env_config.locals.env\n}"
  },
  {
    "path": "test/fixtures/destroy-dependent-module-errors/dev/app2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/destroy-dependent-module-errors/dev/app2/main.tf",
    "content": "variable \"data\" {}\n\nresource \"local_file\" \"file\" {\n  content         = \"local file\"\n  filename        = \"module-a-${var.data}.txt\"\n  file_permission = \"0644\"\n}\n\noutput \"value\" {\n  value = local_file.file.filename\n}\n"
  },
  {
    "path": "test/fixtures/destroy-dependent-module-errors/dev/app2/terragrunt.hcl",
    "content": "locals {\n    env_config  = read_terragrunt_config(find_in_parent_folders(\"env.hcl\"))\n}\n\ndependency \"app1\" {\n  config_path = \"../app1\"\n}\n\ninputs = {\n    data  = dependency.app1.outputs.value\n}"
  },
  {
    "path": "test/fixtures/destroy-dependent-module-errors/env.hcl",
    "content": "locals {\n    env = \"default\"\n}\n"
  },
  {
    "path": "test/fixtures/destroy-dependent-module-errors/prod/app1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/destroy-dependent-module-errors/prod/app1/main.tf",
    "content": "variable \"data\" {}\n\noutput \"output\" {\n  value = var.data\n}\n"
  },
  {
    "path": "test/fixtures/destroy-dependent-module-errors/prod/app1/terragrunt.hcl",
    "content": "inputs = {\n  data = run_cmd(\"run_not_existing_script.sh\")\n}\n"
  },
  {
    "path": "test/fixtures/destroy-dependent-module-errors/prod/app2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/destroy-dependent-module-errors/prod/app2/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/destroy-dependent-module-errors/prod/app2/sops.yaml",
    "content": "creation_rules:\n  - path_regex: prod/secrets.+$\n    encrypted_regex: ^.+$\n    gcp_kms: >-\n      xyz\n  - path_regex: dev/secrets.+$\n    encrypted_regex: ^.+$\n    gcp_kms: >-\n      xyz\n"
  },
  {
    "path": "test/fixtures/destroy-dependent-module-errors/prod/app2/terragrunt.hcl",
    "content": "locals {\n    secrets = yamldecode(sops_decrypt_file(\"sops.yaml\"))\n}\n"
  },
  {
    "path": "test/fixtures/destroy-order/app/module-a/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module A\"\n}\n\nterraform {\n  source = \"../../hello\"\n}\n"
  },
  {
    "path": "test/fixtures/destroy-order/app/module-b/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module B\"\n}\n\nterraform {\n  source = \"../../hello\"\n}\n\ndependencies {\n  paths = [\"../module-a\"]\n}\n"
  },
  {
    "path": "test/fixtures/destroy-order/app/module-c/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module C\"\n}\n\nterraform {\n  source = \"../../hello\"\n}\n"
  },
  {
    "path": "test/fixtures/destroy-order/app/module-d/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module D\"\n}\n\nterraform {\n  source = \"../../hello\"\n}\n\ndependencies {\n  paths = [\"../module-c\"]\n}\n"
  },
  {
    "path": "test/fixtures/destroy-order/app/module-e/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module E\"\n}\n\nterraform {\n  source = \"../../hello\"\n}\n"
  },
  {
    "path": "test/fixtures/destroy-order/hello/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \">= 3.0.0\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/destroy-order/hello/hello/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/destroy-order/hello/hello/main.tf",
    "content": "output \"hello\" {\n  value = \"Hello\"\n}"
  },
  {
    "path": "test/fixtures/destroy-order/hello/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \">= 3.0.0\"\n    }\n  }\n}\n\nvariable \"name\" {\n  description = \"Specify a name\"\n  type        = string\n}\n\nmodule \"hello\" {\n  source = \"./hello\"\n}\n\nresource \"null_resource\" \"test\" {\n  provisioner \"local-exec\" {\n    command = \"echo '${module.hello.hello}, ${var.name}'\"\n  }\n}\n\noutput \"test\" {\n  value = \"${module.hello.hello}, ${var.name}\"\n}\n"
  },
  {
    "path": "test/fixtures/destroy-warning/app-v1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/destroy-warning/app-v1/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/destroy-warning/app-v1/terragrunt.hcl",
    "content": "dependency \"vpc\" {\n  config_path = \"../vpc\"\n\n  mock_outputs = {\n    vpc = \"mock\"\n  }\n}\n\ndependencies {\n  paths = [\"../vpc\"]\n}\n"
  },
  {
    "path": "test/fixtures/destroy-warning/app-v2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/destroy-warning/app-v2/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/destroy-warning/app-v2/terragrunt.hcl",
    "content": "dependency \"vpc\" {\n  config_path = \"../vpc\"\n\n  mock_outputs = {\n    vpc = \"mock\"\n  }\n}\n\ndependencies {\n  paths = [\"../vpc\"]\n}\n"
  },
  {
    "path": "test/fixtures/destroy-warning/root.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/destroy-warning/vpc/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/destroy-warning/vpc/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/destroy-warning/vpc/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/changes/app1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/changes/app1/main.tf",
    "content": "resource \"local_file\" \"example\" {\n  content  = \"Test\"\n  filename = \"${path.module}/example.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/changes/app1/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/changes/app2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/changes/app2/main.tf",
    "content": "resource \"local_file\" \"example\" {\n  content  = \"Test\"\n  filename = \"${path.module}/example.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/changes/app2/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/changes-with-source/app1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/changes-with-source/app1/main.tf",
    "content": "resource \"local_file\" \"example\" {\n  content  = \"Test\"\n  filename = \"${path.module}/example.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/changes-with-source/app1/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/error/app1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/error/app1/main.tf",
    "content": "data \"local_file\" \"read_not_existing_file\" {\n  filename = \"${path.module}/not-existing-file.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/error/app1/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/error/app2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/error/app2/main.tf",
    "content": "resource \"local_file\" \"example\" {\n  content  = \"Test\"\n  filename = \"${path.module}/example.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/error/app2/terragrunt.hcl",
    "content": "/*\n  Sequential dependency on app1 to ensure proper test execution.\n  This prevents flaky tests since:\n  - Local provider returns exit code 2 for managed resources\n  - Local provider returns exit code 1 for data sources\n  - Only the last exit code in the sequence can be checked\n*/\n\ndependencies {\n  paths = [\"../app1\"]\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/fail-on-first-run/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/external\" {\n  version = \"2.3.5\"\n  hashes = [\n    \"h1:+OsaKKx2awgjh6j/2B3VBP6q4Dqg2Fc0uDUZll66/Hg=\",\n    \"h1:VsIY+hWGvWHaGvGTSKZslY13lPeAtSTxfZRPbpLMMhs=\",\n    \"h1:jcVmeuuz74tdRt2kj0MpUG9AORdlAlRRQ3k61y0r5Vc=\",\n    \"zh:1fb9aca1f068374a09d438dba84c9d8ba5915d24934a72b6ef66ef6818329151\",\n    \"zh:3eab30e4fcc76369deffb185b4d225999fc82d2eaaa6484d3b3164a4ed0f7c49\",\n    \"zh:4f8b7a4832a68080f0bf4f155b56a691832d8a91ce8096dac0f13a90081abc50\",\n    \"zh:5ff1935612db62e48e4fe6cfb83dfac401b506a5b7b38342217616fbcab70ce0\",\n    \"zh:993192234d327ec86726041eb6d1efb001e41f32e4518ad8b9b162130b65ee9a\",\n    \"zh:ce445e68282a2c4b2d1f994a2730406df4ea47914c0932fb4a7eb040a7ec7061\",\n    \"zh:e305e17216840c54194141fb852839c2cedd6b41abd70cf8d606d6e88ed40e64\",\n    \"zh:edba65fb241d663c09aa2cbf75026c840e963d5195f27000f216829e49811437\",\n    \"zh:f306cc6f6ec9beaf75bdcefaadb7b77af320b1f9b56d8f50df5ebd2189a93148\",\n    \"zh:fb2ff9e1f86796fda87e1f122d40568912a904da51d477461b850d81a0105f3d\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/fail-on-first-run/main.tf",
    "content": "data \"external\" \"script\" {\n  program = [\n    \"/bin/bash\",\n    \"-c\",\n    <<EOT\n      set -euo pipefail\n      if [[ -f .file ]]; then\n        echo '{}'\n        rm .file\n      else\n        touch .file\n        exit 1\n      fi\n    EOT\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/fail-on-first-run/terragrunt.hcl",
    "content": "errors {\n  retry \"all_errors\" {\n    retryable_errors = [\".*\"]\n    max_attempts = 2\n    sleep_interval_sec = 1\n  }\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/fail-on-first-run-with-status/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/external\" {\n  version = \"2.3.5\"\n  hashes = [\n    \"h1:+OsaKKx2awgjh6j/2B3VBP6q4Dqg2Fc0uDUZll66/Hg=\",\n    \"h1:VsIY+hWGvWHaGvGTSKZslY13lPeAtSTxfZRPbpLMMhs=\",\n    \"h1:jcVmeuuz74tdRt2kj0MpUG9AORdlAlRRQ3k61y0r5Vc=\",\n    \"zh:1fb9aca1f068374a09d438dba84c9d8ba5915d24934a72b6ef66ef6818329151\",\n    \"zh:3eab30e4fcc76369deffb185b4d225999fc82d2eaaa6484d3b3164a4ed0f7c49\",\n    \"zh:4f8b7a4832a68080f0bf4f155b56a691832d8a91ce8096dac0f13a90081abc50\",\n    \"zh:5ff1935612db62e48e4fe6cfb83dfac401b506a5b7b38342217616fbcab70ce0\",\n    \"zh:993192234d327ec86726041eb6d1efb001e41f32e4518ad8b9b162130b65ee9a\",\n    \"zh:ce445e68282a2c4b2d1f994a2730406df4ea47914c0932fb4a7eb040a7ec7061\",\n    \"zh:e305e17216840c54194141fb852839c2cedd6b41abd70cf8d606d6e88ed40e64\",\n    \"zh:edba65fb241d663c09aa2cbf75026c840e963d5195f27000f216829e49811437\",\n    \"zh:f306cc6f6ec9beaf75bdcefaadb7b77af320b1f9b56d8f50df5ebd2189a93148\",\n    \"zh:fb2ff9e1f86796fda87e1f122d40568912a904da51d477461b850d81a0105f3d\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/fail-on-first-run-with-status/main.tf",
    "content": "data \"external\" \"script\" {\n  program = [\n    \"/bin/bash\",\n    \"-c\",\n    <<EOT\n      set -euo pipefail\n      if [[ -f .file ]]; then\n        jq -n '{foo}'\n        rm .file\n      else\n        touch .file\n        exit 1\n      fi\n    EOT\n  ]\n}\n\noutput \"foo\" {\n  value = data.external.script.result.foo\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/fail-on-first-run-with-status/terragrunt.hcl",
    "content": "errors {\n  retry \"all_errors\" {\n    retryable_errors = [\".*\"]\n    max_attempts = 2\n    sleep_interval_sec = 1\n  }\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/runall-retry-after-drift/app_drift/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/runall-retry-after-drift/app_drift/main.tf",
    "content": "resource \"local_file\" \"example\" {\n  content  = \"Test\"\n  filename = \"${path.module}/example.txt\"\n}\n\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/runall-retry-after-drift/app_drift/terragrunt.hcl",
    "content": "# Intentionally empty\n\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/runall-retry-after-drift/app_flaky/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/external\" {\n  version = \"2.3.5\"\n  hashes = [\n    \"h1:+OsaKKx2awgjh6j/2B3VBP6q4Dqg2Fc0uDUZll66/Hg=\",\n    \"h1:VsIY+hWGvWHaGvGTSKZslY13lPeAtSTxfZRPbpLMMhs=\",\n    \"h1:jcVmeuuz74tdRt2kj0MpUG9AORdlAlRRQ3k61y0r5Vc=\",\n    \"zh:1fb9aca1f068374a09d438dba84c9d8ba5915d24934a72b6ef66ef6818329151\",\n    \"zh:3eab30e4fcc76369deffb185b4d225999fc82d2eaaa6484d3b3164a4ed0f7c49\",\n    \"zh:4f8b7a4832a68080f0bf4f155b56a691832d8a91ce8096dac0f13a90081abc50\",\n    \"zh:5ff1935612db62e48e4fe6cfb83dfac401b506a5b7b38342217616fbcab70ce0\",\n    \"zh:993192234d327ec86726041eb6d1efb001e41f32e4518ad8b9b162130b65ee9a\",\n    \"zh:ce445e68282a2c4b2d1f994a2730406df4ea47914c0932fb4a7eb040a7ec7061\",\n    \"zh:e305e17216840c54194141fb852839c2cedd6b41abd70cf8d606d6e88ed40e64\",\n    \"zh:edba65fb241d663c09aa2cbf75026c840e963d5195f27000f216829e49811437\",\n    \"zh:f306cc6f6ec9beaf75bdcefaadb7b77af320b1f9b56d8f50df5ebd2189a93148\",\n    \"zh:fb2ff9e1f86796fda87e1f122d40568912a904da51d477461b850d81a0105f3d\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/runall-retry-after-drift/app_flaky/main.tf",
    "content": "data \"external\" \"flaky\" {\n  program = [\n    \"/bin/bash\",\n    \"-c\",\n    <<EOT\n      set -euo pipefail\n      if [[ ! -f .retry_marker ]]; then\n        echo \"transient fail\" 1>&2\n        touch .retry_marker\n        exit 1\n      fi\n      echo '{}'\n    EOT\n  ]\n}\n\n"
  },
  {
    "path": "test/fixtures/detailed-exitcode/runall-retry-after-drift/app_flaky/terragrunt.hcl",
    "content": "errors {\n  retry \"transient_fail\" {\n    retryable_errors = [\"(?s).*transient fail.*\"]\n    max_attempts       = 2\n    sleep_interval_sec = 1\n  }\n}"
  },
  {
    "path": "test/fixtures/dirs/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/dirs/main.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2\"\n    }\n  }\n}\n\nprovider \"null\" {\n}\n"
  },
  {
    "path": "test/fixtures/dirs/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/disabled/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/disabled/app/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/disabled/app/terragrunt.hcl",
    "content": "dependency \"unit_without_enabled\" {\n  config_path  = \"../unit-without-enabled\"\n  mock_outputs = {\n    \"output1\" = \"mocked_output1\"\n  }\n}\n\ndependency \"unit_disabled\" {\n  config_path = \"../unit-disabled\"\n  enabled     = false\n\n  mock_outputs = {\n    \"output2\" = \"mocked_output2\"\n  }\n}\n\ndependency \"unit_enabled\" {\n  config_path = \"../unit-enabled\"\n  enabled     = true\n\n  mock_outputs = {\n    \"output3\" = \"mocked_output3\"\n  }\n}\n\ndependency \"unit_missing\" {\n  config_path = \"../unit-missing\"\n  enabled     = false\n}\n"
  },
  {
    "path": "test/fixtures/disabled/unit-disabled/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/disabled/unit-disabled/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/disabled/unit-disabled/terragrunt.hcl",
    "content": "broken hcl file\n"
  },
  {
    "path": "test/fixtures/disabled/unit-enabled/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/disabled/unit-enabled/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/disabled/unit-enabled/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/disabled/unit-without-enabled/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/disabled/unit-without-enabled/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/disabled/unit-without-enabled/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/disabled-path/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/disabled-path/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/disabled-path/terragrunt.hcl",
    "content": "terraform {\n  source = \"/dev/null\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-01/foo/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-01/foo/main.tf",
    "content": "resource \"local_file\" \"file\" {\n  content  = \"Hello, World!\"\n  filename = \"${path.module}/hi.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-01/foo/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-01.1/foo/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-01.1/foo/main.tf",
    "content": "variable \"content\" {}\n\nresource \"local_file\" \"file\" {\n  content  = var.content\n  filename = \"${path.module}/hi.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-01.1/foo/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-02/bar/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-02/bar/main.tf",
    "content": "variable \"content\" {}\n\nresource \"local_file\" \"file\" {\n  content  = var.content\n  filename = \"${path.module}/hi.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-02/bar/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-02/foo/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-02/foo/main.tf",
    "content": "variable \"content\" {}\n\nresource \"local_file\" \"file\" {\n  content  = var.content\n  filename = \"${path.module}/hi.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-02/foo/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-03/bar/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-03/bar/main.tf",
    "content": "variable \"content\" {}\n\nmodule \"shared\" {\n  source = \"../shared\"\n\n  content = var.content\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-03/bar/terragrunt.hcl",
    "content": "terraform {\n  source = \"..//bar\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-03/foo/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-03/foo/main.tf",
    "content": "variable \"content\" {}\n\nmodule \"shared\" {\n  source = \"../shared\"\n\n  content = var.content\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-03/foo/terragrunt.hcl",
    "content": "terraform {\n  source = \"..//foo\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-03/shared/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-03/shared/main.tf",
    "content": "variable \"content\" {}\n\nresource \"local_file\" \"file\" {\n  content  = var.content\n  filename = \"${path.module}/hi.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-04/.gitignore",
    "content": ".terragrunt-cache\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-04/bar/terragrunt.hcl",
    "content": "terraform {\n  source = \"../shared\"\n}\n\ninputs = {\n  content = \"Hello from bar, Terragrunt!\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-04/foo/terragrunt.hcl",
    "content": "terraform {\n  source = \"../shared\"\n}\n\ninputs = {\n  content = \"Hello from foo, Terragrunt!\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-04/shared/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-04/shared/main.tf",
    "content": "variable \"content\" {}\n\nresource \"local_file\" \"file\" {\n  content  = var.content\n  filename = \"${path.module}/hi.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-05/.gitignore",
    "content": ".terragrunt-cache\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-05/README.md",
    "content": "Note that this step is the same as the previous step.\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-05/bar/terragrunt.hcl",
    "content": "terraform {\n  source = \"../shared\"\n}\n\ninputs = {\n  content = \"Hello from bar, Terragrunt!\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-05/foo/terragrunt.hcl",
    "content": "terraform {\n  source = \"../shared\"\n}\n\ninputs = {\n  content = \"Hello from foo, Terragrunt!\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-05/shared/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-05/shared/main.tf",
    "content": "variable \"content\" {}\n\nresource \"local_file\" \"file\" {\n  content  = var.content\n  filename = \"${path.module}/hi.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-05.1/.gitignore",
    "content": ".terragrunt-cache\nhi.txt "
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-05.1/README.md",
    "content": "This example demonstrates how to control output file locations using get_terragrunt_dir().\n\nThe hi.txt files will be created directly in the foo and bar directories instead of the .terragrunt-cache directory. "
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-05.1/bar/terragrunt.hcl",
    "content": "terraform {\n  source = \"../shared\"\n}\n\ninputs = {\n  output_dir = get_terragrunt_dir()\n  content    = \"Hello from bar, Terragrunt!\"\n} "
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-05.1/foo/terragrunt.hcl",
    "content": "terraform {\n  source = \"../shared\"\n}\n\ninputs = {\n  output_dir = get_terragrunt_dir()\n  content    = \"Hello from foo, Terragrunt!\"\n} "
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-05.1/shared/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-05.1/shared/main.tf",
    "content": "variable \"content\" {}\nvariable \"output_dir\" {}\n\nresource \"local_file\" \"file\" {\n  content  = var.content\n  filename = \"${var.output_dir}/hi.txt\"\n} "
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-06/.gitignore",
    "content": ".terragrunt-cache\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-06/bar/terragrunt.hcl",
    "content": "terraform {\n  source = \"../shared\"\n}\n\ndependency \"foo\" {\n  config_path = \"../foo\"\n}\n\ninputs = {\n  content = \"Foo content: ${dependency.foo.outputs.content}\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-06/foo/terragrunt.hcl",
    "content": "terraform {\n  source = \"../shared\"\n}\n\ninputs = {\n  content = \"Hello from foo, Terragrunt!\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-06/shared/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-06/shared/main.tf",
    "content": "variable \"content\" {}\n\nresource \"local_file\" \"file\" {\n  content  = var.content\n  filename = \"${path.module}/hi.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-06/shared/output.tf",
    "content": "output \"content\" {\n  value = local_file.file.content\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-07/.gitignore",
    "content": ".terragrunt-cache\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-07/bar/terragrunt.hcl",
    "content": "terraform {\n  source = \"../shared\"\n}\n\ndependency \"foo\" {\n  config_path = \"../foo\"\n\n  mock_outputs = {\n    content = \"Mocked content from foo\"\n  }\n}\n\ninputs = {\n  content = \"Foo content: ${dependency.foo.outputs.content}\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-07/foo/terragrunt.hcl",
    "content": "terraform {\n  source = \"../shared\"\n}\n\ninputs = {\n  content = \"Hello from foo, Terragrunt!\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-07/shared/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-07/shared/main.tf",
    "content": "variable \"content\" {}\n\nresource \"local_file\" \"file\" {\n  content  = var.content\n  filename = \"${path.module}/hi.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-07/shared/output.tf",
    "content": "output \"content\" {\n  value = local_file.file.content\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-07.1/.gitignore",
    "content": ".terragrunt-cache\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-07.1/bar/terragrunt.hcl",
    "content": "terraform {\n  source = \"../shared\"\n}\n\ndependency \"foo\" {\n  config_path = \"../foo\"\n\n  mock_outputs = {\n    content = \"Mocked content from foo\"\n  }\n\n  mock_outputs_allowed_terraform_commands = [\"plan\"]\n}\n\ninputs = {\n  content = \"Foo content: ${dependency.foo.outputs.content}\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-07.1/foo/terragrunt.hcl",
    "content": "terraform {\n  source = \"../shared\"\n}\n\ninputs = {\n  content = \"Hello from foo, Terragrunt!\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-07.1/shared/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-07.1/shared/main.tf",
    "content": "variable \"content\" {}\n\nresource \"local_file\" \"file\" {\n  content  = var.content\n  filename = \"${path.module}/hi.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/01-quick-start/step-07.1/shared/output.tf",
    "content": "output \"content\" {\n  value = local_file.file.content\n}\n"
  },
  {
    "path": "test/fixtures/docs/02-overview/step-01-terragrunt.hcl/terragrunt.hcl",
    "content": "# Configure the remote backend\nremote_state {\n  backend = \"s3\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n\n    key            = \"tofu.tfstate\"\n    region         = \"__FILL_IN_REGION__\"\n    encrypt        = true\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n  }\n}\n\n# Configure the AWS provider\ngenerate \"provider\" {\n  path = \"provider.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nprovider \"aws\" {\n  region = \"__FILL_IN_REGION__\"\n}\nEOF\n}\n\n# Configure the module\n#\n# The URL used here is a shorthand for\n# \"tfr://registry.terraform.io/terraform-aws-modules/vpc/aws?version=5.16.0\".\n#\n# You can find the module at:\n# https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest\n#\n# Note the extra `/` after the `tfr` protocol is required for the shorthand\n# notation.\nterraform {\n  source = \"tfr:///terraform-aws-modules/vpc/aws?version=5.16.0\"\n}\n\n# Configure the inputs for the module\ninputs = {\n  name = \"step-one-vpc\"\n  cidr = \"10.0.0.0/16\"\n\n  azs             = [\"us-east-1a\", \"us-east-1b\", \"us-east-1c\"]\n  private_subnets = [\"10.0.1.0/24\", \"10.0.2.0/24\", \"10.0.3.0/24\"]\n  public_subnets  = [\"10.0.101.0/24\", \"10.0.102.0/24\", \"10.0.103.0/24\"]\n\n  enable_nat_gateway = false\n  enable_vpn_gateway = false\n\n  tags = {\n    IaC = \"true\"\n    Environment = \"dev\"\n  }\n}\n\n"
  },
  {
    "path": "test/fixtures/docs/02-overview/step-02-dependencies/ec2/terragrunt.hcl",
    "content": "# ec2/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"tfr:///terraform-aws-modules/ec2-instance/aws?version=6.2.0\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n\ninputs = {\n  name = \"single-instance\"\n\n  instance_type = \"t2.micro\"\n  monitoring    = true\n  subnet_id     = dependency.vpc.outputs.private_subnets[0]\n\n  tags = {\n    IaC         = \"true\"\n    Environment = \"dev\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/docs/02-overview/step-02-dependencies/root.hcl",
    "content": "remote_state {\n  backend = \"s3\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n\n    key            = \"${path_relative_to_include()}/tofu.tfstate\"\n    region         = \"__FILL_IN_REGION__\"\n    encrypt        = true\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n  }\n}\n\ngenerate \"provider\" {\n  path = \"provider.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nprovider \"aws\" {\n  region = \"__FILL_IN_REGION__\"\n}\nEOF\n}\n"
  },
  {
    "path": "test/fixtures/docs/02-overview/step-02-dependencies/vpc/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"tfr:///terraform-aws-modules/vpc/aws?version=5.16.0\"\n}\n\ninputs = {\n  name = \"step-two-vpc\"\n  cidr = \"10.0.0.0/16\"\n\n  azs             = [\"us-east-1a\", \"us-east-1b\", \"us-east-1c\"]\n  private_subnets = [\"10.0.1.0/24\", \"10.0.2.0/24\", \"10.0.3.0/24\"]\n  public_subnets  = [\"10.0.101.0/24\", \"10.0.102.0/24\", \"10.0.103.0/24\"]\n\n  enable_nat_gateway = false\n  enable_vpn_gateway = false\n\n  tags = {\n    IaC = \"true\"\n    Environment = \"dev\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/docs/02-overview/step-03-mock-outputs/ec2/terragrunt.hcl",
    "content": "# ec2/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"tfr:///terraform-aws-modules/ec2-instance/aws?version=6.2.0\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n\n  mock_outputs = {\n    private_subnets = [\"mock-subnet\"]\n  }\n}\n\ninputs = {\n  name = \"single-instance\"\n\n  instance_type = \"t2.micro\"\n  monitoring    = true\n  subnet_id     = dependency.vpc.outputs.private_subnets[0]\n\n  tags = {\n    IaC         = \"true\"\n    Environment = \"dev\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/docs/02-overview/step-03-mock-outputs/root.hcl",
    "content": "remote_state {\n  backend = \"s3\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n\n    key            = \"${path_relative_to_include()}/tofu.tfstate\"\n    region         = \"__FILL_IN_REGION__\"\n    encrypt        = true\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n  }\n}\n\ngenerate \"provider\" {\n  path = \"provider.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nprovider \"aws\" {\n  region = \"__FILL_IN_REGION__\"\n}\nEOF\n}\n"
  },
  {
    "path": "test/fixtures/docs/02-overview/step-03-mock-outputs/vpc/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"tfr:///terraform-aws-modules/vpc/aws?version=5.16.0\"\n}\n\ninputs = {\n  name = \"step-three-vpc\"\n  cidr = \"10.0.0.0/16\"\n\n  azs             = [\"us-east-1a\", \"us-east-1b\", \"us-east-1c\"]\n  private_subnets = [\"10.0.1.0/24\", \"10.0.2.0/24\", \"10.0.3.0/24\"]\n  public_subnets  = [\"10.0.101.0/24\", \"10.0.102.0/24\", \"10.0.103.0/24\"]\n\n  enable_nat_gateway = false\n  enable_vpn_gateway = false\n\n  tags = {\n    IaC = \"true\"\n    Environment = \"dev\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/docs/02-overview/step-04-configuration-hierarchy/root.hcl",
    "content": "locals {\n  region_hcl = find_in_parent_folders(\"region.hcl\")\n  region     = read_terragrunt_config(local.region_hcl).locals.region\n}\n\nremote_state {\n  backend = \"s3\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n\n    key            = \"${path_relative_to_include()}/tofu.tfstate\"\n    region         = \"__FILL_IN_REGION__\"\n    encrypt        = true\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n  }\n}\n\ngenerate \"provider\" {\n  path = \"provider.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nprovider \"aws\" {\n  region = \"${local.region}\"\n}\nEOF\n}\n"
  },
  {
    "path": "test/fixtures/docs/02-overview/step-04-configuration-hierarchy/us-east-1/ec2/terragrunt.hcl",
    "content": "# ec2/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"tfr:///terraform-aws-modules/ec2-instance/aws?version=6.2.0\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n\n  mock_outputs = {\n    private_subnets = [\"mock-subnet\"]\n  }\n}\n\ninputs = {\n  name = \"single-instance\"\n\n  instance_type = \"t2.micro\"\n  monitoring    = true\n  subnet_id     = dependency.vpc.outputs.private_subnets[0]\n\n  tags = {\n    IaC         = \"true\"\n    Environment = \"dev\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/docs/02-overview/step-04-configuration-hierarchy/us-east-1/region.hcl",
    "content": "locals {\n  region = \"us-east-1\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/02-overview/step-04-configuration-hierarchy/us-east-1/vpc/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"tfr:///terraform-aws-modules/vpc/aws?version=5.16.0\"\n}\n\ninputs = {\n  name = \"step-four-vpc\"\n  cidr = \"10.0.0.0/16\"\n\n  azs             = [\"us-east-1a\", \"us-east-1b\", \"us-east-1c\"]\n  private_subnets = [\"10.0.1.0/24\", \"10.0.2.0/24\", \"10.0.3.0/24\"]\n  public_subnets  = [\"10.0.101.0/24\", \"10.0.102.0/24\", \"10.0.103.0/24\"]\n\n  enable_nat_gateway = false\n  enable_vpn_gateway = false\n\n  tags = {\n    IaC = \"true\"\n    Environment = \"dev\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/docs/02-overview/step-05-exposed-includes/root.hcl",
    "content": "locals {\n  region_hcl = find_in_parent_folders(\"region.hcl\")\n  region     = read_terragrunt_config(local.region_hcl).locals.region\n}\n\nremote_state {\n  backend = \"s3\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n\n    key            = \"${path_relative_to_include()}/tofu.tfstate\"\n    region         = \"__FILL_IN_REGION__\"\n    encrypt        = true\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n  }\n}\n\ngenerate \"provider\" {\n  path = \"provider.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nprovider \"aws\" {\n  region = \"${local.region}\"\n}\nEOF\n}\n"
  },
  {
    "path": "test/fixtures/docs/02-overview/step-05-exposed-includes/us-east-1/ec2/terragrunt.hcl",
    "content": "# ec2/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"tfr:///terraform-aws-modules/ec2-instance/aws?version=6.2.0\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n\n  mock_outputs = {\n    private_subnets = [\"mock-subnet\"]\n  }\n}\n\ninputs = {\n  name = \"single-instance\"\n\n  instance_type = \"t2.micro\"\n  monitoring    = true\n  subnet_id     = dependency.vpc.outputs.private_subnets[0]\n\n  tags = {\n    IaC         = \"true\"\n    Environment = \"dev\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/docs/02-overview/step-05-exposed-includes/us-east-1/region.hcl",
    "content": "locals {\n  region = \"us-east-1\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/02-overview/step-05-exposed-includes/us-east-1/vpc/terragrunt.hcl",
    "content": "include \"root\" {\n  path   = find_in_parent_folders(\"root.hcl\")\n  expose = true\n}\n\nlocals {\n  region = include.root.locals.region\n}\n\nterraform {\n  source = \"tfr:///terraform-aws-modules/vpc/aws?version=5.16.0\"\n}\n\ninputs = {\n  name = \"step-five-vpc-${local.region}\"\n  cidr = \"10.0.0.0/16\"\n\n  azs             = [\"${local.region}a\", \"${local.region}b\", \"${local.region}c\"]\n  private_subnets = [\"10.0.1.0/24\", \"10.0.2.0/24\", \"10.0.3.0/24\"]\n  public_subnets  = [\"10.0.101.0/24\", \"10.0.102.0/24\", \"10.0.103.0/24\"]\n\n  enable_nat_gateway = false\n  enable_vpn_gateway = false\n\n  tags = {\n    IaC = \"true\"\n    Environment = \"dev\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/docs/02-overview/step-05-exposed-includes/us-west-2/ec2/terragrunt.hcl",
    "content": "# ec2/terragrunt.hcl\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"tfr:///terraform-aws-modules/ec2-instance/aws?version=6.2.0\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n\n  mock_outputs = {\n    private_subnets = [\"mock-subnet\"]\n  }\n}\n\ninputs = {\n  name = \"single-instance\"\n\n  instance_type = \"t2.micro\"\n  monitoring    = true\n  subnet_id     = dependency.vpc.outputs.private_subnets[0]\n\n  tags = {\n    IaC         = \"true\"\n    Environment = \"dev\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/docs/02-overview/step-05-exposed-includes/us-west-2/region.hcl",
    "content": "locals {\n  region = \"us-west-2\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/02-overview/step-05-exposed-includes/us-west-2/vpc/terragrunt.hcl",
    "content": "include \"root\" {\n  path   = find_in_parent_folders(\"root.hcl\")\n  expose = true\n}\n\nlocals {\n  region = include.root.locals.region\n}\n\nterraform {\n  source = \"tfr:///terraform-aws-modules/vpc/aws?version=5.16.0\"\n}\n\ninputs = {\n  name = \"step-five-vpc-${local.region}\"\n  cidr = \"10.0.0.0/16\"\n\n  azs             = [\"${local.region}a\", \"${local.region}b\", \"${local.region}c\"]\n  private_subnets = [\"10.0.1.0/24\", \"10.0.2.0/24\", \"10.0.3.0/24\"]\n  public_subnets  = [\"10.0.101.0/24\", \"10.0.102.0/24\", \"10.0.103.0/24\"]\n\n  enable_nat_gateway = false\n  enable_vpn_gateway = false\n\n  tags = {\n    IaC = \"true\"\n    Environment = \"dev\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/docs/03-stacks-with-local-state/.gitignore",
    "content": ".terragrunt-local-state\n"
  },
  {
    "path": "test/fixtures/docs/03-stacks-with-local-state/live/terragrunt.stack.hcl",
    "content": "unit \"foo\" {\n  source = \"${find_in_parent_folders(\"units/basic\")}\"\n  path   = \"foo\"\n}\n\nunit \"bar\" {\n  source = \"${find_in_parent_folders(\"units/basic\")}\"\n  path   = \"bar\"\n}\n\nunit \"baz\" {\n  source = \"${find_in_parent_folders(\"units/basic\")}\"\n  path   = \"baz\"\n}\n"
  },
  {
    "path": "test/fixtures/docs/03-stacks-with-local-state/root.hcl",
    "content": "remote_state {\n  backend = \"local\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    path = \"${get_parent_terragrunt_dir()}/.terragrunt-local-state/${path_relative_to_include()}/tofu.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/docs/03-stacks-with-local-state/units/basic/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/docs/03-stacks-with-local-state/units/basic/main.tf",
    "content": "resource \"null_resource\" \"basic\" {\n  triggers = {\n    hello = \"world\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/docs/03-stacks-with-local-state/units/basic/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/download/custom-lock-file-module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/aws\" {\n  version     = \"5.23.0\"\n  constraints = \"5.23.0\"\n  hashes = [\n    \"h1:8kN8V5QKer9PrSqKqVr2Mc/cr9LvgQHyCArIczMmG+I=\",\n    \"h1:nrvJsxhay2nts34LIUgEtFEOe3ORnShNYyDzkybRj0E=\",\n    \"h1:zLL9flxp5eQ3GWH+HahbdnAx3q/2etmmdEQGefrrM0w=\",\n    \"zh:1d529f875e2a5cbeb80b28c8e3ad41707ec329a7f790a7031868b88705df4965\",\n    \"zh:22f4b50cf437686cdab64d1d822d80518a846b80ab032b13e25ed5b128a74868\",\n    \"zh:25b23f38fd9bc0b07d1dad705d8afec0b1d4c6eab4bae7c707c8f7e4caed8856\",\n    \"zh:5a29c392a116ae44a1ab769efaa699bab5372f3605ec1f1f1749249526a21a76\",\n    \"zh:65a2177174f91c0382621a5e02fd5b077e9270895a20764bf5784f5d38d54f7b\",\n    \"zh:6b5b3bdb1134480c7a038287e5e3772298fcebc4cf7c5b0587c9ee61b15e4cb1\",\n    \"zh:7ec42646e253a29621283aeefd1ec6f1381d166ca6fb3cb7a35a18b05f6f09ef\",\n    \"zh:8950b9f706533ae017d6776db0ed7d165f8b90ffd1f5f79c91bc93d3a666b5e1\",\n    \"zh:bd92ded5eafdcc3bd423ca3477f4eec2737e7b6b2ccb68aedc38fd761de20ae8\",\n    \"zh:d00bd63e362a3ae90d2d9409d2f3adc0198b893637690d6640cc0bcb9b67a066\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/download/custom-lock-file-module/main.tf",
    "content": "# A dirt simple module for use with the custom-lock-file test\n\n# This provider is not actually used in the module, but by having it here, Terraform will download the code for it\n# when we run 'init'. If the lock file copying works as expected in the custom-lock-file test, then we'll end up\n# with an older version of the provider. If there is a bug, Terraform will end up downloading the latest version of\n# the provider, as we're not pinning the version in the Terraform code (only in the lock file).\nterraform {\n  required_providers {\n    aws = {\n      source  = \"registry.opentofu.org/hashicorp/aws\"\n      version = \"5.23.0\"\n    }\n  }\n}\n\nprovider \"aws\" {\n  region = \"eu-west-1\"\n}\n\nvariable \"name\" {\n  description = \"The name to use\"\n  type        = string\n}\n\noutput \"text\" {\n  value = \"Hello, ${var.name}\"\n}"
  },
  {
    "path": "test/fixtures/download/custom-lock-file-terraform/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"terraform init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.terraform.io/hashicorp/aws\" {\n  version     = \"5.23.0\"\n  constraints = \"5.23.0\"\n  hashes = [\n    \"h1:AwjyBYctD8UKCXcm+kLJfRjYdUYzG0hetStKrw8UL9M=\",\n    \"h1:jV3S2mVUT0sc3pxG6XrQLizk5epHYEFd8Eh1Wciw4Mw=\",\n    \"zh:100966f25b1878b7c4ee250dcbaf09e5a2dad4bcebba2482d77c4cc4e48957da\",\n    \"zh:57ed5e66949568d25788ebcd170abf5961f81bb141f69d3acca9a7454994d0c5\",\n    \"zh:5acf55f8901d5443b6994463d7b2dcbb137a242486f47963e0f33c4cce30171a\",\n    \"zh:7036770df1223d15e0982be39bedf32b2e2cae1eabac717138cbc90bbf94e30e\",\n    \"zh:79f3f151984a97a7dee14e74ca9d9926b2add30982fe44a450645b89a6da6e00\",\n    \"zh:8a1b0bc5e237609fc1ad7af17e15a95f93a56c3403c0d022d94163ac1989507c\",\n    \"zh:94f3baf6a3ba728e31844d6786dae9aa505323389c6323e2eb820a3c81e82229\",\n    \"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425\",\n    \"zh:ac4059a4f45c77432897605efb3642451c125ddddabe14d36a4a85dad13ae6cb\",\n    \"zh:d2a8d1c9a9100ae3fec34f119d3a90faefb89bf93780fc6934898533c6900cba\",\n    \"zh:de647167adb585a54cfbfc4c5d204c5d0a444624d386a773eae75789aa75f363\",\n    \"zh:edb533b3df81f2d1ef7387380cab843877f3f3c756f7a87cbba1961b3f01e4a2\",\n    \"zh:f56491ecb31b1ebde35cbfe8261e3c82c983b3039837f8756834cf27018bd93a\",\n    \"zh:fba46b50c35e40ea27947f4305320aaa61cdc22812b138571841e9bf8c7f5db9\",\n    \"zh:fcb92b5c6fbb70ae9137291ffc8ef06c48daec9cf0fafb980d178fe925658160\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/download/custom-lock-file-terraform/terragrunt.hcl",
    "content": "inputs = {\n  name = \"World\"\n}\n\nterraform {\n  source = \"../custom-lock-file-module\"\n}\n"
  },
  {
    "path": "test/fixtures/download/custom-lock-file-tofu/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/aws\" {\n  version     = \"5.23.0\"\n  constraints = \"5.23.0\"\n  hashes = [\n    \"h1:nrvJsxhay2nts34LIUgEtFEOe3ORnShNYyDzkybRj0E=\",\n    \"zh:1d529f875e2a5cbeb80b28c8e3ad41707ec329a7f790a7031868b88705df4965\",\n    \"zh:22f4b50cf437686cdab64d1d822d80518a846b80ab032b13e25ed5b128a74868\",\n    \"zh:25b23f38fd9bc0b07d1dad705d8afec0b1d4c6eab4bae7c707c8f7e4caed8856\",\n    \"zh:5a29c392a116ae44a1ab769efaa699bab5372f3605ec1f1f1749249526a21a76\",\n    \"zh:65a2177174f91c0382621a5e02fd5b077e9270895a20764bf5784f5d38d54f7b\",\n    \"zh:6b5b3bdb1134480c7a038287e5e3772298fcebc4cf7c5b0587c9ee61b15e4cb1\",\n    \"zh:7ec42646e253a29621283aeefd1ec6f1381d166ca6fb3cb7a35a18b05f6f09ef\",\n    \"zh:8950b9f706533ae017d6776db0ed7d165f8b90ffd1f5f79c91bc93d3a666b5e1\",\n    \"zh:bd92ded5eafdcc3bd423ca3477f4eec2737e7b6b2ccb68aedc38fd761de20ae8\",\n    \"zh:d00bd63e362a3ae90d2d9409d2f3adc0198b893637690d6640cc0bcb9b67a066\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/download/custom-lock-file-tofu/terragrunt.hcl",
    "content": "inputs = {\n  name = \"World\"\n}\n\nterraform {\n  source = \"../custom-lock-file-module\"\n}\n"
  },
  {
    "path": "test/fixtures/download/extra-args/common.tfvars",
    "content": "name = \"extra args\""
  },
  {
    "path": "test/fixtures/download/hello-world/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2, ~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/download/hello-world/hello/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/download/hello-world/hello/main.tf",
    "content": "output \"hello\" {\n  value = \"Hello\"\n}"
  },
  {
    "path": "test/fixtures/download/hello-world/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nvariable \"name\" {\n  description = \"Specify a name\"\n  type        = string\n}\n\nmodule \"hello\" {\n  source = \"./hello\"\n}\n\nresource \"null_resource\" \"test\" {\n  provisioner \"local-exec\" {\n    command = \"echo '${module.hello.hello}, ${var.name}'\"\n  }\n}\n\noutput \"test\" {\n  value = \"${module.hello.hello}, ${var.name}\"\n}\n\nmodule \"remote\" {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/download/hello-world-no-remote?ref=v0.93.2\"\n  name   = var.name\n}\n"
  },
  {
    "path": "test/fixtures/download/hello-world-no-remote/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/download/hello-world-no-remote/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2\"\n    }\n  }\n}\n\nvariable \"name\" {\n  description = \"Specify a name\"\n  type        = string\n}\n\nmodule \"hello\" {\n  source = \"..//hello-world/hello\"\n}\n\nresource \"null_resource\" \"test\" {\n  provisioner \"local-exec\" {\n    command = \"echo '${module.hello.hello}, ${var.name}'\"\n  }\n}\n\noutput \"test\" {\n  value = \"${module.hello.hello}, ${var.name}\"\n}\n"
  },
  {
    "path": "test/fixtures/download/hello-world-with-backend/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/download/hello-world-with-backend/main.tf",
    "content": "terraform {\n  # These settings will be filled in by Terragrunt\n  backend \"s3\" {}\n\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2\"\n    }\n  }\n}\n\nvariable \"name\" {\n  description = \"Specify a name\"\n  type        = string\n}\n\nresource \"null_resource\" \"test\" {\n  provisioner \"local-exec\" {\n    command = \"echo 'hello, ${var.name}'\"\n  }\n}\n\noutput \"test\" {\n  value = \"hello, ${var.name}\"\n}\n"
  },
  {
    "path": "test/fixtures/download/init-on-source-change/terragrunt.hcl",
    "content": "terraform {\n  // v0.35.1 v0.35.2\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/dirs?ref=__TAG_VALUE__\"\n}\n"
  },
  {
    "path": "test/fixtures/download/invalid-path/terragrunt.hcl",
    "content": "terraform {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/download/non-existent-path?ref=v0.83.2\"\n}\n"
  },
  {
    "path": "test/fixtures/download/local/terragrunt.hcl",
    "content": "inputs = {\n  name = \"World\"\n}\n\nterraform {\n  source = \"../hello-world\"\n}\n"
  },
  {
    "path": "test/fixtures/download/local-disable-copy-terraform-lock-file/terragrunt.hcl",
    "content": "inputs = {\n  name = \"World\"\n}\n\nterraform {\n  source                   = \"../hello-world\"\n  copy_terraform_lock_file = false\n}\n"
  },
  {
    "path": "test/fixtures/download/local-include-disable-copy-lock-file/module-a/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module A\"\n}\n\nterraform {\n  source = \"../../hello-world\"\n}\n"
  },
  {
    "path": "test/fixtures/download/local-include-disable-copy-lock-file/module-b/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module B\"\n}\n\nterraform {\n  source = \"../../hello-world\"\n  copy_terraform_lock_file = false\n}\n\nprevent_destroy = true\n\ninclude {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/download/local-include-disable-copy-lock-file/root.hcl",
    "content": "terraform {\n  include_in_copy = [\"**/.terraform-version\"]\n}\n\ndependencies {\n  paths = [\"../module-a\"]\n}"
  },
  {
    "path": "test/fixtures/download/local-include-with-prevent-destroy-dependencies/module-a/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module A\"\n}\n\nterraform {\n  source = \"../../hello-world\"\n}\n"
  },
  {
    "path": "test/fixtures/download/local-include-with-prevent-destroy-dependencies/module-b/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module B\"\n}\n\nterraform {\n  source = \"../../hello-world\"\n}\n\nprevent_destroy = true\n\ninclude {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/download/local-include-with-prevent-destroy-dependencies/module-c/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module C\"\n}\n\nterraform {\n  source = \"../../hello-world\"\n}\n"
  },
  {
    "path": "test/fixtures/download/local-include-with-prevent-destroy-dependencies/root.hcl",
    "content": "dependencies {\n  paths = [\"../module-a\"]\n}\n"
  },
  {
    "path": "test/fixtures/download/local-no-source/main.tf",
    "content": "# Simple terraform file for testing cache creation without source\n\nvariable \"test_value\" {\n  description = \"Test value\"\n  type        = string\n  default     = \"default\"\n}\n\noutput \"test_output\" {\n  value = var.test_value\n}\n"
  },
  {
    "path": "test/fixtures/download/local-no-source/terragrunt.hcl",
    "content": "# Minimal terragrunt config without terraform.source block\n# This tests that .terragrunt-cache is still created when no source is specified\n\ninputs = {\n  test_value = \"no-source-test\"\n}\n"
  },
  {
    "path": "test/fixtures/download/local-relative/terragrunt.hcl",
    "content": "inputs = {\n  name = \"World\"\n}\n\nterraform {\n  source = \"..//relative\"\n}\n"
  },
  {
    "path": "test/fixtures/download/local-relative-extra-args-unix/terragrunt.hcl",
    "content": "terraform {\n  source = \"../hello-world\"\n\n  extra_arguments \"custom_vars\" {\n    commands = [\n      \"apply\",\n      \"plan\",\n      \"import\",\n      \"push\",\n      \"refresh\"\n    ]\n\n    arguments = [\n      \"-var-file=${get_terragrunt_dir()}/../extra-args/common.tfvars\"\n    ]\n  }\n}"
  },
  {
    "path": "test/fixtures/download/local-windows/JZwoL6Viko8bzuRvTOQFx3Jh8vs/3mU4huxMLOXOW5ZgJOFXGUFDKc8/hello/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/download/local-windows/JZwoL6Viko8bzuRvTOQFx3Jh8vs/3mU4huxMLOXOW5ZgJOFXGUFDKc8/hello/main.tf",
    "content": "output \"hello\" {\n  value = \"Hello\"\n}"
  },
  {
    "path": "test/fixtures/download/local-windows/JZwoL6Viko8bzuRvTOQFx3Jh8vs/3mU4huxMLOXOW5ZgJOFXGUFDKc8/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nvariable \"name\" {\n  description = \"Specify a name\"\n  type        = string\n}\n\nmodule \"hello\" {\n  source = \"./hello\"\n}\n\nresource \"null_resource\" \"test\" {\n  provisioner \"local-exec\" {\n    command = \"echo '${module.hello.hello}, ${var.name}'\"\n  }\n}\n\noutput \"test\" {\n  value = \"${module.hello.hello}, ${var.name}\"\n}\n\nmodule \"remote\" {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/download/hello-world?ref=v0.83.2\"\n  name   = var.name\n}\n"
  },
  {
    "path": "test/fixtures/download/local-windows/terragrunt.hcl",
    "content": "terraform {\r\n  source = \"..\\\\hello-world\"\r\n\r\n  extra_arguments \"custom_vars\" {\r\n    commands = [\r\n      \"apply\",\r\n      \"plan\",\r\n      \"import\",\r\n      \"push\",\r\n      \"refresh\"\r\n    ]\r\n\r\n    arguments = [\r\n      \"-var-file=${get_terragrunt_dir()}\\\\..\\\\extra-args\\\\common.tfvars\"\r\n    ]\r\n  }\r\n}"
  },
  {
    "path": "test/fixtures/download/local-with-allowed-hidden/live/terragrunt.hcl",
    "content": "terraform {\n  source          = \"../modules\"\n  include_in_copy = [\".nonce\"]\n}\n"
  },
  {
    "path": "test/fixtures/download/local-with-allowed-hidden/modules/.nonce",
    "content": "Hello world\n"
  },
  {
    "path": "test/fixtures/download/local-with-allowed-hidden/modules/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/download/local-with-allowed-hidden/modules/main.tf",
    "content": "output \"text\" {\n  value = trimspace(file(\".nonce\"))\n}\n"
  },
  {
    "path": "test/fixtures/download/local-with-backend/terragrunt.hcl",
    "content": "inputs = {\n  name = \"World\"\n}\n\nterraform {\n  source = \"../hello-world-with-backend\"\n}\n\n# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"terraform.tfstate\"\n    region = \"us-west-2\"\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n  }\n}"
  },
  {
    "path": "test/fixtures/download/local-with-exclude-dir/integration-env/aws/module-aws-a/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module AWS A\"\n}\n\nterraform {\n  source = \"../../../..//hello-world-no-remote\"\n}\n"
  },
  {
    "path": "test/fixtures/download/local-with-exclude-dir/integration-env/gce/module-gce-b/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module GCE B\"\n}\n\nterraform {\n  source = \"../../../..//hello-world-no-remote\"\n}\n\ndependencies {\n  paths = [\"../../aws/module-aws-a\"]\n}\n"
  },
  {
    "path": "test/fixtures/download/local-with-exclude-dir/integration-env/gce/module-gce-c/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module GCE C\"\n}\n\nterraform {\n  source = \"../../../..//hello-world-no-remote\"\n}\n"
  },
  {
    "path": "test/fixtures/download/local-with-exclude-dir/production-env/aws/module-aws-d/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module AWS D\"\n}\n\nterraform {\n  source = \"../../../..//hello-world-no-remote\"\n}\n"
  },
  {
    "path": "test/fixtures/download/local-with-exclude-dir/production-env/gce/module-gce-e/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module GCE E\"\n}\n\nterraform {\n  source = \"../../../..//hello-world-no-remote\"\n}\n\ndependencies {\n  paths = [\"../../aws/module-aws-d\"]\n}\n"
  },
  {
    "path": "test/fixtures/download/local-with-hidden-folder/.hidden-folder/README.md",
    "content": "# Hidden folder\n\nThis is a \"hidden folder\" (one that starts with a dot) that we use to check that Terragrunt does not copy hidden \nfolders--such as .git or .terraform--to the temporary folder when downloading remote Terraform configurations.\n \nThis file has intentionally been made read-only so that if it is copied to the temp folder, the second time you run \nTerragrunt, it will exit with an error as it tries to overwrite this read-only file."
  },
  {
    "path": "test/fixtures/download/local-with-hidden-folder/terragrunt.hcl",
    "content": "inputs = {\n  name = \"World\"\n}\n\nterraform {\n  source = \"../hello-world\"\n}\n"
  },
  {
    "path": "test/fixtures/download/local-with-include-dir/integration-env/aws/module-aws-a/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module AWS A\"\n}\n\nterraform {\n  source = \"../../../..//hello-world-no-remote\"\n}\n"
  },
  {
    "path": "test/fixtures/download/local-with-include-dir/integration-env/gce/module-gce-b/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module GCE B\"\n}\n\nterraform {\n  source = \"../../../..//hello-world-no-remote\"\n}\n\ndependencies {\n  paths = [\"../../aws/module-aws-a\"]\n}\n"
  },
  {
    "path": "test/fixtures/download/local-with-include-dir/integration-env/gce/module-gce-c/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module GCE C\"\n}\n\nterraform {\n  source = \"../../../..//hello-world-no-remote\"\n}\n"
  },
  {
    "path": "test/fixtures/download/local-with-include-dir/production-env/aws/module-aws-d/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module AWS D\"\n}\n\nterraform {\n  source = \"../../../..//hello-world-no-remote\"\n}\n"
  },
  {
    "path": "test/fixtures/download/local-with-include-dir/production-env/gce/module-gce-e/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module GCE E\"\n}\n\nterraform {\n  source = \"../../../..//hello-world-no-remote\"\n}\n\ndependencies {\n  paths = [\"../../aws/module-aws-d\"]\n}\n\n"
  },
  {
    "path": "test/fixtures/download/local-with-missing-backend/terragrunt.hcl",
    "content": "inputs = {\n  name = \"World\"\n}\n\nterraform {\n  source = \"../hello-world\"\n}\n\n# We configure remote state here, but the module in the source parameter does not specify a backend, so we should\n# get an error when trying to use this module\nremote_state {\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"terraform.tfstate\"\n    region = \"us-west-2\"\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n  }\n}"
  },
  {
    "path": "test/fixtures/download/local-with-prevent-destroy/terragrunt.hcl",
    "content": "inputs = {\n  name = \"World\"\n}\n\nterraform {\n  source = \"../hello-world\"\n}\n\nprevent_destroy = true\n"
  },
  {
    "path": "test/fixtures/download/local-with-prevent-destroy-dependencies/module-a/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module A\"\n}\n\nterraform {\n  source = \"../../hello-world\"\n}\n"
  },
  {
    "path": "test/fixtures/download/local-with-prevent-destroy-dependencies/module-b/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module B\"\n}\n\nterraform {\n  source = \"../../hello-world\"\n}\n\ndependencies {\n  paths = [\"../module-a\"]\n}\n\nprevent_destroy = true\n"
  },
  {
    "path": "test/fixtures/download/local-with-prevent-destroy-dependencies/module-c/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module C\"\n}\n\nterraform {\n  source = \"../../hello-world\"\n}\n"
  },
  {
    "path": "test/fixtures/download/local-with-prevent-destroy-dependencies/module-d/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module D\"\n}\n\nterraform {\n  source = \"../../hello-world\"\n}\n\ndependencies {\n  paths = [\"../module-c\"]\n}\n"
  },
  {
    "path": "test/fixtures/download/local-with-prevent-destroy-dependencies/module-e/terragrunt.hcl",
    "content": "inputs = {\n  name = \"Module E\"\n}\n\nterraform {\n  source = \"../../hello-world\"\n}\n"
  },
  {
    "path": "test/fixtures/download/override/terragrunt.hcl",
    "content": "inputs = {\n  name = \"World\"\n}\n\n# This URL is intentionally invalid, as it should be overridden in the test case via command-line params\nterraform {\n  source = \"invalid-url-should-be-overridden-at-test-time\"\n}\n"
  },
  {
    "path": "test/fixtures/download/relative/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/download/relative/main.tf",
    "content": "module \"foo\" {\n  source = \"../hello-world-no-remote\"\n  name   = var.name\n}\n\nvariable \"name\" {\n  description = \"Specify a name\"\n}\n\noutput \"test\" {\n  value = module.foo.test\n}\n"
  },
  {
    "path": "test/fixtures/download/remote/terragrunt.hcl",
    "content": "inputs = {\n  name = \"World\"\n}\n\nterraform {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/download/hello-world-no-remote?ref=v0.93.2\"\n}\n"
  },
  {
    "path": "test/fixtures/download/remote-invalid/terragrunt.hcl",
    "content": "terraform {\n  source = \"https://github.com/totallyfakedoesnotexist/notreal.git//foo?ref=v1.2.3\"\n}\n"
  },
  {
    "path": "test/fixtures/download/remote-invalid-with-retries/terragrunt.hcl",
    "content": "terraform {\n  source = \"https://github.com/totallyfakedoesnotexist/notreal.git//foo?ref=v1.2.3\"\n\n}\n\n errors {\n   retry \"any\" {\n     retryable_errors = [\".*\"]\n     max_attempts = 2\n     sleep_interval_sec = 1\n   }\n}\n\n"
  },
  {
    "path": "test/fixtures/download/remote-module-in-root/terragrunt.hcl",
    "content": "terraform {\n  source = \"github.com/gruntwork-io/terraform-module-in-root-for-terragrunt-test.git\"\n}\n"
  },
  {
    "path": "test/fixtures/download/remote-ref/terragrunt.hcl",
    "content": "inputs = {\n  name = \"World\"\n}\n\nterraform {\n  source = \"git::git@github.com:gruntwork-io/terragrunt.git//test/fixtures/download/hello-world-no-remote?ref=v0.93.2\"\n}\n"
  },
  {
    "path": "test/fixtures/download/remote-relative/terragrunt.hcl",
    "content": "inputs = {\n  name = \"World\"\n}\n\nterraform {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/download/relative?ref=v0.93.2\"\n}\n"
  },
  {
    "path": "test/fixtures/download/remote-relative-with-slash/terragrunt.hcl",
    "content": "inputs = {\n  name = \"World\"\n}\n\nterraform {\n  source = \"github.com/gruntwork-io/terragrunt.git?ref=v0.93.2//test/fixtures/download/relative\"\n}\n"
  },
  {
    "path": "test/fixtures/download/remote-with-backend/terragrunt.hcl",
    "content": "inputs = {\n  name = \"World\"\n}\n\nterraform {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/download/hello-world-with-backend?ref=v0.83.2\"\n}\n\n# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"terraform.tfstate\"\n    region = \"us-west-2\"\n    # Intentionally keeping this at the old (deprecated) name of \"lock_table\" instead of \"dynamodb_table\" to test for\n    # backwards compatibility\n    lock_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/download/stdout/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/download/stdout/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"foo\" {\n  provisioner \"local-exec\" {\n    command = \"echo foo\"\n  }\n}\n\noutput \"foo\" {\n  value = \"foo\"\n}\n"
  },
  {
    "path": "test/fixtures/download/stdout-test/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/download/stdout-test/terragrunt.hcl",
    "content": "terraform {\n  source = \"../stdout\"\n}\n"
  },
  {
    "path": "test/fixtures/download-source/download-dir-version-file/version-file.txt",
    "content": "zqg_v-R2bnqTbCe-ZO3mHRTRKX0"
  },
  {
    "path": "test/fixtures/download-source/download-dir-version-file-local-hash/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/download-source/download-dir-version-file-local-hash/main.tf",
    "content": "# Local file hash test"
  },
  {
    "path": "test/fixtures/download-source/download-dir-version-file-local-hash/version-file.txt",
    "content": "h1:iZ9U+r4EPyqBU2l8Ih0StqixGyMnZTNoNqSGeEMT0QI="
  },
  {
    "path": "test/fixtures/download-source/download-dir-version-file-no-query/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/download-source/download-dir-version-file-no-query/main.tf",
    "content": "# This file is just a placeholder"
  },
  {
    "path": "test/fixtures/download-source/download-dir-version-file-no-query/version-file.txt",
    "content": "2jmj7l5rSw0yVb_vlWAYkK_YBwk"
  },
  {
    "path": "test/fixtures/download-source/download-dir-version-file-tf-code/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/download-source/download-dir-version-file-tf-code/main.tf",
    "content": "# This file is just a placeholder"
  },
  {
    "path": "test/fixtures/download-source/download-dir-version-file-tf-code/version-file.txt",
    "content": "zqg_v-R2bnqTbCe-ZO3mHRTRKX0"
  },
  {
    "path": "test/fixtures/download-source/hello-world/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/download-source/hello-world/main.tf",
    "content": "# Hello, World"
  },
  {
    "path": "test/fixtures/download-source/hello-world-2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/download-source/hello-world-2/main.tf",
    "content": "# Hello, World 2"
  },
  {
    "path": "test/fixtures/download-source/hello-world-2/version-file.txt",
    "content": "2jmj7l5rSw0yVb_vlWAYkK_YBwk"
  },
  {
    "path": "test/fixtures/download-source/hello-world-local-hash/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/download-source/hello-world-local-hash/main.tf",
    "content": "# Local file hash test"
  },
  {
    "path": "test/fixtures/download-source/hello-world-local-hash-failed/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/download-source/hello-world-local-hash-failed/main.tf",
    "content": "# Local file hash test"
  },
  {
    "path": "test/fixtures/download-source/hello-world-version-remote/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/download-source/hello-world-version-remote/main.tf",
    "content": "# Hello, World version remote"
  },
  {
    "path": "test/fixtures/download-source/hello-world-version-remote/version-file.txt",
    "content": "VfXVGR1zzP_GpoK2lMFIlJyW-ko"
  },
  {
    "path": "test/fixtures/empty-state/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/empty-state/main.tf",
    "content": "\n"
  },
  {
    "path": "test/fixtures/empty-state/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/endswith/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/endswith/main.tf",
    "content": "variable \"endswith1\" {\n  type = bool\n}\n\nvariable \"endswith2\" {\n  type = bool\n}\n\nvariable \"endswith3\" {\n  type = bool\n}\n\nvariable \"endswith4\" {\n  type = bool\n}\n\nvariable \"endswith5\" {\n  type = bool\n}\n\nvariable \"endswith6\" {\n  type = bool\n}\n\nvariable \"endswith7\" {\n  type = bool\n}\n\nvariable \"endswith8\" {\n  type = bool\n}\n\nvariable \"endswith9\" {\n  type = bool\n}\n\noutput \"endswith1\" {\n  value = var.endswith1\n}\n\noutput \"endswith2\" {\n  value = var.endswith2\n}\n\noutput \"endswith3\" {\n  value = var.endswith3\n}\n\noutput \"endswith4\" {\n  value = var.endswith4\n}\n\noutput \"endswith5\" {\n  value = var.endswith5\n}\n\noutput \"endswith6\" {\n  value = var.endswith6\n}\n\noutput \"endswith7\" {\n  value = var.endswith7\n}\n\noutput \"endswith8\" {\n  value = var.endswith8\n}\n\noutput \"endswith9\" {\n  value = var.endswith9\n}\n\n\n"
  },
  {
    "path": "test/fixtures/endswith/terragrunt.hcl",
    "content": "inputs = {\n  endswith1 = endswith(\"hello world\", \"world\")\n  endswith2 = endswith(\"hello world\", \"hello\")\n  endswith3 = endswith(\"hello world\", \"\")\n  endswith4 = endswith(\"hello world\", \" \")\n  endswith5 = endswith(\"\", \"\")\n  endswith6 = endswith(\"\", \" \")\n  endswith7 = endswith(\" \", \"\")\n  endswith8 = endswith(\"\", \"hello\")\n  endswith9 = endswith(\" \", \"hello\")\n}\n"
  },
  {
    "path": "test/fixtures/engine/engine-dependencies/.gitignore",
    "content": "backend.gen.tf\ntest.txt\n"
  },
  {
    "path": "test/fixtures/engine/engine-dependencies/app1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/engine/engine-dependencies/app1/main.tf",
    "content": "output \"value\" {\n  value = \"app1-test\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/engine-dependencies/app1/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\n"
  },
  {
    "path": "test/fixtures/engine/engine-dependencies/app2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/engine/engine-dependencies/app2/main.tf",
    "content": "variable \"app1_output\" {\n  type = string\n}\n\nresource \"local_file\" \"test\" {\n  content  = var.app1_output\n  filename = \"${path.module}/test.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/engine-dependencies/app2/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"app1\" {\n  config_path = \"../app1\"\n\n  mock_outputs = {\n    value = \"app1-test\"\n  }\n}\n\ninputs = {\n  app1_output = dependency.app1.outputs.value\n}\n"
  },
  {
    "path": "test/fixtures/engine/engine-dependencies/root.hcl",
    "content": "remote_state {\n  backend = \"local\"\n  generate = {\n    path      = \"backend.gen.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n  config = {\n    path = \"${get_terragrunt_dir()}/terraform.tfstate\"\n  }\n}\n\nengine {\n  source  = \"github.com/gruntwork-io/terragrunt-engine-opentofu\"\n  version = \"v0.1.0\"\n  type    = \"rpc\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/local-engine/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/engine/local-engine/main.tf",
    "content": "\nvariable \"value\" {}\n\nresource \"local_file\" \"test\" {\n  content  = \"test ${var.value}\"\n  filename = \"${path.module}/test.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/local-engine/terragrunt.hcl",
    "content": "engine {\n  source  = \"__engine_source__\"\n}\n\ninputs = {\n  value = \"test_input_value_from_terragrunt\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-engine/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-engine/main.tf",
    "content": "\nvariable \"value\" {}\n\nresource \"local_file\" \"test\" {\n  content  = \"test ${var.value}\"\n  filename = \"${path.module}/test.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-engine/terragrunt.hcl",
    "content": "engine {\n  source  = \"github.com/gruntwork-io/terragrunt-engine-opentofu\"\n  version = \"v0.1.0\"\n  type    = \"rpc\"\n}\n\ninputs = {\n  value = \"test_input_value_from_terragrunt\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-latest-run-all/app1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-latest-run-all/app1/main.tf",
    "content": "\nresource \"local_file\" \"test\" {\n  content  = \"app1\"\n  filename = \"${path.module}/test.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-latest-run-all/app1/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-latest-run-all/app2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-latest-run-all/app2/main.tf",
    "content": "\nresource \"local_file\" \"test\" {\n  content  = \"app1\"\n  filename = \"${path.module}/test.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-latest-run-all/app2/terragrunt.hcl",
    "content": "include {\n  path   = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-latest-run-all/app3/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-latest-run-all/app3/main.tf",
    "content": "\nresource \"local_file\" \"test\" {\n  content  = \"app1\"\n  filename = \"${path.module}/test.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-latest-run-all/app3/terragrunt.hcl",
    "content": "include {\n  path   = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-latest-run-all/app4/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-latest-run-all/app4/main.tf",
    "content": "\nresource \"local_file\" \"test\" {\n  content  = \"app1\"\n  filename = \"${path.module}/test.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-latest-run-all/app4/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-latest-run-all/app5/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-latest-run-all/app5/main.tf",
    "content": "\nresource \"local_file\" \"test\" {\n  content  = \"app1\"\n  filename = \"${path.module}/test.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-latest-run-all/app5/terragrunt.hcl",
    "content": "include {\n  path   = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-latest-run-all/root.hcl",
    "content": "engine {\n  // use latest OpenTofu engine to do basic validation of implementation\n  source  = \"github.com/gruntwork-io/terragrunt-engine-opentofu\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-run-all/app1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-run-all/app1/main.tf",
    "content": "\nresource \"local_file\" \"test\" {\n  content  = \"app1\"\n  filename = \"${path.module}/test.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-run-all/app1/terragrunt.hcl",
    "content": "include {\n  path   = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-run-all/app2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-run-all/app2/main.tf",
    "content": "\nresource \"local_file\" \"test\" {\n  content  = \"app1\"\n  filename = \"${path.module}/test.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-run-all/app2/terragrunt.hcl",
    "content": "include {\n  path   = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-run-all/app3/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-run-all/app3/main.tf",
    "content": "\nresource \"local_file\" \"test\" {\n  content  = \"app1\"\n  filename = \"${path.module}/test.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-run-all/app3/terragrunt.hcl",
    "content": "include {\n  path   = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-run-all/app4/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-run-all/app4/main.tf",
    "content": "\nresource \"local_file\" \"test\" {\n  content  = \"app1\"\n  filename = \"${path.module}/test.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-run-all/app4/terragrunt.hcl",
    "content": "include {\n  path   = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-run-all/app5/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-run-all/app5/main.tf",
    "content": "\nresource \"local_file\" \"test\" {\n  content  = \"app1\"\n  filename = \"${path.module}/test.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-run-all/app5/terragrunt.hcl",
    "content": "include {\n  path   = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/engine/opentofu-run-all/root.hcl",
    "content": "engine {\n  source  = \"github.com/gruntwork-io/terragrunt-engine-opentofu\"\n  version = \"v0.1.0\"\n  type    = \"rpc\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/remote-engine/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/engine/remote-engine/main.tf",
    "content": "\nvariable \"value\" {}\n\nresource \"local_file\" \"test\" {\n  content  = \"test ${var.value}\"\n  filename = \"${path.module}/test.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/remote-engine/terragrunt.hcl",
    "content": "engine {\n  source  = \"__hardcoded_url__\"\n}\n\ninputs = {\n  value = \"test_input_value_from_terragrunt\"\n}\n"
  },
  {
    "path": "test/fixtures/engine/trace-parent/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/external\" {\n  version = \"2.3.5\"\n  hashes = [\n    \"h1:+OsaKKx2awgjh6j/2B3VBP6q4Dqg2Fc0uDUZll66/Hg=\",\n    \"h1:VsIY+hWGvWHaGvGTSKZslY13lPeAtSTxfZRPbpLMMhs=\",\n    \"h1:jcVmeuuz74tdRt2kj0MpUG9AORdlAlRRQ3k61y0r5Vc=\",\n    \"zh:1fb9aca1f068374a09d438dba84c9d8ba5915d24934a72b6ef66ef6818329151\",\n    \"zh:3eab30e4fcc76369deffb185b4d225999fc82d2eaaa6484d3b3164a4ed0f7c49\",\n    \"zh:4f8b7a4832a68080f0bf4f155b56a691832d8a91ce8096dac0f13a90081abc50\",\n    \"zh:5ff1935612db62e48e4fe6cfb83dfac401b506a5b7b38342217616fbcab70ce0\",\n    \"zh:993192234d327ec86726041eb6d1efb001e41f32e4518ad8b9b162130b65ee9a\",\n    \"zh:ce445e68282a2c4b2d1f994a2730406df4ea47914c0932fb4a7eb040a7ec7061\",\n    \"zh:e305e17216840c54194141fb852839c2cedd6b41abd70cf8d606d6e88ed40e64\",\n    \"zh:edba65fb241d663c09aa2cbf75026c840e963d5195f27000f216829e49811437\",\n    \"zh:f306cc6f6ec9beaf75bdcefaadb7b77af320b1f9b56d8f50df5ebd2189a93148\",\n    \"zh:fb2ff9e1f86796fda87e1f122d40568912a904da51d477461b850d81a0105f3d\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/engine/trace-parent/get_traceparent.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\necho \"$1 {\\\"traceparent\\\": \\\"${TRACEPARENT}\\\"}\"\n"
  },
  {
    "path": "test/fixtures/engine/trace-parent/main.tf",
    "content": "data \"external\" \"traceparent\" {\n  program = [\"${path.module}/get_traceparent.sh\"]\n\n  query = {\n    nonce = timestamp()\n  }\n}\n\noutput \"traceparent_value\" {\n  value = data.external.traceparent.result[\"traceparent\"]\n}\n"
  },
  {
    "path": "test/fixtures/engine/trace-parent/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n  before_hook \"hook_print_traceparent\" {\n    commands = [\"apply\"]\n    execute = [\"./get_traceparent.sh\", \"hook_print_traceparent\"]\n  }\n}\n\nengine {\n  source  = \"github.com/gruntwork-io/terragrunt-engine-opentofu\"\n  version = \"v0.1.0\"\n  type    = \"rpc\"\n}\n"
  },
  {
    "path": "test/fixtures/env-vars-block/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/env-vars-block/main.tf",
    "content": "variable \"custom_var\" {\n  description = \"Provided as TF_VAR_custom_var in terraform.tfvars\"\n}\n\noutput \"test\" {\n  value = var.custom_var\n}\n"
  },
  {
    "path": "test/fixtures/env-vars-block/terragrunt.hcl",
    "content": "terraform {\n  extra_arguments \"test\" {\n    commands = [\"apply\"]\n    env_vars = {\n      TF_VAR_custom_var = \"I'm set in extra_arguments env_vars\"\n    }\n  }\n\n  extra_arguments \"shouldnotapply\" {\n    commands = [ \"refresh\" ]\n\n    env_vars = {\n      TF_VAR_custom_var = \"I'm only set for refresh command, so will be ignored for apply\"\n    }\n\n  }\n}\n"
  },
  {
    "path": "test/fixtures/ephemeral-inputs/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/ephemeral-inputs/main.tf",
    "content": "variable \"test\" {\n  type      = string\n  default   = \"test_value\"\n  ephemeral = true\n}\n\noutput \"output\" {\n  value = ephemeralasnull(var.test)\n}\n\nresource \"local_file\" \"file\" {\n  content  = \"test\"\n  filename = \"${path.module}/test.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/ephemeral-inputs/terragrunt.hcl",
    "content": "\ninputs = {\n  test = \"test input 46521694\"\n}\n"
  },
  {
    "path": "test/fixtures/error-print/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/error-print/custom-tf-script.sh",
    "content": "#!/usr/bin/env bash\n\necho \"Custom error from script\" >&2\n\nexit 1\n"
  },
  {
    "path": "test/fixtures/error-print/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/error-print/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/errors/default/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/errors/default/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/errors/default/terragrunt.hcl",
    "content": "feature \"feature_name\" {\n  default = false\n}\n\nerrors {\n  # Retry configuration block that allows for retrying errors that are known to be intermittent\n  # Note that this replaces `retryable_errors`, `retry_max_attempts` and `retry_sleep_interval_sec` fields.\n  # Those fields will still be supported for backwards compatibility, but this block will take precedence.\n  retry \"foo\" {\n    retryable_errors = !feature.feature_name.value ? [] : [\n      \".*Error: foo.*\"\n    ]\n    max_attempts       = 3\n    sleep_interval_sec = 5\n  }\n\n  # Ignore configuration block that allows for ignoring errors that are known to be safe to ignore\n  ignore \"bar\" {\n    # Specify a pattern that will be detected in the error for ignores, or just ignore any error\n    ignorable_errors = [\n      \".*Error: bar.*\", # If STDERR includes \"Error: bar\", ignore it\n      \"!.*Error: baz.*\" # If STDERR includes \"Error: baz\", do not ignore it\n    ]\n    message = \"Ignoring error bar\" # Add an optional warning message if it fails\n    # Key-value map that can be used to emit signals to external systems on failure\n    signals = {\n      safe_to_revert = true # Signal that the apply is safe to revert on failure\n    }\n\n  }\n}"
  },
  {
    "path": "test/fixtures/errors/get-default-errors/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/errors/get-default-errors/main.tf",
    "content": "variable \"default_retryable_errors\" {\n  type = list(string)\n}\n\nvariable \"custom_error\" {\n  type = string\n}\n\noutput \"default_retryable_errors\" {\n  value = var.default_retryable_errors\n}\n\noutput \"custom_error\" {\n  value = var.custom_error\n}\n"
  },
  {
    "path": "test/fixtures/errors/get-default-errors/terragrunt.hcl",
    "content": "errors {\n  retry \"default_errors\" {\n    retryable_errors = get_default_retryable_errors()\n    max_attempts = 3\n    sleep_interval_sec = 5\n  }\n\n  retry \"custom_errors\" {\n    retryable_errors = [\"my special snowflake\"]\n    max_attempts = 2\n    sleep_interval_sec = 1\n  }\n}\n\ninputs = {\n  default_retryable_errors = get_default_retryable_errors()\n  custom_error = \"my special snowflake\"\n}\n"
  },
  {
    "path": "test/fixtures/errors/ignore/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/errors/ignore/main.tf",
    "content": "resource \"null_resource\" \"error_generator\" {\n  provisioner \"local-exec\" {\n    command = \"echo 'Generating example1 error' && exit 1\"\n\n    interpreter = [\"/bin/sh\", \"-c\"]\n    on_failure  = fail\n  }\n\n  triggers = {\n    always_run = timestamp()\n  }\n}\n"
  },
  {
    "path": "test/fixtures/errors/ignore/terragrunt.hcl",
    "content": "errors {\n  ignore \"example1\" {\n    ignorable_errors = [\n      \".*example1.*\",\n    ]\n    message = \"Ignoring error example1\"\n  }\n\n  ignore \"example2\" {\n    ignorable_errors = [\n      \".*example2.*\",\n    ]\n    message = \"Ignoring error example2\"\n  }\n}"
  },
  {
    "path": "test/fixtures/errors/ignore-negative-pattern/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/errors/ignore-negative-pattern/main.tf",
    "content": "resource \"null_resource\" \"error_generator\" {\n  provisioner \"local-exec\" {\n    command = \"echo 'Error: baz' && exit 1\"\n\n    interpreter = [\"/bin/sh\", \"-c\"]\n    on_failure  = fail\n  }\n\n  triggers = {\n    always_run = timestamp()\n  }\n}\n"
  },
  {
    "path": "test/fixtures/errors/ignore-negative-pattern/terragrunt.hcl",
    "content": "errors {\n  ignore \"baz\" {\n    ignorable_errors = [\n      \"!.*Error: baz.*\" # If STDERR includes \"Error: baz\", do not ignore it\n    ]\n    message = \"Error handler baz\"\n  }\n\n}"
  },
  {
    "path": "test/fixtures/errors/ignore-signal/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/errors/ignore-signal/main.tf",
    "content": "resource \"null_resource\" \"error_generator\" {\n  provisioner \"local-exec\" {\n    command = \"echo 'Generating example1 error' && exit 1\"\n\n    interpreter = [\"/bin/sh\", \"-c\"]\n    on_failure  = fail\n  }\n\n  triggers = {\n    always_run = timestamp()\n  }\n}\n"
  },
  {
    "path": "test/fixtures/errors/ignore-signal/terragrunt.hcl",
    "content": "errors {\n  ignore \"example1\" {\n    ignorable_errors = [\n      \".*example1.*\",\n    ]\n    message = \"Ignoring error example1\"\n\n    signals = {\n      failed          = false\n      failed_example1 = true\n      message         = \"Failed example1\"\n    }\n  }\n\n  ignore \"example2\" {\n    ignorable_errors = [\n      \".*example2.*\",\n    ]\n    message = \"Ignoring error example2\"\n\n    signals = {\n      failed          = false\n      failed_example2 = true\n      message         = \"Failed example2\"\n    }\n  }\n}"
  },
  {
    "path": "test/fixtures/errors/multi-line/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/errors/multi-line/main.tf",
    "content": "\nresource \"null_resource\" \"script_runner\" {\n  provisioner \"local-exec\" {\n    command = \"./script.sh\"\n\n    interpreter = [\"/bin/sh\", \"-c\"]\n    on_failure  = fail\n  }\n\n  triggers = {\n    always_run = timestamp()\n  }\n}\n\noutput \"value\" {\n  value = \"valid value from failing_dep\"\n}"
  },
  {
    "path": "test/fixtures/errors/multi-line/script.sh",
    "content": "#!/usr/bin/env bash\n\necho \"Error: creating Route in Route Table (rtb-46521694) with destination (10.0.0.0/8): operation error EC2: CreateRoute, https response error StatusCode: 400, RequestID: JD40-14127-2022, api error InvalidTransitGatewayID.NotFound: The transitGateway ID 'tgw-xxxxxxxxxxxxxx' does not exist.\"\nexit 1\n"
  },
  {
    "path": "test/fixtures/errors/multi-line/terragrunt.hcl",
    "content": "errors {\n  # Retry block for transient errors\n  retry \"transient_errors\" {\n    retryable_errors = [\n      \"(?s).*cannot create resource \\\"storageclasses\\\" in API group.*\",\n    ]\n    max_attempts       = 3\n    sleep_interval_sec = 20\n  }\n\n  ignore \"transit_gateway_errors\" {\n    ignorable_errors = [\n      \".*creating Route in Route Table*\"\n    ]\n    message = \"Ignoring transit gateway not found when creating internal route.\"\n  }\n}"
  },
  {
    "path": "test/fixtures/errors/no-auto-retry/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/errors/no-auto-retry/main.tf",
    "content": "output \"result\" {\n  value = \"success\"\n}\n"
  },
  {
    "path": "test/fixtures/errors/no-auto-retry/terragrunt.hcl",
    "content": "errors {\n  retry \"transient_errors\" {\n    retryable_errors = [\".*Transient error.*\"]\n    max_attempts = 3\n    sleep_interval_sec = 1\n  }\n}\n\nterraform {\n  before_hook \"simulate_transient_error\" {\n    commands = [\"apply\"]\n    execute  = [\"bash\", \"-c\", \"if [ ! -f success.txt ]; then echo 'Transient error - will succeed on retry' >&2 && touch success.txt && exit 1; fi\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/errors/retry/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/errors/retry/main.tf",
    "content": "resource \"null_resource\" \"script_runner\" {\n  provisioner \"local-exec\" {\n    command = \"./script.sh 3\"\n\n    interpreter = [\"/bin/sh\", \"-c\"]\n    on_failure  = fail\n  }\n\n  triggers = {\n    always_run = timestamp()\n  }\n}\n"
  },
  {
    "path": "test/fixtures/errors/retry/script.sh",
    "content": "#!/usr/bin/env bash\n# script that will fail before $1 attempts\n\nRETRY_ATTEMPTS=\"$1\"\nCOUNTER_FILE=\"attempt_counter.txt\"\n\nif [[ ! -f \"$COUNTER_FILE\" ]]; then\n    echo \"0\" > \"$COUNTER_FILE\"\nfi\n\nCURRENT_COUNT=$(($(< \"$COUNTER_FILE\") + 1))\n\necho \"$CURRENT_COUNT\" > \"$COUNTER_FILE\"\n\necho \"Current attempt: $CURRENT_COUNT\"\n\nif (( CURRENT_COUNT == RETRY_ATTEMPTS )); then\n    echo \"Success !\"\n    echo \"0\" > \"$COUNTER_FILE\"\n    exit 0\nelse\n    echo \"Script error: Attempt $CURRENT_COUNT failed. Will succeed on attempt $RETRY_ATTEMPTS.\" >&2\n    exit 1\nfi"
  },
  {
    "path": "test/fixtures/errors/retry/terragrunt.hcl",
    "content": "errors {\n\n  retry \"script_errors\" {\n    retryable_errors = [\".*Script error.*\"]\n    max_attempts       = 3\n    sleep_interval_sec = 2\n  }\n\n  retry \"aws_errors\" {\n    retryable_errors = [\".*AWS error.*\"]\n    max_attempts       = 3\n    sleep_interval_sec = 2\n  }\n\n}"
  },
  {
    "path": "test/fixtures/errors/retry-fail/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/errors/retry-fail/main.tf",
    "content": "resource \"null_resource\" \"script_runner\" {\n  provisioner \"local-exec\" {\n    command = \"./script.sh 10\"\n\n    interpreter = [\"/bin/sh\", \"-c\"]\n    on_failure  = fail\n  }\n\n  triggers = {\n    always_run = timestamp()\n  }\n}\n"
  },
  {
    "path": "test/fixtures/errors/retry-fail/script.sh",
    "content": "#!/usr/bin/env bash\n# script that will  fail before $1 attempts\n\nRETRY_ATTEMPTS=\"$1\"\nCOUNTER_FILE=\"attempt_counter.txt\"\n\nif [[ ! -f \"$COUNTER_FILE\" ]]; then\n    echo \"0\" > \"$COUNTER_FILE\"\nfi\n\nCURRENT_COUNT=$(($(< \"$COUNTER_FILE\") + 1))\n\necho \"$CURRENT_COUNT\" > \"$COUNTER_FILE\"\n\necho \"Current attempt: $CURRENT_COUNT\"\n\nif (( CURRENT_COUNT == RETRY_ATTEMPTS )); then\n    echo \"Success !\"\n    echo \"0\" > \"$COUNTER_FILE\"\n    exit 0\nelse\n    echo \"Script error: Attempt $CURRENT_COUNT failed. Will succeed on attempt $RETRY_ATTEMPTS.\" >&2\n    exit 1\nfi"
  },
  {
    "path": "test/fixtures/errors/retry-fail/terragrunt.hcl",
    "content": "errors {\n\n  retry \"script_errors\" {\n    retryable_errors = [\".*Script error.*\"]\n    max_attempts       = 3\n    sleep_interval_sec = 2\n  }\n\n  retry \"aws_errors\" {\n    retryable_errors = [\".*AWS error.*\"]\n    max_attempts       = 3\n    sleep_interval_sec = 2\n  }\n\n}"
  },
  {
    "path": "test/fixtures/errors/run-all/app1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/errors/run-all/app1/main.tf",
    "content": "resource \"null_resource\" \"error_generator\" {\n  provisioner \"local-exec\" {\n    command = \"echo 'Generating example1 error' && exit 1\"\n\n    interpreter = [\"/bin/sh\", \"-c\"]\n    on_failure  = fail\n  }\n\n  triggers = {\n    always_run = timestamp()\n  }\n}\n"
  },
  {
    "path": "test/fixtures/errors/run-all/app1/terragrunt.hcl",
    "content": "include \"common\" {\n  path = find_in_parent_folders(\"common.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/errors/run-all/app2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/errors/run-all/app2/main.tf",
    "content": "resource \"null_resource\" \"script_runner\" {\n  provisioner \"local-exec\" {\n    command = \"./script.sh 3\"\n\n    interpreter = [\"/bin/sh\", \"-c\"]\n    on_failure  = fail\n  }\n\n  triggers = {\n    always_run = timestamp()\n  }\n}\n"
  },
  {
    "path": "test/fixtures/errors/run-all/app2/script.sh",
    "content": "#!/usr/bin/env bash\n# script that will fail before $1 attempts\n\nRETRY_ATTEMPTS=\"$1\"\nCOUNTER_FILE=\"attempt_counter.txt\"\n\nif [[ ! -f \"$COUNTER_FILE\" ]]; then\n    echo \"0\" > \"$COUNTER_FILE\"\nfi\n\nCURRENT_COUNT=$(($(< \"$COUNTER_FILE\") + 1))\n\necho \"$CURRENT_COUNT\" > \"$COUNTER_FILE\"\n\necho \"Current attempt: $CURRENT_COUNT\"\n\nif (( CURRENT_COUNT == RETRY_ATTEMPTS )); then\n    echo \"Success !\"\n    echo \"0\" > \"$COUNTER_FILE\"\n    exit 0\nelse\n    echo \"Script error: Attempt $CURRENT_COUNT failed. Will succeed on attempt $RETRY_ATTEMPTS.\" >&2\n    exit 1\nfi"
  },
  {
    "path": "test/fixtures/errors/run-all/app2/terragrunt.hcl",
    "content": "include \"common\" {\n  path = find_in_parent_folders(\"common.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/errors/run-all/common.hcl",
    "content": "feature \"unstable\" {\n  default = true\n}\n\nerrors {\n  ignore \"example1\" {\n    ignorable_errors = feature.unstable.value ? [\n      \".*example1.*\",\n    ] : []\n    message = \"Ignoring error example1\"\n  }\n\n  ignore \"example2\" {\n    ignorable_errors = feature.unstable.value ? [\n      \".*example2.*\",\n    ] : []\n    message = \"Ignoring error example2\"\n  }\n\n  retry \"script_errors\" {\n    retryable_errors   = feature.unstable.value ? [\".*Script error.*\"] : []\n    max_attempts       = 3\n    sleep_interval_sec = 2\n  }\n\n  retry \"aws_errors\" {\n    retryable_errors   = feature.unstable.value ? [\".*AWS error.*\"] : []\n    max_attempts       = 3\n    sleep_interval_sec = 2\n  }\n\n}"
  },
  {
    "path": "test/fixtures/errors/run-all-ignore/app1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/errors/run-all-ignore/app1/main.tf",
    "content": "resource \"null_resource\" \"error_generator\" {\n  provisioner \"local-exec\" {\n    command = \"echo 'Generating example1 error' && exit 1\"\n\n    interpreter = [\"/bin/sh\", \"-c\"]\n    on_failure  = fail\n  }\n\n  triggers = {\n    always_run = timestamp()\n  }\n}\n"
  },
  {
    "path": "test/fixtures/errors/run-all-ignore/app1/terragrunt.hcl",
    "content": "errors {\n  ignore \"example1\" {\n    ignorable_errors = [\n      \".*example1.*\",\n    ]\n    message = \"Ignoring error example1\"\n  }\n\n  ignore \"example2\" {\n    ignorable_errors = [\n      \".*example2.*\",\n    ]\n    message = \"Ignoring error example2\"\n  }\n}"
  },
  {
    "path": "test/fixtures/errors/run-all-ignore/app2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/errors/run-all-ignore/app2/main.tf",
    "content": "output \"app2\" {\n  value = \"value-from-app-2\"\n}"
  },
  {
    "path": "test/fixtures/errors/run-all-ignore/app2/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/exclude/basic/unit1/terragrunt.hcl",
    "content": "exclude {\n  if      = true\n  actions = [\"plan\"]\n}\n"
  },
  {
    "path": "test/fixtures/exclude/basic/unit2/terragrunt.hcl",
    "content": "exclude {\n  if      = true\n  actions = [\"apply\"]\n}\n"
  },
  {
    "path": "test/fixtures/exclude/basic/unit3/terragrunt.hcl",
    "content": "// No exclude configuration\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/action-mismatch/main.tf",
    "content": "# Minimal terraform config\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/action-mismatch/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\n# Excluded with no_run=true but only for plan action\n# When running apply, this unit should run (action doesn't match)\nexclude {\n  if      = true\n  no_run  = true\n  actions = [\"plan\"]\n}\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/always-excluded/main.tf",
    "content": "# Minimal terraform config\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/always-excluded/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\n# Always excluded for all actions\nexclude {\n  if      = true\n  actions = [\"all\"]\n}\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/conditional-flag/main.tf",
    "content": "# Minimal terraform config\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/conditional-flag/terragrunt.hcl",
    "content": "include \"flags\" {\n  path = find_in_parent_folders(\"flags.hcl\")\n}\n\nterraform {\n  source = \".\"\n}\n\n# Excluded based on feature flag\nexclude {\n  if      = feature.exclude.value\n  actions = [\"all\"]\n}\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/conditional-no-run/main.tf",
    "content": "# Minimal terraform config\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/conditional-no-run/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\nfeature \"enable_unit\" {\n  default = false\n}\n\n# Excluded based on feature flag with no_run = true\n# When enable_unit=false (default), excluded with early exit\n# When enable_unit=true, runs normally\nexclude {\n  if      = !feature.enable_unit.value\n  no_run  = true\n  actions = [\"all\"]\n}\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/dep-unit/main.tf",
    "content": "# Minimal terraform config\noutput \"data\" {\n  value = \"data\"\n}\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/dep-unit/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\n# Normal unit that is a dependency of with-dep\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/exclude-all-except-output/main.tf",
    "content": "# Minimal terraform config\noutput \"data\" {\n  value = \"test\"\n}\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/exclude-all-except-output/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\n# Excluded for all actions except output\nexclude {\n  if      = true\n  actions = [\"all_except_output\"]\n}\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/exclude-apply-only/main.tf",
    "content": "# Minimal terraform config\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/exclude-apply-only/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\n# Excluded only for apply action\nexclude {\n  if      = true\n  actions = [\"apply\"]\n}\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/exclude-plan-only/main.tf",
    "content": "# Minimal terraform config\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/exclude-plan-only/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\n# Excluded only for plan action\nexclude {\n  if      = true\n  actions = [\"plan\"]\n}\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/flags.hcl",
    "content": "# Feature flags for exclude block testing\n\nfeature \"exclude\" {\n  default = false\n}\n\nfeature \"exclude_deps\" {\n  default = false\n}\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/never-excluded/main.tf",
    "content": "# Minimal terraform config\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/never-excluded/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\n# Never excluded (if = false)\nexclude {\n  if      = false\n  actions = [\"all\"]\n}\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/no-run-false/main.tf",
    "content": "# Minimal terraform config\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/no-run-false/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\n# Excluded but no_run = false - unit runs in single mode, excluded in run --all\nexclude {\n  if      = true\n  no_run  = false\n  actions = [\"plan\", \"apply\"]\n}\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/no-run-not-set/main.tf",
    "content": "# Minimal terraform config\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/no-run-not-set/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\n# Excluded but no_run is not set (defaults to false) - unit runs in single mode\nexclude {\n  if      = true\n  actions = [\"plan\"]\n}\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/no-run-true/main.tf",
    "content": "# Minimal terraform config\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/no-run-true/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\n# Excluded with no_run = true - causes early exit in single unit mode\nexclude {\n  if      = true\n  no_run  = true\n  actions = [\"all\"]\n}\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/normal-unit/main.tf",
    "content": "# Minimal terraform config\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/normal-unit/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\n# Normal unit without any exclude block\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/with-dep/main.tf",
    "content": "# Minimal terraform config\n"
  },
  {
    "path": "test/fixtures/exclude/comprehensive/with-dep/terragrunt.hcl",
    "content": "include \"flags\" {\n  path = find_in_parent_folders(\"flags.hcl\")\n}\n\nterraform {\n  source = \".\"\n}\n\ndependency \"dep\" {\n  config_path = \"../dep-unit\"\n\n  mock_outputs = {\n    data = \"mock\"\n  }\n}\n\n# Excluded based on feature flag, with dependency exclusion\nexclude {\n  if                   = feature.exclude.value\n  actions              = [\"all\"]\n  exclude_dependencies = feature.exclude_deps.value\n}\n"
  },
  {
    "path": "test/fixtures/exclude-by-default/_stacks/terragrunt.stack.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/exclude-by-default/unit1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/exclude-by-default/unit1/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/exclude-by-default/unit1/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/excludes-file/.terragrunt-excludes",
    "content": "# Comment line\na/\n#b/\nc/\n"
  },
  {
    "path": "test/fixtures/excludes-file/a/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/excludes-file/a/main.tf",
    "content": "variable \"value\" {\n  type    = string\n  default = \"a\"\n}\n\noutput \"value\" {\n  value = var.value\n}\n"
  },
  {
    "path": "test/fixtures/excludes-file/a/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/excludes-file/b/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/excludes-file/b/main.tf",
    "content": "variable \"value\" {\n  type    = string\n  default = \"b\"\n}\n\noutput \"value\" {\n  value = var.value\n}\n"
  },
  {
    "path": "test/fixtures/excludes-file/b/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/excludes-file/c/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/excludes-file/c/main.tf",
    "content": "variable \"value\" {\n  type    = string\n  default = \"c\"\n}\n\noutput \"value\" {\n  value = var.value\n}\n"
  },
  {
    "path": "test/fixtures/excludes-file/c/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/excludes-file/d/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/excludes-file/d/main.tf",
    "content": "variable \"value\" {\n  type    = string\n  default = \"d\"\n}\n\noutput \"value\" {\n  value = var.value\n}\n"
  },
  {
    "path": "test/fixtures/excludes-file/d/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/excludes-file/excludes-file-pass-as-flag",
    "content": "b/\nd/\n"
  },
  {
    "path": "test/fixtures/exec-cmd/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/exec-cmd/app/main.tf",
    "content": "variable \"foo\" {\n  type = string\n}\n"
  },
  {
    "path": "test/fixtures/exec-cmd/app/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\ninputs = {\n  foo = \"FOO\"\n  bar = \"BAR\"\n}\n"
  },
  {
    "path": "test/fixtures/exec-cmd/script.sh",
    "content": "#!/usr/bin/env bash\n\n# Required environment variables:\n# TF_VAR_foo - Should be set to \"FOO\"\n# TF_VAR_bar - Should be set to \"BAR\"\n\necho \"The first arg is $1. The second arg is $2. The script is running in the directory $PWD\"\n\nif [[ \"$TF_VAR_foo\" != \"FOO\" ]]\nthen\n    echo \"error: TF_VAR_foo must be set to 'FOO' (current value: ${TF_VAR_foo:-not set})\" >&2\n    exit 1\nfi\n\nif [[ \"$TF_VAR_bar\" != \"BAR\" ]]\nthen\n    echo \"error: TF_VAR_bar must be set to 'BAR' (current value: ${TF_VAR_bar:-not set})\" >&2\n    exit 1\nfi\n"
  },
  {
    "path": "test/fixtures/exec-cmd-tf-path/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/exec-cmd-tf-path/app/main.tf",
    "content": "variable \"baz\" {\n  type = string\n}"
  },
  {
    "path": "test/fixtures/exec-cmd-tf-path/app/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\ndependency \"dep\" {\n    config_path = \"../dep\"\n}\n\ninputs = {\n  baz = dependency.dep.outputs.baz\n}\n"
  },
  {
    "path": "test/fixtures/exec-cmd-tf-path/dep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/exec-cmd-tf-path/dep/main.tf",
    "content": "output \"baz\" {\n  value = \"baz\"\n}"
  },
  {
    "path": "test/fixtures/exec-cmd-tf-path/dep/terragrunt.hcl",
    "content": "# Intentionally empty"
  },
  {
    "path": "test/fixtures/exec-cmd-tf-path/script.sh",
    "content": "#!/usr/bin/env bash\n\necho \"baz is ${TF_VAR_baz:-not set}\""
  },
  {
    "path": "test/fixtures/exec-cmd-tf-path/terraform-output-json.sh",
    "content": "#!/usr/bin/env bash\n\n# Handle -version\nif [[ \"$1\" = \"-version\" ]]; then\n  echo \"Terraform v1.0.0\"\n  exit 0\nfi\n\n# Output variable\ncat << 'EOF'\n{\n\"baz\": {\n  \"sensitive\": false,\n  \"type\": \"string\",\n  \"value\": \"terraform\"\n}\n}\nEOF"
  },
  {
    "path": "test/fixtures/exec-cmd-tf-path/tofu-output-json.sh",
    "content": "#!/usr/bin/env bash\n\n# Handle -version\nif [[ \"$1\" = \"-version\" ]]; then\n  echo \"OpenToFu v1.0.0\"\n  exit 0\nfi\n\n# Output variable\ncat << 'EOF'\n{\n\"baz\": {\n  \"sensitive\": false,\n  \"type\": \"string\",\n  \"value\": \"tofu\"\n}\n}\nEOF"
  },
  {
    "path": "test/fixtures/external-dependencies/module-a/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/external-dependencies/module-a/main.tf",
    "content": "output \"result\" {\n  value = \"Hello World, module-a\"\n}\n"
  },
  {
    "path": "test/fixtures/external-dependencies/module-a/terragrunt.hcl",
    "content": "# Intentionally empty"
  },
  {
    "path": "test/fixtures/external-dependencies/module-b/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/external-dependencies/module-b/main.tf",
    "content": "output \"result\" {\n  value = \"Hello World, module-b\"\n}\n"
  },
  {
    "path": "test/fixtures/external-dependencies/module-b/terragrunt.hcl",
    "content": "dependencies {\n  paths = [\n    \"../module-a\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/external-dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/external-dependency/main.tf",
    "content": "output \"value\" {\n  value = \"dep1\"\n}\n"
  },
  {
    "path": "test/fixtures/external-dependency/terragrunt.hcl",
    "content": "feature \"dep\" {\n  default = \"/tmp/external\"\n}\n\ndependencies {\n  paths = [feature.dep.value]\n}\n"
  },
  {
    "path": "test/fixtures/extra-args/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/extra-args/dev.tfvars",
    "content": "extra_var = \"Hello, World from dev!\"\n"
  },
  {
    "path": "test/fixtures/extra-args/extra.tfvars",
    "content": "extra_var = \"Hello, World!\"\n"
  },
  {
    "path": "test/fixtures/extra-args/main.tf",
    "content": "variable \"extra_var\" {\n  description = \"Should be loaded from extra.tfvars\"\n}\n\noutput \"test\" {\n  value = var.extra_var\n}\n"
  },
  {
    "path": "test/fixtures/extra-args/terragrunt.hcl",
    "content": "terraform {\n  extra_arguments \"var-files\" {\n    required_var_files = [\n      \"extra.tfvars\",\n    ]\n\n    optional_var_files = [\n      \"${get_terragrunt_dir()}/${get_env(\"TF_VAR_env\", \"dev\")}.tfvars\",\n      \"${get_terragrunt_dir()}/${get_env(\"TF_VAR_region\", \"us-east-1\")}.tfvars\",\n    ]\n\n    commands = [\n      \"apply\",\n      \"plan\",\n      \"import\",\n      \"push\",\n      \"refresh\",\n    ]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/extra-args/us-west-2.tfvars",
    "content": "extra_var = \"Hello, World from Oregon!\"\n"
  },
  {
    "path": "test/fixtures/fail-fast/unit-a/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/fail-fast/unit-a/main.tf",
    "content": "\noutput \"data\" {\n  value = \"unit-a\"\n}\n\nresource \"null_resource\" \"fail_if_marker_present\" {\n  triggers = {\n    always_run = timestamp()\n  }\n\n  provisioner \"local-exec\" {\n    when    = \"create\"\n    command = \"test -f ${path.module}/fail.txt && echo 'Failing on apply due to fail.txt' && exit 1 || echo 'No fail.txt on apply, continuing...'\"\n  }\n\n  provisioner \"local-exec\" {\n    when    = \"destroy\"\n    command = \"test -f ${path.module}/fail.txt && echo 'Failing on destroy due to fail.txt' && exit 1 || echo 'No fail.txt on destroy, continuing...'\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/fail-fast/unit-a/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/fail-fast/unit-b/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/fail-fast/unit-b/main.tf",
    "content": "\noutput \"data\" {\n  value = \"unit-b\"\n}\n\nresource \"null_resource\" \"fail_if_marker_present\" {\n  triggers = {\n    always_run = timestamp()\n  }\n\n  provisioner \"local-exec\" {\n    when    = \"create\"\n    command = \"test -f ${path.module}/fail.txt && echo 'Failing on apply due to fail.txt' && exit 1 || echo 'No fail.txt on apply, continuing...'\"\n  }\n\n  provisioner \"local-exec\" {\n    when    = \"destroy\"\n    command = \"test -f ${path.module}/fail.txt && echo 'Failing on destroy due to fail.txt' && exit 1 || echo 'No fail.txt on destroy, continuing...'\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/fail-fast/unit-b/terragrunt.hcl",
    "content": "dependency \"unita\" {\n  config_path = \"../unit-a\"\n  mock_outputs_allowed_terraform_commands = [\"validate\"]\n  mock_outputs = {\n    data = \"test-data\"\n  }\n}\n\ninputs = {\n    data = dependency.unita.outputs.data\n}"
  },
  {
    "path": "test/fixtures/fail-fast/unit-c/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/fail-fast/unit-c/main.tf",
    "content": "\noutput \"data\" {\n  value = \"unit-c\"\n}\n\nresource \"null_resource\" \"fail_if_marker_present\" {\n  triggers = {\n    always_run = timestamp()\n  }\n\n  provisioner \"local-exec\" {\n    when    = \"create\"\n    command = \"test -f ${path.module}/fail.txt && echo 'Failing on apply due to fail.txt' && exit 1 || echo 'No fail.txt on apply, continuing...'\"\n  }\n\n  provisioner \"local-exec\" {\n    when    = \"destroy\"\n    command = \"test -f ${path.module}/fail.txt && echo 'Failing on destroy due to fail.txt' && exit 1 || echo 'No fail.txt on destroy, continuing...'\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/fail-fast/unit-c/terragrunt.hcl",
    "content": "dependency \"unita\" {\n  config_path = \"../unit-a\"\n  mock_outputs_allowed_terraform_commands = [\"validate\"]\n  mock_outputs = {\n    data = \"test-data\"\n  }\n}\n\ninputs = {\n  data = dependency.unita.outputs.data\n}"
  },
  {
    "path": "test/fixtures/fail-fast-early-exit/depends-on-failing/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/fail-fast-early-exit/depends-on-failing/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/fail-fast-early-exit/depends-on-failing/terragrunt.hcl",
    "content": "dependency \"failing\" {\n  config_path = \"../failing-unit\"\n\n  mock_outputs = {\n    data = \"mock\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/fail-fast-early-exit/depends-on-succeeding/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/fail-fast-early-exit/depends-on-succeeding/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/fail-fast-early-exit/depends-on-succeeding/terragrunt.hcl",
    "content": "dependency \"succeeding\" {\n  config_path = \"../succeeding-unit\"\n  mock_outputs = {\n    data = \"mock\"\n  }\n}\n\nterraform {\n  before_hook \"wait_for_signal\" {\n    commands = [\"apply\"]\n    execute  = [\n      \"bash\", \"-c\",\n      \"for i in $(seq 1 30); do [ -f '${get_terragrunt_dir()}/../.fail-signal' ] && exit 0; sleep 0.1; done; exit 0\"\n    ]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/fail-fast-early-exit/failing-unit/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \">= 3.2.0\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/fail-fast-early-exit/failing-unit/main.tf",
    "content": "terraform {\n  required_version = \">= 1.0\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \">= 3.2\"\n    }\n  }\n}\n\nresource \"null_resource\" \"fail\" {\n  provisioner \"local-exec\" {\n    command = \"echo 'failing-unit running' && exit 1\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/fail-fast-early-exit/failing-unit/terragrunt.hcl",
    "content": "terraform {\n  after_hook \"signal\" {\n    commands     = [\"apply\"]\n    execute      = [\"touch\", \"${get_terragrunt_dir()}/../.fail-signal\"]\n    run_on_error = true\n  }\n}\n"
  },
  {
    "path": "test/fixtures/fail-fast-early-exit/succeeding-unit/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/fail-fast-early-exit/succeeding-unit/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/fail-fast-early-exit/succeeding-unit/terragrunt.hcl",
    "content": "terraform {\n  before_hook \"wait_for_signal\" {\n    commands = [\"apply\"]\n    execute  = [\n      \"bash\", \"-c\",\n      \"for i in $(seq 1 30); do [ -f '${get_terragrunt_dir()}/../.fail-signal' ] && exit 0; sleep 0.1; done; exit 0\"\n    ]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/failure/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/failure/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/failure/missingvars/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/failure/missingvars/main.tf",
    "content": "\nmodule \"sub\" {\n  source = \"./submod\"\n}\n\n\n"
  },
  {
    "path": "test/fixtures/failure/missingvars/submod/main.tf",
    "content": "variable \"missingvar1\" {}\nvariable \"missingvar2\" {}\n\n"
  },
  {
    "path": "test/fixtures/failure/missingvars/terragrunt.hcl",
    "content": "# Intentionally empty"
  },
  {
    "path": "test/fixtures/failure/submod/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/failure/submod/main.tf",
    "content": "variable \"missingvar1\" {}\nvariable \"missingvar2\" {}\n\n"
  },
  {
    "path": "test/fixtures/failure/terragrunt.hcl",
    "content": "# Intentionally empty"
  },
  {
    "path": "test/fixtures/feature-flags/error-empty-flag/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/feature-flags/error-empty-flag/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/feature-flags/error-empty-flag/terragrunt.hcl",
    "content": "feature \"test1\" {}\n\nfeature \"test2\" {}\n\nfeature \"test3\" {}"
  },
  {
    "path": "test/fixtures/feature-flags/include-flag/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/feature-flags/include-flag/app/main.tf",
    "content": "variable \"string_feature_flag\" {\n  type = string\n}\n\nvariable \"int_feature_flag\" {\n  type = number\n}\n\nvariable \"bool_feature_flag\" {\n  type = bool\n}\n\noutput \"string_feature_flag\" {\n  value = var.string_feature_flag\n}\n\noutput \"int_feature_flag\" {\n  value = var.int_feature_flag\n}\n\noutput \"bool_feature_flag\" {\n  value = var.bool_feature_flag\n}\n"
  },
  {
    "path": "test/fixtures/feature-flags/include-flag/app/terragrunt.hcl",
    "content": "include \"root\" {\n  path   = \"${find_in_parent_folders(\"root.hcl\")}\"\n}\n\ninputs = {\n  string_feature_flag = feature.string_feature_flag.value\n  int_feature_flag = feature.int_feature_flag.value\n  bool_feature_flag = feature.bool_feature_flag.value\n}\n"
  },
  {
    "path": "test/fixtures/feature-flags/include-flag/root.hcl",
    "content": "\nfeature \"string_feature_flag\" {\n  default = \"test\"\n}\n\nfeature \"int_feature_flag\" {\n  default = 666\n}\n\nfeature \"bool_feature_flag\" {\n  default = false\n}\n"
  },
  {
    "path": "test/fixtures/feature-flags/run-all/app1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/feature-flags/run-all/app1/main.tf",
    "content": "variable \"string_feature_flag\" {\n  type = string\n}\n\nvariable \"int_feature_flag\" {\n  type = number\n}\n\nvariable \"bool_feature_flag\" {\n  type = bool\n}\n\noutput \"string_feature_flag\" {\n  value = var.string_feature_flag\n}\n\noutput \"int_feature_flag\" {\n  value = var.int_feature_flag\n}\n\noutput \"bool_feature_flag\" {\n  value = var.bool_feature_flag\n}\n"
  },
  {
    "path": "test/fixtures/feature-flags/run-all/app1/terragrunt.hcl",
    "content": "include \"root\" {\n  path   = find_in_parent_folders(\"common.hcl\")\n}\n\ninputs = {\n  string_feature_flag = feature.string_feature_flag.value\n  int_feature_flag = feature.int_feature_flag.value\n  bool_feature_flag = feature.bool_feature_flag.value\n}\n"
  },
  {
    "path": "test/fixtures/feature-flags/run-all/app2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/feature-flags/run-all/app2/main.tf",
    "content": "variable \"string_feature_flag\" {\n  type = string\n}\n\nvariable \"int_feature_flag\" {\n  type = number\n}\n\nvariable \"bool_feature_flag\" {\n  type = bool\n}\n\noutput \"string_feature_flag\" {\n  value = var.string_feature_flag\n}\n\noutput \"int_feature_flag\" {\n  value = var.int_feature_flag\n}\n\noutput \"bool_feature_flag\" {\n  value = var.bool_feature_flag\n}\n"
  },
  {
    "path": "test/fixtures/feature-flags/run-all/app2/terragrunt.hcl",
    "content": "include \"root\" {\n  path   = find_in_parent_folders(\"common.hcl\")\n}\n\ninputs = {\n  string_feature_flag = feature.string_feature_flag.value\n  int_feature_flag = feature.int_feature_flag.value\n  bool_feature_flag = feature.bool_feature_flag.value\n}\n"
  },
  {
    "path": "test/fixtures/feature-flags/run-all/common.hcl",
    "content": "\nfeature \"string_feature_flag\" {\n  default = \"test\"\n}\n\nfeature \"int_feature_flag\" {\n  default = 666\n}\n\nfeature \"bool_feature_flag\" {\n  default = false\n}\n"
  },
  {
    "path": "test/fixtures/feature-flags/simple-flag/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/feature-flags/simple-flag/main.tf",
    "content": "variable \"string_feature_flag\" {\n  type = string\n}\n\nvariable \"int_feature_flag\" {\n  type = number\n}\n\nvariable \"bool_feature_flag\" {\n  type = bool\n}\n\noutput \"string_feature_flag\" {\n  value = var.string_feature_flag\n}\n\noutput \"int_feature_flag\" {\n  value = var.int_feature_flag\n}\n\noutput \"bool_feature_flag\" {\n  value = var.bool_feature_flag\n}\n"
  },
  {
    "path": "test/fixtures/feature-flags/simple-flag/terragrunt.hcl",
    "content": "\nfeature \"string_feature_flag\" {\n  default = \"test\"\n}\n\nfeature \"int_feature_flag\" {\n  default = 666\n}\n\nfeature \"bool_feature_flag\" {\n  default = false\n}\n\nterraform {\n  source = \".\"\n\n  before_hook \"conditional_command\" {\n    commands = [\"apply\", \"plan\", \"destroy\"]\n    execute  = feature.bool_feature_flag.value ? [\"sh\", \"-c\", \"echo running conditional bool_feature_flag\"] : [ \"sh\", \"-c\", \"exit\", \"0\" ]\n  }\n}\n\ninputs = {\n  string_feature_flag = feature.string_feature_flag.value\n  int_feature_flag = feature.int_feature_flag.value\n  bool_feature_flag = feature.bool_feature_flag.value\n}\n"
  },
  {
    "path": "test/fixtures/filter/mark-as-read/unit-duplicate/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/filter/mark-as-read/unit-duplicate/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/filter/mark-as-read/unit-duplicate/terragrunt.hcl",
    "content": "locals {\n  marked1 = mark_as_read(\"foo.txt\")\n  marked2 = mark_as_read(\"foo.txt\")\n  marked3 = mark_as_read(\"bar.txt\")\n}\n\nterraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/filter/mark-as-read/unit-empty/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/filter/mark-as-read/unit-empty/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/filter/mark-as-read/unit-empty/terragrunt.hcl",
    "content": "locals {\n  marked = mark_as_read(\"\")\n}\n\nterraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/filter/mark-as-read/unit-no-mark/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/filter/mark-as-read/unit-no-mark/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/filter/mark-as-read/unit-no-mark/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/filter/mark-as-read/unit-normal/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/filter/mark-as-read/unit-normal/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/filter/mark-as-read/unit-normal/terragrunt.hcl",
    "content": "locals {\n  marked = mark_as_read(\"foo.txt\")\n}\n\nterraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing/dependency-unit/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing/dependency-unit/main.tf",
    "content": "# Minimal terraform config\n\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing/dependency-unit/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing/excluded-unit-1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing/excluded-unit-1/main.tf",
    "content": "# Minimal terraform config\n\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing/excluded-unit-1/terragrunt.hcl",
    "content": "locals {\n  test = run_cmd(\"--terragrunt-quiet\", \"bash\", \"-c\", \"exit 1\")\n}\n\nterraform {\n  source = \".\"\n}\n\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing/excluded-unit-2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing/excluded-unit-2/main.tf",
    "content": "# Minimal terraform config\n\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing/excluded-unit-2/terragrunt.hcl",
    "content": "locals {\n  test = run_cmd(\"--terragrunt-quiet\", \"bash\", \"-c\", \"exit 1\")\n}\n\nterraform {\n  source = \".\"\n}\n\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing/excluded-unit-3/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing/excluded-unit-3/main.tf",
    "content": "# Minimal terraform config\n\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing/excluded-unit-3/terragrunt.hcl",
    "content": "locals {\n  test = run_cmd(\"--terragrunt-quiet\", \"bash\", \"-c\", \"exit 1\")\n}\n\nterraform {\n  source = \".\"\n}\n\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing/target-unit/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing/target-unit/main.tf",
    "content": "# Minimal terraform config\n\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing/target-unit/terragrunt.hcl",
    "content": "dependency \"dep\" {\n  config_path = \"../dependency-unit\"\n\n  mock_outputs = {}\n}\n\nterraform {\n  source = \".\"\n}\n\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing-destroy/landmine-unit-1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing-destroy/landmine-unit-1/main.tf",
    "content": "# Minimal terraform config\n\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing-destroy/landmine-unit-1/terragrunt.hcl",
    "content": "locals {\n  test = run_cmd(\"--terragrunt-quiet\", \"bash\", \"-c\", \"exit 1\")\n}\n\nterraform {\n  source = \".\"\n}\n\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing-destroy/landmine-unit-2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing-destroy/landmine-unit-2/main.tf",
    "content": "# Minimal terraform config\n\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing-destroy/landmine-unit-2/terragrunt.hcl",
    "content": "locals {\n  test = run_cmd(\"--terragrunt-quiet\", \"bash\", \"-c\", \"exit 1\")\n}\n\nterraform {\n  source = \".\"\n}\n\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing-destroy/unit-a/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing-destroy/unit-a/main.tf",
    "content": "# Minimal terraform config\n\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing-destroy/unit-a/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing-destroy/unit-b/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing-destroy/unit-b/main.tf",
    "content": "# Minimal terraform config\n\n"
  },
  {
    "path": "test/fixtures/filter/minimize-parsing-destroy/unit-b/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\n"
  },
  {
    "path": "test/fixtures/filter-source/github-acme-bar/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/filter-source/github-acme-bar/main.tf",
    "content": "# Empty module\n\n"
  },
  {
    "path": "test/fixtures/filter-source/github-acme-bar/terragrunt.hcl",
    "content": "terraform {\n  source = \"git::git@github.com:acme/bar?ref=v1.0.0\"\n}\n\n"
  },
  {
    "path": "test/fixtures/filter-source/github-acme-foo/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/filter-source/github-acme-foo/main.tf",
    "content": "# Empty module\n\n"
  },
  {
    "path": "test/fixtures/filter-source/github-acme-foo/terragrunt.hcl",
    "content": "terraform {\n  source = \"github.com/acme/foo\"\n}\n\n"
  },
  {
    "path": "test/fixtures/filter-source/gitlab-example-baz/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/filter-source/gitlab-example-baz/main.tf",
    "content": "# Empty module\n\n"
  },
  {
    "path": "test/fixtures/filter-source/gitlab-example-baz/terragrunt.hcl",
    "content": "terraform {\n  source = \"gitlab.com/example/baz\"\n}\n\n"
  },
  {
    "path": "test/fixtures/filter-source/local-module/module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/filter-source/local-module/module/main.tf",
    "content": "# Empty module\n\n"
  },
  {
    "path": "test/fixtures/filter-source/local-module/terragrunt.hcl",
    "content": "terraform {\n  source = \"./module\"\n}\n\n"
  },
  {
    "path": "test/fixtures/find/basic/stack/terragrunt.stack.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/find/basic/unit/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/find/dag/a-dependent/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/find/dag/a-dependent/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/find/dag/a-dependent/terragrunt.hcl",
    "content": "dependency \"dep\" {\n  config_path = \"../b-dependency\"\n\n  mock_outputs = {\n    value = \"mock value\"\n  }\n}\n\nterraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/find/dag/b-dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/find/dag/b-dependency/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/find/dag/b-dependency/terragrunt.hcl",
    "content": "terraform {\n    source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/find/dag/c-mixed-deps/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/find/dag/c-mixed-deps/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/find/dag/c-mixed-deps/terragrunt.hcl",
    "content": "# Uses both dependency and dependencies blocks\ndependency \"single_dep\" {\n  config_path = \"../a-dependent\"\n\n  mock_outputs = {\n    value = \"mock value\"\n  }\n}\n\ndependencies {\n  paths = [\"../d-dependencies-only\"]\n}\n\nterraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/find/dag/d-dependencies-only/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/find/dag/d-dependencies-only/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/find/dag/d-dependencies-only/terragrunt.hcl",
    "content": "dependencies {\n    paths = [\"../a-dependent\"]\n}\n\nterraform {\n    source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/find/hidden/.hide/unit/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/find/hidden/stack/terragrunt.stack.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/find/hidden/unit/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/find/include/bar/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/find/include/bar/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/find/include/bar/terragrunt.hcl",
    "content": "include \"cloud\" {\n  path = find_in_parent_folders(\"cloud.hcl\")\n}"
  },
  {
    "path": "test/fixtures/find/include/cloud.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/find/include/foo/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/find/include/foo/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/find/include/foo/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/find/internal-v-external/external/c-dependency/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/find/internal-v-external/internal/a-dependent/terragrunt.hcl",
    "content": "dependency \"dep_b\" {\n  config_path = \"../b-dependency\"\n}\n\ndependency \"dep_c\" {\n  config_path = \"../../external/c-dependency\"\n}\n"
  },
  {
    "path": "test/fixtures/find/internal-v-external/internal/b-dependency/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/find/read-terragrunt-config/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/find/read-terragrunt-config/common_deps.hcl",
    "content": "dependency \"module\" {\n  config_path = \"./module\"\n}\n\ninputs = {\n  module_value = dependency.module.outputs.value\n}\n\n"
  },
  {
    "path": "test/fixtures/find/read-terragrunt-config/main.tf",
    "content": "\n"
  },
  {
    "path": "test/fixtures/find/read-terragrunt-config/module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/find/read-terragrunt-config/module/main.tf",
    "content": "output \"value\" {\n  value = \"yes\"\n}\n\n"
  },
  {
    "path": "test/fixtures/find/read-terragrunt-config/module/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\n"
  },
  {
    "path": "test/fixtures/find/read-terragrunt-config/terragrunt.hcl",
    "content": "locals {\n  common_deps = read_terragrunt_config(\"${get_terragrunt_dir()}/common_deps.hcl\")\n}\n\nterraform {\n  source = \".\"\n}\n\ninputs = {\n  value = local.common_deps.dependency.module.outputs.value\n  module_value = local.common_deps.inputs.module_value\n}\n\n"
  },
  {
    "path": "test/fixtures/find-parent/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/find-parent/app/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/find-parent/app/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/find-parent/root.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/find-parent-with-deprecated-root/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/find-parent-with-deprecated-root/app/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/find-parent-with-deprecated-root/app/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"terragrunt.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/find-parent-with-deprecated-root/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/gcs/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/gcs/main.tf",
    "content": "terraform {\n  backend \"gcs\" {}\n\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"test\" {\n  provisioner \"local-exec\" {\n    command = \"echo Hello, World!\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/gcs/terragrunt.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in a GCS bucket\nremote_state {\n  backend = \"gcs\"\n\n  config = {\n    project  = \"__FILL_IN_PROJECT__\"\n    location = \"__FILL_IN_LOCATION__\"\n    bucket   = \"__FILL_IN_BUCKET_NAME__\"\n    prefix   = \"tofu.tfstate\"\n\n    gcs_bucket_labels = {\n      owner = \"terragrunt_test\"\n      name  = \"terraform_state_storage\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/gcs-backend/common.hcl",
    "content": "feature \"disable_versioning\" {\n  default = false\n}\n\nfeature \"key_prefix\" {\n  default = \"\"\n}\n\nremote_state {\n  backend = \"gcs\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n\n  config = {\n    prefix   = \"${feature.key_prefix.value}${path_relative_to_include()}/tofu.tfstate\"\n    location = \"__FILL_IN_LOCATION__\"\n    project  = \"__FILL_IN_PROJECT__\"\n    bucket   = \"__FILL_IN_BUCKET_NAME__\"\n\n    skip_bucket_versioning = feature.disable_versioning.value\n  }\n}\n"
  },
  {
    "path": "test/fixtures/gcs-backend/unit1/terragrunt.hcl",
    "content": "terraform {\n  source = \"tfr://registry.terraform.io/yorinasub17/terragrunt-registry-test/null//modules/one?version=0.0.2\"\n}\n\ninclude \"common\" {\n  path = find_in_parent_folders(\"common.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/gcs-backend/unit2/terragrunt.hcl",
    "content": "terraform {\n  source = \"tfr://registry.terraform.io/yorinasub17/terragrunt-registry-test/null//modules/one?version=0.0.2\"\n}\n\ninclude \"common\" {\n  path = find_in_parent_folders(\"common.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/gcs-byo-bucket/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/gcs-byo-bucket/main.tf",
    "content": "terraform {\n  backend \"gcs\" {}\n\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\n# Create an arbitrary local resource\nresource \"null_resource\" \"test\" {\n  provisioner \"local-exec\" {\n    command = \"echo Hello, World!\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/gcs-byo-bucket/terragrunt.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an existing GCS bucket\nremote_state {\n  backend = \"gcs\"\n\n  config = {\n    # we are explicitly testing that a GCS bucket already exists and terragrunt should\n    # work without project and location.\n    #project  = \"\"\n    #location = \"\"\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    prefix = \"terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/gcs-impersonate/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/gcs-impersonate/main.tf",
    "content": "terraform {\n  backend \"gcs\" {}\n}\n\noutput \"value\" {\n  value = \"42\"\n}\n"
  },
  {
    "path": "test/fixtures/gcs-impersonate/terragrunt.hcl",
    "content": "remote_state {\n  backend = \"gcs\"\n\n  config = {\n    project                     = \"__FILL_IN_PROJECT__\"\n    location                    = \"__FILL_IN_LOCATION__\"\n    bucket                      = \"__FILL_IN_BUCKET_NAME__\"\n    impersonate_service_account = \"__FILL_IN_GCP_EMAIL__\"\n    prefix                      = \"terraform.tfstate\"\n\n    gcs_bucket_labels = {\n      owner = \"terragrunt_test\"\n      name  = \"terraform_state_storage\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/gcs-no-bucket/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/gcs-no-bucket/main.tf",
    "content": "terraform {\n  backend \"gcs\" {}\n}\n\noutput \"result\" {\n  value = \"42\"\n}\n"
  },
  {
    "path": "test/fixtures/gcs-no-bucket/terragrunt.hcl",
    "content": "# Test validation for missing bucket name in configuration\nremote_state {\n  backend = \"gcs\"\n\n  config = {\n    project  = \"__FILL_IN_PROJECT__\"\n    location = \"__FILL_IN_LOCATION__\"\n    prefix   = \"terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/gcs-no-prefix/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/gcs-no-prefix/main.tf",
    "content": "terraform {\n  backend \"gcs\" {}\n}\n\noutput \"result\" {\n  value = \"42\"\n}\n"
  },
  {
    "path": "test/fixtures/gcs-no-prefix/terragrunt.hcl",
    "content": "# Track that can be used GCS storage without prefix\nremote_state {\n  backend = \"gcs\"\n\n  config = {\n    project  = \"__FILL_IN_PROJECT__\"\n    location = \"__FILL_IN_LOCATION__\"\n    bucket   = \"__FILL_IN_BUCKET_NAME__\"\n\n  }\n}\n"
  },
  {
    "path": "test/fixtures/gcs-parallel-state-init/root.hcl",
    "content": "remote_state {\n  backend = \"gcs\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    project        = \"__FILL_IN_PROJECT__\"\n    location       = \"__FILL_IN_LOCATION__\"\n    bucket         = \"__FILL_IN_BUCKET_NAME__\"\n    prefix         = \"${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/gcs-parallel-state-init/template/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/gcs-parallel-state-init/template/main.tf",
    "content": "resource \"local_file\" \"file\" {\n  content  = \"test file\"\n  filename = \"${path.module}/file.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/gcs-parallel-state-init/template/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/get-aws-account-alias/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-aws-account-alias/main.tf",
    "content": "variable \"account_alias\" {\n  type = string\n}\n\noutput \"account_alias\" {\n  value = var.account_alias\n}\n"
  },
  {
    "path": "test/fixtures/get-aws-account-alias/terragrunt.hcl",
    "content": "inputs = {\n  account_alias = get_aws_account_alias()\n}\n"
  },
  {
    "path": "test/fixtures/get-aws-caller-identity/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-aws-caller-identity/main.tf",
    "content": "variable \"account\" {\n  type = string\n}\n\nvariable \"arn\" {\n  type = string\n}\n\nvariable \"user_id\" {\n  type = string\n}\n\noutput \"account\" {\n  value = var.account\n}\n\noutput \"arn\" {\n  value = var.arn\n}\n\noutput \"user_id\" {\n  value = var.user_id\n}\n"
  },
  {
    "path": "test/fixtures/get-aws-caller-identity/terragrunt.hcl",
    "content": "inputs = {\n  account = get_aws_account_id()\n  arn = get_aws_caller_identity_arn()\n  user_id = get_aws_caller_identity_user_id()\n}\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/aa/foo/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/aa/foo/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/aa/foo/terragrunt.hcl",
    "content": "dependency \"foo\" {\n  config_path = \"../foo\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/aba/bar/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/aba/bar/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/aba/bar/terragrunt.hcl",
    "content": "dependency \"foo\" {\n  config_path = \"../foo\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/aba/foo/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/aba/foo/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/aba/foo/terragrunt.hcl",
    "content": "dependency \"bar\" {\n  config_path = \"../bar\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abca/bar/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abca/bar/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abca/bar/terragrunt.hcl",
    "content": "dependency \"foo\" {\n  config_path = \"../foo\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abca/baz/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abca/baz/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abca/baz/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abca/foo/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abca/foo/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abca/foo/terragrunt.hcl",
    "content": "dependency \"bar\" {\n  config_path = \"../bar\"\n}\n\ndependency \"baz\" {\n  config_path = \"../baz\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abcda/bar/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abcda/bar/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abcda/bar/terragrunt.hcl",
    "content": "dependency \"baz\" {\n  config_path = \"../baz\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abcda/baz/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abcda/baz/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abcda/baz/terragrunt.hcl",
    "content": "dependency \"car\" {\n  config_path = \"../car\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abcda/car/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abcda/car/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abcda/car/terragrunt.hcl",
    "content": "dependency \"foo\" {\n  config_path = \"../foo\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abcda/foo/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abcda/foo/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/get-output/cycle/abcda/foo/terragrunt.hcl",
    "content": "dependency \"bar\" {\n  config_path = \"../bar\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/download-dir/in-config/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/download-dir/in-config/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/get-output/download-dir/in-config/terragrunt.hcl",
    "content": "remote_state {\n  backend = \"local\"\n  config = {\n    path = \"${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n\ndownload_dir = \"${get_terragrunt_dir()}/.download\"\n"
  },
  {
    "path": "test/fixtures/get-output/download-dir/not-set/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/download-dir/not-set/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/get-output/download-dir/not-set/terragrunt.hcl",
    "content": "remote_state {\n  backend = \"local\"\n  config = {\n    path = \"${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/integration/app1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/integration/app1/main.tf",
    "content": "output \"x\" {\n  value = 14\n}\n"
  },
  {
    "path": "test/fixtures/get-output/integration/app1/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/get-output/integration/app2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/integration/app2/main.tf",
    "content": "output \"y\" {\n  value = 28\n}\n"
  },
  {
    "path": "test/fixtures/get-output/integration/app2/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/get-output/integration/app3/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/integration/app3/main.tf",
    "content": "variable \"x\" {}\n\nvariable \"y\" {}\n\noutput \"z\" {\n  value = var.x + var.y\n}\n"
  },
  {
    "path": "test/fixtures/get-output/integration/app3/terragrunt.hcl",
    "content": "locals {\n  app1_path = \"../app1\"\n  app2_path = \"../app2\"\n}\n\ndependency \"app1\" {\n  config_path = local.app1_path\n}\n\ndependency \"app2\" {\n  config_path = local.app2_path\n}\n\ninputs = {\n  x = dependency.app1.outputs.x\n  y = dependency.app2.outputs.y\n}\n"
  },
  {
    "path": "test/fixtures/get-output/integration/empty/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/integration/empty/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/get-output/integration/empty/terragrunt.hcl",
    "content": "# Intentionally only has one dependency with skip_outputs to test logic that it doesn't attempt to pull the outputs.\ndependency \"app1\" {\n  config_path = \"../app1\"\n  skip_outputs = true\n}\n"
  },
  {
    "path": "test/fixtures/get-output/localstate/live/child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../../modules/child\"\n}\n\ndependency \"x\" {\n  config_path = \"../parent\"\n}\n\ninputs = {\n  x = dependency.x.outputs.x\n}\n"
  },
  {
    "path": "test/fixtures/get-output/localstate/live/parent/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../../modules/parent\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/localstate/live/root.hcl",
    "content": "remote_state {\n  backend = \"local\"\n  config = {\n    path = \"${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/localstate/modules/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/localstate/modules/child/main.tf",
    "content": "terraform {\n  backend \"local\" {}\n}\n\nvariable \"x\" {}\n\noutput \"y\" {\n  value = var.x * 3\n}\n"
  },
  {
    "path": "test/fixtures/get-output/localstate/modules/parent/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/localstate/modules/parent/main.tf",
    "content": "terraform {\n  backend \"local\" {}\n}\n\noutput \"x\" {\n  value = 14\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs/dependent1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs/dependent1/main.tf",
    "content": "variable \"the_answer\" {}\n\noutput \"truth\" {\n  value = \"The answer is ${var.the_answer}\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs/dependent1/terragrunt.hcl",
    "content": "dependency \"source\" {\n  config_path = \"../source\"\n  mock_outputs = {\n    the_answer = \"0\"\n  }\n}\n\ninputs = {\n  the_answer = dependency.source.outputs.the_answer\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs/dependent2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs/dependent2/main.tf",
    "content": "variable \"the_answer\" {}\n\noutput \"fake\" {\n  value = \"never ${var.the_answer}\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs/dependent2/terragrunt.hcl",
    "content": "dependency \"source\" {\n  config_path = \"../source\"\n  mock_outputs = {\n    the_answer = \"0\"\n  }\n  mock_outputs_allowed_terraform_commands = [\"validate\"]\n}\n\ninputs = {\n  the_answer = dependency.source.outputs.the_answer\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs/dependent3/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs/dependent3/main.tf",
    "content": "variable \"the_answer\" {}\n\noutput \"truth\" {\n  value = \"The answer is ${var.the_answer}\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs/dependent3/terragrunt.hcl",
    "content": "dependency \"source\" {\n  config_path = \"../source\"\n  mock_outputs = {\n    the_answer = \"0\"\n  }\n  skip_outputs = true\n}\n\ninputs = {\n  the_answer = dependency.source.outputs.the_answer\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs/source/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs/source/main.tf",
    "content": "output \"the_answer\" {\n  value = 42\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs/source/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/live/child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"x\" {\n  config_path = \"../parent\"\n\n  mock_outputs_merge_with_state = \"false\"\n  mock_outputs_merge_strategy_with_state = \"true\"\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"apply\", \"output\"]\n  mock_outputs = {\n    test_output1 = \"fake-output1\"\n    test_output_map_map_string = {\n      map_root1 = {\n        map_root1_sub1 = \"fake-map_root1_sub1\"\n      }\n      not_in_state = {\n        abc = \"fake-abc\"\n      }\n    }\n    test_output_list_string = [\"fake-list-data\"]\n  }\n}\n\ninputs = {\n  test_input1 = dependency.x.outputs.test_output1\n\n  test_input_map_map_string = dependency.x.outputs.test_output_map_map_string\n\n  test_input_list_string = dependency.x.outputs.test_output_list_string\n}\n\nterraform {\n  source = \"../..//modules/child\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/live/parent/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../..//modules/parent\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/live/root.hcl",
    "content": "remote_state {\n  backend = \"local\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    path = \"${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/modules/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/modules/child/main.tf",
    "content": "variable \"test_input1\" {\n  type = string\n}\n\noutput \"test_output1_from_parent\" {\n  value = var.test_input1\n}\n\nvariable \"test_input_map_map_string\" {\n  type = map(map(string))\n}\n\noutput \"test_output_map_map_string_from_parent\" {\n  value = var.test_input_map_map_string\n}\n\nvariable \"test_input_list_string\" {\n  type = list(string)\n}\n\noutput \"test_output_list_string\" {\n  value = var.test_input_list_string\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/modules/parent/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-conflict/modules/parent/main.tf",
    "content": "output \"test_output1\" {\n  value = \"value1\"\n}\n\noutput \"test_output_map_map_string\" {\n  value = {\n    map_root1 = {\n      map_root1_sub1 = \"map_root1_sub1_value\"\n    }\n    not_in_state = {\n      abc = \"123\"\n    }\n  }\n}\n\noutput \"test_output_list_string\" {\n  value = [\n    \"a\",\n    \"b\",\n    \"c\"\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/live/child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"x\" {\n  config_path = \"../parent\"\n\n  mock_outputs_merge_with_state = \"false\"\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"apply\", \"output\"]\n  mock_outputs = {\n    test_output1 = \"fake-output1\"\n    test_output_map_map_string = {\n      map_root1 = {\n        map_root1_sub1 = \"fake-map_root1_sub1\"\n      }\n      not_in_state = {\n        abc = \"fake-abc\"\n      }\n    }\n    test_output_list_string = [\"fake-list-data\"]\n  }\n}\n\ninputs = {\n  test_input1 = dependency.x.outputs.test_output1\n\n  test_input_map_map_string = dependency.x.outputs.test_output_map_map_string\n\n  test_input_list_string = dependency.x.outputs.test_output_list_string\n}\n\nterraform {\n  source = \"../..//modules/child\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/live/parent/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../..//modules/parent\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/live/root.hcl",
    "content": "remote_state {\n  backend = \"local\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    path = \"${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/modules/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/modules/child/main.tf",
    "content": "variable \"test_input1\" {\n  type = string\n}\n\noutput \"test_output1_from_parent\" {\n  value = var.test_input1\n}\n\nvariable \"test_input_map_map_string\" {\n  type = map(map(string))\n}\n\noutput \"test_output_map_map_string_from_parent\" {\n  value = var.test_input_map_map_string\n}\n\nvariable \"test_input_list_string\" {\n  type = list(string)\n}\n\noutput \"test_output_list_string\" {\n  value = var.test_input_list_string\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/modules/parent/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-false/modules/parent/main.tf",
    "content": "output \"test_output1\" {\n  value = \"value1\"\n}\n\noutput \"test_output_map_map_string\" {\n  value = {\n    map_root1 = {\n      map_root1_sub1 = \"map_root1_sub1_value\"\n    }\n    not_in_state = {\n      abc = \"123\"\n    }\n  }\n}\n\noutput \"test_output_list_string\" {\n  value = [\n    \"a\",\n    \"b\",\n    \"c\"\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/live/child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"x\" {\n  config_path = \"../parent\"\n\n  mock_outputs_merge_with_state = \"true\"\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"apply\", \"output\"]\n  mock_outputs = {\n    test_output1 = \"fake-output1\"\n    test_output_map_map_string = {\n      map_root1 = {\n        map_root1_sub1 = \"fake-map_root1_sub1\"\n      }\n      not_in_state = {\n        abc = \"fake-abc\"\n      }\n    }\n    test_output_list_string = [\"fake-list-data\"]\n  }\n}\n\ninputs = {\n  test_input1 = dependency.x.outputs.test_output1\n\n  test_input_map_map_string = dependency.x.outputs.test_output_map_map_string\n\n  test_input_list_string = dependency.x.outputs.test_output_list_string\n}\n\nterraform {\n  source = \"../..//modules/child\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/live/parent/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../..//modules/parent\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/live/root.hcl",
    "content": "remote_state {\n  backend = \"local\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    path = \"${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/modules/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/modules/child/main.tf",
    "content": "variable \"test_input1\" {\n  type = string\n}\n\noutput \"test_output1_from_parent\" {\n  value = var.test_input1\n}\n\nvariable \"test_input_map_map_string\" {\n  type = map(map(string))\n}\n\noutput \"test_output_map_map_string_from_parent\" {\n  value = var.test_input_map_map_string\n}\n\nvariable \"test_input_list_string\" {\n  type = list(string)\n}\n\noutput \"test_output_list_string\" {\n  value = var.test_input_list_string\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/modules/parent/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-compat-true/modules/parent/main.tf",
    "content": "output \"test_output1\" {\n  value = \"value1\"\n}\n\noutput \"test_output_map_map_string\" {\n  value = {\n    map_root1 = {\n      map_root1_sub1 = \"map_root1_sub1_value\"\n    }\n    not_in_state = {\n      abc = \"123\"\n    }\n  }\n}\n\noutput \"test_output_list_string\" {\n  value = [\n    \"a\",\n    \"b\",\n    \"c\"\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/live/child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"x\" {\n  config_path = \"../parent\"\n\n  mock_outputs_merge_strategy_with_state = \"deep_map_only\"\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"apply\", \"output\"]\n  mock_outputs = {\n    test_output1 = \"fake-output1\"\n    test_output_map_map_string = {\n      map_root1 = {\n        map_root1_sub1 = \"fake-map_root1_sub1\"\n      }\n      not_in_state = {\n        abc = \"fake-abc\"\n      }\n    }\n    test_output_list_string = [\"fake-list-data\"]\n  }\n}\n\ninputs = {\n  test_input1 = dependency.x.outputs.test_output1\n  test_input2 = dependency.x.outputs.test_output_map_map_string.not_in_state.abc\n\n  test_input_map_map_string = dependency.x.outputs.test_output_map_map_string\n\n  test_input_list_string = dependency.x.outputs.test_output_list_string\n}\n\nterraform {\n  source = \"../..//modules/child\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/live/parent/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../..//modules/parent\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/live/root.hcl",
    "content": "remote_state {\n  backend = \"local\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    path = \"${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/modules/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/modules/child/main.tf",
    "content": "variable \"test_input1\" {\n  type = string\n}\n\noutput \"test_output1_from_parent\" {\n  value = var.test_input1\n}\n\nvariable \"test_input2\" {\n  type = string\n}\n\noutput \"test_output2_from_parent\" {\n  value = var.test_input2\n}\n\nvariable \"test_input_map_map_string\" {\n  type = map(map(string))\n}\n\noutput \"test_output_map_map_string_from_parent\" {\n  value = var.test_input_map_map_string\n}\n\nvariable \"test_input_list_string\" {\n  type = list(string)\n}\n\noutput \"test_output_list_string\" {\n  value = var.test_input_list_string\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/modules/parent/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-deep-map-only/modules/parent/main.tf",
    "content": "output \"test_output1\" {\n  value = \"value1\"\n}\n\noutput \"test_output_map_map_string\" {\n  value = {\n    map_root1 = {\n      map_root1_sub1 = \"map_root1_sub1_value\"\n    }\n    not_in_state = {\n      abc = \"123\"\n    }\n  }\n}\n\noutput \"test_output_list_string\" {\n  value = [\n    \"a\",\n    \"b\",\n    \"c\"\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/live/child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"x\" {\n  config_path = \"../parent\"\n\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"apply\", \"output\"]\n  mock_outputs = {\n    test_output1 = \"fake-output1\"\n    test_output_map_map_string = {\n      map_root1 = {\n        map_root1_sub1 = \"fake-map_root1_sub1\"\n      }\n      not_in_state = {\n        abc = \"fake-abc\"\n      }\n    }\n    test_output_list_string = [\"fake-list-data\"]\n  }\n}\n\ninputs = {\n  test_input1 = dependency.x.outputs.test_output1\n\n  test_input_map_map_string = dependency.x.outputs.test_output_map_map_string\n\n  test_input_list_string = dependency.x.outputs.test_output_list_string\n}\n\nterraform {\n  source = \"../..//modules/child\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/live/parent/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../..//modules/parent\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/live/root.hcl",
    "content": "remote_state {\n  backend = \"local\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    path = \"${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/modules/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/modules/child/main.tf",
    "content": "variable \"test_input1\" {\n  type = string\n}\n\noutput \"test_output1_from_parent\" {\n  value = var.test_input1\n}\n\nvariable \"test_input_map_map_string\" {\n  type = map(map(string))\n}\n\noutput \"test_output_map_map_string_from_parent\" {\n  value = var.test_input_map_map_string\n}\n\nvariable \"test_input_list_string\" {\n  type = list(string)\n}\n\noutput \"test_output_list_string\" {\n  value = var.test_input_list_string\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/modules/parent/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-default/modules/parent/main.tf",
    "content": "output \"test_output1\" {\n  value = \"value1\"\n}\n\noutput \"test_output_map_map_string\" {\n  value = {\n    map_root1 = {\n      map_root1_sub1 = \"map_root1_sub1_value\"\n    }\n    not_in_state = {\n      abc = \"123\"\n    }\n  }\n}\n\noutput \"test_output_list_string\" {\n  value = [\n    \"a\",\n    \"b\",\n    \"c\"\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/live/child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"x\" {\n  config_path = \"../parent\"\n\n  mock_outputs_merge_strategy_with_state = \"no_merge\"\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"apply\", \"output\"]\n  mock_outputs = {\n    test_output1 = \"fake-output1\"\n    test_output_map_map_string = {\n      map_root1 = {\n        map_root1_sub1 = \"fake-map_root1_sub1\"\n      }\n      not_in_state = {\n        abc = \"fake-abc\"\n      }\n    }\n    test_output_list_string = [\"fake-list-data\"]\n  }\n}\n\ninputs = {\n  test_input1 = dependency.x.outputs.test_output1\n\n  test_input_map_map_string = dependency.x.outputs.test_output_map_map_string\n\n  test_input_list_string = dependency.x.outputs.test_output_list_string\n}\n\nterraform {\n  source = \"../..//modules/child\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/live/parent/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../..//modules/parent\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/live/root.hcl",
    "content": "remote_state {\n  backend = \"local\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    path = \"${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/modules/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/modules/child/main.tf",
    "content": "variable \"test_input1\" {\n  type = string\n}\n\noutput \"test_output1_from_parent\" {\n  value = var.test_input1\n}\n\nvariable \"test_input_map_map_string\" {\n  type = map(map(string))\n}\n\noutput \"test_output_map_map_string_from_parent\" {\n  value = var.test_input_map_map_string\n}\n\nvariable \"test_input_list_string\" {\n  type = list(string)\n}\n\noutput \"test_output_list_string\" {\n  value = var.test_input_list_string\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/modules/parent/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-no-merge/modules/parent/main.tf",
    "content": "output \"test_output1\" {\n  value = \"value1\"\n}\n\noutput \"test_output_map_map_string\" {\n  value = {\n    map_root1 = {\n      map_root1_sub1 = \"map_root1_sub1_value\"\n    }\n    not_in_state = {\n      abc = \"123\"\n    }\n  }\n}\n\noutput \"test_output_list_string\" {\n  value = [\n    \"a\",\n    \"b\",\n    \"c\"\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/live/child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"x\" {\n  config_path = \"../parent\"\n\n  mock_outputs_merge_strategy_with_state = \"shallow\"\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"apply\", \"output\"]\n  mock_outputs = {\n    test_output1 = \"fake-output1\"\n    test_output_map_map_string = {\n      map_root1 = {\n        map_root1_sub1 = \"fake-map_root1_sub1\"\n      }\n      not_in_state = {\n        abc = \"fake-abc\"\n      }\n    }\n    test_output_list_string = [\"fake-list-data\"]\n  }\n}\n\ninputs = {\n  test_input1 = dependency.x.outputs.test_output1\n\n  test_input_map_map_string = dependency.x.outputs.test_output_map_map_string\n\n  test_input_list_string = dependency.x.outputs.test_output_list_string\n}\n\nterraform {\n  source = \"../..//modules/child\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/live/parent/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../..//modules/parent\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/live/root.hcl",
    "content": "remote_state {\n  backend = \"local\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    path = \"${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/modules/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/modules/child/main.tf",
    "content": "variable \"test_input1\" {\n  type = string\n}\n\noutput \"test_output1_from_parent\" {\n  value = var.test_input1\n}\n\nvariable \"test_input_map_map_string\" {\n  type = map(map(string))\n}\n\noutput \"test_output_map_map_string_from_parent\" {\n  value = var.test_input_map_map_string\n}\n\nvariable \"test_input_list_string\" {\n  type = list(string)\n}\n\noutput \"test_output_list_string\" {\n  value = var.test_input_list_string\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/modules/parent/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-strategy-with-state/merge-strategy-with-state-shallow/modules/parent/main.tf",
    "content": "output \"test_output1\" {\n  value = \"value1\"\n}\n\noutput \"test_output_map_map_string\" {\n  value = {\n    map_root1 = {\n      map_root1_sub1 = \"map_root1_sub1_value\"\n    }\n    not_in_state = {\n      abc = \"123\"\n    }\n  }\n}\n\noutput \"test_output_list_string\" {\n  value = [\n    \"a\",\n    \"b\",\n    \"c\"\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-default/live/child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"x\" {\n  config_path = \"../parent\"\n\n  mock_outputs_allowed_terraform_commands = [\"output\", \"validate\", \"init\", \"destroy\", \"plan\", \"apply\", \"info\"]\n  mock_outputs = {\n    test_output1 = \"fake-data\"\n    test_output2 = \"fake-data2\"\n  }\n}\n\ninputs = {\n  test_input1 = dependency.x.outputs.test_output1\n  test_input2 = dependency.x.outputs.test_output2\n}\n\nterraform {\n  source = \"../..//modules/child\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-default/live/parent/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../..//modules/parent\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-default/live/root.hcl",
    "content": "remote_state {\n  backend = \"local\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    path = \"${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-default/modules/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-default/modules/child/main.tf",
    "content": "variable \"test_input1\" {\n  type = string\n}\n\noutput \"test_output1_from_parent\" {\n  value = var.test_input1\n}\n\nvariable \"test_input2\" {\n  type = string\n}\n\noutput \"test_output2_from_parent\" {\n  value = var.test_input2\n}"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-default/modules/parent/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-default/modules/parent/main.tf",
    "content": "output \"test_output1\" {\n  value = \"value1\"\n}\n\noutput \"test_output2\" {\n  value = \"value2\"\n}"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-false/live/child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"x\" {\n  config_path = \"../parent\"\n\n  mock_outputs_allowed_terraform_commands = [\"output\", \"validate\", \"init\", \"destroy\", \"plan\", \"apply\", \"info\"]\n  mock_outputs_merge_with_state = false\n  mock_outputs = {\n    test_output1 = \"fake-data\"\n    test_output2 = \"fake-data2\"\n  }\n}\n\ninputs = {\n  test_input1 = dependency.x.outputs.test_output1\n  test_input2 = dependency.x.outputs.test_output2\n}\n\nterraform {\n  source = \"../..//modules/child\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-false/live/parent/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../..//modules/parent\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-false/live/root.hcl",
    "content": "remote_state {\n  backend = \"local\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    path = \"${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-false/modules/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-false/modules/child/main.tf",
    "content": "variable \"test_input1\" {\n  type = string\n}\n\noutput \"test_output1_from_parent\" {\n  value = var.test_input1\n}\n\nvariable \"test_input2\" {\n  type = string\n}\n\noutput \"test_output2_from_parent\" {\n  value = var.test_input2\n}"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-false/modules/parent/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-false/modules/parent/main.tf",
    "content": "output \"test_output1\" {\n  value = \"value1\"\n}\n\noutput \"test_output2\" {\n  value = \"value2\"\n}"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-no-override/live/child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"x\" {\n  config_path = \"../parent\"\n\n  mock_outputs_allowed_terraform_commands = [\"output\", \"validate\", \"init\", \"destroy\", \"plan\", \"apply\", \"info\"]\n  mock_outputs_merge_with_state = true\n  mock_outputs = {\n    test_output1 = \"fake-data\"\n    test_output2 = \"fake-data2\"\n  }\n}\n\ninputs = {\n  test_input1 = dependency.x.outputs.test_output1\n  test_input2 = dependency.x.outputs.test_output2\n}\n\nterraform {\n  source = \"../..//modules/child\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-no-override/live/parent/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../..//modules/parent\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-no-override/live/root.hcl",
    "content": "remote_state {\n  backend = \"local\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    path = \"${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-no-override/modules/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-no-override/modules/child/main.tf",
    "content": "variable \"test_input1\" {\n  type = string\n}\n\noutput \"test_output1_from_parent\" {\n  value = var.test_input1\n}\n\nvariable \"test_input2\" {\n  type = string\n}\n\noutput \"test_output2_from_parent\" {\n  value = var.test_input2\n}"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-no-override/modules/parent/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-no-override/modules/parent/main.tf",
    "content": "output \"test_output1\" {\n  value = \"value1\"\n}\n\noutput \"test_output2\" {\n  value = \"value2\"\n}"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-true/live/child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"x\" {\n  config_path = \"../parent\"\n\n  mock_outputs_allowed_terraform_commands = [\"output\", \"validate\", \"init\", \"destroy\", \"plan\", \"apply\", \"info\"]\n  mock_outputs_merge_with_state = true\n  mock_outputs = {\n    test_output1 = \"fake-data\"\n    test_output2 = \"fake-data2\"\n  }\n}\n\ninputs = {\n  test_input1 = dependency.x.outputs.test_output1\n  test_input2 = dependency.x.outputs.test_output2\n}\n\nterraform {\n  source = \"../..//modules/child\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-true/live/parent/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../..//modules/parent\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-true/live/root.hcl",
    "content": "remote_state {\n  backend = \"local\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    path = \"${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-true/modules/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-true/modules/child/main.tf",
    "content": "variable \"test_input1\" {\n  type = string\n}\n\noutput \"test_output1_from_parent\" {\n  value = var.test_input1\n}\n\nvariable \"test_input2\" {\n  type = string\n}\n\noutput \"test_output2_from_parent\" {\n  value = var.test_input2\n}"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-true/modules/parent/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-true/modules/parent/main.tf",
    "content": "output \"test_output1\" {\n  value = \"value1\"\n}\n\noutput \"test_output2\" {\n  value = \"value2\"\n}"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-true-validate-only/live/child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"x\" {\n  config_path = \"../parent\"\n\n  mock_outputs_allowed_terraform_commands = [\"validate\"]\n  mock_outputs_merge_with_state = true\n  mock_outputs = {\n    test_output1 = \"fake-data\"\n    test_output2 = \"fake-data2\"\n  }\n}\n\ninputs = {\n  test_input1 = dependency.x.outputs.test_output1\n  test_input2 = dependency.x.outputs.test_output2\n}\n\nterraform {\n  source = \"../..//modules/child\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-true-validate-only/live/parent/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../..//modules/parent\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-true-validate-only/live/root.hcl",
    "content": "remote_state {\n  backend = \"local\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    path = \"${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-true-validate-only/modules/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-true-validate-only/modules/child/main.tf",
    "content": "variable \"test_input1\" {\n  type = string\n}\n\noutput \"test_output1_from_parent\" {\n  value = var.test_input1\n}\n\nvariable \"test_input2\" {\n  type = string\n}\n\noutput \"test_output2_from_parent\" {\n  value = var.test_input2\n}"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-true-validate-only/modules/parent/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/mock-outputs-merge-with-state/merge-with-state-true-validate-only/modules/parent/main.tf",
    "content": "output \"test_output1\" {\n  value = \"value1\"\n}\n\noutput \"test_output2\" {\n  value = \"value2\"\n}"
  },
  {
    "path": "test/fixtures/get-output/nested-mocks/deepdep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/nested-mocks/deepdep/main.tf",
    "content": "output \"output\" {\n  value = \"The answer is 42\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-mocks/deepdep/terragrunt.hcl",
    "content": "# Intentionally empty.\n"
  },
  {
    "path": "test/fixtures/get-output/nested-mocks/dep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/nested-mocks/dep/main.tf",
    "content": "variable \"input\" {}\noutput \"output\" {\n  value = \"No, ${var.input}\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-mocks/dep/terragrunt.hcl",
    "content": "dependency \"deepdep\" {\n  config_path = \"../deepdep\"\n  mock_outputs = {\n    output = \"I am a mock\"\n  }\n  mock_outputs_allowed_terraform_commands = [\"validate\"]\n}\n\ninputs = {\n  input = dependency.deepdep.outputs.output\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-mocks/live/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/nested-mocks/live/main.tf",
    "content": "variable \"input\" {}\noutput \"output\" {\n  value = \"They said, \\\"${var.input}\\\"\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-mocks/live/terragrunt.hcl",
    "content": "dependency \"dep\" {\n  config_path = \"../dep\"\n  mock_outputs = {\n    output = \"I am a shallow mock\"\n  }\n  mock_outputs_allowed_terraform_commands = [\"validate\"]\n}\n\ninputs = {\n  input = dependency.dep.outputs.output\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization/.gitignore",
    "content": "backend.tf\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization/deepdep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization/deepdep/main.tf",
    "content": "output \"output\" {\n  value = \"The answer is 42\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization/deepdep/terragrunt.hcl",
    "content": "# Intentionally empty.\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization/dep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization/dep/main.tf",
    "content": "variable \"input\" {}\noutput \"output\" {\n  value = \"No, ${var.input}\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization/dep/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\n# Retrieve a dependency. In the test, we will destroy this state and verify we can still get the output.\ndependency \"deepdep\" {\n  config_path = \"../deepdep\"\n}\n\ninputs = {\n  input = dependency.deepdep.outputs.output\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization/live/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization/live/main.tf",
    "content": "variable \"input\" {}\noutput \"output\" {\n  value = \"They said, \\\"${var.input}\\\"\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization/live/terragrunt.hcl",
    "content": "# Retrieve a dependency. In the test, we will destroy this state and verify we can still get the output.\ndependency \"dep\" {\n  config_path = \"../dep\"\n}\n\ninputs = {\n  input = dependency.dep.outputs.output\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization/root.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"${path_relative_to_include()}/terraform.tfstate\"\n    region = \"us-west-2\"\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n    enable_lock_table_ssencryption = true\n  }\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-disable/.gitignore",
    "content": "backend.tf\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-disable/deepdep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-disable/deepdep/main.tf",
    "content": "output \"output\" {\n  value = \"The answer is 42\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-disable/deepdep/terragrunt.hcl",
    "content": "# Intentionally empty.\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-disable/dep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-disable/dep/main.tf",
    "content": "variable \"input\" {}\noutput \"output\" {\n  value = \"No, ${var.input}\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-disable/dep/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\n# Retrieve a dependency. In the test, we will destroy this state and verify we can still get the output.\ndependency \"deepdep\" {\n  config_path = \"../deepdep\"\n}\n\ninputs = {\n  input = dependency.deepdep.outputs.output\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-disable/live/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-disable/live/main.tf",
    "content": "variable \"input\" {}\noutput \"output\" {\n  value = \"They said, \\\"${var.input}\\\"\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-disable/live/terragrunt.hcl",
    "content": "# Retrieve a dependency. In the test, we will destroy this state and verify we can still get the output.\ndependency \"dep\" {\n  config_path = \"../dep\"\n}\n\ninputs = {\n  input = dependency.dep.outputs.output\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-disable/root.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  disable_dependency_optimization = true\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"${path_relative_to_include()}/terraform.tfstate\"\n    region = \"us-west-2\"\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n    enable_lock_table_ssencryption = true\n  }\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-nogen/.gitignore",
    "content": "backend.tf\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-nogen/deepdep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-nogen/deepdep/main.tf",
    "content": "output \"output\" {\n  value = \"The answer is 42\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-nogen/deepdep/terragrunt.hcl",
    "content": "# Intentionally empty.\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-nogen/dep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-nogen/dep/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n}\n\nvariable \"input\" {}\noutput \"output\" {\n  value = \"No, ${var.input}\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-nogen/dep/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\n# Retrieve a dependency. In the test, we will destroy this state and verify we can still get the output.\ndependency \"deepdep\" {\n  config_path = \"../deepdep\"\n}\n\ninputs = {\n  input = dependency.deepdep.outputs.output\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-nogen/live/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-nogen/live/main.tf",
    "content": "variable \"input\" {}\noutput \"output\" {\n  value = \"They said, \\\"${var.input}\\\"\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-nogen/live/terragrunt.hcl",
    "content": "# Retrieve a dependency. In the test, we will destroy this state and verify we can still get the output.\ndependency \"dep\" {\n  config_path = \"../dep\"\n}\n\ninputs = {\n  input = dependency.dep.outputs.output\n}\n"
  },
  {
    "path": "test/fixtures/get-output/nested-optimization-nogen/root.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"${path_relative_to_include()}/terraform.tfstate\"\n    region = \"us-west-2\"\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n    enable_lock_table_ssencryption = true\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-1102/.gitignore",
    "content": ".terraform.lock.hcl\nfoo.tfstate"
  },
  {
    "path": "test/fixtures/get-output/regression-1102/backend.tf",
    "content": "# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa\nterraform {\n  backend \"local\" {\n    path          = \"foo.tfstate\"\n    workspace_dir = \".\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-1102/main.tf",
    "content": "\n"
  },
  {
    "path": "test/fixtures/get-output/regression-1102/terragrunt.hcl",
    "content": "remote_state {\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  backend = \"local\"\n  config = {\n    workspace_dir  = \".\"\n    path = \"foo.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-1124/live/app/terragrunt.hcl",
    "content": "terraform {\n  source = \"../../modules//app\"\n}\n\ndependency \"dep\" {\n  config_path = \"../dependency\"\n}\n\ninputs = {\n  foo = dependency.dep.outputs.foo\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-1124/live/dependency/terragrunt.hcl",
    "content": "terraform {\n  source = \"../../modules//dependency\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-1124/modules/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/regression-1124/modules/app/main.tf",
    "content": "variable \"foo\" {}\noutput \"foo\" { value = var.foo }\n"
  },
  {
    "path": "test/fixtures/get-output/regression-1124/modules/dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/random\" {\n  version = \"3.8.0\"\n  hashes = [\n    \"h1:aEaTEHutDdKNaztKFmInhfzmZK0/OaVL8uxmncM9YF8=\",\n    \"h1:ey4eBIHiuAC5xsblxtXghXE3nWwUvGqTT6KAsggiAwo=\",\n    \"h1:nRPdhXsZpGPMppuUgBe/ZcAtD73NaCLGROYHXv41qz8=\",\n    \"zh:2d5e0bbfac7f15595739fe54a9ab8b8eea92fd6d879706139dad7ecaa5c01c19\",\n    \"zh:349e637066625d97aaa84db1b1418c86d6457cf9c5a62f6dcc3f55cbd535112c\",\n    \"zh:5f4456d53f5256ccfdb87dd35d3bf34578d01bd9b71cffaf507f0692805eac8a\",\n    \"zh:6c1ecfacc5f7079a068d7f8eb8924485d4ec8183f36e6318a6e748d35921ddac\",\n    \"zh:6d86641edeb8c394f121f7b0a691d72f89cf9b938b987a01fc32aad396a50555\",\n    \"zh:76947bd7bc7033b33980538da149c94e386f9b0abb2ce63733f25a57517e4742\",\n    \"zh:79c07f4c8b3a63d9f89e25e4348b462c57e179bca66ba533710851c485e282db\",\n    \"zh:ac1c2b941d994728a3a93aba093fd2202f9311d099ff85f66678897c792161ba\",\n    \"zh:cbb2aa867fd828fcb4125239e00862b9a3bc2f280e945c760224276b476f4c49\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-1124/modules/dependency/main.tf",
    "content": "resource \"random_string\" \"random\" {\n  length = 16\n}\n\noutput \"foo\" {\n  value = random_string.random.result\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-1273/dep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/regression-1273/dep/main.tf",
    "content": "output \"output\" {\n  value = \"hello, world\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-1273/dep/terragrunt.hcl",
    "content": "terraform {\n  # This hook configures Terragrunt to create an empty file called file.out\n  # after execution of terragrunt\n  before_hook \"before_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\", \"file.out\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-1273/main/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/regression-1273/main/main.tf",
    "content": "variable \"input\" {}\noutput \"output\" {\n  value = var.input\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-1273/main/terragrunt.hcl",
    "content": "dependency \"dep\" {\n  config_path = \"../dep\"\n}\n\ninputs = {\n  input = dependency.dep.outputs.output\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-854/root/environments/network/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/regression-854/root/environments/network/main.tf",
    "content": "output \"id\" {\n  value = 42\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-854/root/environments/network/terragrunt.hcl",
    "content": "include {\n  path = \"../../terragrunt.hcl\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-854/root/environments/web/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/regression-854/root/environments/web/main.tf",
    "content": "output \"id\" {\n  value = 42\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-854/root/environments/web/sg/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/regression-854/root/environments/web/sg/main.tf",
    "content": "output \"id\" {\n  value = 42\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-854/root/environments/web/sg/terragrunt.hcl",
    "content": "include {\n  path = \"../../../terragrunt.hcl\"\n}\n\ndependency \"network\" {\n  config_path = \"../../network\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-854/root/environments/web/terragrunt.hcl",
    "content": "include {\n  path = \"../../terragrunt.hcl\"\n}\n\ndependency \"network\" {\n  config_path = \"../network\"\n}\n\ndependency \"sg\" {\n  config_path = \"./sg\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-854/root/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/get-output/regression-906/a/terragrunt.hcl",
    "content": "dependency \"hello_world\" {\n  config_path = \"../common-dep\"\n}\n\ninputs = {\n  name = dependency.hello_world.outputs.rendered_template\n}\n\nterraform {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/download/hello-world?ref=v0.83.2\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-906/b/terragrunt.hcl",
    "content": "dependency \"hello_world\" {\n  config_path = \"../common-dep\"\n}\n\ninputs = {\n  name = dependency.hello_world.outputs.rendered_template\n}\n\nterraform {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/download/hello-world?ref=v0.83.2\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-906/c/terragrunt.hcl",
    "content": "dependency \"hello_world\" {\n  config_path = \"../common-dep\"\n}\n\ninputs = {\n  name = dependency.hello_world.outputs.rendered_template\n}\n\nterraform {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/download/hello-world?ref=v0.83.2\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-906/common-dep/terragrunt.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket  = \"__FILL_IN_BUCKET_NAME__\"\n    key     = \"terraform.tfstate\"\n    region  = \"us-west-2\"\n  }\n}\n\nterraform {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixture?ref=v0.21.0\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-906/d/terragrunt.hcl",
    "content": "dependency \"hello_world\" {\n  config_path = \"../common-dep\"\n}\n\ninputs = {\n  name = dependency.hello_world.outputs.rendered_template\n}\n\nterraform {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/download/hello-world?ref=v0.83.2\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-906/e/terragrunt.hcl",
    "content": "dependency \"hello_world\" {\n  config_path = \"../common-dep\"\n}\n\ninputs = {\n  name = dependency.hello_world.outputs.rendered_template\n}\n\nterraform {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/download/hello-world?ref=v0.83.2\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-906/f/terragrunt.hcl",
    "content": "dependency \"hello_world\" {\n  config_path = \"../common-dep\"\n}\n\ninputs = {\n  name = dependency.hello_world.outputs.rendered_template\n}\n\nterraform {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/download/hello-world?ref=v0.83.2\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/regression-906/g/terragrunt.hcl",
    "content": "dependency \"hello_world\" {\n  config_path = \"../common-dep\"\n}\n\ninputs = {\n  name = dependency.hello_world.outputs.rendered_template\n}\n\nterraform {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/download/hello-world?ref=v0.83.2\"\n}\n"
  },
  {
    "path": "test/fixtures/get-output/run-all-source/live/unit1/terragrunt.hcl",
    "content": "terraform {\n  source = \"../../modules-default//module1\"\n}\n\n"
  },
  {
    "path": "test/fixtures/get-output/run-all-source/live/unit2/terragrunt.hcl",
    "content": "terraform {\n  source = \"../../modules-default//module2\"\n}\n\n"
  },
  {
    "path": "test/fixtures/get-output/run-all-source/modules-default/module1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/run-all-source/modules-default/module1/main.tf",
    "content": "variable \"test_var\" {\n  default = \"module1\"\n}\n\noutput \"module_name\" {\n  value = var.test_var\n}\n\n"
  },
  {
    "path": "test/fixtures/get-output/run-all-source/modules-default/module2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/run-all-source/modules-default/module2/main.tf",
    "content": "variable \"test_var\" {\n  default = \"module2\"\n}\n\noutput \"module_name\" {\n  value = var.test_var\n}\n\n"
  },
  {
    "path": "test/fixtures/get-output/run-all-source/modules-marked/module1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/run-all-source/modules-marked/module1/MODULE1_MARKER",
    "content": "This is a marker file for module1 to verify the correct source path was used.\n\n"
  },
  {
    "path": "test/fixtures/get-output/run-all-source/modules-marked/module1/main.tf",
    "content": "variable \"test_var\" {\n  default = \"module1\"\n}\n\noutput \"module_name\" {\n  value = var.test_var\n}\n\n"
  },
  {
    "path": "test/fixtures/get-output/run-all-source/modules-marked/module2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-output/run-all-source/modules-marked/module2/MODULE2_MARKER",
    "content": "This is a marker file for module2 to verify the correct source path was used.\n\n"
  },
  {
    "path": "test/fixtures/get-output/run-all-source/modules-marked/module2/main.tf",
    "content": "variable \"test_var\" {\n  default = \"module2\"\n}\n\noutput \"module_name\" {\n  value = var.test_var\n}\n\n"
  },
  {
    "path": "test/fixtures/get-output/type-conversion/terragrunt.hcl",
    "content": "terraform {\n  source = \"${get_terragrunt_dir()}/../../inputs\"\n}\n\ndependency \"inputs\" {\n  config_path = \"../../inputs\"\n}\n\ninputs = dependency.inputs.outputs\n"
  },
  {
    "path": "test/fixtures/get-path/get-path-from-repo-root/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-path/get-path-from-repo-root/main.tf",
    "content": "variable \"path_from_root\" {\n  type = string\n}\n\noutput \"path_from_root\" {\n  value = var.path_from_root\n}\n"
  },
  {
    "path": "test/fixtures/get-path/get-path-from-repo-root/terragrunt.hcl",
    "content": "inputs = {\n  path_from_root  = get_path_from_repo_root()\n}\n"
  },
  {
    "path": "test/fixtures/get-path/get-path-to-repo-root/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-path/get-path-to-repo-root/main.tf",
    "content": "variable \"path_to_root\" {\n  type = string\n}\n\nvariable \"path_to_modules\" {\n  type = string\n}\n\noutput \"path_to_root\" {\n  value = var.path_to_root\n}\n\noutput \"path_to_modules\" {\n  value = var.path_to_modules\n}\n"
  },
  {
    "path": "test/fixtures/get-path/get-path-to-repo-root/terragrunt.hcl",
    "content": "inputs = {\n  path_to_root = get_path_to_repo_root()\n  path_to_modules = \"${get_path_to_repo_root()}/modules\"\n}\n"
  },
  {
    "path": "test/fixtures/get-path/path_relative_from_include/lives/dev/base/terragrunt.hcl",
    "content": "terraform {\n  source = \"../../../modules//base\"\n}\n\ninclude {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/get-path/path_relative_from_include/lives/dev/base/tier.hcl",
    "content": "locals {\n  tier = \"base\"\n}\n"
  },
  {
    "path": "test/fixtures/get-path/path_relative_from_include/lives/dev/cluster/terragrunt.hcl",
    "content": "terraform {\n  source = \"../../../modules//cluster\"\n}\n\ninclude {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"base\" {\n  config_path = \"../base\"\n}\n\ninputs = {\n  some_input = dependency.base.outputs.some_output\n}\n"
  },
  {
    "path": "test/fixtures/get-path/path_relative_from_include/lives/dev/cluster/tier.hcl",
    "content": "locals {\n  tier = \"cluster\"\n}\n"
  },
  {
    "path": "test/fixtures/get-path/path_relative_from_include/lives/dev/env.hcl",
    "content": "locals {\n  environment = \"dev\"\n}\n"
  },
  {
    "path": "test/fixtures/get-path/path_relative_from_include/lives/org.hcl",
    "content": "locals {\n  organization_unit = \"test\"\n}\n"
  },
  {
    "path": "test/fixtures/get-path/path_relative_from_include/lives/root.hcl",
    "content": "locals {\n  org_vars = read_terragrunt_config(\"${get_parent_terragrunt_dir()}/org.hcl\")\n  env_vars = read_terragrunt_config(\"${get_parent_terragrunt_dir()}/dev/env.hcl\")\n  tier_vars = read_terragrunt_config(\"tier.hcl\")\n\n  organization_unit = local.org_vars.locals.organization_unit\n  environment       = local.env_vars.locals.environment\n  tier              = local.tier_vars.locals.tier\n}\n\ngenerate \"provider\" {\n  path      = \"terraform.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents  = <<-EOF\n    terraform {\n      backend \"local\" {\n        path = \"${local.environment}-${local.tier}.state\"\n      }\n    }\n    EOF\n}\n\ninputs = merge(\n  local.org_vars.locals,\n  local.env_vars.locals,\n  local.tier_vars.locals,\n  )\n"
  },
  {
    "path": "test/fixtures/get-path/path_relative_from_include/modules/base/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-path/path_relative_from_include/modules/base/main.tf",
    "content": "output \"some_output\" {\n  value = \"something\"\n}\n"
  },
  {
    "path": "test/fixtures/get-path/path_relative_from_include/modules/cluster/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-path/path_relative_from_include/modules/cluster/main.tf",
    "content": "variable \"some_input\" {}\n\noutput \"some_output\" {\n  value = \"${var.some_input} else\"\n}\n"
  },
  {
    "path": "test/fixtures/get-platform/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-platform/main.tf",
    "content": "variable \"platform\" {\n  type = string\n}\n\noutput \"platform\" {\n  value = var.platform\n}\n"
  },
  {
    "path": "test/fixtures/get-platform/terragrunt.hcl",
    "content": "inputs = {\n  platform = get_platform()\n}\n"
  },
  {
    "path": "test/fixtures/get-repo-root/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-repo-root/main.tf",
    "content": "variable \"repo_root\" {\n  type = string\n}\n\noutput \"repo_root\" {\n  value = var.repo_root\n}\n"
  },
  {
    "path": "test/fixtures/get-repo-root/terragrunt.hcl",
    "content": "inputs = {\n  repo_root = get_repo_root()\n  repo_root_2 = get_repo_root()\n}\n"
  },
  {
    "path": "test/fixtures/get-terragrunt-source-cli/terraform_config_cli/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-terragrunt-source-cli/terraform_config_cli/main.tf",
    "content": "variable \"terragrunt_source\" {\n  type = string\n}\n\noutput \"terragrunt_source\" {\n  value = \"CLI: ${var.terragrunt_source}\"\n}\n"
  },
  {
    "path": "test/fixtures/get-terragrunt-source-cli/terragrunt.hcl",
    "content": "inputs = {\n  terragrunt_source = get_terragrunt_source_cli_flag()\n}\n"
  },
  {
    "path": "test/fixtures/get-terragrunt-source-hcl/terraform_config_hcl/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-terragrunt-source-hcl/terraform_config_hcl/main.tf",
    "content": "variable \"terragrunt_source\" {\n  type = string\n}\n\noutput \"terragrunt_source\" {\n  value = \"HCL: ${var.terragrunt_source}\"\n}\n"
  },
  {
    "path": "test/fixtures/get-terragrunt-source-hcl/terragrunt.hcl",
    "content": "inputs = {\n  terragrunt_source = get_terragrunt_source_cli_flag()\n}\n\nterraform {\n  source = \"./terraform_config_hcl\"\n}\n"
  },
  {
    "path": "test/fixtures/get-working-dir/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-working-dir/main.tf",
    "content": "variable \"working_dir\" {\n  type = string\n}\n\noutput \"working_dir\" {\n  value = var.working_dir\n}\n"
  },
  {
    "path": "test/fixtures/get-working-dir/modules/a/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/get-working-dir/modules/a/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/get-working-dir/terragrunt.hcl",
    "content": "terraform {\n  source = \"${local.source_base_url}/a\"\n}\n\nlocals {\n  source_base_url = \"modules\"\n}\n\ninputs = {\n  working_dir = get_working_dir()\n}\n"
  },
  {
    "path": "test/fixtures/graph/eks/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/graph/eks/main.tf",
    "content": "output \"result\" {\n  value = \"42\"\n}"
  },
  {
    "path": "test/fixtures/graph/eks/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/graph/lambda/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/graph/lambda/main.tf",
    "content": "output \"result\" {\n  value = \"42\"\n}"
  },
  {
    "path": "test/fixtures/graph/lambda/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/graph/services/eks-service-1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-1/main.tf",
    "content": "output \"result\" {\n  value = \"42\"\n}"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-1/terragrunt.hcl",
    "content": "dependency \"eks\" {\n  config_path = \"../../eks\"\n}"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-2/main.tf",
    "content": "output \"result\" {\n  value = \"42\"\n}"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-2/terragrunt.hcl",
    "content": "dependency \"eks\" {\n  config_path = \"../../eks\"\n}"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-2-v2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-2-v2/main.tf",
    "content": "output \"result\" {\n  value = \"42\"\n}"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-2-v2/terragrunt.hcl",
    "content": "dependency \"eks-service-2\" {\n  config_path = \"../eks-service-2\"\n}"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-3/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-3/main.tf",
    "content": "output \"result\" {\n  value = \"42\"\n}"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-3/terragrunt.hcl",
    "content": "dependency \"eks-service-1\" {\n  config_path = \"../eks-service-1\"\n}\n\ndependency \"eks-service-2\" {\n  config_path = \"../eks-service-2\"\n}"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-3-v2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-3-v2/main.tf",
    "content": "output \"result\" {\n  value = \"42\"\n}"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-3-v2/terragrunt.hcl",
    "content": "dependency \"eks-service-3\" {\n  config_path = \"../eks-service-3\"\n}"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-3-v3/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-3-v3/main.tf",
    "content": "output \"result\" {\n  value = \"42\"\n}"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-3-v3/terragrunt.hcl",
    "content": "dependency \"eks-service-3-v2\" {\n  config_path = \"../eks-service-3-v2\"\n}"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-4/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-4/main.tf",
    "content": "output \"result\" {\n  value = \"42\"\n}"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-4/terragrunt.hcl",
    "content": "dependency \"eks-service-3\" {\n  config_path = \"../eks-service-3\"\n}"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-5/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-5/main.tf",
    "content": "output \"result\" {\n  value = \"42\"\n}"
  },
  {
    "path": "test/fixtures/graph/services/eks-service-5/terragrunt.hcl",
    "content": "dependency \"eks\" {\n  config_path = \"../../eks\"\n}\n\ndependency \"eks-service-2\" {\n  config_path = \"../eks-service-2\"\n}"
  },
  {
    "path": "test/fixtures/graph/services/lambda-service-1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/graph/services/lambda-service-1/main.tf",
    "content": "output \"result\" {\n  value = \"42\"\n}"
  },
  {
    "path": "test/fixtures/graph/services/lambda-service-1/terragrunt.hcl",
    "content": "dependency \"lambda\" {\n  config_path = \"../../lambda\"\n}\n"
  },
  {
    "path": "test/fixtures/graph/services/lambda-service-2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/graph/services/lambda-service-2/main.tf",
    "content": "output \"result\" {\n  value = \"42\"\n}"
  },
  {
    "path": "test/fixtures/graph/services/lambda-service-2/terragrunt.hcl",
    "content": "dependency \"lambda-service-1\" {\n  config_path = \"../lambda-service-1\"\n}"
  },
  {
    "path": "test/fixtures/graph-dependencies/root/backend-app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/graph-dependencies/root/backend-app/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n}\n"
  },
  {
    "path": "test/fixtures/graph-dependencies/root/backend-app/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../mysql\", \"../redis\", \"../vpc\"]\n}\n\n"
  },
  {
    "path": "test/fixtures/graph-dependencies/root/frontend-app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/graph-dependencies/root/frontend-app/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n}\n"
  },
  {
    "path": "test/fixtures/graph-dependencies/root/frontend-app/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../backend-app\", \"../vpc\"]\n}\n"
  },
  {
    "path": "test/fixtures/graph-dependencies/root/mysql/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/graph-dependencies/root/mysql/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n}\n"
  },
  {
    "path": "test/fixtures/graph-dependencies/root/mysql/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../vpc\"]\n}\n\n"
  },
  {
    "path": "test/fixtures/graph-dependencies/root/redis/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/graph-dependencies/root/redis/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n}\n"
  },
  {
    "path": "test/fixtures/graph-dependencies/root/redis/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../vpc\"]\n}\n\n"
  },
  {
    "path": "test/fixtures/graph-dependencies/root/vpc/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/graph-dependencies/root/vpc/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n}\n"
  },
  {
    "path": "test/fixtures/graph-dependencies/root/vpc/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\n\n"
  },
  {
    "path": "test/fixtures/graph-dependencies/root.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"${path_relative_to_include()}/terraform.tfstate\"\n    region = \"us-west-2\"\n  }\n}\n\ninputs = {\n  terraform_remote_state_s3_bucket = \"__FILL_IN_BUCKET_NAME__\"\n}\n"
  },
  {
    "path": "test/fixtures/hcl-filter/fmt/already-formatted/app1/terragrunt.hcl",
    "content": "inputs = {\n  # Properly formatted\n  app_name    = \"app1\"\n  port        = 3000\n  environment = \"dev\"\n  enabled     = true\n}\n\n"
  },
  {
    "path": "test/fixtures/hcl-filter/fmt/already-formatted/app2/terragrunt.hcl",
    "content": "inputs = {\n  # Properly formatted\n  app_name    = \"app2\"\n  port        = 3001\n  environment = \"staging\"\n  enabled     = false\n}\n\n"
  },
  {
    "path": "test/fixtures/hcl-filter/fmt/needs-formatting/db/terragrunt.hcl",
    "content": "inputs = {\n  # Badly formatted database config\n  database_name=\"mydb\"\n  max_connections     =        100\n  timeout=30\n  pool_size   =10\n}\n\n"
  },
  {
    "path": "test/fixtures/hcl-filter/fmt/needs-formatting/nested/api/terragrunt.hcl",
    "content": "inputs = {\n  # Also badly formatted\n  api_name=\"api-service\"\n  max_connections     =        100\n  timeout=30\n  retry_count   =5\n}\n\n"
  },
  {
    "path": "test/fixtures/hcl-filter/fmt/needs-formatting/nested/deep/web/terragrunt.hcl",
    "content": "inputs = {\n  # Badly formatted\n  web_name =                    \"web-service\"\n  port=8080\n  environment  =   \"production\"\n  replicas=3\n}\n\n"
  },
  {
    "path": "test/fixtures/hcl-filter/fmt/stacks/already-formatted/stack2/terragrunt.stack.hcl",
    "content": "units = {\n  # Properly formatted stack\n  unit1 = {\n    path = \"./unit1\"\n  }\n  unit2 = {\n    path = \"./unit2\"\n  }\n}\n\n"
  },
  {
    "path": "test/fixtures/hcl-filter/fmt/stacks/needs-formatting/stack1/terragrunt.stack.hcl",
    "content": "units = {\n  # Badly formatted stack\n  unit1={\n    path=\"./unit1\"\n  }\n  unit2  =  {\n    path     =\"./unit2\"\n  }\n}\n\n"
  },
  {
    "path": "test/fixtures/hcl-filter/validate/semantic-error/incomplete-block/terragrunt.hcl",
    "content": "locals {\n  t =\n}\n\n"
  },
  {
    "path": "test/fixtures/hcl-filter/validate/semantic-error/missing-value/terragrunt.hcl",
    "content": "inputs = {\n  name = \n  port = 8080\n}\n\n"
  },
  {
    "path": "test/fixtures/hcl-filter/validate/stacks/syntax-error/stack2/terragrunt.stack.hcl",
    "content": "# Invalid syntax in stack file - missing value for path\nunit \"unit1\" {\n  source = \"git::https://github.com/example/repo.git//modules/unit1\"\n  path =\n}\n\n"
  },
  {
    "path": "test/fixtures/hcl-filter/validate/stacks/valid/stack1/terragrunt.stack.hcl",
    "content": "unit \"unit1\" {\n  source = \"git::https://github.com/example/repo.git//modules/unit1\"\n  path = \"./unit1\"\n}\n\nunit \"unit2\" {\n  source = \"git::https://github.com/example/repo.git//modules/unit2\"\n  path = \"./unit2\"\n}\n\n"
  },
  {
    "path": "test/fixtures/hcl-filter/validate/syntax-error/invalid-char/terragrunt.hcl",
    "content": "# Invalid starting character\n$foo = \"bar\"\n\n"
  },
  {
    "path": "test/fixtures/hcl-filter/validate/syntax-error/invalid-key/terragrunt.hcl",
    "content": "# Invalid key syntax\nfoo.bar.baz = \"xyz\"\n\n"
  },
  {
    "path": "test/fixtures/hcl-filter/validate/valid/db/terragrunt.hcl",
    "content": "inputs = {\n  database_name   = \"mydb\"\n  max_connections = 100\n  timeout         = 30\n}\n\n"
  },
  {
    "path": "test/fixtures/hcl-filter/validate/valid/nested/api/terragrunt.hcl",
    "content": "inputs = {\n  api_name = \"api-service\"\n  port     = 3000\n  timeout  = 30\n}\n\n"
  },
  {
    "path": "test/fixtures/hcl-filter/validate/valid/nested/deep/web/terragrunt.hcl",
    "content": "inputs = {\n  web_name    = \"web-service\"\n  port        = 8080\n  environment = \"production\"\n}\n\n"
  },
  {
    "path": "test/fixtures/hclfmt-check/a/b/c/d/e/terragrunt.hcl",
    "content": "inputs = {\n  # comments\n  foo = \"bar\"\n  bar = \"baz\"\n\n  inputs   = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n    \"foo\",\n    \"bar\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hclfmt-check/a/b/c/d/services.hcl",
    "content": "inputs = {\n  # comments\n  foo = \"bar\"\n  bar = \"baz\"\n\n  inputs   = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n    \"foo\",\n    \"bar\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hclfmt-check/a/b/c/terragrunt.hcl",
    "content": "inputs = {\n  # comments\n  foo = \"bar\"\n  bar = \"baz\"\n\n  inputs   = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n    \"foo\",\n    \"bar\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hclfmt-check/a/terragrunt.hcl",
    "content": "inputs = {\n  # comments\n  foo = \"bar\"\n  bar = \"baz\"\n\n  inputs   = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n    \"foo\",\n    \"bar\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hclfmt-check/expected.hcl",
    "content": "inputs = {\n  # comments\n  foo = \"bar\"\n  bar = \"baz\"\n\n  inputs   = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n    \"foo\",\n    \"bar\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hclfmt-check/terragrunt.hcl",
    "content": "inputs = {\n  # comments\n  foo = \"bar\"\n  bar = \"baz\"\n\n  inputs   = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n    \"foo\",\n    \"bar\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hclfmt-check-errors/a/b/c/d/e/terragrunt.hcl",
    "content": "inputs = {\n# comments\n  foo =                               \"bar\"\n  bar=\"baz\"\n\n  inputs = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n\"foo\",\n\"bar\",\n]\n}\n"
  },
  {
    "path": "test/fixtures/hclfmt-check-errors/a/b/c/d/services.hcl",
    "content": "inputs = {\n# comments\n  foo =                               \"bar\"\n  bar=\"baz\"\n\n  inputs = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n\"foo\",\n\"bar\",\n]\n}\n"
  },
  {
    "path": "test/fixtures/hclfmt-check-errors/a/b/c/terragrunt.hcl",
    "content": "inputs = {\n# comments\n  foo =                               \"bar\"\n  bar=\"baz\"\n\n  inputs = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n\"foo\",\n\"bar\",\n]\n}\n"
  },
  {
    "path": "test/fixtures/hclfmt-check-errors/a/terragrunt.hcl",
    "content": "inputs = {\n# comments\n  foo =                               \"bar\"\n  bar=\"baz\"\n\n  inputs = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n\"foo\",\n\"bar\",\n]\n}\n"
  },
  {
    "path": "test/fixtures/hclfmt-check-errors/expected.hcl",
    "content": "inputs = {\n# comments\n  foo =                               \"bar\"\n  bar=\"baz\"\n\n  inputs = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n\"foo\",\n\"bar\",\n]\n}\n"
  },
  {
    "path": "test/fixtures/hclfmt-check-errors/terragrunt.hcl",
    "content": "inputs = {\n# comments\n  foo =                               \"bar\"\n  bar=\"baz\"\n\n  inputs = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n\"foo\",\n\"bar\",\n]\n}\n"
  },
  {
    "path": "test/fixtures/hclfmt-diff/expected.diff",
    "content": "@@ -1,11 +1,11 @@\n inputs = {\n-# comments\n-  foo =                               \"bar\"\n-  bar=\"baz\"\n-  inputs = \"disjoint\"\n+  # comments\n+  foo      = \"bar\"\n+  bar      = \"baz\"\n+  inputs   = \"disjoint\"\n   disjoint = true\n   listInput = [\n-\"foo\",\n-\"bar\",\n-]\n+    \"foo\",\n+    \"bar\",\n+  ]\n }"
  },
  {
    "path": "test/fixtures/hclfmt-diff/terragrunt.hcl",
    "content": "inputs = {\n# comments\n  foo =                               \"bar\"\n  bar=\"baz\"\n  inputs = \"disjoint\"\n  disjoint = true\n  listInput = [\n\"foo\",\n\"bar\",\n]\n}"
  },
  {
    "path": "test/fixtures/hclfmt-errors/dangling-attribute/terragrunt.hcl",
    "content": "# Dangling attribute, where no value is set\ninputs =\n"
  },
  {
    "path": "test/fixtures/hclfmt-errors/invalid-character/terragrunt.hcl",
    "content": "# Invalid starting character\n$foo = \"bar\"\n"
  },
  {
    "path": "test/fixtures/hclfmt-errors/invalid-key/terragrunt.hcl",
    "content": "# Invalid key\nfoo.bar.baz = \"xyz\"\n"
  },
  {
    "path": "test/fixtures/hclfmt-heredoc/expected.hcl",
    "content": "inputs = {\n  foo     = <<EOF\nHello\nWorld\nEOF\n  version = \"V1\"\n  bar     = \"foo\"\n}\n"
  },
  {
    "path": "test/fixtures/hclfmt-heredoc/terragrunt.hcl",
    "content": "inputs = {\n  foo = <<EOF\nHello\nWorld\nEOF\n  version = \"V1\"\n  bar = \"foo\"\n}\n"
  },
  {
    "path": "test/fixtures/hclfmt-stdin/expected.hcl",
    "content": "inputs = {\n  # comments\n  foo = \"bar\"\n  bar = \"baz\"\n\n  inputs   = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n    \"foo\",\n    \"bar\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hclfmt-stdin/terragrunt.hcl",
    "content": "inputs = {\n# comments\n  foo =                               \"bar\"\n  bar=\"baz\"\n\n  inputs = \"disjoint\"\n  disjoint = true\n\n  listInput = [\n\"foo\",\n\"bar\",\n]\n}\n"
  },
  {
    "path": "test/fixtures/hclvalidate/first/b/terragrunt.hcl",
    "content": "dependency \"a\" {\n  config_path = \"${path_relative_from_include()}/${path_relative_to_include()}/../a\"\n}\n"
  },
  {
    "path": "test/fixtures/hclvalidate/second/a/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/hclvalidate/second/a/main.tf",
    "content": "variable \"a\" {\n  type    = string\n  default = \"a\"\n}\n\noutput \"a\" {\n  value = var.a\n}\n"
  },
  {
    "path": "test/fixtures/hclvalidate/second/a/terragrunt.hcl",
    "content": "locals {\n  t =\n}\n"
  },
  {
    "path": "test/fixtures/hclvalidate/second/c/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/hclvalidate/second/c/main.tf",
    "content": "variable \"c\" {\n  type    = string\n  default = \"c\"\n}\n\noutput \"c\" {\n  value = var.c\n}\n"
  },
  {
    "path": "test/fixtures/hclvalidate/second/c/terragrunt.hcl",
    "content": "include \"b\" {\n  path = \"../../first/b/terragrunt.hcl\"\n}\n\ninputs = {\n  c = dependency.a.outputs.z\n}\n\nlocals {\n  vvv = dependency.a.outputs.z\n\n  ddd = dependency.d\n}\n\n// should not cause a dependency cycle\ndependency iam {\n  //config_path = \"../iam\"\n}\n"
  },
  {
    "path": "test/fixtures/hclvalidate/second/d/main.tf",
    "content": "variabl \"d\" { //codespell:ignore\n  type    = string\n  default = \"d\"\n}\n\noutput \"d\" {\n  value = var.d\n}\n"
  },
  {
    "path": "test/fixtures/hclvalidate/second/d/terragrunt.hcl",
    "content": "dependency \"c\" {\n  config_path = \"../c\"\n\n  mock_outputs = {\n    c = \"mocked-c\"\n  }\n}\n\ninputs = {\n  d = dependency.c.outputs.c\n}\n"
  },
  {
    "path": "test/fixtures/hclvalidate/valid/circular-reference/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/hclvalidate/valid/circular-reference/main.tf",
    "content": "locals {\n  first  = local.second\n  second = local.first\n}\n"
  },
  {
    "path": "test/fixtures/hclvalidate/valid/circular-reference/terragrunt.hcl",
    "content": "// intentionally blank\n"
  },
  {
    "path": "test/fixtures/hclvalidate/valid/invalid-local/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/hclvalidate/valid/invalid-local/main.tf",
    "content": "# This terraform code is intentionally invalid\noutput \"out\" {\n  value = local.nonexistent\n}\n"
  },
  {
    "path": "test/fixtures/hclvalidate/valid/invalid-local/terragrunt.hcl",
    "content": "// intentionally blank\n"
  },
  {
    "path": "test/fixtures/hclvalidate/valid/single-required-input/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/hclvalidate/valid/single-required-input/main.tf",
    "content": "variable \"input\" {\n}\n"
  },
  {
    "path": "test/fixtures/hclvalidate/valid/single-required-input/terragrunt.hcl",
    "content": "inputs = {\n  input = \"value\"\n}\n"
  },
  {
    "path": "test/fixtures/hclvalidate/valid/validation-block/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/hclvalidate/valid/validation-block/main.tf",
    "content": "variable \"example_var\" {\n  type        = string\n  description = \"A variable that requires validation.\"\n  default     = \"test\"\n\n  validation {\n    condition     = length(var.example_var) > 0\n    error_message = \"The example_var must not be empty.\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hclvalidate/valid/validation-block/terragrunt.hcl",
    "content": "// intentionally blank\n"
  },
  {
    "path": "test/fixtures/hclvalidate/valid/var-in-source/main.tf",
    "content": "locals {\n  variable_source = \"github.com/foo/bar\"\n}\n\nmodule \"module\" {\n  source  = local.variable_source\n  version = \"0.0.0\"\n}\n"
  },
  {
    "path": "test/fixtures/hclvalidate/valid/var-in-source/terragrunt.hcl",
    "content": "// intentionally blank\n"
  },
  {
    "path": "test/fixtures/hclvalidate/valid/var-in-version/main.tf",
    "content": "locals {\n  variable_version = \"0.0.0\"\n}\n\nmodule \"module\" {\n  source  = \"github.com/foo/bar\"\n  version = local.variable_version\n}\n"
  },
  {
    "path": "test/fixtures/hclvalidate/valid/var-in-version/terragrunt.hcl",
    "content": "// intentionally blank\n"
  },
  {
    "path": "test/fixtures/hidden-runall/.cloud/terraform/app1/main.tf",
    "content": "output \"text\" {\n  value = \"hidden1\"\n}\n\n"
  },
  {
    "path": "test/fixtures/hidden-runall/.cloud/terraform/app1/terragrunt.hcl",
    "content": "\n"
  },
  {
    "path": "test/fixtures/hidden-runall/.cloud/terraform/app2/main.tf",
    "content": "output \"text\" {\n  value = \"hidden2\"\n}\n\n"
  },
  {
    "path": "test/fixtures/hidden-runall/.cloud/terraform/app2/terragrunt.hcl",
    "content": "\n"
  },
  {
    "path": "test/fixtures/hooks/after-only/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hooks/after-only/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"example\" {\n  provisioner \"local-exec\" {\n    command = \"echo hello, world\"\n  }\n}\n\noutput \"example\" {\n  value = \"hello, world\"\n}\n"
  },
  {
    "path": "test/fixtures/hooks/after-only/terragrunt.hcl",
    "content": "terraform {\n  # This hook configures Terragrunt to create an empty file called file.out\n  # after execution of terragrunt\n  after_hook \"after_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\",\"${get_terragrunt_dir()}/file.out\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/all/after-only/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hooks/all/after-only/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"example\" {\n  provisioner \"local-exec\" {\n    command = \"echo hello, world\"\n  }\n}\n\noutput \"example\" {\n  value = \"hello, world\"\n}\n"
  },
  {
    "path": "test/fixtures/hooks/all/after-only/terragrunt.hcl",
    "content": "terraform {\n  # This hook configures Terragrunt to create an empty file called file.out\n  # after execution of terragrunt\n  after_hook \"after_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\",\"${get_terragrunt_dir()}/file.out\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/all/before-only/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hooks/all/before-only/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"example\" {\n  provisioner \"local-exec\" {\n    command = \"echo hello, world\"\n  }\n}\n\noutput \"example\" {\n  value = \"hello, world\"\n}\n"
  },
  {
    "path": "test/fixtures/hooks/all/before-only/terragrunt.hcl",
    "content": "terraform {\n\n  # This hook configures Terragrunt to create an empty file called file.out\n  # before execution of terragrunt\n  before_hook \"before_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\",\"${get_terragrunt_dir()}/file.out\"]\n    run_on_error = true\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/bad-arg-action/empty-command-list/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hooks/bad-arg-action/empty-command-list/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"test\" {\n  provisioner \"local-exec\" {\n    command = \"echo Hello, World!\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/bad-arg-action/empty-command-list/terragrunt.hcl",
    "content": "terraform {\n  # This hook is purposely misconfigured to trigger an error\n  before_hook \"before_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = []\n    run_on_error = true\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/bad-arg-action/empty-string-command/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hooks/bad-arg-action/empty-string-command/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"test\" {\n  provisioner \"local-exec\" {\n    command = \"echo Hello, World!\"\n  }\n}\n\noutput \"example\" {\n  value = data.template_file.example.rendered\n}\n"
  },
  {
    "path": "test/fixtures/hooks/bad-arg-action/empty-string-command/terragrunt.hcl",
    "content": "terraform {\n  # This hook is purposely misconfigured to trigger an error\n  before_hook \"before_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"\"]\n    run_on_error = true\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/before-after-and-error-merge/qa/my-app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hooks/before-after-and-error-merge/qa/my-app/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2\"\n    }\n  }\n}\n\nresource \"null_resource\" \"example\" {\n  provisioner \"local-exec\" {\n    command = \"echo hello, world\"\n  }\n}\n\noutput \"example\" {\n  value = \"hello, world\"\n}\n"
  },
  {
    "path": "test/fixtures/hooks/before-after-and-error-merge/qa/my-app/terragrunt.hcl",
    "content": "terraform {\n  # This hook configures Terragrunt to create an empty file called before.out\n  # before execution of terragrunt\n  before_hook \"before_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\",\"before.out\"]\n    run_on_error = true\n  }\n\n  # This hook configures Terragrunt to create an empty file called before-child.out\n  # This will merge up and override the parent before_hook\n  # before execution of terragrunt\n  before_hook \"before_hook_merge_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\",\"before-child.out\"]\n    run_on_error = true\n  }\n\n  # This hook configures Terragrunt to create an empty file called after.out\n  # after execution of terragrunt\n  after_hook \"after_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\",\"after.out\"]\n    run_on_error = true\n  }\n\n  error_hook \"error_hook_merge_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\",\"error-hook-merge-child.out\"]\n    on_errors = [\".*\"]\n  }\n\n  error_hook \"error_hook_child\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\",\"error-hook-child.out\"]\n    on_errors = [\".*\"]\n  }\n}\n\ninclude {\n  path = \"${find_in_parent_folders(\"root.hcl\")}\"\n}\n"
  },
  {
    "path": "test/fixtures/hooks/before-after-and-error-merge/root.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"${path_relative_to_include()}/terraform.tfstate\"\n    region = \"us-west-2\"\n  }\n}\n\nterraform {\n  # This hook configures Terragrunt to attempt to create an empty file called before-parent.out\n  # This will be overridden by the child before_hook\n  # before execution of terragrunt\n  before_hook \"before_hook_merge_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\",\"before-parent.out\"]\n    run_on_error = true\n  }\n\n  # This hook configures Terragrunt to create an empty file called after-parent.out\n  # after execution of terragrunt\n  after_hook \"after_hook_parent_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\",\"after-parent.out\"]\n    run_on_error = true\n  }\n\n  after_hook \"produce_error_to_test_error_hook\" {\n    commands = [\"apply\"]\n    execute = [\"exit\", \"1\"]\n    run_on_error = true\n  }\n\n  # This will be overridden by the child error_hook\n  error_hook \"error_hook_merge_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\",\"error-hook-merge-parent.out\"]\n    on_errors = [\".*\"]\n  }\n\n  error_hook \"error_hook_parent\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\",\"error-hook-parent.out\"]\n    on_errors = [\".*\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/before-after-and-on-error/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hooks/before-after-and-on-error/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"example\" {\n  provisioner \"local-exec\" {\n    command = \"echo hello, world\"\n  }\n}\n\noutput \"example\" {\n  value = \"hello, world\"\n}\n"
  },
  {
    "path": "test/fixtures/hooks/before-after-and-on-error/terragrunt.hcl",
    "content": "terraform {\n  # This hook configures Terragrunt to create an empty file called before.out\n  # before execution of terragrunt\n  before_hook \"before_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\",\"before.out\"]\n    run_on_error = true\n  }\n\n  # This hook configures Terragrunt to create an empty file called after.out\n  # after execution of terragrunt\n  after_hook \"after_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\",\"after.out\"]\n    run_on_error = true\n  }\n\n  before_hook \"before_hook_2\" {\n    commands = [\"terragrunt-read-config\"]\n    execute = [\"echo\", \"BEFORE_TERRAGRUNT_READ_CONFIG\"]\n    run_on_error = true\n  }\n\n  after_hook \"after_hook_2\" {\n    commands = [\"terragrunt-read-config\"]\n    execute = [\"echo\", \"AFTER_TERRAGRUNT_READ_CONFIG\"]\n    run_on_error = true\n  }\n\n  error_hook \"error_hook_1\" {\n    commands  = [\"apply\"]\n    execute   = [\"echo\", \"ON_APPLY_ERROR\"]\n    on_errors = [\".*\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/before-and-after/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hooks/before-and-after/hook.sh",
    "content": "#!/usr/bin/env bash\n\necho \"TF_PATH=${TG_CTX_TF_PATH} COMMAND=${TG_CTX_COMMAND} HOOK_NAME=${TG_CTX_HOOK_NAME}\"\n"
  },
  {
    "path": "test/fixtures/hooks/before-and-after/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"example\" {\n  provisioner \"local-exec\" {\n    command = \"echo hello, world\"\n  }\n}\n\noutput \"example\" {\n  value = \"hello, world\"\n}\n"
  },
  {
    "path": "test/fixtures/hooks/before-and-after/terragrunt.hcl",
    "content": "terraform {\n  # This hook configures Terragrunt to create an empty file called before.out\n  # before execution of terragrunt\n  before_hook \"before_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\",\"${get_terragrunt_dir()}/before.out\"]\n    run_on_error = true\n  }\n\n  # This hook configures Terragrunt to create an empty file called after.out\n  # after execution of terragrunt\n  after_hook \"after_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\",\"${get_terragrunt_dir()}/after.out\"]\n    run_on_error = true\n  }\n\n  before_hook \"before_hook_2\" {\n    commands = [\"terragrunt-read-config\"]\n    execute = [\"echo\", \"BEFORE_TERRAGRUNT_READ_CONFIG\"]\n    run_on_error = true\n  }\n\n  after_hook \"after_hook_2\" {\n    commands = [\"terragrunt-read-config\"]\n    execute = [\"echo\", \"AFTER_TERRAGRUNT_READ_CONFIG\"]\n    run_on_error = true\n  }\n\n  after_hook \"after_hook_3\" {\n    commands = [\"terragrunt-read-config\"]\n    execute = [\"./hook.sh\"]\n    run_on_error = true\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/before-only/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hooks/before-only/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"example\" {\n  provisioner \"local-exec\" {\n    command = \"echo hello, world\"\n  }\n}\n\noutput \"example\" {\n  value = \"hello, world\"\n}\n"
  },
  {
    "path": "test/fixtures/hooks/before-only/terragrunt.hcl",
    "content": "terraform {\n\n  # This hook configures Terragrunt to create an empty file called file.out\n  # before execution of terragrunt\n  before_hook \"before_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\",\"${get_terragrunt_dir()}/file.out\"]\n    run_on_error = true\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/error-hooks/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hooks/error-hooks/main.tf",
    "content": "\ndata \"local_file\" \"read_not_existing_file\" {\n  filename = \"${path.module}/not-existing-file.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/hooks/error-hooks/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n\n  error_hook \"pattern_matching_hook\" {\n    commands = [\"apply\"]\n    execute  = [\"echo\", \"pattern_matching_hook\"]\n    on_errors = [\n      \"not-existing-file.txt\"\n    ]\n  }\n\n  error_hook \"catch_all_matching_hook\" {\n    commands = [\"apply\"]\n    execute  = [\"echo\", \"catch_all_matching_hook\"]\n    on_errors = [\n      \".*\"\n    ]\n  }\n\n  error_hook \"not_matching_hook\" {\n    commands = [\"apply\"]\n    execute  = [\"echo\", \"not_matching_hook\"]\n    on_errors = [\n      \".*random-not-matching-pattern.*\"\n    ]\n  }\n\n}\n\n"
  },
  {
    "path": "test/fixtures/hooks/error-hooks/tf.sh",
    "content": "#!/usr/bin/env bash\n\n(set -x && exec \"${TG_TF_PATH:-tofu}\" \"$@\" 2>&1)\n"
  },
  {
    "path": "test/fixtures/hooks/error-hooks-source-download-fail/terragrunt.hcl",
    "content": "terraform {\n  # Invalid source URL that will cause download to fail\n  source = \"/totallyfakedoesnotexist/notreal\"\n\n  # This is the correct way to handle source download errors\n  error_hook \"error_on_source_download\" {\n    commands  = [\"init-from-module\"]\n    execute   = [\"echo\", \"ERROR_HOOK_TRIGGERED_ON_INIT_FROM_MODULE\"]\n    on_errors = [\".*\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/exit-code-error/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n}\n\noutput \"example\" {\n  value = \"test\"\n}\n"
  },
  {
    "path": "test/fixtures/hooks/exit-code-error/terragrunt.hcl",
    "content": "terraform {\n  before_hook \"hook_exit_nonzero\" {\n    commands = [\"apply\", \"plan\"]\n    execute  = [\"sh\", \"-c\", \"echo 'lint warning: something is wrong' >&2 && exit 2\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/if-parameter/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/hooks/if-parameter/main.tf",
    "content": "output \"example\" {\n  value = \"hello, world\"\n}\n"
  },
  {
    "path": "test/fixtures/hooks/if-parameter/terragrunt.hcl",
    "content": "locals {\n  run_hook = true\n}\n\nterraform {\n  before_hook \"run_this_one\" {\n    if = local.run_hook\n    commands = [\"apply\", \"plan\"]\n    execute = [\"echo\", \"running before hook\"]\n  }\n\n  after_hook \"skip_this_one\" {\n    if = !local.run_hook\n    commands = [\"apply\", \"plan\"]\n    execute = [\"echo\", \"skip after hook\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/init-once/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/hooks/init-once/backend.tf",
    "content": "terraform {\r\n  backend \"s3\" {}\r\n}"
  },
  {
    "path": "test/fixtures/hooks/init-once/base-module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hooks/init-once/base-module/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2\"\n    }\n  }\n}\n\nresource \"null_resource\" \"example\" {\n  provisioner \"local-exec\" {\n    command = \"echo hello, world\"\n  }\n}\n\noutput \"example\" {\n  value = \"hello, world\"\n}\n"
  },
  {
    "path": "test/fixtures/hooks/init-once/no-source-no-backend/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hooks/init-once/no-source-no-backend/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"example\" {\n  provisioner \"local-exec\" {\n    command = \"echo hello, world\"\n  }\n}\n\noutput \"example\" {\n  value = \"hello, world\"\n}\n"
  },
  {
    "path": "test/fixtures/hooks/init-once/no-source-no-backend/terragrunt.hcl",
    "content": "terraform {\n  # Should NOT execute. With no source, init-from-module should never execute.\n  # If AFTER_INIT_FROM_MODULE_ONLY_ONCE is present in output, the test failed\n  after_hook \"after_init_from_module\" {\n    commands = [\"init-from-module\"]\n    execute = [\"echo\",\"AFTER_INIT_FROM_MODULE_ONLY_ONCE\"]\n  }\n\n  # SHOULD execute\n  # If AFTER_INIT_ONLY_ONCE is not present exactly once in output, the test failed\n  after_hook \"after_init\" {\n    commands = [\"init\"]\n    execute = [\"echo\",\"AFTER_INIT_ONLY_ONCE\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/init-once/no-source-with-backend/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hooks/init-once/no-source-with-backend/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2\"\n    }\n  }\n}\n\nresource \"null_resource\" \"test\" {\n  provisioner \"local-exec\" {\n    command = \"echo Hello, World!\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/init-once/no-source-with-backend/terragrunt.hcl",
    "content": "remote_state {\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"terraform.tfstate\"\n    region = \"__FILL_IN_REGION__\"\n  }\n}\n\nterraform {\n  # Should NOT execute. With no source, init-from-module should never execute.\n  # If AFTER_INIT_FROM_MODULE_ONLY_ONCE is present in output, the test failed\n  after_hook \"after_init_from_module\" {\n    commands = [\"init-from-module\"]\n    execute = [\"echo\",\"AFTER_INIT_FROM_MODULE_ONLY_ONCE\"]\n  }\n\n  # SHOULD execute.\n  # If AFTER_INIT_ONLY_ONCE is not echoed exactly once, the test failed\n  after_hook \"after_init\" {\n    commands = [\"init\"]\n    execute = [\"echo\",\"AFTER_INIT_ONLY_ONCE\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/init-once/with-source-no-backend/terragrunt.hcl",
    "content": "terraform {\n  source = \"../base-module\"\n\n  # SHOULD execute.\n  # If AFTER_INIT_FROM_MODULE_ONLY_ONCE is not echoed exactly once, the test failed\n  after_hook \"after_init_from_module\" {\n    commands = [\"init-from-module\"]\n    execute = [\"echo\",\"AFTER_INIT_FROM_MODULE_ONLY_ONCE\"]\n  }\n\n  # SHOULD execute.\n  # If AFTER_INIT_ONLY_ONCE is not echoed exactly once, the test failed\n  after_hook \"after_init\" {\n    commands = [\"init\"]\n    execute = [\"echo\",\"AFTER_INIT_ONLY_ONCE\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/init-once/with-source-no-backend-suppress-hook-stdout/terragrunt.hcl",
    "content": "terraform {\n  source = \"../base-module\"\n\n  # SHOULD execute.\n  # If AFTER_INIT_FROM_MODULE_ONLY_ONCE is not echoed exactly once, the test failed\n  after_hook \"after_init_from_module\" {\n    commands = [\"init-from-module\"]\n    execute = [\"echo\",\"AFTER_INIT_FROM_MODULE_ONLY_ONCE\"]\n    suppress_stdout = true\n  }\n\n  # SHOULD execute.\n  # If AFTER_INIT_ONLY_ONCE is not echoed exactly once, the test failed\n  after_hook \"after_init\" {\n    commands = [\"init\"]\n    execute = [\"echo\",\"AFTER_INIT_ONLY_ONCE\"]\n    suppress_stdout = true\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/init-once/with-source-with-backend/terragrunt.hcl",
    "content": "remote_state {\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"terraform.tfstate\"\n    region = \"__FILL_IN_REGION__\"\n  }\n}\n\nterraform {\n  source = \"../base-module\"\n\n  after_hook \"backend\" {\n    commands = [\"init-from-module\"]\n    execute  = [\"cp\", \"${get_terragrunt_dir()}/../backend.tf\", \".\"]\n  }\n\n  # SHOULD execute.\n  # If AFTER_INIT_FROM_MODULE_ONLY_ONCE is not echoed exactly once, the test failed\n  after_hook \"after_init_from_module\" {\n    commands = [\"init-from-module\"]\n    execute = [\"echo\",\"AFTER_INIT_FROM_MODULE_ONLY_ONCE\"]\n  }\n\n  # SHOULD execute.\n  # If AFTER_INIT_ONLY_ONCE is not echoed exactly once, the test failed\n  after_hook \"after_init\" {\n    commands = [\"init\"]\n    execute = [\"echo\",\"AFTER_INIT_ONLY_ONCE\"]\n  }\n}\n\n"
  },
  {
    "path": "test/fixtures/hooks/interpolations/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hooks/interpolations/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"example\" {\n  provisioner \"local-exec\" {\n    command = \"echo hello, world\"\n  }\n}\n\noutput \"example\" {\n  value = \"hello, world\"\n}\n"
  },
  {
    "path": "test/fixtures/hooks/interpolations/terragrunt.hcl",
    "content": "terraform {\n  # This hook echos out user's HOME path or HelloWorld\n  before_hook \"before_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"echo\", get_env(\"HOME\", \"HelloWorld\")]\n    run_on_error = true\n  }\n}"
  },
  {
    "path": "test/fixtures/hooks/one-arg-action/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hooks/one-arg-action/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"example\" {\n  provisioner \"local-exec\" {\n    command = \"echo hello, world\"\n  }\n}\n\noutput \"example\" {\n  value = \"hello, world\"\n}\n"
  },
  {
    "path": "test/fixtures/hooks/one-arg-action/terragrunt.hcl",
    "content": "terraform {\n  # This hook tests execution of agrgs that take no parameters\n  before_hook \"before_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"date\"]\n    run_on_error = true\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/path-preservation/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n}\n\noutput \"example\" {\n  value = \"test\"\n}\n"
  },
  {
    "path": "test/fixtures/hooks/path-preservation/terragrunt.hcl",
    "content": "terraform {\n  before_hook \"test_path_hook\" {\n    commands = [\"apply\", \"plan\"]\n    execute  = [\"sh\", \"-c\", \"echo 'export TG_PROVIDER_CACHE_DIR=\\\"/home/testuser/.terraform.d/plugin-cache\\\"' >&2 && exit 1\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/skip-on-error/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/hooks/skip-on-error/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"example\" {\n  provisioner \"local-exec\" {\n    command = \"echo hello, world\"\n  }\n}\n\noutput \"example\" {\n  value = \"hello, world\"\n}\n"
  },
  {
    "path": "test/fixtures/hooks/skip-on-error/terragrunt.hcl",
    "content": "terraform {\n  # This hook purposely causes an error\n  before_hook \"before_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"exit\",\"1\"]\n    run_on_error = true\n  }\n\n  before_hook \"before_hook_2\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"echo\",\"BEFORE_NODISPLAY\"]\n    run_on_error = false\n  }\n\n  before_hook \"before_hook_3\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"echo\",\"BEFORE_SHOULD_DISPLAY\"]\n    run_on_error = true\n  }\n\n  after_hook \"after_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"echo\",\"AFTER_NODISPLAY\"]\n    run_on_error = false\n  }\n\n  after_hook \"after_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"echo\",\"AFTER_SHOULD_DISPLAY\"]\n    run_on_error = true\n  }\n  error_hook \"error_hook\" {\n    commands  = [\"apply\", \"plan\"]\n    execute   = [\"echo\", \"ERROR_HOOK_EXECUTED\"]\n    on_errors = [\".*\"]\n  }\n\n  error_hook \"not_matching_error_hook\" {\n    commands  = [\"apply\", \"plan\"]\n    execute   = [\"echo\", \"NOT_MATCHING_ERROR_HOOK\"]\n    on_errors = [\".*custom-matcher.*\"]\n  }\n\n  # hook to match error \"executable file not found in $PATH\"\n  error_hook \"e\" {\n    commands  = [\"apply\", \"plan\"]\n    execute   = [\"echo\", \"PATTERN_MATCHING_ERROR_HOOK\"]\n    on_errors = [\"(?m).*executable file not found.*\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/hooks/working_dir/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/hooks/working_dir/main.tf",
    "content": "output \"example\" {\n  value = \"hello world\"\n}\n"
  },
  {
    "path": "test/fixtures/hooks/working_dir/mydir/hello_world",
    "content": ""
  },
  {
    "path": "test/fixtures/hooks/working_dir/terragrunt.hcl",
    "content": "terraform {\n  before_hook \"before_hook\" {\n    commands    = [\"validate\"]\n    execute     = [\"ls\", \"hello_world\"]\n    working_dir = \"${get_terragrunt_dir()}/mydir\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/include/qa/my-app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/include/qa/my-app/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\n# Create an arbitrary local resource\nresource \"null_resource\" \"test\" {\n  provisioner \"local-exec\" {\n    command = \"echo Hello, World!\"\n  }\n}\n\nvariable \"reflect\" {\n  type = string\n}\n\noutput \"reflect\" {\n  value = var.reflect\n}\n"
  },
  {
    "path": "test/fixtures/include/qa/my-app/terragrunt.hcl",
    "content": "include \"root\" {\n  path   = \"${find_in_parent_folders(\"root.hcl\")}\"\n  expose = true\n\n  # Don't merge in remote state block so we store state locally\n  merge_strategy = \"no_merge\"\n}\n\ninputs = {\n  reflect = include.root.remote_state\n}\n"
  },
  {
    "path": "test/fixtures/include/root.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"${path_relative_to_include()}/terraform.tfstate\"\n    region = \"us-west-2\"\n  }\n}\n\n"
  },
  {
    "path": "test/fixtures/include/stage/my-app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/include/stage/my-app/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\n# Create an arbitrary local resource\nresource \"null_resource\" \"test\" {\n  provisioner \"local-exec\" {\n    command = \"echo Hello, World!\"\n  }\n}\n\nvariable \"reflect\" {\n  type = string\n}\n\n\noutput \"reflect\" {\n  value = var.reflect\n}\n"
  },
  {
    "path": "test/fixtures/include/stage/my-app/terragrunt.hcl",
    "content": "include \"root\" {\n  path   = \"${find_in_parent_folders(\"root.hcl\")}\"\n  expose = true\n}\n\ninputs = {\n  reflect = include.root.remote_state\n}\n"
  },
  {
    "path": "test/fixtures/include-deep/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/include-deep/child/main.tf",
    "content": "variable \"attribute\" {\n  type = string\n}\n\nvariable \"new_attribute\" {\n  type = string\n}\n\nvariable \"old_attribute\" {\n  type = string\n}\n\nvariable \"list_attr\" {\n  type = list(string)\n}\n\nvariable \"map_attr\" {\n  type = map(string)\n}\n\nvariable \"dep_out\" {\n  type = any\n}\n\noutput \"attribute\" {\n  value = var.attribute\n}\n\noutput \"new_attribute\" {\n  value = var.new_attribute\n}\n\noutput \"old_attribute\" {\n  value = var.old_attribute\n}\n\noutput \"list_attr\" {\n  value = var.list_attr\n}\n\noutput \"map_attr\" {\n  value = var.map_attr\n}\n\noutput \"dep_out\" {\n  value = var.dep_out\n}\n"
  },
  {
    "path": "test/fixtures/include-deep/child/terragrunt.hcl",
    "content": "include {\n  path           = find_in_parent_folders(\"root.hcl\")\n  merge_strategy = \"deep\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n  mock_outputs = {\n    attribute     = \"mock\"\n    new_attribute = \"new val\"\n    list_attr     = [\"mock\"]\n    map_attr = {\n      bar = \"baz\"\n    }\n  }\n}\n\ninputs = {\n  attribute     = \"mock\"\n  new_attribute = \"new val\"\n  list_attr     = [\"mock\"]\n  map_attr = {\n    bar = \"baz\"\n  }\n\n  dep_out = dependency.vpc.outputs\n}\n"
  },
  {
    "path": "test/fixtures/include-deep/root.hcl",
    "content": "dependency \"vpc\" {\n  # This will get overridden by child terragrunt.hcl configs\n  config_path = \"\"\n\n  mock_outputs = {\n    attribute     = \"hello\"\n    old_attribute = \"old val\"\n    list_attr     = [\"hello\"]\n    map_attr = {\n      foo = \"bar\"\n    }\n  }\n  mock_outputs_allowed_terraform_commands = [\"apply\", \"plan\", \"destroy\", \"output\"]\n}\n\ninputs = {\n  attribute     = \"hello\"\n  old_attribute = \"old val\"\n  list_attr     = [\"hello\"]\n  map_attr = {\n    foo = \"bar\"\n    test = dependency.vpc.outputs.new_attribute\n  }\n}\n"
  },
  {
    "path": "test/fixtures/include-deep/vpc/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/include-deep/vpc/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/include-deep/vpc/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/include-expose/mixed-with-bare/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/include-expose/mixed-with-bare/child/main.tf",
    "content": "variable \"region\" {}\n\noutput \"region\" {\n  value = var.region\n}\n"
  },
  {
    "path": "test/fixtures/include-expose/mixed-with-bare/child/terragrunt.hcl",
    "content": "include {\n  path   = find_in_parent_folders(\"root.hcl\")\n  expose = true\n}\n\ninclude \"env\" {\n  path   = find_in_parent_folders(\"terragrunt_env.hcl\")\n  expose = true\n}\n\nlocals {\n  parent_region = \"${include[\"\"].locals.region}-${include.env.locals.environment}\"\n}\n\ninputs = {\n  region = local.parent_region\n}\n"
  },
  {
    "path": "test/fixtures/include-expose/multiple/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/include-expose/multiple/child/main.tf",
    "content": "variable \"region\" {}\n\noutput \"region\" {\n  value = var.region\n}\n"
  },
  {
    "path": "test/fixtures/include-expose/multiple/child/terragrunt.hcl",
    "content": "include \"root\" {\n  path   = find_in_parent_folders(\"root.hcl\")\n  expose = true\n}\n\ninclude \"env\" {\n  path   = find_in_parent_folders(\"terragrunt_env.hcl\")\n  expose = true\n}\n\nlocals {\n  parent_region = \"${include.root.locals.region}-${include.env.locals.environment}\"\n}\n\ninputs = {\n  region = local.parent_region\n}\n"
  },
  {
    "path": "test/fixtures/include-expose/root.hcl",
    "content": "locals {\n  region = \"us-west-1\"\n}\n"
  },
  {
    "path": "test/fixtures/include-expose/single/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/include-expose/single/child/main.tf",
    "content": "variable \"region\" {}\n\noutput \"region\" {\n  value = var.region\n}\n"
  },
  {
    "path": "test/fixtures/include-expose/single/child/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n  expose = true\n}\n\nlocals {\n  environment   = \"test\"\n  parent_region = \"${include.root.locals.region}-${local.environment}\"\n}\n\ninputs = {\n  region = local.parent_region\n}\n"
  },
  {
    "path": "test/fixtures/include-expose/single-bare/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/include-expose/single-bare/child/main.tf",
    "content": "variable \"region\" {}\n\noutput \"region\" {\n  value = var.region\n}\n"
  },
  {
    "path": "test/fixtures/include-expose/single-bare/child/terragrunt.hcl",
    "content": "include {\n  path   = find_in_parent_folders(\"root.hcl\")\n  expose = true\n}\n\nlocals {\n  environment   = \"test\"\n  parent_region = \"${include.locals.region}-${local.environment}\"\n}\n\ninputs = {\n  region = local.parent_region\n}\n"
  },
  {
    "path": "test/fixtures/include-expose/terragrunt_env.hcl",
    "content": "locals {\n  environment = \"test\"\n}\n"
  },
  {
    "path": "test/fixtures/include-expose/with-dependency/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/include-expose/with-dependency/child/main.tf",
    "content": "variable \"region\" {}\n\noutput \"region\" {\n  value = var.region\n}\n"
  },
  {
    "path": "test/fixtures/include-expose/with-dependency/child/terragrunt.hcl",
    "content": "include \"root\" {\n  path           = find_in_parent_folders(\"root.hcl\")\n  expose         = true\n  merge_strategy = \"deep\"\n}\n\ninputs = {\n  region = \"${include.root.locals.region}-${dependency.dep.outputs.env}\"\n}\n"
  },
  {
    "path": "test/fixtures/include-expose/with-dependency/dep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/include-expose/with-dependency/dep/main.tf",
    "content": "output \"env\" {\n  value = \"test\"\n}\n"
  },
  {
    "path": "test/fixtures/include-expose/with-dependency/dep/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/include-expose/with-dependency/root.hcl",
    "content": "locals {\n  region = \"us-west-1\"\n}\n\ndependency \"dep\" {\n  config_path = \"${get_terragrunt_dir()}/../dep\"\n}\n"
  },
  {
    "path": "test/fixtures/include-expose/with-dependency-reference-input/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/include-expose/with-dependency-reference-input/child/main.tf",
    "content": "variable \"region\" {}\n\noutput \"region\" {\n  value = var.region\n}\n"
  },
  {
    "path": "test/fixtures/include-expose/with-dependency-reference-input/child/terragrunt.hcl",
    "content": "include \"root\" {\n  path           = find_in_parent_folders(\"root.hcl\")\n  expose         = true\n}\n\ndependency \"dep\" {\n  config_path = include.root.inputs.dep_path\n}\n\ninputs = {\n  region = \"${include.root.locals.region}-${dependency.dep.outputs.env}\"\n}\n"
  },
  {
    "path": "test/fixtures/include-expose/with-dependency-reference-input/dep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/include-expose/with-dependency-reference-input/dep/main.tf",
    "content": "output \"env\" {\n  value = \"test\"\n}\n"
  },
  {
    "path": "test/fixtures/include-expose/with-dependency-reference-input/dep/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/include-expose/with-dependency-reference-input/root.hcl",
    "content": "locals {\n  region = \"us-west-1\"\n}\n\ninputs = {\n  dep_path = \"${get_terragrunt_dir()}/../dep\"\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/deep-merge-nonoverlapping/child/terragrunt.hcl",
    "content": "terraform {\n  source = \"${get_terragrunt_dir()}/../../modules/reflect\"\n}\n\ninclude \"inputs\" {\n  path           = find_in_parent_folders(\"terragrunt_inputs.hcl\")\n  merge_strategy = \"deep\"\n}\n\ninclude \"vpc_dep\" {\n  path           = find_in_parent_folders(\"terragrunt_vpc_dep.hcl\")\n  merge_strategy = \"deep\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n  mock_outputs = {\n    attribute     = \"mock\"\n    new_attribute = \"new val\"\n    list_attr     = [\"mock\", \"foo\"]\n    map_attr = {\n      bar = \"baz\"\n    }\n  }\n}\n\ninputs = {\n  attribute     = \"mock\"\n  new_attribute = \"new val\"\n  list_attr     = [\"mock\", \"foo\"]\n  map_attr = {\n    bar = \"baz\"\n  }\n\n  dep_out = dependency.vpc.outputs\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/deep-merge-nonoverlapping/vpc/terragrunt.hcl",
    "content": "terraform {\n  source = \"${get_terragrunt_dir()}/../../modules/empty\"\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/deep-merge-overlapping/child/terragrunt.hcl",
    "content": "terraform {\n  source = \"${get_terragrunt_dir()}/../../modules/reflect\"\n}\n\ninclude \"inputs\" {\n  path           = find_in_parent_folders(\"terragrunt_inputs.hcl\")\n  merge_strategy = \"deep\"\n}\n\ninclude \"vpc_dep\" {\n  path           = find_in_parent_folders(\"terragrunt_vpc_dep.hcl\")\n  merge_strategy = \"deep\"\n}\n\ninclude \"inputs_override\" {\n  path           = find_in_parent_folders(\"terragrunt_inputs_override.hcl\")\n  merge_strategy = \"deep\"\n}\n\ninclude \"vpc_dep_override\" {\n  path           = find_in_parent_folders(\"terragrunt_vpc_dep_override.hcl\")\n  merge_strategy = \"deep\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n  mock_outputs = {\n    attribute = \"mock\"\n    list_attr = [\"foo\"]\n  }\n}\n\ninputs = {\n  attribute = \"mock\"\n  list_attr = [\"foo\"]\n\n  dep_out = dependency.vpc.outputs\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/deep-merge-overlapping/vpc/terragrunt.hcl",
    "content": "terraform {\n  source = \"${get_terragrunt_dir()}/../../modules/empty\"\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/expose/child/terragrunt.hcl",
    "content": "terraform {\n  source = \"${get_terragrunt_dir()}/../../modules/reflect\"\n}\n\ninclude \"inputs_override\" {\n  path           = find_in_parent_folders(\"terragrunt_inputs_override.hcl\")\n  expose         = true\n  merge_strategy = \"no_merge\"\n}\n\ninclude \"vpc_dep\" {\n  path           = find_in_parent_folders(\"terragrunt_vpc_dep_for_expose.hcl\")\n  expose         = true\n  merge_strategy = \"no_merge\"\n}\n\ndependency \"vpc\" {\n  config_path = include.vpc_dep.dependency.vpc.config_path\n  mock_outputs = merge(\n    include.vpc_dep.dependency.vpc.mock_outputs,\n    {\n      attribute     = \"mock\"\n      new_attribute = \"new val\"\n      list_attr     = [\"hello\", \"mock\", \"foo\"]\n      map_attr = {\n        foo = \"bar\"\n        bar = \"baz\"\n      }\n    },\n  )\n  mock_outputs_allowed_terraform_commands = include.vpc_dep.dependency.vpc.mock_outputs_allowed_terraform_commands\n}\n\ninputs = merge(\n  include.inputs_override.inputs,\n  {\n    attribute     = \"mock\"\n    old_attribute = \"old val\"\n    list_attr     = [\"hello\", \"mock\", \"foo\"]\n    map_attr = {\n      bar  = \"baz\"\n      foo  = \"bar\"\n      test = dependency.vpc.outputs.new_attribute\n    }\n    dep_out = dependency.vpc.outputs\n  },\n)\n"
  },
  {
    "path": "test/fixtures/include-multiple/expose/vpc/terragrunt.hcl",
    "content": "terraform {\n  source = \"${get_terragrunt_dir()}/../../modules/empty\"\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/has-bare-include/child/terragrunt.hcl",
    "content": "terraform {\n  source = \"${get_terragrunt_dir()}/../../modules/reflect\"\n}\n\ninclude {\n  path           = find_in_parent_folders(\"terragrunt_inputs.hcl\")\n  merge_strategy = \"deep\"\n}\n\ninclude \"vpc_dep\" {\n  path           = find_in_parent_folders(\"terragrunt_vpc_dep.hcl\")\n  merge_strategy = \"deep\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n  mock_outputs = {\n    attribute     = \"mock\"\n    new_attribute = \"new val\"\n    list_attr     = [\"mock\", \"foo\"]\n    map_attr = {\n      bar = \"baz\"\n    }\n  }\n}\n\ninputs = {\n  attribute     = \"mock\"\n  new_attribute = \"new val\"\n  list_attr     = [\"mock\", \"foo\"]\n  map_attr = {\n    bar = \"baz\"\n  }\n\n  dep_out = dependency.vpc.outputs\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/has-bare-include/vpc/terragrunt.hcl",
    "content": "terraform {\n  source = \"${get_terragrunt_dir()}/../../modules/empty\"\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/json/child/terragrunt.hcl.json",
    "content": "{\n  \"terraform\": {\n    \"source\": \"${get_terragrunt_dir()}/../../modules/reflect\"\n  },\n  \"include\": {\n    \"\": {\n      \"path\": \"${find_in_parent_folders(\\\"terragrunt_inputs.hcl\\\")}\",\n      \"merge_strategy\": \"deep\"\n    },\n    \"vpc_dep\": {\n      \"path\": \"${find_in_parent_folders(\\\"terragrunt_vpc_dep.hcl\\\")}\",\n      \"merge_strategy\": \"deep\"\n    }\n  },\n  \"dependency\": {\n    \"vpc\": {\n      \"config_path\": \"../vpc\",\n      \"mock_outputs\": {\n        \"attribute\": \"mock\",\n        \"new_attribute\": \"new val\",\n        \"list_attr\": [\"mock\", \"foo\"],\n        \"map_attr\": {\n          \"bar\": \"baz\"\n        }\n      }\n    }\n  },\n  \"inputs\": {\n    \"attribute\": \"mock\",\n    \"new_attribute\": \"new val\",\n    \"list_attr\": [\"mock\", \"foo\"],\n    \"map_attr\": {\n      \"bar\": \"baz\"\n    },\n    \"dep_out\": \"${dependency.vpc.outputs}\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/json/vpc/terragrunt.hcl",
    "content": "terraform {\n  source = \"${get_terragrunt_dir()}/../../modules/empty\"\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/modules/empty/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/include-multiple/modules/empty/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/include-multiple/modules/reflect/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/include-multiple/modules/reflect/main.tf",
    "content": "variable \"attribute\" {\n  type = string\n}\n\nvariable \"new_attribute\" {\n  type = string\n}\n\nvariable \"old_attribute\" {\n  type = string\n}\n\nvariable \"list_attr\" {\n  type = list(string)\n}\n\nvariable \"map_attr\" {\n  type = map(string)\n}\n\nvariable \"dep_out\" {\n  type = any\n}\n\noutput \"attribute\" {\n  value = var.attribute\n}\n\noutput \"new_attribute\" {\n  value = var.new_attribute\n}\n\noutput \"old_attribute\" {\n  value = var.old_attribute\n}\n\noutput \"list_attr\" {\n  value = var.list_attr\n}\n\noutput \"map_attr\" {\n  value = var.map_attr\n}\n\noutput \"dep_out\" {\n  value = var.dep_out\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/shallow-deep-merge-overlapping/child/terragrunt.hcl",
    "content": "terraform {\n  source = \"${get_terragrunt_dir()}/../../modules/reflect\"\n}\n\ninclude \"inputs\" {\n  path           = find_in_parent_folders(\"terragrunt_inputs.hcl\")\n  merge_strategy = \"deep\"\n}\n\ninclude \"inputs_override\" {\n  path = find_in_parent_folders(\"terragrunt_inputs_override.hcl\")\n}\n\n# NOTE: This shallow merge is expected to be a noop, as the deep merge between vpc_dep and the child config completes\n# the expected dependency.vpc block.\ninclude \"vpc_dep_override\" {\n  path = find_in_parent_folders(\"terragrunt_vpc_dep_override.hcl\")\n}\n\ninclude \"vpc_dep\" {\n  path           = find_in_parent_folders(\"terragrunt_vpc_dep.hcl\")\n  merge_strategy = \"deep\"\n}\n\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n  mock_outputs = {\n    attribute     = \"mock\"\n    new_attribute = \"new val\"\n    list_attr     = [\"mock\", \"foo\"]\n    map_attr = {\n      bar = \"baz\"\n    }\n  }\n}\n\ninputs = {\n  attribute = \"mock\"\n  list_attr = [\"mock\", \"foo\"]\n\n  dep_out = dependency.vpc.outputs\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/shallow-deep-merge-overlapping/vpc/terragrunt.hcl",
    "content": "terraform {\n  source = \"${get_terragrunt_dir()}/../../modules/empty\"\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/shallow-merge/child/terragrunt.hcl",
    "content": "terraform {\n  source = \"${get_terragrunt_dir()}/../../modules/reflect\"\n}\n\ninclude \"inputs\" {\n  path = find_in_parent_folders(\"terragrunt_inputs.hcl\")\n}\n\ninclude \"inputs_final\" {\n  path = find_in_parent_folders(\"terragrunt_inputs_final.hcl\")\n}\n\ninclude \"vpc_dep\" {\n  path = find_in_parent_folders(\"terragrunt_vpc_dep.hcl\")\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n  mock_outputs = {\n    attribute     = \"mock\"\n    old_attribute = \"old val\"\n    new_attribute = \"new val\"\n    list_attr     = [\"hello\", \"mock\", \"foo\"]\n    map_attr = {\n      foo = \"bar\"\n      bar = \"baz\"\n    }\n  }\n}\n\ninputs = {\n  dep_out = dependency.vpc.outputs\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/shallow-merge/vpc/terragrunt.hcl",
    "content": "terraform {\n  source = \"${get_terragrunt_dir()}/../../modules/empty\"\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/terragrunt_inputs.hcl",
    "content": "inputs = {\n  attribute     = \"hello\"\n  old_attribute = \"old val\"\n  list_attr     = [\"hello\"]\n  map_attr = {\n    foo  = \"bar\"\n    test = dependency.vpc.outputs.new_attribute\n  }\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/terragrunt_inputs_final.hcl",
    "content": "inputs = {\n  attribute     = \"mock\"\n  new_attribute = \"new val\"\n  list_attr     = [\"hello\", \"mock\", \"foo\"]\n  map_attr = {\n    bar  = \"baz\"\n    foo  = \"bar\"\n    test = dependency.vpc.outputs.new_attribute\n  }\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/terragrunt_inputs_override.hcl",
    "content": "inputs = {\n  attribute     = \"will be replaced\"\n  new_attribute = \"new val\"\n  list_attr     = [\"mock\"]\n  map_attr = {\n    bar = \"baz\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/terragrunt_vpc_dep.hcl",
    "content": "dependency \"vpc\" {\n  # This will get overridden by child terragrunt.hcl configs\n  config_path = \"\"\n\n  mock_outputs = {\n    attribute     = \"hello\"\n    old_attribute = \"old val\"\n    list_attr     = [\"hello\"]\n    map_attr = {\n      foo = \"bar\"\n    }\n  }\n  mock_outputs_allowed_terraform_commands = [\"apply\", \"plan\", \"destroy\", \"output\"]\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/terragrunt_vpc_dep_for_expose.hcl",
    "content": "dependency \"vpc\" {\n  config_path = \"${get_terragrunt_dir()}/../vpc\"\n  mock_outputs = {\n    attribute     = \"hello\"\n    old_attribute = \"old val\"\n  }\n  mock_outputs_allowed_terraform_commands = [\"apply\", \"plan\", \"destroy\", \"output\"]\n}\n"
  },
  {
    "path": "test/fixtures/include-multiple/terragrunt_vpc_dep_override.hcl",
    "content": "dependency \"vpc\" {\n  config_path = \"\"\n  mock_outputs = {\n    attribute     = \"will be replaced\"\n    new_attribute = \"new val\"\n    list_attr     = [\"mock\"]\n    map_attr = {\n      bar = \"baz\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/include-parent/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/include-parent/app/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/include-parent/app/terragrunt.hcl",
    "content": "include \"parent\" {\n  path = \"../parent.hcl\"\n}"
  },
  {
    "path": "test/fixtures/include-parent/common.hcl",
    "content": "locals {\n  common = run_cmd(\"echo\", \"common_hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/include-parent/dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/include-parent/dependency/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/include-parent/dependency/terragrunt.hcl",
    "content": "locals {\n  parent_var = run_cmd(\"echo\", \"dependency_hcl\")\n}\n\ninclude \"common\" {\n  path = \"../common.hcl\"\n}\n"
  },
  {
    "path": "test/fixtures/include-parent/parent.hcl",
    "content": "locals {\n  parent_var = run_cmd(\"echo\", \"parent_hcl_file\")\n}\n\ndependency \"dependency\" {\n  config_path = \"../dependency\"\n\n  mock_outputs = {\n    mock_key = \"mock_value\"\n  }\n\n}\n"
  },
  {
    "path": "test/fixtures/include-runall/a/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/include-runall/a/main.tf",
    "content": "output \"text\" {\n  value = \"alpha\"\n}\n"
  },
  {
    "path": "test/fixtures/include-runall/a/terragrunt.hcl",
    "content": "include \"alpha\" {\n  path = find_in_parent_folders(\"alpha.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/include-runall/alpha.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/include-runall/b/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/include-runall/b/main.tf",
    "content": "output \"text\" {\n  value = \"beta\"\n}\n"
  },
  {
    "path": "test/fixtures/include-runall/b/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/include-runall/c/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/include-runall/c/main.tf",
    "content": "output \"text\" {\n  value = \"charlie\"\n}\n"
  },
  {
    "path": "test/fixtures/include-runall/c/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/init-cache/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/init-cache/app/main.tf",
    "content": "variable \"test\" {}\n\noutput \"result\" {\n  value = var.test\n}\n"
  },
  {
    "path": "test/fixtures/init-cache/app/terragrunt.hcl",
    "content": "include \"root\" {\n  path   = find_in_parent_folders(\"root.hcl\")\n}\n\ninputs = {\n  env = \"nonprod\"\n}\n\n"
  },
  {
    "path": "test/fixtures/init-cache/root.hcl",
    "content": "\ngenerate \"provider\" {\n  path = \"provider.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nprovider \"aws\" {\n  region = \"eu-central-1\"\n}\nEOF\n}\n\nterraform {\n  source = \".\"\n  extra_arguments \"common_vars\" {\n    commands = get_terraform_commands_that_need_vars()\n    arguments = [\n      \"-var\", \"test=qwe\",\n    ]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/init-error/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/init-error/main.tf",
    "content": "terraform {\n  backend \"s3\" {\n    bucket = \"mybucket\"\n    key    = \"path/to/my/key\"\n    region = \"us-east-1\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/init-error/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/init-once/module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/random\" {\n  version     = \"3.8.0\"\n  constraints = \">= 2.2.0\"\n  hashes = [\n    \"h1:aEaTEHutDdKNaztKFmInhfzmZK0/OaVL8uxmncM9YF8=\",\n    \"h1:ey4eBIHiuAC5xsblxtXghXE3nWwUvGqTT6KAsggiAwo=\",\n    \"h1:nRPdhXsZpGPMppuUgBe/ZcAtD73NaCLGROYHXv41qz8=\",\n    \"zh:2d5e0bbfac7f15595739fe54a9ab8b8eea92fd6d879706139dad7ecaa5c01c19\",\n    \"zh:349e637066625d97aaa84db1b1418c86d6457cf9c5a62f6dcc3f55cbd535112c\",\n    \"zh:5f4456d53f5256ccfdb87dd35d3bf34578d01bd9b71cffaf507f0692805eac8a\",\n    \"zh:6c1ecfacc5f7079a068d7f8eb8924485d4ec8183f36e6318a6e748d35921ddac\",\n    \"zh:6d86641edeb8c394f121f7b0a691d72f89cf9b938b987a01fc32aad396a50555\",\n    \"zh:76947bd7bc7033b33980538da149c94e386f9b0abb2ce63733f25a57517e4742\",\n    \"zh:79c07f4c8b3a63d9f89e25e4348b462c57e179bca66ba533710851c485e282db\",\n    \"zh:ac1c2b941d994728a3a93aba093fd2202f9311d099ff85f66678897c792161ba\",\n    \"zh:cbb2aa867fd828fcb4125239e00862b9a3bc2f280e945c760224276b476f4c49\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/init-once/module/main.tf",
    "content": "module \"vpc\" {\n  source = \"github.com/cloudposse/terraform-example-module?ref=3.0.1\"\n}\n"
  },
  {
    "path": "test/fixtures/init-once/terragrunt.hcl",
    "content": "terraform {\n  source = \".//module\"\n}\n"
  },
  {
    "path": "test/fixtures/inputs/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/inputs/main.tf",
    "content": "variable \"string\" {\n  type = string\n}\n\noutput \"string\" {\n  value = var.string\n}\n\nvariable \"number\" {\n  type = number\n}\n\noutput \"number\" {\n  value = var.number\n}\n\nvariable \"bool\" {\n  type = bool\n}\n\noutput \"bool\" {\n  value = var.bool\n}\n\nvariable \"list_string\" {\n  type = list(string)\n}\n\noutput \"list_string\" {\n  value = var.list_string\n}\n\nvariable \"list_number\" {\n  type = list(number)\n}\n\noutput \"list_number\" {\n  value = var.list_number\n}\n\nvariable \"list_bool\" {\n  type = list(bool)\n}\n\noutput \"list_bool\" {\n  value = var.list_bool\n}\n\nvariable \"map_string\" {\n  type = map(string)\n}\n\noutput \"map_string\" {\n  value = var.map_string\n}\n\nvariable \"map_number\" {\n  type = map(number)\n}\n\noutput \"map_number\" {\n  value = var.map_number\n}\n\nvariable \"map_bool\" {\n  type = map(bool)\n}\n\noutput \"map_bool\" {\n  value = var.map_bool\n}\n\nvariable \"object\" {\n  type = object({\n    str  = string\n    num  = number\n    list = list(number)\n    map  = map(string)\n  })\n}\n\noutput \"object\" {\n  value = var.object\n}\n\nvariable \"from_env\" {\n  type = string\n}\n\noutput \"from_env\" {\n  value = var.from_env\n}"
  },
  {
    "path": "test/fixtures/inputs/terragrunt.hcl",
    "content": "inputs = {\n  string      = \"string\"\n  number      = 42\n  bool        = true\n  list_string = [\"a\", \"b\", \"c\"]\n  list_number = [1, 2, 3]\n  list_bool   = [true, false]\n\n  map_string = {\n    foo = \"bar\"\n  }\n\n  map_number = {\n    foo = 42\n    bar = 12345\n  }\n\n  map_bool = {\n    foo = true\n    bar = false\n    baz = true\n  }\n\n  object = {\n    str  = \"string\"\n    num  = 42\n    list = [1, 2, 3]\n\n    map = {\n      foo = \"bar\"\n    }\n  }\n\n  from_env = get_env(\"FROM_ENV\", \"default\")\n\n  undefined_var = \"this var does not exist in module\"\n}\n"
  },
  {
    "path": "test/fixtures/inputs-defaults/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/inputs-defaults/main.tf",
    "content": "variable \"project_name\" {\n  type        = string\n  description = \"Project name\"\n\n}\n\nvariable \"open_port\" {\n  type        = number\n  description = \"Port to open\"\n}\n\nvariable \"enable_backups\" {\n  type = bool\n}\n\nvariable \"no_type_value_var\" {}\n\n\nvariable \"default_var\" {\n  default = {\n    x = 1\n  }\n}\n\nvariable \"number_default\" {\n  type        = number\n  default     = 42\n  description = \"number variable with default\"\n}\n\nvariable \"object_var\" {\n  type = object({\n    str = string\n    num = number\n  })\n\n  default = {\n    str = \"default\"\n    num = 42\n  }\n}\n\nvariable \"map_var\" {\n  type = map(string)\n  default = {\n    key = \"value42\"\n  }\n}\n\nvariable \"list_var\" {\n  type    = list(number)\n  default = [1, 2, 3]\n}\n\nvariable \"enabled\" {\n  description = \"Enable or disable the module\"\n  type        = bool\n  default     = true\n}\n\nvariable \"vpc\" {\n  type        = string\n  description = \"VPC to be used\"\n  default     = \"default-vpc\"\n\n}\n"
  },
  {
    "path": "test/fixtures/inputs-interpolation/main.tf",
    "content": "variable \"map_with_interpolation\" {\n  type = map(string)\n}\n\noutput \"map_with_interpolation\" {\n  value = var.map_with_interpolation\n}\n"
  },
  {
    "path": "test/fixtures/inputs-interpolation/stuff.json",
    "content": "{\n  \"foo\": \"test ${bar} test\",\n  \"baz\": \"no interpolation here\"\n}\n"
  },
  {
    "path": "test/fixtures/inputs-interpolation/terragrunt.hcl",
    "content": "inputs = {\n  map_with_interpolation = jsondecode(file(\"stuff.json\"))\n}\n"
  },
  {
    "path": "test/fixtures/list/basic/a-unit/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/list/basic/b-unit/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/list/dag/stacks/live/dev/terragrunt.stack.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/list/dag/stacks/live/prod/terragrunt.stack.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/list/dag/units/live/dev/db/terragrunt.hcl",
    "content": "dependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n"
  },
  {
    "path": "test/fixtures/list/dag/units/live/dev/ec2/terragrunt.hcl",
    "content": "dependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n\ndependency \"db\" {\n  config_path = \"../db\"\n}\n"
  },
  {
    "path": "test/fixtures/list/dag/units/live/dev/vpc/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/list/dag/units/live/prod/db/terragrunt.hcl",
    "content": "dependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n"
  },
  {
    "path": "test/fixtures/list/dag/units/live/prod/ec2/terragrunt.hcl",
    "content": "dependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n\ndependency \"db\" {\n  config_path = \"../db\"\n}\n"
  },
  {
    "path": "test/fixtures/list/dag/units/live/prod/vpc/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/list/long/unit-0/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/list/long/unit-1/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/list/long/unit-2/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/list/long/unit-3/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/list/long/unit-4/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/list/long/unit-5/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/list/long/unit-6/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/list/long/unit-7/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/list/long/unit-8/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/list/long/unit-9/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/locals/canonical/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/locals/canonical/contents.txt",
    "content": "Hello world\n"
  },
  {
    "path": "test/fixtures/locals/canonical/main.tf",
    "content": "variable \"data\" {\n  type = string\n}\n\noutput \"data\" {\n  value = var.data\n}\n\nvariable \"answer\" {\n  type = number\n}\n\noutput \"answer\" {\n  value = var.answer\n}\n"
  },
  {
    "path": "test/fixtures/locals/canonical/terragrunt.hcl",
    "content": "locals {\n  x = 2\n  file_contents     = file(\"./contents.txt\")\n  number_expression = 40+local.x\n}\n\ninputs = {\n  data = local.file_contents\n  answer = local.number_expression\n}\n"
  },
  {
    "path": "test/fixtures/locals/local-in-include/qa/my-app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/locals/local-in-include/qa/my-app/main.tf",
    "content": "variable \"parent_terragrunt_dir\" {}\nvariable \"terragrunt_dir\" {}\nvariable \"terraform_command\" {}\nvariable \"terraform_cli_args\" {}\n\noutput \"parent_terragrunt_dir\" {\n  value = var.parent_terragrunt_dir\n}\n\noutput \"terragrunt_dir\" {\n  value = var.terragrunt_dir\n}\n\noutput \"terraform_command\" {\n  value = var.terraform_command\n}\n\noutput \"terraform_cli_args\" {\n  value = var.terraform_cli_args\n}"
  },
  {
    "path": "test/fixtures/locals/local-in-include/qa/my-app/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/locals/local-in-include/root.hcl",
    "content": "locals {\n  parent_terragrunt_dir = get_parent_terragrunt_dir()\n  terragrunt_dir = get_terragrunt_dir()\n  terraform_command = get_terraform_command()\n  terraform_cli_args = get_terraform_cli_args()\n}\n\ninputs = {\n  parent_terragrunt_dir = local.parent_terragrunt_dir\n  terragrunt_dir = local.terragrunt_dir\n  terraform_command = local.terraform_command\n  terraform_cli_args = local.terraform_cli_args\n}\n"
  },
  {
    "path": "test/fixtures/locals/run-multiple/terragrunt.hcl",
    "content": "locals {\n  bar = run_cmd(\"echo\", \"echo_foo\")\n  foo = run_cmd(\"echo\", \"echo_bar\")\n\n  potato = run_cmd(\"echo\", \"echo_potato\")\n  potato2 = run_cmd(\"echo\", \"echo_potato\")\n\n  carrot = run_cmd(\"echo\", \"echo_carrot\")\n\n  random_arg = run_cmd(\"echo\", \"echo_random_arg\",  uuid())\n  random_arg2 = run_cmd(\"echo\", \"echo_random_arg\",  uuid())\n\n  uuid = run_cmd(\"echo\", \"echo_uuid_locals\",  uuid())\n\n  another_arg = run_cmd(\"echo\", \"echo_another_arg\",  uuid())\n\n}\n\ninputs = {\n  fileName = run_cmd(\"echo\", \"echo_carrot\")\n  uuid2 = run_cmd(\"echo\", \"echo_uuid_input\", uuid())\n  another_arg2 = run_cmd(\"echo\", \"echo_another_arg\",  uuid())\n  input_variable = run_cmd(\"echo\", \"echo_input_variable\", uuid())\n}\n"
  },
  {
    "path": "test/fixtures/locals/run-once/terragrunt.hcl",
    "content": "locals {\n  bar = run_cmd(\"echo\", \"foo\")\n}\n"
  },
  {
    "path": "test/fixtures/locals-errors/undefined-local/terragrunt.hcl",
    "content": "inputs = {\n  data = local.file_contents\n}\n"
  },
  {
    "path": "test/fixtures/locals-errors/undefined-local-but-input/terragrunt.hcl",
    "content": "inputs = {\n  a = \"foo\"\n  b = local.a\n}\n"
  },
  {
    "path": "test/fixtures/log/formatter/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/log/formatter/app/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/log/formatter/app/terragrunt.hcl",
    "content": "dependency \"dependency\" {\n  config_path  = \"../dep\"\n  skip_outputs = true\n}\n"
  },
  {
    "path": "test/fixtures/log/formatter/dep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/log/formatter/dep/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/log/formatter/dep/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/log/levels/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/log/levels/main.tf",
    "content": "output \"text\" {\n  value = \"output\"\n}\n"
  },
  {
    "path": "test/fixtures/log/levels/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/log/rel-paths/duplicate-dir-names/workspace/one/two/aaa/bbb/ccc/module-b/terragrunt.hcl",
    "content": "dependency \"databricks_workspace\" {\n  config_path  = \"../workspace\"\n  skip_outputs = true\n}\n\nterraform {\n  source = \"../../../..//tf\"\n}\n"
  },
  {
    "path": "test/fixtures/log/rel-paths/duplicate-dir-names/workspace/one/two/aaa/bbb/ccc/workspace/terragrunt.hcl",
    "content": "terraform {\n  source = \"../../../..//tf\"\n}\n"
  },
  {
    "path": "test/fixtures/log/rel-paths/duplicate-dir-names/workspace/one/two/tf/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/log/rel-paths/duplicate-dir-names/workspace/one/two/tf/main.tf",
    "content": "resource \"null_resource\" \"this\" {\n}\n\noutput \"dummy\" {\n  value = \"dummy\"\n}\n"
  },
  {
    "path": "test/fixtures/manifest/version-1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/manifest/version-1/main.tf",
    "content": "# Hello world\n"
  },
  {
    "path": "test/fixtures/manifest/version-1/stale.tf",
    "content": "# this is stale file\n"
  },
  {
    "path": "test/fixtures/manifest/version-2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/manifest/version-2/main.tf",
    "content": "# Hello world\n"
  },
  {
    "path": "test/fixtures/manifest/version-3-subfolder/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/manifest/version-3-subfolder/main.tf",
    "content": "# Hello world\n"
  },
  {
    "path": "test/fixtures/manifest/version-3-subfolder/sub/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/manifest/version-3-subfolder/sub/main.tf",
    "content": "# hello subfolder\n"
  },
  {
    "path": "test/fixtures/manifest/version-4-subfolder-empty/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/manifest/version-4-subfolder-empty/main.tf",
    "content": "# Hello world\n"
  },
  {
    "path": "test/fixtures/manifest/version-5-not-empty-subfolder/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/manifest/version-5-not-empty-subfolder/main.tf",
    "content": "# Hello world\n"
  },
  {
    "path": "test/fixtures/manifest/version-5-not-empty-subfolder/sub2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/manifest/version-5-not-empty-subfolder/sub2/main.tf",
    "content": "# hello, I'm here\n"
  },
  {
    "path": "test/fixtures/manifest-removal/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/manifest-removal/app/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/manifest-removal/app/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/manifest-removal/root.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/missing-dependencies/main/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/missing-dependencies/main/main.tf",
    "content": "output \"result\" {\n  value = \"Hello World\"\n}\n"
  },
  {
    "path": "test/fixtures/missing-dependencies/main/terragrunt.hcl",
    "content": "dependencies {\n  paths = [\n    \"../hl3-release\",\n    \"../module-a\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/missing-dependencies/module-a/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/missing-dependencies/module-a/main.tf",
    "content": "output \"result\" {\n  value = \"Hello World, module-a\"\n}\n"
  },
  {
    "path": "test/fixtures/missing-dependencies/module-a/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/mixed-config/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/mixed-config/app/main.tf",
    "content": "\n"
  },
  {
    "path": "test/fixtures/mixed-config/app/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/mixed-config/stack/terragrunt.stack.hcl",
    "content": "\nunit \"app1\" {\n  source = \"../unit\"\n  path   = \"app1\"\n}\n"
  },
  {
    "path": "test/fixtures/mixed-config/unit/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/mixed-config/unit/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/mixed-config/unit/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/module-path-in-error/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/module-path-in-error/app/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/module-path-in-error/app/terragrunt.hcl",
    "content": "dependency \"d1\" {\n  config_path = \"../d1\"\n\n  mock_outputs = {\n    d1 = \"d1-value\"\n  }\n}\n\n"
  },
  {
    "path": "test/fixtures/module-path-in-error/d1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/aws\" {\n  version = \"6.28.0\"\n  hashes = [\n    \"h1:i0G7vt2sNy0oz84IiuG4gplonNVyOLRdprKLurU8pe4=\",\n    \"h1:tcau98fkhZ2RhbPHo8LdiiUk2RGpZUgT/t06sdMLids=\",\n    \"h1:wek8vEEZpTPulbLi9xCf2wnxvc97JXAN4qcOhduSg7k=\",\n    \"zh:38d58305206953783c150fb96d5c4f3ea5fe0b9e0987d927c884a6b0f2adf7a9\",\n    \"zh:43fd483251165f98b7a44360b41b437d309b007ef2bfff818eedcf3730e3f5cb\",\n    \"zh:4753decc5a718cb74b08244a02d00c150f0ddd6ebf2e1227f6a985c647c03ce9\",\n    \"zh:5956525650554bd3fbc4b695eb5250193f0ebf94c45862a7730457ab6a315069\",\n    \"zh:76d98fa1146750c01f607bae4421952ee9cd14ed3a4a59deb7136749adb9e0ae\",\n    \"zh:792c29e5ec91356baddb6219ac7f6f1df09c251cbe4ab6e089fc25d64270b22a\",\n    \"zh:856424380caa7c1536dc00515d12beac2693db1a8425da654eed5530abeb17d9\",\n    \"zh:e8982ec2bc692efa7236e3565e7094a09f52c5b71d8860a570a36fb31a40f27f\",\n    \"zh:f5e7ff825dc3f7356fb80936bfe7bb1b54a728ccf429cb753cfe590932f0403b\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/module-path-in-error/d1/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/module-path-in-error/d1/provider.tf",
    "content": "# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa\n\nprovider \"aws\" {\n  region = \"ca-central-1\"\n}\n\nterraform {\n  backend \"s3\" {\n    encrypt        = true\n    bucket         = \"test-bucket-666\"\n    dynamodb_table = \"test-666\"\n    region         = \"ca-central-1\"\n    key            = \"terraform.tfstate\"\n  }\n}\n\n"
  },
  {
    "path": "test/fixtures/module-path-in-error/d1/terragrunt.hcl",
    "content": "include \"common\" {\n  path   = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/module-path-in-error/root.hcl",
    "content": "generate \"provider\" {\n  path = \"provider.tf\"\n  if_exists = \"overwrite\"\n  contents = <<EOF\n\nprovider \"aws\" {\n  region  = \"ca-central-1\"\n}\n\nterraform {\n  backend \"s3\" {\n    encrypt = true\n    bucket = \"test-bucket-666\"\n    dynamodb_table = \"test-666\"\n    region = \"ca-central-1\"\n    key = \"terraform.tfstate\"\n  }\n}\n\nEOF\n}\n"
  },
  {
    "path": "test/fixtures/modules/hcl-module-b/module-b-child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/modules/hcl-module-b/module-b-child/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/modules/hcl-module-b/module-b-child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl.json\")\n}\n"
  },
  {
    "path": "test/fixtures/modules/hcl-module-b/root.hcl.json",
    "content": "{\n  \"remote_state\": {\n    \"backend\": \"s3\",\n    \"config\": {\n      \"bucket\": \"bucket\",\n      \"key\": \"${path_relative_to_include()}/terraform.tfstate\"\n    }\n  },\n  \"terraform\": {\n    \"source\": \"...\"\n  }\n}"
  },
  {
    "path": "test/fixtures/modules/hcl-module-c/terragrunt.hcl",
    "content": "terraform {\n  source = \"temp\"\n}\n\ndependencies {\n  paths = [\"../json-module-a\"]\n}\n"
  },
  {
    "path": "test/fixtures/modules/json-module-a/terragrunt.hcl.json",
    "content": "{\n  \"terraform\": {\n    \"source\": \"test\"\n  }\n}"
  },
  {
    "path": "test/fixtures/modules/json-module-b/module-b-child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/modules/json-module-b/module-b-child/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/modules/json-module-b/module-b-child/terragrunt.hcl.json",
    "content": "{\n  \"include\": {\n    \"path\": \"${find_in_parent_folders(\\\"root.hcl\\\")}\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/modules/json-module-b/root.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket = \"bucket\"\n    key = \"${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\nterraform {\n  source = \"...\"\n}\n"
  },
  {
    "path": "test/fixtures/modules/json-module-c/terragrunt.hcl.json",
    "content": "{\n  \"terraform\": {\n    \"source\": \"temp\"\n  },\n  \"dependencies\": {\n    \"paths\": [\n      \"../module-a\"\n    ]\n  }\n}"
  },
  {
    "path": "test/fixtures/modules/json-module-d/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/modules/json-module-d/main.tf",
    "content": "\n"
  },
  {
    "path": "test/fixtures/modules/json-module-d/terragrunt.hcl.json",
    "content": "{\n  \"dependencies\": {\n    \"paths\": [\n      \"../module-a\",\n      \"../json-module-b/module-b-child\",\n      \"../module-c\"\n    ]\n  }\n}"
  },
  {
    "path": "test/fixtures/modules/module-a/terragrunt.hcl",
    "content": "terraform {\n  source = \"test\"\n}\n"
  },
  {
    "path": "test/fixtures/modules/module-abba/terragrunt.hcl",
    "content": "terraform {\n  source = \"temp\"\n}\n\ndependencies {\n  paths = [\"../module-a\"]\n}\n"
  },
  {
    "path": "test/fixtures/modules/module-b/module-b-child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/modules/module-b/module-b-child/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/modules/module-b/module-b-child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/modules/module-b/root.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket = \"bucket\"\n    key = \"${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\nterraform {\n  source = \"...\"\n}\n"
  },
  {
    "path": "test/fixtures/modules/module-c/terragrunt.hcl",
    "content": "terraform {\n  source = \"temp\"\n}\n\ndependencies {\n  paths = [\"../module-a\"]\n}\n"
  },
  {
    "path": "test/fixtures/modules/module-d/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/modules/module-d/main.tf",
    "content": "\n"
  },
  {
    "path": "test/fixtures/modules/module-d/terragrunt.hcl",
    "content": "dependencies {\n  paths = [\"../module-a\", \"../module-b/module-b-child\", \"../module-c\"]\n}\n"
  },
  {
    "path": "test/fixtures/modules/module-e/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/modules/module-e/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/modules/module-e/module-e-child/terragrunt.hcl",
    "content": "terraform {\n  source = \"test\"\n}\n\ninclude {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../../module-a\", \"../../module-b/module-b-child\"]\n}\n"
  },
  {
    "path": "test/fixtures/modules/module-e/root.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket = \"bucket\"\n    key = \"${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n\n"
  },
  {
    "path": "test/fixtures/modules/module-f/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/modules/module-f/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/modules/module-f/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/modules/module-g/terragrunt.hcl",
    "content": "terraform {\n  source = \"test\"\n}\n\ndependencies {\n  paths = [\"../module-f\"]\n}\n\n"
  },
  {
    "path": "test/fixtures/modules/module-h/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/modules/module-h/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/modules/module-h/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/modules/module-i/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/modules/module-i/terragrunt.hcl",
    "content": "dependencies {\n  paths = [\"../module-h\"]\n}\n"
  },
  {
    "path": "test/fixtures/modules/module-i/test.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/modules/module-j/terragrunt.hcl",
    "content": "terraform {\n  source = \"temp\"\n}\n\ndependencies {\n  paths = [\"../module-i\"]\n}\n"
  },
  {
    "path": "test/fixtures/modules/module-k/terragrunt.hcl",
    "content": "dependencies {\n  paths = [\"../module-h\"]\n}\n\nterraform {\n  source = \"fire\"\n}\n"
  },
  {
    "path": "test/fixtures/modules/module-l/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/modules/module-m/env.hcl",
    "content": "locals {\n  environment = \"dev\"\n}\n"
  },
  {
    "path": "test/fixtures/modules/module-m/module-m-child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/modules/module-m/module-m-child/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/modules/module-m/module-m-child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/modules/module-m/module-m-child/tier.hcl",
    "content": "locals {\n  tier = \"base\"\n}\n"
  },
  {
    "path": "test/fixtures/modules/module-m/root.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    bucket = \"bucket\"\n    key = \"${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\nterraform {\n  source = \"...\"\n}\nlocals {\n  env_vars = read_terragrunt_config(\"${get_parent_terragrunt_dir()}/env.hcl\")\n  tier_vars = read_terragrunt_config(\"tier.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/modules/module-missing-dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/modules/module-missing-dependency/main.tf",
    "content": "# Intentionally empty"
  },
  {
    "path": "test/fixtures/modules/module-missing-dependency/terragrunt.hcl",
    "content": "dependencies {\n  paths = [\"../not-a-real-dependency\"]\n}\n"
  },
  {
    "path": "test/fixtures/multiinclude-dependency/depa/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/multiinclude-dependency/depa/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/multiinclude-dependency/depa/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/multiinclude-dependency/depa.hcl",
    "content": "dependency \"depa\" {\n  config_path = \"${get_parent_terragrunt_dir()}/depa\"\n}\n"
  },
  {
    "path": "test/fixtures/multiinclude-dependency/depb/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/multiinclude-dependency/depb/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/multiinclude-dependency/depb/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/multiinclude-dependency/depb.hcl",
    "content": "dependency \"depb\" {\n  config_path = \"${get_parent_terragrunt_dir()}/depb\"\n}\n"
  },
  {
    "path": "test/fixtures/multiinclude-dependency/depc/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/multiinclude-dependency/depc/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/multiinclude-dependency/depc/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/multiinclude-dependency/depc.hcl",
    "content": "dependency \"depc\" {\n  config_path = \"${get_parent_terragrunt_dir()}/depc\"\n}\n"
  },
  {
    "path": "test/fixtures/multiinclude-dependency/main/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/multiinclude-dependency/main/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/multiinclude-dependency/main/terragrunt.hcl",
    "content": "include \"depa\" {\n  path = find_in_parent_folders(\"depa.hcl\")\n}\n\ninclude \"depb\" {\n  path = find_in_parent_folders(\"depb.hcl\")\n}\n\ninclude \"depc\" {\n  path = find_in_parent_folders(\"depc.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/multiinclude-dependency/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/no-color/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/no-color/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/no-color/terragrunt.hcl",
    "content": "inputs = {\n}\n"
  },
  {
    "path": "test/fixtures/no-color-dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/no-color-dependency/main.tf",
    "content": "output \"outputX\" {\n  value = \"Output from x\"\n}\n"
  },
  {
    "path": "test/fixtures/no-color-dependency/terragrunt.hcl",
    "content": "dependency \"y\" {\n  config_path = \"./y\"\n\n  mock_outputs = {\n    value = \"mock value\"\n  }\n}\n\nterraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/no-color-dependency/y/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/no-color-dependency/y/main.tf",
    "content": "output \"outputY\" {\n  value = \"Output from y\"\n}\n"
  },
  {
    "path": "test/fixtures/no-color-dependency/y/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/no-submodules/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/no-submodules/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/no-submodules/terragrunt.hcl",
    "content": "terraform {\n  source = \"git::git@github.com:terraform-google-modules/terraform-google-folders.git?ref=v4.0.0\"\n}\n"
  },
  {
    "path": "test/fixtures/null-values/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/null-values/main.tf",
    "content": "variable \"var1\" {}\n\nvariable \"var2\" {}\n\noutput \"output1\" {\n  value = var.var1\n}\n\noutput \"output2\" {\n  value = var.var2\n}\n"
  },
  {
    "path": "test/fixtures/null-values/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\ninputs = {\n  var1 = null\n  var2 = \"variable 2\"\n}\n"
  },
  {
    "path": "test/fixtures/out-dir/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/out-dir/app/main.tf",
    "content": "variable \"input_value\" {}\n\noutput \"output_value\" {\n  value = var.input_value\n}\n\nresource \"local_file\" \"app_file\" {\n  content  = \"app file\"\n  filename = \"${path.module}/app_file.txt\"\n}"
  },
  {
    "path": "test/fixtures/out-dir/app/terragrunt.hcl",
    "content": "\ndependency \"dependency\" {\n  config_path = \"../dependency\"\n\n  mock_outputs = {\n    result = \"42\"\n  }\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"apply\", \"show\"]\n}\n\ninputs = {\n  input_value = dependency.dependency.outputs.result\n}\n"
  },
  {
    "path": "test/fixtures/out-dir/dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/out-dir/dependency/main.tf",
    "content": "resource \"local_file\" \"file\" {\n  content  = \"dependency file\"\n  filename = \"${path.module}/dependency_file.txt\"\n}\n\noutput \"result\" {\n\n  value = \"42\"\n}"
  },
  {
    "path": "test/fixtures/out-dir/dependency/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/output-all/env1/app1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/output-all/env1/app1/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n}\n\noutput \"app1_text\" {\n  value = \"app1 output\"\n}\n"
  },
  {
    "path": "test/fixtures/output-all/env1/app1/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../app3\"]\n}\n\n"
  },
  {
    "path": "test/fixtures/output-all/env1/app2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/output-all/env1/app2/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n}\n\noutput \"app2_text\" {\n  value = \"app2 output\"\n}\n"
  },
  {
    "path": "test/fixtures/output-all/env1/app2/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../app3\", \"../app1\"]\n}\n\n"
  },
  {
    "path": "test/fixtures/output-all/env1/app3/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/output-all/env1/app3/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n}\n\noutput \"app3_text\" {\n  value = \"app3 output\"\n}\n"
  },
  {
    "path": "test/fixtures/output-all/env1/app3/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/output-all/root.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"${path_relative_to_include()}/terraform.tfstate\"\n    region = \"us-west-2\"\n  }\n}\n\ninputs = {\n  terraform_remote_state_s3_bucket = \"__FILL_IN_BUCKET_NAME__\"\n}\n"
  },
  {
    "path": "test/fixtures/output-from-dependency/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/output-from-dependency/app/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/output-from-dependency/app/terragrunt.hcl",
    "content": "dependencies {\n  paths = [\"../dependency\"]\n}\n\ndependency \"test\" {\n  config_path = \"../dependency\"\n}\n\ninputs = {\n  vpc_config = dependency.test.outputs\n}\n"
  },
  {
    "path": "test/fixtures/output-from-dependency/dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/output-from-dependency/dependency/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/output-from-dependency/dependency/outputs.tf",
    "content": "output \"foo\" {\n  value       = var.foo\n  description = \"Test foo value\"\n}\n"
  },
  {
    "path": "test/fixtures/output-from-dependency/dependency/terragrunt.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n\n  config = {\n    encrypt = true\n    bucket  = \"__FILL_IN_BUCKET_NAME__\"\n    key     = \"${path_relative_to_include()}/terraform.tfstate\"\n    region  = \"__FILL_IN_REGION__\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/output-from-dependency/dependency/variables.tf",
    "content": "variable \"foo\" {\n  description = \"The value to be returned from the module\"\n  type        = string\n  default     = \"test-foo-value\"\n}\n"
  },
  {
    "path": "test/fixtures/output-from-remote-state/env1/app1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/output-from-remote-state/env1/app1/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n}\n\noutput \"app1_text\" {\n  value = \"app1 output\"\n}\n"
  },
  {
    "path": "test/fixtures/output-from-remote-state/env1/app1/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../app3\"]\n}\n"
  },
  {
    "path": "test/fixtures/output-from-remote-state/env1/app2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/output-from-remote-state/env1/app2/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n}\n\noutput \"app1_text\" {\n  value = var.app1_text\n}\n\noutput \"app2_text\" {\n  value = \"app2 output\"\n}\n\noutput \"app3_text\" {\n  value = var.app3_text\n}\n"
  },
  {
    "path": "test/fixtures/output-from-remote-state/env1/app2/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"app1\" {\n  config_path = \"../app1\"\n\n  mock_outputs = {\n    app1_text = \"(known after run --all apply)\"\n  }\n}\n\ndependency \"app3\" {\n  config_path = \"../app3\"\n\n  mock_outputs = {\n    app3_text = \"(known after run --all apply)\"\n  }\n}\n\ninputs = {\n  app1_text = dependency.app1.outputs.app1_text\n  app3_text = dependency.app3.outputs.app3_text\n}\n"
  },
  {
    "path": "test/fixtures/output-from-remote-state/env1/app2/variables.tf",
    "content": "variable \"app1_text\" {\n  type = string\n}\n\nvariable \"app3_text\" {\n  type = string\n}\n"
  },
  {
    "path": "test/fixtures/output-from-remote-state/env1/app3/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/output-from-remote-state/env1/app3/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n}\n\noutput \"app3_text\" {\n  value = \"app3 output\"\n}\n"
  },
  {
    "path": "test/fixtures/output-from-remote-state/env1/app3/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/output-from-remote-state/root.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"${path_relative_to_include()}/terraform.tfstate\"\n    region = \"us-west-2\"\n  }\n}\n\ninputs = {\n  terraform_remote_state_s3_bucket = \"__FILL_IN_BUCKET_NAME__\"\n}\n"
  },
  {
    "path": "test/fixtures/output-module-groups/root/backend-app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/output-module-groups/root/backend-app/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n}\n"
  },
  {
    "path": "test/fixtures/output-module-groups/root/backend-app/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../mysql\", \"../redis\", \"../vpc\"]\n}\n\n"
  },
  {
    "path": "test/fixtures/output-module-groups/root/frontend-app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/output-module-groups/root/frontend-app/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n}\n"
  },
  {
    "path": "test/fixtures/output-module-groups/root/frontend-app/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../backend-app\", \"../vpc\"]\n}\n"
  },
  {
    "path": "test/fixtures/output-module-groups/root/mysql/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/output-module-groups/root/mysql/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n}\n"
  },
  {
    "path": "test/fixtures/output-module-groups/root/mysql/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../vpc\"]\n}\n\n"
  },
  {
    "path": "test/fixtures/output-module-groups/root/redis/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/output-module-groups/root/redis/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n}\n"
  },
  {
    "path": "test/fixtures/output-module-groups/root/redis/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../vpc\"]\n}\n\n"
  },
  {
    "path": "test/fixtures/output-module-groups/root/root.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"${path_relative_to_include()}/terraform.tfstate\"\n    region = \"us-west-2\"\n  }\n}\n\ninputs = {\n  terraform_remote_state_s3_bucket = \"__FILL_IN_BUCKET_NAME__\"\n}\n"
  },
  {
    "path": "test/fixtures/output-module-groups/root/vpc/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/output-module-groups/root/vpc/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n}\n"
  },
  {
    "path": "test/fixtures/output-module-groups/root/vpc/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\n\n"
  },
  {
    "path": "test/fixtures/parallel-run/.tflint.hcl",
    "content": "plugin \"aws-cis\" {\n  enabled = true\n  version = \"0.0.2\"\n  source  = \"github.com/gruntwork-io/tflint-ruleset-aws-cis\"\n}\n\nconfig {\n  module = true\n}\n\nplugin \"terraform\" {\n  enabled = false\n}\n"
  },
  {
    "path": "test/fixtures/parallel-run/common/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/aws\" {\n  version = \"6.30.0\"\n  hashes = [\n    \"h1:183n3MCQJFoK24qq12nreIUTJeiF/cl1671WNJvroPU=\",\n    \"h1:1cZvSrqXPq5FzDtKwWAvBcNjpv2R7K7d2q0el7Kuf/A=\",\n    \"h1:FCq9CnVll6bPGxoPS8mdc8vj624kXpCjzKgpOhQMnvA=\",\n    \"zh:0e28cc366c4983b2e561c3d98484077cee9dce099ef089c216cc9ce37ea2eef1\",\n    \"zh:4867aed567af2e7cc4d76c2efd388c1e4cfe63f7d1c8671216390a06d1d1cff3\",\n    \"zh:5c3df2978d902de86088f7f4bc1c25f7b24751d5f159112c545bb368d8713395\",\n    \"zh:663655ea24dd10f26b0b97b7415e7a29828a3350fdb209ac420c26fd87a50815\",\n    \"zh:68599b6100d63cd5c93895afa2bfd07f41aed41353c7a855ee44d6e44e09feb6\",\n    \"zh:716af9513928e3bebbeb5a46ce25679538c42fd1f586033731cee38f20eca008\",\n    \"zh:8f6265128a8656a5bc669785f4558fe2f757f8f698521af117ac1b2e84afafed\",\n    \"zh:c03241fe3fad6fe6634770fed349ce25f1b7d431f25e4ec0b6127d31fa66e232\",\n    \"zh:e7c74dbd72e35f07ffdfc3f8227fccadd150d3d50f9dca06042548afee93951b\",\n  ]\n}\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/parallel-run/common/terragrunt.hcl",
    "content": "terraform {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/dirs?ref=v0.99.1\"\n}\n\ngenerate \"providers\" {\n  path      = \"providers.tf\"\n  if_exists = \"overwrite\"\n  contents  = <<EOF\nprovider \"aws\" {\n  region              = \"us-east-1\"\n}\nEOF\n}\n\ngenerate \"outputs_1\" {\n  path      = \"outputs_1.tf\"\n  if_exists = \"overwrite\"\n  contents  = <<EOF\noutput \"outputs_1\" {\n    value = \"outputs_1\"\n}\nEOF\n}\n\ngenerate \"outputs_2\" {\n  path      = \"outputs_2.tf\"\n  if_exists = \"overwrite\"\n  contents  = <<EOF\noutput \"outputs_2\" {\n    value = \"outputs_2\"\n}\nEOF\n}\n\ngenerate \"outputs_3\" {\n  path      = \"outputs_3.tf\"\n  if_exists = \"overwrite\"\n  contents  = <<EOF\noutput \"outputs_3\" {\n    value = \"outputs_3\"\n}\nEOF\n}\n\ngenerate \"outputs_4\" {\n  path      = \"outputs_4.tf\"\n  if_exists = \"overwrite\"\n  contents  = <<EOF\noutput \"outputs_4\" {\n    value = \"outputs_4\"\n}\nEOF\n}\n\ngenerate \"outputs_5\" {\n  path      = \"outputs_5.tf\"\n  if_exists = \"overwrite\"\n  contents  = <<EOF\noutput \"outputs_5\" {\n    value = \"outputs_5\"\n}\nEOF\n}\n"
  },
  {
    "path": "test/fixtures/parallel-run/dev/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/aws\" {\n  version = \"6.30.0\"\n  hashes = [\n    \"h1:183n3MCQJFoK24qq12nreIUTJeiF/cl1671WNJvroPU=\",\n    \"h1:1cZvSrqXPq5FzDtKwWAvBcNjpv2R7K7d2q0el7Kuf/A=\",\n    \"h1:FCq9CnVll6bPGxoPS8mdc8vj624kXpCjzKgpOhQMnvA=\",\n    \"zh:0e28cc366c4983b2e561c3d98484077cee9dce099ef089c216cc9ce37ea2eef1\",\n    \"zh:4867aed567af2e7cc4d76c2efd388c1e4cfe63f7d1c8671216390a06d1d1cff3\",\n    \"zh:5c3df2978d902de86088f7f4bc1c25f7b24751d5f159112c545bb368d8713395\",\n    \"zh:663655ea24dd10f26b0b97b7415e7a29828a3350fdb209ac420c26fd87a50815\",\n    \"zh:68599b6100d63cd5c93895afa2bfd07f41aed41353c7a855ee44d6e44e09feb6\",\n    \"zh:716af9513928e3bebbeb5a46ce25679538c42fd1f586033731cee38f20eca008\",\n    \"zh:8f6265128a8656a5bc669785f4558fe2f757f8f698521af117ac1b2e84afafed\",\n    \"zh:c03241fe3fad6fe6634770fed349ce25f1b7d431f25e4ec0b6127d31fa66e232\",\n    \"zh:e7c74dbd72e35f07ffdfc3f8227fccadd150d3d50f9dca06042548afee93951b\",\n  ]\n}\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/parallel-run/dev/app/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/dirs?ref=v0.99.1\"\n}\n\ndependency \"common\" {\n  config_path                             = \"../../common\"\n  mock_outputs_allowed_terraform_commands = [\"validate\", \"plan\"]\n  mock_outputs = {\n    vpc_id = \"fake-vpc-id\"\n  }\n\n  skip_outputs = \"true\"\n}\n\n\ngenerate \"provider\" {\n  path      = \"providers.tf\"\n  if_exists = \"overwrite\"\n  contents  = <<EOF\nprovider \"aws\" {\n  region              = \"us-east-1\"\n}\nEOF\n}\n\n\ngenerate \"outputs_1\" {\n  path      = \"outputs_1.tf\"\n  if_exists = \"overwrite\"\n  contents  = <<EOF\noutput \"outputs_1\" {\n    value = \"outputs_1\"\n}\nEOF\n}\n\ngenerate \"outputs_2\" {\n  path      = \"outputs_2.tf\"\n  if_exists = \"overwrite\"\n  contents  = <<EOF\noutput \"outputs_2\" {\n    value = \"outputs_2\"\n}\nEOF\n}\n"
  },
  {
    "path": "test/fixtures/parallel-run/root.hcl",
    "content": "terraform {\n  include_in_copy = [\".terraform-version\"]\n  before_hook \"tflint\" {\n    commands = [\"apply\", \"plan\", \"init\"]\n    execute  = [\"tflint\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/parallel-state-init/root.hcl",
    "content": "remote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    bucket         = \"__FILL_IN_BUCKET_NAME__\"\n    key            = \"${path_relative_to_include()}/terraform.tfstate\"\n    region         = \"us-west-2\"\n    encrypt        = true\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/parallel-state-init/template/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/parallel-state-init/template/main.tf",
    "content": "resource \"local_file\" \"file\" {\n  content  = \"test file\"\n  filename = \"${path.module}/file.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/parallel-state-init/template/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/parallelism/template/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/parallelism/template/main.tf",
    "content": "terraform {\n  backend \"local\" {}\n}\n\nvariable \"sleep_seconds\" {\n  type = number\n}\n\nresource \"null_resource\" \"sleep\" {\n  provisioner \"local-exec\" {\n    command = \"sleep ${var.sleep_seconds}\"\n  }\n}\n\nresource \"local_file\" \"timestamp\" {\n  content    = timestamp()\n  filename   = \"${path.module}/timestamp.txt\"\n  depends_on = [null_resource.sleep]\n}\n\noutput \"out\" {\n  value = local_file.timestamp.content\n}\n"
  },
  {
    "path": "test/fixtures/parallelism/template/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/parallelism/terragrunt.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"local\"\n  config = {}\n}\n\ninputs = {\n  terraform_remote_state_s3_bucket = \"__FILL_IN_BUCKET_NAME__\"\n}\n"
  },
  {
    "path": "test/fixtures/parent-folders/in-another-subfolder/common/foo.txt",
    "content": "some text\n"
  },
  {
    "path": "test/fixtures/parent-folders/in-another-subfolder/live/terragrunt.hcl",
    "content": "include \"common\" {\n  path = find_in_parent_folders(\"common/foo.txt\")\n}\n"
  },
  {
    "path": "test/fixtures/parent-folders/multiple-terragrunt-in-parents/child/root.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/parent-folders/multiple-terragrunt-in-parents/child/sub-child/root.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\n"
  },
  {
    "path": "test/fixtures/parent-folders/multiple-terragrunt-in-parents/child/sub-child/sub-sub-child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/parent-folders/multiple-terragrunt-in-parents/root.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket = \"my-bucket\"\n    key = \"${path_relative_to_include()}/terraform.tfstate\"\n    region = \"us-east-1\"\n  }\n}\n\n"
  },
  {
    "path": "test/fixtures/parent-folders/no-terragrunt-in-root/child/sub-child/terragrunt.hcl",
    "content": "# Placeholder"
  },
  {
    "path": "test/fixtures/parent-folders/other-file-names/child/terragrunt.hcl",
    "content": "# Placeholder"
  },
  {
    "path": "test/fixtures/parent-folders/other-file-names/foo.txt",
    "content": "# Placeholder"
  },
  {
    "path": "test/fixtures/parent-folders/terragrunt-in-root/child/sub-child/sub-sub-child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/parent-folders/terragrunt-in-root/child/sub-child/sub-sub-child/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\n# Create an arbitrary local resource\nresource \"null_resource\" \"test\" {\n  provisioner \"local-exec\" {\n    command = \"echo Hello, World!\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/parent-folders/terragrunt-in-root/child/sub-child/sub-sub-child/override/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/parent-folders/terragrunt-in-root/child/sub-child/sub-sub-child/terragrunt.hcl",
    "content": "# Placeholder"
  },
  {
    "path": "test/fixtures/parent-folders/terragrunt-in-root/root.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket = \"my-bucket\"\n    key = \"${path_relative_to_include()}/terraform.tfstate\"\n    region = \"us-east-1\"\n  }\n}\n\n"
  },
  {
    "path": "test/fixtures/parent-folders/with-params/tfwork/test-var/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/parent-folders/with-params/tfwork/test-var/providers.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/parent-folders/with-params/tfwork/tg/terragrunt.hcl",
    "content": "include \"module\" {\n  path = \"${find_in_parent_folders(\"tfwork\")}/test-var/providers.tf\"\n}\n"
  },
  {
    "path": "test/fixtures/parsing/exposed-include-with-deprecated-inputs/child/terragrunt.hcl",
    "content": "# Child configuration that includes compcommon with expose = true\n# This should trigger the bug where the deprecated syntax in compcommon isn't detected\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n  expose = true\n}\n\ninclude \"compcommon\" {\n  path = find_in_parent_folders(\"compcommon.hcl\")\n  expose = true\n}\n\nterraform {\n  source = \"git::git@github.com:gruntwork-io/terragrunt.git//test/fixtures/download/hello-world\"\n}\n\ndependency \"service\" {\n  config_path = \"../dep\"\n  mock_outputs_allowed_terraform_commands = [\"validate\", \"plan\"]\n  mock_outputs = {\n    some_value = \"mock-service-value\"\n  }\n  mock_outputs_merge_strategy_with_state = \"shallow\"\n}\n\n# Reference the exposed include - this will try to evaluate compcommon\n# which contains the deprecated syntax\ninputs = {\n  from_common = try(include.compcommon.inputs.value_from_dep, \"fallback\")\n  from_service = dependency.service.outputs.some_value\n}\n"
  },
  {
    "path": "test/fixtures/parsing/exposed-include-with-deprecated-inputs/compcommon.hcl",
    "content": "# Common configuration that uses deprecated dependency.*.inputs.* syntax\n# This should trigger the bug when included with expose = true\n\ndependency \"dep\" {\n  config_path = \"../dep\"\n  mock_outputs_allowed_terraform_commands = [\"validate\", \"plan\"]\n  mock_outputs = {\n    some_value = \"mock-value\"\n  }\n  mock_outputs_merge_strategy_with_state = \"shallow\"\n}\n\n# Using deprecated syntax - this should be caught but isn't in partial parse\ninputs = {\n  value_from_dep = dependency.dep.inputs.some_value\n}\n"
  },
  {
    "path": "test/fixtures/parsing/exposed-include-with-deprecated-inputs/dep/terragrunt.hcl",
    "content": "# Mock dependency configuration\nterraform {\n  source = \"git::git@github.com:gruntwork-io/terragrunt.git//test/fixtures/download/hello-world\"\n}\n\ninputs = {\n  some_value = \"test-value\"\n}\n"
  },
  {
    "path": "test/fixtures/parsing/exposed-include-with-deprecated-inputs/root.hcl",
    "content": "# Minimal root configuration\nlocals {\n  region = \"us-east-1\"\n}\n"
  },
  {
    "path": "test/fixtures/partial-parse/ignore-bad-block-in-parent/child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/partial-parse/ignore-bad-block-in-parent/root.hcl",
    "content": "dependencies {\n  # This function call will fail when attempting to decode\n  paths = [file(\"i-am-a-file-that-does-not-exist\")]\n}\n\nprevent_destroy = true\n"
  },
  {
    "path": "test/fixtures/partial-parse/partial-inheritance/child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/partial-parse/partial-inheritance/root.hcl",
    "content": "dependencies {\n  # This function call will fail when attempting to decode\n  paths = [\"../app1\"]\n}\n\nprevent_destroy = true\n"
  },
  {
    "path": "test/fixtures/partial-parse/terragrunt-version-constraint/terragrunt.hcl",
    "content": "terragrunt_version_constraint                                                              = \">= 0.23.0\"\ni_am_an_attribute_that_terragrunt_doesnt_understand_that_might_be_introduced_in_the_future = \"Hello World\"\n"
  },
  {
    "path": "test/fixtures/planfile-order-test/.gitignore",
    "content": "test-provider.tf\n"
  },
  {
    "path": "test/fixtures/planfile-order-test/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/planfile-order-test/inputs.tf",
    "content": "variable \"resource_count\" {\n  type = number\n}\n"
  },
  {
    "path": "test/fixtures/planfile-order-test/resource.tf",
    "content": "resource \"null_resource\" \"test-resources\" {\n  count = var.resource_count\n}\n"
  },
  {
    "path": "test/fixtures/planfile-order-test/terragrunt.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\ngenerate \"test-null-provider\" {\n  path      = \"test-provider.tf\"\n  if_exists = \"overwrite_terragrunt\"\n\n  contents = <<EOF\nprovider \"null\" {\n}\nEOF\n}\n\nterraform {\n  extra_arguments \"plan_vars\" {\n    commands = [\n      \"plan\",\n    ]\n\n    arguments = [\n      \"-out=${get_terragrunt_dir()}/default.tfplan\",\n      \"-var-file\",\n      \"${get_terragrunt_dir()}/vars/variables.tfvars\",\n      \"-no-color\",\n    ]\n  }\n\n  extra_arguments \"apply_vars\" {\n    commands = [\n      \"apply\",\n    ]\n\n    arguments = [\n      \"${get_terragrunt_dir()}/default.tfplan\",\n      \"-no-color\",\n    ]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/planfile-order-test/vars/variables.tfvars",
    "content": "resource_count = 3\n"
  },
  {
    "path": "test/fixtures/prevent-destroy-not-set/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/prevent-destroy-not-set/child/main.tf",
    "content": "resource \"null_resource\" \"example\" {}\n\noutput \"foo\" {\n  value = \"bar\"\n}\n"
  },
  {
    "path": "test/fixtures/prevent-destroy-not-set/child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/prevent-destroy-not-set/root.hcl",
    "content": "prevent_destroy = true\n"
  },
  {
    "path": "test/fixtures/prevent-destroy-override/child/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/prevent-destroy-override/child/main.tf",
    "content": "resource \"null_resource\" \"example\" {}\n\noutput \"foo\" {\n  value = \"bar\"\n}\n"
  },
  {
    "path": "test/fixtures/prevent-destroy-override/child/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nprevent_destroy = false\n"
  },
  {
    "path": "test/fixtures/prevent-destroy-override/root.hcl",
    "content": "prevent_destroy = true\n"
  },
  {
    "path": "test/fixtures/private-registry/env.tfrc",
    "content": "credentials \"__registry_host__\" {\n    token = \"__registry_token__\"\n}"
  },
  {
    "path": "test/fixtures/private-registry/terragrunt.hcl",
    "content": "# Retrieve a module from the public terraform registry to use with terragrunt\nterraform {\n  source = \"tfr://__registry_url__\"\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/dependency/.gitignore",
    "content": "# Ignore lock files to allow tests to run with both Terraform and OpenTofu\n.terraform.lock.hcl\n"
  },
  {
    "path": "test/fixtures/provider-cache/dependency/app/main.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"hashicorp/null\"\n      version = \"3.2.4\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/dependency/app/terragrunt.hcl",
    "content": "dependency \"dep\" {\n  config_path = \"../dep\"\n\n  mock_outputs = {\n    result = \"mock\"\n  }\n}\n\ninputs = {\n  dep_value = dependency.dep.outputs.result\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/dependency/dep/main.tf",
    "content": "terraform {\n  required_providers {\n    local = {\n      source  = \"hashicorp/local\"\n      version = \"2.7.0\"\n    }\n  }\n}\n\noutput \"result\" {\n  value = \"hello\"\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/dependency/dep/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/.gitignore",
    "content": "# Ignore lock files to allow tests to run with both Terraform and OpenTofu\n.terraform.lock.hcl\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.36.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n  }\n}\n\nmodule \"naming\" {\n  source  = \"cloudposse/label/null\"\n  version = \"0.25.0\"\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app1/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.36.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app1/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app2/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.36.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n  }\n}\n\n\nmodule \"naming\" {\n  source = \"cloudposse/label/null\"\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app2/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app3/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.36.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app3/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app4/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.36.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app4/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app5/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.36.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app5/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app6/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.36.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app6/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app7/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.36.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app7/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app8/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.36.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app8/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app9/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.36.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/first/app9/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.40.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n    kubernetes = {\n      source  = \"hashicorp/kubernetes\"\n      version = \"2.27.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app1/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.40.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n    kubernetes = {\n      source  = \"hashicorp/kubernetes\"\n      version = \"2.27.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app1/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app2/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.40.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n    kubernetes = {\n      source  = \"hashicorp/kubernetes\"\n      version = \"2.27.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app2/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app3/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.40.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n    kubernetes = {\n      source  = \"hashicorp/kubernetes\"\n      version = \"2.27.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app3/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app4/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.40.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n    kubernetes = {\n      source  = \"hashicorp/kubernetes\"\n      version = \"2.27.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app4/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app5/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.40.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n    kubernetes = {\n      source  = \"hashicorp/kubernetes\"\n      version = \"2.27.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app5/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app6/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.40.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n    kubernetes = {\n      source  = \"hashicorp/kubernetes\"\n      version = \"2.27.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app6/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app7/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.40.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n    kubernetes = {\n      source  = \"hashicorp/kubernetes\"\n      version = \"2.27.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app7/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app8/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.40.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n    kubernetes = {\n      source  = \"hashicorp/kubernetes\"\n      version = \"2.27.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app8/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app9/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"5.40.0\"\n    }\n    azure = {\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n    kubernetes = {\n      source  = \"hashicorp/kubernetes\"\n      version = \"2.27.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/direct/second/app9/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/filesystem-mirror/app/main.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source = \"registry.opentofu.org/hashicorp/null\"\n    }\n    aws = {\n      source  = \"example.com/hashicorp/aws\"\n      version = \"5.59.0\"\n    }\n    azurerm = {\n      source  = \"example.com/hashicorp/azurerm\"\n      version = \"3.113.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/filesystem-mirror/app/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/multiple-platforms/.gitignore",
    "content": "# The fixtures in this directory are used to test the interaction between\n# the Terragrunt provider cache server and generated OpenTofu/Terraform lock files.\n#\n# As such, we need to make sure we don't commit any particular lock file here so\n# that we can test what is generated when the provider cache server is used without one.\n.terraform.lock.hcl\n\n"
  },
  {
    "path": "test/fixtures/provider-cache/multiple-platforms/app1/main.tf",
    "content": "terraform {\n  required_version = \">= 1.0\"\n\n  required_providers {\n    aws = {\n      # Not fully qualified to allow tests to run with both Terraform and OpenTofu\n      # and verify that a different lock file will be generated for each.\n      source  = \"hashicorp/aws\"\n      version = \"5.36.0\"\n    }\n    azure = {\n      # Not fully qualified to allow tests to run with both Terraform and OpenTofu\n      # and verify that a different lock file will be generated for each.\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/multiple-platforms/app1/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/multiple-platforms/app2/main.tf",
    "content": "terraform {\n  required_version = \">= 1.0\"\n\n  required_providers {\n    aws = {\n      # Not fully qualified to allow tests to run with both Terraform and OpenTofu\n      # and verify that a different lock file will be generated for each.\n      source  = \"hashicorp/aws\"\n      version = \"5.36.0\"\n    }\n    azure = {\n      # Not fully qualified to allow tests to run with both Terraform and OpenTofu\n      # and verify that a different lock file will be generated for each.\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/multiple-platforms/app2/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/multiple-platforms/app3/main.tf",
    "content": "terraform {\n  required_version = \">= 1.0\"\n  required_providers {\n    aws = {\n      # Not fully qualified to allow tests to run with both Terraform and OpenTofu\n      # and verify that a different lock file will be generated for each.\n      source  = \"hashicorp/aws\"\n      version = \"5.36.0\"\n    }\n    azure = {\n      # Not fully qualified to allow tests to run with both Terraform and OpenTofu\n      # and verify that a different lock file will be generated for each.\n      source  = \"hashicorp/azurerm\"\n      version = \"3.95.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/multiple-platforms/app3/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/network-mirror/apps/app0/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"example.com/hashicorp/aws\"\n      version = \"5.59.0\"\n    }\n    azurerm = {\n      source  = \"example.com/hashicorp/azurerm\"\n      version = \"3.113.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/network-mirror/apps/app0/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/network-mirror/apps/app1/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"example.com/hashicorp/aws\"\n      version = \"5.58.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/network-mirror/apps/app1/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/provider-cache/weak-constraint/.gitignore",
    "content": ".terraform.lock.hcl\n"
  },
  {
    "path": "test/fixtures/provider-cache/weak-constraint/app/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.0\"\n\n  required_providers {\n    cloudflare = {\n      # Source is not fully qualified so the registry is resolved dynamically\n      # based on the Terraform/OpenTofu implementation, allowing the provider\n      # cache to intercept requests.\n      source  = \"cloudflare/cloudflare\"\n      version = \"~> 4.0\"\n    }\n    time = {\n      # Source is not fully qualified so the registry is resolved dynamically\n      # based on the Terraform/OpenTofu implementation, allowing the provider\n      # cache to intercept requests.\n      source  = \"hashicorp/time\"\n      version = \">= 0.10.0\"\n    }\n  }\n}\n"
  },
  {
    "path": "test/fixtures/provider-cache/weak-constraint/app/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/queue-strict-include/dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/queue-strict-include/dependency/main.tf",
    "content": "resource \"terraform_data\" \"test\" {\n  input = \"test\"\n}\n"
  },
  {
    "path": "test/fixtures/queue-strict-include/dependency/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\ndependency \"transitive_dep\" {\n  config_path = \"../transitive-dependency\"\n\n  mock_outputs = {}\n}\n\n"
  },
  {
    "path": "test/fixtures/queue-strict-include/dependent/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/queue-strict-include/dependent/main.tf",
    "content": "resource \"terraform_data\" \"test\" {\n  input = \"test\"\n}\n"
  },
  {
    "path": "test/fixtures/queue-strict-include/dependent/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\ndependency \"dep\" {\n  config_path = \"../dependency\"\n\n  mock_outputs = {}\n}\n"
  },
  {
    "path": "test/fixtures/queue-strict-include/transitive-dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/queue-strict-include/transitive-dependency/main.tf",
    "content": "resource \"terraform_data\" \"test\" {\n  input = \"test\"\n}\n"
  },
  {
    "path": "test/fixtures/queue-strict-include/transitive-dependency/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/queue-strict-include-units-reading/live/foo/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/queue-strict-include-units-reading/live/foo/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/queue-strict-include-units-reading/live/foo/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\nlocals {\n  source_config = read_terragrunt_config(\"../../sources/source.hcl\")\n}\n\n"
  },
  {
    "path": "test/fixtures/queue-strict-include-units-reading/sources/source.hcl",
    "content": "locals {\n  common_value = \"from-source\"\n}\n\n"
  },
  {
    "path": "test/fixtures/read-config/from_dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/read-config/from_dependency/dep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/read-config/from_dependency/dep/main.tf",
    "content": "variable \"foo\" {}\n\noutput \"foo\" { value = var.foo }\n"
  },
  {
    "path": "test/fixtures/read-config/from_dependency/dep/terragrunt.hcl",
    "content": "locals {\n  vars = read_terragrunt_config(\"vars.hcl\")\n}\n\ninputs = {\n  foo = local.vars.locals.foo\n}\n"
  },
  {
    "path": "test/fixtures/read-config/from_dependency/dep/vars.hcl",
    "content": "locals {\n  foo = \"hello world\"\n}\n"
  },
  {
    "path": "test/fixtures/read-config/from_dependency/main.tf",
    "content": "variable \"bar\" {}\noutput \"bar\" { value = var.bar }\n"
  },
  {
    "path": "test/fixtures/read-config/from_dependency/terragrunt.hcl",
    "content": "dependency \"dep\" {\n  config_path = \"./dep/\"\n}\n\ninputs = {\n  bar = dependency.dep.outputs.foo\n}\n"
  },
  {
    "path": "test/fixtures/read-config/full/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/read-config/full/main.tf",
    "content": "variable \"localstg\" {}\noutput \"localstg\" { value = var.localstg }\n\nvariable \"generate\" {}\noutput \"generate\" { value = var.generate }\n\nvariable \"remote_state\" {}\noutput \"remote_state\" { value = var.remote_state }\n\nvariable \"terraformtg\" {}\noutput \"terraformtg\" { value = var.terraformtg }\n\nvariable \"dependencies\" {}\noutput \"dependencies\" { value = var.dependencies }\n\nvariable \"terraform_binary\" {}\noutput \"terraform_binary\" { value = var.terraform_binary }\n\nvariable \"terraform_version_constraint\" {}\noutput \"terraform_version_constraint\" { value = var.terraform_version_constraint }\n\nvariable \"terragrunt_version_constraint\" {}\noutput \"terragrunt_version_constraint\" { value = var.terragrunt_version_constraint }\n\nvariable \"download_dir\" {}\noutput \"download_dir\" { value = var.download_dir }\n\nvariable \"prevent_destroy\" {}\noutput \"prevent_destroy\" { value = var.prevent_destroy }\n\nvariable \"exclude\" {}\noutput \"exclude\" { value = var.exclude }\n\nvariable \"iam_role\" {}\noutput \"iam_role\" { value = var.iam_role }\n\nvariable \"inputs\" {}\noutput \"inputs\" { value = var.inputs }\n"
  },
  {
    "path": "test/fixtures/read-config/full/source.hcl",
    "content": "locals {\n  the_answer = 42\n}\n\ngenerate \"provider\" {\n  path = \"provider.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\nEOF\n}\n\nremote_state {\n  backend = \"local\"\n  generate = {\n    path = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n  config = {\n    path = \"foo.tfstate\"\n  }\n  encryption = {\n    key_provider = \"foo\"\n  }\n}\n\nterraform {\n  source                   = \"./delorean\"\n  include_in_copy          = [\"time_machine.*\"]\n  exclude_from_copy        = [\"excluded_time_machine.*\"]\n  copy_terraform_lock_file = true\n\n  extra_arguments \"var-files\" {\n    commands = [\"apply\", \"plan\"]\n    required_var_files = [\"extra.tfvars\"]\n    optional_var_files = [\"optional.tfvars\"]\n    env_vars = {\n      TF_VAR_custom_var = \"I'm set in extra_arguments env_vars\"\n    }\n  }\n\n  before_hook \"before_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\", \"before.out\"]\n    run_on_error = true\n  }\n\n  after_hook \"after_hook_1\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"touch\", \"after.out\"]\n    run_on_error = true\n  }\n}\n\ndependencies {\n  paths = [\"../../terragrunt\"]\n}\n\nterraform_binary = \"terragrunt\"\nterraform_version_constraint = \"= 0.12.20\"\nterragrunt_version_constraint = \"= 0.23.18\"\ndownload_dir = \".terragrunt-cache\"\nprevent_destroy = true\niam_role = \"TerragruntIAMRole\"\n\nexclude {\n  if = true\n  actions = [\"all\"]\n  no_run = true\n}\n\ninputs = {\n  doc = \"Emmett Brown\"\n}\n"
  },
  {
    "path": "test/fixtures/read-config/full/terragrunt.hcl",
    "content": "locals {\n  config = read_terragrunt_config(\"${get_terragrunt_dir()}/source.hcl\")\n}\n\ninputs = {\n  localstg                     = local.config.locals\n  generate                     = local.config.generate\n  remote_state                 = local.config.remote_state\n  terraformtg                  = local.config.terraform\n  dependencies                 = local.config.dependencies\n  terraform_binary             = local.config.terraform_binary\n  terraform_version_constraint = local.config.terraform_version_constraint\n  terragrunt_version_constraint = local.config.terragrunt_version_constraint\n  download_dir                 = local.config.download_dir\n  prevent_destroy              = local.config.prevent_destroy\n  exclude                      = local.config.exclude\n  iam_role                     = local.config.iam_role\n  inputs                       = local.config.inputs\n}\n"
  },
  {
    "path": "test/fixtures/read-config/iam_role_in_file/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/read-config/iam_role_in_file/main.tf",
    "content": "terraform {\n  backend \"local\" {}\n}\n"
  },
  {
    "path": "test/fixtures/read-config/iam_role_in_file/terragrunt.hcl",
    "content": "iam_role = \"arn:aws:iam::666666666666:role/terragrunttest\"\n\nremote_state {\n  backend = \"local\"\n  generate = {\n    // state file should load value from iam_role\n    path      = \"${get_aws_account_id()}.txt\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    path = \"terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/read-config/iam_roles_multiple_modules/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/read-config/iam_roles_multiple_modules/component1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/read-config/iam_roles_multiple_modules/component1/main.tf",
    "content": "# Empty\n"
  },
  {
    "path": "test/fixtures/read-config/iam_roles_multiple_modules/component1/terragrunt.hcl",
    "content": "iam_role = \"arn:aws:iam::${local.aws_id_b}:role/terragrunt\"\n\nlocals {\n  aws_id_b = \"component1\"\n}\n"
  },
  {
    "path": "test/fixtures/read-config/iam_roles_multiple_modules/component2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/read-config/iam_roles_multiple_modules/component2/main.tf",
    "content": "# Empty\n"
  },
  {
    "path": "test/fixtures/read-config/iam_roles_multiple_modules/component2/terragrunt.hcl",
    "content": "iam_role = \"arn:aws:iam::${local.aws_id_b}:role/terragrunt\"\n\nlocals {\n  aws_id_b = \"component2\"\n}\n"
  },
  {
    "path": "test/fixtures/read-config/iam_roles_multiple_modules/main.tf",
    "content": "# Empty\n"
  },
  {
    "path": "test/fixtures/read-config/iam_roles_multiple_modules/terragrunt.hcl",
    "content": "\ndependency \"component1\" {\n  config_path = \"${get_parent_terragrunt_dir()}/component1\"\n}\n\ndependency \"component2\" {\n  config_path = \"${get_parent_terragrunt_dir()}/component2\"\n}\n"
  },
  {
    "path": "test/fixtures/read-config/with_constraints/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/read-config/with_constraints/main.tf",
    "content": "output \"foo\" { value = \"bar\" }"
  },
  {
    "path": "test/fixtures/read-config/with_default/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/read-config/with_default/main.tf",
    "content": "variable \"data\" {}\noutput \"data\" {\n  value = var.data\n}\n"
  },
  {
    "path": "test/fixtures/read-config/with_default/terragrunt.hcl",
    "content": "locals {\n  config_does_not_exist = read_terragrunt_config(\"${get_terragrunt_dir()}/i-dont-exist.hcl\", {data = \"default value\"})\n}\n\ninputs = local.config_does_not_exist\n"
  },
  {
    "path": "test/fixtures/read-config/with_dependency/dep/terragrunt.hcl",
    "content": "dependency \"inputs\" {\n  config_path = \"../../../inputs\"\n}\n"
  },
  {
    "path": "test/fixtures/read-config/with_dependency/terragrunt.hcl",
    "content": "locals {\n  config_with_dependency = read_terragrunt_config(\"${get_terragrunt_dir()}/dep/terragrunt.hcl\")\n}\n\nterraform {\n  source = \"${get_terragrunt_dir()}/../../inputs\"\n}\n\ninputs = local.config_with_dependency.dependency.inputs.outputs\n"
  },
  {
    "path": "test/fixtures/read-config/with_original_terragrunt_dir/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/read-config/with_original_terragrunt_dir/dep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/read-config/with_original_terragrunt_dir/dep/main.tf",
    "content": "variable \"terragrunt_dir\" {\n  type = string\n}\n\nvariable \"original_terragrunt_dir\" {\n  type = string\n}\n\nvariable \"bar_terragrunt_dir\" {\n  type = string\n}\n\nvariable \"bar_original_terragrunt_dir\" {\n  type = string\n}\n\noutput \"terragrunt_dir\" {\n  value = var.terragrunt_dir\n}\n\noutput \"original_terragrunt_dir\" {\n  value = var.original_terragrunt_dir\n}\n\noutput \"bar_terragrunt_dir\" {\n  value = var.bar_terragrunt_dir\n}\n\noutput \"bar_original_terragrunt_dir\" {\n  value = var.bar_original_terragrunt_dir\n}\n"
  },
  {
    "path": "test/fixtures/read-config/with_original_terragrunt_dir/dep/terragrunt.hcl",
    "content": "locals {\n  bar = read_terragrunt_config(\"../foo/bar.hcl\")\n}\n\ninputs = {\n  terragrunt_dir          = get_terragrunt_dir()\n  original_terragrunt_dir = get_original_terragrunt_dir()\n\n  bar_terragrunt_dir          = local.bar.locals.terragrunt_dir\n  bar_original_terragrunt_dir = local.bar.locals.original_terragrunt_dir\n}"
  },
  {
    "path": "test/fixtures/read-config/with_original_terragrunt_dir/foo/bar.hcl",
    "content": "locals {\n  terragrunt_dir          = get_terragrunt_dir()\n  original_terragrunt_dir = get_original_terragrunt_dir()\n}"
  },
  {
    "path": "test/fixtures/read-config/with_original_terragrunt_dir/main.tf",
    "content": "variable \"terragrunt_dir\" {\n  type = string\n}\n\nvariable \"original_terragrunt_dir\" {\n  type = string\n}\n\nvariable \"dep_terragrunt_dir\" {\n  type = string\n}\n\nvariable \"dep_original_terragrunt_dir\" {\n  type = string\n}\n\nvariable \"dep_bar_terragrunt_dir\" {\n  type = string\n}\n\nvariable \"dep_bar_original_terragrunt_dir\" {\n  type = string\n}\n\noutput \"terragrunt_dir\" {\n  value = var.terragrunt_dir\n}\n\noutput \"original_terragrunt_dir\" {\n  value = var.original_terragrunt_dir\n}\n\noutput \"dep_terragrunt_dir\" {\n  value = var.dep_terragrunt_dir\n}\n\noutput \"dep_original_terragrunt_dir\" {\n  value = var.dep_original_terragrunt_dir\n}\n\noutput \"dep_bar_terragrunt_dir\" {\n  value = var.dep_bar_terragrunt_dir\n}\n\noutput \"dep_bar_original_terragrunt_dir\" {\n  value = var.dep_bar_original_terragrunt_dir\n}\n"
  },
  {
    "path": "test/fixtures/read-config/with_original_terragrunt_dir/terragrunt.hcl",
    "content": "locals {\n  bar = read_terragrunt_config(\"foo/bar.hcl\")\n}\n\ndependency \"dep\" {\n  config_path = \"./dep\"\n}\n\ninputs = {\n  terragrunt_dir          = local.bar.locals.terragrunt_dir\n  original_terragrunt_dir = local.bar.locals.original_terragrunt_dir\n\n  dep_terragrunt_dir          = dependency.dep.outputs.terragrunt_dir\n  dep_original_terragrunt_dir = dependency.dep.outputs.original_terragrunt_dir\n\n  dep_bar_terragrunt_dir          = dependency.dep.outputs.bar_terragrunt_dir\n  dep_bar_original_terragrunt_dir = dependency.dep.outputs.bar_original_terragrunt_dir\n}"
  },
  {
    "path": "test/fixtures/read-tf-vars/empty.tfvars",
    "content": ""
  },
  {
    "path": "test/fixtures/read-tf-vars/my.tfvars",
    "content": "string_var = \"string\"\nnumber_var = 42\nbool_var = true\nlist_var = [\"hello\", \"world\"]"
  },
  {
    "path": "test/fixtures/read-tf-vars/my.tfvars.json",
    "content": "{\n    \"string_var\": \"another string\",\n    \"number_var\": 24,\n    \"bool_var\": false\n}"
  },
  {
    "path": "test/fixtures/read-tf-vars/only-comments.tfvars",
    "content": "# Line 1\n# Line 2"
  },
  {
    "path": "test/fixtures/read-tf-vars/terragrunt.hcl",
    "content": "locals {\n  vars         = jsondecode(read_tfvars_file(\"my.tfvars\"))\n  json_vars    = jsondecode(read_tfvars_file(\"my.tfvars.json\"))\n  empty_vars   = jsondecode(read_tfvars_file(\"empty.tfvars\"))\n  empty_vars_2 = jsondecode(read_tfvars_file(\"only-comments.tfvars\"))\n  string_var   = local.vars.string_var\n  bool_var     = local.vars.bool_var\n  number_var   = local.vars.number_var\n  list_var     = local.vars.list_var\n\n  json_string_var = local.json_vars.string_var\n  json_bool_var   = local.json_vars.bool_var\n  json_number_var = local.json_vars.number_var\n}\n"
  },
  {
    "path": "test/fixtures/regressions/5195-scope-escape/bastion/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/regressions/5195-scope-escape/bastion/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/regressions/5195-scope-escape/bastion/terragrunt.hcl",
    "content": "dependency \"shared\" {\n  config_path = \"../module2\"\n\n  # Mock outputs to avoid needing actual terraform state\n  mock_outputs = {\n    output_value = \"mock\"\n  }\n  mock_outputs_allowed_terraform_commands = [\"plan\", \"destroy\"]\n}\n"
  },
  {
    "path": "test/fixtures/regressions/5195-scope-escape/module1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/regressions/5195-scope-escape/module1/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/regressions/5195-scope-escape/module1/terragrunt.hcl",
    "content": "# This module depends on bastion - it should NOT be discovered when running from bastion/\ndependency \"bastion\" {\n  config_path = \"../bastion\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/5195-scope-escape/module2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/regressions/5195-scope-escape/module2/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/regressions/5195-scope-escape/module2/terragrunt.hcl",
    "content": "# Simple unit with no dependencies - should NOT be discovered when running from bastion/\n"
  },
  {
    "path": "test/fixtures/regressions/accesslogging-bucket/no-target-prefix-input/.gitignore",
    "content": "backend.tf\n"
  },
  {
    "path": "test/fixtures/regressions/accesslogging-bucket/no-target-prefix-input/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/regressions/accesslogging-bucket/no-target-prefix-input/main.tf",
    "content": "resource \"null_resource\" \"foo\" {}\n"
  },
  {
    "path": "test/fixtures/regressions/accesslogging-bucket/no-target-prefix-input/remote_terragrunt.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"terraform.tfstate\"\n    region = \"us-west-2\"\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n    enable_lock_table_ssencryption = true\n    accesslogging_bucket_name = \"__FILL_IN_LOGS_BUCKET_NAME__\"\n    bucket_sse_algorithm = \"AES256\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/regressions/accesslogging-bucket/with-target-prefix-input/.gitignore",
    "content": "backend.tf\n"
  },
  {
    "path": "test/fixtures/regressions/accesslogging-bucket/with-target-prefix-input/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/regressions/accesslogging-bucket/with-target-prefix-input/main.tf",
    "content": "resource \"null_resource\" \"foo\" {}\n"
  },
  {
    "path": "test/fixtures/regressions/accesslogging-bucket/with-target-prefix-input/remote_terragrunt.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"terraform.tfstate\"\n    region = \"us-west-2\"\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n    enable_lock_table_ssencryption = true\n    accesslogging_bucket_name = \"__FILL_IN_LOGS_BUCKET_NAME__\"\n    accesslogging_target_prefix = \"logs/\"\n    bucket_sse_algorithm = \"AES256\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/regressions/apply-all-envvar/module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/regressions/apply-all-envvar/module/main.tf",
    "content": "variable \"seed\" {}\noutput \"text\" {\n  value = \"Hello ${var.seed}\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/apply-all-envvar/no-require-envvar/terragrunt.hcl",
    "content": "terraform {\n  source = \"../module\"\n}\n\ninputs = {\n  seed = \"world\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/apply-all-envvar/require-envvar/terragrunt.hcl",
    "content": "terraform {\n  source = \"../module\"\n}\n\ninputs = {}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing/modules/dummy-module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing/modules/dummy-module/main.tf",
    "content": "resource \"null_resource\" \"null_resource_simple\" {\n  provisioner \"local-exec\" {\n    command = \"echo ${var.name}\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing/modules/dummy-module/outputs.tf",
    "content": "output \"name\" {\n  value = \"this-is-valuable\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing/modules/dummy-module/variables.tf",
    "content": "variable \"name\" {\n  type = string\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing/modules/dummy-module/versions.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing/production/dependency-group-template/app.hcl",
    "content": "locals {\n  current_dir = get_terragrunt_dir()\n  name        = basename(local.current_dir)\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing/production/dependency-group-template/webserver/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root-terragrunt.hcl\")\n}\n\nlocals {\n  environment_vars = read_terragrunt_config(find_in_parent_folders(\"environment.hcl\")).locals\n  app_vars         = read_terragrunt_config(find_in_parent_folders(\"app.hcl\")).locals\n}\n\nterraform {\n  source = \"${get_terragrunt_dir()}/modules/dummy-module\"\n}\n\ninputs = {\n  name = \"${local.environment_vars.environment}-${local.app_vars.name}\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing/production/deployment-group-1/app.hcl",
    "content": "locals {\n  current_dir = get_terragrunt_dir()\n  name        = basename(local.current_dir)\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing/production/deployment-group-1/webserver/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root-terragrunt.hcl\")\n}\n\nlocals {\n  environment_vars = read_terragrunt_config(find_in_parent_folders(\"environment.hcl\")).locals\n  app_vars         = read_terragrunt_config(find_in_parent_folders(\"app.hcl\")).locals\n}\n\n# Create dependencies to require PartialParseConfig to be fired multiple times\n# with a similar file content so that the caching is used.\n# The more similar dependencies, the more potential cache reuse.\n# If more dependency lookups are desired for benchmarking/testing,\n# create new symlinks from dependency-group-template/\n# and add the dependencies below.\ndependency \"dependency_group_1\" {\n  config_path = \"../../dependency-group-1/webserver\"\n  mock_outputs = {\n    name = \"mock-name\"\n  }\n}\n\ndependency \"dependency_group_2\" {\n  config_path = \"../../dependency-group-2/webserver\"\n  mock_outputs = {\n    name = \"mock-name\"\n  }\n}\n\ndependency \"dependency_group_3\" {\n  config_path = \"../../dependency-group-3/webserver\"\n  mock_outputs = {\n    name = \"mock-name\"\n  }\n}\n\ndependency \"dependency_group_4\" {\n  config_path = \"../../dependency-group-4/webserver\"\n  mock_outputs = {\n    name = \"mock-name\"\n  }\n}\n\nterraform {\n  source = \"${get_terragrunt_dir()}/modules/dummy-module\"\n}\n\ninputs = {\n  name = \"${local.environment_vars.environment}-${local.app_vars.name}\"\n\n  dependency_output = [\n    dependency.dependency_group_1.outputs.name,\n    dependency.dependency_group_2.outputs.name,\n    dependency.dependency_group_3.outputs.name,\n    dependency.dependency_group_4.outputs.name,\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing/production/environment.hcl",
    "content": "# Artificial file to create a 'worst' case scenario for PartialParseConfig caching\n# Bloat file and locals with lots of stuff to increase:\n# 1. Time required for parsing\n# 2. Increase key length used in the cache map\n# 3. Further exhaust resources by using built-in functions such as fileexists()\nlocals {\n  environment = basename(get_terragrunt_dir())\n  root_dir    = dirname(find_in_parent_folders(\"root-terragrunt.hcl\"))\n\n  processed_stuff = {\n    for k, v in local.tags :\n    k => v if !fileexists(\"${local.root_dir}/${k}/something.hcl\")\n  }\n\n  stuff = \"blubb\"\n  id         = \"62cbf4a6b1dcde4239eb93a9\"\n  index      = 0\n  guid       = \"9efe882c-d1fa-428d-8c36-8f57cc733ecb\"\n  isActive   = true\n  balance    = \"$3,263.61\"\n  picture    = \"http://placehold.it/32x32\"\n  age        = 35\n  eyeColor   = \"brown\"\n  name       = \"Mindy Bond\"\n  gender     = \"female\"\n  company    = \"FORTEAN\"\n  email      = \"mindybond@fortean.com\"\n  phone      = \"+1 (845) 577-3240\"\n  address    = \"901 Highland Place, Blanco, Iowa, 9253\"\n  about      = \"Et minim ut cupidatat enim aliquip culpa occaecat do labore. Ut commodo cillum occaecat enim nisi ipsum voluptate ex sit. Enim cupidatat pariatur nostrud dolore qui sunt eu incididunt magna exercitation labore quis enim. Eiusmod adipisicing tempor velit pariatur id aute. In anim magna do eiusmod consectetur dolore dolore excepteur sunt ullamco adipisicing ad do. Non incididunt irure excepteur incididunt cillum dolor magna consequat officia aute deserunt nostrud exercitation et.\\r\\n\"\n  registered = \"2017-09-12T10:25:58 -02:00\"\n  latitude   = -84.008483\n  longitude  = 146.467391\n  tags = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  friends = [\n    {\n      id   = 0\n      name = \"Mcdaniel Ball\"\n    },\n    {\n      id   = 1\n      name = \"Beatrice York\"\n    },\n    {\n      id   = 2\n      name = \"Deann Gonzalez\"\n    }\n  ]\n  cotton = [\n    false,\n    false,\n    true,\n    true,\n    false,\n    true,\n    false,\n    true,\n    false,\n    false,\n    true,\n    true,\n    false,\n    true,\n    false,\n    true\n  ]\n  roll     = true\n  identity = \"been\"\n  can      = \"safe\"\n  take     = \"event\"\n  beneath  = -1081532790.140633\n  gate     = \"lift\"\n  interest = {\n    city           = 1133683537\n    familiar       = true\n    desert         = 1350295690.2622042\n    carbon         = 401427978.6728978\n    tired          = true\n    straight       = \"gradually\"\n    characteristic = true\n    count          = true\n    map            = \"exciting\"\n    building       = \"good\"\n    fastened       = \"picture\"\n    slept          = 278532186.74346924\n    cent           = -1553983802.2229981\n    class          = \"move\"\n    log            = \"worse\"\n    bush           = -1906022707.3490677\n    information    = \"sold\"\n    scale          = false\n    easy           = 1369512070.179213\n  }\n  speed       = \"company\"\n  brave       = 716366682.125714\n  joy         = \"cut\"\n  twenty      = false\n  softly      = 1184770655\n  hurt        = false\n  angry       = false\n  watch       = \"search\"\n  never       = -118660654\n  letter      = \"continued\"\n  form        = true\n  by          = false\n  popular     = true\n  beat        = 2032654810.1097903\n  expect      = \"gave\"\n  nose        = true\n  combination = true\n  floating    = false\n\n  abc02 = \"abc02\"\n  abc03 = \"abc03\"\n  abc04 = \"abc04\"\n  abc05 = \"abc05\"\n  abc06 = \"abc06\"\n  abc07 = \"abc07\"\n  abc08 = \"abc08\"\n  abc09 = \"abc09\"\n  abc10 = \"abc10\"\n  abc11 = \"abc11\"\n  abc12 = \"abc12\"\n  abc13 = \"abc13\"\n  abc14 = \"abc14\"\n  abc15 = \"abc15\"\n  abc16 = \"abc16\"\n  abc17 = \"abc17\"\n  abc18 = \"abc18\"\n  abc19 = \"abc19\"\n  abc20 = \"abc20\"\n  abc21 = \"abc21\"\n  abc22 = \"abc22\"\n  abc23 = \"abc23\"\n  abc24 = \"abc24\"\n  abc25 = \"abc25\"\n  abc26 = \"abc26\"\n  abc27 = \"abc27\"\n  abc28 = \"abc28\"\n  abc29 = \"abc29\"\n\n  test01 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test02 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test03 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test04 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test05 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test06 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test07 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test08 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test09 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test10 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test11 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test12 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test13 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test14 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test15 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test16 = {\n    \"ad\"          = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xad\"\n    \"magna\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xmagna\"\n    \"irure\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xirure\"\n    \"sit\"         = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xsit\"\n    \"et\"          = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xet\"\n    \"labore\"      = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xlabore\"\n    \"adipisicing\" = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xadipisicing\"\n    \"abc1\"        = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc1\"\n    \"abc02\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc02\"\n    \"abc03\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc03\"\n    \"abc04\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc04\"\n    \"abc05\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc05\"\n    \"abc06\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc06\"\n    \"abc07\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc07\"\n    \"abc08\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc08\"\n    \"abc09\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc09\"\n    \"abc10\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc10\"\n    \"abc11\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc11\"\n    \"abc12\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc12\"\n    \"abc13\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc13\"\n    \"abc14\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc14\"\n    \"abc15\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc15\"\n    \"abc16\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc16\"\n    \"abc17\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc17\"\n    \"abc18\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc18\"\n    \"abc19\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc19\"\n    \"abc20\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc20\"\n    \"abc21\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc21\"\n    \"abc22\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc22\"\n    \"abc23\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc23\"\n    \"abc24\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc24\"\n    \"abc25\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc25\"\n    \"abc26\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc26\"\n    \"abc27\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc27\"\n    \"abc28\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc28\"\n    \"abc29\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc29\"\n    \"test-01\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-01\"\n    \"test-02\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-02\"\n    \"test-03\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-03\"\n    \"test-04\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-04\"\n    \"test-05\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-05\"\n    \"test-06\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-06\"\n    \"test-07\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-07\"\n    \"test-08\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-08\"\n    \"test-09\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-09\"\n    \"test-10\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-10\"\n    \"test-11\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-11\"\n    \"test-12\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-12\"\n    \"test-13\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-13\"\n    \"test-14\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-14\"\n    \"test-15\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-15\"\n    \"test-16\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-16\"\n    \"test-17\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-17\"\n    \"test-18\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-18\"\n    \"test-19\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-19\"\n    \"test-20\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-20\"\n  }\n  test17 = {\n    \"fsyhkudfzsykeufzyksuefzkyusefad\"          = \"ad\"\n    \"fsyhkudfzsykeufzyksuefzkyusefmagna\"       = \"magna\"\n    \"fsyhkudfzsykeufzyksuefzkyusefirure\"       = \"irure\"\n    \"fsyhkudfzsykeufzyksuefzkyusefsit\"         = \"sit\"\n    \"fsyhkudfzsykeufzyksuefzkyusefet\"          = \"et\"\n    \"fsyhkudfzsykeufzyksuefzkyuseflabore\"      = \"labore\"\n    \"fsyhkudfzsykeufzyksuefzkyusefadipisicing\" = \"adipisicing\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc1\"        = \"abc1\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc02\"       = \"abc02\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc03\"       = \"abc03\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc04\"       = \"abc04\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc05\"       = \"abc05\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc06\"       = \"abc06\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc07\"       = \"abc07\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc08\"       = \"abc08\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc09\"       = \"abc09\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc10\"       = \"abc10\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc11\"       = \"abc11\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc12\"       = \"abc12\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc13\"       = \"abc13\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc14\"       = \"abc14\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc15\"       = \"abc15\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc16\"       = \"abc16\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc17\"       = \"abc17\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc18\"       = \"abc18\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc19\"       = \"abc19\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc20\"       = \"abc20\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc21\"       = \"abc21\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc22\"       = \"abc22\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc23\"       = \"abc23\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc24\"       = \"abc24\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc25\"       = \"abc25\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc26\"       = \"abc26\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc27\"       = \"abc27\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc28\"       = \"abc28\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc29\"       = \"abc29\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-01\"     = \"test-01\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-02\"     = \"test-02\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-03\"     = \"test-03\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-04\"     = \"test-04\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-05\"     = \"test-05\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-06\"     = \"test-06\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-07\"     = \"test-07\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-08\"     = \"test-08\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-09\"     = \"test-09\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-10\"     = \"test-10\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-11\"     = \"test-11\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-12\"     = \"test-12\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-13\"     = \"test-13\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-14\"     = \"test-14\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-15\"     = \"test-15\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-16\"     = \"test-16\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-17\"     = \"test-17\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-18\"     = \"test-18\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-19\"     = \"test-19\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-20\"     = \"test-20\"\n  }\n  test18 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test19 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test20 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing/root-terragrunt.hcl",
    "content": "locals {\n  environment_vars = read_terragrunt_config(find_in_parent_folders(\"environment.hcl\")).locals\n}\n\nremote_state {\n  backend = \"local\"\n  config = {\n    path = \"${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing-includes/modules/dummy-module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing-includes/modules/dummy-module/main.tf",
    "content": "resource \"null_resource\" \"null_resource_simple\" {\n  provisioner \"local-exec\" {\n    command = \"echo ${var.name}\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing-includes/modules/dummy-module/outputs.tf",
    "content": "output \"name\" {\n  value = \"this-is-valuable\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing-includes/modules/dummy-module/variables.tf",
    "content": "variable \"name\" {\n  type = string\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing-includes/modules/dummy-module/versions.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing-includes/production/dependency-group-template/app.hcl",
    "content": "locals {\n  current_dir = get_terragrunt_dir()\n  name        = basename(local.current_dir)\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing-includes/production/dependency-group-template/webserver/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root-terragrunt.hcl\")\n}\n\ninclude \"environment_vars\" {\n  path   = find_in_parent_folders(\"environment.hcl\")\n  expose = true\n}\n\ninclude \"app_vars\" {\n  path   = find_in_parent_folders(\"app.hcl\")\n  expose = true\n}\n\nterraform {\n  source = \"${get_terragrunt_dir()}/modules/dummy-module\"\n}\n\ninputs = {\n  name = \"${include.environment_vars.locals.environment}-${include.app_vars.locals.name}\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing-includes/production/deployment-group-1/app.hcl",
    "content": "locals {\n  current_dir = get_terragrunt_dir()\n  name        = basename(local.current_dir)\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing-includes/production/deployment-group-1/webserver/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root-terragrunt.hcl\")\n}\n\ninclude \"environment_vars\" {\n  path   = find_in_parent_folders(\"environment.hcl\")\n  expose = true\n}\n\ninclude \"app_vars\" {\n  path   = find_in_parent_folders(\"app.hcl\")\n  expose = true\n}\n\n# Create dependencies to require PartialParseConfig to be fired multiple times\n# with a similar file content so that the caching is used.\n# The more similar dependencies, the more potential cache reuse.\n# If more dependency lookups are desired for benchmarking/testing,\n# create new symlinks from dependency-group-template/\n# and add the dependencies below.\ndependency \"dependency_group_1\" {\n  config_path = \"../../dependency-group-1/webserver\"\n  mock_outputs = {\n    name = \"mock-name\"\n  }\n}\n\ndependency \"dependency_group_2\" {\n  config_path = \"../../dependency-group-2/webserver\"\n  mock_outputs = {\n    name = \"mock-name\"\n  }\n}\n\ndependency \"dependency_group_3\" {\n  config_path = \"../../dependency-group-3/webserver\"\n  mock_outputs = {\n    name = \"mock-name\"\n  }\n}\n\ndependency \"dependency_group_4\" {\n  config_path = \"../../dependency-group-4/webserver\"\n  mock_outputs = {\n    name = \"mock-name\"\n  }\n}\n\nterraform {\n  source = \"${get_terragrunt_dir()}/modules/dummy-module\"\n}\n\ninputs = {\n  name = \"${include.environment_vars.locals.environment}-${include.app_vars.locals.name}\"\n\n  dependency_output = [\n    dependency.dependency_group_1.outputs.name,\n    dependency.dependency_group_2.outputs.name,\n    dependency.dependency_group_3.outputs.name,\n    dependency.dependency_group_4.outputs.name,\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing-includes/production/environment.hcl",
    "content": "# Artificial file to create a 'worst' case scenario for PartialParseConfig caching\n# Bloat file and locals with lots of stuff to increase:\n# 1. Time required for parsing\n# 2. Increase key length used in the cache map\n# 3. Further exhaust resources by using built-in functions such as fileexists()\nlocals {\n  environment = basename(get_terragrunt_dir())\n  root_dir    = dirname(find_in_parent_folders(\"root-terragrunt.hcl\"))\n\n  processed_stuff = {\n    for k, v in local.tags :\n    k => v if !fileexists(\"${local.root_dir}/${k}/something.hcl\")\n  }\n\n  stuff = \"blubb\"\n  id         = \"62cbf4a6b1dcde4239eb93a9\"\n  index      = 0\n  guid       = \"9efe882c-d1fa-428d-8c36-8f57cc733ecb\"\n  isActive   = true\n  balance    = \"$3,263.61\"\n  picture    = \"http://placehold.it/32x32\"\n  age        = 35\n  eyeColor   = \"brown\"\n  name       = \"Mindy Bond\"\n  gender     = \"female\"\n  company    = \"FORTEAN\"\n  email      = \"mindybond@fortean.com\"\n  phone      = \"+1 (845) 577-3240\"\n  address    = \"901 Highland Place, Blanco, Iowa, 9253\"\n  about      = \"Et minim ut cupidatat enim aliquip culpa occaecat do labore. Ut commodo cillum occaecat enim nisi ipsum voluptate ex sit. Enim cupidatat pariatur nostrud dolore qui sunt eu incididunt magna exercitation labore quis enim. Eiusmod adipisicing tempor velit pariatur id aute. In anim magna do eiusmod consectetur dolore dolore excepteur sunt ullamco adipisicing ad do. Non incididunt irure excepteur incididunt cillum dolor magna consequat officia aute deserunt nostrud exercitation et.\\r\\n\"\n  registered = \"2017-09-12T10:25:58 -02:00\"\n  latitude   = -84.008483\n  longitude  = 146.467391\n  tags = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  friends = [\n    {\n      id   = 0\n      name = \"Mcdaniel Ball\"\n    },\n    {\n      id   = 1\n      name = \"Beatrice York\"\n    },\n    {\n      id   = 2\n      name = \"Deann Gonzalez\"\n    }\n  ]\n  cotton = [\n    false,\n    false,\n    true,\n    true,\n    false,\n    true,\n    false,\n    true,\n    false,\n    false,\n    true,\n    true,\n    false,\n    true,\n    false,\n    true\n  ]\n  roll     = true\n  identity = \"been\"\n  can      = \"safe\"\n  take     = \"event\"\n  beneath  = -1081532790.140633\n  gate     = \"lift\"\n  interest = {\n    city           = 1133683537\n    familiar       = true\n    desert         = 1350295690.2622042\n    carbon         = 401427978.6728978\n    tired          = true\n    straight       = \"gradually\"\n    characteristic = true\n    count          = true\n    map            = \"exciting\"\n    building       = \"good\"\n    fastened       = \"picture\"\n    slept          = 278532186.74346924\n    cent           = -1553983802.2229981\n    class          = \"move\"\n    log            = \"worse\"\n    bush           = -1906022707.3490677\n    information    = \"sold\"\n    scale          = false\n    easy           = 1369512070.179213\n  }\n  speed       = \"company\"\n  brave       = 716366682.125714\n  joy         = \"cut\"\n  twenty      = false\n  softly      = 1184770655\n  hurt        = false\n  angry       = false\n  watch       = \"search\"\n  never       = -118660654\n  letter      = \"continued\"\n  form        = true\n  by          = false\n  popular     = true\n  beat        = 2032654810.1097903\n  expect      = \"gave\"\n  nose        = true\n  combination = true\n  floating    = false\n\n  abc02 = \"abc02\"\n  abc03 = \"abc03\"\n  abc04 = \"abc04\"\n  abc05 = \"abc05\"\n  abc06 = \"abc06\"\n  abc07 = \"abc07\"\n  abc08 = \"abc08\"\n  abc09 = \"abc09\"\n  abc10 = \"abc10\"\n  abc11 = \"abc11\"\n  abc12 = \"abc12\"\n  abc13 = \"abc13\"\n  abc14 = \"abc14\"\n  abc15 = \"abc15\"\n  abc16 = \"abc16\"\n  abc17 = \"abc17\"\n  abc18 = \"abc18\"\n  abc19 = \"abc19\"\n  abc20 = \"abc20\"\n  abc21 = \"abc21\"\n  abc22 = \"abc22\"\n  abc23 = \"abc23\"\n  abc24 = \"abc24\"\n  abc25 = \"abc25\"\n  abc26 = \"abc26\"\n  abc27 = \"abc27\"\n  abc28 = \"abc28\"\n  abc29 = \"abc29\"\n\n  test01 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test02 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test03 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test04 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test05 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test06 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test07 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test08 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test09 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test10 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test11 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test12 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test13 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test14 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test15 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test16 = {\n    \"ad\"          = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xad\"\n    \"magna\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xmagna\"\n    \"irure\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xirure\"\n    \"sit\"         = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xsit\"\n    \"et\"          = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xet\"\n    \"labore\"      = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xlabore\"\n    \"adipisicing\" = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xadipisicing\"\n    \"abc1\"        = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc1\"\n    \"abc02\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc02\"\n    \"abc03\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc03\"\n    \"abc04\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc04\"\n    \"abc05\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc05\"\n    \"abc06\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc06\"\n    \"abc07\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc07\"\n    \"abc08\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc08\"\n    \"abc09\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc09\"\n    \"abc10\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc10\"\n    \"abc11\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc11\"\n    \"abc12\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc12\"\n    \"abc13\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc13\"\n    \"abc14\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc14\"\n    \"abc15\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc15\"\n    \"abc16\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc16\"\n    \"abc17\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc17\"\n    \"abc18\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc18\"\n    \"abc19\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc19\"\n    \"abc20\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc20\"\n    \"abc21\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc21\"\n    \"abc22\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc22\"\n    \"abc23\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc23\"\n    \"abc24\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc24\"\n    \"abc25\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc25\"\n    \"abc26\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc26\"\n    \"abc27\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc27\"\n    \"abc28\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc28\"\n    \"abc29\"       = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xabc29\"\n    \"test-01\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-01\"\n    \"test-02\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-02\"\n    \"test-03\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-03\"\n    \"test-04\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-04\"\n    \"test-05\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-05\"\n    \"test-06\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-06\"\n    \"test-07\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-07\"\n    \"test-08\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-08\"\n    \"test-09\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-09\"\n    \"test-10\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-10\"\n    \"test-11\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-11\"\n    \"test-12\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-12\"\n    \"test-13\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-13\"\n    \"test-14\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-14\"\n    \"test-15\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-15\"\n    \"test-16\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-16\"\n    \"test-17\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-17\"\n    \"test-18\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-18\"\n    \"test-19\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-19\"\n    \"test-20\"     = \"asekrbgys3ke754y3458nbgks4e58xksd4g7xtest-20\"\n  }\n  test17 = {\n    \"fsyhkudfzsykeufzyksuefzkyusefad\"          = \"ad\"\n    \"fsyhkudfzsykeufzyksuefzkyusefmagna\"       = \"magna\"\n    \"fsyhkudfzsykeufzyksuefzkyusefirure\"       = \"irure\"\n    \"fsyhkudfzsykeufzyksuefzkyusefsit\"         = \"sit\"\n    \"fsyhkudfzsykeufzyksuefzkyusefet\"          = \"et\"\n    \"fsyhkudfzsykeufzyksuefzkyuseflabore\"      = \"labore\"\n    \"fsyhkudfzsykeufzyksuefzkyusefadipisicing\" = \"adipisicing\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc1\"        = \"abc1\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc02\"       = \"abc02\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc03\"       = \"abc03\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc04\"       = \"abc04\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc05\"       = \"abc05\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc06\"       = \"abc06\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc07\"       = \"abc07\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc08\"       = \"abc08\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc09\"       = \"abc09\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc10\"       = \"abc10\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc11\"       = \"abc11\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc12\"       = \"abc12\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc13\"       = \"abc13\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc14\"       = \"abc14\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc15\"       = \"abc15\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc16\"       = \"abc16\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc17\"       = \"abc17\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc18\"       = \"abc18\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc19\"       = \"abc19\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc20\"       = \"abc20\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc21\"       = \"abc21\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc22\"       = \"abc22\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc23\"       = \"abc23\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc24\"       = \"abc24\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc25\"       = \"abc25\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc26\"       = \"abc26\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc27\"       = \"abc27\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc28\"       = \"abc28\"\n    \"fsyhkudfzsykeufzyksuefzkyusefabc29\"       = \"abc29\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-01\"     = \"test-01\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-02\"     = \"test-02\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-03\"     = \"test-03\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-04\"     = \"test-04\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-05\"     = \"test-05\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-06\"     = \"test-06\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-07\"     = \"test-07\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-08\"     = \"test-08\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-09\"     = \"test-09\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-10\"     = \"test-10\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-11\"     = \"test-11\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-12\"     = \"test-12\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-13\"     = \"test-13\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-14\"     = \"test-14\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-15\"     = \"test-15\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-16\"     = \"test-16\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-17\"     = \"test-17\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-18\"     = \"test-18\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-19\"     = \"test-19\"\n    \"fsyhkudfzsykeufzyksuefzkyuseftest-20\"     = \"test-20\"\n  }\n  test18 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test19 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n  test20 = {\n    \"ad\"          = \"ad\"\n    \"magna\"       = \"magna\"\n    \"irure\"       = \"irure\"\n    \"sit\"         = \"sit\"\n    \"et\"          = \"et\"\n    \"labore\"      = \"labore\"\n    \"adipisicing\" = \"adipisicing\"\n    \"abc1\"        = \"abc1\"\n    \"abc02\"       = \"abc02\"\n    \"abc03\"       = \"abc03\"\n    \"abc04\"       = \"abc04\"\n    \"abc05\"       = \"abc05\"\n    \"abc06\"       = \"abc06\"\n    \"abc07\"       = \"abc07\"\n    \"abc08\"       = \"abc08\"\n    \"abc09\"       = \"abc09\"\n    \"abc10\"       = \"abc10\"\n    \"abc11\"       = \"abc11\"\n    \"abc12\"       = \"abc12\"\n    \"abc13\"       = \"abc13\"\n    \"abc14\"       = \"abc14\"\n    \"abc15\"       = \"abc15\"\n    \"abc16\"       = \"abc16\"\n    \"abc17\"       = \"abc17\"\n    \"abc18\"       = \"abc18\"\n    \"abc19\"       = \"abc19\"\n    \"abc20\"       = \"abc20\"\n    \"abc21\"       = \"abc21\"\n    \"abc22\"       = \"abc22\"\n    \"abc23\"       = \"abc23\"\n    \"abc24\"       = \"abc24\"\n    \"abc25\"       = \"abc25\"\n    \"abc26\"       = \"abc26\"\n    \"abc27\"       = \"abc27\"\n    \"abc28\"       = \"abc28\"\n    \"abc29\"       = \"abc29\"\n    \"test-01\"     = \"test-01\"\n    \"test-02\"     = \"test-02\"\n    \"test-03\"     = \"test-03\"\n    \"test-04\"     = \"test-04\"\n    \"test-05\"     = \"test-05\"\n    \"test-06\"     = \"test-06\"\n    \"test-07\"     = \"test-07\"\n    \"test-08\"     = \"test-08\"\n    \"test-09\"     = \"test-09\"\n    \"test-10\"     = \"test-10\"\n    \"test-11\"     = \"test-11\"\n    \"test-12\"     = \"test-12\"\n    \"test-13\"     = \"test-13\"\n    \"test-14\"     = \"test-14\"\n    \"test-15\"     = \"test-15\"\n    \"test-16\"     = \"test-16\"\n    \"test-17\"     = \"test-17\"\n    \"test-18\"     = \"test-18\"\n    \"test-19\"     = \"test-19\"\n    \"test-20\"     = \"test-20\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/regressions/benchmark-parsing-includes/root-terragrunt.hcl",
    "content": "remote_state {\n  backend = \"local\"\n  config = {\n    path = \"${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/regressions/dependency-empty-config-path/_source/units/consumer/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/regressions/dependency-empty-config-path/_source/units/consumer/main.tf",
    "content": "terraform {}\n\n\n"
  },
  {
    "path": "test/fixtures/regressions/dependency-empty-config-path/_source/units/consumer/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\ndependency \"ecr_cache\" {\n  config_path = try(values.dependency_path.ecr-cache, \"\")\n  # force enabled so error manifests even when empty\n  enabled     = true\n\n  mock_outputs = {\n    token = \"mock-token\"\n  }\n\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n  mock_outputs_allowed_terraform_commands = [\"init\", \"validate\", \"destroy\", \"plan\"]\n}\n\ninputs = {\n  token = dependency.ecr_cache.outputs.token\n}\n\n\n"
  },
  {
    "path": "test/fixtures/regressions/dependency-empty-config-path/live/terragrunt.stack.hcl",
    "content": "\nunit \"consumer\" {\n  path   = \"consumer\"\n  source = \"${get_repo_root()}/_source/units/consumer\"\n\n  values = {\n  }\n}\n\n\n"
  },
  {
    "path": "test/fixtures/regressions/dependency-generate/modules/other-module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/regressions/dependency-generate/modules/other-module/main.tf",
    "content": "variable \"test_value\" {\n  type = string\n}\n\noutput \"secrets\" {\n  value = {\n    test_provider_token = var.test_value\n  }\n  sensitive = true\n}\n"
  },
  {
    "path": "test/fixtures/regressions/dependency-generate/modules/test-module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/regressions/dependency-generate/modules/test-module/main.tf",
    "content": "variable \"token_via_input\" {\n  type = string\n}\n\nresource \"local_file\" \"test\" {\n  filename = \"./test-output.txt\"\n  content  = \"Token via input: ${var.token_via_input}\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/dependency-generate/other/terragrunt.hcl",
    "content": "terraform {\n  source = \"../modules/other-module\"\n}\n\ninputs = {\n  test_value = \"test-token-12345\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/dependency-generate/testing/terragrunt.hcl",
    "content": "terraform {\n  source = \"../modules/test-module\"\n}\n\ndependencies {\n  paths = [\"../other\"]\n}\n\ndependency \"other\" {\n  config_path = \"../other\"\n}\n\ngenerate \"provider_test\" {\n  path      = \"provider_test.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents  = <<EOF\n# Generated provider config with token from dependency\n# Token: ${dependency.other.outputs.secrets.test_provider_token}\nEOF\n}\n\ninputs = {\n  # This should work even in broken version\n  token_via_input = dependency.other.outputs.secrets.test_provider_token\n}\n"
  },
  {
    "path": "test/fixtures/regressions/dependency-include-error/dep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/regressions/dependency-include-error/dep/main.tf",
    "content": "output \"value\" {\n  value = \"dep_output_value\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/dependency-include-error/dep/terragrunt.hcl",
    "content": "# Dependency module\nterraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/dependency-include-error/layer.hcl",
    "content": "# Layer config with dependency block and inputs referencing dependency outputs\n# This triggers the false positive error when parsed via include directive\n\ndependency \"dep\" {\n  config_path = \"${get_terragrunt_dir()}/../dep\"\n}\n\ninputs = {\n  # These lines reference dependency.dep.outputs which haven't been resolved yet\n  # during include parsing, causing false positive \"Unknown variable\" errors\n  dep_output_value = dependency.dep.outputs.value\n}\n"
  },
  {
    "path": "test/fixtures/regressions/dependency-include-error/root.hcl",
    "content": "# Root config that reads layer.hcl via read_terragrunt_config\n# This tests that read_terragrunt_config properly suppresses diagnostics\n\nterraform {\n  source = \".\"\n}\n\nlocals {\n  # This read_terragrunt_config call should have diagnostics suppressed\n  layer_config = try(read_terragrunt_config(find_in_parent_folders(\"layer.hcl\")), { locals = {} })\n}\n\ninputs = {\n  root_value = \"from_root\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/dependency-include-error/unit/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/regressions/dependency-include-error/unit/main.tf",
    "content": "variable \"root_value\" {\n  type    = string\n  default = \"\"\n}\n\nvariable \"dep_output_value\" {\n  type    = string\n  default = \"\"\n}\n\noutput \"combined\" {\n  value = \"${var.root_value}-${var.dep_output_value}\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/dependency-include-error/unit/terragrunt.hcl",
    "content": "# Unit that includes both root.hcl and layer.hcl\n# This reproduces issue #5169 where include directive parsing\n# shows false positive \"Unknown variable\" errors\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ninclude \"layer\" {\n  path = find_in_parent_folders(\"layer.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/regressions/disabled-dependency-empty-config-path/modules/id/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/random\" {\n  version = \"3.8.0\"\n  hashes = [\n    \"h1:aEaTEHutDdKNaztKFmInhfzmZK0/OaVL8uxmncM9YF8=\",\n    \"h1:ey4eBIHiuAC5xsblxtXghXE3nWwUvGqTT6KAsggiAwo=\",\n    \"h1:nRPdhXsZpGPMppuUgBe/ZcAtD73NaCLGROYHXv41qz8=\",\n    \"zh:2d5e0bbfac7f15595739fe54a9ab8b8eea92fd6d879706139dad7ecaa5c01c19\",\n    \"zh:349e637066625d97aaa84db1b1418c86d6457cf9c5a62f6dcc3f55cbd535112c\",\n    \"zh:5f4456d53f5256ccfdb87dd35d3bf34578d01bd9b71cffaf507f0692805eac8a\",\n    \"zh:6c1ecfacc5f7079a068d7f8eb8924485d4ec8183f36e6318a6e748d35921ddac\",\n    \"zh:6d86641edeb8c394f121f7b0a691d72f89cf9b938b987a01fc32aad396a50555\",\n    \"zh:76947bd7bc7033b33980538da149c94e386f9b0abb2ce63733f25a57517e4742\",\n    \"zh:79c07f4c8b3a63d9f89e25e4348b462c57e179bca66ba533710851c485e282db\",\n    \"zh:ac1c2b941d994728a3a93aba093fd2202f9311d099ff85f66678897c792161ba\",\n    \"zh:cbb2aa867fd828fcb4125239e00862b9a3bc2f280e945c760224276b476f4c49\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/regressions/disabled-dependency-empty-config-path/modules/id/main.tf",
    "content": "variable \"prefix\" {\n  type    = string\n  default = \"\"\n}\n\nvariable \"suffix\" {\n  type    = string\n  default = \"\"\n}\n\nvariable \"separator\" {\n  type    = string\n  default = \"-\"\n}\n\nresource \"random_string\" \"this\" {\n  length  = 4\n  upper   = false\n  special = false\n}\n\noutput \"random_string\" {\n  value = random_string.this.result\n}\n\noutput \"id\" {\n  value = format(\n    \"%s%s%s\",\n    (var.prefix == \"\" ? \"\" : format(\"%s%s\", trimsuffix(var.prefix, var.separator), var.separator)),\n    random_string.this.result,\n    (var.suffix == \"\" ? \"\" : format(\"%s%s\", var.separator, trimprefix(var.suffix, var.separator))),\n  )\n}\n"
  },
  {
    "path": "test/fixtures/regressions/disabled-dependency-empty-config-path/root.hcl",
    "content": "# Root configuration file\n"
  },
  {
    "path": "test/fixtures/regressions/disabled-dependency-empty-config-path/unit-a/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../modules/id\"\n}\n\ninputs = {}\n"
  },
  {
    "path": "test/fixtures/regressions/disabled-dependency-empty-config-path/unit-b/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../modules/id\"\n}\n\nlocals {\n  # Test case: disabled dependency with empty config_path\n  # This should NOT cause cycle errors - the empty path should be ignored\n  # because the dependency is disabled\n  unit_a_path = \"\"\n}\n\ndependency \"unit_a\" {\n  config_path = try(local.unit_a_path, \"\")\n\n  enabled = false\n\n  mock_outputs = {\n    random_string = \"\"\n  }\n\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n  mock_outputs_allowed_terraform_commands = [\"init\", \"validate\", \"destroy\"]\n}\n\ninputs = {\n  suffix = try(dependency.unit_a.outputs.random_string, \"\")\n}\n"
  },
  {
    "path": "test/fixtures/regressions/exclude-dependency/amazing-app/k8s/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${get_terragrunt_dir()}/../../modules/k8s\"\n}\n\ndependency \"eks\" {\n  config_path  = \"${get_terragrunt_dir()}/../../clusters/eks\"\n  skip_outputs = true\n  mock_outputs = {\n    random_string = \"foo\"\n  }\n}\n\ninputs = {\n  cluster = dependency.eks.outputs.random_string\n}\n"
  },
  {
    "path": "test/fixtures/regressions/exclude-dependency/clusters/eks/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${get_terragrunt_dir()}/../../modules/eks\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/exclude-dependency/modules/eks/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/random\" {\n  version = \"3.8.0\"\n  hashes = [\n    \"h1:aEaTEHutDdKNaztKFmInhfzmZK0/OaVL8uxmncM9YF8=\",\n    \"h1:ey4eBIHiuAC5xsblxtXghXE3nWwUvGqTT6KAsggiAwo=\",\n    \"h1:nRPdhXsZpGPMppuUgBe/ZcAtD73NaCLGROYHXv41qz8=\",\n    \"zh:2d5e0bbfac7f15595739fe54a9ab8b8eea92fd6d879706139dad7ecaa5c01c19\",\n    \"zh:349e637066625d97aaa84db1b1418c86d6457cf9c5a62f6dcc3f55cbd535112c\",\n    \"zh:5f4456d53f5256ccfdb87dd35d3bf34578d01bd9b71cffaf507f0692805eac8a\",\n    \"zh:6c1ecfacc5f7079a068d7f8eb8924485d4ec8183f36e6318a6e748d35921ddac\",\n    \"zh:6d86641edeb8c394f121f7b0a691d72f89cf9b938b987a01fc32aad396a50555\",\n    \"zh:76947bd7bc7033b33980538da149c94e386f9b0abb2ce63733f25a57517e4742\",\n    \"zh:79c07f4c8b3a63d9f89e25e4348b462c57e179bca66ba533710851c485e282db\",\n    \"zh:ac1c2b941d994728a3a93aba093fd2202f9311d099ff85f66678897c792161ba\",\n    \"zh:cbb2aa867fd828fcb4125239e00862b9a3bc2f280e945c760224276b476f4c49\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/regressions/exclude-dependency/modules/eks/main.tf",
    "content": "resource \"random_string\" \"random\" {\n  length = 16\n}\n\noutput \"random_string\" {\n  value = random_string.random.result\n}\n"
  },
  {
    "path": "test/fixtures/regressions/exclude-dependency/modules/k8s/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/regressions/exclude-dependency/modules/k8s/main.tf",
    "content": "variable \"cluster\" {}\noutput \"cluster\" { value = var.cluster }\n"
  },
  {
    "path": "test/fixtures/regressions/exclude-dependency/root.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/regressions/exclude-dependency/testapp/k8s/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${get_terragrunt_dir()}/../../modules/k8s\"\n}\n\ndependency \"eks\" {\n  config_path = \"${get_terragrunt_dir()}/../../clusters/eks\"\n  skip_outputs = true\n  mock_outputs = {\n    random_string = \"foo\"\n  }\n}\n\ninputs = {\n  cluster = dependency.eks.outputs.random_string\n}\n"
  },
  {
    "path": "test/fixtures/regressions/include-error/_envcommon.hcl",
    "content": "\ninputs = {\n  common_config = \"Common Config\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/include-error/project/app/terragrunt.hcl",
    "content": "\ninclude {\n  path = find_in_parent_folders(\"eng_teams.hcl\")\n}\n\ninclude {\n  path = find_in_parent_folders(\"_envcommon.hcl\")\n}\n\ninputs = {\n  app = {\n    \"kind\"      = \"deployment\",\n    \"namespace\" = \"foobar\",\n    \"slug\"      = \"bar-foo\",\n    \"data\"      = \"46521694\",\n  }\n}\n"
  },
  {
    "path": "test/fixtures/regressions/include-error/project/eng_teams.hcl",
    "content": "\ninputs = {\n  team_name = \"Test Team\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/mocks-merge-with-state/deep-map/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/regressions/mocks-merge-with-state/deep-map/main.tf",
    "content": "output \"check_field\" {\n  value = \"deep-map-executed\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/mocks-merge-with-state/deep-map/terragrunt.hcl",
    "content": "dependency \"module\" {\n  config_path = \"../module\"\n  mock_outputs_merge_strategy_with_state = \"deep_map_only\"\n}\n\ninputs = {\n  field = dependency.module.outputs.data.field\n}\n"
  },
  {
    "path": "test/fixtures/regressions/mocks-merge-with-state/module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/regressions/mocks-merge-with-state/module/main.tf",
    "content": "output \"data\" {\n  value = {\n    field = \"field value\"\n  }\n}\n\noutput \"check_field\" {\n  value = \"module-executed\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/mocks-merge-with-state/module/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/regressions/mocks-merge-with-state/shallow/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/regressions/mocks-merge-with-state/shallow/main.tf",
    "content": "output \"check_field\" {\n  value = \"shallow-map-executed\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/mocks-merge-with-state/shallow/terragrunt.hcl",
    "content": "dependency \"module\" {\n  config_path = \"../module\"\n  mock_outputs_merge_strategy_with_state = \"shallow\"\n}\n\ninputs = {\n  field = dependency.module.outputs.data.field\n}\n"
  },
  {
    "path": "test/fixtures/regressions/multiple-dependency-load-sync/dep1/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root-terragrunt.hcl\")\n}\n\ninputs = {\n  name = \"dep1\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/multiple-dependency-load-sync/dep2/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root-terragrunt.hcl\")\n}\n\ninputs = {\n  name = \"dep2\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/multiple-dependency-load-sync/main/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root-terragrunt.hcl\")\n}\n\ndependency \"dependency_1\" {\n  config_path = \"../dep1\"\n  skip_outputs = true\n  mock_outputs = {\n    name = \"dummy\"\n  }\n}\n\ndependency \"dependency_2\" {\n  config_path = \"../dep2\"\n  skip_outputs = true\n  mock_outputs = {\n    name = \"dummy\"\n  }\n}\n\ninputs = {\n  name = format(\n    \"%s:%s\",\n    dependency.dependency_1.outputs.name,\n    dependency.dependency_2.outputs.name,\n  )\n}\n"
  },
  {
    "path": "test/fixtures/regressions/multiple-dependency-load-sync/modules/dummy-module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/regressions/multiple-dependency-load-sync/modules/dummy-module/main.tf",
    "content": "terraform {\n  backend \"local\" {\n  }\n}\nresource \"null_resource\" \"null_resource_simple\" {\n  provisioner \"local-exec\" {\n    command = \"echo ${var.name}\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/regressions/multiple-dependency-load-sync/modules/dummy-module/outputs.tf",
    "content": "output \"name\" {\n  value = var.name\n}\n"
  },
  {
    "path": "test/fixtures/regressions/multiple-dependency-load-sync/modules/dummy-module/variables.tf",
    "content": "variable \"name\" {\n  type = string\n}\n"
  },
  {
    "path": "test/fixtures/regressions/multiple-dependency-load-sync/modules/dummy-module/versions.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/regressions/multiple-dependency-load-sync/root-terragrunt.hcl",
    "content": "remote_state {\n  backend = \"local\"\n  config = {\n    path = \"${path_relative_to_include()}/terraform.tfstate\"\n  }\n}\n\nterraform {\n  source = \"../modules/dummy-module\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/multiple-stacks/live/appv2.terragrunt.stack.hcl",
    "content": "\nunit \"unit3\" {\n  source = \"../units/template\"\n  path   = \"unit3\"\n}\n\nunit \"unit4\" {\n  source = \"../units/template\"\n  path   = \"unit4\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/multiple-stacks/live/terragrunt.stack.hcl",
    "content": "unit \"unit1\" {\n  source = \"../units/template\"\n  path   = \"unit1\"\n}\n\nunit \"unit2\" {\n  source = \"../units/template\"\n  path   = \"unit2\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/multiple-stacks/units/template/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/regressions/multiple-stacks/units/template/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/regressions/multiple-stacks/units/template/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/regressions/not-existing-dependency/invalid-path/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/regressions/not-existing-dependency/invalid-path/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/regressions/not-existing-dependency/invalid-path/terragrunt.hcl",
    "content": "\ndependency \"dep_123\" {\n  config_path = 123\n}\n"
  },
  {
    "path": "test/fixtures/regressions/not-existing-dependency/parent-find-fail/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/regressions/not-existing-dependency/parent-find-fail/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/regressions/not-existing-dependency/parent-find-fail/terragrunt.hcl",
    "content": "dependency \"organisation\" {\n  config_path =  find_in_parent_folders(\"wrong-dir-name\")\n}\n\n"
  },
  {
    "path": "test/fixtures/regressions/parsing-run-all-with-generate/root.hcl",
    "content": "locals {\n  aws_provider_version = \"6.15.0\"\n}\n\n# Generate an AWS provider block\ngenerate \"provider\" {\n  path      = \"provider.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents  = <<EOF\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\nEOF\n}\n\ngenerate \"versions_override\" {\n  path      = \"versions_override.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents  = <<EOF\nterraform {\n  required_providers {\n    aws = {\n      source  = \"hashicorp/aws\"\n      version = \"${local.aws_provider_version}\"\n    }\n  }\n}\nEOF\n}\n"
  },
  {
    "path": "test/fixtures/regressions/parsing-run-all-with-generate/services/test1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/regressions/parsing-run-all-with-generate/services/test1/main.tf",
    "content": "variable \"name\" {\n  type = string\n}\n\noutput \"service_name\" {\n  value = var.name\n}\n\noutput \"service_desired_count_initial_value\" {\n  value = 1\n}\n\noutput \"docker_images\" {\n  value = [\n    {\n      repository = \"123456789012.dkr.ecr.us-east-1.amazonaws.com/${var.name}-FAKE\"\n    }\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/regressions/parsing-run-all-with-generate/services/test1/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n  expose = true\n}\n\nterraform {\n  source = \"${get_terragrunt_dir()}/.\"\n}\n\nlocals {\n    name = \"service1\"\n}\n\ninputs = {\n    name = local.name\n}\n"
  },
  {
    "path": "test/fixtures/regressions/parsing-run-all-with-generate/services-info/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/regressions/parsing-run-all-with-generate/services-info/main.tf",
    "content": "variable \"desired_count\" {\n  description = \"The desired count of the ECS Service\"\n  type        = map(number)\n}\n\nvariable \"use_api_image\" {\n  description = \"Whether to use the API image\"\n  type        = map(bool)\n}\n\nresource \"null_resource\" \"services_info\" {\n  triggers = {\n    desired_count = jsonencode(var.desired_count)\n    use_api_image = jsonencode(var.use_api_image)\n  }\n}\n\noutput \"desired_count\" {\n  description = \"The desired count of the ECS Service\"\n  value       = var.desired_count\n}\n\noutput \"use_api_image\" {\n  description = \"Whether to use the API image\"\n  value       = var.use_api_image\n}\n"
  },
  {
    "path": "test/fixtures/regressions/parsing-run-all-with-generate/services-info/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n  expose = true\n}\n\nterraform {\n  source = \"${get_terragrunt_dir()}/.\"\n}\n\ndependency \"service-test1\" {\n  config_path = \"../services/test1\"\n  mock_outputs_allowed_terraform_commands = [\"validate\", \"plan\"]\n  mock_outputs = {\n    service_desired_count_initial_value = null\n    service_name = \"FAKE-SERVICE-1\"\n    docker_images = [\n        {\n            repository = \"123456789012.dkr.ecr.us-east-1.amazonaws.com/test1-FAKE\"\n        }\n    ]\n  }\n  mock_outputs_merge_strategy_with_state = \"shallow\"\n}\n\nlocals {\n  repository_url = \"123456789012.dkr.ecr.us-east-1.amazonaws.com/api-FAKE\"\n  name = \"services-info\"\n}\n\ninputs = {\n  desired_count = {\n    test1 = dependency.service-test1.outputs.service_desired_count_initial_value\n  }\n  use_api_image = {\n    test1 = dependency.service-test1.outputs.docker_images[0].repository == local.repository_url\n  }\n}\n"
  },
  {
    "path": "test/fixtures/regressions/run-cmd-include-output/root.hcl",
    "content": "# Root config that is included by child units\n# This tests that run_cmd output from included files is visible in stack runs\n\nterraform {\n  source = \".\"\n}\n\nlocals {\n  # Use the directory containing root.hcl (found via find_in_parent_folders) to construct\n  # a consistent path regardless of which unit includes this file.\n  root_dir = dirname(find_in_parent_folders(\"root.hcl\"))\n  # This run_cmd should emit output that is visible during stack runs.\n  # We use --terragrunt-global-cache to ensure both units share the same cache entry.\n  marker = run_cmd(\"--terragrunt-global-cache\", \"${local.root_dir}/scripts/emit_output.sh\")\n}\n\ninputs = {\n  marker = local.marker\n}\n"
  },
  {
    "path": "test/fixtures/regressions/run-cmd-include-output/scripts/emit_output.sh",
    "content": "#!/usr/bin/env bash\n# Emit a unique marker to stdout that can be detected in test output\necho \"RUN_CMD_OUTPUT_MARKER_12345\"\n"
  },
  {
    "path": "test/fixtures/regressions/run-cmd-include-output/unit-a/main.tf",
    "content": "variable \"marker\" {\n  type    = string\n  default = \"\"\n}\n\noutput \"marker_value\" {\n  value = var.marker\n}\n"
  },
  {
    "path": "test/fixtures/regressions/run-cmd-include-output/unit-a/terragrunt.hcl",
    "content": "# Unit A that includes root.hcl\n# The run_cmd in root.hcl should have its output visible\n\ninclude \"root\" {\n  path   = find_in_parent_folders(\"root.hcl\")\n  expose = true\n}\n"
  },
  {
    "path": "test/fixtures/regressions/run-cmd-include-output/unit-b/main.tf",
    "content": "variable \"marker\" {\n  type    = string\n  default = \"\"\n}\n\noutput \"marker_value\" {\n  value = var.marker\n}\n"
  },
  {
    "path": "test/fixtures/regressions/run-cmd-include-output/unit-b/terragrunt.hcl",
    "content": "# Unit B that includes root.hcl\n# The run_cmd in root.hcl should have its output visible\n\ninclude \"root\" {\n  path   = find_in_parent_folders(\"root.hcl\")\n  expose = true\n}\n"
  },
  {
    "path": "test/fixtures/regressions/sensitive-values/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/regressions/sensitive-values/dev.enc.yaml",
    "content": "# reference secret value\npassword: dev-secret-password-12345\n"
  },
  {
    "path": "test/fixtures/regressions/sensitive-values/main.tf",
    "content": "variable \"password\" {\n  type      = string\n  sensitive = true\n}\n\noutput \"password_length\" {\n  value     = length(var.password)\n  sensitive = true\n}\n"
  },
  {
    "path": "test/fixtures/regressions/sensitive-values/terragrunt.hcl",
    "content": "locals {\n  environment = \"dev\"\n\n  password = {\n    dev = sensitive(yamldecode(file(\"dev.enc.yaml\")).password)\n  }[local.environment]\n}\n\ninputs = {\n  password = local.password\n}\n\nterraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/regressions/skip-init/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/regressions/skip-init/main.tf",
    "content": "module \"mod\" {\n  source = \"./module\"\n}\n\noutput \"foo\" {\n  value = module.mod.foo\n}\n"
  },
  {
    "path": "test/fixtures/regressions/skip-init/module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/regressions/skip-init/module/main.tf",
    "content": "resource \"null_resource\" \"foo\" {}\n\noutput \"foo\" {\n  value = null_resource.foo.id\n}\n"
  },
  {
    "path": "test/fixtures/regressions/skip-init/terragrunt.hcl",
    "content": "# Intenionally empty\n"
  },
  {
    "path": "test/fixtures/regressions/skip-versioning/.gitignore",
    "content": "backend.tf\n"
  },
  {
    "path": "test/fixtures/regressions/skip-versioning/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/regressions/skip-versioning/local_terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/regressions/skip-versioning/main.tf",
    "content": "resource \"null_resource\" \"foo\" {}\n"
  },
  {
    "path": "test/fixtures/regressions/skip-versioning/remote_terragrunt.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"terraform.tfstate\"\n    region = \"us-west-2\"\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n    enable_lock_table_ssencryption = true\n    skip_bucket_versioning = true\n  }\n}\n"
  },
  {
    "path": "test/fixtures/regressions/yamldecode/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/regressions/yamldecode/main.tf",
    "content": "variable \"test1\" {}\nvariable \"test2\" {}\nvariable \"test3\" {}\n\noutput \"test1\" {\n  value = var.test1\n}\noutput \"test2\" {\n  value = var.test2\n}\noutput \"test3\" {\n  value = var.test3\n}\n"
  },
  {
    "path": "test/fixtures/regressions/yamldecode/terragrunt.hcl",
    "content": "inputs = {\n  test1 = yamldecode(\"a: '003'\")[\"a\"]\n  test2 = yamldecode(\"b: '1.00'\")[\"b\"]\n  test3 = yamldecode(\"c: 0ba\")[\"c\"]\n}\n"
  },
  {
    "path": "test/fixtures/relative-include-cmd/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/relative-include-cmd/app/app.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/relative-include-cmd/app/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"terragrunt-test.hcl\")\n}\n\n"
  },
  {
    "path": "test/fixtures/relative-include-cmd/terragrunt-test.hcl",
    "content": "locals {\n  path = run_cmd(\"echo\", \"path_relative_to_inclue:\", path_relative_to_include())\n}\n"
  },
  {
    "path": "test/fixtures/render-json/common_vars.hcl",
    "content": "dependency \"dep\" {\n  config_path = \"../dep\"\n}\n\ninputs = {\n  env  = \"qa\"\n  name = dependency.dep.outputs.name\n}\n"
  },
  {
    "path": "test/fixtures/render-json/dep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/render-json/dep/main.tf",
    "content": "output \"name\" {\n  value = \"dep\"\n}\n"
  },
  {
    "path": "test/fixtures/render-json/dep/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/render-json/main/module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/render-json/main/module/main.tf",
    "content": "variable \"aws_region\" {}\nvariable \"env\" {}\nvariable \"name\" {}\nvariable \"type\" {}\n\noutput \"aws_region\" {\n  value = var.aws_region\n}\noutput \"env\" {\n  value = var.env\n}\noutput \"name\" {\n  value = var.name\n}\noutput \"type\" {\n  value = var.type\n}\n"
  },
  {
    "path": "test/fixtures/render-json/main/terragrunt.hcl",
    "content": "terraform {\n  source = \"./module\"\n}\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ninclude \"envcommon\" {\n  path = find_in_parent_folders(\"common_vars.hcl\")\n}\n\ninputs = {\n  type = \"main\"\n}\n"
  },
  {
    "path": "test/fixtures/render-json/root.hcl",
    "content": "remote_state {\n  backend = \"local\"\n  generate = {\n    path = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n  config = {\n    path = \"foo.tfstate\"\n  }\n}\n\ngenerate \"provider\" {\n  path = \"provider.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\nEOF\n}\n\ninputs = {\n  aws_region = \"us-east-1\"\n}\n"
  },
  {
    "path": "test/fixtures/render-json-inputs/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/render-json-inputs/app/main.tf",
    "content": "variable \"x\" {}\n"
  },
  {
    "path": "test/fixtures/render-json-inputs/app/terragrunt.hcl",
    "content": "\ndependency \"dep\" {\n  config_path = \"../dependency\"\n\n}\n\ninputs = {\n  static_value = \"static_value\"\n  value = dependency.dep.outputs.value\n  not_existing_value = dependency.dep.outputs.not_existing_value\n}\n"
  },
  {
    "path": "test/fixtures/render-json-inputs/dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/render-json-inputs/dependency/main.tf",
    "content": "\noutput \"value\" {\n  value = \"output_value\"\n}\n"
  },
  {
    "path": "test/fixtures/render-json-inputs/dependency/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/render-json-metadata/attributes/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/render-json-metadata/attributes/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/render-json-metadata/attributes/terragrunt.hcl",
    "content": "inputs = {\n  region = local.aws_region\n  name   = \"${local.aws_region}-bucket\"\n}\n\nlocals {\n  aws_region = \"us-east-1\"\n}\n\ndownload_dir = \"/tmp\"\n\nprevent_destroy = true\n\nexclude {\n  if = true\n  actions = [\"all\"]\n  no_run = true\n}\n\niam_role = \"arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME\"\niam_assume_role_duration = 666\n\nterraform_binary = get_env(\"TG_TF_PATH\", \"tofu\")\nterraform_version_constraint = \">= 0.11\"\n\nerrors {\n  retry \"custom_errors\" {\n    retryable_errors = [\n      \"(?s).*Error installing provider.*tcp.*connection reset by peer.*\",\n      \"(?s).*ssh_exchange_identification.*Connection closed by remote host.*\"\n    ]\n    max_attempts = 3\n    sleep_interval_sec = 5\n  }\n}\n\niam_assume_role_session_name = \"qwe\"\n\n"
  },
  {
    "path": "test/fixtures/render-json-metadata/dependencies/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/render-json-metadata/dependencies/app/include.hcl",
    "content": "dependencies {\n  paths = [\"../dependency2\"]\n}\n\ninputs = {\n  test_input = \"test_value\"\n}"
  },
  {
    "path": "test/fixtures/render-json-metadata/dependencies/app/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/render-json-metadata/dependencies/app/terragrunt.hcl",
    "content": "dependencies {\n  paths = [\"../dependency1\" ]\n}\n\ninclude \"include\" {\n  path   = \"./include.hcl\"\n}\n"
  },
  {
    "path": "test/fixtures/render-json-metadata/dependencies/dependency1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/render-json-metadata/dependencies/dependency1/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/render-json-metadata/dependencies/dependency1/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/render-json-metadata/dependencies/dependency2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/render-json-metadata/dependencies/dependency2/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/render-json-metadata/dependencies/dependency2/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/render-json-metadata/dependency/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/render-json-metadata/dependency/app/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/render-json-metadata/dependency/app/terragrunt.hcl",
    "content": "\ndependency \"dep\" {\n  config_path = \"../dependency\"\n      \n    mock_outputs = {\n      test = \"value\"\n    }\n}\n\ndependency \"dep2\" {\n  config_path = \"../dependency2\"\n\n  mock_outputs = {\n    test2 = \"value2\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/render-json-metadata/dependency/dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/render-json-metadata/dependency/dependency/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/render-json-metadata/dependency/dependency/terragrunt.hcl",
    "content": "locals {\n\n    x = run_cmd(\"echo\", \"HCL file evaluation\")\n}"
  },
  {
    "path": "test/fixtures/render-json-metadata/dependency/dependency2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/render-json-metadata/dependency/dependency2/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/render-json-metadata/dependency/dependency2/terragrunt.hcl",
    "content": "locals {\n\n    x = run_cmd(\"echo\", \"HCL file evaluation\")\n}"
  },
  {
    "path": "test/fixtures/render-json-metadata/includes/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/render-json-metadata/includes/app/generate.hcl",
    "content": "generate \"provider\" {\n  path      = \"provider.tf\"\n  if_exists = \"overwrite\"\n  contents = <<EOF\n# test\nEOF\n}"
  },
  {
    "path": "test/fixtures/render-json-metadata/includes/app/inputs.hcl",
    "content": "inputs = {\n  qwe = \"123\"\n}"
  },
  {
    "path": "test/fixtures/render-json-metadata/includes/app/locals.hcl",
    "content": "locals {\n  a1 = \"qwe\"\n  content = \"test\"\n}\n\ninputs = {\n  content = local.content\n}"
  },
  {
    "path": "test/fixtures/render-json-metadata/includes/app/main.tf",
    "content": "variable \"content\" {}\n\nresource \"local_file\" \"file\" {\n  content  = \"content: ${var.content}\"\n  filename = \"${path.module}/cluster_name.txt\"\n}"
  },
  {
    "path": "test/fixtures/render-json-metadata/includes/app/terragrunt.hcl",
    "content": "include \"common\" {\n  path   = \"../common/common.hcl\"\n}\n\ninclude \"inputs\" {\n  path   = \"./inputs.hcl\"\n}\n\ninclude \"locals\" {\n  path   = \"./locals.hcl\"\n}\n\ninclude \"generate\" {\n  path   = \"./generate.hcl\"\n}\n\nlocals {\n  abc = \"xyz\"\n\n}"
  },
  {
    "path": "test/fixtures/render-json-metadata/includes/common/common.hcl",
    "content": "remote_state {\n  backend = \"s3\"\n  config = {\n    bucket = \"mybucket\"\n    key    = \"path/to/my/key\"\n    region = \"us-east-1\"\n  }\n}"
  },
  {
    "path": "test/fixtures/render-json-metadata/terraform-remote-state/app/terragrunt.hcl",
    "content": "include \"terraform\" {\n  path   = \"../common/terraform.hcl\"\n}\n\ninclude \"remote_state\" {\n  path   = \"../common/remote_state.hcl\"\n}\n"
  },
  {
    "path": "test/fixtures/render-json-metadata/terraform-remote-state/common/remote_state.hcl",
    "content": "remote_state {\n  backend = \"s3\"\n  config = {\n    bucket = \"mybucket\"\n    key    = \"path/to/my/key\"\n    region = \"us-east-1\"\n  }\n}"
  },
  {
    "path": "test/fixtures/render-json-metadata/terraform-remote-state/common/terraform.hcl",
    "content": "terraform {\n  source = \"../terraform\"\n}\n\n"
  },
  {
    "path": "test/fixtures/render-json-metadata/terraform-remote-state/terraform/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/render-json-metadata/terraform-remote-state/terraform/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/render-json-mock-outputs/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/render-json-mock-outputs/app/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/render-json-mock-outputs/app/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"module\" {\n  config_path = \"../dependency\"\n\n  mock_outputs = {\n    security_group_id = \"sg-abcd1234\"\n    bastion_host_security_group_id = \"123\"\n  }\n  mock_outputs_allowed_terraform_commands = [\"validate\" ]\n}\n"
  },
  {
    "path": "test/fixtures/render-json-mock-outputs/dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/render-json-mock-outputs/dependency/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/render-json-mock-outputs/dependency/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/render-json-mock-outputs/root.hcl",
    "content": "remote_state {\n  backend = \"s3\"\n  config = {\n    encrypt                   = true\n    bucket                    = \"test-tf-state\"\n    key                       = \"${path_relative_to_include()}/terraform.tfstate\"\n    dynamodb_table            = \"test-terraform-locks\"\n    accesslogging_bucket_name = \"test-tf-logs\"\n  }\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/render-json-regression/bar/terragrunt.hcl",
    "content": "inputs = {\n  from_root = \"Hi\"\n}\n"
  },
  {
    "path": "test/fixtures/render-json-regression/baz/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/render-json-regression/baz/main.tf",
    "content": "output \"baz\" {\n  value = \"baz\"\n}\n"
  },
  {
    "path": "test/fixtures/render-json-regression/baz/terragrunt.hcl",
    "content": "locals {\n  # This is intentionally unused, and is used to check the validity of the json output from render-json.\n  self = \"baz\"\n}\n"
  },
  {
    "path": "test/fixtures/render-json-regression/foo/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/render-json-regression/foo/main.tf",
    "content": "output \"foo\" {\n  value = \"foo\"\n}\n\nvariable \"foo\" {}\nvariable \"baz\" {}\n"
  },
  {
    "path": "test/fixtures/render-json-regression/terragrunt.hcl",
    "content": "terraform {\n  source = \"./foo\"\n}\n\nlocals {\n  foo = \"bar\"\n}\n\ninclude \"root\" {\n  path   = \"./bar/terragrunt.hcl\"\n  expose = true\n}\n\ndependency \"baz\" {\n  config_path = \"./baz\"\n}\n\ngenerate \"provider\" {\n  path = \"provider.tf\"\n  if_exists = \"overwrite\"\n  contents = \"# This is just a test\"\n}\n\ninputs = {\n  foo = \"bar\"\n  baz = \"blah\"\n  another = dependency.baz.outputs.baz\n}\n"
  },
  {
    "path": "test/fixtures/render-json-with-encryption/common_vars.hcl",
    "content": "dependency \"dep\" {\n  config_path = \"../dep\"\n}\n\ninputs = {\n  env  = \"qa\"\n  name = dependency.dep.outputs.name\n}\n"
  },
  {
    "path": "test/fixtures/render-json-with-encryption/dep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/render-json-with-encryption/dep/main.tf",
    "content": "output \"name\" {\n  value = \"dep\"\n}\n"
  },
  {
    "path": "test/fixtures/render-json-with-encryption/dep/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/render-json-with-encryption/main/module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/render-json-with-encryption/main/module/main.tf",
    "content": "variable \"aws_region\" {}\nvariable \"env\" {}\nvariable \"name\" {}\nvariable \"type\" {}\n\noutput \"aws_region\" {\n  value = var.aws_region\n}\noutput \"env\" {\n  value = var.env\n}\noutput \"name\" {\n  value = var.name\n}\noutput \"type\" {\n  value = var.type\n}\n"
  },
  {
    "path": "test/fixtures/render-json-with-encryption/main/terragrunt.hcl",
    "content": "terraform {\n  source = \"./module\"\n}\n\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ninclude \"envcommon\" {\n  path = find_in_parent_folders(\"common_vars.hcl\")\n}\n\ninputs = {\n  type = \"main\"\n}\n"
  },
  {
    "path": "test/fixtures/render-json-with-encryption/root.hcl",
    "content": "remote_state {\n  backend = \"local\"\n  generate = {\n    path = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n  config = {\n    path = \"foo.tfstate\"\n  }\n  encryption = {\n    key_provider = \"pbkdf2\"\n    passphrase   = \"correct-horse-battery-staple\"\n  }\n}\n\ngenerate \"provider\" {\n  path = \"provider.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\nEOF\n}\n\ninputs = {\n  aws_region = \"us-east-1\"\n}\n"
  },
  {
    "path": "test/fixtures/report/chain-a/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/report/chain-a/main.tf",
    "content": "resource \"null_resource\" \"chain_a\" {\n  triggers = {\n    always_fail = \"true\"\n  }\n\n  provisioner \"local-exec\" {\n    command = \"exit 1\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/report/chain-a/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\n# This module will fail\n"
  },
  {
    "path": "test/fixtures/report/chain-b/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/report/chain-b/main.tf",
    "content": "resource \"null_resource\" \"chain_b\" {\n  triggers = {\n    depends_on_a = dependency.chain_a.outputs\n  }\n}\n"
  },
  {
    "path": "test/fixtures/report/chain-b/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\ndependency \"chain_a\" {\n  config_path = \"../chain-a\"\n}\n"
  },
  {
    "path": "test/fixtures/report/chain-c/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/report/chain-c/main.tf",
    "content": "resource \"null_resource\" \"chain_c\" {\n  triggers = {\n    depends_on_b = dependency.chain_b.outputs\n  }\n}\n"
  },
  {
    "path": "test/fixtures/report/chain-c/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\ndependency \"chain_b\" {\n  config_path = \"../chain-b\"\n}\n"
  },
  {
    "path": "test/fixtures/report/error-ignore/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/report/error-ignore/main.tf",
    "content": "resource \"null_resource\" \"ignore_test\" {\n  triggers = {\n    # This will always fail\n    always_fail = \"true\"\n  }\n\n  provisioner \"local-exec\" {\n    command = \"exit 1\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/report/error-ignore/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\nerrors {\n  ignore \"ignore_everything\" {\n    ignorable_errors = [\".*\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/report/first-early-exit/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/report/first-early-exit/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/report/first-early-exit/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\ndependency \"first_failure\" {\n  config_path = \"../first-failure\"\n}\n"
  },
  {
    "path": "test/fixtures/report/first-exclude/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/report/first-exclude/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/report/first-exclude/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\nexclude {\n  if      = true\n  actions = [\"all\"]\n}\n"
  },
  {
    "path": "test/fixtures/report/first-failure/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/report/first-failure/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"test\" {\n  provisioner \"local-exec\" {\n    command = \"exit 1\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/report/first-failure/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/report/first-success/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/report/first-success/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/report/first-success/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/report/retry-success/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/report/retry-success/main.tf",
    "content": "resource \"null_resource\" \"retry_test\" {\n  provisioner \"local-exec\" {\n    command = \"ls success.txt\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/report/retry-success/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n\n  error_hook \"create_file\" {\n    commands  = [\"apply\"]\n    execute   = [\"touch\", \"success.txt\"]\n    on_errors = [\".*\"]\n  }\n}\n\nerrors {\n  retry \"file_not_there_yet\" {\n    max_attempts       = 2\n    sleep_interval_sec = 1\n    retryable_errors   = [\".*\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/report/second-early-exit/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/report/second-early-exit/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/report/second-early-exit/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\ndependency \"second_failure\" {\n  config_path = \"../second-failure\"\n}\n"
  },
  {
    "path": "test/fixtures/report/second-exclude/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/report/second-exclude/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/report/second-exclude/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\nexclude {\n  if      = true\n  actions = [\"all\"]\n}\n"
  },
  {
    "path": "test/fixtures/report/second-failure/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/report/second-failure/main.tf",
    "content": "terraform {\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"test\" {\n  provisioner \"local-exec\" {\n    command = \"exit 1\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/report/second-failure/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/report/second-success/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/report/second-success/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/report/second-success/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/root-terragrunt-hcl-regression/bar/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/root-terragrunt-hcl-regression/bar/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/root-terragrunt-hcl-regression/bar/terragrunt.hcl",
    "content": "include \"root\" {\n  // This is deprecated behavior, but we want to test that it still works.\n  path = find_in_parent_folders(\"terragrunt.hcl\")\n}\n\nterraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/root-terragrunt-hcl-regression/baz/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/root-terragrunt-hcl-regression/baz/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/root-terragrunt-hcl-regression/baz/terragrunt.hcl",
    "content": "include \"root\" {\n  // This is deprecated behavior, but we want to test that it still works.\n  path = find_in_parent_folders(\"terragrunt.hcl\")\n}\n\nterraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/root-terragrunt-hcl-regression/foo/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/root-terragrunt-hcl-regression/foo/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/root-terragrunt-hcl-regression/foo/terragrunt.hcl",
    "content": "include \"root\" {\n  // This is deprecated behavior, but we want to test that it still works.\n  path = find_in_parent_folders(\"terragrunt.hcl\")\n}\n\nterraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/root-terragrunt-hcl-regression/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/run-cmd-flags/module-conflict/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/run-cmd-flags/module-conflict/main.tf",
    "content": "variable \"conflict_value\" {\n  type = string\n}\n\noutput \"conflict_value\" {\n  value = var.conflict_value\n}\n"
  },
  {
    "path": "test/fixtures/run-cmd-flags/module-conflict/terragrunt.hcl",
    "content": "locals {\n  scripts_dir = \"${get_terragrunt_dir()}/../scripts\"\n  conflict    = run_cmd(\"--terragrunt-global-cache\", \"--terragrunt-no-cache\", \"${local.scripts_dir}/global_counter.sh\")\n}\n\ninputs = {\n  conflict_value = local.conflict\n}\n"
  },
  {
    "path": "test/fixtures/run-cmd-flags/module-global-cache-a/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/run-cmd-flags/module-global-cache-a/main.tf",
    "content": "variable \"cached_value\" {\n  type = string\n}\n\noutput \"cached_value_a\" {\n  value = var.cached_value\n}\n"
  },
  {
    "path": "test/fixtures/run-cmd-flags/module-global-cache-a/terragrunt.hcl",
    "content": "locals {\n  scripts_dir = \"${get_terragrunt_dir()}/../scripts\"\n  cached      = run_cmd(\"--terragrunt-global-cache\", \"${local.scripts_dir}/global_counter.sh\")\n}\n\ninputs = {\n  cached_value = local.cached\n}\n"
  },
  {
    "path": "test/fixtures/run-cmd-flags/module-global-cache-b/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/run-cmd-flags/module-global-cache-b/main.tf",
    "content": "variable \"cached_value\" {\n  type = string\n}\n\noutput \"cached_value_b\" {\n  value = var.cached_value\n}\n"
  },
  {
    "path": "test/fixtures/run-cmd-flags/module-global-cache-b/terragrunt.hcl",
    "content": "locals {\n  scripts_dir = \"${get_terragrunt_dir()}/../scripts\"\n  cached      = run_cmd(\"--terragrunt-global-cache\", \"${local.scripts_dir}/global_counter.sh\")\n}\n\ninputs = {\n  cached_value = local.cached\n}\n"
  },
  {
    "path": "test/fixtures/run-cmd-flags/module-no-cache/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/run-cmd-flags/module-no-cache/main.tf",
    "content": "variable \"first_value\" {\n  type = string\n}\n\nvariable \"second_value\" {\n  type = string\n}\n\noutput \"first_value\" {\n  value = var.first_value\n}\n\noutput \"second_value\" {\n  value = var.second_value\n}\n"
  },
  {
    "path": "test/fixtures/run-cmd-flags/module-no-cache/terragrunt.hcl",
    "content": "locals {\n  scripts_dir = \"${get_terragrunt_dir()}/../scripts\"\n  first       = run_cmd(\"--terragrunt-no-cache\", \"${local.scripts_dir}/no_cache_counter.sh\")\n  second      = run_cmd(\"--terragrunt-no-cache\", \"${local.scripts_dir}/no_cache_counter.sh\")\n}\n\ninputs = {\n  first_value  = local.first\n  second_value = local.second\n}\n"
  },
  {
    "path": "test/fixtures/run-cmd-flags/module-quiet/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/run-cmd-flags/module-quiet/main.tf",
    "content": "variable \"secret\" {\n  type = string\n}\n\noutput \"quiet_secret\" {\n  value = var.secret\n}\n"
  },
  {
    "path": "test/fixtures/run-cmd-flags/module-quiet/terragrunt.hcl",
    "content": "locals {\n  scripts_dir = \"${get_terragrunt_dir()}/../scripts\"\n  secret      = run_cmd(\"--terragrunt-quiet\", \"${local.scripts_dir}/emit_secret.sh\")\n}\n\ninputs = {\n  secret = local.secret\n}\n"
  },
  {
    "path": "test/fixtures/run-cmd-flags/scripts/.gitignore",
    "content": "# Ignore counter files created during test runs\nglobal_counter.txt\nno_cache_counter.txt\n\n"
  },
  {
    "path": "test/fixtures/run-cmd-flags/scripts/emit_secret.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\necho \"TOP_SECRET_TOKEN\"\n"
  },
  {
    "path": "test/fixtures/run-cmd-flags/scripts/global_counter.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\nscript_dir=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\ncounter_file=\"${script_dir}/global_counter.txt\"\n\ncount=0\nif [[ -f \"${counter_file}\" ]]; then\n  if ! read -r count <\"${counter_file}\"; then\n    count=0\n  fi\nfi\n\ncount=$((count + 1))\nprintf \"%s\" \"${count}\" >\"${counter_file}\"\n\necho \"global-value-${count}\"\n"
  },
  {
    "path": "test/fixtures/run-cmd-flags/scripts/no_cache_counter.sh",
    "content": "#!/usr/bin/env bash\n\nset -euo pipefail\n\nscript_dir=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\ncounter_file=\"${script_dir}/no_cache_counter.txt\"\n\ncount=0\nif [[ -f \"${counter_file}\" ]]; then\n  if ! read -r count <\"${counter_file}\"; then\n    count=0\n  fi\nfi\n\ncount=$((count + 1))\nprintf \"%s\" \"${count}\" >\"${counter_file}\"\n\necho \"no-cache-value-${count}\"\n"
  },
  {
    "path": "test/fixtures/run-filter/cache/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/run-filter/cache/main.tf",
    "content": "variable \"vpc_id\" {\n  type = string\n}\n\nresource \"null_resource\" \"cache\" {\n  triggers = {\n    name   = \"cache\"\n    vpc_id = var.vpc_id\n  }\n}\n\noutput \"cache_id\" {\n  value = \"cache-${var.vpc_id}\"\n}\n\n"
  },
  {
    "path": "test/fixtures/run-filter/cache/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n  mock_outputs = {\n    vpc_id = \"vpc-12345\"\n  }\n}\n\ninputs = {\n  vpc_id = dependency.vpc.outputs.vpc_id\n}\n\n"
  },
  {
    "path": "test/fixtures/run-filter/db/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/run-filter/db/main.tf",
    "content": "variable \"vpc_id\" {\n  type = string\n}\n\nresource \"null_resource\" \"db\" {\n  triggers = {\n    name   = \"db\"\n    vpc_id = var.vpc_id\n  }\n}\n\noutput \"db_id\" {\n  value = \"db-${var.vpc_id}\"\n}\n\n"
  },
  {
    "path": "test/fixtures/run-filter/db/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n  mock_outputs = {\n    vpc_id = \"vpc-12345\"\n  }\n}\n\ninputs = {\n  vpc_id = dependency.vpc.outputs.vpc_id\n}\n"
  },
  {
    "path": "test/fixtures/run-filter/service/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/run-filter/service/main.tf",
    "content": "variable \"vpc_id\" {\n  type = string\n}\n\nvariable \"db_id\" {\n  type = string\n}\n\nvariable \"cache_id\" {\n  type = string\n}\n\nresource \"null_resource\" \"service\" {\n  triggers = {\n    name     = \"service\"\n    vpc_id   = var.vpc_id\n    db_id    = var.db_id\n    cache_id = var.cache_id\n  }\n}\n\noutput \"service_id\" {\n  value = \"service-${var.vpc_id}-${var.db_id}-${var.cache_id}\"\n}\n\n"
  },
  {
    "path": "test/fixtures/run-filter/service/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n  mock_outputs = {\n    vpc_id = \"vpc-12345\"\n  }\n}\n\ndependency \"db\" {\n  config_path = \"../db\"\n  mock_outputs = {\n    db_id = \"db-vpc-12345\"\n  }\n}\n\ndependency \"cache\" {\n  config_path = \"../cache\"\n  mock_outputs = {\n    cache_id = \"cache-vpc-12345\"\n  }\n}\n\ninputs = {\n  vpc_id   = dependency.vpc.outputs.vpc_id\n  db_id    = dependency.db.outputs.db_id\n  cache_id = dependency.cache.outputs.cache_id\n}\n\n"
  },
  {
    "path": "test/fixtures/run-filter/vpc/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/run-filter/vpc/main.tf",
    "content": "resource \"null_resource\" \"vpc\" {\n  triggers = {\n    name = \"vpc\"\n  }\n}\n\noutput \"vpc_id\" {\n  value = \"vpc-12345\"\n}\n\n"
  },
  {
    "path": "test/fixtures/run-filter/vpc/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\n"
  },
  {
    "path": "test/fixtures/runner-pool-remote-source/unit-a/terragrunt.hcl",
    "content": "terraform {\n    source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/fail-fast/unit-a?ref=v0.84.1\"\n}\n"
  },
  {
    "path": "test/fixtures/runner-pool-remote-source/unit-b/terragrunt.hcl",
    "content": "terraform {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/fail-fast/unit-a?ref=v0.84.1\"\n}\n\ndependency \"unit-a\" {\n  config_path = \"../unit-a\"\n  mock_outputs = {\n    data = \"test-data\"\n  }\n}\n\ninputs = {\n  data = dependency.unit-a.outputs.data\n}\n"
  },
  {
    "path": "test/fixtures/s3-backend/common.hcl",
    "content": "feature \"disable_versioning\" {\n  default = false\n}\n\nfeature \"enable_lock_table_ssencryption\" {\n  default = false\n}\n\nfeature \"access_logging_bucket\" {\n  default = \"\"\n}\n\nfeature \"key_prefix\" {\n  default = \"\"\n}\n\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    key            = \"${feature.key_prefix.value}${path_relative_to_include()}/tofu.tfstate\"\n    bucket         = \"__FILL_IN_BUCKET_NAME__\"\n    region         = \"__FILL_IN_REGION__\"\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n\n    skip_bucket_versioning         = feature.disable_versioning.value\n    enable_lock_table_ssencryption = feature.enable_lock_table_ssencryption.value\n    accesslogging_bucket_name      = feature.access_logging_bucket.value\n  }\n}\n"
  },
  {
    "path": "test/fixtures/s3-backend/dual-locking/terragrunt.hcl",
    "content": "# Configure OpenTofu/Terraform state to be stored in S3 with DUAL locking (migration scenario)\n# This uses both DynamoDB and S3 native locking simultaneously\n# Both locks must be successfully acquired before operations can proceed\n\n# Feature flag for SSE encryption on the DynamoDB lock table\n# This allows the --feature flag to control SSE during bootstrap\nfeature \"enable_lock_table_ssencryption\" {\n  default = false\n}\n\n# Feature flag for access logging bucket\n# This allows the --feature flag to control access logging during bootstrap\nfeature \"access_logging_bucket\" {\n  default = \"\"\n}\n\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    bucket                         = \"__FILL_IN_BUCKET_NAME__\"\n    key                            = \"dual-locking/terraform.tfstate\"\n    region                         = \"__FILL_IN_REGION__\"\n    encrypt                        = true\n    dynamodb_table                 = \"__FILL_IN_LOCK_TABLE_NAME__\" # Traditional DynamoDB locking\n    use_lockfile                   = true                          # New S3 native locking\n    enable_lock_table_ssencryption = feature.enable_lock_table_ssencryption.value\n    accesslogging_bucket_name      = feature.access_logging_bucket.value\n  }\n}\n\nterraform {\n  source = \"tfr://registry.terraform.io/yorinasub17/terragrunt-registry-test/null//modules/one?version=0.0.2\"\n}\n"
  },
  {
    "path": "test/fixtures/s3-backend/unit1/terragrunt.hcl",
    "content": "terraform {\n  source = \"tfr://registry.terraform.io/yorinasub17/terragrunt-registry-test/null//modules/one?version=0.0.2\"\n}\n\ninclude \"common\" {\n  path = find_in_parent_folders(\"common.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/s3-backend/unit2/terragrunt.hcl",
    "content": "terraform {\n  source = \"tfr://registry.terraform.io/yorinasub17/terragrunt-registry-test/null//modules/one?version=0.0.2\"\n}\n\ninclude \"common\" {\n  path = find_in_parent_folders(\"common.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/s3-backend/use-lockfile/terragrunt.hcl",
    "content": "# Configure OpenTofu/Terraform state to be stored in S3 with native S3 locking\n# This uses S3 object conditional writes for state locking, which requires OpenTofu >= 1.10\n\n# Feature flag for access logging bucket\n# This allows the --feature flag to control access logging during bootstrap\nfeature \"access_logging_bucket\" {\n  default = \"\"\n}\n\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    bucket                    = \"__FILL_IN_BUCKET_NAME__\"\n    key                       = \"use-lockfile/terraform.tfstate\"\n    region                    = \"__FILL_IN_REGION__\"\n    encrypt                   = true\n    use_lockfile              = true\n    accesslogging_bucket_name = feature.access_logging_bucket.value\n  }\n}\n\nterraform {\n  source = \"tfr://registry.terraform.io/yorinasub17/terragrunt-registry-test/null//modules/one?version=0.0.2\"\n}\n"
  },
  {
    "path": "test/fixtures/s3-backend-disable-init/main.tf",
    "content": "# Explicit backend block required when remote_state has no generate block.\n# Terragrunt validates that a backend block exists; the actual config is\n# injected via -backend-config= args from GetTFInitArgs().\nterraform {\n  backend \"s3\" {}\n}\n\noutput \"hello\" {\n  value = \"world\"\n}\n"
  },
  {
    "path": "test/fixtures/s3-backend-disable-init/terragrunt.hcl",
    "content": "remote_state {\n  backend      = \"s3\"\n  disable_init = true\n  config = {\n    bucket  = \"__FILL_IN_BUCKET_NAME__\"\n    key     = \"terraform.tfstate\"\n    region  = \"__FILL_IN_REGION__\"\n    encrypt = true\n  }\n}\n"
  },
  {
    "path": "test/fixtures/s3-backend-migrate/unit1/terragrunt.hcl",
    "content": "terraform {\n  source = \"tfr://registry.terraform.io/yorinasub17/terragrunt-registry-test/null//modules/one?version=0.0.2\"\n}\n\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    key            = \"unit1/tofu.tfstate\"\n    bucket         = \"__FILL_IN_BUCKET_NAME__\"\n    region         = \"__FILL_IN_REGION__\"\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/s3-backend-migrate/unit2/terragrunt.hcl",
    "content": "terraform {\n  source = \"tfr://registry.terraform.io/yorinasub17/terragrunt-registry-test/null//modules/one?version=0.0.2\"\n}\n\nremote_state {\n  backend = \"gcs\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n\n  config = {\n    prefix   = \"unit2/tofu.tfstate\"\n    location = \"__FILL_IN_LOCATION__\"\n    project  = \"__FILL_IN_PROJECT__\"\n    bucket   = \"__FILL_IN_BUCKET_NAME__\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/s3-encryption/basic-encryption/terragrunt.hcl",
    "content": "remote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    encrypt                        = true\n    bucket                         = \"__FILL_IN_BUCKET_NAME__\"\n    key                            = \"terraform.tfstate\"\n    region                         = \"us-west-2\"\n    dynamodb_table                 = \"__FILL_IN_LOCK_TABLE_NAME__\"\n    enable_lock_table_ssencryption = true\n    bucket_sse_kms_key_id          = \"alias/dedicated-test-key\"\n\n  }\n}\n"
  },
  {
    "path": "test/fixtures/s3-encryption/custom-key/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/s3-encryption/custom-key/backend.tf",
    "content": "# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa\nterraform {\n  backend \"s3\" {\n    bucket         = \"terragrunt-test-bucket-qh7pvm\"\n    dynamodb_table = \"terragrunt-test-locks-jmktrj\"\n    encrypt        = true\n    key            = \"terraform.tfstate\"\n    region         = \"us-west-2\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/s3-encryption/custom-key/main.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.0\"\n    }\n  }\n}\n\nresource \"null_resource\" \"main\" {}\n\noutput \"id\" {\n  value = null_resource.main.id\n}\n"
  },
  {
    "path": "test/fixtures/s3-encryption/custom-key/terragrunt.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    encrypt                        = true\n    bucket                         = \"__FILL_IN_BUCKET_NAME__\"\n    key                            = \"terraform.tfstate\"\n    region                         = \"us-west-2\"\n    dynamodb_table                 = \"__FILL_IN_LOCK_TABLE_NAME__\"\n    enable_lock_table_ssencryption = true\n    bucket_sse_kms_key_id          = \"alias/dedicated-test-key\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/s3-encryption/sse-aes/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/s3-encryption/sse-aes/main.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.0\"\n    }\n  }\n}\n\nresource \"null_resource\" \"main\" {}\n\noutput \"id\" {\n  value = null_resource.main.id\n}\n"
  },
  {
    "path": "test/fixtures/s3-encryption/sse-aes/terragrunt.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"terraform.tfstate\"\n    region = \"us-west-2\"\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n    enable_lock_table_ssencryption = true\n    bucket_sse_algorithm           = \"AES256\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/s3-encryption/sse-kms/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/s3-encryption/sse-kms/main.tf",
    "content": "terraform {\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.0\"\n    }\n  }\n}\n\nresource \"null_resource\" \"main\" {}\n\noutput \"id\" {\n  value = null_resource.main.id\n}\n"
  },
  {
    "path": "test/fixtures/s3-encryption/sse-kms/terragrunt.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"terraform.tfstate\"\n    region = \"us-west-2\"\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n    enable_lock_table_ssencryption = true\n    bucket_sse_algorithm           = \"aws:kms\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/s3-errors/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/s3-errors/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n}\n"
  },
  {
    "path": "test/fixtures/s3-errors/terragrunt.hcl",
    "content": "remote_state {\n  backend = \"s3\"\n  config = {\n    region = \"__FILL_IN_REGION__\"\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"terraform.tfstate\"\n    encrypt = true\n  }\n}\n"
  },
  {
    "path": "test/fixtures/scaffold/catalog-config-test/terragrunt.hcl",
    "content": "# Terragrunt configuration for testing catalog config override behavior\ncatalog {\n  urls = [\"../with-shell-and-hooks\"]\n  no_shell = true\n  no_hooks = true\n}\n"
  },
  {
    "path": "test/fixtures/scaffold/custom-default-template/root.hcl",
    "content": "catalog {\n  default_template = \"git@github.com:gruntwork-io/terragrunt.git//test/fixtures/scaffold/external-template/template\"\n}\n"
  },
  {
    "path": "test/fixtures/scaffold/custom-default-template/unit/.gitkeep",
    "content": ""
  },
  {
    "path": "test/fixtures/scaffold/dependency-prompt-template/.boilerplate/boilerplate.yml",
    "content": "# boilerplate config\n\ndependencies:\n  - name: base\n    template-url: ../base\n    output-folder: base/\n\n  - name: leaf\n    template-url: ../leaf\n    output-folder: leaf/\n"
  },
  {
    "path": "test/fixtures/scaffold/dependency-prompt-template/base/boilerplate.yml",
    "content": ""
  },
  {
    "path": "test/fixtures/scaffold/dependency-prompt-template/base/test.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/scaffold/dependency-prompt-template/leaf/boilerplate.yml",
    "content": ""
  },
  {
    "path": "test/fixtures/scaffold/dependency-prompt-template/leaf/terragrunt.hcl",
    "content": "# Project template dir\nterraform {\n  source = \"{{ .sourceUrl }}\"\n}\n\ninputs = {\n  project_name = \"template\"\n}\n"
  },
  {
    "path": "test/fixtures/scaffold/external-template/dependency/boilerplate.yml",
    "content": ""
  },
  {
    "path": "test/fixtures/scaffold/external-template/dependency/dependency.txt",
    "content": "# Dependency"
  },
  {
    "path": "test/fixtures/scaffold/external-template/template/boilerplate.yml",
    "content": "# boilerplate config\n\ndependencies:\n  - name: test-dependency\n    template-url: ../dependency\n    output-folder: dependency/\n"
  },
  {
    "path": "test/fixtures/scaffold/external-template/template/external-template.txt",
    "content": "# External template"
  },
  {
    "path": "test/fixtures/scaffold/external-template/template/terragrunt.hcl",
    "content": "# Project template dir\nterraform {\n  source = \"{{ .sourceUrl }}\"\n}\n\ninputs = {\n  project_name = \"template\"\n\n}"
  },
  {
    "path": "test/fixtures/scaffold/module-with-template/.boilerplate/boilerplate.yml",
    "content": "# boilerplate config\nvariables:\n"
  },
  {
    "path": "test/fixtures/scaffold/module-with-template/.boilerplate/template-file.txt",
    "content": "# Template file"
  },
  {
    "path": "test/fixtures/scaffold/module-with-template/.boilerplate/terragrunt.hcl",
    "content": "# Project template dir\nterraform {\n  source = \"{{ .sourceUrl }}\"\n}\n\ninputs = {\n  project_name = \"template\"\n\n}"
  },
  {
    "path": "test/fixtures/scaffold/module-with-template/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/scaffold/module-with-template/main.tf",
    "content": "\nvariable \"project_name\" {\n  type        = string\n  description = \"Project name\"\n}\n\nvariable \"vpc\" {\n  type        = string\n  description = \"VPC to be used\"\n  default     = \"default-vpc\"\n}\n\nvariable \"replica_count\" {\n  type    = number\n  default = 666\n}\n\nresource \"local_file\" \"config\" {\n  content  = \"${var.project_name} vpc: ${var.vpc} replica_count: ${var.replica_count}\"\n  filename = \"config.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/scaffold/root-hcl/root.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/scaffold/root-hcl/unit/.gitkeep",
    "content": ""
  },
  {
    "path": "test/fixtures/scaffold/scaffold-module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/scaffold/scaffold-module/main.tf",
    "content": "\nresource \"local_file\" \"config\" {\n  content  = \"${var.project_name} vpc: ${var.vpc} replica_count: ${var.replica_count}\"\n  filename = \"config.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/scaffold/scaffold-module/variables.tf",
    "content": "variable \"project_name\" {\n  type        = string\n  description = \"Project name\"\n}\n\nvariable \"vpc\" {\n  type        = string\n  description = \"VPC to be used\"\n  default     = \"default-vpc\"\n}\n\nvariable \"replica_count\" {\n  type    = number\n  default = 666\n}\n\nvariable \"enabled\" {\n  description = \"Enable or disable the module\"\n  type        = bool\n  default     = true\n}\n\nvariable \"open_port\" {\n  type        = number\n  description = <<-EOF\n    Port to be opened in the security group\n    Can be a single port or a range\n  EOF\n}\n\nvariable \"enable_backups\" {\n  type = bool\n}\n\nvariable \"users\" {\n  type        = list(string)\n  description = \"List of users\"\n}\n\nvariable \"policy_map\" {\n  type        = map(string)\n  description = \"Map of policies\"\n}\n\nvariable \"test_1\" {}\n\nvariable \"test_2\" {\n  default = {\n    x = 1\n  }\n}\n\nvariable \"test_3\" {\n  type        = number\n  default     = 666\n  description = \"description test 3\"\n}\n"
  },
  {
    "path": "test/fixtures/scaffold/scaffold-module-tofu/main.tofu",
    "content": "\nresource \"local_file\" \"config\" {\n  content  = \"${var.project_name} vpc: ${var.vpc} replica_count: ${var.replica_count}\"\n  filename = \"config.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/scaffold/scaffold-module-tofu/variables.tofu",
    "content": "variable \"project_name\" {\n  type        = string\n  description = \"Project name\"\n}\n\nvariable \"vpc\" {\n  type        = string\n  description = \"VPC to be used\"\n  default     = \"default-vpc\"\n}\n\nvariable \"replica_count\" {\n  type    = number\n  default = 666\n}\n\nvariable \"enabled\" {\n  description = \"Enable or disable the module\"\n  type        = bool\n  default     = true\n}\n\nvariable \"open_port\" {\n  type        = number\n  description = <<-EOF\n    Port to be opened in the security group\n    Can be a single port or a range\n  EOF\n}\n\nvariable \"enable_backups\" {\n  type = bool\n}\n\nvariable \"users\" {\n  type        = list(string)\n  description = \"List of users\"\n}\n\nvariable \"policy_map\" {\n  type        = map(string)\n  description = \"Map of policies\"\n}\n\nvariable \"test_1\" {}\n\nvariable \"test_2\" {\n  default = {\n    x = 1\n  }\n}\n\nvariable \"test_3\" {\n  type        = number\n  default     = 666\n  description = \"description test 3\"\n}\n"
  },
  {
    "path": "test/fixtures/scaffold/with-hooks/.boilerplate/boilerplate.yml",
    "content": "variables:\n  - name: TestVariable\n    description: A test variable\n    type: string\n    default: \"test-value\"\n\nhooks:\n  before:\n    - command: echo\n      args:\n        - BEFORE_HOOK_EXECUTED\n      description: \"Test hook that should execute before rendering\"\n  after:\n    - command: echo\n      args:\n        - AFTER_HOOK_EXECUTED\n      description: \"Test hook that should execute after rendering\"\n"
  },
  {
    "path": "test/fixtures/scaffold/with-hooks/.boilerplate/terragrunt.hcl",
    "content": "# Test template with hooks\nterraform {\n  source = \"git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=v0.53.8\"\n}\n\ninputs = {\n  test_var = \"{{ .TestVariable }}\"\n}\n"
  },
  {
    "path": "test/fixtures/scaffold/with-hooks/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/scaffold/with-hooks/main.tf",
    "content": "# Test module for hooks testing\nvariable \"test_var\" {\n  description = \"Test variable\"\n  type        = string\n  default     = \"\"\n}\n\noutput \"test_output\" {\n  value = var.test_var\n}\n\n"
  },
  {
    "path": "test/fixtures/scaffold/with-shell-and-hooks/.boilerplate/boilerplate.yml",
    "content": "variables:\n  - name: TestVariable\n    description: A test variable\n    type: string\n    default: \"test-value\"\n\nhooks:\n  before:\n    - command: echo\n      args:\n        - BEFORE_HOOK_EXECUTED\n      description: \"Test hook before rendering\"\n  after:\n    - command: echo\n      args:\n        - AFTER_HOOK_EXECUTED\n      description: \"Test hook after rendering\"\n"
  },
  {
    "path": "test/fixtures/scaffold/with-shell-and-hooks/.boilerplate/terragrunt.hcl",
    "content": "# Test template with both shell template functions and hooks\nterraform {\n  source = \"git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=v0.53.8\"\n}\n\ninputs = {\n  test_var = \"{{ .TestVariable }}\"\n  shell_result_1 = \"{{ shell \"echo\" \"-n\" \"SHELL_OUTPUT_1\" }}\"\n  shell_result_2 = \"{{ shell \"echo\" \"-n\" \"SHELL_OUTPUT_2\" }}\"\n}\n"
  },
  {
    "path": "test/fixtures/scaffold/with-shell-and-hooks/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/scaffold/with-shell-and-hooks/main.tf",
    "content": "# Test module for shell and hooks testing\nvariable \"shell_result_1\" {\n  description = \"Output from shell command 1\"\n  type        = string\n  default     = \"\"\n}\n\nvariable \"shell_result_2\" {\n  description = \"Output from shell command 2\"\n  type        = string\n  default     = \"\"\n}\n\noutput \"shell_output_1\" {\n  value = var.shell_result_1\n}\n\noutput \"shell_output_2\" {\n  value = var.shell_result_2\n}\n\n"
  },
  {
    "path": "test/fixtures/scaffold/with-shell-commands/.boilerplate/boilerplate.yml",
    "content": "variables:\n  - name: TestVariable\n    description: A test variable\n    type: string\n    default: \"test-value\"\n"
  },
  {
    "path": "test/fixtures/scaffold/with-shell-commands/.boilerplate/terragrunt.hcl",
    "content": "# Test template with shell template function\nterraform {\n  source = \"git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=v0.53.8\"\n}\n\ninputs = {\n  test_var = \"{{ .TestVariable }}\"\n  shell_output_1 = \"{{ shell \"echo\" \"-n\" \"SHELL_EXECUTED_VALUE_1\" }}\"\n  shell_output_2 = \"{{ shell \"echo\" \"-n\" \"SHELL_EXECUTED_VALUE_2\" }}\"\n}\n"
  },
  {
    "path": "test/fixtures/scaffold/with-shell-commands/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/scaffold/with-shell-commands/main.tf",
    "content": "# Test module for shell command testing\nvariable \"shell_output_1\" {\n  description = \"Output from shell command 1\"\n  type        = string\n  default     = \"\"\n}\n\nvariable \"shell_output_2\" {\n  description = \"Output from shell command 2\"\n  type        = string\n  default     = \"\"\n}\n\noutput \"shell_result_1\" {\n  value = var.shell_output_1\n}\n\noutput \"shell_result_2\" {\n  value = var.shell_output_2\n}\n\n"
  },
  {
    "path": "test/fixtures/skip/base-module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version     = \"2.5.2\"\n  constraints = \"2.5.2\"\n  hashes = [\n    \"h1:6lS+5A/4WFAqY3/RHWFRBSiFVLPRjvLaUgxPQvjXLHU=\",\n    \"h1:BUewjbhAQWuGHH36SozCTuESFJhbiHMaCFLnVVNZ1Es=\",\n    \"h1:MBgBjJljfDl1i2JPcIoH4hW+2XLJ+D1l12iH/xd3uTo=\",\n    \"zh:25b95b76ceaa62b5c95f6de2fa6e6242edbf51e7fc6c057b7f7101aa4081f64f\",\n    \"zh:3c974fdf6b42ca6f93309cf50951f345bfc5726ec6013b8832bcd3be0eb3429e\",\n    \"zh:5de843bf6d903f5cca97ce1061e2e06b6441985c68d013eabd738a9e4b828278\",\n    \"zh:86beead37c7b4f149a54d2ae633c99ff92159c748acea93ff0f3603d6b4c9f4f\",\n    \"zh:8e52e81d3dc50c3f79305d257da7fde7af634fed65e6ab5b8e214166784a720e\",\n    \"zh:9882f444c087c69559873b2d72eec406a40ede21acb5ac334d6563bf3a2387df\",\n    \"zh:a4484193d110da4a06c7bffc44cc6b61d3b5e881cd51df2a83fdda1a36ea25d2\",\n    \"zh:a53342426d173e29d8ee3106cb68abecdf4be301a3f6589e4e8d42015befa7da\",\n    \"zh:d25ef2aef6a9004363fc6db80305d30673fc1f7dd0b980d41d863b12dacd382a\",\n    \"zh:fa2d522fb323e2121f65b79709fd596514b293d816a1d969af8f72d108888e4c\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/skip/base-module/main.tf",
    "content": "terraform {\n  required_version = \">= 0.12\"\n  required_providers {\n    local = {\n      source  = \"registry.opentofu.org/hashicorp/local\"\n      version = \"2.5.2\"\n    }\n  }\n}\n\nvariable \"person\" {\n  type = string\n}\n\nresource \"local_file\" \"example\" {\n  content  = \"hello, ${var.person}\"\n  filename = \"example.txt\"\n}\n\noutput \"example\" {\n  value = local_file.example.content\n}\n"
  },
  {
    "path": "test/fixtures/skip/skip-false/resource1/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../../base-module\"\n}\n\ninputs = {\n  person = \"Ernie\"\n}\n\n"
  },
  {
    "path": "test/fixtures/skip/skip-false/resource2/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../../base-module\"\n}\n\ninputs = {\n  person = \"Bert\"\n}\n"
  },
  {
    "path": "test/fixtures/skip/skip-false/root.hcl",
    "content": "exclude {\n  if = false\n  actions = [\"all\"]\n  no_run = true\n}\n\nterraform {\n  source = \"../base-module\"\n}\n\ninputs = {\n  person = \"Hobbs\"\n}\n\n"
  },
  {
    "path": "test/fixtures/skip/skip-true/resource1/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nexclude {\n  if = false\n  actions = [\"all\"]\n  no_run = true\n}\n\ninputs = {\n  person = \"Ernie\"\n}\n\n"
  },
  {
    "path": "test/fixtures/skip/skip-true/resource2/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ninputs = {\n  person = \"Bert\"\n}\n"
  },
  {
    "path": "test/fixtures/skip/skip-true/root.hcl",
    "content": "exclude {\n  if = true\n  actions = [\"all\"]\n  no_run = true\n}\n\nterraform {\n  source = \"../../base-module\"\n}\n"
  },
  {
    "path": "test/fixtures/skip-dependencies/first/foo.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/skip-dependencies/first/terragrunt.hcl",
    "content": "exclude {\n  if = true\n  actions = [\"all\"]\n  no_run = true\n}\n\nterraform {\n  source = \"../module\"\n}\n\ninclude \"foo\" {\n  path = \"foo.hcl\"\n}\n\ninputs = {\n  input = \"first\"\n}"
  },
  {
    "path": "test/fixtures/skip-dependencies/module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/skip-dependencies/module/main.tf",
    "content": "resource \"local_file\" \"test_file\" {\n  content  = \"test_file_content\"\n  filename = \"${path.module}/test_file.txt\"\n}\n\n"
  },
  {
    "path": "test/fixtures/skip-dependencies/second/terragrunt.hcl",
    "content": "exclude {\n  if = true\n  actions = [\"all\"]\n  no_run = true\n}\n\nterraform {\n  source = \"../module\"\n}\n\ndependency \"first\" {\n  config_path = \"../first\"\n\n  mock_outputs_allowed_terraform_commands = [\"init\", \"destroy\", \"validate\"]\n  mock_outputs_merge_strategy_with_state  = \"deep_map_only\"\n  mock_outputs = {\n    random_output = \"\"\n  }\n}\n\ninputs = {\n  input = dependency.first.outputs.random_output\n}"
  },
  {
    "path": "test/fixtures/skip-legacy-root/base-module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version     = \"2.5.2\"\n  constraints = \"2.5.2\"\n  hashes = [\n    \"h1:6lS+5A/4WFAqY3/RHWFRBSiFVLPRjvLaUgxPQvjXLHU=\",\n    \"h1:BUewjbhAQWuGHH36SozCTuESFJhbiHMaCFLnVVNZ1Es=\",\n    \"h1:MBgBjJljfDl1i2JPcIoH4hW+2XLJ+D1l12iH/xd3uTo=\",\n    \"zh:25b95b76ceaa62b5c95f6de2fa6e6242edbf51e7fc6c057b7f7101aa4081f64f\",\n    \"zh:3c974fdf6b42ca6f93309cf50951f345bfc5726ec6013b8832bcd3be0eb3429e\",\n    \"zh:5de843bf6d903f5cca97ce1061e2e06b6441985c68d013eabd738a9e4b828278\",\n    \"zh:86beead37c7b4f149a54d2ae633c99ff92159c748acea93ff0f3603d6b4c9f4f\",\n    \"zh:8e52e81d3dc50c3f79305d257da7fde7af634fed65e6ab5b8e214166784a720e\",\n    \"zh:9882f444c087c69559873b2d72eec406a40ede21acb5ac334d6563bf3a2387df\",\n    \"zh:a4484193d110da4a06c7bffc44cc6b61d3b5e881cd51df2a83fdda1a36ea25d2\",\n    \"zh:a53342426d173e29d8ee3106cb68abecdf4be301a3f6589e4e8d42015befa7da\",\n    \"zh:d25ef2aef6a9004363fc6db80305d30673fc1f7dd0b980d41d863b12dacd382a\",\n    \"zh:fa2d522fb323e2121f65b79709fd596514b293d816a1d969af8f72d108888e4c\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/skip-legacy-root/base-module/main.tf",
    "content": "terraform {\n  required_version = \">= 0.12\"\n  required_providers {\n    local = {\n      source  = \"registry.opentofu.org/hashicorp/local\"\n      version = \"2.5.2\"\n    }\n  }\n}\n\nvariable \"person\" {\n  type = string\n}\n\nresource \"local_file\" \"example\" {\n  content  = \"hello, ${var.person}\"\n  filename = \"example.txt\"\n}\n\noutput \"example\" {\n  value = local_file.example.content\n}\n"
  },
  {
    "path": "test/fixtures/skip-legacy-root/skip-false/resource1/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../../base-module\"\n}\n\ninputs = {\n  person = \"Ernie\"\n}\n\n"
  },
  {
    "path": "test/fixtures/skip-legacy-root/skip-false/resource2/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../../base-module\"\n}\n\ninputs = {\n  person = \"Bert\"\n}\n"
  },
  {
    "path": "test/fixtures/skip-legacy-root/skip-false/terragrunt.hcl",
    "content": "exclude {\n  if = false\n  actions = [\"all\"]\n  no_run = true\n}\n\nterraform {\n  source = \"../base-module\"\n}\n\ninputs = {\n  person = \"Hobbs\"\n}\n\n"
  },
  {
    "path": "test/fixtures/skip-legacy-root/skip-true/resource1/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nexclude {\n  if = false\n  actions = [\"all\"]\n  no_run = true\n}\n\ninputs = {\n  person = \"Ernie\"\n}\n\n"
  },
  {
    "path": "test/fixtures/skip-legacy-root/skip-true/resource2/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ninputs = {\n  person = \"Bert\"\n}\n"
  },
  {
    "path": "test/fixtures/skip-legacy-root/skip-true/terragrunt.hcl",
    "content": "exclude {\n  if = true\n  actions = [\"all\"]\n  no_run = true\n}\n\nterraform {\n  source = \"../base-module\"\n}\n"
  },
  {
    "path": "test/fixtures/sops/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/sops/main.tf",
    "content": "variable \"json_string_array\" {\n  type = list(string)\n}\n\nvariable \"json_bool_array\" {\n  type = list(bool)\n}\n\nvariable \"json_string\" {\n  type = string\n}\n\nvariable \"json_number\" {\n  type = number\n}\n\nvariable \"json_hello\" {\n  type = string\n}\n\nvariable \"yaml_string_array\" {\n  type = list(string)\n}\n\nvariable \"yaml_bool_array\" {\n  type = list(bool)\n}\n\nvariable \"yaml_string\" {\n  type = string\n}\n\nvariable \"yaml_number\" {\n  type = number\n}\n\nvariable \"yaml_hello\" {\n  type = string\n}\n\nvariable \"text_value\" {\n  type = string\n}\n\nvariable \"env_value\" {\n  type = string\n}\n\nvariable \"ini_value\" {\n  type = string\n}\n\noutput \"json_string_array\" {\n  value = var.json_string_array\n}\n\noutput \"json_bool_array\" {\n  value = var.json_bool_array\n}\n\noutput \"json_string\" {\n  value = var.json_string\n}\n\noutput \"json_number\" {\n  value = var.json_number\n}\n\noutput \"json_hello\" {\n  value = var.json_hello\n}\n\noutput \"yaml_string_array\" {\n  value = var.yaml_string_array\n}\n\noutput \"yaml_bool_array\" {\n  value = var.yaml_bool_array\n}\n\noutput \"yaml_string\" {\n  value = var.yaml_string\n}\n\noutput \"yaml_number\" {\n  value = var.yaml_number\n}\n\noutput \"yaml_hello\" {\n  value = var.yaml_hello\n}\n\noutput \"text_value\" {\n  value = var.text_value\n}\n\noutput \"env_value\" {\n  value = var.env_value\n}\n\noutput \"ini_value\" {\n  value = var.ini_value\n}\n"
  },
  {
    "path": "test/fixtures/sops/secrets.env",
    "content": "DB_USER=ENC[AES256_GCM,data:4yoBr0w=,iv:ePKzjYwS4yhJKGLKV2+KmtlXiFgtvzuP5+ZfgTvxtEA=,tag:IZyZ7UybZt+nHiFgFYjPNQ==,type:str]\nDB_PASSWORD=ENC[AES256_GCM,data:fQjK+ByY,iv:xhY1TLraqFZfYSGhW8nAO4jnkLnsUNFSJD9Y3+f48NQ=,tag:n39JC2ndmGuvb3F5Ily6aw==,type:str]\nsops_unencrypted_suffix=_unencrypted\nsops_pgp__list_0__map_enc=-----BEGIN PGP MESSAGE-----\\n\\nhQEMA0sXzMgpEabgAQf/SvsLSgWPoYfaeZTRspheA93oZvA4WWXxklP320JOBLlN\\nIC5PH55OyxDde6l+HnpQpgNqp3QlPS15dtPs+U9NoObRfhNl9Bxd2rtdouiHK7LT\\nWFFp6FJx+CBuVvMDMt8eEYPT1cNJA5A1gMnjDjt3ByJmV1xDVvHruU/EsL9bT1Br\\nizl+4OszzADZ3Ih1vz4FkC7gwT0wmprk3b2IbXqP7wrgpk+BOkCVjzdkwbJIqNAW\\nMGA6AHCrL2lSUm1UvhFjgDtlOnWZwtFHiiI8kqM90gtbzQG08nvN81UXTWkzseSJ\\nv1AvKLLDpTikD7klugD+4GzSvQEkcZlVy0zxwIppSNJeAXXeFN0cT1dus1s6rSKI\\nMBlI1Wc+slUx/zErenHeOxeF4SpPqwCvixoe94kf4kPzluRkS0tuHTpPbNbgpl9o\\nQ5Up3V2L+ifq3xiJVGAhxeefJRHADpmvhVnpegUcWg==\\n=ilH5\\n-----END PGP MESSAGE-----\\n\nsops_pgp__list_0__map_fp=3EF98802EEDCAF0C688B81F419546E0C123C664E\nsops_lastmodified=2021-12-17T18:43:48Z\nsops_mac=ENC[AES256_GCM,data:lRGaGq2usY4shlcsX5dR3g06CtNipQvQv8vXUSAV5yzvGSQHl1n/DmKXVwJZgIg6a0xQhr3L9R/1NL106md6kKxUSjeXJPdWjEteHX2qE24NGKBWgeR28JRjSuGytf7O4K10Q8SZm/JYhokAScgjsg3gzRAb8/LGzWzXpfV5ioE=,iv:JFBjodBw2thMVqoh0ItbRvTS5gx4YInt6udczff+CBQ=,tag:5/iJzP+sJPTf4+ZMNV7pgQ==,type:str]\nsops_pgp__list_0__map_created_at=2021-12-17T18:43:48Z\nsops_version=3.7.1\n"
  },
  {
    "path": "test/fixtures/sops/secrets.ini",
    "content": "[terragrunt]\nuser     = ENC[AES256_GCM,data:jnACR2o=,iv:Qh5xex81KAqgJE+e1R0Xk95fNK6iQpBEejiuG8PcGiA=,tag:pB0/iBG0WxvpZ+1F50cK+A==,type:str]\npassword = ENC[AES256_GCM,data:AMuq3/fS,iv:x+y4FAB/dgDwjdxBA6NXtLKbp/qvAlXtHboLTnoirNI=,tag:wsaMJth3eTJb+pNJrNr+hg==,type:str]\n\n[sops]\npgp__list_0__map_created_at = 2021-12-17T18:43:08Z\nversion                     = 3.7.1\npgp__list_0__map_enc        = -----BEGIN PGP MESSAGE-----\\n\\nhQEMA0sXzMgpEabgAQf+MWG+tWNvydG/jVWwXQfg/ch2WdarMXKO0b/RZ/NkJT0n\\nv4ozaGeATooEDuZXZXm0qJs1NQLYnCBp5PakkVfabHbSR2MByE7AwclgjUV6g4H7\\nKHWsw0L6hKfZFeIU9+FTEVIjpNkFIE9EEdkD762ZF4B6n31HxgcK+z7r/sW+2PII\\nZf+HKZPuPik0Og0SmmtiCr/nO2p5wAdjEdBBHAaAfD00UCbIR34UG9iAErhJxJ5U\\ngSpOHSli8VISaB1LWJldV51F7uT1qhdukcCeBl0W1lWM4wmBdVLxSV6oKuK2gUPF\\nuXm9hrlUsDRN6TmViJb9TvgWOe1Quva+pwnwJnJw5dJeAX7tTavnEIxfgJW/cQ8u\\n3gP/kRtuGCOhUFukl75ZkmqqbheXGiKmhwRGtzmFMqM3xQBtXw56TxZ9JCu4p09Y\\nua8othAS0G2L84/r5pJN5krfdvgsoKAP7XQwrpGYPQ==\\n=7lpK\\n-----END PGP MESSAGE-----\\n\nunencrypted_suffix          = _unencrypted\nlastmodified                = 2021-12-17T18:43:08Z\nmac                         = ENC[AES256_GCM,data:SXl3JEg+gMPfN0c2pzRm/XmBTmI70ZQJ2gr11S8lVZehRpfdAc2UNwz6B5uca0hGRKViO7KhLWPAo4yxNXNi+QznJkeHpxa1fYnT1p+UhYjvOnfg8MQeL4+qEz+Oq5Gx4LJJm62oC8+bAWWZPlFM4L5844R8bSZ6Pd1FUfFF6b4=,iv:x4pQi8UFuSwKLuv4EvXOG4DJx4IxoUEfd/+32s6nsHg=,tag:P6SHIsFol+5n2YylQ5cQYg==,type:str]\npgp__list_0__map_fp         = 3EF98802EEDCAF0C688B81F419546E0C123C664E\n\n"
  },
  {
    "path": "test/fixtures/sops/secrets.json",
    "content": "{\n\t\"hello\": \"ENC[AES256_GCM,data:czz2N4sewALmwVOmYQE6lY5S/Uo0WTLHHbrQ3QgbBWD5BWulD2EAo8qWwkLUPw==,iv:QrBTXslkGvOB2gYgC/oWqhRGWQjj6sp/IQrxjaND2Js=,tag:c9b2vgvRT7pU/sGFjWpSFQ==,type:str]\",\n\t\"example_key\": \"ENC[AES256_GCM,data:0JMhNukgGpjkH9jQGw==,iv:UoFzcGT33yS6KDDWBimbA5Xcr9HBQga/5J+nNhaEOmc=,tag:EKhnCZ+aqBlLrkCZe85BMA==,type:str]\",\n\t\"example_array\": [\n\t\t\"ENC[AES256_GCM,data:Gge+IhtDUwhodsKzxDo=,iv:IUo6syqstu5OYLcz3EXT5ajBP+3CrqpEG45ixCuBygI=,tag:7xAMjyG4YGhnF54eWIbn3A==,type:str]\",\n\t\t\"ENC[AES256_GCM,data:KLqMXJnvjiN2UGN3lNk=,iv:lfJ08yt1RuK8x9LAQCzJR27byk/XMsfhNeHDHSMd9cA=,tag:VW1+amEjWNk+479O/JhNcw==,type:str]\"\n\t],\n\t\"example_number\": \"ENC[AES256_GCM,data:90klYjpd2VumbA==,iv:JdTOzC4R1albHbp/XdTaON49Mb2gDe4BkwUqvAI/1Dw=,tag:N8o07zAtn8D0ynr6A4Hvhg==,type:float]\",\n\t\"example_booleans\": [\n\t\t\"ENC[AES256_GCM,data:Sax/KQ==,iv:0+wdeRktsHynRlOsD1m8pkqgUwXrc4YL5Af33H90j1Q=,tag:AJzmFncYeOoAX2e5CMYQzA==,type:bool]\",\n\t\t\"ENC[AES256_GCM,data:0T4bGzc=,iv:wwHxRKp6rq5dmm9mqX7vcJky9oKzF35HUGctzqLLwck=,tag:ee3GMCHNg7pBdlybgzlEDA==,type:bool]\"\n\t],\n\t\"sops\": {\n\t\t\"kms\": null,\n\t\t\"gcp_kms\": null,\n\t\t\"azure_kv\": null,\n\t\t\"lastmodified\": \"2020-05-14T22:54:59Z\",\n\t\t\"mac\": \"ENC[AES256_GCM,data:5l5pv5lMgiahrP7OgtCIuqWRjFrAdc2jreiq827PSwwwRKvn3pA7OvozW1jN5DqAgaYXtnBkg1pH1sgGzK3GMm2sJ/ImhFhIqfpkqNNtKWg1QJtS7wzZBDtO4CXXGB8B60Mzz9kHcBF9aIRofC9XpiSrIamWrsvEVt1oOdDX4+Q=,iv:M03JAzYGGdnT0B1TJH59Yvyg1mNk2cZxHc5npsTCFJs=,tag:uSx61XQMD+HRZJRFH+ji9Q==,type:str]\",\n\t\t\"pgp\": [\n\t\t\t{\n\t\t\t\t\"created_at\": \"2020-05-14T22:54:34Z\",\n\t\t\t\t\"enc\": \"-----BEGIN PGP MESSAGE-----\\n\\nhQEMAwAAAAAAAAAAAQf/cy16+5UwIMKYQCm3qswZ+oFLZIgatwa+74I09SHz6jKw\\nAwaESTl1Jn7JUXBh59BFq5gKa4i3Mb1/n2tvl3j/tGNzeE8xT+cdKcJuvEtx21xI\\n5n1NPwE9PXJoL3CUTtrWL6Ybf3NzKLLmUmStnwmM9NDMU5LvqapREilfdTjU3CrP\\n1zSevbUpzQS8cMpJKMm1Vpctcvp/jvX179MGNljrIAeB27Q/02X5z/fKGFfi7Qug\\nscH5JiSCr56xqea7rl6XMHK/lhduqHK+sO04pISq+QHwkKzz4q43WjuP7aF7751l\\nPaGu2PQthzbmaA3YQGPvhsYNVLELKSi3FVtRlwmahdJeAZXghrZf5/th9B5UH6t4\\nyJ0B8pChWDT3ON/Wx8p8Le//u4CS+RF9ohp7O6vg0M47+ozErjrKo4spMXG+BBpL\\ncy7TGBnhyWmRr7yQVNNJiEJ4jho+4TQyGAPTLcoxyw==\\n=ZBi5\\n-----END PGP MESSAGE-----\\n\",\n\t\t\t\t\"fp\": \"4B17CCC82911A6E0\"\n\t\t\t}\n\t\t],\n\t\t\"unencrypted_suffix\": \"_unencrypted\",\n\t\t\"version\": \"3.4.0\"\n\t}\n}"
  },
  {
    "path": "test/fixtures/sops/secrets.txt",
    "content": "{\n\t\"data\": \"ENC[AES256_GCM,data:w2jDRJR9BeIMSKE4+qnKWhfM,iv:08ACLYrUGtWriOV/ua4X6NZt57VmiTmAcnxB5V+8AUc=,tag:cVdkIO4EXAmyV3y7n/zbiA==,type:str]\",\n\t\"sops\": {\n\t\t\"kms\": null,\n\t\t\"gcp_kms\": null,\n\t\t\"azure_kv\": null,\n\t\t\"hc_vault\": null,\n\t\t\"age\": null,\n\t\t\"lastmodified\": \"2021-12-17T18:38:13Z\",\n\t\t\"mac\": \"ENC[AES256_GCM,data:8lPZmY8YgA0DqPRxLC9hVoRUXmbzaXgUBv3MHTm4iK44/6URIgJBUnPFPUbwIN7xbIgXd+QPQEMvfsmifqXorynGEwt2WtMKCPANg+2Ctf2KMmj7fGpe3HIlRhQiixip7/xzrIMbSdIRMS098D42JTvOIFNbWVQhByfN64AnDJY=,iv:wtouC/mWjhFwiJKDS6+5LqnQMcAeejElXLaL3H15jbY=,tag:6Bmemr2BMgShaMO3v4uiXw==,type:str]\",\n\t\t\"pgp\": [\n\t\t\t{\n\t\t\t\t\"created_at\": \"2021-12-17T18:38:12Z\",\n\t\t\t\t\"enc\": \"-----BEGIN PGP MESSAGE-----\\n\\nhQEMA0sXzMgpEabgAQf+KHsPp4Pp8YNtG7ChRpZO2qB/bFncWtAF9evO+RjAEahb\\nM+hzxkB5KDUSMYs0aeWeOrOqYPrjPPJxCspZtQhy8/qrC064kA7gq2PWhYAqGcKP\\ntnPI8D0SYDZBgoyHRqFuuD5TZio8swE89SxphftL0W3KkHay7WKQHj/cFqNoISNl\\nn0XeCgbacIwo5WxWz1qNFvaeo0rFFFhIhbfaegx/SWwUi1y6WK7sB0QobMRwXHj+\\nORiUWVvx/fCIMCaerPN/SjIA/DgzbZ3DWaixYXpW85Ipz7myu/zUQcWnWcGXnMRQ\\nERMYc6GyyLHwjZN1XuvXdPXvAt6vvaH4w5U9kW2l19JeAZXkcM14ivDoGwY1oLcX\\n4d2/MAS7vM7SgmcPBGmpNsJJgkWTgoc8qeFtu9u3e4e9pR4+dcJCbGQLQ5RiyM2Z\\nsyHjL6em/j4JLdtbM16orP6Q3oEPelphG7sxbDXBeA==\\n=6u1S\\n-----END PGP MESSAGE-----\\n\",\n\t\t\t\t\"fp\": \"3EF98802EEDCAF0C688B81F419546E0C123C664E\"\n\t\t\t}\n\t\t],\n\t\t\"unencrypted_suffix\": \"_unencrypted\",\n\t\t\"version\": \"3.7.1\"\n\t}\n}"
  },
  {
    "path": "test/fixtures/sops/secrets.yaml",
    "content": "hello: ENC[AES256_GCM,data:SMrBP0yM6rO1O7A9NuwDU+lRYmP+RltHEjuI2EMU9odttDwfWykU8hxsMdqexA==,iv:eOF2RRRse/D48LRfVkstSTKJQ2GvLW9L3IWpabcKtEg=,tag:JaucBpVJltT/i+qZIOGL2w==,type:str]\nexample_key: ENC[AES256_GCM,data:S3wpblMCgvzIyW/KvQ==,iv:lyAqWzJ/qJuWbEvjwh0iJrkewbIAptZTEi0S3azSnkY=,tag:X8rU5wocQ6GTVkurK3ghSw==,type:str]\n#ENC[AES256_GCM,data:l1CEO/yH5v4iDnus77GcHw==,iv:GPTiAcRYD9rhBfV/+4jyCOJ6rdjFDnvgpVPj/7UjGjA=,tag:/1f6AdIIVUYIvPUiHtVaVQ==,type:comment]\nexample_array:\n- ENC[AES256_GCM,data:H57Dlw0YTNVkqmQHRjQ=,iv:G+eegqxGo33HC09TmmYQDjR/r2SCKqDtUPR5O/xi83I=,tag:gMn0NT3GGM0lf5vLfVngMg==,type:str]\n- ENC[AES256_GCM,data:ctXWSUPJAuNIHTY5Ud4=,iv:ydRXZyuALBaaLdXm8VBUmP2iEJNKjzX4V9W4hv6eA0Q=,tag:Zo715k7a4Z92IKUTf5/aKQ==,type:str]\nexample_number: ENC[AES256_GCM,data:QrQSLxeiSZce,iv:WiLlOYU/SkGo9IcqHCGb8uqbEE6GL7kew8Cjl4BApsU=,tag:hSYiUL0ySi6o+nvF2ZPW7g==,type:float]\nexample_booleans:\n- ENC[AES256_GCM,data:GTv3uw==,iv:XSNZ5zaUeYHSs5e+E/ofx2TlXm6ZbsUuQuacM1zzxTY=,tag:nx2TASzlrTOJRWEuctVlYQ==,type:bool]\n- ENC[AES256_GCM,data:vw5I2zA=,iv:qL8VRphDMxgOjkoNMrngF5Md4PKkhne256kUcGY/Nhc=,tag:rJ5r2/hwFLHS4a9aLHcJJA==,type:bool]\nsops:\n    kms: []\n    gcp_kms: []\n    azure_kv: []\n    lastmodified: '2020-05-14T22:55:33Z'\n    mac: ENC[AES256_GCM,data:jT7Wd5mlZ7pqXJlt7if7eCKd3Qei6UXDeiZf/IHzWEPQ2NWKLAXs274N9pTSkoBO58x88vchjuF8q8pFTwQVbbtw2yGw3P3xYi4WUibPomboEtubKUjVylRHW8ftTTNniws3ndlMYQxgR17upltAS6tvdtgO/Juiq73pti459ls=,iv:UImNvKpc8v1NQc5tpbCnz/ZHIxkUWeKk62FWyp9osms=,tag:TyOSy6HsehWAKBw02jgWLg==,type:str]\n    pgp:\n    -   created_at: '2020-05-14T22:55:27Z'\n        enc: |\n            -----BEGIN PGP MESSAGE-----\n\n            hQEMAwAAAAAAAAAAAQf+OaAb9t8dZ7d909yC0wL7s4YZqiffYM6wD2/SMAN9nO/L\n            5HS+vbk8X2Np6kVxQT++o8dVCHUS9HCNcekwnAhzMT5Ibtu0V6BwHBbrkaGnxCPr\n            i1d4Ne+W1Ku7WHHEPwRVo+otNoCJpDSbtCOWHtkKHkCrLYEETn8WkrsO9VkC390/\n            qLWGHYK/CgR42bokXpKVkZgwzE1S+IOSlcCQwz9X01KqF1lv7ijtB1pMA6eYrIP3\n            SH0avz9VEOeyyXrd9Eq6qjiAHCmRos8/6CQp9qTG9Fzhi7WQQATRnvSAAHIU3nX7\n            hhrib9EEH6mqj2xF18gB6GveoorJRvs38OStPb+RKtJeAW7qE9Z+kT8eOOuBsLfD\n            XcHFD+Zqf59h89qtJGEnbV4/rW1QyAJc1vo5LkfBQTAV5QZPwUFs6vs2lT9/Jq1w\n            HcAcProHvIuY/V1s7oUnVmmZLTyD9LV83CUScvGcfw==\n            =zY+z\n            -----END PGP MESSAGE-----\n        fp: 4B17CCC82911A6E0\n    unencrypted_suffix: _unencrypted\n    version: 3.4.0\n"
  },
  {
    "path": "test/fixtures/sops/terragrunt.hcl",
    "content": "locals {\n  json = jsondecode(sops_decrypt_file(\"${get_terragrunt_dir()}/secrets.json\"))\n  yaml = yamldecode(sops_decrypt_file(\"${get_terragrunt_dir()}/secrets.yaml\"))\n  text = sops_decrypt_file(\"${get_terragrunt_dir()}/secrets.txt\")\n  env  = sops_decrypt_file(\"${get_terragrunt_dir()}/secrets.env\")\n  ini  = sops_decrypt_file(\"${get_terragrunt_dir()}/secrets.ini\")\n}\n\ninputs = {\n  json_string_array = local.json[\"example_array\"]\n  json_bool_array   = local.json[\"example_booleans\"]\n  json_string       = local.json[\"example_key\"]\n  json_number       = local.json[\"example_number\"]\n  json_hello        = local.json[\"hello\"]\n  yaml_string_array = local.yaml[\"example_array\"]\n  yaml_bool_array   = local.yaml[\"example_booleans\"]\n  yaml_string       = local.yaml[\"example_key\"]\n  yaml_number       = local.yaml[\"example_number\"]\n  yaml_hello        = local.yaml[\"hello\"]\n  text_value        = local.text\n  env_value         = local.env\n  ini_value         = local.ini\n}\n"
  },
  {
    "path": "test/fixtures/sops/test_pgp_key.asc",
    "content": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nlQOYBF69y60BCAC0Ton9jmk5/ZV3yKi2o0rY22G41dh9dLSoxTRJNKsKeSJePHK0\nqtrHriPjnHxFLXrXfYiv6FkdkPv9dhmKjMf1op/RRw0uTDZ6CsQd5C8nT9LBPiB6\nJsO89+GCwoVsPQGGVgqq7PWSbA8fJkAPBNvdQvxJnSbm4EJTcaU7XZmmY/eZnhPt\npho66X3PIQSx8Xm9Dc7jHo/7bBCoEV15OdFYi44imDW9sCdCpYTVzMF2ZRtXGylQ\nU6P5Iu3SEMTnn4jwPpyKllT4qRFdqMQ4GvqE47uRb+k/FDXP8UC6h/5GNHfbQtEO\nCMngw5vJciLEUWW1e97baBPiccLXNaBXiecXABEBAAEAB/44Mkpb1qkBRAHz5XyA\nAADx+d5JR41D/L3Z5CzZrCqrBvopONnfaWjq12GkLT+mJ/ijdSLHALnVtzxy0P5A\n6oPgESGSjnyTM3m/K9/YGSiBLiXXyM9CgxZ6rR+CK7J9+72f6u1EPLqFOly0Lq3E\ngJUuLxSGtQ5M4xSJAWCoUhHzg6rUP978RQXc4AgfkfXaq/ILGyOOa7VSz4NVs3cZ\n+WtPqlFhy5AsoMoLvXrwqLhVa9QsCiW0dYScRPD9q29wRHnKDN2nSxTuHbWu55r+\nkrevzk5gPOTdj61vz6/xD/rqNFakZDjFfZIu+srqjnLMVEPYudrH0buIAF9RFYPp\n8x+BBADTH3Q5+b+n770aFBDldIjD+biEnjnw6A396RNX2YIPqWoz0b7lQSYBM+DI\nTTm7OFmacH2DSYHxx/e2GGOOglbc2czPlWGyegSfZblwcPPT47uVY6ZWIXAsszF6\nopL/ahQ336FEl8Ws8vEjUoTXWFzF50gfmdC2xPYYmoxymmZ1IQQA2qIvbPausqO7\ntHk9Bco1MLLzP5JIAvuLUNHOj9H+bUXh5btkm2gZRIGglScljF6HT180bJ4gt07n\nH1QNpyWKuH0h0YSTDlBjLPQvElQAvxMuvT7RAWagXQo8ew+2FHbrH9Bhlys1wcG3\n+h1S2G8M6TyY0dv98TvqHV9RG+YCHTcEAMQTpJBn+yelK8LXyVrw9OmH0qmQ9TOF\nuJihLBSd7nen+l4a3yDpISk6hrb/q4AjpzMJ8fK17AEuH9OxUkO8vgtxefvnoDTe\nGYwuhszZRbWLYVDHZEkEjiGbCiqZw5530tHShA/IdBc5LMd9fwsag4Bru0cKhyCN\noe1FvVcABEHXR+O0EFRlcnJhZ3J1bnQgVGVzdHOJAUwEEwEKADYWIQQ++YgC7tyv\nDGiLgfQZVG4MEjxmTgUCXr3LrQIbAwQLCQgHBBUKCQgFFgIDAQACHgECF4AACgkQ\nGVRuDBI8Zk76SwgAkfEmyr9qOtNXpD4m8SnCrg8EJRX584hJyAu08/uB+QQV7Xzj\nAzns4oVGGpZYEB7sUd22ejw8BGe6OI4puL43fnpDcFj4UA7FAIzQTKNztWFci2Ho\nL0IAwcdzSlJb6qREMJacit8G4Aayn/bqZSNF4cu0XipRlOtK28FXP1PQThlNtSJL\nyPpYRyU6s2CLSeRRqbi1MJ4KgKcnfTvv/4+VYQ6y7/rsDnXDA0nLgzYKMqtNdBPY\nr4u7kZsGcWP8w9xQDWNJtOqj8NiRSAtnglqiYTHuEQsRjYGjH+vHqnGfw7dcK4+3\nRlc7ZPGON2NCmJ6OajedIxZR6yNiDwj1AfuXxZ0DmARevcutAQgAtV1FnBgLKuqg\nBVs09UbyrFuVJIx6M4e0IFac4bVzyfGJKjmxHx2n7D8slTZs+lcBNDy+CseAsAuc\nixv/jVAsl6saip5nUIAMmHkPkPM+BVAuqBRWoYWGFw3TL83MaYAgBkSkurx+Koxv\nw2bTumxrb9xsyiy7Bn7kaIyZU/nN8Xb0xf6QC+y7ckzaI6ZT4utrtJGXya4Cu1rp\nqUUYs/4ssWjZS7KuRwu8ijNwq/Zl0920IXH/sX/gUhL2AO5sgExC+52yjTTvhlIb\nee8tjMB4JT6Hq1RS9JGkNqcUOvJ99WlUoOtZUJU9xAN4eyPUy+r7v9wxcwoguanC\nLWaKSNe9hQARAQABAAf8CToRuLNtHLBWFspWmI8/o67Ubh5qzc9UGycSO9YVlMsM\nfZD00WPwBJq22RqCkyk0/vmDePHrD/RvRjvU6wAOsg1QQDLMh8cT8k01Elya+v3i\nzxtFyFmOLQMU4O7j547PUkemEnf/eokC20U9H/60Z+WmGpJfAMwY27bMytMVJqPJ\ngL1BNo9l0HGSTkU7mMqq48MsozTJvmCYbrVeKhnHdpAFHHSNQ9hGzgpZkpTisDk7\nqDIHhF1Nv7IRZgX4OGUbj1hBk63ao1TclhbB8d7gitvTODbMxFOF9crm7ttWtq/C\nCIBM9X5ilufEnuN2eV4LxLHeghQOGsJhl/FB8gov+wQAx/Ja/2WFzHoEc5evJtNU\nifKpaIAp4Qj673dx21Vzr43qnuNNxLuG53FvgRpfXhVRyzWmNnJByQ3NRVUv4J1j\nGXymlPPPzmAbML8874zShMnMcd1+UCZ+0dgFeB6CetVySExO0p+qUW+fioP5jrJk\nspNxtY01RE5AUSWUeQN3sdcEAOg1T1cUIEq/dgPLDQE3mPQta7fJeiDddheNgQRp\nxKHxDKSpWd1RtmiUxG0cT3z68M8aRcL/X+q731WGadNhM+yp4xg6wVTpL8USdLZr\nqqiuvMYqowryZOdvPUP8OE1lQwtWizCFOoNL+yJyKVzt7+Z0CrkE8s3li68sLMeg\nz5gDBACflcuTWLMNt3buo/31YrNWLDRxDMdKpNZ0Tpj+Kxda9+GjRGWdMZHwOsIO\nWhGnftxtbKSWu2+PabFBchiwLC1r4WHMFy/fxFFhufJtYI/c58kRd+9I0vw6/JQx\nWvGunELyeTnNu3u+uvagUSchBhNln5hZZOtpaBaDhNngm5DXM0g+iQE2BBgBCgAg\nFiEEPvmIAu7crwxoi4H0GVRuDBI8Zk4FAl69y60CGwwACgkQGVRuDBI8Zk4gogf/\nSyXhch/Ep9ESW+Zx7T+ImfQAVPBmka0NNCEvSF5i/ghTm700Iv/sLcRqhSJDiKLb\n1imeqVN6khmr6+d21uCqqgiv/6X4w6zmxH9h4uPMv3H6WnHi40kDUawR7hFctNLX\ntwArH+xCX5MJgzhqAP5Yzgsk5XLwzgjCuXz06RqtmZVW4ofOP4GWBT24Gg4TyUF9\nIbl1/QLKeYOarD4a/PcEfHhJyvnAmD68uIFr1gABJJneW8vvO4OPjLcptZMIZ1Nf\nxqUuLBAJ71OI3RSSowpI3qCpFhH2j5vICM7jX2gIL9PQEGGg2ljNJoorJIEjYZzS\n0S9IMjn4Mj7CYtAZarnIQw==\n=fO3n\n-----END PGP PRIVATE KEY BLOCK-----\n-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQENBF69y60BCAC0Ton9jmk5/ZV3yKi2o0rY22G41dh9dLSoxTRJNKsKeSJePHK0\nqtrHriPjnHxFLXrXfYiv6FkdkPv9dhmKjMf1op/RRw0uTDZ6CsQd5C8nT9LBPiB6\nJsO89+GCwoVsPQGGVgqq7PWSbA8fJkAPBNvdQvxJnSbm4EJTcaU7XZmmY/eZnhPt\npho66X3PIQSx8Xm9Dc7jHo/7bBCoEV15OdFYi44imDW9sCdCpYTVzMF2ZRtXGylQ\nU6P5Iu3SEMTnn4jwPpyKllT4qRFdqMQ4GvqE47uRb+k/FDXP8UC6h/5GNHfbQtEO\nCMngw5vJciLEUWW1e97baBPiccLXNaBXiecXABEBAAG0EFRlcnJhZ3J1bnQgVGVz\ndHOJAUwEEwEKADYWIQQ++YgC7tyvDGiLgfQZVG4MEjxmTgUCXr3LrQIbAwQLCQgH\nBBUKCQgFFgIDAQACHgECF4AACgkQGVRuDBI8Zk76SwgAkfEmyr9qOtNXpD4m8SnC\nrg8EJRX584hJyAu08/uB+QQV7XzjAzns4oVGGpZYEB7sUd22ejw8BGe6OI4puL43\nfnpDcFj4UA7FAIzQTKNztWFci2HoL0IAwcdzSlJb6qREMJacit8G4Aayn/bqZSNF\n4cu0XipRlOtK28FXP1PQThlNtSJLyPpYRyU6s2CLSeRRqbi1MJ4KgKcnfTvv/4+V\nYQ6y7/rsDnXDA0nLgzYKMqtNdBPYr4u7kZsGcWP8w9xQDWNJtOqj8NiRSAtnglqi\nYTHuEQsRjYGjH+vHqnGfw7dcK4+3Rlc7ZPGON2NCmJ6OajedIxZR6yNiDwj1AfuX\nxbkBDQRevcutAQgAtV1FnBgLKuqgBVs09UbyrFuVJIx6M4e0IFac4bVzyfGJKjmx\nHx2n7D8slTZs+lcBNDy+CseAsAucixv/jVAsl6saip5nUIAMmHkPkPM+BVAuqBRW\noYWGFw3TL83MaYAgBkSkurx+Koxvw2bTumxrb9xsyiy7Bn7kaIyZU/nN8Xb0xf6Q\nC+y7ckzaI6ZT4utrtJGXya4Cu1rpqUUYs/4ssWjZS7KuRwu8ijNwq/Zl0920IXH/\nsX/gUhL2AO5sgExC+52yjTTvhlIbee8tjMB4JT6Hq1RS9JGkNqcUOvJ99WlUoOtZ\nUJU9xAN4eyPUy+r7v9wxcwoguanCLWaKSNe9hQARAQABiQE2BBgBCgAgFiEEPvmI\nAu7crwxoi4H0GVRuDBI8Zk4FAl69y60CGwwACgkQGVRuDBI8Zk4gogf/SyXhch/E\np9ESW+Zx7T+ImfQAVPBmka0NNCEvSF5i/ghTm700Iv/sLcRqhSJDiKLb1imeqVN6\nkhmr6+d21uCqqgiv/6X4w6zmxH9h4uPMv3H6WnHi40kDUawR7hFctNLXtwArH+xC\nX5MJgzhqAP5Yzgsk5XLwzgjCuXz06RqtmZVW4ofOP4GWBT24Gg4TyUF9Ibl1/QLK\neYOarD4a/PcEfHhJyvnAmD68uIFr1gABJJneW8vvO4OPjLcptZMIZ1NfxqUuLBAJ\n71OI3RSSowpI3qCpFhH2j5vICM7jX2gIL9PQEGGg2ljNJoorJIEjYZzS0S9IMjn4\nMj7CYtAZarnIQw==\n=dO7W\n-----END PGP PUBLIC KEY BLOCK-----\n"
  },
  {
    "path": "test/fixtures/sops-errors/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/sops-errors/file.yaml",
    "content": "sops:\n  kms:\n    - arn: arn:aws:kms:us-east-1:123456789012:key/abcd1234-a123-456a-a12b-a123b4cd56ef\n      created_at: '2024-05-31T12:00:00Z'\n      enc: AQICAHh/wUk47iRfX5z0YYrj2L8dXbIf3+m0XL7B3BQmXU1F3wG8O5k3AoUmK9AekxyAmNndAAAAcTB2BgkqhkiG9w0BBwagZTBjAgEAMGkGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMzjt8jKK8XHdR+NTbAgEQgDtLGz9iI4HhXJTh3x/aRb9nsJhEALwJhHMRUnYtQ==\n  gcp_kms: []\n  azure_kv: []\n  hc_vault: []\n  age: []\n  lastmodified: '2024-05-31T12:00:00Z'\n  mac: ENC[AES256_GCM,data:crypteddata,iv:ivvalue,tag:tagvalue]\n  pgp: []\n  encrypted_regex: ^(data|stringData)$\n  version: 3.7.1\n\napiVersion: v1\nkind: Secret\nmetadata:\n  name: example-secret\n  namespace: default\ndata:\n  example-key: ENC[AES256_GCM,data:crypteddata,iv:ivvalue,tag:tagvalue]\n"
  },
  {
    "path": "test/fixtures/sops-errors/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/sops-errors/terragrunt.hcl",
    "content": "locals {\n  secret_vars = yamldecode(sops_decrypt_file(\"${get_terragrunt_dir()}/file.yaml\"))\n}\n"
  },
  {
    "path": "test/fixtures/sops-kms/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/sops-kms/main.tf",
    "content": "variable \"json_string_array\" {\n  type = list(string)\n}\n\nvariable \"json_bool_array\" {\n  type = list(bool)\n}\n\nvariable \"json_string\" {\n  type = string\n}\n\nvariable \"json_number\" {\n  type = number\n}\n\nvariable \"json_hello\" {\n  type = string\n}\n\nvariable \"yaml_string_array\" {\n  type = list(string)\n}\n\nvariable \"yaml_bool_array\" {\n  type = list(bool)\n}\n\nvariable \"yaml_string\" {\n  type = string\n}\n\nvariable \"yaml_number\" {\n  type = number\n}\n\nvariable \"yaml_hello\" {\n  type = string\n}\n\nvariable \"text_value\" {\n  type = string\n}\n\nvariable \"env_value\" {\n  type = string\n}\n\nvariable \"ini_value\" {\n  type = string\n}\n\noutput \"json_string_array\" {\n  value = var.json_string_array\n}\n\noutput \"json_bool_array\" {\n  value = var.json_bool_array\n}\n\noutput \"json_string\" {\n  value = var.json_string\n}\n\noutput \"json_number\" {\n  value = var.json_number\n}\n\noutput \"json_hello\" {\n  value = var.json_hello\n}\n\noutput \"yaml_string_array\" {\n  value = var.yaml_string_array\n}\n\noutput \"yaml_bool_array\" {\n  value = var.yaml_bool_array\n}\n\noutput \"yaml_string\" {\n  value = var.yaml_string\n}\n\noutput \"yaml_number\" {\n  value = var.yaml_number\n}\n\noutput \"yaml_hello\" {\n  value = var.yaml_hello\n}\n\noutput \"text_value\" {\n  value = var.text_value\n}\n\noutput \"env_value\" {\n  value = var.env_value\n}\n\noutput \"ini_value\" {\n  value = var.ini_value\n}\n"
  },
  {
    "path": "test/fixtures/sops-kms/secrets.env",
    "content": "DB_USER=ENC[AES256_GCM,data:5SBE7/M=,iv:bdPwqOI4bg1NW3x2a37ye499E3pq/S0A9QGX18TIItA=,tag:O32iYf2WobYPVSWzDXNugg==,type:str]\nDB_PASSWORD=ENC[AES256_GCM,data:bO8vY9Oo,iv:zyTBtocanf47wtTlMO1KvAQ6aBkgZ2Qe1qYYu4UsrEA=,tag:fzkv+7EDZ4ZwC8us70lnCQ==,type:str]\nsops_kms__list_0__map_arn=arn:aws:kms:us-east-1:654954254241:key/7a8b0c4e-ff3c-49d0-93ba-15e3ca3488fb\nsops_kms__list_0__map_aws_profile=\nsops_kms__list_0__map_created_at=2025-07-24T15:34:04Z\nsops_kms__list_0__map_enc=AQICAHhHPy++UbYHYaSeo34uZaMsxPhT+PDk7Hd1dYS/NZi4YQGErWXqYJ5KNWUbvgWpfCQpAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMcyzrCuxYU7m3+/hEAgEQgDs4uZ3YZNLTvvqK8fIvgIdvJqlI1gLezOt0Zu/OjxSqf/tBeqTEt1xu3C+vr9hC4eJ7DygTThnaJtZXkA==\nsops_lastmodified=2025-07-24T15:34:04Z\nsops_mac=ENC[AES256_GCM,data:QmNU4HXBZHnq8QDxequHJI+6tfTToeC1x4AL68uv2ZG/NMhdlOQR8pl1EK2QgtbzF7z7tCv/asoNkqeSbaSoA8qc0wXitb10/2BLYAFRYIXa9xtsZYLaoNBH2F8ra2EJ054lzytRQwUK9v2UKlQAUj9FstvH4aMeud2i0KTtS40=,iv:F7quUFy7THBu9h+PA6fmoUm30dRZDUVhYS5nfplb2gI=,tag:OL6O79r1h1afAHzx/nzeag==,type:str]\nsops_unencrypted_suffix=_unencrypted\nsops_version=3.9.0\n"
  },
  {
    "path": "test/fixtures/sops-kms/secrets.ini",
    "content": "[terragrunt]\nuser     = ENC[AES256_GCM,data:zqFVE/o=,iv:AZfrdCSXrdCRymVX/NvRpJGChHmOKgXGW3AZ/B9kDhE=,tag:9Evk0Y8vPBeA8F/d2Ego8g==,type:str]\npassword = ENC[AES256_GCM,data:uTMlh3FM,iv:2uxdTY5WdaHS73EuxC2lYTVWe23G4EiB33+yMlsB3hI=,tag:j/MwW5I/Jk/LEVvgCqCVqA==,type:str]\n\n[sops]\nunencrypted_suffix           = _unencrypted\nkms__list_0__map_enc         = AQICAHhHPy++UbYHYaSeo34uZaMsxPhT+PDk7Hd1dYS/NZi4YQE2Q/vctA8UtvHJZj37nmdNAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMRL1oot1M7wW3HFtQAgEQgDtoMixCYF3uAV0wEMD3Rh5ja5TMKqmi66RXj39Uqd1Bg64NViBCpiu4UYe/IRrrSy2hUAaSSjLbjz00aQ==\nkms__list_0__map_arn         = arn:aws:kms:us-east-1:654954254241:key/7a8b0c4e-ff3c-49d0-93ba-15e3ca3488fb\nlastmodified                 = 2025-07-24T15:33:38Z\nversion                      = 3.9.0\nmac                          = ENC[AES256_GCM,data:APXNbbAiNfEXb2bR6JNmhoIas/ujaUaaMGhSArBGzqmAR1rknZVWH2E63SdHtpqMTA/89lLrXFouOSlfjM2trZHOXz/n+dOWXAefp/VnM8deDeall2OqlqR0fiHFByguG9wVAUXWwOLKbgwy1idrvbgBxCrgD/6rnIsVkJoT2oE=,iv:EsXaRQbjsXUGHiRauCjJ3uU/5vxlSerchnFwglp/rgI=,tag:ZnW59Pv++aTT3S/KIevfGg==,type:str]\nkms__list_0__map_aws_profile = \nkms__list_0__map_created_at  = 2025-07-24T15:33:38Z\n"
  },
  {
    "path": "test/fixtures/sops-kms/secrets.json",
    "content": "{\n\t\"hello\": \"ENC[AES256_GCM,data:o6qZF1pvmdZrYSqscrrQjZpdG/8tNgcKz5QZT7bBl5M9jP2jEJCP9/S3yREDGQ==,iv:MZqGHMS6KgmDCHr/YRiTJwHkVkGfznUi9DGyfEpNIck=,tag:adcGOm/IRfgpQUqK7HtRTQ==,type:str]\",\n\t\"example_key\": \"ENC[AES256_GCM,data:eQ5G+CbF+T4cMpEoKw==,iv:vUack1lOe/MP8Hao2pI0ScuTkMxKwA1z/7LRH4uG0HQ=,tag:sLBflkEv4nwsp8MCYj+NaQ==,type:str]\",\n\t\"example_array\": [\n\t\t\"ENC[AES256_GCM,data:SeV9rYVy6LEWrPPm28U=,iv:Nl96BPCQfAS6BN+TzFu4jE8GIyF2+DcNgXaVK3WMMJ4=,tag:C8SoQZIegNghoxxfao2tHw==,type:str]\",\n\t\t\"ENC[AES256_GCM,data:OIOHVMt9F5BfiAKILNI=,iv:diMgpmxlysLCzKd5Vk8dsSNJQCVkkupfFfyTrt1s9hM=,tag:6WwXTn9x72PWS+R0hY3lVg==,type:str]\"\n\t],\n\t\"example_number\": \"ENC[AES256_GCM,data:J71l1wpx1E9rrw==,iv:hKtVkjmrJMenqhMNwa+Vun12P8aP8JbnbVOVpvNl5Ts=,tag:DUfWUTQdSu8iIQwwwOaehg==,type:float]\",\n\t\"example_booleans\": [\n\t\t\"ENC[AES256_GCM,data:U4LF8w==,iv:m0c/cNaO7zRnCDP4ChDe+ByoHF6RDOG7VWTm5CqqzD0=,tag:Bzk/HLrAj+PNdHV9YLUOuw==,type:bool]\",\n\t\t\"ENC[AES256_GCM,data:qhyx3UA=,iv:HFV0KBpcsGHVz5LTwcq4IlvHnxHtRaYmqyNbHxpCQVU=,tag:JCC9psMzdR60+dvdQDsFcQ==,type:bool]\"\n\t],\n\t\"sops\": {\n\t\t\"kms\": [\n\t\t\t{\n\t\t\t\t\"arn\": \"arn:aws:kms:us-east-1:654954254241:key/7a8b0c4e-ff3c-49d0-93ba-15e3ca3488fb\",\n\t\t\t\t\"created_at\": \"2025-07-24T15:59:32Z\",\n\t\t\t\t\"enc\": \"AQICAHhHPy++UbYHYaSeo34uZaMsxPhT+PDk7Hd1dYS/NZi4YQGZJsBX2LiTUhKhEBXvG36AAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMua4SiSLgUrvWDL3oAgEQgDsJ5w69NN4wwdU7TFrAW2mjmA/DKe6gkXmbcPVTEutg5T463W/4186nD/OElo3HnvHTeK7goyR+rt7rFg==\",\n\t\t\t\t\"aws_profile\": \"\"\n\t\t\t}\n\t\t],\n\t\t\"gcp_kms\": null,\n\t\t\"azure_kv\": null,\n\t\t\"hc_vault\": null,\n\t\t\"age\": null,\n\t\t\"lastmodified\": \"2025-07-24T15:59:32Z\",\n\t\t\"mac\": \"ENC[AES256_GCM,data:vBrDreCGKhoXdvkae6iHub+VRNELIWAhxtivr4HUpPfLluTsm8gW4U/O3xPsrzf7/djNw2dnBuc2cuXikiO7i6i9IkhcXDOFBAoGT9X8DMq8a4fVJNYZLzaDGKC3xJ+WcehDgMTgik/9Ub8J72KQu2pwN9+V3qP5yUQjU9RPYuw=,iv:rV+vwPVHHQU3rNgy7FZZEG4BLrvYeCBqxAywS5CbyGE=,tag:uOOtxPsrrHD5IivXOf+jDA==,type:str]\",\n\t\t\"pgp\": null,\n\t\t\"unencrypted_suffix\": \"_unencrypted\",\n\t\t\"version\": \"3.9.0\"\n\t}\n}"
  },
  {
    "path": "test/fixtures/sops-kms/secrets.txt",
    "content": "{\n\t\"data\": \"ENC[AES256_GCM,data:1B6Fe4qPr/xf9PlfUylJ7uRg,iv:In7WM8XpFjFk94MY72ohtx7+2Rp5Xlnayw2uDMFJE20=,tag:P1drwZyPIpwFEc5Eu366Dw==,type:str]\",\n\t\"sops\": {\n\t\t\"kms\": [\n\t\t\t{\n\t\t\t\t\"arn\": \"arn:aws:kms:us-east-1:654954254241:key/7a8b0c4e-ff3c-49d0-93ba-15e3ca3488fb\",\n\t\t\t\t\"created_at\": \"2025-07-24T15:33:14Z\",\n\t\t\t\t\"enc\": \"AQICAHhHPy++UbYHYaSeo34uZaMsxPhT+PDk7Hd1dYS/NZi4YQGPi8Jw+TmeePkWCSS3NunRAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMPg8lkNvPSLmtR27zAgEQgDscd3xjaWMdupWiHNojKSY5lQ1cROdfikExD1OJXqzoyPrEaFZXkPGFdB04+MH8v0trJbu5Vz7RtSawOg==\",\n\t\t\t\t\"aws_profile\": \"\"\n\t\t\t}\n\t\t],\n\t\t\"gcp_kms\": null,\n\t\t\"azure_kv\": null,\n\t\t\"hc_vault\": null,\n\t\t\"age\": null,\n\t\t\"lastmodified\": \"2025-07-24T15:33:14Z\",\n\t\t\"mac\": \"ENC[AES256_GCM,data:vRif143YoN9NMKCx6LyjY5xIJBXkSK2a9RKcE9FLkAIqg7mAY7deuCJMlH1Z2O5gLbp42rWTICQsyhc7moSdl02F1zIt99qA92B5r8BzVmb6znKtQKDOc6Ct+IS9FnxYnalq2k8xBQUTgnk3E9OvUNHfxKP75hhV8ApD7XC44aQ=,iv:6ZDuf1Bzaqv2rpGVdf6GFK7w7QbLg/xb/ClQF04Djq8=,tag:Y/LVGExcN7qszMYWN3hRWQ==,type:str]\",\n\t\t\"pgp\": null,\n\t\t\"unencrypted_suffix\": \"_unencrypted\",\n\t\t\"version\": \"3.9.0\"\n\t}\n}"
  },
  {
    "path": "test/fixtures/sops-kms/secrets.yaml",
    "content": "hello: ENC[AES256_GCM,data:tum7WMVVS82kdgyT8SwiCGzeUH+0Pdu344QSPU6wAm/1PvGiU3Rid0d4sOxGwA==,iv:jCff4TJ64ojlCUXkT2aVEkoaBKK9ylL1PivYfaq0w/A=,tag:5A/p4exCnzwe/w8VnxnZfQ==,type:str]\nexample_key: ENC[AES256_GCM,data:g4rdOvKnuxnT1gZvLw==,iv:QeVxFlWaTCvBPszEoeHz2kN3zKYv3fmkrx376FfK1mg=,tag:eXmqauzmULXZy6DV9pZX7A==,type:str]\n#ENC[AES256_GCM,data:/t9xY2ZLcizd8D1vt856kg==,iv:IOz3z+F2FR7G3gKGMsCX8i3iYYiE/UdNuecW/VQNyso=,tag:tmzC7N0vsciDAkm9HlknEA==,type:comment]\nexample_array:\n    - ENC[AES256_GCM,data:JRBECLpU8pSa3sA7q9c=,iv:syrOYsTRZeNOh3LOgyf1QNxvYxElG3ZbV8mlUn76RSc=,tag:W35bMO9sMSkmfS4b8IVGLQ==,type:str]\n    - ENC[AES256_GCM,data:7OxMWoWvSqhyfo9uR1g=,iv:sBymoOx4+Zee8aqwz2glRnn6v+6ev+QqhOGbzHQt/lU=,tag:9J4XnbcLsMP6c0k0W0CMyw==,type:str]\nexample_number: ENC[AES256_GCM,data:LHZtKpccUfe+,iv:KneDdLG5YPjCo77E3ntmCSbBcxD4CxsQtlQwIiiwG9Y=,tag:bWItFosaSiWay+92HuHkMA==,type:float]\nexample_booleans:\n    - ENC[AES256_GCM,data:aHMD0w==,iv:gjqUcMlQOAYk2Vzb5V50KMMXThb4xTUG6mb82O+8/Ho=,tag:PllRHZK75izA+gHbNpw8Vg==,type:bool]\n    - ENC[AES256_GCM,data:Sto2lek=,iv:sfzI32Xr7NRjz+7TUYJtIPCOiZNhct1GRbce7esffEs=,tag:zfmuXmVFa6gWhdtowacYMg==,type:bool]\nsops:\n    kms:\n        - arn: arn:aws:kms:us-east-1:654954254241:key/7a8b0c4e-ff3c-49d0-93ba-15e3ca3488fb\n          created_at: \"2025-07-24T15:32:43Z\"\n          enc: AQICAHhHPy++UbYHYaSeo34uZaMsxPhT+PDk7Hd1dYS/NZi4YQGvMYFjZIQpoLfucF1R1jk7AAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMebHdbrjzieeOBw2mAgEQgDsr+TrXiDCf1PXrezG8DyDyP+2bmhxFJfIXseQa0KaHyBj/dexOwTy9T1GRYhPw6NxCND+gWVQhp9zctA==\n          aws_profile: \"\"\n    gcp_kms: []\n    azure_kv: []\n    hc_vault: []\n    age: []\n    lastmodified: \"2025-07-24T15:32:43Z\"\n    mac: ENC[AES256_GCM,data:r4TUGC6sZUobE+tdgCjeZREvVDXISZwwKf+9FQPv0OBa8BsSpOkz/IzBn3sadymOayAZTQZsuWzlKImZ704jClNzoFiqQKUiGnwE3fix2G87Xy5u7ithYBqq+T5nMIoRm/Tv38UVxQ1MwFLLuEUbthbZXSdM32xYOO1VnAE+RB4=,iv:II7oQfqIOKSSX49UTdhwmdYeoY1qHYx8QgK0/f/LlEQ=,tag:pLY1NUST7cQZXtlPmkuQWw==,type:str]\n    pgp: []\n    unencrypted_suffix: _unencrypted\n    version: 3.9.0\n"
  },
  {
    "path": "test/fixtures/sops-kms/terragrunt.hcl",
    "content": "locals {\n  json = jsondecode(sops_decrypt_file(\"${get_terragrunt_dir()}/secrets.json\"))\n  yaml = yamldecode(sops_decrypt_file(\"${get_terragrunt_dir()}/secrets.yaml\"))\n  text = sops_decrypt_file(\"${get_terragrunt_dir()}/secrets.txt\")\n  env  = sops_decrypt_file(\"${get_terragrunt_dir()}/secrets.env\")\n  ini  = sops_decrypt_file(\"${get_terragrunt_dir()}/secrets.ini\")\n}\n\ninputs = {\n  json_string_array = local.json[\"example_array\"]\n  json_bool_array   = local.json[\"example_booleans\"]\n  json_string       = local.json[\"example_key\"]\n  json_number       = local.json[\"example_number\"]\n  json_hello        = local.json[\"hello\"]\n  yaml_string_array = local.yaml[\"example_array\"]\n  yaml_bool_array   = local.yaml[\"example_booleans\"]\n  yaml_string       = local.yaml[\"example_key\"]\n  yaml_number       = local.yaml[\"example_number\"]\n  yaml_hello        = local.yaml[\"hello\"]\n  text_value        = local.text\n  env_value         = local.env\n  ini_value         = local.ini\n}\n"
  },
  {
    "path": "test/fixtures/sops-missing/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/sops-missing/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/sops-missing/terragrunt.hcl",
    "content": "locals {\n  secret_vars = yamldecode(sops_decrypt_file(\"${get_terragrunt_dir()}/missing.yaml\"))\n}\n"
  },
  {
    "path": "test/fixtures/source-map/modules/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/source-map/modules/app/main.tf",
    "content": "variable \"name\" {}\n\nvariable \"vpc_id\" {}\n\noutput \"app_url\" {\n  value = \"https://${var.name}.${var.vpc_id}.foo.io\"\n}\n"
  },
  {
    "path": "test/fixtures/source-map/modules/vpc/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/source-map/modules/vpc/main.tf",
    "content": "variable \"name\" {}\n\noutput \"vpc_id\" {\n  value = \"vpc-${var.name}-asdf1234\"\n}\n"
  },
  {
    "path": "test/fixtures/source-map/multiple-match/terragrunt-vpc/terragrunt.hcl",
    "content": "terraform {\n  source = \"git::ssh://git@github.com/gruntwork-io/another-dont-exist.git//fixtures/source-map/modules/vpc?ref=master\"\n}\n\ninputs = {\n  name = \"terragrunt\"\n}\n"
  },
  {
    "path": "test/fixtures/source-map/multiple-match/terratest-vpc/terragrunt.hcl",
    "content": "terraform {\n  source = \"git::ssh://git@github.com/gruntwork-io/i-dont-exist.git//fixtures/source-map/modules/vpc?ref=master\"\n}\n\ninputs = {\n  name = \"terratest\"\n}\n"
  },
  {
    "path": "test/fixtures/source-map/multiple-only-one-match/terragrunt-vpc/terragrunt.hcl",
    "content": "terraform {\n  source = \"../../modules/vpc\"\n}\n\ninputs = {\n  name = \"terragrunt\"\n}\n"
  },
  {
    "path": "test/fixtures/source-map/multiple-only-one-match/terratest-vpc/terragrunt.hcl",
    "content": "terraform {\n  source = \"git::ssh://git@github.com/gruntwork-io/i-dont-exist.git//fixtures/source-map/modules/vpc?ref=master\"\n}\n\ninputs = {\n  name = \"terratest\"\n}\n"
  },
  {
    "path": "test/fixtures/source-map/multiple-with-dependency/app/terragrunt.hcl",
    "content": "terraform {\n  source = \"git::ssh://git@github.com/gruntwork-io/another-dont-exist.git//fixtures/source-map/modules/app?ref=master\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n\ninputs = {\n  name   = \"terragrunt\"\n  vpc_id = dependency.vpc.outputs.vpc_id\n}\n"
  },
  {
    "path": "test/fixtures/source-map/multiple-with-dependency/vpc/terragrunt.hcl",
    "content": "terraform {\n  source = \"git::ssh://git@github.com/gruntwork-io/i-dont-exist.git//fixtures/source-map/modules/vpc?ref=master\"\n}\n\ninputs = {\n  name = \"terragrunt\"\n}\n"
  },
  {
    "path": "test/fixtures/source-map/multiple-with-dependency-same-url/app/terragrunt.hcl",
    "content": "terraform {\n  source = \"git::ssh://git@github.com/gruntwork-io/i-dont-exist.git//fixtures/source-map/modules/app?ref=master\"\n}\n\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n\ninputs = {\n  name   = \"terragrunt\"\n  vpc_id = dependency.vpc.outputs.vpc_id\n}\n"
  },
  {
    "path": "test/fixtures/source-map/multiple-with-dependency-same-url/vpc/terragrunt.hcl",
    "content": "terraform {\n  source = \"git::ssh://git@github.com/gruntwork-io/i-dont-exist.git//fixtures/source-map/modules/vpc?ref=master\"\n}\n\ninputs = {\n  name = \"terragrunt\"\n}\n"
  },
  {
    "path": "test/fixtures/source-map/single/terragrunt.hcl",
    "content": "terraform {\n  source = \"git::ssh://git@github.com/gruntwork-io/i-dont-exist.git//fixtures/source-map/modules/vpc?ref=master\"\n}\n\ninputs = {\n  name = \"terragrunt\"\n}\n"
  },
  {
    "path": "test/fixtures/source-map/slashes-in-ref/terragrunt.hcl",
    "content": "terraform {\n  source = \"git::ssh://git@github.com/gruntwork-io/i-dont-exist.git//test/fixtures/download/hello-world-no-remote\"\n}\n\ninputs = {\n  name = \"terragrunt\"\n}\n"
  },
  {
    "path": "test/fixtures/stack/disjoint/a/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stack/disjoint/a/main.tf",
    "content": "resource \"null_resource\" \"a\" {}\n\noutput \"a\" {\n  value = null_resource.a.id\n}\n"
  },
  {
    "path": "test/fixtures/stack/disjoint/a/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/stack/disjoint/b/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stack/disjoint/b/main.tf",
    "content": "resource \"null_resource\" \"b\" {}\n\noutput \"b\" {\n  value = null_resource.b.id\n}\n"
  },
  {
    "path": "test/fixtures/stack/disjoint/b/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/stack/disjoint/c/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stack/disjoint/c/main.tf",
    "content": "resource \"null_resource\" \"c\" {}\n\noutput \"c\" {\n  value = null_resource.c.id\n}\n"
  },
  {
    "path": "test/fixtures/stack/disjoint/c/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/stack/disjoint-symlinks/a/terragrunt.hcl",
    "content": "terraform {\n  source = \"../module\"\n}\n"
  },
  {
    "path": "test/fixtures/stack/disjoint-symlinks/module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stack/disjoint-symlinks/module/main.tf",
    "content": "resource \"null_resource\" \"a\" {}\n\noutput \"a\" {\n  value = null_resource.a.id\n}\n"
  },
  {
    "path": "test/fixtures/stack/mgmt/bastion-host/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stack/mgmt/bastion-host/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\n# Create an arbitrary local resource\nresource \"null_resource\" \"text\" {\n  provisioner \"local-exec\" {\n    command = \"echo '[I am a bastion host template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}]'\"\n  }\n}\n\noutput \"text\" {\n  value = \"[I am a bastion host template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}]\"\n}\n\nvariable \"terraform_remote_state_s3_bucket\" {\n  description = \"The name of the S3 bucket where Terraform remote state is stored\"\n  type        = string\n}\n\ndata \"terraform_remote_state\" \"vpc\" {\n  backend = \"s3\"\n  config = {\n    region = \"us-west-2\"\n    bucket = var.terraform_remote_state_s3_bucket\n    key    = \"mgmt/vpc/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/stack/mgmt/bastion-host/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../vpc\", \"../kms-master-key\"]\n}\n"
  },
  {
    "path": "test/fixtures/stack/mgmt/kms-master-key/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stack/mgmt/kms-master-key/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\n# Create an arbitrary local resource\nresource \"null_resource\" \"text\" {\n  provisioner \"local-exec\" {\n    command = \"echo '[I am a kms-master-key template.]'\"\n  }\n}\n\noutput \"text\" {\n  value = \"[I am a kms-master-key template.]\"\n}\n"
  },
  {
    "path": "test/fixtures/stack/mgmt/kms-master-key/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\n"
  },
  {
    "path": "test/fixtures/stack/mgmt/vpc/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stack/mgmt/vpc/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\n# Create an arbitrary local resource\nresource \"null_resource\" \"text\" {\n  provisioner \"local-exec\" {\n    command = \"echo '[I am a mgmt vpc template. I have no dependencies.]'\"\n  }\n}\n\noutput \"text\" {\n  value = \"[I am a mgmt vpc template. I have no dependencies.]\"\n}\n"
  },
  {
    "path": "test/fixtures/stack/mgmt/vpc/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\n"
  },
  {
    "path": "test/fixtures/stack/root.hcl",
    "content": "# Configure Terragrunt to automatically store tfstate files in an S3 bucket\nremote_state {\n  backend = \"s3\"\n  config = {\n    encrypt = true\n    bucket = \"__FILL_IN_BUCKET_NAME__\"\n    key = \"${path_relative_to_include()}/terraform.tfstate\"\n    region = \"us-west-2\"\n    # Intentionally keeping this at the old (deprecated) name of \"lock_table\" instead of \"dynamodb_table\" to test for\n    # backwards compatibility\n    lock_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n  }\n}\n\ninputs = {\n  terraform_remote_state_s3_bucket = \"__FILL_IN_BUCKET_NAME__\"\n}\n\nerrors {\n  retry \"s3_errors\" {\n    retryable_errors = [\n      \"(?s).*A conflicting conditional operation is currently in progress against this resource. Please try again.*\"\n    ]\n    max_attempts = 3\n    sleep_interval_sec = 5\n  }\n}\n"
  },
  {
    "path": "test/fixtures/stack/stage/backend-app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stack/stage/backend-app/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\n# Create an arbitrary local resource\nresource \"null_resource\" \"text\" {\n  provisioner \"local-exec\" {\n    command = \"echo '[I am a backend-app template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}, bastion-host = ${data.terraform_remote_state.bastion_host.outputs.text}, mysql = ${data.terraform_remote_state.mysql.outputs.text}, search-app = ${data.terraform_remote_state.search_app.outputs.text}]'\"\n  }\n}\n\noutput \"text\" {\n  value = \"[I am a backend-app template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}, bastion-host = ${data.terraform_remote_state.bastion_host.outputs.text}, mysql = ${data.terraform_remote_state.mysql.outputs.text}, search-app = ${data.terraform_remote_state.search_app.outputs.text}]\"\n}\n\nvariable \"terraform_remote_state_s3_bucket\" {\n  description = \"The name of the S3 bucket where Terraform remote state is stored\"\n  type        = string\n}\n\ndata \"terraform_remote_state\" \"vpc\" {\n  backend = \"s3\"\n  config = {\n    region = \"us-west-2\"\n    bucket = var.terraform_remote_state_s3_bucket\n    key    = \"stage/vpc/terraform.tfstate\"\n  }\n}\n\ndata \"terraform_remote_state\" \"mysql\" {\n  backend = \"s3\"\n  config = {\n    region = \"us-west-2\"\n    bucket = var.terraform_remote_state_s3_bucket\n    key    = \"stage/mysql/terraform.tfstate\"\n  }\n}\n\ndata \"terraform_remote_state\" \"search_app\" {\n  backend = \"s3\"\n  config = {\n    region = \"us-west-2\"\n    bucket = var.terraform_remote_state_s3_bucket\n    key    = \"stage/search-app/terraform.tfstate\"\n  }\n}\n\ndata \"terraform_remote_state\" \"bastion_host\" {\n  backend = \"s3\"\n  config = {\n    region = \"us-west-2\"\n    bucket = var.terraform_remote_state_s3_bucket\n    key    = \"mgmt/bastion-host/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/stack/stage/backend-app/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../vpc\", \"../../mgmt/bastion-host\", \"../mysql\", \"../search-app\"]\n}\n\n"
  },
  {
    "path": "test/fixtures/stack/stage/frontend-app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stack/stage/frontend-app/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\n# Create an arbitrary local resource\nresource \"null_resource\" \"text\" {\n  provisioner \"local-exec\" {\n    command = \"echo '[I am a frontend-app template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}, bastion-host = ${data.terraform_remote_state.bastion_host.outputs.text}, backend-app = ${data.terraform_remote_state.backend_app.outputs.text}]'\"\n  }\n}\n\noutput \"text\" {\n  value = \"[I am a frontend-app template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}, bastion-host = ${data.terraform_remote_state.bastion_host.outputs.text}, backend-app = ${data.terraform_remote_state.backend_app.outputs.text}]\"\n}\n\nvariable \"terraform_remote_state_s3_bucket\" {\n  description = \"The name of the S3 bucket where Terraform remote state is stored\"\n  type        = string\n}\n\ndata \"terraform_remote_state\" \"vpc\" {\n  backend = \"s3\"\n  config = {\n    region = \"us-west-2\"\n    bucket = var.terraform_remote_state_s3_bucket\n    key    = \"stage/vpc/terraform.tfstate\"\n  }\n}\n\ndata \"terraform_remote_state\" \"backend_app\" {\n  backend = \"s3\"\n  config = {\n    region = \"us-west-2\"\n    bucket = var.terraform_remote_state_s3_bucket\n    key    = \"stage/backend-app/terraform.tfstate\"\n  }\n}\n\ndata \"terraform_remote_state\" \"bastion_host\" {\n  backend = \"s3\"\n  config = {\n    region = \"us-west-2\"\n    bucket = var.terraform_remote_state_s3_bucket\n    key    = \"mgmt/bastion-host/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/stack/stage/frontend-app/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../vpc\", \"../../mgmt/bastion-host\", \"../backend-app\"]\n}\n\n"
  },
  {
    "path": "test/fixtures/stack/stage/mysql/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stack/stage/mysql/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\n# Create an arbitrary local resource\nresource \"null_resource\" \"text\" {\n  provisioner \"local-exec\" {\n    command = \"echo '[I am a mysql template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}]'\"\n  }\n}\n\noutput \"text\" {\n  value = \"[I am a mysql template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}]\"\n}\n\nvariable \"terraform_remote_state_s3_bucket\" {\n  description = \"The name of the S3 bucket where Terraform remote state is stored\"\n  type        = string\n}\n\ndata \"terraform_remote_state\" \"vpc\" {\n  backend = \"s3\"\n  config = {\n    region = \"us-west-2\"\n    bucket = var.terraform_remote_state_s3_bucket\n    key    = \"stage/vpc/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/stack/stage/mysql/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../vpc\"]\n}\n\n"
  },
  {
    "path": "test/fixtures/stack/stage/redis/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stack/stage/redis/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\n# Create an arbitrary local resource\nresource \"null_resource\" \"text\" {\n  provisioner \"local-exec\" {\n    command = \"echo '[I am a redis template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}]'\"\n  }\n}\n\noutput \"text\" {\n  value = \"[I am a redis template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}]\"\n}\n\nvariable \"terraform_remote_state_s3_bucket\" {\n  description = \"The name of the S3 bucket where Terraform remote state is stored\"\n  type        = string\n}\n\ndata \"terraform_remote_state\" \"vpc\" {\n  backend = \"s3\"\n  config = {\n    region = \"us-west-2\"\n    bucket = var.terraform_remote_state_s3_bucket\n    key    = \"stage/vpc/terraform.tfstate\"\n  }\n}\n\n"
  },
  {
    "path": "test/fixtures/stack/stage/redis/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../vpc\"]\n}\n\n"
  },
  {
    "path": "test/fixtures/stack/stage/search-app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stack/stage/search-app/example-module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stack/stage/search-app/example-module/main.tf",
    "content": "# Create an arbitrary local resource\nresource \"null_resource\" \"test\" {\n  provisioner \"local-exec\" {\n    command = \"echo Hello, World!\"\n  }\n}\n\noutput \"text\" {\n  value = \"[I am an example module template.]\"\n}\n"
  },
  {
    "path": "test/fixtures/stack/stage/search-app/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\n# Create an arbitrary local resource\nresource \"null_resource\" \"test\" {\n  provisioner \"local-exec\" {\n    command = \"echo '[I am a search-app template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}, redis = ${data.terraform_remote_state.redis.outputs.text}, example_module = ${module.example_module.text}]'\"\n  }\n}\n\nmodule \"example_module\" {\n  source = \"./example-module\"\n}\n\nvariable \"terraform_remote_state_s3_bucket\" {\n  description = \"The name of the S3 bucket where Terraform remote state is stored\"\n  type        = string\n}\n\ndata \"terraform_remote_state\" \"vpc\" {\n  backend = \"s3\"\n  config = {\n    region = \"us-west-2\"\n    bucket = var.terraform_remote_state_s3_bucket\n    key    = \"stage/vpc/terraform.tfstate\"\n  }\n}\n\ndata \"terraform_remote_state\" \"redis\" {\n  backend = \"s3\"\n  config = {\n    region = \"us-west-2\"\n    bucket = var.terraform_remote_state_s3_bucket\n    key    = \"stage/redis/terraform.tfstate\"\n  }\n}\n\noutput \"text\" {\n  value = \"[I am a search-app template. Data from my dependencies: vpc = ${data.terraform_remote_state.vpc.outputs.text}, redis = ${data.terraform_remote_state.redis.outputs.text}, example_module = ${module.example_module.text}]\"\n}\n"
  },
  {
    "path": "test/fixtures/stack/stage/search-app/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../vpc\", \"../redis\"]\n}\n\n"
  },
  {
    "path": "test/fixtures/stack/stage/vpc/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stack/stage/vpc/main.tf",
    "content": "terraform {\n  backend \"s3\" {}\n\n  required_version = \">= 1.5.7\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\n# Create an arbitrary local resource\nresource \"null_resource\" \"text\" {\n  provisioner \"local-exec\" {\n    command = \"echo '[I am a stage vpc template. Data from my dependencies: vpc = ${data.terraform_remote_state.mgmt_vpc.outputs.text}]'\"\n  }\n}\n\noutput \"text\" {\n  value = \"[I am a stage vpc template. Data from my dependencies: vpc = ${data.terraform_remote_state.mgmt_vpc.outputs.text}]\"\n}\n\nvariable \"terraform_remote_state_s3_bucket\" {\n  description = \"The name of the S3 bucket where Terraform remote state is stored\"\n  type        = string\n}\n\ndata \"terraform_remote_state\" \"mgmt_vpc\" {\n  backend = \"s3\"\n  config = {\n    region = \"us-west-2\"\n    bucket = var.terraform_remote_state_s3_bucket\n    key    = \"mgmt/vpc/terraform.tfstate\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/stack/stage/vpc/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependencies {\n  paths = [\"../../mgmt/vpc\"]\n}\n\n"
  },
  {
    "path": "test/fixtures/stacks/all-no-stack-dir/live/terragrunt.stack.hcl",
    "content": "unit \"foo\" {\n  source                  = \"../unit\"\n  path                    = \"foo\"\n  no_dot_terragrunt_stack = true\n}\n\nunit \"bar\" {\n  source                  = \"../unit\"\n  path                    = \"bar\"\n  no_dot_terragrunt_stack = true\n}\n\n"
  },
  {
    "path": "test/fixtures/stacks/all-no-stack-dir/unit/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/all-no-stack-dir/unit/main.tf",
    "content": "output \"test\" {\n  value = var.test\n}\n\nvariable \"test\" {\n  type = string\n}\n\n"
  },
  {
    "path": "test/fixtures/stacks/all-no-stack-dir/unit/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\ninputs = {\n  test = \"value\"\n}\n\n"
  },
  {
    "path": "test/fixtures/stacks/basic/live/terragrunt.stack.hcl",
    "content": "unit \"mother\" {\n\tsource = \"../units/chicken\"\n\tpath   = \"mother\"\n}\n\nunit \"father\" {\n\tsource = \"../units/chicken\"\n\tpath   = \"father\"\n}\n\nunit \"chick_1\" {\n\tsource = \"../units/chick\"\n\tpath   = \"chicks/chick-1\"\n}\n\nunit \"chick_2\" {\n\tsource = \"../units/chick\"\n\tpath   = \"chicks/chick-2\"\n}\n\n"
  },
  {
    "path": "test/fixtures/stacks/basic/units/chick/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stacks/basic/units/chick/main.tf",
    "content": "\nresource \"local_file\" \"file\" {\n  content  = \"chick\"\n  filename = \"${path.module}/test.txt\"\n}\n\noutput \"output\" {\n  value = local_file.file.filename\n}\n"
  },
  {
    "path": "test/fixtures/stacks/basic/units/chick/terragrunt.hcl",
    "content": "\nterraform {\n  source = \".\"\n}"
  },
  {
    "path": "test/fixtures/stacks/basic/units/chicken/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stacks/basic/units/chicken/main.tf",
    "content": "\nresource \"local_file\" \"file\" {\n  content  = \"chicken\"\n  filename = \"${path.module}/test.txt\"\n}\n\noutput \"output\" {\n  value = local_file.file.filename\n}\n"
  },
  {
    "path": "test/fixtures/stacks/basic/units/chicken/terragrunt.hcl",
    "content": "\nterraform {\n  source = \".\"\n}"
  },
  {
    "path": "test/fixtures/stacks/basic/units/father/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stacks/basic/units/father/main.tf",
    "content": "\nresource \"local_file\" \"file\" {\n  content  = \"father\"\n  filename = \"${path.module}/test.txt\"\n}\n\noutput \"output\" {\n  value = local_file.file.filename\n}\n"
  },
  {
    "path": "test/fixtures/stacks/basic/units/father/terragrunt.hcl",
    "content": "\nterraform {\n  source = \".\"\n}"
  },
  {
    "path": "test/fixtures/stacks/basic/units/mother/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stacks/basic/units/mother/main.tf",
    "content": "\nresource \"local_file\" \"file\" {\n  content  = \"mother\"\n  filename = \"${path.module}/test.txt\"\n}\n\noutput \"output\" {\n  value = local_file.file.filename\n}\n"
  },
  {
    "path": "test/fixtures/stacks/basic/units/mother/terragrunt.hcl",
    "content": "\nterraform {\n  source = \".\"\n}"
  },
  {
    "path": "test/fixtures/stacks/coexist-hcl-and-stack/modules/test/main.tf",
    "content": "output \"test\" {\n  value = \"test\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/coexist-hcl-and-stack/non-prod/dev/terragrunt.stack.hcl",
    "content": "stack \"test\" {\n\tsource = \"${get_repo_root()}/stacks/test\"\n\tpath   = \"test\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/coexist-hcl-and-stack/stacks/test/terragrunt.stack.hcl",
    "content": "unit \"test\" {\n\tsource = \"${get_repo_root()}/units/test\"\n\tpath   = \"test\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/coexist-hcl-and-stack/units/test/main.tf",
    "content": "output \"test\" {\n  value = \"test\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/coexist-hcl-and-stack/units/test/terragrunt.hcl",
    "content": "terraform {\n\tsource = \"../../modules/test\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/dependencies/live/terragrunt.stack.hcl",
    "content": "\nunit \"app1\" {\n\tsource = \"../units/app\"\n\tpath   = \"app1\"\n\tvalues = {\n\t\tinput = \"app1\"\n\t}\n}\n\nunit \"app2\" {\n\tsource = \"../units/app\"\n\tpath   = \"app2\"\n\tvalues = {\n\t\tinput = \"app2\"\n\t}\n}\n\nunit \"app3\" {\n\tsource = \"../units/app\"\n\tpath   = \"app3\"\n\tvalues = {\n\t\tinput = \"app3\"\n\t}\n}\n\nunit \"app-with-dependency\" {\n\tsource = \"../units/app-with-dependency\"\n\tpath   = \"app-with-dependency\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/dependencies/units/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stacks/dependencies/units/app/main.tf",
    "content": "terraform {\n  required_version = \">= 0.12\"\n  required_providers {\n    local = {\n      source  = \"registry.opentofu.org/hashicorp/local\"\n      version = \">= 2.5.2\"\n    }\n  }\n}\n\nvariable \"input\" {\n  description = \"Project input\"\n  type        = string\n}\n\nresource \"local_file\" \"file\" {\n  content  = var.input\n  filename = \"${path.module}/data.txt\"\n}\n\noutput \"result\" {\n  value = var.input\n}\n"
  },
  {
    "path": "test/fixtures/stacks/dependencies/units/app/terragrunt.hcl",
    "content": "\ninputs = {\n  input = values.input\n}"
  },
  {
    "path": "test/fixtures/stacks/dependencies/units/app-with-dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stacks/dependencies/units/app-with-dependency/main.tf",
    "content": "terraform {\n  required_version = \">= 0.12\"\n  required_providers {\n    local = {\n      source  = \"registry.opentofu.org/hashicorp/local\"\n      version = \">= 2.5.2\"\n    }\n  }\n}\n\nvariable \"input\" {\n  description = \"Project input\"\n  type        = string\n}\n\nresource \"local_file\" \"file\" {\n  content  = var.input\n  filename = \"${path.module}/data.txt\"\n}\n\noutput \"result\" {\n  value = var.input\n}\n"
  },
  {
    "path": "test/fixtures/stacks/dependencies/units/app-with-dependency/terragrunt.hcl",
    "content": "\ndependency \"app1\" {\n  config_path = \"../app1\"\n}\n\ninputs = {\n  input = dependency.app1.outputs.result\n}"
  },
  {
    "path": "test/fixtures/stacks/errors/absolute-path/live/terragrunt.stack.hcl",
    "content": "\nunit \"app1\" {\n\tsource = \"../units/app\"\n\tpath   = \"${get_repo_root()}/app1\"\n\tvalues = {\n\t\tinput = \"app1\"\n\t}\n}\n\nunit \"app2\" {\n\tsource = \"../units/app\"\n\tpath   = \"${get_repo_root()}/app2\"\n\tvalues = {\n\t\tinput = \"app2\"\n\t}\n}\n\nunit \"app3\" {\n\tsource = \"../units/app\"\n\tpath   = \"app3\"\n\tvalues = {\n\t\tinput = \"app3\"\n\t}\n}\n"
  },
  {
    "path": "test/fixtures/stacks/errors/absolute-path/units/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stacks/errors/absolute-path/units/app/main.tf",
    "content": "variable \"input\" {\n  description = \"Project input\"\n  type        = string\n}\n\nresource \"local_file\" \"file\" {\n  content  = var.input\n  filename = \"${path.module}/data.txt\"\n}\n\noutput \"result\" {\n  value = var.input\n}\n"
  },
  {
    "path": "test/fixtures/stacks/errors/absolute-path/units/app/terragrunt.hcl",
    "content": "\ninputs = {\n  input = values.input\n}"
  },
  {
    "path": "test/fixtures/stacks/errors/cycles/live/terragrunt.stack.hcl",
    "content": "stack \"stack\" {\n  source = \"${get_repo_root()}/stack\"\n  path   = \"stack\"\n}\n\n"
  },
  {
    "path": "test/fixtures/stacks/errors/cycles/stack/terragrunt.stack.hcl",
    "content": "stack \"stack\" {\n  source = \"${get_repo_root()}/stack\"\n  path   = \"stack\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/errors/cycles/unit/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/errors/cycles/unit/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/stacks/errors/cycles/unit/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}"
  },
  {
    "path": "test/fixtures/stacks/errors/incorrect-source/live/terragrunt.stack.hcl",
    "content": "\nunit \"api\" {\n\tsource = \"../../units/api\" # incorrect source path\n\tpath = \"api\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/errors/incorrect-source/units/api/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/errors/incorrect-source/units/api/main.tf",
    "content": "variable \"ver\" {}\n\noutput \"data\" {\n  value = \"api ${var.ver}\"\n}"
  },
  {
    "path": "test/fixtures/stacks/errors/incorrect-source/units/api/terragrunt.hcl",
    "content": "inputs = {\n  ver = values.ver\n}"
  },
  {
    "path": "test/fixtures/stacks/errors/locals-error/terragrunt.stack.hcl",
    "content": "locals {\n\tchicken = \"units/chicken\"\n}\n\nlocals {\n\tchick = \"units/chick\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/errors/not-existing-path/live/terragrunt.stack.hcl",
    "content": "\nunit \"app1\" {\n\tsource = \"../units/app1\"\n\tpath   = \"app1\"\n}\n\nunit \"stack1\" {\n\tsource = \"../stacks/stack1\"\n\tpath   = \"stack1\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/errors/relative-path-outside-of-stack/live/terragrunt.stack.hcl",
    "content": "\nunit \"app1\" {\n\tsource = \"../units/app1\"\n\tpath   = \"../project1/app1\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/errors/relative-path-outside-of-stack/units/app1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/errors/relative-path-outside-of-stack/units/app1/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/stacks/errors/relative-path-outside-of-stack/units/app1/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}"
  },
  {
    "path": "test/fixtures/stacks/errors/stack-empty-path/terragrunt.stack.hcl",
    "content": "stack \"prod\" {\n\tsource = \"stacks/prod\"\n\tpath = \"\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/errors/unit-empty-path/live/terragrunt.stack.hcl",
    "content": "# This fixture tests the validation of unit paths in stack configurations.\n# It includes units with empty paths (app1, app2) and a valid path (app3)\n# to verify the validation logic.\n\nunit \"app1_empty_path\" {\n\tsource = \"../units/app\"\n\tpath   = \"\"\n}\n\nunit \"app2_empty_path\" {\n\tsource = \"../units/app\"\n\tpath   = \"\"\n}\n\nunit \"app3_not_empty_path\" {\n\tsource = \"../units/app\"\n\tpath   = \"app3\"\n}\n\n\n"
  },
  {
    "path": "test/fixtures/stacks/errors/unit-empty-path/units/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/errors/unit-empty-path/units/app/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/stacks/errors/unit-empty-path/units/app/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/stacks/errors/unknown-value/live/terragrunt.stack.hcl",
    "content": "unit \"bad_unit\" {\n\tsource = \"../units/bad-unit\"\n\tpath   = \"bad-unit\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/errors/unknown-value/units/bad-unit/terragrunt.hcl",
    "content": "locals {\n  my_value = local.missing_var  # Undefined - causes \"value is not known\" error\n}\n\nterraform {\n  source = local.my_value  # Uses undefined value, source cannot be determined\n}\n"
  },
  {
    "path": "test/fixtures/stacks/errors/validation-stack/live/terragrunt.stack.hcl",
    "content": "\nstack \"stack-v1\" {\n\tsource = \"${get_repo_root()}/stacks\"\n\tpath = \"stack-v1\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/errors/validation-stack/stacks/v1/terragrunt.stack.hcl",
    "content": "\nunit \"unit-v1\" {\n\tsource = \"${get_repo_root()}/units/api\"\n\tpath = \"unit-v1\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/errors/validation-stack/units/api/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/errors/validation-stack/units/api/main.tf",
    "content": "variable \"ver\" {}\n\noutput \"data\" {\n  value = \"api ${var.ver}\"\n}"
  },
  {
    "path": "test/fixtures/stacks/errors/validation-stack/units/api/terragrunt.hcl",
    "content": "inputs = {\n  ver = values.ver\n}"
  },
  {
    "path": "test/fixtures/stacks/errors/validation-unit/live/terragrunt.stack.hcl",
    "content": "\nunit \"v1\" {\n\tsource = \"${get_repo_root()}/units/v1\"\n\tpath = \"v1\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/errors/validation-unit/units/v1/api/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/errors/validation-unit/units/v1/api/main.tf",
    "content": "variable \"ver\" {}\n\noutput \"data\" {\n  value = \"api ${var.ver}\"\n}"
  },
  {
    "path": "test/fixtures/stacks/errors/validation-unit/units/v1/api/terragrunt.hcl",
    "content": "inputs = {\n  ver = values.ver\n}"
  },
  {
    "path": "test/fixtures/stacks/errors/validation-unit/units/v1/db/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/errors/validation-unit/units/v1/db/main.tf",
    "content": "variable \"ver\" {}\n\noutput \"data\" {\n  value = \"db ${var.ver}\"\n}"
  },
  {
    "path": "test/fixtures/stacks/errors/validation-unit/units/v1/db/terragrunt.hcl",
    "content": "inputs = {\n  ver = values.ver\n}"
  },
  {
    "path": "test/fixtures/stacks/errors/validation-unit/units/v1/web/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/errors/validation-unit/units/v1/web/main.tf",
    "content": "variable \"ver\" {}\n\noutput \"data\" {\n  value = \"web ${var.ver}\"\n}"
  },
  {
    "path": "test/fixtures/stacks/errors/validation-unit/units/v1/web/terragrunt.hcl",
    "content": "inputs = {\n  ver = values.ver\n}"
  },
  {
    "path": "test/fixtures/stacks/find-in-parent-folders/live/stack/terragrunt.stack.hcl",
    "content": "locals {\n  mock_vars = read_terragrunt_config(find_in_parent_folders(\"mock.hcl\"))\n  mock       = local.mock_vars.locals.mock\n}\n\nunit \"foo\" {\n  source = \"../../units/foo\"\n  path   = \"foo\"\n\n  values = {\n    mock = local.mock\n  }\n}\n"
  },
  {
    "path": "test/fixtures/stacks/find-in-parent-folders/mock.hcl",
    "content": "locals {\n  mock = \"mock\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/find-in-parent-folders/units/foo/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/find-in-parent-folders/units/foo/main.tf",
    "content": "variable \"mock\" {\n  description = \"Mock value to be passed through\"\n  type        = string\n}\n\noutput \"mock\" {\n  value = var.mock\n}\n"
  },
  {
    "path": "test/fixtures/stacks/find-in-parent-folders/units/foo/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\ninputs = {\n  mock = values.mock\n}\n"
  },
  {
    "path": "test/fixtures/stacks/get-original-terragrunt-dir/live/account1/no-locals-nested/no-locals/terragrunt.stack.hcl",
    "content": "stack \"units\" {\n  source = find_in_parent_folders(\"stacks/no-locals\")\n  path   = \"unit_dirs\"\n\n  values = {\n    stack_dir = get_original_terragrunt_dir()\n  }\n}\n"
  },
  {
    "path": "test/fixtures/stacks/get-original-terragrunt-dir/live/account1/no-locals-nested/read-config/terragrunt.stack.hcl",
    "content": "locals {\n  common = read_terragrunt_config(find_in_parent_folders(\"common/stack_config.hcl\"))\n}\n\nstack \"units\" {\n  source = find_in_parent_folders(\"stacks/no-locals\")\n  path   = \"unit_dirs\"\n\n  values = {\n    stack_dir = local.common.locals.stack_dir\n  }\n}\n"
  },
  {
    "path": "test/fixtures/stacks/get-original-terragrunt-dir/live/account1/no-locals-nested/with-locals/terragrunt.stack.hcl",
    "content": "locals {\n  stack_dir = get_original_terragrunt_dir()\n}\n\nstack \"units\" {\n  source = find_in_parent_folders(\"stacks/no-locals\")\n  path   = \"unit_dirs\"\n\n  values = {\n    stack_dir = local.stack_dir\n  }\n}\n"
  },
  {
    "path": "test/fixtures/stacks/get-original-terragrunt-dir/live/account1/non-nested/no-locals/terragrunt.stack.hcl",
    "content": "unit \"unit_1\" {\n  source = find_in_parent_folders(\"units\")\n  path = \"unit_1\"\n\n  values = {\n    stack_dir = get_original_terragrunt_dir()\n  }\n}\n"
  },
  {
    "path": "test/fixtures/stacks/get-original-terragrunt-dir/live/account1/non-nested/read-config/terragrunt.stack.hcl",
    "content": "locals {\n  common = read_terragrunt_config(find_in_parent_folders(\"common/stack_config.hcl\"))\n}\n\nunit \"unit_1\" {\n  source = find_in_parent_folders(\"units\")\n  path = \"unit_1\"\n\n  values = {\n    stack_dir = local.common.locals.stack_dir\n  }\n}\n"
  },
  {
    "path": "test/fixtures/stacks/get-original-terragrunt-dir/live/account1/non-nested/with-locals/terragrunt.stack.hcl",
    "content": "locals {\n  stack_dir = get_original_terragrunt_dir()\n}\n\nunit \"unit_1\" {\n  source = find_in_parent_folders(\"units\")\n  path = \"unit_1\"\n\n  values = {\n    stack_dir = local.stack_dir\n  }\n}\n"
  },
  {
    "path": "test/fixtures/stacks/get-original-terragrunt-dir/live/account1/read-config-nested/no-locals/terragrunt.stack.hcl",
    "content": "stack \"units\" {\n  source = find_in_parent_folders(\"stacks/read-config\")\n  path   = \"unit_dirs\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/get-original-terragrunt-dir/live/account1/read-config-nested/read-config/terragrunt.stack.hcl",
    "content": "locals {\n  common = read_terragrunt_config(find_in_parent_folders(\"common/stack_config.hcl\"))\n}\n\nstack \"units\" {\n  source = find_in_parent_folders(\"stacks/read-config\")\n  path   = \"unit_dirs\"\n\n  values = {\n    stack_dir = local.common.locals.stack_dir\n  }\n}\n"
  },
  {
    "path": "test/fixtures/stacks/get-original-terragrunt-dir/live/account1/read-config-nested/with-locals/terragrunt.stack.hcl",
    "content": "locals {\n  stack_dir = get_original_terragrunt_dir()\n}\n\nstack \"units\" {\n  source = find_in_parent_folders(\"stacks/read-config\")\n  path   = \"unit_dirs\"\n\n  values = {\n    stack_dir = local.stack_dir\n  }\n}\n"
  },
  {
    "path": "test/fixtures/stacks/get-original-terragrunt-dir/live/account1/with-locals-nested/no-locals/terragrunt.stack.hcl",
    "content": "stack \"units\" {\n  source = find_in_parent_folders(\"stacks/with-locals\")\n  path   = \"unit_dirs\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/get-original-terragrunt-dir/live/account1/with-locals-nested/read-config/terragrunt.stack.hcl",
    "content": "locals {\n  common = read_terragrunt_config(find_in_parent_folders(\"common/stack_config.hcl\"))\n}\n\nstack \"units\" {\n  source = find_in_parent_folders(\"stacks/with-locals\")\n  path   = \"unit_dirs\"\n\n  values = {\n    stack_dir = local.common.locals.stack_dir\n  }\n}\n"
  },
  {
    "path": "test/fixtures/stacks/get-original-terragrunt-dir/live/account1/with-locals-nested/with-locals/terragrunt.stack.hcl",
    "content": "locals {\n  stack_dir = get_original_terragrunt_dir()\n}\n\nstack \"units\" {\n  source = find_in_parent_folders(\"stacks/with-locals\")\n  path   = \"unit_dirs\"\n\n  values = {\n    stack_dir = local.stack_dir\n  }\n}\n"
  },
  {
    "path": "test/fixtures/stacks/get-original-terragrunt-dir/live/common/stack_config.hcl",
    "content": "locals {\n  stack_dir = get_original_terragrunt_dir()\n}\n"
  },
  {
    "path": "test/fixtures/stacks/get-original-terragrunt-dir/stacks/no-locals/terragrunt.stack.hcl",
    "content": "unit \"unit_1\" {\n  source = find_in_parent_folders(\"units\")\n  path = \"unit_1\"\n\n  values = try(values, {})\n\n  no_dot_terragrunt_stack = true\n}\n\nunit \"unit_2\" {\n  source = find_in_parent_folders(\"units\")\n  path = \"unit_2\"\n\n  values = try(values, {})\n\n  no_dot_terragrunt_stack = true\n}\n"
  },
  {
    "path": "test/fixtures/stacks/get-original-terragrunt-dir/stacks/read-config/terragrunt.stack.hcl",
    "content": "locals {\n  common = read_terragrunt_config(find_in_parent_folders(\"live/common/stack_config.hcl\"))\n}\n\nunit \"unit_1\" {\n  source = find_in_parent_folders(\"units\")\n  path = \"unit_1\"\n\n  values = merge({ stack_dir = local.common.locals.stack_dir }, try(values, {}))\n\n  no_dot_terragrunt_stack = true\n}\n\nunit \"unit_2\" {\n  source = find_in_parent_folders(\"units\")\n  path = \"unit_2\"\n\n  values = merge({ stack_dir = local.common.locals.stack_dir }, try(values, {}))\n\n  no_dot_terragrunt_stack = true\n}\n"
  },
  {
    "path": "test/fixtures/stacks/get-original-terragrunt-dir/stacks/with-locals/terragrunt.stack.hcl",
    "content": "locals {\n  stack_dir = get_original_terragrunt_dir()\n}\n\nunit \"unit_1\" {\n  source = find_in_parent_folders(\"units\")\n  path = \"unit_1\"\n\n  values = merge({ stack_dir = local.stack_dir }, try(values, {}))\n\n  no_dot_terragrunt_stack = true\n}\n\nunit \"unit_2\" {\n  source = find_in_parent_folders(\"units\")\n  path = \"unit_2\"\n\n  values = merge({ stack_dir = local.stack_dir }, try(values, {}))\n\n  no_dot_terragrunt_stack = true\n}\n"
  },
  {
    "path": "test/fixtures/stacks/get-original-terragrunt-dir/units/terragrunt.hcl",
    "content": "inputs = {\n  stack_dir = values.stack_dir\n}\n"
  },
  {
    "path": "test/fixtures/stacks/inputs/live/terragrunt.stack.hcl",
    "content": "unit \"unit1\" {\n\tsource = \"../units/app\"\n\tpath   = \"unit1\"\n}\n\nunit \"unit2\" {\n\tsource = \"../units/app\"\n\tpath   = \"unit2\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/inputs/units/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stacks/inputs/units/app/main.tf",
    "content": "variable \"content\" {\n  type = string\n}\n\nvariable \"filename\" {\n  type    = string\n  default = \"file.txt\"\n}\n\nresource \"local_file\" \"file\" {\n  content  = var.content\n  filename = \"${path.module}/${var.filename}\"\n}\n\noutput \"output\" {\n  value = local_file.file.filename\n}\n"
  },
  {
    "path": "test/fixtures/stacks/inputs/units/app/terragrunt.hcl",
    "content": "\nterraform {\n  source = \".\"\n}\n\ninputs = {\n  content = \"content\"\n}"
  },
  {
    "path": "test/fixtures/stacks/locals/live/terragrunt.stack.hcl",
    "content": "locals {\n\tchicken = \"../units/chicken\"\n\tchick = \"units/chick\"\n\trepo_path = \"${get_repo_root()}\"\n}\n\nunit \"mother\" {\n\tsource = local.chicken\n\tpath   = \"mother\"\n}\n\nunit \"father\" {\n\tsource = local.chicken\n\tpath   = \"father\"\n}\n\nunit \"chick_1\" {\n\tsource = \"../${local.chick}\"\n\tpath   = \"chicks/chick-1\"\n}\n\nunit \"chick_2\" {\n\tsource = \"${local.repo_path}/fixtures/stacks/locals/${local.chick}\"\n\tpath   = \"chicks/chick-2\"\n}\n\n"
  },
  {
    "path": "test/fixtures/stacks/locals/units/chick/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/stacks/locals/units/chicken/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/stacks/locals/units/father/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/stacks/locals/units/mother/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/stacks/multiple-stacks/dev/terragrunt.stack.hcl",
    "content": "unit \"v2-unit1\" {\n\tsource = \"../unit\"\n\tpath   = \"unit1\"\n}\n\nunit \"v2-unit2\" {\n\tsource = \"../unit\"\n\tpath   = \"unit2\"\n}\n\nunit \"v2-unit3\" {\n\tsource = \"../unit\"\n\tpath   = \"unit3\"\n}\n\n"
  },
  {
    "path": "test/fixtures/stacks/multiple-stacks/live/terragrunt.stack.hcl",
    "content": "unit \"v1-unit1\" {\n\tsource = \"../unit\"\n\tpath   = \"unit1\"\n}\n\nunit \"v1-unit2\" {\n\tsource = \"../unit\"\n\tpath   = \"unit2\"\n}\n\nunit \"v1-unit3\" {\n\tsource = \"../unit\"\n\tpath   = \"unit3\"\n}\n\n"
  },
  {
    "path": "test/fixtures/stacks/multiple-stacks/unit/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/multiple-stacks/unit/main.tf",
    "content": "\noutput \"output\" {\n  value = \"unit\"\n}"
  },
  {
    "path": "test/fixtures/stacks/multiple-stacks/unit/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/stacks/nested/live/terragrunt.stack.hcl",
    "content": "stack \"dev\" {\n\tsource = \"${get_repo_root()}/stacks/dev\"\n\tpath = \"dev\"\n}\n\n\nstack \"prod\" {\n\tsource = \"${get_repo_root()}/stacks/prod\"\n\tpath = \"prod\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/nested/live-v2/terragrunt.stack.hcl",
    "content": "\nstack \"dev\" {\n\tsource = \"${get_repo_root()}/stacks/dev\"\n\tpath = \"dev\"\n}\n\n\nstack \"prod\" {\n\tsource = \"${get_repo_root()}/stacks/prod\"\n\tpath = \"prod\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/nested/stacks/dev/terragrunt.stack.hcl",
    "content": "unit \"dev-api\" {\n\tsource = \"${get_repo_root()}/units/api\"\n\tpath   = \"api\"\n\tvalues = {\n\t\tver = \"dev-api 1.0.0\"\n\t}\n}\n\nunit \"dev-db\" {\n\tsource = \"${get_repo_root()}/units/db\"\n\tpath   = \"db\"\n\tvalues = {\n\t\tver = \"dev-db 1.0.0\"\n\t}\n}\n\nunit \"dev-web\" {\n\tsource = \"${get_repo_root()}/units/web\"\n\tpath   = \"web\"\n\tvalues = {\n\t\tver = \"dev-web 1.0.0\"\n\t}\n}\n"
  },
  {
    "path": "test/fixtures/stacks/nested/stacks/prod/terragrunt.stack.hcl",
    "content": "unit \"prod-api\" {\n\tsource = \"${get_repo_root()}/units/api\"\n\tpath   = \"api\"\n\tvalues = {\n\t\tver = \"prod-api 1.0.0\"\n\t}\n}\n\nunit \"prod-db\" {\n\tsource = \"${get_repo_root()}/units/db\"\n\tpath   = \"db\"\n\tvalues = {\n\t\tver = \"prod-db 1.0.0\"\n\t}\n}\n\nunit \"prod-web\" {\n\tsource = \"${get_repo_root()}/units/web\"\n\tpath   = \"web\"\n\tvalues = {\n\t\tver = \"prod-web 1.0.0\"\n\t}\n}\n"
  },
  {
    "path": "test/fixtures/stacks/nested/units/api/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/nested/units/api/main.tf",
    "content": "variable \"ver\" {}\n\noutput \"data\" {\n  value = \"api ${var.ver}\"\n}"
  },
  {
    "path": "test/fixtures/stacks/nested/units/api/terragrunt.hcl",
    "content": "inputs = {\n  ver = values.ver\n}"
  },
  {
    "path": "test/fixtures/stacks/nested/units/db/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/nested/units/db/main.tf",
    "content": "variable \"ver\" {}\n\noutput \"data\" {\n  value = \"db ${var.ver}\"\n}"
  },
  {
    "path": "test/fixtures/stacks/nested/units/db/terragrunt.hcl",
    "content": "inputs = {\n  ver = values.ver\n}"
  },
  {
    "path": "test/fixtures/stacks/nested/units/web/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/nested/units/web/main.tf",
    "content": "variable \"ver\" {}\n\noutput \"data\" {\n  value = \"web ${var.ver}\"\n}"
  },
  {
    "path": "test/fixtures/stacks/nested/units/web/terragrunt.hcl",
    "content": "inputs = {\n  ver = values.ver\n}"
  },
  {
    "path": "test/fixtures/stacks/nested-outputs/live/terragrunt.stack.hcl",
    "content": "unit \"app_1\" {\n\tsource = \"${get_repo_root()}/units/app\"\n\tpath   = \"app_1\"\n\tvalues = {\n\t\tdata = \"app_1\"\n\t}\n}\n\nunit \"app_2\" {\n\tsource = \"${get_repo_root()}/units/app\"\n\tpath   = \"app_2\"\n\tvalues = {\n\t\tdata = \"app_2\"\n\t}\n}\n\nstack \"root_stack_1\" {\n\tsource = \"${get_repo_root()}/stacks/v1\"\n\tpath   = \"stack_1\"\n}\n\nstack \"root_stack_2\" {\n\tsource = \"${get_repo_root()}/stacks/v2\"\n\tpath   = \"stack_2\"\n}\n\nstack \"root_stack_3\" {\n\tsource = \"${get_repo_root()}/stacks/v3\"\n\tpath   = \"stack_3\"\n}\n\n"
  },
  {
    "path": "test/fixtures/stacks/nested-outputs/stacks/v1/terragrunt.stack.hcl",
    "content": "unit \"app_3\" {\n\tsource = \"${get_repo_root()}/units/app\"\n\tpath   = \"app_3\"\n\tvalues = {\n\t\tdata = \"app_3\"\n\t}\n}\n\nunit \"app_4\" {\n\tsource = \"${get_repo_root()}/units/app\"\n\tpath   = \"app_4\"\n\tvalues = {\n\t\tdata = \"app_4\"\n\t}\n}\n\n"
  },
  {
    "path": "test/fixtures/stacks/nested-outputs/stacks/v2/terragrunt.stack.hcl",
    "content": "stack \"stack_v2\" {\n\tsource = \"${get_repo_root()}/stacks/v1\"\n\tpath   = \"stack_v2\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/nested-outputs/stacks/v3/terragrunt.stack.hcl",
    "content": "stack \"stack_v3\" {\n\tsource = \"${get_repo_root()}/stacks/v2\"\n\tpath   = \"stack_v3\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/nested-outputs/units/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/nested-outputs/units/app/main.tf",
    "content": "variable \"data\" {}\noutput \"data\" {\n  value = var.data\n}"
  },
  {
    "path": "test/fixtures/stacks/nested-outputs/units/app/terragrunt.hcl",
    "content": "inputs = {\n  data = values.data\n}"
  },
  {
    "path": "test/fixtures/stacks/no-dot-terragrunt-stack-output/live/terragrunt.stack.hcl",
    "content": "unit \"app1\" {\n  source                  = \"../units/app1\"\n  path                    = \"app1\"\n  no_dot_terragrunt_stack = true\n}\n"
  },
  {
    "path": "test/fixtures/stacks/no-dot-terragrunt-stack-output/units/app1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/no-dot-terragrunt-stack-output/units/app1/main.tf",
    "content": "terraform {\n  required_version = \">= 0.13\"\n}\n\nvariable \"name\" {\n  description = \"Name of the application\"\n  type        = string\n}\n\noutput \"name\" {\n  value = var.name\n}\n"
  },
  {
    "path": "test/fixtures/stacks/no-dot-terragrunt-stack-output/units/app1/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\ninputs = {\n  name = \"app1\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/no-stack/config/config.txt",
    "content": "# example configuration file\n"
  },
  {
    "path": "test/fixtures/stacks/no-stack/live/terragrunt.stack.hcl",
    "content": "stack \"stack-config\" {\n  source                  = \"${get_repo_root()}/config\"\n  path                    = \"stack-config\"\n  no_dot_terragrunt_stack = true\n}\n\nunit \"unit-config\" {\n  source                  = \"${get_repo_root()}/config\"\n  path                    = \"unit-config\"\n  no_dot_terragrunt_stack = true\n}\n\nstack \"dev\" {\n  source = \"${get_repo_root()}/stacks/dev\"\n  path   = \"dev\"\n}\n\n"
  },
  {
    "path": "test/fixtures/stacks/no-stack/stacks/dev/terragrunt.stack.hcl",
    "content": "unit \"second-stack-unit-config\" {\n  source                  = \"${get_repo_root()}/config\"\n  path                    = \"second-stack-unit-config\"\n  no_dot_terragrunt_stack = true\n}\n\nunit \"dev-api\" {\n  source = \"${get_repo_root()}/units/api\"\n  path   = \"api\"\n  values = {\n    ver = \"dev-api 1.0.0\"\n  }\n}\n\nunit \"dev-db\" {\n  source = \"${get_repo_root()}/units/db\"\n  path   = \"db\"\n  values = {\n    ver = \"dev-db 1.0.0\"\n  }\n}\n\nunit \"dev-web\" {\n  source = \"${get_repo_root()}/units/web\"\n  path   = \"web\"\n  values = {\n    ver = \"dev-web 1.0.0\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/stacks/no-stack/units/api/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/no-stack/units/api/main.tf",
    "content": "variable \"ver\" {}\n\noutput \"data\" {\n  value = \"api ${var.ver}\"\n}"
  },
  {
    "path": "test/fixtures/stacks/no-stack/units/api/terragrunt.hcl",
    "content": "inputs = {\n  ver = values.ver\n}"
  },
  {
    "path": "test/fixtures/stacks/no-stack/units/db/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/no-stack/units/db/main.tf",
    "content": "variable \"ver\" {}\n\noutput \"data\" {\n  value = \"db ${var.ver}\"\n}"
  },
  {
    "path": "test/fixtures/stacks/no-stack/units/db/terragrunt.hcl",
    "content": "inputs = {\n  ver = values.ver\n}"
  },
  {
    "path": "test/fixtures/stacks/no-stack/units/web/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/no-stack/units/web/main.tf",
    "content": "variable \"ver\" {}\n\noutput \"data\" {\n  value = \"web ${var.ver}\"\n}"
  },
  {
    "path": "test/fixtures/stacks/no-stack/units/web/terragrunt.hcl",
    "content": "inputs = {\n  ver = values.ver\n}"
  },
  {
    "path": "test/fixtures/stacks/no-stack-dir/live/terragrunt.stack.hcl",
    "content": "unit \"unit1\" {\n  source                  = \"../unit\"\n  path                    = \"unit1\"\n  no_dot_terragrunt_stack = true\n}\n\nunit \"unit2\" {\n  source                  = \"../unit\"\n  path                    = \"unit2\"\n  no_dot_terragrunt_stack = true\n}\n\n"
  },
  {
    "path": "test/fixtures/stacks/no-stack-dir/unit/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/no-stack-dir/unit/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/stacks/no-stack-dir/unit/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/stacks/no-validation/live/terragrunt.stack.hcl",
    "content": "unit \"unit1\" {\n  source        = \"../units\"\n  path          = \"unit1\"\n  no_validation = true\n}\n\nunit \"stack1\" {\n  source        = \"../stacks\"\n  path          = \"stack1\"\n  no_validation = true\n}\n\n"
  },
  {
    "path": "test/fixtures/stacks/no-validation/stacks/stack1/terragrunt.stack.hcl",
    "content": "unit \"unit2\" {\n  source        = \"../../../../units\"\n  path          = \"unit2\"\n  no_validation = true\n}"
  },
  {
    "path": "test/fixtures/stacks/no-validation/units/app1/code/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stacks/no-validation/units/app1/code/main.tf",
    "content": "resource \"local_file\" \"file\" {\n  content  = \"test file\"\n  filename = \"${path.module}/file.txt\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/no-validation/units/app1/code/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/stacks/outputs/live/terragrunt.stack.hcl",
    "content": "\nunit \"filtered_app1\" {\n\tsource = \"../units/app1\"\n\tpath   = \"project1/app1\"\n}\n\nunit \"filtered_app2\" {\n\tsource = \"../units/app2\"\n\tpath   = \"project1/app2\"\n}\n\nunit \"project2_app1\" {\n\tsource = \"../units/app1\"\n\tpath   = \"project2/app1\"\n}\n\nunit \"project2_app2\" {\n\tsource = \"../units/app2\"\n\tpath   = \"project2/app2\"\n}\n\n\n"
  },
  {
    "path": "test/fixtures/stacks/outputs/units/app1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/outputs/units/app1/main.tf",
    "content": "output \"data\" {\n  value = \"app1\"\n}\n\noutput \"custom_value1\" {\n  value = \"value1\"\n}\n\noutput \"complex\" {\n  value = {\n    name      = \"name1\"\n    id        = 2\n    timestamp = timestamp()\n    delta     = 0.02\n  }\n}\n\noutput \"list\" {\n  value = [\"1\", \"2\", \"3\"]\n}\n\noutput \"complex_list\" {\n  value = [\n    {\n      name      = \"name1\"\n      id        = 10\n      timestamp = timestamp()\n      delta     = 0.02\n    },\n    {\n      name      = \"name10\"\n      id        = 20\n      timestamp = timestamp()\n      delta     = 0.03\n    }\n  ]\n}"
  },
  {
    "path": "test/fixtures/stacks/outputs/units/app1/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}"
  },
  {
    "path": "test/fixtures/stacks/outputs/units/app2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/outputs/units/app2/main.tf",
    "content": "output \"data\" {\n  value = \"app2\"\n}\n\noutput \"custom_value2\" {\n  value = \"value2\"\n}\n\noutput \"complex\" {\n  value = {\n    name      = \"name2\"\n    id        = 2\n    timestamp = timestamp()\n    delta     = 0.02\n  }\n}\n\noutput \"list\" {\n  value = [\"a\", \"b\", \"c\"]\n}\n\noutput \"complex_list\" {\n  value = [\n    {\n      name      = \"name2\"\n      id        = 2\n      timestamp = timestamp()\n      delta     = 0.02\n    },\n    {\n      name      = \"name3\"\n      id        = 2\n      timestamp = timestamp()\n      delta     = 0.03\n    }\n  ]\n}"
  },
  {
    "path": "test/fixtures/stacks/outputs/units/app2/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}"
  },
  {
    "path": "test/fixtures/stacks/read-stack/live/terragrunt.stack.hcl",
    "content": "locals {\n  project = \"test-project\"\n  version = \"6.6.6\"\n  env     = \"test\"\n}\n\nunit \"test_app\" {\n  source = \"../units/app\"\n  path   = \"app\"\n  values = {\n    app     = \"test-app\"\n    project = local.project\n    version = local.version\n    env     = local.env\n    data    = \"test\"\n  }\n}\n\nstack \"dev\" {\n  source = \"${get_repo_root()}/stacks/dev\"\n  path   = \"dev\"\n  values = {\n    project = \"dev-project\"\n    env     = \"dev\"\n  }\n}\n\nstack \"prod\" {\n  source = \"${get_repo_root()}/stacks/prod\"\n  path   = \"prod\"\n  values = {\n    project = \"prod-project\"\n    env     = \"prod\"\n  }\n}\n\n"
  },
  {
    "path": "test/fixtures/stacks/read-stack/stacks/dev/terragrunt.stack.hcl",
    "content": "unit \"dev-app-1\" {\n\tsource = \"${get_repo_root()}/units/app\"\n\tpath   = \"dev-app-1\"\n\tvalues = {\n\t\tproject = values.project\n\t\tenv = values.env\n\t\tdata = \"dev-app-1\"\n\t}\n}\n\nunit \"dev-app-2\" {\n\tsource = \"${get_repo_root()}/units/app\"\n\tpath   = \"dev-app-2\"\n\tvalues = {\n\t\tproject = values.project\n\t\tenv = values.env\n\t\tdata = \"dev-app-2\"\n\t}\n}\n"
  },
  {
    "path": "test/fixtures/stacks/read-stack/stacks/prod/terragrunt.stack.hcl",
    "content": "unit \"prod-app-1\" {\n\tsource = \"${get_repo_root()}/units/app\"\n\tpath   = \"prod-app-1\"\n\tvalues = {\n\t\tproject = values.project\n\t\tenv = values.env\n\t\tdata = \"prod-app-1\"\n\t}\n}\n\nunit \"prod-app-2\" {\n\tsource = \"${get_repo_root()}/units/app\"\n\tpath   = \"prod-app-2\"\n\tvalues = {\n\t\tproject = values.project\n\t\tenv = values.env\n\t\tdata = \"prod-app-2\"\n\t}\n}\n"
  },
  {
    "path": "test/fixtures/stacks/read-stack/units/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stacks/read-stack/units/app/main.tf",
    "content": "variable \"project\" {\n  description = \"The project identifier\"\n  type        = string\n}\n\nvariable \"env\" {\n  description = \"The environment name\"\n  type        = string\n}\n\nvariable \"data\" {\n  description = \"Additional data for the configuration\"\n  type        = string\n}\n\nlocals {\n  config = \"${var.project} ${var.env} ${var.data}\"\n}\n\nresource \"local_file\" \"file\" {\n  content  = local.config\n  filename = \"${path.module}/file.txt\"\n}\n\noutput \"config\" {\n  value       = local.config\n  description = \"The combined configuration string\"\n}\n\noutput \"project\" {\n  value       = var.project\n  description = \"The project identifier\"\n}\n\noutput \"env\" {\n  value       = var.env\n  description = \"The environment name\"\n}\n\noutput \"data\" {\n  value       = var.data\n  description = \"Additional data used in the configuration\"\n}\n\nvariable \"stack_local_project\" {\n  description = \"The local project identifier for the stack configuration\"\n  type        = string\n}\n\nvariable \"unit_source\" {\n  description = \"The source location for the unit configuration\"\n  type        = string\n}\n\nvariable \"unit_name\" {\n  description = \"The name assigned to the unit\"\n  type        = string\n}\n\nvariable \"unit_value_version\" {\n  description = \"The version identifier for the unit value\"\n  type        = string\n}\n\nvariable \"stack_source\" {\n  description = \"The source location for the stack configuration\"\n  type        = string\n}\n\nvariable \"stack_value_env\" {\n  description = \"The environment setting for the stack values\"\n  type        = string\n}\noutput \"stack_local_project\" {\n  value       = var.stack_local_project\n  description = \"The local project identifier from the stack configuration\"\n}\n\noutput \"unit_source\" {\n  value       = var.unit_source\n  description = \"The source location for the unit\"\n}\n\noutput \"unit_name\" {\n  value       = var.unit_name\n  description = \"The name identifier of the unit\"\n}\n\noutput \"unit_value_version\" {\n  value       = var.unit_value_version\n  description = \"The version of the unit's value configuration\"\n}\n\noutput \"stack_source\" {\n  value       = var.stack_source\n  description = \"The source location for the stack\"\n}\n\noutput \"stack_value_env\" {\n  value       = var.stack_value_env\n  description = \"The environment configuration value for the stack\"\n}"
  },
  {
    "path": "test/fixtures/stacks/read-stack/units/app/terragrunt.hcl",
    "content": "locals {\n  read_stack = read_terragrunt_config(\"${get_repo_root()}/live/terragrunt.stack.hcl\")\n  read_values = read_terragrunt_config(\"terragrunt.values.hcl\")\n}\n\ninputs = {\n  stack_local_project = local.read_stack.local.project\n  unit_source         = local.read_stack.unit.test_app.source\n  unit_name           = local.read_stack.unit.test_app.name\n  unit_value_version  = local.read_stack.unit.test_app.values.version\n  stack_source        = local.read_stack.stack.dev.source\n  stack_value_env     = local.read_stack.stack.dev.values.env\n  project             = \"${local.read_values.project}\"\n  env                 = local.read_values.env\n  data                = local.read_values.data\n}"
  },
  {
    "path": "test/fixtures/stacks/remote/terragrunt.stack.hcl",
    "content": "locals {\n\tversion = \"main\"\n}\n\nunit \"app1\" {\n\tsource = \"git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/stacks/basic/units/chick?ref=${local.version}&depth=1\"\n\tpath   = \"app1\"\n}\n\nunit \"app2\" {\n\tsource = \"git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/stacks/basic/units/chick?ref=${local.version}&depth=1\"\n\tpath   = \"app2\"\n}\n\n"
  },
  {
    "path": "test/fixtures/stacks/self-include/live/terragrunt.stack.hcl",
    "content": "locals {\n  version = \"main\"\n}\n\nunit \"app1\" {\n  source = \"git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/stacks/self-include/unit?ref=${local.version}\"\n  path   = \"app1\"\n  values = {\n    data = \"example-data\"\n  }\n}\n\n\n"
  },
  {
    "path": "test/fixtures/stacks/self-include/module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/self-include/module/main.tf",
    "content": "variable \"data\" {}\n\noutput \"data\" {\n  value = var.data\n}"
  },
  {
    "path": "test/fixtures/stacks/self-include/module/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n}\n\ninputs = {\n  data = values.data\n}"
  },
  {
    "path": "test/fixtures/stacks/self-include/unit/terragrunt.hcl",
    "content": "terraform {\n  source = \"git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/stacks/self-include/module?ref=main\"\n}\n\ninputs = {\n  data = values.data\n}"
  },
  {
    "path": "test/fixtures/stacks/source-map/live/terragrunt.stack.hcl",
    "content": "\nunit \"app1\" {\n\tsource = \"git::https://git-host.com/not-existing-repo.git//fixtures/stacks/source-map/units/app\"\n\tpath   = \"app1\"\n\tvalues = {\n\t\tinput = \"app1\"\n\t}\n}\n\nunit \"app2\" {\n\tsource = \"git::https://git-host.com/not-existing-repo.git//fixtures/stacks/source-map/units/app\"\n\tpath   = \"app2\"\n\tvalues = {\n\t\tinput = \"app2\"\n\t}\n}\n"
  },
  {
    "path": "test/fixtures/stacks/source-map/tf/modules/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stacks/source-map/tf/modules/main.tf",
    "content": "\n\nvariable \"input\" {\n  description = \"Project input\"\n  type        = string\n}\n\nresource \"local_file\" \"file\" {\n  content  = var.input\n  filename = \"${path.module}/data.txt\"\n}\n\noutput \"result\" {\n  value = var.input\n}\n"
  },
  {
    "path": "test/fixtures/stacks/source-map/units/app/terragrunt.hcl",
    "content": "terraform {\n  source = \"git::https://git-host.com/not-existing-repo.git//fixtures/stacks/source-map/tf/modules\"\n}\n\ninputs = {\n  input = values.input\n}"
  },
  {
    "path": "test/fixtures/stacks/stack-values/live/terragrunt.stack.hcl",
    "content": "\nstack \"dev\" {\n  source = \"${get_repo_root()}/stacks/dev\"\n  path = \"dev\"\n  values = {\n    project = \"dev-project\"\n    env = \"dev\"\n  }\n}\n\nstack \"prod\" {\n  source = \"${get_repo_root()}/stacks/prod\"\n  path = \"prod\"\n  values = {\n      project = \"prod-project\"\n      env = \"prod\"\n  }\n}\n\n"
  },
  {
    "path": "test/fixtures/stacks/stack-values/stacks/dev/terragrunt.stack.hcl",
    "content": "unit \"dev-app-1\" {\n\tsource = \"${get_repo_root()}/units/app\"\n\tpath   = \"dev-app-1\"\n\tvalues = {\n\t\tproject = values.project\n\t\tenv = values.env\n\t\tdata = \"dev-app-1\"\n\t}\n}\n\nunit \"dev-app-2\" {\n\tsource = \"${get_repo_root()}/units/app\"\n\tpath   = \"dev-app-2\"\n\tvalues = {\n\t\tproject = values.project\n\t\tenv = values.env\n\t\tdata = \"dev-app-2\"\n\t}\n}\n"
  },
  {
    "path": "test/fixtures/stacks/stack-values/stacks/prod/terragrunt.stack.hcl",
    "content": "unit \"prod-app-1\" {\n\tsource = \"${get_repo_root()}/units/app\"\n\tpath   = \"prod-app-1\"\n\tvalues = {\n\t\tproject = values.project\n\t\tenv = values.env\n\t\tdata = \"prod-app-1\"\n\t}\n}\n\nunit \"prod-app-2\" {\n\tsource = \"${get_repo_root()}/units/app\"\n\tpath   = \"prod-app-2\"\n\tvalues = {\n\t\tproject = values.project\n\t\tenv = values.env\n\t\tdata = \"prod-app-2\"\n\t}\n}\n"
  },
  {
    "path": "test/fixtures/stacks/stack-values/units/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/local\" {\n  version = \"2.6.1\"\n  hashes = [\n    \"h1:+XfQ7VmNtYMp0eOnoQH6cZpSMk12IP1X6tEkMoMGQ/A=\",\n    \"h1:Dd5MP04TnE9qaFD8BQkJYkluiJCOsL7fwUTJx26KIP0=\",\n    \"h1:QH/Ay/SWVoOLgvFacjcvQcrw2WfEktZHxCcIQG0A9/w=\",\n    \"zh:0416d7bf0b459a995cf48f202af7b7ffa252def7d23386fc05b34f67347a22ba\",\n    \"zh:24743d559026b59610eb3d9fa9ec7fbeb06399c0ef01272e46fe5c313eb5c6ff\",\n    \"zh:2561cdfbc90090fee7f844a5cb5cbed8472ce264f5d505acb18326650a5b563f\",\n    \"zh:3ebc3f2dc7a099bd83e5c4c2b6918e5b56ec746766c58a31a3f5d189cb837db5\",\n    \"zh:490e0ce925fc3848027e10017f960e9e19e7f9c3b620524f67ce54217d1c6390\",\n    \"zh:bf08934295877f831f2e5f17a0b3ebb51dd608b2509077f7b22afa7722e28950\",\n    \"zh:c298c0f72e1485588a73768cb90163863b6c3d4c71982908c219e9b87904f376\",\n    \"zh:cedbaed4967818903ef378675211ed541c8243c4597304161363e828c7dc3d36\",\n    \"zh:edda76726d7874128cf1e182640c332c5a5e6a66a053c0aa97e2a0e4267b3b92\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/stacks/stack-values/units/app/main.tf",
    "content": "variable \"project\" {\n  description = \"The project identifier\"\n  type        = string\n}\n\nvariable \"env\" {\n  description = \"The environment name\"\n  type        = string\n}\n\nvariable \"data\" {\n  description = \"Additional data for the configuration\"\n  type        = string\n}\n\nlocals {\n  config = \"${var.project} ${var.env} ${var.data}\"\n}\n\nresource \"local_file\" \"file\" {\n  content  = local.config\n  filename = \"${path.module}/file.txt\"\n}\n\noutput \"config\" {\n  value       = local.config\n  description = \"The combined configuration string\"\n}\n\noutput \"project\" {\n  value       = var.project\n  description = \"The project identifier\"\n}\n\noutput \"env\" {\n  value       = var.env\n  description = \"The environment name\"\n}\n\noutput \"data\" {\n  value       = var.data\n  description = \"Additional data used in the configuration\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/stack-values/units/app/terragrunt.hcl",
    "content": "\ninputs = {\n  project = values.project\n  env = values.env\n  data = values.data\n}"
  },
  {
    "path": "test/fixtures/stacks/terragrunt-dir/live/root.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/stacks/terragrunt-dir/live/tennant_1/terragrunt.stack.hcl",
    "content": "unit \"unit_a\" {\n  source = \"../../unit_a\"\n  path   = \"unit_a\"\n  values = {\n    terragrunt_dir = get_terragrunt_dir()\n  }\n}"
  },
  {
    "path": "test/fixtures/stacks/terragrunt-dir/unit_a/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/terragrunt-dir/unit_a/main.tf",
    "content": "variable \"terragrunt_dir\" {}\n\noutput \"terragrunt_dir\" {\n  value = var.terragrunt_dir\n}"
  },
  {
    "path": "test/fixtures/stacks/terragrunt-dir/unit_a/terragrunt.hcl",
    "content": "\ninclude \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \".\"\n}\n\ninputs = {\n  terragrunt_dir = values.terragrunt_dir\n}\n"
  },
  {
    "path": "test/fixtures/stacks/unit-values/live/terragrunt.stack.hcl",
    "content": "locals {\n\tproject = \"test-project\"\n}\n\nunit \"app1\" {\n\tsource = \"../units/app\"\n\tpath   = \"app1\"\n\n\tvalues = {\n\t\tproject    = local.project\n\t\tdeployment = \"app1\"\n\t}\n}\n\nunit \"app2\" {\n\tsource = \"../units/app\"\n\tpath   = \"app2\"\n\n\tvalues = {\n\t\tproject    = local.project\n\t\tdeployment = \"app2\"\n\t}\n}\n"
  },
  {
    "path": "test/fixtures/stacks/unit-values/units/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/unit-values/units/app/main.tf",
    "content": "\nvariable \"deployment\" {}\n\nvariable \"project\" {}\n\nvariable \"data\" {}\n\noutput \"data\" {\n  value = var.data\n}\n\noutput \"deployment\" {\n  value = var.deployment\n}\n\noutput \"project\" {\n  value = var.project\n}"
  },
  {
    "path": "test/fixtures/stacks/unit-values/units/app/terragrunt.hcl",
    "content": "\nlocals {\n  data = \"payload: ${values.deployment}-${values.project}\"\n}\n\ninputs = {\n  deployment = values.deployment\n  project = values.project\n  data = local.data\n}"
  },
  {
    "path": "test/fixtures/stacks/version-constraints/live/terragrunt.stack.hcl",
    "content": "unit \"app\" {\n  source = \"../unit\"\n  path   = \"app\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/version-constraints/unit/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/stacks/version-constraints/unit/main.tf",
    "content": "# Empty module - just needed for terraform init\noutput \"test\" {\n  value = \"test\"\n}\n"
  },
  {
    "path": "test/fixtures/stacks/version-constraints/unit/terragrunt.hcl",
    "content": "# Constraint that will fail with test version\nterragrunt_version_constraint = \">= 99.0.0\"\n\nterraform {\n  source = \".\"\n}\n"
  },
  {
    "path": "test/fixtures/startswith/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/startswith/main.tf",
    "content": "variable \"startswith1\" {\n  type = bool\n}\n\nvariable \"startswith2\" {\n  type = bool\n}\n\nvariable \"startswith3\" {\n  type = bool\n}\n\nvariable \"startswith4\" {\n  type = bool\n}\n\nvariable \"startswith5\" {\n  type = bool\n}\n\nvariable \"startswith6\" {\n  type = bool\n}\n\nvariable \"startswith7\" {\n  type = bool\n}\n\nvariable \"startswith8\" {\n  type = bool\n}\n\nvariable \"startswith9\" {\n  type = bool\n}\n\noutput \"startswith1\" {\n  value = var.startswith1\n}\n\noutput \"startswith2\" {\n  value = var.startswith2\n}\n\noutput \"startswith3\" {\n  value = var.startswith3\n}\n\noutput \"startswith4\" {\n  value = var.startswith4\n}\n\noutput \"startswith5\" {\n  value = var.startswith5\n}\n\noutput \"startswith6\" {\n  value = var.startswith6\n}\n\noutput \"startswith7\" {\n  value = var.startswith7\n}\n\noutput \"startswith8\" {\n  value = var.startswith8\n}\n\noutput \"startswith9\" {\n  value = var.startswith9\n}\n\n\n"
  },
  {
    "path": "test/fixtures/startswith/terragrunt.hcl",
    "content": "inputs = {\n  startswith1 = startswith(\"hello world\", \"hello\")\n  startswith2 = startswith(\"hello world\", \"world\")\n  startswith3 = startswith(\"hello world\", \"\")\n  startswith4 = startswith(\"hello world\", \" \")\n  startswith5 = startswith(\"\", \"\")\n  startswith6 = startswith(\"\", \" \")\n  startswith7 = startswith(\" \", \"\")\n  startswith8 = startswith(\"\", \"hello\")\n  startswith9 = startswith(\" \", \"hello\")\n}\n"
  },
  {
    "path": "test/fixtures/strcontains/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/strcontains/main.tf",
    "content": "variable \"i1\" {\n  type = bool\n}\n\nvariable \"i2\" {\n  type = bool\n}\n\noutput \"o1\" {\n  value = var.i1\n}\n\noutput \"o2\" {\n  value = var.i2\n}\n"
  },
  {
    "path": "test/fixtures/strcontains/terragrunt.hcl",
    "content": "inputs = {\n  i1 = strcontains(\"hello world\", \"hello\")\n  i2 = strcontains(\"hello world\", \"test\")\n}\n"
  },
  {
    "path": "test/fixtures/streaming/unit1/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/streaming/unit1/main.tf",
    "content": "terraform {\n  required_version = \">= 1.0\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"empty\" {\n  triggers = {\n    always_run = timestamp()\n  }\n\n  provisioner \"local-exec\" {\n    command = \"echo 'sleeping...'; sleep 3; echo 'done sleeping'\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/streaming/unit1/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/streaming/unit2/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/streaming/unit2/main.tf",
    "content": "terraform {\n  required_version = \">= 1.0\"\n\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"empty\" {\n  triggers = {\n    always_run = timestamp()\n  }\n\n  provisioner \"local-exec\" {\n    command = \"echo 'sleeping...'; sleep 3; echo 'done sleeping'\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/streaming/unit2/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/strict-bare-include/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/strict-bare-include/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/strict-bare-include/parent.hcl",
    "content": "locals {\n  greeting = \"hello\"\n}\n"
  },
  {
    "path": "test/fixtures/strict-bare-include/terragrunt.hcl",
    "content": "include {\n  path = \"parent.hcl\"\n}\n"
  },
  {
    "path": "test/fixtures/terragrunt-info-error/module-a/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/terragrunt-info-error/module-a/main.tf",
    "content": "output \"test_var\" {\n  value = \"hello\"\n}"
  },
  {
    "path": "test/fixtures/terragrunt-info-error/module-a/terragrunt.hcl",
    "content": "terraform {\n  source = \".//\"\n}\n"
  },
  {
    "path": "test/fixtures/terragrunt-info-error/module-b/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/terragrunt-info-error/module-b/main.tf",
    "content": "variable \"test_var\" {\n  type = string\n}\n\noutput \"result\" {\n  value = var.test_var\n}\n"
  },
  {
    "path": "test/fixtures/terragrunt-info-error/module-b/terragrunt.hcl",
    "content": "terraform {\n  source = \".//\"\n}\n\ndependency \"module_a\" {\n  config_path                             = \"../module-a\"\n  mock_outputs_allowed_terraform_commands = [\"init\", \"plan\", \"validate\"]\n  mock_outputs_merge_strategy_with_state  = \"shallow\"\n  mock_outputs = {\n    test_mock = \"abc\"\n  }\n}\n\ninputs = {\n  test_var = dependency.module_a.outputs.test_mock\n}"
  },
  {
    "path": "test/fixtures/terragrunt.hcl",
    "content": "terraform {\n  source = \"tfr://registry.terraform.io/yorinasub17/terragrunt-registry-test/null//modules/one?version=0.0.2\"\n}\n\nremote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    key            = \"${path_relative_to_include()}/terraform.tfstate\"\n    bucket         = \"__FILL_IN_BUCKET_NAME__\"\n    region         = \"__FILL_IN_REGION__\"\n    dynamodb_table = \"__FILL_IN_LOCK_TABLE_NAME__\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/tf-path/basic/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/tf-path/basic/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/tf-path/basic/other-tf.sh",
    "content": "#!/usr/bin/env bash\n\necho \"Other TF script used!\" >&2\n"
  },
  {
    "path": "test/fixtures/tf-path/basic/terragrunt.hcl",
    "content": "# We can't explicitly specify `tofu` or `terraform` because a CircleCI job contains either `terraform` or `tofu` binary, but not both in the same job.\nterraform_binary = \"./tf.sh\"\n"
  },
  {
    "path": "test/fixtures/tf-path/basic/tf.sh",
    "content": "#!/usr/bin/env bash\n\necho \"TF script used!\" >&2\n"
  },
  {
    "path": "test/fixtures/tf-path/dependency/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/tf-path/dependency/app/main.tf",
    "content": "variable \"dep_value\" {\n  type = string\n}\n\noutput \"result\" {\n  value = \"app got ${var.dep_value}\"\n}\n\n"
  },
  {
    "path": "test/fixtures/tf-path/dependency/app/terragrunt.hcl",
    "content": "# This config intentionally sets terraform_binary to a path that doesn't exist.\n# If the --tf-path CLI argument is properly respected, this should be overridden.\nterraform_binary = \"./non-existent\"\n\ndependency \"dep\" {\n  config_path = \"../dep\"\n}\n\ninputs = {\n  dep_value = dependency.dep.outputs.value\n}\n"
  },
  {
    "path": "test/fixtures/tf-path/dependency/custom-tf.sh",
    "content": "#!/usr/bin/env bash\n# This script is used as a custom tofu binary for testing\necho \"Custom TF script used in $PWD!\" >&2\ntofu \"$@\"\n"
  },
  {
    "path": "test/fixtures/tf-path/dependency/dep/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/tf-path/dependency/dep/main.tf",
    "content": "output \"value\" {\n  value = \"from_dep\"\n}\n\n"
  },
  {
    "path": "test/fixtures/tf-path/dependency/dep/terragrunt.hcl",
    "content": "# This config intentionally sets terraform_binary to a path that doesn't exist.\n# If the --tf-path CLI argument is properly respected, this should be overridden.\nterraform_binary = \"./non-existent-tf\"\n\n"
  },
  {
    "path": "test/fixtures/tf-path/tofu-terraform/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/tf-path/tofu-terraform/main.tf",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/tf-path/tofu-terraform/terragrunt.hcl",
    "content": "feature \"binary\" {\n    default = \"tofu\"\n}\n\nterraform_binary = feature.binary.value\n"
  },
  {
    "path": "test/fixtures/tflint/custom-tflint-config/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/aws\" {\n  version     = \"5.11.0\"\n  constraints = \"5.11.0\"\n  hashes = [\n    \"h1:2IiEX8whcDTcOIYQffFHicthX0p07oGGcgIKmU/7qYo=\",\n    \"h1:4uGqee8u/cznRtpXblJRXts7i2ZZkB8YtKqRUSDeI8Q=\",\n    \"h1:GG7b3g/hQnvAUFkHahOUva1pGRfZvSR7Xx1bNn0I9Ss=\",\n    \"zh:06944814fc592596796a5f99605dcf92882c495640652c90497bc616448664b1\",\n    \"zh:079096253113fd93a33d655c0339c571dffa7dadfaacafda20c9ca3eebd23a11\",\n    \"zh:08817ffa86d612819a07cd13cec5b66d50f4b8f40c1048e88e4761ccf17172c1\",\n    \"zh:0d010c2bba5c3d4bb3e74b3ed106269481a4109e35ec39b61bb08de1582d6571\",\n    \"zh:46d81b9c6b873c563aa4a69e6d03f9c5c35ae120829b84d4dfea6089595acd24\",\n    \"zh:470dcc6c34c203d2884852d22436ef6f928c1fdbc5a96466f34561cb2c817244\",\n    \"zh:787add339e00161a71e605ab11fd460d4c30c094d9a5f49bf71bcee5eb8617fa\",\n    \"zh:9cb468ec55a6710259e16dd889d80fc58bcafbeb06c9a424167df85f57945a3d\",\n    \"zh:f1dcd170ef0479d91abe57b2151939d1a584fb03b6c517d507f2f49f4e1b9f0e\",\n    \"zh:f52e479435675ab227ed5eb4922f1c314c66c5dd0ae84ed33263c0251638b70f\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/tflint/custom-tflint-config/custom.tflint.hcl",
    "content": "plugin \"terraform\" {\n  enabled = true\n  version = \"0.2.1\"\n  source  = \"github.com/terraform-linters/tflint-ruleset-terraform\"\n}\n\nplugin \"aws\" {\n  enabled = true\n  version = \"0.25.0\"\n  source  = \"github.com/terraform-linters/tflint-ruleset-aws\"\n}\n\nconfig {\n  module = true\n}\n\nrule \"aws_s3_bucket_name\" {\n  enabled = true\n  regex = \"my-prefix-.*\"\n  prefix = \"my-prefix-\"\n}\n"
  },
  {
    "path": "test/fixtures/tflint/custom-tflint-config/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"registry.opentofu.org/hashicorp/aws\"\n      version = \"5.11.0\"\n    }\n  }\n  required_version = \">= 1.2.7\"\n}\n\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\n\nresource \"aws_s3_bucket\" \"bucket\" {\n  bucket = var.bucket_name\n\n}\n"
  },
  {
    "path": "test/fixtures/tflint/custom-tflint-config/outputs.tf",
    "content": "output \"bucket_name\" {\n  description = \"bucket name\"\n  value       = var.bucket_name\n}\n"
  },
  {
    "path": "test/fixtures/tflint/custom-tflint-config/terragrunt.hcl",
    "content": "terraform {\n  before_hook \"tflint\" {\n    commands = [\"plan\"]\n    execute  = [\"tflint\", \"--terragrunt-external-tflint\", \"--config\", \"custom.tflint.hcl\"]\n  }\n}\n\ninputs = {\n  bucket_name = \"my-prefix-qwe\"\n}\n"
  },
  {
    "path": "test/fixtures/tflint/custom-tflint-config/variables.tf",
    "content": "variable \"bucket_name\" {\n  description = \"bucket name\"\n  type        = string\n}\n\n"
  },
  {
    "path": "test/fixtures/tflint/external-tflint/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/aws\" {\n  version     = \"5.11.0\"\n  constraints = \"5.11.0\"\n  hashes = [\n    \"h1:2IiEX8whcDTcOIYQffFHicthX0p07oGGcgIKmU/7qYo=\",\n    \"h1:4uGqee8u/cznRtpXblJRXts7i2ZZkB8YtKqRUSDeI8Q=\",\n    \"h1:GG7b3g/hQnvAUFkHahOUva1pGRfZvSR7Xx1bNn0I9Ss=\",\n    \"zh:06944814fc592596796a5f99605dcf92882c495640652c90497bc616448664b1\",\n    \"zh:079096253113fd93a33d655c0339c571dffa7dadfaacafda20c9ca3eebd23a11\",\n    \"zh:08817ffa86d612819a07cd13cec5b66d50f4b8f40c1048e88e4761ccf17172c1\",\n    \"zh:0d010c2bba5c3d4bb3e74b3ed106269481a4109e35ec39b61bb08de1582d6571\",\n    \"zh:46d81b9c6b873c563aa4a69e6d03f9c5c35ae120829b84d4dfea6089595acd24\",\n    \"zh:470dcc6c34c203d2884852d22436ef6f928c1fdbc5a96466f34561cb2c817244\",\n    \"zh:787add339e00161a71e605ab11fd460d4c30c094d9a5f49bf71bcee5eb8617fa\",\n    \"zh:9cb468ec55a6710259e16dd889d80fc58bcafbeb06c9a424167df85f57945a3d\",\n    \"zh:f1dcd170ef0479d91abe57b2151939d1a584fb03b6c517d507f2f49f4e1b9f0e\",\n    \"zh:f52e479435675ab227ed5eb4922f1c314c66c5dd0ae84ed33263c0251638b70f\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/tflint/external-tflint/.tflint.hcl",
    "content": "plugin \"terraform\" {\n  enabled = true\n  version = \"0.2.1\"\n  source  = \"github.com/terraform-linters/tflint-ruleset-terraform\"\n}\n\nplugin \"aws\" {\n  enabled = true\n  version = \"0.25.0\"\n  source  = \"github.com/terraform-linters/tflint-ruleset-aws\"\n}\n\nconfig {\n  module = true\n}\n\nrule \"aws_s3_bucket_name\" {\n  enabled = true\n  regex = \"my-prefix-.*\"\n  prefix = \"my-prefix-\"\n}\n"
  },
  {
    "path": "test/fixtures/tflint/external-tflint/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"registry.opentofu.org/hashicorp/aws\"\n      version = \"5.11.0\"\n    }\n  }\n  required_version = \">= 1.2.7\"\n}\n\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\n\nresource \"aws_s3_bucket\" \"bucket\" {\n  bucket = var.bucket_name\n\n}\n"
  },
  {
    "path": "test/fixtures/tflint/external-tflint/outputs.tf",
    "content": "output \"bucket_name\" {\n  description = \"bucket name\"\n  value       = var.bucket_name\n}\n"
  },
  {
    "path": "test/fixtures/tflint/external-tflint/terragrunt.hcl",
    "content": "terraform {\n  before_hook \"tflint\" {\n    commands = [\"plan\"]\n    execute  = [\"tflint\", \"--terragrunt-external-tflint\"]\n  }\n}\n\ninputs = {\n  bucket_name = \"my-prefix-qwe\"\n}\n"
  },
  {
    "path": "test/fixtures/tflint/external-tflint/variables.tf",
    "content": "variable \"bucket_name\" {\n  description = \"bucket name\"\n  type        = string\n}\n\n"
  },
  {
    "path": "test/fixtures/tflint/issues-found/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/random\" {\n  version     = \"3.8.0\"\n  constraints = \">= 3.4.0\"\n  hashes = [\n    \"h1:aEaTEHutDdKNaztKFmInhfzmZK0/OaVL8uxmncM9YF8=\",\n    \"h1:ey4eBIHiuAC5xsblxtXghXE3nWwUvGqTT6KAsggiAwo=\",\n    \"h1:nRPdhXsZpGPMppuUgBe/ZcAtD73NaCLGROYHXv41qz8=\",\n    \"zh:2d5e0bbfac7f15595739fe54a9ab8b8eea92fd6d879706139dad7ecaa5c01c19\",\n    \"zh:349e637066625d97aaa84db1b1418c86d6457cf9c5a62f6dcc3f55cbd535112c\",\n    \"zh:5f4456d53f5256ccfdb87dd35d3bf34578d01bd9b71cffaf507f0692805eac8a\",\n    \"zh:6c1ecfacc5f7079a068d7f8eb8924485d4ec8183f36e6318a6e748d35921ddac\",\n    \"zh:6d86641edeb8c394f121f7b0a691d72f89cf9b938b987a01fc32aad396a50555\",\n    \"zh:76947bd7bc7033b33980538da149c94e386f9b0abb2ce63733f25a57517e4742\",\n    \"zh:79c07f4c8b3a63d9f89e25e4348b462c57e179bca66ba533710851c485e282db\",\n    \"zh:ac1c2b941d994728a3a93aba093fd2202f9311d099ff85f66678897c792161ba\",\n    \"zh:cbb2aa867fd828fcb4125239e00862b9a3bc2f280e945c760224276b476f4c49\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/tflint/issues-found/.tflint.hcl",
    "content": "plugin \"terraform\" {\n  enabled = true\n  version = \"0.2.1\"\n  source  = \"github.com/terraform-linters/tflint-ruleset-terraform\"\n}"
  },
  {
    "path": "test/fixtures/tflint/issues-found/main.tf",
    "content": "terraform {\n  required_providers {\n    random = {\n      version = \">= 3.4.0\"\n    }\n  }\n\n  required_version = \">= 1.2.7\"\n}\n\n// It's all in the same file, so tflint will return issues.\nvariable \"aws_region\" {\n  type        = string\n  description = \"The AWS region.\"\n}\n\nvariable \"env\" {\n  type        = string\n  description = \"The environment name.\"\n}\n\n\nresource \"random_id\" \"env\" {\n  byte_length = 8\n}\n\noutput \"aws_region\" {\n  description = \"The AWS region's name.\"\n  value       = var.aws_region\n}\noutput \"env\" {\n  description = \"The randomized environment's name.\"\n  value       = \"${var.env}-${random_id.env.hex}\"\n}\n"
  },
  {
    "path": "test/fixtures/tflint/issues-found/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n\n  before_hook \"tflint\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"tflint\"]\n  }\n}\n\ninputs = {\n  aws_region = \"us-west-2\"\n  env = \"dev\"\n}\n"
  },
  {
    "path": "test/fixtures/tflint/module-found/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/tflint/module-found/.tflint.hcl",
    "content": "config {\n  call_module_type = \"all\"\n}\n\nplugin \"terraform\" {\n  enabled = true\n  version = \"0.2.1\"\n  source  = \"github.com/terraform-linters/tflint-ruleset-terraform\"\n}\n"
  },
  {
    "path": "test/fixtures/tflint/module-found/dummy_module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version     = \"3.2.4\"\n  constraints = \"~> 3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/tflint/module-found/dummy_module/main.tf",
    "content": "terraform {\n  required_version = \">= 0.12\"\n  required_providers {\n    null = {\n      source  = \"registry.opentofu.org/hashicorp/null\"\n      version = \"~> 3.2.4\"\n    }\n  }\n}\n\nresource \"null_resource\" \"dummy\" {}\n"
  },
  {
    "path": "test/fixtures/tflint/module-found/main.tf",
    "content": "terraform {\n  required_version = \">= 1.2.7\"\n  required_providers {\n  }\n}\n\nmodule \"dummy_module\" {\n  source = \"./dummy_module\"\n}\n"
  },
  {
    "path": "test/fixtures/tflint/module-found/outputs.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/tflint/module-found/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n\n  before_hook \"tflint\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"tflint\"]\n  }\n}\n\n"
  },
  {
    "path": "test/fixtures/tflint/module-found/variables.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/tflint/no-config-file/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/random\" {\n  version     = \"3.8.0\"\n  constraints = \">= 3.4.0\"\n  hashes = [\n    \"h1:aEaTEHutDdKNaztKFmInhfzmZK0/OaVL8uxmncM9YF8=\",\n    \"h1:ey4eBIHiuAC5xsblxtXghXE3nWwUvGqTT6KAsggiAwo=\",\n    \"h1:nRPdhXsZpGPMppuUgBe/ZcAtD73NaCLGROYHXv41qz8=\",\n    \"zh:2d5e0bbfac7f15595739fe54a9ab8b8eea92fd6d879706139dad7ecaa5c01c19\",\n    \"zh:349e637066625d97aaa84db1b1418c86d6457cf9c5a62f6dcc3f55cbd535112c\",\n    \"zh:5f4456d53f5256ccfdb87dd35d3bf34578d01bd9b71cffaf507f0692805eac8a\",\n    \"zh:6c1ecfacc5f7079a068d7f8eb8924485d4ec8183f36e6318a6e748d35921ddac\",\n    \"zh:6d86641edeb8c394f121f7b0a691d72f89cf9b938b987a01fc32aad396a50555\",\n    \"zh:76947bd7bc7033b33980538da149c94e386f9b0abb2ce63733f25a57517e4742\",\n    \"zh:79c07f4c8b3a63d9f89e25e4348b462c57e179bca66ba533710851c485e282db\",\n    \"zh:ac1c2b941d994728a3a93aba093fd2202f9311d099ff85f66678897c792161ba\",\n    \"zh:cbb2aa867fd828fcb4125239e00862b9a3bc2f280e945c760224276b476f4c49\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/tflint/no-config-file/main.tf",
    "content": "terraform {\n  required_providers {\n    random = {\n      version = \">= 3.4.0\"\n      source  = \"registry.opentofu.org/hashicorp/random\"\n    }\n  }\n\n  required_version = \">= 1.2.7\"\n}\n\n// It's all in the same file, so tflint will return issues.\nvariable \"aws_region\" {\n  type        = string\n  description = \"The AWS region.\"\n}\n\nvariable \"env\" {\n  type        = string\n  description = \"The environment name.\"\n}\n\nresource \"random_id\" \"env\" {\n  byte_length = 8\n}\n\noutput \"aws_region\" {\n  description = \"The AWS region's name.\"\n  value       = var.aws_region\n}\noutput \"env\" {\n  description = \"The randomized environment's name.\"\n  value       = \"${var.env}-${random_id.env.hex}\"\n}\n"
  },
  {
    "path": "test/fixtures/tflint/no-config-file/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n\n  before_hook \"tflint\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"tflint\"]\n  }\n}\n\ninputs = {\n  aws_region = \"us-west-2\"\n  env = \"dev\"\n}\n"
  },
  {
    "path": "test/fixtures/tflint/no-issues-found/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/random\" {\n  version     = \"3.8.0\"\n  constraints = \">= 3.4.0\"\n  hashes = [\n    \"h1:aEaTEHutDdKNaztKFmInhfzmZK0/OaVL8uxmncM9YF8=\",\n    \"h1:ey4eBIHiuAC5xsblxtXghXE3nWwUvGqTT6KAsggiAwo=\",\n    \"h1:nRPdhXsZpGPMppuUgBe/ZcAtD73NaCLGROYHXv41qz8=\",\n    \"zh:2d5e0bbfac7f15595739fe54a9ab8b8eea92fd6d879706139dad7ecaa5c01c19\",\n    \"zh:349e637066625d97aaa84db1b1418c86d6457cf9c5a62f6dcc3f55cbd535112c\",\n    \"zh:5f4456d53f5256ccfdb87dd35d3bf34578d01bd9b71cffaf507f0692805eac8a\",\n    \"zh:6c1ecfacc5f7079a068d7f8eb8924485d4ec8183f36e6318a6e748d35921ddac\",\n    \"zh:6d86641edeb8c394f121f7b0a691d72f89cf9b938b987a01fc32aad396a50555\",\n    \"zh:76947bd7bc7033b33980538da149c94e386f9b0abb2ce63733f25a57517e4742\",\n    \"zh:79c07f4c8b3a63d9f89e25e4348b462c57e179bca66ba533710851c485e282db\",\n    \"zh:ac1c2b941d994728a3a93aba093fd2202f9311d099ff85f66678897c792161ba\",\n    \"zh:cbb2aa867fd828fcb4125239e00862b9a3bc2f280e945c760224276b476f4c49\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/tflint/no-issues-found/.tflint.hcl",
    "content": "plugin \"terraform\" {\n  enabled = true\n  version = \"0.2.1\"\n  source  = \"github.com/terraform-linters/tflint-ruleset-terraform\"\n}"
  },
  {
    "path": "test/fixtures/tflint/no-issues-found/d1/file.txt",
    "content": ""
  },
  {
    "path": "test/fixtures/tflint/no-issues-found/main.tf",
    "content": "terraform {\n  required_providers {\n    random = {\n      version = \">= 3.4.0\"\n    }\n  }\n\n  required_version = \">= 1.2.7\"\n}\n\nresource \"random_id\" \"env\" {\n  byte_length = 8\n}\n\n"
  },
  {
    "path": "test/fixtures/tflint/no-issues-found/outputs.tf",
    "content": "output \"aws_region\" {\n  description = \"The AWS region's name.\"\n  value       = var.aws_region\n}\noutput \"env\" {\n  description = \"The randomized environment's name.\"\n  value       = \"${var.env}-${random_id.env.hex}\"\n}"
  },
  {
    "path": "test/fixtures/tflint/no-issues-found/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n\n  before_hook \"tflint\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"tflint\"]\n  }\n}\n\ninputs = {\n  aws_region = \"us-west-2\"\n  env = \"dev\"\n}\n"
  },
  {
    "path": "test/fixtures/tflint/no-issues-found/variables.tf",
    "content": "variable \"aws_region\" {\n  type        = string\n  description = \"The AWS region.\"\n}\n\nvariable \"env\" {\n  type        = string\n  description = \"The environment name.\"\n}\n"
  },
  {
    "path": "test/fixtures/tflint/no-tf-source/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/random\" {\n  version     = \"3.8.0\"\n  constraints = \">= 3.4.0\"\n  hashes = [\n    \"h1:aEaTEHutDdKNaztKFmInhfzmZK0/OaVL8uxmncM9YF8=\",\n    \"h1:ey4eBIHiuAC5xsblxtXghXE3nWwUvGqTT6KAsggiAwo=\",\n    \"h1:nRPdhXsZpGPMppuUgBe/ZcAtD73NaCLGROYHXv41qz8=\",\n    \"zh:2d5e0bbfac7f15595739fe54a9ab8b8eea92fd6d879706139dad7ecaa5c01c19\",\n    \"zh:349e637066625d97aaa84db1b1418c86d6457cf9c5a62f6dcc3f55cbd535112c\",\n    \"zh:5f4456d53f5256ccfdb87dd35d3bf34578d01bd9b71cffaf507f0692805eac8a\",\n    \"zh:6c1ecfacc5f7079a068d7f8eb8924485d4ec8183f36e6318a6e748d35921ddac\",\n    \"zh:6d86641edeb8c394f121f7b0a691d72f89cf9b938b987a01fc32aad396a50555\",\n    \"zh:76947bd7bc7033b33980538da149c94e386f9b0abb2ce63733f25a57517e4742\",\n    \"zh:79c07f4c8b3a63d9f89e25e4348b462c57e179bca66ba533710851c485e282db\",\n    \"zh:ac1c2b941d994728a3a93aba093fd2202f9311d099ff85f66678897c792161ba\",\n    \"zh:cbb2aa867fd828fcb4125239e00862b9a3bc2f280e945c760224276b476f4c49\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/tflint/no-tf-source/.tflint.hcl",
    "content": "plugin \"terraform\" {\n  enabled = true\n  version = \"0.2.1\"\n  source  = \"github.com/terraform-linters/tflint-ruleset-terraform\"\n}\n\nconfig { format = \"compact\" }\n"
  },
  {
    "path": "test/fixtures/tflint/no-tf-source/main.tf",
    "content": "terraform {\n  required_providers {\n    random = {\n      version = \">= 3.4.0\"\n    }\n  }\n\n  required_version = \">= 1.2.7\"\n}\n\nresource \"random_id\" \"env\" {\n  byte_length = 8\n}\n"
  },
  {
    "path": "test/fixtures/tflint/no-tf-source/outputs.tf",
    "content": "# Empty outputs\n"
  },
  {
    "path": "test/fixtures/tflint/no-tf-source/terragrunt.hcl",
    "content": "terraform {\n\n  before_hook \"tflint\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"tflint\"]\n  }\n}\n\n"
  },
  {
    "path": "test/fixtures/tflint/no-tf-source/variables.tf",
    "content": "# Empty variables\n"
  },
  {
    "path": "test/fixtures/tflint/tflint-args/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/aws\" {\n  version     = \"5.11.0\"\n  constraints = \"5.11.0\"\n  hashes = [\n    \"h1:2IiEX8whcDTcOIYQffFHicthX0p07oGGcgIKmU/7qYo=\",\n    \"h1:4uGqee8u/cznRtpXblJRXts7i2ZZkB8YtKqRUSDeI8Q=\",\n    \"h1:GG7b3g/hQnvAUFkHahOUva1pGRfZvSR7Xx1bNn0I9Ss=\",\n    \"zh:06944814fc592596796a5f99605dcf92882c495640652c90497bc616448664b1\",\n    \"zh:079096253113fd93a33d655c0339c571dffa7dadfaacafda20c9ca3eebd23a11\",\n    \"zh:08817ffa86d612819a07cd13cec5b66d50f4b8f40c1048e88e4761ccf17172c1\",\n    \"zh:0d010c2bba5c3d4bb3e74b3ed106269481a4109e35ec39b61bb08de1582d6571\",\n    \"zh:46d81b9c6b873c563aa4a69e6d03f9c5c35ae120829b84d4dfea6089595acd24\",\n    \"zh:470dcc6c34c203d2884852d22436ef6f928c1fdbc5a96466f34561cb2c817244\",\n    \"zh:787add339e00161a71e605ab11fd460d4c30c094d9a5f49bf71bcee5eb8617fa\",\n    \"zh:9cb468ec55a6710259e16dd889d80fc58bcafbeb06c9a424167df85f57945a3d\",\n    \"zh:f1dcd170ef0479d91abe57b2151939d1a584fb03b6c517d507f2f49f4e1b9f0e\",\n    \"zh:f52e479435675ab227ed5eb4922f1c314c66c5dd0ae84ed33263c0251638b70f\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/tflint/tflint-args/.tflint.hcl",
    "content": "plugin \"terraform\" {\n  enabled = true\n  version = \"0.2.1\"\n  source  = \"github.com/terraform-linters/tflint-ruleset-terraform\"\n}\n\nplugin \"aws\" {\n  enabled = true\n  version = \"0.25.0\"\n  source  = \"github.com/terraform-linters/tflint-ruleset-aws\"\n}\n\nconfig {\n  module = true\n}\n\nrule \"aws_s3_bucket_name\" {\n  enabled = true\n  regex = \"my-prefix-.*\"\n  prefix = \"my-prefix-\"\n}\n"
  },
  {
    "path": "test/fixtures/tflint/tflint-args/extra.tfvars",
    "content": "bucket_name = \"my-prefix-123\"\n"
  },
  {
    "path": "test/fixtures/tflint/tflint-args/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"registry.opentofu.org/hashicorp/aws\"\n      version = \"5.11.0\"\n    }\n  }\n  required_version = \">= 1.2.7\"\n}\n\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\n\nresource \"aws_s3_bucket\" \"bucket\" {\n  bucket = var.bucket_name\n}\n"
  },
  {
    "path": "test/fixtures/tflint/tflint-args/outputs.tf",
    "content": "output \"q1\" {\n  description = \"output\"\n  value       = var.bucket_name\n}\n"
  },
  {
    "path": "test/fixtures/tflint/tflint-args/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n\n  before_hook \"tflint\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"tflint\" , \"--terragrunt-external-tflint\", \"--minimum-failure-severity=error\"]\n  }\n\n  extra_arguments \"var-files\" {\n    commands = [\"apply\", \"plan\"]\n    required_var_files = [\"extra.tfvars\"]\n  }\n}\n\n"
  },
  {
    "path": "test/fixtures/tflint/tflint-args/variables.tf",
    "content": "variable \"bucket_name\" {\n  description = \"bucket name\"\n  type        = string\n}\n\n"
  },
  {
    "path": "test/fixtures/tflint/tfvar-passing/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/aws\" {\n  version     = \"5.11.0\"\n  constraints = \"5.11.0\"\n  hashes = [\n    \"h1:2IiEX8whcDTcOIYQffFHicthX0p07oGGcgIKmU/7qYo=\",\n    \"h1:4uGqee8u/cznRtpXblJRXts7i2ZZkB8YtKqRUSDeI8Q=\",\n    \"h1:GG7b3g/hQnvAUFkHahOUva1pGRfZvSR7Xx1bNn0I9Ss=\",\n    \"zh:06944814fc592596796a5f99605dcf92882c495640652c90497bc616448664b1\",\n    \"zh:079096253113fd93a33d655c0339c571dffa7dadfaacafda20c9ca3eebd23a11\",\n    \"zh:08817ffa86d612819a07cd13cec5b66d50f4b8f40c1048e88e4761ccf17172c1\",\n    \"zh:0d010c2bba5c3d4bb3e74b3ed106269481a4109e35ec39b61bb08de1582d6571\",\n    \"zh:46d81b9c6b873c563aa4a69e6d03f9c5c35ae120829b84d4dfea6089595acd24\",\n    \"zh:470dcc6c34c203d2884852d22436ef6f928c1fdbc5a96466f34561cb2c817244\",\n    \"zh:787add339e00161a71e605ab11fd460d4c30c094d9a5f49bf71bcee5eb8617fa\",\n    \"zh:9cb468ec55a6710259e16dd889d80fc58bcafbeb06c9a424167df85f57945a3d\",\n    \"zh:f1dcd170ef0479d91abe57b2151939d1a584fb03b6c517d507f2f49f4e1b9f0e\",\n    \"zh:f52e479435675ab227ed5eb4922f1c314c66c5dd0ae84ed33263c0251638b70f\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/tflint/tfvar-passing/.tflint.hcl",
    "content": "plugin \"terraform\" {\n  enabled = true\n  version = \"0.2.1\"\n  source  = \"github.com/terraform-linters/tflint-ruleset-terraform\"\n}\n\nplugin \"aws\" {\n  enabled = true\n  version = \"0.25.0\"\n  source  = \"github.com/terraform-linters/tflint-ruleset-aws\"\n}\n\nconfig {\n  module = true\n}\n\nrule \"aws_s3_bucket_name\" {\n  enabled = true\n  regex = \"my-prefix-.*\"\n  prefix = \"my-prefix-\"\n}\n"
  },
  {
    "path": "test/fixtures/tflint/tfvar-passing/extra.tfvars",
    "content": "bucket_name = \"my-prefix-qwe\"\n"
  },
  {
    "path": "test/fixtures/tflint/tfvar-passing/main.tf",
    "content": "terraform {\n  required_providers {\n    aws = {\n      source  = \"registry.opentofu.org/hashicorp/aws\"\n      version = \"5.11.0\"\n    }\n  }\n  required_version = \">= 1.2.7\"\n}\n\nprovider \"aws\" {\n  region = \"us-east-1\"\n}\n\nresource \"aws_s3_bucket\" \"bucket\" {\n  bucket = var.bucket_name\n}\n"
  },
  {
    "path": "test/fixtures/tflint/tfvar-passing/outputs.tf",
    "content": "output \"q1\" {\n  description = \"output\"\n  value       = var.bucket_name\n}\n"
  },
  {
    "path": "test/fixtures/tflint/tfvar-passing/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n\n  before_hook \"tflint\" {\n    commands = [\"apply\", \"plan\"]\n    execute = [\"tflint\" , \"--terragrunt-external-tflint\"]\n  }\n\n  extra_arguments \"var-files\" {\n    commands = [\"apply\", \"plan\"]\n    required_var_files = [\"extra.tfvars\"]\n  }\n}\n\n"
  },
  {
    "path": "test/fixtures/tflint/tfvar-passing/variables.tf",
    "content": "variable \"bucket_name\" {\n  description = \"bucket name\"\n  type        = string\n}\n\n"
  },
  {
    "path": "test/fixtures/tfr/root/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/tfr/root/terragrunt.hcl",
    "content": "# Retrieve a module from the public terraform registry to use with terragrunt\nterraform {\n  source = \"tfr://registry.terraform.io/yorinasub17/terragrunt-registry-test/null?version=0.0.2\"\n}\n"
  },
  {
    "path": "test/fixtures/tfr/root-shorthand/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/tfr/root-shorthand/terragrunt.hcl",
    "content": "# Retrieve a module from the public Hashicorp/OpenTofu registry to use with terragrunt\nterraform {\n  source = \"tfr:///gruntwork-io/terragrunt-registry-test/null?version=0.0.2\"\n}\n"
  },
  {
    "path": "test/fixtures/tfr/subdir/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/tfr/subdir/terragrunt.hcl",
    "content": "# Retrieve a module from the public terraform registry to use with terragrunt\nterraform {\n  source = \"tfr://registry.terraform.io/yorinasub17/terragrunt-registry-test/null//modules/one?version=0.0.2\"\n}\n"
  },
  {
    "path": "test/fixtures/tfr/subdir-with-reference/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/null\" {\n  version = \"3.2.4\"\n  hashes = [\n    \"h1:i+WKhUHL2REY5EGmiHjfUljJB8UKZ9QdhdM5uTeUhC4=\",\n    \"h1:jsKjBiLb+v3OIC3xuDiY4sR0r1OHUMSWPYKult9MhT0=\",\n    \"h1:wg0cxqzzWFNnEzw0LSlPL9Ovqy1VFW44A7ayHEU/A1I=\",\n    \"zh:1769783386610bed8bb1e861a119fe25058be41895e3996d9216dd6bb8a7aee3\",\n    \"zh:32c62a9387ad0b861b5262b41c5e9ed6e940eda729c2a0e58100e6629af27ddb\",\n    \"zh:339bf8c2f9733fce068eb6d5612701144c752425cebeafab36563a16be460fb2\",\n    \"zh:36731f23343aee12a7e078067a98644c0126714c4fe9ac930eecb0f2361788c4\",\n    \"zh:3d106c7e32a929e2843f732625a582e562ff09120021e510a51a6f5d01175b8d\",\n    \"zh:74bcb3567708171ad83b234b92c9d63ab441ef882b770b0210c2b14fdbe3b1b6\",\n    \"zh:90b55bdbffa35df9204282251059e62c178b0ac7035958b93a647839643c0072\",\n    \"zh:ae24c0e5adc692b8f94cb23a000f91a316070fdc19418578dcf2134ff57cf447\",\n    \"zh:b5c10d4ad860c4c21273203d1de6d2f0286845edf1c64319fa2362df526b5f58\",\n    \"zh:e05bbd88e82e1d6234988c85db62fd66f11502645838fff594a2ec25352ecd80\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/tfr/subdir-with-reference/terragrunt.hcl",
    "content": "# Retrieve a module from the public terraform registry to use with terragrunt\nterraform {\n  source = \"tfr://registry.terraform.io/yorinasub17/terragrunt-registry-test/null//modules/two?version=0.0.2\"\n}\n"
  },
  {
    "path": "test/fixtures/tftest/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/aws\" {\n  version = \"6.28.0\"\n  hashes = [\n    \"h1:i0G7vt2sNy0oz84IiuG4gplonNVyOLRdprKLurU8pe4=\",\n    \"h1:tcau98fkhZ2RhbPHo8LdiiUk2RGpZUgT/t06sdMLids=\",\n    \"h1:wek8vEEZpTPulbLi9xCf2wnxvc97JXAN4qcOhduSg7k=\",\n    \"zh:38d58305206953783c150fb96d5c4f3ea5fe0b9e0987d927c884a6b0f2adf7a9\",\n    \"zh:43fd483251165f98b7a44360b41b437d309b007ef2bfff818eedcf3730e3f5cb\",\n    \"zh:4753decc5a718cb74b08244a02d00c150f0ddd6ebf2e1227f6a985c647c03ce9\",\n    \"zh:5956525650554bd3fbc4b695eb5250193f0ebf94c45862a7730457ab6a315069\",\n    \"zh:76d98fa1146750c01f607bae4421952ee9cd14ed3a4a59deb7136749adb9e0ae\",\n    \"zh:792c29e5ec91356baddb6219ac7f6f1df09c251cbe4ab6e089fc25d64270b22a\",\n    \"zh:856424380caa7c1536dc00515d12beac2693db1a8425da654eed5530abeb17d9\",\n    \"zh:e8982ec2bc692efa7236e3565e7094a09f52c5b71d8860a570a36fb31a40f27f\",\n    \"zh:f5e7ff825dc3f7356fb80936bfe7bb1b54a728ccf429cb753cfe590932f0403b\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/tftest/main.tf",
    "content": "provider \"aws\" {\n  region = \"us-east-1\"\n}\n\nvariable \"bucket_prefix\" {\n  type = string\n}\n\nresource \"aws_s3_bucket\" \"bucket\" {\n  bucket = \"${var.bucket_prefix}-bucket\"\n}\n\noutput \"bucket_name\" {\n  value = aws_s3_bucket.bucket.bucket\n}\n"
  },
  {
    "path": "test/fixtures/tftest/terragrunt.hcl",
    "content": "inputs = {\n  bucket_prefix = \"tg-test\"\n}\n"
  },
  {
    "path": "test/fixtures/tftest/validate_name.tftest.hcl",
    "content": "run \"valid_string_concat\" {\n  command = plan\n  assert {\n    condition     = aws_s3_bucket.bucket.bucket == \"tg-test-bucket\"\n    error_message = \"S3 bucket name expected to be tg-test-bucket\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/timecmp/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/timecmp/main.tf",
    "content": "variable \"timecmp1\" {\n  type = number\n}\n\nvariable \"timecmp2\" {\n  type = number\n}\n\nvariable \"timecmp3\" {\n  type = number\n}\n\nvariable \"timecmp4\" {\n  type = number\n}\n\nvariable \"timecmp5\" {\n  type = number\n}\n\nvariable \"timecmp6\" {\n  type = number\n}\n\noutput \"timecmp1\" {\n  value = var.timecmp1\n}\n\noutput \"timecmp2\" {\n  value = var.timecmp2\n}\n\noutput \"timecmp3\" {\n  value = var.timecmp3\n}\n\noutput \"timecmp4\" {\n  value = var.timecmp4\n}\n\noutput \"timecmp5\" {\n  value = var.timecmp5\n}\n\noutput \"timecmp6\" {\n  value = var.timecmp6\n}\n"
  },
  {
    "path": "test/fixtures/timecmp/terragrunt.hcl",
    "content": "inputs = {\n  timecmp1 = timecmp(\"2017-11-22T00:00:00Z\", \"2017-11-22T00:00:00Z\")\n  timecmp2 = timecmp(\"2017-11-22T00:00:00Z\", \"2017-11-22T01:00:00+01:00\")\n  timecmp3 = timecmp(\"2017-11-22T00:00:01Z\", \"2017-11-22T01:00:00+01:00\")\n  timecmp4 = timecmp(\"2017-11-22T01:00:00Z\", \"2017-11-22T00:59:00-01:00\")\n  timecmp5 = timecmp(\"2017-11-22T01:00:00+01:00\", \"2017-11-22T01:00:00-01:00\")\n  timecmp6 = timecmp(\"2017-11-22T01:00:00-01:00\", \"2017-11-22T01:00:00+01:00\")\n}\n"
  },
  {
    "path": "test/fixtures/timecmp-errors/invalid-timestamp/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/timecmp-errors/invalid-timestamp/main.tf",
    "content": "variable \"timecmp1\" {\n  type = number\n}\n\noutput \"timecmp1\" {\n  value = var.timecmp1\n}\n"
  },
  {
    "path": "test/fixtures/timecmp-errors/invalid-timestamp/terragrunt.hcl",
    "content": "inputs = {\n  timecmp1 = timecmp(\"2017-11-22 00:00:00Z\", \"2017-11-22T01:00:00+01:00\")\n}\n"
  },
  {
    "path": "test/fixtures/tips/terragrunt.hcl",
    "content": "# This fixture is designed to cause an error to test tip display\nterraform {\n  source = \"./non-existent-module\"\n}\n"
  },
  {
    "path": "test/fixtures/tofu-http-encryption/app/main.tf",
    "content": "variable \"dep_value\" {\n  type = string\n}\n\nterraform {\n  required_version = \">= 1.0\"\n\n  required_providers {\n    local = {\n      source  = \"hashicorp/local\"\n      version = \">= 2.1\"\n    }\n  }\n}\n\nresource \"local_file\" \"some_file\" {\n  content  = \"Dep had value: ${var.dep_value}\"\n  filename = \"${path.module}/some_file.txt\"\n}\n\noutput \"my_value\" {\n  value = local_file.some_file.content\n}\n"
  },
  {
    "path": "test/fixtures/tofu-http-encryption/app/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ndependency \"dep\" {\n  config_path = \"../dep\"\n}\n\ninputs = {\n  dep_value = dependency.dep.outputs.some_value\n}\n"
  },
  {
    "path": "test/fixtures/tofu-http-encryption/dep/main.tf",
    "content": "terraform {\n  required_version = \">= 1.0\"\n\n  required_providers {\n    local = {\n      source  = \"hashicorp/local\"\n      version = \">= 2.1\"\n    }\n  }\n}\n\nresource \"local_file\" \"some_file\" {\n  content  = \"hello-from-dep\"\n  filename = \"${path.module}/some_file.txt\"\n}\n\noutput \"some_value\" {\n  value = local_file.some_file.content\n}\n"
  },
  {
    "path": "test/fixtures/tofu-http-encryption/dep/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/tofu-http-encryption/root.hcl",
    "content": "remote_state {\n  backend = \"http\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n  config = {\n    address        = \"__HTTP_SERVER_URL__/state/${path_relative_to_include()}\"\n    lock_address   = \"__HTTP_SERVER_URL__/state/${path_relative_to_include()}\"\n    unlock_address = \"__HTTP_SERVER_URL__/state/${path_relative_to_include()}\"\n    username       = \"admin\"\n    password       = \"secret\"\n  }\n  encryption = {\n    key_provider = \"pbkdf2\"\n    passphrase   = \"testpassphrase123\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/tofu-state-encryption/aws-kms/terragrunt.hcl",
    "content": "# Test AWS KMS encryption with local state\nremote_state {\n  backend = \"local\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    path = \"${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate\"\n  }\n\n  encryption = {\n    key_provider = \"aws_kms\"\n    region       = \"__FILL_IN_AWS_REGION__\"\n    kms_key_id   = \"__FILL_IN_KMS_KEY_ID__\"\n    key_spec     = \"AES_256\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/tofu-state-encryption/gcp-kms/terragrunt.hcl",
    "content": "# Test GCP KMS encryption with local state\nremote_state {\n  backend = \"local\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    path = \"${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate\"\n  }\n\n  encryption = {\n    key_provider       = \"gcp_kms\"\n    kms_encryption_key = \"__FILL_IN_KMS_KEY_ID__\"\n    key_length         = 1024\n  }\n}\n"
  },
  {
    "path": "test/fixtures/tofu-state-encryption/openbao/terragrunt.hcl",
    "content": "# Test openbao encryption with local state\nremote_state {\n  backend = \"local\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    path = \"${get_terragrunt_dir()}/${path_relative_to_include()}/tofu.tfstate\"\n  }\n\n  encryption = {\n    key_provider = \"openbao\"\n    key_name     = \"__FILL_IN_OPENBAO_KEY_NAME__\"\n    address      = \"__FILL_IN_OPENBAO_ADDRESS__\"\n    token        = \"__FILL_IN_OPENBAO_TOKEN__\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/tofu-state-encryption/pbkdf2/terragrunt.hcl",
    "content": "# Test PBKDF2 encryption with local state\nremote_state {\n  backend = \"local\"\n\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite_terragrunt\"\n  }\n\n  config = {\n    path = \"${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate\"\n  }\n\n  encryption = {\n    key_provider = \"pbkdf2\"\n    passphrase = \"randompassphrase123456\"\n  }\n}\n"
  },
  {
    "path": "test/fixtures/trace-parent/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n\nprovider \"registry.opentofu.org/hashicorp/external\" {\n  version = \"2.3.5\"\n  hashes = [\n    \"h1:+OsaKKx2awgjh6j/2B3VBP6q4Dqg2Fc0uDUZll66/Hg=\",\n    \"h1:VsIY+hWGvWHaGvGTSKZslY13lPeAtSTxfZRPbpLMMhs=\",\n    \"h1:jcVmeuuz74tdRt2kj0MpUG9AORdlAlRRQ3k61y0r5Vc=\",\n    \"zh:1fb9aca1f068374a09d438dba84c9d8ba5915d24934a72b6ef66ef6818329151\",\n    \"zh:3eab30e4fcc76369deffb185b4d225999fc82d2eaaa6484d3b3164a4ed0f7c49\",\n    \"zh:4f8b7a4832a68080f0bf4f155b56a691832d8a91ce8096dac0f13a90081abc50\",\n    \"zh:5ff1935612db62e48e4fe6cfb83dfac401b506a5b7b38342217616fbcab70ce0\",\n    \"zh:993192234d327ec86726041eb6d1efb001e41f32e4518ad8b9b162130b65ee9a\",\n    \"zh:ce445e68282a2c4b2d1f994a2730406df4ea47914c0932fb4a7eb040a7ec7061\",\n    \"zh:e305e17216840c54194141fb852839c2cedd6b41abd70cf8d606d6e88ed40e64\",\n    \"zh:edba65fb241d663c09aa2cbf75026c840e963d5195f27000f216829e49811437\",\n    \"zh:f306cc6f6ec9beaf75bdcefaadb7b77af320b1f9b56d8f50df5ebd2189a93148\",\n    \"zh:fb2ff9e1f86796fda87e1f122d40568912a904da51d477461b850d81a0105f3d\",\n  ]\n}\n"
  },
  {
    "path": "test/fixtures/trace-parent/get_traceparent.sh",
    "content": "#!/usr/bin/env bash\nset -e\n\necho \"$1 {\\\"traceparent\\\": \\\"${TRACEPARENT}\\\"}\"\n"
  },
  {
    "path": "test/fixtures/trace-parent/main.tf",
    "content": "data \"external\" \"traceparent\" {\n  program = [\"${path.module}/get_traceparent.sh\"]\n\n  query = {\n    nonce = timestamp()\n  }\n}\n\noutput \"traceparent_value\" {\n  value = data.external.traceparent.result[\"traceparent\"]\n}\n"
  },
  {
    "path": "test/fixtures/trace-parent/terragrunt.hcl",
    "content": "terraform {\n  source = \".\"\n  before_hook \"hook_print_traceparent\" {\n    commands = [\"apply\"]\n    execute = [\"./get_traceparent.sh\", \"hook_print_traceparent\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/units-reading/including/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/units-reading/including/main.tf",
    "content": "# Empty main.tf for units-reading test fixture\n"
  },
  {
    "path": "test/fixtures/units-reading/including/terragrunt.hcl",
    "content": "include \"shared\" {\n\tpath = find_in_parent_folders(\"shared.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/units-reading/indirect/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/units-reading/indirect/main.tf",
    "content": ""
  },
  {
    "path": "test/fixtures/units-reading/indirect/src/test.txt",
    "content": ""
  },
  {
    "path": "test/fixtures/units-reading/indirect/terragrunt.hcl",
    "content": "locals {\n  all_files        = split(\"\\n\", run_cmd(\"--terragrunt-quiet\", \"ls\", \"-p\", \"src\"))\n  all_files_marked = [for f in local.all_files : mark_as_read(\"src/${f}\")]\n}\n"
  },
  {
    "path": "test/fixtures/units-reading/reading-from-tf/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/units-reading/reading-from-tf/main.tf",
    "content": "variable \"filename\" {}\noutput \"shared\" {\n  value = jsondecode(file(var.filename))\n}\n"
  },
  {
    "path": "test/fixtures/units-reading/reading-from-tf/terragrunt.hcl",
    "content": "locals {\n\tfilename = mark_as_read(find_in_parent_folders(\"shared.json\"))\n}\n\ninputs = {\n\tfilename = local.filename\n}\n"
  },
  {
    "path": "test/fixtures/units-reading/reading-hcl/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/units-reading/reading-hcl/main.tf",
    "content": "variable \"shared\" {}\noutput \"shared\" {\n  value = var.shared\n}\n"
  },
  {
    "path": "test/fixtures/units-reading/reading-hcl/terragrunt.hcl",
    "content": "locals {\n\tshared = read_terragrunt_config(find_in_parent_folders(\"shared.hcl\")).locals\n}\n\ninputs = {\n\tshared = local.shared\n}\n"
  },
  {
    "path": "test/fixtures/units-reading/reading-hcl-and-tfvars/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/units-reading/reading-hcl-and-tfvars/main.tf",
    "content": "variable \"shared_hcl\" {}\noutput \"shared_hcl\" {\n  value = var.shared_hcl\n}\n\nvariable \"shared_tfvars\" {}\noutput \"shared_tfvars\" {\n  value = var.shared_tfvars\n}\n"
  },
  {
    "path": "test/fixtures/units-reading/reading-hcl-and-tfvars/terragrunt.hcl",
    "content": "locals {\n\tshared_hcl = read_terragrunt_config(find_in_parent_folders(\"shared.hcl\")).locals\n\tshared_tfvars = read_tfvars_file(find_in_parent_folders(\"shared.tfvars\"))\n}\n\ninputs = {\n\tshared_hcl    = local.shared_hcl\n\tshared_tfvars = local.shared_tfvars\n}\n"
  },
  {
    "path": "test/fixtures/units-reading/reading-json/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/units-reading/reading-json/main.tf",
    "content": "variable \"shared\" {}\noutput \"shared\" {\n  value = var.shared\n}\n"
  },
  {
    "path": "test/fixtures/units-reading/reading-json/terragrunt.hcl",
    "content": "locals {\n\tshared = jsondecode(file(mark_as_read(find_in_parent_folders(\"shared.json\"))))\n}\n\ninputs = {\n\tshared = local.shared\n}\n"
  },
  {
    "path": "test/fixtures/units-reading/reading-sops/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/units-reading/reading-sops/main.tf",
    "content": "variable \"shared\" {}\noutput \"shared\" {\n  value = var.shared\n}\n"
  },
  {
    "path": "test/fixtures/units-reading/reading-sops/terragrunt.hcl",
    "content": "locals {\n\tshared = sops_decrypt_file(find_in_parent_folders(\"secrets.txt\"))\n}\n\ninputs = {\n\tshared = local.shared\n}\n"
  },
  {
    "path": "test/fixtures/units-reading/reading-tfvars/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/units-reading/reading-tfvars/main.tf",
    "content": "variable \"shared\" {}\noutput \"shared\" {\n  value = var.shared\n}\n"
  },
  {
    "path": "test/fixtures/units-reading/reading-tfvars/terragrunt.hcl",
    "content": "locals {\n\tshared = read_tfvars_file(find_in_parent_folders(\"shared.tfvars\"))\n}\n\ninputs = {\n\tshared = local.shared\n}\n"
  },
  {
    "path": "test/fixtures/units-reading/secrets.txt",
    "content": "{\n\t\"data\": \"ENC[AES256_GCM,data:w2jDRJR9BeIMSKE4+qnKWhfM,iv:08ACLYrUGtWriOV/ua4X6NZt57VmiTmAcnxB5V+8AUc=,tag:cVdkIO4EXAmyV3y7n/zbiA==,type:str]\",\n\t\"sops\": {\n\t\t\"kms\": null,\n\t\t\"gcp_kms\": null,\n\t\t\"azure_kv\": null,\n\t\t\"hc_vault\": null,\n\t\t\"age\": null,\n\t\t\"lastmodified\": \"2021-12-17T18:38:13Z\",\n\t\t\"mac\": \"ENC[AES256_GCM,data:8lPZmY8YgA0DqPRxLC9hVoRUXmbzaXgUBv3MHTm4iK44/6URIgJBUnPFPUbwIN7xbIgXd+QPQEMvfsmifqXorynGEwt2WtMKCPANg+2Ctf2KMmj7fGpe3HIlRhQiixip7/xzrIMbSdIRMS098D42JTvOIFNbWVQhByfN64AnDJY=,iv:wtouC/mWjhFwiJKDS6+5LqnQMcAeejElXLaL3H15jbY=,tag:6Bmemr2BMgShaMO3v4uiXw==,type:str]\",\n\t\t\"pgp\": [\n\t\t\t{\n\t\t\t\t\"created_at\": \"2021-12-17T18:38:12Z\",\n\t\t\t\t\"enc\": \"-----BEGIN PGP MESSAGE-----\\n\\nhQEMA0sXzMgpEabgAQf+KHsPp4Pp8YNtG7ChRpZO2qB/bFncWtAF9evO+RjAEahb\\nM+hzxkB5KDUSMYs0aeWeOrOqYPrjPPJxCspZtQhy8/qrC064kA7gq2PWhYAqGcKP\\ntnPI8D0SYDZBgoyHRqFuuD5TZio8swE89SxphftL0W3KkHay7WKQHj/cFqNoISNl\\nn0XeCgbacIwo5WxWz1qNFvaeo0rFFFhIhbfaegx/SWwUi1y6WK7sB0QobMRwXHj+\\nORiUWVvx/fCIMCaerPN/SjIA/DgzbZ3DWaixYXpW85Ipz7myu/zUQcWnWcGXnMRQ\\nERMYc6GyyLHwjZN1XuvXdPXvAt6vvaH4w5U9kW2l19JeAZXkcM14ivDoGwY1oLcX\\n4d2/MAS7vM7SgmcPBGmpNsJJgkWTgoc8qeFtu9u3e4e9pR4+dcJCbGQLQ5RiyM2Z\\nsyHjL6em/j4JLdtbM16orP6Q3oEPelphG7sxbDXBeA==\\n=6u1S\\n-----END PGP MESSAGE-----\\n\",\n\t\t\t\t\"fp\": \"3EF98802EEDCAF0C688B81F419546E0C123C664E\"\n\t\t\t}\n\t\t],\n\t\t\"unencrypted_suffix\": \"_unencrypted\",\n\t\t\"version\": \"3.7.1\"\n\t}\n}"
  },
  {
    "path": "test/fixtures/units-reading/shared.hcl",
    "content": "locals {\n\tshared_hcl = \"value\"\n}\n"
  },
  {
    "path": "test/fixtures/units-reading/shared.json",
    "content": "{\n\t\"key\": \"value\"\n}\n"
  },
  {
    "path": "test/fixtures/units-reading/shared.tfvars",
    "content": "shared_tfvars = \"value\"\n"
  },
  {
    "path": "test/fixtures/units-reading/test_pgp_key.asc",
    "content": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\nlQOYBF69y60BCAC0Ton9jmk5/ZV3yKi2o0rY22G41dh9dLSoxTRJNKsKeSJePHK0\nqtrHriPjnHxFLXrXfYiv6FkdkPv9dhmKjMf1op/RRw0uTDZ6CsQd5C8nT9LBPiB6\nJsO89+GCwoVsPQGGVgqq7PWSbA8fJkAPBNvdQvxJnSbm4EJTcaU7XZmmY/eZnhPt\npho66X3PIQSx8Xm9Dc7jHo/7bBCoEV15OdFYi44imDW9sCdCpYTVzMF2ZRtXGylQ\nU6P5Iu3SEMTnn4jwPpyKllT4qRFdqMQ4GvqE47uRb+k/FDXP8UC6h/5GNHfbQtEO\nCMngw5vJciLEUWW1e97baBPiccLXNaBXiecXABEBAAEAB/44Mkpb1qkBRAHz5XyA\nAADx+d5JR41D/L3Z5CzZrCqrBvopONnfaWjq12GkLT+mJ/ijdSLHALnVtzxy0P5A\n6oPgESGSjnyTM3m/K9/YGSiBLiXXyM9CgxZ6rR+CK7J9+72f6u1EPLqFOly0Lq3E\ngJUuLxSGtQ5M4xSJAWCoUhHzg6rUP978RQXc4AgfkfXaq/ILGyOOa7VSz4NVs3cZ\n+WtPqlFhy5AsoMoLvXrwqLhVa9QsCiW0dYScRPD9q29wRHnKDN2nSxTuHbWu55r+\nkrevzk5gPOTdj61vz6/xD/rqNFakZDjFfZIu+srqjnLMVEPYudrH0buIAF9RFYPp\n8x+BBADTH3Q5+b+n770aFBDldIjD+biEnjnw6A396RNX2YIPqWoz0b7lQSYBM+DI\nTTm7OFmacH2DSYHxx/e2GGOOglbc2czPlWGyegSfZblwcPPT47uVY6ZWIXAsszF6\nopL/ahQ336FEl8Ws8vEjUoTXWFzF50gfmdC2xPYYmoxymmZ1IQQA2qIvbPausqO7\ntHk9Bco1MLLzP5JIAvuLUNHOj9H+bUXh5btkm2gZRIGglScljF6HT180bJ4gt07n\nH1QNpyWKuH0h0YSTDlBjLPQvElQAvxMuvT7RAWagXQo8ew+2FHbrH9Bhlys1wcG3\n+h1S2G8M6TyY0dv98TvqHV9RG+YCHTcEAMQTpJBn+yelK8LXyVrw9OmH0qmQ9TOF\nuJihLBSd7nen+l4a3yDpISk6hrb/q4AjpzMJ8fK17AEuH9OxUkO8vgtxefvnoDTe\nGYwuhszZRbWLYVDHZEkEjiGbCiqZw5530tHShA/IdBc5LMd9fwsag4Bru0cKhyCN\noe1FvVcABEHXR+O0EFRlcnJhZ3J1bnQgVGVzdHOJAUwEEwEKADYWIQQ++YgC7tyv\nDGiLgfQZVG4MEjxmTgUCXr3LrQIbAwQLCQgHBBUKCQgFFgIDAQACHgECF4AACgkQ\nGVRuDBI8Zk76SwgAkfEmyr9qOtNXpD4m8SnCrg8EJRX584hJyAu08/uB+QQV7Xzj\nAzns4oVGGpZYEB7sUd22ejw8BGe6OI4puL43fnpDcFj4UA7FAIzQTKNztWFci2Ho\nL0IAwcdzSlJb6qREMJacit8G4Aayn/bqZSNF4cu0XipRlOtK28FXP1PQThlNtSJL\nyPpYRyU6s2CLSeRRqbi1MJ4KgKcnfTvv/4+VYQ6y7/rsDnXDA0nLgzYKMqtNdBPY\nr4u7kZsGcWP8w9xQDWNJtOqj8NiRSAtnglqiYTHuEQsRjYGjH+vHqnGfw7dcK4+3\nRlc7ZPGON2NCmJ6OajedIxZR6yNiDwj1AfuXxZ0DmARevcutAQgAtV1FnBgLKuqg\nBVs09UbyrFuVJIx6M4e0IFac4bVzyfGJKjmxHx2n7D8slTZs+lcBNDy+CseAsAuc\nixv/jVAsl6saip5nUIAMmHkPkPM+BVAuqBRWoYWGFw3TL83MaYAgBkSkurx+Koxv\nw2bTumxrb9xsyiy7Bn7kaIyZU/nN8Xb0xf6QC+y7ckzaI6ZT4utrtJGXya4Cu1rp\nqUUYs/4ssWjZS7KuRwu8ijNwq/Zl0920IXH/sX/gUhL2AO5sgExC+52yjTTvhlIb\nee8tjMB4JT6Hq1RS9JGkNqcUOvJ99WlUoOtZUJU9xAN4eyPUy+r7v9wxcwoguanC\nLWaKSNe9hQARAQABAAf8CToRuLNtHLBWFspWmI8/o67Ubh5qzc9UGycSO9YVlMsM\nfZD00WPwBJq22RqCkyk0/vmDePHrD/RvRjvU6wAOsg1QQDLMh8cT8k01Elya+v3i\nzxtFyFmOLQMU4O7j547PUkemEnf/eokC20U9H/60Z+WmGpJfAMwY27bMytMVJqPJ\ngL1BNo9l0HGSTkU7mMqq48MsozTJvmCYbrVeKhnHdpAFHHSNQ9hGzgpZkpTisDk7\nqDIHhF1Nv7IRZgX4OGUbj1hBk63ao1TclhbB8d7gitvTODbMxFOF9crm7ttWtq/C\nCIBM9X5ilufEnuN2eV4LxLHeghQOGsJhl/FB8gov+wQAx/Ja/2WFzHoEc5evJtNU\nifKpaIAp4Qj673dx21Vzr43qnuNNxLuG53FvgRpfXhVRyzWmNnJByQ3NRVUv4J1j\nGXymlPPPzmAbML8874zShMnMcd1+UCZ+0dgFeB6CetVySExO0p+qUW+fioP5jrJk\nspNxtY01RE5AUSWUeQN3sdcEAOg1T1cUIEq/dgPLDQE3mPQta7fJeiDddheNgQRp\nxKHxDKSpWd1RtmiUxG0cT3z68M8aRcL/X+q731WGadNhM+yp4xg6wVTpL8USdLZr\nqqiuvMYqowryZOdvPUP8OE1lQwtWizCFOoNL+yJyKVzt7+Z0CrkE8s3li68sLMeg\nz5gDBACflcuTWLMNt3buo/31YrNWLDRxDMdKpNZ0Tpj+Kxda9+GjRGWdMZHwOsIO\nWhGnftxtbKSWu2+PabFBchiwLC1r4WHMFy/fxFFhufJtYI/c58kRd+9I0vw6/JQx\nWvGunELyeTnNu3u+uvagUSchBhNln5hZZOtpaBaDhNngm5DXM0g+iQE2BBgBCgAg\nFiEEPvmIAu7crwxoi4H0GVRuDBI8Zk4FAl69y60CGwwACgkQGVRuDBI8Zk4gogf/\nSyXhch/Ep9ESW+Zx7T+ImfQAVPBmka0NNCEvSF5i/ghTm700Iv/sLcRqhSJDiKLb\n1imeqVN6khmr6+d21uCqqgiv/6X4w6zmxH9h4uPMv3H6WnHi40kDUawR7hFctNLX\ntwArH+xCX5MJgzhqAP5Yzgsk5XLwzgjCuXz06RqtmZVW4ofOP4GWBT24Gg4TyUF9\nIbl1/QLKeYOarD4a/PcEfHhJyvnAmD68uIFr1gABJJneW8vvO4OPjLcptZMIZ1Nf\nxqUuLBAJ71OI3RSSowpI3qCpFhH2j5vICM7jX2gIL9PQEGGg2ljNJoorJIEjYZzS\n0S9IMjn4Mj7CYtAZarnIQw==\n=fO3n\n-----END PGP PRIVATE KEY BLOCK-----\n-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQENBF69y60BCAC0Ton9jmk5/ZV3yKi2o0rY22G41dh9dLSoxTRJNKsKeSJePHK0\nqtrHriPjnHxFLXrXfYiv6FkdkPv9dhmKjMf1op/RRw0uTDZ6CsQd5C8nT9LBPiB6\nJsO89+GCwoVsPQGGVgqq7PWSbA8fJkAPBNvdQvxJnSbm4EJTcaU7XZmmY/eZnhPt\npho66X3PIQSx8Xm9Dc7jHo/7bBCoEV15OdFYi44imDW9sCdCpYTVzMF2ZRtXGylQ\nU6P5Iu3SEMTnn4jwPpyKllT4qRFdqMQ4GvqE47uRb+k/FDXP8UC6h/5GNHfbQtEO\nCMngw5vJciLEUWW1e97baBPiccLXNaBXiecXABEBAAG0EFRlcnJhZ3J1bnQgVGVz\ndHOJAUwEEwEKADYWIQQ++YgC7tyvDGiLgfQZVG4MEjxmTgUCXr3LrQIbAwQLCQgH\nBBUKCQgFFgIDAQACHgECF4AACgkQGVRuDBI8Zk76SwgAkfEmyr9qOtNXpD4m8SnC\nrg8EJRX584hJyAu08/uB+QQV7XzjAzns4oVGGpZYEB7sUd22ejw8BGe6OI4puL43\nfnpDcFj4UA7FAIzQTKNztWFci2HoL0IAwcdzSlJb6qREMJacit8G4Aayn/bqZSNF\n4cu0XipRlOtK28FXP1PQThlNtSJLyPpYRyU6s2CLSeRRqbi1MJ4KgKcnfTvv/4+V\nYQ6y7/rsDnXDA0nLgzYKMqtNdBPYr4u7kZsGcWP8w9xQDWNJtOqj8NiRSAtnglqi\nYTHuEQsRjYGjH+vHqnGfw7dcK4+3Rlc7ZPGON2NCmJ6OajedIxZR6yNiDwj1AfuX\nxbkBDQRevcutAQgAtV1FnBgLKuqgBVs09UbyrFuVJIx6M4e0IFac4bVzyfGJKjmx\nHx2n7D8slTZs+lcBNDy+CseAsAucixv/jVAsl6saip5nUIAMmHkPkPM+BVAuqBRW\noYWGFw3TL83MaYAgBkSkurx+Koxvw2bTumxrb9xsyiy7Bn7kaIyZU/nN8Xb0xf6Q\nC+y7ckzaI6ZT4utrtJGXya4Cu1rpqUUYs/4ssWjZS7KuRwu8ijNwq/Zl0920IXH/\nsX/gUhL2AO5sgExC+52yjTTvhlIbee8tjMB4JT6Hq1RS9JGkNqcUOvJ99WlUoOtZ\nUJU9xAN4eyPUy+r7v9wxcwoguanCLWaKSNe9hQARAQABiQE2BBgBCgAgFiEEPvmI\nAu7crwxoi4H0GVRuDBI8Zk4FAl69y60CGwwACgkQGVRuDBI8Zk4gogf/SyXhch/E\np9ESW+Zx7T+ImfQAVPBmka0NNCEvSF5i/ghTm700Iv/sLcRqhSJDiKLb1imeqVN6\nkhmr6+d21uCqqgiv/6X4w6zmxH9h4uPMv3H6WnHi40kDUawR7hFctNLXtwArH+xC\nX5MJgzhqAP5Yzgsk5XLwzgjCuXz06RqtmZVW4ofOP4GWBT24Gg4TyUF9Ibl1/QLK\neYOarD4a/PcEfHhJyvnAmD68uIFr1gABJJneW8vvO4OPjLcptZMIZ1NfxqUuLBAJ\n71OI3RSSowpI3qCpFhH2j5vICM7jX2gIL9PQEGGg2ljNJoorJIEjYZzS0S9IMjn4\nMj7CYtAZarnIQw==\n=dO7W\n-----END PGP PUBLIC KEY BLOCK-----\n"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-generated-var/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-generated-var/main.tf",
    "content": "output \"output\" {\n  value = var.input\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-generated-var/terragrunt.hcl",
    "content": "generate \"variables\" {\n  path = \"variables.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents = <<EOF\nvariable \"input\" {}\nEOF\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-generated-var/variables.tf",
    "content": "# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa\nvariable \"input\" {}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-included-unused/module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-included-unused/module/main.tf",
    "content": "variable \"input\" {}\n\noutput \"output\" {\n  value = var.input\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-included-unused/module/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\ninputs = {\n  input = \"hello\"\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-included-unused/root.hcl",
    "content": "inputs = {\n  inherited = \"This input is inherited\"\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-no-inputs/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-no-inputs/main.tf",
    "content": "variable \"input\" {}\n\noutput \"output\" {\n  value = var.input\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-no-inputs/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-remote-module/terragrunt.hcl",
    "content": "terraform {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/download/hello-world?ref=v0.83.2\"\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-unused-inputs/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-unused-inputs/main.tf",
    "content": "variable \"input\" {}\n\noutput \"output\" {\n  value = var.input\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-unused-inputs/terragrunt.hcl",
    "content": "inputs = {\n  input = \"hello world\"\n  unused_input = \"I am unused\"\n}\n\n# the existence of a `source` directive manifests the bug in https://github.com/gruntwork-io/terragrunt/issues/1793\nterraform {\n  source = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/download/hello-world?ref=v0.83.2\"\n}"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-unused-varfile/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-unused-varfile/main.tf",
    "content": "variable \"input\" {}\n\noutput \"output\" {\n  value = var.input\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-unused-varfile/terragrunt.hcl",
    "content": "terraform {\n  extra_arguments \"varfiles\" {\n    commands           = get_terraform_commands_that_need_vars()\n    required_var_files = [\"${get_terragrunt_dir()}/varfiles/main.tfvars\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/fail-unused-varfile/varfiles/main.tfvars",
    "content": "input = \"hello world\"\nunused_input = \"I am unused\"\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-autovar-file/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-autovar-file/bar.auto.tfvars.json",
    "content": "{\n    \"input_autotfvarsjson\": \"hello world from bar.auto.tfvars.json\"\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-autovar-file/foo.auto.tfvars",
    "content": "input_autotfvars = \"hello world from foo.auto.tfvars\"\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-autovar-file/main.tf",
    "content": "variable \"input_terraformtfvars\" {}\nvariable \"input_terraformtfvarsjson\" {}\nvariable \"input_autotfvars\" {}\nvariable \"input_autotfvarsjson\" {}\n\noutput \"output\" {\n  value = <<-EOF\n  ${var.input_terraformtfvars}\n  ${var.input_terraformtfvarsjson}\n  ${var.input_autotfvars}\n  ${var.input_autotfvarsjson}\n  EOF\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-autovar-file/terraform.tfvars",
    "content": "input_terraformtfvars = \"hello world from terraform.tfvars\"\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-autovar-file/terraform.tfvars.json",
    "content": "{\n    \"input_terraformtfvarsjson\": \"hello world from terraform.tfvars.json\"\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-autovar-file/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-cli-args/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-cli-args/main.tf",
    "content": "variable \"input\" {}\nvariable \"other_input\" {}\n\noutput \"output\" {\n  value = \"${var.input} ${var.other_input}\"\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-cli-args/terragrunt.hcl",
    "content": "terraform {\n  extra_arguments \"varfiles\" {\n    commands  = get_terraform_commands_that_need_vars()\n    arguments = [\"-var=other_input=world\", \"-var-file=${get_terragrunt_dir()}/varfiles/main.tfvars\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-cli-args/varfiles/main.tfvars",
    "content": "input = \"hello\"\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-included/module/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-included/module/main.tf",
    "content": "variable \"input\" {}\n\noutput \"output\" {\n  value = var.input\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-included/module/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-included/root.hcl",
    "content": "inputs = {\n  input = \"hello\"\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-inputs-only/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-inputs-only/main.tf",
    "content": "variable \"input\" {}\n\noutput \"output\" {\n  value = var.input\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-inputs-only/terragrunt.hcl",
    "content": "inputs = {\n  input = \"hello world\"\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-mixed/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-mixed/main.tf",
    "content": "variable \"input_from_env_var\" {}\nvariable \"input_from_input\" {}\nvariable \"input_from_varfile\" {}\n\noutput \"output\" {\n  value = <<-EOF\n  ${var.input_from_env_var}\n  ${var.input_from_input}\n  ${var.input_from_varfile}\n  EOF\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-mixed/terragrunt.hcl",
    "content": "terraform {\n  extra_arguments \"variables\" {\n    commands           = get_terraform_commands_that_need_vars()\n    required_var_files = [\"${get_terragrunt_dir()}/varfiles/main.tfvars\"]\n    env_vars = {\n      TF_VAR_input_from_env_var = \"from env var\"\n    }\n  }\n}\n\ninputs = {\n  input_from_input = \"from input\"\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-mixed/varfiles/main.tfvars",
    "content": "input_from_varfile = \"from var file\"\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-null-default/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-null-default/main.tf",
    "content": "variable \"input\" {\n  default = null\n}\n\noutput \"output\" {\n  value = var.input == null ? \"Hello, World\" : \"\"\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-null-default/terragrunt.hcl",
    "content": "# Intentionally empty\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-var-file/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-var-file/main.tf",
    "content": "variable \"input\" {}\n\noutput \"output\" {\n  value = var.input\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-var-file/terragrunt.hcl",
    "content": "terraform {\n  extra_arguments \"varfiles\" {\n    commands           = get_terraform_commands_that_need_vars()\n    required_var_files = [\"${get_terragrunt_dir()}/varfiles/main.tfvars\"]\n  }\n}\n"
  },
  {
    "path": "test/fixtures/validate-inputs/success-var-file/varfiles/main.tfvars",
    "content": "input = \"hello world\"\n"
  },
  {
    "path": "test/fixtures/version-check/a/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/version-check/a/main.tf",
    "content": "output \"foo\" {\n  value = \"Hello, World\"\n}\n"
  },
  {
    "path": "test/fixtures/version-check/a/terragrunt.hcl",
    "content": "include {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/version-check/b/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/version-check/b/main.tf",
    "content": "output \"foo\" {\n  value = \"Hello, World\"\n}\n"
  },
  {
    "path": "test/fixtures/version-check/b/terragrunt.hcl",
    "content": "include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n"
  },
  {
    "path": "test/fixtures/version-check/root.hcl",
    "content": "terraform_version_constraint  = \">= 0.12.20\"\nterragrunt_version_constraint = \">= 0.23.20\"\n"
  },
  {
    "path": "test/fixtures/version-files-cache-key/.terraform-version",
    "content": ""
  },
  {
    "path": "test/fixtures/version-files-cache-key/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/version-files-cache-key/.tool-versions",
    "content": ""
  },
  {
    "path": "test/fixtures/version-files-cache-key/main.tf",
    "content": "output \"output\" {\n  value = \"message\"\n}\n"
  },
  {
    "path": "test/fixtures/version-files-cache-key/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/version-invocation/app/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/version-invocation/app/main.tf",
    "content": "variable \"input_value\" {}\n\noutput \"output_value\" {\n  value = var.input_value\n}"
  },
  {
    "path": "test/fixtures/version-invocation/app/terragrunt.hcl",
    "content": "\ndependency \"dependency\" {\n  config_path = \"../dependency\"\n}\n\ndependency \"dependency-with-custom-version\" {\n  config_path = \"../dependency-with-custom-version\"\n}\n\ninputs = {\n  input_value = dependency.dependency.outputs.result\n}"
  },
  {
    "path": "test/fixtures/version-invocation/dependency/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/version-invocation/dependency/main.tf",
    "content": "output \"result\" {\n\n  value = \"42\"\n}"
  },
  {
    "path": "test/fixtures/version-invocation/dependency/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/fixtures/version-invocation/dependency-with-custom-version/.terraform.lock.hcl",
    "content": "# This file is maintained automatically by \"tofu init\".\n# Manual edits may be lost in future updates.\n"
  },
  {
    "path": "test/fixtures/version-invocation/dependency-with-custom-version/.tool-versions",
    "content": "tofu 1.9.4\n"
  },
  {
    "path": "test/fixtures/version-invocation/dependency-with-custom-version/main.tf",
    "content": "output \"result\" {\n\n  value = \"42\"\n}"
  },
  {
    "path": "test/fixtures/version-invocation/dependency-with-custom-version/terragrunt.hcl",
    "content": ""
  },
  {
    "path": "test/helpers/aws.go",
    "content": "//go:build aws || awsgcp\n\n// Package helpers provides helper functions for tests.\npackage helpers\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// DeleteS3BucketWithRetry will attempt to delete the specified S3 bucket, retrying up to 3 times if there are errors to\n// handle eventual consistency issues.\nfunc DeleteS3BucketWithRetry(t *testing.T, awsRegion string, bucketName string) {\n\tt.Helper()\n\n\tfor range 3 {\n\t\terr := DeleteS3Bucket(t, awsRegion, bucketName)\n\t\tif err == nil {\n\t\t\treturn\n\t\t}\n\n\t\tt.Logf(\"Error deleting s3 bucket %s. Sleeping for 10 seconds before retrying.\", bucketName)\n\t\ttime.Sleep(10 * time.Second) //nolint:mnd\n\t}\n\n\tt.Fatalf(\"Max retries attempting to delete s3 bucket %s in region %s\", bucketName, awsRegion)\n}\n\n// GetS3BucketLoggingTarget returns the target bucket for access logging on the given S3 bucket.\nfunc GetS3BucketLoggingTarget(t *testing.T, region, bucket string) string {\n\tt.Helper()\n\n\tclient := CreateS3ClientForTest(t, region)\n\n\tresp, err := client.GetBucketLogging(t.Context(), &s3.GetBucketLoggingInput{\n\t\tBucket: aws.String(bucket),\n\t})\n\trequire.NoError(t, err)\n\n\tif resp.LoggingEnabled == nil {\n\t\treturn \"\"\n\t}\n\n\treturn aws.ToString(resp.LoggingEnabled.TargetBucket)\n}\n\n// GetS3BucketLoggingTargetPrefix returns the target prefix for access logging on the given S3 bucket.\nfunc GetS3BucketLoggingTargetPrefix(t *testing.T, region, bucket string) string {\n\tt.Helper()\n\n\tclient := CreateS3ClientForTest(t, region)\n\n\tresp, err := client.GetBucketLogging(t.Context(), &s3.GetBucketLoggingInput{\n\t\tBucket: aws.String(bucket),\n\t})\n\trequire.NoError(t, err)\n\n\tif resp.LoggingEnabled == nil {\n\t\treturn \"\"\n\t}\n\n\treturn aws.ToString(resp.LoggingEnabled.TargetPrefix)\n}\n"
  },
  {
    "path": "test/helpers/logger/logger.go",
    "content": "// Package logger provides a convenient logger for tests.\npackage logger\n\nimport (\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n)\n\nfunc CreateLogger() log.Logger {\n\tformatter := format.NewFormatter(format.NewKeyValueFormatPlaceholders())\n\tformatter.SetDisabledColors(true)\n\n\treturn log.New(log.WithLevel(log.DebugLevel), log.WithFormatter(formatter))\n}\n"
  },
  {
    "path": "test/helpers/package.go",
    "content": "// Package helpers provides helper functions for tests.\npackage helpers\n\nimport (\n\t\"archive/zip\"\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\"io\"\n\t\"io/fs\"\n\t\"math/big\"\n\tmathRand \"math/rand\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/awshelper\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/mattn/go-shellwords\"\n\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/NYTimes/gziphandler\"\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/dynamodb\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\ts3types \"github.com/aws/aws-sdk-go-v2/service/s3/types\"\n\t\"github.com/aws/smithy-go\"\n\t\"github.com/gruntwork-io/go-commons/version\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tTerraformFolder = \".terraform\"\n\n\tTerraformState = \"terraform.tfstate\"\n\n\tTerraformRemoteStateS3Region = \"us-west-2\"\n\n\tTerraformStateBackup = \"terraform.tfstate.backup\"\n\tTerragruntCache      = \".terragrunt-cache\"\n\n\tTerraformBinary = \"terraform\"\n\tTofuBinary      = \"tofu\"\n\n\tTerragruntDebugFile = \"terragrunt-debug.tfvars.json\"\n\n\t// Repeated right now, but it might not be later.\n\tTestFixtureOutDir = \"fixtures/out-dir\"\n\n\tReportFile = \"report.json\"\n\n\treadPermissions      = 0444\n\treadWritePermissions = 0666\n\tallPermissions       = 0777\n\n\tcaKeyBits = 4096\n\n\tsemverPartsLen = 3\n)\n\ntype TerraformOutput struct {\n\tType      any  `json:\"Type\"`\n\tValue     any  `json:\"Value\"`\n\tSensitive bool `json:\"Sensitive\"`\n}\n\nfunc CopyEnvironment(t *testing.T, environmentPath string, includeInCopy ...string) string {\n\tt.Helper()\n\n\ttmpDir := TmpDirWOSymlinks(t)\n\n\tt.Logf(\"Copying %s to %s\", environmentPath, tmpDir)\n\n\t// Exclude OpenTofu/Terraform/Terragrunt cache directories\n\t// that may have been created when manually running in the fixtures directory.\n\texcludeFromCopy := []string{\n\t\t\"**/.terraform/**\",\n\t\t\"**/.terragrunt-cache/**\",\n\t\t\"**/terragrunt-debug.tfvars.json\",\n\t}\n\n\trequire.NoError(\n\t\tt,\n\t\tutil.CopyFolderContents(\n\t\t\tlogger.CreateLogger(),\n\t\t\tenvironmentPath,\n\t\t\tfilepath.Join(tmpDir, environmentPath),\n\t\t\t\".terragrunt-test\",\n\t\t\tincludeInCopy,\n\t\t\texcludeFromCopy,\n\t\t),\n\t)\n\n\treturn tmpDir\n}\n\nfunc CreateTmpTerragruntConfig(\n\tt *testing.T,\n\ttemplatesPath string,\n\ts3BucketName string,\n\tlockTableName string,\n\tconfigFileName string,\n) string {\n\tt.Helper()\n\n\ttmpFolder := TmpDirWOSymlinks(t)\n\n\ttmpTerragruntConfigFile := filepath.Join(tmpFolder, configFileName)\n\toriginalTerragruntConfigPath := filepath.Join(templatesPath, configFileName)\n\tCopyTerragruntConfigAndFillPlaceholders(\n\t\tt,\n\t\toriginalTerragruntConfigPath,\n\t\ttmpTerragruntConfigFile,\n\t\ts3BucketName,\n\t\tlockTableName,\n\t\t\"not-used\",\n\t)\n\n\treturn tmpTerragruntConfigFile\n}\n\nfunc CreateTmpTerragruntConfigContent(t *testing.T, contents string, configFileName string) string {\n\tt.Helper()\n\n\ttmpFolder := TmpDirWOSymlinks(t)\n\n\ttmpTerragruntConfigFile := filepath.Join(tmpFolder, configFileName)\n\n\tif err := os.WriteFile(tmpTerragruntConfigFile, []byte(contents), readPermissions); err != nil {\n\t\tt.Fatalf(\"Error writing temp Terragrunt config to %s: %v\", tmpTerragruntConfigFile, err)\n\t}\n\n\treturn tmpTerragruntConfigFile\n}\n\nfunc CopyTerragruntConfigAndFillPlaceholders(t *testing.T, configSrcPath string, configDestPath string, s3BucketName string, lockTableName string, region string) {\n\tt.Helper()\n\n\tCopyAndFillMapPlaceholders(t, configSrcPath, configDestPath, map[string]string{\n\t\t\"__FILL_IN_BUCKET_NAME__\":      s3BucketName,\n\t\t\"__FILL_IN_LOCK_TABLE_NAME__\":  lockTableName,\n\t\t\"__FILL_IN_REGION__\":           region,\n\t\t\"__FILL_IN_LOGS_BUCKET_NAME__\": s3BucketName + \"-tf-state-logs\",\n\t})\n}\n\nfunc CopyAndFillMapPlaceholders(t *testing.T, srcPath string, destPath string, placeholders map[string]string) {\n\tt.Helper()\n\n\tcontents, err := util.ReadFileAsString(srcPath)\n\trequire.NoError(t, err, \"Error reading file at %s: %v\", srcPath, err)\n\n\t// iterate over placeholders and replace placeholders\n\tfor k, v := range placeholders {\n\t\tcontents = strings.ReplaceAll(contents, k, v)\n\t}\n\n\terr = os.WriteFile(destPath, []byte(contents), readPermissions)\n\trequire.NoError(t, err, \"Error writing temp file to %s: %v\", destPath, err)\n}\n\n// UniqueID returns a unique (ish) id we can attach to resources and tfstate files so they don't conflict with each other\n// Uses base 62 to generate a 6 character string that's unlikely to collide with the handful of tests we run in\n// parallel. Based on code here: http://stackoverflow.com/a/9543797/483528\nfunc UniqueID() string {\n\tconst (\n\t\tbase62Chars    = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\"\n\t\tuniqueIDLength = 6 // Should be good for 62^6 = 56+ billion combinations\n\t)\n\n\tvar out bytes.Buffer\n\n\tfor range uniqueIDLength {\n\t\tout.WriteByte(base62Chars[mathRand.Intn(len(base62Chars))])\n\t}\n\n\treturn out.String()\n}\n\n// CreateS3ClientForTest creates a S3 client we can use at test time. If there are any errors creating the client, fail the test.\nfunc CreateS3ClientForTest(t *testing.T, awsRegion string, opts ...options.TerragruntOptionsFunc) *s3.Client {\n\tt.Helper()\n\n\tmockOptions, err := options.NewTerragruntOptionsForTest(\"aws_s3_test\")\n\trequire.NoError(t, err, \"Error creating mockOptions\")\n\n\tfor _, opt := range opts {\n\t\topt(mockOptions)\n\t}\n\n\tawsConfig := &awshelper.AwsSessionConfig{Region: awsRegion}\n\n\tcfg, err := awshelper.NewAWSConfigBuilder().\n\t\tWithSessionConfig(awsConfig).\n\t\tWithEnv(mockOptions.Env).\n\t\tWithIAMRoleOptions(mockOptions.IAMRoleOptions).\n\t\tBuild(t.Context(), logger.CreateLogger())\n\trequire.NoError(t, err, \"Error creating S3 client\")\n\n\treturn s3.NewFromConfig(cfg)\n}\n\n// CreateDynamoDBClientForTest creates a DynamoDB client we can use at test time. If there are any errors creating the client, fail the test.\nfunc CreateDynamoDBClientForTest(t *testing.T, awsRegion, awsProfile, iamRoleArn string) *dynamodb.Client {\n\tt.Helper()\n\n\tmockOptions, err := options.NewTerragruntOptionsForTest(\"aws_dynamodb_test\")\n\trequire.NoError(t, err, \"Error creating mockOptions\")\n\n\tsessionConfig := &awshelper.AwsSessionConfig{\n\t\tRegion:  awsRegion,\n\t\tProfile: awsProfile,\n\t\tRoleArn: iamRoleArn,\n\t}\n\n\tcfg, err := awshelper.NewAWSConfigBuilder().\n\t\tWithSessionConfig(sessionConfig).\n\t\tWithEnv(mockOptions.Env).\n\t\tWithIAMRoleOptions(mockOptions.IAMRoleOptions).\n\t\tBuild(t.Context(), logger.CreateLogger())\n\trequire.NoError(t, err, \"Error creating DynamoDB client\")\n\n\treturn dynamodb.NewFromConfig(cfg)\n}\n\n// DeleteS3Bucket deletes the specified S3 bucket potentially with error to clean up after a test.\nfunc DeleteS3Bucket(t *testing.T, awsRegion string, bucketName string, opts ...options.TerragruntOptionsFunc) error {\n\tt.Helper()\n\n\tclient := CreateS3ClientForTest(t, awsRegion, opts...)\n\n\tt.Logf(\"Deleting test s3 bucket %s\", bucketName)\n\n\t// First check if bucket exists\n\t_, err := client.HeadBucket(t.Context(), &s3.HeadBucketInput{Bucket: aws.String(bucketName)})\n\tif err != nil {\n\t\tif isAWSResourceNotFoundError(err) {\n\t\t\tt.Logf(\"S3 bucket %s does not exist, cleanup already complete\", bucketName)\n\t\t\treturn nil\n\t\t}\n\n\t\tt.Logf(\"Error checking if S3 bucket %s exists: %v\", bucketName, err)\n\t}\n\n\tcleanS3Bucket(t, client, bucketName)\n\n\tif _, err := client.DeleteBucket(t.Context(), &s3.DeleteBucketInput{Bucket: aws.String(bucketName)}); err != nil {\n\t\tif isAWSResourceNotFoundError(err) {\n\t\t\tt.Logf(\"S3 bucket %s was already deleted\", bucketName)\n\t\t\treturn nil\n\t\t}\n\n\t\tt.Logf(\"Failed to delete S3 bucket %s: %v\", bucketName, err)\n\n\t\t// If the bucket is not empty, try to clean it again before deleting it.\n\t\t// This is a workaround for a race condition in eventual consistency.\n\t\t// Sleep for a little bit first to give the bucket a chance to be ready.\n\t\ttime.Sleep(1 * time.Second)\n\n\t\tcleanS3Bucket(t, client, bucketName)\n\n\t\tif _, err = client.DeleteBucket(t.Context(), &s3.DeleteBucketInput{Bucket: aws.String(bucketName)}); err != nil {\n\t\t\tif isAWSResourceNotFoundError(err) {\n\t\t\t\tt.Logf(\"S3 bucket %s was already deleted\", bucketName)\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tt.Logf(\"Failed to delete S3 bucket %s: %v\", bucketName, err)\n\n\t\t\treturn err\n\t\t}\n\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc cleanS3Bucket(t *testing.T, client *s3.Client, bucketName string) {\n\tt.Helper()\n\n\tversionsInput := &s3.ListObjectVersionsInput{\n\t\tBucket: aws.String(bucketName),\n\t}\n\n\tfor {\n\t\tout, err := client.ListObjectVersions(t.Context(), versionsInput)\n\t\tif err != nil {\n\t\t\tif isAWSResourceNotFoundError(err) {\n\t\t\t\tt.Logf(\"S3 bucket %s does not exist, skipping cleanup\", bucketName)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\t\t}\n\n\t\tif len(out.Versions) == 0 && len(out.DeleteMarkers) == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tif len(out.Versions) > 0 {\n\t\t\tvar objectsToDelete []s3types.ObjectIdentifier\n\n\t\t\tfor i := range out.Versions {\n\t\t\t\tversion := &out.Versions[i]\n\t\t\t\tobjectsToDelete = append(objectsToDelete, s3types.ObjectIdentifier{\n\t\t\t\t\tKey:       version.Key,\n\t\t\t\t\tVersionId: version.VersionId,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tdeleteInput := &s3.DeleteObjectsInput{\n\t\t\t\tBucket: aws.String(bucketName),\n\t\t\t\tDelete: &s3types.Delete{\n\t\t\t\t\tObjects: objectsToDelete,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\t_, err := client.DeleteObjects(t.Context(), deleteInput)\n\t\t\tif err != nil {\n\t\t\t\tif isAWSResourceNotFoundError(err) {\n\t\t\t\t\tt.Logf(\"S3 bucket %s was deleted during cleanup\", bucketName)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t}\n\n\t\tif len(out.DeleteMarkers) > 0 {\n\t\t\tvar objectsToDelete []s3types.ObjectIdentifier\n\n\t\t\tfor _, marker := range out.DeleteMarkers {\n\t\t\t\tobjectsToDelete = append(objectsToDelete, s3types.ObjectIdentifier{\n\t\t\t\t\tKey:       marker.Key,\n\t\t\t\t\tVersionId: marker.VersionId,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tdeleteInput := &s3.DeleteObjectsInput{\n\t\t\t\tBucket: aws.String(bucketName),\n\t\t\t\tDelete: &s3types.Delete{\n\t\t\t\t\tObjects: objectsToDelete,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\t_, err := client.DeleteObjects(t.Context(), deleteInput)\n\t\t\tif err != nil {\n\t\t\t\tif isAWSResourceNotFoundError(err) {\n\t\t\t\t\tt.Logf(\"S3 bucket %s was deleted during cleanup\", bucketName)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t}\n\n\t\tif out.IsTruncated == nil || !*out.IsTruncated {\n\t\t\tbreak\n\t\t}\n\n\t\tversionsInput.KeyMarker = out.NextKeyMarker\n\t\tversionsInput.VersionIdMarker = out.NextVersionIdMarker\n\t}\n}\n\nfunc FileIsInFolder(t *testing.T, name string, path string) bool {\n\tt.Helper()\n\n\tfound := false\n\terr := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {\n\t\trequire.NoError(t, err)\n\n\t\tif filepath.Base(path) == name {\n\t\t\tfound = true\n\t\t}\n\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n\n\treturn found\n}\n\nfunc RunValidateAllWithIncludeAndGetIncludedModules(\n\tt *testing.T,\n\trootModulePath string,\n\tincludeModulePaths []string,\n) []string {\n\tt.Helper()\n\n\tcmdParts := make([]string, 0, 9+2*len(includeModulePaths)) //nolint:mnd\n\tcmdParts = append(cmdParts,\n\t\t\"terragrunt\", \"run\", \"--all\", \"validate\",\n\t\t\"--non-interactive\",\n\t\t\"--log-level\", \"debug\",\n\t\t\"--working-dir\", rootModulePath,\n\t)\n\n\tfor _, module := range includeModulePaths {\n\t\tcmdParts = append(cmdParts, \"--queue-include-dir\", module)\n\t}\n\n\tcmd := strings.Join(cmdParts, \" \")\n\n\tvalidateAllStdout := bytes.Buffer{}\n\tvalidateAllStderr := bytes.Buffer{}\n\terr := RunTerragruntCommand(\n\t\tt,\n\t\tcmd,\n\t\t&validateAllStdout,\n\t\t&validateAllStderr,\n\t)\n\n\tLogBufferContentsLineByLine(t, validateAllStdout, \"run --all validate stdout\")\n\tLogBufferContentsLineByLine(t, validateAllStderr, \"run --all validate stderr\")\n\n\trequire.NoError(t, err)\n\n\tincludedModulesRegexp := regexp.MustCompile(`=> Unit (.+) \\(excluded: (true|false)`)\n\n\tmatches := includedModulesRegexp.FindAllStringSubmatch(validateAllStderr.String(), -1)\n\tincludedModules := []string{}\n\n\tfor _, match := range matches {\n\t\tif match[2] == \"false\" {\n\t\t\tincludedModules = append(includedModules, match[1])\n\t\t}\n\t}\n\n\tsort.Strings(includedModules)\n\n\treturn includedModules\n}\n\nfunc RunValidateAllWithFilteredPlusDependenciesAndGetIncludedModules(\n\tt *testing.T,\n\tworkDir string,\n\tunits []string,\n) []string {\n\tt.Helper()\n\n\tcmdParts := make([]string, 0, 9+2*len(units)) //nolint:mnd\n\tcmdParts = append(cmdParts,\n\t\t\"terragrunt\", \"run\", \"--all\", \"validate\",\n\t\t\"--non-interactive\",\n\t\t\"--log-level\", \"debug\",\n\t\t\"--working-dir\", workDir,\n\t)\n\n\tfor _, unit := range units {\n\t\tcmdParts = append(cmdParts, \"--filter\", fmt.Sprintf(\"'{%s}...'\", unit))\n\t}\n\n\tcmd := strings.Join(cmdParts, \" \")\n\n\tvalidateAllStdout := bytes.Buffer{}\n\tvalidateAllStderr := bytes.Buffer{}\n\terr := RunTerragruntCommand(\n\t\tt,\n\t\tcmd,\n\t\t&validateAllStdout,\n\t\t&validateAllStderr,\n\t)\n\n\tLogBufferContentsLineByLine(t, validateAllStdout, \"run --all validate stdout\")\n\tLogBufferContentsLineByLine(t, validateAllStderr, \"run --all validate stderr\")\n\n\trequire.NoError(t, err)\n\n\tincludedModulesRegexp := regexp.MustCompile(`=> Unit (.+) \\(excluded: (true|false)`)\n\n\tmatches := includedModulesRegexp.FindAllStringSubmatch(validateAllStderr.String(), -1)\n\tincludedModules := []string{}\n\n\tfor _, match := range matches {\n\t\tif match[2] == \"false\" {\n\t\t\tincludedModules = append(includedModules, match[1])\n\t\t}\n\t}\n\n\tsort.Strings(includedModules)\n\n\treturn includedModules\n}\n\nfunc GetPathRelativeTo(t *testing.T, path string, basePath string) string {\n\tt.Helper()\n\n\trelPath, err := util.GetPathRelativeTo(path, basePath)\n\trequire.NoError(t, err)\n\n\treturn relPath\n}\n\nfunc GetPathsRelativeTo(t *testing.T, basePath string, paths []string) []string {\n\tt.Helper()\n\n\trelPaths := make([]string, len(paths))\n\n\tfor i, path := range paths {\n\t\trelPath, err := util.GetPathRelativeTo(path, basePath)\n\t\trequire.NoError(t, err)\n\n\t\trelPaths[i] = relPath\n\t}\n\n\treturn relPaths\n}\n\nfunc TestRunAllPlan(t *testing.T, args string) (string, string, string, error) {\n\tt.Helper()\n\n\ttmpEnvPath := CopyEnvironment(t, TestFixtureOutDir)\n\tCleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, TestFixtureOutDir)\n\n\t// run plan with output directory\n\tstdout, stderr, err := RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terraform run --all plan --non-interactive --working-dir %s %s\", testPath, args))\n\n\treturn tmpEnvPath, stdout, stderr, err\n}\n\nfunc RunNetworkMirrorServer(t *testing.T, ctx context.Context, urlPrefix, providerDir, token string) *url.URL {\n\tt.Helper()\n\n\tserverTLSConf, clientTLSConf := certSetup(t)\n\n\thttp.DefaultTransport = &http.Transport{\n\t\tTLSClientConfig: clientTLSConf,\n\t}\n\n\tmux := http.NewServeMux()\n\n\tfs := http.FileServer(http.Dir(providerDir))\n\n\twithGz := gziphandler.GzipHandler(http.StripPrefix(urlPrefix, fs))\n\n\tmux.HandleFunc(urlPrefix, func(resp http.ResponseWriter, req *http.Request) {\n\t\tif token != \"\" {\n\t\t\tauthHeaders := req.Header.Values(\"Authorization\")\n\t\t\tassert.Contains(t, authHeaders, \"Bearer \"+token)\n\t\t}\n\n\t\twithGz.ServeHTTP(resp, req)\n\t})\n\n\tln, err := tls.Listen(\"tcp\", \"localhost:8888\", serverTLSConf)\n\trequire.NoError(t, err)\n\n\tserver := &http.Server{\n\t\tAddr:    ln.Addr().String(),\n\t\tHandler: mux,\n\t}\n\n\tgo func() {\n\t\terr := server.Serve(ln)\n\t\tassert.NoError(t, err)\n\t}()\n\n\tgo func() {\n\t\t<-ctx.Done()\n\t\terr := server.Shutdown(ctx)\n\t\tassert.NoError(t, err)\n\t}()\n\n\treturn &url.URL{\n\t\tScheme: \"https\",\n\t\tHost:   ln.Addr().String(),\n\t\tPath:   urlPrefix,\n\t}\n}\n\ntype FakeProvider struct {\n\tRegistryName string\n\tNamespace    string\n\tName         string\n\tVersion      string\n\tPlatformOS   string\n\tPlatformArch string\n}\n\nfunc (provider *FakeProvider) archiveName() string {\n\treturn fmt.Sprintf(\n\t\t\"terraform-provider-%s_%s_%s_%s.zip\",\n\t\tprovider.Name,\n\t\tprovider.Version,\n\t\tprovider.PlatformOS,\n\t\tprovider.PlatformArch,\n\t)\n}\n\nfunc (provider *FakeProvider) filename() string {\n\treturn fmt.Sprintf(\"terraform-provider-%s_v%s_x5\", provider.Name, provider.Version)\n}\n\nfunc (provider *FakeProvider) CreateMirror(t *testing.T, rootDir string) {\n\tt.Helper()\n\n\tproviderDir := filepath.Join(rootDir, provider.RegistryName, provider.Namespace, provider.Name)\n\n\terr := os.MkdirAll(providerDir, os.ModePerm)\n\trequire.NoError(t, err)\n\n\tprovider.createIndexJSON(t, providerDir)\n\tprovider.createVersionJSON(t, providerDir)\n\tprovider.createZipArchive(t, providerDir)\n}\n\nfunc (provider *FakeProvider) createVersionJSON(t *testing.T, providerDir string) {\n\tt.Helper()\n\n\ttype VersionProvider struct {\n\t\tURL    string   `json:\"url\"`\n\t\tHashes []string `json:\"hashes\"`\n\t}\n\n\ttype Version struct {\n\t\tArchives map[string]VersionProvider `json:\"archives\"`\n\t}\n\n\tversion := &Version{Archives: make(map[string]VersionProvider)}\n\tfilename := filepath.Join(providerDir, provider.Version+\".json\")\n\tplatform := fmt.Sprintf(\"%s_%s\", provider.PlatformOS, provider.PlatformArch)\n\n\tunmarshalFile(t, filename, version)\n\tversion.Archives[platform] = VersionProvider{URL: provider.archiveName()}\n\tmarshalFile(t, filename, version)\n}\n\nfunc (provider *FakeProvider) createIndexJSON(t *testing.T, providerDir string) {\n\tt.Helper()\n\n\ttype Index struct {\n\t\tVersions map[string]any `json:\"versions\"`\n\t}\n\n\tindex := &Index{Versions: make(map[string]any)}\n\tfilename := filepath.Join(providerDir, \"index.json\")\n\n\tunmarshalFile(t, filename, index)\n\tindex.Versions[provider.Version] = struct{}{}\n\tmarshalFile(t, filename, index)\n}\n\nfunc (provider *FakeProvider) createZipArchive(t *testing.T, providerDir string) {\n\tt.Helper()\n\n\tfile, err := os.Create(filepath.Join(providerDir, provider.filename()))\n\trequire.NoError(t, err)\n\n\tdefer func() {\n\t\tfile.Close()\n\t\trequire.NoError(t, os.Remove(filepath.Join(providerDir, provider.filename())))\n\t}()\n\n\t// I wouldn't ignore this lint, but I actually don't know what\n\t// the number is there for.\n\terr = file.Truncate(1e7) //nolint:mnd\n\trequire.NoError(t, err)\n\n\terr = file.Sync()\n\trequire.NoError(t, err)\n\n\tzipFile, err := os.Create(filepath.Join(providerDir, provider.archiveName()))\n\trequire.NoError(t, err)\n\n\tdefer zipFile.Close()\n\n\tzipWriter := zip.NewWriter(zipFile)\n\tdefer require.NoError(t, zipWriter.Close())\n\n\tfileInfo, err := file.Stat()\n\trequire.NoError(t, err)\n\n\theader, err := zip.FileInfoHeader(fileInfo)\n\trequire.NoError(t, err)\n\n\theader.Method = zip.Deflate\n\theader.Name = provider.filename()\n\n\theaderWriter, err := zipWriter.CreateHeader(header)\n\trequire.NoError(t, err)\n\n\t_, err = io.Copy(headerWriter, file)\n\trequire.NoError(t, err)\n}\n\nfunc unmarshalFile(t *testing.T, filename string, dest any) {\n\tt.Helper()\n\n\tif !util.FileExists(filename) {\n\t\treturn\n\t}\n\n\tdata, err := os.ReadFile(filename)\n\trequire.NoError(t, err)\n\terr = json.Unmarshal(data, dest)\n\trequire.NoError(t, err)\n}\n\nfunc marshalFile(t *testing.T, filename string, dest any) {\n\tt.Helper()\n\n\tdata, err := json.Marshal(dest)\n\trequire.NoError(t, err)\n\terr = os.WriteFile(filename, data, readWritePermissions)\n\trequire.NoError(t, err)\n}\n\nfunc certSetup(t *testing.T) (*tls.Config, *tls.Config) {\n\tt.Helper()\n\n\t// set up our CA certificate\n\tserialNumber, err := strconv.ParseInt(time.Now().Format(\"20060102150405\"), 10, 64)\n\trequire.NoError(t, err)\n\n\tca := &x509.Certificate{\n\t\tSerialNumber: big.NewInt(serialNumber),\n\t\tSubject: pkix.Name{\n\t\t\tOrganization:  []string{\"Company, INC.\"},\n\t\t\tCountry:       []string{\"US\"},\n\t\t\tProvince:      []string{\"\"},\n\t\t\tLocality:      []string{\"San Francisco\"},\n\t\t\tStreetAddress: []string{\"Golden Gate Bridge\"},\n\t\t\tPostalCode:    []string{\"94016\"},\n\t\t},\n\t\tNotBefore:             time.Now(),\n\t\tNotAfter:              time.Now().AddDate(10, 0, 0), //nolint:mnd\n\t\tIsCA:                  true,\n\t\tExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},\n\t\tKeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,\n\t\tBasicConstraintsValid: true,\n\t}\n\n\t// create our private and public key\n\tcaPrivKey, err := rsa.GenerateKey(rand.Reader, caKeyBits)\n\trequire.NoError(t, err)\n\n\t// create the CA\n\tcaBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)\n\trequire.NoError(t, err)\n\n\t// pem encode\n\tcaPEM := new(bytes.Buffer)\n\trequire.NoError(t, pem.Encode(caPEM, &pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: caBytes,\n\t}))\n\n\tcaPrivKeyPEM := new(bytes.Buffer)\n\trequire.NoError(t, pem.Encode(caPrivKeyPEM, &pem.Block{\n\t\tType:  \"RSA PRIVATE KEY\",\n\t\tBytes: x509.MarshalPKCS1PrivateKey(caPrivKey),\n\t}))\n\n\t// set up our server certificate\n\tcert := &x509.Certificate{\n\t\tSerialNumber: big.NewInt(serialNumber),\n\t\tSubject: pkix.Name{\n\t\t\tOrganization:  []string{\"Company, INC.\"},\n\t\t\tCountry:       []string{\"US\"},\n\t\t\tProvince:      []string{\"\"},\n\t\t\tLocality:      []string{\"San Francisco\"},\n\t\t\tStreetAddress: []string{\"Golden Gate Bridge\"},\n\t\t\tPostalCode:    []string{\"94016\"},\n\t\t},\n\t\tIPAddresses:  []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, //nolint:mnd\n\t\tNotBefore:    time.Now(),\n\t\tNotAfter:     time.Now().AddDate(10, 0, 0), //nolint:mnd\n\t\tSubjectKeyId: []byte{1, 2, 3, 4, 6},\n\t\tExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},\n\t\tKeyUsage:     x509.KeyUsageDigitalSignature,\n\t}\n\n\tcertPrivKey, err := rsa.GenerateKey(rand.Reader, caKeyBits)\n\trequire.NoError(t, err)\n\n\tcertBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey)\n\trequire.NoError(t, err)\n\n\tcertPEM := new(bytes.Buffer)\n\trequire.NoError(t, pem.Encode(certPEM, &pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: certBytes,\n\t}))\n\n\tcertPrivKeyPEM := new(bytes.Buffer)\n\trequire.NoError(t, pem.Encode(certPrivKeyPEM, &pem.Block{\n\t\tType:  \"RSA PRIVATE KEY\",\n\t\tBytes: x509.MarshalPKCS1PrivateKey(certPrivKey),\n\t}))\n\n\tserverCert, err := tls.X509KeyPair(certPEM.Bytes(), certPrivKeyPEM.Bytes())\n\trequire.NoError(t, err)\n\n\tserverTLSConf := &tls.Config{\n\t\tCertificates: []tls.Certificate{serverCert},\n\t}\n\n\tcertpool := x509.NewCertPool()\n\tcertpool.AppendCertsFromPEM(caPEM.Bytes())\n\tclientTLSConf := &tls.Config{\n\t\tRootCAs:            certpool,\n\t\tInsecureSkipVerify: true,\n\t}\n\n\treturn serverTLSConf, clientTLSConf\n}\n\nfunc ValidateOutput(t *testing.T, outputs map[string]TerraformOutput, key string, value any) {\n\tt.Helper()\n\n\toutput, hasPlatform := outputs[key]\n\n\tassert.Truef(t, hasPlatform, \"Expected output %s to be defined\", key)\n\tassert.Equalf(t, output.Value, value, \"Expected output %s to be %t\", key, value)\n}\n\n// WrappedBinary - return which binary will be wrapped by Terragrunt, useful in CICD to run same tests against tofu and terraform\nfunc WrappedBinary() string {\n\tvalue, found := os.LookupEnv(\"TG_TF_PATH\")\n\tif !found {\n\t\t// if env variable is not defined, try to check through executing command\n\t\tif util.IsCommandExecutable(context.Background(), TofuBinary, \"-version\") {\n\t\t\treturn TofuBinary\n\t\t}\n\n\t\treturn TerraformBinary\n\t}\n\n\treturn filepath.Base(value)\n}\n\n// ExpectedWrongCommandErr - return expected error message for wrong command\nfunc ExpectedWrongCommandErr(command string) error {\n\tif WrappedBinary() == TofuBinary {\n\t\treturn run.WrongTofuCommand(command)\n\t}\n\n\treturn run.WrongTerraformCommand(command)\n}\n\n// IsTerraform checks if the wrapped binary currently in use is the Terraform binary.\nfunc IsTerraform() bool {\n\treturn WrappedBinary() == TerraformBinary\n}\n\n// IsTerraform110OrHigher checks if the installed Terraform binary is version 1.10.0 or higher.\nfunc IsTerraform110OrHigher(t *testing.T) bool {\n\tt.Helper()\n\n\tconst (\n\t\trequiredMajor = 1\n\t\trequiredMinor = 10\n\t)\n\n\tif !IsTerraform() {\n\t\treturn false\n\t}\n\n\toutput, err := exec.CommandContext(t.Context(), WrappedBinary(), \"-version\").Output()\n\trequire.NoError(t, err)\n\n\tmatches := regexp.MustCompile(`Terraform v(\\d+)\\.(\\d+)\\.`).FindStringSubmatch(string(output))\n\trequire.Len(t, matches, semverPartsLen, \"Expected Terraform version to be in the format 'Terraform v1.10.0'\")\n\n\tmajor, err := strconv.Atoi(matches[1])\n\trequire.NoError(t, err)\n\n\tminor, err := strconv.Atoi(matches[2])\n\trequire.NoError(t, err)\n\n\treturn major > requiredMajor || (major == requiredMajor && minor >= requiredMinor)\n}\n\n// IsOpenTofuInstalled checks if OpenTofu is installed.\nfunc IsOpenTofuInstalled() bool {\n\treturn util.IsCommandExecutable(context.Background(), TofuBinary, \"-version\")\n}\n\n// IsTerraformInstalled checks if Terraform is installed.\nfunc IsTerraformInstalled() bool {\n\treturn util.IsCommandExecutable(context.Background(), TerraformBinary, \"-version\")\n}\n\n// IsNativeS3LockingSupported checks if the installed Terraform binary supports native S3 locking.\n// This is the case when using Terraform 1.11 or higher, or using OpenTofu 1.10 or higher.\nfunc IsNativeS3LockingSupported(t *testing.T) bool {\n\tt.Helper()\n\n\tconst (\n\t\tterraformRequiredMajor = 1\n\t\tterraformRequiredMinor = 11\n\t\ttofuRequiredMajor      = 1\n\t\ttofuRequiredMinor      = 10\n\t)\n\n\tif IsTerraform() {\n\t\toutput, err := exec.CommandContext(t.Context(), TerraformBinary, \"-version\").Output()\n\t\trequire.NoError(t, err)\n\n\t\tmatches := regexp.MustCompile(`Terraform v(\\d+)\\.(\\d+)\\.`).FindStringSubmatch(string(output))\n\t\trequire.Len(t, matches, semverPartsLen, \"Expected Terraform version to be in the format 'Terraform v1.10.0'\")\n\n\t\tmajor, err := strconv.Atoi(matches[1])\n\t\trequire.NoError(t, err)\n\n\t\tminor, err := strconv.Atoi(matches[2])\n\t\trequire.NoError(t, err)\n\n\t\treturn major > terraformRequiredMajor || (major == terraformRequiredMajor && minor >= terraformRequiredMinor)\n\t}\n\n\toutput, err := exec.CommandContext(t.Context(), TofuBinary, \"-version\").Output()\n\trequire.NoError(t, err)\n\n\tmatches := regexp.MustCompile(`OpenTofu v(\\d+)\\.(\\d+)\\.`).FindStringSubmatch(string(output))\n\trequire.Len(t, matches, semverPartsLen, \"Expected OpenTofu version to be in the format 'OpenTofu v1.10.0'\")\n\n\tmajor, err := strconv.Atoi(matches[1])\n\trequire.NoError(t, err)\n\n\tminor, err := strconv.Atoi(matches[2])\n\trequire.NoError(t, err)\n\n\treturn major > tofuRequiredMajor || (major == tofuRequiredMajor && minor >= tofuRequiredMinor)\n}\n\nfunc FindFilesWithExtension(dir string, ext string) ([]string, error) {\n\tvar files []string\n\n\terr := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !info.IsDir() && filepath.Ext(path) == ext {\n\t\t\tfiles = append(files, path)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\treturn files, err\n}\n\nfunc CleanupTerraformFolder(t *testing.T, templatesPath string) {\n\tt.Helper()\n\n\tRemoveFile(t, filepath.Join(templatesPath, TerraformState))\n\tRemoveFile(t, filepath.Join(templatesPath, TerraformStateBackup))\n\tRemoveFile(t, filepath.Join(templatesPath, TerragruntDebugFile))\n\tRemoveFolder(t, filepath.Join(templatesPath, TerraformFolder))\n}\n\nfunc CleanupTerragruntFolder(t *testing.T, templatesPath string) {\n\tt.Helper()\n\n\tRemoveFolder(t, filepath.Join(templatesPath, TerragruntCache))\n}\n\nfunc RemoveFile(t *testing.T, path string) {\n\tt.Helper()\n\n\tif util.FileExists(path) {\n\t\tif err := os.Remove(path); err != nil {\n\t\t\tt.Fatalf(\"Error while removing %s: %v\", path, err)\n\t\t}\n\t}\n}\n\nfunc RemoveFolder(t *testing.T, path string) {\n\tt.Helper()\n\n\tif util.FileExists(path) {\n\t\tif err := os.RemoveAll(path); err != nil {\n\t\t\tt.Fatalf(\"Error while removing %s: %v\", path, err)\n\t\t}\n\t}\n}\n\nfunc RunTerragruntCommandWithContext(\n\tt *testing.T,\n\tctx context.Context,\n\tcommand string,\n\twriter,\n\terrwriter io.Writer,\n\textraArgs ...string,\n) error {\n\tt.Helper()\n\n\tparser := shellwords.NewParser()\n\n\t// Convert backslashes to forward slashes before parsing.\n\t// shellwords treats backslashes as escape characters, corrupting Windows paths\n\t// like C:\\foo\\bar into C:foobar. Forward slashes work fine since Terragrunt CLI\n\t// normalizes paths internally (see cli/commands/commands.go).\n\targs, err := parser.Parse(filepath.ToSlash(command))\n\trequire.NoError(t, err)\n\n\tif !strings.Contains(command, \"-log-format\") && !strings.Contains(command, \"-log-custom-format\") {\n\t\tvar builtinCmd []string\n\n\t\tfor i := range args {\n\t\t\tif args[i] == \"--\" {\n\t\t\t\tbuiltinCmd = make([]string, len(args[i:]))\n\t\t\t\tcopy(builtinCmd, args[i:])\n\t\t\t\targs = args[:i]\n\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\targs = append(append(args, \"--log-format=key-value\"), builtinCmd...)\n\t}\n\n\tt.Log(args)\n\n\t// Wrap writers with SyncWriter to prevent race conditions when multiple\n\t// goroutines write concurrently (e.g., during \"run --all\" operations).\n\tsyncWriter := util.NewSyncWriter(writer)\n\tsyncErrWriter := util.NewSyncWriter(errwriter)\n\n\topts := options.NewTerragruntOptionsWithWriters(syncWriter, syncErrWriter)\n\n\tl := log.New(\n\t\tlog.WithOutput(syncErrWriter),\n\t\tlog.WithLevel(options.DefaultLogLevel),\n\t\tlog.WithFormatter(format.NewFormatter(format.NewPrettyFormatPlaceholders())),\n\t)\n\n\tapp := cli.NewApp(l, opts)\n\n\tctx = log.ContextWithLogger(ctx, l)\n\n\treturn app.RunContext(ctx, args)\n}\n\nfunc RunTerragruntCommand(t *testing.T, command string, writer io.Writer, errwriter io.Writer) error {\n\tt.Helper()\n\n\treturn RunTerragruntCommandWithContext(t, t.Context(), command, writer, errwriter)\n}\n\nfunc RunTerragruntVersionCommand(t *testing.T, ver string, command string, writer io.Writer, errwriter io.Writer) error {\n\tt.Helper()\n\n\tversion.Version = ver\n\n\treturn RunTerragruntCommand(t, command, writer, errwriter)\n}\n\nfunc RunTerragrunt(t *testing.T, command string) {\n\tt.Helper()\n\n\tRunTerragruntRedirectOutput(t, command, os.Stdout, os.Stderr)\n}\n\nfunc LogBufferContentsLineByLine(t *testing.T, out bytes.Buffer, label string) {\n\tt.Helper()\n\tt.Logf(\"[%s] Full contents of %s:\", t.Name(), label)\n\n\tlines := strings.SplitSeq(out.String(), \"\\n\")\n\tfor line := range lines {\n\t\tt.Logf(\"[%s] %s\", t.Name(), line)\n\t}\n}\n\nfunc RunTerragruntCommandWithOutputWithContext(t *testing.T, ctx context.Context, command string) (string, string, error) {\n\tt.Helper()\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := RunTerragruntCommandWithContext(t, ctx, command, &stdout, &stderr)\n\tLogBufferContentsLineByLine(t, stdout, \"stdout\")\n\tLogBufferContentsLineByLine(t, stderr, \"stderr\")\n\n\treturn stdout.String(), stderr.String(), err\n}\n\nfunc RunTerragruntCommandWithOutput(t *testing.T, command string) (string, string, error) {\n\tt.Helper()\n\n\treturn RunTerragruntCommandWithOutputWithContext(t, t.Context(), command)\n}\n\nfunc RunTerragruntRedirectOutput(t *testing.T, command string, writer io.Writer, errwriter io.Writer) {\n\tt.Helper()\n\n\tif err := RunTerragruntCommand(t, command, writer, errwriter); err != nil {\n\t\tstdout := \"(see log output above)\"\n\t\tif stdoutAsBuffer, stdoutIsBuffer := writer.(*bytes.Buffer); stdoutIsBuffer {\n\t\t\tstdout = stdoutAsBuffer.String()\n\t\t}\n\n\t\tstderr := \"(see log output above)\"\n\t\tif stderrAsBuffer, stderrIsBuffer := errwriter.(*bytes.Buffer); stderrIsBuffer {\n\t\t\tstderr = stderrAsBuffer.String()\n\t\t}\n\n\t\tt.Fatalf(\"Failed to run Terragrunt command '%s' due to error: %s\\n\\nStdout: %s\\n\\nStderr: %s\", command, errors.ErrorStack(err), stdout, stderr)\n\t}\n}\n\nfunc CreateEmptyStateFile(t *testing.T, testPath string) {\n\tt.Helper()\n\n\t// create empty terraform.tfstate file\n\tfile, err := os.Create(filepath.Join(testPath, TerraformState))\n\trequire.NoError(t, err)\n\trequire.NoError(t, file.Close())\n}\n\nfunc RunTerragruntValidateInputs(t *testing.T, moduleDir string, extraArgs []string, isSuccessTest bool) {\n\tt.Helper()\n\n\tmaybeNested := filepath.Join(moduleDir, \"module\")\n\tif util.FileExists(maybeNested) {\n\t\t// Nested module test case with included file, so run terragrunt from the nested module.\n\t\tmoduleDir = maybeNested\n\t}\n\n\tcmd := fmt.Sprintf(\n\t\t\"terragrunt hcl validate --inputs %s --non-interactive --working-dir %s\",\n\t\tstrings.Join(extraArgs, \" \"),\n\t\tmoduleDir,\n\t)\n\tt.Logf(\"Command: %s\", cmd)\n\t_, _, err := RunTerragruntCommandWithOutput(t, cmd)\n\n\tif isSuccessTest {\n\t\trequire.NoError(t, err)\n\t} else {\n\t\trequire.Error(t, err)\n\t}\n}\n\nfunc CreateTmpTerragruntConfigWithParentAndChild(t *testing.T, parentPath string, childRelPath string, s3BucketName string, parentConfigFileName string, childConfigFileName string) string {\n\tt.Helper()\n\n\ttmpDir := TmpDirWOSymlinks(t)\n\n\tchildDestPath := filepath.Join(tmpDir, childRelPath)\n\n\tif err := os.MkdirAll(childDestPath, allPermissions); err != nil {\n\t\tt.Fatalf(\"Failed to create temp dir %s due to error %v\", childDestPath, err)\n\t}\n\n\tparentTerragruntSrcPath := filepath.Join(parentPath, parentConfigFileName)\n\tparentTerragruntDestPath := filepath.Join(tmpDir, parentConfigFileName)\n\tCopyTerragruntConfigAndFillPlaceholders(t, parentTerragruntSrcPath, parentTerragruntDestPath, s3BucketName, \"not-used\", \"not-used\")\n\n\tchildTerragruntSrcPath := filepath.Join(parentPath, childRelPath, childConfigFileName)\n\tchildTerragruntDestPath := filepath.Join(childDestPath, childConfigFileName)\n\tCopyTerragruntConfigAndFillPlaceholders(t, childTerragruntSrcPath, childTerragruntDestPath, s3BucketName, \"not-used\", \"not-used\")\n\n\treturn childTerragruntDestPath\n}\n\nfunc IsTerragruntProviderCacheEnabled(t *testing.T) bool {\n\tt.Helper()\n\n\tfor _, envName := range []string{\"TERRAGRUNT_PROVIDER_CACHE\", \"TG_PROVIDER_CACHE\"} {\n\t\tif val := os.Getenv(envName); val != \"\" {\n\t\t\tproviderCache, err := strconv.ParseBool(val)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif providerCache {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n\n// HCLFilesInDir returns a list of all HCL files in a directory.\nfunc HCLFilesInDir(t *testing.T, dir string) []string {\n\tt.Helper()\n\n\tfiles := []string{}\n\n\twalkFn := func(path string, d fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif d.IsDir() {\n\t\t\treturn nil\n\t\t}\n\n\t\tif strings.HasSuffix(path, \".hcl\") {\n\t\t\tfiles = append(files, path)\n\t\t}\n\n\t\treturn nil\n\t}\n\n\terr := filepath.WalkDir(dir, walkFn)\n\trequire.NoError(t, err)\n\n\treturn files\n}\n\n// CopyDir copies the contents of the directory at src to dst.\nfunc CopyDir(t *testing.T, src, dst string) {\n\tt.Helper()\n\n\t// First, ensure the destination directory exists\n\trequire.NoError(t, os.MkdirAll(dst, allPermissions))\n\n\terr := filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {\n\t\trequire.NoError(t, err)\n\n\t\trelPath, err := filepath.Rel(src, path)\n\t\trequire.NoError(t, err)\n\n\t\tdstPath := filepath.Join(dst, relPath)\n\n\t\tif d.IsDir() {\n\t\t\t// Get the source directory info to preserve permissions\n\t\t\tsrcInfo, err := os.Stat(path)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NoError(t, os.MkdirAll(dstPath, srcInfo.Mode()))\n\t\t} else {\n\t\t\t// Ensure parent directory exists\n\t\t\tparentDir := filepath.Dir(dstPath)\n\t\t\trequire.NoError(t, os.MkdirAll(parentDir, allPermissions))\n\t\t\tCopyFile(t, path, dstPath)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n}\n\n// CopyFile copies a single file from src to dst and preserves permissions.\nfunc CopyFile(t *testing.T, src, dst string) {\n\tt.Helper()\n\n\tsourceFile, err := os.Open(src)\n\trequire.NoError(t, err)\n\n\tdefer sourceFile.Close()\n\n\tdestFile, err := os.Create(dst)\n\trequire.NoError(t, err)\n\n\tdefer destFile.Close()\n\n\t_, err = io.Copy(destFile, sourceFile)\n\trequire.NoError(t, err)\n\n\tsourceInfo, err := os.Stat(src)\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, os.Chmod(dst, sourceInfo.Mode()))\n}\n\n// isAWSResourceNotFoundError checks if an error indicates that an AWS resource (S3 bucket, DynamoDB table, etc.) was not found\nfunc isAWSResourceNotFoundError(err error) bool {\n\tvar apiErr smithy.APIError\n\n\treturn errors.As(err, &apiErr) && (apiErr.ErrorCode() == \"NoSuchBucket\" ||\n\t\tapiErr.ErrorCode() == \"NotFound\" ||\n\t\tapiErr.ErrorCode() == \"ResourceNotFoundException\")\n}\n"
  },
  {
    "path": "test/helpers/test_helpers.go",
    "content": "package helpers\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/component\"\n\t\"github.com/gruntwork-io/terragrunt/internal/os/signal\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run/creds/providers/externalcmd\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst defaultDirPerms = 0755\n\nfunc IsWindows() bool {\n\treturn runtime.GOOS == \"windows\"\n}\n\nfunc ValidateHookTraceParent(t *testing.T, hook, str string) {\n\tt.Helper()\n\n\ttraceparentLine := \"\"\n\n\tfor line := range strings.SplitSeq(str, \"\\n\") {\n\t\tif strings.HasPrefix(line, hook+\" {\\\"traceparent\\\": \\\"\") {\n\t\t\ttraceparentLine = line\n\t\t\tbreak\n\t\t}\n\t}\n\n\trequire.NotEmpty(t, traceparentLine, \"Expected \"+hook+\" output with traceparent value\")\n\tre := regexp.MustCompile(hook + ` \\{\"traceparent\": \"([^\"]+)\"\\}`)\n\tmatches := re.FindStringSubmatch(traceparentLine)\n\n\tconst matchesCount = 2\n\n\trequire.Len(t, matches, matchesCount, \"Expected to extract traceparent value from hook output\")\n\n\ttraceparentValue := matches[1]\n\trequire.NotEmpty(t, traceparentValue, \"Traceparent value should not be empty\")\n\trequire.Regexp(t, `^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$`, traceparentValue, \"Traceparent value should match W3C traceparent format\")\n}\n\n// CreateFile creates an empty file at the given path, creating parent directories if needed.\nfunc CreateFile(t *testing.T, paths ...string) {\n\tt.Helper()\n\n\tfullPath := filepath.Join(paths...)\n\n\terr := os.MkdirAll(filepath.Dir(fullPath), defaultDirPerms)\n\trequire.NoError(t, err)\n\n\tf, err := os.Create(fullPath)\n\trequire.NoError(t, err)\n\n\terr = f.Close()\n\trequire.NoError(t, err)\n}\n\n// CreateGitRepo initializes a git repository at the given path and creates an initial commit.\nfunc CreateGitRepo(t *testing.T, path string) {\n\tt.Helper()\n\n\tctx := t.Context()\n\n\t// Initialize git repo\n\tcmd := exec.CommandContext(ctx, \"git\", \"init\")\n\tcmd.Dir = path\n\n\toutput, err := cmd.CombinedOutput()\n\trequire.NoError(t, err, \"git init failed: %s\", string(output))\n\n\t// Configure user for the repo\n\tcmd = exec.CommandContext(ctx, \"git\", \"config\", \"user.email\", \"test@test.com\")\n\tcmd.Dir = path\n\t_, err = cmd.CombinedOutput()\n\trequire.NoError(t, err, \"git config user.email failed\")\n\n\tcmd = exec.CommandContext(ctx, \"git\", \"config\", \"user.name\", \"Test User\")\n\tcmd.Dir = path\n\t_, err = cmd.CombinedOutput()\n\trequire.NoError(t, err, \"git config user.name failed\")\n\n\t// Add all files and commit\n\tcmd = exec.CommandContext(ctx, \"git\", \"add\", \"-A\")\n\tcmd.Dir = path\n\t_, err = cmd.CombinedOutput()\n\trequire.NoError(t, err, \"git add failed\")\n\n\tcmd = exec.CommandContext(ctx, \"git\", \"commit\", \"-m\", \"initial commit\", \"--allow-empty\")\n\tcmd.Dir = path\n\t_, err = cmd.CombinedOutput()\n\trequire.NoError(t, err, \"git commit failed\")\n}\n\n// CloneGitRepo creates an isolated git clone for parallel testing.\n// Uses git clone --local --no-hardlinks to preserve file modes, symlinks,\n// and avoid worktree race conditions when multiple tests operate on same repo.\nfunc CloneGitRepo(t *testing.T, srcDir string) string {\n\tt.Helper()\n\n\tdstDir := TmpDirWOSymlinks(t)\n\n\tcmd := exec.CommandContext(t.Context(), \"git\", \"clone\", \"--local\", \"--no-hardlinks\", srcDir, dstDir)\n\toutput, err := cmd.CombinedOutput()\n\trequire.NoError(t, err, \"git clone failed: %s\", string(output))\n\n\treturn dstDir\n}\n\n// MakeDiscoveryContext creates a discovery context copy with an updated WorkingDir.\nfunc MakeDiscoveryContext(baseCtx *component.DiscoveryContext, dir string) *component.DiscoveryContext {\n\treturn &component.DiscoveryContext{\n\t\tWorkingDir: dir,\n\t\tCmd:        baseCtx.Cmd,\n\t\tArgs:       append([]string{}, baseCtx.Args...),\n\t}\n}\n\n// MakeOpts creates terragrunt options for a given directory.\nfunc MakeOpts(dir string) *options.TerragruntOptions {\n\topts := options.NewTerragruntOptions()\n\topts.WorkingDir = dir\n\topts.RootWorkingDir = dir\n\n\treturn opts\n}\n\n// IsExperimentMode returns true if the TG_EXPERIMENT_MODE environment variable is set.\nfunc IsExperimentMode(t *testing.T) bool {\n\tt.Helper()\n\t// Enable only on explicit true\n\tval := strings.TrimSpace(os.Getenv(\"TG_EXPERIMENT_MODE\"))\n\n\treturn strings.EqualFold(val, \"true\")\n}\n\n// ExecWithTestLogger executes a command and logs the output to the test logger.\nfunc ExecWithTestLogger(t *testing.T, dir, command string, args ...string) {\n\tt.Helper()\n\n\tctx := t.Context()\n\tcmd := exec.CommandContext(ctx, command, args...)\n\tcmd.Dir = dir\n\tcmd.Cancel = func() error {\n\t\tif cmd.Process == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tif sig := signal.SignalFromContext(ctx); sig != nil {\n\t\t\treturn cmd.Process.Signal(sig)\n\t\t}\n\n\t\treturn cmd.Process.Signal(os.Kill)\n\t}\n\n\tvar stdout, stderr bytes.Buffer\n\n\tprefix := strings.Join(append([]string{command}, args...), \" \")\n\n\tstdoutLogger := &testLogger{t: t, prefix: prefix + \" stdout\"}\n\tstderrLogger := &testLogger{t: t, prefix: prefix + \" stderr\"}\n\n\tcmd.Stdout = io.MultiWriter(&stdout, stdoutLogger)\n\tcmd.Stderr = io.MultiWriter(&stderr, stderrLogger)\n\n\terr := cmd.Run()\n\tif err != nil {\n\t\tt.Logf(\"Command failed: %s %v\", command, args)\n\t\tt.Logf(\"Full stdout:\\n%s\", stdout.String())\n\t\tt.Logf(\"Full stderr:\\n%s\", stderr.String())\n\t}\n\n\trequire.NoError(t, err)\n}\n\n// PointerTo returns a pointer to the given parameter.\n// Useful for constructing pointers to primitive types in test tables, etc.\nfunc PointerTo[T any](v T) *T {\n\treturn &v\n}\n\n// TmpDirWOSymlinks returns a temporary directory, evaluating any symlinks that might be there.\n//\n// This is useful for macOS tests where the standard library creates a temporary directory pointed to via a symlink\n// when using t.TempDir(). It's generally annoying to deal with in tests, as it breaks filepath comparisons, so\n// using this function is preferred over calling t.TempDir() directly unless you are sure it doesn't matter if the\n// temporary directory is a symlink.\nfunc TmpDirWOSymlinks(t *testing.T) string {\n\tt.Helper()\n\n\ttmpDir := t.TempDir()\n\ttmpDir, err := filepath.EvalSymlinks(tmpDir)\n\trequire.NoError(t, err)\n\n\treturn tmpDir\n}\n\ntype testLogger struct {\n\tt      *testing.T\n\tprefix string\n\tbuffer bytes.Buffer\n}\n\nfunc (tl *testLogger) Write(p []byte) (n int, err error) {\n\tn = len(p)\n\ttl.buffer.Write(p)\n\n\tfor {\n\t\tline, err := tl.buffer.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\ttl.buffer.Write(line)\n\t\t\tbreak\n\t\t}\n\n\t\tif len(line) > 0 && line[len(line)-1] == '\\n' {\n\t\t\tline = line[:len(line)-1]\n\t\t}\n\n\t\tif len(line) > 0 {\n\t\t\ttl.t.Logf(\"[%s] %s\", tl.prefix, string(line))\n\t\t}\n\t}\n\n\t//nolint:nilerr\n\treturn n, nil\n}\n\n// ExecWithMiseAndTestLogger executes a command using mise and logs the output to the test logger.\nfunc ExecWithMiseAndTestLogger(t *testing.T, dir, command string, args ...string) {\n\tt.Helper()\n\n\ttool := determineToolName(command)\n\n\targs = append([]string{\"x\", tool, \"--\", command}, args...)\n\n\tExecWithTestLogger(t, dir, \"mise\", args...)\n}\n\n// ExecAndCaptureOutput executes a command and captures the stdout and stderr.\nfunc ExecAndCaptureOutput(t *testing.T, dir, command string, args ...string) (string, string) {\n\tt.Helper()\n\n\tcmd := exec.CommandContext(t.Context(), command, args...)\n\tcmd.Dir = dir\n\n\tvar stdout, stderr bytes.Buffer\n\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\n\terr := cmd.Start()\n\trequire.NoError(t, err)\n\n\terr = cmd.Wait()\n\trequire.NoError(t, err)\n\n\treturn stdout.String(), stderr.String()\n}\n\n// ExecWithMiseAndCaptureOutput executes a command using mise and captures the stdout and stderr.\n// This is useful for commands that are being tested as installed via mise, as it doesn't depend\n// on the PATH being set correctly.\nfunc ExecWithMiseAndCaptureOutput(t *testing.T, dir, command string, args ...string) (string, string) {\n\tt.Helper()\n\n\ttool := determineToolName(command)\n\n\targs = append([]string{\"x\", tool, \"--\", command}, args...)\n\n\treturn ExecAndCaptureOutput(t, dir, \"mise\", args...)\n}\n\n// determineToolName determines the tool name to use for the given command.\nfunc determineToolName(command string) string {\n\tswitch command {\n\tcase \"tofu\":\n\t\treturn \"opentofu\"\n\tcase \"npm\":\n\t\treturn \"node\"\n\t}\n\n\treturn command\n}\n\n// FindCacheWorkingDir finds the working directory inside the .terragrunt-cache folder.\n// The cache layout is <root>/.terragrunt-cache/<hash>/<module>/..., so we expect exactly\n// one directory at each level. Fails the test if the structure is unexpected.\nfunc FindCacheWorkingDir(t *testing.T, rootDir string) string {\n\tt.Helper()\n\n\trequire.NotEmpty(t, rootDir, \"rootDir path cannot be empty\")\n\n\tcacheDir := filepath.Join(rootDir, \".terragrunt-cache\")\n\trequire.DirExists(t, cacheDir, \".terragrunt-cache directory should exist in %s\", rootDir)\n\n\tfirstLevel, err := os.ReadDir(cacheDir)\n\trequire.NoError(t, err)\n\n\t// Filter to only directories\n\tvar firstLevelDirs []os.DirEntry\n\n\tfor _, entry := range firstLevel {\n\t\tif entry.IsDir() {\n\t\t\tfirstLevelDirs = append(firstLevelDirs, entry)\n\t\t}\n\t}\n\n\trequire.Len(t, firstLevelDirs, 1,\n\t\t\"expected exactly one hash directory in %s, found %d: %v\",\n\t\tcacheDir, len(firstLevelDirs), dirNames(firstLevelDirs))\n\n\tfirstPath := filepath.Join(cacheDir, firstLevelDirs[0].Name())\n\tsecondLevel, err := os.ReadDir(firstPath)\n\trequire.NoError(t, err)\n\n\t// Filter to only directories\n\tvar secondLevelDirs []os.DirEntry\n\n\tfor _, entry := range secondLevel {\n\t\tif entry.IsDir() {\n\t\t\tsecondLevelDirs = append(secondLevelDirs, entry)\n\t\t}\n\t}\n\n\trequire.Len(t, secondLevelDirs, 1,\n\t\t\"expected exactly one module directory in %s, found %d: %v\",\n\t\tfirstPath, len(secondLevelDirs), dirNames(secondLevelDirs))\n\n\treturn filepath.Join(firstPath, secondLevelDirs[0].Name())\n}\n\n// dirNames extracts directory names from DirEntry slice for error messages.\nfunc dirNames(entries []os.DirEntry) []string {\n\tnames := make([]string, len(entries))\n\tfor i, e := range entries {\n\t\tnames[i] = e.Name()\n\t}\n\n\treturn names\n}\n\n// FileExistsInCache checks if a file exists within the cache directory structure.\nfunc FileExistsInCache(t *testing.T, rootDir, filename string) bool {\n\tt.Helper()\n\n\tcacheWorkingDir := FindCacheWorkingDir(t, rootDir)\n\tfilePath := filepath.Join(cacheWorkingDir, filename)\n\t_, err := os.Stat(filePath)\n\n\treturn err == nil\n}\n\n// ValidateAuthProviderScript runs the given auth provider script in the specified directory\n// and validates its response against the expected schema.\nfunc ValidateAuthProviderScript(t *testing.T, dir string, script string) {\n\tt.Helper()\n\n\tscriptStdout := bytes.Buffer{}\n\n\tcmd := exec.CommandContext(t.Context(), script)\n\tcmd.Dir = dir\n\tcmd.Stdout = &scriptStdout\n\n\terr := cmd.Run()\n\trequire.NoError(t, err)\n\n\terr = externalcmd.ValidateResponse(scriptStdout.Bytes())\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "test/helpers/test_helpers_unix.go",
    "content": "//go:build !windows\n// +build !windows\n\npackage helpers\n\nvar RootFolder = \"/\"\n"
  },
  {
    "path": "test/helpers/test_helpers_windows.go",
    "content": "//go:build windows\n// +build windows\n\npackage helpers\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\nvar RootFolder = retrieveRootFolder()\n\nfunc retrieveRootFolder() string {\n\tcwd, _ := os.Getwd()\n\n\treturn fmt.Sprintf(\"%s:/\", cwd[0:1])\n}\n"
  },
  {
    "path": "test/helpers/testcontainer_helpers.go",
    "content": "package helpers\n\nimport (\n\t\"io\"\n\t\"net\"\n\t\"net/url\"\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/docker/go-connections/nat\"\n\t\"github.com/mattn/go-shellwords\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/testcontainers/testcontainers-go\"\n\ttcexec \"github.com/testcontainers/testcontainers-go/exec\"\n\t\"github.com/testcontainers/testcontainers-go/wait\"\n)\n\nfunc ContExecNoOutput(tb testing.TB, container *testcontainers.DockerContainer, cmd string, options ...tcexec.ProcessOption) {\n\ttb.Helper()\n\tctx := tb.Context()\n\n\targs, err := shellwords.Parse(cmd)\n\trequire.NoError(tb, err)\n\n\tc, output, err := container.Exec(ctx, args, options...)\n\trequire.NoError(tb, err)\n\n\toutbytes, _ := io.ReadAll(output)\n\trequire.Zero(tb, c, string(outbytes))\n}\n\ntype tLogger struct {\n\ttb testing.TB\n}\n\nfunc (l tLogger) Printf(format string, v ...any) {\n\tl.tb.Helper()\n\tl.tb.Logf(format, v...)\n}\n\nfunc RunContainer(tb testing.TB, image string, port int, opts ...testcontainers.ContainerCustomizer) (c *testcontainers.DockerContainer, addr string) {\n\ttb.Helper()\n\n\tif testing.Short() {\n\t\ttb.Skip(\"Skipping testcontainer test in short mode\")\n\t}\n\n\tif os.Getenv(\"SKIP_TESTCONTAINERS\") != \"\" {\n\t\ttb.Skip(\"Skipping testcontainer test because SKIP_TESTCONTAINERS is set\")\n\t}\n\n\tctx := tb.Context()\n\n\tportStr := strconv.Itoa(port) + \"/tcp\"\n\n\topts = append(opts,\n\t\ttestcontainers.WithExposedPorts(portStr),\n\t\ttestcontainers.WithLogger(tLogger{tb}),\n\t\ttestcontainers.WithAdditionalWaitStrategy(\n\t\t\twait.ForListeningPort(nat.Port(portStr)),\n\t\t),\n\t)\n\n\tc, err := testcontainers.Run(ctx, image, opts...)\n\ttestcontainers.CleanupContainer(tb, c)\n\trequire.NoError(tb, err)\n\n\tmappedPort, err := c.MappedPort(ctx, nat.Port(portStr))\n\trequire.NoError(tb, err)\n\tmappedIP, err := c.Host(ctx)\n\trequire.NoError(tb, err)\n\n\tmappedAddr := (&url.URL{\n\t\tScheme: \"http\",\n\t\tHost:   net.JoinHostPort(mappedIP, mappedPort.Port()),\n\t}).String()\n\n\treturn c, mappedAddr\n}\n"
  },
  {
    "path": "test/integration_aws_oidc_test.go",
    "content": "//go:build awsoidc\n\n// These tests aren't hooked up to CI right now, as\n// we'll soon be moving to a new CI system (GitHub Actions)\n// and we don't want to add complexity to the migration by handling\n// both CircleCI and GitHub Actions at the same time.\n\npackage test_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\t// This is the environment variable that GitHub Actions injects into the environment.\n\t// We use this to detect if we are running in GitHub Actions.\n\tgithubActionsEnvVar = \"GITHUB_ACTIONS\"\n\t// Environment variable from GitHub Actions containing the URL to request the OIDC token.\n\tactionsIDTokenRequestURLEnvVar = \"ACTIONS_ID_TOKEN_REQUEST_URL\"\n\t// Environment variable from GitHub Actions containing the bearer token to authenticate the OIDC token request.\n\tactionsIDTokenRequestTokenEnvVar = \"ACTIONS_ID_TOKEN_REQUEST_TOKEN\"\n\n\t// This is a fixture that tests the assume-role-web-identity-file flag.\n\ttestFixtureAssumeRoleWebIdentityFile = \"fixtures/assume-role-web-identity/file-path\"\n)\n\nfunc TestAwsAssumeRoleWebIdentityFile(t *testing.T) {\n\t// t.Parallel() cannot be used together with t.Setenv()\n\t// t.Parallel()\n\ttoken := fetchGitHubOIDCToken(t)\n\n\t// These tests need to be run without the static key + secret\n\t// used by most AWS tests here.\n\tt.Setenv(\"AWS_ACCESS_KEY_ID\", \"\")\n\tos.Unsetenv(\"AWS_ACCESS_KEY_ID\")\n\tt.Setenv(\"AWS_SECRET_ACCESS_KEY\", \"\")\n\tos.Unsetenv(\"AWS_SECRET_ACCESS_KEY\")\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureAssumeRoleWebIdentityFile)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureAssumeRoleWebIdentityFile)\n\n\toriginalTerragruntConfigPath := filepath.Join(testFixtureAssumeRoleWebIdentityFile, \"terragrunt.hcl\")\n\ttmpTerragruntConfigFile := filepath.Join(testPath, \"terragrunt.hcl\")\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\trole := os.Getenv(\"AWS_TEST_OIDC_ROLE_ARN\")\n\trequire.NotEmpty(t, role)\n\n\ttokenFile := helpers.TmpDirWOSymlinks(t) + \"/oidc-token\"\n\trequire.NoError(t, os.WriteFile(tokenFile, []byte(token), 0400))\n\n\tdefer func() {\n\t\thelpers.DeleteS3Bucket(\n\t\t\tt,\n\t\t\thelpers.TerraformRemoteStateS3Region,\n\t\t\ts3BucketName,\n\t\t\toptions.WithIAMRoleARN(role),\n\t\t\toptions.WithIAMWebIdentityToken(token),\n\t\t)\n\t}()\n\n\thelpers.CopyAndFillMapPlaceholders(t, originalTerragruntConfigPath, tmpTerragruntConfigFile, map[string]string{\n\t\t\"__FILL_IN_BUCKET_NAME__\":              s3BucketName,\n\t\t\"__FILL_IN_REGION__\":                   helpers.TerraformRemoteStateS3Region,\n\t\t\"__FILL_IN_ASSUME_ROLE__\":              role,\n\t\t\"__FILL_IN_IDENTITY_TOKEN_FILE_PATH__\": tokenFile,\n\t})\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --backend-bootstrap --working-dir \"+testPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\toutput := fmt.Sprintf(\"%s %s\", stderr.String(), stdout.String())\n\tassert.Contains(t, output, \"Apply complete! Resources: 1 added, 0 changed, 0 destroyed.\")\n}\n\nfunc TestAwsAssumeRoleWebIdentityFlag(t *testing.T) {\n\t// t.Parallel() cannot be used together with t.Setenv()\n\t// t.Parallel()\n\ttoken := fetchGitHubOIDCToken(t)\n\n\t// These tests need to be run without the static key + secret\n\t// used by most AWS tests here.\n\tt.Setenv(\"AWS_ACCESS_KEY_ID\", \"\")\n\tos.Unsetenv(\"AWS_ACCESS_KEY_ID\")\n\tt.Setenv(\"AWS_SECRET_ACCESS_KEY\", \"\")\n\tos.Unsetenv(\"AWS_SECRET_ACCESS_KEY\")\n\n\ttmp := helpers.TmpDirWOSymlinks(t)\n\n\temptyTerragruntConfigPath := filepath.Join(tmp, \"terragrunt.hcl\")\n\trequire.NoError(t, os.WriteFile(emptyTerragruntConfigPath, []byte(\"\"), 0400))\n\n\temptyMainTFPath := filepath.Join(tmp, \"main.tf\")\n\trequire.NoError(t, os.WriteFile(emptyMainTFPath, []byte(\"\"), 0400))\n\n\troleARN := os.Getenv(\"AWS_TEST_OIDC_ROLE_ARN\")\n\trequire.NotEmpty(t, roleARN)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply --non-interactive --working-dir \"+tmp+\" --iam-assume-role \"+roleARN+\" --iam-assume-role-web-identity-token \"+token)\n}\n\nfunc TestAwsReadTerragruntAuthProviderCmdWithOIDC(t *testing.T) {\n\t// t.Parallel() cannot be used together with t.Setenv()\n\t// t.Parallel()\n\ttoken := fetchGitHubOIDCToken(t)\n\n\tt.Setenv(\"OIDC_TOKEN\", token)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureAuthProviderCmd)\n\toidcPath := filepath.Join(tmpEnvPath, testFixtureAuthProviderCmd, \"oidc\")\n\thelpers.CleanupTerraformFolder(t, oidcPath)\n\tmockAuthCmd := filepath.Join(oidcPath, \"mock-auth-cmd.sh\")\n\n\thelpers.ValidateAuthProviderScript(t, oidcPath, mockAuthCmd)\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(`terragrunt apply -auto-approve --non-interactive --working-dir %s --auth-provider-cmd %s`, oidcPath, mockAuthCmd))\n}\n\nfunc TestAwsReadTerragruntAuthProviderCmdWithOIDCRemoteState(t *testing.T) {\n\t// t.Parallel() cannot be used together with t.Setenv()\n\t// t.Parallel()\n\ttoken := fetchGitHubOIDCToken(t)\n\n\t// These tests need to be run without the static key + secret\n\t// used by most AWS tests here.\n\tt.Setenv(\"AWS_ACCESS_KEY_ID\", \"\")\n\tos.Unsetenv(\"AWS_ACCESS_KEY_ID\")\n\tt.Setenv(\"AWS_SECRET_ACCESS_KEY\", \"\")\n\tos.Unsetenv(\"AWS_SECRET_ACCESS_KEY\")\n\n\tt.Setenv(\"OIDC_TOKEN\", token)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureAuthProviderCmd)\n\tremoteStateOIDCPath := filepath.Join(tmpEnvPath, testFixtureAuthProviderCmd, \"remote-state-w-oidc\")\n\thelpers.CleanupTerraformFolder(t, remoteStateOIDCPath)\n\tmockAuthCmd := filepath.Join(remoteStateOIDCPath, \"mock-auth-cmd.sh\")\n\n\thelpers.ValidateAuthProviderScript(t, remoteStateOIDCPath, mockAuthCmd)\n\n\t// Create a temporary terragrunt config with actual values\n\ttmpTerragruntConfigFile := filepath.Join(remoteStateOIDCPath, \"terragrunt.hcl\")\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\trole := os.Getenv(\"AWS_TEST_OIDC_ROLE_ARN\")\n\trequire.NotEmpty(t, role)\n\n\tdefer func() {\n\t\thelpers.DeleteS3Bucket(\n\t\t\tt,\n\t\t\thelpers.TerraformRemoteStateS3Region,\n\t\t\ts3BucketName,\n\t\t\toptions.WithIAMRoleARN(role),\n\t\t\toptions.WithIAMWebIdentityToken(token),\n\t\t)\n\t}()\n\n\thelpers.CopyAndFillMapPlaceholders(t, tmpTerragruntConfigFile, tmpTerragruntConfigFile, map[string]string{\n\t\t\"__FILL_IN_BUCKET_NAME__\": s3BucketName,\n\t\t\"__FILL_IN_REGION__\":      helpers.TerraformRemoteStateS3Region,\n\t})\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt apply --working-dir %s --auth-provider-cmd %s --non-interactive --backend-bootstrap --log-level trace\",\n\t\t\tremoteStateOIDCPath,\n\t\t\tmockAuthCmd,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n}\n\n// oidcTokenResponse defines the structure of the JSON response from GitHub's OIDC token endpoint.\ntype oidcTokenResponse struct {\n\tValue string `json:\"value\"`\n}\n\n// fetchGitHubOIDCToken retrieves the OIDC token from the GitHub Actions environment by calling the token request URL.\n// It skips the test if not running in a GitHub Actions environment or if the required environment variables are not set.\n// It uses t.Fatalf if any part of the token fetching process fails after the initial checks.\nfunc fetchGitHubOIDCToken(t *testing.T) string {\n\tt.Helper()\n\n\tif os.Getenv(githubActionsEnvVar) != \"true\" {\n\t\tt.Skipf(\"Skipping test because it's not running in a GitHub Actions environment (expected %s=true)\", githubActionsEnvVar)\n\t}\n\n\trequestURL := os.Getenv(actionsIDTokenRequestURLEnvVar)\n\tif requestURL == \"\" {\n\t\tt.Skipf(\"Skipping test: Environment variable %s must be set in GitHub Actions to fetch OIDC token.\", actionsIDTokenRequestURLEnvVar)\n\t}\n\n\trequestToken := os.Getenv(actionsIDTokenRequestTokenEnvVar)\n\tif requestToken == \"\" {\n\t\tt.Skipf(\"Skipping test: Environment variable %s must be set in GitHub Actions to fetch OIDC token.\", actionsIDTokenRequestTokenEnvVar)\n\t}\n\n\tclient := &http.Client{}\n\tpostReqBody := strings.NewReader(`{\"aud\": \"sts.amazonaws.com\"}`)\n\treq, err := http.NewRequestWithContext(t.Context(), http.MethodPost, requestURL, postReqBody)\n\trequire.NoError(t, err, \"Failed to create OIDC token request to %s\", requestURL)\n\n\treq.Header.Set(\"Authorization\", \"Bearer \"+requestToken)\n\treq.Header.Set(\"Accept\", \"application/json\")\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := client.Do(req)\n\trequire.NoError(t, err, \"Failed to execute OIDC token request to %s\", requestURL)\n\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbodyBytes, readErr := io.ReadAll(resp.Body)\n\t\tif readErr != nil {\n\t\t\tt.Fatalf(\"OIDC token request to %s failed with status %s. Additionally, failed to read response body: %v\", requestURL, resp.Status, readErr)\n\t\t}\n\n\t\tt.Fatalf(\"OIDC token request to %s failed with status %s. Response: %s\", requestURL, resp.Status, string(bodyBytes))\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err, \"Failed to read OIDC token response body from %s\", requestURL)\n\n\tvar tokenResp oidcTokenResponse\n\n\terr = json.Unmarshal(body, &tokenResp)\n\trequire.NoError(t, err, \"Failed to unmarshal OIDC token response JSON from %s. Response: %s\", requestURL, string(body))\n\n\trequire.NotEmpty(t, tokenResp.Value, \"OIDC token 'value' field is empty in response from %s. Response: %s\", requestURL, string(body))\n\n\treturn tokenResp.Value\n}\n"
  },
  {
    "path": "test/integration_aws_test.go",
    "content": "//go:build aws || awsgcp\n\npackage test_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/dynamodb\"\n\t\"github.com/aws/aws-sdk-go-v2/service/dynamodb/types\"\n\t\"github.com/aws/aws-sdk-go-v2/service/iam\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\ts3types \"github.com/aws/aws-sdk-go-v2/service/s3/types\"\n\t\"github.com/aws/aws-sdk-go-v2/service/sts\"\n\t\"github.com/aws/smithy-go\"\n\n\t\"github.com/gruntwork-io/go-commons/files\"\n\t\"github.com/gruntwork-io/terragrunt/internal/awshelper\"\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\ts3backend \"github.com/gruntwork-io/terragrunt/internal/remotestate/backend/s3\"\n)\n\nconst (\n\ttestFixtureAwsProviderPatch                  = \"fixtures/aws-provider-patch\"\n\ttestFixtureAwsAccountAlias                   = \"fixtures/get-aws-account-alias\"\n\ttestFixtureAwsGetCallerIdentity              = \"fixtures/get-aws-caller-identity\"\n\ttestFixtureS3Errors                          = \"fixtures/s3-errors/\"\n\ttestFixtureAssumeRole                        = \"fixtures/assume-role/external-id\"\n\ttestFixtureAssumeRoleDuration                = \"fixtures/assume-role/duration\"\n\ttestFixtureReadIamRole                       = \"fixtures/read-config/iam_role_in_file\"\n\ttestFixtureOutputFromRemoteState             = \"fixtures/output-from-remote-state\"\n\ttestFixtureOutputFromDependency              = \"fixtures/output-from-dependency\"\n\ttestFixtureS3Backend                         = \"fixtures/s3-backend\"\n\ttestFixtureS3BackendDualLocking              = \"fixtures/s3-backend/dual-locking\"\n\ttestFixtureS3BackendUseLockfile              = \"fixtures/s3-backend/use-lockfile\"\n\ttestFixtureS3BackendDisableInit              = \"fixtures/s3-backend-disable-init\"\n\ttestFixtureAssumeRoleWithExternalIDWithComma = \"fixtures/assume-role/external-id-with-comma\"\n\n\tqaMyAppRelPath = \"qa/my-app\"\n)\n\nfunc TestAwsBootstrapBackend(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tcheckExpectedResultFn func(t *testing.T, output string, s3BucketName, dynamoDBName string, err error)\n\t\tname                  string\n\t\targs                  string\n\t}{\n\t\t{\n\t\t\tname: \"no bootstrap s3 backend without flag\",\n\t\t\targs: \"run apply\",\n\t\t\tcheckExpectedResultFn: func(t *testing.T, output string, s3BucketName, dynamoDBName string, err error) {\n\t\t\t\tt.Helper()\n\n\t\t\t\tassert.Regexp(t, \"(S3 bucket must have been previously created)|(S3 bucket does not exist)\", output)\n\t\t\t\trequire.Error(t, err)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"bootstrap s3 backend with flag\",\n\t\t\targs: \"run apply --backend-bootstrap\",\n\t\t\tcheckExpectedResultFn: func(t *testing.T, output string, s3BucketName, dynamoDBName string, err error) {\n\t\t\t\tt.Helper()\n\n\t\t\t\tvalidateS3BucketExistsAndIsTaggedAndVersioning(t, helpers.TerraformRemoteStateS3Region, s3BucketName, true, nil)\n\t\t\t\tvalidateDynamoDBTableExistsAndIsTaggedAndIsSSEncrypted(t, helpers.TerraformRemoteStateS3Region, dynamoDBName, nil, false)\n\n\t\t\t\trequire.NoError(t, err)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"bootstrap s3 backend with lock table ssencryption\",\n\t\t\targs: \"run apply --backend-bootstrap --feature enable_lock_table_ssencryption=true\",\n\t\t\tcheckExpectedResultFn: func(t *testing.T, output string, s3BucketName, dynamoDBName string, err error) {\n\t\t\t\tt.Helper()\n\n\t\t\t\tvalidateS3BucketExistsAndIsTaggedAndVersioning(t, helpers.TerraformRemoteStateS3Region, s3BucketName, true, nil)\n\t\t\t\tvalidateDynamoDBTableExistsAndIsTaggedAndIsSSEncrypted(t, helpers.TerraformRemoteStateS3Region, dynamoDBName, nil, true)\n\n\t\t\t\trequire.NoError(t, err)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"bootstrap s3 backend by backend command\",\n\t\t\targs: \"backend bootstrap\",\n\t\t\tcheckExpectedResultFn: func(t *testing.T, output string, s3BucketName, dynamoDBName string, err error) {\n\t\t\t\tt.Helper()\n\n\t\t\t\tvalidateS3BucketExistsAndIsTaggedAndVersioning(t, helpers.TerraformRemoteStateS3Region, s3BucketName, true, nil)\n\t\t\t\tvalidateDynamoDBTableExistsAndIsTaggedAndIsSSEncrypted(t, helpers.TerraformRemoteStateS3Region, dynamoDBName, nil, false)\n\n\t\t\t\trequire.NoError(t, err)\n\t\t\t},\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureS3Backend)\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureS3Backend)\n\t\t\trootPath := filepath.Join(tmpEnvPath, testFixtureS3Backend)\n\n\t\t\ttestID := strings.ToLower(helpers.UniqueID())\n\n\t\t\ts3BucketName := \"terragrunt-test-bucket-\" + testID\n\t\t\tdynamoDBName := \"terragrunt-test-dynamodb-\" + testID\n\n\t\t\tif tc.name != \"no bootstrap s3 backend without flag\" {\n\t\t\t\tdefer func() {\n\t\t\t\t\tdeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\t\t\t\t\tcleanupTableForTest(t, dynamoDBName, helpers.TerraformRemoteStateS3Region)\n\t\t\t\t}()\n\t\t\t}\n\n\t\t\tcommonConfigPath := filepath.Join(rootPath, \"common.hcl\")\n\t\t\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\t\t\tt,\n\t\t\t\tcommonConfigPath,\n\t\t\t\tcommonConfigPath,\n\t\t\t\ts3BucketName,\n\t\t\t\tdynamoDBName,\n\t\t\t\thelpers.TerraformRemoteStateS3Region,\n\t\t\t)\n\n\t\t\t// Also replace placeholders in subdirectory config files that are discovered by --all\n\t\t\tdualLockingConfigPath := filepath.Join(rootPath, \"dual-locking\", \"terragrunt.hcl\")\n\t\t\trequire.FileExists(t, dualLockingConfigPath)\n\n\t\t\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\t\t\tt,\n\t\t\t\tdualLockingConfigPath,\n\t\t\t\tdualLockingConfigPath,\n\t\t\t\ts3BucketName,\n\t\t\t\tdynamoDBName,\n\t\t\t\thelpers.TerraformRemoteStateS3Region,\n\t\t\t)\n\n\t\t\tuseLockfileConfigPath := filepath.Join(rootPath, \"use-lockfile\", \"terragrunt.hcl\")\n\t\t\trequire.FileExists(t, useLockfileConfigPath)\n\n\t\t\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\t\t\tt,\n\t\t\t\tuseLockfileConfigPath,\n\t\t\t\tuseLockfileConfigPath,\n\t\t\t\ts3BucketName,\n\t\t\t\tdynamoDBName,\n\t\t\t\thelpers.TerraformRemoteStateS3Region,\n\t\t\t)\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\t\t\tt,\n\t\t\t\t\"terragrunt \"+tc.args+\" --all --non-interactive --log-level debug --working-dir \"+rootPath,\n\t\t\t)\n\n\t\t\ttc.checkExpectedResultFn(t, stdout+stderr, s3BucketName, dynamoDBName, err)\n\t\t})\n\t}\n}\n\n// TestAwsDisableInitS3Backend verifies that remote_state.disable_init=true prevents\n// Terragrunt from bootstrapping S3 resources while still allowing Terraform to initialize\n// the backend normally.\nfunc TestAwsDisableInitS3Backend(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureS3BackendDisableInit)\n\n\t// setupEnv copies the fixture, fills placeholders, and returns the working dir root path.\n\tsetupEnv := func(bucketName string) string {\n\t\ttmpPath := helpers.CopyEnvironment(t, testFixtureS3BackendDisableInit)\n\t\troot := filepath.Join(tmpPath, testFixtureS3BackendDisableInit)\n\t\tcfgPath := filepath.Join(root, \"terragrunt.hcl\")\n\t\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, cfgPath, cfgPath, bucketName, \"\", helpers.TerraformRemoteStateS3Region)\n\n\t\treturn root\n\t}\n\n\t// Case 1: pre-existing bucket + disable_init=true → plan SUCCEEDS.\n\t// Terragrunt passes -backend-config= args (not -backend=false), so Terraform\n\t// can initialize the backend and run plan against the pre-existing bucket.\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\trootPath := setupEnv(s3BucketName)\n\n\tcreateS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\tdefer deleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run plan --non-interactive --log-level debug --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err, \"Expected plan to succeed with disable_init=true and pre-existing S3 bucket. stdout: %s, stderr: %s\", stdout, stderr)\n\n\t// Case 2: no bucket + disable_init=true (no --backend-bootstrap) → Terraform attempts backend init.\n\t// Proves disable_init=true passes -backend-config= args (not -backend=false): the plan fails at\n\t// the Terraform backend-init stage (bucket not found), not silently with disabled backend.\n\ts3BucketName2 := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tdefer deleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName2)\n\n\trootPath2 := setupEnv(s3BucketName2)\n\n\tnoBucketStdout, noBucketStderr, noBucketErr := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run plan --non-interactive --log-level debug --working-dir \"+rootPath2,\n\t)\n\tnoBucketOut := noBucketStdout + noBucketStderr\n\n\trequire.Error(t, noBucketErr, \"Expected plan to fail when backend bucket does not exist\")\n\tassert.Contains(t, noBucketOut, \"Initializing the backend\", \"Terraform should have attempted backend init, proving -backend-config= args were passed (not -backend=false)\")\n\n\t// Case 3: no bucket + disable_init=true + --backend-bootstrap → bootstrap is still SKIPPED.\n\t// This directly exercises the prepareInitCommandRunCfg guard: even with BackendBootstrap=true,\n\t// DisableInit=true must prevent Terragrunt from creating backend resources.\n\t// Expected: plan fails at Terraform backend-init (bucket not found), not from Terragrunt bootstrap.\n\ts3BucketName3 := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tdefer deleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName3)\n\n\trootPath3 := setupEnv(s3BucketName3)\n\n\tbootstrapStdout, bootstrapStderr, bootstrapErr := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run plan --backend-bootstrap --non-interactive --log-level debug --working-dir \"+rootPath3,\n\t)\n\tbootstrapOut := bootstrapStdout + bootstrapStderr\n\n\trequire.Error(t, bootstrapErr, \"Expected plan to fail when backend bucket does not exist even with --backend-bootstrap\")\n\t// Terraform must have reached backend init (received -backend-config= args, not -backend=false).\n\tassert.Contains(t, bootstrapOut, \"Initializing the backend\", \"Terraform should have attempted backend init with --backend-bootstrap + disable_init=true\")\n\t// Terragrunt bootstrap must NOT have run — it would have printed this if it tried to create the bucket.\n\tassert.NotContains(t, bootstrapOut, \"Creating S3 bucket\", \"Terragrunt must not attempt bucket creation when disable_init=true, even with --backend-bootstrap\")\n}\n\nfunc TestAwsDualLockingBackend(t *testing.T) {\n\tt.Parallel()\n\n\tif !helpers.IsNativeS3LockingSupported(t) {\n\t\tt.Skip(\"Wrapped binary does not support native S3 locking\")\n\t\treturn\n\t}\n\n\thelpers.CleanupTerraformFolder(t, testFixtureS3BackendDualLocking)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureS3BackendDualLocking)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureS3BackendDualLocking)\n\n\ttestID := strings.ToLower(helpers.UniqueID())\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + testID\n\tdynamoDBName := \"terragrunt-test-dynamodb-\" + testID\n\n\tdefer func() {\n\t\tdeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\t\tcleanupTableForTest(t, dynamoDBName, helpers.TerraformRemoteStateS3Region)\n\t}()\n\n\tterragruntConfigPath := filepath.Join(rootPath, \"terragrunt.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, terragruntConfigPath, terragruntConfigPath, s3BucketName, dynamoDBName, helpers.TerraformRemoteStateS3Region)\n\n\t// Test backend bootstrap with dual locking\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run apply --backend-bootstrap --non-interactive --log-level debug --working-dir \"+rootPath+\" -- -auto-approve\")\n\trequire.NoError(t, err)\n\n\t// Validate both S3 bucket and DynamoDB table are created\n\tvalidateS3BucketExistsAndIsTaggedAndVersioning(t, helpers.TerraformRemoteStateS3Region, s3BucketName, true, nil)\n\tvalidateDynamoDBTableExistsAndIsTaggedAndIsSSEncrypted(t, helpers.TerraformRemoteStateS3Region, dynamoDBName, nil, false)\n\n\tt.Logf(\"Dual locking test completed successfully. Output: %s, Errors: %s\", stdout, stderr)\n\n\t// Test that subsequent runs work with dual locking (both locks should be acquired)\n\tstdout2, stderr2, err2 := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run plan --non-interactive --log-level debug --working-dir \"+rootPath)\n\trequire.NoError(t, err2)\n\n\tt.Logf(\"Dual locking plan test completed successfully. Output: %s, Errors: %s\", stdout2, stderr2)\n}\n\nfunc TestAwsNativeS3LockingBackend(t *testing.T) {\n\tt.Parallel()\n\n\tif !helpers.IsNativeS3LockingSupported(t) {\n\t\tt.Skip(\"Wrapped binary does not support native S3 locking\")\n\t\treturn\n\t}\n\n\thelpers.CleanupTerraformFolder(t, testFixtureS3BackendUseLockfile)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureS3BackendUseLockfile)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureS3BackendUseLockfile)\n\n\ttestID := strings.ToLower(helpers.UniqueID())\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + testID\n\t// Note: No DynamoDB table needed for native S3 locking\n\n\tdefer func() {\n\t\tdeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\t\t// Note: No DynamoDB cleanup needed for S3 native locking\n\t}()\n\n\tterragruntConfigPath := filepath.Join(rootPath, \"terragrunt.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, terragruntConfigPath, terragruntConfigPath, s3BucketName, \"unused-dynamodb-name\", helpers.TerraformRemoteStateS3Region)\n\n\t// Test backend bootstrap with S3 native locking only\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run apply --backend-bootstrap --non-interactive --log-level debug --working-dir \"+rootPath+\" -- -auto-approve\")\n\trequire.NoError(t, err)\n\n\t// Validate S3 bucket is created and versioned\n\tvalidateS3BucketExistsAndIsTaggedAndVersioning(t, helpers.TerraformRemoteStateS3Region, s3BucketName, true, nil)\n\n\t// Note: No DynamoDB table validation - S3 native locking doesn't use DynamoDB\n\n\tt.Logf(\"S3 native locking test completed successfully. Output: %s, Errors: %s\", stdout, stderr)\n\n\t// Test that subsequent runs work with S3 native locking only\n\tstdout2, stderr2, err2 := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run plan --non-interactive --log-level debug --working-dir \"+rootPath)\n\trequire.NoError(t, err2)\n\n\tt.Logf(\"S3 native locking plan test completed successfully. Output: %s, Errors: %s\", stdout2, stderr2)\n}\n\nfunc TestAwsBootstrapBackendWithoutVersioning(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureS3Backend)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureS3Backend)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureS3Backend)\n\n\ttestID := strings.ToLower(helpers.UniqueID())\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + testID\n\tdynamoDBName := \"terragrunt-test-dynamodb-\" + testID\n\n\tdefer func() {\n\t\tdeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\t\tcleanupTableForTest(t, dynamoDBName, helpers.TerraformRemoteStateS3Region)\n\t}()\n\n\tcommonConfigPath := filepath.Join(rootPath, \"common.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, commonConfigPath, commonConfigPath, s3BucketName, dynamoDBName, helpers.TerraformRemoteStateS3Region)\n\n\t// Also replace placeholders in subdirectory config files that are discovered by --all\n\tdualLockingConfigPath := filepath.Join(rootPath, \"dual-locking\", \"terragrunt.hcl\")\n\trequire.FileExists(t, dualLockingConfigPath)\n\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\tt,\n\t\tdualLockingConfigPath,\n\t\tdualLockingConfigPath,\n\t\ts3BucketName,\n\t\tdynamoDBName,\n\t\thelpers.TerraformRemoteStateS3Region,\n\t)\n\t// Add skip_bucket_versioning to disable_versioning feature\n\tcontents, err := util.ReadFileAsString(dualLockingConfigPath)\n\trequire.NoError(t, err)\n\n\tanchorText := \"    enable_lock_table_ssencryption = feature.enable_lock_table_ssencryption.value\"\n\trequire.Contains(t, contents, anchorText, \"Expected anchor text not found in %s\", dualLockingConfigPath)\n\tnewContents := strings.ReplaceAll(contents, anchorText, anchorText+\"\\n    skip_bucket_versioning         = true\")\n\trequire.NotEqual(t, contents, newContents, \"strings.ReplaceAll did not modify contents of %s\", dualLockingConfigPath)\n\terr = os.WriteFile(dualLockingConfigPath, []byte(newContents), 0644)\n\trequire.NoError(t, err)\n\n\tuseLockfileConfigPath := filepath.Join(rootPath, \"use-lockfile\", \"terragrunt.hcl\")\n\trequire.FileExists(t, useLockfileConfigPath)\n\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\tt,\n\t\tuseLockfileConfigPath,\n\t\tuseLockfileConfigPath,\n\t\ts3BucketName,\n\t\tdynamoDBName,\n\t\thelpers.TerraformRemoteStateS3Region,\n\t)\n\t// Add skip_bucket_versioning for disable_versioning feature\n\tcontents, err = util.ReadFileAsString(useLockfileConfigPath)\n\trequire.NoError(t, err)\n\n\t// Use regex to match use_lockfile with any amount of whitespace before the equals sign\n\tuseLockfileRegex := regexp.MustCompile(`([ \\t]+use_lockfile\\s*=\\s*true)`)\n\trequire.Regexp(t, useLockfileRegex, contents, \"Expected use_lockfile pattern not found in %s\", useLockfileConfigPath)\n\tnewContents = useLockfileRegex.ReplaceAllString(contents, \"$1\\n    skip_bucket_versioning = true\")\n\trequire.NotEqual(t, contents, newContents, \"regex replacement did not modify contents of %s\", useLockfileConfigPath)\n\terr = os.WriteFile(useLockfileConfigPath, []byte(newContents), 0644)\n\trequire.NoError(t, err)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all --non-interactive --log-level debug --working-dir \"+rootPath+\" apply --backend-bootstrap --feature disable_versioning=true\",\n\t)\n\trequire.NoError(t, err)\n\n\tvalidateS3BucketExistsAndIsTaggedAndVersioning(t, helpers.TerraformRemoteStateS3Region, s3BucketName, false, nil)\n\tvalidateDynamoDBTableExistsAndIsTaggedAndIsSSEncrypted(t, helpers.TerraformRemoteStateS3Region, dynamoDBName, nil, false)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt --non-interactive --log-level debug --working-dir \"+rootPath+\" backend delete --backend-bootstrap --feature disable_versioning=true --all\",\n\t)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"backend delete for unit\")\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt --non-interactive --log-level debug --working-dir \"+rootPath+\" backend delete --backend-bootstrap --feature disable_versioning=true --all --force\",\n\t)\n\trequire.NoError(t, err)\n}\n\nfunc TestAwsBootstrapBackendWithAccessLogging(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureS3Backend)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureS3Backend)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureS3Backend)\n\n\ttestID := strings.ToLower(helpers.UniqueID())\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + testID\n\ts3AccessLogsBucketName := \"terragrunt-test-bucket-\" + testID + \"-access-logs\"\n\tdynamoDBName := \"terragrunt-test-dynamodb-\" + testID\n\n\tdefer func() {\n\t\tdeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\t\tdeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3AccessLogsBucketName)\n\t\tcleanupTableForTest(t, dynamoDBName, helpers.TerraformRemoteStateS3Region)\n\t}()\n\n\tcommonConfigPath := filepath.Join(rootPath, \"common.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\tt,\n\t\tcommonConfigPath,\n\t\tcommonConfigPath,\n\t\ts3BucketName,\n\t\tdynamoDBName,\n\t\thelpers.TerraformRemoteStateS3Region,\n\t)\n\n\t// Fill placeholders in use-lockfile and dual-locking configs (they have their own remote_state blocks)\n\tuseLockfilePath := filepath.Join(rootPath, \"use-lockfile\", \"terragrunt.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\tt,\n\t\tuseLockfilePath,\n\t\tuseLockfilePath,\n\t\ts3BucketName,\n\t\tdynamoDBName,\n\t\thelpers.TerraformRemoteStateS3Region,\n\t)\n\n\tdualLockingPath := filepath.Join(rootPath, \"dual-locking\", \"terragrunt.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\tt,\n\t\tdualLockingPath,\n\t\tdualLockingPath,\n\t\ts3BucketName,\n\t\tdynamoDBName,\n\t\thelpers.TerraformRemoteStateS3Region,\n\t)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all --non-interactive --log-level debug --working-dir \"+\n\t\t\trootPath+\" --feature access_logging_bucket=\"+s3AccessLogsBucketName+\n\t\t\t\" apply --backend-bootstrap\",\n\t)\n\trequire.NoError(t, err)\n\n\tvalidateS3BucketExistsAndIsTaggedAndVersioning(t, helpers.TerraformRemoteStateS3Region, s3BucketName, true, nil)\n\tvalidateS3BucketExistsAndIsTaggedAndVersioning(t, helpers.TerraformRemoteStateS3Region, s3AccessLogsBucketName, true, nil)\n\tvalidateDynamoDBTableExistsAndIsTaggedAndIsSSEncrypted(t, helpers.TerraformRemoteStateS3Region, dynamoDBName, nil, false)\n}\n\nfunc TestAwsMigrateBackendWithoutVersioning(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureS3Backend)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureS3Backend)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureS3Backend)\n\tunitPath := filepath.Join(rootPath, \"unit1\")\n\n\ttestID := strings.ToLower(helpers.UniqueID())\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + testID\n\tdynamoDBName := \"terragrunt-test-dynamodb-\" + testID\n\n\tdefer func() {\n\t\tdeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\t\tcleanupTableForTest(t, dynamoDBName, helpers.TerraformRemoteStateS3Region)\n\t}()\n\n\tcommonConfigPath := filepath.Join(rootPath, \"common.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, commonConfigPath, commonConfigPath, s3BucketName, dynamoDBName, helpers.TerraformRemoteStateS3Region)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --non-interactive --log-level debug --working-dir \"+unitPath+\" --feature disable_versioning=true apply --backend-bootstrap -- -auto-approve\")\n\trequire.NoError(t, err)\n\n\tvalidateS3BucketExistsAndIsTaggedAndVersioning(t, helpers.TerraformRemoteStateS3Region, s3BucketName, false, nil)\n\tvalidateDynamoDBTableExistsAndIsTaggedAndIsSSEncrypted(t, helpers.TerraformRemoteStateS3Region, dynamoDBName, nil, false)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt --non-interactive --log-level debug --working-dir \"+rootPath+\" backend migrate --backend-bootstrap --feature disable_versioning=true unit1 unit2\")\n\trequire.Error(t, err)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt --non-interactive --log-level debug --working-dir \"+rootPath+\" backend migrate --backend-bootstrap --feature disable_versioning=true --force unit1 unit2\")\n\trequire.NoError(t, err)\n}\n\nfunc TestAwsDeleteBackend(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureS3Backend)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureS3Backend)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureS3Backend)\n\n\ttestID := strings.ToLower(helpers.UniqueID())\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + testID\n\tdynamoDBName := \"terragrunt-test-dynamodb-\" + testID\n\n\tdefer func() {\n\t\tdeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\t\tcleanupTableForTest(t, dynamoDBName, helpers.TerraformRemoteStateS3Region)\n\t}()\n\n\tcommonConfigPath := filepath.Join(rootPath, \"common.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\tt,\n\t\tcommonConfigPath,\n\t\tcommonConfigPath,\n\t\ts3BucketName,\n\t\tdynamoDBName,\n\t\thelpers.TerraformRemoteStateS3Region,\n\t)\n\n\t// Also replace placeholders in subdirectory config files that are discovered by --all\n\tdualLockingConfigPath := filepath.Join(rootPath, \"dual-locking\", \"terragrunt.hcl\")\n\trequire.FileExists(t, dualLockingConfigPath)\n\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\tt,\n\t\tdualLockingConfigPath,\n\t\tdualLockingConfigPath,\n\t\ts3BucketName,\n\t\tdynamoDBName,\n\t\thelpers.TerraformRemoteStateS3Region,\n\t)\n\n\tuseLockfileConfigPath := filepath.Join(rootPath, \"use-lockfile\", \"terragrunt.hcl\")\n\trequire.FileExists(t, useLockfileConfigPath)\n\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\tt,\n\t\tuseLockfileConfigPath,\n\t\tuseLockfileConfigPath,\n\t\ts3BucketName,\n\t\tdynamoDBName,\n\t\thelpers.TerraformRemoteStateS3Region,\n\t)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run apply --backend-bootstrap --all --non-interactive --log-level debug --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tremoteStateKeys := []string{\n\t\t\"unit1/tofu.tfstate\",\n\t\t\"unit2/tofu.tfstate\",\n\t}\n\n\tfor _, key := range remoteStateKeys {\n\t\ttableKey := path.Join(s3BucketName, key+\"-md5\")\n\n\t\tassert.True(t, doesS3BucketKeyExist(t, helpers.TerraformRemoteStateS3Region, s3BucketName, key), \"S3 bucket key %s must exist\", key)\n\t\tassert.True(t, doesDynamoDBTableItemExist(t, helpers.TerraformRemoteStateS3Region, dynamoDBName, tableKey), \"DynamoDB table key %s must exist\", tableKey)\n\t}\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt backend delete --all --non-interactive --log-level debug --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tfor _, key := range remoteStateKeys {\n\t\ttableKey := path.Join(s3BucketName, key+\"-md5\")\n\n\t\tassert.False(t, doesS3BucketKeyExist(t, helpers.TerraformRemoteStateS3Region, s3BucketName, key), \"S3 bucket key %s must not exist\", key)\n\t\tassert.False(t, doesDynamoDBTableItemExist(t, helpers.TerraformRemoteStateS3Region, dynamoDBName, tableKey), \"DynamoDB table key %s must not exist\", tableKey)\n\t}\n}\n\nfunc TestAwsMigrateBackend(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureS3Backend)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureS3Backend)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureS3Backend)\n\n\ttestID := strings.ToLower(helpers.UniqueID())\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + testID\n\tdynamoDBName := \"terragrunt-test-dynamodb-\" + testID\n\n\tunit1Path := filepath.Join(rootPath, \"unit1\")\n\tunit2Path := filepath.Join(rootPath, \"unit2\")\n\n\tunit1BackendKey := \"unit1/tofu.tfstate\"\n\tunit2BackendKey := \"unit2/tofu.tfstate\"\n\n\tunit1TableKey := path.Join(s3BucketName, unit1BackendKey+\"-md5\")\n\tunit2TableKey := path.Join(s3BucketName, unit2BackendKey+\"-md5\")\n\n\tdefer func() {\n\t\tdeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\t\tcleanupTableForTest(t, dynamoDBName, helpers.TerraformRemoteStateS3Region)\n\t}()\n\n\tcommonConfigPath := filepath.Join(rootPath, \"common.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, commonConfigPath, commonConfigPath, s3BucketName, dynamoDBName, helpers.TerraformRemoteStateS3Region)\n\n\t// Bootstrap backend and create remote state for unit1.\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run apply --backend-bootstrap --non-interactive --log-level debug --working-dir \"+unit1Path+\" -- -auto-approve\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"Changes to Outputs\")\n\n\t// Check for remote states.\n\n\tassert.True(t, doesS3BucketKeyExist(t, helpers.TerraformRemoteStateS3Region, s3BucketName, unit1BackendKey), \"S3 bucket key %s must exist\", unit1BackendKey)\n\tassert.True(t, doesDynamoDBTableItemExist(t, helpers.TerraformRemoteStateS3Region, dynamoDBName, unit1TableKey), \"DynamoDB table key %s must exist\", unit1TableKey)\n\tassert.False(t, doesS3BucketKeyExist(t, helpers.TerraformRemoteStateS3Region, s3BucketName, unit2BackendKey), \"S3 bucket key %s must not exist\", unit2BackendKey)\n\tassert.False(t, doesDynamoDBTableItemExist(t, helpers.TerraformRemoteStateS3Region, dynamoDBName, unit2TableKey), \"DynamoDB table key %s must not exist\", unit2TableKey)\n\n\t// Migrate remote state from unit1 to unit2.\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt backend migrate --log-level debug --working-dir \"+rootPath+\" unit1 unit2\")\n\trequire.NoError(t, err)\n\n\t// Check for remote states after migration.\n\tassert.False(t, doesS3BucketKeyExist(t, helpers.TerraformRemoteStateS3Region, s3BucketName, unit1BackendKey), \"S3 bucket key %s must not exist\", unit1BackendKey)\n\tassert.False(t, doesDynamoDBTableItemExist(t, helpers.TerraformRemoteStateS3Region, dynamoDBName, unit1TableKey), \"DynamoDB table key %s must not exist\", unit1TableKey)\n\tassert.True(t, doesS3BucketKeyExist(t, helpers.TerraformRemoteStateS3Region, s3BucketName, unit2BackendKey), \"S3 bucket key %s must exist\", unit2BackendKey)\n\tassert.True(t, doesDynamoDBTableItemExist(t, helpers.TerraformRemoteStateS3Region, dynamoDBName, unit2TableKey), \"DynamoDB table key %s must exist\", unit2TableKey)\n\n\t// Run `tofu apply` for unit2 with migrated remote state from unit1.\n\n\tstdout, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run apply --backend-bootstrap --non-interactive --log-level debug --working-dir \"+unit2Path+\" -- -auto-approve\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"No changes\")\n}\n\nfunc TestAwsInitHookNoSourceWithBackend(t *testing.T) {\n\tt.Parallel()\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksInitOnceNoSourceWithBackend)\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/hooks/init-once\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksInitOnceNoSourceWithBackend)\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\trootTerragruntConfigPath := filepath.Join(rootPath, \"terragrunt.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, \"not-used\", helpers.TerraformRemoteStateS3Region)\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --backend-bootstrap --working-dir \"+rootPath, &stdout, &stderr)\n\toutput := stdout.String()\n\n\tif err != nil {\n\t\tt.Errorf(\"Did not expect to get error: %s\", err.Error())\n\t}\n\n\tassert.Equal(t, 1, strings.Count(output, \"AFTER_INIT_ONLY_ONCE\"), \"Hooks on init command executed more than once\")\n\t// With always-cache behavior, init-from-module hooks execute even when no source is explicitly specified\n\t// because source=\".\" (local copy to cache) is used internally\n\tassert.Equal(t, 1, strings.Count(output, \"AFTER_INIT_FROM_MODULE_ONLY_ONCE\"), \"Hooks on init-from-module command should execute once\")\n}\n\nfunc TestAwsInitHookWithSourceWithBackend(t *testing.T) {\n\tt.Parallel()\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksInitOnceWithSourceWithBackend)\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/hooks/init-once\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksInitOnceWithSourceWithBackend)\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\trootTerragruntConfigPath := filepath.Join(rootPath, \"terragrunt.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, \"not-used\", helpers.TerraformRemoteStateS3Region)\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --backend-bootstrap --working-dir \"+rootPath, &stdout, &stderr)\n\toutput := stdout.String()\n\n\tif err != nil {\n\t\tt.Errorf(\"Did not expect to get error: %s\", err.Error())\n\t}\n\n\t// `init` hook should execute only once\n\tassert.Equal(t, 1, strings.Count(output, \"AFTER_INIT_ONLY_ONCE\"), \"Hooks on init command executed more than once\")\n\t// `init-from-module` hook should execute only once\n\tassert.Equal(t, 1, strings.Count(output, \"AFTER_INIT_FROM_MODULE_ONLY_ONCE\"), \"Hooks on init-from-module command executed more than once\")\n}\n\nfunc TestAwsBeforeAfterAndErrorMergeHook(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHooksBeforeAfterAndErrorMergePath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksBeforeAfterAndErrorMergePath)\n\tchildPath := filepath.Join(rootPath, qaMyAppRelPath)\n\thelpers.CleanupTerraformFolder(t, childPath)\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\tt.Logf(\"bucketName: %s\", s3BucketName)\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\ttmpTerragruntConfigPath := helpers.CreateTmpTerragruntConfigWithParentAndChild(t, testFixtureHooksBeforeAfterAndErrorMergePath, qaMyAppRelPath, s3BucketName, \"root.hcl\", config.DefaultTerragruntConfigPath)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --backend-bootstrap --config %s --working-dir %s\", tmpTerragruntConfigPath, childPath), &stdout, &stderr)\n\trequire.ErrorContains(t, err, \"executable file not found in $PATH\")\n\n\t// Hook output files are now in .terragrunt-cache directory\n\tcacheDir := helpers.FindCacheWorkingDir(t, childPath)\n\trequire.NotEmpty(t, cacheDir, \"Cache directory should exist\")\n\n\t_, beforeException := os.ReadFile(filepath.Join(cacheDir, \"before.out\"))\n\t_, beforeChildException := os.ReadFile(filepath.Join(cacheDir, \"before-child.out\"))\n\t_, beforeOverriddenParentException := os.ReadFile(filepath.Join(cacheDir, \"before-parent.out\"))\n\t_, afterException := os.ReadFile(filepath.Join(cacheDir, \"after.out\"))\n\t_, afterParentException := os.ReadFile(filepath.Join(cacheDir, \"after-parent.out\"))\n\t_, errorHookParentException := os.ReadFile(filepath.Join(cacheDir, \"error-hook-parent.out\"))\n\t_, errorHookChildException := os.ReadFile(filepath.Join(cacheDir, \"error-hook-child.out\"))\n\t_, errorHookOverridenParentException := os.ReadFile(filepath.Join(cacheDir, \"error-hook-merge-parent.out\"))\n\n\trequire.NoError(t, beforeException)\n\trequire.NoError(t, beforeChildException)\n\trequire.NoError(t, afterException)\n\trequire.NoError(t, afterParentException)\n\trequire.NoError(t, errorHookParentException)\n\trequire.NoError(t, errorHookChildException)\n\n\t// PathError because no file found\n\trequire.Error(t, beforeOverriddenParentException)\n\trequire.Error(t, errorHookOverridenParentException)\n}\n\nfunc TestAwsWorksWithLocalTerraformVersion(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, testFixturePath)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\tdefer cleanupTableForTest(t, lockTableName, helpers.TerraformRemoteStateS3Region)\n\n\ttmpTerragruntConfigPath := helpers.CreateTmpTerragruntConfig(t, rootPath, s3BucketName, lockTableName, config.DefaultTerragruntConfigPath)\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --backend-bootstrap --config %s --working-dir %s\", tmpTerragruntConfigPath, rootPath))\n\n\tvar expectedS3Tags = map[string]string{\n\t\t\"owner\": \"terragrunt integration test\",\n\t\t\"name\":  \"Terraform state storage\"}\n\tvalidateS3BucketExistsAndIsTaggedAndVersioning(t, helpers.TerraformRemoteStateS3Region, s3BucketName, true, expectedS3Tags)\n\n\tvar expectedDynamoDBTableTags = map[string]string{\n\t\t\"owner\": \"terragrunt integration test\",\n\t\t\"name\":  \"Terraform lock table\"}\n\tvalidateDynamoDBTableExistsAndIsTaggedAndIsSSEncrypted(t, helpers.TerraformRemoteStateS3Region, lockTableName, expectedDynamoDBTableTags, true)\n}\n\n// Regression test to ensure that `accesslogging_bucket_name` and `accesslogging_target_prefix` are taken into account\n// & the TargetLogs bucket is set to a new S3 bucket, different from the origin S3 bucket\n// & the logs objects are prefixed with the `accesslogging_target_prefix` value\nfunc TestAwsSetsAccessLoggingForTfSTateS3BuckeToADifferentBucketWithGivenTargetPrefix(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRegressions)\n\texamplePath := filepath.Join(tmpEnvPath, testFixtureRegressions, \"accesslogging-bucket/with-target-prefix-input\")\n\thelpers.CleanupTerraformFolder(t, examplePath)\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\ts3BucketLogsName := s3BucketName + \"-tf-state-logs\"\n\ts3BucketLogsTargetPrefix := \"logs/\"\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\tdefer cleanupTableForTest(t, lockTableName, helpers.TerraformRemoteStateS3Region)\n\n\ttmpTerragruntConfigPath := helpers.CreateTmpTerragruntConfig(\n\t\tt,\n\t\texamplePath,\n\t\ts3BucketName,\n\t\tlockTableName,\n\t\t\"remote_terragrunt.hcl\",\n\t)\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt validate --non-interactive --backend-bootstrap --config %s --working-dir %s\", tmpTerragruntConfigPath, examplePath))\n\n\ttargetLoggingBucket := helpers.GetS3BucketLoggingTarget(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\ttargetLoggingBucketPrefix := helpers.GetS3BucketLoggingTargetPrefix(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\tassert.Equal(t, s3BucketLogsName, targetLoggingBucket)\n\tassert.Equal(t, s3BucketLogsTargetPrefix, targetLoggingBucketPrefix)\n\n\tencryptionConfig, err := bucketEncryption(t, helpers.TerraformRemoteStateS3Region, targetLoggingBucket)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, encryptionConfig)\n\tassert.NotNil(t, encryptionConfig.ServerSideEncryptionConfiguration)\n\n\tfor _, rule := range encryptionConfig.ServerSideEncryptionConfiguration.Rules {\n\t\tif rule.ApplyServerSideEncryptionByDefault != nil {\n\t\t\tassert.Equal(t, s3types.ServerSideEncryptionAes256, rule.ApplyServerSideEncryptionByDefault.SSEAlgorithm)\n\t\t}\n\t}\n\n\tpolicy, err := bucketPolicy(t, helpers.TerraformRemoteStateS3Region, targetLoggingBucket)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, policy.Policy)\n\n\tpolicyInBucket, err := awshelper.UnmarshalPolicy(*policy.Policy)\n\trequire.NoError(t, err)\n\n\tenforceSSE := false\n\n\tif policyInBucket.Statement != nil {\n\t\tfor _, statement := range policyInBucket.Statement {\n\t\t\tif statement.Sid == s3backend.SidEnforcedTLSPolicy {\n\t\t\t\tenforceSSE = true\n\t\t\t}\n\t\t}\n\t}\n\n\tassert.True(t, enforceSSE)\n}\n\n// Regression test to ensure that `accesslogging_bucket_name` is taken into account\n// & when no `accesslogging_target_prefix` provided, then **default** value is used for TargetPrefix\nfunc TestAwsSetsAccessLoggingForTfSTateS3BucketToADifferentBucketWithDefaultTargetPrefix(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRegressions)\n\texamplePath := filepath.Join(tmpEnvPath, testFixtureRegressions, \"accesslogging-bucket/no-target-prefix-input\")\n\thelpers.CleanupTerraformFolder(t, examplePath)\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\ts3BucketLogsName := s3BucketName + \"-tf-state-logs\"\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\tdefer cleanupTableForTest(t, lockTableName, helpers.TerraformRemoteStateS3Region)\n\n\ttmpTerragruntConfigPath := helpers.CreateTmpTerragruntConfig(\n\t\tt,\n\t\texamplePath,\n\t\ts3BucketName,\n\t\tlockTableName,\n\t\t\"remote_terragrunt.hcl\",\n\t)\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt validate --non-interactive --backend-bootstrap --config %s \"+\n\t\t\t\t\"--working-dir %s\",\n\t\t\ttmpTerragruntConfigPath,\n\t\t\texamplePath,\n\t\t),\n\t)\n\n\ttargetLoggingBucket := helpers.GetS3BucketLoggingTarget(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\ttargetLoggingBucketPrefix := helpers.GetS3BucketLoggingTargetPrefix(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\tencryptionConfig, err := bucketEncryption(t, helpers.TerraformRemoteStateS3Region, targetLoggingBucket)\n\trequire.NoError(t, err)\n\tassert.NotNil(t, encryptionConfig)\n\tassert.NotNil(t, encryptionConfig.ServerSideEncryptionConfiguration)\n\n\tfor _, rule := range encryptionConfig.ServerSideEncryptionConfiguration.Rules {\n\t\tif rule.ApplyServerSideEncryptionByDefault != nil {\n\t\t\tassert.Equal(t, s3types.ServerSideEncryptionAes256, rule.ApplyServerSideEncryptionByDefault.SSEAlgorithm)\n\t\t}\n\t}\n\n\tassert.Equal(t, s3BucketLogsName, targetLoggingBucket)\n\tassert.Equal(t, s3backend.DefaultS3BucketAccessLoggingTargetPrefix, targetLoggingBucketPrefix)\n}\n\nfunc TestAwsRunAllCommand(t *testing.T) {\n\tt.Parallel()\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutputAll)\n\n\trootTerragruntConfigPath := filepath.Join(tmpEnvPath, testFixtureOutputAll, \"root.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\tt,\n\t\trootTerragruntConfigPath,\n\t\trootTerragruntConfigPath,\n\t\ts3BucketName,\n\t\t\"not-used\",\n\t\t\"not-used\",\n\t)\n\n\tenvironmentPath := fmt.Sprintf(\"%s/%s/env1\", tmpEnvPath, testFixtureOutputAll)\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\t\"terragrunt run --all init --backend-bootstrap \"+\n\t\t\t\"--non-interactive --working-dir \"+environmentPath,\n\t)\n}\n\nfunc TestAwsOutputAllCommand(t *testing.T) {\n\tt.Parallel()\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutputAll)\n\n\trootTerragruntConfigPath := filepath.Join(tmpEnvPath, testFixtureOutputAll, \"root.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, \"not-used\", \"not-used\")\n\n\tenvironmentPath := fmt.Sprintf(\"%s/%s/env1\", tmpEnvPath, testFixtureOutputAll)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --backend-bootstrap --working-dir \"+environmentPath)\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\thelpers.RunTerragruntRedirectOutput(t, \"terragrunt run --all output --non-interactive --backend-bootstrap --working-dir \"+environmentPath, &stdout, &stderr)\n\toutput := stdout.String()\n\n\tassert.Contains(t, output, \"app1 output\")\n\tassert.Contains(t, output, \"app2 output\")\n\tassert.Contains(t, output, \"app3 output\")\n\n\tassert.True(t, (strings.Index(output, \"app3 output\") < strings.Index(output, \"app1 output\")) && (strings.Index(output, \"app1 output\") < strings.Index(output, \"app2 output\")))\n}\n\nfunc TestAwsOutputFromDependency(t *testing.T) {\n\t// t.Parallel() cannot be used together with t.Setenv()\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutputFromDependency)\n\n\trootTerragruntPath := filepath.Join(tmpEnvPath, testFixtureOutputFromDependency)\n\tdepTerragruntConfigPath := filepath.Join(rootTerragruntPath, \"dependency\", config.DefaultTerragruntConfigPath)\n\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, depTerragruntConfigPath, depTerragruntConfigPath, s3BucketName, \"not-used\", helpers.TerraformRemoteStateS3Region)\n\n\tt.Setenv(\"AWS_CSM_ENABLED\", \"true\")\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all --backend-bootstrap --non-interactive --working-dir %s --log-level trace\",\n\t\t\trootTerragruntPath,\n\t\t)+\n\t\t\t\" -- apply -auto-approve\",\n\t)\n\trequire.NoError(t, err)\n\n\tassert.NotContains(t, stderr, \"invalid character\")\n}\n\nfunc TestAwsValidateAllCommand(t *testing.T) {\n\tt.Parallel()\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutputAll)\n\n\trootTerragruntConfigPath := filepath.Join(tmpEnvPath, testFixtureOutputAll, \"root.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\tt,\n\t\trootTerragruntConfigPath,\n\t\trootTerragruntConfigPath,\n\t\ts3BucketName,\n\t\t\"not-used\",\n\t\t\"not-used\",\n\t)\n\n\tenvironmentPath := fmt.Sprintf(\"%s/%s/env1\", tmpEnvPath, testFixtureOutputAll)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all validate --backend-bootstrap --non-interactive --working-dir \"+environmentPath)\n}\n\nfunc TestAwsOutputAllCommandSpecificVariableIgnoreDependencyErrors(t *testing.T) {\n\tt.Parallel()\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutputAll)\n\n\trootTerragruntConfigPath := filepath.Join(tmpEnvPath, testFixtureOutputAll, \"root.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, \"not-used\", \"not-used\")\n\n\tenvironmentPath := fmt.Sprintf(\"%s/%s/env1\", tmpEnvPath, testFixtureOutputAll)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --backend-bootstrap --working-dir \"+environmentPath)\n\n\t// Call helpers.RunTerragruntCommand directly because this command contains failures (which causes helpers.RunTerragruntRedirectOutput to abort) but we don't care.\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt, \"terragrunt run --all output app2_text --queue-ignore-errors --non-interactive \"+\n\t\t\t\"--backend-bootstrap --working-dir \"+environmentPath,\n\t)\n\trequire.Error(t, err)\n\n\t// Without --queue-ignore-errors, app2 never runs because its dependencies have \"errors\" since they don't have the output \"app2_text\".\n\tassert.Contains(t, stdout, \"app2 output\")\n}\n\nfunc TestAwsStackCommands(t *testing.T) { //nolint paralleltest\n\t// It seems that disabling parallel test execution helps avoid the CircleCi error: \"NoSuchBucket Policy: The bucket policy does not exist.\"\n\t// t.Parallel()\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\tdefer cleanupTableForTest(t, lockTableName, helpers.TerraformRemoteStateS3Region)\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStack)\n\thelpers.CleanupTerragruntFolder(t, testFixtureStack)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStack)\n\n\trootTerragruntConfigPath := filepath.Join(tmpEnvPath, testFixtureStack, \"root.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\tt,\n\t\trootTerragruntConfigPath,\n\t\trootTerragruntConfigPath,\n\t\ts3BucketName,\n\t\tlockTableName,\n\t\t\"not-used\",\n\t)\n\n\tmgmtEnvironmentPath := filepath.Join(tmpEnvPath, testFixtureStack, \"mgmt\")\n\tstageEnvironmentPath := filepath.Join(tmpEnvPath, testFixtureStack, \"stage\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --backend-bootstrap --all apply --non-interactive --working-dir \"+mgmtEnvironmentPath)\n\thelpers.RunTerragrunt(t, \"terragrunt run --backend-bootstrap --all apply --non-interactive --working-dir \"+stageEnvironmentPath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --backend-bootstrap --all output --non-interactive --working-dir \"+mgmtEnvironmentPath)\n\thelpers.RunTerragrunt(t, \"terragrunt run --backend-bootstrap --all output --non-interactive --working-dir \"+stageEnvironmentPath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --backend-bootstrap --all destroy --non-interactive --working-dir \"+stageEnvironmentPath)\n\thelpers.RunTerragrunt(t, \"terragrunt run --backend-bootstrap --all destroy --non-interactive --working-dir \"+mgmtEnvironmentPath)\n}\n\nfunc TestAwsRemoteWithBackend(t *testing.T) {\n\tt.Parallel()\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tlockTableName := \"terragrunt-lock-table-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\tdefer cleanupTableForTest(t, lockTableName, helpers.TerraformRemoteStateS3Region)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRemoteWithBackend)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureRemoteWithBackend)\n\n\trootTerragruntConfigPath := filepath.Join(rootPath, \"terragrunt.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, lockTableName, \"not-used\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --backend-bootstrap --non-interactive --working-dir \"+rootPath)\n\n\t// Run a second time to make sure the temporary folder can be reused without errors\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n}\n\nfunc TestAwsLocalWithBackend(t *testing.T) {\n\tt.Parallel()\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tlockTableName := \"terragrunt-lock-table-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\tdefer cleanupTableForTest(t, lockTableName, helpers.TerraformRemoteStateS3Region)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/download\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLocalWithBackend)\n\n\trootTerragruntConfigPath := filepath.Join(rootPath, \"terragrunt.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, lockTableName, \"not-used\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --backend-bootstrap --working-dir \"+rootPath)\n\n\t// Run a second time to make sure the temporary folder can be reused without errors\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --backend-bootstrap --working-dir \"+rootPath)\n}\n\nfunc TestAwsGetAccountAliasFunctions(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureAwsAccountAlias)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureAwsAccountAlias)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureAwsAccountAlias)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// verify expected outputs are not empty\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\t// Get values from STS\n\tawsCfg, err := awshelper.NewAWSConfigBuilder().Build(t.Context(), createLogger())\n\tif err != nil {\n\t\tt.Fatalf(\"Error while creating AWS config: %v\", err)\n\t}\n\n\tiamClient := iam.NewFromConfig(awsCfg)\n\n\taliases, err := iamClient.ListAccountAliases(t.Context(), &iam.ListAccountAliasesInput{})\n\tif err != nil {\n\t\tt.Fatalf(\"Error while getting AWS account aliases: %v\", err)\n\t}\n\n\talias := \"\"\n\tif len(aliases.AccountAliases) == 1 {\n\t\talias = aliases.AccountAliases[0]\n\t}\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\tassert.Equal(t, outputs[\"account_alias\"].Value, alias)\n}\n\nfunc TestAwsGetCallerIdentityFunctions(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureAwsGetCallerIdentity)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureAwsGetCallerIdentity)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureAwsGetCallerIdentity)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// verify expected outputs are not empty\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt run --non-interactive --working-dir \"+rootPath+\" -- output -no-color -json\", &stdout, &stderr),\n\t)\n\n\t// Get values from STS\n\tawsCfg, err := awshelper.NewAWSConfigBuilder().Build(t.Context(), createLogger())\n\tif err != nil {\n\t\tt.Fatalf(\"Error while creating AWS config: %v\", err)\n\t}\n\n\tstsClient := sts.NewFromConfig(awsCfg)\n\n\tidentity, err := stsClient.GetCallerIdentity(t.Context(), &sts.GetCallerIdentityInput{})\n\tif err != nil {\n\t\tt.Fatalf(\"Error while getting AWS caller identity: %v\", err)\n\t}\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\tassert.Equal(t, outputs[\"account\"].Value, *identity.Account)\n\tassert.Equal(t, outputs[\"arn\"].Value, *identity.Arn)\n\tassert.Equal(t, outputs[\"user_id\"].Value, *identity.UserId)\n}\n\n// We test the path with remote_state blocks by:\n// - Applying all modules initially\n// - Deleting the local state of the nested deep dependency\n// - Running apply on the root module\n// If output optimization is working, we should still get the same correct output even though the state of the upmost\n// module has been destroyed.\nfunc TestAwsDependencyOutputOptimization(t *testing.T) {\n\tt.Parallel()\n\n\texpectedOutput := `They said, \"No, The answer is 42\"`\n\tgeneratedUniqueID := helpers.UniqueID()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"nested-optimization\")\n\trootTerragruntConfigPath := filepath.Join(rootPath, \"root.hcl\")\n\tlivePath := filepath.Join(rootPath, \"live\")\n\tdeepDepPath := filepath.Join(rootPath, \"deepdep\")\n\tdepPath := filepath.Join(rootPath, \"dep\")\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(generatedUniqueID)\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(generatedUniqueID)\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\tdefer cleanupTableForTest(t, lockTableName, helpers.TerraformRemoteStateS3Region)\n\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\tt,\n\t\trootTerragruntConfigPath,\n\t\trootTerragruntConfigPath,\n\t\ts3BucketName,\n\t\tlockTableName,\n\t\thelpers.TerraformRemoteStateS3Region,\n\t)\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\t\"terragrunt apply --all --non-interactive --backend-bootstrap --working-dir \"+rootPath,\n\t)\n\n\t// verify expected output\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt output -no-color -json --non-interactive --working-dir \"+livePath,\n\t)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal([]byte(stdout), &outputs))\n\tassert.Equal(t, expectedOutput, outputs[\"output\"].Value)\n\n\t// If we want to force reinit, delete the relevant .terraform directories\n\t// Since terraform runs from cache, clean the cache directory\n\tdepCacheDir := helpers.FindCacheWorkingDir(t, depPath)\n\trequire.NotEmpty(t, depCacheDir, \"Cache directory for dep should exist\")\n\thelpers.CleanupTerraformFolder(t, depCacheDir)\n\n\t// Now delete the deepdep state and verify still works\n\t// Since terraform runs from cache, the state file is in the cache directory\n\tdeepDepCacheDir := helpers.FindCacheWorkingDir(t, deepDepPath)\n\trequire.NotEmpty(t, deepDepCacheDir, \"Cache directory for deepdep should exist\")\n\trequire.NoError(t, os.Remove(filepath.Join(deepDepCacheDir, \"terraform.tfstate\")))\n\n\treout, reerr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --log-level debug --non-interactive --working-dir \"+livePath+\" -- output -no-color -json\",\n\t)\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, json.Unmarshal([]byte(reout), &outputs))\n\tassert.Equal(t, expectedOutput, outputs[\"output\"].Value)\n\n\tfor _, logRegexp := range []string{`prefix=../dep .+Running command: ` + wrappedBinary() + ` init -get=false`} {\n\t\tassert.Regexp(t, logRegexp, reerr)\n\t}\n}\n\nfunc TestAwsDependencyOutputOptimizationSkipInit(t *testing.T) {\n\tt.Parallel()\n\n\texpectOutputLogs := []string{\n\t\t`prefix=../dep .+Unit '../dep' is already init-ed. Retrieving outputs directly from working directory.`,\n\t}\n\tdependencyOutputOptimizationTest(t, \"nested-optimization\", false, expectOutputLogs)\n}\n\nfunc TestAwsDependencyOutputOptimizationNoGenerate(t *testing.T) {\n\tt.Parallel()\n\n\texpectOutputLogs := []string{\n\t\t`prefix=../dep .+Running command: ` + wrappedBinary() + ` init -get=false`,\n\t}\n\tdependencyOutputOptimizationTest(t, \"nested-optimization-nogen\", true, expectOutputLogs)\n}\n\nfunc TestAwsDependencyOutputOptimizationDisableTest(t *testing.T) {\n\tt.Parallel()\n\n\texpectedOutput := `They said, \"No, The answer is 42\"`\n\tgeneratedUniqueID := helpers.UniqueID()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"nested-optimization-disable\")\n\trootTerragruntConfigPath := filepath.Join(rootPath, \"root.hcl\")\n\tlivePath := filepath.Join(rootPath, \"live\")\n\tdeepDepPath := filepath.Join(rootPath, \"deepdep\")\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(generatedUniqueID)\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(generatedUniqueID)\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\tdefer cleanupTableForTest(t, lockTableName, helpers.TerraformRemoteStateS3Region)\n\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, lockTableName, helpers.TerraformRemoteStateS3Region)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --backend-bootstrap --working-dir \"+rootPath)\n\n\t// verify expected output\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+livePath)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal([]byte(stdout), &outputs))\n\tassert.Equal(t, expectedOutput, outputs[\"output\"].Value)\n\n\t// Now delete the deepdep state and verify it no longer works, because it tries to fetch the deepdep dependency\n\t// Since terraform runs from cache, the state file is in the cache directory\n\tdeepDepCacheDir := helpers.FindCacheWorkingDir(t, deepDepPath)\n\trequire.NotEmpty(t, deepDepCacheDir, \"Cache directory for deepdep should exist\")\n\trequire.NoError(t, os.Remove(filepath.Join(deepDepCacheDir, \"terraform.tfstate\")))\n\trequire.NoError(t, os.RemoveAll(filepath.Join(deepDepCacheDir, \".terraform\")))\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+livePath)\n\trequire.Error(t, err)\n}\n\nfunc TestAwsProviderPatch(t *testing.T) {\n\tt.Parallel()\n\n\trootPath := helpers.CopyEnvironment(t, testFixtureAwsProviderPatch)\n\tmodulePath := filepath.Join(rootPath, testFixtureAwsProviderPatch)\n\tmainTFFile := filepath.Join(modulePath, \"main.tf\")\n\n\t// fill in branch so we can test against updates to the test case file\n\tmainContents, err := util.ReadFileAsString(mainTFFile)\n\trequire.NoError(t, err)\n\tgitRunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\tbranchName := gitRunner.WithWorkDir(modulePath).GetCurrentBranch(t.Context())\n\t// https://www.terraform.io/docs/language/modules/sources.html#modules-in-package-sub-directories\n\t// https://github.com/gruntwork-io/terragrunt/issues/1778\n\tbranchName = url.QueryEscape(branchName)\n\tmainContents = strings.ReplaceAll(mainContents, \"__BRANCH_NAME__\", branchName)\n\trequire.NoError(t, os.WriteFile(mainTFFile, []byte(mainContents), 0444))\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t`terragrunt aws-provider-patch --override-attr 'region=\"eu-west-1\"' --override-attr allowed_account_ids='[\"00000000000\"]' --working-dir %s --log-level trace`,\n\t\t\tmodulePath,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Regexp(t, \"Patching AWS provider in .+test/fixtures/aws-provider-patch/example-module/main.tf\", stderr)\n\n\t// Make sure the resulting terraform code is still valid\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt validate --working-dir \"+modulePath, os.Stdout, os.Stderr),\n\t)\n}\n\nfunc TestAwsPrintAwsErrors(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureS3Errors)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureS3Errors)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\ts3BucketName := \"test-tg-2023-02\"\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(helpers.UniqueID())\n\n\ttmpTerragruntConfigFile := filepath.Join(rootPath, \"terragrunt.hcl\")\n\toriginalTerragruntConfigPath := filepath.Join(rootPath, \"terragrunt.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, originalTerragruntConfigPath, tmpTerragruntConfigFile, s3BucketName, lockTableName, \"us-east-2\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, fmt.Sprintf(\"terragrunt apply --non-interactive --backend-bootstrap --config %s --working-dir %s\", tmpTerragruntConfigFile, rootPath), &stdout, &stderr)\n\trequire.Error(t, err)\n\tmessage := err.Error()\n\tassert.True(t,\n\t\tstrings.Contains(\n\t\t\tmessage,\n\t\t\t\"AllAccessDisabled: All access to this object has been disabled\",\n\t\t) ||\n\t\t\tstrings.Contains(message, \"BucketRegionError: incorrect region\") ||\n\t\t\tstrings.Contains(message, \"MovedPermanently\"),\n\t)\n}\n\nfunc TestAwsErrorWhenStateBucketIsInDifferentRegion(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureS3Errors)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureS3Errors)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(helpers.UniqueID())\n\n\toriginalTerragruntConfigPath := filepath.Join(testFixtureS3Errors, \"terragrunt.hcl\")\n\ttmpTerragruntConfigFile := filepath.Join(rootPath, \"terragrunt.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, originalTerragruntConfigPath, tmpTerragruntConfigFile, s3BucketName, lockTableName, \"us-east-1\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, fmt.Sprintf(\"terragrunt apply --non-interactive --backend-bootstrap --config %s --working-dir %s\", tmpTerragruntConfigFile, rootPath), &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, originalTerragruntConfigPath, tmpTerragruntConfigFile, s3BucketName, lockTableName, \"us-west-2\")\n\n\tstdout = bytes.Buffer{}\n\tstderr = bytes.Buffer{}\n\terr = helpers.RunTerragruntCommand(t, fmt.Sprintf(\"terragrunt apply --non-interactive --backend-bootstrap --config %s --working-dir %s\", tmpTerragruntConfigFile, rootPath), &stdout, &stderr)\n\trequire.Error(t, err)\n\n\tassert.True(t, strings.Contains(\n\t\terr.Error(), \"MovedPermanently\") || strings.Contains(err.Error(), \"BucketRegionError: incorrect region\"),\n\t\t\"Expected error to contain 'MovedPermanently' or 'BucketRegionError: incorrect region', but got: %s\", err.Error(),\n\t)\n}\n\nfunc TestAwsDisableBucketUpdate(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, testFixturePath)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(helpers.UniqueID())\n\n\tcreateS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\tcreateDynamoDBTable(t, helpers.TerraformRemoteStateS3Region, lockTableName)\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\tdefer cleanupTableForTest(t, lockTableName, helpers.TerraformRemoteStateS3Region)\n\n\ttmpTerragruntConfigPath := helpers.CreateTmpTerragruntConfig(t, rootPath, s3BucketName, lockTableName, config.DefaultTerragruntConfigPath)\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply -auto-approve --disable-bucket-update --non-interactive --config %s --working-dir %s\", tmpTerragruntConfigPath, rootPath))\n\n\t_, err := bucketPolicy(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\t// validate that bucket policy is not updated, because of --disable-bucket-update\n\trequire.Error(t, err)\n}\n\nfunc TestAwsUpdatePolicy(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, testFixturePath)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(helpers.UniqueID())\n\n\tcreateS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\tdefer cleanupTableForTest(t, lockTableName, helpers.TerraformRemoteStateS3Region)\n\n\ttmpTerragruntConfigPath := helpers.CreateTmpTerragruntConfig(t, rootPath, s3BucketName, lockTableName, config.DefaultTerragruntConfigPath)\n\n\t// check that there is no policy on created bucket\n\t_, err := bucketPolicy(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\trequire.Error(t, err)\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply -auto-approve --backend-bootstrap --non-interactive --config %s --working-dir %s\", tmpTerragruntConfigPath, rootPath))\n\n\t// check that policy is created\n\t_, err = bucketPolicy(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\trequire.NoError(t, err)\n}\n\nfunc TestAwsAssumeRoleDuration(t *testing.T) {\n\tt.Parallel()\n\n\tif isTerraform() {\n\t\tt.Skip(\"New assume role duration config not supported by Terraform 1.5.x\")\n\t\treturn\n\t}\n\n\tassumeRole := os.Getenv(\"AWS_TEST_S3_ASSUME_ROLE\")\n\tif len(assumeRole) == 0 {\n\t\tt.Error(\"AWS_TEST_S3_ASSUME_ROLE environment variable not set\")\n\t\treturn\n\t}\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureAssumeRoleDuration)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureAssumeRoleDuration)\n\n\toriginalTerragruntConfigPath := filepath.Join(testFixtureAssumeRoleDuration, \"terragrunt.hcl\")\n\ttmpTerragruntConfigFile := filepath.Join(testPath, \"terragrunt.hcl\")\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\thelpers.CopyAndFillMapPlaceholders(t, originalTerragruntConfigPath, tmpTerragruntConfigFile, map[string]string{\n\t\t\"__FILL_IN_BUCKET_NAME__\":      s3BucketName,\n\t\t\"__FILL_IN_REGION__\":           helpers.TerraformRemoteStateS3Region,\n\t\t\"__FILL_IN_LOGS_BUCKET_NAME__\": s3BucketName + \"-tf-state-logs\",\n\t\t\"__FILL_IN_ASSUME_ROLE__\":      assumeRole,\n\t})\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --backend-bootstrap --working-dir \"+testPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\toutput := fmt.Sprintf(\"%s %s\", stderr.String(), stdout.String())\n\tassert.Contains(t, output, \"Apply complete! Resources: 1 added, 0 changed, 0 destroyed.\")\n\t// run one more time to check that no init is performed\n\tstdout = bytes.Buffer{}\n\tstderr = bytes.Buffer{}\n\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --backend-bootstrap --working-dir \"+testPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\toutput = fmt.Sprintf(\"%s %s\", stderr.String(), stdout.String())\n\tassert.NotContains(t, output, \"Initializing the backend...\")\n\tassert.NotContains(t, output, \"has been successfully initialized!\")\n\tassert.Contains(t, output, \"no changes are needed.\")\n}\n\n// Regression testing for https://github.com/gruntwork-io/terragrunt/issues/906\nfunc TestAwsDependencyOutputSameOutputConcurrencyRegression(t *testing.T) {\n\tt.Parallel()\n\n\t// Use func to isolate each test run to a single s3 bucket that is deleted. We run the test multiple times\n\t// because the underlying error we are trying to test against is nondeterministic, and thus may not always work\n\t// the first time.\n\ttt := func() {\n\t\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\t\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"regression-906\")\n\n\t\t// Make sure to fill in the s3 bucket to the config. Also ensure the bucket is deleted before the next for\n\t\t// loop call.\n\t\ts3BucketName := fmt.Sprintf(\"terragrunt-test-bucket-%s%s\", strings.ToLower(helpers.UniqueID()), strings.ToLower(helpers.UniqueID()))\n\t\tdefer helpers.DeleteS3BucketWithRetry(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\t\tcommonDepConfigPath := filepath.Join(rootPath, \"common-dep\", \"terragrunt.hcl\")\n\t\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, commonDepConfigPath, commonDepConfigPath, s3BucketName, \"not-used\", \"not-used\")\n\n\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\t\tt,\n\t\t\t\"terragrunt run --all apply --backend-bootstrap --source-update --non-interactive --working-dir \"+rootPath,\n\t\t)\n\t\trequire.NoError(t, err)\n\t}\n\n\tfor range 3 {\n\t\ttt()\n\t}\n}\n\nfunc TestAwsRemoteStateCodegenGeneratesBackendBlockS3(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"remote-state\", \"s3\")\n\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\tdefer cleanupTableForTest(t, lockTableName, helpers.TerraformRemoteStateS3Region)\n\n\ttmpTerragruntConfigPath := helpers.CreateTmpTerragruntConfig(t, generateTestCase, s3BucketName, lockTableName, config.DefaultTerragruntConfigPath)\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --backend-bootstrap --config %s --working-dir %s\", tmpTerragruntConfigPath, generateTestCase))\n}\n\nfunc TestAwsOutputFromRemoteState(t *testing.T) { //nolint: paralleltest\n\t// NOTE: We can't run this test in parallel because there are other tests that also call `config.ClearOutputCache()`, but this function uses a global variable and sometimes it throws an unexpected error:\n\t// \"fixtures/output-from-remote-state/env1/app2/terragrunt.hcl:23,38-48: Unsupported attribute; This object does not have an attribute named \"app3_text\".\"\n\t// t.Parallel()\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutputFromRemoteState)\n\n\trootTerragruntConfigPath := filepath.Join(tmpEnvPath, testFixtureOutputFromRemoteState, \"root.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\tt,\n\t\trootTerragruntConfigPath,\n\t\trootTerragruntConfigPath,\n\t\ts3BucketName,\n\t\t\"not-used\",\n\t\t\"not-used\",\n\t)\n\n\tenvironmentPath := fmt.Sprintf(\"%s/%s/env1\", tmpEnvPath, testFixtureOutputFromRemoteState)\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --backend-bootstrap --dependency-fetch-output-from-state \"+\n\t\t\t\t\"--non-interactive --working-dir %s/app1 -- apply -auto-approve\",\n\t\t\tenvironmentPath,\n\t\t),\n\t)\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --backend-bootstrap --dependency-fetch-output-from-state \"+\n\t\t\t\t\"--non-interactive --working-dir %s/app3 -- apply -auto-approve\",\n\t\t\tenvironmentPath,\n\t\t),\n\t)\n\t// Now delete dependencies cached state\n\t// Since terraform runs from cache, the state files are in the cache directories\n\tapp1CacheDir := helpers.FindCacheWorkingDir(t, filepath.Join(environmentPath, \"app1\"))\n\trequire.NotEmpty(t, app1CacheDir, \"Cache directory for app1 should exist\")\n\trequire.NoError(t, os.Remove(filepath.Join(app1CacheDir, \".terraform/terraform.tfstate\")))\n\trequire.NoError(t, os.RemoveAll(filepath.Join(app1CacheDir, \".terraform\")))\n\tapp3CacheDir := helpers.FindCacheWorkingDir(t, filepath.Join(environmentPath, \"app3\"))\n\trequire.NotEmpty(t, app3CacheDir, \"Cache directory for app3 should exist\")\n\trequire.NoError(t, os.Remove(filepath.Join(app3CacheDir, \".terraform/terraform.tfstate\")))\n\trequire.NoError(t, os.RemoveAll(filepath.Join(app3CacheDir, \".terraform\")))\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --backend-bootstrap --dependency-fetch-output-from-state \"+\n\t\t\t\t\"--non-interactive --working-dir %s/app2 -- apply -auto-approve\",\n\t\t\tenvironmentPath,\n\t\t),\n\t)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all output --backend-bootstrap --dependency-fetch-output-from-state --non-interactive --working-dir \"+environmentPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"app1 output\")\n\tassert.Contains(t, stdout, \"app2 output\")\n\tassert.Contains(t, stdout, \"app3 output\")\n\tassert.NotContains(t, stderr, \"terraform output -json\")\n\tassert.NotContains(t, stderr, \"tofu output -json\")\n\n\tassert.True(\n\t\tt, (strings.Index(stdout, \"app3 output\") < strings.Index(stdout, \"app1 output\")) &&\n\t\t\t(strings.Index(stdout, \"app1 output\") < strings.Index(stdout, \"app2 output\")),\n\t)\n}\n\nfunc TestAwsNoDependencyFetchOutputFromState(t *testing.T) { //nolint: paralleltest\n\t// NOTE: We can't run this test in parallel because there are other tests that also call `config.ClearOutputCache()`, but this function uses a global variable and sometimes it throws an unexpected error:\n\t// \"fixtures/output-from-remote-state/env1/app2/terragrunt.hcl:23,38-48: Unsupported attribute; This object does not have an attribute named \"app3_text\".\"\n\t// t.Parallel()\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutputFromRemoteState)\n\n\trootTerragruntConfigPath := filepath.Join(tmpEnvPath, testFixtureOutputFromRemoteState, \"root.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, \"not-used\", \"not-used\")\n\n\tenvironmentPath := fmt.Sprintf(\"%s/%s/env1\", tmpEnvPath, testFixtureOutputFromRemoteState)\n\n\t// Apply dependencies first\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt apply --backend-bootstrap --dependency-fetch-output-from-state \"+\n\t\t\t\t\"--auto-approve --non-interactive --working-dir %s/app1\",\n\t\t\tenvironmentPath,\n\t\t),\n\t)\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt apply --backend-bootstrap --dependency-fetch-output-from-state \"+\n\t\t\t\t\"--auto-approve --non-interactive --working-dir %s/app3\",\n\t\t\tenvironmentPath,\n\t\t),\n\t)\n\t// Now delete dependencies cached state\n\t// Since terraform runs from cache, the state files are in the cache directories\n\tapp1CacheDir := helpers.FindCacheWorkingDir(t, filepath.Join(environmentPath, \"app1\"))\n\trequire.NotEmpty(t, app1CacheDir, \"Cache directory for app1 should exist\")\n\trequire.NoError(t, os.Remove(filepath.Join(app1CacheDir, \".terraform/terraform.tfstate\")))\n\trequire.NoError(t, os.RemoveAll(filepath.Join(app1CacheDir, \".terraform\")))\n\tapp3CacheDir := helpers.FindCacheWorkingDir(t, filepath.Join(environmentPath, \"app3\"))\n\trequire.NotEmpty(t, app3CacheDir, \"Cache directory for app3 should exist\")\n\trequire.NoError(t, os.Remove(filepath.Join(app3CacheDir, \".terraform/terraform.tfstate\")))\n\trequire.NoError(t, os.RemoveAll(filepath.Join(app3CacheDir, \".terraform\")))\n\n\t// Apply app2 with experiment enabled but --no-dependency-fetch-output-from-state flag set\n\t// This should fall back to using terraform output instead of fetching from state\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt apply --backend-bootstrap --experiment dependency-fetch-output-from-state \"+\n\t\t\t\t\"--no-dependency-fetch-output-from-state --auto-approve --non-interactive --working-dir %s/app2\",\n\t\t\tenvironmentPath,\n\t\t),\n\t)\n\n\t// Run output command with experiment enabled but flag set to disable\n\t// When the flag is set, it should use terraform output instead of fetching from S3\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --log-level debug --all output --backend-bootstrap --experiment dependency-fetch-output-from-state \"+\n\t\t\t\"--no-dependency-fetch-output-from-state --non-interactive --working-dir \"+environmentPath,\n\t)\n\trequire.NoError(t, err)\n\n\t// Verify outputs are still correct\n\tassert.Contains(t, stdout, \"app1 output\")\n\tassert.Contains(t, stdout, \"app2 output\")\n\tassert.Contains(t, stdout, \"app3 output\")\n\n\t// When --no-dependency-fetch-output-from-state is set, it should use terraform output\n\t// This means we should see \"terraform output -json\" or \"tofu output -json\" in stderr\n\t// (The exact command depends on which terraform implementation is being used)\n\t// This is the opposite of TestAwsOutputFromRemoteState which asserts this is NOT present\n\tassert.True(\n\t\tt,\n\t\tstrings.Contains(\n\t\t\tstderr,\n\t\t\t\"terraform output\",\n\t\t) || strings.Contains(\n\t\t\tstderr,\n\t\t\t\"tofu output\",\n\t\t),\n\t\t\"Expected to see terraform/tofu output command when --no-dependency-fetch-output-from-state flag is set, but stderr was: %s\",\n\t\tstderr,\n\t)\n}\n\nfunc TestAwsMockOutputsFromRemoteState(t *testing.T) { //nolint: paralleltest\n\t// NOTE: We can't run this test in parallel because there are other tests that also call `config.ClearOutputCache()`, but this function uses a global variable and sometimes it throws an unexpected error:\n\t// \"fixtures/output-from-remote-state/env1/app2/terragrunt.hcl:23,38-48: Unsupported attribute; This object does not have an attribute named \"app3_text\".\"\n\t// t.Parallel()\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutputFromRemoteState)\n\n\trootTerragruntConfigPath := filepath.Join(tmpEnvPath, testFixtureOutputFromRemoteState, \"root.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, \"not-used\", \"not-used\")\n\n\tenvironmentPath := filepath.Join(tmpEnvPath, testFixtureOutputFromRemoteState, \"env1\")\n\n\t// applying only the app1 dependency, the app3 dependency was purposely not applied and should be mocked when running the app2 module\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply --dependency-fetch-output-from-state --auto-approve --backend-bootstrap --non-interactive --working-dir %s/app1\", environmentPath))\n\t// Now delete dependencies cached state\n\t// Since terraform runs from cache, the state files are in the cache directories\n\tapp1CacheDir := helpers.FindCacheWorkingDir(t, filepath.Join(environmentPath, \"app1\"))\n\trequire.NotEmpty(t, app1CacheDir, \"Cache directory for app1 should exist\")\n\trequire.NoError(t, os.Remove(filepath.Join(app1CacheDir, \".terraform/terraform.tfstate\")))\n\trequire.NoError(t, os.RemoveAll(filepath.Join(app1CacheDir, \".terraform\")))\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt init --dependency-fetch-output-from-state --non-interactive --working-dir %s/app2\", environmentPath))\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"Failed to read outputs\")\n\tassert.Contains(t, stderr, \"fallback to mock outputs\")\n}\n\nfunc TestAwsParallelStateInit(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\tfor i := range 20 {\n\t\terr := util.CopyFolderContents(logger.CreateLogger(), testFixtureParallelStateInit, tmpEnvPath, \".terragrunt-test\", nil, nil)\n\t\trequire.NoError(t, err)\n\t\terr = os.Rename(\n\t\t\tpath.Join(tmpEnvPath, \"template\"),\n\t\t\tpath.Join(tmpEnvPath, \"app\"+strconv.Itoa(i)))\n\t\trequire.NoError(t, err)\n\t}\n\n\toriginalTerragruntConfigPath := filepath.Join(testFixtureParallelStateInit, \"root.hcl\")\n\ttmpTerragruntConfigFile := filepath.Join(tmpEnvPath, \"root.hcl\")\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(helpers.UniqueID())\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, originalTerragruntConfigPath, tmpTerragruntConfigFile, s3BucketName, lockTableName, \"us-east-2\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all --backend-bootstrap --non-interactive --working-dir \"+tmpEnvPath+\" -- apply -auto-approve\")\n}\n\nfunc TestAwsAssumeRole(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureAssumeRole)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureAssumeRole)\n\n\toriginalTerragruntConfigPath := filepath.Join(testFixtureAssumeRole, \"terragrunt.hcl\")\n\ttmpTerragruntConfigFile := filepath.Join(testPath, \"terragrunt.hcl\")\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(helpers.UniqueID())\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, originalTerragruntConfigPath, tmpTerragruntConfigFile, s3BucketName, lockTableName, \"us-east-2\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt hcl validate --inputs -auto-approve --non-interactive --working-dir \"+testPath)\n\n\t// validate generated backend.tf (now in .terragrunt-cache)\n\tcacheDir := helpers.FindCacheWorkingDir(t, testPath)\n\trequire.NotEmpty(t, cacheDir, \"Cache directory should exist\")\n\tbackendFile := filepath.Join(cacheDir, \"backend.tf\")\n\tassert.FileExists(t, backendFile)\n\n\tcontent, err := files.ReadFileAsString(backendFile)\n\trequire.NoError(t, err)\n\n\topts, err := options.NewTerragruntOptionsForTest(testPath)\n\trequire.NoError(t, err)\n\n\tl := logger.CreateLogger()\n\n\tcfg, err := awshelper.NewAWSConfigBuilder().\n\t\tWithEnv(opts.Env).\n\t\tWithIAMRoleOptions(opts.IAMRoleOptions).\n\t\tBuild(t.Context(), l)\n\trequire.NoError(t, err)\n\n\tidentityARN, err := awshelper.GetAWSIdentityArn(t.Context(), &cfg)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, content, \"role_arn     = \\\"\"+identityARN+\"\\\"\")\n\tassert.Contains(t, content, \"external_id  = \\\"external_id_123\\\"\")\n\tassert.Contains(t, content, \"session_name = \\\"session_name_example\\\"\")\n}\n\nfunc TestAwsAssumeRoleWithExternalIDWithComma(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureAssumeRoleWithExternalIDWithComma)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureAssumeRoleWithExternalIDWithComma)\n\n\toriginalTerragruntConfigPath := filepath.Join(testFixtureAssumeRoleWithExternalIDWithComma, \"terragrunt.hcl\")\n\ttmpTerragruntConfigFile := filepath.Join(testPath, \"terragrunt.hcl\")\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(helpers.UniqueID())\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, originalTerragruntConfigPath, tmpTerragruntConfigFile, s3BucketName, lockTableName, \"us-east-2\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt hcl validate --inputs -auto-approve --non-interactive --working-dir \"+testPath)\n\n\t// validate generated backend.tf (now in .terragrunt-cache)\n\tcacheDir := helpers.FindCacheWorkingDir(t, testPath)\n\trequire.NotEmpty(t, cacheDir, \"Cache directory should exist\")\n\tbackendFile := filepath.Join(cacheDir, \"backend.tf\")\n\tassert.FileExists(t, backendFile)\n\n\tcontent, err := files.ReadFileAsString(backendFile)\n\trequire.NoError(t, err)\n\n\topts, err := options.NewTerragruntOptionsForTest(testPath)\n\trequire.NoError(t, err)\n\n\tl := logger.CreateLogger()\n\n\tcfg, err := awshelper.NewAWSConfigBuilder().\n\t\tWithEnv(opts.Env).\n\t\tWithIAMRoleOptions(opts.IAMRoleOptions).\n\t\tBuild(t.Context(), l)\n\trequire.NoError(t, err)\n\n\tidentityARN, err := awshelper.GetAWSIdentityArn(t.Context(), &cfg)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, content, \"role_arn     = \\\"\"+identityARN+\"\\\"\")\n\tassert.Contains(t, content, \"external_id  = \\\"external_id_123,external_id_456\\\"\")\n\tassert.Contains(t, content, \"session_name = \\\"session_name_example\\\"\")\n}\n\nfunc TestAwsInitConfirmation(t *testing.T) {\n\tt.Parallel()\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutputAll)\n\n\trootTerragruntConfigPath := filepath.Join(tmpEnvPath, testFixtureOutputAll, \"root.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, \"not-used\", \"not-used\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt run --backend-bootstrap --all init --working-dir \"+tmpEnvPath, &stdout, &stderr)\n\t// Expected to fail with EOF since there's no stdin to respond to the confirmation prompt\n\trequire.Error(t, err)\n\n\terrout := stderr.String()\n\tassert.Equal(t, 1, strings.Count(errout, \"does not exist or you don't have permissions to access it. Would you like Terragrunt to create it? (y/n)\"))\n}\n\nfunc TestAwsRunAllCommandPrompt(t *testing.T) {\n\tt.Parallel()\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutputAll)\n\n\trootTerragruntConfigPath := filepath.Join(tmpEnvPath, testFixtureOutputAll, \"root.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, \"not-used\", \"not-used\")\n\n\tenvironmentPath := fmt.Sprintf(\"%s/%s/env1\", tmpEnvPath, testFixtureOutputAll)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt run --all apply --working-dir \"+environmentPath, &stdout, &stderr)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"stderr\")\n\tassert.Contains(t, stderr.String(), \"Are you sure you want to run 'terragrunt apply' in each unit of the run queue displayed above? (y/n)\")\n\trequire.Error(t, err)\n}\n\nfunc TestAwsReadTerragruntAuthProviderCmd(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureAuthProviderCmd)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureAuthProviderCmd)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureAuthProviderCmd, \"multiple-apps\")\n\tappPath := filepath.Join(rootPath, \"app1\")\n\tmockAuthCmd := filepath.Join(tmpEnvPath, testFixtureAuthProviderCmd, \"mock-auth-cmd.sh\")\n\n\thelpers.ValidateAuthProviderScript(t, appPath, mockAuthCmd)\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t`terragrunt run --all --non-interactive --working-dir %s --auth-provider-cmd %s -- apply -auto-approve`,\n\t\t\trootPath,\n\t\t\tmockAuthCmd,\n\t\t),\n\t)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt output -json --working-dir %s --auth-provider-cmd %s\",\n\t\t\tappPath,\n\t\t\tmockAuthCmd,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal([]byte(stdout), &outputs))\n\n\tassert.Equal(t, \"app1-bar\", outputs[\"foo-app1\"].Value)\n\tassert.Equal(t, \"app2-bar\", outputs[\"foo-app2\"].Value)\n\tassert.Equal(t, \"app3-bar\", outputs[\"foo-app3\"].Value)\n}\n\nfunc TestAwsReadTerragruntAuthProviderCmdWithSops(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureAuthProviderCmd)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureAuthProviderCmd)\n\tsopsPath := filepath.Join(tmpEnvPath, testFixtureAuthProviderCmd, \"sops\")\n\tmockAuthCmd := filepath.Join(tmpEnvPath, testFixtureAuthProviderCmd, \"mock-auth-cmd.sh\")\n\n\thelpers.ValidateAuthProviderScript(t, sopsPath, mockAuthCmd)\n\n\thelpers.RunTerragrunt(\n\t\tt, fmt.Sprintf(\n\t\t\t`terragrunt apply -auto-approve --non-interactive --working-dir %s --auth-provider-cmd %s`,\n\t\t\tsopsPath,\n\t\t\tmockAuthCmd,\n\t\t),\n\t)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt output -json --working-dir %s --auth-provider-cmd %s\",\n\t\t\tsopsPath,\n\t\t\tmockAuthCmd,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal([]byte(stdout), &outputs))\n\n\tassert.Equal(t, \"Welcome to SOPS! Edit this file as you please!\", outputs[\"hello\"].Value)\n}\n\nfunc TestAwsReadTerragruntConfigIamRole(t *testing.T) {\n\tt.Parallel()\n\n\tl := logger.CreateLogger()\n\n\tcfg, err := awshelper.NewAWSConfigBuilder().Build(t.Context(), l)\n\trequire.NoError(t, err)\n\n\tidentityArn, err := awshelper.GetAWSIdentityArn(t.Context(), &cfg)\n\trequire.NoError(t, err)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureReadIamRole)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureReadIamRole)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\t// Execution outputs to be verified\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\t// Invoke terragrunt and verify used IAM role\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt init --working-dir \"+rootPath, &stdout, &stderr)\n\n\t// Since are used not existing AWS accounts, for validation are used success and error outputs\n\toutput := fmt.Sprintf(\"%v %v %v\", stderr.String(), stdout.String(), err.Error())\n\n\t// Check that output contains value defined in IAM role\n\tassert.Contains(t, output, \"666666666666\")\n\t// Ensure that state file wasn't created with default IAM value\n\tassert.True(t, util.FileNotExists(filepath.Join(rootPath, identityArn+\".txt\")))\n}\n\nfunc TestTerragruntWorksWithIncludeShallowMerge(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, includeFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, includeFixturePath)\n\tchildPath := filepath.Join(rootPath, includeShallowFixturePath)\n\thelpers.CleanupTerraformFolder(t, childPath)\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\ttmpTerragruntConfigPath := helpers.CreateTmpTerragruntConfigWithParentAndChild(t, rootPath, includeShallowFixturePath, s3BucketName, \"root.hcl\", config.DefaultTerragruntConfigPath)\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --config %s --working-dir %s\", tmpTerragruntConfigPath, childPath))\n\tvalidateIncludeRemoteStateReflection(t, s3BucketName, includeShallowFixturePath, tmpTerragruntConfigPath, childPath)\n}\n\nfunc TestTerragruntWorksWithIncludeNoMerge(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, includeFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, includeFixturePath)\n\tchildPath := filepath.Join(rootPath, includeNoMergeFixturePath)\n\thelpers.CleanupTerraformFolder(t, childPath)\n\n\t// We deliberately pick an s3 bucket name that is invalid, as we don't expect to create this s3 bucket.\n\ts3BucketName := \"__INVALID_NAME__\"\n\n\ttmpTerragruntConfigPath := helpers.CreateTmpTerragruntConfigWithParentAndChild(t, rootPath, includeNoMergeFixturePath, s3BucketName, \"root.hcl\", config.DefaultTerragruntConfigPath)\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --config %s --working-dir %s\", tmpTerragruntConfigPath, childPath))\n\tvalidateIncludeRemoteStateReflection(t, s3BucketName, includeNoMergeFixturePath, tmpTerragruntConfigPath, childPath)\n}\n\nfunc TestErrorExplaining(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureInitError)\n\tinitTestCase := filepath.Join(tmpEnvPath, testFixtureInitError)\n\n\thelpers.CleanupTerraformFolder(t, initTestCase)\n\thelpers.CleanupTerragruntFolder(t, initTestCase)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt init -no-color --tf-forward-stdout --non-interactive --working-dir \"+initTestCase, &stdout, &stderr)\n\trequire.Error(t, err)\n\n\texplanation := shell.ExplainError(err)\n\tassert.Contains(t, explanation, \"Check your credentials and permissions\")\n}\n\nfunc TestTerragruntInvokeTerraformTests(t *testing.T) {\n\tt.Parallel()\n\n\tif isTerraform() {\n\t\tt.Skip(\"Not compatible with Terraform 1.5.x\")\n\t\treturn\n\t}\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureTfTest)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureTfTest)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt test --non-interactive --tf-forward-stdout --working-dir \"+testPath)\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"1 passed, 0 failed\")\n}\n\nfunc dependencyOutputOptimizationTest(t *testing.T, moduleName string, forceInit bool, expectedOutputLogs []string) {\n\tt.Helper()\n\n\texpectedOutput := `They said, \"No, The answer is 42\"`\n\tgeneratedUniqueID := helpers.UniqueID()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, moduleName)\n\trootTerragruntConfigPath := filepath.Join(rootPath, \"root.hcl\")\n\tlivePath := filepath.Join(rootPath, \"live\")\n\tdeepDepPath := filepath.Join(rootPath, \"deepdep\")\n\tdepPath := filepath.Join(rootPath, \"dep\")\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(generatedUniqueID)\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(generatedUniqueID)\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\tdefer cleanupTableForTest(t, lockTableName, helpers.TerraformRemoteStateS3Region)\n\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, lockTableName, helpers.TerraformRemoteStateS3Region)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --backend-bootstrap --working-dir \"+rootPath)\n\n\t// verify expected output\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+livePath)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal([]byte(stdout), &outputs))\n\tassert.Equal(t, expectedOutput, outputs[\"output\"].Value)\n\n\t// If we want to force reinit, delete the relevant .terraform directories\n\t// Since terraform runs from cache, clean the cache directory\n\tif forceInit {\n\t\tdepCacheDir := helpers.FindCacheWorkingDir(t, depPath)\n\t\trequire.NotEmpty(t, depCacheDir, \"Cache directory for dep should exist\")\n\t\thelpers.CleanupTerraformFolder(t, depCacheDir)\n\t}\n\n\t// Now delete the deepdep state and verify still works\n\t// Since terraform runs from cache, the state file is in the cache directory\n\tdeepDepCacheDir := helpers.FindCacheWorkingDir(t, deepDepPath)\n\trequire.NotEmpty(t, deepDepCacheDir, \"Cache directory for deepdep should exist\")\n\trequire.NoError(t, os.Remove(filepath.Join(deepDepCacheDir, \"terraform.tfstate\")))\n\n\treout, reerr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --log-level debug --non-interactive --working-dir \"+livePath+\" -- output -no-color -json\",\n\t)\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, json.Unmarshal([]byte(reout), &outputs))\n\tassert.Equal(t, expectedOutput, outputs[\"output\"].Value)\n\n\tfor _, logRegexp := range expectedOutputLogs {\n\t\tassert.Regexp(t, logRegexp, reerr)\n\t}\n}\n\nfunc assertS3Tags(t *testing.T, expectedTags map[string]string, bucketName string, client *s3.Client) {\n\tt.Helper()\n\n\tctx := t.Context()\n\n\tvar in = s3.GetBucketTaggingInput{}\n\n\tin.Bucket = aws.String(bucketName)\n\n\tvar tags, err2 = client.GetBucketTagging(ctx, &in)\n\tif err2 != nil {\n\t\tt.Fatal(err2)\n\t}\n\n\tvar actualTags = make(map[string]string)\n\n\tfor _, element := range tags.TagSet {\n\t\tactualTags[*element.Key] = *element.Value\n\t}\n\n\tassert.Equal(t, expectedTags, actualTags, \"Did not find expected tags on s3 bucket.\")\n}\n\nfunc assertS3BucketVersioning(t *testing.T, bucketName string, versioning bool, client *s3.Client) {\n\tt.Helper()\n\n\tctx := t.Context()\n\tres, err := client.GetBucketVersioning(ctx, &s3.GetBucketVersioningInput{Bucket: aws.String(bucketName)})\n\trequire.NoError(t, err)\n\trequire.NotNil(t, res)\n\n\tif versioning {\n\t\trequire.NotNil(t, res.Status)\n\t\tassert.Equal(t, s3types.BucketVersioningStatusEnabled, res.Status, \"Versioning is not enabled for the remote state S3 bucket %s\", bucketName)\n\t} else {\n\t\trequire.Empty(t, res.Status, \"Versioning should be disabled for the remote state S3 bucket %s\", bucketName)\n\t}\n}\n\n// Check that the DynamoDB table of the given name and region exists. Terragrunt should create this table during the test.\n// Also check if table got tagged properly\nfunc validateDynamoDBTableExistsAndIsTaggedAndIsSSEncrypted(t *testing.T, awsRegion string, tableName string, expectedTags map[string]string, expectedSSEncrypted bool) {\n\tt.Helper()\n\n\tclient := helpers.CreateDynamoDBClientForTest(t, awsRegion, \"\", \"\")\n\n\tctx := t.Context()\n\tdescription, err := client.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName: aws.String(tableName)})\n\trequire.NoError(t, err, \"DynamoDB table %s does not exist\", tableName)\n\n\tif expectedSSEncrypted {\n\t\trequire.NotNil(t, description.Table.SSEDescription)\n\t\tassert.Equal(t, types.SSEStatusEnabled, description.Table.SSEDescription.Status)\n\t} else {\n\t\trequire.Nil(t, description.Table.SSEDescription)\n\t}\n\n\ttags, err := client.ListTagsOfResource(ctx, &dynamodb.ListTagsOfResourceInput{ResourceArn: description.Table.TableArn})\n\trequire.NoError(t, err)\n\n\tif expectedTags == nil {\n\t\treturn\n\t}\n\n\tvar actualTags = make(map[string]string)\n\n\tfor _, element := range tags.Tags {\n\t\tactualTags[*element.Key] = *element.Value\n\t}\n\n\tassert.Equal(t, expectedTags, actualTags, \"Did not find expected tags on dynamo table.\")\n}\n\nfunc doesDynamoDBTableItemExist(t *testing.T, awsRegion string, tableName, key string) bool {\n\tt.Helper()\n\n\tclient := helpers.CreateDynamoDBClientForTest(t, awsRegion, \"\", \"\")\n\n\tctx := t.Context()\n\t_, err := client.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName: aws.String(tableName)})\n\trequire.NoError(t, err, \"DynamoDB table %s does not exist\", tableName)\n\n\tinput := &dynamodb.GetItemInput{\n\t\tTableName: aws.String(tableName),\n\t\tKey: map[string]types.AttributeValue{\n\t\t\t\"LockID\": &types.AttributeValueMemberS{\n\t\t\t\tValue: key,\n\t\t\t},\n\t\t},\n\t}\n\n\tres, err := client.GetItem(ctx, input)\n\trequire.NoError(t, err)\n\n\texists := len(res.Item) != 0\n\n\treturn exists\n}\n\n// Check that the S3 Bucket of the given name and region exists. Terragrunt should create this bucket during the test.\n// Also check if bucket got tagged properly and that public access is disabled completely.\nfunc validateS3BucketExistsAndIsTaggedAndVersioning(t *testing.T, awsRegion string, bucketName string, versioning bool, expectedTags map[string]string) {\n\tt.Helper()\n\n\tclient := helpers.CreateS3ClientForTest(t, awsRegion)\n\n\tctx := t.Context()\n\n\t// Use the AWS SDK waiter to handle S3 eventual consistency.\n\t// Newly created buckets may not be immediately visible to subsequent API calls.\n\twaiter := s3.NewBucketExistsWaiter(client)\n\terr := waiter.Wait(ctx, &s3.HeadBucketInput{Bucket: aws.String(bucketName)}, 2*time.Minute)\n\trequire.NoError(t, err, \"S3 bucket %s does not exist\", bucketName)\n\n\tif expectedTags != nil {\n\t\tassertS3Tags(t, expectedTags, bucketName, client)\n\t}\n\n\tassertS3BucketVersioning(t, bucketName, versioning, client)\n\n\tassertS3PublicAccessBlocks(t, client, bucketName)\n}\n\nfunc doesS3BucketKeyExist(t *testing.T, awsRegion string, bucketName, key string) bool {\n\tt.Helper()\n\n\tclient := helpers.CreateS3ClientForTest(t, awsRegion)\n\n\tctx := t.Context()\n\n\t// Use the AWS SDK waiter to handle S3 eventual consistency.\n\twaiter := s3.NewBucketExistsWaiter(client)\n\terr := waiter.Wait(ctx, &s3.HeadBucketInput{Bucket: aws.String(bucketName)}, 2*time.Minute)\n\trequire.NoError(t, err, \"S3 bucket %s does not exist\", bucketName)\n\n\t_, err = client.HeadObject(ctx, &s3.HeadObjectInput{\n\t\tBucket: aws.String(bucketName),\n\t\tKey:    aws.String(key),\n\t})\n\tif err != nil {\n\t\tvar apiErr smithy.APIError\n\t\tif errors.As(err, &apiErr) {\n\t\t\tif apiErr.ErrorCode() == \"NotFound\" {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\n\t\trequire.NoError(t, err)\n\t}\n\n\treturn true\n}\n\nfunc assertS3PublicAccessBlocks(t *testing.T, client *s3.Client, bucketName string) {\n\tt.Helper()\n\n\tctx := t.Context()\n\tresp, err := client.GetPublicAccessBlock(\n\t\tctx, &s3.GetPublicAccessBlockInput{Bucket: aws.String(bucketName)},\n\t)\n\trequire.NoError(t, err)\n\n\tpublicAccessBlockConfig := resp.PublicAccessBlockConfiguration\n\tassert.True(t, aws.ToBool(publicAccessBlockConfig.BlockPublicAcls))\n\tassert.True(t, aws.ToBool(publicAccessBlockConfig.BlockPublicPolicy))\n\tassert.True(t, aws.ToBool(publicAccessBlockConfig.IgnorePublicAcls))\n\tassert.True(t, aws.ToBool(publicAccessBlockConfig.RestrictPublicBuckets))\n}\n\nfunc bucketEncryption(t *testing.T, awsRegion string, bucketName string) (*s3.GetBucketEncryptionOutput, error) {\n\tt.Helper()\n\n\tclient := helpers.CreateS3ClientForTest(t, awsRegion)\n\n\tctx := t.Context()\n\tinput := &s3.GetBucketEncryptionInput{Bucket: aws.String(bucketName)}\n\n\toutput, err := client.GetBucketEncryption(ctx, input)\n\tif err != nil {\n\t\t// TODO: Remove this lint suppression\n\t\treturn nil, nil //nolint:nilerr\n\t}\n\n\treturn output, nil\n}\n\n// createS3Bucket create test S3 bucket.\nfunc createS3Bucket(t *testing.T, awsRegion, bucketName string) {\n\tt.Helper()\n\n\tclient := helpers.CreateS3ClientForTest(t, awsRegion)\n\n\tt.Logf(\"Creating test s3 bucket %s\", bucketName)\n\n\tctx := t.Context()\n\n\tinput := &s3.CreateBucketInput{\n\t\tBucket: aws.String(bucketName),\n\t\tCreateBucketConfiguration: &s3types.CreateBucketConfiguration{\n\t\t\tLocationConstraint: s3types.BucketLocationConstraint(awsRegion),\n\t\t},\n\t}\n\n\t_, err := client.CreateBucket(ctx, input)\n\trequire.NoError(t, err, \"Failed to create S3 bucket\")\n}\n\nfunc deleteS3Bucket(t *testing.T, awsRegion, bucketName string) {\n\tt.Helper()\n\n\thelpers.DeleteS3Bucket(t, awsRegion, bucketName)\n}\n\nfunc cleanupTableForTest(t *testing.T, tableName string, awsRegion string) {\n\tt.Helper()\n\n\tclient := helpers.CreateDynamoDBClientForTest(t, awsRegion, \"\", \"\")\n\n\tt.Logf(\"Deleting test DynamoDB table %s\", tableName)\n\n\tctx := t.Context()\n\n\t_, err := client.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName: aws.String(tableName)})\n\tif err != nil {\n\t\tvar apiErr smithy.APIError\n\t\tif errors.As(err, &apiErr) && apiErr.ErrorCode() == \"ResourceNotFoundException\" {\n\t\t\tt.Logf(\"DynamoDB table %s does not exist\", tableName)\n\t\t\treturn\n\t\t}\n\n\t\tt.Errorf(\"Failed to describe DynamoDB table %s: %v\", tableName, err)\n\n\t\treturn\n\t}\n\n\tif _, err := client.DeleteTable(ctx, &dynamodb.DeleteTableInput{TableName: aws.String(tableName)}); err != nil {\n\t\tt.Errorf(\"Failed to delete DynamoDB table %s: %v\", tableName, err)\n\t}\n}\n\nfunc bucketPolicy(t *testing.T, awsRegion string, bucketName string) (*s3.GetBucketPolicyOutput, error) {\n\tt.Helper()\n\n\tclient := helpers.CreateS3ClientForTest(t, awsRegion)\n\n\tctx := t.Context()\n\n\tpolicyOutput, err := client.GetBucketPolicy(ctx, &s3.GetBucketPolicyInput{\n\t\tBucket: aws.String(bucketName),\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn policyOutput, nil\n}\n\n// createDynamoDBTableE creates a test DynamoDB table, and returns an error if the table creation fails.\nfunc createDynamoDBTableE(t *testing.T, awsRegion string, tableName string) error {\n\tt.Helper()\n\n\tclient := helpers.CreateDynamoDBClientForTest(t, awsRegion, \"\", \"\")\n\tctx := t.Context()\n\n\t_, err := client.CreateTable(ctx, &dynamodb.CreateTableInput{\n\t\tAttributeDefinitions: []types.AttributeDefinition{\n\t\t\t{\n\t\t\t\tAttributeName: aws.String(\"LockID\"),\n\t\t\t\tAttributeType: types.ScalarAttributeTypeS,\n\t\t\t},\n\t\t},\n\t\tKeySchema: []types.KeySchemaElement{\n\t\t\t{\n\t\t\t\tAttributeName: aws.String(\"LockID\"),\n\t\t\t\tKeyType:       types.KeyTypeHash,\n\t\t\t},\n\t\t},\n\t\tTableName: aws.String(tableName),\n\t\tProvisionedThroughput: &types.ProvisionedThroughput{\n\t\t\tReadCapacityUnits:  aws.Int64(1),\n\t\t\tWriteCapacityUnits: aws.Int64(1),\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Wait for table to be created\n\twaiter := dynamodb.NewTableExistsWaiter(client)\n\terr = waiter.Wait(ctx, &dynamodb.DescribeTableInput{TableName: aws.String(tableName)}, 5*time.Minute)\n\n\treturn err\n}\n\n// createDynamoDBTable creates a test DynamoDB table.\nfunc createDynamoDBTable(t *testing.T, awsRegion string, tableName string) {\n\tt.Helper()\n\n\terr := createDynamoDBTableE(t, awsRegion, tableName)\n\trequire.NoError(t, err)\n}\n\nfunc validateIncludeRemoteStateReflection(t *testing.T, s3BucketName string, keyPath string, configPath string, workingDir string) {\n\tt.Helper()\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, fmt.Sprintf(\"terragrunt output -no-color -json --non-interactive --config %s --working-dir %s\", configPath, workingDir), &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\tremoteStateOut := map[string]any{}\n\trequire.NoError(t, json.Unmarshal([]byte(outputs[\"reflect\"].Value.(string)), &remoteStateOut))\n\tassert.Equal(\n\t\tt,\n\t\tmap[string]any{\n\t\t\t\"backend\":                         \"s3\",\n\t\t\t\"disable_init\":                    false,\n\t\t\t\"disable_dependency_optimization\": false,\n\t\t\t\"generate\":                        nil,\n\t\t\t\"config\": map[string]any{\n\t\t\t\t\"encrypt\": true,\n\t\t\t\t\"bucket\":  s3BucketName,\n\t\t\t\t\"key\":     keyPath + \"/terraform.tfstate\",\n\t\t\t\t\"region\":  \"us-west-2\",\n\t\t\t},\n\t\t\t\"encryption\": nil,\n\t\t},\n\t\tremoteStateOut,\n\t)\n}\n"
  },
  {
    "path": "test/integration_awsgcp_test.go",
    "content": "//go:build awsgcp\n\npackage test_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureS3BackendMigrate = \"fixtures/s3-backend-migrate\"\n)\n\nfunc TestAwsGcpMigrateBetweenDifferentBackends(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureS3BackendMigrate)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureS3BackendMigrate)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureS3BackendMigrate)\n\n\ttestID := strings.ToLower(helpers.UniqueID())\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + testID\n\tdynamoDBName := \"terragrunt-test-dynamodb-\" + testID\n\tgcsBucketName := \"terragrunt-test-bucket-\" + testID\n\n\tproject := os.Getenv(\"GOOGLE_CLOUD_PROJECT\")\n\n\tunit1Path := filepath.Join(rootPath, \"unit1\")\n\tunit2Path := filepath.Join(rootPath, \"unit2\")\n\n\tdefer func() {\n\t\tdeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\t\tcleanupTableForTest(t, dynamoDBName, helpers.TerraformRemoteStateS3Region)\n\t\tdeleteGCSBucket(t, gcsBucketName)\n\t}()\n\n\tunit1ConfigPath := filepath.Join(unit1Path, \"terragrunt.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, unit1ConfigPath, unit1ConfigPath, s3BucketName, dynamoDBName, helpers.TerraformRemoteStateS3Region)\n\n\tunit2ConfigPath := filepath.Join(unit2Path, \"terragrunt.hcl\")\n\tcopyTerragruntGCSConfigAndFillPlaceholders(t, unit2ConfigPath, unit2ConfigPath, project, terraformRemoteStateGcpRegion, gcsBucketName)\n\n\t// Bootstrap backend and create remote state for unit1.\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run apply --backend-bootstrap --non-interactive --log-level debug --working-dir \"+unit1Path+\" -- -auto-approve\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"Changes to Outputs\")\n\n\tstdout, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run plan --backend-bootstrap --non-interactive --log-level debug --working-dir \"+unit2Path+\"\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"Changes to Outputs\")\n\n\t// Migrate remote state from unit1 to unit2.\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt backend migrate --log-level debug --working-dir \"+rootPath+\" unit1 unit2\")\n\trequire.NoError(t, err)\n\n\t// Run `tofu apply` for unit2 with migrated remote state from unit1.\n\n\tstdout, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run apply --backend-bootstrap --non-interactive --log-level debug --working-dir \"+unit2Path+\" -- -auto-approve\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"No changes\")\n}\n"
  },
  {
    "path": "test/integration_catalog_test.go",
    "content": "package test_test\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/catalog/tui/command\"\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog\"\n\t\"github.com/gruntwork-io/terragrunt/internal/services/catalog/module\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureCatalogLocalTemplate = \"fixtures/catalog/local-template\"\n)\n\nfunc TestCatalogGitRepoUpdate(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\n\t_, err := module.NewRepo(ctx, logger.CreateLogger(), \"github.com/gruntwork-io/terraform-fake-modules.git\", tempDir, false, false, \"\")\n\trequire.NoError(t, err)\n\n\t_, err = module.NewRepo(ctx, logger.CreateLogger(), \"github.com/gruntwork-io/terraform-fake-modules.git\", tempDir, false, false, \"\")\n\trequire.NoError(t, err)\n}\n\nfunc TestScaffoldGitRepo(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\n\trepo, err := module.NewRepo(ctx, logger.CreateLogger(), \"github.com/gruntwork-io/terraform-fake-modules.git\", tempDir, false, false, \"\")\n\trequire.NoError(t, err)\n\n\tmodules, err := repo.FindModules(ctx)\n\trequire.NoError(t, err)\n\tassert.Len(t, modules, 4)\n}\n\nfunc TestScaffoldGitModule(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\n\trepo, err := module.NewRepo(ctx, logger.CreateLogger(), \"https://github.com/gruntwork-io/terraform-fake-modules.git\", tempDir, false, false, \"\")\n\trequire.NoError(t, err)\n\n\tmodules, err := repo.FindModules(ctx)\n\trequire.NoError(t, err)\n\n\tvar auroraModule *module.Module\n\n\tfor _, m := range modules {\n\t\tif m.Title() == \"Terraform Fake AWS Aurora Module\" {\n\t\t\tauroraModule = m\n\t\t}\n\t}\n\n\tassert.NotNil(t, auroraModule)\n\n\ttestPath := helpers.TmpDirWOSymlinks(t)\n\topts, err := options.NewTerragruntOptionsForTest(testPath)\n\trequire.NoError(t, err)\n\n\topts.ScaffoldVars = []string{\"EnableRootInclude=false\"}\n\n\tsvc := catalog.NewCatalogService(opts).WithRepoURL(\"https://github.com/gruntwork-io/terraform-fake-modules.git\")\n\tcmd := command.NewScaffold(createLogger(), opts, svc, auroraModule)\n\terr = cmd.Run()\n\trequire.NoError(t, err)\n\n\tcfg := readConfig(t, opts)\n\tassert.NotEmpty(t, cfg.Inputs)\n\tassert.Len(t, cfg.Inputs, 1)\n\t_, found := cfg.Inputs[\"vpc_id\"]\n\tassert.True(t, found)\n\tassert.Contains(t, *cfg.Terraform.Source, \"git::https://github.com/gruntwork-io/terraform-fake-modules.git//modules/aws/aurora\")\n}\n\nfunc TestScaffoldGitModuleHttps(t *testing.T) {\n\tt.Parallel()\n\n\tctx := t.Context()\n\n\ttempDir := helpers.TmpDirWOSymlinks(t)\n\n\trepo, err := module.NewRepo(ctx, logger.CreateLogger(), \"https://github.com/gruntwork-io/terraform-fake-modules\", tempDir, false, false, \"\")\n\trequire.NoError(t, err)\n\n\tmodules, err := repo.FindModules(ctx)\n\trequire.NoError(t, err)\n\n\tvar auroraModule *module.Module\n\n\tfor _, m := range modules {\n\t\tif m.Title() == \"Terraform Fake AWS Aurora Module\" {\n\t\t\tauroraModule = m\n\t\t}\n\t}\n\n\tassert.NotNil(t, auroraModule)\n\n\ttestPath := helpers.TmpDirWOSymlinks(t)\n\topts, err := options.NewTerragruntOptionsForTest(testPath)\n\trequire.NoError(t, err)\n\n\topts.ScaffoldVars = []string{\"EnableRootInclude=false\"}\n\n\tsvc := catalog.NewCatalogService(opts).WithRepoURL(\"https://github.com/gruntwork-io/terraform-fake-modules\")\n\tcmd := command.NewScaffold(createLogger(), opts, svc, auroraModule)\n\terr = cmd.Run()\n\trequire.NoError(t, err)\n\n\tcfg := readConfig(t, opts)\n\tassert.NotEmpty(t, cfg.Inputs)\n\tassert.Len(t, cfg.Inputs, 1)\n\t_, found := cfg.Inputs[\"vpc_id\"]\n\tassert.True(t, found)\n\tassert.Contains(t, *cfg.Terraform.Source, \"git::https://github.com/gruntwork-io/terraform-fake-modules.git//modules/aws/aurora?ref=v0.0.5\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt init --non-interactive --working-dir \"+opts.WorkingDir)\n}\n\nfunc TestCatalogWithLocalDefaultTemplate(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCatalogLocalTemplate, \".boilerplate\")\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureCatalogLocalTemplate)\n\n\ttargetPath := filepath.Join(rootPath, \"app\")\n\tmoduleURL := \"github.com/gruntwork-io/terragrunt//test/fixtures/inputs\"\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt scaffold --non-interactive --working-dir \"+targetPath+\" \"+moduleURL,\n\t)\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\tassert.FileExists(t, filepath.Join(targetPath, \"terragrunt.hcl\"))\n\tassert.FileExists(t, filepath.Join(targetPath, \"custom-template.txt\"))\n\n\tcontent, err := util.ReadFileAsString(filepath.Join(targetPath, \"terragrunt.hcl\"))\n\trequire.NoError(t, err)\n\tassert.Contains(t, content, \"# Custom local template\")\n}\n\nfunc readConfig(t *testing.T, opts *options.TerragruntOptions) *config.TerragruntConfig {\n\tt.Helper()\n\n\tassert.FileExists(t, opts.WorkingDir+\"/terragrunt.hcl\")\n\n\topts, err := options.NewTerragruntOptionsForTest(filepath.Join(opts.WorkingDir, \"terragrunt.hcl\"))\n\trequire.NoError(t, err)\n\n\tl := logger.CreateLogger()\n\t_, pctx := configbridge.NewParsingContext(t.Context(), l, opts)\n\tcfg, err := config.ReadTerragruntConfig(t.Context(), l, pctx, config.DefaultParserOptions(l, opts.StrictControls))\n\trequire.NoError(t, err)\n\n\treturn cfg\n}\n"
  },
  {
    "path": "test/integration_common_test.go",
    "content": "// common integration test functions\npackage test_test\n\nimport (\n\t\"archive/zip\"\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\"io\"\n\t\"math/big\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/NYTimes/gziphandler\"\n)\n\nfunc createLogger() log.Logger {\n\tformatter := format.NewFormatter(format.NewKeyValueFormatPlaceholders())\n\tformatter.SetDisabledColors(true)\n\n\treturn log.New(log.WithLevel(log.DebugLevel), log.WithFormatter(formatter))\n}\n\nfunc testRunAllPlan(t *testing.T, tgArgs string, tfArgs string) (string, string, string, error) {\n\tt.Helper()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutDir)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureOutDir)\n\n\t// run plan with output directory\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terraform run --all --non-interactive --working-dir %s %s -- plan %s\", testPath, tgArgs, tfArgs))\n\n\treturn tmpEnvPath, stdout, stderr, err\n}\n\nfunc runNetworkMirrorServer(t *testing.T, ctx context.Context, urlPrefix, providerDir, token string) *url.URL {\n\tt.Helper()\n\n\tserverTLSConf, clientTLSConf := certSetup(t)\n\n\thttp.DefaultTransport = &http.Transport{\n\t\tTLSClientConfig: clientTLSConf,\n\t}\n\n\tmux := http.NewServeMux()\n\n\tfs := http.FileServer(http.Dir(providerDir))\n\n\twithGz := gziphandler.GzipHandler(http.StripPrefix(urlPrefix, fs))\n\n\tmux.HandleFunc(urlPrefix, func(resp http.ResponseWriter, req *http.Request) {\n\t\tif token != \"\" {\n\t\t\tauthHeaders := req.Header.Values(\"Authorization\")\n\t\t\tassert.Contains(t, authHeaders, \"Bearer \"+token)\n\t\t}\n\n\t\twithGz.ServeHTTP(resp, req)\n\t})\n\n\tln, err := tls.Listen(\"tcp\", \"localhost:8888\", serverTLSConf)\n\trequire.NoError(t, err)\n\n\tserver := &http.Server{\n\t\tAddr:    ln.Addr().String(),\n\t\tHandler: mux,\n\t}\n\n\tgo func() {\n\t\tserver.Serve(ln)\n\t}()\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tserver.Shutdown(ctx)\n\t}()\n\n\treturn &url.URL{\n\t\tScheme: \"https\",\n\t\tHost:   ln.Addr().String(),\n\t\tPath:   urlPrefix,\n\t}\n}\n\n// runMockRegistryWithAbsoluteModuleURL runs a mock Terraform registry server that returns\n// an absolute URL for modules.v1 in its .well-known/terraform.json discovery response.\n// This is used to test the fix for https://github.com/gruntwork-io/terragrunt/issues/5156\nfunc runMockRegistryWithAbsoluteModuleURL(t *testing.T, ctx context.Context, providerDir, absoluteModulesURL string) *url.URL {\n\tt.Helper()\n\n\tserverTLSConf, clientTLSConf := certSetup(t)\n\n\thttp.DefaultTransport = &http.Transport{\n\t\tTLSClientConfig: clientTLSConf,\n\t}\n\n\tmux := http.NewServeMux()\n\n\tmux.HandleFunc(\"/.well-known/terraform.json\", func(resp http.ResponseWriter, req *http.Request) {\n\t\tdiscovery := map[string]string{\n\t\t\t\"modules.v1\":   absoluteModulesURL,\n\t\t\t\"providers.v1\": \"/v1/providers/\",\n\t\t}\n\n\t\tresp.Header().Set(\"Content-Type\", \"application/json\")\n\n\t\tif err := json.NewEncoder(resp).Encode(discovery); err != nil {\n\t\t\tt.Logf(\"Error encoding discovery response: %v\", err)\n\t\t\thttp.Error(resp, err.Error(), http.StatusInternalServerError)\n\t\t}\n\t})\n\n\tfs := http.FileServer(http.Dir(providerDir))\n\tmux.Handle(\"/v1/providers/\", http.StripPrefix(\"/v1/providers/\", fs))\n\n\tln, err := tls.Listen(\"tcp\", \"localhost:0\", serverTLSConf)\n\trequire.NoError(t, err)\n\n\tserver := &http.Server{\n\t\tAddr:    ln.Addr().String(),\n\t\tHandler: mux,\n\t}\n\n\tgo func() {\n\t\tif err := server.Serve(ln); err != nil && err != http.ErrServerClosed {\n\t\t\tt.Logf(\"Server error: %v\", err)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tserver.Shutdown(ctx) //nolint:errcheck\n\t}()\n\n\treturn &url.URL{\n\t\tScheme: \"https\",\n\t\tHost:   ln.Addr().String(),\n\t}\n}\n\ntype FakeProvider struct {\n\tRegistryName string\n\tNamespace    string\n\tName         string\n\tVersion      string\n\tPlatformOS   string\n\tPlatformArch string\n}\n\nfunc (provider *FakeProvider) archiveName() string {\n\treturn fmt.Sprintf(\"terraform-provider-%s_%s_%s_%s.zip\", provider.Name, provider.Version, provider.PlatformOS, provider.PlatformArch)\n}\n\nfunc (provider *FakeProvider) filename() string {\n\treturn fmt.Sprintf(\"terraform-provider-%s_v%s_x5\", provider.Name, provider.Version)\n}\n\nfunc (provider *FakeProvider) CreateMirror(t *testing.T, rootDir string) {\n\tt.Helper()\n\n\tproviderDir := filepath.Join(rootDir, provider.RegistryName, provider.Namespace, provider.Name)\n\n\terr := os.MkdirAll(providerDir, os.ModePerm)\n\trequire.NoError(t, err)\n\n\tprovider.createIndexJSON(t, providerDir)\n\tprovider.createVersionJSON(t, providerDir)\n\tprovider.createZipArchive(t, providerDir)\n}\n\nfunc (provider *FakeProvider) createVersionJSON(t *testing.T, providerDir string) {\n\tt.Helper()\n\n\ttype VersionProvider struct {\n\t\tURL    string   `json:\"url\"`\n\t\tHashes []string `json:\"hashes\"`\n\t}\n\n\ttype Version struct {\n\t\tArchives map[string]VersionProvider `json:\"archives\"`\n\t}\n\n\tversion := &Version{Archives: make(map[string]VersionProvider)}\n\tfilename := filepath.Join(providerDir, provider.Version+\".json\")\n\tplatform := fmt.Sprintf(\"%s_%s\", provider.PlatformOS, provider.PlatformArch)\n\n\tunmarshalFile(t, filename, version)\n\tversion.Archives[platform] = VersionProvider{URL: provider.archiveName()}\n\tmarshalFile(t, filename, version)\n}\n\nfunc (provider *FakeProvider) createIndexJSON(t *testing.T, providerDir string) {\n\tt.Helper()\n\n\ttype Index struct {\n\t\tVersions map[string]any `json:\"versions\"`\n\t}\n\n\tindex := &Index{Versions: make(map[string]any)}\n\tfilename := filepath.Join(providerDir, \"index.json\")\n\n\tunmarshalFile(t, filename, index)\n\tindex.Versions[provider.Version] = struct{}{}\n\tmarshalFile(t, filename, index)\n}\n\nfunc (provider *FakeProvider) createZipArchive(t *testing.T, providerDir string) {\n\tt.Helper()\n\n\tfile, err := os.Create(filepath.Join(providerDir, provider.filename()))\n\trequire.NoError(t, err)\n\n\tdefer func() {\n\t\tfile.Close()\n\t\trequire.NoError(t, os.Remove(filepath.Join(providerDir, provider.filename())))\n\t}()\n\n\terr = file.Truncate(1e7)\n\trequire.NoError(t, err)\n\n\terr = file.Sync()\n\trequire.NoError(t, err)\n\n\tzipFile, err := os.Create(filepath.Join(providerDir, provider.archiveName()))\n\trequire.NoError(t, err)\n\n\tdefer zipFile.Close()\n\n\tzipWriter := zip.NewWriter(zipFile)\n\tdefer zipWriter.Close()\n\n\tfileInfo, err := file.Stat()\n\trequire.NoError(t, err)\n\n\theader, err := zip.FileInfoHeader(fileInfo)\n\trequire.NoError(t, err)\n\n\theader.Method = zip.Deflate\n\theader.Name = provider.filename()\n\n\theaderWriter, err := zipWriter.CreateHeader(header)\n\trequire.NoError(t, err)\n\n\t_, err = io.Copy(headerWriter, file)\n\trequire.NoError(t, err)\n}\n\nfunc unmarshalFile(t *testing.T, filename string, dest any) {\n\tt.Helper()\n\n\tif !util.FileExists(filename) {\n\t\treturn\n\t}\n\n\tdata, err := os.ReadFile(filename)\n\trequire.NoError(t, err)\n\terr = json.Unmarshal(data, dest)\n\trequire.NoError(t, err)\n}\n\nfunc marshalFile(t *testing.T, filename string, dest any) {\n\tt.Helper()\n\n\tdata, err := json.Marshal(dest)\n\trequire.NoError(t, err)\n\terr = os.WriteFile(filename, data, 0666)\n\trequire.NoError(t, err)\n}\n\nfunc certSetup(t *testing.T) (*tls.Config, *tls.Config) {\n\tt.Helper()\n\n\t// set up our CA certificate\n\tserialNumber, err := strconv.ParseInt(time.Now().Format(\"20060102150405\"), 10, 64)\n\trequire.NoError(t, err)\n\n\tca := &x509.Certificate{\n\t\tSerialNumber: big.NewInt(serialNumber),\n\t\tSubject: pkix.Name{\n\t\t\tOrganization:  []string{\"Company, INC.\"},\n\t\t\tCountry:       []string{\"US\"},\n\t\t\tProvince:      []string{\"\"},\n\t\t\tLocality:      []string{\"San Francisco\"},\n\t\t\tStreetAddress: []string{\"Golden Gate Bridge\"},\n\t\t\tPostalCode:    []string{\"94016\"},\n\t\t},\n\t\tNotBefore:             time.Now(),\n\t\tNotAfter:              time.Now().AddDate(10, 0, 0),\n\t\tIsCA:                  true,\n\t\tExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},\n\t\tKeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,\n\t\tBasicConstraintsValid: true,\n\t}\n\n\t// create our private and public key\n\tcaPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)\n\trequire.NoError(t, err)\n\n\t// create the CA\n\tcaBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey)\n\trequire.NoError(t, err)\n\n\t// pem encode\n\tcaPEM := new(bytes.Buffer)\n\tpem.Encode(caPEM, &pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: caBytes,\n\t})\n\n\tcaPrivKeyPEM := new(bytes.Buffer)\n\tpem.Encode(caPrivKeyPEM, &pem.Block{\n\t\tType:  \"RSA PRIVATE KEY\",\n\t\tBytes: x509.MarshalPKCS1PrivateKey(caPrivKey),\n\t})\n\n\t// set up our server certificate\n\tcert := &x509.Certificate{\n\t\tSerialNumber: big.NewInt(serialNumber),\n\t\tSubject: pkix.Name{\n\t\t\tOrganization:  []string{\"Company, INC.\"},\n\t\t\tCountry:       []string{\"US\"},\n\t\t\tProvince:      []string{\"\"},\n\t\t\tLocality:      []string{\"San Francisco\"},\n\t\t\tStreetAddress: []string{\"Golden Gate Bridge\"},\n\t\t\tPostalCode:    []string{\"94016\"},\n\t\t},\n\t\tIPAddresses:  []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},\n\t\tNotBefore:    time.Now(),\n\t\tNotAfter:     time.Now().AddDate(10, 0, 0),\n\t\tSubjectKeyId: []byte{1, 2, 3, 4, 6},\n\t\tExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},\n\t\tKeyUsage:     x509.KeyUsageDigitalSignature,\n\t}\n\n\tcertPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)\n\trequire.NoError(t, err)\n\n\tcertBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey)\n\trequire.NoError(t, err)\n\n\tcertPEM := new(bytes.Buffer)\n\tpem.Encode(certPEM, &pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: certBytes,\n\t})\n\n\tcertPrivKeyPEM := new(bytes.Buffer)\n\tpem.Encode(certPrivKeyPEM, &pem.Block{\n\t\tType:  \"RSA PRIVATE KEY\",\n\t\tBytes: x509.MarshalPKCS1PrivateKey(certPrivKey),\n\t})\n\n\tserverCert, err := tls.X509KeyPair(certPEM.Bytes(), certPrivKeyPEM.Bytes())\n\trequire.NoError(t, err)\n\n\tserverTLSConf := &tls.Config{\n\t\tCertificates: []tls.Certificate{serverCert},\n\t}\n\n\tcertpool := x509.NewCertPool()\n\tcertpool.AppendCertsFromPEM(caPEM.Bytes())\n\tclientTLSConf := &tls.Config{\n\t\tRootCAs:            certpool,\n\t\tInsecureSkipVerify: true,\n\t}\n\n\treturn serverTLSConf, clientTLSConf\n}\n\nfunc validateOutput(t *testing.T, outputs map[string]helpers.TerraformOutput, key string, value any) {\n\tt.Helper()\n\n\toutput, hasPlatform := outputs[key]\n\tassert.Truef(t, hasPlatform, \"Expected output %s to be defined\", key)\n\tassert.Equalf(t, output.Value, value, \"Expected output %s to be %t\", key, value)\n}\n\n// wrappedBinary - return which binary will be wrapped by Terragrunt, useful in CICD to run same tests against tofu and terraform\nfunc wrappedBinary() string {\n\tvalue, found := os.LookupEnv(\"TG_TF_PATH\")\n\tif !found {\n\t\t// if env variable is not defined, try to check through executing command\n\t\tif util.IsCommandExecutable(context.Background(), helpers.TofuBinary, \"-version\") {\n\t\t\treturn helpers.TofuBinary\n\t\t}\n\n\t\treturn helpers.TerraformBinary\n\t}\n\n\treturn filepath.Base(value)\n}\n\nfunc isTerraform() bool {\n\treturn wrappedBinary() == helpers.TerraformBinary\n}\n\nfunc findFilesWithExtension(dir string, ext string) ([]string, error) {\n\tvar files []string\n\n\terr := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !info.IsDir() && filepath.Ext(path) == ext {\n\t\t\tfiles = append(files, path)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\treturn files, err\n}\n"
  },
  {
    "path": "test/integration_dag_test.go",
    "content": "package test_test\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst testFixtureGraphDAG = \"fixtures/dag-graph\"\n\nfunc TestDagGraphFlagsRegistration(t *testing.T) {\n\tt.Parallel()\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt dag graph -h\")\n\trequire.NoError(t, err)\n\tassert.Empty(t, stderr)\n\n\tassert.Contains(t, stdout, \"--queue-ignore-dag-order\", \"queue-ignore-dag-order flag should be present\")\n\tassert.Contains(t, stdout, \"--queue-ignore-errors\", \"queue-ignore-errors flag should be present\")\n}\n\nfunc TestIncludeExternalInDagGraphCmd(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGraphDAG)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGraphDAG)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\tworkDir := filepath.Join(rootPath, \"region-1\")\n\tworkDir, err := filepath.EvalSymlinks(workDir)\n\trequire.NoError(t, err)\n\n\tcmd := \"terragrunt dag graph --working-dir \" + workDir\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"unit-a\\\" ->\")\n}\n\nfunc TestIncludeExternalInDagGraphCmdWithList(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGraphDAG)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGraphDAG)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\tworkDir := filepath.Join(rootPath, \"region-1\")\n\tworkDir, err := filepath.EvalSymlinks(workDir)\n\trequire.NoError(t, err)\n\n\tcmd := \"terragrunt list --format=dot --dependencies --working-dir \" + workDir\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"unit-a\\\" ->\")\n}\n"
  },
  {
    "path": "test/integration_debug_test.go",
    "content": "package test_test\n\nimport (\n\t\"bytes\"\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/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tfixtureMultiIncludeDependency = \"fixtures/multiinclude-dependency\"\n\tfixtureRenderJSON             = \"fixtures/render-json\"\n\tfixtureRenderJSONRegression   = \"fixtures/render-json-regression\"\n)\n\nfunc TestDebugGeneratedInputs(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureInputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureInputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureInputs)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt plan --non-interactive --log-level debug --inputs-debug --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\t// Debug file is created in the original config directory\n\tdebugFile := filepath.Join(rootPath, helpers.TerragruntDebugFile)\n\tassert.True(t, util.FileExists(debugFile))\n\n\t// Find cache directory for running terraform\n\tcacheWorkingDir := helpers.FindCacheWorkingDir(t, rootPath)\n\trequire.NotEmpty(t, cacheWorkingDir, \"Should find cache working directory\")\n\n\t// If the debug file is generated correctly, we should be able to run terraform apply using the generated var file\n\t// without going through terragrunt.\n\tmockOptions, err := options.NewTerragruntOptionsForTest(\"integration_test\")\n\trequire.NoError(t, err)\n\n\tmockOptions.WorkingDir = cacheWorkingDir\n\n\tl := logger.CreateLogger()\n\n\trequire.NoError(\n\t\tt,\n\t\ttf.RunCommand(t.Context(), l, configbridge.TFRunOptsFromOpts(mockOptions), \"apply\", \"-auto-approve\", \"-var-file\", debugFile),\n\t)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal([]byte(stdout), &outputs))\n\tvalidateInputs(t, outputs)\n\n\t// Also make sure the undefined variable is not included in the json file\n\tdebugJSONContents, err := os.ReadFile(debugFile)\n\trequire.NoError(t, err)\n\n\tvar data map[string]any\n\trequire.NoError(t, json.Unmarshal(debugJSONContents, &data))\n\t_, isDefined := data[\"undefined_var\"]\n\tassert.False(t, isDefined)\n}\n\nfunc TestTerragruntInputsWithDashes(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureInputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureInputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureInputs)\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt init --working-dir=%s --log-level=debug\", rootPath))\n}\n\nfunc TestTerragruntValidateInputs(t *testing.T) {\n\tt.Parallel()\n\n\tmoduleDirs, err := filepath.Glob(filepath.Join(\"fixtures/validate-inputs\", \"*\"))\n\trequire.NoError(t, err)\n\n\tfor _, module := range moduleDirs {\n\t\tname := filepath.Base(module)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tnameDashSplit := strings.Split(name, \"-\")\n\t\t\thelpers.RunTerragruntValidateInputs(t, module, []string{\"--strict\"}, nameDashSplit[0] == \"success\")\n\t\t})\n\t}\n}\n\nfunc TestTerragruntValidateInputsWithCLIVars(t *testing.T) {\n\tt.Parallel()\n\n\tmoduleDir := filepath.Join(\"fixtures/validate-inputs\", \"fail-no-inputs\")\n\targs := []string{\"-var=input=from_env\"}\n\thelpers.RunTerragruntValidateInputs(t, moduleDir, args, true)\n}\n\nfunc TestTerragruntValidateInputsWithCLIVarFile(t *testing.T) {\n\tt.Parallel()\n\n\tcurdir, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\tmoduleDir := filepath.Join(\"fixtures/validate-inputs\", \"fail-no-inputs\")\n\targs := []string{fmt.Sprintf(\"-var-file=%s/fixtures/validate-inputs/success-var-file/varfiles/main.tfvars\", curdir)}\n\thelpers.RunTerragruntValidateInputs(t, moduleDir, args, true)\n}\n\nfunc TestTerragruntValidateInputsWithStrictMode(t *testing.T) {\n\tt.Parallel()\n\n\tmoduleDir := filepath.Join(\"fixtures/validate-inputs\", \"success-inputs-only\")\n\targs := []string{\"--strict-validate\"}\n\thelpers.RunTerragruntValidateInputs(t, moduleDir, args, true)\n}\n\nfunc TestTerragruntValidateInputsWithStrictModeDisabledAndUnusedVar(t *testing.T) {\n\tt.Parallel()\n\n\tmoduleDir := filepath.Join(\"fixtures/validate-inputs\", \"success-inputs-only\")\n\targs := []string{\"-var=testvariable=testvalue\"}\n\thelpers.RunTerragruntValidateInputs(t, moduleDir, args, true)\n}\n\nfunc TestTerragruntValidateInputsWithStrictModeEnabledAndUnusedVar(t *testing.T) {\n\tt.Parallel()\n\n\tmoduleDir := filepath.Join(\"fixtures/validate-inputs\", \"success-inputs-only\")\n\targs := []string{\"-var=testvariable=testvalue\", \"--strict\"}\n\thelpers.RunTerragruntValidateInputs(t, moduleDir, args, false)\n}\n\nfunc TestTerragruntValidateInputsWithStrictModeEnabledAndUnusedInputs(t *testing.T) {\n\tt.Parallel()\n\n\tmoduleDir := filepath.Join(\"fixtures/validate-inputs\", \"fail-unused-inputs\")\n\thelpers.CleanupTerraformFolder(t, moduleDir)\n\ttmpEnvPath, _ := filepath.EvalSymlinks(helpers.CopyEnvironment(t, moduleDir))\n\trootPath := filepath.Join(tmpEnvPath, moduleDir)\n\n\targs := []string{\"--strict\"}\n\thelpers.RunTerragruntValidateInputs(t, rootPath, args, false)\n}\n\nfunc TestTerragruntValidateInputsWithStrictModeDisabledAndUnusedInputs(t *testing.T) {\n\tt.Parallel()\n\n\tmoduleDir := filepath.Join(\"fixtures/validate-inputs\", \"fail-unused-inputs\")\n\thelpers.CleanupTerraformFolder(t, moduleDir)\n\ttmpEnvPath, _ := filepath.EvalSymlinks(helpers.CopyEnvironment(t, moduleDir))\n\trootPath := filepath.Join(tmpEnvPath, moduleDir)\n\n\targs := []string{}\n\thelpers.RunTerragruntValidateInputs(t, rootPath, args, true)\n}\n\nfunc TestRenderJSONConfig(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, fixtureRenderJSON)\n\ttmpEnvPath := helpers.CopyEnvironment(t, fixtureRenderJSON)\n\trootPath := filepath.Join(tmpEnvPath, fixtureRenderJSON)\n\n\tfixtureRenderJSONMainModulePath := filepath.Join(rootPath, \"main\")\n\tfixtureRenderJSONDepModulePath := filepath.Join(rootPath, \"dep\")\n\n\thelpers.CleanupTerraformFolder(t, fixtureRenderJSONMainModulePath)\n\thelpers.CleanupTerraformFolder(t, fixtureRenderJSONDepModulePath)\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\tjsonOut := filepath.Join(tmpDir, \"terragrunt.rendered.json\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- apply -auto-approve\")\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt render --json -w --non-interactive --working-dir %s --json-out %s\", fixtureRenderJSONMainModulePath, jsonOut))\n\n\tjsonBytes, err := os.ReadFile(jsonOut)\n\trequire.NoError(t, err)\n\n\t// clean jsonBytes to remove any trailing newlines\n\tcleanString := strings.TrimSpace(string(jsonBytes))\n\n\tvar rendered map[string]any\n\trequire.NoError(t, json.Unmarshal([]byte(cleanString), &rendered))\n\n\t// Make sure all terraform block is visible\n\tterraformBlock, hasTerraform := rendered[\"terraform\"]\n\tif assert.True(t, hasTerraform) {\n\t\tsource, hasSource := terraformBlock.(map[string]any)[\"source\"]\n\t\tassert.True(t, hasSource)\n\t\tassert.Equal(t, \"./module\", source)\n\t}\n\n\t// Make sure included remoteState is rendered out\n\tremoteState, hasRemoteState := rendered[\"remote_state\"]\n\tif assert.True(t, hasRemoteState) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"backend\": \"local\",\n\t\t\t\t\"generate\": map[string]any{\n\t\t\t\t\t\"path\":      \"backend.tf\",\n\t\t\t\t\t\"if_exists\": \"overwrite_terragrunt\",\n\t\t\t\t},\n\t\t\t\t\"config\": map[string]any{\n\t\t\t\t\t\"path\": \"foo.tfstate\",\n\t\t\t\t},\n\t\t\t\t\"disable_init\":                    false,\n\t\t\t\t\"encryption\":                      nil,\n\t\t\t\t\"disable_dependency_optimization\": false,\n\t\t\t},\n\t\t\tremoteState.(map[string]any),\n\t\t)\n\t}\n\n\t// Make sure dependency blocks are rendered out\n\tdependencyBlocks, hasDependency := rendered[\"dependency\"]\n\tif assert.True(t, hasDependency) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"dep\": map[string]any{\n\t\t\t\t\t\"name\":         \"dep\",\n\t\t\t\t\t\"config_path\":  \"../dep\",\n\t\t\t\t\t\"outputs\":      nil,\n\t\t\t\t\t\"inputs\":       nil,\n\t\t\t\t\t\"mock_outputs\": nil,\n\t\t\t\t\t\"enabled\":      nil,\n\t\t\t\t\t\"mock_outputs_allowed_terraform_commands\": nil,\n\t\t\t\t\t\"mock_outputs_merge_strategy_with_state\":  nil,\n\t\t\t\t\t\"mock_outputs_merge_with_state\":           nil,\n\t\t\t\t\t\"skip\":                                    nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tdependencyBlocks.(map[string]any),\n\t\t)\n\t}\n\n\t// Make sure included generate block is rendered out\n\tgenerateBlocks, hasGenerate := rendered[\"generate\"]\n\tif assert.True(t, hasGenerate) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"provider\": map[string]any{\n\t\t\t\t\t\"path\":              \"provider.tf\",\n\t\t\t\t\t\"comment_prefix\":    \"# \",\n\t\t\t\t\t\"disable_signature\": false,\n\t\t\t\t\t\"disable\":           false,\n\t\t\t\t\t\"if_exists\":         \"overwrite_terragrunt\",\n\t\t\t\t\t\"if_disabled\":       \"skip\",\n\t\t\t\t\t\"hcl_fmt\":           nil,\n\t\t\t\t\t\"contents\": `provider \"aws\" {\n  region = \"us-east-1\"\n}\n`,\n\t\t\t\t},\n\t\t\t},\n\t\t\tgenerateBlocks.(map[string]any),\n\t\t)\n\t}\n\n\t// Make sure all inputs are merged together\n\tinputsBlock, hasInputs := rendered[\"inputs\"]\n\tif assert.True(t, hasInputs) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"env\":        \"qa\",\n\t\t\t\t\"name\":       \"dep\",\n\t\t\t\t\"type\":       \"main\",\n\t\t\t\t\"aws_region\": \"us-east-1\",\n\t\t\t},\n\t\t\tinputsBlock.(map[string]any),\n\t\t)\n\t}\n}\n\nfunc TestRenderJSONConfigWithIncludesDependenciesAndLocals(t *testing.T) {\n\tt.Parallel()\n\n\t// This test is kind of wild. I don't know if it's worth keeping.\n\t// Removing it for now to avoid blocking the merge of #5477\n\t// which is more important.\n\t// TODO: Re-evaluate this test after #5477 is merged. See https://github.com/gruntwork-io/terragrunt/pull/5477\n\n\tt.Skip(\"Skipping this test to avoid blocking the merge of #5477. See https://github.com/gruntwork-io/terragrunt/pull/5477\")\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\tjsonOut := filepath.Join(tmpDir, \"terragrunt_rendered.json\")\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, fixtureRenderJSONRegression)\n\tworkDir := filepath.Join(tmpEnvPath, fixtureRenderJSONRegression)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all --non-interactive --working-dir \"+workDir+\" -- apply -auto-approve\")\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt render --json -w --non-interactive --working-dir %s --json-out \", workDir)+jsonOut)\n\n\tjsonBytes, err := os.ReadFile(jsonOut)\n\trequire.NoError(t, err)\n\n\tvar rendered map[string]any\n\trequire.NoError(t, json.Unmarshal(jsonBytes, &rendered))\n\n\t// Make sure all terraform block is visible\n\tterraformBlock, hasTerraform := rendered[\"terraform\"]\n\tif assert.True(t, hasTerraform) {\n\t\tsource, hasSource := terraformBlock.(map[string]any)[\"source\"]\n\t\tassert.True(t, hasSource)\n\t\tassert.Equal(t, \"./foo\", source)\n\t}\n\n\t// Make sure top level locals are rendered out\n\tlocals, hasLocals := rendered[\"locals\"]\n\tif assert.True(t, hasLocals) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t},\n\t\t\tlocals.(map[string]any),\n\t\t)\n\t}\n\n\t// Make sure included dependency block is rendered out, and with the outputs rendered\n\tdependencyBlocks, hasDependency := rendered[\"dependency\"]\n\tif assert.True(t, hasDependency) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"baz\": map[string]any{\n\t\t\t\t\t\"name\":         \"baz\",\n\t\t\t\t\t\"config_path\":  \"./baz\",\n\t\t\t\t\t\"outputs\":      nil,\n\t\t\t\t\t\"inputs\":       nil,\n\t\t\t\t\t\"mock_outputs\": nil,\n\t\t\t\t\t\"enabled\":      nil,\n\t\t\t\t\t\"mock_outputs_allowed_terraform_commands\": nil,\n\t\t\t\t\t\"mock_outputs_merge_strategy_with_state\":  nil,\n\t\t\t\t\t\"mock_outputs_merge_with_state\":           nil,\n\t\t\t\t\t\"skip\":                                    nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tdependencyBlocks.(map[string]any),\n\t\t)\n\t}\n\n\t// Make sure generate block is rendered out\n\tgenerateBlocks, hasGenerate := rendered[\"generate\"]\n\tif assert.True(t, hasGenerate) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"provider\": map[string]any{\n\t\t\t\t\t\"path\":              \"provider.tf\",\n\t\t\t\t\t\"comment_prefix\":    \"# \",\n\t\t\t\t\t\"disable_signature\": false,\n\t\t\t\t\t\"disable\":           false,\n\t\t\t\t\t\"if_exists\":         \"overwrite\",\n\t\t\t\t\t\"if_disabled\":       \"skip\",\n\t\t\t\t\t\"hcl_fmt\":           nil,\n\t\t\t\t\t\"contents\":          \"# This is just a test\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tgenerateBlocks.(map[string]any),\n\t\t)\n\t}\n\n\t// Make sure all inputs are merged together\n\tinputsBlock, hasInputs := rendered[\"inputs\"]\n\tif assert.True(t, hasInputs) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"foo\":       \"bar\",\n\t\t\t\t\"baz\":       \"blah\",\n\t\t\t\t\"another\":   \"baz\",\n\t\t\t\t\"from_root\": \"Hi\",\n\t\t\t},\n\t\t\tinputsBlock.(map[string]any),\n\t\t)\n\t}\n}\n\nfunc TestRenderJSONConfigRunAll(t *testing.T) {\n\tt.Parallel()\n\n\t// This test is kind of wild. I don't know if it's worth keeping.\n\t// Removing it for now to avoid blocking the merge of #5469\n\t// which is more important.\n\n\tt.Skip(\"Skipping this test to avoid blocking the merge of #5469\")\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, fixtureRenderJSONRegression)\n\tworkDir := filepath.Join(tmpEnvPath, fixtureRenderJSONRegression)\n\n\t// NOTE: bar is not rendered out because it is considered a parent terragrunt.hcl config.\n\n\tbazJSONOut := filepath.Join(workDir, \"baz\", \"terragrunt.rendered.json\")\n\trootChildJSONOut := filepath.Join(workDir, \"terragrunt.rendered.json\")\n\n\tdefer os.Remove(bazJSONOut)\n\tdefer os.Remove(rootChildJSONOut)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all --non-interactive --working-dir \"+workDir+\" -- apply -auto-approve\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt render --all --json -w --non-interactive --working-dir \"+workDir)\n\n\tbazJSONBytes, err := os.ReadFile(bazJSONOut)\n\trequire.NoError(t, err)\n\n\tvar bazRendered map[string]any\n\trequire.NoError(t, json.Unmarshal(bazJSONBytes, &bazRendered))\n\n\t// Make sure top level locals are rendered out\n\tbazLocals, bazHasLocals := bazRendered[\"locals\"]\n\tif assert.True(t, bazHasLocals) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"self\": \"baz\",\n\t\t\t},\n\t\t\tbazLocals.(map[string]any),\n\t\t)\n\t}\n\n\trootChildJSONBytes, err := os.ReadFile(rootChildJSONOut)\n\trequire.NoError(t, err)\n\n\tvar rootChildRendered map[string]any\n\trequire.NoError(t, json.Unmarshal(rootChildJSONBytes, &rootChildRendered))\n\n\t// Make sure top level locals are rendered out\n\trootChildLocals, rootChildHasLocals := rootChildRendered[\"locals\"]\n\tif assert.True(t, rootChildHasLocals) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t},\n\t\t\trootChildLocals.(map[string]any),\n\t\t)\n\t}\n}\n\nfunc TestRenderJSONConfigRunAllWithCLIRedesign(t *testing.T) {\n\tt.Parallel()\n\n\t// This test is kind of wild. I don't know if it's worth keeping.\n\t// Removing it for now to avoid blocking the merge of #5469\n\t// which is more important.\n\n\tt.Skip(\"Skipping this test to avoid blocking the merge of #5469\")\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, fixtureRenderJSONRegression)\n\tworkDir := filepath.Join(tmpEnvPath, fixtureRenderJSONRegression)\n\n\t// NOTE: bar is not rendered out because it is considered a parent terragrunt.hcl config.\n\n\tbazJSONOut := filepath.Join(workDir, \"baz\", \"terragrunt.rendered.json\")\n\trootChildJSONOut := filepath.Join(workDir, \"terragrunt.rendered.json\")\n\n\tdefer os.Remove(bazJSONOut)\n\tdefer os.Remove(rootChildJSONOut)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+workDir)\n\n\thelpers.RunTerragrunt(t, \"terragrunt render --all --json -w --non-interactive --working-dir \"+workDir)\n\n\tbazJSONBytes, err := os.ReadFile(bazJSONOut)\n\trequire.NoError(t, err)\n\n\tvar bazRendered map[string]any\n\trequire.NoError(t, json.Unmarshal(bazJSONBytes, &bazRendered))\n\n\t// Make sure top level locals are rendered out\n\tbazLocals, bazHasLocals := bazRendered[\"locals\"]\n\tif assert.True(t, bazHasLocals) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"self\": \"baz\",\n\t\t\t},\n\t\t\tbazLocals.(map[string]any),\n\t\t)\n\t}\n\n\trootChildJSONBytes, err := os.ReadFile(rootChildJSONOut)\n\trequire.NoError(t, err)\n\n\tvar rootChildRendered map[string]any\n\trequire.NoError(t, json.Unmarshal(rootChildJSONBytes, &rootChildRendered))\n\n\t// Make sure top level locals are rendered out\n\trootChildLocals, rootChildHasLocals := rootChildRendered[\"locals\"]\n\tif assert.True(t, rootChildHasLocals) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t},\n\t\t\trootChildLocals.(map[string]any),\n\t\t)\n\t}\n}\n\nfunc TestDependencyGraphWithMultiInclude(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, fixtureMultiIncludeDependency)\n\ttmpEnvPath := helpers.CopyEnvironment(t, fixtureMultiIncludeDependency)\n\trootPath := filepath.Join(tmpEnvPath, fixtureMultiIncludeDependency)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt dag graph --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\tstdoutStr := stdout.String()\n\n\tassert.Contains(t, stdoutStr, `\"main\" -> \"depa\";`)\n\tassert.Contains(t, stdoutStr, `\"main\" -> \"depb\";`)\n\tassert.Contains(t, stdoutStr, `\"main\" -> \"depc\";`)\n}\n"
  },
  {
    "path": "test/integration_deprecated_test.go",
    "content": "package test_test\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// This tests terragrunt properly passes through terraform commands with sub commands\n// and any number of specified args\nfunc TestDeprecatedDefaultCommand_TerraformSubcommandCliArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpected string\n\t\tcommand  []string\n\t}{\n\t\t{\n\t\t\tcommand:  []string{\"force-unlock\"},\n\t\t\texpected: wrappedBinary() + \" force-unlock\",\n\t\t},\n\t\t{\n\t\t\tcommand:  []string{\"force-unlock\", \"foo\"},\n\t\t\texpected: wrappedBinary() + \" force-unlock foo\",\n\t\t},\n\t\t{\n\t\t\tcommand:  []string{\"force-unlock\", \"foo\", \"bar\", \"baz\"},\n\t\t\texpected: wrappedBinary() + \" force-unlock foo bar baz\",\n\t\t},\n\t\t{\n\t\t\tcommand:  []string{\"force-unlock\", \"foo\", \"bar\", \"baz\", \"foobar\"},\n\t\t\texpected: wrappedBinary() + \" force-unlock foo bar baz foobar\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\ttofuCmd := strings.Join(tc.command, \" \")\n\n\t\tt.Run(tofuCmd, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureExtraArgsPath)\n\t\t\trootPath := filepath.Join(tmpEnvPath, testFixtureExtraArgsPath)\n\n\t\t\tcmd := fmt.Sprintf(\n\t\t\t\t\"terragrunt --log-level debug --non-interactive --working-dir %s -- %s\",\n\t\t\t\trootPath,\n\t\t\t\tstrings.Join(tc.command, \" \"),\n\t\t\t)\n\n\t\t\t// Call helpers.RunTerragruntCommand directly because this command\n\t\t\t// contains failures (which causes helpers.RunTerragruntRedirectOutput to abort) but we don't care.\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\t\t\trequire.Error(t, err)\n\n\t\t\tassert.True(t, strings.Contains(stderr, tc.expected) || strings.Contains(stdout, tc.expected))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/integration_destroy_test.go",
    "content": "package test_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureDestroyOrder                 = \"fixtures/destroy-order\"\n\ttestFixturePreventDestroyOverride       = \"fixtures/prevent-destroy-override/child\"\n\ttestFixturePreventDestroyNotSet         = \"fixtures/prevent-destroy-not-set/child\"\n\ttestFixtureDestroyWarning               = \"fixtures/destroy-warning\"\n\ttestFixtureDestroyDependentModule       = \"fixtures/destroy-dependent-module\"\n\ttestFixtureDestroyDependentModuleErrors = \"fixtures/destroy-dependent-module-errors\"\n)\n\nfunc TestTerragruntDestroyOrder(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureDestroyOrder)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDestroyOrder)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureDestroyOrder, \"app\")\n\t// Resolve symlinks to avoid path mismatches on macOS where /var -> /private/var\n\trootPath, err := filepath.EvalSymlinks(rootPath)\n\trequire.NoError(t, err)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// Run destroy with report file to capture run order\n\treportFile := filepath.Join(rootPath, \"report.json\")\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\"terragrunt run --all destroy --non-interactive --working-dir %s --report-file %s\", rootPath, reportFile),\n\t)\n\trequire.NoError(t, err)\n\n\t// Parse the report file to verify dependency order\n\truns, err := report.ParseJSONRunsFromFile(reportFile)\n\trequire.NoError(t, err)\n\n\t// Verify all modules are in the report\n\trunA := runs.FindByName(\"module-a\")\n\trunB := runs.FindByName(\"module-b\")\n\trunC := runs.FindByName(\"module-c\")\n\trunD := runs.FindByName(\"module-d\")\n\trunE := runs.FindByName(\"module-e\")\n\n\trequire.NotNil(t, runA, \"module-a should be in report\")\n\trequire.NotNil(t, runB, \"module-b should be in report\")\n\trequire.NotNil(t, runC, \"module-c should be in report\")\n\trequire.NotNil(t, runD, \"module-d should be in report\")\n\trequire.NotNil(t, runE, \"module-e should be in report\")\n\n\t// Module B depends on A, so B must be destroyed (start) before A\n\tassert.True(t, runB.Started.Before(runA.Started), \"Module B should start before Module A (B depends on A)\")\n\n\t// Module D depends on C, so D must be destroyed (start) before C\n\tassert.True(t, runD.Started.Before(runC.Started), \"Module D should start before Module C (D depends on C)\")\n}\n\nfunc TestTerragruntApplyDestroyOrder(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureDestroyOrder)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDestroyOrder)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureDestroyOrder, \"app\")\n\t// Resolve symlinks to avoid path mismatches on macOS where /var -> /private/var\n\trootPath, err := filepath.EvalSymlinks(rootPath)\n\trequire.NoError(t, err)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// Run destroy with report file to capture run order\n\treportFile := filepath.Join(rootPath, \"report.json\")\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --no-color --all --non-interactive --tf-forward-stdout --working-dir %s --report-file %s -- apply -destroy\",\n\t\t\trootPath,\n\t\t\treportFile,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\t// Parse the report file to verify dependency order\n\truns, err := report.ParseJSONRunsFromFile(reportFile)\n\trequire.NoError(t, err)\n\n\trunA := runs.FindByName(\"module-a\")\n\trunB := runs.FindByName(\"module-b\")\n\trunC := runs.FindByName(\"module-c\")\n\trunD := runs.FindByName(\"module-d\")\n\trunE := runs.FindByName(\"module-e\")\n\n\trequire.NotNil(t, runA, \"module-a should be in report\")\n\trequire.NotNil(t, runB, \"module-b should be in report\")\n\trequire.NotNil(t, runC, \"module-c should be in report\")\n\trequire.NotNil(t, runD, \"module-d should be in report\")\n\trequire.NotNil(t, runE, \"module-e should be in report\")\n\n\t// Module B depends on A, so B must be destroyed (start) before A\n\tassert.True(t, runB.Started.Before(runA.Started), \"Module B should be destroyed before Module A (B depends on A)\")\n\n\t// Module D depends on C, so D must be destroyed (start) before C\n\tassert.True(t, runD.Started.Before(runC.Started), \"Module D should be destroyed before Module C (D depends on C)\")\n}\n\n// TestTerragruntDestroyOrderWithQueueIgnoreErrors tests that --queue-ignore-errors still respects dependency order.\n// This is a regression test for issue #4947.\n// Note: This test verifies the behavior is the same with and without --queue-ignore-errors for successful runs.\n// The unit tests in internal/queue/queue_test.go provide comprehensive coverage of the ordering logic.\nfunc TestTerragruntDestroyOrderWithQueueIgnoreErrors(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureDestroyOrder)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDestroyOrder)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureDestroyOrder, \"app\")\n\t// Resolve symlinks to avoid path mismatches on macOS where /var -> /private/var\n\trootPath, err := filepath.EvalSymlinks(rootPath)\n\trequire.NoError(t, err)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// Run destroy with --queue-ignore-errors flag and capture results in a report file\n\t// The main difference with --queue-ignore-errors is that it allows progress even if dependencies fail,\n\t// but it should still respect the dependency order when dependencies are in terminal states.\n\treportFile := filepath.Join(rootPath, \"report.json\")\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\"terragrunt run --all destroy --non-interactive --queue-ignore-errors --working-dir %s --report-file %s\", rootPath, reportFile),\n\t)\n\trequire.NoError(t, err)\n\n\t// Parse the report file to verify dependency order\n\truns, err := report.ParseJSONRunsFromFile(reportFile)\n\trequire.NoError(t, err)\n\n\t// Verify all modules are in the report\n\trunA := runs.FindByName(\"module-a\")\n\trunB := runs.FindByName(\"module-b\")\n\trunC := runs.FindByName(\"module-c\")\n\trunD := runs.FindByName(\"module-d\")\n\trunE := runs.FindByName(\"module-e\")\n\n\trequire.NotNil(t, runA, \"module-a should be in report\")\n\trequire.NotNil(t, runB, \"module-b should be in report\")\n\trequire.NotNil(t, runC, \"module-c should be in report\")\n\trequire.NotNil(t, runD, \"module-d should be in report\")\n\trequire.NotNil(t, runE, \"module-e should be in report\")\n\n\t// Module B depends on A, so B must be destroyed (start) before A\n\tassert.True(t, runB.Started.Before(runA.Started), \"Module B should start before Module A (B depends on A)\")\n\n\t// Module D depends on C, so D must be destroyed (start) before C\n\tassert.True(t, runD.Started.Before(runC.Started), \"Module D should start before Module C (D depends on C)\")\n}\n\nfunc TestPreventDestroyOverride(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/prevent-destroy-override\")\n\trootPath := filepath.Join(tmpEnvPath, testFixturePreventDestroyOverride)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\trequire.NoError(t, helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --working-dir \"+rootPath, os.Stdout, os.Stderr))\n\trequire.NoError(t, helpers.RunTerragruntCommand(t, \"terragrunt destroy -auto-approve --working-dir \"+rootPath, os.Stdout, os.Stderr))\n}\n\nfunc TestPreventDestroyNotSet(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/prevent-destroy-not-set\")\n\trootPath := filepath.Join(tmpEnvPath, testFixturePreventDestroyNotSet)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\trequire.NoError(t, helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --working-dir \"+rootPath, os.Stdout, os.Stderr))\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt destroy -auto-approve --working-dir \"+rootPath, os.Stdout, os.Stderr)\n\n\tif assert.Error(t, err) {\n\t\tvar target run.ModuleIsProtected\n\t\tassert.ErrorAs(t, err, &target)\n\t}\n}\n\nfunc TestDestroyDependentModule(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureDestroyDependentModule)\n\ttmpEnvPath, _ := filepath.EvalSymlinks(helpers.CopyEnvironment(t, testFixtureDestroyDependentModule))\n\trootPath := filepath.Join(tmpEnvPath, testFixtureDestroyDependentModule)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(rootPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\t// apply each module in order\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\t\"terragrunt run --working-dir \"+filepath.Join(rootPath, \"a\")+\" -- apply -auto-approve\",\n\t)\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\t\"terragrunt run --working-dir \"+filepath.Join(rootPath, \"b\")+\" -- apply -auto-approve\",\n\t)\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\t\"terragrunt run --working-dir \"+filepath.Join(rootPath, \"c\")+\" -- apply -auto-approve\",\n\t)\n\n\t// destroy module which have outputs from other modules\n\tworkingDir := filepath.Join(rootPath, \"c\")\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --log-level debug --working-dir \"+workingDir+\" -- destroy -auto-approve\",\n\t)\n\trequire.NoError(t, err)\n\n\tfor _, path := range []string{\n\t\tfilepath.Join(rootPath, \"b\", \"terragrunt.hcl\"),\n\t\tfilepath.Join(rootPath, \"a\", \"terragrunt.hcl\"),\n\t} {\n\t\trelPath, err := filepath.Rel(workingDir, path)\n\t\trequire.NoError(t, err)\n\t\tassert.Contains(t, stderr, relPath, stderr)\n\t}\n\n\tassert.Contains(t, stderr, \"\\\"value\\\": \\\"module-b.txt\\\"\", stderr)\n\tassert.Contains(t, stderr, \"\\\"value\\\": \\\"module-a.txt\\\"\", stderr)\n}\n\nfunc TestShowWarningWithDependentModulesBeforeDestroy(t *testing.T) {\n\tt.Parallel()\n\n\trootPath := helpers.CopyEnvironment(t, testFixtureDestroyWarning)\n\n\trootPath = filepath.Join(rootPath, testFixtureDestroyWarning)\n\tvpcPath := filepath.Join(rootPath, \"vpc\")\n\tappV1Path := filepath.Join(rootPath, \"app-v1\")\n\tappV2Path := filepath.Join(rootPath, \"app-v2\")\n\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\thelpers.CleanupTerraformFolder(t, vpcPath)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt run --all init --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\t// try to destroy vpc module and check if warning is printed in output\n\tstdout = bytes.Buffer{}\n\tstderr = bytes.Buffer{}\n\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt destroy --non-interactive --destroy-dependencies-check --working-dir \"+vpcPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\toutput := stderr.String()\n\tassert.Equal(t, 1, strings.Count(output, appV1Path))\n\tassert.Equal(t, 1, strings.Count(output, appV2Path))\n}\n\nfunc TestNoShowWarningWithDependentModulesBeforeDestroy(t *testing.T) {\n\tt.Parallel()\n\n\trootPath := helpers.CopyEnvironment(t, testFixtureDestroyWarning)\n\n\trootPath = filepath.Join(rootPath, testFixtureDestroyWarning)\n\tvpcPath := filepath.Join(rootPath, \"vpc\")\n\tappV1Path := filepath.Join(rootPath, \"app-v1\")\n\tappV2Path := filepath.Join(rootPath, \"app-v2\")\n\n\tcleanupTerraformFolder(t, rootPath)\n\tcleanupTerraformFolder(t, vpcPath)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt run --all init --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\t// try to destroy vpc module and check if warning is not printed in output (default behavior - checks disabled)\n\tstdout = bytes.Buffer{}\n\tstderr = bytes.Buffer{}\n\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt destroy --non-interactive --working-dir \"+vpcPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\toutput := stderr.String()\n\tassert.Equal(t, 0, strings.Count(output, appV1Path))\n\tassert.Equal(t, 0, strings.Count(output, appV2Path))\n}\n\nfunc TestPreventDestroyDependenciesIncludedConfig(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/download\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLocalIncludePreventDestroyDependencies)\n\n\t// Populate module paths.\n\tmoduleNames := []string{\n\t\t\"module-a\",\n\t\t\"module-b\",\n\t\t\"module-c\",\n\t}\n\n\tmodulePaths := make(map[string]string, len(moduleNames))\n\tfor _, moduleName := range moduleNames {\n\t\tmodulePaths[moduleName] = filepath.Join(rootPath, moduleName)\n\t}\n\n\t// Cleanup all modules directories.\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\tfor _, modulePath := range modulePaths {\n\t\thelpers.CleanupTerraformFolder(t, modulePath)\n\t}\n\n\tvar (\n\t\tapplyAllStdout bytes.Buffer\n\t\tapplyAllStderr bytes.Buffer\n\t)\n\n\t// Apply and destroy all modules.\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath, &applyAllStdout, &applyAllStderr)\n\thelpers.LogBufferContentsLineByLine(t, applyAllStdout, \"run --all apply stdout\")\n\thelpers.LogBufferContentsLineByLine(t, applyAllStderr, \"run --all apply stderr\")\n\n\tif err != nil {\n\t\tt.Fatalf(\"run --all apply in TestPreventDestroyDependenciesIncludedConfig failed with error: %v. Full std\", err)\n\t}\n\n\tvar (\n\t\tdestroyAllStdout bytes.Buffer\n\t\tdestroyAllStderr bytes.Buffer\n\t)\n\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt run --all destroy --non-interactive --working-dir \"+rootPath, &destroyAllStdout, &destroyAllStderr)\n\thelpers.LogBufferContentsLineByLine(t, destroyAllStdout, \"run --all destroy stdout\")\n\thelpers.LogBufferContentsLineByLine(t, destroyAllStderr, \"run --all destroy stderr\")\n\n\trequire.NoError(t, err)\n\n\t// Check that modules C, D and E were deleted and modules A and B weren't.\n\tfor moduleName, modulePath := range modulePaths {\n\t\tvar (\n\t\t\tshowStdout bytes.Buffer\n\t\t\tshowStderr bytes.Buffer\n\t\t)\n\n\t\terr = helpers.RunTerragruntCommand(t, \"terragrunt show --non-interactive --tf-forward-stdout --working-dir \"+modulePath, &showStdout, &showStderr)\n\t\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout for \"+modulePath)\n\t\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr for \"+modulePath)\n\n\t\trequire.NoError(t, err)\n\n\t\toutput := showStdout.String()\n\n\t\tswitch moduleName {\n\t\tcase \"module-a\":\n\t\t\tassert.Contains(t, output, \"Hello, Module A\")\n\t\tcase \"module-b\":\n\t\t\tassert.Contains(t, output, \"Hello, Module B\")\n\t\tcase \"module-c\":\n\t\t\tassert.NotContains(t, output, \"Hello, Module C\")\n\t\t}\n\t}\n}\n\nfunc TestTerragruntSkipConfirmExternalDependencies(t *testing.T) {\n\t// This test cannot be run using Terragrunt Provider Cache because it causes the flock files to be locked forever, which in turn blocks other TGs (processes).\n\t// We use flock files to prevent multiple TGs from caching the same provider in parallel in a shared cache, which causes to conflicts.\n\tif helpers.IsTerragruntProviderCacheEnabled(t) {\n\t\tt.Skip()\n\t}\n\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureExternalDependency)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureExternalDependency)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpEnvPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\tr, w, _ := os.Pipe()\n\toldStdout := os.Stderr\n\tos.Stderr = w\n\n\ttmp := helpers.TmpDirWOSymlinks(t)\n\n\terr = helpers.RunTerragruntCommand(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt destroy --feature dep=%s --working-dir %s\",\n\t\t\ttmp,\n\t\t\ttestPath,\n\t\t),\n\t\t&stdout,\n\t\t&stderr,\n\t)\n\tos.Stderr = oldStdout\n\n\trequire.NoError(t, w.Close())\n\n\tcapturedOutput := make(chan string)\n\n\tgo func() {\n\t\tvar buf bytes.Buffer\n\n\t\t_, e := io.Copy(&buf, r)\n\t\tassert.NoError(t, e)\n\n\t\tcapturedOutput <- buf.String()\n\t}()\n\n\tcaptured := <-capturedOutput\n\n\trequire.NoError(t, err)\n\tassert.NotContains(t, captured, \"Should Terragrunt apply the external dependency?\")\n\tassert.NotContains(t, captured, tmp)\n}\n\nfunc TestStorePlanFilesRunAllDestroy(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutDir)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureOutDir)\n\n\tdependencyPath := filepath.Join(tmpEnvPath, testFixtureOutDir, \"dependency\")\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt apply -auto-approve --non-interactive --working-dir %s --out-dir %s\",\n\t\t\tdependencyPath,\n\t\t\ttmpDir,\n\t\t),\n\t)\n\n\t// plan and apply\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all plan --non-interactive --working-dir %s --out-dir %s\",\n\t\t\ttestPath,\n\t\t\ttmpDir,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all apply --non-interactive --working-dir %s --out-dir %s\",\n\t\t\ttestPath,\n\t\t\ttmpDir,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\t// remove all tfstate files from temp directory to prepare destroy\n\tlist, err := findFilesWithExtension(tmpDir, \".tfplan\")\n\trequire.NoError(t, err)\n\tassert.Len(t, list, 2)\n\n\tfor _, file := range list {\n\t\tassert.Equal(t, \"tfplan.tfplan\", filepath.Base(file))\n\t}\n\n\t// prepare destroy plan\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all --non-interactive --working-dir %s --out-dir %s -- plan -destroy\",\n\t\t\ttestPath,\n\t\t\ttmpDir,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\t// verify that tfplan files are created in the tmpDir, 2 files\n\tlist, err = findFilesWithExtension(tmpDir, \".tfplan\")\n\trequire.NoError(t, err)\n\tassert.Len(t, list, 2)\n\n\tfor _, file := range list {\n\t\tassert.Equal(t, \"tfplan.tfplan\", filepath.Base(file))\n\t}\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all apply --non-interactive --working-dir %s --out-dir %s\",\n\t\t\ttestPath,\n\t\t\ttmpDir,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n}\n\nfunc TestStorePlanFilesShortcutAllDestroy(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutDir)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureOutDir)\n\n\tdependencyPath := filepath.Join(tmpEnvPath, testFixtureOutDir, \"dependency\")\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt apply -auto-approve --non-interactive --working-dir %s --out-dir %s\",\n\t\t\tdependencyPath,\n\t\t\ttmpDir,\n\t\t),\n\t)\n\n\t// plan and apply\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt plan --all --non-interactive --working-dir %s --out-dir %s\",\n\t\t\ttestPath,\n\t\t\ttmpDir,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt apply --all --non-interactive --working-dir %s --out-dir %s\",\n\t\t\ttestPath,\n\t\t\ttmpDir,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\t// remove all tfstate files from temp directory to prepare destroy\n\tlist, err := findFilesWithExtension(tmpDir, \".tfplan\")\n\trequire.NoError(t, err)\n\tassert.Len(t, list, 2)\n\n\tfor _, file := range list {\n\t\tassert.Equal(t, \"tfplan.tfplan\", filepath.Base(file))\n\t}\n\n\t// prepare destroy plan\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt plan --all -destroy --non-interactive --working-dir %s --out-dir %s\",\n\t\t\ttestPath,\n\t\t\ttmpDir,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\t// verify that tfplan files are created in the tmpDir, 2 files\n\tlist, err = findFilesWithExtension(tmpDir, \".tfplan\")\n\trequire.NoError(t, err)\n\tassert.Len(t, list, 2)\n\n\tfor _, file := range list {\n\t\tassert.Equal(t, \"tfplan.tfplan\", filepath.Base(file))\n\t}\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt apply --all --non-interactive --working-dir %s --out-dir %s\",\n\t\t\ttestPath,\n\t\t\ttmpDir,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n}\n\nfunc TestDestroyDependentModuleParseErrors(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureDestroyDependentModuleErrors)\n\ttmpEnvPath, _ := filepath.EvalSymlinks(helpers.CopyEnvironment(t, testFixtureDestroyDependentModuleErrors))\n\trootPath := filepath.Join(tmpEnvPath, testFixtureDestroyDependentModuleErrors)\n\n\thelpers.CreateGitRepo(t, rootPath)\n\n\t// apply dev\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run -all apply --non-interactive --working-dir \"+filepath.Join(rootPath, \"dev\"),\n\t)\n\trequire.NoError(t, err)\n\n\t// try to destroy app1 to trigger dependent units scanning\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt destroy -auto-approve --non-interactive --working-dir \"+filepath.Join(rootPath, \"dev\", \"app1\"),\n\t)\n\trequire.NoError(t, err)\n\n\t// shouldn't contain SOPS errors which are printed during dependent units discovery\n\tassert.NotContains(t, stderr, \"sops metadata not found\")\n}\n"
  },
  {
    "path": "test/integration_docs_aws_test.go",
    "content": "//go:build aws\n\npackage test_test\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureOverview = \"fixtures/docs/02-overview\"\n)\n\nfunc TestAwsDocsOverview(t *testing.T) {\n\tt.Parallel()\n\n\t// These docs examples specifically run here\n\tregion := \"us-east-1\"\n\n\tt.Run(\"step-01-terragrunt.hcl\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\t\tstepPath := filepath.Join(testFixtureOverview, \"step-01-terragrunt.hcl\")\n\n\t\thelpers.CleanupTerraformFolder(t, stepPath)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, stepPath)\n\t\trootPath := filepath.Join(tmpEnvPath, stepPath)\n\n\t\tdefer helpers.DeleteS3Bucket(t, region, s3BucketName)\n\t\tdefer func() {\n\t\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\t\t\tt,\n\t\t\t\t\"terragrunt destroy -auto-approve --non-interactive --working-dir \"+rootPath,\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\t\t}()\n\n\t\trootTerragruntConfigPath := filepath.Join(rootPath, config.DefaultTerragruntConfigPath)\n\t\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\t\tt,\n\t\t\trootTerragruntConfigPath,\n\t\t\trootTerragruntConfigPath,\n\t\t\ts3BucketName,\n\t\t\t\"not-used\",\n\t\t\tregion,\n\t\t)\n\n\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\t\tt,\n\t\t\t\"terragrunt run --non-interactive --backend-bootstrap --working-dir \"+\n\t\t\t\trootPath+\" -- apply -auto-approve\",\n\t\t)\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"step-02-dependencies\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\t\tstepPath := filepath.Join(testFixtureOverview, \"step-02-dependencies\")\n\n\t\thelpers.CleanupTerraformFolder(t, stepPath)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, stepPath)\n\t\trootPath := filepath.Join(tmpEnvPath, stepPath)\n\n\t\tdefer helpers.DeleteS3Bucket(t, region, s3BucketName)\n\t\tdefer func() {\n\t\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\t\t\tt,\n\t\t\t\t\"terragrunt run --all --non-interactive --working-dir \"+\n\t\t\t\t\trootPath+\" -- destroy -auto-approve\")\n\t\t\trequire.NoError(t, err)\n\t\t}()\n\n\t\trootTerragruntConfigPath := filepath.Join(rootPath, \"root.hcl\")\n\t\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\t\tt,\n\t\t\trootTerragruntConfigPath,\n\t\t\trootTerragruntConfigPath,\n\t\t\ts3BucketName,\n\t\t\t\"not-used\",\n\t\t\tregion,\n\t\t)\n\n\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --backend-bootstrap --working-dir \"+rootPath+\" -- apply -auto-approve\")\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"step-03-mock-outputs\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\t\tstepPath := filepath.Join(testFixtureOverview, \"step-03-mock-outputs\")\n\n\t\thelpers.CleanupTerraformFolder(t, stepPath)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, stepPath)\n\t\trootPath := filepath.Join(tmpEnvPath, stepPath)\n\n\t\tdefer helpers.DeleteS3Bucket(t, region, s3BucketName)\n\t\tdefer func() {\n\t\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- destroy -auto-approve\")\n\t\t\trequire.NoError(t, err)\n\t\t}()\n\n\t\trootTerragruntConfigPath := filepath.Join(rootPath, \"root.hcl\")\n\t\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, \"not-used\", region)\n\n\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --backend-bootstrap --working-dir \"+rootPath+\" -- apply -auto-approve\")\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"step-04-configuration-hierarchy\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\t\tstepPath := filepath.Join(testFixtureOverview, \"step-04-configuration-hierarchy\")\n\n\t\thelpers.CleanupTerraformFolder(t, stepPath)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, stepPath)\n\t\trootPath := filepath.Join(tmpEnvPath, stepPath)\n\n\t\tdefer helpers.DeleteS3Bucket(t, region, s3BucketName)\n\t\tdefer func() {\n\t\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- destroy -auto-approve\")\n\t\t\trequire.NoError(t, err)\n\t\t}()\n\n\t\trootTerragruntConfigPath := filepath.Join(rootPath, \"root.hcl\")\n\t\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, \"not-used\", region)\n\n\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --backend-bootstrap --working-dir \"+rootPath+\" -- apply -auto-approve\")\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"step-05-exposed-includes\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\t\tstepPath := filepath.Join(testFixtureOverview, \"step-05-exposed-includes\")\n\n\t\thelpers.CleanupTerraformFolder(t, stepPath)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, stepPath)\n\t\trootPath := filepath.Join(tmpEnvPath, stepPath)\n\n\t\tdefer helpers.DeleteS3Bucket(t, region, s3BucketName)\n\t\tdefer func() {\n\t\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- destroy -auto-approve\")\n\t\t\trequire.NoError(t, err)\n\t\t}()\n\n\t\trootTerragruntConfigPath := filepath.Join(rootPath, \"root.hcl\")\n\t\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, \"not-used\", region)\n\n\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --backend-bootstrap --working-dir \"+rootPath+\" -- apply -auto-approve\")\n\t\trequire.NoError(t, err)\n\t})\n}\n"
  },
  {
    "path": "test/integration_docs_aws_tofu_test.go",
    "content": "//go:build aws && tofu\n\npackage test_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAwsDocsTerralithToTerragruntGuide(t *testing.T) {\n\tt.Parallel()\n\n\tfixturePath := filepath.Join(\"..\", \"docs\", \"src\", \"fixtures\", \"terralith-to-terragrunt\")\n\n\t// Create a temporary workspace for the test\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\thelpers.ExecWithTestLogger(t, tmpDir, \"mkdir\", \"terralith-to-terragrunt\")\n\n\t// Determine the paths used throughout the steps.\n\trepoDir := filepath.Join(tmpDir, \"terralith-to-terragrunt\")\n\tliveDir := filepath.Join(repoDir, \"live\")\n\tdistDir := filepath.Join(repoDir, \"dist\")\n\tdistStaticDir := filepath.Join(repoDir, \"dist\", \"static\")\n\tcatalogDir := filepath.Join(repoDir, \"catalog\")\n\tcatalogModulesDir := filepath.Join(catalogDir, \"modules\")\n\tdevDir := filepath.Join(liveDir, \"dev\")\n\tprodDir := filepath.Join(liveDir, \"prod\")\n\n\t// Generate unique identifier for the test run\n\tuniqueID := strings.ToLower(helpers.UniqueID())\n\n\tstateBucketName := \"terragrunt-terralith-tfstate-\" + uniqueID\n\tname := \"terragrunt-terralith-project-\" + uniqueID\n\n\tregion := \"us-east-1\"\n\n\t// Defer cleanup of state bucket\n\tdefer helpers.DeleteS3Bucket(t, region, stateBucketName)\n\n\tfunc() {\n\t\tt.Log(\"Running step 0 - Setup\")\n\n\t\thelpers.ExecWithTestLogger(t, repoDir, \"git\", \"init\")\n\n\t\thelpers.ExecWithTestLogger(t, repoDir, \"mise\", \"use\", \"terragrunt@0.95.0\")\n\t\thelpers.ExecWithTestLogger(t, repoDir, \"mise\", \"use\", \"opentofu@1.11.1\")\n\t\thelpers.ExecWithTestLogger(t, repoDir, \"mise\", \"use\", \"aws@2.27.63\")\n\t\thelpers.ExecWithTestLogger(t, repoDir, \"mise\", \"use\", \"node@22.17.1\")\n\n\t\tmiseTomlPath := filepath.Join(repoDir, \"mise.toml\")\n\t\trequire.FileExists(t, miseTomlPath)\n\n\t\t// Run a dummy command to check if the tools are installed\n\t\tstdout, _ := helpers.ExecWithMiseAndCaptureOutput(t, repoDir, \"terragrunt\", \"--version\")\n\t\trequire.Contains(t, stdout, \"terragrunt\")\n\n\t\tstdout, _ = helpers.ExecWithMiseAndCaptureOutput(t, repoDir, \"tofu\", \"--version\")\n\t\trequire.Contains(t, stdout, \"OpenTofu\")\n\n\t\tstdout, _ = helpers.ExecWithMiseAndCaptureOutput(t, repoDir, \"aws\", \"--version\")\n\t\trequire.Contains(t, stdout, \"aws\")\n\n\t\tstdout, _ = helpers.ExecWithMiseAndCaptureOutput(t, repoDir, \"node\", \"--version\")\n\t\trequire.Contains(t, stdout, \"v22.17.1\")\n\n\t\t// Create the backend S3 bucket manually using AWS CLI (as mentioned in the guide)\n\t\t//\n\t\t// Do it earlier than it is in the guide so we can be sure it's going to be cleaned up properly at the end.\n\t\thelpers.ExecWithMiseAndTestLogger(t, tmpDir, \"aws\", \"s3api\", \"create-bucket\",\n\t\t\t\"--bucket\", stateBucketName, \"--region\", region)\n\t\thelpers.ExecWithMiseAndTestLogger(t, tmpDir, \"aws\", \"s3api\", \"put-bucket-versioning\",\n\t\t\t\"--bucket\", stateBucketName, \"--versioning-configuration\", \"Status=Enabled\")\n\n\t\thelpers.ExecWithTestLogger(t, repoDir, \"mkdir\", \"-p\", \"app/best-cat\")\n\n\t\tbestCatPath := filepath.Join(repoDir, \"app\", \"best-cat\")\n\n\t\t// Copy all the best-cat application files\n\t\tbestCatFiles := []string{\n\t\t\t\"package.json\",\n\t\t\t\"index.js\",\n\t\t\t\"template.html\",\n\t\t\t\"styles.css\",\n\t\t\t\"script.js\",\n\t\t\t\"package-lock.json\",\n\t\t}\n\n\t\tfor _, file := range bestCatFiles {\n\t\t\thelpers.CopyFile(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(fixturePath, \"app\", \"best-cat\", file),\n\t\t\t\tfilepath.Join(bestCatPath, file),\n\t\t\t)\n\t\t}\n\n\t\thelpers.ExecWithTestLogger(t, repoDir, \"mkdir\", \"dist\")\n\n\t\thelpers.ExecWithMiseAndTestLogger(t, bestCatPath, \"npm\", \"i\")\n\t\thelpers.ExecWithMiseAndTestLogger(t, bestCatPath, \"npm\", \"run\", \"package\")\n\n\t\trequire.NoError(t, os.Mkdir(filepath.Join(distDir, \"static\"), 0755))\n\n\t\tfor i := range 10 {\n\t\t\trequire.NoError(\n\t\t\t\tt,\n\t\t\t\tos.WriteFile(\n\t\t\t\t\tfilepath.Join(\n\t\t\t\t\t\tdistDir,\n\t\t\t\t\t\t\"static\", fmt.Sprintf(\"%d-cat.png\", i+1)),\n\t\t\t\t\t[]byte(\"\"),\n\t\t\t\t\t0644,\n\t\t\t\t),\n\t\t\t)\n\t\t}\n\n\t\tt.Log(\"Setup complete\")\n\t}()\n\n\tfunc() {\n\t\tt.Log(\"Running step 1 - Starting the Terralith\")\n\n\t\t// We do a check like this to make sure we properly clean up infrastructure only when we fail.\n\t\t//\n\t\t// We need our infrastructure to persist between steps so that we can test stateful refactoring.\n\t\tpass := false\n\t\tdefer func() {\n\t\t\tif !pass {\n\t\t\t\thelpers.ExecWithMiseAndTestLogger(t, liveDir, \"tofu\", \"destroy\", \"-auto-approve\")\n\t\t\t}\n\t\t}()\n\n\t\t// Create the live directory\n\t\thelpers.ExecWithTestLogger(t, repoDir, \"mkdir\", \"live\")\n\n\t\t// Copy all the OpenTofu files from fixtures\n\t\tfixtureStepPath := filepath.Join(fixturePath, \"walkthrough\", \"step-1-starting-the-terralith\", \"live\")\n\n\t\t// List of files to copy\n\t\tterraformFiles := []string{\n\t\t\t\"providers.tf\",\n\t\t\t\"versions.tf\",\n\t\t\t\"data.tf\",\n\t\t\t\"ddb.tf\",\n\t\t\t\"s3.tf\",\n\t\t\t\"iam.tf\",\n\t\t\t\"lambda.tf\",\n\t\t\t\"vars-required.tf\",\n\t\t\t\"vars-optional.tf\",\n\t\t\t\"outputs.tf\",\n\t\t\t\"backend.tf\",\n\t\t}\n\n\t\t// Copy each file from fixture to live directory\n\t\tfor _, file := range terraformFiles {\n\t\t\thelpers.CopyFile(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(fixtureStepPath, file),\n\t\t\t\tfilepath.Join(liveDir, file),\n\t\t\t)\n\t\t}\n\n\t\t// Create the .auto.tfvars file with test-specific values.\n\t\t// We set force_destroy to true to avoid errors when destroying the infrastructure.\n\t\ttfvarsContent := fmt.Sprintf(`# Required: Name used for all resources (must be unique)\nname = \"%s\"\n\n# Required: Path to your Lambda function zip file\nlambda_zip_file = \"../dist/best-cat.zip\"\n\n# AWS region\naws_region = \"%s\"\n\nforce_destroy = true\n`, name, region)\n\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(liveDir, \".auto.tfvars\"),\n\t\t\t[]byte(tfvarsContent),\n\t\t\t0644,\n\t\t))\n\n\t\t// Update backend.tf with unique bucket name\n\t\tbackendContent := fmt.Sprintf(`terraform {\n  backend \"s3\" {\n    bucket       = \"%s\"\n    key          = \"tofu.tfstate\"\n    region       = \"%s\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n`, stateBucketName, region)\n\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(liveDir, \"backend.tf\"),\n\t\t\t[]byte(backendContent),\n\t\t\t0644,\n\t\t))\n\n\t\t// Initialize and apply the Terraform configuration\n\t\thelpers.ExecWithMiseAndTestLogger(t, liveDir, \"tofu\", \"init\")\n\n\t\t// Apply the Terraform configuration\n\t\thelpers.ExecWithMiseAndTestLogger(t, liveDir, \"tofu\", \"apply\", \"-auto-approve\")\n\n\t\t// Verify the apply was successful by checking outputs\n\t\tstdout, _ := helpers.ExecWithMiseAndCaptureOutput(t, liveDir, \"tofu\", \"output\")\n\n\t\t// Check that key outputs exist\n\t\tassert.Contains(t, stdout, \"lambda_function_url\")\n\t\tassert.Contains(t, stdout, \"s3_bucket_name\")\n\t\tassert.Contains(t, stdout, \"dynamodb_table_name\")\n\n\t\t// Get the S3 bucket name from output for asset upload test\n\t\tbucketNameOutput, _ := helpers.ExecWithMiseAndCaptureOutput(t, liveDir, \"tofu\", \"output\", \"-raw\", \"s3_bucket_name\")\n\n\t\tactualBucketName := strings.TrimSpace(bucketNameOutput)\n\n\t\trequire.NotEmpty(t, actualBucketName)\n\n\t\thelpers.ExecWithMiseAndTestLogger(\n\t\t\tt,\n\t\t\tdistStaticDir,\n\t\t\t\"aws\", \"s3\", \"sync\", \".\", fmt.Sprintf(\"s3://%s/\", actualBucketName),\n\t\t)\n\n\t\tt.Log(\"Step 1 - Starting the Terralith completed successfully\")\n\t\tpass = true\n\t}()\n\n\tfunc() {\n\t\tt.Log(\"Running step 2 - Refactoring\")\n\n\t\t// We do a check like this to make sure we properly clean up infrastructure only when we fail.\n\t\t//\n\t\t// We need our infrastructure to persist between steps so that we can test stateful refactoring.\n\t\tpass := false\n\t\tdefer func() {\n\t\t\tif !pass {\n\t\t\t\thelpers.ExecWithMiseAndTestLogger(t, liveDir, \"tofu\", \"destroy\", \"-auto-approve\")\n\t\t\t}\n\t\t}()\n\n\t\t// Create the catalog directory structure\n\t\thelpers.ExecWithTestLogger(t, repoDir, \"bash\", \"-c\", \"mkdir -p catalog/modules/{s3,lambda,iam,ddb}\")\n\n\t\t// Remove the old individual .tf files that will be moved to modules\n\t\toldFiles := []string{\"ddb.tf\", \"iam.tf\", \"data.tf\", \"lambda.tf\", \"s3.tf\"}\n\t\tfor _, file := range oldFiles {\n\t\t\tfilePath := filepath.Join(liveDir, file)\n\t\t\trequire.NoError(t, os.Remove(filePath))\n\t\t}\n\n\t\t// Path to step 2 fixtures\n\t\tfixtureStepPath := filepath.Join(fixturePath, \"walkthrough\", \"step-2-refactoring\")\n\n\t\t// Copy all module files from fixtures to the catalog directory\n\t\tmodules := []string{\"s3\", \"lambda\", \"iam\", \"ddb\"}\n\t\tfor _, module := range modules {\n\t\t\tmoduleSourcePath := filepath.Join(fixtureStepPath, \"catalog\", \"modules\", module)\n\t\t\tmoduleDestPath := filepath.Join(catalogModulesDir, module)\n\n\t\t\t// List of files that may exist in each module\n\t\t\tmoduleFiles := []string{\n\t\t\t\t\"main.tf\",\n\t\t\t\t\"outputs.tf\",\n\t\t\t\t\"vars-required.tf\",\n\t\t\t\t\"vars-optional.tf\",\n\t\t\t\t\"versions.tf\",\n\t\t\t\t\"data.tf\",\n\t\t\t}\n\n\t\t\tfor _, file := range moduleFiles {\n\t\t\t\tsourceFile := filepath.Join(moduleSourcePath, file)\n\t\t\t\tdestFile := filepath.Join(moduleDestPath, file)\n\n\t\t\t\t// Only copy if the source file exists\n\t\t\t\tif _, err := os.Stat(sourceFile); err == nil {\n\t\t\t\t\thelpers.CopyFile(t, sourceFile, destFile)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Update the live directory with refactored files\n\t\tliveSourcePath := filepath.Join(fixtureStepPath, \"live\")\n\n\t\t// Files to copy/update in the live directory\n\t\tliveFiles := []string{\n\t\t\t\"main.tf\",\n\t\t\t\"moved.tf\",\n\t\t\t\"outputs.tf\",\n\t\t\t\"vars-optional.tf\",\n\t\t}\n\n\t\tfor _, file := range liveFiles {\n\t\t\thelpers.CopyFile(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(liveSourcePath, file),\n\t\t\t\tfilepath.Join(liveDir, file),\n\t\t\t)\n\t\t}\n\n\t\t// Re-initialize since we're now using modules\n\t\thelpers.ExecWithMiseAndTestLogger(t, liveDir, \"tofu\", \"init\")\n\n\t\t// Run plan to verify the refactoring - should show 0 changes due to moved blocks\n\t\tstdout, _ := helpers.ExecWithMiseAndCaptureOutput(t, liveDir, \"tofu\", \"plan\")\n\n\t\t// Verify that the plan shows no changes (the moved blocks should handle state migration)\n\t\tassert.Contains(t, stdout, \"0 to add, 0 to change, 0 to destroy\")\n\n\t\t// Apply the configuration to ensure everything works\n\t\thelpers.ExecWithMiseAndTestLogger(t, liveDir, \"tofu\", \"apply\", \"-auto-approve\")\n\n\t\t// Verify outputs still work after refactoring\n\t\toutputStdout, _ := helpers.ExecWithMiseAndCaptureOutput(t, liveDir, \"tofu\", \"output\")\n\n\t\t// Check that key outputs still exist after refactoring\n\t\tassert.Contains(t, outputStdout, \"lambda_function_url\")\n\t\tassert.Contains(t, outputStdout, \"s3_bucket_name\")\n\t\tassert.Contains(t, outputStdout, \"dynamodb_table_name\")\n\n\t\tt.Log(\"Step 2 - Refactoring completed successfully\")\n\t\tpass = true\n\t}()\n\n\tfunc() {\n\t\tt.Log(\"Running step 3 - Adding dev\")\n\n\t\t// We do a check like this to make sure we properly clean up infrastructure only when we fail.\n\t\t//\n\t\t// We need our infrastructure to persist between steps so that we can test stateful refactoring.\n\t\tpass := false\n\t\tdefer func() {\n\t\t\tif !pass {\n\t\t\t\thelpers.ExecWithMiseAndTestLogger(t, liveDir, \"tofu\", \"destroy\", \"-auto-approve\")\n\t\t\t}\n\t\t}()\n\n\t\t// Path to step 3 fixtures\n\t\tfixtureStepPath := filepath.Join(fixturePath, \"walkthrough\", \"step-3-adding-dev\")\n\n\t\t// Create the best_cat module directory\n\t\tbestCatModulePath := filepath.Join(catalogModulesDir, \"best_cat\")\n\t\thelpers.ExecWithTestLogger(t, repoDir, \"mkdir\", \"-p\", bestCatModulePath)\n\n\t\t// Copy the best_cat module files\n\t\tbestCatSourcePath := filepath.Join(fixtureStepPath, \"catalog\", \"modules\", \"best_cat\")\n\t\tbestCatFiles := []string{\n\t\t\t\"main.tf\",\n\t\t\t\"outputs.tf\",\n\t\t\t\"vars-optional.tf\",\n\t\t\t\"vars-required.tf\",\n\t\t}\n\n\t\tfor _, file := range bestCatFiles {\n\t\t\thelpers.CopyFile(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(bestCatSourcePath, file),\n\t\t\t\tfilepath.Join(bestCatModulePath, file),\n\t\t\t)\n\t\t}\n\n\t\t// Update the live directory with step 3 files\n\t\tliveSourcePath := filepath.Join(fixtureStepPath, \"live\")\n\n\t\t// Files to copy/update in the live directory for step 3\n\t\tliveFiles := []string{\n\t\t\t\"main.tf\",\n\t\t\t\"moved.tf\",\n\t\t\t\"outputs.tf\",\n\t\t}\n\n\t\tfor _, file := range liveFiles {\n\t\t\thelpers.CopyFile(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(liveSourcePath, file),\n\t\t\t\tfilepath.Join(liveDir, file),\n\t\t\t)\n\t\t}\n\n\t\t// Re-initialize since we're now using the new best_cat module\n\t\thelpers.ExecWithMiseAndTestLogger(t, liveDir, \"tofu\", \"init\")\n\n\t\t// Run plan to verify the refactoring - should show only new dev resources due to moved blocks\n\t\tstdout, _ := helpers.ExecWithMiseAndCaptureOutput(t, liveDir, \"tofu\", \"plan\")\n\n\t\t// Verify that the plan shows the expected new dev resources (11 new resources for dev environment)\n\t\tassert.Contains(t, stdout, \"11 to add\")\n\t\tassert.Contains(t, stdout, \"0 to change\")\n\t\tassert.Contains(t, stdout, \"0 to destroy\")\n\n\t\t// Apply the configuration to create the dev environment\n\t\thelpers.ExecWithMiseAndTestLogger(t, liveDir, \"tofu\", \"apply\", \"-auto-approve\")\n\n\t\t// Verify outputs for both dev and prod environments\n\t\toutputStdout, _ := helpers.ExecWithMiseAndCaptureOutput(t, liveDir, \"tofu\", \"output\")\n\n\t\t// Check that both dev and prod outputs exist\n\t\tassert.Contains(t, outputStdout, \"dev_lambda_function_url\")\n\t\tassert.Contains(t, outputStdout, \"dev_s3_bucket_name\")\n\t\tassert.Contains(t, outputStdout, \"prod_lambda_function_url\")\n\t\tassert.Contains(t, outputStdout, \"prod_s3_bucket_name\")\n\n\t\t// Verify that we can get the function URLs for both environments\n\t\tdevFunctionURL, _ := helpers.ExecWithMiseAndCaptureOutput(t, liveDir, \"tofu\", \"output\", \"-raw\", \"dev_lambda_function_url\")\n\t\tprodFunctionURL, _ := helpers.ExecWithMiseAndCaptureOutput(t, liveDir, \"tofu\", \"output\", \"-raw\", \"prod_lambda_function_url\")\n\n\t\trequire.NotEmpty(t, strings.TrimSpace(devFunctionURL))\n\t\trequire.NotEmpty(t, strings.TrimSpace(prodFunctionURL))\n\n\t\t// Verify the URLs are different (confirming we have two separate environments)\n\t\tassert.NotEqual(t, strings.TrimSpace(devFunctionURL), strings.TrimSpace(prodFunctionURL))\n\n\t\tt.Log(\"Step 3 - Adding dev completed successfully\")\n\t\tpass = true\n\t}()\n\n\tfunc() {\n\t\tt.Log(\"Running step 4 - Breaking the Terralith\")\n\n\t\t// We do a check like this to make sure we properly clean up infrastructure only when we fail.\n\t\t//\n\t\t// We need our infrastructure to persist between steps so that we can test stateful refactoring.\n\t\tpass := false\n\t\tdefer func() {\n\t\t\tif !pass {\n\t\t\t\t// Cleanup both dev and prod environments if we get here.\n\t\t\t\tif _, err := os.Stat(devDir); err == nil {\n\t\t\t\t\thelpers.ExecWithMiseAndTestLogger(t, devDir, \"tofu\", \"destroy\", \"-auto-approve\")\n\t\t\t\t}\n\n\t\t\t\tif _, err := os.Stat(prodDir); err == nil {\n\t\t\t\t\thelpers.ExecWithMiseAndTestLogger(t, prodDir, \"tofu\", \"destroy\", \"-auto-approve\")\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\n\t\thelpers.ExecWithTestLogger(t, liveDir, \"mkdir\", \"prod\")\n\n\t\t// Get list of files/directories to move (everything except the newly created prod directory)\n\t\tentries, err := os.ReadDir(liveDir)\n\t\trequire.NoError(t, err)\n\n\t\tfor _, entry := range entries {\n\t\t\tif entry.Name() != \"prod\" {\n\t\t\t\toldPath := filepath.Join(liveDir, entry.Name())\n\t\t\t\tnewPath := filepath.Join(prodDir, entry.Name())\n\t\t\t\trequire.NoError(t, os.Rename(oldPath, newPath))\n\t\t\t}\n\t\t}\n\n\t\thelpers.ExecWithTestLogger(t, liveDir, \"cp\", \"-R\", \"prod\", \"dev\")\n\n\t\tfixtureStepPath := filepath.Join(fixturePath, \"walkthrough\", \"step-4-breaking-the-terralith\")\n\n\t\tdevBackendPath := filepath.Join(devDir, \"backend.tf\")\n\t\tdevBackendContent := fmt.Sprintf(`terraform {\n  backend \"s3\" {\n    bucket       = \"%s\"\n    key          = \"dev/tofu.tfstate\"\n    region       = \"%s\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n`, stateBucketName, region)\n\t\trequire.NoError(t, os.WriteFile(devBackendPath, []byte(devBackendContent), 0644))\n\n\t\tprodBackendPath := filepath.Join(prodDir, \"backend.tf\")\n\t\tprodBackendContent := fmt.Sprintf(`terraform {\n  backend \"s3\" {\n    bucket       = \"%s\"\n    key          = \"prod/tofu.tfstate\"\n    region       = \"%s\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n`, stateBucketName, region)\n\t\trequire.NoError(t, os.WriteFile(prodBackendPath, []byte(prodBackendContent), 0644))\n\n\t\tdevMainSourcePath := filepath.Join(fixtureStepPath, \"live\", \"dev\")\n\t\tprodMainSourcePath := filepath.Join(fixtureStepPath, \"live\", \"prod\")\n\n\t\t// Files to copy/update in both directories\n\t\tenvFiles := []string{\n\t\t\t\"main.tf\",\n\t\t\t\"moved.tf\",\n\t\t\t\"outputs.tf\",\n\t\t\t\"removed.tf\",\n\t\t}\n\n\t\t// Copy files to dev environment\n\t\tfor _, file := range envFiles {\n\t\t\thelpers.CopyFile(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(devMainSourcePath, file),\n\t\t\t\tfilepath.Join(devDir, file),\n\t\t\t)\n\t\t}\n\n\t\t// Copy files to prod environment\n\t\tfor _, file := range envFiles {\n\t\t\thelpers.CopyFile(\n\t\t\t\tt,\n\t\t\t\tfilepath.Join(prodMainSourcePath, file),\n\t\t\t\tfilepath.Join(prodDir, file),\n\t\t\t)\n\t\t}\n\n\t\tdevTfvarsContent := fmt.Sprintf(`# Required: Name used for all resources (must be unique)\nname = \"%s-dev\"\n\n# Required: Path to your Lambda function zip file\nlambda_zip_file = \"../../dist/best-cat.zip\"\n\n# AWS region\naws_region = \"%s\"\n\nforce_destroy = true\n`, name, region)\n\n\t\tprodTfvarsContent := fmt.Sprintf(`# Required: Name used for all resources (must be unique)\nname = \"%s\"\n\n# Required: Path to your Lambda function zip file\nlambda_zip_file = \"../../dist/best-cat.zip\"\n\n# AWS region\naws_region = \"%s\"\n\nforce_destroy = true\n`, name, region)\n\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(devDir, \".auto.tfvars\"),\n\t\t\t[]byte(devTfvarsContent),\n\t\t\t0644,\n\t\t))\n\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(prodDir, \".auto.tfvars\"),\n\t\t\t[]byte(prodTfvarsContent),\n\t\t\t0644,\n\t\t))\n\n\t\thelpers.ExecWithTestLogger(\n\t\t\tt, liveDir, \"cp\", \"-R\",\n\t\t\tfilepath.Join(\"prod\", \".terraform\"),\n\t\t\tfilepath.Join(\"dev\", \".terraform\"),\n\t\t)\n\n\t\t// We can't use non-interactive mode here, so we just pipe in \"yes\" to the prompts.\n\t\thelpers.ExecWithMiseAndTestLogger(t, devDir, \"bash\", \"-c\", \"echo 'yes' | tofu init -migrate-state\")\n\t\thelpers.ExecWithMiseAndTestLogger(t, prodDir, \"bash\", \"-c\", \"echo 'yes' | tofu init -migrate-state\")\n\n\t\tdevPlanOutput, _ := helpers.ExecWithMiseAndCaptureOutput(t, devDir, \"tofu\", \"plan\")\n\t\tassert.Contains(t, devPlanOutput, \"0 to add\")\n\t\tassert.Contains(t, devPlanOutput, \"1 to change\")\n\t\tassert.Contains(t, devPlanOutput, \"0 to destroy\")\n\t\tassert.Contains(t, devPlanOutput, \"11 to forget\")\n\n\t\thelpers.ExecWithMiseAndTestLogger(t, devDir, \"tofu\", \"apply\", \"-auto-approve\")\n\n\t\tprodPlanOutput, _ := helpers.ExecWithMiseAndCaptureOutput(t, prodDir, \"tofu\", \"plan\")\n\t\tassert.Contains(t, prodPlanOutput, \"0 to add\")\n\t\tassert.Contains(t, prodPlanOutput, \"1 to change\")\n\t\tassert.Contains(t, prodPlanOutput, \"0 to destroy\")\n\t\tassert.Contains(t, prodPlanOutput, \"11 to forget\")\n\n\t\thelpers.ExecWithMiseAndTestLogger(t, prodDir, \"tofu\", \"apply\", \"-auto-approve\")\n\n\t\tdevOutputStdout, _ := helpers.ExecWithMiseAndCaptureOutput(t, devDir, \"tofu\", \"output\")\n\t\tprodOutputStdout, _ := helpers.ExecWithMiseAndCaptureOutput(t, prodDir, \"tofu\", \"output\")\n\n\t\tassert.Contains(t, devOutputStdout, \"lambda_function_url\")\n\t\tassert.Contains(t, devOutputStdout, \"s3_bucket_name\")\n\t\tassert.Contains(t, prodOutputStdout, \"lambda_function_url\")\n\t\tassert.Contains(t, prodOutputStdout, \"s3_bucket_name\")\n\n\t\tdevFunctionURL, _ := helpers.ExecWithMiseAndCaptureOutput(t, devDir, \"tofu\", \"output\", \"-raw\", \"lambda_function_url\")\n\t\tprodFunctionURL, _ := helpers.ExecWithMiseAndCaptureOutput(t, prodDir, \"tofu\", \"output\", \"-raw\", \"lambda_function_url\")\n\n\t\tassert.NotEqual(t, strings.TrimSpace(devFunctionURL), strings.TrimSpace(prodFunctionURL))\n\n\t\tdevBucketName, _ := helpers.ExecWithMiseAndCaptureOutput(t, devDir, \"tofu\", \"output\", \"-raw\", \"s3_bucket_name\")\n\t\tprodBucketName, _ := helpers.ExecWithMiseAndCaptureOutput(t, prodDir, \"tofu\", \"output\", \"-raw\", \"s3_bucket_name\")\n\n\t\tassert.NotEqual(t, strings.TrimSpace(devBucketName), strings.TrimSpace(prodBucketName))\n\n\t\tt.Log(\"Step 4 - Breaking the Terralith completed successfully\")\n\t\tpass = true\n\t}()\n\n\tfunc() {\n\t\tt.Log(\"Running step 5 - Adding Terragrunt\")\n\n\t\t// We do a check like this to make sure we properly clean up infrastructure only when we fail.\n\t\t//\n\t\t// We need our infrastructure to persist between steps so that we can test stateful refactoring.\n\t\tpass := false\n\t\tdefer func() {\n\t\t\tif !pass {\n\t\t\t\t// Cleanup both dev and prod environments if we get here.\n\t\t\t\tif _, err := os.Stat(devDir); err == nil {\n\t\t\t\t\thelpers.ExecWithMiseAndTestLogger(t, devDir, \"terragrunt\", \"destroy\", \"-auto-approve\", \"--non-interactive\")\n\t\t\t\t}\n\n\t\t\t\tif _, err := os.Stat(prodDir); err == nil {\n\t\t\t\t\thelpers.ExecWithMiseAndTestLogger(t, prodDir, \"terragrunt\", \"destroy\", \"-auto-approve\", \"--non-interactive\")\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\n\t\tfixtureStepPath := filepath.Join(fixturePath, \"walkthrough\", \"step-5-adding-terragrunt\")\n\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(devDir, \"terragrunt.hcl\"), []byte(\"\"), 0644))\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(prodDir, \"terragrunt.hcl\"), []byte(\"\"), 0644))\n\n\t\t_, stderr := helpers.ExecWithMiseAndCaptureOutput(t, liveDir, \"terragrunt\", \"run\", \"--all\", \"plan\", \"--non-interactive\")\n\n\t\t// This version of Terragrunt uses the new \"Module\" term instead of \"Unit\"\n\t\tassert.Contains(t, stderr, \"Unit dev\")\n\t\tassert.Contains(t, stderr, \"Unit prod\")\n\n\t\toldFiles := []string{\"main.tf\", \"outputs.tf\", \"vars-required.tf\", \"vars-optional.tf\", \"versions.tf\"}\n\t\tfor _, file := range oldFiles {\n\t\t\trequire.NoError(t, os.Remove(filepath.Join(devDir, file)))\n\t\t\trequire.NoError(t, os.Remove(filepath.Join(prodDir, file)))\n\t\t}\n\n\t\trequire.NoError(t, os.Remove(filepath.Join(devDir, \".auto.tfvars\")))\n\t\trequire.NoError(t, os.Remove(filepath.Join(prodDir, \".auto.tfvars\")))\n\n\t\trequire.NoError(t, os.Remove(filepath.Join(devDir, \"backend.tf\")))\n\t\trequire.NoError(t, os.Remove(filepath.Join(prodDir, \"backend.tf\")))\n\t\trequire.NoError(t, os.Remove(filepath.Join(devDir, \"providers.tf\")))\n\t\trequire.NoError(t, os.Remove(filepath.Join(prodDir, \"providers.tf\")))\n\n\t\trequire.NoError(t, os.Remove(filepath.Join(devDir, \"removed.tf\")))\n\t\trequire.NoError(t, os.Remove(filepath.Join(prodDir, \"removed.tf\")))\n\n\t\trootHclContent := fmt.Sprintf(`remote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    bucket       = \"%s\"\n    key          = \"${path_relative_to_include()}/tofu.tfstate\"\n    region       = \"%s\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n\ngenerate \"providers\" {\n  path      = \"providers.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents  = <<EOF\nprovider \"aws\" {\n  region = \"%s\"\n}\nEOF\n}\n`, stateBucketName, region, region)\n\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(liveDir, \"root.hcl\"),\n\t\t\t[]byte(rootHclContent),\n\t\t\t0644,\n\t\t))\n\n\t\t// Create dev terragrunt.hcl\n\t\tdevTerragruntContent := fmt.Sprintf(`include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../../catalog/modules//best_cat\"\n}\n\ninputs = {\n  name            = \"%s-dev\"\n  lambda_zip_file = \"${get_repo_root()}/dist/best-cat.zip\"\n\n  force_destroy = true\n}\n`, name)\n\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(devDir, \"terragrunt.hcl\"),\n\t\t\t[]byte(devTerragruntContent),\n\t\t\t0644,\n\t\t))\n\n\t\t// Create prod terragrunt.hcl\n\t\tprodTerragruntContent := fmt.Sprintf(`include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"../../catalog/modules//best_cat\"\n}\n\ninputs = {\n  name            = \"%s\"\n  lambda_zip_file = \"${get_repo_root()}/dist/best-cat.zip\"\n\n  force_destroy = true\n}\n`, name)\n\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(prodDir, \"terragrunt.hcl\"),\n\t\t\t[]byte(prodTerragruntContent),\n\t\t\t0644,\n\t\t))\n\n\t\thelpers.CopyFile(\n\t\t\tt,\n\t\t\tfilepath.Join(fixtureStepPath, \"live\", \"dev\", \"moved.tf\"),\n\t\t\tfilepath.Join(devDir, \"moved.tf\"),\n\t\t)\n\n\t\thelpers.CopyFile(\n\t\t\tt,\n\t\t\tfilepath.Join(fixtureStepPath, \"live\", \"prod\", \"moved.tf\"),\n\t\t\tfilepath.Join(prodDir, \"moved.tf\"),\n\t\t)\n\n\t\tdevPlanOutput, _ := helpers.ExecWithMiseAndCaptureOutput(t, devDir, \"terragrunt\", \"plan\")\n\t\tassert.Contains(t, devPlanOutput, \"0 to add, 1 to change, 0 to destroy\")\n\n\t\tprodPlanOutput, _ := helpers.ExecWithMiseAndCaptureOutput(t, prodDir, \"terragrunt\", \"plan\")\n\t\tassert.Contains(t, prodPlanOutput, \"0 to add, 1 to change, 0 to destroy\")\n\n\t\thelpers.ExecWithMiseAndTestLogger(t, devDir, \"terragrunt\", \"apply\", \"-auto-approve\", \"--non-interactive\")\n\t\thelpers.ExecWithMiseAndTestLogger(t, prodDir, \"terragrunt\", \"apply\", \"-auto-approve\", \"--non-interactive\")\n\n\t\trunAllPlanStdout, runAllPlanStderr := helpers.ExecWithMiseAndCaptureOutput(t, liveDir, \"terragrunt\", \"run\", \"--all\", \"plan\")\n\t\tassert.Contains(t, runAllPlanStderr, \"Unit dev\")\n\t\tassert.Contains(t, runAllPlanStderr, \"Unit prod\")\n\t\tassert.Contains(t, runAllPlanStdout, \"found no differences, so no changes are needed.\")\n\n\t\tdevOnlyPlanStdout, devOnlyPlanStderr := helpers.ExecWithMiseAndCaptureOutput(t, liveDir, \"terragrunt\", \"run\", \"--all\", \"--queue-include-dir\", \"dev\", \"plan\", \"--non-interactive\")\n\t\tassert.Contains(t, devOnlyPlanStderr, \"Unit dev\")\n\t\tassert.NotContains(t, devOnlyPlanStderr, \"Unit prod\")\n\t\tassert.Contains(t, devOnlyPlanStdout, \"found no differences, so no changes are needed.\")\n\n\t\tdevOutputStdout, _ := helpers.ExecWithMiseAndCaptureOutput(t, devDir, \"terragrunt\", \"output\")\n\t\tprodOutputStdout, _ := helpers.ExecWithMiseAndCaptureOutput(t, prodDir, \"terragrunt\", \"output\")\n\n\t\tassert.Contains(t, devOutputStdout, \"lambda_function_url\")\n\t\tassert.Contains(t, devOutputStdout, \"s3_bucket_name\")\n\t\tassert.Contains(t, prodOutputStdout, \"lambda_function_url\")\n\t\tassert.Contains(t, prodOutputStdout, \"s3_bucket_name\")\n\n\t\tt.Log(\"Step 5 - Adding Terragrunt completed successfully\")\n\t\tpass = true\n\t}()\n\n\tfunc() {\n\t\tt.Log(\"Running step 6 - Breaking the Terralith Further\")\n\n\t\t// We do a check like this to make sure we properly clean up infrastructure only when we fail.\n\t\t//\n\t\t// We need our infrastructure to persist between steps so that we can test stateful refactoring.\n\t\tpass := false\n\t\tdefer func() {\n\t\t\tif !pass {\n\t\t\t\t// Cleanup all component units if we get here.\n\t\t\t\thelpers.ExecWithMiseAndTestLogger(t, liveDir, \"terragrunt\", \"run\", \"--all\", \"--non-interactive\", \"--\", \"destroy\", \"-auto-approve\")\n\t\t\t}\n\t\t}()\n\n\t\tfixtureStepPath := filepath.Join(fixturePath, \"walkthrough\", \"step-6-breaking-the-terralith-further\")\n\n\t\t// Ensure root.hcl uses the correct stateBucketName\n\t\trootHclContent := fmt.Sprintf(`remote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    bucket       = \"%s\"\n    key          = \"${path_relative_to_include()}/tofu.tfstate\"\n    region       = \"%s\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n\ngenerate \"providers\" {\n  path      = \"providers.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents  = <<EOF\nprovider \"aws\" {\n  region = \"%s\"\n}\nEOF\n}\n`, stateBucketName, region, region)\n\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(liveDir, \"root.hcl\"),\n\t\t\t[]byte(rootHclContent),\n\t\t\t0644,\n\t\t))\n\n\t\t// Create directories for each component in both environments\n\t\tcomponents := []string{\"s3\", \"ddb\", \"iam\", \"lambda\"}\n\t\tenvironments := []string{\"dev\", \"prod\"}\n\n\t\tfor _, env := range environments {\n\t\t\tfor _, component := range components {\n\t\t\t\tcomponentDir := filepath.Join(liveDir, env, component)\n\t\t\t\trequire.NoError(t, os.MkdirAll(componentDir, 0755))\n\t\t\t}\n\t\t}\n\n\t\t// Copy terragrunt.hcl files from fixtures for each component\n\t\tfor _, env := range environments {\n\t\t\tfor _, component := range components {\n\t\t\t\tsourceFile := filepath.Join(fixtureStepPath, \"live\", env, component, \"terragrunt.hcl\")\n\t\t\t\tdestFile := filepath.Join(liveDir, env, component, \"terragrunt.hcl\")\n\n\t\t\t\t// Read the source file and replace the hardcoded name with our test name\n\t\t\t\tsourceContent, err := os.ReadFile(sourceFile)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t// Replace the hardcoded name in the fixture with our test-specific name\n\t\t\t\tnameToUse := name\n\t\t\t\tif env == \"dev\" {\n\t\t\t\t\tnameToUse = name + \"-dev\"\n\t\t\t\t}\n\n\t\t\t\tcontent := strings.ReplaceAll(string(sourceContent), \"best-cat-2025-09-24-2359\", name)\n\t\t\t\tcontent = strings.ReplaceAll(content, name+\"-dev\", nameToUse)\n\t\t\t\tcontent = strings.ReplaceAll(content, name, nameToUse)\n\n\t\t\t\trequire.NoError(t, os.WriteFile(destFile, []byte(content), 0644))\n\t\t\t}\n\t\t}\n\n\t\t// Migrate state from existing units to new component units\n\t\t// First, pull state from existing dev and prod units\n\t\tfor _, env := range environments {\n\t\t\tenvDir := filepath.Join(liveDir, env)\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\t\t\ttempStateFile := filepath.Join(tmpDir, \"tofu-\"+env+\".tfstate\")\n\n\t\t\t// Pull state from existing environment unit\n\t\t\tstateContent, _ := helpers.ExecWithMiseAndCaptureOutput(t, envDir, \"terragrunt\", \"state\", \"pull\")\n\t\t\trequire.NoError(t, os.WriteFile(tempStateFile, []byte(stateContent), 0644))\n\n\t\t\tfor _, component := range components {\n\t\t\t\tcomponentDir := filepath.Join(envDir, component)\n\t\t\t\thelpers.ExecWithMiseAndTestLogger(t, componentDir, \"terragrunt\", \"state\", \"push\", tempStateFile)\n\t\t\t}\n\t\t}\n\n\t\tdevS3Content := fmt.Sprintf(`include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${find_in_parent_folders(\"catalog/modules\")}//s3\"\n}\n\ninputs = {\n  name = \"%s-dev\"\n  force_destroy = true\n}\n`, name)\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(liveDir, \"dev\", \"s3\", \"terragrunt.hcl\"), []byte(devS3Content), 0644))\n\n\t\tprodS3Content := fmt.Sprintf(`include \"root\" {\n  path = find_in_parent_folders(\"root.hcl\")\n}\n\nterraform {\n  source = \"${find_in_parent_folders(\"catalog/modules\")}//s3\"\n}\n\ninputs = {\n  name = \"%s\"\n  force_destroy = true\n}\n`, name)\n\t\trequire.NoError(t, os.WriteFile(filepath.Join(liveDir, \"prod\", \"s3\", \"terragrunt.hcl\"), []byte(prodS3Content), 0644))\n\n\t\t// Remove the old terragrunt.hcl and moved.tf files from the environment root directories\n\t\tfor _, env := range environments {\n\t\t\tenvDir := filepath.Join(liveDir, env)\n\t\t\trequire.NoError(t, os.Remove(filepath.Join(envDir, \"terragrunt.hcl\")))\n\t\t\trequire.NoError(t, os.Remove(filepath.Join(envDir, \"moved.tf\")))\n\t\t}\n\n\t\t// Copy moved.tf and removed.tf files for state transitions\n\t\tfor _, env := range environments {\n\t\t\tfor _, component := range components {\n\t\t\t\tsourceMovedFile := filepath.Join(fixtureStepPath, \"live\", env, component, \"moved.tf\")\n\t\t\t\tdestMovedFile := filepath.Join(liveDir, env, component, \"moved.tf\")\n\t\t\t\thelpers.CopyFile(t, sourceMovedFile, destMovedFile)\n\n\t\t\t\tsourceRemovedFile := filepath.Join(fixtureStepPath, \"live\", env, component, \"removed.tf\")\n\t\t\t\tdestRemovedFile := filepath.Join(liveDir, env, component, \"removed.tf\")\n\t\t\t\thelpers.CopyFile(t, sourceRemovedFile, destRemovedFile)\n\t\t\t}\n\t\t}\n\n\t\t// Verify plans show no destroys across all components\n\t\t_, planStderr := helpers.ExecWithMiseAndCaptureOutput(t, liveDir, \"terragrunt\", \"run\", \"--all\", \"plan\", \"--non-interactive\")\n\n\t\t// The plan output should show modules for all components\n\t\tfor _, env := range environments {\n\t\t\tfor _, component := range components {\n\t\t\t\texpectedModulePath := fmt.Sprintf(\"Unit %s/%s\", env, component)\n\t\t\t\tassert.Contains(t, planStderr, expectedModulePath)\n\t\t\t}\n\t\t}\n\n\t\t// Apply all changes to complete the migration\n\t\thelpers.ExecWithMiseAndTestLogger(t, liveDir, \"terragrunt\", \"run\", \"--all\", \"apply\", \"--non-interactive\")\n\n\t\t// Verify outputs still work after breaking down into components\n\t\t// Check a few key components to ensure they're working\n\t\tdevS3Output, _ := helpers.ExecWithMiseAndCaptureOutput(t, filepath.Join(liveDir, \"dev\", \"s3\"), \"terragrunt\", \"output\")\n\t\tprodS3Output, _ := helpers.ExecWithMiseAndCaptureOutput(t, filepath.Join(liveDir, \"prod\", \"s3\"), \"terragrunt\", \"output\")\n\n\t\tassert.Contains(t, devS3Output, \"name\")\n\t\tassert.Contains(t, prodS3Output, \"name\")\n\n\t\tdevLambdaOutput, _ := helpers.ExecWithMiseAndCaptureOutput(t, filepath.Join(liveDir, \"dev\", \"lambda\"), \"terragrunt\", \"output\")\n\t\tprodLambdaOutput, _ := helpers.ExecWithMiseAndCaptureOutput(t, filepath.Join(liveDir, \"prod\", \"lambda\"), \"terragrunt\", \"output\")\n\n\t\tassert.Contains(t, devLambdaOutput, \"url\")\n\t\tassert.Contains(t, prodLambdaOutput, \"url\")\n\n\t\t// Verify dependency resolution works by running a plan on lambda (which depends on other components)\n\t\tdevLambdaPlan, _ := helpers.ExecWithMiseAndCaptureOutput(t, filepath.Join(liveDir, \"dev\", \"lambda\"), \"terragrunt\", \"plan\")\n\t\tassert.Contains(t, devLambdaPlan, \"found no differences, so no changes are needed.\")\n\n\t\tprodLambdaPlan, _ := helpers.ExecWithMiseAndCaptureOutput(t, filepath.Join(liveDir, \"prod\", \"lambda\"), \"terragrunt\", \"plan\")\n\t\tassert.Contains(t, prodLambdaPlan, \"found no differences, so no changes are needed.\")\n\n\t\tt.Log(\"Step 6 - Breaking the Terralith Further completed successfully\")\n\t\tpass = true\n\t}()\n\n\tfunc() {\n\t\tt.Log(\"Running step 7 - Taking advantage of Terragrunt Stacks\")\n\n\t\t// We do a check like this to make sure we properly clean up infrastructure only when we fail.\n\t\t//\n\t\t// We need our infrastructure to persist between steps so that we can test stateful refactoring.\n\t\tpass := false\n\t\tdefer func() {\n\t\t\tif !pass {\n\t\t\t\t// Cleanup all component units if we get here.\n\t\t\t\thelpers.ExecWithMiseAndTestLogger(t, liveDir, \"terragrunt\", \"run\", \"--all\", \"--non-interactive\", \"--\", \"destroy\", \"-auto-approve\")\n\t\t\t}\n\t\t}()\n\n\t\t// Path to step 7 fixtures\n\t\tfixtureStepPath := filepath.Join(fixturePath, \"walkthrough\", \"step-7-taking-advantage-of-terragrunt-stacks\")\n\n\t\t// Ensure root.hcl uses the correct stateBucketName\n\t\trootHclContent := fmt.Sprintf(`remote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    bucket       = \"%s\"\n    key          = \"${path_relative_to_include()}/tofu.tfstate\"\n    region       = \"%s\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n\ngenerate \"providers\" {\n  path      = \"providers.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents  = <<EOF\nprovider \"aws\" {\n  region = \"%s\"\n}\nEOF\n}\n`, stateBucketName, region, region)\n\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(liveDir, \"root.hcl\"),\n\t\t\t[]byte(rootHclContent),\n\t\t\t0644,\n\t\t))\n\n\t\t// Create the catalog/units directory structure for unit definitions\n\t\tcatalogUnitsDir := filepath.Join(catalogDir, \"units\")\n\t\tcomponents := []string{\"ddb\", \"iam\", \"lambda\", \"s3\"}\n\n\t\tfor _, component := range components {\n\t\t\tcomponentUnitsDir := filepath.Join(catalogUnitsDir, component)\n\t\t\trequire.NoError(t, os.MkdirAll(componentUnitsDir, 0755))\n\t\t}\n\n\t\t// Copy terragrunt.hcl files from fixtures to catalog/units\n\t\tfor _, component := range components {\n\t\t\tsourceFile := filepath.Join(fixtureStepPath, \"catalog\", \"units\", component, \"terragrunt.hcl\")\n\t\t\tdestFile := filepath.Join(catalogUnitsDir, component, \"terragrunt.hcl\")\n\t\t\thelpers.CopyFile(t, sourceFile, destFile)\n\t\t}\n\n\t\t// Copy .terraform.lock.hcl files from dev component directories to catalog/units if they exist\n\t\tfor _, component := range components {\n\t\t\tdevComponentDir := filepath.Join(liveDir, \"dev\", component)\n\t\t\tcatalogComponentDir := filepath.Join(catalogUnitsDir, component)\n\n\t\t\t// Copy .terraform.lock.hcl if it exists\n\t\t\tlockFilePath := filepath.Join(devComponentDir, \".terraform.lock.hcl\")\n\t\t\tcatalogLockFilePath := filepath.Join(catalogComponentDir, \".terraform.lock.hcl\")\n\t\t\tif _, err := os.Stat(lockFilePath); err == nil {\n\t\t\t\thelpers.CopyFile(t, lockFilePath, catalogLockFilePath)\n\t\t\t}\n\t\t}\n\n\t\t// Copy and customize terragrunt.stack.hcl files from fixtures\n\t\t// Read dev stack template and replace the hardcoded name\n\t\tdevStackSourceFile := filepath.Join(fixtureStepPath, \"live\", \"dev\", \"terragrunt.stack.hcl\")\n\t\tdevStackContent, err := os.ReadFile(devStackSourceFile)\n\t\trequire.NoError(t, err)\n\n\t\t// Replace the hardcoded name with our test-specific name\n\t\tcustomizedDevStackContent := strings.ReplaceAll(string(devStackContent), \"best-cat-2025-09-24-2359-dev\", name+\"-dev\")\n\t\tcustomizedDevStackContent = strings.ReplaceAll(customizedDevStackContent, \"us-east-1\", region)\n\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(devDir, \"terragrunt.stack.hcl\"),\n\t\t\t[]byte(customizedDevStackContent),\n\t\t\t0644,\n\t\t))\n\n\t\t// Read prod stack template and replace the hardcoded name\n\t\tprodStackSourceFile := filepath.Join(fixtureStepPath, \"live\", \"prod\", \"terragrunt.stack.hcl\")\n\t\tprodStackContent, err := os.ReadFile(prodStackSourceFile)\n\t\trequire.NoError(t, err)\n\n\t\t// Replace the hardcoded name with our test-specific name\n\t\tcustomizedProdStackContent := strings.ReplaceAll(string(prodStackContent), \"best-cat-2025-09-24-2359\", name)\n\t\tcustomizedProdStackContent = strings.ReplaceAll(customizedProdStackContent, \"us-east-1\", region)\n\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(prodDir, \"terragrunt.stack.hcl\"),\n\t\t\t[]byte(customizedProdStackContent),\n\t\t\t0644,\n\t\t))\n\n\t\t// Copy .gitignore files from fixtures\n\t\thelpers.CopyFile(\n\t\t\tt,\n\t\t\tfilepath.Join(fixtureStepPath, \"live\", \"dev\", \".gitignore\"),\n\t\t\tfilepath.Join(devDir, \".gitignore\"),\n\t\t)\n\n\t\thelpers.CopyFile(\n\t\t\tt,\n\t\t\tfilepath.Join(fixtureStepPath, \"live\", \"prod\", \".gitignore\"),\n\t\t\tfilepath.Join(prodDir, \".gitignore\"),\n\t\t)\n\n\t\t// Remove the old individual component directories since they'll be generated on-demand\n\t\tenvironments := []string{\"dev\", \"prod\"}\n\t\tfor _, env := range environments {\n\t\t\tfor _, component := range components {\n\t\t\t\tcomponentDir := filepath.Join(liveDir, env, component)\n\t\t\t\trequire.NoError(t, os.RemoveAll(componentDir))\n\t\t\t}\n\t\t}\n\n\t\t// Test that terragrunt run --all plan works with the new stack configuration\n\t\t_, planStderr := helpers.ExecWithMiseAndCaptureOutput(t, liveDir, \"terragrunt\", \"run\", \"--all\", \"plan\", \"--non-interactive\")\n\n\t\t// The plan output should show modules for all components generated from stacks\n\t\tfor _, env := range environments {\n\t\t\tfor _, component := range components {\n\t\t\t\texpectedModulePath := fmt.Sprintf(\"Unit %s/%s\", env, component)\n\t\t\t\tassert.Contains(t, planStderr, expectedModulePath)\n\t\t\t}\n\t\t}\n\n\t\t// Apply the stack configuration to ensure everything works\n\t\thelpers.ExecWithMiseAndTestLogger(t, liveDir, \"terragrunt\", \"run\", \"--all\", \"apply\", \"--non-interactive\")\n\n\t\t// Verify outputs still work after migrating to stacks\n\t\t// Check a few key components to ensure they're working\n\t\tdevS3Output, _ := helpers.ExecWithMiseAndCaptureOutput(t, filepath.Join(liveDir, \"dev\", \"s3\"), \"terragrunt\", \"output\")\n\t\tprodS3Output, _ := helpers.ExecWithMiseAndCaptureOutput(t, filepath.Join(liveDir, \"prod\", \"s3\"), \"terragrunt\", \"output\")\n\n\t\tassert.Contains(t, devS3Output, \"name\")\n\t\tassert.Contains(t, prodS3Output, \"name\")\n\n\t\tdevLambdaOutput, _ := helpers.ExecWithMiseAndCaptureOutput(t, filepath.Join(liveDir, \"dev\", \"lambda\"), \"terragrunt\", \"output\")\n\t\tprodLambdaOutput, _ := helpers.ExecWithMiseAndCaptureOutput(t, filepath.Join(liveDir, \"prod\", \"lambda\"), \"terragrunt\", \"output\")\n\n\t\tassert.Contains(t, devLambdaOutput, \"url\")\n\t\tassert.Contains(t, prodLambdaOutput, \"url\")\n\n\t\t// Verify dependency resolution still works correctly with stacks\n\t\tdevLambdaPlan, _ := helpers.ExecWithMiseAndCaptureOutput(t, filepath.Join(liveDir, \"dev\", \"lambda\"), \"terragrunt\", \"plan\")\n\t\tassert.Contains(t, devLambdaPlan, \"found no differences, so no changes are needed.\")\n\n\t\tprodLambdaPlan, _ := helpers.ExecWithMiseAndCaptureOutput(t, filepath.Join(liveDir, \"prod\", \"lambda\"), \"terragrunt\", \"plan\")\n\t\tassert.Contains(t, prodLambdaPlan, \"found no differences, so no changes are needed.\")\n\n\t\tt.Log(\"Step 7 - Taking advantage of Terragrunt Stacks completed successfully\")\n\t\tpass = true\n\t}()\n\n\tfunc() {\n\t\tt.Log(\"Running step 8 - Refactoring state with Terragrunt Stacks\")\n\n\t\t// We do a check like this to make sure we properly clean up infrastructure only when we fail.\n\t\t//\n\t\t// We need our infrastructure to persist between steps so that we can test stateful refactoring.\n\t\tpass := false\n\t\tdefer func() {\n\t\t\tif !pass {\n\t\t\t\t// Cleanup all component units if we get here.\n\t\t\t\thelpers.ExecWithMiseAndTestLogger(t, liveDir, \"terragrunt\", \"run\", \"--all\", \"--non-interactive\", \"--\", \"destroy\", \"-auto-approve\")\n\t\t\t}\n\t\t}()\n\n\t\t// Path to step 8 fixtures\n\t\tfixtureStepPath := filepath.Join(fixturePath, \"walkthrough\", \"step-8-refactoring-state-with-terragrunt-stacks\")\n\n\t\t// Ensure root.hcl uses the correct stateBucketName\n\t\trootHclContent := fmt.Sprintf(`remote_state {\n  backend = \"s3\"\n  generate = {\n    path      = \"backend.tf\"\n    if_exists = \"overwrite\"\n  }\n  config = {\n    bucket       = \"%s\"\n    key          = \"${path_relative_to_include()}/tofu.tfstate\"\n    region       = \"%s\"\n    encrypt      = true\n    use_lockfile = true\n  }\n}\n\ngenerate \"providers\" {\n  path      = \"providers.tf\"\n  if_exists = \"overwrite_terragrunt\"\n  contents  = <<EOF\nprovider \"aws\" {\n  region = \"%s\"\n}\nEOF\n}\n`, stateBucketName, region, region)\n\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(liveDir, \"root.hcl\"),\n\t\t\t[]byte(rootHclContent),\n\t\t\t0644,\n\t\t))\n\n\t\t// First, generate the current stack to ensure everything is present\n\t\thelpers.ExecWithMiseAndTestLogger(t, liveDir, \"terragrunt\", \"stack\", \"generate\")\n\n\t\t// Update the terragrunt.stack.hcl files to remove no_dot_terragrunt_stack attribute\n\t\t// Read dev stack template and replace the hardcoded name\n\t\tdevStackSourceFile := filepath.Join(fixtureStepPath, \"live\", \"dev\", \"terragrunt.stack.hcl\")\n\t\tdevStackContent, err := os.ReadFile(devStackSourceFile)\n\t\trequire.NoError(t, err)\n\n\t\t// Replace the hardcoded name with our test-specific name\n\t\tcustomizedDevStackContent := strings.ReplaceAll(string(devStackContent), \"best-cat-2025-09-24-2359-dev\", name+\"-dev\")\n\t\tcustomizedDevStackContent = strings.ReplaceAll(customizedDevStackContent, \"us-east-1\", region)\n\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(devDir, \"terragrunt.stack.hcl\"),\n\t\t\t[]byte(customizedDevStackContent),\n\t\t\t0644,\n\t\t))\n\n\t\t// Read prod stack template and replace the hardcoded name\n\t\tprodStackSourceFile := filepath.Join(fixtureStepPath, \"live\", \"prod\", \"terragrunt.stack.hcl\")\n\t\tprodStackContent, err := os.ReadFile(prodStackSourceFile)\n\t\trequire.NoError(t, err)\n\n\t\t// Replace the hardcoded name with our test-specific name\n\t\tcustomizedProdStackContent := strings.ReplaceAll(string(prodStackContent), \"best-cat-2025-09-24-2359\", name)\n\t\tcustomizedProdStackContent = strings.ReplaceAll(customizedProdStackContent, \"us-east-1\", region)\n\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(prodDir, \"terragrunt.stack.hcl\"),\n\t\t\t[]byte(customizedProdStackContent),\n\t\t\t0644,\n\t\t))\n\n\t\t// Generate the new stack structure with .terragrunt-stack directories\n\t\thelpers.ExecWithMiseAndTestLogger(t, liveDir, \"terragrunt\", \"stack\", \"generate\")\n\n\t\t// Verify that .terragrunt-stack directories are created\n\t\tcomponents := []string{\"ddb\", \"iam\", \"lambda\", \"s3\"}\n\t\tenvironments := []string{\"dev\", \"prod\"}\n\n\t\tfor _, env := range environments {\n\t\t\tterragruntStackDir := filepath.Join(liveDir, env, \".terragrunt-stack\")\n\t\t\trequire.DirExists(t, terragruntStackDir)\n\n\t\t\tfor _, component := range components {\n\t\t\t\tcomponentDir := filepath.Join(terragruntStackDir, component)\n\t\t\t\trequire.DirExists(t, componentDir)\n\n\t\t\t\t// Verify terragrunt.hcl exists in the .terragrunt-stack location\n\t\t\t\tterragruntHclPath := filepath.Join(componentDir, \"terragrunt.hcl\")\n\t\t\t\trequire.FileExists(t, terragruntHclPath)\n\t\t\t}\n\t\t}\n\n\t\t// Migrate state from old unit paths to new .terragrunt-stack paths\n\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\t\ttempStateFile := filepath.Join(tmpDir, \"tofu.tfstate\")\n\n\t\tfor _, env := range environments {\n\t\t\tfor _, component := range components {\n\t\t\t\toldUnitDir := filepath.Join(liveDir, env, component)\n\t\t\t\tnewUnitDir := filepath.Join(liveDir, env, \".terragrunt-stack\", component)\n\n\t\t\t\t// Pull state from old location\n\t\t\t\tstateContent, _ := helpers.ExecWithMiseAndCaptureOutput(t, oldUnitDir, \"terragrunt\", \"state\", \"pull\")\n\t\t\t\trequire.NoError(t, os.WriteFile(tempStateFile, []byte(stateContent), 0644))\n\n\t\t\t\t// Push state to new location\n\t\t\t\thelpers.ExecWithMiseAndTestLogger(t, newUnitDir, \"terragrunt\", \"state\", \"push\", tempStateFile)\n\t\t\t}\n\t\t}\n\n\t\t// Remove the old individual component directories from dev and prod\n\t\t// since they're now in .terragrunt-stack subdirectories\n\t\tfor _, env := range environments {\n\t\t\tfor _, component := range components {\n\t\t\t\tcomponentDir := filepath.Join(liveDir, env, component)\n\t\t\t\tif util.FileExists(componentDir) {\n\t\t\t\t\trequire.NoError(t, os.RemoveAll(componentDir))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Remove the .gitignore files since we no longer need them\n\t\trequire.NoError(t, os.Remove(filepath.Join(devDir, \".gitignore\")))\n\t\trequire.NoError(t, os.Remove(filepath.Join(prodDir, \".gitignore\")))\n\n\t\t// Verify that the migration was successful by running a plan\n\t\t// The plan should show no changes since we migrated state properly\n\t\t_, planStderr := helpers.ExecWithMiseAndCaptureOutput(t, liveDir, \"terragrunt\", \"run\", \"--all\", \"plan\", \"--non-interactive\")\n\n\t\t// The plan output should show modules for all components in .terragrunt-stack directories\n\t\tfor _, env := range environments {\n\t\t\tfor _, component := range components {\n\t\t\t\texpectedModulePath := fmt.Sprintf(\"Unit %s/.terragrunt-stack/%s\", env, component)\n\t\t\t\tassert.Contains(t, planStderr, expectedModulePath)\n\t\t\t}\n\t\t}\n\n\t\t// Apply to ensure everything works with the new structure\n\t\thelpers.ExecWithMiseAndTestLogger(t, liveDir, \"terragrunt\", \"run\", \"--all\", \"apply\", \"--non-interactive\")\n\n\t\t// Verify outputs still work after state migration\n\t\t// Check a few key components to ensure they're working\n\t\tdevS3Output, _ := helpers.ExecWithMiseAndCaptureOutput(t, filepath.Join(liveDir, \"dev\", \".terragrunt-stack\", \"s3\"), \"terragrunt\", \"output\")\n\t\tprodS3Output, _ := helpers.ExecWithMiseAndCaptureOutput(t, filepath.Join(liveDir, \"prod\", \".terragrunt-stack\", \"s3\"), \"terragrunt\", \"output\")\n\n\t\tassert.Contains(t, devS3Output, \"name\")\n\t\tassert.Contains(t, prodS3Output, \"name\")\n\n\t\tdevLambdaOutput, _ := helpers.ExecWithMiseAndCaptureOutput(t, filepath.Join(liveDir, \"dev\", \".terragrunt-stack\", \"lambda\"), \"terragrunt\", \"output\")\n\t\tprodLambdaOutput, _ := helpers.ExecWithMiseAndCaptureOutput(t, filepath.Join(liveDir, \"prod\", \".terragrunt-stack\", \"lambda\"), \"terragrunt\", \"output\")\n\n\t\tassert.Contains(t, devLambdaOutput, \"url\")\n\t\tassert.Contains(t, prodLambdaOutput, \"url\")\n\n\t\t// Verify dependency resolution still works correctly with the new structure\n\t\tdevLambdaPlan, _ := helpers.ExecWithMiseAndCaptureOutput(t, filepath.Join(liveDir, \"dev\", \".terragrunt-stack\", \"lambda\"), \"terragrunt\", \"plan\")\n\t\tassert.Contains(t, devLambdaPlan, \"found no differences, so no changes are needed.\")\n\n\t\tprodLambdaPlan, _ := helpers.ExecWithMiseAndCaptureOutput(t, filepath.Join(liveDir, \"prod\", \".terragrunt-stack\", \"lambda\"), \"terragrunt\", \"plan\")\n\t\tassert.Contains(t, prodLambdaPlan, \"found no differences, so no changes are needed.\")\n\n\t\t// Verify the directory structure is clean - dev and prod should only contain terragrunt.stack.hcl\n\t\tdevEntries, err := os.ReadDir(devDir)\n\t\trequire.NoError(t, err)\n\t\tdevFileNames := make([]string, 0, len(devEntries))\n\t\tfor _, entry := range devEntries {\n\t\t\tif !strings.HasPrefix(entry.Name(), \".\") { // Ignore hidden files/dirs like .terragrunt-stack\n\t\t\t\tdevFileNames = append(devFileNames, entry.Name())\n\t\t\t}\n\t\t}\n\t\tassert.Equal(t, []string{\"terragrunt.stack.hcl\"}, devFileNames)\n\n\t\tprodEntries, err := os.ReadDir(prodDir)\n\t\trequire.NoError(t, err)\n\t\tprodFileNames := make([]string, 0, len(prodEntries))\n\t\tfor _, entry := range prodEntries {\n\t\t\tif !strings.HasPrefix(entry.Name(), \".\") { // Ignore hidden files/dirs like .terragrunt-stack\n\t\t\t\tprodFileNames = append(prodFileNames, entry.Name())\n\t\t\t}\n\t\t}\n\t\tassert.Equal(t, []string{\"terragrunt.stack.hcl\"}, prodFileNames)\n\n\t\tt.Log(\"Step 8 - Refactoring state with Terragrunt Stacks completed successfully\")\n\t\tpass = true\n\t}()\n\n\tfunc() {\n\t\tt.Log(\"Cleanup\")\n\n\t\thelpers.ExecWithMiseAndTestLogger(\n\t\t\tt,\n\t\t\tliveDir,\n\t\t\t\"terragrunt\",\n\t\t\t\"run\", \"--all\", \"--non-interactive\", \"--\", \"destroy\")\n\t}()\n}\n"
  },
  {
    "path": "test/integration_docs_test.go",
    "content": "package test_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureQuickStart       = \"fixtures/docs/01-quick-start\"\n\ttestFixtureStacksLocalState = \"fixtures/docs/03-stacks-with-local-state\"\n)\n\nfunc TestDocsQuickStart(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"step-01\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tstepPath := filepath.Join(testFixtureQuickStart, \"step-01\", \"foo\")\n\n\t\thelpers.CleanupTerraformFolder(t, stepPath)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, stepPath)\n\t\trootPath := filepath.Join(tmpEnvPath, stepPath)\n\n\t\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt plan --non-interactive --working-dir \"+rootPath)\n\t\trequire.NoError(t, err)\n\t\tassert.Contains(t, stdout, \"Plan: 1 to add, 0 to change, 0 to destroy.\")\n\n\t\tstdout, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\t\trequire.NoError(t, err)\n\t\tassert.Contains(t, stdout, \"Apply complete! Resources: 1 added, 0 changed, 0 destroyed.\")\n\n\t})\n\n\tt.Run(\"step-01.1\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tstepPath := filepath.Join(testFixtureQuickStart, \"step-01.1\", \"foo\")\n\n\t\thelpers.CleanupTerraformFolder(t, stepPath)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, stepPath)\n\t\trootPath := filepath.Join(tmpEnvPath, stepPath)\n\n\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt plan -var content='Hello, Terragrunt!' --non-interactive --working-dir \"+rootPath)\n\t\trequire.NoError(t, err)\n\n\t\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve -var content='Hello, Terragrunt!' --non-interactive --working-dir \"+rootPath)\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"step-02\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tstepPath := filepath.Join(testFixtureQuickStart, \"step-02\")\n\n\t\thelpers.CleanupTerraformFolder(t, stepPath)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, stepPath)\n\t\trootPath := filepath.Join(tmpEnvPath, stepPath)\n\n\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- plan -var content='Hello, Terragrunt!'\")\n\t\trequire.NoError(t, err)\n\n\t\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- apply -var content='Hello, Terragrunt!'\")\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"step-03\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tstepPath := filepath.Join(testFixtureQuickStart, \"step-03\")\n\n\t\thelpers.CleanupTerraformFolder(t, stepPath)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, stepPath)\n\t\trootPath := filepath.Join(tmpEnvPath, stepPath)\n\n\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- plan -var content='Hello, Terragrunt!'\")\n\t\trequire.NoError(t, err)\n\n\t\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- apply -var content='Hello, Terragrunt!'\")\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"step-04\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tstepPath := filepath.Join(testFixtureQuickStart, \"step-04\")\n\n\t\thelpers.CleanupTerraformFolder(t, stepPath)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, stepPath)\n\t\trootPath := filepath.Join(tmpEnvPath, stepPath)\n\n\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all plan --non-interactive --working-dir \"+rootPath)\n\t\trequire.NoError(t, err)\n\n\t\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"step-05\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tstepPath := filepath.Join(testFixtureQuickStart, \"step-05\")\n\n\t\thelpers.CleanupTerraformFolder(t, stepPath)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, stepPath)\n\t\trootPath := filepath.Join(tmpEnvPath, stepPath)\n\n\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all plan --non-interactive --working-dir \"+rootPath)\n\t\trequire.NoError(t, err)\n\n\t\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"step-06\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tstepPath := filepath.Join(testFixtureQuickStart, \"step-06\")\n\n\t\thelpers.CleanupTerraformFolder(t, stepPath)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, stepPath)\n\t\trootPath := filepath.Join(tmpEnvPath, stepPath)\n\n\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all plan --non-interactive --working-dir \"+rootPath)\n\t\trequire.Error(t, err)\n\t})\n\n\tt.Run(\"step-07\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tstepPath := filepath.Join(testFixtureQuickStart, \"step-07\")\n\n\t\thelpers.CleanupTerraformFolder(t, stepPath)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, stepPath)\n\t\trootPath := filepath.Join(tmpEnvPath, stepPath)\n\n\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all plan --non-interactive --working-dir \"+rootPath)\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"step-07.1\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\tstepPath := filepath.Join(testFixtureQuickStart, \"step-07.1\")\n\n\t\thelpers.CleanupTerraformFolder(t, stepPath)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, stepPath)\n\t\trootPath := filepath.Join(tmpEnvPath, stepPath)\n\n\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all plan --non-interactive --working-dir \"+rootPath)\n\t\trequire.NoError(t, err)\n\t})\n}\n\nfunc TestStacksWithLocalState(t *testing.T) {\n\tt.Parallel()\n\n\t// Clean up the test fixture\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksLocalState)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksLocalState)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksLocalState)\n\tlivePath := filepath.Join(rootPath, \"live\")\n\tlocalStatePath := filepath.Join(rootPath, \".terragrunt-local-state\")\n\n\t// Ensure local state directory doesn't exist initially\n\trequire.NoError(t, os.RemoveAll(localStatePath))\n\n\t// Step 1: Generate the stack\n\thelpers.RunTerragrunt(t, \"terragrunt stack generate --working-dir \"+livePath)\n\n\t// Verify .terragrunt-stack directory was created\n\tstackPath := filepath.Join(livePath, \".terragrunt-stack\")\n\trequire.DirExists(t, stackPath)\n\n\t// Verify individual units were generated\n\tfooPath := filepath.Join(stackPath, \"foo\")\n\tbarPath := filepath.Join(stackPath, \"bar\")\n\tbazPath := filepath.Join(stackPath, \"baz\")\n\trequire.DirExists(t, fooPath)\n\trequire.DirExists(t, barPath)\n\trequire.DirExists(t, bazPath)\n\n\t// Step 2: Apply the stack to create state files\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+livePath)\n\n\t// Verify local state files were created in .terragrunt-local-state\n\t// Note: path_relative_to_include() returns \"live/.terragrunt-stack/foo\" etc.\n\tfooStatePath := filepath.Join(localStatePath, \"live\", \".terragrunt-stack\", \"foo\", \"tofu.tfstate\")\n\tbarStatePath := filepath.Join(localStatePath, \"live\", \".terragrunt-stack\", \"bar\", \"tofu.tfstate\")\n\tbazStatePath := filepath.Join(localStatePath, \"live\", \".terragrunt-stack\", \"baz\", \"tofu.tfstate\")\n\n\trequire.FileExists(t, fooStatePath)\n\trequire.FileExists(t, barStatePath)\n\trequire.FileExists(t, bazStatePath)\n\n\t// Verify state files contain actual state (not empty)\n\tfooStateContent, err := util.ReadFileAsString(fooStatePath)\n\trequire.NoError(t, err)\n\tbarStateContent, err := util.ReadFileAsString(barStatePath)\n\trequire.NoError(t, err)\n\tbazStateContent, err := util.ReadFileAsString(bazStatePath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, fooStateContent, \"null_resource\")\n\tassert.Contains(t, barStateContent, \"null_resource\")\n\tassert.Contains(t, bazStateContent, \"null_resource\")\n\n\t// Step 3: Clean and regenerate the stack\n\thelpers.RunTerragrunt(t, \"terragrunt stack clean --working-dir \"+livePath)\n\n\t// Verify .terragrunt-stack directory was removed\n\trequire.NoDirExists(t, stackPath)\n\n\t// Verify local state files still exist\n\trequire.FileExists(t, fooStatePath)\n\trequire.FileExists(t, barStatePath)\n\trequire.FileExists(t, bazStatePath)\n\n\t// Regenerate the stack\n\thelpers.RunTerragrunt(t, \"terragrunt stack generate --working-dir \"+livePath)\n\n\t// Verify .terragrunt-stack directory was recreated\n\trequire.DirExists(t, stackPath)\n\trequire.DirExists(t, fooPath)\n\trequire.DirExists(t, barPath)\n\trequire.DirExists(t, bazPath)\n\n\t// Step 4: Verify that existing state is recognized after regeneration\n\t// Run plan to make sure it recognizes existing resources\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack run plan --non-interactive --working-dir \"+livePath)\n\trequire.NoError(t, err)\n\n\t// The plan output should indicate no changes are needed since resources already exist\n\tassert.Contains(t, stdout, \"No changes\")\n\n\t// Step 5: Destroy resources to clean up\n\thelpers.RunTerragrunt(t, \"terragrunt stack run destroy --non-interactive --working-dir \"+livePath)\n\n\t// Verify state files still exist but are now empty/clean\n\trequire.FileExists(t, fooStatePath)\n\trequire.FileExists(t, barStatePath)\n\trequire.FileExists(t, bazStatePath)\n}\n\nfunc TestStacksWithLocalStateFileStructure(t *testing.T) {\n\tt.Parallel()\n\n\t// Test that verifies the exact file structure created by the local state configuration\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksLocalState)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksLocalState)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksLocalState)\n\tlivePath := filepath.Join(rootPath, \"live\")\n\tlocalStatePath := filepath.Join(rootPath, \".terragrunt-local-state\")\n\n\t// Ensure local state directory doesn't exist initially\n\trequire.NoError(t, os.RemoveAll(localStatePath))\n\n\t// Generate and apply the stack\n\thelpers.RunTerragrunt(t, \"terragrunt stack generate --working-dir \"+livePath)\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+livePath)\n\n\t// Test the exact structure of .terragrunt-local-state\n\trequire.DirExists(t, localStatePath)\n\n\t// Check that each unit has its own subdirectory\n\t// Note: path structure reflects live/.terragrunt-stack/[unit]\n\tfooLocalStateDir := filepath.Join(localStatePath, \"live\", \".terragrunt-stack\", \"foo\")\n\tbarLocalStateDir := filepath.Join(localStatePath, \"live\", \".terragrunt-stack\", \"bar\")\n\tbazLocalStateDir := filepath.Join(localStatePath, \"live\", \".terragrunt-stack\", \"baz\")\n\n\trequire.DirExists(t, fooLocalStateDir)\n\trequire.DirExists(t, barLocalStateDir)\n\trequire.DirExists(t, bazLocalStateDir)\n\n\t// Check that state files are in the correct locations\n\trequire.FileExists(t, filepath.Join(fooLocalStateDir, \"tofu.tfstate\"))\n\trequire.FileExists(t, filepath.Join(barLocalStateDir, \"tofu.tfstate\"))\n\trequire.FileExists(t, filepath.Join(bazLocalStateDir, \"tofu.tfstate\"))\n\n\t// Since backend.tf is generated in the .terragrunt-cache directory during execution,\n\t// we verify the state files exist in the expected .terragrunt-local-state directory structure\n\t// This confirms that the backend configuration is working correctly\n\n\t// Verify the .terragrunt-local-state directory structure matches path_relative_to_include()\n\tliveStateDir := filepath.Join(localStatePath, \"live\", \".terragrunt-stack\")\n\trequire.DirExists(t, liveStateDir)\n\n\t// Clean up\n\thelpers.RunTerragrunt(t, \"terragrunt stack run destroy --non-interactive --working-dir \"+livePath)\n}\n\nfunc TestFilterDocumentationExamples(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDirRaw := helpers.TmpDirWOSymlinks(t)\n\ttmpDir, err := filepath.EvalSymlinks(tmpDirRaw)\n\trequire.NoError(t, err)\n\n\tgenerateNameBasedFixture(t, tmpDir)\n\tgenerateAttributeBasedFixture(t, tmpDir)\n\tgeneratePathBasedFixture(t, tmpDir)\n\tgenerateNegationFixture(t, tmpDir)\n\tgenerateIntersectionFixture(t, tmpDir)\n\tgenerateReadingFixture(t, tmpDir)\n\tgenerateGraphBasedFixture(t, tmpDir)\n\tgenerateSourceBasedFixture(t, tmpDir)\n\n\ttestCases := []struct {\n\t\tname           string\n\t\tfixtureDir     string\n\t\tfilterQuery    string\n\t\texpectedOutput string\n\t\textraFlags     string\n\t}{\n\t\t// Name-based filtering\n\t\t{\n\t\t\tname:           \"name-based-exact-match\",\n\t\t\tfixtureDir:     \"name-based\",\n\t\t\tfilterQuery:    \"app1\",\n\t\t\texpectedOutput: \"apps/app1\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"name-based-glob-pattern\",\n\t\t\tfixtureDir:     \"name-based\",\n\t\t\tfilterQuery:    \"app*\",\n\t\t\texpectedOutput: \"apps/app1\\napps/app2\\n\",\n\t\t},\n\n\t\t// Path-based filtering\n\t\t{\n\t\t\tname:           \"path-based-relative-exact-match\",\n\t\t\tfixtureDir:     \"path-based\",\n\t\t\tfilterQuery:    \"./envs/prod/apps/app1\",\n\t\t\texpectedOutput: \"envs/prod/apps/app1\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"path-based-relative-glob-pattern\",\n\t\t\tfixtureDir:     \"path-based\",\n\t\t\tfilterQuery:    \"./envs/stage/**\",\n\t\t\texpectedOutput: \"envs/stage/apps/app1\\nenvs/stage/apps/app2\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"path-based-absolute-exact-match\",\n\t\t\tfixtureDir:     \"path-based\",\n\t\t\tfilterQuery:    filepath.Join(tmpDir, \"path-based\", \"root\", \"envs\", \"dev\", \"apps\", \"*\"),\n\t\t\texpectedOutput: \"envs/dev/apps/app1\\nenvs/dev/apps/app2\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"path-based-braced-exact-match\",\n\t\t\tfixtureDir:     \"path-based\",\n\t\t\tfilterQuery:    \"{./envs/prod/apps/app2}\",\n\t\t\texpectedOutput: \"envs/prod/apps/app2\\n\",\n\t\t},\n\n\t\t// Attribute-based filtering\n\t\t{\n\t\t\tname:           \"attribute-type-unit\",\n\t\t\tfixtureDir:     \"attribute-based\",\n\t\t\tfilterQuery:    \"type=unit\",\n\t\t\texpectedOutput: \"unit1\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"attribute-type-stack\",\n\t\t\tfixtureDir:     \"attribute-based\",\n\t\t\tfilterQuery:    \"type=stack\",\n\t\t\texpectedOutput: \"stack1\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"attribute-based-external-false\",\n\t\t\tfixtureDir:     \"attribute-based\",\n\t\t\tfilterQuery:    \"{./*}... | external=false\",\n\t\t\texpectedOutput: \"stack1\\nunit1\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"attribute-based-external-true\",\n\t\t\tfixtureDir:     \"attribute-based\",\n\t\t\tfilterQuery:    \"{./*}... | external=true\",\n\t\t\texpectedOutput: \"../dependencies/dependency-of-app1\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"attribute-based-name-glob\",\n\t\t\tfixtureDir:     \"attribute-based\",\n\t\t\tfilterQuery:    \"name=stack*\",\n\t\t\texpectedOutput: \"stack1\\n\",\n\t\t},\n\n\t\t// Negation\n\t\t{\n\t\t\tname:           \"negation-by-name\",\n\t\t\tfixtureDir:     \"negation\",\n\t\t\tfilterQuery:    \"!app1\",\n\t\t\texpectedOutput: \"envs/prod/apps/app2\\nenvs/prod/stacks/stack1\\nenvs/stage/apps/app2\\nenvs/stage/stacks/stack1\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"negation-by-path\",\n\t\t\tfixtureDir:     \"negation\",\n\t\t\tfilterQuery:    \"!./envs/prod/**\",\n\t\t\texpectedOutput: \"envs/stage/apps/app1\\nenvs/stage/apps/app2\\nenvs/stage/stacks/stack1\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"negation-by-attribute\",\n\t\t\tfixtureDir:     \"negation\",\n\t\t\tfilterQuery:    \"!type=stack\",\n\t\t\texpectedOutput: \"envs/prod/apps/app1\\nenvs/prod/apps/app2\\nenvs/stage/apps/app1\\nenvs/stage/apps/app2\\n\",\n\t\t},\n\n\t\t// Intersection\n\t\t{\n\t\t\tname:           \"intersection-by-path-and-attribute\",\n\t\t\tfixtureDir:     \"intersection\",\n\t\t\tfilterQuery:    \"./prod/** | type=unit\",\n\t\t\texpectedOutput: \"prod/units/unit1\\nprod/units/unit2\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"intersection-by-path-and-negation\",\n\t\t\tfixtureDir:     \"intersection\",\n\t\t\tfilterQuery:    \"./prod/** | !type=unit\",\n\t\t\texpectedOutput: \"prod/stacks/stack1\\nprod/stacks/stack2\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"intersection-by-path-type-and-negation\",\n\t\t\tfixtureDir:     \"intersection\",\n\t\t\tfilterQuery:    \"./dev/** | type=unit | !name=unit1\",\n\t\t\texpectedOutput: \"dev/units/unit2\\n\",\n\t\t},\n\n\t\t// Reading attribute filtering\n\t\t{\n\t\t\tname:           \"reading-exact-file-match\",\n\t\t\tfixtureDir:     \"reading\",\n\t\t\tfilterQuery:    \"reading=shared.hcl\",\n\t\t\texpectedOutput: \"apps/app1\\napps/app2\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"reading-glob-pattern\",\n\t\t\tfixtureDir:     \"reading\",\n\t\t\tfilterQuery:    \"reading=shared*\",\n\t\t\texpectedOutput: \"apps/app1\\napps/app2\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"reading-nested-path\",\n\t\t\tfixtureDir:     \"reading\",\n\t\t\tfilterQuery:    \"reading=common/vars.hcl\",\n\t\t\texpectedOutput: \"apps/app3\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"reading-negation\",\n\t\t\tfixtureDir:     \"reading\",\n\t\t\tfilterQuery:    \"!reading=shared.hcl\",\n\t\t\texpectedOutput: \"apps/app3\\nlibs/lib1\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"reading-intersection\",\n\t\t\tfixtureDir:     \"reading\",\n\t\t\tfilterQuery:    \"./apps/** | reading=shared.hcl\",\n\t\t\texpectedOutput: \"apps/app1\\napps/app2\\n\",\n\t\t},\n\n\t\t// Graph-based filtering\n\t\t{\n\t\t\tname:           \"graph-dependency-traversal\",\n\t\t\tfixtureDir:     \"graph-based\",\n\t\t\tfilterQuery:    \"service...\",\n\t\t\texpectedOutput: \"cache\\ndb\\nservice\\nvpc\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"graph-dependent-traversal\",\n\t\t\tfixtureDir:     \"graph-based\",\n\t\t\tfilterQuery:    \"...vpc\",\n\t\t\texpectedOutput: \"cache\\ndb\\nservice\\nvpc\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"graph-both-directions\",\n\t\t\tfixtureDir:     \"graph-based\",\n\t\t\tfilterQuery:    \"...db...\",\n\t\t\texpectedOutput: \"db\\nservice\\nvpc\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"graph-exclude-target\",\n\t\t\tfixtureDir:     \"graph-based\",\n\t\t\tfilterQuery:    \"^service...\",\n\t\t\texpectedOutput: \"cache\\ndb\\nvpc\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"graph-with-path-filter\",\n\t\t\tfixtureDir:     \"graph-based\",\n\t\t\tfilterQuery:    \"{./service}...\",\n\t\t\texpectedOutput: \"cache\\ndb\\nservice\\nvpc\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"graph-with-attribute-filter\",\n\t\t\tfixtureDir:     \"graph-based\",\n\t\t\tfilterQuery:    \"...name=vpc\",\n\t\t\texpectedOutput: \"cache\\ndb\\nservice\\nvpc\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"graph-with-intersection\",\n\t\t\tfixtureDir:     \"graph-based\",\n\t\t\tfilterQuery:    \"service... | !^db...\",\n\t\t\texpectedOutput: \"cache\\ndb\\nservice\\n\",\n\t\t},\n\n\t\t// Depth-limited graph traversal\n\t\t{\n\t\t\tname:           \"graph-depth-limited-dependencies-1-level\",\n\t\t\tfixtureDir:     \"graph-based\",\n\t\t\tfilterQuery:    \"service...1\",\n\t\t\texpectedOutput: \"cache\\ndb\\nservice\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"graph-depth-limited-dependents-1-level\",\n\t\t\tfixtureDir:     \"graph-based\",\n\t\t\tfilterQuery:    \"1...vpc\",\n\t\t\texpectedOutput: \"cache\\ndb\\nvpc\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"graph-depth-limited-both-directions\",\n\t\t\tfixtureDir:     \"graph-based\",\n\t\t\tfilterQuery:    \"1...db...2\",\n\t\t\texpectedOutput: \"db\\nservice\\nvpc\\n\",\n\t\t},\n\n\t\t// Source-based filtering\n\t\t{\n\t\t\tname:           \"source-exact-match-github\",\n\t\t\tfixtureDir:     \"source-based\",\n\t\t\tfilterQuery:    \"source=github.com/acme/foo\",\n\t\t\texpectedOutput: \"github-acme-foo\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"source-exact-match-gitlab\",\n\t\t\tfixtureDir:     \"source-based\",\n\t\t\tfilterQuery:    \"source=gitlab.com/example/baz\",\n\t\t\texpectedOutput: \"gitlab-example-baz\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"source-exact-match-local\",\n\t\t\tfixtureDir:     \"source-based\",\n\t\t\tfilterQuery:    \"source=./module\",\n\t\t\texpectedOutput: \"local-module\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"source-glob-github-org\",\n\t\t\tfixtureDir:     \"source-based\",\n\t\t\tfilterQuery:    \"source=*github.com**acme/*\",\n\t\t\texpectedOutput: \"github-acme-bar\\ngithub-acme-foo\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"source-glob-github-ssh\",\n\t\t\tfixtureDir:     \"source-based\",\n\t\t\tfilterQuery:    \"source=git::git@github.com:acme/**\",\n\t\t\texpectedOutput: \"github-acme-bar\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"source-glob-all-github\",\n\t\t\tfixtureDir:     \"source-based\",\n\t\t\tfilterQuery:    \"source=**github.com**\",\n\t\t\texpectedOutput: \"github-acme-bar\\ngithub-acme-foo\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"source-glob-gitlab\",\n\t\t\tfixtureDir:     \"source-based\",\n\t\t\tfilterQuery:    \"source=gitlab.com/**\",\n\t\t\texpectedOutput: \"gitlab-example-baz\\n\",\n\t\t},\n\t}\n\n\tfor _, tt := range testCases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tfixturePath := filepath.Join(tmpDir, tt.fixtureDir)\n\t\t\tworkingDir := filepath.Join(fixturePath, \"root\")\n\n\t\t\tcommand := fmt.Sprintf(\n\t\t\t\t\"terragrunt find --no-color --filter '%s' %s --working-dir %s\",\n\t\t\t\ttt.filterQuery,\n\t\t\t\ttt.extraFlags,\n\t\t\t\tworkingDir,\n\t\t\t)\n\t\t\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, command)\n\n\t\t\tif err != nil {\n\t\t\t\tt.Logf(\"Command failed: %s\", command)\n\t\t\t\tt.Logf(\"Error: %v\", err)\n\t\t\t\tt.Logf(\"Output: %s\", stdout)\n\t\t\t}\n\n\t\t\trequire.NoError(t, err, \"Command should succeed\")\n\t\t\tassert.Equal(t, tt.expectedOutput, stdout, \"Output should match expected result\")\n\t\t})\n\t}\n}\n\nfunc TestFilterDocumentationExamplesWithUnion(t *testing.T) {\n\tt.Parallel()\n\n\t// Create temporary directory for dynamic fixtures\n\ttmpDirRaw := helpers.TmpDirWOSymlinks(t)\n\ttmpDir, err := filepath.EvalSymlinks(tmpDirRaw)\n\trequire.NoError(t, err)\n\n\t// Generate fixtures for testing\n\tgenerateUnionFixture(t, tmpDir)\n\n\t// Test cases based on the documentation examples\n\t// Note: These tests demonstrate the intended functionality and will be updated\n\t// as the filter feature matures and becomes fully functional\n\ttestCases := []struct {\n\t\tname           string\n\t\tfixtureDir     string\n\t\tfilterQueries  []string\n\t\texpectedOutput string\n\t}{\n\t\t{\n\t\t\tname:           \"union-by-two-names\",\n\t\t\tfixtureDir:     \"union\",\n\t\t\tfilterQueries:  []string{\"unit1\", \"stack1\"},\n\t\t\texpectedOutput: \"dev/stack1\\ndev/unit1\\nenvs/prod/stack1\\nenvs/prod/unit1\\nenvs/stage/stack1\\nenvs/stage/unit1\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"union-by-two-paths\",\n\t\t\tfixtureDir:     \"union\",\n\t\t\tfilterQueries:  []string{\"./envs/prod/**\", \"./envs/stage/**\"},\n\t\t\texpectedOutput: \"envs/prod/stack1\\nenvs/prod/stack2\\nenvs/prod/unit1\\nenvs/prod/unit2\\nenvs/stage/stack1\\nenvs/stage/stack2\\nenvs/stage/unit1\\nenvs/stage/unit2\\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"union-by-name-and-negation\",\n\t\t\tfixtureDir:     \"union\",\n\t\t\tfilterQueries:  []string{\"stack2\", \"!./envs/prod/**\", \"!./envs/stage/**\"},\n\t\t\texpectedOutput: \"dev/stack2\\n\",\n\t\t},\n\t}\n\n\t// Run all test cases\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tfixturePath := filepath.Join(tmpDir, tc.fixtureDir)\n\t\t\tworkingDir := filepath.Join(fixturePath, \"root\")\n\n\t\t\t// Run the find command with the filter\n\t\t\tvar filterArgs []string\n\t\t\tfor _, query := range tc.filterQueries {\n\t\t\t\tfilterArgs = append(filterArgs, fmt.Sprintf(\"--filter %s\", query))\n\t\t\t}\n\t\t\tcommand := fmt.Sprintf(\"terragrunt find %s --working-dir %s\", strings.Join(filterArgs, \" \"), workingDir)\n\t\t\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, command)\n\t\t\trequire.NoError(t, err, \"Command should succeed\")\n\n\t\t\tassert.Equal(t, tc.expectedOutput, stdout, \"Output should match expected result\")\n\t\t})\n\t}\n}\n\n// Helper functions to generate dynamic fixtures based on documentation examples\n\nfunc generateNameBasedFixture(t *testing.T, baseDir string) {\n\tfixtureDir := filepath.Join(baseDir, \"name-based\", \"root\", \"apps\")\n\trequire.NoError(t, os.MkdirAll(fixtureDir, 0755))\n\n\t// Create app1\n\tcreateTerragruntUnit(t, filepath.Join(fixtureDir, \"app1\"))\n\t// Create app2\n\tcreateTerragruntUnit(t, filepath.Join(fixtureDir, \"app2\"))\n\t// Create other (not matching the patterns)\n\tcreateTerragruntUnit(t, filepath.Join(fixtureDir, \"other\"))\n}\n\nfunc generateAttributeBasedFixture(t *testing.T, baseDir string) {\n\trootDir := filepath.Join(baseDir, \"attribute-based\", \"root\")\n\trequire.NoError(t, os.MkdirAll(rootDir, 0755))\n\n\t// Create unit1\n\tcreateTerragruntUnitWithDependency(t, filepath.Join(rootDir, \"unit1\"), \"../../dependencies/dependency-of-app1\")\n\t// Create stack1\n\tcreateTerragruntStack(t, filepath.Join(rootDir, \"stack1\"))\n\n\t// Create external dependency\n\tdepsDir := filepath.Join(baseDir, \"attribute-based\", \"dependencies\")\n\trequire.NoError(t, os.MkdirAll(depsDir, 0755))\n\tcreateTerragruntUnit(t, filepath.Join(depsDir, \"dependency-of-app1\"))\n}\n\nfunc generatePathBasedFixture(t *testing.T, baseDir string) {\n\trootDir := filepath.Join(baseDir, \"path-based\", \"root\")\n\trequire.NoError(t, os.MkdirAll(rootDir, 0755))\n\n\t// Create envs/prod/apps/app1\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"envs\", \"prod\", \"apps\", \"app1\"))\n\t// Create envs/prod/apps/app2\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"envs\", \"prod\", \"apps\", \"app2\"))\n\t// Create envs/stage/apps/app1\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"envs\", \"stage\", \"apps\", \"app1\"))\n\t// Create envs/stage/apps/app2\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"envs\", \"stage\", \"apps\", \"app2\"))\n\t// Create envs/dev/apps/app1\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"envs\", \"dev\", \"apps\", \"app1\"))\n\t// Create envs/dev/apps/app2\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"envs\", \"dev\", \"apps\", \"app2\"))\n}\n\nfunc generateNegationFixture(t *testing.T, baseDir string) {\n\trootDir := filepath.Join(baseDir, \"negation\", \"root\")\n\trequire.NoError(t, os.MkdirAll(rootDir, 0755))\n\n\t// Create envs/prod/apps/app1\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"envs\", \"prod\", \"apps\", \"app1\"))\n\t// Create envs/prod/apps/app2\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"envs\", \"prod\", \"apps\", \"app2\"))\n\t// Create envs/prod/stacks/stack1\n\tcreateTerragruntStack(t, filepath.Join(rootDir, \"envs\", \"prod\", \"stacks\", \"stack1\"))\n\t// Create envs/stage/apps/app1\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"envs\", \"stage\", \"apps\", \"app1\"))\n\t// Create envs/stage/apps/app2\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"envs\", \"stage\", \"apps\", \"app2\"))\n\t// Create envs/stage/stacks/stack1\n\tcreateTerragruntStack(t, filepath.Join(rootDir, \"envs\", \"stage\", \"stacks\", \"stack1\"))\n}\n\nfunc generateIntersectionFixture(t *testing.T, baseDir string) {\n\trootDir := filepath.Join(baseDir, \"intersection\", \"root\")\n\trequire.NoError(t, os.MkdirAll(rootDir, 0755))\n\n\t// Create prod/units/unit1\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"prod\", \"units\", \"unit1\"))\n\t// Create prod/units/unit2\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"prod\", \"units\", \"unit2\"))\n\t// Create prod/stacks/stack1\n\tcreateTerragruntStack(t, filepath.Join(rootDir, \"prod\", \"stacks\", \"stack1\"))\n\t// Create prod/stacks/stack2\n\tcreateTerragruntStack(t, filepath.Join(rootDir, \"prod\", \"stacks\", \"stack2\"))\n\t// Create dev/units/unit1\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"dev\", \"units\", \"unit1\"))\n\t// Create dev/units/unit2\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"dev\", \"units\", \"unit2\"))\n\t// Create dev/stacks/stack1\n\tcreateTerragruntStack(t, filepath.Join(rootDir, \"dev\", \"stacks\", \"stack1\"))\n\t// Create dev/stacks/stack2\n\tcreateTerragruntStack(t, filepath.Join(rootDir, \"dev\", \"stacks\", \"stack2\"))\n}\n\nfunc generateUnionFixture(t *testing.T, baseDir string) {\n\trootDir := filepath.Join(baseDir, \"union\", \"root\")\n\trequire.NoError(t, os.MkdirAll(rootDir, 0755))\n\n\t// Create envs/prod/unit1\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"envs\", \"prod\", \"unit1\"))\n\t// Create envs/prod/unit2\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"envs\", \"prod\", \"unit2\"))\n\t// Create envs/prod/stack1\n\tcreateTerragruntStack(t, filepath.Join(rootDir, \"envs\", \"prod\", \"stack1\"))\n\t// Create envs/prod/stack2\n\tcreateTerragruntStack(t, filepath.Join(rootDir, \"envs\", \"prod\", \"stack2\"))\n\t// Create envs/stage/unit1\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"envs\", \"stage\", \"unit1\"))\n\t// Create envs/stage/unit2\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"envs\", \"stage\", \"unit2\"))\n\t// Create envs/stage/stack1\n\tcreateTerragruntStack(t, filepath.Join(rootDir, \"envs\", \"stage\", \"stack1\"))\n\t// Create envs/stage/stack2\n\tcreateTerragruntStack(t, filepath.Join(rootDir, \"envs\", \"stage\", \"stack2\"))\n\t// Create dev/unit1\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"dev\", \"unit1\"))\n\t// Create dev/unit2\n\tcreateTerragruntUnit(t, filepath.Join(rootDir, \"dev\", \"unit2\"))\n\t// Create dev/stack1\n\tcreateTerragruntStack(t, filepath.Join(rootDir, \"dev\", \"stack1\"))\n\t// Create dev/stack2\n\tcreateTerragruntStack(t, filepath.Join(rootDir, \"dev\", \"stack2\"))\n}\n\nfunc generateReadingFixture(t *testing.T, baseDir string) {\n\trootDir := filepath.Join(baseDir, \"reading\", \"root\")\n\trequire.NoError(t, os.MkdirAll(rootDir, 0755))\n\n\t// Create shared configuration files\n\trequire.NoError(t, os.WriteFile(filepath.Join(rootDir, \"shared.hcl\"), []byte(`\nlocals {\n  common_value = \"shared\"\n}\n`), 0644))\n\n\trequire.NoError(t, os.WriteFile(filepath.Join(rootDir, \"shared.tfvars\"), []byte(`\ntest_var = \"value\"\n`), 0644))\n\n\tcommonDir := filepath.Join(rootDir, \"common\")\n\trequire.NoError(t, os.MkdirAll(commonDir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(commonDir, \"vars.hcl\"), []byte(`\nlocals {\n  vpc_cidr = \"10.0.0.0/16\"\n}\n`), 0644))\n\n\t// Create apps/app1 - reads shared.hcl\n\tapp1Dir := filepath.Join(rootDir, \"apps\", \"app1\")\n\trequire.NoError(t, os.MkdirAll(app1Dir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(app1Dir, \"terragrunt.hcl\"), []byte(`\nlocals {\n  shared = read_terragrunt_config(\"../../shared.hcl\")\n}\n\nterraform {\n  source = \".\"\n}\n`), 0644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(app1Dir, \"main.tf\"), []byte(\"\"), 0644))\n\n\t// Create apps/app2 - reads shared.hcl and shared.tfvars\n\tapp2Dir := filepath.Join(rootDir, \"apps\", \"app2\")\n\trequire.NoError(t, os.MkdirAll(app2Dir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(app2Dir, \"terragrunt.hcl\"), []byte(`\nlocals {\n  shared = read_terragrunt_config(\"../../shared.hcl\")\n  vars = read_tfvars_file(\"../../shared.tfvars\")\n}\n\nterraform {\n  source = \".\"\n}\n`), 0644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(app2Dir, \"main.tf\"), []byte(\"\"), 0644))\n\n\t// Create apps/app3 - reads common/vars.hcl\n\tapp3Dir := filepath.Join(rootDir, \"apps\", \"app3\")\n\trequire.NoError(t, os.MkdirAll(app3Dir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(app3Dir, \"terragrunt.hcl\"), []byte(`\nlocals {\n  common = read_terragrunt_config(\"../../common/vars.hcl\")\n}\n\nterraform {\n  source = \".\"\n}\n`), 0644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(app3Dir, \"main.tf\"), []byte(\"\"), 0644))\n\n\t// Create libs/lib1 - doesn't read any files\n\tlib1Dir := filepath.Join(rootDir, \"libs\", \"lib1\")\n\tcreateTerragruntUnit(t, lib1Dir)\n}\n\nfunc generateGraphBasedFixture(t *testing.T, baseDir string) {\n\trootDir := filepath.Join(baseDir, \"graph-based\", \"root\")\n\trequire.NoError(t, os.MkdirAll(rootDir, 0755))\n\n\t// Create a dependency graph:\n\t// vpc (no dependencies)\n\t// db -> vpc\n\t// cache -> vpc\n\t// service -> db, cache\n\n\t// Create vpc (base dependency)\n\tvpcDir := filepath.Join(rootDir, \"vpc\")\n\tcreateTerragruntUnit(t, vpcDir)\n\n\t// Create db (depends on vpc)\n\tdbDir := filepath.Join(rootDir, \"db\")\n\tcreateTerragruntUnitWithDependency(t, dbDir, \"../vpc\")\n\n\t// Create cache (depends on vpc)\n\tcacheDir := filepath.Join(rootDir, \"cache\")\n\tcreateTerragruntUnitWithDependency(t, cacheDir, \"../vpc\")\n\n\t// Create service (depends on db and cache)\n\tserviceDir := filepath.Join(rootDir, \"service\")\n\trequire.NoError(t, os.MkdirAll(serviceDir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(serviceDir, \"terragrunt.hcl\"), []byte(`terraform {\n\tsource = \".\"\n}\n\ndependency \"db\" {\n\tconfig_path = \"../db\"\n}\n\ndependency \"cache\" {\n\tconfig_path = \"../cache\"\n}\n`), 0644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(serviceDir, \"main.tf\"), []byte(\"\"), 0644))\n}\n\nfunc generateSourceBasedFixture(t *testing.T, baseDir string) {\n\trootDir := filepath.Join(baseDir, \"source-based\", \"root\")\n\trequire.NoError(t, os.MkdirAll(rootDir, 0755))\n\n\t// Create github-acme-foo with source github.com/acme/foo\n\tgithubAcmeFooDir := filepath.Join(rootDir, \"github-acme-foo\")\n\trequire.NoError(t, os.MkdirAll(githubAcmeFooDir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(githubAcmeFooDir, \"terragrunt.hcl\"), []byte(`terraform {\n  source = \"github.com/acme/foo\"\n}\n`), 0644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(githubAcmeFooDir, \"main.tf\"), []byte(\"\"), 0644))\n\n\t// Create github-acme-bar with source git::git@github.com:acme/bar\n\tgithubAcmeBarDir := filepath.Join(rootDir, \"github-acme-bar\")\n\trequire.NoError(t, os.MkdirAll(githubAcmeBarDir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(githubAcmeBarDir, \"terragrunt.hcl\"), []byte(`terraform {\n  source = \"git::git@github.com:acme/bar\"\n}\n`), 0644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(githubAcmeBarDir, \"main.tf\"), []byte(\"\"), 0644))\n\n\t// Create gitlab-example-baz with source gitlab.com/example/baz\n\tgitlabExampleBazDir := filepath.Join(rootDir, \"gitlab-example-baz\")\n\trequire.NoError(t, os.MkdirAll(gitlabExampleBazDir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(gitlabExampleBazDir, \"terragrunt.hcl\"), []byte(`terraform {\n  source = \"gitlab.com/example/baz\"\n}\n`), 0644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(gitlabExampleBazDir, \"main.tf\"), []byte(\"\"), 0644))\n\n\t// Create local-module with source ./module\n\tlocalModuleDir := filepath.Join(rootDir, \"local-module\")\n\trequire.NoError(t, os.MkdirAll(localModuleDir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(localModuleDir, \"terragrunt.hcl\"), []byte(`terraform {\n  source = \"./module\"\n}\n`), 0644))\n\t// Create the module directory with main.tf\n\tmoduleDir := filepath.Join(localModuleDir, \"module\")\n\trequire.NoError(t, os.MkdirAll(moduleDir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(moduleDir, \"main.tf\"), []byte(\"\"), 0644))\n\n\t// Create other-unit with source s3://bucket/module (for non-matching examples)\n\totherUnitDir := filepath.Join(rootDir, \"other-unit\")\n\trequire.NoError(t, os.MkdirAll(otherUnitDir, 0755))\n\trequire.NoError(t, os.WriteFile(filepath.Join(otherUnitDir, \"terragrunt.hcl\"), []byte(`terraform {\n  source = \"s3://bucket/module\"\n}\n`), 0644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(otherUnitDir, \"main.tf\"), []byte(\"\"), 0644))\n}\n\n// Helper functions to create Terragrunt configuration files\n\nfunc createTerragruntUnit(t *testing.T, dir string) {\n\trequire.NoError(t, os.MkdirAll(dir, 0755))\n\t// Create minimal terragrunt.hcl file\n\trequire.NoError(t, os.WriteFile(filepath.Join(dir, \"terragrunt.hcl\"), []byte(\"terraform {\\n  source = \\\".\\\"\\n}\"), 0644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(dir, \"main.tf\"), []byte(\"\"), 0644))\n}\n\nfunc createTerragruntStack(t *testing.T, dir string) {\n\trequire.NoError(t, os.MkdirAll(dir, 0755))\n\t// Create minimal terragrunt.stack.hcl file\n\trequire.NoError(t, os.WriteFile(filepath.Join(dir, \"terragrunt.stack.hcl\"), []byte(\"terraform {\\n  source = \\\".\\\"\\n}\"), 0644))\n}\n\nfunc createTerragruntUnitWithDependency(t *testing.T, dir, dep string) {\n\trequire.NoError(t, os.MkdirAll(dir, 0755))\n\t// Create minimal terragrunt.hcl file\n\trequire.NoError(t, os.WriteFile(filepath.Join(dir, \"terragrunt.hcl\"), []byte(`terraform {\n\tsource = \".\"\n}\n\ndependency \"dep\" {\n\tconfig_path = \"`+dep+`\"\n}\n`), 0644))\n\trequire.NoError(t, os.WriteFile(filepath.Join(dir, \"main.tf\"), []byte(\"\"), 0644))\n}\n"
  },
  {
    "path": "test/integration_download_test.go",
    "content": "package test_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureLocalDownloadPath                      = \"fixtures/download/local\"\n\ttestFixtureCustomLockFile                         = \"fixtures/download/custom-lock-file\"\n\ttestFixtureRemoteDownloadPath                     = \"fixtures/download/remote\"\n\ttestFixtureInvalidRemoteDownloadPath              = \"fixtures/download/remote-invalid\"\n\ttestFixtureInvalidRemoteDownloadPathWithRetries   = \"fixtures/download/remote-invalid-with-retries\"\n\ttestFixtureOverrideDownloadPath                   = \"fixtures/download/override\"\n\ttestFixtureLocalRelativeDownloadPath              = \"fixtures/download/local-relative\"\n\ttestFixtureRemoteRelativeDownloadPath             = \"fixtures/download/remote-relative\"\n\ttestFixtureRemoteRelativeDownloadPathWithSlash    = \"fixtures/download/remote-relative-with-slash\"\n\ttestFixtureLocalWithBackend                       = \"fixtures/download/local-with-backend\"\n\ttestFixtureLocalWithExcludeDir                    = \"fixtures/download/local-with-exclude-dir\"\n\ttestFixtureLocalWithIncludeDir                    = \"fixtures/download/local-with-include-dir\"\n\ttestFixtureRemoteWithBackend                      = \"fixtures/download/remote-with-backend\"\n\ttestFixtureRemoteModuleInRoot                     = \"fixtures/download/remote-module-in-root\"\n\ttestFixtureLocalMissingBackend                    = \"fixtures/download/local-with-missing-backend\"\n\ttestFixtureLocalWithHiddenFolder                  = \"fixtures/download/local-with-hidden-folder\"\n\ttestFixtureLocalWithAllowedHidden                 = \"fixtures/download/local-with-allowed-hidden\"\n\ttestFixtureLocalPreventDestroy                    = \"fixtures/download/local-with-prevent-destroy\"\n\ttestFixtureLocalPreventDestroyDependencies        = \"fixtures/download/local-with-prevent-destroy-dependencies\"\n\ttestFixtureLocalIncludePreventDestroyDependencies = \"fixtures/download/local-include-with-prevent-destroy-dependencies\"\n\ttestFixtureNotExistingSource                      = \"fixtures/download/invalid-path\"\n\ttestFixtureDisableCopyLockFilePath                = \"fixtures/download/local-disable-copy-terraform-lock-file\"\n\ttestFixtureIncludeDisableCopyLockFilePath         = \"fixtures/download/local-include-disable-copy-lock-file/module-b\"\n)\n\nfunc TestLocalDownload(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/download\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLocalDownloadPath)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\t// As of Terraform 0.14.0 we should be copying the lock file from .terragrunt-cache to the working directory\n\tassert.FileExists(t, filepath.Join(rootPath, util.TerraformLockFile))\n\n\t// Run a second time to make sure the temporary folder can be reused without errors\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n}\n\nfunc TestLocalDownloadDisableCopyTerraformLockFile(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/download\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureDisableCopyLockFilePath)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\t// The terraform lock file should not be copied if `copy_terraform_lock_file = false`\n\tassert.NoFileExists(t, filepath.Join(rootPath, util.TerraformLockFile))\n\n\t// Run a second time to make sure the temporary folder can be reused without errors\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n}\n\nfunc TestLocalIncludeDisableCopyTerraformLockFile(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/download\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureIncludeDisableCopyLockFilePath)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\t// The terraform lock file should not be copied if `copy_terraform_lock_file = false`\n\tassert.NoFileExists(t, filepath.Join(rootPath, util.TerraformLockFile))\n\n\t// Run a second time to make sure the temporary folder can be reused without errors\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n}\n\nfunc TestLocalDownloadWithHiddenFolder(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/download\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLocalWithHiddenFolder)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\t// Run a second time to make sure the temporary folder can be reused without errors\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n}\n\nfunc TestLocalDownloadWithAllowedHiddenFiles(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureLocalWithAllowedHidden)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLocalWithAllowedHidden)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\t// CopyFolderContents skips hidden files; the fixture's modules/.nonce is required for the apply.\n\tnoncePath := filepath.Join(rootPath, \"modules\", \".nonce\")\n\trequire.NoError(t, os.WriteFile(noncePath, []byte(\"Hello world\\n\"), 0600))\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --working-dir %s/live\", rootPath))\n\n\t// Run a second time to make sure the temporary folder can be reused without errors\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --working-dir %s/live\", rootPath))\n\n\t// Validate that the hidden file was copied\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\terr := helpers.RunTerragruntCommand(t, fmt.Sprintf(\"terragrunt output -raw text --non-interactive --working-dir %s/live\", rootPath), &stdout, &stderr)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"output stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"output stderr\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"Hello world\", stdout.String())\n}\n\nfunc TestLocalDownloadWithRelativePath(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/download\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLocalRelativeDownloadPath)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\t// Run a second time to make sure the temporary folder can be reused without errors\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n}\n\nfunc TestLocalWithMissingBackend(t *testing.T) {\n\tt.Parallel()\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tlockTableName := \"terragrunt-lock-table-\" + strings.ToLower(helpers.UniqueID())\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/download\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLocalMissingBackend)\n\n\trootTerragruntConfigPath := filepath.Join(rootPath, config.DefaultTerragruntConfigPath)\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, lockTableName, \"not-used\")\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath, os.Stdout, os.Stderr)\n\tif assert.Error(t, err) {\n\t\tvar target run.BackendNotDefined\n\t\tassert.ErrorAs(t, err, &target)\n\t}\n}\n\nfunc TestRemoteDownload(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRemoteDownloadPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureRemoteDownloadPath)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\t// Run a second time to make sure the temporary folder can be reused without errors\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n}\n\nfunc TestInvalidRemoteDownload(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureInvalidRemoteDownloadPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureInvalidRemoteDownloadPath)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\tapplyStdout := bytes.Buffer{}\n\tapplyStderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath, &applyStdout, &applyStderr)\n\n\thelpers.LogBufferContentsLineByLine(t, applyStdout, \"apply stdout\")\n\thelpers.LogBufferContentsLineByLine(t, applyStderr, \"apply stderr\")\n\n\trequire.Error(t, err)\n\n\terrMessage := \"downloading source url\"\n\tassert.Containsf(t, err.Error(), errMessage, \"expected error containing %q, got %s\", errMessage, err)\n}\n\nfunc TestInvalidRemoteDownloadWithRetries(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureInvalidRemoteDownloadPathWithRetries)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureInvalidRemoteDownloadPathWithRetries)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\trequire.Error(t, err)\n\n\terrMessage := \"max retry attempts (2) reached for error\"\n\tassert.Containsf(t, err.Error(), errMessage, \"expected error containing %q, got %s\", errMessage, err)\n}\n\nfunc TestRemoteDownloadWithRelativePath(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRemoteRelativeDownloadPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureRemoteRelativeDownloadPath)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\t// Run a second time to make sure the temporary folder can be reused without errors\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n}\n\nfunc TestRemoteDownloadWithRelativePathAndSlashInBranch(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRemoteRelativeDownloadPathWithSlash)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureRemoteRelativeDownloadPathWithSlash)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\t// Run a second time to make sure the temporary folder can be reused without errors\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n}\n\nfunc TestRemoteDownloadOverride(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/download\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureOverrideDownloadPath)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --working-dir %s --source %s\", rootPath, \"../hello-world\"))\n\n\t// Run a second time to make sure the temporary folder can be reused without errors\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --working-dir %s --source %s\", rootPath, \"../hello-world\"))\n}\n\nfunc TestRemoteWithModuleInRoot(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRemoteModuleInRoot)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureRemoteModuleInRoot)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\t// Run a second time to make sure the temporary folder can be reused without errors\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n}\n\n// As of Terraform 0.14.0, if there's already a lock file in the working directory, we should be copying it into\n// .terragrunt-cache\nfunc TestCustomLockFile(t *testing.T) {\n\tt.Parallel()\n\n\tpath := fmt.Sprintf(\"%s-%s\", testFixtureCustomLockFile, wrappedBinary())\n\ttmpEnvPath := helpers.CopyEnvironment(t, filepath.Dir(testFixtureCustomLockFile))\n\trootPath := filepath.Join(tmpEnvPath, path)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\tsource := \"../custom-lock-file-module\"\n\tdownloadDir := filepath.Join(rootPath, helpers.TerragruntCache)\n\tresult, err := tf.NewSource(createLogger(), source, downloadDir, rootPath, false)\n\trequire.NoError(t, err)\n\n\tlockFilePath := filepath.Join(result.WorkingDir, util.TerraformLockFile)\n\tassert.FileExists(t, lockFilePath)\n\n\treadFile, err := os.ReadFile(lockFilePath)\n\trequire.NoError(t, err)\n\n\t// In our lock file, we intentionally have hashes for an older version of the AWS provider. If the lock file\n\t// copying works, then Terraform will stick with this older version. If there is a bug, Terraform will end up\n\t// installing a newer version (since the version is not pinned in the .tf code, only in the lock file).\n\tassert.Contains(t, string(readFile), `version     = \"5.23.0\"`)\n}\n\nfunc TestExcludeDirs(t *testing.T) {\n\tt.Parallel()\n\n\t// Populate module paths.\n\tmoduleNames := []string{\n\t\t\"integration-env/aws/module-aws-a\",\n\t\t\"integration-env/gce/module-gce-b\",\n\t\t\"integration-env/gce/module-gce-c\",\n\t\t\"production-env/aws/module-aws-d\",\n\t\t\"production-env/gce/module-gce-e\",\n\t}\n\n\ttestCases := []struct {\n\t\tname                  string\n\t\texcludeArgs           string\n\t\texcludedModuleOutputs []string\n\t}{\n\t\t{\n\t\t\tname:                  \"exclude gce modules with double star\",\n\t\t\texcludeArgs:           \"--queue-exclude-dir **/gce/**\",\n\t\t\texcludedModuleOutputs: []string{\"Module GCE B\", \"Module GCE C\", \"Module GCE E\"},\n\t\t},\n\t\t{\n\t\t\tname:                  \"exclude production env and gce c modules with double star\",\n\t\t\texcludeArgs:           \"--queue-exclude-dir production-env/**/* --queue-exclude-dir **/module-gce-c\",\n\t\t\texcludedModuleOutputs: []string{\"Module GCE C\", \"Module AWS D\", \"Module GCE E\"},\n\t\t},\n\t\t{\n\t\t\tname:                  \"exclude integration env gce b and c modules and aws modules with double star\",\n\t\t\texcludeArgs:           \"--queue-exclude-dir integration-env/gce/module-gce-b --queue-exclude-dir integration-env/gce/module-gce-c --queue-exclude-dir **/module-aws*\",\n\t\t\texcludedModuleOutputs: []string{\"Module AWS A\", \"Module GCE B\", \"Module GCE C\", \"Module AWS D\"},\n\t\t},\n\t}\n\n\tfor _, tt := range testCases {\n\t\ttmpDir := helpers.CopyEnvironment(t, \"fixtures/download\")\n\t\tworkingDir := filepath.Join(tmpDir, testFixtureLocalWithExcludeDir)\n\t\tworkingDir, err := filepath.EvalSymlinks(workingDir)\n\t\trequire.NoError(t, err)\n\n\t\tmodulePaths := make(map[string]string, len(moduleNames))\n\t\tfor _, moduleName := range moduleNames {\n\t\t\tmodulePaths[moduleName] = filepath.Join(workingDir, moduleName)\n\t\t}\n\n\t\tapplyAllStdout := bytes.Buffer{}\n\t\tapplyAllStderr := bytes.Buffer{}\n\n\t\t// Apply modules according to test cases\n\t\terr = helpers.RunTerragruntCommand(\n\t\t\tt,\n\t\t\tfmt.Sprintf(\n\t\t\t\t\"terragrunt run --all apply --non-interactive --working-dir %s %s\",\n\t\t\t\tworkingDir,\n\t\t\t\ttt.excludeArgs,\n\t\t\t),\n\t\t\t&applyAllStdout,\n\t\t\t&applyAllStderr,\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\thelpers.LogBufferContentsLineByLine(t, applyAllStdout, \"run --all apply stdout\")\n\t\thelpers.LogBufferContentsLineByLine(t, applyAllStderr, \"run --all apply stderr\")\n\n\t\t// Check that the excluded module output is not present\n\t\tfor _, modulePath := range modulePaths {\n\t\t\tshowStdout := bytes.Buffer{}\n\t\t\tshowStderr := bytes.Buffer{}\n\n\t\t\terr = helpers.RunTerragruntCommand(\n\t\t\t\tt,\n\t\t\t\t\"terragrunt show --non-interactive --working-dir \"+modulePath,\n\t\t\t\t&showStdout,\n\t\t\t\t&showStderr,\n\t\t\t)\n\t\t\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout for \"+modulePath)\n\t\t\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr for \"+modulePath)\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\toutput := showStdout.String()\n\t\t\tfor _, excludedModuleOutput := range tt.excludedModuleOutputs {\n\t\t\t\tassert.NotContains(t, output, excludedModuleOutput)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestExcludeDirsWithFilter(t *testing.T) {\n\tt.Parallel()\n\n\t// Populate module paths.\n\tmoduleNames := []string{\n\t\t\"integration-env/aws/module-aws-a\",\n\t\t\"integration-env/gce/module-gce-b\",\n\t\t\"integration-env/gce/module-gce-c\",\n\t\t\"production-env/aws/module-aws-d\",\n\t\t\"production-env/gce/module-gce-e\",\n\t}\n\n\ttestCases := []struct {\n\t\tname                  string\n\t\texcludeArgs           string\n\t\texcludedModuleOutputs []string\n\t}{\n\t\t{\n\t\t\tname:                  \"exclude gce modules\",\n\t\t\texcludeArgs:           \"--filter '!./**/gce/**'\",\n\t\t\texcludedModuleOutputs: []string{\"Module GCE B\", \"Module GCE C\", \"Module GCE E\"},\n\t\t},\n\t\t{\n\t\t\tname:                  \"exclude production env and gce c modules\",\n\t\t\texcludeArgs:           \"--filter '!./production-env/**' --filter '!./**/module-gce-c'\",\n\t\t\texcludedModuleOutputs: []string{\"Module GCE C\", \"Module AWS D\", \"Module GCE E\"},\n\t\t},\n\t\t{\n\t\t\tname:                  \"exclude integration env gce b and c modules and aws modules\",\n\t\t\texcludeArgs:           \"--filter '!./integration-env/gce/module-gce-b' --filter '!./integration-env/gce/module-gce-c' --filter '!./**/module-aws*'\",\n\t\t\texcludedModuleOutputs: []string{\"Module AWS A\", \"Module GCE B\", \"Module GCE C\", \"Module AWS D\"},\n\t\t},\n\t}\n\n\tfor _, tt := range testCases {\n\t\ttmpDir := helpers.CopyEnvironment(t, \"fixtures/download\")\n\t\tworkingDir := filepath.Join(tmpDir, testFixtureLocalWithExcludeDir)\n\t\tworkingDir, err := filepath.EvalSymlinks(workingDir)\n\t\trequire.NoError(t, err)\n\n\t\tmodulePaths := make(map[string]string, len(moduleNames))\n\t\tfor _, moduleName := range moduleNames {\n\t\t\tmodulePaths[moduleName] = filepath.Join(workingDir, moduleName)\n\t\t}\n\n\t\t// Apply modules according to test cases\n\t\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\t\tt,\n\t\t\tfmt.Sprintf(\n\t\t\t\t\"terragrunt run --all apply --non-interactive --working-dir %s %s\",\n\t\t\t\tworkingDir,\n\t\t\t\ttt.excludeArgs,\n\t\t\t),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\t// Check that the excluded module output is not present\n\t\tfor _, modulePath := range modulePaths {\n\t\t\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt show --non-interactive --working-dir \"+modulePath)\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\toutput := stdout\n\t\t\tfor _, excludedModuleOutput := range tt.excludedModuleOutputs {\n\t\t\t\tassert.NotContains(t, output, excludedModuleOutput)\n\t\t\t}\n\t\t}\n\t}\n}\n\n/*\n\tTestIncludeDirs tests that the --queue-include-dir flag works as expected.\n\nMAINTAINER NOTE: Why is this test _so slow_? It took 2 mins on my machine...\n\nWe really need to start reporting on test durations and decide on a budget for each test.\nI'm not sure we're getting good value from the time taken on tests like this.\n*/\nfunc TestIncludeDirs(t *testing.T) {\n\tt.Parallel()\n\n\t// Populate module paths.\n\tunitNames := []string{\n\t\t\"integration-env/aws/module-aws-a\",\n\t\t\"integration-env/gce/module-gce-b\",\n\t\t\"integration-env/gce/module-gce-c\",\n\t\t\"production-env/aws/module-aws-d\",\n\t\t\"production-env/gce/module-gce-e\",\n\t}\n\n\ttestCases := []struct {\n\t\tname                string\n\t\tincludeArgs         string\n\t\tincludedUnitOutputs []string\n\t}{\n\t\t{\n\t\t\tname:                \"no-match\",\n\t\t\tincludeArgs:         \"--queue-include-dir xyz\",\n\t\t\tincludedUnitOutputs: []string{},\n\t\t},\n\t\t{\n\t\t\tname:                \"wildcard-aws\",\n\t\t\tincludeArgs:         \"--queue-include-dir */aws\",\n\t\t\tincludedUnitOutputs: []string{\"Module GCE B\", \"Module GCE C\", \"Module GCE E\"},\n\t\t},\n\t\t{\n\t\t\tname:                \"production-and-gce-c\",\n\t\t\tincludeArgs:         \"--queue-include-dir production-env --queue-include-dir **/module-gce-c\",\n\t\t\tincludedUnitOutputs: []string{\"Module GCE B\", \"Module AWS A\"},\n\t\t},\n\t\t{\n\t\t\tname:                \"specific-modules\",\n\t\t\tincludeArgs:         \"--queue-include-dir integration-env/gce/module-gce-b --queue-include-dir integration-env/gce/module-gce-c --queue-include-dir **/module-aws*\",\n\t\t\tincludedUnitOutputs: []string{\"Module GCE E\"},\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.Parallel()\n\n\t\t\ttmpDir := helpers.CopyEnvironment(t, \"fixtures/download\")\n\t\t\tworkingDir := filepath.Join(tmpDir, testFixtureLocalWithIncludeDir)\n\t\t\tworkingDir, err := filepath.EvalSymlinks(workingDir)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tunitPaths := make(map[string]string, len(unitNames))\n\t\t\tfor _, unitName := range unitNames {\n\t\t\t\tunitPaths[unitName] = filepath.Join(workingDir, unitName)\n\t\t\t}\n\n\t\t\tapplyAllStdout := bytes.Buffer{}\n\t\t\tapplyAllStderr := bytes.Buffer{}\n\n\t\t\t// Apply modules according to test case\n\t\t\terr = helpers.RunTerragruntCommand(\n\t\t\t\tt,\n\t\t\t\tfmt.Sprintf(\n\t\t\t\t\t\"terragrunt run --all apply --non-interactive --working-dir %s %s\",\n\t\t\t\t\tworkingDir, tc.includeArgs,\n\t\t\t\t),\n\t\t\t\t&applyAllStdout,\n\t\t\t\t&applyAllStderr,\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\thelpers.LogBufferContentsLineByLine(t, applyAllStdout, \"run --all apply stdout\")\n\t\t\thelpers.LogBufferContentsLineByLine(t, applyAllStderr, \"run --all apply stderr\")\n\n\t\t\t// Check that the included module output is present\n\t\t\tfor _, modulePath := range unitPaths {\n\t\t\t\tshowStdout := bytes.Buffer{}\n\t\t\t\tshowStderr := bytes.Buffer{}\n\n\t\t\t\terr = helpers.RunTerragruntCommand(\n\t\t\t\t\tt,\n\t\t\t\t\t\"terragrunt show --non-interactive --working-dir \"+modulePath,\n\t\t\t\t\t&showStdout,\n\t\t\t\t\t&showStderr,\n\t\t\t\t)\n\t\t\t\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout for \"+modulePath)\n\t\t\t\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr for \"+modulePath)\n\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\toutput := showStdout.String()\n\t\t\t\tfor _, includedUnitOutput := range tc.includedUnitOutputs {\n\t\t\t\t\tassert.NotContains(t, output, includedUnitOutput)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n/*\n\tTestIncludeDirsWithFilter tests that the --filter flag works as expected, just like in TestIncludeDirs.\n\nMAINTAINER NOTE: Why is this test _so slow_? It took 2 mins on my machine...\n\nWe really need to start reporting on test durations and decide on a budget for each test.\nI'm not sure we're getting good value from the time taken on tests like this.\n*/\nfunc TestIncludeDirsWithFilter(t *testing.T) {\n\tt.Parallel()\n\n\t// Copy the entire download fixture directory to ensure all referenced sources are available\n\ttmpDir := helpers.CopyEnvironment(t, \"fixtures/download\")\n\tworkingDir := filepath.Join(tmpDir, testFixtureLocalWithIncludeDir)\n\tworkingDir, err := filepath.EvalSymlinks(workingDir)\n\trequire.NoError(t, err)\n\n\t// Populate paths.\n\tunitNames := []string{\n\t\t\"integration-env/aws/module-aws-a\",\n\t\t\"integration-env/gce/module-gce-b\",\n\t\t\"integration-env/gce/module-gce-c\",\n\t\t\"production-env/aws/module-aws-d\",\n\t\t\"production-env/gce/module-gce-e\",\n\t}\n\n\ttestCases := []struct {\n\t\tincludeArgs         string\n\t\tincludedUnitOutputs []string\n\t}{\n\t\t{\n\t\t\tincludeArgs:         \"--filter xyz\",\n\t\t\tincludedUnitOutputs: []string{},\n\t\t},\n\t\t{\n\t\t\tincludeArgs:         \"--filter ./*/aws/*\",\n\t\t\tincludedUnitOutputs: []string{\"Module GCE B\", \"Module GCE C\", \"Module GCE E\"},\n\t\t},\n\t\t{\n\t\t\tincludeArgs:         \"--filter production-env --filter ./**/module-gce-c\",\n\t\t\tincludedUnitOutputs: []string{\"Module GCE B\", \"Module AWS A\"},\n\t\t},\n\t\t{\n\t\t\tincludeArgs:         \"--filter ./integration-env/gce/module-gce-b --filter ./integration-env/gce/module-gce-c --filter ./**/module-aws**\",\n\t\t\tincludedUnitOutputs: []string{\"Module GCE E\"},\n\t\t},\n\t}\n\n\tunitPaths := make(map[string]string, len(unitNames))\n\tfor _, unitName := range unitNames {\n\t\tunitPaths[unitName] = filepath.Join(workingDir, unitName)\n\t}\n\n\tfor _, tc := range testCases {\n\t\tapplyAllStdout := bytes.Buffer{}\n\t\tapplyAllStderr := bytes.Buffer{}\n\n\t\t// Cleanup all modules directories.\n\t\thelpers.CleanupTerragruntFolder(t, workingDir)\n\n\t\tfor _, unitPath := range unitPaths {\n\t\t\thelpers.CleanupTerragruntFolder(t, unitPath)\n\t\t}\n\n\t\t// Apply modules according to test cases\n\t\terr := helpers.RunTerragruntCommand(\n\t\t\tt,\n\t\t\tfmt.Sprintf(\n\t\t\t\t\"terragrunt run --all apply --non-interactive --working-dir %s %s\",\n\t\t\t\tworkingDir, tc.includeArgs,\n\t\t\t),\n\t\t\t&applyAllStdout,\n\t\t\t&applyAllStderr,\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\thelpers.LogBufferContentsLineByLine(t, applyAllStdout, \"run --all apply stdout\")\n\t\thelpers.LogBufferContentsLineByLine(t, applyAllStderr, \"run --all apply stderr\")\n\n\t\t// Check that the included module output is present\n\t\tfor _, unitPath := range unitPaths {\n\t\t\tshowStdout := bytes.Buffer{}\n\t\t\tshowStderr := bytes.Buffer{}\n\n\t\t\terr = helpers.RunTerragruntCommand(\n\t\t\t\tt,\n\t\t\t\t\"terragrunt show --non-interactive --working-dir \"+unitPath,\n\t\t\t\t&showStdout,\n\t\t\t\t&showStderr,\n\t\t\t)\n\t\t\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout for \"+unitPath)\n\t\t\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr for \"+unitPath)\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\toutput := showStdout.String()\n\t\t\tfor _, includedUnitOutput := range tc.includedUnitOutputs {\n\t\t\t\tassert.NotContains(t, output, includedUnitOutput)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestIncludeDirsDependencyConsistencyRegression(t *testing.T) {\n\tt.Parallel()\n\n\tmodulePaths := []string{\n\t\t\"amazing-app/k8s\",\n\t\t\"clusters/eks\",\n\t\t\"testapp/k8s\",\n\t}\n\n\ttmpPath, err := filepath.EvalSymlinks(helpers.CopyEnvironment(t, testFixtureRegressions))\n\trequire.NoError(t, err)\n\n\ttestPath := filepath.Join(tmpPath, testFixtureRegressions, \"exclude-dependency\")\n\tfor _, modulePath := range modulePaths {\n\t\thelpers.CleanupTerragruntFolder(t, filepath.Join(testPath, modulePath))\n\t}\n\n\tincludedModulesWithNone := helpers.RunValidateAllWithFilteredPlusDependenciesAndGetIncludedModules(t, testPath, []string{})\n\tassert.NotEmpty(t, includedModulesWithNone)\n\n\tincludedModulesWithAmzApp := helpers.RunValidateAllWithFilteredPlusDependenciesAndGetIncludedModules(\n\t\tt,\n\t\ttestPath,\n\t\t[]string{\"amazing-app/k8s\"},\n\t)\n\tassert.Equal(\n\t\tt,\n\t\t[]string{\"amazing-app/k8s\", \"clusters/eks\"},\n\t\tincludedModulesWithAmzApp,\n\t)\n\n\tincludedModulesWithTestApp := helpers.RunValidateAllWithFilteredPlusDependenciesAndGetIncludedModules(\n\t\tt,\n\t\ttestPath,\n\t\t[]string{\"testapp/k8s\"},\n\t)\n\tassert.Equal(\n\t\tt,\n\t\t[]string{\"clusters/eks\", \"testapp/k8s\"},\n\t\tincludedModulesWithTestApp,\n\t)\n}\n\nfunc TestIncludeDirsStrict(t *testing.T) {\n\tt.Parallel()\n\n\tmodulePaths := []string{\n\t\t\"amazing-app/k8s\",\n\t\t\"clusters/eks\",\n\t\t\"testapp/k8s\",\n\t}\n\n\ttmpPath, err := filepath.EvalSymlinks(helpers.CopyEnvironment(t, testFixtureRegressions))\n\trequire.NoError(t, err)\n\n\ttestPath := filepath.Join(tmpPath, testFixtureRegressions, \"exclude-dependency\")\n\thelpers.CleanupTerragruntFolder(t, testPath)\n\n\tfor _, modulePath := range modulePaths {\n\t\thelpers.CleanupTerragruntFolder(t, filepath.Join(testPath, modulePath))\n\t}\n\n\tincludedModulesWithAmzApp := helpers.RunValidateAllWithIncludeAndGetIncludedModules(\n\t\tt,\n\t\ttestPath,\n\t\t[]string{\"amazing-app/k8s\"},\n\t)\n\tassert.Equal(\n\t\tt,\n\t\t[]string{\"amazing-app/k8s\"},\n\t\tincludedModulesWithAmzApp,\n\t)\n\n\tincludedModulesWithTestApp := helpers.RunValidateAllWithIncludeAndGetIncludedModules(\n\t\tt,\n\t\ttestPath,\n\t\t[]string{\"testapp/k8s\"},\n\t)\n\tassert.Equal(\n\t\tt,\n\t\t[]string{\"testapp/k8s\"},\n\t\tincludedModulesWithTestApp,\n\t)\n}\n\nfunc TestTerragruntExternalDependencies(t *testing.T) {\n\tt.Parallel()\n\n\tmodules := []string{\n\t\t\"module-a\",\n\t\t\"module-b\",\n\t}\n\n\thelpers.CleanupTerraformFolder(t, testFixtureExternalDependence)\n\n\tfor _, module := range modules {\n\t\thelpers.CleanupTerraformFolder(t, filepath.Join(testFixtureExternalDependence, module))\n\t}\n\n\tvar (\n\t\tapplyAllStdout bytes.Buffer\n\t\tapplyAllStderr bytes.Buffer\n\t)\n\n\trootPath := helpers.CopyEnvironment(t, testFixtureExternalDependence)\n\tmodulePath := filepath.Join(rootPath, testFixtureExternalDependence, \"module-b\")\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt run --all apply --non-interactive --queue-include-external --tf-forward-stdout --working-dir \"+modulePath, &applyAllStdout, &applyAllStderr)\n\thelpers.LogBufferContentsLineByLine(t, applyAllStdout, \"run --all apply stdout\")\n\thelpers.LogBufferContentsLineByLine(t, applyAllStderr, \"run --all apply stderr\")\n\n\tapplyAllStdoutString := applyAllStdout.String()\n\n\tif err != nil {\n\t\tt.Errorf(\"Did not expect to get error: %s\", err.Error())\n\t}\n\n\tfor _, module := range modules {\n\t\tassert.Contains(t, applyAllStdoutString, \"Hello World, \"+module)\n\t}\n}\n\nfunc TestTerragruntExternalDependenciesWithFilter(t *testing.T) {\n\tt.Parallel()\n\n\tmodules := []string{\n\t\t\"module-a\",\n\t\t\"module-b\",\n\t}\n\n\thelpers.CleanupTerraformFolder(t, testFixtureExternalDependence)\n\n\tfor _, module := range modules {\n\t\thelpers.CleanupTerraformFolder(t, filepath.Join(testFixtureExternalDependence, module))\n\t}\n\n\tvar (\n\t\tapplyAllStdout bytes.Buffer\n\t\tapplyAllStderr bytes.Buffer\n\t)\n\n\trootPath := helpers.CopyEnvironment(t, testFixtureExternalDependence)\n\tmodulePath := filepath.Join(rootPath, testFixtureExternalDependence, \"module-b\")\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt run --all apply --non-interactive --filter '{./**}...' --tf-forward-stdout --working-dir \"+modulePath, &applyAllStdout, &applyAllStderr)\n\thelpers.LogBufferContentsLineByLine(t, applyAllStdout, \"run --all apply stdout\")\n\thelpers.LogBufferContentsLineByLine(t, applyAllStderr, \"run --all apply stderr\")\n\n\tapplyAllStdoutString := applyAllStdout.String()\n\n\tif err != nil {\n\t\tt.Errorf(\"Did not expect to get error: %s\", err.Error())\n\t}\n\n\tfor _, module := range modules {\n\t\tassert.Contains(t, applyAllStdoutString, \"Hello World, \"+module)\n\t}\n}\n\nfunc TestPreventDestroy(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/download\")\n\tfixtureRoot := filepath.Join(tmpEnvPath, testFixtureLocalPreventDestroy)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+fixtureRoot)\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt destroy -auto-approve --non-interactive --working-dir \"+fixtureRoot, os.Stdout, os.Stderr)\n\n\tif assert.Error(t, err) {\n\t\tvar target run.ModuleIsProtected\n\t\tassert.ErrorAs(t, err, &target)\n\t}\n}\n\nfunc TestPreventDestroyApply(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/download\")\n\n\tfixtureRoot := filepath.Join(tmpEnvPath, testFixtureLocalPreventDestroy)\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+fixtureRoot)\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -destroy -auto-approve --non-interactive --working-dir \"+fixtureRoot, os.Stdout, os.Stderr)\n\n\tif assert.Error(t, err) {\n\t\tvar target run.ModuleIsProtected\n\t\tassert.ErrorAs(t, err, &target)\n\t}\n}\n\nfunc TestPreventDestroyDependencies(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/download\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLocalPreventDestroyDependencies)\n\n\t// Populate module paths.\n\tmoduleNames := []string{\n\t\t\"module-a\",\n\t\t\"module-b\",\n\t\t\"module-c\",\n\t\t\"module-d\",\n\t\t\"module-e\",\n\t}\n\n\tmodulePaths := make(map[string]string, len(moduleNames))\n\tfor _, moduleName := range moduleNames {\n\t\tmodulePaths[moduleName] = filepath.Join(rootPath, moduleName)\n\t}\n\n\t// Cleanup all modules directories.\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\tfor _, modulePath := range modulePaths {\n\t\thelpers.CleanupTerraformFolder(t, modulePath)\n\t}\n\n\tvar (\n\t\tapplyAllStdout bytes.Buffer\n\t\tapplyAllStderr bytes.Buffer\n\t)\n\n\t// Apply and destroy all modules.\n\terr := helpers.RunTerragruntCommand(\n\t\tt,\n\t\t\"terragrunt run --all apply --non-interactive --working-dir \"+rootPath,\n\t\t&applyAllStdout,\n\t\t&applyAllStderr,\n\t)\n\trequire.NoError(t, err)\n\n\thelpers.LogBufferContentsLineByLine(t, applyAllStdout, \"run --all apply stdout\")\n\thelpers.LogBufferContentsLineByLine(t, applyAllStderr, \"run --all apply stderr\")\n\n\tvar (\n\t\tdestroyAllStdout bytes.Buffer\n\t\tdestroyAllStderr bytes.Buffer\n\t)\n\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt run --all destroy --non-interactive --working-dir \"+rootPath, &destroyAllStdout, &destroyAllStderr)\n\thelpers.LogBufferContentsLineByLine(t, destroyAllStdout, \"run --all destroy stdout\")\n\thelpers.LogBufferContentsLineByLine(t, destroyAllStderr, \"run --all destroy stderr\")\n\n\trequire.NoError(t, err)\n\n\t// Check that modules C, D and E were deleted and modules A and B weren't.\n\tfor moduleName, modulePath := range modulePaths {\n\t\tvar (\n\t\t\tshowStdout bytes.Buffer\n\t\t\tshowStderr bytes.Buffer\n\t\t)\n\n\t\terr = helpers.RunTerragruntCommand(t, \"terragrunt show --non-interactive --tf-forward-stdout --working-dir \"+modulePath, &showStdout, &showStderr)\n\t\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout for \"+modulePath)\n\t\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr for \"+modulePath)\n\n\t\trequire.NoError(t, err)\n\n\t\toutput := showStdout.String()\n\n\t\tswitch moduleName {\n\t\tcase \"module-a\":\n\t\t\tassert.Contains(t, output, \"Hello, Module A\")\n\t\tcase \"module-b\":\n\t\t\tassert.Contains(t, output, \"Hello, Module B\")\n\t\tcase \"module-c\":\n\t\t\tassert.NotContains(t, output, \"Hello, Module C\")\n\t\tcase \"module-d\":\n\t\t\tassert.NotContains(t, output, \"Hello, Module D\")\n\t\tcase \"module-e\":\n\t\t\tassert.NotContains(t, output, \"Hello, Module E\")\n\t\t}\n\t}\n}\n\nfunc TestDownloadWithCASEnabled(t *testing.T) {\n\tt.Parallel()\n\n\tfixturePath := \"fixtures/download/remote\"\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, fixturePath)\n\ttestPath := filepath.Join(tmpEnvPath, fixturePath)\n\thelpers.CleanupTerraformFolder(t, testPath)\n\n\t// Run with CAS experiment enabled\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\tcmd := \"terragrunt apply --auto-approve --non-interactive --experiment cas --log-level debug --working-dir \" + testPath\n\terr := helpers.RunTerragruntCommand(t, cmd, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr.String(), \"Downloading Terraform configurations\")\n}\n\nfunc TestCASStorageDirectory(t *testing.T) {\n\tt.Parallel()\n\n\thomeDir, err := os.UserHomeDir()\n\trequire.NoError(t, err)\n\n\texpectedCASDir := filepath.Join(homeDir, \".cache\", \"terragrunt\", \"cas\")\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/download\")\n\ttestPath := filepath.Join(tmpEnvPath, \"fixtures/download/local\")\n\n\thelpers.CleanupTerraformFolder(t, testPath)\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\tcmd := \"terragrunt plan --experiment cas --working-dir \" + testPath\n\t_ = helpers.RunTerragruntCommand(t, cmd, &stdout, &stderr)\n\n\t// Use require.Eventually to handle potential timing issues with CAS directory creation\n\trequire.Eventually(t, func() bool {\n\t\t_, err := os.Stat(expectedCASDir)\n\t\treturn err == nil\n\t}, 10*time.Second, 100*time.Millisecond, \"CAS directory should be created at %s\", expectedCASDir)\n\n\tstoreDir := filepath.Join(expectedCASDir, \"store\")\n\trequire.Eventually(t, func() bool {\n\t\t_, err := os.Stat(storeDir)\n\t\treturn err == nil\n\t}, 10*time.Second, 100*time.Millisecond, \"CAS store directory should be created at %s\", storeDir)\n}\n"
  },
  {
    "path": "test/integration_encryption_shared_test.go",
    "content": "package test_test\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tstateFile = \"tofu.tfstate\" //nolint:unused\n)\n\n// Check the statefile contains an encrypted_data key\n// and that the encrypted_data is base64 encoded\nfunc validateStateIsEncrypted(t *testing.T, fileName string, path string) { //nolint:unused\n\tt.Helper()\n\n\tfilePath := filepath.Join(path, fileName)\n\tfile, err := os.Open(filePath)\n\trequire.NoError(t, err)\n\n\tdefer file.Close()\n\n\tbyteValue, err := io.ReadAll(file)\n\trequire.NoError(t, err)\n\n\tvar result map[string]any\n\n\terr = json.Unmarshal(byteValue, &result)\n\trequire.NoError(t, err, \"Error unmarshalling the state file '%s'\", fileName)\n\n\tencryptedData, exists := result[\"encrypted_data\"]\n\tassert.True(t, exists, \"The key 'encrypted_data' should exist in the state '%s'\", fileName)\n\n\t// Check if the encrypted_data is base64 encoded (common for AES-256 encrypted data)\n\tencryptedDataStr, ok := encryptedData.(string)\n\tassert.True(t, ok, \"The value of 'encrypted_data' should be a string\")\n\n\t_, err = base64.StdEncoding.DecodeString(encryptedDataStr)\n\tassert.NoError(t, err, \"The value of 'encrypted_data' should be base64 encoded, indicating AES-256 encryption\")\n}\n"
  },
  {
    "path": "test/integration_engine_test.go",
    "content": "//go:build engine\n\npackage test_test\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/hashicorp/go-getter/v2\"\n\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureEngineDependency     = \"fixtures/engine/engine-dependencies\"\n\ttestFixtureLocalEngine          = \"fixtures/engine/local-engine\"\n\ttestFixtureRemoteEngine         = \"fixtures/engine/remote-engine\"\n\ttestFixtureOpenTofuEngine       = \"fixtures/engine/opentofu-engine\"\n\ttestFixtureOpenTofuRunAll       = \"fixtures/engine/opentofu-run-all\"\n\ttestFixtureOpenTofuLatestRunAll = \"fixtures/engine/opentofu-latest-run-all\"\n\ttestFixtureEngineTraceParent    = \"fixtures/engine/trace-parent\"\n\n\tenvVarExperimental = \"TG_EXPERIMENTAL_ENGINE\"\n)\n\nvar (\n\tengineAssetName    = \"terragrunt-iac-engine-opentofu_rpc_\" + testEngineVersion() + \"_\" + runtime.GOOS + \"_\" + runtime.GOARCH\n\tengineAssetArchive = engineAssetName + \".zip\"\n\tdownloadURL        = fmt.Sprintf(\n\t\t\"https://github.com/gruntwork-io/terragrunt-engine-opentofu/releases/download/%s/%s\",\n\t\ttestEngineVersion(),\n\t\tengineAssetArchive,\n\t)\n)\n\n//nolint:paralleltest\nfunc TestEngineLocalPlan(t *testing.T) {\n\trootPath := setupLocalEngine(t)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --log-level debug --non-interactive --tf-forward-stdout --working-dir \"+\n\t\t\trootPath+\" -- plan\",\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, engineAssetName)\n\tassert.Contains(t, stderr, \"Tofu Initialization started\")\n\tassert.Contains(t, stderr, \"Tofu Initialization completed\")\n\tassert.Contains(t, stderr, \"Tofu Shutdown completed\")\n\tassert.Contains(t, stdout, \"1 to add, 0 to change, 0 to destroy.\")\n}\n\n//nolint:paralleltest\nfunc TestEngineLocalApply(t *testing.T) {\n\trootPath := setupLocalEngine(t)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --non-interactive --log-level debug --tf-forward-stdout --working-dir \"+\n\t\t\trootPath+\" -- apply -auto-approve\",\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, engineAssetName)\n\tassert.Contains(t, stderr, \"Tofu Initialization started\")\n\tassert.Contains(t, stderr, \"Tofu Initialization completed\")\n\tassert.Contains(t, stderr, \"Tofu Shutdown completed\")\n\tassert.Contains(t, stdout, \"Apply complete! Resources: 1 added, 0 changed, 0 destroyed.\")\n}\n\nfunc TestEngineOpentofu(t *testing.T) {\n\tt.Setenv(envVarExperimental, \"1\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureOpenTofuEngine)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOpenTofuEngine)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureOpenTofuEngine)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --non-interactive --log-level debug --tf-forward-stdout --working-dir \"+\n\t\t\trootPath+\" -- apply -auto-approve\",\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"Tofu Initialization started\")\n\tassert.Contains(t, stderr, \"Tofu Initialization completed\")\n\tassert.Contains(t, stderr, \"Tofu Shutdown completed\")\n\tassert.Contains(t, stdout, \"OpenTofu has been successfully initialized\")\n\tassert.Contains(t, stdout, \"Apply complete! Resources: 1 added, 0 changed, 0 destroyed.\")\n}\n\nfunc TestEngineRunAllOpentofu(t *testing.T) {\n\tt.Setenv(envVarExperimental, \"1\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureOpenTofuRunAll)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOpenTofuRunAll)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureOpenTofuRunAll)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all --log-level debug --non-interactive --tf-forward-stdout --working-dir %s -- apply -no-color -auto-approve\",\n\t\t\trootPath,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"Tofu Initialization started\")\n\tassert.Contains(t, stdout, \"resource \\\"local_file\\\" \\\"test\\\"\")\n\tassert.Contains(t, stdout, \"filename             = \\\"./test.txt\\\"\\n\")\n\tassert.Contains(t, stdout, \"OpenTofu has been successful\")\n\tassert.Contains(t, stderr, \"Tofu Shutdown completed\")\n\tassert.Contains(t, stdout, \"Apply complete!\")\n}\n\nfunc TestEngineRunAllOpentofuCustomPath(t *testing.T) {\n\tt.Setenv(envVarExperimental, \"1\")\n\n\tcacheDir, rootPath := setupEngineCache(t)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all --log-level debug --non-interactive --tf-forward-stdout --working-dir %s -- apply -no-color -auto-approve\",\n\t\t\trootPath,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"OpenTofu has been successful\")\n\tassert.Contains(t, stderr, \"Tofu Shutdown completed\")\n\tassert.Contains(t, stdout, \"Apply complete!\")\n\n\t// check if cache folder is not empty\n\tfiles, err := os.ReadDir(cacheDir)\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, files)\n}\n\nfunc TestEngineDownloadOverHttp(t *testing.T) {\n\tt.Setenv(envVarExperimental, \"1\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureRemoteEngine)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRemoteEngine)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureRemoteEngine)\n\n\tplatform := runtime.GOOS\n\tarch := runtime.GOARCH\n\n\thelpers.CopyAndFillMapPlaceholders(\n\t\tt,\n\t\tfilepath.Join(\n\t\t\ttestFixtureRemoteEngine,\n\t\t\t\"terragrunt.hcl\",\n\t\t), filepath.Join(\n\t\t\trootPath,\n\t\t\tconfig.DefaultTerragruntConfigPath,\n\t\t), map[string]string{\n\t\t\t\"__hardcoded_url__\": fmt.Sprintf(\n\t\t\t\t\"https://github.com/gruntwork-io/terragrunt-engine-opentofu/releases/download/v0.1.0/terragrunt-iac-engine-opentofu_rpc_v0.1.0_%s_%s.zip\",\n\t\t\t\tplatform,\n\t\t\t\tarch,\n\t\t\t),\n\t\t})\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --non-interactive --log-level debug --tf-forward-stdout --working-dir \"+\n\t\t\trootPath+\" -- apply -auto-approve\",\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"Tofu Initialization started\")\n\tassert.Contains(t, stderr, \"Tofu Initialization completed\")\n\tassert.Contains(t, stderr, \"Tofu Shutdown completed\")\n\tassert.Contains(t, stdout, \"OpenTofu has been successfully initialized\")\n\tassert.Contains(t, stdout, \"Apply complete! Resources: 1 added, 0 changed, 0 destroyed.\")\n}\n\nfunc TestEngineChecksumVerification(t *testing.T) {\n\tt.Setenv(envVarExperimental, \"1\")\n\n\tcachePath, rootPath := setupEngineCache(t)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all --log-level debug --non-interactive --working-dir %s -- apply -no-color -auto-approve\",\n\t\t\trootPath,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\t// change the checksum of the package file\n\tversion := \"v0.1.0\"\n\tplatform := runtime.GOOS\n\tarch := runtime.GOARCH\n\texecutablePath := fmt.Sprintf(\n\t\t\"terragrunt/plugins/iac-engine/rpc/%s/%s/%s/terragrunt-iac-engine-opentofu_rpc_%s_%s_%s\",\n\t\tversion,\n\t\tplatform,\n\t\tarch,\n\t\tversion,\n\t\tplatform,\n\t\tarch,\n\t)\n\tfullPath := filepath.Join(cachePath, executablePath)\n\n\t// open the file and write some data\n\tfile, err := os.OpenFile(fullPath, os.O_APPEND|os.O_WRONLY, 0600)\n\trequire.NoError(t, err)\n\n\tnonExecutableData := []byte{0x00}\n\tif _, err := file.Write(nonExecutableData); err != nil {\n\t\trequire.NoError(t, err)\n\t}\n\n\trequire.NoError(t, file.Close())\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all --log-level debug --non-interactive --working-dir %s -- apply -no-color -auto-approve\",\n\t\t\trootPath,\n\t\t),\n\t)\n\trequire.Error(t, err)\n\n\trequire.Contains(t, stderr, \"checksum list has unexpected SHA-256 hash\")\n}\n\nfunc TestEngineDisableChecksumCheck(t *testing.T) {\n\tt.Setenv(envVarExperimental, \"1\")\n\n\tcachePath, rootPath := setupEngineCache(t)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all --non-interactive --log-level debug --working-dir %s -- apply -no-color -auto-approve\",\n\t\t\trootPath,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\terr = filepath.Walk(cachePath, func(path string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif strings.HasSuffix(filepath.Base(path), \"_SHA256SUMS\") {\n\t\t\tif err := os.Truncate(path, 0); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\trequire.NoError(t, err)\n\n\t// create separated directory for new tests\n\thelpers.CleanupTerraformFolder(t, testFixtureOpenTofuRunAll)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOpenTofuRunAll)\n\trootPath = filepath.Join(tmpEnvPath, testFixtureOpenTofuRunAll)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all --non-interactive --working-dir %s -- apply -no-color -auto-approve\",\n\t\t\trootPath,\n\t\t),\n\t)\n\trequire.Error(t, err)\n\trequire.Contains(t, stderr, \"verification failure\")\n\n\t// disable checksum check\n\tt.Setenv(\"TG_ENGINE_SKIP_CHECK\", \"1\")\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all --non-interactive --working-dir %s -- apply -no-color -auto-approve\",\n\t\t\trootPath,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n}\n\nfunc TestEngineOpentofuLatestRunAll(t *testing.T) {\n\tt.Setenv(envVarExperimental, \"1\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureOpenTofuLatestRunAll)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOpenTofuLatestRunAll)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureOpenTofuLatestRunAll)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all --log-level debug --non-interactive --tf-forward-stdout --working-dir %s -- apply -no-color -auto-approve\",\n\t\t\trootPath,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"resource \\\"local_file\\\" \\\"test\\\"\")\n\tassert.Contains(t, stdout, \"filename             = \\\"./test.txt\\\"\\n\")\n\tassert.Contains(t, stderr, \"Tofu Shutdown completed\")\n\tassert.Contains(t, stdout, \"Apply complete!\")\n}\n\nfunc TestEngineDependency(t *testing.T) {\n\tt.Setenv(envVarExperimental, \"1\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureEngineDependency)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureEngineDependency)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureEngineDependency)\n\n\tterragruntCmd := \"terragrunt run --log-level debug --non-interactive --tf-forward-stdout --working-dir %s -- apply -auto-approve -no-color\"\n\n\t// Run apply in app1, make sure it uses engine\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(terragruntCmd, filepath.Join(rootPath, \"app1\")))\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Using engine to run command: tofu apply -auto-approve -no-color\")\n\tassert.Contains(t, stdout, \"Changes to Outputs:\")\n\tassert.Contains(t, stdout, \"value = \\\"app1-test\\\"\")\n\n\t// Run apply in app2, make sure it uses engine for both app1 output and apply\n\tstdout, stderr, err = helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(terragruntCmd, filepath.Join(rootPath, \"app2\")))\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"prefix=../app1 msg=Using engine to run command: tofu output -json\")\n\tassert.Contains(t, stderr, \"msg=Using engine to run command: tofu apply -auto-approve -no-color\")\n\tassert.Contains(t, stdout, \"resource \\\"local_file\\\" \\\"test\\\"\")\n\tassert.Contains(t, stdout, \"content              = \\\"app1-test\\\"\")\n\tassert.Contains(t, stdout, \"filename             = \\\"./test.txt\\\"\\n\")\n}\n\nfunc TestEngineLogLevel(t *testing.T) {\n\tt.Setenv(envVarExperimental, \"1\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureOpenTofuLatestRunAll)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOpenTofuLatestRunAll)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureOpenTofuLatestRunAll)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\n\t\t\"terragrunt run --log-level debug --all --non-interactive --tf-forward-stdout \"+\n\t\t\t\"--working-dir %s -- apply -no-color -auto-approve\",\n\t\trootPath,\n\t),\n\t)\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"level=debug\")\n\tassert.Contains(t, stderr, \"[DEBUG] terragrunt-iac-engine-opentofu_rpc\")\n\tassert.Contains(t, stderr, \"[DEBUG] plugin exited\")\n}\n\nfunc TestEngineTelemetry(t *testing.T) {\n\tt.Setenv(\"TG_TELEMETRY_TRACE_EXPORTER\", \"console\")\n\tt.Setenv(envVarExperimental, \"1\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureEngineTraceParent)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureEngineTraceParent)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureEngineTraceParent)\n\n\tstr, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\thelpers.ValidateHookTraceParent(t, \"hook_print_traceparent\", str)\n}\n\n//nolint:paralleltest\nfunc TestNoEngineFlagDisablesEngine(t *testing.T) {\n\tt.Setenv(envVarExperimental, \"1\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureOpenTofuEngine)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOpenTofuEngine)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureOpenTofuEngine)\n\n\t// First, verify engine is used when experiment is enabled\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt plan --non-interactive --tf-forward-stdout --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Tofu Initialization started\")\n\tassert.Contains(t, stderr, \"Tofu Initialization completed\")\n\tassert.Contains(t, stderr, \"Tofu Shutdown completed\")\n\n\t// Then, verify engine is NOT used when --no-engine flag is set\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt plan --non-interactive --log-level debug --tf-forward-stdout --no-engine --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\tassert.NotContains(t, stderr, \"Tofu Initialization started\")\n\tassert.NotContains(t, stderr, \"Tofu Initialization completed\")\n\tassert.NotContains(t, stderr, \"Tofu Shutdown completed\")\n\tassert.Contains(t, stdout, \"1 to add, 0 to change, 0 to destroy.\")\n}\n\n//nolint:paralleltest\nfunc TestNoEngineFlagWithExperimentFlag(t *testing.T) {\n\thelpers.CleanupTerraformFolder(t, testFixtureOpenTofuEngine)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOpenTofuEngine)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureOpenTofuEngine)\n\n\t// Verify engine is used when --experiment iac-engine is set\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt plan --non-interactive --log-level debug --tf-forward-stdout --experiment iac-engine --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Tofu Initialization started\")\n\tassert.Contains(t, stderr, \"Tofu Initialization completed\")\n\tassert.Contains(t, stderr, \"Tofu Shutdown completed\")\n\n\t// Verify engine is NOT used when both --experiment iac-engine and --no-engine are set\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt plan --non-interactive --tf-forward-stdout --experiment iac-engine --no-engine --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\tassert.NotContains(t, stderr, \"Tofu Initialization started\")\n\tassert.NotContains(t, stderr, \"Tofu Initialization completed\")\n\tassert.NotContains(t, stderr, \"Tofu Shutdown completed\")\n\tassert.Contains(t, stdout, \"1 to add, 0 to change, 0 to destroy.\")\n}\n\n//nolint:paralleltest\nfunc TestNoEngineFlagWithRunAll(t *testing.T) {\n\tt.Setenv(envVarExperimental, \"1\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureOpenTofuRunAll)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOpenTofuRunAll)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureOpenTofuRunAll)\n\n\t// Verify engine is used in run --all when experiment is enabled\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all --log-level debug --non-interactive --tf-forward-stdout --working-dir %s -- plan -no-color\",\n\t\t\trootPath,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Tofu Initialization started\")\n\tassert.Contains(t, stderr, \"Tofu Initialization completed\")\n\tassert.Contains(t, stderr, \"Tofu Shutdown completed\")\n\n\t// Verify engine is NOT used in run --all when --no-engine is set\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all --non-interactive --log-level debug --tf-forward-stdout --no-engine --working-dir %s -- plan -no-color\",\n\t\t\trootPath,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\tassert.NotContains(t, stderr, \"Tofu Initialization started\")\n\tassert.NotContains(t, stderr, \"Tofu Initialization completed\")\n\tassert.NotContains(t, stderr, \"Tofu Shutdown completed\")\n\tassert.Contains(t, stdout, \"1 to add, 0 to change, 0 to destroy.\")\n}\n\nfunc setupEngineCache(t *testing.T) (string, string) {\n\tt.Helper()\n\n\t// create a temporary folder\n\tcacheDir := helpers.TmpDirWOSymlinks(t)\n\tt.Setenv(\"TG_ENGINE_CACHE_PATH\", cacheDir)\n\n\thelpers.CleanupTerraformFolder(t, testFixtureOpenTofuRunAll)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOpenTofuRunAll)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureOpenTofuRunAll)\n\n\treturn cacheDir, rootPath\n}\n\nfunc setupLocalEngine(t *testing.T) string {\n\tt.Helper()\n\n\tt.Setenv(envVarExperimental, \"1\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureLocalEngine)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureLocalEngine)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLocalEngine)\n\n\t// download engine to a local directory\n\tengineDir := filepath.Join(rootPath, \"engine\")\n\tif err := os.MkdirAll(engineDir, 0755); err != nil {\n\t\trequire.NoError(t, err)\n\t}\n\n\t_, err := getter.GetAny(t.Context(), engineDir, downloadURL)\n\trequire.NoError(t, err)\n\n\thelpers.CopyAndFillMapPlaceholders(\n\t\tt,\n\t\tfilepath.Join(\n\t\t\ttestFixtureLocalEngine,\n\t\t\t\"terragrunt.hcl\",\n\t\t),\n\t\tfilepath.Join(\n\t\t\trootPath,\n\t\t\tconfig.DefaultTerragruntConfigPath,\n\t\t), map[string]string{\n\t\t\t\"__engine_source__\": filepath.Join(engineDir, engineAssetName),\n\t\t},\n\t)\n\n\treturn rootPath\n}\n\n// testEngineVersion returns the version of the engine to be used in the test\nfunc testEngineVersion() string {\n\tvalue, found := os.LookupEnv(\"TOFU_ENGINE_VERSION\")\n\tif !found {\n\t\treturn \"v0.1.0\"\n\t}\n\n\treturn value\n}\n"
  },
  {
    "path": "test/integration_errors_test.go",
    "content": "package test_test\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestSimpleErrors          = \"fixtures/errors/default\"\n\ttestIgnoreErrors          = \"fixtures/errors/ignore\"\n\ttestIgnoreSignalErrors    = \"fixtures/errors/ignore-signal\"\n\ttestRunAllIgnoreErrors    = \"fixtures/errors/run-all-ignore\"\n\ttestRetryErrors           = \"fixtures/errors/retry\"\n\ttestRetryFailErrors       = \"fixtures/errors/retry-fail\"\n\ttestRunAllErrors          = \"fixtures/errors/run-all\"\n\ttestNegativePatternErrors = \"fixtures/errors/ignore-negative-pattern\"\n\ttestMultiLineErrors       = \"fixtures/errors/multi-line\"\n\ttestGetDefaultErrors      = \"fixtures/errors/get-default-errors\"\n\ttestNoAutoRetry           = \"fixtures/errors/no-auto-retry\"\n)\n\nfunc TestErrorsHandling(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testSimpleErrors)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testSimpleErrors)\n\trootPath := filepath.Join(tmpEnvPath, testSimpleErrors)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\trequire.NoError(t, err)\n}\n\nfunc TestIgnoreError(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testIgnoreErrors)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testIgnoreErrors)\n\trootPath := filepath.Join(tmpEnvPath, testIgnoreErrors)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Ignoring error example1\")\n\tassert.NotContains(t, stderr, \"Ignoring error example2\")\n}\n\nfunc TestRunAllIgnoreError(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testRunAllIgnoreErrors)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testRunAllIgnoreErrors)\n\trootPath := filepath.Join(tmpEnvPath, testRunAllIgnoreErrors)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- apply -auto-approve\")\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Ignoring error example1\")\n\tassert.NotContains(t, stderr, \"Ignoring error example2\")\n\tassert.Contains(t, stdout, \"value-from-app-2\")\n}\n\nfunc TestRetryError(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testRetryErrors)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testRetryErrors)\n\trootPath := filepath.Join(tmpEnvPath, testRetryErrors)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Encountered retryable error: script_errors\")\n\tassert.NotContains(t, stderr, \"aws_errors\")\n}\n\nfunc TestRetryFailError(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testRetryFailErrors)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testRetryFailErrors)\n\trootPath := filepath.Join(tmpEnvPath, testRetryFailErrors)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\trequire.Error(t, err)\n\tassert.Contains(t, stderr, \"Encountered retryable error: script_errors\")\n}\n\nfunc TestIgnoreSignal(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testIgnoreSignalErrors)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testIgnoreSignalErrors)\n\trootPath := filepath.Join(tmpEnvPath, testIgnoreSignalErrors)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Ignoring error example1\")\n\tassert.NotContains(t, stderr, \"Ignoring error example2\")\n\n\t// Signals file is written to original config directory (opts.WorkingDir during error handling)\n\tsignalsFile := filepath.Join(rootPath, \"error-signals.json\")\n\tassert.FileExists(t, signalsFile)\n\n\tcontent, err := os.ReadFile(signalsFile)\n\trequire.NoError(t, err, \"Failed to read error-signals.json\")\n\n\tvar signals struct {\n\t\tMessage string `json:\"message\"`\n\t}\n\n\terr = json.Unmarshal(content, &signals)\n\trequire.NoError(t, err, \"Failed to parse error-signals.json\")\n\tassert.Equal(t, \"Failed example1\", signals.Message, \"Unexpected error message\")\n}\n\nfunc TestRunAllError(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testRunAllErrors)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testRunAllErrors)\n\trootPath := filepath.Join(tmpEnvPath, testRunAllErrors)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- apply -auto-approve\")\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Ignoring error example1\")\n\tassert.NotContains(t, stderr, \"Ignoring error example2\")\n\tassert.Contains(t, stderr, \"Encountered retryable error: script_errors\")\n}\n\nfunc TestRunAllFail(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testRunAllErrors)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testRunAllErrors)\n\trootPath := filepath.Join(tmpEnvPath, testRunAllErrors)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all --feature unstable=false --non-interactive --working-dir \"+rootPath+\" -- apply -auto-approve\",\n\t)\n\trequire.Error(t, err)\n}\n\nfunc TestIgnoreNegativePattern(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testNegativePatternErrors)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testNegativePatternErrors)\n\trootPath := filepath.Join(tmpEnvPath, testNegativePatternErrors)\n\n\t_, stdout, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\trequire.Error(t, err)\n\tassert.Contains(t, stdout, \"Error: baz\")\n}\n\nfunc TestHandleMultiLineErrors(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testMultiLineErrors)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testMultiLineErrors)\n\trootPath := filepath.Join(tmpEnvPath, testMultiLineErrors)\n\n\t_, stdout, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"Ignoring transit gateway not found when creating internal route\")\n}\n\nfunc TestGetDefaultRetryableErrors(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testGetDefaultErrors)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testGetDefaultErrors)\n\trootPath := filepath.Join(tmpEnvPath, testGetDefaultErrors)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal([]byte(stdout), &outputs))\n\n\t// Verify get_default_retryable_errors() returns a non-empty list\n\tdefaultErrors := outputs[\"default_retryable_errors\"]\n\tassert.NotEmpty(t, defaultErrors.Value)\n\n\t// Verify custom error is passed through\n\tcustomError := outputs[\"custom_error\"]\n\tassert.Equal(t, \"my special snowflake\", customError.Value)\n}\n\nfunc TestNoAutoRetryFlag(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testNoAutoRetry)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testNoAutoRetry)\n\trootPath := filepath.Join(tmpEnvPath, testNoAutoRetry)\n\n\t// Test with --no-auto-retry flag - should fail without retry\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --no-auto-retry --non-interactive --working-dir \"+rootPath)\n\trequire.Error(t, err)\n\tassert.Contains(t, stderr, \"Transient error\")\n\n\t// Cleanup for second test - success.txt is created in cache directory (default hook behavior)\n\tcacheDir := helpers.FindCacheWorkingDir(t, rootPath)\n\tsuccessFile := filepath.Join(cacheDir, \"success.txt\")\n\terr = os.Remove(successFile)\n\trequire.NoError(t, err)\n\tcleanupTerraformFolder(t, testNoAutoRetry)\n\n\t// Test without flag - should succeed with retry\n\t_, stderr2, err2 := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err2)\n\tassert.Contains(t, stderr2, \"Encountered retryable error\")\n}\n"
  },
  {
    "path": "test/integration_example_live_stacks_test.go",
    "content": "//go:build aws && tofu\n\npackage test_test\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/service/sts\"\n\t\"github.com/gruntwork-io/terragrunt/internal/awshelper\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestExampleLiveStacks(t *testing.T) {\n\tuniqueID := strings.ToLower(helpers.UniqueID())\n\n\tawsCfg, err := awshelper.NewAWSConfigBuilder().Build(t.Context(), createLogger())\n\trequire.NoError(t, err, \"Error creating AWS config\")\n\n\tstsClient := sts.NewFromConfig(awsCfg)\n\n\tidentity, err := stsClient.GetCallerIdentity(t.Context(), &sts.GetCallerIdentityInput{})\n\trequire.NoError(t, err, \"Error getting AWS caller identity\")\n\n\taccountID := *identity.Account\n\n\tt.Setenv(\"EX_APP_PREFIX\", \"tg-test-\"+uniqueID+\"-\")\n\tt.Setenv(\"EX_BUCKET_PREFIX\", \"tg-test-\"+uniqueID+\"-\")\n\tt.Setenv(\"EX_NON_PROD_ACCOUNT_ID\", accountID)\n\tt.Setenv(\"EX_PROD_ACCOUNT_ID\", accountID)\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\trepoDir := filepath.Join(tmpDir, \"live-stacks-example\")\n\n\thelpers.ExecWithTestLogger(\n\t\tt,\n\t\ttmpDir,\n\t\t\"git\",\n\t\t\"clone\",\n\t\t\"https://github.com/gruntwork-io/terragrunt-infrastructure-live-stacks-example.git\",\n\t\trepoDir,\n\t)\n\thelpers.ExecWithTestLogger(t, repoDir, \"git\", \"checkout\", \"3649bd95c93074e6a3742bc5122e411505c24c3a\")\n\n\thelpers.ExecWithTestLogger(t, repoDir, \"mise\", \"trust\")\n\thelpers.ExecWithTestLogger(t, repoDir, \"mise\", \"install\")\n\n\tstdout, _ := helpers.ExecWithMiseAndCaptureOutput(t, repoDir, \"terragrunt\", \"--version\")\n\trequire.Contains(t, stdout, \"terragrunt\")\n\n\tstdout, _ = helpers.ExecWithMiseAndCaptureOutput(t, repoDir, \"tofu\", \"--version\")\n\trequire.Contains(t, stdout, \"OpenTofu\")\n\n\tregion := \"us-east-1\"\n\tstateBucketName := fmt.Sprintf(\"tg-test-%s-terragrunt-example-tf-state-non-prod-%s\", uniqueID, region)\n\n\tdefer helpers.DeleteS3Bucket(t, region, stateBucketName)\n\n\tstackDir := filepath.Join(repoDir, \"non-prod\", \"us-east-1\", \"stateful-lambda-service\")\n\n\tdefer func() {\n\t\tt.Log(\"Running destroy\")\n\t\thelpers.ExecWithMiseAndTestLogger(\n\t\t\tt,\n\t\t\tstackDir,\n\t\t\t\"terragrunt\",\n\t\t\t\"run\",\n\t\t\t\"--all\",\n\t\t\t\"--non-interactive\",\n\t\t\t\"--\",\n\t\t\t\"destroy\",\n\t\t\t\"-auto-approve\",\n\t\t)\n\t}()\n\n\tt.Log(\"Running apply\")\n\thelpers.ExecWithMiseAndTestLogger(\n\t\tt,\n\t\tstackDir,\n\t\t\"terragrunt\",\n\t\t\"run\",\n\t\t\"--all\",\n\t\t\"--non-interactive\",\n\t\t\"--backend-bootstrap\",\n\t\t\"--\",\n\t\t\"apply\",\n\t)\n\n\tserviceDir := filepath.Join(stackDir, \".terragrunt-stack\", \"service\")\n\tstdoutOutput, _ := helpers.ExecWithMiseAndCaptureOutput(t, serviceDir, \"terragrunt\", \"output\", \"-json\")\n\n\tvar outputs map[string]helpers.TerraformOutput\n\trequire.NoError(t, json.Unmarshal([]byte(stdoutOutput), &outputs))\n\n\tfunctionURLOutput, ok := outputs[\"function_url\"]\n\trequire.True(t, ok, \"Expected 'function_url' output to be defined\")\n\n\tfunctionURL, ok := functionURLOutput.Value.(string)\n\trequire.True(t, ok, \"Expected 'function_url' to be a string\")\n\trequire.NotEmpty(t, functionURL, \"function_url should not be empty\")\n\n\tt.Logf(\"Lambda function URL: %s\", functionURL)\n\n\tt.Log(\"GET initial count\")\n\n\tconst (\n\t\tmaxRetries = 5\n\t\tretryDelay = 5 * time.Second\n\t)\n\n\tvar initialCount float64\n\n\tfor i := range maxRetries {\n\t\treq, reqErr := http.NewRequestWithContext(t.Context(), http.MethodGet, functionURL, nil)\n\t\trequire.NoError(t, reqErr)\n\n\t\tresp, err := http.DefaultClient.Do(req)\n\t\tif err == nil && resp.StatusCode == http.StatusOK {\n\t\t\tvar body map[string]any\n\t\t\trequire.NoError(t, json.NewDecoder(resp.Body).Decode(&body))\n\t\t\tresp.Body.Close()\n\n\t\t\tcountVal, exists := body[\"count\"]\n\t\t\trequire.True(t, exists, \"Expected 'count' field in response JSON\")\n\n\t\t\tinitialCount, ok = countVal.(float64)\n\t\t\trequire.True(t, ok, \"Expected 'count' to be a number\")\n\n\t\t\tt.Logf(\"Initial count: %v\", initialCount)\n\n\t\t\tbreak\n\t\t}\n\n\t\tif resp != nil {\n\t\t\tresp.Body.Close()\n\t\t}\n\n\t\tt.Logf(\"GET attempt %d/%d: err=%v, retrying in %s\", i+1, maxRetries, err, retryDelay)\n\t\ttime.Sleep(retryDelay)\n\n\t\trequire.Less(t, i, maxRetries-1, \"Failed to reach Lambda function URL after %d retries: %v\", maxRetries, err)\n\t}\n\n\tt.Log(\"POST to increment count\")\n\n\tpostReq, err := http.NewRequestWithContext(t.Context(), http.MethodPost, functionURL, nil)\n\trequire.NoError(t, err)\n\n\tpostResp, err := http.DefaultClient.Do(postReq)\n\trequire.NoError(t, err, \"POST request failed\")\n\n\trequire.Equal(t, http.StatusOK, postResp.StatusCode, \"Expected HTTP 200 from POST\")\n\n\tvar postBody map[string]any\n\trequire.NoError(t, json.NewDecoder(postResp.Body).Decode(&postBody))\n\tpostResp.Body.Close()\n\n\tpostCount, exists := postBody[\"count\"]\n\trequire.True(t, exists, \"Expected 'count' field in POST response JSON\")\n\n\tpostCountVal, ok := postCount.(float64)\n\trequire.True(t, ok, \"Expected 'count' to be a number in POST response\")\n\n\tassert.Equal(t, int(initialCount)+1, int(postCountVal), \"Expected count to increment by 1 after POST\")\n\tt.Logf(\"Post-increment count: %v\", postCountVal)\n\n\tt.Log(\"GET to verify persisted count\")\n\n\tgetReq, err := http.NewRequestWithContext(t.Context(), http.MethodGet, functionURL, nil)\n\trequire.NoError(t, err)\n\n\tgetResp, err := http.DefaultClient.Do(getReq)\n\trequire.NoError(t, err, \"GET request failed\")\n\n\trequire.Equal(t, http.StatusOK, getResp.StatusCode, \"Expected HTTP 200 from final GET\")\n\n\tvar getBody map[string]any\n\trequire.NoError(t, json.NewDecoder(getResp.Body).Decode(&getBody))\n\tgetResp.Body.Close()\n\n\tfinalCount, exists := getBody[\"count\"]\n\trequire.True(t, exists, \"Expected 'count' field in final GET response JSON\")\n\n\tfinalCountVal, ok := finalCount.(float64)\n\trequire.True(t, ok, \"Expected 'count' to be a number in final GET response\")\n\n\tassert.Equal(t, int(postCountVal), int(finalCountVal), \"Expected final GET count to match POST count\")\n\tt.Logf(\"Final verified count: %v\", finalCountVal)\n}\n"
  },
  {
    "path": "test/integration_exclude_test.go",
    "content": "package test_test\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestExcludeComprehensive = \"fixtures/exclude/comprehensive\"\n)\n\n// expectedResult defines the expected outcome for a unit in a test case.\ntype expectedResult struct {\n\tresult string\n\treason string\n}\n\n// excludeTestCase defines a single test case for the exclude block behavior.\ntype excludeTestCase struct {\n\texpectedUnits   map[string]expectedResult\n\tname            string\n\tcommand         string\n\tworkingDir      string\n\tfeatureFlags    []string\n\trunAll          bool\n\texpectEarlyExit bool\n\texpectRuns      bool\n}\n\nfunc TestExcludeBlockBehavior(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []*excludeTestCase{\n\t\t// ========== Run --all Mode Tests ==========\n\t\t{\n\t\t\tname:    \"run_all_basic_exclusion\",\n\t\t\tcommand: \"plan\",\n\t\t\trunAll:  true,\n\t\t\texpectedUnits: map[string]expectedResult{\n\t\t\t\t\"always-excluded\": {result: \"excluded\", reason: \"exclude block\"},\n\t\t\t\t\"never-excluded\":  {result: \"succeeded\"},\n\t\t\t\t\"normal-unit\":     {result: \"succeeded\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"run_all_action_specific_plan\",\n\t\t\tcommand: \"plan\",\n\t\t\trunAll:  true,\n\t\t\texpectedUnits: map[string]expectedResult{\n\t\t\t\t\"exclude-plan-only\":  {result: \"excluded\", reason: \"exclude block\"},\n\t\t\t\t\"exclude-apply-only\": {result: \"succeeded\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"run_all_action_specific_apply\",\n\t\t\tcommand: \"apply -auto-approve\",\n\t\t\trunAll:  true,\n\t\t\texpectedUnits: map[string]expectedResult{\n\t\t\t\t\"exclude-plan-only\":  {result: \"succeeded\"},\n\t\t\t\t\"exclude-apply-only\": {result: \"excluded\", reason: \"exclude block\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"run_all_all_except_output_apply\",\n\t\t\tcommand: \"apply -auto-approve\",\n\t\t\trunAll:  true,\n\t\t\texpectedUnits: map[string]expectedResult{\n\t\t\t\t\"exclude-all-except-output\": {result: \"excluded\", reason: \"exclude block\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"run_all_all_except_output_cmd\",\n\t\t\tcommand: \"output\",\n\t\t\trunAll:  true,\n\t\t\texpectedUnits: map[string]expectedResult{\n\t\t\t\t\"exclude-all-except-output\": {result: \"succeeded\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:    \"run_all_ignores_no_run\",\n\t\t\tcommand: \"plan\",\n\t\t\trunAll:  true,\n\t\t\texpectedUnits: map[string]expectedResult{\n\t\t\t\t\"no-run-true\":  {result: \"excluded\", reason: \"exclude block\"},\n\t\t\t\t\"no-run-false\": {result: \"excluded\", reason: \"exclude block\"},\n\t\t\t\t\"normal-unit\":  {result: \"succeeded\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:         \"run_all_feature_flag_true\",\n\t\t\tcommand:      \"plan\",\n\t\t\trunAll:       true,\n\t\t\tfeatureFlags: []string{\"exclude=true\"},\n\t\t\texpectedUnits: map[string]expectedResult{\n\t\t\t\t\"conditional-flag\": {result: \"excluded\", reason: \"exclude block\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:         \"run_all_feature_flag_false\",\n\t\t\tcommand:      \"plan\",\n\t\t\trunAll:       true,\n\t\t\tfeatureFlags: []string{\"exclude=false\"},\n\t\t\texpectedUnits: map[string]expectedResult{\n\t\t\t\t\"conditional-flag\": {result: \"succeeded\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:         \"run_all_exclude_dependencies_true\",\n\t\t\tcommand:      \"plan\",\n\t\t\trunAll:       true,\n\t\t\tfeatureFlags: []string{\"exclude=true\", \"exclude_deps=true\"},\n\t\t\texpectedUnits: map[string]expectedResult{\n\t\t\t\t\"with-dep\": {result: \"excluded\", reason: \"exclude block\"},\n\t\t\t\t\"dep-unit\": {result: \"excluded\", reason: \"exclude block\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:         \"run_all_exclude_dependencies_false\",\n\t\t\tcommand:      \"plan\",\n\t\t\trunAll:       true,\n\t\t\tfeatureFlags: []string{\"exclude=true\", \"exclude_deps=false\"},\n\t\t\texpectedUnits: map[string]expectedResult{\n\t\t\t\t\"with-dep\": {result: \"excluded\", reason: \"exclude block\"},\n\t\t\t\t\"dep-unit\": {result: \"succeeded\"},\n\t\t\t},\n\t\t},\n\n\t\t// ========== Single Unit Mode Tests ==========\n\t\t// Single-unit mode uses stderr to verify behavior since reports are not generated\n\t\t{\n\t\t\tname:            \"single_no_run_true_early_exit\",\n\t\t\tcommand:         \"plan\",\n\t\t\trunAll:          false,\n\t\t\tworkingDir:      \"no-run-true\",\n\t\t\texpectEarlyExit: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"single_no_run_false_runs\",\n\t\t\tcommand:    \"plan\",\n\t\t\trunAll:     false,\n\t\t\tworkingDir: \"no-run-false\",\n\t\t\texpectRuns: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"single_no_run_not_set_runs\",\n\t\t\tcommand:    \"plan\",\n\t\t\trunAll:     false,\n\t\t\tworkingDir: \"no-run-not-set\",\n\t\t\texpectRuns: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"single_action_mismatch_runs\",\n\t\t\tcommand:    \"apply -auto-approve\",\n\t\t\trunAll:     false,\n\t\t\tworkingDir: \"action-mismatch\",\n\t\t\texpectRuns: true,\n\t\t},\n\t\t{\n\t\t\tname:            \"single_conditional_no_run_excluded\",\n\t\t\tcommand:         \"plan\",\n\t\t\trunAll:          false,\n\t\t\tworkingDir:      \"conditional-no-run\",\n\t\t\texpectEarlyExit: true,\n\t\t},\n\t\t{\n\t\t\tname:         \"single_conditional_no_run_runs\",\n\t\t\tcommand:      \"plan\",\n\t\t\trunAll:       false,\n\t\t\tworkingDir:   \"conditional-no-run\",\n\t\t\tfeatureFlags: []string{\"enable_unit=true\"},\n\t\t\texpectRuns:   true,\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.Parallel()\n\n\t\t\tcleanupTerraformFolder(t, testExcludeComprehensive)\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testExcludeComprehensive)\n\n\t\t\tvar rootPath string\n\t\t\tif tc.runAll {\n\t\t\t\trootPath = filepath.Join(tmpEnvPath, testExcludeComprehensive)\n\t\t\t} else {\n\t\t\t\trootPath = filepath.Join(tmpEnvPath, testExcludeComprehensive, tc.workingDir)\n\t\t\t}\n\n\t\t\treportFile := filepath.Join(t.TempDir(), \"report.json\")\n\n\t\t\tcmd := buildExcludeTestCommand(tc, rootPath, reportFile)\n\n\t\t\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif !tc.runAll {\n\t\t\t\tif tc.expectEarlyExit {\n\t\t\t\t\tassert.Contains(t, stderr, \"Early exit in terragrunt unit\")\n\t\t\t\t\tassert.Contains(t, stderr, \"due to exclude block with no_run = true\")\n\t\t\t\t}\n\n\t\t\t\tif tc.expectRuns {\n\t\t\t\t\tassert.NotContains(t, stderr, \"Early exit in terragrunt unit\")\n\t\t\t\t\tassert.NotContains(t, stderr, \"due to exclude block with no_run = true\")\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\truns, err := report.ParseJSONRunsFromFile(reportFile)\n\t\t\trequire.NoError(t, err, \"Failed to parse report file\")\n\n\t\t\tfor unitName, expected := range tc.expectedUnits {\n\t\t\t\trun := runs.FindByName(unitName)\n\t\t\t\trequire.NotNil(t, run, \"unit %s not found in report. Found: %v\", unitName, runs.Names())\n\t\t\t\tassert.Equal(\n\t\t\t\t\tt,\n\t\t\t\t\texpected.result,\n\t\t\t\t\trun.Result,\n\t\t\t\t\t\"unit %s: expected result %q, got %q\",\n\t\t\t\t\tunitName,\n\t\t\t\t\texpected.result,\n\t\t\t\t\trun.Result,\n\t\t\t\t)\n\n\t\t\t\tswitch expected.result {\n\t\t\t\tcase \"excluded\":\n\t\t\t\t\trequire.NotEmpty(\n\t\t\t\t\t\tt,\n\t\t\t\t\t\texpected.reason,\n\t\t\t\t\t\t\"test bug: excluded unit %s must specify expected reason\",\n\t\t\t\t\t\tunitName,\n\t\t\t\t\t)\n\t\t\t\t\trequire.NotNil(\n\t\t\t\t\t\tt,\n\t\t\t\t\t\trun.Reason,\n\t\t\t\t\t\t\"unit %s: expected reason %q but got nil\",\n\t\t\t\t\t\tunitName,\n\t\t\t\t\t\texpected.reason,\n\t\t\t\t\t)\n\t\t\t\t\tassert.Equal(\n\t\t\t\t\t\tt,\n\t\t\t\t\t\texpected.reason,\n\t\t\t\t\t\t*run.Reason,\n\t\t\t\t\t\t\"unit %s: expected reason %q, got %q\",\n\t\t\t\t\t\tunitName,\n\t\t\t\t\t\texpected.reason,\n\t\t\t\t\t\t*run.Reason,\n\t\t\t\t\t)\n\t\t\t\tcase \"succeeded\":\n\t\t\t\t\tassert.Nil(\n\t\t\t\t\t\tt,\n\t\t\t\t\t\trun.Reason,\n\t\t\t\t\t\t\"unit %s: succeeded units should not have a reason, got %v\",\n\t\t\t\t\t\tunitName,\n\t\t\t\t\t\trun.Reason,\n\t\t\t\t\t)\n\t\t\t\tdefault:\n\t\t\t\t\tt.Fatalf(\"Unexpected result %q for unit %s\", expected.result, unitName)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// buildExcludeTestCommand constructs the terragrunt command for a test case.\nfunc buildExcludeTestCommand(tc *excludeTestCase, rootPath, reportFile string) string {\n\tif tc.runAll {\n\t\tcmd := fmt.Sprintf(\n\t\t\t\"terragrunt run --all --non-interactive --working-dir %s \"+\n\t\t\t\t\"--report-file %s --report-format json\",\n\t\t\trootPath,\n\t\t\treportFile,\n\t\t)\n\n\t\tvar cmdSB strings.Builder\n\t\tfor _, flag := range tc.featureFlags {\n\t\t\tcmdSB.WriteString(\" --feature \" + flag)\n\t\t}\n\n\t\tcmd += cmdSB.String()\n\n\t\tcmd += \" -- \" + tc.command\n\n\t\treturn cmd\n\t}\n\n\tcmd := fmt.Sprintf(\n\t\t\"terragrunt %s --non-interactive --working-dir %s\",\n\t\ttc.command,\n\t\trootPath,\n\t)\n\n\tvar cmdSB strings.Builder\n\tfor _, flag := range tc.featureFlags {\n\t\tcmdSB.WriteString(\" --feature \" + flag)\n\t}\n\n\tcmd += cmdSB.String()\n\n\treturn cmd\n}\n"
  },
  {
    "path": "test/integration_exec_test.go",
    "content": "package test_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestExecCommand(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tscriptPath string\n\t\trunInDir   string\n\t\targs       []string\n\t}{\n\t\t{\n\t\t\tscriptPath: \"./script.sh arg1 arg2\",\n\t\t\trunInDir:   \"\",\n\t\t},\n\t\t{\n\t\t\targs:       []string{\"--in-download-dir\"},\n\t\t\tscriptPath: \"./script.sh arg1 arg2\",\n\t\t\trunInDir:   \".terragrunt-cache\",\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureExecCmd)\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureExecCmd)\n\n\t\t\trootPath := filepath.Join(tmpEnvPath, testFixtureExecCmd, \"app\")\n\t\t\trootPath, err := filepath.EvalSymlinks(rootPath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tdownloadDirPath := filepath.Join(rootPath, \".terragrunt-cache\")\n\t\t\tscriptPath := filepath.Join(tmpEnvPath, testFixtureExecCmd, tc.scriptPath)\n\n\t\t\terr = os.Mkdir(downloadDirPath, os.ModePerm)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt exec --working-dir \"+rootPath+\" \"+strings.Join(tc.args, \" \")+\" -- \"+scriptPath)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Contains(t, stdout, \"The first arg is arg1. The second arg is arg2. The script is running in the directory \"+filepath.Join(rootPath, tc.runInDir))\n\t\t})\n\t}\n}\n\nfunc TestExecCommandTfPath(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpected string\n\t\ttfPath   string\n\t}{\n\t\t{\n\t\t\texpected: \"baz is baz\",\n\t\t},\n\t\t{\n\t\t\texpected: \"baz is terraform\",\n\t\t\ttfPath:   \"terraform-output-json.sh\",\n\t\t},\n\t\t{\n\t\t\texpected: \"baz is tofu\",\n\t\t\ttfPath:   \"tofu-output-json.sh\",\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureExecCmdTfPath)\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureExecCmdTfPath)\n\n\t\t\trootPath := filepath.Join(tmpEnvPath, testFixtureExecCmdTfPath, \"app\")\n\t\t\trootPath, err := filepath.EvalSymlinks(rootPath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tdownloadDirPath := filepath.Join(rootPath, \".terragrunt-cache\")\n\t\t\tscriptPath := filepath.Join(tmpEnvPath, testFixtureExecCmdTfPath, \"./script.sh\")\n\n\t\t\ttfPath := \"\"\n\t\t\tif tc.tfPath != \"\" {\n\t\t\t\ttfPath = \"--tf-path \" + filepath.Join(tmpEnvPath, testFixtureExecCmdTfPath, tc.tfPath)\n\t\t\t}\n\n\t\t\terr = os.Mkdir(downloadDirPath, os.ModePerm)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tdepPath := filepath.Join(tmpEnvPath, testFixtureExecCmdTfPath, \"dep\")\n\t\t\tdepStdout := bytes.Buffer{}\n\t\t\tdepStderr := bytes.Buffer{}\n\t\t\trequire.NoError(t, helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive -no-color --no-color --log-format=pretty --working-dir \"+depPath, &depStdout, &depStderr))\n\n\t\t\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt --log-level debug exec \"+tfPath+\" --working-dir \"+rootPath+\"  -- \"+scriptPath)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Contains(t, stdout, tc.expected)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/integration_feature_flags_test.go",
    "content": "package test_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n)\n\nconst (\n\ttestSimpleFlag     = \"fixtures/feature-flags/simple-flag\"\n\ttestIncludeFlag    = \"fixtures/feature-flags/include-flag\"\n\ttestRunAllFlag     = \"fixtures/feature-flags/run-all\"\n\ttestErrorEmptyFlag = \"fixtures/feature-flags/error-empty-flag\"\n)\n\nfunc TestFeatureFlagDefaults(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testSimpleFlag)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testSimpleFlag)\n\trootPath := filepath.Join(tmpEnvPath, testSimpleFlag)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\tvalidateOutputs(t, rootPath)\n}\n\nfunc TestFeatureFlagCli(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testSimpleFlag)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testSimpleFlag)\n\trootPath := filepath.Join(tmpEnvPath, testSimpleFlag)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --feature int_feature_flag=777 --feature bool_feature_flag=true --feature string_feature_flag=tomato --non-interactive --working-dir \"+rootPath)\n\n\texpected := expectedDefaults()\n\texpected[\"int_feature_flag\"] = 777\n\texpected[\"bool_feature_flag\"] = true\n\texpected[\"string_feature_flag\"] = \"tomato\"\n\tvalidateOutputsMap(t, rootPath, expected)\n}\n\nfunc TestFeatureApplied(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testSimpleFlag)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testSimpleFlag)\n\trootPath := filepath.Join(tmpEnvPath, testSimpleFlag)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --feature bool_feature_flag=true --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"running conditional bool_feature_flag\")\n\n\tstdout, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --feature bool_feature_flag=false --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\tassert.NotContains(t, stdout, \"running conditional bool_feature_flag\")\n}\n\nfunc TestFeatureFlagEnv(t *testing.T) {\n\tt.Setenv(\"TG_FEATURE\", \"int_feature_flag=111,bool_feature_flag=true,string_feature_flag=xyz\")\n\n\tcleanupTerraformFolder(t, testSimpleFlag)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testSimpleFlag)\n\trootPath := filepath.Join(tmpEnvPath, testSimpleFlag)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\texpected := expectedDefaults()\n\texpected[\"int_feature_flag\"] = 111\n\texpected[\"bool_feature_flag\"] = true\n\texpected[\"string_feature_flag\"] = \"xyz\"\n\tvalidateOutputsMap(t, rootPath, expected)\n}\n\nfunc TestFeatureIncludeFlag(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testIncludeFlag)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testIncludeFlag)\n\trootPath := filepath.Join(tmpEnvPath, testIncludeFlag, \"app\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\tvalidateOutputs(t, rootPath)\n}\n\nfunc TestFeatureFlagRunAll(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testRunAllFlag)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testRunAllFlag)\n\trootPath := filepath.Join(tmpEnvPath, testRunAllFlag)\n\tapp1 := filepath.Join(tmpEnvPath, testRunAllFlag, \"app1\")\n\tapp2 := filepath.Join(tmpEnvPath, testRunAllFlag, \"app2\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- apply -auto-approve\")\n\n\tvalidateOutputs(t, app1)\n\tvalidateOutputs(t, app2)\n}\n\nfunc TestFailOnEmptyFeatureFlag(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testErrorEmptyFlag)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testErrorEmptyFlag)\n\trootPath := filepath.Join(tmpEnvPath, testErrorEmptyFlag)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\trequire.Error(t, err)\n\n\tmessage := err.Error()\n\tassert.Contains(t, message, \"feature flag test1 does not have a default value\")\n\tassert.Contains(t, message, \"feature flag test2 does not have a default value\")\n\tassert.Contains(t, message, \"feature flag test3 does not have a default value\")\n}\n\nfunc expectedDefaults() map[string]any {\n\treturn map[string]any{\n\t\t\"string_feature_flag\": \"test\",\n\t\t\"int_feature_flag\":    666,\n\t\t\"bool_feature_flag\":   false,\n\t}\n}\n\nfunc validateOutputs(t *testing.T, rootPath string) {\n\tt.Helper()\n\tvalidateOutputsMap(t, rootPath, expectedDefaults())\n}\n\nfunc validateOutputsMap(t *testing.T, rootPath string, expected map[string]any) {\n\tt.Helper()\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\tcmd := \"terragrunt output -no-color -json --non-interactive --working-dir \" + rootPath\n\terr := helpers.RunTerragruntCommand(t, cmd, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\t// Validate outputs against expected values\n\tfor key, expected := range expected {\n\t\tassert.EqualValues(t, expected, outputs[key].Value) //nolint:testifylint\n\t}\n}\n"
  },
  {
    "path": "test/integration_filter_graph_test.go",
    "content": "package test_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tgogit \"github.com/go-git/go-git/v6\"\n\t\"github.com/go-git/go-git/v6/plumbing/object\"\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureFilterGraphDAG = \"fixtures/find/dag\"\n\ttestFixtureRunFilter      = \"fixtures/run-filter\"\n)\n\nfunc TestFilterFlagWithFindGraphExpressions(t *testing.T) {\n\tt.Parallel()\n\n\t// Skip if filter-flag experiment is not enabled\n\n\ttestCases := []struct {\n\t\tname           string\n\t\tfilterQuery    string\n\t\texpectedOutput string\n\t\texpectError    bool\n\t}{\n\t\t{\n\t\t\t// a-dependent -> b-dependency\n\t\t\t// So \"a-dependent...\" should find a-dependent and b-dependency\n\t\t\tname:           \"dependency traversal - a-dependent...\",\n\t\t\tfilterQuery:    \"a-dependent...\",\n\t\t\texpectedOutput: \"a-dependent\\nb-dependency\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\t// b-dependency is a dependency of a-dependent, c-mixed-deps, and d-dependencies-only\n\t\t\t// So \"...b-dependency\" should find b-dependency and all its dependents\n\t\t\t// Note: Actually, b-dependency has no dependents in this graph - it's only a dependency\n\t\t\t// But c-mixed-deps depends on a-dependent which depends on b-dependency\n\t\t\t// And d-dependencies-only depends on a-dependent which depends on b-dependency\n\t\t\t// So ...b-dependency should find: b-dependency, a-dependent, c-mixed-deps, d-dependencies-only\n\t\t\tname:           \"dependent traversal - ...b-dependency\",\n\t\t\tfilterQuery:    \"...b-dependency\",\n\t\t\texpectedOutput: \"a-dependent\\nb-dependency\\nc-mixed-deps\\nd-dependencies-only\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\t// a-dependent has dependencies (b-dependency) and dependents (c-mixed-deps, d-dependencies-only)\n\t\t\t// So \"...a-dependent...\" should find all: b-dependency, a-dependent, c-mixed-deps, d-dependencies-only\n\t\t\tname:           \"both directions - ...a-dependent...\",\n\t\t\tfilterQuery:    \"...a-dependent...\",\n\t\t\texpectedOutput: \"a-dependent\\nb-dependency\\nc-mixed-deps\\nd-dependencies-only\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\t// \"a-dependent...\" finds a-dependent and b-dependency\n\t\t\t// \"^a-dependent...\" excludes a-dependent, so only b-dependency\n\t\t\tname:           \"exclude target - ^a-dependent...\",\n\t\t\tfilterQuery:    \"^a-dependent...\",\n\t\t\texpectedOutput: \"b-dependency\\n\",\n\t\t\texpectError:    false,\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureFilterGraphDAG)\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureFilterGraphDAG)\n\t\t\tworkingDir := filepath.Join(tmpEnvPath, testFixtureFilterGraphDAG)\n\n\t\t\tcmd := \"terragrunt find --no-color --working-dir \" + workingDir + \" --filter '\" + tc.filterQuery + \"'\"\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter query: %s\", tc.filterQuery)\n\t\t\t\tassert.NotEmpty(t, stderr, \"Expected error message in stderr\")\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err, \"Unexpected error for filter query: %s\", tc.filterQuery)\n\t\t\t\t// Allow warnings in stderr (e.g., suppressed parsing errors during discovery)\n\t\t\t\t// but ensure there are no actual errors\n\t\t\t\tif stderr != \"\" {\n\t\t\t\t\t// Check that stderr only contains expected warnings, not actual errors\n\t\t\t\t\tlowerStderr := strings.ToLower(stderr)\n\t\t\t\t\tif strings.Contains(lowerStderr, \"error\") && !strings.Contains(lowerStderr, \"suppressed\") && !strings.Contains(lowerStderr, \"warning\") {\n\t\t\t\t\t\tt.Errorf(\"Unexpected error in stderr: %s\", stderr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Sort both outputs for comparison (find output order may vary)\n\t\t\t\texpectedLines := strings.Fields(tc.expectedOutput)\n\t\t\t\tactualLines := strings.Fields(stdout)\n\t\t\t\tassert.ElementsMatch(t, expectedLines, actualLines, \"Output mismatch for filter query: %s\", tc.filterQuery)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFilterFlagWithFindGraphExpressionsJSON(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tfilterQuery   string\n\t\texpectedPaths []string\n\t\texpectError   bool\n\t}{\n\t\t{\n\t\t\tname:          \"dependency traversal - a-dependent... JSON\",\n\t\t\tfilterQuery:   \"a-dependent...\",\n\t\t\texpectedPaths: []string{\"a-dependent\", \"b-dependency\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"dependent traversal - ...b-dependency JSON\",\n\t\t\tfilterQuery:   \"...b-dependency\",\n\t\t\texpectedPaths: []string{\"a-dependent\", \"b-dependency\", \"c-mixed-deps\", \"d-dependencies-only\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"both directions - ...a-dependent... JSON\",\n\t\t\tfilterQuery:   \"...a-dependent...\",\n\t\t\texpectedPaths: []string{\"a-dependent\", \"b-dependency\", \"c-mixed-deps\", \"d-dependencies-only\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"exclude target - ^a-dependent... JSON\",\n\t\t\tfilterQuery:   \"^a-dependent...\",\n\t\t\texpectedPaths: []string{\"b-dependency\"},\n\t\t\texpectError:   false,\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureFilterGraphDAG)\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureFilterGraphDAG)\n\t\t\tworkingDir := filepath.Join(tmpEnvPath, testFixtureFilterGraphDAG)\n\n\t\t\tcmd := \"terragrunt find --no-color --working-dir \" + workingDir + \" --json --filter '\" + tc.filterQuery + \"'\"\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter query: %s\", tc.filterQuery)\n\t\t\t\tassert.NotEmpty(t, stderr, \"Expected error message in stderr\")\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err, \"Unexpected error for filter query: %s\", tc.filterQuery)\n\n\t\t\t// Parse JSON output and verify paths\n\t\t\t// The JSON output should be an array of objects with \"path\" field\n\t\t\tassert.NotEmpty(t, stdout, \"JSON output should not be empty\")\n\t\t\tassert.Contains(t, stdout, \"[\", \"JSON output should be an array\")\n\n\t\t\t// Verify each expected path appears in the JSON output\n\t\t\tfor _, expectedPath := range tc.expectedPaths {\n\t\t\t\tassert.Contains(t, stdout, `\"path\"`, \"JSON output should contain path field\")\n\t\t\t\t// The path might be relative or absolute, so we check for the component name\n\t\t\t\tassert.Contains(t, stdout, expectedPath, \"JSON output should contain path: %s\", expectedPath)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFilterFlagWithRunGraphExpressions(t *testing.T) {\n\tt.Parallel()\n\n\t// Skip if filter-flag experiment is not enabled\n\n\ttestCases := []struct {\n\t\tname         string\n\t\tfilterQuery  string\n\t\terrorPattern string\n\t\texpectError  bool\n\t}{\n\t\t{\n\t\t\tname:        \"dependency traversal - a-dependent...\",\n\t\t\tfilterQuery: \"a-dependent...\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"dependent traversal - ...b-dependency\",\n\t\t\tfilterQuery: \"...b-dependency\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"both directions - ...a-dependent...\",\n\t\t\tfilterQuery: \"...a-dependent...\",\n\t\t\texpectError: false,\n\t\t},\n\t\t{\n\t\t\tname:        \"exclude target - ^a-dependent...\",\n\t\t\tfilterQuery: \"^a-dependent...\",\n\t\t\texpectError: false,\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureFilterGraphDAG)\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureFilterGraphDAG)\n\t\t\tworkingDir := filepath.Join(tmpEnvPath, testFixtureFilterGraphDAG)\n\n\t\t\t// Use a non-destructive command like `plan` to verify the filter works\n\t\t\t// The actual terraform commands will likely fail due to missing providers/resources,\n\t\t\t// but we can verify that the filter parsing and discovery works correctly\n\t\t\t// by checking that we don't get filter-related errors\n\t\t\tcmd := \"terragrunt run --all --non-interactive --working-dir \" + workingDir + \" --filter '\" + tc.filterQuery + \"' -- plan\"\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter query: %s\", tc.filterQuery)\n\n\t\t\t\tif tc.errorPattern != \"\" {\n\t\t\t\t\tassert.Contains(t, stderr, tc.errorPattern, \"Error message should contain expected pattern\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// The command might fail due to terraform init/plan errors (missing providers, etc),\n\t\t\t\t// which is expected in a test environment without full terraform setup.\n\t\t\t\t// The important thing is that the filter was parsed correctly and discovery worked.\n\t\t\t\toutput := stdout + stderr\n\n\t\t\t\t// Verify we don't get filter parsing or evaluation errors\n\t\t\t\terrStr := \"\"\n\t\t\t\tif err != nil {\n\t\t\t\t\terrStr = err.Error()\n\t\t\t\t}\n\n\t\t\t\t// Check for filter-related errors (these would indicate a problem with graph expressions)\n\t\t\t\tif strings.Contains(output, \"filter\") {\n\t\t\t\t\tif strings.Contains(output, \"parse\") || strings.Contains(output, \"syntax\") || strings.Contains(output, \"invalid\") {\n\t\t\t\t\t\tt.Fatalf(\"Filter parsing/evaluation error detected in output: %s\\nOutput: %s\\nStderr: %s\", errStr, stdout, stderr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Check error string directly for filter issues\n\t\t\t\tif err != nil {\n\t\t\t\t\tif strings.Contains(errStr, \"filter\") && (strings.Contains(errStr, \"parse\") || strings.Contains(errStr, \"syntax\") || strings.Contains(errStr, \"invalid\")) {\n\t\t\t\t\t\tt.Fatalf(\"Filter parsing/evaluation error: %v\\nOutput: %s\\nStderr: %s\", err, stdout, stderr)\n\t\t\t\t\t}\n\t\t\t\t\t// Terraform execution errors are acceptable - we're just verifying filter discovery works\n\t\t\t\t\tt.Logf(\"Command completed (Terraform execution errors are expected in test environment): %v\", err)\n\t\t\t\t}\n\n\t\t\t\t// Verify that the command at least attempted to process units (discovery phase completed)\n\t\t\t\t// This is a basic sanity check - if discovery failed, we'd see different errors\n\t\t\t\tassert.NotEmpty(t, output, \"Command should produce some output\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFilterFlagWithRunAllGraphExpressions(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tfilterQuery   string\n\t\texpectedUnits []string\n\t\texpectError   bool\n\t}{\n\t\t{\n\t\t\t// service -> db, cache, vpc (all dependencies)\n\t\t\t// So \"service...\" should execute service and all its dependencies\n\t\t\tname:          \"dependency traversal - service... executes dependencies\",\n\t\t\tfilterQuery:   \"service...\",\n\t\t\texpectedUnits: []string{\"service\", \"db\", \"cache\", \"vpc\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\t// vpc has dependents: db, cache, service (all depend on vpc)\n\t\t\t// So \"...vpc\" should execute all: vpc, db, cache, service\n\t\t\tname:          \"dependent traversal - ...vpc executes all dependents\",\n\t\t\tfilterQuery:   \"...vpc\",\n\t\t\texpectedUnits: []string{\"vpc\", \"db\", \"cache\", \"service\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\t// db has dependency (vpc) and dependent (service)\n\t\t\t// So \"...db...\" should execute all: vpc, db, service\n\t\t\tname:          \"both directions - ...db... executes related units\",\n\t\t\tfilterQuery:   \"...db...\",\n\t\t\texpectedUnits: []string{\"vpc\", \"db\", \"service\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\t// cache has dependency (vpc) and dependent (service)\n\t\t\t// So \"...cache...\" should execute all: vpc, cache, service\n\t\t\tname:          \"both directions - ...cache... executes related units\",\n\t\t\tfilterQuery:   \"...cache...\",\n\t\t\texpectedUnits: []string{\"vpc\", \"cache\", \"service\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\t// \"service...\" finds service, db, cache, vpc\n\t\t\t// \"^service...\" excludes service, so only dependencies should execute\n\t\t\tname:          \"exclude target - ^service... executes only dependencies\",\n\t\t\tfilterQuery:   \"^service...\",\n\t\t\texpectedUnits: []string{\"db\", \"cache\", \"vpc\"},\n\t\t\texpectError:   false,\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureRunFilter)\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRunFilter)\n\t\t\tworkingDir := filepath.Join(tmpEnvPath, testFixtureRunFilter)\n\n\t\t\treportFile := filepath.Join(workingDir, \"report.json\")\n\t\t\tcmd := \"terragrunt run --all --non-interactive --working-dir \" + workingDir + \" --filter '\" + tc.filterQuery + \"' --report-file \" + reportFile + \" --report-format json -- plan\"\n\t\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter query: %s\", tc.filterQuery)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.FileExists(t, reportFile)\n\n\t\t\truns, parseErr := report.ParseJSONRunsFromFile(reportFile)\n\t\t\trequire.NoError(t, parseErr)\n\n\t\t\treportUnits := runs.Names()\n\n\t\t\treportUnitMap := make(map[string]struct{})\n\t\t\tfor _, unit := range reportUnits {\n\t\t\t\treportUnitMap[unit] = struct{}{}\n\t\t\t}\n\n\t\t\tassert.ElementsMatch(t, tc.expectedUnits, reportUnits)\n\t\t})\n\t}\n}\n\nfunc TestFilterFlagWithRunAllGraphExpressionsVerifyExecutionOrder(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureRunFilter)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRunFilter)\n\tworkingDir := filepath.Join(tmpEnvPath, testFixtureRunFilter)\n\n\t// Test that \"service...\" executes vpc, db, cache (dependencies) before service\n\treportFile := filepath.Join(workingDir, \"report.json\")\n\tcmd := \"terragrunt run --all --non-interactive --working-dir \" + workingDir + \" --filter 'service...' --report-file \" + reportFile + \" --report-format json -- plan\"\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\trequire.NoError(t, err)\n\n\trequire.FileExists(t, reportFile)\n\n\truns, parseErr := report.ParseJSONRunsFromFile(reportFile)\n\trequire.NoError(t, parseErr)\n\n\t// Verify execution order: dependencies (vpc, db, cache) should start before service\n\t// We expect: vpc, db, cache should have started before service\n\tdependencies := []string{\"vpc\", \"db\", \"cache\"}\n\tdependent := \"service\"\n\n\tservice := runs.FindByName(dependent)\n\trequire.NotNil(t, service)\n\n\t// Verify each dependency started before service\n\tfor _, depName := range dependencies {\n\t\tdep := runs.FindByName(depName)\n\t\trequire.NotNil(t, dep)\n\n\t\tassert.True(\n\t\t\tt,\n\t\t\tdep.Started.Before(service.Started),\n\t\t)\n\t}\n}\n\n// TestFilterFlagWithFindCombinedGitAndGraphExpressions tests the combination of git-based\n// queries with dependency graph traversal.\nfunc TestFilterFlagWithFindCombinedGitAndGraphExpressions(t *testing.T) {\n\tt.Parallel()\n\n\tsetup := func(t *testing.T) string {\n\t\tt.Helper()\n\n\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\n\t\trunner = runner.WithWorkDir(tmpDir)\n\n\t\terr = runner.Init(t.Context())\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoOpenRepo()\n\t\trequire.NoError(t, err)\n\n\t\tt.Cleanup(func() {\n\t\t\terr = runner.GoCloseStorage()\n\t\t\tif err != nil {\n\t\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t\t}\n\t\t})\n\n\t\t// Create a dependency chain: app -> db -> vpc\n\t\t// We'll modify 'db' and use git+graph filter to find its dependencies and dependents\n\n\t\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\t\terr = os.MkdirAll(vpcDir, 0755)\n\t\trequire.NoError(t, err)\n\n\t\tvpcHCL := `# VPC unit - no dependencies`\n\t\terr = os.WriteFile(filepath.Join(vpcDir, \"terragrunt.hcl\"), []byte(vpcHCL), 0644)\n\t\trequire.NoError(t, err)\n\n\t\tdbDir := filepath.Join(tmpDir, \"db\")\n\t\terr = os.MkdirAll(dbDir, 0755)\n\t\trequire.NoError(t, err)\n\n\t\tdbHCL := `# DB unit - depends on vpc\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n`\n\t\terr = os.WriteFile(filepath.Join(dbDir, \"terragrunt.hcl\"), []byte(dbHCL), 0644)\n\t\trequire.NoError(t, err)\n\n\t\tappDir := filepath.Join(tmpDir, \"app\")\n\t\terr = os.MkdirAll(appDir, 0755)\n\t\trequire.NoError(t, err)\n\n\t\tappHCL := `# App unit - depends on db\ndependency \"db\" {\n  config_path = \"../db\"\n}\n`\n\t\terr = os.WriteFile(filepath.Join(appDir, \"terragrunt.hcl\"), []byte(appHCL), 0644)\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoAdd(\".\")\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoCommit(\"Initial commit with vpc, db, app chain\", &gogit.CommitOptions{\n\t\t\tAuthor: &object.Signature{\n\t\t\t\tName:  \"Test User\",\n\t\t\t\tEmail: \"test@example.com\",\n\t\t\t\tWhen:  time.Now(),\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tmodifiedDBHCL := `# DB unit - depends on vpc (MODIFIED)\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n}\n`\n\t\terr = os.WriteFile(filepath.Join(dbDir, \"terragrunt.hcl\"), []byte(modifiedDBHCL), 0644)\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoAdd(\".\")\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoCommit(\"Modify db unit\", &gogit.CommitOptions{\n\t\t\tAuthor: &object.Signature{\n\t\t\t\tName:  \"Test User\",\n\t\t\t\tEmail: \"test@example.com\",\n\t\t\t\tWhen:  time.Now(),\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\treturn tmpDir\n\t}\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tfilterQuery   string\n\t\tdescription   string\n\t\texpectedUnits []string\n\t\texpectError   bool\n\t}{\n\t\t{\n\t\t\tname:          \"git filter only - baseline\",\n\t\t\tfilterQuery:   \"[HEAD~1...HEAD]\",\n\t\t\texpectedUnits: []string{\"db\"},\n\t\t\tdescription:   \"Baseline: git filter alone should find the modified db unit\",\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"dependencies of git changes - [HEAD~1...HEAD]...\",\n\t\t\tfilterQuery:   \"[HEAD~1...HEAD]...\",\n\t\t\texpectedUnits: []string{\"db\", \"vpc\"},\n\t\t\tdescription:   \"Should find db (git-matched) and vpc (its dependency)\",\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"dependents of git changes - ...[HEAD~1...HEAD]\",\n\t\t\tfilterQuery:   \"...[HEAD~1...HEAD]\",\n\t\t\texpectedUnits: []string{\"db\", \"app\"},\n\t\t\tdescription:   \"Should find db (git-matched) and app (its dependent)\",\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"both directions - ...[HEAD~1...HEAD]...\",\n\t\t\tfilterQuery:   \"...[HEAD~1...HEAD]...\",\n\t\t\texpectedUnits: []string{\"vpc\", \"db\", \"app\"},\n\t\t\tdescription:   \"Issue #5307: Should find db (git-matched), vpc (dependency), and app (dependent)\",\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"exclude target - ^[HEAD~1...HEAD]...\",\n\t\t\tfilterQuery:   \"^[HEAD~1...HEAD]...\",\n\t\t\texpectedUnits: []string{\"vpc\"},\n\t\t\tdescription:   \"Should find vpc (dependency of db), excluding db itself\",\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"exclude target - ^...[HEAD~1...HEAD]\",\n\t\t\tfilterQuery:   \"...^[HEAD~1...HEAD]\",\n\t\t\texpectedUnits: []string{\"app\"},\n\t\t\tdescription:   \"Should find app (dependent of db), excluding db itself\",\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"exclude target both directions - ...^[HEAD~1...HEAD]...\",\n\t\t\tfilterQuery:   \"...^[HEAD~1...HEAD]...\",\n\t\t\texpectedUnits: []string{\"vpc\", \"app\"},\n\t\t\tdescription:   \"Should find vpc (dependency) and app (dependent), excluding db itself\",\n\t\t\texpectError:   false,\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.Parallel()\n\t\t\ttmpDir := setup(t)\n\n\t\t\tcmd := \"terragrunt find --no-color --working-dir \" + tmpDir + \" --filter '\" + tc.filterQuery + \"'\"\n\t\t\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter query: %s\", tc.filterQuery)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tactualUnits := []string{}\n\n\t\t\tfor line := range strings.SplitSeq(strings.TrimSpace(stdout), \"\\n\") {\n\t\t\t\tif line != \"\" {\n\t\t\t\t\tactualUnits = append(actualUnits, filepath.Base(line))\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tassert.ElementsMatch(\n\t\t\t\tt,\n\t\t\t\ttc.expectedUnits,\n\t\t\t\tactualUnits,\n\t\t\t)\n\t\t})\n\t}\n}\n\n// TestFilterFlagWithRunAllCombinedGitAndGraphExpressions tests the `run --all` command\n// with combined git + graph filter expressions.\nfunc TestFilterFlagWithRunAllCombinedGitAndGraphExpressions(t *testing.T) {\n\tt.Parallel()\n\n\tsetup := func(t *testing.T) string {\n\t\tt.Helper()\n\n\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\trunner, err := git.NewGitRunner()\n\t\trequire.NoError(t, err)\n\n\t\trunner = runner.WithWorkDir(tmpDir)\n\n\t\terr = runner.Init(t.Context())\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoOpenRepo()\n\t\trequire.NoError(t, err)\n\n\t\tt.Cleanup(func() {\n\t\t\terr = runner.GoCloseStorage()\n\t\t\tif err != nil {\n\t\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t\t}\n\t\t})\n\n\t\t// Create a dependency chain: service -> cache -> vpc\n\t\t// We'll modify 'cache' and use git+graph filter\n\n\t\tvpcDir := filepath.Join(tmpDir, \"vpc\")\n\t\terr = os.MkdirAll(vpcDir, 0755)\n\t\trequire.NoError(t, err)\n\n\t\tvpcHCL := `# VPC unit`\n\t\terr = os.WriteFile(filepath.Join(vpcDir, \"terragrunt.hcl\"), []byte(vpcHCL), 0644)\n\t\trequire.NoError(t, err)\n\n\t\tvpcTF := `# VPC TF`\n\t\terr = os.WriteFile(filepath.Join(vpcDir, \"main.tf\"), []byte(vpcTF), 0644)\n\t\trequire.NoError(t, err)\n\n\t\tcacheDir := filepath.Join(tmpDir, \"cache\")\n\t\terr = os.MkdirAll(cacheDir, 0755)\n\t\trequire.NoError(t, err)\n\n\t\tcacheHCL := `# Cache unit\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n\n  mock_outputs = {\n    value = \"mock value\"\n  }\n}\n`\n\t\terr = os.WriteFile(filepath.Join(cacheDir, \"terragrunt.hcl\"), []byte(cacheHCL), 0644)\n\t\trequire.NoError(t, err)\n\n\t\tcacheTF := `# Cache TF`\n\t\terr = os.WriteFile(filepath.Join(cacheDir, \"main.tf\"), []byte(cacheTF), 0644)\n\t\trequire.NoError(t, err)\n\n\t\tserviceDir := filepath.Join(tmpDir, \"service\")\n\t\terr = os.MkdirAll(serviceDir, 0755)\n\t\trequire.NoError(t, err)\n\n\t\tserviceHCL := `# Service unit\ndependency \"cache\" {\n  config_path = \"../cache\"\n\n  mock_outputs = {\n    value = \"mock value\"\n  }\n}\n`\n\t\terr = os.WriteFile(filepath.Join(serviceDir, \"terragrunt.hcl\"), []byte(serviceHCL), 0644)\n\t\trequire.NoError(t, err)\n\n\t\tserviceTF := `# Service TF`\n\t\terr = os.WriteFile(filepath.Join(serviceDir, \"main.tf\"), []byte(serviceTF), 0644)\n\t\trequire.NoError(t, err)\n\n\t\t// Initial commit\n\t\terr = runner.GoAdd(\".\")\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoCommit(\"Initial commit\", &gogit.CommitOptions{\n\t\t\tAuthor: &object.Signature{\n\t\t\t\tName:  \"Test User\",\n\t\t\t\tEmail: \"test@example.com\",\n\t\t\t\tWhen:  time.Now(),\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tmodifiedCacheHCL := `# Cache unit (MODIFIED)\ndependency \"vpc\" {\n  config_path = \"../vpc\"\n\n  mock_outputs = {\n    value = \"mock value\"\n  }\n}\n`\n\t\terr = os.WriteFile(filepath.Join(cacheDir, \"terragrunt.hcl\"), []byte(modifiedCacheHCL), 0644)\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoAdd(\".\")\n\t\trequire.NoError(t, err)\n\n\t\terr = runner.GoCommit(\"Modify cache\", &gogit.CommitOptions{\n\t\t\tAuthor: &object.Signature{\n\t\t\t\tName:  \"Test User\",\n\t\t\t\tEmail: \"test@example.com\",\n\t\t\t\tWhen:  time.Now(),\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\treturn tmpDir\n\t}\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tfilterQuery   string\n\t\tdescription   string\n\t\texpectedUnits []string\n\t}{\n\t\t{\n\t\t\tname:          \"git filter only - run baseline\",\n\t\t\tfilterQuery:   \"[HEAD~1...HEAD]\",\n\t\t\texpectedUnits: []string{\"cache\"},\n\t\t\tdescription:   \"Baseline: run with git filter should execute cache\",\n\t\t},\n\t\t{\n\t\t\tname:          \"dependencies of git changes - run\",\n\t\t\tfilterQuery:   \"[HEAD~1...HEAD]...\",\n\t\t\texpectedUnits: []string{\"cache\", \"vpc\"},\n\t\t\tdescription:   \"Should run cache and its dependency vpc\",\n\t\t},\n\t\t{\n\t\t\tname:          \"dependents of git changes - run\",\n\t\t\tfilterQuery:   \"...[HEAD~1...HEAD]\",\n\t\t\texpectedUnits: []string{\"cache\", \"service\"},\n\t\t\tdescription:   \"Should run cache and its dependent service\",\n\t\t},\n\t\t{\n\t\t\tname:          \"both directions - run\",\n\t\t\tfilterQuery:   \"...[HEAD~1...HEAD]...\",\n\t\t\texpectedUnits: []string{\"vpc\", \"cache\", \"service\"},\n\t\t\tdescription:   \"Should run vpc (dep), cache (target), service (dependent)\",\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.Parallel()\n\n\t\t\ttmpDir := setup(t)\n\n\t\t\treportFile := filepath.Join(tmpDir, \"report.json\")\n\t\t\tcmd := \"terragrunt run --all --non-interactive --no-color --working-dir \" + tmpDir +\n\t\t\t\t\" --filter '\" + tc.filterQuery + \"' --report-file \" + reportFile + \" --report-format json -- plan\"\n\n\t\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.FileExists(t, reportFile)\n\n\t\t\truns, parseErr := report.ParseJSONRunsFromFile(reportFile)\n\t\t\trequire.NoError(t, parseErr)\n\n\t\t\tassert.ElementsMatch(t, tc.expectedUnits, runs.Names())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/integration_filter_test.go",
    "content": "package test_test\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\tgogit \"github.com/go-git/go-git/v6\"\n\t\"github.com/go-git/go-git/v6/plumbing\"\n\t\"github.com/go-git/go-git/v6/plumbing/object\"\n)\n\nconst (\n\ttestFixtureFilterBasic            = \"fixtures/find/basic\"\n\ttestFixtureFilterDAG              = \"fixtures/find/dag\"\n\ttestFixtureFilterList             = \"fixtures/list/basic\"\n\ttestFixtureFilterSource           = \"fixtures/filter-source\"\n\ttestFixtureMinimizeParsing        = \"fixtures/filter/minimize-parsing\"\n\ttestFixtureMinimizeParsingDestroy = \"fixtures/filter/minimize-parsing-destroy\"\n\ttestFixtureExcludeByDefault       = \"fixtures/exclude-by-default\"\n\ttestFixtureFilterMarkAsRead       = \"fixtures/filter/mark-as-read\"\n)\n\n// createTestUnit creates a unit directory with terragrunt.hcl and main.tf files.\n// Returns the path to the terragrunt.hcl file for later modification.\nfunc createTestUnit(t *testing.T, dir, comment string) string {\n\tt.Helper()\n\n\terr := os.MkdirAll(dir, 0755)\n\trequire.NoError(t, err)\n\n\thclPath := filepath.Join(dir, \"terragrunt.hcl\")\n\terr = os.WriteFile(hclPath, []byte(comment), 0644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(dir, \"main.tf\"), []byte(`# Minimal terraform config`), 0644)\n\trequire.NoError(t, err)\n\n\treturn hclPath\n}\n\nfunc TestFilterFlagWithFind(t *testing.T) {\n\tt.Parallel()\n\n\tworkingDir, err := filepath.Abs(testFixtureFilterBasic)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname           string\n\t\tfilterQuery    string\n\t\texpectedOutput string\n\t\texpectError    bool\n\t}{\n\t\t{\n\t\t\tname:           \"filter by path - exact match\",\n\t\t\tfilterQuery:    \"unit\",\n\t\t\texpectedOutput: \"unit\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by path - wildcard\",\n\t\t\tfilterQuery:    \"./*\",\n\t\t\texpectedOutput: \"stack\\nunit\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by name - exact match\",\n\t\t\tfilterQuery:    \"unit\",\n\t\t\texpectedOutput: \"unit\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by type - unit only\",\n\t\t\tfilterQuery:    \"type=unit\",\n\t\t\texpectedOutput: \"unit\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by type - stack only\",\n\t\t\tfilterQuery:    \"type=stack\",\n\t\t\texpectedOutput: \"stack\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter with negation - exclude unit\",\n\t\t\tfilterQuery:    \"!unit\",\n\t\t\texpectedOutput: \"stack\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter with negation - exclude path\",\n\t\t\tfilterQuery:    \"!./unit\",\n\t\t\texpectedOutput: \"stack\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter with intersection - path and type\",\n\t\t\tfilterQuery:    \"./unit | type=unit\",\n\t\t\texpectedOutput: \"unit\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter with intersection - path and negation\",\n\t\t\tfilterQuery:    \"./* | !unit\",\n\t\t\texpectedOutput: \"stack\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter with braced path\",\n\t\t\tfilterQuery:    \"{./unit}\",\n\t\t\texpectedOutput: \"unit\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter with non-matching query\",\n\t\t\tfilterQuery:    \"nonexistent\",\n\t\t\texpectedOutput: \"\",\n\t\t\texpectError:    false,\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, workingDir)\n\n\t\t\tcmd := \"terragrunt find --no-color --working-dir \" + workingDir + \" --filter '\" + tc.filterQuery + \"'\"\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter query: %s\", tc.filterQuery)\n\t\t\t\tassert.NotEmpty(t, stderr, \"Expected error message in stderr\")\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err, \"Unexpected error for filter query: %s\", tc.filterQuery)\n\t\t\t\tassert.Empty(t, stderr, \"Unexpected error message in stderr\")\n\t\t\t\tassert.Equal(t, tc.expectedOutput, stdout, \"Output mismatch for filter query: %s\", tc.filterQuery)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFilterFlagWithFindJSON(t *testing.T) {\n\tt.Parallel()\n\n\tworkingDir, err := filepath.Abs(testFixtureFilterBasic)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname           string\n\t\tfilterQuery    string\n\t\texpectedOutput string\n\t\texpectError    bool\n\t}{\n\t\t{\n\t\t\tname:           \"filter by type - unit only JSON\",\n\t\t\tfilterQuery:    \"type=unit\",\n\t\t\texpectedOutput: `[{\"type\": \"unit\", \"path\": \"unit\"}]`,\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by type - stack only JSON\",\n\t\t\tfilterQuery:    \"type=stack\",\n\t\t\texpectedOutput: `[{\"type\": \"stack\", \"path\": \"stack\"}]`,\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by name - exact match JSON\",\n\t\t\tfilterQuery:    \"unit\",\n\t\t\texpectedOutput: `[{\"type\": \"unit\", \"path\": \"unit\"}]`,\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter with negation - exclude unit JSON\",\n\t\t\tfilterQuery:    \"!unit\",\n\t\t\texpectedOutput: `[{\"type\": \"stack\", \"path\": \"stack\"}]`,\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter with intersection JSON\",\n\t\t\tfilterQuery:    \"./unit | type=unit\",\n\t\t\texpectedOutput: `[{\"type\": \"unit\", \"path\": \"unit\"}]`,\n\t\t\texpectError:    false,\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, workingDir)\n\n\t\t\tcmd := \"terragrunt find --no-color --working-dir \" + workingDir + \" --json --filter \" + tc.filterQuery\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter query: %s\", tc.filterQuery)\n\t\t\t\tassert.NotEmpty(t, stderr, \"Expected error message in stderr\")\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err, \"Unexpected error for filter query: %s\", tc.filterQuery)\n\t\t\t\tassert.Empty(t, stderr, \"Unexpected error message in stderr\")\n\t\t\t\tassert.JSONEq(t, tc.expectedOutput, stdout, \"JSON output mismatch for filter query: %s\", tc.filterQuery)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFilterFlagWithList(t *testing.T) {\n\tt.Parallel()\n\n\t// The CLI constructor ensures that the working directory is always absolute.\n\tworkingDir, err := filepath.Abs(testFixtureFilterList)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname            string\n\t\tfilterQuery     string\n\t\texpectedResults []string\n\t\texpectError     bool\n\t}{\n\t\t{\n\t\t\tname:            \"filter by name - exact match\",\n\t\t\tfilterQuery:     \"a-unit\",\n\t\t\texpectedResults: []string{\"a-unit\"},\n\t\t\texpectError:     false,\n\t\t},\n\t\t{\n\t\t\tname:            \"filter by name - exact match with equals\",\n\t\t\tfilterQuery:     \"name=a-unit\",\n\t\t\texpectedResults: []string{\"a-unit\"},\n\t\t\texpectError:     false,\n\t\t},\n\t\t{\n\t\t\tname:            \"filter by type - unit only\",\n\t\t\tfilterQuery:     \"type=unit\",\n\t\t\texpectedResults: []string{\"a-unit\", \"b-unit\"},\n\t\t\texpectError:     false,\n\t\t},\n\t\t{\n\t\t\tname:            \"filter with negation - exclude a-unit\",\n\t\t\tfilterQuery:     \"!a-unit\",\n\t\t\texpectedResults: []string{\"b-unit\"},\n\t\t\texpectError:     false,\n\t\t},\n\t\t{\n\t\t\tname:            \"filter with negation - exclude path\",\n\t\t\tfilterQuery:     \"!./a-unit\",\n\t\t\texpectedResults: []string{\"b-unit\"},\n\t\t\texpectError:     false,\n\t\t},\n\t\t{\n\t\t\tname:            \"filter with intersection - name and type\",\n\t\t\tfilterQuery:     \"a-unit | type=unit\",\n\t\t\texpectedResults: []string{\"a-unit\"},\n\t\t\texpectError:     false,\n\t\t},\n\t\t{\n\t\t\tname:            \"filter with wildcard path\",\n\t\t\tfilterQuery:     \"./*\",\n\t\t\texpectedResults: []string{\"a-unit\", \"b-unit\"},\n\t\t\texpectError:     false,\n\t\t},\n\t\t{\n\t\t\tname:            \"filter with braced path\",\n\t\t\tfilterQuery:     \"{a-unit}\",\n\t\t\texpectedResults: []string{\"a-unit\"},\n\t\t\texpectError:     false,\n\t\t},\n\t\t{\n\t\t\tname:            \"filter with non-matching query\",\n\t\t\tfilterQuery:     \"nonexistent\",\n\t\t\texpectedResults: []string{},\n\t\t\texpectError:     false,\n\t\t},\n\t\t{\n\t\t\tname:            \"filter with invalid syntax\",\n\t\t\tfilterQuery:     \"invalid{filter\",\n\t\t\texpectedResults: []string{},\n\t\t\texpectError:     true,\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, workingDir)\n\n\t\t\tcmd := \"terragrunt list --no-color --working-dir \" + workingDir + \" --filter \" + tc.filterQuery\n\t\t\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter query: %s\", tc.filterQuery)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err, \"Unexpected error for filter query: %s\", tc.filterQuery)\n\n\t\t\tresults := strings.Fields(stdout)\n\t\t\tassert.ElementsMatch(t, tc.expectedResults, results, \"Output mismatch for filter query: %s\", tc.filterQuery)\n\t\t})\n\t}\n}\n\nfunc TestFilterFlagWithListLong(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname           string\n\t\tworkingDir     string\n\t\tfilterQuery    string\n\t\texpectedOutput string\n\t\texpectError    bool\n\t}{\n\t\t{\n\t\t\tname:           \"filter by name - exact match long format\",\n\t\t\tworkingDir:     testFixtureFilterList,\n\t\t\tfilterQuery:    \"a-unit\",\n\t\t\texpectedOutput: \"Type  Path\\nunit  a-unit\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by type - unit only long format\",\n\t\t\tworkingDir:     testFixtureFilterList,\n\t\t\tfilterQuery:    \"type=unit\",\n\t\t\texpectedOutput: \"Type  Path\\nunit  a-unit\\nunit  b-unit\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter with negation - exclude a-unit long format\",\n\t\t\tworkingDir:     testFixtureFilterList,\n\t\t\tfilterQuery:    \"!a-unit\",\n\t\t\texpectedOutput: \"Type  Path\\nunit  b-unit\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter with intersection long format\",\n\t\t\tworkingDir:     testFixtureFilterList,\n\t\t\tfilterQuery:    \"a-unit | type=unit\",\n\t\t\texpectedOutput: \"Type  Path\\nunit  a-unit\\n\",\n\t\t\texpectError:    false,\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, tc.workingDir)\n\n\t\t\tcmd := \"terragrunt list --no-color --working-dir \" + tc.workingDir + \" --long --filter \" + tc.filterQuery\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter query: %s\", tc.filterQuery)\n\t\t\t\tassert.NotEmpty(t, stderr, \"Expected error message in stderr\")\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err, \"Unexpected error for filter query: %s\", tc.filterQuery)\n\t\t\t\tassert.Empty(t, stderr, \"Unexpected error message in stderr\")\n\t\t\t\tassert.Equal(t, tc.expectedOutput, stdout, \"Output mismatch for filter query: %s\", tc.filterQuery)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFilterFlagWithListTree(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname           string\n\t\tworkingDir     string\n\t\tfilterQuery    string\n\t\texpectedOutput string\n\t\texpectError    bool\n\t}{\n\t\t{\n\t\t\tname:           \"filter by name - exact match tree format\",\n\t\t\tworkingDir:     testFixtureFilterList,\n\t\t\tfilterQuery:    \"a-unit\",\n\t\t\texpectedOutput: \".\\n╰── a-unit\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by type - unit only tree format\",\n\t\t\tworkingDir:     testFixtureFilterList,\n\t\t\tfilterQuery:    \"type=unit\",\n\t\t\texpectedOutput: \".\\n├── a-unit\\n╰── b-unit\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter with negation - exclude a-unit tree format\",\n\t\t\tworkingDir:     testFixtureFilterList,\n\t\t\tfilterQuery:    \"!a-unit\",\n\t\t\texpectedOutput: \".\\n╰── b-unit\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter with intersection tree format\",\n\t\t\tworkingDir:     testFixtureFilterList,\n\t\t\tfilterQuery:    \"a-unit | type=unit\",\n\t\t\texpectedOutput: \".\\n╰── a-unit\\n\",\n\t\t\texpectError:    false,\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, tc.workingDir)\n\n\t\t\tcmd := \"terragrunt list --no-color --working-dir \" + tc.workingDir + \" --tree --filter \" + tc.filterQuery\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter query: %s\", tc.filterQuery)\n\t\t\t\tassert.NotEmpty(t, stderr, \"Expected error message in stderr\")\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err, \"Unexpected error for filter query: %s\", tc.filterQuery)\n\t\t\t\tassert.Empty(t, stderr, \"Unexpected error message in stderr\")\n\t\t\t\tassert.Equal(t, tc.expectedOutput, stdout, \"Output mismatch for filter query: %s\", tc.filterQuery)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFilterFlagWithDAG(t *testing.T) {\n\tt.Parallel()\n\n\tworkingDir, err := filepath.Abs(testFixtureFilterDAG)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname           string\n\t\tfilterQuery    string\n\t\texpectedOutput string\n\t\texpectError    bool\n\t}{\n\t\t{\n\t\t\tname:           \"filter by path - specific component\",\n\t\t\tfilterQuery:    \"./a-dependent\",\n\t\t\texpectedOutput: \"a-dependent\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by name - specific component\",\n\t\t\tfilterQuery:    \"a-dependent\",\n\t\t\texpectedOutput: \"a-dependent\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by type - unit only\",\n\t\t\tfilterQuery:    \"type=unit\",\n\t\t\texpectedOutput: \"a-dependent\\nb-dependency\\nc-mixed-deps\\nd-dependencies-only\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter with negation - exclude specific component\",\n\t\t\tfilterQuery:    \"!a-dependent\",\n\t\t\texpectedOutput: \"b-dependency\\nc-mixed-deps\\nd-dependencies-only\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter with wildcard - all components\",\n\t\t\tfilterQuery:    \"./*\",\n\t\t\texpectedOutput: \"a-dependent\\nb-dependency\\nc-mixed-deps\\nd-dependencies-only\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter with intersection - path and type\",\n\t\t\tfilterQuery:    \"./a-dependent | type=unit\",\n\t\t\texpectedOutput: \"a-dependent\\n\",\n\t\t\texpectError:    false,\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, workingDir)\n\n\t\t\tcmd := \"terragrunt find --no-color --working-dir \" + workingDir + \" --filter \" + tc.filterQuery\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter query: %s\", tc.filterQuery)\n\t\t\t\tassert.NotEmpty(t, stderr, \"Expected error message in stderr\")\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err, \"Unexpected error for filter query: %s\", tc.filterQuery)\n\t\t\t\tassert.Empty(t, stderr, \"Unexpected error message in stderr\")\n\t\t\t\tassert.Equal(t, tc.expectedOutput, stdout, \"Output mismatch for filter query: %s\", tc.filterQuery)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFilterFlagMultipleFilters(t *testing.T) {\n\tt.Parallel()\n\n\tworkingDir, err := filepath.Abs(testFixtureFilterBasic)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname           string\n\t\texpectedOutput string\n\t\tfilterQueries  []string\n\t\texpectError    bool\n\t}{\n\t\t{\n\t\t\tname:           \"multiple filters - union semantics\",\n\t\t\tfilterQueries:  []string{\"./unit\", \"./stack\"},\n\t\t\texpectedOutput: \"stack\\nunit\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"multiple filters with negation\",\n\t\t\tfilterQueries:  []string{\"./*\", \"!unit\"},\n\t\t\texpectedOutput: \"stack\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"multiple filters with type\",\n\t\t\tfilterQueries:  []string{\"type=unit\", \"type=stack\"},\n\t\t\texpectedOutput: \"stack\\nunit\\n\",\n\t\t\texpectError:    false,\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, workingDir)\n\n\t\t\t// Build command with multiple --filter flags\n\t\t\tcmd := \"terragrunt find --no-color --working-dir \" + workingDir\n\n\t\t\tvar cmdSb551 strings.Builder\n\n\t\t\tfor _, filter := range tc.filterQueries {\n\t\t\t\tcmdSb551.WriteString(\" --filter \" + filter)\n\t\t\t}\n\n\t\t\tcmd += cmdSb551.String()\n\n\t\t\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter queries: %v\", tc.filterQueries)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err, \"Unexpected error for filter queries: %v\", tc.filterQueries)\n\t\t\t\tassert.Equal(t, tc.expectedOutput, stdout, \"Output mismatch for filter queries: %v\", tc.filterQueries)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFilterFlagEdgeCases(t *testing.T) {\n\tt.Parallel()\n\n\tworkingDir, err := filepath.Abs(testFixtureFilterBasic)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname           string\n\t\tfilterQuery    string\n\t\texpectedOutput string\n\t\texpectError    bool\n\t}{\n\t\t{\n\t\t\tname:           \"filter with spaces in name\",\n\t\t\tfilterQuery:    \"unit\",\n\t\t\texpectedOutput: \"unit\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter with double negation\",\n\t\t\tfilterQuery:    \"!!unit\",\n\t\t\texpectedOutput: \"unit\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter with empty intersection\",\n\t\t\tfilterQuery:    \"unit|nonexistent\", // Our testing arg parsing is busted. Don't put whitespace between these.\n\t\t\texpectedOutput: \"\",\n\t\t\texpectError:    false,\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, workingDir)\n\n\t\t\tcmd := \"terragrunt find --no-color --working-dir \" + workingDir + \" --filter '\" + tc.filterQuery + \"'\"\n\t\t\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter query: %s\", tc.filterQuery)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err, \"Unexpected error for filter query: %s\", tc.filterQuery)\n\t\t\t\tassert.Equal(t, tc.expectedOutput, stdout, \"Output mismatch for filter query: %s\", tc.filterQuery)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFilterFlagWithSource(t *testing.T) {\n\tt.Parallel()\n\n\tworkingDir, err := filepath.Abs(testFixtureFilterSource)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname           string\n\t\tfilterQuery    string\n\t\texpectedOutput string\n\t\texpectError    bool\n\t}{\n\t\t{\n\t\t\tname:           \"filter by source - exact match github.com/acme/foo\",\n\t\t\tfilterQuery:    \"source=github.com/acme/foo\",\n\t\t\texpectedOutput: \"github-acme-foo\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by source - glob pattern *github.com**acme/*\",\n\t\t\tfilterQuery:    \"source=*github.com**acme/*\",\n\t\t\texpectedOutput: \"github-acme-foo\\ngithub-acme-bar\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by source - glob pattern git::git@github.com:acme/**\",\n\t\t\tfilterQuery:    \"source=git::git@github.com:acme/**\",\n\t\t\texpectedOutput: \"github-acme-bar\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by source - glob pattern **github.com**\",\n\t\t\tfilterQuery:    \"source=**github.com**\",\n\t\t\texpectedOutput: \"github-acme-foo\\ngithub-acme-bar\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by source - exact match gitlab.com/example/baz\",\n\t\t\tfilterQuery:    \"source=gitlab.com/example/baz\",\n\t\t\texpectedOutput: \"gitlab-example-baz\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by source - glob pattern gitlab.com/**\",\n\t\t\tfilterQuery:    \"source=gitlab.com/**\",\n\t\t\texpectedOutput: \"gitlab-example-baz\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by source - local module\",\n\t\t\tfilterQuery:    \"source=./module\",\n\t\t\texpectedOutput: \"local-module\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by source - non-matching query\",\n\t\t\tfilterQuery:    \"source=nonexistent\",\n\t\t\texpectedOutput: \"\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by source with negation - exclude github.com/acme/foo\",\n\t\t\tfilterQuery:    \"!source=github.com/acme/foo\",\n\t\t\texpectedOutput: \"github-acme-bar\\ngitlab-example-baz\\nlocal-module\\n\",\n\t\t\texpectError:    false,\n\t\t},\n\t\t{\n\t\t\tname:           \"filter by source with intersection - github.com/acme/* and path\",\n\t\t\tfilterQuery:    \"source=github.com/acme/* | ./github-acme-foo\",\n\t\t\texpectedOutput: \"github-acme-foo\\n\",\n\t\t\texpectError:    false,\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, workingDir)\n\n\t\t\tcmd := \"terragrunt find --no-color --working-dir \" + workingDir + \" --filter '\" + tc.filterQuery + \"'\"\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter query: %s\", tc.filterQuery)\n\t\t\t\tassert.NotEmpty(t, stderr, \"Expected error message in stderr\")\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err, \"Unexpected error for filter query: %s\", tc.filterQuery)\n\t\t\t\tassert.Empty(t, stderr, \"Unexpected error message in stderr\")\n\t\t\t\t// Sort both outputs for comparison since order may vary\n\t\t\t\texpectedLines := strings.Fields(tc.expectedOutput)\n\t\t\t\tactualLines := strings.Fields(stdout)\n\t\t\t\tassert.ElementsMatch(t, expectedLines, actualLines, \"Output mismatch for filter query: %s\", tc.filterQuery)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFilterFlagWithFindGitFilter(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpDir)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\terr = runner.GoOpenRepo()\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\terr = runner.GoCloseStorage()\n\t\tif err != nil {\n\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t}\n\t})\n\n\t// Create three units initially\n\tunitToBeModifiedDir := filepath.Join(tmpDir, \"unit-to-be-modified\")\n\tunitToBeRemovedDir := filepath.Join(tmpDir, \"unit-to-be-removed\")\n\tunitToBeUntouchedDir := filepath.Join(tmpDir, \"unit-to-be-untouched\")\n\n\terr = os.MkdirAll(unitToBeModifiedDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.MkdirAll(unitToBeRemovedDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.MkdirAll(unitToBeUntouchedDir, 0755)\n\trequire.NoError(t, err)\n\n\t// Create minimal terragrunt.hcl files for each unit\n\tunitToBeModifiedHCLPath := filepath.Join(unitToBeModifiedDir, \"terragrunt.hcl\")\n\terr = os.WriteFile(unitToBeModifiedHCLPath, []byte(`# Unit to be modified`), 0644)\n\trequire.NoError(t, err)\n\n\tunitToBeRemovedHCLPath := filepath.Join(unitToBeRemovedDir, \"terragrunt.hcl\")\n\terr = os.WriteFile(unitToBeRemovedHCLPath, []byte(`# Unit to be removed`), 0644)\n\trequire.NoError(t, err)\n\n\tunitToBeUntouchedHCLPath := filepath.Join(unitToBeUntouchedDir, \"terragrunt.hcl\")\n\terr = os.WriteFile(unitToBeUntouchedHCLPath, []byte(`# Unit to be untouched`), 0644)\n\trequire.NoError(t, err)\n\n\t// Initial commit\n\terr = runner.GoAdd(\".\")\n\trequire.NoError(t, err)\n\n\terr = runner.GoCommit(\"Initial commit\", &gogit.CommitOptions{\n\t\tAuthor: &object.Signature{\n\t\t\tName:  \"Test User\",\n\t\t\tEmail: \"test@example.com\",\n\t\t\tWhen:  time.Now(),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\thead, err := runner.GoOpenRepoHead()\n\trequire.NoError(t, err)\n\n\t// If users don't have a default branch set, we'll make sure that the `main` branch exists\n\tb, err := runner.Config(t.Context(), \"init.defaultBranch\")\n\tif err != nil || b != \"main\" {\n\t\terr = runner.GoCheckout(&gogit.CheckoutOptions{\n\t\t\tBranch: plumbing.ReferenceName(\"refs/heads/main\"),\n\t\t\tCreate: true,\n\t\t\tHash:   head.Hash(),\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n\n\t// We'll checkout a new branch so that we can compare against main in the filter-affected flag test\n\terr = runner.GoCheckout(&gogit.CheckoutOptions{\n\t\tBranch: plumbing.ReferenceName(\"refs/heads/filter-affected-test\"),\n\t\tCreate: true,\n\t\tHash:   head.Hash(),\n\t})\n\trequire.NoError(t, err)\n\n\t// Modify the unit to be modified\n\terr = os.WriteFile(unitToBeModifiedHCLPath, []byte(`# Unit modified`), 0644)\n\trequire.NoError(t, err)\n\n\t// Remove the unit to be removed (delete the directory)\n\terr = os.RemoveAll(unitToBeRemovedDir)\n\trequire.NoError(t, err)\n\n\t// Add a unit to be created\n\tunitToBeCreatedDir := filepath.Join(tmpDir, \"unit-to-be-created\")\n\terr = os.MkdirAll(unitToBeCreatedDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(unitToBeCreatedDir, \"terragrunt.hcl\"), []byte(`# Unit created`), 0644)\n\trequire.NoError(t, err)\n\n\t// Do nothing to the unit to be untouched\n\n\t// Commit the modification and removal in a single commit\n\terr = runner.GoAdd(\".\")\n\trequire.NoError(t, err)\n\n\terr = runner.GoCommit(\"Create, modify, and remove units\", &gogit.CommitOptions{\n\t\tAuthor: &object.Signature{\n\t\t\tName:  \"Test User\",\n\t\t\tEmail: \"test@example.com\",\n\t\t\tWhen:  time.Now(),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Clean up terraform folders before running\n\thelpers.CleanupTerraformFolder(t, tmpDir)\n\n\ttestCases := []struct {\n\t\tname                  string\n\t\tfilterQuery           string\n\t\texpectedUnits         []string\n\t\tuseFilterAffectedFlag bool\n\t\texpectError           bool\n\t}{\n\t\t{\n\t\t\tname:          \"standard git filter\",\n\t\t\tfilterQuery:   \"[HEAD~1...HEAD]\",\n\t\t\texpectedUnits: []string{\"unit-to-be-created\", \"unit-to-be-modified\", \"unit-to-be-removed\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:                  \"filter-affected flag\",\n\t\t\texpectedUnits:         []string{\"unit-to-be-created\", \"unit-to-be-modified\", \"unit-to-be-removed\"},\n\t\t\tuseFilterAffectedFlag: true,\n\t\t\texpectError:           false,\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.Parallel()\n\t\t\thelpers.CleanupTerraformFolder(t, tmpDir)\n\n\t\t\tcmd := \"terragrunt find --no-color --working-dir \" + tmpDir\n\t\t\tif tc.useFilterAffectedFlag {\n\t\t\t\tcmd += \" --filter-affected\"\n\t\t\t}\n\n\t\t\tif tc.filterQuery != \"\" {\n\t\t\t\tcmd += \" --filter '\" + tc.filterQuery + \"'\"\n\t\t\t}\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter query: %s\", tc.filterQuery)\n\t\t\t\tassert.NotEmpty(t, stderr, \"Expected error message in stderr\")\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tresults := strings.Split(strings.TrimSpace(stdout), \"\\n\")\n\t\t\tassert.ElementsMatch(t, tc.expectedUnits, results)\n\t\t})\n\t}\n}\n\nfunc TestFilterFlagWithFindGitFilterRelativeInclude(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpDir)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\terr = runner.GoOpenRepo()\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\terr = runner.GoCloseStorage()\n\t\tif err != nil {\n\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t}\n\t})\n\n\t// Create a root.hcl at the repo root that the nested unit will include\n\trootHCLPath := filepath.Join(tmpDir, \"root.hcl\")\n\terr = os.WriteFile(rootHCLPath, []byte(`# Root config\n`), 0644)\n\trequire.NoError(t, err)\n\n\t// Create a deeply nested unit that uses get_path_to_repo_root() in its include path\n\tnestedUnitDir := filepath.Join(tmpDir, \"level1\", \"level2\", \"level3\", \"nested-unit\")\n\terr = os.MkdirAll(nestedUnitDir, 0755)\n\trequire.NoError(t, err)\n\n\tnestedUnitHCLPath := filepath.Join(nestedUnitDir, \"terragrunt.hcl\")\n\terr = os.WriteFile(nestedUnitHCLPath, []byte(`include \"root\" {\n  path = \"${get_path_to_repo_root()}/root.hcl\"\n}\n`), 0644)\n\trequire.NoError(t, err)\n\n\t// Initial commit on main\n\terr = runner.GoAdd(\".\")\n\trequire.NoError(t, err)\n\n\terr = runner.GoCommit(\"Initial commit\", &gogit.CommitOptions{\n\t\tAuthor: &object.Signature{\n\t\t\tName:  \"Test User\",\n\t\t\tEmail: \"test@example.com\",\n\t\t\tWhen:  time.Now(),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\thead, err := runner.GoOpenRepoHead()\n\trequire.NoError(t, err)\n\n\t// Ensure the main branch exists\n\tb, err := runner.Config(t.Context(), \"init.defaultBranch\")\n\tif err != nil || b != \"main\" {\n\t\terr = runner.GoCheckout(&gogit.CheckoutOptions{\n\t\t\tBranch: plumbing.ReferenceName(\"refs/heads/main\"),\n\t\t\tCreate: true,\n\t\t\tHash:   head.Hash(),\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n\n\t// Create a feature branch\n\terr = runner.GoCheckout(&gogit.CheckoutOptions{\n\t\tBranch: plumbing.ReferenceName(\"refs/heads/relative-include-test\"),\n\t\tCreate: true,\n\t\tHash:   head.Hash(),\n\t})\n\trequire.NoError(t, err)\n\n\t// Modify the nested unit\n\terr = os.WriteFile(nestedUnitHCLPath, []byte(`include \"root\" {\n  path = \"${get_path_to_repo_root()}/root.hcl\"\n}\n\n# Modified on feature branch\n`), 0644)\n\trequire.NoError(t, err)\n\n\terr = runner.GoAdd(\".\")\n\trequire.NoError(t, err)\n\n\terr = runner.GoCommit(\"Modify nested unit\", &gogit.CommitOptions{\n\t\tAuthor: &object.Signature{\n\t\t\tName:  \"Test User\",\n\t\t\tEmail: \"test@example.com\",\n\t\t\tWhen:  time.Now(),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\thelpers.CleanupTerraformFolder(t, tmpDir)\n\n\tcmd := \"terragrunt find --no-color --working-dir \" + tmpDir + \" --filter '[main...HEAD]'\"\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\trequire.NoError(t, err, \"terragrunt find with git filter failed: %s\", stderr)\n\n\tresults := strings.Split(strings.TrimSpace(stdout), \"\\n\")\n\tassert.ElementsMatch(t, []string{\"level1/level2/level3/nested-unit\"}, results)\n}\n\nfunc TestFilterFlagWithRunAllGitFilter(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname               string\n\t\tfilterQuery        string\n\t\tdescription        string\n\t\texpectedUnits      []string\n\t\tignoredUnits       []string\n\t\texpectedExcluded   []string\n\t\tfilterAllowDestroy bool\n\t\texpectError        bool\n\t}{\n\t\t{\n\t\t\tname:               \"git filter discovers modified, created, and removed units and excludes untouched\",\n\t\t\tfilterQuery:        \"[HEAD~1...HEAD]\",\n\t\t\tfilterAllowDestroy: false,\n\t\t\texpectedUnits:      []string{\"unit-to-be-created\", \"unit-to-be-modified\"},\n\t\t\tignoredUnits:       []string{\"unit-to-be-untouched\"},\n\t\t\texpectedExcluded:   []string{\"unit-to-be-removed\"},\n\t\t\texpectError:        false,\n\t\t\tdescription:        \"Git filter should discover units that were created, modified, or removed between commits, and exclude untouched units. Removed unit should be excluded without --filter-allow-destroy\",\n\t\t},\n\t\t{\n\t\t\tname:               \"git filter with --filter-allow-destroy includes removed unit\",\n\t\t\tfilterQuery:        \"[HEAD~1...HEAD]\",\n\t\t\tfilterAllowDestroy: true,\n\t\t\texpectedUnits:      []string{\"unit-to-be-created\", \"unit-to-be-modified\", \"unit-to-be-removed\"},\n\t\t\tignoredUnits:       []string{\"unit-to-be-untouched\"},\n\t\t\texpectedExcluded:   []string{},\n\t\t\texpectError:        false,\n\t\t\tdescription:        \"Git filter with --filter-allow-destroy should include removed unit for destroy operations\",\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.Parallel()\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\trunner, err := git.NewGitRunner()\n\t\t\trequire.NoError(t, err)\n\n\t\t\trunner = runner.WithWorkDir(tmpDir)\n\n\t\t\terr = runner.Init(t.Context())\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = runner.GoOpenRepo()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tt.Cleanup(func() {\n\t\t\t\terr = runner.GoCloseStorage()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\t// Create three units initially using helper\n\t\t\tunitToBeModifiedDir := filepath.Join(tmpDir, \"unit-to-be-modified\")\n\t\t\tunitToBeRemovedDir := filepath.Join(tmpDir, \"unit-to-be-removed\")\n\t\t\tunitToBeUntouchedDir := filepath.Join(tmpDir, \"unit-to-be-untouched\")\n\n\t\t\tunitToBeModifiedHCLPath := createTestUnit(t, unitToBeModifiedDir, `# Unit to be modified`)\n\t\t\t_ = createTestUnit(t, unitToBeRemovedDir, `# Unit to be removed`)\n\t\t\t_ = createTestUnit(t, unitToBeUntouchedDir, `# Unit to be untouched`)\n\n\t\t\t// Initial commit\n\t\t\terr = runner.GoAdd(\".\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = runner.GoCommit(\"Initial commit\", &gogit.CommitOptions{\n\t\t\t\tAuthor: &object.Signature{\n\t\t\t\t\tName:  \"Test User\",\n\t\t\t\t\tEmail: \"test@example.com\",\n\t\t\t\t\tWhen:  time.Now(),\n\t\t\t\t},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Modify the unit to be modified\n\t\t\terr = os.WriteFile(unitToBeModifiedHCLPath, []byte(`# Unit modified`), 0644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Remove the unit to be removed (delete the directory)\n\t\t\terr = os.RemoveAll(unitToBeRemovedDir)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Add a unit to be created\n\t\t\tunitToBeCreatedDir := filepath.Join(tmpDir, \"unit-to-be-created\")\n\t\t\t_ = createTestUnit(t, unitToBeCreatedDir, `# Unit created`)\n\n\t\t\t// Do nothing to the unit to be untouched\n\n\t\t\t// Commit the modification and removal in a single commit\n\t\t\terr = runner.GoAdd(\".\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = runner.GoCommit(\"Create, modify, and remove units\", &gogit.CommitOptions{\n\t\t\t\tAuthor: &object.Signature{\n\t\t\t\t\tName:  \"Test User\",\n\t\t\t\t\tEmail: \"test@example.com\",\n\t\t\t\t\tWhen:  time.Now(),\n\t\t\t\t},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Run terragrunt run --all --filter with git filter\n\t\t\t// Note: We use 'plan' command which should work even without terraform init\n\t\t\t// Note: --experiment-mode enables the filter-flag experiment required for --filter\n\t\t\tcmd := \"terragrunt run --all --no-color --experiment-mode --working-dir \" + tmpDir + \" --filter '\" + tc.filterQuery + \"' --report-file \" + helpers.ReportFile\n\n\t\t\tif tc.filterAllowDestroy {\n\t\t\t\tcmd += \" --filter-allow-destroy\"\n\t\t\t}\n\n\t\t\tcmd += \" -- plan\"\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter query: %s\", tc.filterQuery)\n\t\t\t\tassert.NotEmpty(t, stderr, \"Expected error message in stderr\")\n\t\t\t} else {\n\t\t\t\t// For run commands, we expect some output even if terraform isn't fully initialized\n\t\t\t\t// The key is that the command should execute and process the filtered units\n\t\t\t\tif err != nil {\n\t\t\t\t\t// If there's an error, it might be because terraform isn't initialized\n\t\t\t\t\t// but we should still see that the filter worked (units were discovered)\n\t\t\t\t\t// Let's check if the error is about terraform init or similar\n\t\t\t\t\tif !strings.Contains(stderr, \"terraform\") && !strings.Contains(stderr, \"tofu\") {\n\t\t\t\t\t\t// Unexpected error\n\t\t\t\t\t\trequire.NoError(t, err, \"Unexpected error for filter query: %s\\nstdout: %s\\nstderr: %s\", tc.filterQuery, stdout, stderr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Verify the report file exists\n\t\t\t\treportFilePath := filepath.Join(tmpDir, helpers.ReportFile)\n\t\t\t\tassert.FileExists(t, reportFilePath, \"Report file should exist\")\n\n\t\t\t\t// Read and parse the report file\n\t\t\t\truns, err := report.ParseJSONRunsFromFile(reportFilePath)\n\t\t\t\trequire.NoError(t, err, \"Should be able to parse report JSON\")\n\n\t\t\t\t// Create a map of unit names to records for easier lookup\n\t\t\t\t// The report contains full paths, so we extract the unit name from the path\n\t\t\t\trecordsByUnit := make(map[string]*report.JSONRun)\n\n\t\t\t\tfor i := range runs {\n\t\t\t\t\trun := &runs[i]\n\t\t\t\t\tfullPath := run.Name\n\t\t\t\t\t// Extract unit name from path (e.g., \"unit-to-be-created\" from \"/tmp/.../unit-to-be-created\")\n\t\t\t\t\tbaseName := filepath.Base(fullPath)\n\t\t\t\t\trecordsByUnit[baseName] = run\n\t\t\t\t\t// Also store by full path for fallback\n\t\t\t\t\trecordsByUnit[fullPath] = run\n\t\t\t\t\t// Store by any part of the path that matches our unit pattern\n\t\t\t\t\tparts := strings.SplitSeq(fullPath, string(filepath.Separator))\n\t\t\t\t\tfor part := range parts {\n\t\t\t\t\t\tif strings.HasPrefix(part, \"unit-to-be-\") {\n\t\t\t\t\t\t\trecordsByUnit[part] = run\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Verify expected units are in the report and not excluded\n\t\t\t\tfor _, expectedUnit := range tc.expectedUnits {\n\t\t\t\t\trun, found := recordsByUnit[expectedUnit]\n\t\t\t\t\tif !found {\n\t\t\t\t\t\t// Try to find by partial match\n\t\t\t\t\t\tfor name, rec := range recordsByUnit {\n\t\t\t\t\t\t\tif strings.Contains(name, expectedUnit) {\n\t\t\t\t\t\t\t\trun = rec\n\t\t\t\t\t\t\t\tfound = true\n\n\t\t\t\t\t\t\t\tbreak\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\trequire.True(t, found, \"Expected unit '%s' should be in report. Found units: %v\", expectedUnit, getJSONRunNames(recordsByUnit))\n\t\t\t\t\tassert.NotEqual(t, \"excluded\", run.Result, \"Expected unit '%s' should not be excluded\", expectedUnit)\n\t\t\t\t}\n\n\t\t\t\t// Verify excluded units are NOT in the report\n\t\t\t\tfor _, excludedUnit := range tc.ignoredUnits {\n\t\t\t\t\tfound := false\n\n\t\t\t\t\tfor name := range recordsByUnit {\n\t\t\t\t\t\tif strings.Contains(name, excludedUnit) {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tassert.False(t, found, \"Excluded unit '%s' should NOT be in report\", excludedUnit)\n\t\t\t\t}\n\n\t\t\t\t// Verify expected excluded units are in the report but marked as excluded\n\t\t\t\tfor _, excludedUnit := range tc.expectedExcluded {\n\t\t\t\t\trun, found := recordsByUnit[excludedUnit]\n\t\t\t\t\tif !found {\n\t\t\t\t\t\t// Try to find by partial match\n\t\t\t\t\t\tfor name, rec := range recordsByUnit {\n\t\t\t\t\t\t\tif strings.Contains(name, excludedUnit) {\n\t\t\t\t\t\t\t\trun = rec\n\t\t\t\t\t\t\t\tfound = true\n\n\t\t\t\t\t\t\t\tbreak\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\trequire.True(t, found, \"Expected excluded unit '%s' should be in report\", excludedUnit)\n\t\t\t\t\tassert.Equal(t, \"excluded\", run.Result, \"Unit '%s' should be marked as excluded\", excludedUnit)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFilterFlagWithRunAllGitFilterRemovedUnitDestroyFlag(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpDir)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\terr = runner.GoOpenRepo()\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\terr = runner.GoCloseStorage()\n\t\tif err != nil {\n\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t}\n\t})\n\n\tunitToBeRemovedDir := filepath.Join(tmpDir, \"unit-to-be-removed\")\n\terr = os.MkdirAll(unitToBeRemovedDir, 0755)\n\trequire.NoError(t, err)\n\n\tterragruntHCL := `# Unit to be removed\nterraform {\n  source = \".\"\n}\n`\n\terr = os.WriteFile(filepath.Join(unitToBeRemovedDir, \"terragrunt.hcl\"), []byte(terragruntHCL), 0644)\n\trequire.NoError(t, err)\n\n\tmainTF := `resource \"null_resource\" \"test\" {\n  triggers = {\n    test = \"value\"\n  }\n}\n`\n\terr = os.WriteFile(filepath.Join(unitToBeRemovedDir, \"main.tf\"), []byte(mainTF), 0644)\n\trequire.NoError(t, err)\n\n\terr = runner.GoAdd(\".\")\n\trequire.NoError(t, err)\n\n\terr = runner.GoCommit(\"Initial commit with unit\", &gogit.CommitOptions{\n\t\tAuthor: &object.Signature{\n\t\t\tName:  \"Test User\",\n\t\t\tEmail: \"test@example.com\",\n\t\t\tWhen:  time.Now(),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Apply the unit so that it shows up in state first.\n\tcmd := \"terragrunt run --non-interactive --all --no-color --working-dir \" + tmpDir + \" -- apply\"\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\trequire.NoError(t, err)\n\n\tassert.Contains(\n\t\tt,\n\t\tstderr,\n\t\t\"Unit unit-to-be-removed\",\n\t\t\"unit-to-be-removed should be discovered and run\",\n\t)\n\n\tassert.Contains(\n\t\tt,\n\t\tstdout,\n\t\t\"Apply complete! Resources: 1 added\",\n\t\t\"unit-to-be-removed should be applied\",\n\t)\n\n\terr = os.RemoveAll(unitToBeRemovedDir)\n\trequire.NoError(t, err)\n\n\terr = runner.GoAdd(\".\")\n\trequire.NoError(t, err)\n\n\terr = runner.GoCommit(\"Remove unit\", &gogit.CommitOptions{\n\t\tAuthor: &object.Signature{\n\t\t\tName:  \"Test User\",\n\t\t\tEmail: \"test@example.com\",\n\t\t\tWhen:  time.Now(),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tcmd = \"terragrunt run --non-interactive --all --no-color --working-dir \" + tmpDir +\n\t\t\" --filter '[HEAD~1]' --filter-allow-destroy -- plan\"\n\n\tstdout, stderr, err = helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\tcombinedOutput := stdout + stderr\n\n\tassert.Contains(\n\t\tt,\n\t\tcombinedOutput,\n\t\t\"unit-to-be-removed\",\n\t\t\"Removed unit should be discovered and processed\",\n\t)\n\n\t// Check for destroy-related output. The message \"No changes. No objects need to be destroyed\"\n\t// is what Terraform outputs when plan -destroy is run but there's no state to destroy.\n\t// This is expected when using worktrees with local state (state is in original dir, not worktree).\n\t// The important thing is that the -destroy flag was passed, which we verify by checking for\n\t// this specific message that only appears with -destroy flag.\n\thasDestroyFlag := strings.Contains(combinedOutput, \"to destroy\") ||\n\t\tstrings.Contains(combinedOutput, \"No objects need to be destroyed\") ||\n\t\tstrings.Contains(combinedOutput, \"will be destroyed\")\n\n\tassert.True(t, hasDestroyFlag,\n\t\t\"Removed unit should be planned with -destroy flag. Output should contain 'to destroy', 'No objects need to be destroyed', or 'will be destroyed'. \"+\n\t\t\t\"Current output:\\nstdout: %s\\nstderr: %s\", stdout, stderr)\n}\n\nfunc TestFilterFlagWithRunAllGitFilterLocalStateWarning(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tunitConfig    string\n\t\tdescription   string\n\t\texpectWarning bool\n\t}{\n\t\t{\n\t\t\tname:          \"warning fires when unit has no remote_state\",\n\t\t\tunitConfig:    `# Unit with no remote_state`,\n\t\t\texpectWarning: true,\n\t\t\tdescription:   \"Warning should fire when unit discovered via Git ref has no remote_state configuration\",\n\t\t},\n\t\t{\n\t\t\tname: \"warning fires when unit has local backend\",\n\t\t\tunitConfig: `remote_state {\n  backend = \"local\"\n  config = {\n    path = \"terraform.tfstate\"\n  }\n}\n# Unit with local backend`,\n\t\t\texpectWarning: true,\n\t\t\tdescription:   \"Warning should fire when unit discovered via Git ref has local backend\",\n\t\t},\n\t\t{\n\t\t\tname: \"no warning when unit has remote state backend\",\n\t\t\tunitConfig: `remote_state {\n  backend = \"s3\"\n  config = {\n    bucket = \"test-bucket\"\n    key    = \"terraform.tfstate\"\n    region = \"us-east-1\"\n  }\n}\n# Unit with remote state`,\n\t\t\texpectWarning: false,\n\t\t\tdescription:   \"Warning should not fire when unit discovered via Git ref has remote state backend\",\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.Parallel()\n\n\t\t\ttmpDir := t.TempDir()\n\t\t\ttmpDir, err := filepath.EvalSymlinks(tmpDir)\n\t\t\trequire.NoError(t, err)\n\n\t\t\trunner, err := git.NewGitRunner()\n\t\t\trequire.NoError(t, err)\n\n\t\t\trunner = runner.WithWorkDir(tmpDir)\n\n\t\t\terr = runner.Init(t.Context())\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = runner.GoOpenRepo()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tt.Cleanup(func() {\n\t\t\t\terr = runner.GoCloseStorage()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\t// Create a unit with the specified configuration\n\t\t\tunitDir := filepath.Join(tmpDir, \"test-unit\")\n\t\t\tunitHCLPath := createTestUnit(t, unitDir, tc.unitConfig)\n\n\t\t\t// Initial commit\n\t\t\terr = runner.GoAdd(\".\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = runner.GoCommit(\"Initial commit\", &gogit.CommitOptions{\n\t\t\t\tAuthor: &object.Signature{\n\t\t\t\t\tName:  \"Test User\",\n\t\t\t\t\tEmail: \"test@example.com\",\n\t\t\t\t\tWhen:  time.Now(),\n\t\t\t\t},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Modify the unit to trigger Git filter detection\n\t\t\terr = os.WriteFile(unitHCLPath, []byte(tc.unitConfig+\"\\n# Modified\"), 0644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Commit the modification\n\t\t\terr = runner.GoAdd(\".\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = runner.GoCommit(\"Modify unit\", &gogit.CommitOptions{\n\t\t\t\tAuthor: &object.Signature{\n\t\t\t\t\tName:  \"Test User\",\n\t\t\t\t\tEmail: \"test@example.com\",\n\t\t\t\t\tWhen:  time.Now(),\n\t\t\t\t},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Run terragrunt run --all --filter with git filter\n\t\t\tcmd := \"terragrunt run --all --no-color --working-dir \" + tmpDir + \" --filter '[HEAD~1...HEAD]' --report-file \" + helpers.ReportFile + \" -- plan\"\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\t// Check for the warning in stderr\n\t\t\t// The warning message should contain this unique substring\n\t\t\twarningMessage := \"do not have a remote_state configuration\"\n\t\t\thasWarning := strings.Contains(stderr, warningMessage) && strings.Contains(stderr, \"Git-based filter expressions\")\n\n\t\t\tif tc.expectWarning {\n\t\t\t\tassert.True(t, hasWarning, \"Expected warning message in stderr. stderr: %s\\nstdout: %s\", stderr, stdout)\n\t\t\t} else {\n\t\t\t\tassert.False(t, hasWarning, \"Did not expect warning message in stderr. stderr: %s\\nstdout: %s\", stderr, stdout)\n\t\t\t}\n\n\t\t\t// The command may fail due to the backend not being bootstrapped, but that's okay.\n\t\t\t// We're just checking for the warning\n\t\t\t_ = err\n\t\t})\n\t}\n}\n\nfunc TestFilterFlagWithExplicitStacksGitFilter(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname               string\n\t\tfilterQuery        string\n\t\tdescription        string\n\t\texpectedUnits      []string\n\t\tignoredUnits       []string\n\t\texpectedExcluded   []string\n\t\tfilterAllowDestroy bool\n\t\texpectError        bool\n\t}{\n\t\t{\n\t\t\tname:               \"git filter discovers units from modified, created, and removed stacks and excludes untouched\",\n\t\t\tfilterQuery:        \"[HEAD~1...HEAD]\",\n\t\t\tfilterAllowDestroy: false,\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"unit-to-be-added\",\n\t\t\t\t\"unit-to-be-modified\",\n\t\t\t\t\"unit-to-be-created-1\",\n\t\t\t\t\"unit-to-be-created-2\",\n\t\t\t},\n\t\t\tignoredUnits: []string{\n\t\t\t\t\"unit-to-be-untouched\",\n\t\t\t},\n\t\t\texpectedExcluded: []string{\n\t\t\t\t\"unit-to-be-removed-from-stack\",\n\t\t\t},\n\t\t\texpectError: false,\n\t\t\tdescription: \"Git filter should discover units from stacks that were created, modified, or removed between commits, and exclude untouched stacks. Units from removed stack should be excluded without --filter-allow-destroy\",\n\t\t},\n\t\t{\n\t\t\tname:               \"git filter with --filter-allow-destroy includes units from removed stack\",\n\t\t\tfilterQuery:        \"[HEAD~1...HEAD]\",\n\t\t\tfilterAllowDestroy: true,\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"unit-to-be-added\",\n\t\t\t\t\"unit-to-be-modified\",\n\t\t\t\t\"unit-to-be-created-1\",\n\t\t\t\t\"unit-to-be-created-2\",\n\t\t\t\t\"unit-to-be-removed-from-stack\",\n\t\t\t},\n\t\t\tignoredUnits: []string{\n\t\t\t\t\"unit-to-be-untouched\",\n\t\t\t},\n\t\t\texpectedExcluded: []string{},\n\t\t\texpectError:      false,\n\t\t\tdescription:      \"Git filter with --filter-allow-destroy should include units from removed stack for destroy operations\",\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.Parallel()\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\trunner, err := git.NewGitRunner()\n\t\t\trequire.NoError(t, err)\n\n\t\t\trunner = runner.WithWorkDir(tmpDir)\n\n\t\t\terr = runner.Init(t.Context())\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = runner.GoOpenRepo()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tt.Cleanup(func() {\n\t\t\t\terr = runner.GoCloseStorage()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\t// Create a catalog of units that will be referenced by stacks\n\t\t\tlegacyUnitDir := filepath.Join(tmpDir, \"catalog\", \"units\", \"legacy\")\n\t\t\terr = os.MkdirAll(legacyUnitDir, 0755)\n\t\t\trequire.NoError(t, err)\n\t\t\t_ = createTestUnit(t, legacyUnitDir, `# Legacy unit`)\n\n\t\t\tmodernUnitDir := filepath.Join(tmpDir, \"catalog\", \"units\", \"modern\")\n\t\t\terr = os.MkdirAll(modernUnitDir, 0755)\n\t\t\trequire.NoError(t, err)\n\t\t\t_ = createTestUnit(t, modernUnitDir, `# Modern unit`)\n\n\t\t\t// Create initial stacks\n\t\t\tstackToBeModifiedDir := filepath.Join(tmpDir, \"live\", \"stack-to-be-modified\")\n\t\t\terr = os.MkdirAll(stackToBeModifiedDir, 0755)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tstackToBeRemovedDir := filepath.Join(tmpDir, \"live\", \"stack-to-be-removed\")\n\t\t\terr = os.MkdirAll(stackToBeRemovedDir, 0755)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tstackToBeUntouchedDir := filepath.Join(tmpDir, \"live\", \"stack-to-be-untouched\")\n\t\t\terr = os.MkdirAll(stackToBeUntouchedDir, 0755)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Initial stack file contents\n\t\t\tinitialStackContent := `unit \"unit-to-be-modified\" {\n\tsource = \"${get_repo_root()}/catalog/units/legacy\"\n\tpath   = \"unit-to-be-modified\"\n}\n\nunit \"unit-to-be-removed-from-stack\" {\n\tsource = \"${get_repo_root()}/catalog/units/legacy\"\n\tpath   = \"unit-to-be-removed-from-stack\"\n}\n`\n\n\t\t\tuntouchedStackContent := `unit \"unit-to-be-untouched\" {\n\tsource = \"${get_repo_root()}/catalog/units/legacy\"\n\tpath   = \"unit-to-be-untouched\"\n}\n`\n\n\t\t\t// Write initial stack files\n\t\t\terr = os.WriteFile(filepath.Join(stackToBeModifiedDir, \"terragrunt.stack.hcl\"), []byte(initialStackContent), 0644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = os.WriteFile(filepath.Join(stackToBeRemovedDir, \"terragrunt.stack.hcl\"), []byte(initialStackContent), 0644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = os.WriteFile(filepath.Join(stackToBeUntouchedDir, \"terragrunt.stack.hcl\"), []byte(untouchedStackContent), 0644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Initial commit\n\t\t\terr = runner.GoAdd(\".\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = runner.GoCommit(\"Initial commit with stacks\", &gogit.CommitOptions{\n\t\t\t\tAuthor: &object.Signature{\n\t\t\t\t\tName:  \"Test User\",\n\t\t\t\t\tEmail: \"test@example.com\",\n\t\t\t\t\tWhen:  time.Now(),\n\t\t\t\t},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Modify the stack-to-be-modified: add a unit, modify a unit, remove a unit\n\t\t\tmodifiedStackContent := `unit \"unit-to-be-added\" {\n\tsource = \"${get_repo_root()}/catalog/units/modern\"\n\tpath   = \"unit-to-be-added\"\n}\n\nunit \"unit-to-be-modified\" {\n\tsource = \"${get_repo_root()}/catalog/units/modern\"\n\tpath   = \"unit-to-be-modified\"\n}\n`\n\t\t\terr = os.WriteFile(filepath.Join(stackToBeModifiedDir, \"terragrunt.stack.hcl\"), []byte(modifiedStackContent), 0644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Remove the stack-to-be-removed\n\t\t\terr = os.RemoveAll(stackToBeRemovedDir)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Add a new stack\n\t\t\tstackToBeCreatedDir := filepath.Join(tmpDir, \"live\", \"stack-to-be-created\")\n\t\t\terr = os.MkdirAll(stackToBeCreatedDir, 0755)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tnewStackContent := `unit \"unit-to-be-created-1\" {\n\tsource = \"${get_repo_root()}/catalog/units/modern\"\n\tpath   = \"unit-to-be-created-1\"\n}\n\nunit \"unit-to-be-created-2\" {\n\tsource = \"${get_repo_root()}/catalog/units/modern\"\n\tpath   = \"unit-to-be-created-2\"\n}\n`\n\t\t\terr = os.WriteFile(filepath.Join(stackToBeCreatedDir, \"terragrunt.stack.hcl\"), []byte(newStackContent), 0644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Leave stack-to-be-untouched unchanged\n\n\t\t\t// Commit the changes\n\t\t\terr = runner.GoAdd(\".\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = runner.GoCommit(\"Modify, create, and remove stacks\", &gogit.CommitOptions{\n\t\t\t\tAuthor: &object.Signature{\n\t\t\t\t\tName:  \"Test User\",\n\t\t\t\t\tEmail: \"test@example.com\",\n\t\t\t\t\tWhen:  time.Now(),\n\t\t\t\t},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Run terragrunt run --all --filter with git filter\n\t\t\tcmd := \"terragrunt run --all --no-color --experiment-mode --working-dir \" + tmpDir + \" --filter '\" + tc.filterQuery + \"' --report-file \" + helpers.ReportFile\n\n\t\t\tif tc.filterAllowDestroy {\n\t\t\t\tcmd += \" --filter-allow-destroy\"\n\t\t\t}\n\n\t\t\tcmd += \" -- plan\"\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for filter query: %s\", tc.filterQuery)\n\t\t\t\tassert.NotEmpty(t, stderr, \"Expected error message in stderr\")\n\t\t\t} else {\n\t\t\t\t// For run commands, we expect some output even if terraform isn't fully initialized\n\t\t\t\t// The key is that the command should execute and process the filtered units\n\t\t\t\tif err != nil {\n\t\t\t\t\t// If there's an error, it might be because terraform isn't initialized\n\t\t\t\t\t// but we should still see that the filter worked (units were discovered)\n\t\t\t\t\t// Let's check if the error is about terraform init or similar\n\t\t\t\t\tif !strings.Contains(stderr, \"terraform\") && !strings.Contains(stderr, \"tofu\") {\n\t\t\t\t\t\t// Unexpected error\n\t\t\t\t\t\trequire.NoError(t, err, \"Unexpected error for filter query: %s\\nstdout: %s\\nstderr: %s\", tc.filterQuery, stdout, stderr)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Verify the report file exists\n\t\t\t\treportFilePath := filepath.Join(tmpDir, helpers.ReportFile)\n\t\t\t\tassert.FileExists(t, reportFilePath, \"Report file should exist\")\n\n\t\t\t\t// Read and parse the report file\n\t\t\t\truns, err := report.ParseJSONRunsFromFile(reportFilePath)\n\t\t\t\trequire.NoError(t, err, \"Should be able to parse report JSON\")\n\n\t\t\t\t// Create a map of unit names to records for easier lookup\n\t\t\t\t// The report contains full paths, so we extract the unit name from the path\n\t\t\t\trecordsByUnit := make(map[string]*report.JSONRun)\n\n\t\t\t\tfor i := range runs {\n\t\t\t\t\trun := &runs[i]\n\t\t\t\t\tfullPath := run.Name\n\t\t\t\t\t// Extract unit name from path\n\t\t\t\t\t// Paths might be like: /tmp/.../live/stack-to-be-modified/.terragrunt-stack/unit-to-be-added\n\t\t\t\t\tbaseName := filepath.Base(fullPath)\n\t\t\t\t\trecordsByUnit[baseName] = run\n\t\t\t\t\t// Also store by full path for fallback\n\t\t\t\t\trecordsByUnit[fullPath] = run\n\t\t\t\t\t// Store by any part of the path that matches our unit pattern\n\t\t\t\t\tparts := strings.SplitSeq(fullPath, string(filepath.Separator))\n\t\t\t\t\tfor part := range parts {\n\t\t\t\t\t\tif strings.HasPrefix(part, \"unit-to-be-\") {\n\t\t\t\t\t\t\trecordsByUnit[part] = run\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Verify expected units are in the report and not excluded\n\t\t\t\tfor _, expectedUnit := range tc.expectedUnits {\n\t\t\t\t\trun, found := recordsByUnit[expectedUnit]\n\t\t\t\t\tif !found {\n\t\t\t\t\t\t// Try to find by partial match\n\t\t\t\t\t\tfor name, rec := range recordsByUnit {\n\t\t\t\t\t\t\tif strings.Contains(name, expectedUnit) {\n\t\t\t\t\t\t\t\trun = rec\n\t\t\t\t\t\t\t\tfound = true\n\n\t\t\t\t\t\t\t\tbreak\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\trequire.True(t, found, \"Expected unit '%s' should be in report. Found units: %v\", expectedUnit, getJSONRunNames(recordsByUnit))\n\t\t\t\t\tassert.NotEqual(t, \"excluded\", run.Result, \"Expected unit '%s' should not be excluded\", expectedUnit)\n\t\t\t\t}\n\n\t\t\t\t// Verify excluded units are NOT in the report\n\t\t\t\tfor _, excludedUnit := range tc.ignoredUnits {\n\t\t\t\t\tfound := false\n\n\t\t\t\t\tfor name := range recordsByUnit {\n\t\t\t\t\t\tif strings.Contains(name, excludedUnit) {\n\t\t\t\t\t\t\tfound = true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tassert.False(t, found, \"Excluded unit '%s' should NOT be in report\", excludedUnit)\n\t\t\t\t}\n\n\t\t\t\t// Verify expected excluded units are in the report but marked as excluded\n\t\t\t\tfor _, excludedUnit := range tc.expectedExcluded {\n\t\t\t\t\trun, found := recordsByUnit[excludedUnit]\n\t\t\t\t\tif !found {\n\t\t\t\t\t\t// Try to find by partial match\n\t\t\t\t\t\tfor name, rec := range recordsByUnit {\n\t\t\t\t\t\t\tif strings.Contains(name, excludedUnit) {\n\t\t\t\t\t\t\t\trun = rec\n\t\t\t\t\t\t\t\tfound = true\n\n\t\t\t\t\t\t\t\tbreak\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\trequire.True(t, found, \"Expected excluded unit '%s' should be in report\", excludedUnit)\n\t\t\t\t\tassert.Equal(t, \"excluded\", run.Result, \"Unit '%s' should be marked as excluded\", excludedUnit)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFiltersFileFlag(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tsetupFile     func(t *testing.T, dir string) string // Returns path to filter file, empty if no file\n\t\tcmdFlags      string                                // Additional flags like --filters-file or --no-filters-file\n\t\texpectedUnits []string\n\t\texpectError   bool\n\t}{\n\t\t{\n\t\t\tname: \"custom filters file with --filters-file flag\",\n\t\t\tsetupFile: func(t *testing.T, dir string) string {\n\t\t\t\tt.Helper()\n\n\t\t\t\tfilterFile := filepath.Join(dir, \"custom-filters.txt\")\n\t\t\t\terr := os.WriteFile(filterFile, []byte(\"type=unit\\n\"), 0644)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\treturn filterFile\n\t\t\t},\n\t\t\tcmdFlags:      \"\", // Will be set in test\n\t\t\texpectedUnits: []string{\"unit\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname: \"default .terragrunt-filters file is automatically read when experiment enabled\",\n\t\t\tsetupFile: func(t *testing.T, dir string) string {\n\t\t\t\tt.Helper()\n\n\t\t\t\tfilterFile := filepath.Join(dir, \".terragrunt-filters\")\n\t\t\t\terr := os.WriteFile(filterFile, []byte(\"type=unit\\n\"), 0644)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\treturn filterFile\n\t\t\t},\n\t\t\tcmdFlags:      \"\",               // No flag, should auto-detect and read .terragrunt-filters\n\t\t\texpectedUnits: []string{\"unit\"}, // Should filter to only unit, proving file was read\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname: \"--no-filters-file disables auto-reading\",\n\t\t\tsetupFile: func(t *testing.T, dir string) string {\n\t\t\t\tt.Helper()\n\n\t\t\t\tfilterFile := filepath.Join(dir, \".terragrunt-filters\")\n\t\t\t\terr := os.WriteFile(filterFile, []byte(\"type=unit\\n\"), 0644)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\treturn filterFile\n\t\t\t},\n\t\t\tcmdFlags:      \"--no-filters-file\",\n\t\t\texpectedUnits: []string{\"stack\", \"unit\"}, // Should show all units, not filtered\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname: \"filter file with comments and empty lines\",\n\t\t\tsetupFile: func(t *testing.T, dir string) string {\n\t\t\t\tt.Helper()\n\n\t\t\t\tfilterFile := filepath.Join(dir, \".terragrunt-filters\")\n\t\t\t\tcontent := \"# This is a comment\\n\\ntype=unit\\n  \\n# Another comment\\n\"\n\t\t\t\terr := os.WriteFile(filterFile, []byte(content), 0644)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\treturn filterFile\n\t\t\t},\n\t\t\tcmdFlags:      \"\",\n\t\t\texpectedUnits: []string{\"unit\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname: \"multiple filters in file\",\n\t\t\tsetupFile: func(t *testing.T, dir string) string {\n\t\t\t\tt.Helper()\n\n\t\t\t\tfilterFile := filepath.Join(dir, \".terragrunt-filters\")\n\t\t\t\tcontent := \"unit\\nstack\\n\"\n\t\t\t\terr := os.WriteFile(filterFile, []byte(content), 0644)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\treturn filterFile\n\t\t\t},\n\t\t\tcmdFlags:      \"\",\n\t\t\texpectedUnits: []string{\"stack\", \"unit\"}, // Union of both filters\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname: \"filters file combined with --filter flags\",\n\t\t\tsetupFile: func(t *testing.T, dir string) string {\n\t\t\t\tt.Helper()\n\n\t\t\t\tfilterFile := filepath.Join(dir, \".terragrunt-filters\")\n\t\t\t\terr := os.WriteFile(filterFile, []byte(\"type=unit\\n\"), 0644)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\treturn filterFile\n\t\t\t},\n\t\t\tcmdFlags:      \"--filter type=stack\",\n\t\t\texpectedUnits: []string{\"stack\", \"unit\"}, // Union: file has unit, flag has stack\n\t\t\texpectError:   false,\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.Parallel()\n\n\t\t\t// Copy fixture to temporary directory\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureFilterBasic)\n\t\t\ttmpDir := filepath.Join(tmpEnvPath, testFixtureFilterBasic)\n\n\t\t\thelpers.CleanupTerraformFolder(t, tmpDir)\n\n\t\t\t// Setup filter file if needed\n\t\t\tvar filterFilePath string\n\t\t\tif tc.setupFile != nil {\n\t\t\t\tfilterFilePath = tc.setupFile(t, tmpDir)\n\t\t\t}\n\n\t\t\t// Build command\n\t\t\tcmd := \"terragrunt find --no-color --working-dir \" + tmpDir\n\t\t\tif tc.cmdFlags != \"\" {\n\t\t\t\tcmd += \" \" + tc.cmdFlags\n\t\t\t}\n\t\t\t// For custom filter files (not .terragrunt-filters), add --filters-file flag\n\t\t\tif filterFilePath != \"\" && filepath.Base(filterFilePath) != \".terragrunt-filters\" && !strings.Contains(tc.cmdFlags, \"--filters-file\") && !strings.Contains(tc.cmdFlags, \"--no-filters-file\") {\n\t\t\t\tcmd += \" --filters-file \" + filterFilePath\n\t\t\t}\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err, \"Expected error for test case: %s\", tc.name)\n\t\t\t\tassert.NotEmpty(t, stderr, \"Expected error message in stderr\")\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err, \"Unexpected error for test case: %s\\nstdout: %s\\nstderr: %s\", tc.name, stdout, stderr)\n\t\t\t// Parse output into unit names (split by newlines and filter empty strings)\n\t\t\tresults := strings.Split(strings.TrimSpace(stdout), \"\\n\")\n\t\t\t// Filter out empty strings and extract basename from each path\n\t\t\tvar actualUnits []string\n\n\t\t\tfor _, r := range results {\n\t\t\t\tif r != \"\" {\n\t\t\t\t\t// Extract basename from path (handles both relative and absolute paths)\n\t\t\t\t\tunitName := filepath.Base(strings.TrimSpace(r))\n\t\t\t\t\tactualUnits = append(actualUnits, unitName)\n\t\t\t\t}\n\t\t\t}\n\t\t\t// For .terragrunt-filters auto-detection test: the file contains \"type=unit\"\n\t\t\t// and we expect only \"unit\" in output, proving the file WAS automatically read\n\t\t\tassert.ElementsMatch(t, tc.expectedUnits, actualUnits, \"Output mismatch for test case: %s\", tc.name)\n\t\t})\n\t}\n}\n\nfunc TestFilterFlagMinimizesParsing(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"single unit filter\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\thelpers.CleanupTerraformFolder(t, testFixtureMinimizeParsing)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureMinimizeParsing)\n\t\trootPath := filepath.Join(tmpEnvPath, testFixtureMinimizeParsing)\n\n\t\t// Run with filter targeting only target-unit\n\t\t// This will parse target-unit and its dependency (dependency-unit) for outputs,\n\t\t// but only target-unit will be run and appear in the report\n\t\t// The excluded units with land-mine configs should NOT be parsed\n\t\tcmd := \"terragrunt run --all plan --no-color --experiment-mode --working-dir \" + rootPath + \" --filter './target-unit' --report-file \" + helpers.ReportFile\n\t\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t// Command should succeed\n\t\trequire.NoError(t, err)\n\n\t\t// Verify no errors from land-mine units in stderr\n\t\tassert.NotContains(t, stderr, \"excluded-unit-1\", \"excluded-unit-1 should not be parsed\")\n\t\tassert.NotContains(t, stderr, \"excluded-unit-2\", \"excluded-unit-2 should not be parsed\")\n\t\tassert.NotContains(t, stderr, \"excluded-unit-3\", \"excluded-unit-3 should not be parsed\")\n\n\t\t// Verify that dependency-unit is still being parsed\n\t\tassert.Contains(t, stderr, \"dependency-unit\", \"dependency-unit should be parsed\")\n\n\t\t// Verify the report file exists and parse it\n\t\treportFilePath := filepath.Join(rootPath, helpers.ReportFile)\n\t\tif util.FileExists(reportFilePath) {\n\t\t\truns, err := report.ParseJSONRunsFromFile(reportFilePath)\n\t\t\trequire.NoError(t, err, \"Should be able to parse report JSON\")\n\n\t\t\tnames := runs.Names()\n\n\t\t\t// Verify expected units are in the report\n\t\t\tfound := false\n\n\t\t\tfor _, name := range names {\n\t\t\t\tif strings.Contains(name, \"target-unit\") {\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\trequire.True(t, found, \"target-unit should be in report. Found units: %v\", names)\n\n\t\t\t// Verify land-mine units are NOT in the report\n\t\t\tfor _, excludedUnit := range []string{\"excluded-unit-1\", \"excluded-unit-2\", \"excluded-unit-3\"} {\n\t\t\t\tfound := false\n\n\t\t\t\tfor _, name := range names {\n\t\t\t\t\tif strings.Contains(name, excludedUnit) {\n\t\t\t\t\t\tfound = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tassert.False(t, found, \"Excluded unit '%s' should NOT be in report\", excludedUnit)\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"multiple units filter\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\thelpers.CleanupTerraformFolder(t, testFixtureMinimizeParsing)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureMinimizeParsing)\n\t\trootPath := filepath.Join(tmpEnvPath, testFixtureMinimizeParsing)\n\n\t\t// Run with filter targeting both target-unit and dependency-unit (OR semantics)\n\t\tcmd := \"terragrunt run --all plan --no-color --experiment-mode --working-dir \" + rootPath + \" --filter './target-unit' --filter './dependency-unit' --report-file \" + helpers.ReportFile\n\t\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t// Command should succeed - if land-mines were parsed, we'd get errors\n\t\trequire.NoError(t, err)\n\n\t\t// Verify no errors from land-mine units in stderr\n\t\tassert.NotContains(t, stderr, \"excluded-unit-1\", \"excluded-unit-1 should not be parsed\")\n\t\tassert.NotContains(t, stderr, \"excluded-unit-2\", \"excluded-unit-2 should not be parsed\")\n\t\tassert.NotContains(t, stderr, \"excluded-unit-3\", \"excluded-unit-3 should not be parsed\")\n\n\t\t// Verify the report file exists and parse it\n\t\treportFilePath := filepath.Join(rootPath, helpers.ReportFile)\n\t\tif util.FileExists(reportFilePath) {\n\t\t\truns, err := report.ParseJSONRunsFromFile(reportFilePath)\n\t\t\trequire.NoError(t, err, \"Should be able to parse report JSON\")\n\n\t\t\tnames := runs.Names()\n\n\t\t\t// Verify expected units are in the report\n\t\t\tfound := false\n\n\t\t\tfor _, name := range names {\n\t\t\t\tif strings.Contains(name, \"target-unit\") {\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\trequire.True(t, found, \"target-unit should be in report. Found units: %v\", names)\n\n\t\t\tfound = false\n\n\t\t\tfor _, name := range names {\n\t\t\t\tif strings.Contains(name, \"dependency-unit\") {\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\trequire.True(t, found, \"dependency-unit should be in report. Found units: %v\", names)\n\n\t\t\t// Verify land-mine units are NOT in the report\n\t\t\tfor _, excludedUnit := range []string{\"excluded-unit-1\", \"excluded-unit-2\", \"excluded-unit-3\"} {\n\t\t\t\tfound := false\n\n\t\t\t\tfor _, name := range names {\n\t\t\t\t\tif strings.Contains(name, excludedUnit) {\n\t\t\t\t\t\tfound = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tassert.False(t, found, \"Excluded unit '%s' should NOT be in report\", excludedUnit)\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"destroy without graph filter\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\thelpers.CleanupTerraformFolder(t, testFixtureMinimizeParsingDestroy)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureMinimizeParsingDestroy)\n\t\trootPath := filepath.Join(tmpEnvPath, testFixtureMinimizeParsingDestroy)\n\n\t\t// Run destroy with filter targeting only unit-a\n\t\t// This should only parse unit-a, NOT all units in the repository\n\t\t// The land-mine units should NOT be parsed\n\t\tcmd := \"terragrunt run --all destroy --non-interactive --no-color --experiment-mode --working-dir \" + rootPath + \" --filter './unit-a' --report-file \" + helpers.ReportFile\n\t\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t// Command should succeed - if land-mines were parsed, we'd get errors\n\t\trequire.NoError(t, err)\n\n\t\t// Verify no errors from land-mine units in stderr\n\t\tassert.NotContains(t, stderr, \"landmine-unit-1\", \"landmine-unit-1 should not be parsed during destroy\")\n\t\tassert.NotContains(t, stderr, \"landmine-unit-2\", \"landmine-unit-2 should not be parsed during destroy\")\n\n\t\t// Verify the report file exists and parse it\n\t\treportFilePath := filepath.Join(rootPath, helpers.ReportFile)\n\t\tif util.FileExists(reportFilePath) {\n\t\t\truns, err := report.ParseJSONRunsFromFile(reportFilePath)\n\t\t\trequire.NoError(t, err, \"Should be able to parse report JSON\")\n\n\t\t\tnames := runs.Names()\n\n\t\t\t// Verify expected unit is in the report\n\t\t\tfound := false\n\n\t\t\tfor _, name := range names {\n\t\t\t\tif strings.Contains(name, \"unit-a\") {\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\trequire.True(t, found, \"unit-a should be in report. Found units: %v\", names)\n\n\t\t\t// Verify land-mine units are NOT in the report\n\t\t\tfor _, excludedUnit := range []string{\"landmine-unit-1\", \"landmine-unit-2\"} {\n\t\t\t\tfound := false\n\n\t\t\t\tfor _, name := range names {\n\t\t\t\t\tif strings.Contains(name, excludedUnit) {\n\t\t\t\t\t\tfound = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tassert.False(t, found, \"Excluded unit '%s' should NOT be in report\", excludedUnit)\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"destroy with graph filter\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\thelpers.CleanupTerraformFolder(t, testFixtureMinimizeParsingDestroy)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureMinimizeParsingDestroy)\n\t\trootPath := filepath.Join(tmpEnvPath, testFixtureMinimizeParsingDestroy)\n\n\t\t// Run destroy with graph filter targeting unit-a\n\t\t// Graph filters explicitly request dependency discovery, so this is expected behavior\n\t\t// The land-mine units should still NOT be parsed (they're not dependencies)\n\t\tcmd := \"terragrunt run --all destroy --non-interactive --no-color --experiment-mode --working-dir \" + rootPath + \" --filter '{./unit-a}...' --report-file \" + helpers.ReportFile\n\t\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t// Command should succeed - if land-mines were parsed, we'd get errors\n\t\t// Note: destroy might fail for other reasons (e.g., no state), but it shouldn't fail due to parsing land-mines\n\t\trequire.NoError(t, err)\n\n\t\t// Verify no errors from land-mine units in stderr\n\t\tassert.NotContains(t, stderr, \"landmine-unit-1\", \"landmine-unit-1 should not be parsed during destroy with graph filter\")\n\t\tassert.NotContains(t, stderr, \"landmine-unit-2\", \"landmine-unit-2 should not be parsed during destroy with graph filter\")\n\n\t\t// Verify the report file exists and parse it\n\t\treportFilePath := filepath.Join(rootPath, helpers.ReportFile)\n\t\tif util.FileExists(reportFilePath) {\n\t\t\truns, err := report.ParseJSONRunsFromFile(reportFilePath)\n\t\t\trequire.NoError(t, err, \"Should be able to parse report JSON\")\n\n\t\t\tnames := runs.Names()\n\n\t\t\t// Verify expected unit is in the report\n\t\t\tfound := false\n\n\t\t\tfor _, name := range names {\n\t\t\t\tif strings.Contains(name, \"unit-a\") {\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\trequire.True(t, found, \"unit-a should be in report. Found units: %v\", names)\n\n\t\t\t// Verify land-mine units are NOT in the report\n\t\t\tfor _, excludedUnit := range []string{\"landmine-unit-1\", \"landmine-unit-2\"} {\n\t\t\t\tfound := false\n\n\t\t\t\tfor _, name := range names {\n\t\t\t\t\tif strings.Contains(name, excludedUnit) {\n\t\t\t\t\t\tfound = true\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tassert.False(t, found, \"Excluded unit '%s' should NOT be in report\", excludedUnit)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc TestFilterFlagAutoEnablesAll(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tcmd           string\n\t\texpectedUnits []string\n\t}{\n\t\t{\n\t\t\tname:          \"filter flag without --all processes multiple units\",\n\t\t\tcmd:           \"terragrunt run --no-color --filter './**' --report-file \" + helpers.ReportFile + \" plan\",\n\t\t\texpectedUnits: []string{\"a-dependent\", \"b-dependency\", \"c-mixed-deps\", \"d-dependencies-only\"},\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureFilterDAG)\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureFilterDAG)\n\t\t\trootPath := filepath.Join(tmpEnvPath, testFixtureFilterDAG)\n\n\t\t\tcmd := tc.cmd + \" --working-dir \" + rootPath\n\t\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Verify the report file exists\n\t\t\treportFilePath := filepath.Join(rootPath, helpers.ReportFile)\n\t\t\tassert.FileExists(t, reportFilePath)\n\n\t\t\tr, err := report.ParseJSONRunsFromFile(reportFilePath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\truns := r.Names()\n\n\t\t\t// Verify expected units are in the report\n\t\t\tassert.ElementsMatch(t, tc.expectedUnits, runs)\n\t\t})\n\t}\n}\n\n// getJSONRunNames extracts unit names from records map for error messages\nfunc getJSONRunNames(recordsByUnit map[string]*report.JSONRun) []string {\n\tnames := make([]string, 0, len(recordsByUnit))\n\tfor name := range recordsByUnit {\n\t\tnames = append(names, name)\n\t}\n\n\tsort.Strings(names)\n\n\treturn names\n}\n\n// TestOutDirWithGitFilter verifies that --out-dir works correctly with git-based filters.\n// This is a regression test for https://github.com/gruntwork-io/terragrunt/issues/5287\n// The bug was that plan files were written to the temporary git worktree directory\n// instead of the specified --out-dir path.\nfunc TestOutDirWithGitFilter(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\toutDir := helpers.TmpDirWOSymlinks(t)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpDir)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\terr = runner.GoOpenRepo()\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\terr = runner.GoCloseStorage()\n\t\tif err != nil {\n\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t}\n\t})\n\n\t// Create initial unit\n\tunitDir := filepath.Join(tmpDir, \"unit-initial\")\n\terr = os.MkdirAll(unitDir, 0755)\n\trequire.NoError(t, err)\n\n\t// Create terragrunt.hcl\n\terr = os.WriteFile(filepath.Join(unitDir, \"terragrunt.hcl\"), []byte(`# Initial unit`), 0644)\n\trequire.NoError(t, err)\n\n\t// Create main.tf with a simple null resource\n\terr = os.WriteFile(filepath.Join(unitDir, \"main.tf\"), []byte(`\nresource \"null_resource\" \"test\" {}\n`), 0644)\n\trequire.NoError(t, err)\n\n\t// Initial commit\n\terr = runner.GoAdd(\".\")\n\trequire.NoError(t, err)\n\n\terr = runner.GoCommit(\"Initial commit\", &gogit.CommitOptions{\n\t\tAuthor: &object.Signature{\n\t\t\tName:  \"Test User\",\n\t\t\tEmail: \"test@example.com\",\n\t\t\tWhen:  time.Now(),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Create a new unit (this will be detected by the git filter)\n\tnewUnitDir := filepath.Join(tmpDir, \"unit-new\")\n\terr = os.MkdirAll(newUnitDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(newUnitDir, \"terragrunt.hcl\"), []byte(`# New unit`), 0644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(newUnitDir, \"main.tf\"), []byte(`\nresource \"null_resource\" \"test\" {}\n`), 0644)\n\trequire.NoError(t, err)\n\n\t// Commit the new unit\n\terr = runner.GoAdd(\".\")\n\trequire.NoError(t, err)\n\n\terr = runner.GoCommit(\"Add new unit\", &gogit.CommitOptions{\n\t\tAuthor: &object.Signature{\n\t\t\tName:  \"Test User\",\n\t\t\tEmail: \"test@example.com\",\n\t\t\tWhen:  time.Now(),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// Run terragrunt with --out-dir and git filter\n\t// The bug was that plan files went to /tmp/terragrunt-worktree-... instead of outDir\n\tcmd := \"terragrunt run --all --no-color --experiment-mode --non-interactive --working-dir \" + tmpDir +\n\t\t\" --out-dir \" + outDir + \" --filter '[HEAD~1...HEAD]' -- plan\"\n\n\thelpers.RunTerragrunt(t, cmd)\n\n\t// Verify plan files are in outDir, NOT in a worktree path\n\t// The key assertion: plan files should be in outDir/unit-new/\n\t// NOT in /tmp/terragrunt-worktree-*/unit-new/\n\tfiles, err := filepath.Glob(filepath.Join(outDir, \"**\", \"*.tfplan\"))\n\tif err != nil {\n\t\t// Glob with ** doesn't work on all systems, try a walk\n\t\tfiles = []string{}\n\t\t_ = filepath.Walk(outDir, func(path string, info os.FileInfo, err error) error {\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif strings.HasSuffix(path, \".tfplan\") {\n\t\t\t\tfiles = append(files, path)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t// Should have at least 1 plan file in outDir\n\tassert.NotEmpty(t, files, \"Expected plan files in outDir %s\", outDir)\n\n\t// None of the files should be in a worktree path\n\tfor _, file := range files {\n\t\tassert.NotContains(t, file, \"terragrunt-worktree\",\n\t\t\t\"Plan file %s should not be in a worktree directory\", file)\n\t\tassert.True(t, strings.HasPrefix(file, outDir),\n\t\t\t\"Plan file %s should be in outDir %s\", file, outDir)\n\t}\n}\n\nfunc TestDestroyWithOutDirGitFilter(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\toutDir := helpers.TmpDirWOSymlinks(t)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpDir)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\terr = runner.GoOpenRepo()\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\terr = runner.GoCloseStorage()\n\t\tif err != nil {\n\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t}\n\t})\n\n\t// create unit to destroy\n\tunitDir := filepath.Join(tmpDir, \"unit-to-destroy\")\n\terr = os.MkdirAll(unitDir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(unitDir, \"terragrunt.hcl\"), []byte(`# Unit to destroy`), 0644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(unitDir, \"main.tf\"), []byte(`\nresource \"null_resource\" \"test\" {}\n`), 0644)\n\trequire.NoError(t, err)\n\n\t// Initial commit\n\terr = runner.GoAdd(\".\")\n\trequire.NoError(t, err)\n\n\terr = runner.GoCommit(\"Initial commit\", &gogit.CommitOptions{\n\t\tAuthor: &object.Signature{\n\t\t\tName:  \"Test User\",\n\t\t\tEmail: \"test@example.com\",\n\t\t\tWhen:  time.Now(),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\t// do initial apply\n\tcmd := \"terragrunt run --all --no-color --non-interactive --working-dir \" + tmpDir +\n\t\t\" -- apply\"\n\thelpers.RunTerragrunt(t, cmd)\n\n\t// remove unit to trigger destruction\n\terr = os.RemoveAll(unitDir)\n\trequire.NoError(t, err)\n\n\terr = runner.GoAdd(\".\")\n\trequire.NoError(t, err)\n\n\terr = runner.GoCommit(\"Unit removal\", &gogit.CommitOptions{\n\t\tAuthor: &object.Signature{\n\t\t\tName:  \"Test User\",\n\t\t\tEmail: \"test@example.com\",\n\t\t\tWhen:  time.Now(),\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\tcmd = \"terragrunt run --all --no-color --experiment-mode --non-interactive --working-dir \" + tmpDir +\n\t\t\" --out-dir \" + outDir + \" --filter-allow-destroy --filter '[HEAD~1...HEAD]' -- plan\"\n\n\thelpers.RunTerragrunt(t, cmd)\n\n\t// check creation of plan files\n\tvar planFiles []string\n\n\terr = filepath.Walk(outDir, func(path string, info fs.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif strings.HasSuffix(path, \".tfplan\") {\n\t\t\tplanFiles = append(planFiles, path)\n\t\t}\n\n\t\treturn nil\n\t})\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, planFiles)\n\n\t// Bug regression test\n\n\tcmd = \"terragrunt run --all --no-color --non-interactive --working-dir \" + tmpDir +\n\t\t\" --out-dir \" + outDir + \" --filter-allow-destroy --filter '[HEAD~1...HEAD]' -- apply\"\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\trequire.NoError(t, err)\n\n\toutput := stdout + stderr\n\trequire.NotContains(t, output, \"Too many command line arguments\")\n\trequire.NotContains(t, output, \"Expected at most one positional argument\")\n}\n\nfunc TestFilterExcludeByDefault(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureExcludeByDefault)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureExcludeByDefault)\n\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\tcmd := \"terragrunt run --all --no-color --working-dir \" + rootPath + \" --filter '!_stacks | type=stack' -- plan\"\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\trequire.NoError(t, err)\n\n\tassert.NotContains(t, stderr, \"No units discovered\", \"Filter should discover units, not result in empty discovery\")\n}\n\nfunc TestFilterFlagWithMarkAsRead(t *testing.T) {\n\tt.Parallel()\n\n\tworkingDir, err := filepath.Abs(testFixtureFilterMarkAsRead)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tfilterQuery   string\n\t\texpectedUnits []string\n\t\texpectError   bool\n\t}{\n\t\t{\n\t\t\tname:          \"filter by reading - exact match with unit path\",\n\t\t\tfilterQuery:   \"reading=unit-normal/foo.txt\",\n\t\t\texpectedUnits: []string{\"unit-normal\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"filter by reading - wildcard matches foo.txt in multiple units\",\n\t\t\tfilterQuery:   \"reading=*/foo.txt\",\n\t\t\texpectedUnits: []string{\"unit-normal\", \"unit-duplicate\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"filter by reading - bar.txt only in duplicate unit\",\n\t\t\tfilterQuery:   \"reading=unit-duplicate/bar.txt\",\n\t\t\texpectedUnits: []string{\"unit-duplicate\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"filter by reading - wildcard *.txt matches all txt files\",\n\t\t\tfilterQuery:   \"reading=*/*.txt\",\n\t\t\texpectedUnits: []string{\"unit-normal\", \"unit-duplicate\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"filter by reading - double wildcard\",\n\t\t\tfilterQuery:   \"reading=**/foo.txt\",\n\t\t\texpectedUnits: []string{\"unit-normal\", \"unit-duplicate\"},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"filter by reading - non-matching file\",\n\t\t\tfilterQuery:   \"reading=*/nonexistent.txt\",\n\t\t\texpectedUnits: []string{},\n\t\t\texpectError:   false,\n\t\t},\n\t\t{\n\t\t\tname:          \"filter by reading - empty string is parse error\",\n\t\t\tfilterQuery:   \"reading=\",\n\t\t\texpectedUnits: []string{},\n\t\t\texpectError:   true, // Empty value after = is a parse error\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, workingDir)\n\n\t\t\tcmd := \"terragrunt find --no-color --working-dir \" + workingDir + \" --filter '\" + tc.filterQuery + \"'\"\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\trequire.Error(t, err)\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err, \"Unexpected error for filter query: %s\\nstderr: %s\", tc.filterQuery, stderr)\n\n\t\t\tresults := strings.Fields(stdout)\n\t\t\tassert.ElementsMatch(t, tc.expectedUnits, results, \"Output mismatch for filter query: %s\", tc.filterQuery)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/integration_find_test.go",
    "content": "package test_test\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/find\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"github.com/wI2L/jsondiff\"\n)\n\nconst (\n\ttestFixtureFindBasic                = \"fixtures/find/basic\"\n\ttestFixtureFindHidden               = \"fixtures/find/hidden\"\n\ttestFixtureFindDAG                  = \"fixtures/find/dag\"\n\ttestFixtureFindInternalVExternal    = \"fixtures/find/internal-v-external\"\n\ttestFixtureFindExclude              = \"fixtures/exclude/basic\"\n\ttestFixtureFindInclude              = \"fixtures/find/include\"\n\ttestFixtureFindReadTerragruntConfig = \"fixtures/find/read-terragrunt-config\"\n)\n\nfunc TestFindBasic(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureFindBasic)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt find --no-color --working-dir \"+testFixtureFindBasic)\n\trequire.NoError(t, err)\n\n\tassert.Empty(t, stderr)\n\tassert.Equal(t, \"stack\\nunit\\n\", stdout)\n}\n\nfunc TestFindBasicJSON(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureFindBasic)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt find --no-color --working-dir \"+testFixtureFindBasic+\" --json\")\n\trequire.NoError(t, err)\n\n\tassert.Empty(t, stderr)\n\tassert.JSONEq(t, `[{\"type\": \"stack\", \"path\": \"stack\"}, {\"type\": \"unit\", \"path\": \"unit\"}]`, stdout)\n}\n\nfunc TestFindHidden(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname     string\n\t\texpected string\n\t\tnoHidden bool\n\t}{\n\t\t{\n\t\t\tname:     \"default (includes hidden)\",\n\t\t\texpected: filepath.Join(\".hide\", \"unit\") + \"\\nstack\\nunit\\n\",\n\t\t},\n\t\t{\n\t\t\tname:     \"no-hidden flag excludes hidden\",\n\t\t\tnoHidden: true,\n\t\t\texpected: \"stack\\nunit\\n\",\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureFindHidden)\n\n\t\t\tcmd := \"terragrunt find --no-color --working-dir \" + testFixtureFindHidden\n\n\t\t\tif tc.noHidden {\n\t\t\t\tcmd += \" --no-hidden\"\n\t\t\t}\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Empty(t, stderr)\n\t\t\tassert.Equal(t, tc.expected, stdout)\n\t\t})\n\t}\n}\n\nfunc TestFindDAG(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tsort     string\n\t\texpected string\n\t}{\n\t\t{name: \"alpha\", sort: \"alpha\", expected: \"a-dependent\\nb-dependency\\nc-mixed-deps\\nd-dependencies-only\\n\"},\n\t\t{name: \"dag\", sort: \"dag\", expected: \"b-dependency\\na-dependent\\nd-dependencies-only\\nc-mixed-deps\\n\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureFindDAG)\n\n\t\t\tcmd := \"terragrunt find --no-color --working-dir \" + testFixtureFindDAG\n\n\t\t\tif tc.sort == \"dag\" {\n\t\t\t\tcmd += \" --dag\"\n\t\t\t}\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Empty(t, stderr)\n\t\t\tassert.Equal(t, tc.expected, stdout)\n\t\t})\n\t}\n}\n\nfunc TestFindDAGWithMixedDependencies(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureFindDAG)\n\n\ttestCases := []struct {\n\t\tname     string\n\t\targs     string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"dag with dependencies output\",\n\t\t\targs:     \"--dag --dependencies\",\n\t\t\texpected: \"b-dependency\\na-dependent\\nd-dependencies-only\\nc-mixed-deps\\n\",\n\t\t},\n\t\t{\n\t\t\tname:     \"dag with dependencies json output\",\n\t\t\targs:     \"--dag --dependencies --json\",\n\t\t\texpected: `[{\"type\":\"unit\",\"path\":\"b-dependency\"},{\"type\":\"unit\",\"path\":\"a-dependent\",\"dependencies\":[\"b-dependency\"]},{\"type\":\"unit\",\"path\":\"d-dependencies-only\",\"dependencies\":[\"a-dependent\"]},{\"type\":\"unit\",\"path\":\"c-mixed-deps\",\"dependencies\":[\"a-dependent\",\"d-dependencies-only\"]}]`,\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureFindDAG)\n\n\t\t\tcmd := \"terragrunt find --no-color --working-dir \" + testFixtureFindDAG + \" \" + tc.args\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Empty(t, stderr)\n\n\t\t\tif strings.Contains(tc.args, \"--json\") {\n\t\t\t\tjsonStringsEqual(t, tc.expected, stdout)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tc.expected, stdout)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// jsonStringsEqual compares two JSON strings for equivalence, ignoring the order of nested arrays.\nfunc jsonStringsEqual(t *testing.T, expected, actual string, msgAndArgs ...any) bool {\n\tt.Helper()\n\n\tpatch, err := jsondiff.CompareJSON([]byte(expected), []byte(actual), jsondiff.Equivalent())\n\trequire.NoErrorf(t, err, fmt.Sprintf(\"Error comparing JSON strings: %v\", err), msgAndArgs...)\n\trequire.Emptyf(t, patch, fmt.Sprintf(\"JSON strings are not equal\\nExpected: %s\\nActual: %s\", expected, actual), msgAndArgs...)\n\n\treturn true\n}\n\nfunc TestFindExternalDependencies(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureFindInternalVExternal)\n\n\tinternalDir := filepath.Join(testFixtureFindInternalVExternal, \"internal\")\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt find --no-color --working-dir \"+internalDir+\" --dependencies --external\",\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Empty(t, stderr)\n\tassert.Equal(t, filepath.Join(\"..\", \"external\", \"c-dependency\")+\"\\na-dependent\\nb-dependency\\n\", stdout)\n\n\tstdout, stderr, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt find --no-color --working-dir \"+internalDir+\" --dependencies\",\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Empty(t, stderr)\n\tassert.Equal(t, \"a-dependent\\nb-dependency\\n\", stdout)\n}\n\nfunc TestFindExternalDependenciesWithFilterFlag(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureFindInternalVExternal)\n\n\tinternalDir := filepath.Join(testFixtureFindInternalVExternal, \"internal\")\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt find --no-color --working-dir \"+internalDir+\" --dependencies --external --filter '{./**}...'\",\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Empty(t, stderr)\n\tassert.Equal(t, filepath.Join(\"..\", \"external\", \"c-dependency\")+\"\\na-dependent\\nb-dependency\\n\", stdout)\n\n\tstdout, stderr, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt find --no-color --working-dir \"+internalDir+\" --dependencies\")\n\trequire.NoError(t, err)\n\n\tassert.Empty(t, stderr)\n\tassert.Equal(t, \"a-dependent\\nb-dependency\\n\", stdout)\n}\n\nfunc TestFindInclude(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureFindInclude)\n\n\tworkdir := testFixtureFindInclude\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt find --no-color --working-dir \"+workdir+\" --include --json\")\n\trequire.NoError(t, err)\n\n\tassert.Empty(t, stderr)\n\tassert.JSONEq(t, `[{\"type\":\"unit\",\"path\":\"bar\",\"include\":{\"cloud\":\"cloud.hcl\"}},{\"type\":\"unit\",\"path\":\"foo\"}]`, stdout)\n}\n\nfunc TestFindExclude(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname           string\n\t\targs           string\n\t\texpectedOutput string\n\t\texpectedPaths  []string\n\t}{\n\t\t{\n\t\t\tname:          \"show exclude configs\",\n\t\t\targs:          \"--exclude\",\n\t\t\texpectedPaths: []string{\"unit1\", \"unit2\", \"unit3\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"exclude plan command\",\n\t\t\targs:          \"--queue-construct-as plan\",\n\t\t\texpectedPaths: []string{\"unit2\", \"unit3\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"exclude apply command\",\n\t\t\targs:          \"--queue-construct-as apply\",\n\t\t\texpectedPaths: []string{\"unit1\", \"unit3\"},\n\t\t},\n\t\t{\n\t\t\tname:          \"show exclude configs with json\",\n\t\t\targs:          \"--exclude --json\",\n\t\t\texpectedPaths: []string{\"unit1\", \"unit2\", \"unit3\"},\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureFindExclude)\n\n\t\t\tcmd := fmt.Sprintf(\"terragrunt find --no-color --working-dir %s %s\", testFixtureFindExclude, tc.args)\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Empty(t, stderr)\n\n\t\t\tif strings.Contains(tc.args, \"--json\") {\n\t\t\t\tvar configs find.FoundComponents\n\n\t\t\t\terr = json.Unmarshal([]byte(stdout), &configs)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tpaths := make([]string, 0, len(configs))\n\t\t\t\tfor _, config := range configs {\n\t\t\t\t\tpaths = append(paths, config.Path)\n\t\t\t\t\tif strings.Contains(tc.args, \"--exclude\") {\n\t\t\t\t\t\tswitch config.Path {\n\t\t\t\t\t\tcase \"unit1\":\n\t\t\t\t\t\t\tassert.NotNil(t, config.Exclude)\n\t\t\t\t\t\t\tassert.Contains(t, config.Exclude.Actions, \"plan\")\n\t\t\t\t\t\tcase \"unit2\":\n\t\t\t\t\t\t\tassert.NotNil(t, config.Exclude)\n\t\t\t\t\t\t\tassert.Contains(t, config.Exclude.Actions, \"apply\")\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tassert.Nil(t, config.Exclude)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tassert.ElementsMatch(t, tc.expectedPaths, paths)\n\t\t\t} else {\n\t\t\t\tpaths := strings.Fields(stdout)\n\t\t\t\tassert.ElementsMatch(t, tc.expectedPaths, paths)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFindQueueConstructAs(t *testing.T) {\n\tt.Parallel()\n\n\t// I'm using the list fixture here because it's more convenient.\n\ttestFixtureQueueConstruct := \"fixtures/list/dag\"\n\thelpers.CleanupTerraformFolder(t, testFixtureQueueConstruct)\n\n\ttestCases := []struct {\n\t\tname           string\n\t\targs           string\n\t\texpectedOutput string\n\t\texpectedPaths  []string\n\t}{\n\t\t{\n\t\t\tname: \"up command\",\n\t\t\targs: \"--queue-construct-as plan\",\n\t\t\texpectedPaths: []string{\n\t\t\t\tfilepath.Join(\"stacks\", \"live\", \"dev\"),\n\t\t\t\tfilepath.Join(\"stacks\", \"live\", \"prod\"),\n\t\t\t\tfilepath.Join(\"units\", \"live\", \"dev\", \"vpc\"),\n\t\t\t\tfilepath.Join(\"units\", \"live\", \"prod\", \"vpc\"),\n\t\t\t\tfilepath.Join(\"units\", \"live\", \"dev\", \"db\"),\n\t\t\t\tfilepath.Join(\"units\", \"live\", \"prod\", \"db\"),\n\t\t\t\tfilepath.Join(\"units\", \"live\", \"dev\", \"ec2\"),\n\t\t\t\tfilepath.Join(\"units\", \"live\", \"prod\", \"ec2\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"down command\",\n\t\t\targs: \"--queue-construct-as destroy\",\n\t\t\texpectedPaths: []string{\n\t\t\t\tfilepath.Join(\"stacks\", \"live\", \"dev\"),\n\t\t\t\tfilepath.Join(\"stacks\", \"live\", \"prod\"),\n\t\t\t\tfilepath.Join(\"units\", \"live\", \"dev\", \"ec2\"),\n\t\t\t\tfilepath.Join(\"units\", \"live\", \"prod\", \"ec2\"),\n\t\t\t\tfilepath.Join(\"units\", \"live\", \"dev\", \"db\"),\n\t\t\t\tfilepath.Join(\"units\", \"live\", \"prod\", \"db\"),\n\t\t\t\tfilepath.Join(\"units\", \"live\", \"dev\", \"vpc\"),\n\t\t\t\tfilepath.Join(\"units\", \"live\", \"prod\", \"vpc\"),\n\t\t\t},\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureQueueConstruct)\n\n\t\t\tcmd := fmt.Sprintf(\"terragrunt find --json --no-color --working-dir %s %s\", testFixtureQueueConstruct, tc.args)\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Empty(t, stderr)\n\n\t\t\tvar configs find.FoundComponents\n\n\t\t\terr = json.Unmarshal([]byte(stdout), &configs)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tpaths := make([]string, 0, len(configs))\n\t\t\tfor _, config := range configs {\n\t\t\t\tpaths = append(paths, config.Path)\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectedPaths, paths)\n\t\t})\n\t}\n}\n\n// TestFindWithReadTerragruntConfig tests that the find command works correctly\n// when using read_terragrunt_config with dependencies.\nfunc TestFindWithReadTerragruntConfig(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureFindReadTerragruntConfig)\n\n\ttestCases := []struct {\n\t\tname     string\n\t\targs     string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"find with dag and json\",\n\t\t\targs:     \"--dag --json\",\n\t\t\texpected: `[{\"type\":\"unit\",\"path\":\"module\"},{\"type\":\"unit\",\"path\":\".\"}]`,\n\t\t},\n\t\t{\n\t\t\tname:     \"find with json\",\n\t\t\targs:     \"--json\",\n\t\t\texpected: `[{\"type\":\"unit\",\"path\":\".\"},{\"type\":\"unit\",\"path\":\"module\"}]`,\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureFindReadTerragruntConfig)\n\n\t\t\tcmd := fmt.Sprintf(\"terragrunt find --no-color --working-dir %s %s\", testFixtureFindReadTerragruntConfig, tc.args)\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\t// The command should succeed without errors\n\t\t\trequire.NoError(t, err, \"find command should not fail\")\n\n\t\t\t// There should be no error output\n\t\t\tassert.Empty(t, stderr, \"stderr should be empty - no parse errors should occur\")\n\n\t\t\t// Verify the JSON output matches expected\n\t\t\tjsonStringsEqual(t, tc.expected, stdout)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/integration_functions_test.go",
    "content": "package test_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureStartswith              = \"fixtures/startswith\"\n\ttestFixtureTimecmp                 = \"fixtures/timecmp\"\n\ttestFixtureTimecmpInvalidTimestamp = \"fixtures/timecmp-errors/invalid-timestamp\"\n\ttestFixtureEndswith                = \"fixtures/endswith\"\n\ttestFixtureStrcontains             = \"fixtures/strcontains\"\n\ttestFixtureGetRepoRoot             = \"fixtures/get-repo-root\"\n\ttestFixtureGetWorkingDir           = \"fixtures/get-working-dir\"\n\ttestFixtureRelativeIncludeCmd      = \"fixtures/relative-include-cmd\"\n\ttestFixturePathRelativeFromInclude = \"fixtures/get-path/path_relative_from_include\"\n\ttestFixtureGetPathFromRepoRoot     = \"fixtures/get-path/get-path-from-repo-root\"\n\ttestFixtureGetPathToRepoRoot       = \"fixtures/get-path/get-path-to-repo-root\"\n\ttestFixtureGetPlatform             = \"fixtures/get-platform\"\n)\n\nfunc TestStartsWith(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStartswith)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStartswith)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStartswith)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// verify expected outputs are not empty\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\tvalidateOutput(t, outputs, \"startswith1\", true)\n\tvalidateOutput(t, outputs, \"startswith2\", false)\n\tvalidateOutput(t, outputs, \"startswith3\", true)\n\tvalidateOutput(t, outputs, \"startswith4\", false)\n\tvalidateOutput(t, outputs, \"startswith5\", true)\n\tvalidateOutput(t, outputs, \"startswith6\", false)\n\tvalidateOutput(t, outputs, \"startswith7\", true)\n\tvalidateOutput(t, outputs, \"startswith8\", false)\n\tvalidateOutput(t, outputs, \"startswith9\", false)\n}\n\nfunc TestTimeCmp(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureTimecmp)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureTimecmp)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureTimecmp)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// verify expected outputs are not empty\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\tvalidateOutput(t, outputs, \"timecmp1\", float64(0))\n\tvalidateOutput(t, outputs, \"timecmp2\", float64(0))\n\tvalidateOutput(t, outputs, \"timecmp3\", float64(1))\n\tvalidateOutput(t, outputs, \"timecmp4\", float64(-1))\n\tvalidateOutput(t, outputs, \"timecmp5\", float64(-1))\n\tvalidateOutput(t, outputs, \"timecmp6\", float64(1))\n}\n\nfunc TestTimeCmpInvalidTimestamp(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureTimecmpInvalidTimestamp)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureTimecmpInvalidTimestamp)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureTimecmpInvalidTimestamp)\n\n\t// verify expected outputs are not empty\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\n\texpectedError := `not a valid RFC3339 timestamp: missing required time introducer 'T'`\n\trequire.ErrorContains(t, err, expectedError)\n}\n\nfunc TestEndsWith(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureEndswith)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureEndswith)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureEndswith)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// verify expected outputs are not empty\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\tvalidateOutput(t, outputs, \"endswith1\", true)\n\tvalidateOutput(t, outputs, \"endswith2\", false)\n\tvalidateOutput(t, outputs, \"endswith3\", true)\n\tvalidateOutput(t, outputs, \"endswith4\", false)\n\tvalidateOutput(t, outputs, \"endswith5\", true)\n\tvalidateOutput(t, outputs, \"endswith6\", false)\n\tvalidateOutput(t, outputs, \"endswith7\", true)\n\tvalidateOutput(t, outputs, \"endswith8\", false)\n\tvalidateOutput(t, outputs, \"endswith9\", false)\n}\n\nfunc TestStrContains(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStrcontains)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStrcontains)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStrcontains)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// verify expected outputs are not empty\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\tvalidateOutput(t, outputs, \"o1\", true)\n\tvalidateOutput(t, outputs, \"o2\", false)\n}\n\nfunc TestGetRepoRootCaching(t *testing.T) {\n\tt.Parallel()\n\thelpers.CleanupTerraformFolder(t, testFixtureGetRepoRoot)\n\ttmpEnvPath, _ := filepath.EvalSymlinks(helpers.CopyEnvironment(t, testFixtureGetRepoRoot))\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetRepoRoot)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(rootPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --log-level debug --all plan --non-interactive --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\toutput := fmt.Sprintf(\"%s %s\", stdout, stderr)\n\tassert.Contains(t, output, \"git show-toplevel result\")\n\tassert.Contains(t, output, \"git rev-parse --show-toplevel\")\n\tassert.Contains(t, output, fmt.Sprintf(`repo_root = \"%s\"`, rootPath))\n}\n\nfunc TestGetRepoRoot(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetRepoRoot)\n\ttmpEnvPath, _ := filepath.EvalSymlinks(helpers.CopyEnvironment(t, testFixtureGetRepoRoot))\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetRepoRoot)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(rootPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// verify expected outputs are not empty\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(\n\t\t\tt,\n\t\t\t\"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath,\n\t\t\t&stdout,\n\t\t\t&stderr,\n\t\t),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\trepoRoot, ok := outputs[\"repo_root\"]\n\n\tassert.True(t, ok)\n\tassert.Regexp(t, \"/.*/TestGetRepoRoot.*/fixtures/get-repo-root\", repoRoot.Value)\n}\n\nfunc TestGetWorkingDirBuiltInFunc(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetWorkingDir)\n\ttmpEnvPath, _ := filepath.EvalSymlinks(helpers.CopyEnvironment(t, testFixtureGetWorkingDir))\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetWorkingDir)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(rootPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// verify expected outputs are not empty\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\tworkingDir, ok := outputs[\"working_dir\"]\n\n\texpectedWorkingDir := filepath.Join(rootPath, util.TerragruntCacheDir)\n\tcurWalkStep := 0\n\n\terr = filepath.WalkDir(expectedWorkingDir,\n\t\tfunc(path string, d fs.DirEntry, err error) error {\n\t\t\tif err != nil || !d.IsDir() {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\texpectedWorkingDir = path\n\n\t\t\tif curWalkStep == 2 {\n\t\t\t\treturn filepath.SkipDir\n\t\t\t}\n\n\t\t\tcurWalkStep++\n\n\t\t\treturn nil\n\t\t})\n\trequire.NoError(t, err)\n\n\tassert.True(t, ok)\n\tassert.Equal(t, expectedWorkingDir, workingDir.Value)\n}\n\nfunc TestPathRelativeToIncludeInvokedInCorrectPathFromChild(t *testing.T) {\n\tt.Parallel()\n\n\tappPath := path.Join(testFixtureRelativeIncludeCmd, \"app\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --non-interactive --working-dir \"+appPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\toutput := stdout.String()\n\tassert.Equal(t, 1, strings.Count(output, \"path_relative_to_inclue: app\\n\"))\n\tassert.Equal(t, 0, strings.Count(output, \"path_relative_to_inclue: .\\n\"))\n}\n\nfunc TestPathRelativeFromInclude(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixturePathRelativeFromInclude)\n\ttmpEnvPath, err := filepath.EvalSymlinks(helpers.CopyEnvironment(t, testFixturePathRelativeFromInclude))\n\trequire.NoError(t, err)\n\n\trootPath := filepath.Join(tmpEnvPath, testFixturePathRelativeFromInclude, \"lives/dev\")\n\tbasePath := filepath.Join(rootPath, \"base\")\n\tclusterPath := filepath.Join(rootPath, \"cluster\")\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpEnvPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// verify expected outputs are not empty\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+clusterPath)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal([]byte(stdout), &outputs))\n\n\tval, hasVal := outputs[\"some_output\"]\n\tassert.True(t, hasVal)\n\tassert.Equal(t, \"something else\", val.Value)\n\n\t// try to destroy module and check if warning is printed in output, also test `get_parent_terragrunt_dir()` func in the parent terragrunt config.\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --non-interactive --working-dir \"+basePath+\" -- destroy -auto-approve\")\n\trequire.NoError(t, err)\n\n\tassert.NotContains(t, stderr, \"Detected dependent units:\\n\"+clusterPath)\n\n\t_, stderr, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --destroy-dependencies-check --non-interactive --working-dir \"+basePath+\" -- destroy -auto-approve\")\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"Detected dependent units:\\n\"+clusterPath)\n}\n\nfunc TestGetPathFromRepoRoot(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetPathFromRepoRoot)\n\ttmpEnvPath, _ := filepath.EvalSymlinks(helpers.CopyEnvironment(t, testFixtureGetPathFromRepoRoot))\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetPathFromRepoRoot)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpEnvPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// verify expected outputs are not empty\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\tpathFromRoot, hasPathFromRoot := outputs[\"path_from_root\"]\n\n\tassert.True(t, hasPathFromRoot)\n\tassert.Equal(t, testFixtureGetPathFromRepoRoot, pathFromRoot.Value)\n}\n\nfunc TestGetPathToRepoRoot(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath, _ := filepath.EvalSymlinks(helpers.CopyEnvironment(t, testFixtureGetPathToRepoRoot))\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetPathToRepoRoot)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpEnvPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// verify expected outputs are not empty\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\texpectedToRoot, err := filepath.Rel(rootPath, tmpEnvPath)\n\trequire.NoError(t, err)\n\n\tfor name, expected := range map[string]string{\n\t\t\"path_to_root\":    expectedToRoot,\n\t\t\"path_to_modules\": filepath.Join(expectedToRoot, \"modules\"),\n\t} {\n\t\tvalue, hasValue := outputs[name]\n\n\t\tassert.True(t, hasValue)\n\t\tassert.Equal(t, expected, value.Value)\n\t}\n}\n\nfunc TestGetPlatform(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetPlatform)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetPlatform)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetPlatform)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// verify expected outputs are not empty\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\tplatform, hasPlatform := outputs[\"platform\"]\n\tassert.True(t, hasPlatform)\n\tassert.Equal(t, runtime.GOOS, platform.Value)\n}\n"
  },
  {
    "path": "test/integration_gcp_test.go",
    "content": "//go:build gcp || awsgcp\n\npackage test_test\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"maps\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"cloud.google.com/go/storage\"\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\tgcsbackend \"github.com/gruntwork-io/terragrunt/internal/remotestate/backend/gcs\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"google.golang.org/api/iterator\"\n)\n\nconst (\n\tterraformRemoteStateGcpRegion = \"eu\"\n\n\ttestFixtureGcsPath              = \"fixtures/gcs/\"\n\ttestFixtureGcsByoBucketPath     = \"fixtures/gcs-byo-bucket/\"\n\ttestFixtureGcsImpersonatePath   = \"fixtures/gcs-impersonate/\"\n\ttestFixtureGcsNoBucket          = \"fixtures/gcs-no-bucket/\"\n\ttestFixtureGcsNoPrefix          = \"fixtures/gcs-no-prefix/\"\n\ttestFixtureGcsParallelStateInit = \"fixtures/gcs-parallel-state-init\"\n\ttestFixtureGCSBackend           = \"fixtures/gcs-backend\"\n)\n\nfunc TestGcpBootstrapBackend(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tcheckExpectedResultFn func(t *testing.T, stderr string, gcsBucketNameName string, err error)\n\t\tname                  string\n\t\targs                  string\n\t}{\n\t\t{\n\t\t\tname: \"no bootstrap gcs backend without flag\",\n\t\t\targs: \"run apply\",\n\t\t\tcheckExpectedResultFn: func(t *testing.T, stderr string, gcsBucketNameName string, err error) {\n\t\t\t\tt.Helper()\n\n\t\t\t\tassert.Contains(t, stderr, \"bucket doesn't exist\")\n\t\t\t\trequire.Error(t, err)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"bootstrap gcs backend with flag\",\n\t\t\targs: \"run apply --backend-bootstrap\",\n\t\t\tcheckExpectedResultFn: func(t *testing.T, stderr string, gcsBucketName string, err error) {\n\t\t\t\tt.Helper()\n\n\t\t\t\tvalidateGCSBucketExistsAndIsLabeled(t, terraformRemoteStateGcpRegion, gcsBucketName, nil)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"bootstrap gcs backend by backend command\",\n\t\t\targs: \"backend bootstrap --backend-bootstrap\",\n\t\t\tcheckExpectedResultFn: func(t *testing.T, stderr string, gcsBucketName string, err error) {\n\t\t\t\tt.Helper()\n\n\t\t\t\tvalidateGCSBucketExistsAndIsLabeled(t, terraformRemoteStateGcpRegion, gcsBucketName, nil)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t},\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureGCSBackend)\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGCSBackend)\n\t\t\trootPath := filepath.Join(tmpEnvPath, testFixtureGCSBackend)\n\n\t\t\tgcsBucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\t\t\tdefer func() {\n\t\t\t\tdeleteGCSBucket(t, gcsBucketName)\n\t\t\t}()\n\n\t\t\tproject := os.Getenv(\"GOOGLE_CLOUD_PROJECT\")\n\t\t\tcommonConfigPath := filepath.Join(rootPath, \"common.hcl\")\n\t\t\tcopyTerragruntGCSConfigAndFillPlaceholders(t, commonConfigPath, commonConfigPath, project, terraformRemoteStateGcpRegion, gcsBucketName)\n\n\t\t\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt \"+tc.args+\" --all --non-interactive --log-level debug --working-dir \"+rootPath)\n\n\t\t\ttc.checkExpectedResultFn(t, stderr, gcsBucketName, err)\n\t\t})\n\t}\n}\n\nfunc TestGcpBootstrapBackendWithoutVersioning(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGCSBackend)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGCSBackend)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGCSBackend)\n\n\tgcsBucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer func() {\n\t\tdeleteGCSBucket(t, gcsBucketName)\n\t}()\n\n\tproject := os.Getenv(\"GOOGLE_CLOUD_PROJECT\")\n\tcommonConfigPath := filepath.Join(rootPath, \"common.hcl\")\n\tcopyTerragruntGCSConfigAndFillPlaceholders(t, commonConfigPath, commonConfigPath, project, terraformRemoteStateGcpRegion, gcsBucketName)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all --non-interactive --log-level debug --working-dir \"+rootPath+\" --feature disable_versioning=true apply --backend-bootstrap\",\n\t)\n\trequire.NoError(t, err)\n\n\tvalidateGCSBucketExistsAndIsLabeled(t, terraformRemoteStateGcpRegion, gcsBucketName, nil)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt, \"terragrunt --non-interactive --log-level debug --working-dir \"+rootPath+\" backend delete --all --feature disable_versioning=true\",\n\t)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"backend delete for unit\")\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt, \"terragrunt --non-interactive --log-level debug --working-dir \"+rootPath+\" backend delete --backend-bootstrap --feature disable_versioning=true --all --force\",\n\t)\n\trequire.NoError(t, err)\n}\n\nfunc TestGcpMigrateBackendWithoutVersioning(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGCSBackend)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGCSBackend)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGCSBackend)\n\tunitPath := filepath.Join(rootPath, \"unit1\")\n\n\tgcsBucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer func() {\n\t\tdeleteGCSBucket(t, gcsBucketName)\n\t}()\n\n\tproject := os.Getenv(\"GOOGLE_CLOUD_PROJECT\")\n\tcommonConfigPath := filepath.Join(rootPath, \"common.hcl\")\n\tcopyTerragruntGCSConfigAndFillPlaceholders(t, commonConfigPath, commonConfigPath, project, terraformRemoteStateGcpRegion, gcsBucketName)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --non-interactive --log-level debug --working-dir \"+unitPath+\" --feature disable_versioning=true apply --backend-bootstrap -- -auto-approve\")\n\trequire.NoError(t, err)\n\n\tvalidateGCSBucketExistsAndIsLabeled(t, terraformRemoteStateGcpRegion, gcsBucketName, nil)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt --non-interactive --log-level debug --working-dir \"+rootPath+\" backend migrate --backend-bootstrap --feature disable_versioning=true unit1 unit2\")\n\trequire.Error(t, err)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt --non-interactive --log-level debug --working-dir \"+rootPath+\" backend migrate --backend-bootstrap --feature disable_versioning=true --force unit1 unit2\")\n\trequire.NoError(t, err)\n}\n\nfunc TestGcpDeleteBackend(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGCSBackend)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGCSBackend)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGCSBackend)\n\n\tgcsBucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer func() {\n\t\tdeleteGCSBucket(t, gcsBucketName)\n\t}()\n\n\tproject := os.Getenv(\"GOOGLE_CLOUD_PROJECT\")\n\tcommonConfigPath := filepath.Join(rootPath, \"common.hcl\")\n\tcopyTerragruntGCSConfigAndFillPlaceholders(t, commonConfigPath, commonConfigPath, project, terraformRemoteStateGcpRegion, gcsBucketName)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run apply --backend-bootstrap --all --non-interactive --log-level debug --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tremoteStateObjectNames := []string{\n\t\t\"unit1/tofu.tfstate/default.tfstate\",\n\t\t\"unit2/tofu.tfstate/default.tfstate\",\n\t}\n\n\tfor _, objectName := range remoteStateObjectNames {\n\t\tassert.True(t, doesGCSBucketObjectExist(t, gcsBucketName, objectName), \"GCS bucket object %s must exist\", objectName)\n\t}\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt backend delete --all --non-interactive --log-level debug --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tfor _, objectName := range remoteStateObjectNames {\n\t\tassert.False(t, doesGCSBucketObjectExist(t, gcsBucketName, objectName), \"GCS bucket object %s must not exist\", objectName)\n\t}\n}\n\nfunc TestGcpMigrateBackend(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGCSBackend)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGCSBackend)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGCSBackend)\n\n\tunit1Path := filepath.Join(rootPath, \"unit1\")\n\tunit2Path := filepath.Join(rootPath, \"unit2\")\n\n\tunit1BackendKey := \"unit1/tofu.tfstate/default.tfstate\"\n\tunit2BackendKey := \"unit2/tofu.tfstate/default.tfstate\"\n\n\tproject := os.Getenv(\"GOOGLE_CLOUD_PROJECT\")\n\tgcsBucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer func() {\n\t\tdeleteGCSBucket(t, gcsBucketName)\n\t}()\n\n\tcommonConfigPath := filepath.Join(rootPath, \"common.hcl\")\n\tcopyTerragruntGCSConfigAndFillPlaceholders(t, commonConfigPath, commonConfigPath, project, terraformRemoteStateGcpRegion, gcsBucketName)\n\n\t// Bootstrap backend and create remote state for unit1.\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run apply --backend-bootstrap --non-interactive --log-level debug --working-dir \"+unit1Path+\" -- -auto-approve\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"Changes to Outputs\")\n\n\t// Check for remote states.\n\n\tassert.True(t, doesGCSBucketObjectExist(t, gcsBucketName, unit1BackendKey), \"GCS bucket object %s must exist\", unit1BackendKey)\n\tassert.False(t, doesGCSBucketObjectExist(t, gcsBucketName, unit2BackendKey), \"GCS bucket object %s must not exist\", unit2BackendKey)\n\n\t// Migrate remote state from unit1 to unit2.\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt backend migrate --log-level debug --working-dir \"+rootPath+\" unit1 unit2\")\n\trequire.NoError(t, err)\n\n\t// Check for remote states after migration.\n\tassert.False(t, doesGCSBucketObjectExist(t, gcsBucketName, unit1BackendKey), \"GCS bucket object %s must not exist\", unit1BackendKey)\n\tassert.True(t, doesGCSBucketObjectExist(t, gcsBucketName, unit2BackendKey), \"GCS bucket object %s must exist\", unit2BackendKey)\n\n\t// Run `tofu apply` for unit2 with migrated remote state from unit1.\n\n\tstdout, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run apply --backend-bootstrap --non-interactive --log-level debug --working-dir \"+unit2Path+\" -- -auto-approve\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"No changes\")\n}\n\nfunc TestGcpWorksWithBackend(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGcsPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGcsPath)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\thelpers.CleanupTerragruntFolder(t, rootPath)\n\n\t// We need a project to create the bucket in, so we pull one from the recommended environment variable.\n\tproject := os.Getenv(\"GOOGLE_CLOUD_PROJECT\")\n\tgcsBucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer deleteGCSBucket(t, gcsBucketName)\n\n\ttmpTerragruntGCSConfigPath := createTmpTerragruntGCSConfig(\n\t\tt,\n\t\trootPath,\n\t\tproject,\n\t\tterraformRemoteStateGcpRegion,\n\t\tgcsBucketName,\n\t\tconfig.DefaultTerragruntConfigPath,\n\t)\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt apply -auto-approve --non-interactive --backend-bootstrap --config %s --working-dir %s\",\n\t\t\ttmpTerragruntGCSConfigPath,\n\t\t\trootPath,\n\t\t),\n\t)\n\n\tvar expectedGCSLabels = map[string]string{\n\t\t\"owner\": \"terragrunt_test\",\n\t\t\"name\":  \"terraform_state_storage\"}\n\tvalidateGCSBucketExistsAndIsLabeled(t, terraformRemoteStateGcpRegion, gcsBucketName, expectedGCSLabels)\n}\n\nfunc TestGcpWorksWithExistingBucket(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGcsByoBucketPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGcsByoBucketPath)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\t// We need a project to create the bucket in, so we pull one from the recommended environment variable.\n\tproject := os.Getenv(\"GOOGLE_CLOUD_PROJECT\")\n\tgcsBucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer deleteGCSBucket(t, gcsBucketName)\n\n\t// manually create the GCS bucket outside the US (default) to test Terragrunt works correctly with an existing bucket.\n\tlocation := terraformRemoteStateGcpRegion\n\tcreateGCSBucket(t, project, location, gcsBucketName)\n\n\ttmpTerragruntGCSConfigPath := createTmpTerragruntGCSConfig(\n\t\tt,\n\t\trootPath,\n\t\tproject,\n\t\tterraformRemoteStateGcpRegion,\n\t\tgcsBucketName,\n\t\tconfig.DefaultTerragruntConfigPath,\n\t)\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt apply -auto-approve --non-interactive --config %s --working-dir %s\",\n\t\t\ttmpTerragruntGCSConfigPath,\n\t\t\trootPath,\n\t\t),\n\t)\n\n\tvalidateGCSBucketExistsAndIsLabeled(t, location, gcsBucketName, nil)\n}\n\nfunc TestGcpCheckMissingBucket(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGcsNoBucket)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGcsNoBucket)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\t// We need a project to create the bucket in, so we pull one from the recommended environment variable.\n\tproject := os.Getenv(\"GOOGLE_CLOUD_PROJECT\")\n\tgcsBucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\ttmpTerragruntGCSConfigPath := createTmpTerragruntGCSConfig(\n\t\tt,\n\t\trootPath,\n\t\tproject,\n\t\tterraformRemoteStateGcpRegion,\n\t\tgcsBucketName,\n\t\tconfig.DefaultTerragruntConfigPath,\n\t)\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt apply -auto-approve --backend-bootstrap --non-interactive --config %s --working-dir %s\",\n\t\t\ttmpTerragruntGCSConfigPath,\n\t\t\trootPath,\n\t\t),\n\t)\n\trequire.Error(t, err)\n\n\tassert.Contains(t, err.Error(), \"Missing required GCS remote state configuration bucket\")\n}\n\nfunc TestGcpNoPrefixBucket(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGcsNoPrefix)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGcsNoPrefix)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\t// We need a project to create the bucket in, so we pull one from the recommended environment variable.\n\tproject := os.Getenv(\"GOOGLE_CLOUD_PROJECT\")\n\tgcsBucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer deleteGCSBucket(t, gcsBucketName)\n\n\ttmpTerragruntGCSConfigPath := createTmpTerragruntGCSConfig(t, rootPath, project, terraformRemoteStateGcpRegion, gcsBucketName, config.DefaultTerragruntConfigPath)\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt apply -auto-approve --backend-bootstrap --non-interactive --config %s --working-dir %s\",\n\t\t\ttmpTerragruntGCSConfigPath,\n\t\t\trootPath,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n}\n\nfunc TestGcpParallelStateInit(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath, err := os.MkdirTemp(\"\", \"terragrunt-test\") //nolint:usetesting\n\tif err != nil {\n\t\trequire.NoError(t, err)\n\t}\n\n\tfor i := range 20 {\n\t\terr := util.CopyFolderContents(createLogger(), testFixtureGcsParallelStateInit, tmpEnvPath, \".terragrunt-test\", nil, nil)\n\t\trequire.NoError(t, err)\n\n\t\terr = os.Rename(\n\t\t\tpath.Join(tmpEnvPath, \"template\"),\n\t\t\tpath.Join(tmpEnvPath, \"app\"+strconv.Itoa(i)),\n\t\t)\n\n\t\trequire.NoError(t, err)\n\t}\n\n\ttmpTerragruntConfigFile := filepath.Join(tmpEnvPath, \"root.hcl\")\n\tproject := os.Getenv(\"GOOGLE_CLOUD_PROJECT\")\n\tgcsBucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\ttmpTerragruntGCSConfigPath := createTmpTerragruntGCSConfig(\n\t\tt,\n\t\ttestFixtureGcsParallelStateInit,\n\t\tproject,\n\t\tterraformRemoteStateGcpRegion,\n\t\tgcsBucketName,\n\t\t\"root.hcl\",\n\t)\n\terr = util.CopyFile(tmpTerragruntGCSConfigPath, tmpTerragruntConfigFile)\n\trequire.NoError(t, err)\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\t\"terragrunt run --all --backend-bootstrap --non-interactive --working-dir \"+tmpEnvPath+\" -- apply\",\n\t)\n}\n\nfunc createTmpTerragruntGCSConfig(t *testing.T, templatesPath string, project string, location string, gcsBucketName string, configFileName string) string {\n\tt.Helper()\n\n\ttmpFolder, err := os.MkdirTemp(\"\", \"terragrunt-test\") //nolint:usetesting\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create temp folder due to error: %v\", err)\n\t}\n\n\ttmpTerragruntConfigFile := filepath.Join(tmpFolder, configFileName)\n\toriginalTerragruntConfigPath := filepath.Join(templatesPath, configFileName)\n\tcopyTerragruntGCSConfigAndFillPlaceholders(t, originalTerragruntConfigPath, tmpTerragruntConfigFile, project, location, gcsBucketName)\n\n\treturn tmpTerragruntConfigFile\n}\n\nfunc copyTerragruntGCSConfigAndFillPlaceholders(t *testing.T, configSrcPath string, configDestPath string, project string, location string, gcsBucketName string) {\n\tt.Helper()\n\n\temail := os.Getenv(\"GOOGLE_IDENTITY_EMAIL\")\n\n\thelpers.CopyAndFillMapPlaceholders(t, configSrcPath, configDestPath, map[string]string{\n\t\t\"__FILL_IN_PROJECT__\":     project,\n\t\t\"__FILL_IN_LOCATION__\":    location,\n\t\t\"__FILL_IN_BUCKET_NAME__\": gcsBucketName,\n\t\t\"__FILL_IN_GCP_EMAIL__\":   email,\n\t})\n}\n\n// Check that the GCS Bucket of the given name and location exists. Terragrunt should create this bucket during the test.\n// Also check if bucket got labeled properly.\nfunc validateGCSBucketExistsAndIsLabeled(t *testing.T, location string, bucketName string, expectedLabels map[string]string) {\n\tt.Helper()\n\n\textGCSCfg := &gcsbackend.ExtendedRemoteStateConfigGCS{\n\t\tRemoteStateConfigGCS: gcsbackend.RemoteStateConfigGCS{\n\t\t\tBucket: bucketName,\n\t\t},\n\t}\n\n\topts := options.NewTerragruntOptions()\n\n\tgcsClient, err := gcsbackend.NewClient(t.Context(), extGCSCfg, configbridge.BackendOptsFromOpts(opts))\n\trequire.NoError(t, err, \"Error creating GCS client\")\n\n\t// verify the bucket exists\n\tassert.True(t, gcsClient.DoesGCSBucketExist(t.Context(), bucketName), \"Terragrunt failed to create remote state GCS bucket %s\", bucketName)\n\n\t// verify the bucket location\n\tbucket := gcsClient.Bucket(bucketName)\n\tattrs, err := bucket.Attrs(t.Context())\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, strings.ToUpper(location), attrs.Location, \"Did not find GCS bucket in expected location.\")\n\n\tif expectedLabels != nil {\n\t\tassertGCSLabels(t, expectedLabels, bucketName, gcsClient.Client)\n\t}\n}\n\nfunc doesGCSBucketObjectExist(t *testing.T, bucketName, prefix string) bool {\n\tt.Helper()\n\n\tctx := t.Context()\n\n\textGCSCfg := &gcsbackend.ExtendedRemoteStateConfigGCS{\n\t\tRemoteStateConfigGCS: gcsbackend.RemoteStateConfigGCS{\n\t\t\tBucket: bucketName,\n\t\t},\n\t}\n\n\topts := options.NewTerragruntOptions()\n\n\tgcsClient, err := gcsbackend.NewClient(ctx, extGCSCfg, configbridge.BackendOptsFromOpts(opts))\n\trequire.NoError(t, err, \"Error creating GCS client\")\n\n\tdefer gcsClient.Close()\n\n\tbucket := gcsClient.Bucket(bucketName)\n\n\tit := bucket.Objects(ctx, &storage.Query{\n\t\tPrefix: prefix,\n\t})\n\n\tif _, err := it.Next(); err != nil {\n\t\tif errors.Is(err, iterator.Done) {\n\t\t\treturn false\n\t\t}\n\n\t\trequire.NoError(t, err)\n\t}\n\n\treturn true\n}\n\n// gcsObjectAttrs returns the attributes of the specified object in the bucket\nfunc gcsObjectAttrs(t *testing.T, bucketName string, objectName string) *storage.ObjectAttrs {\n\tt.Helper()\n\n\tctx := t.Context()\n\n\textGCSCfg := &gcsbackend.ExtendedRemoteStateConfigGCS{\n\t\tRemoteStateConfigGCS: gcsbackend.RemoteStateConfigGCS{\n\t\t\tBucket: bucketName,\n\t\t},\n\t}\n\n\topts := options.NewTerragruntOptions()\n\n\tgcsClient, err := gcsbackend.NewClient(ctx, extGCSCfg, configbridge.BackendOptsFromOpts(opts))\n\trequire.NoError(t, err, \"Error creating GCS client\")\n\n\tbucket := gcsClient.Bucket(bucketName)\n\n\thandle := bucket.Object(objectName)\n\n\tattrs, err := handle.Attrs(ctx)\n\tif err != nil {\n\t\tt.Fatalf(\"Error reading object attributes %s %v\", objectName, err)\n\t}\n\n\treturn attrs\n}\n\nfunc assertGCSLabels(t *testing.T, expectedLabels map[string]string, bucketName string, client *storage.Client) {\n\tt.Helper()\n\n\tctx := t.Context()\n\tbucket := client.Bucket(bucketName)\n\n\tattrs, err := bucket.Attrs(ctx)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tvar actualLabels = make(map[string]string)\n\n\tmaps.Copy(actualLabels, attrs.Labels)\n\n\tassert.Equal(t, expectedLabels, actualLabels, \"Did not find expected labels on GCS bucket.\")\n}\n\n// Create the specified GCS bucket\nfunc createGCSBucket(t *testing.T, projectID string, location string, bucketName string) {\n\tt.Helper()\n\n\tctx := t.Context()\n\n\textGCSCfg := &gcsbackend.ExtendedRemoteStateConfigGCS{}\n\n\topts := options.NewTerragruntOptions()\n\n\tgcsClient, err := gcsbackend.NewClient(ctx, extGCSCfg, configbridge.BackendOptsFromOpts(opts))\n\trequire.NoError(t, err, \"Error creating GCS client\")\n\n\tt.Logf(\"Creating test GCS bucket %s in project %s, location %s\", bucketName, projectID, location)\n\n\tbucket := gcsClient.Bucket(bucketName)\n\n\tbucketAttrs := &storage.BucketAttrs{\n\t\tLocation:          location,\n\t\tVersioningEnabled: true,\n\t}\n\n\tif err := bucket.Create(ctx, projectID, bucketAttrs); err != nil {\n\t\tt.Fatalf(\"Failed to create GCS bucket %s: %v\", bucketName, err)\n\t}\n}\n\n// Delete the specified GCS bucket to clean up after a test\nfunc deleteGCSBucket(t *testing.T, bucketName string) {\n\tt.Helper()\n\n\tctx := t.Context()\n\n\textGCSCfg := &gcsbackend.ExtendedRemoteStateConfigGCS{}\n\n\topts := options.NewTerragruntOptions()\n\n\tgcsClient, err := gcsbackend.NewClient(ctx, extGCSCfg, configbridge.BackendOptsFromOpts(opts))\n\trequire.NoError(t, err, \"Error creating GCS client\")\n\n\tt.Logf(\"Deleting test GCS bucket %s\", bucketName)\n\n\t// List all objects including their versions in the bucket\n\tbucket := gcsClient.Bucket(bucketName)\n\tq := &storage.Query{\n\t\tVersions: true,\n\t}\n\n\tit := bucket.Objects(ctx, q)\n\tfor {\n\t\tobjectAttrs, err := it.Next()\n\n\t\tif errors.Is(err, iterator.Done) {\n\t\t\tbreak\n\t\t}\n\n\t\tif err != nil {\n\t\t\tt.Logf(\"Failed to list objects and versions in GCS bucket %s: %v\", bucketName, err)\n\t\t\treturn\n\t\t}\n\n\t\t// purge the object version\n\t\tif err := bucket.Object(objectAttrs.Name).Generation(objectAttrs.Generation).Delete(ctx); err != nil {\n\t\t\tt.Logf(\"Failed to delete GCS bucket object %s: %v\", objectAttrs.Name, err)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// remote empty bucket\n\tif err := bucket.Delete(ctx); err != nil {\n\t\tt.Fatalf(\"Failed to delete GCS bucket %s: %v\", bucketName, err)\n\t}\n}\n"
  },
  {
    "path": "test/integration_graph_test.go",
    "content": "package test_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureGraph = \"fixtures/graph\"\n)\n\nfunc TestTerragruntDestroyGraph(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tpath               string\n\t\texpectedModules    []string\n\t\tnotExpectedModules []string\n\t}{\n\t\t{\n\t\t\tpath:               \"eks\",\n\t\t\texpectedModules:    []string{\"services/eks-service-3-v3\", \"services/eks-service-3-v2\", \"services/eks-service-3\", \"services/eks-service-4\", \"services/eks-service-5\", \"services/eks-service-2-v2\", \"services/eks-service-2\", \"services/eks-service-1\"},\n\t\t\tnotExpectedModules: []string{\"lambda\", \"services/lambda-service-1\", \"services/lambda-service-2\"},\n\t\t},\n\t\t{\n\t\t\tpath:               \"services/lambda-service-1\",\n\t\t\texpectedModules:    []string{\"services/lambda-service-2\"},\n\t\t\tnotExpectedModules: []string{\"lambda\"},\n\t\t},\n\t\t{\n\t\t\tpath:               \"services/eks-service-3\",\n\t\t\texpectedModules:    []string{\"services/eks-service-3-v2\", \"services/eks-service-4\", \"services/eks-service-3-v3\"},\n\t\t\tnotExpectedModules: []string{\"eks\", \"services/eks-service-1\", \"services/eks-service-2\"},\n\t\t},\n\t\t{\n\t\t\tpath:               \"services/lambda-service-2\",\n\t\t\texpectedModules:    []string{\"services/lambda-service-2\"},\n\t\t\tnotExpectedModules: []string{\"services/lambda-service-1\", \"lambda\"},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.path, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpEnvPath := prepareGraphFixture(t)\n\t\t\tfixturePath := filepath.Join(tmpEnvPath, testFixtureGraph)\n\t\t\ttmpModulePath := filepath.Join(fixturePath, tc.path)\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt run --graph destroy --non-interactive --working-dir %s --graph-root %s\", tmpModulePath, tmpEnvPath))\n\t\t\trequire.NoError(t, err)\n\n\t\t\toutput := fmt.Sprintf(\"%v\\n%v\\n\", stdout, stderr)\n\n\t\t\tfor _, modulePath := range tc.expectedModules {\n\t\t\t\tmodulePath = filepath.Join(fixturePath, modulePath)\n\n\t\t\t\trelPath, err := filepath.Rel(tmpEnvPath, modulePath)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tassert.Containsf(t, output, relPath+\"\\n\", \"Expected module %s to be in output: %s\", relPath, output)\n\t\t\t}\n\n\t\t\tfor _, modulePath := range tc.notExpectedModules {\n\t\t\t\tmodulePath = filepath.Join(fixturePath, modulePath)\n\n\t\t\t\trelPath, err := filepath.Rel(tmpEnvPath, modulePath)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tassert.NotContainsf(t, output, \"Unit \"+relPath+\"\\n\", \"Expected module %s must not to be in output: %s\", relPath, output)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTerragruntApplyGraph(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\targs               string\n\t\tpath               string\n\t\texpectedModules    []string\n\t\tnotExpectedModules []string\n\t}{\n\t\t{\n\t\t\targs:               \"run --graph apply --non-interactive --working-dir %s --graph-root %s\",\n\t\t\tpath:               \"lambda\",\n\t\t\texpectedModules:    []string{\"lambda\", \"services/lambda-service-1\", \"services/lambda-service-2\"},\n\t\t\tnotExpectedModules: []string{\"eks\", \"services/eks-service-1\", \"services/eks-service-2\", \"services/eks-service-3\"},\n\t\t},\n\t\t{\n\t\t\targs:               \"run apply --graph --non-interactive --working-dir %s --graph-root %s\",\n\t\t\tpath:               \"services/eks-service-5\",\n\t\t\texpectedModules:    []string{\"services/eks-service-5\"},\n\t\t\tnotExpectedModules: []string{\"eks\", \"lambda\", \"services/eks-service-1\", \"services/eks-service-2\", \"services/eks-service-3\"},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.path, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpEnvPath := prepareGraphFixture(t)\n\t\t\tfixturePath := filepath.Join(tmpEnvPath, testFixtureGraph)\n\t\t\ttmpModulePath := filepath.Join(fixturePath, tc.path)\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt \"+tc.args, tmpModulePath, tmpEnvPath))\n\t\t\trequire.NoError(t, err)\n\n\t\t\toutput := fmt.Sprintf(\"%v\\n%v\\n\", stdout, stderr)\n\n\t\t\tfor _, modulePath := range tc.expectedModules {\n\t\t\t\tmodulePath = filepath.Join(fixturePath, modulePath)\n\n\t\t\t\trelPath, err := filepath.Rel(tmpEnvPath, modulePath)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tassert.Containsf(t, output, relPath+\"\\n\", \"Expected module %s to be in output: %s\", relPath, output)\n\t\t\t}\n\n\t\t\tfor _, modulePath := range tc.notExpectedModules {\n\t\t\t\tmodulePath = filepath.Join(fixturePath, modulePath)\n\n\t\t\t\trelPath, err := filepath.Rel(tmpEnvPath, modulePath)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tassert.NotContainsf(t, output, \"Unit \"+relPath+\"\\n\", \"Expected module %s must not to be in output: %s\", relPath, output)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc prepareGraphFixture(t *testing.T) string {\n\tt.Helper()\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGraph)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureGraph)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt run --all apply --non-interactive --working-dir \"+testPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\treturn tmpEnvPath\n}\n"
  },
  {
    "path": "test/integration_hcl_filter_test.go",
    "content": "package test_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/gruntwork-io/terragrunt/internal/cli/commands/hcl/format\"\n\t\"github.com/gruntwork-io/terragrunt/internal/filter\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureHCLFilter = \"fixtures/hcl-filter\"\n)\n\nfunc TestHCLFormatCheckWithFilter(t *testing.T) {\n\tt.Parallel()\n\n\t// Create a temporary directory for this test case\n\thelpers.CleanupTerraformFolder(t, testFixtureHCLFilter)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHCLFilter)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHCLFilter, \"fmt\")\n\trootPath, err := filepath.EvalSymlinks(rootPath)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\terrorAs     error\n\t\tname        string\n\t\tfilterArgs  []string\n\t\texpectError bool\n\t}{\n\t\t// Path-based filtering\n\t\t{\n\t\t\tname:        \"path-based: recursive in needs-formatting\",\n\t\t\tfilterArgs:  []string{\"./needs-formatting/**\"},\n\t\t\texpectError: true,\n\t\t\terrorAs: format.FileNeedsFormattingError{\n\t\t\t\tPath: filepath.Join(rootPath, \"needs-formatting/nested/deep/web/terragrunt.hcl\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"path-based: specific directory with hcl file\",\n\t\t\tfilterArgs: []string{\"./already-formatted/app1/**\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"path-based: nested recursive\",\n\t\t\tfilterArgs:  []string{\"./needs-formatting/nested/deep/**\"},\n\t\t\texpectError: true,\n\t\t\terrorAs: format.FileNeedsFormattingError{\n\t\t\t\tPath: filepath.Join(rootPath, \"needs-formatting/nested/deep/web/terragrunt.hcl\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"path-based: wrapped path\",\n\t\t\tfilterArgs: []string{\"{./already-formatted/app2/**}\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"path-based: stack files with .stack.hcl extension\",\n\t\t\tfilterArgs:  []string{\"./stacks/**\"},\n\t\t\texpectError: true,\n\t\t\terrorAs: format.FileNeedsFormattingError{\n\t\t\t\tPath: filepath.Join(rootPath, \"stacks/needs-formatting/stack1/terragrunt.stack.hcl\"),\n\t\t\t},\n\t\t},\n\n\t\t// Negation with path filters\n\t\t{\n\t\t\tname:        \"negation: exclude path '!./needs-formatting/**'\",\n\t\t\tfilterArgs:  []string{\"!./needs-formatting/**\"},\n\t\t\texpectError: true,\n\t\t\terrorAs: format.FileNeedsFormattingError{\n\t\t\t\tPath: filepath.Join(rootPath, \"stacks/needs-formatting/stack1/terragrunt.stack.hcl\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"negation: exclude stack files by name\",\n\t\t\tfilterArgs:  []string{\"!./**stack.hcl\"},\n\t\t\texpectError: true,\n\t\t\terrorAs: format.FileNeedsFormattingError{\n\t\t\t\tPath: filepath.Join(rootPath, \"needs-formatting/nested/deep/web/terragrunt.hcl\"),\n\t\t\t},\n\t\t},\n\n\t\t// Intersection (refinement)\n\t\t{\n\t\t\tname:        \"intersection: path AND filename\",\n\t\t\tfilterArgs:  []string{\"./needs-formatting/** | ./**/terragrunt.hcl\"},\n\t\t\texpectError: true,\n\t\t\terrorAs: format.FileNeedsFormattingError{\n\t\t\t\tPath: filepath.Join(rootPath, \"needs-formatting/nested/deep/web/terragrunt.hcl\"),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"intersection: path AND negated filename\",\n\t\t\tfilterArgs: []string{\"./stacks/** | !./**/stack1/*\"},\n\t\t},\n\n\t\t// Union (multiple filters)\n\t\t{\n\t\t\tname:        \"union: multiple paths\",\n\t\t\tfilterArgs:  []string{\"./needs-formatting/db/**\", \"./already-formatted/app1/**\"},\n\t\t\texpectError: true,\n\t\t\terrorAs: format.FileNeedsFormattingError{\n\t\t\t\tPath: filepath.Join(rootPath, \"needs-formatting/db/terragrunt.hcl\"),\n\t\t\t},\n\t\t},\n\n\t\t// Attribute filters\n\t\t{\n\t\t\tname:        \"error: name=*.stack.hcl requires HCL parsing\",\n\t\t\tfilterArgs:  []string{\"name=*.stack.hcl\"},\n\t\t\texpectError: true,\n\t\t\terrorAs:     filter.FilterQueryRequiresDiscoveryError{Query: \"name=*.stack.hcl\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"error: type=unit requires HCL parsing\",\n\t\t\tfilterArgs:  []string{\"type=unit\"},\n\t\t\texpectError: true,\n\t\t\terrorAs:     filter.FilterQueryRequiresDiscoveryError{Query: \"type=unit\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"error: type=stack requires HCL parsing\",\n\t\t\tfilterArgs:  []string{\"type=stack\"},\n\t\t\texpectError: true,\n\t\t\terrorAs:     filter.FilterQueryRequiresDiscoveryError{Query: \"type=stack\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"error: external=true requires HCL parsing\",\n\t\t\tfilterArgs:  []string{\"external=true\"},\n\t\t\texpectError: true,\n\t\t\terrorAs:     filter.FilterQueryRequiresDiscoveryError{Query: \"external=true\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"error: intersection with type filter\",\n\t\t\tfilterArgs:  []string{\"./needs-formatting/** | type=unit\"},\n\t\t\texpectError: true,\n\t\t\terrorAs:     filter.FilterQueryRequiresDiscoveryError{Query: \"./needs-formatting/** | type=unit\"},\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.Parallel()\n\n\t\t\t// Build filter arguments\n\t\t\tfilterStr := \"\"\n\n\t\t\tvar filterStrSb152 strings.Builder\n\n\t\t\tfor _, filter := range tc.filterArgs {\n\t\t\t\tfilterStrSb152.WriteString(fmt.Sprintf(\" --filter '%s'\", filter))\n\t\t\t}\n\n\t\t\tfilterStr += filterStrSb152.String()\n\n\t\t\tcmd := fmt.Sprintf(\n\t\t\t\t\"terragrunt hcl fmt %s --check --working-dir %s\",\n\t\t\t\tfilterStr,\n\t\t\t\trootPath,\n\t\t\t)\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectError {\n\t\t\t\t// Command should fail with expected error\n\t\t\t\trequire.Error(t, err, \"Expected command to fail but it succeeded\")\n\n\t\t\t\trequire.ErrorContains(t, err, tc.errorAs.Error())\n\t\t\t} else {\n\t\t\t\t// Command should succeed\n\t\t\t\trequire.NoError(\n\t\t\t\t\tt,\n\t\t\t\t\terr,\n\t\t\t\t\t\"Expected command to succeed but got error: %v\\nstdout: %s\\nstderr: %s\",\n\t\t\t\t\terr, stdout, stderr,\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHCLValidateWithFilter(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname         string\n\t\tfilterArgs   []string\n\t\texpectErrors bool\n\t}{\n\t\t// Path-based filtering\n\t\t{\n\t\t\tname:       \"path-based: valid files only\",\n\t\t\tfilterArgs: []string{\"./valid**\"},\n\t\t},\n\t\t{\n\t\t\tname:       \"path-based: nested recursive valid\",\n\t\t\tfilterArgs: []string{\"./valid/nested/deep/**\"},\n\t\t},\n\n\t\t// Attribute-based filtering\n\t\t{\n\t\t\tname:         \"attribute-based: type=unit\",\n\t\t\tfilterArgs:   []string{\"type=unit\"},\n\t\t\texpectErrors: true, // includes all units, including syntax-error and semantic-error\n\t\t},\n\t\t{\n\t\t\tname:         \"attribute-based: type=stack\",\n\t\t\tfilterArgs:   []string{\"type=stack\"},\n\t\t\texpectErrors: true, // includes all stacks, including syntax-error stacks\n\t\t},\n\n\t\t// Negation\n\t\t{\n\t\t\tname:       \"negation: exclude all error directories\",\n\t\t\tfilterArgs: []string{\"!./syntax-error/**\", \"!./semantic-error/**\", \"!type=stack\"},\n\t\t},\n\t\t{\n\t\t\tname:         \"negation: exclude stacks\",\n\t\t\tfilterArgs:   []string{\"!type=stack\"},\n\t\t\texpectErrors: true, // includes all units, including error units\n\t\t},\n\n\t\t// Intersection (refinement)\n\t\t{\n\t\t\tname:       \"intersection: valid AND type=unit\",\n\t\t\tfilterArgs: []string{\"./valid** | type=unit\"},\n\t\t},\n\t\t{\n\t\t\tname:       \"intersection: valid AND NOT db\",\n\t\t\tfilterArgs: []string{\"./valid** | !name=db\"},\n\t\t},\n\t\t{\n\t\t\tname:       \"intersection: stacks/valid only\",\n\t\t\tfilterArgs: []string{\"./**stack** | ./**valid**\"},\n\t\t},\n\n\t\t// Union (multiple filters)\n\t\t{\n\t\t\tname:       \"union: valid files OR valid stacks\",\n\t\t\tfilterArgs: []string{\"./valid**\", \"./stacks/valid/**\"},\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureHCLFilter)\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHCLFilter)\n\t\t\trootPath := filepath.Join(tmpEnvPath, testFixtureHCLFilter, \"validate\")\n\t\t\trootPath, err := filepath.EvalSymlinks(rootPath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Build filter arguments with proper quoting\n\t\t\tfilterStr := \"\"\n\n\t\t\tvar filterStrSb256 strings.Builder\n\n\t\t\tfor _, filter := range tc.filterArgs {\n\t\t\t\tfilterStrSb256.WriteString(fmt.Sprintf(\" --filter '%s'\", filter))\n\t\t\t}\n\n\t\t\tfilterStr += filterStrSb256.String()\n\n\t\t\tcmd := fmt.Sprintf(\"terragrunt hcl validate%s --working-dir %s\", filterStr, rootPath)\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t\tif tc.expectErrors {\n\t\t\t\trequire.Error(t, err, \"Expected validation to find errors for test case: %s\", tc.name)\n\t\t\t\tassert.NotEmpty(t, stdout, \"Expected validation errors in output for test case: %s\", tc.name)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err, \"Expected validation to succeed but got error for test case: %s\\nstdout: %s\\nstderr: %s\", tc.name, stdout, stderr)\n\t\t\t\tassert.Empty(t, stderr, \"Expected no errors but got stderr for test case: %s\\nstderr: %s\", tc.name, stderr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHCLFormatFilterIntegration(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHCLFilter)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHCLFilter)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHCLFilter, \"fmt\")\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt find --json --filter './needs-formatting/**' --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\tassert.Empty(t, stderr)\n\n\ttype foundComponents struct {\n\t\tPath string `json:\"path\"`\n\t\tType string `json:\"type\"`\n\n\t\tContents []byte `json:\"contents,omitempty\"`\n\t}\n\n\tvar components []foundComponents\n\n\terr = json.Unmarshal([]byte(stdout), &components)\n\trequire.NoError(t, err)\n\n\tfor _, component := range components {\n\t\tbasename := \"terragrunt.hcl\"\n\t\tif component.Type == \"stack\" {\n\t\t\tbasename = \"terragrunt.stack.hcl\"\n\t\t}\n\n\t\tfilename := filepath.Join(rootPath, component.Path, basename)\n\t\tcontent, readErr := os.ReadFile(filename)\n\t\trequire.NoError(t, readErr)\n\n\t\tcomponent.Contents = content //nolint:govet\n\t}\n\n\tcheckCmd := \"terragrunt hcl format --filter './needs-formatting/**' --check --working-dir \" + rootPath\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, checkCmd)\n\trequire.Error(t, err, \"Expected --check to find unformatted files\")\n\n\tformatCmd := \"terragrunt hcl format --filter './needs-formatting/**' --working-dir \" + rootPath\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, formatCmd)\n\trequire.NoError(t, err, \"Format command should succeed\")\n\n\tfor _, component := range components {\n\t\tbasename := \"terragrunt.hcl\"\n\t\tif component.Type == \"stack\" {\n\t\t\tbasename = \"terragrunt.stack.hcl\"\n\t\t}\n\n\t\tfilename := filepath.Join(rootPath, component.Path, basename)\n\t\tcontent, err := os.ReadFile(filename)\n\t\trequire.NoError(t, err)\n\t\tassert.NotEqual(t, content, component.Contents)\n\t}\n}\n"
  },
  {
    "path": "test/integration_hooks_test.go",
    "content": "package test_test\n\nimport (\n\t\"bytes\"\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/gruntwork-io/terragrunt/internal/cli/commands/info/print\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureHooksBeforeOnlyPath                                = \"fixtures/hooks/before-only\"\n\ttestFixtureHooksAllPath                                       = \"fixtures/hooks/all\"\n\ttestFixtureHooksAfterOnlyPath                                 = \"fixtures/hooks/after-only\"\n\ttestFixtureHooksBeforeAndAfterPath                            = \"fixtures/hooks/before-and-after\"\n\ttestFixtureHooksBeforeAfterAndErrorMergePath                  = \"fixtures/hooks/before-after-and-error-merge\"\n\ttestFixtureHooksSkipOnErrorPath                               = \"fixtures/hooks/skip-on-error\"\n\ttestFixtureErrorHooksPath                                     = \"fixtures/hooks/error-hooks\"\n\ttestFixtureErrorHooksSourceDownloadFail                       = \"fixtures/hooks/error-hooks-source-download-fail\"\n\ttestFixtureHooksOneArgActionPath                              = \"fixtures/hooks/one-arg-action\"\n\ttestFixtureHooksEmptyStringCommandPath                        = \"fixtures/hooks/bad-arg-action/empty-string-command\"\n\ttestFixtureHooksEmptyCommandListPath                          = \"fixtures/hooks/bad-arg-action/empty-command-list\"\n\ttestFixtureHooksInterpolationsPath                            = \"fixtures/hooks/interpolations\"\n\ttestFixtureHooksInitOnceNoSourceNoBackend                     = \"fixtures/hooks/init-once/no-source-no-backend\"\n\ttestFixtureHooksInitOnceNoSourceWithBackend                   = \"fixtures/hooks/init-once/no-source-with-backend\"\n\ttestFixtureHooksInitOnceWithSourceNoBackend                   = \"fixtures/hooks/init-once/with-source-no-backend\"\n\ttestFixtureHooksInitOnceWithSourceNoBackendSuppressHookStdout = \"fixtures/hooks/init-once/with-source-no-backend-suppress-hook-stdout\"\n\ttestFixtureHooksInitOnceWithSourceWithBackend                 = \"fixtures/hooks/init-once/with-source-with-backend\"\n\ttestFixtureTerragruntHookIfParameter                          = \"fixtures/hooks/if-parameter\"\n\ttestFixtureHooksPathPreservation                              = \"fixtures/hooks/path-preservation\"\n\ttestFixtureHooksExitCodeError                                 = \"fixtures/hooks/exit-code-error\"\n)\n\nfunc TestTerragruntHookIfParameter(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureTerragruntHookIfParameter)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureTerragruntHookIfParameter)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureTerragruntHookIfParameter)\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\n\trequire.NoError(t, err)\n\n\toutput := stdout.String()\n\n\tassert.Contains(t, output, \"running before hook\")\n\tassert.NotContains(t, output, \"skip after hook\")\n}\n\nfunc TestTerragruntBeforeHook(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksBeforeOnlyPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHooksBeforeOnlyPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksBeforeOnlyPath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\t_, exception := os.ReadFile(rootPath + \"/file.out\")\n\n\trequire.NoError(t, exception)\n}\n\nfunc TestTerragruntInitHookNoSourceNoBackend(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksInitOnceNoSourceNoBackend)\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/hooks/init-once\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksInitOnceNoSourceNoBackend)\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\toutput := stdout.String()\n\n\tif err != nil {\n\t\tt.Errorf(\"Did not expect to get error: %s\", err.Error())\n\t}\n\n\tassert.Equal(t, 1, strings.Count(output, \"AFTER_INIT_ONLY_ONCE\"), \"Hooks on init command executed more than once\")\n\t// With source always being \".\" (current directory), init-from-module executes once\n\tassert.Equal(t, 1, strings.Count(output, \"AFTER_INIT_FROM_MODULE_ONLY_ONCE\"), \"Hooks on init-from-module command should execute once\")\n}\n\nfunc TestTerragruntInitHookWithSourceNoBackend(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksInitOnceWithSourceNoBackend)\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/hooks/init-once\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksInitOnceWithSourceNoBackend)\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\terr := helpers.RunTerragruntCommand(\n\t\tt,\n\t\tfmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --working-dir %s --log-level trace\", rootPath),\n\t\t&stdout,\n\t\t&stderr,\n\t)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"apply stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"apply stderr\")\n\n\toutput := stdout.String()\n\n\tif err != nil {\n\t\tt.Errorf(\"Did not expect to get error: %s\", err.Error())\n\t}\n\n\tassert.Equal(t, 1, strings.Count(\n\t\toutput, \"AFTER_INIT_ONLY_ONCE\\n\",\n\t), \"Hooks on init command executed more than once\")\n\n\tassert.Equal(t, 1, strings.Count(\n\t\toutput, \"AFTER_INIT_FROM_MODULE_ONLY_ONCE\\n\",\n\t), \"Hooks on init-from-module command executed more than once\")\n}\n\nfunc TestTerragruntHookRunAllApply(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksAllPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHooksAllPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksAllPath)\n\tbeforeOnlyPath := filepath.Join(rootPath, \"before-only\")\n\tafterOnlyPath := filepath.Join(rootPath, \"after-only\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- apply -auto-approve\")\n\n\t_, beforeErr := os.ReadFile(beforeOnlyPath + \"/file.out\")\n\trequire.NoError(t, beforeErr)\n\n\t_, afterErr := os.ReadFile(afterOnlyPath + \"/file.out\")\n\trequire.NoError(t, afterErr)\n}\n\nfunc TestTerragruntHookApplyAll(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksAllPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHooksAllPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksAllPath)\n\tbeforeOnlyPath := filepath.Join(rootPath, \"before-only\")\n\tafterOnlyPath := filepath.Join(rootPath, \"after-only\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t_, beforeErr := os.ReadFile(beforeOnlyPath + \"/file.out\")\n\trequire.NoError(t, beforeErr)\n\n\t_, afterErr := os.ReadFile(afterOnlyPath + \"/file.out\")\n\trequire.NoError(t, afterErr)\n}\n\nfunc TestTerragruntHookWorkingDir(t *testing.T) {\n\tt.Parallel()\n\n\tfixturePath := \"fixtures/hooks/working_dir\"\n\thelpers.CleanupTerraformFolder(t, fixturePath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, fixturePath)\n\trootPath := filepath.Join(tmpEnvPath, fixturePath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt validate --non-interactive --working-dir \"+rootPath)\n}\n\nfunc TestTerragruntAfterHook(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksAfterOnlyPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHooksAfterOnlyPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksAfterOnlyPath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\t_, exception := os.ReadFile(rootPath + \"/file.out\")\n\n\trequire.NoError(t, exception)\n}\n\nfunc TestTerragruntBeforeAndAfterHook(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksBeforeAndAfterPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHooksBeforeAndAfterPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksBeforeAndAfterPath)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\n\t_, beforeException := os.ReadFile(rootPath + \"/before.out\")\n\t_, afterException := os.ReadFile(rootPath + \"/after.out\")\n\n\toutput := stdout.String()\n\n\tif err != nil {\n\t\tt.Errorf(\"Did not expect to get error: %s\", err.Error())\n\t}\n\n\tassert.Equal(t, 0, strings.Count(output, \"BEFORE_TERRAGRUNT_READ_CONFIG\"), \"terragrunt-read-config before_hook should not be triggered\")\n\tt.Logf(\"output: %s\", output)\n\n\tassert.Equal(t, 1, strings.Count(output, \"AFTER_TERRAGRUNT_READ_CONFIG\"), \"Hooks on terragrunt-read-config command executed more than once\")\n\n\texpectedHookOutput := fmt.Sprintf(\"TF_PATH=%s COMMAND=terragrunt-read-config HOOK_NAME=after_hook_3\", wrappedBinary())\n\tassert.Equal(t, 1, strings.Count(output, expectedHookOutput))\n\n\trequire.NoError(t, beforeException)\n\trequire.NoError(t, afterException)\n}\n\nfunc TestTerragruntSkipOnError(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksSkipOnErrorPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHooksSkipOnErrorPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksSkipOnErrorPath)\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\n\trequire.Error(t, err)\n\n\toutput := stdout.String()\n\n\tassert.Contains(t, output, \"BEFORE_SHOULD_DISPLAY\")\n\tassert.NotContains(t, output, \"BEFORE_NODISPLAY\")\n\n\tassert.Contains(t, output, \"AFTER_SHOULD_DISPLAY\")\n\tassert.NotContains(t, output, \"AFTER_NODISPLAY\")\n\n\tassert.Contains(t, output, \"ERROR_HOOK_EXECUTED\")\n\tassert.NotContains(t, output, \"NOT_MATCHING_ERROR_HOOK\")\n\tassert.Contains(t, output, \"PATTERN_MATCHING_ERROR_HOOK\")\n}\n\nfunc TestTerragruntCatchErrorsInTerraformExecution(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureErrorHooksPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureErrorHooksPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureErrorHooksPath)\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\n\trequire.Error(t, err)\n\n\toutput := stderr.String()\n\n\tassert.Contains(t, output, \"pattern_matching_hook\")\n\tassert.Contains(t, output, \"catch_all_matching_hook\")\n\tassert.NotContains(t, output, \"not_matching_hook\")\n}\n\nfunc TestTerragruntCatchErrorsFromStdout(t *testing.T) {\n\tt.Parallel()\n\n\tif helpers.IsTerragruntProviderCacheEnabled(t) {\n\t\tt.Skip()\n\t}\n\n\thelpers.CleanupTerraformFolder(t, testFixtureErrorHooksPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureErrorHooksPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureErrorHooksPath)\n\ttfPath := filepath.Join(rootPath, \"tf.sh\")\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath+\" --tf-path \"+tfPath, &stdout, &stderr)\n\n\trequire.Error(t, err)\n\n\toutput := stderr.String()\n\n\tassert.Contains(t, output, \"pattern_matching_hook\")\n\tassert.Contains(t, output, \"catch_all_matching_hook\")\n\tassert.NotContains(t, output, \"not_matching_hook\")\n}\n\nfunc TestTerragruntErrorHookTriggeredOnSourceDownloadFail(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureErrorHooksSourceDownloadFail)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureErrorHooksSourceDownloadFail)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureErrorHooksSourceDownloadFail)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --non-interactive --working-dir \"+rootPath+\n\t\t\t\" -- apply -auto-approve\",\n\t)\n\trequire.Error(t, err)\n\n\t// Hook output goes to stdout, check both stdout and stderr\n\toutput := stdout + stderr\n\t// Verify error hook for init-from-module is triggered when source download fails\n\tassert.Contains(t, output, \"ERROR_HOOK_TRIGGERED_ON_INIT_FROM_MODULE\",\n\t\t\"Error hook for 'init-from-module' should be triggered when source download fails\")\n}\n\nfunc TestTerragruntBeforeOneArgAction(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksOneArgActionPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHooksOneArgActionPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksOneArgActionPath)\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\terr := helpers.RunTerragruntCommand(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --working-dir %s --log-level trace\", rootPath), &stdout, &stderr)\n\toutput := stderr.String()\n\n\tif err != nil {\n\t\tt.Error(\"Expected successful execution of terragrunt with 1 before hook execution.\")\n\t} else {\n\t\tassert.Contains(t, output, \"Running command: date\")\n\t}\n}\n\nfunc TestTerragruntEmptyStringCommandHook(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksEmptyStringCommandPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHooksEmptyStringCommandPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksEmptyStringCommandPath)\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\tif err != nil {\n\t\tassert.Contains(t, err.Error(), \"Need at least one non-empty argument in 'execute'.\")\n\t} else {\n\t\tt.Error(\"Expected an Error with message: 'Need at least one argument'\")\n\t}\n}\n\nfunc TestTerragruntEmptyCommandListHook(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksEmptyCommandListPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHooksEmptyCommandListPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksEmptyCommandListPath)\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\tif err != nil {\n\t\tassert.Contains(t, err.Error(), \"Need at least one non-empty argument in 'execute'.\")\n\t} else {\n\t\tt.Error(\"Expected an Error with message: 'Need at least one argument'\")\n\t}\n}\n\nfunc TestTerragruntHookInterpolation(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksInterpolationsPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHooksInterpolationsPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksInterpolationsPath)\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\toutput := stdout.String()\n\n\thomePath := os.Getenv(\"HOME\")\n\tif homePath == \"\" {\n\t\thomePath = \"HelloWorld\"\n\t}\n\n\tif err != nil {\n\t\tt.Errorf(\"Did not expect to get error: %s\", err.Error())\n\t}\n\n\tassert.Contains(t, output, homePath)\n}\n\nfunc TestTerragruntInfo(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksInitOnceWithSourceNoBackendSuppressHookStdout)\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/hooks/init-once\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksInitOnceWithSourceNoBackendSuppressHookStdout)\n\n\tshowStdout := bytes.Buffer{}\n\tshowStderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt info print --non-interactive --working-dir \"+rootPath, &showStdout, &showStderr)\n\trequire.NoError(t, err)\n\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\n\tvar dat print.InfoOutput\n\n\terrUnmarshal := json.Unmarshal(showStdout.Bytes(), &dat)\n\trequire.NoError(t, errUnmarshal)\n\n\tassert.Equal(t, fmt.Sprintf(\"%s/%s\", rootPath, helpers.TerragruntCache), dat.DownloadDir)\n\tassert.Equal(t, wrappedBinary(), dat.TerraformBinary)\n\tassert.Empty(t, dat.IAMRole)\n}\n\nfunc TestTerragruntHookPreservesAbsolutePaths(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksPathPreservation)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHooksPathPreservation)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksPathPreservation)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath,\n\t)\n\n\trequire.Error(t, err)\n\n\t// The absolute path should be preserved exactly as the hook output it\n\t// NOT converted to a relative path like \"../../../.terraform.d/plugin-cache\"\n\tassert.Contains(t, stderr, \"/home/testuser/.terraform.d/plugin-cache\")\n\tassert.NotContains(t, stderr, \"../\")\n}\n\nfunc TestTerragruntHookExitCodeError(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksExitCodeError)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHooksExitCodeError)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksExitCodeError)\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\trequire.Error(t, err)\n\n\toutput := stderr.String()\n\t// Error message should show exit code and the actual hook output\n\tassert.Contains(t, output, `exited with non-zero exit code 2`)\n\tassert.Contains(t, output, \"lint warning: something is wrong\")\n}\n"
  },
  {
    "path": "test/integration_include_test.go",
    "content": "package test_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\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/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tincludeDeepFixturePath       = \"fixtures/include-deep/\"\n\tincludeDeepFixtureChildPath  = \"child\"\n\tincludeFixturePath           = \"fixtures/include/\"\n\tincludeShallowFixturePath    = \"stage/my-app\"\n\tincludeNoMergeFixturePath    = \"qa/my-app\"\n\tincludeExposeFixturePath     = \"fixtures/include-expose/\"\n\tincludeChildFixturePath      = \"child\"\n\tincludeMultipleFixturePath   = \"fixtures/include-multiple/\"\n\tincludeRunAllFixturePath     = \"fixtures/include-runall/\"\n\trootTerragruntHCLFixturePath = \"fixtures/root-terragrunt-hcl-regression/\"\n)\n\nfunc TestTerragruntWorksWithIncludeLocals(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, includeExposeFixturePath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, includeExposeFixturePath)\n\ttmpEnvPath = filepath.Join(tmpEnvPath, includeExposeFixturePath)\n\n\tfiles, err := os.ReadDir(tmpEnvPath)\n\trequire.NoError(t, err)\n\n\ttestCases := []string{}\n\n\tfor _, finfo := range files {\n\t\tif finfo.IsDir() {\n\t\t\ttestCases = append(testCases, finfo.Name())\n\t\t}\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(filepath.Base(tc), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tchildPath := filepath.Join(tmpEnvPath, tc, includeChildFixturePath)\n\t\t\thelpers.CleanupTerraformFolder(t, childPath)\n\t\t\thelpers.RunTerragrunt(t, \"terragrunt run --all --queue-include-external --non-interactive --working-dir \"+childPath+\" -- apply -auto-approve\")\n\n\t\t\tstdout := bytes.Buffer{}\n\t\t\tstderr := bytes.Buffer{}\n\t\t\terr := helpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+childPath, &stdout, &stderr)\n\t\t\trequire.NoError(t, err)\n\n\t\t\toutputs := map[string]helpers.TerraformOutput{}\n\t\t\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\t\t\tassert.Equal(t, \"us-west-1-test\", outputs[\"region\"].Value.(string))\n\t\t})\n\t}\n}\nfunc TestTerragruntWorksWithIncludeLocalsWithFilter(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, includeExposeFixturePath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, includeExposeFixturePath)\n\ttmpEnvPath = filepath.Join(tmpEnvPath, includeExposeFixturePath)\n\n\tfiles, err := os.ReadDir(tmpEnvPath)\n\trequire.NoError(t, err)\n\n\ttestCases := []string{}\n\n\tfor _, finfo := range files {\n\t\tif finfo.IsDir() {\n\t\t\ttestCases = append(testCases, finfo.Name())\n\t\t}\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(filepath.Base(tc), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tchildPath := filepath.Join(tmpEnvPath, tc, includeChildFixturePath)\n\t\t\thelpers.CleanupTerraformFolder(t, childPath)\n\t\t\thelpers.RunTerragrunt(t, \"terragrunt run --all --filter '{./**}...' --non-interactive --working-dir \"+childPath+\" -- apply -auto-approve\")\n\n\t\t\tstdout := bytes.Buffer{}\n\t\t\tstderr := bytes.Buffer{}\n\t\t\terr := helpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+childPath, &stdout, &stderr)\n\t\t\trequire.NoError(t, err)\n\n\t\t\toutputs := map[string]helpers.TerraformOutput{}\n\t\t\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\t\t\tassert.Equal(t, \"us-west-1-test\", outputs[\"region\"].Value.(string))\n\t\t})\n\t}\n}\n\nfunc TestTerragruntFilterReadingRestrictsSet(t *testing.T) {\n\tt.Parallel()\n\n\trootPath := helpers.CopyEnvironment(t, includeRunAllFixturePath)\n\tmodulePath := filepath.Join(rootPath, includeRunAllFixturePath)\n\thelpers.CleanupTerraformFolder(t, modulePath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all plan --non-interactive --working-dir \"+modulePath+\" --filter 'reading=alpha.hcl'\",\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"alpha\")\n\tassert.NotContains(t, stdout, \"beta\")\n\tassert.NotContains(t, stdout, \"charlie\")\n}\n\nfunc TestTerragruntRunAllModulesWithPrefix(t *testing.T) {\n\tt.Parallel()\n\n\trootPath := helpers.CopyEnvironment(t, includeRunAllFixturePath)\n\tmodulePath := filepath.Join(rootPath, includeRunAllFixturePath)\n\thelpers.CleanupTerraformFolder(t, modulePath)\n\n\t// Retry to handle intermittent failures due to network issues on CICD\n\trequire.NoError(t, util.DoWithRetry(t.Context(), \"Run all modules with prefix verification\", 3, 0, logger.CreateLogger(), log.DebugLevel, func(ctx context.Context) error {\n\t\thelpers.CleanupTerraformFolder(t, modulePath)\n\n\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutputWithContext(\n\t\t\tt,\n\t\t\tctx,\n\t\t\t\"terragrunt run --all plan --non-interactive --tf-forward-stdout --working-dir \"+modulePath,\n\t\t)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"command failed: %w\", err)\n\t\t}\n\n\t\t// Check if all expected outputs are present\n\t\thasAlpha := strings.Contains(stdout, \"alpha\")\n\t\thasBeta := strings.Contains(stdout, \"beta\")\n\t\thasCharlie := strings.Contains(stdout, \"charlie\")\n\n\t\tif !hasAlpha || !hasBeta || !hasCharlie {\n\t\t\treturn fmt.Errorf(\"missing outputs: alpha=%v, beta=%v, charlie=%v\", hasAlpha, hasBeta, hasCharlie)\n\t\t}\n\n\t\t// All outputs present, verify prefixes\n\t\tstdoutLines := strings.SplitSeq(stderr, \"\\n\")\n\t\tfor line := range stdoutLines {\n\t\t\tif strings.Contains(line, \"alpha\") && !strings.Contains(line, \"prefix=a\") {\n\t\t\t\treturn fmt.Errorf(\"alpha found but wrong prefix in line: %s\", line)\n\t\t\t}\n\n\t\t\tif strings.Contains(line, \"beta\") && !strings.Contains(line, \"prefix=b\") {\n\t\t\t\treturn fmt.Errorf(\"beta found but wrong prefix in line: %s\", line)\n\t\t\t}\n\n\t\t\tif strings.Contains(line, \"charlie\") && !strings.Contains(line, \"prefix=c\") {\n\t\t\t\treturn fmt.Errorf(\"charlie found but wrong prefix in line: %s\", line)\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}))\n}\n\nfunc TestTerragruntWorksWithIncludeDeepMerge(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, includeDeepFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, includeDeepFixturePath)\n\tchildPath := filepath.Join(rootPath, \"child\")\n\thelpers.CleanupTerraformFolder(t, childPath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+childPath)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+childPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\tassert.Equal(t, \"mock\", outputs[\"attribute\"].Value.(string))\n\tassert.Equal(t, \"new val\", outputs[\"new_attribute\"].Value.(string))\n\tassert.Equal(t, \"old val\", outputs[\"old_attribute\"].Value.(string))\n\tassert.Equal(t, []any{\"hello\", \"mock\"}, outputs[\"list_attr\"].Value.([]any))\n\tassert.Equal(t, map[string]any{\"foo\": \"bar\", \"bar\": \"baz\", \"test\": \"new val\"}, outputs[\"map_attr\"].Value.(map[string]any))\n\n\tassert.Equal(\n\t\tt,\n\t\tmap[string]any{\n\t\t\t\"attribute\":     \"mock\",\n\t\t\t\"new_attribute\": \"new val\",\n\t\t\t\"old_attribute\": \"old val\",\n\t\t\t\"list_attr\":     []any{\"hello\", \"mock\"},\n\t\t\t\"map_attr\": map[string]any{\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t\"bar\": \"baz\",\n\t\t\t},\n\t\t},\n\t\toutputs[\"dep_out\"].Value.(map[string]any),\n\t)\n}\n\nfunc TestTerragruntWorksWithMultipleInclude(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, includeMultipleFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, includeMultipleFixturePath)\n\n\tfiles, err := os.ReadDir(rootPath)\n\trequire.NoError(t, err)\n\n\ttestCases := []string{}\n\n\tfor _, finfo := range files {\n\t\tif finfo.IsDir() && filepath.Base(finfo.Name()) != \"modules\" {\n\t\t\ttestCases = append(testCases, finfo.Name())\n\t\t}\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(filepath.Base(tc), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tchildPath := filepath.Join(rootPath, tc, includeDeepFixtureChildPath)\n\t\t\thelpers.CleanupTerraformFolder(t, childPath)\n\t\t\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+childPath)\n\n\t\t\tstdout := bytes.Buffer{}\n\t\t\tstderr := bytes.Buffer{}\n\t\t\terr := helpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+childPath, &stdout, &stderr)\n\t\t\trequire.NoError(t, err)\n\n\t\t\toutputs := map[string]helpers.TerraformOutput{}\n\t\t\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\t\t\tvalidateMultipleIncludeTestOutput(t, outputs)\n\t\t})\n\t}\n}\n\nfunc TestTerragruntWorksWithRootTerragruntHCL(t *testing.T) {\n\tt.Parallel()\n\n\t// This is a regression test to ensure that users can still have a root terragrunt.hcl at the project root,\n\t// and run Terragrunt from that root, as long as they exclude that directory from the queue.\n\t//\n\t// In this fixture, the child units include the root config via:\n\t//   path = find_in_parent_folders(\"terragrunt.hcl\")\n\t//\n\t// The key requirement is: excluding \".\" should prevent Terragrunt from trying to treat the root directory\n\t// itself as a unit, while still allowing the root terragrunt.hcl to be used for includes.\n\ttmpEnvPath := helpers.CopyEnvironment(t, rootTerragruntHCLFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, rootTerragruntHCLFixturePath)\n\trootPath, err := filepath.EvalSymlinks(rootPath)\n\trequire.NoError(t, err)\n\n\treportFile := filepath.Join(rootPath, helpers.ReportFile)\n\n\tcommand := fmt.Sprintf(\n\t\t\"terragrunt run --all --non-interactive --working-dir %s --queue-exclude-dir=. --report-file %s --report-format json -- plan\",\n\t\trootPath,\n\t\treportFile,\n\t)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, command)\n\trequire.NoError(t, err)\n\n\t// Parse the report file to verify the correct units ran\n\truns, err := report.ParseJSONRunsFromFile(reportFile)\n\trequire.NoError(t, err, \"Should be able to parse JSON report\")\n\n\trunNames := runs.Names()\n\n\t// Verify exactly 3 child units ran (bar, baz, foo)\n\tassert.Len(t, runs, 3, \"Expected exactly 3 units in report. Found: %v\", runNames)\n\n\t// Verify each child unit was discovered and executed successfully\n\tbarRun := runs.FindByName(\"bar\")\n\trequire.NotNil(t, barRun, \"Expected bar unit to be in report. Found: %v\", runNames)\n\tassert.Equal(t, \"succeeded\", barRun.Result)\n\n\tbazRun := runs.FindByName(\"baz\")\n\trequire.NotNil(t, bazRun, \"Expected baz unit to be in report. Found: %v\", runNames)\n\tassert.Equal(t, \"succeeded\", bazRun.Result)\n\n\tfooRun := runs.FindByName(\"foo\")\n\trequire.NotNil(t, fooRun, \"Expected foo unit to be in report. Found: %v\", runNames)\n\tassert.Equal(t, \"succeeded\", fooRun.Result)\n\n\t// Root should not be treated as a runnable unit (the \".\" directory should be excluded)\n\trootRun := runs.FindByName(\".\")\n\tassert.Nil(t, rootRun, \"Root directory should not be in the report as it should be excluded\")\n}\n\nfunc validateMultipleIncludeTestOutput(t *testing.T, outputs map[string]helpers.TerraformOutput) {\n\tt.Helper()\n\n\tassert.Equal(t, \"mock\", outputs[\"attribute\"].Value.(string))\n\tassert.Equal(t, \"new val\", outputs[\"new_attribute\"].Value.(string))\n\tassert.Equal(t, \"old val\", outputs[\"old_attribute\"].Value.(string))\n\tassert.Equal(t, []any{\"hello\", \"mock\", \"foo\"}, outputs[\"list_attr\"].Value.([]any))\n\tassert.Equal(t, map[string]any{\"foo\": \"bar\", \"bar\": \"baz\", \"test\": \"new val\"}, outputs[\"map_attr\"].Value.(map[string]any))\n\n\tassert.Equal(\n\t\tt,\n\t\tmap[string]any{\n\t\t\t\"attribute\":     \"mock\",\n\t\t\t\"new_attribute\": \"new val\",\n\t\t\t\"old_attribute\": \"old val\",\n\t\t\t\"list_attr\":     []any{\"hello\", \"mock\", \"foo\"},\n\t\t\t\"map_attr\": map[string]any{\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t\t\"bar\": \"baz\",\n\t\t\t},\n\t\t},\n\t\toutputs[\"dep_out\"].Value.(map[string]any),\n\t)\n}\n"
  },
  {
    "path": "test/integration_json_test.go",
    "content": "package test_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureRenderJSONMetadata    = \"fixtures/render-json-metadata\"\n\ttestFixtureRenderJSONMockOutputs = \"fixtures/render-json-mock-outputs\"\n\ttestFixtureRenderJSONInputs      = \"fixtures/render-json-inputs\"\n)\n\nfunc TestRenderJsonAttributesMetadata(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRenderJSONMetadata)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttmpDir := filepath.Join(tmpEnvPath, testFixtureRenderJSONMetadata, \"attributes\")\n\n\tterragruntHCL := filepath.Join(tmpEnvPath, testFixtureRenderJSONMetadata, \"attributes\", \"terragrunt.hcl\")\n\n\tvar expectedMetadata = map[string]any{\n\t\t\"found_in_file\": terragruntHCL,\n\t}\n\n\tjsonOut := filepath.Join(tmpDir, \"terragrunt.rendered.json\")\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt render --json -w --with-metadata --non-interactive --working-dir %s  --out %s\", tmpDir, jsonOut))\n\n\tjsonBytes, err := os.ReadFile(jsonOut)\n\trequire.NoError(t, err)\n\n\tvar renderedJSON = map[string]any{}\n\trequire.NoError(t, json.Unmarshal(jsonBytes, &renderedJSON))\n\n\tvar (\n\t\tinputs         = renderedJSON[config.MetadataInputs]\n\t\texpectedInputs = map[string]any{\"name\": map[string]any{\"metadata\": expectedMetadata, \"value\": \"us-east-1-bucket\"}, \"region\": map[string]any{\"metadata\": expectedMetadata, \"value\": \"us-east-1\"}}\n\t)\n\n\tassert.True(t, reflect.DeepEqual(expectedInputs, inputs))\n\n\tvar (\n\t\tlocals         = renderedJSON[config.MetadataLocals]\n\t\texpectedLocals = map[string]any{\"aws_region\": map[string]any{\"metadata\": expectedMetadata, \"value\": \"us-east-1\"}}\n\t)\n\n\tassert.True(t, reflect.DeepEqual(expectedLocals, locals))\n\n\tvar (\n\t\tdownloadDir        = renderedJSON[config.MetadataDownloadDir]\n\t\texpecteDownloadDir = map[string]any{\"metadata\": expectedMetadata, \"value\": \"/tmp\"}\n\t)\n\n\tassert.True(t, reflect.DeepEqual(expecteDownloadDir, downloadDir))\n\n\tvar iamAssumeRoleDuration = renderedJSON[config.MetadataIamAssumeRoleDuration]\n\n\texpectedIamAssumeRoleDuration := map[string]any{\n\t\t\"metadata\": expectedMetadata,\n\t\t\"value\":    float64(666),\n\t}\n\tassert.True(t, reflect.DeepEqual(expectedIamAssumeRoleDuration, iamAssumeRoleDuration))\n\n\tvar iamAssumeRoleName = renderedJSON[config.MetadataIamAssumeRoleSessionName]\n\n\texpectedIamAssumeRoleName := map[string]any{\n\t\t\"metadata\": expectedMetadata,\n\t\t\"value\":    \"qwe\",\n\t}\n\tassert.True(t, reflect.DeepEqual(expectedIamAssumeRoleName, iamAssumeRoleName))\n\n\tvar iamRole = renderedJSON[config.MetadataIamRole]\n\n\texpectedIamRole := map[string]any{\n\t\t\"metadata\": expectedMetadata,\n\t\t\"value\":    \"arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME\",\n\t}\n\tassert.True(t, reflect.DeepEqual(expectedIamRole, iamRole))\n\n\tvar preventDestroy = renderedJSON[config.MetadataPreventDestroy]\n\n\texpectedPreventDestroy := map[string]any{\n\t\t\"metadata\": expectedMetadata,\n\t\t\"value\":    true,\n\t}\n\tassert.True(t, reflect.DeepEqual(expectedPreventDestroy, preventDestroy))\n\n\tvar terraformBinary = renderedJSON[config.MetadataTerraformBinary]\n\n\texpectedTerraformBinary := map[string]any{\n\t\t\"metadata\": expectedMetadata,\n\t\t\"value\":    wrappedBinary(),\n\t}\n\tassert.True(t, reflect.DeepEqual(expectedTerraformBinary, terraformBinary), \"expected: %v, got: %v\", expectedTerraformBinary, terraformBinary)\n\n\tvar terraformVersionConstraint = renderedJSON[config.MetadataTerraformVersionConstraint]\n\n\texpectedTerraformVersionConstraint := map[string]any{\n\t\t\"metadata\": expectedMetadata,\n\t\t\"value\":    \">= 0.11\",\n\t}\n\tassert.True(t, reflect.DeepEqual(expectedTerraformVersionConstraint, terraformVersionConstraint))\n}\n\nfunc TestRenderJsonWithInputsNotExistingOutput(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRenderJSONInputs)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\tdependencyPath := filepath.Join(tmpEnvPath, testFixtureRenderJSONInputs, \"dependency\")\n\tappPath := filepath.Join(tmpEnvPath, testFixtureRenderJSONInputs, \"app\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+dependencyPath)\n\thelpers.RunTerragrunt(t, \"terragrunt render --json -w --with-metadata --non-interactive --working-dir \"+appPath)\n\n\tjsonOut := filepath.Join(appPath, \"terragrunt.rendered.json\")\n\n\tjsonBytes, err := os.ReadFile(jsonOut)\n\trequire.NoError(t, err)\n\n\tvar renderedJSON = map[string]any{}\n\trequire.NoError(t, json.Unmarshal(jsonBytes, &renderedJSON))\n\n\tvar includeMetadata = map[string]any{\n\t\t\"found_in_file\": filepath.Join(appPath, \"terragrunt.hcl\"),\n\t}\n\n\tvar (\n\t\tinputs         = renderedJSON[config.MetadataInputs]\n\t\texpectedInputs = map[string]any{\"static_value\": map[string]any{\"metadata\": includeMetadata, \"value\": \"static_value\"}, \"value\": map[string]any{\"metadata\": includeMetadata, \"value\": \"output_value\"}, \"not_existing_value\": map[string]any{\"metadata\": includeMetadata, \"value\": \"\"}}\n\t)\n\n\tassert.True(t, reflect.DeepEqual(expectedInputs, inputs))\n}\n\nfunc TestRenderJsonWithMockOutputs(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRenderJSONMockOutputs)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttmpDir := filepath.Join(tmpEnvPath, testFixtureRenderJSONMockOutputs, \"app\")\n\n\tvar expectedMetadata = map[string]any{\n\t\t\"found_in_file\": filepath.Join(tmpDir, \"terragrunt.hcl\"),\n\t}\n\n\tjsonOut := filepath.Join(tmpDir, \"terragrunt.rendered.json\")\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt render --json -w --with-metadata --non-interactive --working-dir %s  --out %s\", tmpDir, jsonOut))\n\n\tjsonBytes, err := os.ReadFile(jsonOut)\n\trequire.NoError(t, err)\n\n\tvar renderedJSON = map[string]any{}\n\trequire.NoError(t, json.Unmarshal(jsonBytes, &renderedJSON))\n\n\tdependency := renderedJSON[config.MetadataDependency]\n\n\tvar expectedDependency = map[string]any{\n\t\t\"module\": map[string]any{\n\t\t\t\"metadata\": expectedMetadata,\n\t\t\t\"value\": map[string]any{\n\t\t\t\t\"config_path\": \"../dependency\",\n\t\t\t\t\"enabled\":     nil,\n\t\t\t\t\"mock_outputs\": map[string]any{\n\t\t\t\t\t\"bastion_host_security_group_id\": \"123\",\n\t\t\t\t\t\"security_group_id\":              \"sg-abcd1234\",\n\t\t\t\t},\n\t\t\t\t\"mock_outputs_allowed_terraform_commands\": [1]string{\"validate\"},\n\t\t\t\t\"mock_outputs_merge_strategy_with_state\":  nil,\n\t\t\t\t\"mock_outputs_merge_with_state\":           nil,\n\t\t\t\t\"name\":                                    \"module\",\n\t\t\t\t\"outputs\":                                 nil,\n\t\t\t\t\"inputs\":                                  nil,\n\t\t\t\t\"skip\":                                    nil,\n\t\t\t},\n\t\t},\n\t}\n\n\tserializedDependency, err := json.Marshal(dependency)\n\trequire.NoError(t, err)\n\n\tserializedExpectedDependency, err := json.Marshal(expectedDependency)\n\trequire.NoError(t, err)\n\tassert.Equal(t, string(serializedExpectedDependency), string(serializedDependency))\n}\n\nfunc TestRenderJsonMetadataIncludes(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRenderJSONMetadata)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttmpDir := filepath.Join(tmpEnvPath, testFixtureRenderJSONMetadata, \"includes\", \"app\")\n\n\tterragruntHcl := filepath.Join(tmpEnvPath, testFixtureRenderJSONMetadata, \"includes\", \"app\", \"terragrunt.hcl\")\n\tlocalsHcl := filepath.Join(tmpEnvPath, testFixtureRenderJSONMetadata, \"includes\", \"app\", \"locals.hcl\")\n\tinputHcl := filepath.Join(tmpEnvPath, testFixtureRenderJSONMetadata, \"includes\", \"app\", \"inputs.hcl\")\n\tgenerateHcl := filepath.Join(tmpEnvPath, testFixtureRenderJSONMetadata, \"includes\", \"app\", \"generate.hcl\")\n\tcommonHcl := filepath.Join(tmpEnvPath, testFixtureRenderJSONMetadata, \"includes\", \"common\", \"common.hcl\")\n\n\tvar (\n\t\tterragruntMetadata = map[string]any{\"found_in_file\": terragruntHcl}\n\t\tlocalsMetadata     = map[string]any{\"found_in_file\": localsHcl}\n\t\tinputMetadata      = map[string]any{\"found_in_file\": inputHcl}\n\t\tgenerateMetadata   = map[string]any{\"found_in_file\": generateHcl}\n\t\tcommonMetadata     = map[string]any{\"found_in_file\": commonHcl}\n\t)\n\n\tjsonOut := filepath.Join(tmpDir, \"terragrunt.rendered.json\")\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt render --json -w --with-metadata --non-interactive --working-dir %s  --out %s\", tmpDir, jsonOut))\n\n\tjsonBytes, err := os.ReadFile(jsonOut)\n\trequire.NoError(t, err)\n\n\tvar renderedJSON = map[string]any{}\n\trequire.NoError(t, json.Unmarshal(jsonBytes, &renderedJSON))\n\n\tvar (\n\t\tinputs         = renderedJSON[config.MetadataInputs]\n\t\texpectedInputs = map[string]any{\"content\": map[string]any{\"metadata\": localsMetadata, \"value\": \"test\"}, \"qwe\": map[string]any{\"metadata\": inputMetadata, \"value\": \"123\"}}\n\t)\n\n\tassert.True(t, reflect.DeepEqual(expectedInputs, inputs))\n\n\tvar (\n\t\tlocals         = renderedJSON[config.MetadataLocals]\n\t\texpectedLocals = map[string]any{\"abc\": map[string]any{\"metadata\": terragruntMetadata, \"value\": \"xyz\"}}\n\t)\n\n\tassert.True(t, reflect.DeepEqual(expectedLocals, locals))\n\n\tvar (\n\t\tgenerate         = renderedJSON[config.MetadataGenerateConfigs]\n\t\texpectedGenerate = map[string]any{\"provider\": map[string]any{\"metadata\": generateMetadata, \"value\": map[string]any{\"comment_prefix\": \"# \", \"contents\": \"# test\\n\", \"disable_signature\": false, \"disable\": false, \"if_exists\": \"overwrite\", \"if_disabled\": \"skip\", \"hcl_fmt\": nil, \"path\": \"provider.tf\"}}}\n\t)\n\n\t// compare fields by serialization in json since map from \"value\" field is not deterministic\n\tserializedGenerate, err := json.Marshal(generate)\n\trequire.NoError(t, err)\n\n\tserializedExpectedGenerate, err := json.Marshal(expectedGenerate)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, string(serializedExpectedGenerate), string(serializedGenerate))\n\n\tvar (\n\t\tremoteState         = renderedJSON[config.MetadataRemoteState]\n\t\texpectedRemoteState = map[string]any{\"metadata\": commonMetadata, \"value\": map[string]any{\"backend\": \"s3\", \"disable_dependency_optimization\": false, \"disable_init\": false, \"generate\": nil, \"config\": map[string]any{\"bucket\": \"mybucket\", \"key\": \"path/to/my/key\", \"region\": \"us-east-1\"}, \"encryption\": nil}}\n\t)\n\n\t// compare fields by serialization in json since map from \"value\" field is not deterministic\n\tserializedRemoteState, err := json.Marshal(remoteState)\n\trequire.NoError(t, err)\n\n\tserializedExpectedRemoteState, err := json.Marshal(expectedRemoteState)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, string(serializedExpectedRemoteState), string(serializedRemoteState))\n}\n\nfunc TestRenderJsonMetadataDependency(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRenderJSONMetadata)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttmpDir := filepath.Join(tmpEnvPath, testFixtureRenderJSONMetadata, \"dependency\", \"app\")\n\n\tterragruntHcl := filepath.Join(tmpEnvPath, testFixtureRenderJSONMetadata, \"dependency\", \"app\", \"terragrunt.hcl\")\n\n\tvar terragruntMetadata = map[string]any{\n\t\t\"found_in_file\": terragruntHcl,\n\t}\n\n\tjsonOut := filepath.Join(tmpDir, \"terragrunt.rendered.json\")\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt render --json -w --with-metadata --non-interactive --working-dir %s  --out %s\", tmpDir, jsonOut))\n\n\tjsonBytes, err := os.ReadFile(jsonOut)\n\trequire.NoError(t, err)\n\n\tvar renderedJSON = map[string]any{}\n\trequire.NoError(t, json.Unmarshal(jsonBytes, &renderedJSON))\n\n\tvar dependency = renderedJSON[config.MetadataDependency]\n\n\tvar expectedDependency = map[string]any{\n\t\t\"dep\": map[string]any{\n\t\t\t\"metadata\": terragruntMetadata,\n\t\t\t\"value\": map[string]any{\n\t\t\t\t\"config_path\": \"../dependency\",\n\t\t\t\t\"mock_outputs\": map[string]any{\n\t\t\t\t\t\"test\": \"value\",\n\t\t\t\t},\n\t\t\t\t\"mock_outputs_allowed_terraform_commands\": nil,\n\t\t\t\t\"mock_outputs_merge_strategy_with_state\":  nil,\n\t\t\t\t\"mock_outputs_merge_with_state\":           nil,\n\t\t\t\t\"name\":                                    \"dep\",\n\t\t\t\t\"outputs\":                                 nil,\n\t\t\t\t\"inputs\":                                  nil,\n\t\t\t\t\"skip\":                                    nil,\n\t\t\t\t\"enabled\":                                 nil,\n\t\t\t},\n\t\t},\n\t\t\"dep2\": map[string]any{\n\t\t\t\"metadata\": terragruntMetadata,\n\t\t\t\"value\": map[string]any{\n\t\t\t\t\"config_path\": \"../dependency2\",\n\t\t\t\t\"enabled\":     nil,\n\t\t\t\t\"mock_outputs\": map[string]any{\n\t\t\t\t\t\"test2\": \"value2\",\n\t\t\t\t},\n\t\t\t\t\"mock_outputs_allowed_terraform_commands\": nil,\n\t\t\t\t\"mock_outputs_merge_strategy_with_state\":  nil,\n\t\t\t\t\"mock_outputs_merge_with_state\":           nil,\n\t\t\t\t\"name\":                                    \"dep2\",\n\t\t\t\t\"outputs\":                                 nil,\n\t\t\t\t\"inputs\":                                  nil,\n\t\t\t\t\"skip\":                                    nil,\n\t\t\t},\n\t\t},\n\t}\n\n\t// compare fields by serialization in json since map from \"value\" field is not deterministic\n\tserializedDependency, err := json.Marshal(dependency)\n\trequire.NoError(t, err)\n\n\tserializedExpectedDependency, err := json.Marshal(expectedDependency)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, string(serializedExpectedDependency), string(serializedDependency))\n}\n\nfunc TestRenderJsonMetadataTerraform(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRenderJSONMetadata)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttmpDir := filepath.Join(tmpEnvPath, testFixtureRenderJSONMetadata, \"terraform-remote-state\", \"app\")\n\n\tcommonHcl := filepath.Join(tmpEnvPath, testFixtureRenderJSONMetadata, \"terraform-remote-state\", \"common\", \"terraform.hcl\")\n\tremoteStateHcl := filepath.Join(tmpEnvPath, testFixtureRenderJSONMetadata, \"terraform-remote-state\", \"common\", \"remote_state.hcl\")\n\n\tvar (\n\t\tterragruntMetadata = map[string]any{\"found_in_file\": commonHcl}\n\t\tremoteMetadata     = map[string]any{\"found_in_file\": remoteStateHcl}\n\t)\n\n\tjsonOut := filepath.Join(tmpDir, \"terragrunt.rendered.json\")\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt render --json -w --with-metadata --non-interactive --working-dir %s  --out %s\", tmpDir, jsonOut))\n\n\tjsonBytes, err := os.ReadFile(jsonOut)\n\trequire.NoError(t, err)\n\n\tvar renderedJSON = map[string]any{}\n\trequire.NoError(t, json.Unmarshal(jsonBytes, &renderedJSON))\n\n\tvar (\n\t\tterraform         = renderedJSON[config.MetadataTerraform]\n\t\texpectedTerraform = map[string]any{\"metadata\": terragruntMetadata, \"value\": map[string]any{\"after_hook\": map[string]any{}, \"before_hook\": map[string]any{}, \"error_hook\": map[string]any{}, \"extra_arguments\": map[string]any{}, \"include_in_copy\": nil, \"exclude_from_copy\": nil, \"source\": \"../terraform\", \"copy_terraform_lock_file\": nil}}\n\t)\n\n\t// compare fields by serialization in json since map from \"value\" field is not deterministic\n\tserializedTerraform, err := json.Marshal(terraform)\n\trequire.NoError(t, err)\n\n\tserializedExpectedTerraform, err := json.Marshal(expectedTerraform)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, string(serializedExpectedTerraform), string(serializedTerraform))\n\n\tvar (\n\t\tremoteState         = renderedJSON[config.MetadataRemoteState]\n\t\texpectedRemoteState = map[string]any{\"metadata\": remoteMetadata, \"value\": map[string]any{\"backend\": \"s3\", \"config\": map[string]any{\"bucket\": \"mybucket\", \"key\": \"path/to/my/key\", \"region\": \"us-east-1\"}, \"encryption\": nil, \"disable_dependency_optimization\": false, \"disable_init\": false, \"generate\": nil}}\n\t)\n\n\t// compare fields by serialization in json since map from \"value\" field is not deterministic\n\tserializedRemoteState, err := json.Marshal(remoteState)\n\trequire.NoError(t, err)\n\n\tserializedExpectedRemoteState, err := json.Marshal(expectedRemoteState)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, string(serializedExpectedRemoteState), string(serializedRemoteState))\n}\n\nfunc TestTerragruntRenderJsonHelp(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksInitOnceWithSourceNoBackend)\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/hooks/init-once\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksInitOnceWithSourceNoBackend)\n\n\tshowStdout := bytes.Buffer{}\n\tshowStderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt render --help --non-interactive --working-dir \"+rootPath, &showStdout, &showStderr)\n\trequire.NoError(t, err)\n\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\n\toutput := showStdout.String()\n\n\tassert.Contains(t, output, \"terragrunt render\")\n\tassert.Contains(t, output, \"--with-metadata\")\n}\n"
  },
  {
    "path": "test/integration_list_test.go",
    "content": "package test_test\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureListBasic  = \"fixtures/list/basic\"\n\ttestFixtureListDag    = \"fixtures/list/dag\"\n\ttestFixtureListHidden = \"fixtures/find/hidden\"\n)\n\nfunc TestListCommand(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname                      string\n\t\tworkingDir                string\n\t\texpectedOutput            string\n\t\targs                      []string\n\t\tunnecessaryExperimentFlag bool\n\t}{\n\t\t{\n\t\t\tname:           \"Basic list with default format\",\n\t\t\tworkingDir:     testFixtureListBasic,\n\t\t\targs:           []string{\"list\"},\n\t\t\texpectedOutput: \"a-unit  b-unit  \\n\",\n\t\t},\n\t\t{\n\t\t\tname:       \"List with long format\",\n\t\t\tworkingDir: testFixtureListBasic,\n\t\t\targs:       []string{\"list\", \"--long\"},\n\t\t\texpectedOutput: `Type  Path\nunit  a-unit\nunit  b-unit\n`,\n\t\t},\n\t\t{\n\t\t\tname:       \"List with tree format\",\n\t\t\tworkingDir: testFixtureListBasic,\n\t\t\targs:       []string{\"list\", \"--tree\"},\n\t\t\texpectedOutput: `.\n├── a-unit\n╰── b-unit\n`,\n\t\t},\n\t\t{\n\t\t\tname:           \"Basic list with default format\",\n\t\t\tworkingDir:     testFixtureListBasic,\n\t\t\targs:           []string{\"list\"},\n\t\t\texpectedOutput: \"a-unit  b-unit  \\n\",\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, tc.workingDir)\n\n\t\t\targs := []string{\"terragrunt\", \"--no-color\"}\n\t\t\tif tc.unnecessaryExperimentFlag {\n\t\t\t\targs = append(args, \"--experiment\", \"cli-redesign\")\n\t\t\t}\n\n\t\t\targs = append(args, tc.args...)\n\t\t\targs = append(args, \"--working-dir\", tc.workingDir)\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, strings.Join(args, \" \"))\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif tc.unnecessaryExperimentFlag {\n\t\t\t\trequire.Contains(t, stderr, \"The following experiment(s) are already completed: cli-redesign. Please remove any completed experiments, as setting them no longer does anything. For a list of all ongoing experiments, and the outcomes of previous experiments, see https://docs.terragrunt.com/reference/experiments\")\n\t\t\t} else {\n\t\t\t\trequire.Empty(t, stderr)\n\t\t\t}\n\n\t\t\tassert.Equal(t, tc.expectedOutput, stdout)\n\t\t})\n\t}\n}\n\nfunc TestListCommandWithDependencies(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname       string\n\t\tworkingDir string\n\t\texpected   string\n\t\targs       []string\n\t}{\n\t\t{\n\t\t\tname:       \"List with dependencies in tree format\",\n\t\t\tworkingDir: testFixtureListDag,\n\t\t\targs:       []string{\"list\", \"--tree\", \"--dag\"},\n\t\t\texpected: `.\n├── stacks/live/dev\n├── stacks/live/prod\n├── units/live/dev/vpc\n│   ├── units/live/dev/db\n│   │   ╰── units/live/dev/ec2\n│   ╰── units/live/dev/ec2\n╰── units/live/prod/vpc\n    ├── units/live/prod/db\n    │   ╰── units/live/prod/ec2\n    ╰── units/live/prod/ec2\n`,\n\t\t},\n\t\t{\n\t\t\tname:       \"List with dependencies in long format\",\n\t\t\tworkingDir: testFixtureListDag,\n\t\t\targs:       []string{\"list\", \"--long\", \"--dependencies\"},\n\t\t\texpected: `Type  Path                 Dependencies\nstack stacks/live/dev\nstack stacks/live/prod\nunit  units/live/dev/db    units/live/dev/vpc\nunit  units/live/dev/ec2   units/live/dev/db, units/live/dev/vpc\nunit  units/live/dev/vpc\nunit  units/live/prod/db   units/live/prod/vpc\nunit  units/live/prod/ec2  units/live/prod/db, units/live/prod/vpc\nunit  units/live/prod/vpc\n`,\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, tc.workingDir)\n\n\t\t\targs := make([]string, 0, 2+len(tc.args)+2)\n\t\t\targs = append(args, \"terragrunt\", \"--no-color\")\n\t\t\targs = append(args, tc.args...)\n\t\t\targs = append(args, \"--working-dir\", tc.workingDir)\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, strings.Join(args, \" \"))\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Empty(t, stderr)\n\n\t\t\tassert.Equal(t, tc.expected, stdout)\n\t\t})\n\t}\n}\n\nfunc TestListCommandWithExclude(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname           string\n\t\texpectedOutput string\n\t\targs           []string\n\t}{\n\t\t{\n\t\t\tname:           \"List with queue-construct-as plan\",\n\t\t\targs:           []string{\"list\", \"--queue-construct-as\", \"plan\"},\n\t\t\texpectedOutput: \"unit2  unit3  \\n\",\n\t\t},\n\t\t{\n\t\t\tname:           \"List with queue-construct-as apply\",\n\t\t\targs:           []string{\"list\", \"--queue-construct-as\", \"apply\"},\n\t\t\texpectedOutput: \"unit1  unit3  \\n\",\n\t\t},\n\t\t{\n\t\t\tname: \"List with queue-construct-as plan in long format\",\n\t\t\targs: []string{\"list\", \"--queue-construct-as\", \"plan\", \"--long\"},\n\t\t\texpectedOutput: `Type  Path\nunit  unit2\nunit  unit3\n`,\n\t\t},\n\t\t{\n\t\t\tname: \"List with queue-construct-as apply in tree format\",\n\t\t\targs: []string{\"list\", \"--queue-construct-as\", \"apply\", \"--tree\"},\n\t\t\texpectedOutput: `.\n├── unit1\n╰── unit3\n`,\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureFindExclude)\n\n\t\t\targs := make([]string, 0, 2+len(tc.args)+2)\n\t\t\targs = append(args, \"terragrunt\", \"--no-color\")\n\t\t\targs = append(args, tc.args...)\n\t\t\targs = append(args, \"--working-dir\", testFixtureFindExclude)\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, strings.Join(args, \" \"))\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Empty(t, stderr)\n\t\t\tassert.Equal(t, tc.expectedOutput, stdout)\n\t\t})\n\t}\n}\n\nfunc TestListHidden(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname     string\n\t\texpected string\n\t\tnoHidden bool\n\t}{\n\t\t{\n\t\t\tname:     \"default (includes hidden)\",\n\t\t\texpected: filepath.Join(\".hide\", \"unit\") + \"  stack       unit        \\n\",\n\t\t},\n\t\t{\n\t\t\tname:     \"no-hidden flag excludes hidden\",\n\t\t\tnoHidden: true,\n\t\t\texpected: \"stack  unit   \\n\",\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureListHidden)\n\n\t\t\tcmd := \"terragrunt list --no-color --working-dir \" + testFixtureListHidden\n\n\t\t\tif tc.noHidden {\n\t\t\t\tcmd += \" --no-hidden\"\n\t\t\t}\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Empty(t, stderr)\n\t\t\tassert.Equal(t, tc.expected, stdout)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/integration_local_dev_test.go",
    "content": "package test_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureGetTerragruntSourceHcl = \"fixtures/get-terragrunt-source-hcl\"\n)\n\nfunc TestTerragruntSourceMap(t *testing.T) {\n\tt.Parallel()\n\n\tfixtureSourceMapPath := filepath.Join(\"fixtures\", \"source-map\")\n\thelpers.CleanupTerraformFolder(t, fixtureSourceMapPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, fixtureSourceMapPath)\n\trootPath := filepath.Join(tmpEnvPath, fixtureSourceMapPath)\n\tsourceMapArgs := fmt.Sprintf(\n\t\t\"--source-map %s --source-map %s\",\n\t\t\"git::ssh://git@github.com/gruntwork-io/i-dont-exist.git=\"+tmpEnvPath,\n\t\t\"git::ssh://git@github.com/gruntwork-io/another-dont-exist.git=\"+tmpEnvPath,\n\t)\n\n\ttestCases := []struct {\n\t\tname     string\n\t\tapplyAll bool\n\t}{\n\t\t{\n\t\t\tname:     \"multiple-match\",\n\t\t\tapplyAll: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple-only-one-match\",\n\t\t\tapplyAll: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple-with-dependency\",\n\t\t\tapplyAll: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"multiple-with-dependency-same-url\",\n\t\t\tapplyAll: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"single\",\n\t\t\tapplyAll: false,\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.Parallel()\n\n\t\t\ttgPath := filepath.Join(rootPath, tc.name)\n\n\t\t\taction := \"run\"\n\t\t\tif tc.applyAll {\n\t\t\t\taction = \"run --all\"\n\t\t\t}\n\n\t\t\ttgArgs := fmt.Sprintf(\"terragrunt %s --non-interactive --working-dir %s %s -- apply -auto-approve\", action, tgPath, sourceMapArgs)\n\t\t\thelpers.RunTerragrunt(t, tgArgs)\n\t\t})\n\t}\n}\n\nfunc TestGetTerragruntSourceHCL(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetTerragruntSourceHcl)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetTerragruntSourceHcl)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetTerragruntSourceHcl)\n\tterraformSource := \"\" // get_terragrunt_source_cli_flag() only returns the source when it is passed in via the CLI\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\t// verify expected outputs are not empty\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\tassert.Equal(t, \"HCL: \"+terraformSource, outputs[\"terragrunt_source\"].Value)\n}\n\nfunc TestGetTerragruntSourceCLI(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetTerragruntSourceCli)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetTerragruntSourceCli)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetTerragruntSourceCli)\n\tterraformSource := \"terraform_config_cli\"\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --working-dir %s --source %s\", rootPath, terraformSource))\n\n\t// verify expected outputs are not empty\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, fmt.Sprintf(\"terragrunt output -no-color -json --non-interactive --working-dir %s --source %s\", rootPath, terraformSource), &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\tassert.Equal(t, \"CLI: \"+terraformSource, outputs[\"terragrunt_source\"].Value)\n}\n"
  },
  {
    "path": "test/integration_locals_test.go",
    "content": "package test_test\n\nimport (\n\t\"bytes\"\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/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureLocalsErrorUndefinedLocal         = \"fixtures/locals-errors/undefined-local\"\n\ttestFixtureLocalsErrorUndefinedLocalButInput = \"fixtures/locals-errors/undefined-local-but-input\"\n\ttestFixtureLocalsCanonical                   = \"fixtures/locals/canonical\"\n\ttestFixtureLocalsInInclude                   = \"fixtures/locals/local-in-include\"\n\ttestFixtureLocalRunOnce                      = \"fixtures/locals/run-once\"\n\ttestFixtureLocalRunMultiple                  = \"fixtures/locals/run-multiple\"\n\ttestFixtureLocalsInIncludeChildRelPath       = \"qa/my-app\"\n\ttestFixtureBrokenLocals                      = \"fixtures/broken-locals\"\n)\n\nfunc TestUndefinedLocalsReferenceBreaks(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureLocalsErrorUndefinedLocal)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLocalsErrorUndefinedLocal)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath, os.Stdout, os.Stderr)\n\trequire.Error(t, err)\n}\n\nfunc TestUndefinedLocalsReferenceToInputsBreaks(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureLocalsErrorUndefinedLocalButInput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLocalsErrorUndefinedLocalButInput)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath, os.Stdout, os.Stderr)\n\trequire.Error(t, err)\n}\n\nfunc TestLocalsParsing(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureLocalsCanonical)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLocalsCanonical)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal([]byte(stdout), &outputs))\n\n\tassert.Equal(t, \"Hello world\\n\", outputs[\"data\"].Value)\n\tassert.InEpsilon(t, 42.0, outputs[\"answer\"].Value, 0.0000000001)\n}\n\nfunc TestLocalsInInclude(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureLocalsInInclude)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureLocalsInInclude)\n\tchildPath := filepath.Join(tmpEnvPath, testFixtureLocalsInInclude, testFixtureLocalsInIncludeChildRelPath)\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve -no-color --non-interactive --working-dir \"+childPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+childPath)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal([]byte(stdout), &outputs))\n\n\tassert.Equal(\n\t\tt,\n\t\tfilepath.Join(tmpEnvPath, testFixtureLocalsInInclude),\n\t\toutputs[\"parent_terragrunt_dir\"].Value,\n\t)\n\tassert.Equal(\n\t\tt,\n\t\tchildPath,\n\t\toutputs[\"terragrunt_dir\"].Value,\n\t)\n\tassert.Equal(\n\t\tt,\n\t\t\"apply\",\n\t\toutputs[\"terraform_command\"].Value,\n\t)\n\tassert.Equal(\n\t\tt,\n\t\t\"[\\\"apply\\\",\\\"-auto-approve\\\",\\\"-no-color\\\"]\",\n\t\toutputs[\"terraform_cli_args\"].Value,\n\t)\n}\n\nfunc TestLogFailedLocalsEvaluation(t *testing.T) {\n\tt.Parallel()\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\terr := helpers.RunTerragruntCommand(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --working-dir %s --log-level trace\", testFixtureBrokenLocals), &stdout, &stderr)\n\trequire.Error(t, err)\n\n\toutput := stderr.String()\n\tassert.Contains(t, output, \"Encountered error while evaluating locals in file \"+filepath.FromSlash(\"./terragrunt.hcl\"))\n}\n\nfunc TestTerragruntInitRunCmd(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureLocalRunMultiple)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureLocalRunMultiple)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLocalRunMultiple)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt init --working-dir \"+rootPath)\n\trequire.Error(t, err)\n\n\t// Check for cached values between locals and inputs sections\n\tassert.Equal(t, 1, strings.Count(stdout, \"echo_potato\"))\n\tassert.Equal(t, 1, strings.Count(stdout, \"echo_carrot\"))\n\tassert.Equal(t, 1, strings.Count(stdout, \"echo_bar\"))\n\tassert.Equal(t, 1, strings.Count(stdout, \"echo_foo\"))\n\n\tassert.Equal(t, 1, strings.Count(stdout, \"echo_input_variable\"))\n\n\tassert.Contains(t, stdout, \"echo_uuid_input\")\n\tassert.Contains(t, stdout, \"echo_uuid_locals\")\n\tassert.Contains(t, stdout, \"echo_random_arg\")\n\tassert.Contains(t, stdout, \"echo_another_arg\")\n}\n\nfunc TestTerragruntLocalRunOnce(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureLocalRunOnce)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLocalRunOnce)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt init --working-dir \"+rootPath, &stdout, &stderr)\n\trequire.Error(t, err)\n\n\terrout := stdout.String()\n\n\tassert.Equal(t, 1, strings.Count(errout, \"foo\"))\n}\n"
  },
  {
    "path": "test/integration_parse_test.go",
    "content": "//go:build parse\n\n// These tests consume so much memory that they cause the CI runner to crash.\n// As a result, we have to run them on their own.\n//\n// In the future, we should make improvements to parsing so that this isn't necessary.\n\npackage test_test\n\nimport (\n\t\"context\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/configbridge\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/options\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar knownBadFiles = []string{\n\t\"fixtures/catalog/local-template/.boilerplate/terragrunt.hcl\",\n\t\"fixtures/disabled/unit-disabled/terragrunt.hcl\",\n\t\"fixtures/hcl-filter/validate/semantic-error/incomplete-block/terragrunt.hcl\",\n\t\"fixtures/hcl-filter/validate/semantic-error/missing-value/terragrunt.hcl\",\n\t\"fixtures/hcl-filter/validate/stacks/syntax-error/stack2/terragrunt.stack.hcl\",\n\t\"fixtures/hcl-filter/validate/syntax-error/invalid-char/terragrunt.hcl\",\n\t\"fixtures/hcl-filter/validate/syntax-error/invalid-key/terragrunt.hcl\",\n\t\"fixtures/hclfmt-errors/dangling-attribute/terragrunt.hcl\",\n\t\"fixtures/hclfmt-errors/invalid-character/terragrunt.hcl\",\n\t\"fixtures/hclfmt-errors/invalid-key/terragrunt.hcl\",\n\t\"fixtures/hclvalidate/second/a/terragrunt.hcl\",\n\t\"fixtures/parsing/exposed-include-with-deprecated-inputs/compcommon.hcl\",\n\t\"fixtures/scaffold/with-shell-and-hooks/.boilerplate/terragrunt.hcl\",\n\t\"fixtures/scaffold/with-shell-commands/.boilerplate/terragrunt.hcl\",\n\t\"fixtures/stacks/errors/unknown-value/units/bad-unit/terragrunt.hcl\",\n\t// Files that require AWS credentials (will fail/timeout without them)\n\t\"fixtures/assume-role/external-id-with-comma/terragrunt.hcl\",\n\t\"fixtures/assume-role/external-id/terragrunt.hcl\",\n\t\"fixtures/auth-provider-cmd/creds-for-dependency/dependency/terragrunt.hcl\",\n\t\"fixtures/auth-provider-cmd/oidc/terragrunt.hcl\",\n\t\"fixtures/auth-provider-cmd/remote-state-w-oidc/terragrunt.hcl\",\n\t\"fixtures/auth-provider-cmd/remote-state/terragrunt.hcl\",\n\t\"fixtures/get-aws-account-alias/terragrunt.hcl\",\n\t\"fixtures/get-aws-caller-identity/terragrunt.hcl\",\n\t\"fixtures/read-config/iam_role_in_file/terragrunt.hcl\",\n}\n\nfunc TestParseAllFixtureFiles(t *testing.T) {\n\tt.Parallel()\n\n\tfiles := helpers.HCLFilesInDir(t, \"fixtures\")\n\n\tfor _, file := range files {\n\t\t// Skip files in a .terragrunt-cache directory\n\t\tif strings.Contains(file, \".terragrunt-cache\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tt.Run(file, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t// Skip known bad files early to avoid timeouts (e.g., AWS credential errors)\n\t\t\tif slices.Contains(knownBadFiles, file) {\n\t\t\t\tt.Skip(\"Skipping known bad file\")\n\t\t\t}\n\n\t\t\tdir := filepath.Dir(file)\n\n\t\t\topts, err := options.NewTerragruntOptionsForTest(dir)\n\t\t\trequire.NoError(t, err)\n\n\t\t\topts.Experiments.ExperimentMode()\n\n\t\t\tl := logger.CreateLogger()\n\n\t\t\tctx, pctx := configbridge.NewParsingContext(\n\t\t\t\tcontext.TODO(), // Using context.TODO() instead of t.Context() here because we end up storing way too much in context otherwise.\n\t\t\t\tl,\n\t\t\t\topts,\n\t\t\t)\n\n\t\t\tcfg, _ := config.ParseConfigFile(ctx, pctx, l, file, nil)\n\n\t\t\tassert.NotNil(t, cfg)\n\n\t\t\t// Suggest garbage collection to free up memory.\n\t\t\t// Parsing config files can be memory intensive, and we don't need the config\n\t\t\t// files in memory after we've parsed them.\n\t\t\truntime.GC()\n\t\t})\n\t}\n}\n\nfunc TestParseFindListAllComponents(t *testing.T) {\n\tt.Parallel()\n\n\ttc := []struct {\n\t\tname    string\n\t\tcommand string\n\t}{\n\t\t{name: \"find\", command: \"terragrunt find --no-color\"},\n\t\t{name: \"list\", command: \"terragrunt list --no-color\"},\n\t}\n\n\tfor _, tt := range tc {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\t\t\tt,\n\t\t\t\ttt.command,\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// stderr can be non-empty if there are deprecations\n\t\t\tt.Logf(\"stderr: %s\", stderr)\n\t\t\tassert.NotEmpty(t, stdout)\n\n\t\t\tfields := strings.Fields(stdout)\n\n\t\t\taDepLine := 0\n\t\t\tbDepLine := 0\n\n\t\t\tfor i, field := range fields {\n\t\t\t\tif field == \"fixtures/find/dag/a-dependent\" {\n\t\t\t\t\taDepLine = i\n\t\t\t\t}\n\n\t\t\t\tif field == \"fixtures/find/dag/b-dependency\" {\n\t\t\t\t\tbDepLine = i\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tassert.Less(t, aDepLine, bDepLine)\n\t\t})\n\t}\n}\n\nfunc TestParseFindListAllComponentsWithDAG(t *testing.T) {\n\tt.Parallel()\n\n\ttc := []struct {\n\t\tname    string\n\t\tcommand string\n\t}{\n\t\t{name: \"find\", command: \"terragrunt find --no-color --dag\"},\n\t\t{name: \"list\", command: \"terragrunt list --no-color --dag\"},\n\t}\n\n\tfor _, tt := range tc {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\t\t\tt,\n\t\t\t\ttt.command,\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// stderr can be non-empty if there are deprecations\n\t\t\tt.Logf(\"stderr: %s\", stderr)\n\t\t\tassert.NotEmpty(t, stdout)\n\n\t\t\tfields := strings.Fields(stdout)\n\n\t\t\t// Find positions of all fixtures in the output\n\t\t\taDepLine := -1\n\t\t\tbDepLine := -1\n\t\t\tcMixedLine := -1\n\t\t\tdDepsLine := -1\n\n\t\t\tfor i, field := range fields {\n\t\t\t\tswitch field {\n\t\t\t\tcase \"fixtures/find/dag/a-dependent\":\n\t\t\t\t\taDepLine = i\n\t\t\t\tcase \"fixtures/find/dag/b-dependency\":\n\t\t\t\t\tbDepLine = i\n\t\t\t\tcase \"fixtures/find/dag/c-mixed-deps\":\n\t\t\t\t\tcMixedLine = i\n\t\t\t\tcase \"fixtures/find/dag/d-dependencies-only\":\n\t\t\t\t\tdDepsLine = i\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Verify DAG ordering:\n\t\t\t// b-dependency (no deps) should come first\n\t\t\t// a-dependent (depends on b) should come after b\n\t\t\t// d-dependencies-only (depends on b) should come after b\n\t\t\t// c-mixed-deps (depends on a and d) should come last\n\t\t\tassert.Greater(t, aDepLine, bDepLine, \"a-dependent should come after b-dependency\")\n\t\t\tassert.Greater(t, dDepsLine, bDepLine, \"d-dependencies-only should come after b-dependency\")\n\t\t\tassert.Greater(t, cMixedLine, aDepLine, \"c-mixed-deps should come after a-dependent\")\n\t\t\tassert.Greater(t, cMixedLine, dDepsLine, \"c-mixed-deps should come after d-dependencies-only\")\n\t\t})\n\t}\n}\n\nfunc TestParseFindListAllComponentsWithDAGAndExternal(t *testing.T) {\n\tt.Parallel()\n\n\ttc := []struct {\n\t\tname    string\n\t\tcommand string\n\t}{\n\t\t{name: \"find\", command: \"terragrunt find --no-color --dag --external\"},\n\t\t{name: \"list\", command: \"terragrunt list --no-color --dag --external\"},\n\t}\n\n\tfor _, tt := range tc {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\t\t\tt,\n\t\t\t\ttt.command,\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.NotEmpty(t, stderr)\n\t\t\tassert.NotEmpty(t, stdout)\n\n\t\t\tfields := strings.Fields(stdout)\n\n\t\t\t// Find positions of all fixtures in the output\n\t\t\taDepLine := -1\n\t\t\tbDepLine := -1\n\t\t\tcMixedLine := -1\n\t\t\tdDepsLine := -1\n\n\t\t\tfor i, field := range fields {\n\t\t\t\tswitch field {\n\t\t\t\tcase \"fixtures/find/dag/a-dependent\":\n\t\t\t\t\taDepLine = i\n\t\t\t\tcase \"fixtures/find/dag/b-dependency\":\n\t\t\t\t\tbDepLine = i\n\t\t\t\tcase \"fixtures/find/dag/c-mixed-deps\":\n\t\t\t\t\tcMixedLine = i\n\t\t\t\tcase \"fixtures/find/dag/d-dependencies-only\":\n\t\t\t\t\tdDepsLine = i\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Verify DAG ordering for the core dependencies\n\t\t\t// The exact ordering may vary with external dependencies included,\n\t\t\t// but the basic dependency relationship should hold\n\t\t\tif aDepLine >= 0 && bDepLine >= 0 {\n\t\t\t\tassert.Greater(t, aDepLine, bDepLine, \"a-dependent should come after b-dependency\")\n\t\t\t}\n\n\t\t\tif dDepsLine >= 0 && bDepLine >= 0 {\n\t\t\t\tassert.Greater(t, dDepsLine, bDepLine, \"d-dependencies-only should come after b-dependency\")\n\t\t\t}\n\n\t\t\tif cMixedLine >= 0 && aDepLine >= 0 && dDepsLine >= 0 {\n\t\t\t\tassert.Greater(t, cMixedLine, aDepLine, \"c-mixed-deps should come after a-dependent\")\n\t\t\t\tassert.Greater(t, cMixedLine, dDepsLine, \"c-mixed-deps should come after d-dependencies-only\")\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/integration_private_registry_test.go",
    "content": "//go:build private_registry\n\npackage test_test\n\nimport (\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tprivateRegistryFixturePath = \"fixtures/private-registry\"\n)\n\nfunc setupPrivateRegistryTest(t *testing.T) (string, string, string) {\n\tt.Helper()\n\n\tregistryToken := os.Getenv(\"PRIVATE_REGISTRY_TOKEN\")\n\n\t// the private registry test is recommended to be a clone of gruntwork-io/terraform-null-terragrunt-registry-test\n\tregistryURL := os.Getenv(\"PRIVATE_REGISTRY_URL\")\n\n\tif registryToken == \"\" || registryURL == \"\" {\n\t\tt.Skip(\"Skipping test because it requires a valid Terraform registry token and url\")\n\t}\n\n\thelpers.CleanupTerraformFolder(t, privateRegistryFixturePath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, privateRegistryFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, privateRegistryFixturePath)\n\n\tURL, err := url.Parse(\"tfr://\" + registryURL)\n\tif err != nil {\n\t\tt.Fatalf(\"REGISTRY_URL is invalid: %v\", err)\n\t}\n\n\tif URL.Hostname() == \"\" {\n\t\tt.Fatal(\"REGISTRY_URL is invalid\")\n\t}\n\n\thelpers.CopyAndFillMapPlaceholders(t, filepath.Join(privateRegistryFixturePath, \"terragrunt.hcl\"), filepath.Join(rootPath, \"terragrunt.hcl\"), map[string]string{\n\t\t\"__registry_url__\": registryURL,\n\t})\n\n\treturn rootPath, URL.Hostname(), registryToken\n}\n\nfunc TestPrivateRegistryWithConfgFileToken(t *testing.T) {\n\trootPath, host, token := setupPrivateRegistryTest(t)\n\n\thelpers.CopyAndFillMapPlaceholders(t, filepath.Join(privateRegistryFixturePath, \"env.tfrc\"), filepath.Join(rootPath, \"env.tfrc\"), map[string]string{\n\t\t\"__registry_token__\": token,\n\t\t\"__registry_host__\":  host,\n\t})\n\n\tt.Setenv(\"TF_CLI_CONFIG_FILE\", filepath.Join(rootPath, \"env.tfrc\"))\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt init --non-interactive --log-level=trace --working-dir=\"+rootPath)\n\n\t// the hashicorp/null provider errors on install, but that indicates that the private tfr module was downloaded\n\trequire.Contains(t, err.Error(), \"hashicorp/null\", \"Error accessing the private registry\")\n}\n\nfunc TestPrivateRegistryWithEnvToken(t *testing.T) {\n\trootPath, host, token := setupPrivateRegistryTest(t)\n\n\t// Convert host to format suitable for Terraform env vars.\n\t// This is based on the tf/cliconfig/credentials.go collectCredentialsFromEnv\n\thost = strings.ReplaceAll(strings.ReplaceAll(host, \".\", \"_\"), \"-\", \"__\")\n\n\tt.Setenv(\"TF_TOKEN_\"+host, token)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt init --non-interactive --log-level=trace --working-dir=\"+rootPath)\n\n\t// The main test is for authentication against the private registry, so if the null provider fails then we know\n\t// that terragrunt authenticated and downloaded the module.\n\trequire.Contains(t, err.Error(), \"hashicorp/null\", \"Error accessing the private registry\")\n}\n"
  },
  {
    "path": "test/integration_provider_cache_constraint_test.go",
    "content": "package test_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/hashicorp/hcl/v2/hclwrite\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureProviderCacheWeakConstraint = \"fixtures/provider-cache/weak-constraint\"\n)\n\n// TestTerragruntProviderCacheWeakConstraint tests that provider cache preserves\n// module constraints instead of pinning exact versions in .terraform.lock.hcl files.\n// Reproduces and validates the fix for GitHub issue #4512.\n//\n//nolint:paralleltest,tparallel\nfunc TestTerragruntProviderCacheWeakConstraint(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureProviderCacheWeakConstraint)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureProviderCacheWeakConstraint)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureProviderCacheWeakConstraint)\n\tappPath := filepath.Join(rootPath, \"app\")\n\n\tproviderCacheDir := helpers.TmpDirWOSymlinks(t)\n\n\tt.Run(\"initial_setup_preserves_module_constraints\", func(t *testing.T) {\n\t\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt init --provider-cache --provider-cache-dir %s --non-interactive --working-dir %s\", providerCacheDir, appPath))\n\n\t\tconstraintsValue := extractConstraintsFromLockFile(t, appPath, \"cloudflare/cloudflare\")\n\n\t\texpectedConstraints := \"~> 4.0.0\"\n\t\tassert.Equal(t, expectedConstraints, constraintsValue, \"Initial lock file should preserve module's required_providers constraints\")\n\t})\n\n\tt.Run(\"upgrade_updates_constraints_to_match_module\", func(t *testing.T) {\n\t\t// Update the main.tf file to change cloudflare version constraint from \"~> 4.0\" to \"~> 4.40\"\n\t\tmainTfPath := filepath.Join(appPath, \"main.tf\")\n\t\toriginalContent, err := os.ReadFile(mainTfPath)\n\t\trequire.NoError(t, err)\n\n\t\t// Replace the version constraint\n\t\tupdatedContent := strings.ReplaceAll(string(originalContent), `version = \"~> 4.0\"`, `version = \"~> 4.40\"`)\n\t\trequire.NotEqual(t, string(originalContent), updatedContent, \"Content should be different after replacement\")\n\n\t\terr = os.WriteFile(mainTfPath, []byte(updatedContent), 0644)\n\t\trequire.NoError(t, err)\n\n\t\tlockFilePreInit, err := os.ReadFile(filepath.Join(appPath, \".terraform.lock.hcl\"))\n\t\trequire.NoError(t, err)\n\n\t\t// Run terragrunt init and check that the lock file isn't updated\n\t\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt init --provider-cache --provider-cache-dir %s --non-interactive --working-dir %s\", providerCacheDir, appPath))\n\t\tlockFilePostInit, err := os.ReadFile(filepath.Join(appPath, \".terraform.lock.hcl\"))\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, string(lockFilePreInit), string(lockFilePostInit), \"Lock file should not be updated\")\n\n\t\t// Run terragrunt init -upgrade to update the lock file\n\t\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt init -upgrade --provider-cache --provider-cache-dir %s --non-interactive --working-dir %s\", providerCacheDir, appPath))\n\n\t\tlockFilePostUpgrade, err := os.ReadFile(filepath.Join(appPath, \".terraform.lock.hcl\"))\n\t\trequire.NoError(t, err)\n\t\tassert.NotEqual(t, string(lockFilePostInit), string(lockFilePostUpgrade), \"Lock file should be updated\")\n\n\t\t// Verify the lock file constraints are updated to match the module\n\t\tconstraintsValue := extractConstraintsFromLockFile(t, appPath, \"cloudflare/cloudflare\")\n\n\t\texpectedConstraints := \"~> 4.40.0\"\n\t\tassert.Equal(t, expectedConstraints, constraintsValue, \"Constraints should be updated to match the module's required_providers\")\n\t})\n\n\tt.Run(\"fresh_start_uses_module_constraints\", func(t *testing.T) {\n\t\t// Delete the lock file\n\t\tlockfilePath := filepath.Join(appPath, \".terraform.lock.hcl\")\n\t\terr := os.Remove(lockfilePath)\n\t\trequire.NoError(t, err)\n\n\t\t// Also clean up .terraform directory to ensure fresh start\n\t\tterraformDir := filepath.Join(appPath, \".terraform\")\n\t\tif util.FileExists(terraformDir) {\n\t\t\terr = os.RemoveAll(terraformDir)\n\t\t\trequire.NoError(t, err)\n\t\t}\n\n\t\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt init --provider-cache --provider-cache-dir %s --non-interactive --working-dir %s\", providerCacheDir, appPath))\n\n\t\tconstraintsValue := extractConstraintsFromLockFile(t, appPath, \"cloudflare/cloudflare\")\n\n\t\texpectedConstraints := \"~> 4.40.0\"\n\t\tassert.Equal(t, expectedConstraints, constraintsValue, \"Fresh lock file should use module's required_providers constraints\")\n\t})\n}\n\n// Helper function to extract constraints value from lock file\nfunc extractConstraintsFromLockFile(t *testing.T, appPath string, providerName string) string {\n\tt.Helper()\n\n\tlockfilePath := filepath.Join(appPath, \".terraform.lock.hcl\")\n\trequire.True(t, util.FileExists(lockfilePath), \"Lock file should exist\")\n\n\t// Read and parse the lock file\n\tlockfileContent, err := os.ReadFile(lockfilePath)\n\trequire.NoError(t, err)\n\n\tlockfile, diags := hclwrite.ParseConfig(lockfileContent, lockfilePath, hcl.Pos{Line: 1, Column: 1})\n\trequire.False(t, diags.HasErrors(), \"Lock file should be valid HCL\")\n\n\t// Find the provider block (handle both short and full provider names)\n\tvar providerBlock *hclwrite.Block\n\tif strings.Contains(providerName, \"/\") {\n\t\t// Full name like \"cloudflare/cloudflare\"\n\t\tproviderBlock = lockfile.Body().FirstMatchingBlock(\"provider\", []string{\"registry.terraform.io/\" + providerName})\n\t\tif providerBlock == nil {\n\t\t\t// Try OpenTofu registry as well\n\t\t\tproviderBlock = lockfile.Body().FirstMatchingBlock(\"provider\", []string{\"registry.opentofu.org/\" + providerName})\n\t\t}\n\t} else {\n\t\t// Short name - search for matching block\n\t\tfor _, block := range lockfile.Body().Blocks() {\n\t\t\tif block.Type() == \"provider\" && len(block.Labels()) > 0 {\n\t\t\t\tif strings.Contains(block.Labels()[0], providerName) {\n\t\t\t\t\tproviderBlock = block\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\trequire.NotNil(t, providerBlock, \"Provider block should exist in lock file\")\n\n\t// Get the constraints attribute\n\tconstraintsAttr := providerBlock.Body().GetAttribute(\"constraints\")\n\trequire.NotNil(t, constraintsAttr, \"Constraints attribute should exist\")\n\n\tconstraintsValue := strings.Trim(string(constraintsAttr.Expr().BuildTokens(nil).Bytes()), ` \"`)\n\n\treturn constraintsValue\n}\n"
  },
  {
    "path": "test/integration_queue_strict_include_test.go",
    "content": "package test_test\n\nimport (\n\t\"fmt\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureQueueStrictInclude             = \"fixtures/queue-strict-include\"\n\ttestFixtureQueueStrictIncludeUnitsReading = \"fixtures/queue-strict-include-units-reading\"\n)\n\n// TestQueueStrictIncludeWithDependencyNotInQueue tests that when using --queue-include-dir\n// or --filter, units can run even when their dependencies are not in the queue (but have existing state).\nfunc TestQueueStrictIncludeWithDependencyNotInQueue(t *testing.T) {\n\tt.Parallel()\n\n\tsetup := func(t *testing.T) string {\n\t\tt.Helper()\n\n\t\t// Create test fixture with dependency chain: transitive-dependency -> dependency -> dependent\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureQueueStrictInclude)\n\t\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\t\ttestPath := filepath.Join(tmpEnvPath, testFixtureQueueStrictInclude)\n\n\t\t// First, apply all units to create state\n\t\t// This simulates the scenario where units have been previously applied\n\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t,\n\t\t\tfmt.Sprintf(\n\t\t\t\t\"terragrunt run --log-level debug --all --non-interactive --working-dir %s -- apply -auto-approve\",\n\t\t\t\ttestPath,\n\t\t\t),\n\t\t)\n\t\trequire.NoError(t, err, \"Failed to apply all units initially\\nstdout: %s\\nstderr: %s\", stdout, stderr)\n\n\t\t// Verify all units were applied\n\t\tassert.Contains(t, stdout+stderr, \"transitive-dependency\", \"transitive-dependency should be applied\")\n\t\tassert.Contains(t, stdout+stderr, \"dependency\", \"dependency should be applied\")\n\t\tassert.Contains(t, stdout+stderr, \"dependent\", \"dependent should be applied\")\n\n\t\treturn testPath\n\t}\n\n\tt.Run(\"queue-include-dir\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttestPath := setup(t)\n\n\t\thelpers.CleanupTerraformFolder(t, testPath)\n\n\t\t// Test with --queue-include-dir to only include dependency\n\t\t// The dependency unit depends on transitive-dependency, which should not be in the queue\n\t\t// but should be considered ready because it has existing state\n\t\tcmd := fmt.Sprintf(\n\t\t\t\"terragrunt run --log-level debug --all --non-interactive --working-dir %s --queue-include-dir 'dependency' -- plan\",\n\t\t\ttestPath,\n\t\t)\n\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t// The command should succeed\n\t\trequire.NoError(\n\t\t\tt,\n\t\t\terr,\n\t\t\t\"Command should succeed when dependency has existing state\\nstdout: %s\\nstderr: %s\",\n\t\t\tstdout,\n\t\t\tstderr,\n\t\t)\n\n\t\t// Verify that dependency unit was processed\n\t\toutput := stdout + stderr\n\t\tassert.Contains(t, output, \"dependency\", \"dependency unit should be processed\")\n\n\t\t// Verify that transitive-dependency is NOT in the queue (filtered out)\n\t\t// but dependency still runs successfully\n\t\tassert.Contains(t, output, \"found 1 readyEntries tasks\",\n\t\t\t\"Should show 'found 1 readyEntries tasks' - dependency should run even though transitive-dependency is not in queue\")\n\t})\n\n\tt.Run(\"queue-include-dir and destroy\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttestPath := setup(t)\n\n\t\thelpers.CleanupTerraformFolder(t, testPath)\n\n\t\t// Test with --queue-include-dir to only include dependency\n\t\t// The dependency unit depends on transitive-dependency, which should not be in the queue\n\t\t// but should be considered ready because it has existing state\n\t\tcmd := fmt.Sprintf(\n\t\t\t\"terragrunt run --log-level debug --all --non-interactive --working-dir %s --queue-include-dir 'dependency' -- destroy -auto-approve\",\n\t\t\ttestPath,\n\t\t)\n\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t// The command should succeed\n\t\trequire.NoError(\n\t\t\tt,\n\t\t\terr,\n\t\t\t\"Command should succeed when dependency has existing state\\nstdout: %s\\nstderr: %s\",\n\t\t\tstdout,\n\t\t\tstderr,\n\t\t)\n\n\t\t// Verify that dependency unit was processed\n\t\toutput := stdout + stderr\n\t\tassert.Contains(t, output, \"dependency\", \"dependency unit should be processed\")\n\n\t\t// Verify that transitive-dependency is NOT in the queue (filtered out)\n\t\t// but dependency still runs successfully\n\t\tassert.Contains(\n\t\t\tt,\n\t\t\toutput,\n\t\t\t\"found 1 readyEntries tasks\",\n\t\t\t\"Should show 'found 1 readyEntries tasks' - dependency should run even though transitive-dependency is not in queue\",\n\t\t)\n\t})\n\n\tt.Run(\"filter flag\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttestPath := setup(t)\n\n\t\thelpers.CleanupTerraformFolder(t, testPath)\n\n\t\t// Test with --filter to only include dependency\n\t\tcmd := fmt.Sprintf(\n\t\t\t\"terragrunt run --log-level debug --all --non-interactive --working-dir %s --filter './dependency' -- plan\",\n\t\t\ttestPath,\n\t\t)\n\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t// The command should succeed\n\t\trequire.NoError(\n\t\t\tt,\n\t\t\terr,\n\t\t\t\"Command should succeed when dependency has existing state\\nstdout: %s\\nstderr: %s\",\n\t\t\tstdout,\n\t\t\tstderr,\n\t\t)\n\n\t\t// Verify that dependency unit was processed\n\t\toutput := stdout + stderr\n\t\tassert.Contains(t, output, \"dependency\", \"dependency unit should be processed\")\n\n\t\t// Verify that transitive-dependency is NOT in the queue (filtered out)\n\t\t// but dependency still runs successfully\n\t\tassert.Contains(\n\t\t\tt,\n\t\t\toutput,\n\t\t\t\"found 1 readyEntries tasks\",\n\t\t\t\"Should show 'found 1 readyEntries tasks' - dependency should run even though transitive-dependency is not in queue\",\n\t\t)\n\t})\n\n\tt.Run(\"filter flag and destroy\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\ttestPath := setup(t)\n\n\t\thelpers.CleanupTerraformFolder(t, testPath)\n\n\t\t// Test with --filter to only include dependency\n\t\tcmd := fmt.Sprintf(\n\t\t\t\"terragrunt run --log-level debug --all --non-interactive --working-dir %s --filter './dependency' -- destroy -auto-approve\",\n\t\t\ttestPath,\n\t\t)\n\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t\t// The command should succeed\n\t\trequire.NoError(\n\t\t\tt,\n\t\t\terr,\n\t\t\t\"Command should succeed when dependency has existing state\\nstdout: %s\\nstderr: %s\",\n\t\t\tstdout,\n\t\t\tstderr,\n\t\t)\n\n\t\t// Verify that dependency unit was processed\n\t\toutput := stdout + stderr\n\t\tassert.Contains(t, output, \"dependency\", \"dependency unit should be processed\")\n\n\t\t// Verify that transitive-dependency is NOT in the queue (filtered out)\n\t\t// but dependency still runs successfully\n\t\tassert.Contains(\n\t\t\tt,\n\t\t\toutput,\n\t\t\t\"found 1 readyEntries tasks\",\n\t\t\t\"Should show 'found 1 readyEntries tasks' - dependency should run even though transitive-dependency is not in queue\",\n\t\t)\n\t})\n}\n\n// TestQueueStrictIncludeWithUnitsReadingWithoutIncludeDir reproduces the bug where\n// --queue-include-units-reading (but no --queue-include-dir)\n// fails to discover units that read the specified file.\nfunc TestQueueStrictIncludeWithUnitsReadingWithoutIncludeDir(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureQueueStrictIncludeUnitsReading)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureQueueStrictIncludeUnitsReading)\n\n\t// This reproduces the bug: --queue-include-units-reading\n\t// without --queue-include-dir should still only include units that read the file\n\tcmd := fmt.Sprintf(\"terragrunt run --all --non-interactive --working-dir %s --queue-include-units-reading=sources/source.hcl -- plan\", testPath)\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\n\t// The command should succeed and discover the unit\n\trequire.NoError(t, err, \"Command should succeed\\nstdout: %s\\nstderr: %s\", stdout, stderr)\n\n\toutput := stdout + stderr\n\n\t// Verify that the unit reading sources/source.hcl is discovered and included\n\t// This should pass after the fix, but currently fails due to the bug\n\tassert.Contains(t, output, \"live/foo\", \"Unit live/foo that reads sources/source.hcl should be included\")\n\tassert.NotContains(t, output, \"No units discovered\", \"Should discover units reading sources/source.hcl\")\n}\n"
  },
  {
    "path": "test/integration_registry_test.go",
    "content": "package test_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tregistryFixturePath                          = \"fixtures/tfr\"\n\tregistryFixtureRootModulePath                = \"root\"\n\tregistryFixtureRootShorthandModulePath       = \"root-shorthand\"\n\tregistryFixtureSubdirModulePath              = \"subdir\"\n\tregistryFixtureSubdirWithReferenceModulePath = \"subdir-with-reference\"\n)\n\nfunc TestTerraformRegistryFetchingRootModule(t *testing.T) {\n\tt.Parallel()\n\ttestTerraformRegistryFetching(t, registryFixtureRootModulePath, \"root_null_resource\")\n}\n\nfunc TestRegistryFetchingRootShorthandModule(t *testing.T) {\n\tt.Parallel()\n\ttestTerraformRegistryFetching(t, registryFixtureRootShorthandModulePath, \"root_null_resource\")\n}\n\nfunc TestTerraformRegistryFetchingSubdirModule(t *testing.T) {\n\tt.Parallel()\n\ttestTerraformRegistryFetching(t, registryFixtureSubdirModulePath, \"one_null_resource\")\n}\n\nfunc TestTerraformRegistryFetchingSubdirWithReferenceModule(t *testing.T) {\n\tt.Parallel()\n\ttestTerraformRegistryFetching(t, registryFixtureSubdirWithReferenceModulePath, \"two\")\n}\n\nfunc testTerraformRegistryFetching(t *testing.T, modPath, expectedOutputKey string) {\n\tt.Helper()\n\n\tmodFullPath := filepath.Join(registryFixturePath, modPath)\n\thelpers.CleanupTerraformFolder(t, modFullPath)\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+modFullPath)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+modFullPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\t_, hasOutput := outputs[expectedOutputKey]\n\tassert.True(t, hasOutput)\n}\n"
  },
  {
    "path": "test/integration_regressions_test.go",
    "content": "package test_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureRegressions                       = \"fixtures/regressions\"\n\ttestFixtureDependencyGenerate                = \"fixtures/regressions/dependency-generate\"\n\ttestFixtureDependencyEmptyConfigPath         = \"fixtures/regressions/dependency-empty-config-path\"\n\ttestFixtureDisabledDependencyEmptyConfigPath = \"fixtures/regressions/disabled-dependency-empty-config-path\"\n\ttestFixtureParsingDeprecated                 = \"fixtures/parsing/exposed-include-with-deprecated-inputs\"\n\ttestFixtureSensitiveValues                   = \"fixtures/regressions/sensitive-values\"\n\ttestFixtureStackDetection                    = \"fixtures/regressions/multiple-stacks\"\n\ttestFixtureScopeEscape                       = \"fixtures/regressions/5195-scope-escape\"\n\ttestFixtureNotExistingDependency             = \"fixtures/regressions/not-existing-dependency\"\n\ttestFixtureDependencyIncludeError            = \"fixtures/regressions/dependency-include-error\"\n)\n\nfunc TestNoAutoInit(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureRegressions)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRegressions)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureRegressions, \"skip-init\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply --no-auto-init --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"no force apply stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"no force apply stderr\")\n\trequire.Error(t, err)\n\tassert.Contains(t, stderr.String(), \"Required plugins are not installed\")\n}\n\n// Test case for yamldecode bug: https://github.com/gruntwork-io/terragrunt/issues/834\nfunc TestYamlDecodeRegressions(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureRegressions)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRegressions)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureRegressions, \"yamldecode\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\t// Check the output of yamldecode and make sure it doesn't parse the string incorrectly\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\tassert.Equal(t, \"003\", outputs[\"test1\"].Value)\n\tassert.Equal(t, \"1.00\", outputs[\"test2\"].Value)\n\tassert.Equal(t, \"0ba\", outputs[\"test3\"].Value)\n}\n\nfunc TestMockOutputsMergeWithState(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureRegressions)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRegressions)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureRegressions, \"mocks-merge-with-state\")\n\n\tmodulePath := filepath.Join(rootPath, \"module\")\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply --non-interactive -auto-approve --working-dir \"+modulePath, &stdout, &stderr)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"module-executed\")\n\trequire.NoError(t, err)\n\n\tdeepMapPath := filepath.Join(rootPath, \"deep-map\")\n\tstdout = bytes.Buffer{}\n\tstderr = bytes.Buffer{}\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt apply --non-interactive -auto-approve --working-dir \"+deepMapPath, &stdout, &stderr)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"deep-map-executed\")\n\trequire.NoError(t, err)\n\n\tshallowPath := filepath.Join(rootPath, \"shallow\")\n\tstdout = bytes.Buffer{}\n\tstderr = bytes.Buffer{}\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt apply --non-interactive -auto-approve --working-dir \"+shallowPath, &stdout, &stderr)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"shallow-map-executed\")\n\trequire.NoError(t, err)\n}\n\nfunc TestIncludeError(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureRegressions)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRegressions)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureRegressions, \"include-error\", \"project\", \"app\")\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt plan --non-interactive --working-dir \"+rootPath)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"include blocks without label\")\n}\n\n// TestDependencyOutputInGenerateBlock tests that dependency outputs can be used in generate blocks.\n// This is a regression test for issue #4962 where using dependency outputs in generate blocks\n// started failing with \"Unsuitable value: value must be known\" error in v0.89.0+.\n//\n// The bug occurred because during `run --all`, the discovery phase was calling ParseConfigFile\n// instead of PartialParseConfigFile, which caused generate blocks to be evaluated before\n// dependency outputs were resolved. The fix ensures generate blocks are only evaluated when\n// each unit runs individually with full dependency resolution.\n//\n// See: https://github.com/gruntwork-io/terragrunt/issues/4962\nfunc TestDependencyOutputInGenerateBlock(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDependencyGenerate)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureDependencyGenerate)\n\totherPath := filepath.Join(rootPath, \"other\")\n\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\t\"terragrunt apply --non-interactive --working-dir \"+otherPath+\" -- -auto-approve\",\n\t)\n\n\t_, runAllStderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all plan --non-interactive --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\tassert.NotContains(t, runAllStderr, \"Unsuitable value: value must be known\",\n\t\t\"Should not fail with 'Unsuitable value' error when using dependency outputs in generate blocks\")\n\tassert.NotContains(t, runAllStderr, \"Unsuitable value type\",\n\t\t\"Should not fail with 'Unsuitable value type' error\")\n}\n\n// TestDependencyOutputInGenerateBlockDirectRun tests that dependency outputs work when running directly\n// This test verifies that even in the broken version, running directly (without --all) works\nfunc TestDependencyOutputInGenerateBlockDirectRun(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDependencyGenerate)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureDependencyGenerate)\n\totherPath := filepath.Join(rootPath, \"other\")\n\ttestingPath := filepath.Join(rootPath, \"testing\")\n\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\t\"terragrunt apply --auto-approve --non-interactive --working-dir \"+otherPath,\n\t)\n\n\t_, planStderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt plan --non-interactive --working-dir \"+testingPath,\n\t)\n\trequire.NoError(t, err)\n\n\tassert.NotContains(t, planStderr, \"Unsuitable value\",\n\t\t\"Direct run should never fail with 'Unsuitable value' error\")\n}\n\n// TestDependencyOutputInInputsStillWorks verifies that dependency outputs can be used in inputs\nfunc TestDependencyOutputInInputsStillWorks(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDependencyGenerate)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureDependencyGenerate)\n\totherPath := filepath.Join(rootPath, \"other\")\n\n\t// Apply the \"other\" module\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\thelpers.RunTerragrunt(t,\n\t\t\"terragrunt apply --auto-approve --non-interactive --working-dir \"+otherPath,\n\t)\n\n\trunAllStdout, runAllStderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all apply --non-interactive --working-dir \"+rootPath+\" -- --auto-approve\",\n\t)\n\trequire.NoError(t, err)\n\n\tassert.True(t, strings.Contains(runAllStdout, \"test-token-12345\") ||\n\t\tstrings.Contains(runAllStderr, \"test-token-12345\"),\n\t\t\"Token should be passed via inputs\")\n}\n\nfunc TestDependencyEmptyConfigPath_ReportsError(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureDependencyEmptyConfigPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDependencyEmptyConfigPath)\n\tgitPath := filepath.Join(tmpEnvPath, testFixtureDependencyEmptyConfigPath)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(gitPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\t// Run directly against the consumer unit to force evaluation of dependency outputs\n\tconsumerPath := filepath.Join(gitPath, \"_source\", \"units\", \"consumer\")\n\t_, stderr, runErr := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt plan --non-interactive --working-dir \"+consumerPath)\n\trequire.Error(t, runErr)\n\t// Accept match in either stderr or the returned error string\n\tif !strings.Contains(stderr, \"has invalid config_path\") && !strings.Contains(runErr.Error(), \"has invalid config_path\") {\n\t\tt.Fatalf(\"unexpected error; want invalid config_path message, got: %v\\nstderr: %s\", runErr, stderr)\n\t}\n}\n\n// TestExposedIncludeWithDeprecatedInputsSyntax tests that deprecated dependency.*.inputs.* syntax\n// is properly detected even when used in an included config with expose = true.\n// This is a regression test for a bug introduced in v0.91.1 where the partial parse path\n// did not call DetectDeprecatedConfigurations(), causing cryptic \"Could not find Terragrunt\n// configuration settings\" errors instead of clear deprecation messages.\n//\n// The bug occurs when:\n// 1. An included config (e.g., compcommon.hcl) uses deprecated dependency.*.inputs.* syntax\n// 2. The child config includes it with expose = true\n// 3. The included config is parsed via PartialParseConfig() which skips deprecation detection\n// 4. When evaluating the exposed include, Terragrunt encounters unsupported syntax and fails\n//\n// See: https://github.com/gruntwork-io/terragrunt/issues/4983\nfunc TestExposedIncludeWithDeprecatedInputsSyntax(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureParsingDeprecated)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureParsingDeprecated)\n\tchildPath := filepath.Join(tmpEnvPath, testFixtureParsingDeprecated, \"child\")\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt plan --non-interactive --working-dir \"+childPath,\n\t)\n\trequire.Error(t, err)\n\n\t// After the fix, we should get a clear error about deprecated syntax\n\t// instead of the cryptic \"Could not find Terragrunt configuration settings\" error\n\t// The error message appears in the error object, not necessarily stderr\n\terrorMessage := stderr\n\tif err != nil {\n\t\terrorMessage = errorMessage + \" \" + err.Error()\n\t}\n\n\tassert.Contains(t, errorMessage, \"Reading inputs from dependencies is no longer supported\")\n\n\t// Should NOT get the cryptic error that users were seeing\n\tassert.NotContains(t, errorMessage, \"Could not find Terragrunt configuration settings\")\n}\n\n// TestParsingWithGenerateAndExpose tests that config parsing works correctly with:\n// - Exposed include blocks with generate blocks\n// - Dependencies between units\n// - Complex inputs with map comparisons\n//\n// This is a regression test for parsing errors that occurred in v0.90.1+ where\n// configs with exposed includes containing generate blocks would fail during\n// discovery with \"Could not find Terragrunt configuration settings\" errors.\n//\n// Uses `list --dag --format=dot` instead of `run --all plan` since this is a parsing test\n// and doesn't need to run terraform operations.\n//\n// See: https://github.com/gruntwork-io/terragrunt/issues/4983\nfunc TestParsingWithGenerateAndExpose(t *testing.T) {\n\tt.Parallel()\n\n\ttestFixture := \"fixtures/regressions/parsing-run-all-with-generate\"\n\thelpers.CleanupTerraformFolder(t, testFixture)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixture)\n\trootPath := filepath.Join(tmpEnvPath, testFixture, \"services-info\")\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt list --dag --format=dot --non-interactive --working-dir \"+rootPath,\n\t)\n\n\t// The command should succeed\n\trequire.NoError(t, err, \"list --dag --format=dot should succeed\")\n\n\t// Should not see parsing errors\n\tassert.NotContains(t, stderr, \"Could not find Terragrunt configuration settings\",\n\t\t\"Should not see parsing errors\")\n\tassert.NotContains(t, stderr, \"Unrecoverable parse error\",\n\t\t\"Should not see unrecoverable parse errors\")\n\n\t// Should not see fmt formatting artifacts from %w (e.g., %!w(...))\n\tassert.NotContains(t, stderr, \"%!w(\",\n\t\t\"Should not see formatting artifacts in error output\")\n\n\t// Verify both units are discovered in the dependency graph\n\t// list --dag --format=dot outputs DOT format showing dependencies\n\tassert.Contains(t, stdout, \"test1\", \"Should discover the service dependency\")\n}\n\n// TestParsingWithGenerateAndExpose_WithExternalDependencies tests that config parsing\n// works correctly when external dependencies exist. This is a variant of TestParsingWithGenerateAndExpose\n// that verifies the same parsing behavior works with the full fixture including external dependencies.\n//\n// Uses `list --dag --format=dot` instead of `run --all plan` since this is a parsing test\n// and doesn't need to run terraform operations.\nfunc TestParsingWithGenerateAndExpose_WithExternalDependencies(t *testing.T) {\n\tt.Parallel()\n\n\ttestFixture := \"fixtures/regressions/parsing-run-all-with-generate\"\n\thelpers.CleanupTerraformFolder(t, testFixture)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixture)\n\trootPath := filepath.Join(tmpEnvPath, testFixture, \"services-info\")\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt list --dag --format=dot --non-interactive --working-dir \"+rootPath,\n\t)\n\n\t// The command should succeed\n\trequire.NoError(t, err)\n\n\t// Should not see parsing errors or formatting artifacts\n\tassert.NotContains(t, stderr, \"Could not find Terragrunt configuration settings\")\n\tassert.NotContains(t, stderr, \"Unrecoverable parse error\")\n\tassert.NotContains(t, stderr, \"%!w(\")\n\n\t// Verify units are discovered in the dependency graph\n\tassert.Contains(t, stdout, \"test1\", \"Should discover the service dependency\")\n}\n\n// TestSensitiveValues tests that sensitive values can be properly handled\n// when reading from YAML files and using the sensitive() function in locals.\n// This validates that:\n// 1. YAML files can be decoded and accessed in a map lookup based on environment\n// 2. The sensitive() wrapper properly marks values as sensitive\n// 3. Sensitive values can be passed as inputs to Terraform\n// 4. The password length can be validated in outputs\nfunc TestSensitiveValues(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureSensitiveValues)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureSensitiveValues)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureSensitiveValues)\n\n\t// Run terragrunt apply\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\t\"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath,\n\t)\n\n\t// Get the output to verify password length\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal([]byte(stdout), &outputs))\n\n\t// Verify the password length output exists and is a number\n\trequire.Contains(t, outputs, \"password_length\", \"Should have password_length output\")\n\tassert.Equal(t, \"number\", outputs[\"password_length\"].Type, \"password_length should be of type number\")\n\n\t// Verify the password length matches the dev password length (25 characters)\n\tpasswordLengthStr := fmt.Sprintf(\"%v\", outputs[\"password_length\"].Value)\n\tassert.Equal(t, \"25\", passwordLengthStr,\n\t\t\"Password length should match dev password\")\n}\n\n// TestDisabledDependencyEmptyConfigPath_NoCycleError tests that disabled dependencies with empty\n// config_path values do not cause cycle detection errors during discovery.\n// This is a regression test for issue #4977 where setting enabled = false on a dependency\n// with an empty config_path (\"\") was still causing terragrunt to throw cycle errors.\n//\n// The expected behavior is that disabled dependencies should be completely ignored during\n// dependency graph construction and cycle detection, regardless of their config_path value.\n//\n// See: https://github.com/gruntwork-io/terragrunt/issues/4977\nfunc TestDisabledDependencyEmptyConfigPath_NoCycleError(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureDisabledDependencyEmptyConfigPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDisabledDependencyEmptyConfigPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureDisabledDependencyEmptyConfigPath)\n\thelpers.CreateGitRepo(t, rootPath)\n\n\tunitBPath := filepath.Join(rootPath, \"unit-b\")\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt plan --non-interactive --working-dir \"+unitBPath,\n\t)\n\n\trequire.NoError(t, err, \"plan should succeed when disabled dependency has empty config_path\")\n\n\tcombinedOutput := stdout + stderr\n\tassert.NotContains(t, combinedOutput, \"cycle\",\n\t\t\"Should not see cycle detection errors for disabled dependencies\")\n\tassert.NotContains(t, combinedOutput, \"Cycle detected\",\n\t\t\"Should not see 'Cycle detected' error\")\n\n\tassert.NotContains(t, combinedOutput, \"has invalid config_path\",\n\t\t\"Should not see invalid config_path error for disabled dependency\")\n\n\t_, runAllStderr, runAllErr := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all plan --non-interactive --working-dir \"+rootPath,\n\t)\n\n\trequire.NoError(t, runAllErr, \"run --all plan should succeed\")\n\n\tassert.NotContains(t, runAllStderr, \"cycle\",\n\t\t\"run --all should not see cycle errors\")\n\tassert.NotContains(t, runAllStderr, \"dependency graph\",\n\t\t\"run --all should not see dependency graph errors\")\n}\n\nfunc TestMultipleStacksDetection(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackDetection)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackDetection)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackDetection, \"live\")\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"terragrunt.stack.hcl\")\n\tassert.Contains(t, stderr, \"unit1\")\n\tassert.Contains(t, stderr, \"unit2\")\n\n\tassert.NotContains(t, stderr, \"appv2.terragrunt.stack.hcl\")\n\tassert.NotContains(t, stderr, \"unit4\")\n\tassert.NotContains(t, stderr, \"unit3\")\n}\n\n// flushTrackingWriter wraps a writer and tracks writes and output size changes (which indicate flushes)\ntype flushTrackingWriter struct {\n\tw      io.Writer\n\tsignal chan<- struct{}\n\tmu     sync.Mutex\n\twrites int\n\tonce   bool\n}\n\nfunc (ftw *flushTrackingWriter) Write(p []byte) (int, error) {\n\tftw.mu.Lock()\n\tftw.writes++\n\n\tshouldSignal := !ftw.once && ftw.signal != nil\n\tif shouldSignal {\n\t\tftw.once = true\n\t}\n\n\tftw.mu.Unlock()\n\n\tif shouldSignal {\n\t\tselect {\n\t\tcase ftw.signal <- struct{}{}:\n\t\tdefault:\n\t\t}\n\t}\n\n\treturn ftw.w.Write(p)\n}\n\nfunc (ftw *flushTrackingWriter) getWriteCount() int {\n\tftw.mu.Lock()\n\tdefer ftw.mu.Unlock()\n\n\treturn ftw.writes\n}\n\n// TestOutputFlushOnInterrupt verifies that buffered output is flushed when context is cancelled.\nfunc TestOutputFlushOnInterrupt(t *testing.T) {\n\tif helpers.IsWindows() {\n\t\tt.Skip(\"Skipping test on Windows - signal handling differs\")\n\t}\n\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureDependencyOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDependencyOutput)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureDependencyOutput, \"app\")\n\tdependencyPath := filepath.Join(tmpEnvPath, testFixtureDependencyOutput, \"dependency\")\n\n\t// Initialize and apply dependency first so outputs are available\n\thelpers.RunTerragrunt(t, \"terragrunt init --non-interactive --working-dir \"+dependencyPath)\n\thelpers.RunTerragrunt(t, \"terragrunt apply --auto-approve --non-interactive --working-dir \"+dependencyPath)\n\thelpers.RunTerragrunt(t, \"terragrunt init --non-interactive --working-dir \"+testPath)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tfirstWrite := make(chan struct{}, 1)\n\n\tvar stdoutBuf, stderrBuf strings.Builder\n\n\tstdout := &flushTrackingWriter{w: &stdoutBuf, signal: firstWrite}\n\tstderr := &flushTrackingWriter{w: &stderrBuf, signal: firstWrite}\n\tcmdErr := make(chan error, 1)\n\n\tgo func() {\n\t\tcmdErr <- helpers.RunTerragruntCommandWithContext(t, ctx, \"terragrunt run --all apply --non-interactive --working-dir \"+testPath, stdout, stderr)\n\t}()\n\n\t// Wait for first write, then cancel to test flush on interrupt\n\tvar outputBeforeCancel string\n\n\tvar writesBeforeCancel int\n\n\tselect {\n\tcase <-firstWrite:\n\t\toutputBeforeCancel = stdoutBuf.String() + stderrBuf.String()\n\t\twritesBeforeCancel = stdout.getWriteCount() + stderr.getWriteCount()\n\t\tt.Logf(\"First write detected (%d bytes, %d writes), cancelling context\", len(outputBeforeCancel), writesBeforeCancel)\n\t\tcancel()\n\tcase <-cmdErr:\n\t\tt.Fatal(\"Command finished before we could interrupt it\")\n\tcase <-time.After(3 * time.Second):\n\t\tt.Fatal(\"No output appeared before timeout\")\n\t}\n\n\t// Wait briefly for flush to occur after cancellation\n\ttime.Sleep(200 * time.Millisecond)\n\n\toutputAfterCancel := stdoutBuf.String() + stderrBuf.String()\n\twritesAfterCancel := stdout.getWriteCount() + stderr.getWriteCount()\n\n\t// Wait for command to finish or timeout\n\tselect {\n\tcase <-cmdErr:\n\t\t// Command finished\n\tcase <-time.After(5 * time.Second):\n\t\tt.Logf(\"Command still running after cancellation\")\n\t}\n\n\toutput := stdoutBuf.String() + stderrBuf.String()\n\ttotalWrites := stdout.getWriteCount() + stderr.getWriteCount()\n\n\tt.Logf(\"Output length: before cancel=%d, after cancel=%d, final=%d\", len(outputBeforeCancel), len(outputAfterCancel), len(output))\n\tt.Logf(\"Total writes: before cancel=%d, after cancel=%d, final=%d\", writesBeforeCancel, writesAfterCancel, totalWrites)\n\n\t// Verify that output increased after cancellation (indicating flush occurred)\n\trequire.Greater(t, len(outputAfterCancel), len(outputBeforeCancel), \"Output should increase after cancellation due to flush\")\n\trequire.Greater(t, totalWrites, writesBeforeCancel, \"Additional writes should occur after cancellation (flush writes)\")\n\trequire.NotEmpty(t, output, \"Expected output to be flushed after cancellation\")\n}\n\n// TestRunAllDoesNotIncludeExternalDepsInQueue tests that running `terragrunt run --all` from a subdirectory\n// does NOT include external dependencies in the execution queue.\n// This is a regression test for issue #5195 where v0.94.0 incorrectly included external dependencies\n// in the run queue, causing dangerous operations like destroy to execute against unintended modules.\n//\n// The test structure is:\n//\n//\tfixtures/regressions/5195-scope-escape/\n//\t├── bastion/           <- Run from here, has dependency on module2\n//\t│   ├── terragrunt.hcl (depends on ../module2 with mock_outputs)\n//\t│   └── main.tf\n//\t├── module1/           <- Has dependency on bastion\n//\t│   ├── terragrunt.hcl\n//\t│   └── main.tf\n//\t└── module2/           <- External dependency of bastion\n//\t    ├── terragrunt.hcl\n//\t    └── main.tf\n//\n// Expected behavior (v0.93.13 and after fix):\n//   - External dependency (module2) is discovered but EXCLUDED from execution\n//   - Summary shows \"Excluded 1\" for the external dep\n//   - Only bastion (Unit .) is actually executed\n//\n// Bug behavior (v0.94.0):\n//   - This causes unintended operations on external modules\n//\n// See: https://github.com/gruntwork-io/terragrunt/issues/5195\nfunc TestRunAllDoesNotIncludeExternalDepsInQueue(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureScopeEscape)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureScopeEscape)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureScopeEscape)\n\tbastionPath := filepath.Join(rootPath, \"bastion\")\n\n\t// Initialize git repo - this is important because discovery uses git root for scope\n\thelpers.CreateGitRepo(t, rootPath)\n\n\t// Run terragrunt run --all plan from the bastion directory\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all plan --log-level debug --non-interactive --working-dir \"+bastionPath,\n\t)\n\n\t// The command should succeed\n\trequire.NoError(t, err)\n\n\t// Should see bastion (displayed as \".\" since it's the current directory)\n\tassert.Contains(t, stderr, \"Unit .\",\n\t\t\"Should discover the current directory (bastion) as '.'\")\n\n\t// Report shows 2 units (bastion + excluded external dep)\n\tassert.Contains(t, stdout, \"1 unit\",\n\t\t\"Should have 1 unit total (bastion)\")\n\tassert.Contains(t, stdout, \"Succeeded    1\",\n\t\t\"Only bastion should succeed\")\n}\n\n// TestRunAllFromParentDiscoversAllModules verifies that running from the parent directory\n// correctly discovers all modules in the hierarchy. This is the control test for\n// TestRunAllDoesNotEscapeWorkingDir.\nfunc TestRunAllFromParentDiscoversAllModules(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureScopeEscape)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureScopeEscape)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureScopeEscape)\n\n\t// Initialize git repo - this is important because discovery uses git root for scope\n\thelpers.CreateGitRepo(t, rootPath)\n\n\t// Run terragrunt run --all plan from the parent directory (live/)\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all plan --non-interactive --working-dir \"+rootPath,\n\t)\n\n\t// The command should succeed in terms of discovery\n\t_ = err\n\n\t// Should see all three modules when running from parent directory\n\tassert.Contains(t, stderr, \"bastion\",\n\t\t\"Should discover bastion when running from parent directory\")\n\tassert.Contains(t, stderr, \"module1\",\n\t\t\"Should discover module1 when running from parent directory\")\n\tassert.Contains(t, stderr, \"module2\",\n\t\t\"Should discover module2 when running from parent directory\")\n}\n\nfunc TestNotExistingDependency(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureNotExistingDependency)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureNotExistingDependency)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureNotExistingDependency)\n\n\tinvalidPath := filepath.Join(rootPath, \"invalid-path\")\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply --non-interactive --working-dir \"+invalidPath)\n\trequire.Error(t, err)\n\t// should be reported that dependency have invalid path\n\tassert.Contains(t, err.Error(), \"dependency \\\"dep_123\\\" has invalid config_path\")\n\n\tparentFindFail := filepath.Join(rootPath, \"parent-find-fail\")\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply --non-interactive --working-dir \"+parentFindFail)\n\trequire.Error(t, err)\n\t// should be reported that find_in_parent_folders() fail\n\tassert.Contains(t, err.Error(), \"Error in function call; Call to function \\\"find_in_parent_folders\\\" failed: ParentFileNotFoundError: Could not find a wrong-dir-name\")\n}\n\n// TestDependencyIncludeError tests that include directives for configs with dependency blocks\n// don't produce false positive \"Unknown variable\" errors. This is a regression test for issue #5169.\n// The bug occurs when:\n// 1. A config file (layer.hcl) has a dependency block AND inputs that reference dependency.*.outputs\n// 2. Another config (unit/terragrunt.hcl) includes layer.hcl via include directive\n// 3. During parsing, the inputs block is evaluated before dependencies are resolved\n// 4. This causes HCL to report \"There is no variable named dependency\" errors\nfunc TestDependencyIncludeError(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureDependencyIncludeError)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDependencyIncludeError)\n\tdepPath := filepath.Join(tmpEnvPath, testFixtureDependencyIncludeError, \"dep\")\n\tunitPath := filepath.Join(tmpEnvPath, testFixtureDependencyIncludeError, \"unit\")\n\n\t// First apply the dependency so it has outputs\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\t\"terragrunt apply -auto-approve --non-interactive --working-dir \"+depPath,\n\t)\n\n\t// Now apply the unit that includes layer.hcl with dependency block\n\t// This should NOT produce any ERROR-level diagnostics about \"Unknown variable\"\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt apply -auto-approve --non-interactive --working-dir \"+unitPath,\n\t)\n\trequire.NoError(t, err)\n\n\t// Verify no false positive \"Unknown variable\" errors appear in stderr\n\tassert.NotContains(t, stderr, \"There is no variable named \\\"dependency\\\"\",\n\t\t\"Should not see false positive 'Unknown variable' errors for dependency references\")\n\tassert.NotContains(t, stderr, \"Unknown variable\",\n\t\t\"Should not see any 'Unknown variable' errors\")\n\n\t// Verify the command actually completed successfully\n\tassert.Contains(t, stdout, \"Apply complete!\",\n\t\t\"Apply should complete successfully\")\n}\n"
  },
  {
    "path": "test/integration_report_test.go",
    "content": "package test_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/csv\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\tgogit \"github.com/go-git/go-git/v6\"\n\t\"github.com/go-git/go-git/v6/plumbing/object\"\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureReportPath = \"fixtures/report\"\n)\n\nfunc TestTerragruntReport(t *testing.T) {\n\tt.Parallel()\n\n\t// Set up test environment\n\thelpers.CleanupTerraformFolder(t, testFixtureReportPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureReportPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureReportPath)\n\n\t// Run terragrunt with report experiment enabled\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\trequire.Error(t, err)\n\n\t// Verify the report output contains expected information\n\tstdoutStr := stdout.String()\n\n\t// Replace the timing information with fixed values\n\tre := regexp.MustCompile(`❯❯ Run Summary\\s+\\d+\\s+units\\s+\\S+`)\n\tstdoutStr = re.ReplaceAllString(stdoutStr, \"❯❯ Run Summary  13 units  x\")\n\n\t// Trim stdout to only the run summary.\n\t// Find the summary section\n\tlines := strings.Split(stdoutStr, \"\\n\")\n\n\t// Find the \"Run Summary\" line\n\tsummaryStartIdx := -1\n\n\tfor i, line := range lines {\n\t\tif strings.Contains(line, \"Run Summary\") {\n\t\t\tsummaryStartIdx = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\trequire.NotEqual(t, -1, summaryStartIdx, \"Could not find 'Run Summary' line\")\n\n\t// Extract the summary section\n\tsummaryLines := lines[summaryStartIdx:]\n\tstdoutStr = strings.Join(summaryLines, \"\\n\")\n\n\tassert.Equal(t, strings.TrimSpace(`\n❯❯ Run Summary  13 units  x\n   ────────────────────────────\n   Succeeded    4\n   Failed       3\n   Early Exits  4\n   Excluded     2\n`), strings.TrimSpace(stdoutStr))\n}\n\nfunc TestTerragruntReportDisableSummary(t *testing.T) {\n\tt.Parallel()\n\n\t// Set up test environment\n\thelpers.CleanupTerraformFolder(t, testFixtureReportPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureReportPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureReportPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all apply --non-interactive --working-dir \"+\n\t\t\trootPath+\" --summary-disable\",\n\t)\n\trequire.Error(t, err)\n\n\t// Verify the report output does not contain the summary\n\tassert.NotContains(t, stdout, \"Run Summary\")\n}\n\nfunc TestTerragruntReportSaveToFile(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname   string\n\t\tformat string\n\t}{\n\t\t{\n\t\t\tname:   \"CSV format\",\n\t\t\tformat: \"csv\",\n\t\t},\n\t\t{\n\t\t\tname:   \"JSON format\",\n\t\t\tformat: \"json\",\n\t\t},\n\t}\n\n\texpectedHeader := []string{\"Name\", \"Started\", \"Ended\", \"Result\", \"Reason\", \"Cause\", \"Ref\", \"Cmd\", \"Args\"}\n\n\texpectedRecords := []map[string]string{\n\t\t{\"Name\": \"chain-a\", \"Result\": \"failed\", \"Reason\": \"run error\", \"Cause\": \"\"},\n\t\t{\"Name\": \"chain-b\", \"Result\": \"early exit\", \"Reason\": \"ancestor error\", \"Cause\": \"chain-a\"},\n\t\t{\"Name\": \"chain-c\", \"Result\": \"early exit\", \"Reason\": \"ancestor error\", \"Cause\": \"chain-b\"},\n\t\t{\"Name\": \"error-ignore\", \"Result\": \"succeeded\", \"Reason\": \"error ignored\", \"Cause\": \"ignore_everything\"},\n\t\t{\"Name\": \"first-early-exit\", \"Result\": \"early exit\", \"Reason\": \"ancestor error\", \"Cause\": \"first-failure\"},\n\t\t{\"Name\": \"first-exclude\", \"Result\": \"excluded\", \"Reason\": \"exclude block\", \"Cause\": \"\"},\n\t\t{\"Name\": \"first-failure\", \"Result\": \"failed\", \"Reason\": \"run error\", \"Cause\": \".*Failed to execute.*\"},\n\t\t{\"Name\": \"first-success\", \"Result\": \"succeeded\", \"Reason\": \"\", \"Cause\": \"\"},\n\t\t{\"Name\": \"retry-success\", \"Result\": \"succeeded\", \"Reason\": \"retry succeeded\", \"Cause\": \"file_not_there_yet\"},\n\t\t{\"Name\": \"second-early-exit\", \"Result\": \"early exit\", \"Reason\": \"ancestor error\", \"Cause\": \"second-failure\"},\n\t\t{\"Name\": \"second-failure\", \"Result\": \"failed\", \"Reason\": \"run error\", \"Cause\": \".*Failed to execute.*\"},\n\t\t{\"Name\": \"second-success\", \"Result\": \"succeeded\", \"Reason\": \"\", \"Cause\": \"\"},\n\t}\n\n\tvalidResults := map[string]bool{\n\t\t\"succeeded\":  true,\n\t\t\"failed\":     true,\n\t\t\"early exit\": true,\n\t\t\"excluded\":   true,\n\t}\n\n\tfor _, tt := range testCases {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\texpectedRecordsCopy := slices.Clone(expectedRecords)\n\n\t\t\t// Set up test environment\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureReportPath)\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureReportPath)\n\t\t\trootPath := filepath.Join(tmpEnvPath, testFixtureReportPath)\n\n\t\t\t// Run terragrunt with report experiment enabled and save to file\n\t\t\tvar (\n\t\t\t\tstdout bytes.Buffer\n\t\t\t\tstderr bytes.Buffer\n\t\t\t)\n\n\t\t\treportFile := \"report.\" + tt.format\n\t\t\tcmd := fmt.Sprintf(\n\t\t\t\t\"terragrunt run --all apply --non-interactive --working-dir %s --queue-exclude-dir %s --report-file %s\",\n\t\t\t\trootPath,\n\t\t\t\tfilepath.Join(rootPath, \"second-exclude\"),\n\t\t\t\treportFile)\n\t\t\terr := helpers.RunTerragruntCommand(t, cmd, &stdout, &stderr)\n\t\t\trequire.Error(t, err)\n\n\t\t\t// Verify the report file exists\n\t\t\treportFilePath := filepath.Join(rootPath, reportFile)\n\t\t\tassert.FileExists(t, reportFilePath)\n\n\t\t\t// Read and parse the file based on format\n\t\t\tvar records []map[string]string\n\n\t\t\tif tt.format == \"csv\" {\n\t\t\t\tfile, err := os.Open(reportFilePath)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tdefer file.Close()\n\n\t\t\t\treader := csv.NewReader(file)\n\t\t\t\tcsvRecords, err := reader.ReadAll()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t// Verify header\n\t\t\t\tassert.Equal(t, expectedHeader, csvRecords[0])\n\n\t\t\t\t// Convert CSV records to map format\n\t\t\t\tfor _, record := range csvRecords[1:] {\n\t\t\t\t\trecordMap := make(map[string]string)\n\t\t\t\t\tfor i, value := range record {\n\t\t\t\t\t\trecordMap[expectedHeader[i]] = value\n\t\t\t\t\t}\n\n\t\t\t\t\trecords = append(records, recordMap)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// JSON format\n\t\t\t\tcontent, err := os.ReadFile(reportFilePath)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = json.Unmarshal(content, &records)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\t// Verify we have the expected number of records\n\t\t\trequire.Len(t, records, len(expectedRecordsCopy))\n\n\t\t\t// Sort records by name for consistent comparison\n\t\t\tsort.Slice(records, func(i, j int) bool {\n\t\t\t\treturn records[i][\"Name\"] < records[j][\"Name\"]\n\t\t\t})\n\n\t\t\t// Verify each record\n\t\t\tfor i, record := range records {\n\t\t\t\t_, err := time.Parse(time.RFC3339, record[\"Started\"])\n\t\t\t\trequire.NoError(t, err, \"Started timestamp in record %d is not in RFC3339 format\", i+1)\n\n\t\t\t\t_, err = time.Parse(time.RFC3339, record[\"Ended\"])\n\t\t\t\trequire.NoError(t, err, \"Ended timestamp in record %d is not in RFC3339 format\", i+1)\n\n\t\t\t\t// Verify Result is one of the expected values\n\t\t\t\tassert.True(t, validResults[record[\"Result\"]], \"Invalid result value in record %d: %s\", i+1, record[\"Result\"])\n\n\t\t\t\t// Create a new map with only the fields we want to compare\n\t\t\t\tcompareRecord := map[string]string{\n\t\t\t\t\t\"Name\":   record[\"Name\"],\n\t\t\t\t\t\"Result\": record[\"Result\"],\n\t\t\t\t\t\"Reason\": record[\"Reason\"],\n\t\t\t\t\t\"Cause\":  record[\"Cause\"],\n\t\t\t\t}\n\n\t\t\t\t// Check that the cause is the error message\n\t\t\t\tif record[\"Reason\"] == \"run error\" {\n\t\t\t\t\texpectedCausePattern := expectedRecordsCopy[i][\"Cause\"]\n\t\t\t\t\tassert.Regexp(t, expectedCausePattern, record[\"Cause\"])\n\n\t\t\t\t\tcompareRecord[\"Cause\"] = \"\"\n\t\t\t\t\texpectedRecordsCopy[i] = map[string]string{\n\t\t\t\t\t\t\"Name\":   expectedRecordsCopy[i][\"Name\"],\n\t\t\t\t\t\t\"Result\": expectedRecordsCopy[i][\"Result\"],\n\t\t\t\t\t\t\"Reason\": expectedRecordsCopy[i][\"Reason\"],\n\t\t\t\t\t\t\"Cause\":  \"\",\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Verify the record matches the expected record\n\t\t\t\tassert.Equal(t, expectedRecordsCopy[i], compareRecord)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTerragruntReportSaveToFileWithFormat(t *testing.T) {\n\tt.Parallel()\n\n\tsetup := func(t *testing.T) string {\n\t\tt.Helper()\n\n\t\thelpers.CleanupTerraformFolder(t, testFixtureReportPath)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureReportPath)\n\t\trootPath := filepath.Join(tmpEnvPath, testFixtureReportPath)\n\n\t\treturn rootPath\n\t}\n\n\ttestCases := []struct {\n\t\tname           string\n\t\treportFile     string\n\t\treportFormat   string\n\t\texpectedFormat string\n\t\tschemaFile     string\n\t}{\n\t\t{\n\t\t\tname:           \"default format with no extension\",\n\t\t\treportFile:     \"report\",\n\t\t\treportFormat:   \"\",\n\t\t\texpectedFormat: \"csv\",\n\t\t},\n\t\t{\n\t\t\tname:           \"csv format from extension\",\n\t\t\treportFile:     \"report.csv\",\n\t\t\treportFormat:   \"\",\n\t\t\texpectedFormat: \"csv\",\n\t\t},\n\t\t{\n\t\t\tname:           \"json format from extension\",\n\t\t\treportFile:     \"report.json\",\n\t\t\treportFormat:   \"\",\n\t\t\texpectedFormat: \"json\",\n\t\t},\n\t\t{\n\t\t\tname:           \"explicit csv format overrides extension\",\n\t\t\treportFile:     \"report.json\",\n\t\t\treportFormat:   \"csv\",\n\t\t\texpectedFormat: \"csv\",\n\t\t},\n\t\t{\n\t\t\tname:           \"explicit json format overrides extension\",\n\t\t\treportFile:     \"report.csv\",\n\t\t\treportFormat:   \"json\",\n\t\t\texpectedFormat: \"json\",\n\t\t},\n\t\t{\n\t\t\tname:           \"generate schema file\",\n\t\t\treportFile:     \"report.json\",\n\t\t\treportFormat:   \"json\",\n\t\t\texpectedFormat: \"json\",\n\t\t\tschemaFile:     \"schema.json\",\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.Parallel()\n\n\t\t\trootPath := setup(t)\n\n\t\t\t// Build command with appropriate flags\n\t\t\tcmd := \"terragrunt run --all apply --non-interactive --working-dir \" + rootPath\n\t\t\tif tc.reportFile != \"\" {\n\t\t\t\tcmd += \" --report-file \" + tc.reportFile\n\t\t\t}\n\n\t\t\tif tc.reportFormat != \"\" {\n\t\t\t\tcmd += \" --report-format \" + tc.reportFormat\n\t\t\t}\n\n\t\t\tif tc.schemaFile != \"\" {\n\t\t\t\tcmd += \" --report-schema-file \" + tc.schemaFile\n\t\t\t}\n\n\t\t\t// Run terragrunt command\n\t\t\tvar (\n\t\t\t\tstdout bytes.Buffer\n\t\t\t\tstderr bytes.Buffer\n\t\t\t)\n\n\t\t\terr := helpers.RunTerragruntCommand(t, cmd, &stdout, &stderr)\n\t\t\trequire.Error(t, err)\n\n\t\t\t// Verify the report file exists\n\t\t\treportFile := filepath.Join(rootPath, tc.reportFile)\n\t\t\tassert.FileExists(t, reportFile)\n\n\t\t\t// Read the file content\n\t\t\tcontent, err := os.ReadFile(reportFile)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Verify the format based on content\n\t\t\tswitch tc.expectedFormat {\n\t\t\tcase \"csv\":\n\t\t\t\t// For CSV, verify it starts with the expected header\n\t\t\t\tassert.True(t, strings.HasPrefix(string(content), \"Name,Started,Ended,Result,Reason,Cause,Ref,Cmd,Args\"))\n\t\t\tcase \"json\":\n\t\t\t\t// For JSON, verify it's valid JSON and has the expected structure\n\t\t\t\tvar jsonContent []map[string]any\n\n\t\t\t\terr := json.Unmarshal(content, &jsonContent)\n\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\trequire.NotEmpty(t, jsonContent)\n\n\t\t\t\t// Verify the first record has the expected fields\n\t\t\t\tfirstRecord := jsonContent[0]\n\t\t\t\tassert.Contains(t, firstRecord, \"Name\")\n\t\t\t\tassert.Contains(t, firstRecord, \"Started\")\n\t\t\t\tassert.Contains(t, firstRecord, \"Ended\")\n\t\t\t\tassert.Contains(t, firstRecord, \"Result\")\n\t\t\t}\n\n\t\t\t// If schema file is specified, verify it exists and is valid JSON\n\t\t\tif tc.schemaFile != \"\" {\n\t\t\t\tschemaFilePath := filepath.Join(rootPath, tc.schemaFile)\n\t\t\t\tassert.FileExists(t, schemaFilePath)\n\n\t\t\t\t// Read and verify schema file content\n\t\t\t\tschemaContent, err := os.ReadFile(schemaFilePath)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t// Verify it's valid JSON\n\t\t\t\tvar schema map[string]any\n\n\t\t\t\terr = json.Unmarshal(schemaContent, &schema)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t// Verify basic schema structure\n\t\t\t\tassert.Equal(t, \"array\", schema[\"type\"])\n\t\t\t\tassert.Equal(t, \"Array of Terragrunt runs\", schema[\"description\"])\n\t\t\t\tassert.Equal(t, \"Terragrunt Run Report Schema\", schema[\"title\"])\n\n\t\t\t\t// Verify items schema\n\t\t\t\titems, ok := schema[\"items\"].(map[string]any)\n\t\t\t\trequire.True(t, ok)\n\n\t\t\t\t// Verify required fields\n\t\t\t\trequired, ok := items[\"required\"].([]any)\n\t\t\t\trequire.True(t, ok)\n\t\t\t\tassert.Contains(t, required, \"Name\")\n\t\t\t\tassert.Contains(t, required, \"Started\")\n\t\t\t\tassert.Contains(t, required, \"Ended\")\n\t\t\t\tassert.Contains(t, required, \"Result\")\n\n\t\t\t\t// Verify properties\n\t\t\t\tproperties, ok := items[\"properties\"].(map[string]any)\n\t\t\t\trequire.True(t, ok)\n\n\t\t\t\t// Verify field types\n\t\t\t\tassert.Equal(t, \"string\", properties[\"Name\"].(map[string]any)[\"type\"])\n\t\t\t\tassert.Equal(t, \"string\", properties[\"Result\"].(map[string]any)[\"type\"])\n\t\t\t\tassert.Equal(t, \"string\", properties[\"Started\"].(map[string]any)[\"type\"])\n\t\t\t\tassert.Equal(t, \"string\", properties[\"Ended\"].(map[string]any)[\"type\"])\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestTerragruntReportWithUnitTiming(t *testing.T) {\n\tt.Parallel()\n\n\t// Set up test environment\n\thelpers.CleanupTerraformFolder(t, testFixtureReportPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureReportPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureReportPath)\n\n\t// Run terragrunt with report experiment enabled and unit timing enabled\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath+\" --summary-per-unit\", &stdout, &stderr)\n\trequire.Error(t, err)\n\n\t// Verify the report output contains expected information\n\tstdoutStr := stdout.String()\n\n\t// Replace the timing information with fixed values\n\tre := regexp.MustCompile(`❯❯ Run Summary\\s+\\d+\\s+units\\s+\\S+`)\n\tstdoutStr = re.ReplaceAllString(stdoutStr, \"❯❯ Run Summary  13 units  x\")\n\n\t// Replace unit timing durations with x (including minutes, seconds, milliseconds, microseconds, nanoseconds)\n\tre = regexp.MustCompile(`(?m)\\d+(\\.\\d+)?(m|s|ms|µs|μs|ns)$`)\n\tstdoutStr = re.ReplaceAllString(stdoutStr, \"x\")\n\n\t// Find and extract the run summary section\n\tlines := strings.Split(stdoutStr, \"\\n\")\n\n\t// Find the \"Run Summary\" line\n\tsummaryStartIdx := -1\n\n\tfor i, line := range lines {\n\t\tif strings.Contains(line, \"Run Summary\") {\n\t\t\tsummaryStartIdx = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\trequire.NotEqual(t, -1, summaryStartIdx, \"Could not find 'Run Summary' line\")\n\n\t// Find the end of the summary (last non-empty line after summary start)\n\tsummaryEndIdx := len(lines) - 1\n\tfor i := summaryEndIdx; i > summaryStartIdx; i-- {\n\t\tif strings.TrimSpace(lines[i]) != \"\" {\n\t\t\tsummaryEndIdx = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// Extract the summary section\n\tsummaryLines := lines[summaryStartIdx : summaryEndIdx+1]\n\tstdoutStr = strings.Join(summaryLines, \"\\n\")\n\n\t// Sort lines within each category to make the test deterministic\n\t// We're not testing the sorting functionality here, just the per-unit timing display\n\tstdoutStr = sortLinesWithinCategories(stdoutStr)\n\n\t// The expected format has units grouped by status with timing (sorted alphabetically within categories)\n\texpectedOutput := `\n❯❯ Run Summary  13 units  x\n   ────────────────────────────\n   Succeeded (4)\n      error-ignore ...... x\n      first-success ..... x\n      retry-success ..... x\n      second-success .... x\n   Failed (3)\n      chain-a ........... x\n      first-failure ..... x\n      second-failure .... x\n   Early Exits (4)\n      chain-b ........... x\n      chain-c ........... x\n      first-early-exit .. x\n      second-early-exit . x\n   Excluded (2)\n      first-exclude ..... x\n      second-exclude .... x`\n\n\tassert.Equal(t, strings.TrimSpace(expectedOutput), strings.TrimSpace(stdoutStr))\n}\n\n// lineType represents the type of line we're processing\ntype lineType int\n\nconst (\n\tcategoryHeaderLine lineType = iota\n\tunitLine\n\totherLine\n)\n\n// getLineType determines what type of line we're dealing with\nfunc getLineType(line string, inCategory bool) lineType {\n\ttrimmed := strings.TrimSpace(line)\n\n\t// Check if this is a category header line (ends with a count in parentheses)\n\tif strings.Contains(line, \"(\") && strings.Contains(line, \")\") &&\n\t\t(strings.Contains(line, \"Succeeded\") || strings.Contains(line, \"Failed\") ||\n\t\t\tstrings.Contains(line, \"Early Exits\") || strings.Contains(line, \"Excluded\")) {\n\t\treturn categoryHeaderLine\n\t}\n\n\t// Check if this is a unit line within a category\n\tif inCategory && strings.HasPrefix(line, \"      \") && trimmed != \"\" {\n\t\treturn unitLine\n\t}\n\n\treturn otherLine\n}\n\n// sortLinesWithinCategories sorts the unit lines within each category alphabetically\n// to make the test deterministic regardless of actual execution timing\nfunc sortLinesWithinCategories(input string) string {\n\tlines := strings.Split(input, \"\\n\")\n\n\tvar (\n\t\tresult               []string\n\t\tcurrentCategoryLines []string\n\t)\n\n\tinCategory := false\n\n\tfor _, line := range lines {\n\t\tswitch getLineType(line, inCategory) {\n\t\tcase categoryHeaderLine:\n\t\t\t// If we were in a category, sort and add those lines first\n\t\t\tif inCategory && len(currentCategoryLines) > 0 {\n\t\t\t\tsort.Strings(currentCategoryLines)\n\t\t\t\tresult = append(result, currentCategoryLines...)\n\t\t\t\tcurrentCategoryLines = nil\n\t\t\t}\n\t\t\t// Add the category header\n\t\t\tresult = append(result, line)\n\t\t\tinCategory = true\n\t\tcase unitLine:\n\t\t\t// This is a unit line within a category\n\t\t\tcurrentCategoryLines = append(currentCategoryLines, line)\n\t\tcase otherLine:\n\t\t\t// If we were in a category, sort and add those lines first\n\t\t\tif inCategory && len(currentCategoryLines) > 0 {\n\t\t\t\tsort.Strings(currentCategoryLines)\n\t\t\t\tresult = append(result, currentCategoryLines...)\n\t\t\t\tcurrentCategoryLines = nil\n\t\t\t\tinCategory = false\n\t\t\t}\n\t\t\t// Add non-category lines as-is\n\t\t\tresult = append(result, line)\n\t\t}\n\t}\n\n\t// Handle any remaining category lines\n\tif inCategory && len(currentCategoryLines) > 0 {\n\t\tsort.Strings(currentCategoryLines)\n\t\tresult = append(result, currentCategoryLines...)\n\t}\n\n\treturn strings.Join(result, \"\\n\")\n}\n\n// TestTerragruntReportWithGitFilter tests that report generation works correctly\n// with Git-based filters (worktree scenarios). This test verifies:\n// 1. Reports contain relative paths, not absolute worktree paths\n// 2. The report can be parsed using the utility functions\n// 3. The report passes schema validation\nfunc TestTerragruntReportWithGitFilter(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname           string\n\t\treportFormat   string\n\t\texpectedUnits  []string\n\t\texcludedUnits  []string\n\t\tignoredUnits   []string\n\t\tallowDestroy   bool\n\t\tvalidateSchema bool\n\t}{\n\t\t{\n\t\t\tname:           \"JSON format with git filter\",\n\t\t\treportFormat:   \"json\",\n\t\t\texpectedUnits:  []string{\"unit-created\", \"unit-modified\"},\n\t\t\texcludedUnits:  []string{\"unit-removed\"},\n\t\t\tignoredUnits:   []string{\"unit-untouched\"},\n\t\t\tallowDestroy:   false,\n\t\t\tvalidateSchema: true,\n\t\t},\n\t\t{\n\t\t\tname:           \"CSV format with git filter\",\n\t\t\treportFormat:   \"csv\",\n\t\t\texpectedUnits:  []string{\"unit-created\", \"unit-modified\"},\n\t\t\texcludedUnits:  []string{\"unit-removed\"},\n\t\t\tignoredUnits:   []string{\"unit-untouched\"},\n\t\t\tallowDestroy:   false,\n\t\t\tvalidateSchema: false, // CSV doesn't have schema validation\n\t\t},\n\t\t{\n\t\t\tname:           \"JSON format with git filter and allow destroy\",\n\t\t\treportFormat:   \"json\",\n\t\t\texpectedUnits:  []string{\"unit-created\", \"unit-modified\", \"unit-removed\"},\n\t\t\texcludedUnits:  []string{},\n\t\t\tignoredUnits:   []string{\"unit-untouched\"},\n\t\t\tallowDestroy:   true,\n\t\t\tvalidateSchema: true,\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.Parallel()\n\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t\t\trunner, err := git.NewGitRunner()\n\t\t\trequire.NoError(t, err)\n\n\t\t\trunner = runner.WithWorkDir(tmpDir)\n\n\t\t\terr = runner.Init(t.Context())\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = runner.GoOpenRepo()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tt.Cleanup(func() {\n\t\t\t\terr = runner.GoCloseStorage()\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Logf(\"Error closing storage: %s\", err)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tcreateReportTestUnit(t, filepath.Join(tmpDir, \"unit-modified\"), \"# Unit to be modified\")\n\t\t\tcreateReportTestUnit(t, filepath.Join(tmpDir, \"unit-removed\"), \"# Unit to be removed\")\n\t\t\tcreateReportTestUnit(t, filepath.Join(tmpDir, \"unit-untouched\"), \"# Unit to be untouched\")\n\n\t\t\terr = runner.GoAdd(\".\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = runner.GoCommit(\"Initial commit\", &gogit.CommitOptions{\n\t\t\t\tAuthor: &object.Signature{\n\t\t\t\t\tName:  \"Test User\",\n\t\t\t\t\tEmail: \"test@example.com\",\n\t\t\t\t\tWhen:  time.Now(),\n\t\t\t\t},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = os.WriteFile(filepath.Join(tmpDir, \"unit-modified\", \"terragrunt.hcl\"), []byte(\"# Modified\"), 0644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = os.RemoveAll(filepath.Join(tmpDir, \"unit-removed\"))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcreateReportTestUnit(t, filepath.Join(tmpDir, \"unit-created\"), \"# Unit created\")\n\n\t\t\terr = runner.GoAdd(\".\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = runner.GoCommit(\"Modify, create, and remove units\", &gogit.CommitOptions{\n\t\t\t\tAuthor: &object.Signature{\n\t\t\t\t\tName:  \"Test User\",\n\t\t\t\t\tEmail: \"test@example.com\",\n\t\t\t\t\tWhen:  time.Now(),\n\t\t\t\t},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\treportFile := \"report.\" + tc.reportFormat\n\t\t\tcmd := fmt.Sprintf(\n\t\t\t\t\"terragrunt run --all plan --no-color --non-interactive --working-dir %s --filter '[HEAD~1...HEAD]' --report-file %s --report-format %s\",\n\t\t\t\ttmpDir,\n\t\t\t\treportFile,\n\t\t\t\ttc.reportFormat,\n\t\t\t)\n\n\t\t\tif tc.allowDestroy {\n\t\t\t\tcmd += \" --filter-allow-destroy\"\n\t\t\t}\n\n\t\t\tvar stdout, stderr bytes.Buffer\n\n\t\t\terr = helpers.RunTerragruntCommand(t, cmd, &stdout, &stderr)\n\t\t\trequire.NoError(t, err)\n\n\t\t\treportFilePath := filepath.Join(tmpDir, reportFile)\n\t\t\trequire.FileExists(t, reportFilePath, \"Report file should exist\")\n\n\t\t\tswitch tc.reportFormat {\n\t\t\tcase \"json\":\n\t\t\t\truns, err := report.ParseJSONRunsFromFile(reportFilePath)\n\t\t\t\trequire.NoError(t, err, \"Should be able to parse JSON report\")\n\n\t\t\t\trunNames := runs.Names()\n\n\t\t\t\tfor _, expectedUnit := range tc.expectedUnits {\n\t\t\t\t\trun := runs.FindByName(expectedUnit)\n\t\t\t\t\trequire.NotNil(t, run, \"Expected unit '%s' should be in report. Found: %v\", expectedUnit, runNames)\n\n\t\t\t\t\tassert.NotContains(t, run.Name, \"terragrunt-worktree\",\n\t\t\t\t\t\t\"Report path should not contain worktree directory. Got: %s\", run.Name)\n\t\t\t\t\tassert.NotContains(t, run.Name, \"/tmp/\",\n\t\t\t\t\t\t\"Report path should be relative, not absolute. Got: %s\", run.Name)\n\n\t\t\t\t\tassert.NotEqual(t, \"excluded\", run.Result,\n\t\t\t\t\t\t\"Expected unit '%s' should not be excluded\", expectedUnit)\n\t\t\t\t}\n\n\t\t\t\tfor _, excludedUnit := range tc.excludedUnits {\n\t\t\t\t\trun := runs.FindByName(excludedUnit)\n\t\t\t\t\trequire.NotNil(t, run, \"Excluded unit '%s' should be in report. Found: %v\", excludedUnit, runNames)\n\t\t\t\t\tassert.Equal(t, \"excluded\", run.Result, \"Unit '%s' should be marked as excluded\", excludedUnit)\n\t\t\t\t}\n\n\t\t\t\tfor _, ignoredUnit := range tc.ignoredUnits {\n\t\t\t\t\trun := runs.FindByName(ignoredUnit)\n\t\t\t\t\tassert.Nil(t, run, \"Ignored unit '%s' should NOT be in report\", ignoredUnit)\n\t\t\t\t}\n\n\t\t\tcase \"csv\":\n\t\t\t\truns, err := report.ParseCSVRunsFromFile(reportFilePath)\n\t\t\t\trequire.NoError(t, err, \"Should be able to parse CSV report\")\n\n\t\t\t\trunNames := runs.Names()\n\n\t\t\t\tfor _, expectedUnit := range tc.expectedUnits {\n\t\t\t\t\trun := runs.FindByName(expectedUnit)\n\t\t\t\t\trequire.NotNil(t, run, \"Expected unit '%s' should be in report. Found: %v\", expectedUnit, runNames)\n\n\t\t\t\t\tassert.NotContains(t, run.Name, \"terragrunt-worktree\",\n\t\t\t\t\t\t\"Report path should not contain worktree directory. Got: %s\", run.Name)\n\t\t\t\t\tassert.NotContains(t, run.Name, \"/tmp/\",\n\t\t\t\t\t\t\"Report path should be relative, not absolute. Got: %s\", run.Name)\n\n\t\t\t\t\tassert.NotEqual(t, \"excluded\", run.Result,\n\t\t\t\t\t\t\"Expected unit '%s' should not be excluded\", expectedUnit)\n\t\t\t\t}\n\n\t\t\t\tfor _, excludedUnit := range tc.excludedUnits {\n\t\t\t\t\trun := runs.FindByName(excludedUnit)\n\t\t\t\t\trequire.NotNil(t, run, \"Excluded unit '%s' should be in report. Found: %v\", excludedUnit, runNames)\n\t\t\t\t\tassert.Equal(t, \"excluded\", run.Result, \"Unit '%s' should be marked as excluded\", excludedUnit)\n\t\t\t\t}\n\n\t\t\t\tfor _, ignoredUnit := range tc.ignoredUnits {\n\t\t\t\t\trun := runs.FindByName(ignoredUnit)\n\t\t\t\t\tassert.Nil(t, run, \"Ignored unit '%s' should NOT be in report\", ignoredUnit)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestTerragruntReportSingleUnit tests that report generation works correctly\n// for single unit runs (not --all). This verifies the fix that adds report\n// generation support to single `terragrunt run` commands.\nfunc TestTerragruntReportSingleUnit(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname           string\n\t\treportFormat   string\n\t\tschemaFile     string\n\t\tvalidateSchema bool\n\t}{\n\t\t{\n\t\t\tname:         \"JSON format\",\n\t\t\treportFormat: \"json\",\n\t\t},\n\t\t{\n\t\t\tname:         \"CSV format\",\n\t\t\treportFormat: \"csv\",\n\t\t},\n\t\t{\n\t\t\tname:           \"JSON format with schema\",\n\t\t\treportFormat:   \"json\",\n\t\t\tschemaFile:     \"schema.json\",\n\t\t\tvalidateSchema: true,\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureReportPath)\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureReportPath)\n\t\t\tunitPath := filepath.Join(tmpEnvPath, testFixtureReportPath, \"first-success\")\n\n\t\t\treportFile := \"report.\" + tc.reportFormat\n\t\t\treportFilePath := filepath.Join(unitPath, reportFile)\n\n\t\t\tcmd := fmt.Sprintf(\n\t\t\t\t\"terragrunt run plan --non-interactive --working-dir %s --report-file %s --report-format %s\",\n\t\t\t\tunitPath,\n\t\t\t\treportFilePath,\n\t\t\t\ttc.reportFormat,\n\t\t\t)\n\n\t\t\tif tc.schemaFile != \"\" {\n\t\t\t\tcmd += \" --report-schema-file \" + filepath.Join(unitPath, tc.schemaFile)\n\t\t\t}\n\n\t\t\tvar stdout, stderr bytes.Buffer\n\n\t\t\terr := helpers.RunTerragruntCommand(t, cmd, &stdout, &stderr)\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.FileExists(t, reportFilePath, \"Report file should exist for single unit run\")\n\n\t\t\tswitch tc.reportFormat {\n\t\t\tcase \"json\":\n\t\t\t\truns, err := report.ParseJSONRunsFromFile(reportFilePath)\n\t\t\t\trequire.NoError(t, err, \"Should be able to parse JSON report\")\n\n\t\t\t\trequire.Len(t, runs, 1, \"Single unit run should have exactly one entry in report\")\n\n\t\t\t\trun := runs[0]\n\t\t\t\tassert.Equal(t, \"first-success\", run.Name, \"Report should contain the unit name\")\n\t\t\t\tassert.Equal(t, \"succeeded\", run.Result, \"Unit should have succeeded\")\n\n\t\t\t\tassert.False(t, run.Started.IsZero(), \"Started timestamp should not be zero\")\n\t\t\t\tassert.False(t, run.Ended.IsZero(), \"Ended timestamp should not be zero\")\n\n\t\t\tcase \"csv\":\n\t\t\t\truns, err := report.ParseCSVRunsFromFile(reportFilePath)\n\t\t\t\trequire.NoError(t, err, \"Should be able to parse CSV report\")\n\n\t\t\t\trequire.Len(t, runs, 1, \"Single unit run should have exactly one entry in report\")\n\n\t\t\t\trun := runs[0]\n\t\t\t\tassert.Equal(t, \"first-success\", run.Name, \"Report should contain the unit name\")\n\t\t\t\tassert.Equal(t, \"succeeded\", run.Result, \"Unit should have succeeded\")\n\t\t\t}\n\n\t\t\tif tc.schemaFile != \"\" {\n\t\t\t\tschemaFilePath := filepath.Join(unitPath, tc.schemaFile)\n\t\t\t\trequire.FileExists(t, schemaFilePath, \"Schema file should exist\")\n\n\t\t\t\tschemaContent, err := os.ReadFile(schemaFilePath)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tvar schema map[string]any\n\n\t\t\t\terr = json.Unmarshal(schemaContent, &schema)\n\t\t\t\trequire.NoError(t, err, \"Schema should be valid JSON\")\n\n\t\t\t\tassert.Equal(t, \"array\", schema[\"type\"])\n\t\t\t\tassert.Equal(t, \"Terragrunt Run Report Schema\", schema[\"title\"])\n\t\t\t}\n\t\t})\n\t}\n}\n\n// createReportTestUnit creates a unit directory with terragrunt.hcl and main.tf files.\nfunc createReportTestUnit(t *testing.T, dir, comment string) {\n\tt.Helper()\n\n\terr := os.MkdirAll(dir, 0755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(dir, \"terragrunt.hcl\"), []byte(comment), 0644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(dir, \"main.tf\"), []byte(`# Minimal terraform config`), 0644)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "test/integration_run_cmd_flags_test.go",
    "content": "package test_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n)\n\nconst (\n\ttestFixtureRunCmdFlags            = \"fixtures/run-cmd-flags\"\n\ttestFixtureRunCmdModuleQuiet      = \"fixtures/run-cmd-flags/module-quiet\"\n\ttestFixtureRunCmdModuleGlobalA    = \"fixtures/run-cmd-flags/module-global-cache-a\"\n\ttestFixtureRunCmdModuleGlobalB    = \"fixtures/run-cmd-flags/module-global-cache-b\"\n\ttestFixtureRunCmdModuleNoCache    = \"fixtures/run-cmd-flags/module-no-cache\"\n\ttestFixtureRunCmdModuleConflict   = \"fixtures/run-cmd-flags/module-conflict\"\n\trunCmdSecretValue                 = \"TOP_SECRET_TOKEN\"\n\texpectedGlobalCachedValue         = \"global-value-1\"\n\tunexpectedGlobalCachedSecondValue = \"global-value-2\"\n\texpectedNoCacheFirstValue         = \"no-cache-value-1\"\n)\n\ntype runCmdFixtureResult struct {\n\trootPath string\n\tstdout   string\n\tstderr   string\n}\n\nfunc runCmdFlagsFixture(t *testing.T) runCmdFixtureResult {\n\tt.Helper()\n\n\tfor _, modulePath := range []string{\n\t\ttestFixtureRunCmdModuleQuiet,\n\t\ttestFixtureRunCmdModuleGlobalA,\n\t\ttestFixtureRunCmdModuleGlobalB,\n\t\ttestFixtureRunCmdModuleNoCache,\n\t\ttestFixtureRunCmdModuleConflict,\n\t} {\n\t\thelpers.CleanupTerraformFolder(t, modulePath)\n\t}\n\n\t// Clean up counter files from previous test runs in the fixture directory\n\tscriptsPath := filepath.Join(testFixtureRunCmdFlags, \"scripts\")\n\t_ = os.Remove(filepath.Join(scriptsPath, \"global_counter.txt\"))\n\t_ = os.Remove(filepath.Join(scriptsPath, \"no_cache_counter.txt\"))\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRunCmdFlags)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureRunCmdFlags)\n\n\t// Remove the conflicting module so the happy-path tests can run `terragrunt run --all` without errors.\n\tconflictDir := filepath.Join(rootPath, \"module-conflict\")\n\trequire.NoError(t, os.RemoveAll(conflictDir))\n\n\tcmd := \"terragrunt run --all plan --non-interactive --log-level debug --working-dir \" + rootPath\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\trequire.NoError(t, err)\n\n\t// Clean up counter files after test execution\n\tt.Cleanup(func() {\n\t\tscriptsPath := filepath.Join(testFixtureRunCmdFlags, \"scripts\")\n\t\t_ = os.Remove(filepath.Join(scriptsPath, \"global_counter.txt\"))\n\t\t_ = os.Remove(filepath.Join(scriptsPath, \"no_cache_counter.txt\"))\n\t})\n\n\treturn runCmdFixtureResult{\n\t\trootPath: rootPath,\n\t\tstdout:   stdout,\n\t\tstderr:   stderr,\n\t}\n}\n\nfunc TestRunCmdQuietRedactsOutput(t *testing.T) {\n\tt.Parallel()\n\n\tresult := runCmdFlagsFixture(t)\n\n\tassert.Contains(t, result.stderr, \"run_cmd output: [REDACTED]\")\n\tassert.NotContains(t, result.stderr, runCmdSecretValue)\n}\n\nfunc TestRunCmdGlobalCacheSharesResultAcrossModules(t *testing.T) {\n\tt.Parallel()\n\n\tresult := runCmdFlagsFixture(t)\n\n\tcombinedOutput := strings.Join([]string{result.stdout, result.stderr}, \"\\n\")\n\n\tglobalCounterPath := filepath.Join(result.rootPath, \"scripts\", \"global_counter.txt\")\n\tglobalCounterBytes, readErr := os.ReadFile(globalCounterPath)\n\trequire.NoError(t, readErr)\n\n\tassert.Equal(t, \"1\", strings.TrimSpace(string(globalCounterBytes)))\n\tassert.Contains(t, combinedOutput, expectedGlobalCachedValue)\n\tassert.NotContains(t, combinedOutput, unexpectedGlobalCachedSecondValue)\n}\n\nfunc TestRunCmdNoCacheSkipsCachedValue(t *testing.T) {\n\tt.Parallel()\n\n\tresult := runCmdFlagsFixture(t)\n\n\tassert.Contains(t, result.stderr, \"run_cmd output: [\"+expectedNoCacheFirstValue+\"]\")\n\tassert.NotContains(t, result.stderr, \"run_cmd, cached output: [\"+expectedNoCacheFirstValue+\"]\")\n}\n\nfunc TestRunCmdConflictingCacheOptionsFails(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureRunCmdModuleConflict)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRunCmdFlags)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureRunCmdFlags)\n\n\tcmd := \"terragrunt run --all plan --non-interactive --log-level debug --working-dir \" + rootPath\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\trequire.Error(t, err)\n\tassert.Contains(t, stderr, \"--terragrunt-global-cache and --terragrunt-no-cache options cannot be used together\")\n}\n"
  },
  {
    "path": "test/integration_run_cmd_include_output_test.go",
    "content": "package test_test\n\nimport (\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n)\n\nconst (\n\ttestFixtureRunCmdIncludeOutput = \"fixtures/regressions/run-cmd-include-output\"\n\trunCmdOutputMarker             = \"RUN_CMD_OUTPUT_MARKER_12345\"\n)\n\n// TestRunCmdOutputFromIncludedFileInStack verifies that run_cmd output from included\n// files (like root.hcl) is visible when running terragrunt commands on a stack.\n// This is a regression test for issue #5400.\nfunc TestRunCmdOutputFromIncludedFileInStack(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureRunCmdIncludeOutput)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRunCmdIncludeOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureRunCmdIncludeOutput)\n\n\tcmd := \"terragrunt run --all plan --non-interactive --working-dir \" + rootPath\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\trequire.NoError(t, err)\n\n\tcombinedOutput := strings.Join([]string{stdout, stderr}, \"\\n\")\n\n\t// The run_cmd output marker should appear exactly once as direct output.\n\t// Before the fix, this output was suppressed because the command ran during\n\t// discovery phase with io.Discard writers, and the cached result was reused\n\t// during the execution phase without replaying the output.\n\t//\n\t// The marker also appears in Terraform plan output (as the marker_value variable),\n\t// so we count only occurrences that are NOT inside quotes (direct run_cmd output).\n\t// Direct output: \"RUN_CMD_OUTPUT_MARKER_12345\\n\"\n\t// Terraform output: `+ marker_value = \"RUN_CMD_OUTPUT_MARKER_12345\"`\n\tdirectOutputMarker := runCmdOutputMarker + \"\\n\"\n\tcount := strings.Count(combinedOutput, directOutputMarker)\n\tassert.Equal(\n\t\tt,\n\t\t1,\n\t\tcount,\n\t\t\"run_cmd output from included file should appear exactly once, got %d occurrences\",\n\t\tcount,\n\t)\n}\n"
  },
  {
    "path": "test/integration_run_test.go",
    "content": "package test_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// TestRunStacksGenerate verifies that stack generation works correctly when running terragrunt with --all flag.\n// It ensures that:\n// 1. The stack directory is created\n// 2. The stack is properly applied\n// 3. The expected number of test.txt files are generated\nfunc TestRunStacksGenerate(t *testing.T) {\n\tt.Parallel()\n\n\t// Set up test environment\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksBasic)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksBasic, \"live\")\n\n\t// Run terragrunt with --all flag to trigger stack generation\n\thelpers.RunTerragrunt(t, \"terragrunt run apply --all --non-interactive --working-dir \"+rootPath)\n\n\t// Verify stack directory exists and validate its contents\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, path)\n\n\t// Collect all test.txt files in the stack directory to verify correct generation\n\tvar txtFiles []string\n\n\terr := filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !info.IsDir() && info.Name() == \"test.txt\" {\n\t\t\ttxtFiles = append(txtFiles, filePath)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n\t// Verify that exactly 4 test.txt files were generated\n\tassert.Len(t, txtFiles, 4)\n}\n\n// TestRunNoStacksGenerate verifies that stack generation is skipped in appropriate scenarios:\n// 1. When running without --all flag on directory which contains only terragrunt.stack.hcl\n// 2. When running with --all but --no-stack-generate flag is set on directory which contains only terragrunt.stack.hcl\n// 3. When running without --all flag on standard terragrunt directory\n// 4. When running with --all but --no-stack-generate on directory without terragrunt.stack.hcl\nfunc TestRunNoStacksGenerate(t *testing.T) {\n\tt.Parallel()\n\n\t// Define test cases for different scenarios where stack generation should be skipped\n\ttestdata := []struct {\n\t\tname       string\n\t\tcmd        string\n\t\tsubfolder  string\n\t\tshouldFail bool\n\t}{\n\t\t{\n\t\t\tname:       \"NoAll\",\n\t\t\tcmd:        \"terragrunt run apply --non-interactive\",\n\t\t\tsubfolder:  \"live\",\n\t\t\tshouldFail: true,\n\t\t},\n\t\t{\n\t\t\tname:       \"AllNoGenerate\",\n\t\t\tcmd:        \"terragrunt run apply --all --no-stack-generate --non-interactive\",\n\t\t\tsubfolder:  \"live\",\n\t\t\tshouldFail: false,\n\t\t},\n\t\t{\n\t\t\tname:       \"Standard\",\n\t\t\tcmd:        \"terragrunt run apply --non-interactive\",\n\t\t\tsubfolder:  \"units/chicken\",\n\t\t\tshouldFail: false,\n\t\t},\n\t\t{\n\t\t\tname:       \"AllNoStackToGenerate\",\n\t\t\tcmd:        \"terragrunt run apply --all --no-stack-generate --non-interactive\",\n\t\t\tsubfolder:  \"units\",\n\t\t\tshouldFail: false,\n\t\t},\n\t}\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksBasic)\n\n\t// Run each test case and verify stack generation is skipped\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\t// Set up test environment\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic)\n\t\t\tpath := filepath.Join(tmpEnvPath, testFixtureStacksBasic, tt.subfolder)\n\t\t\tcmd := tt.cmd + \" --working-dir \" + path + \" -- -auto-approve\"\n\n\t\t\t// Execute terragrunt command and verify no output\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\t\t\tif tt.shouldFail {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Empty(t, stdout)\n\t\t\t\t// We should explicitly avoid asserting on stderr, because information\n\t\t\t\t// might be logged to stderr, even if the command succeeds.\n\t\t\t\t//\n\t\t\t\t// e.g. Usage of the provider cache server.\n\t\t\t\t//\n\t\t\t\t// assert.Empty(t, stderr)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.NotEmpty(t, stderr)\n\t\t\t}\n\n\t\t\t// Verify that stack directory was not created\n\t\t\tgenPath := filepath.Join(path, \".terragrunt-stack\")\n\t\t\tassert.NoDirExists(t, genPath)\n\t\t})\n\t}\n}\n\nfunc TestRunVersionFilesCacheKey(t *testing.T) {\n\tt.Parallel()\n\n\ttestdata := []struct {\n\t\tname         string\n\t\texpect       string\n\t\tversionFiles []string\n\t}{\n\t\t{\n\t\t\tname:         \"use default\",\n\t\t\texpect:       \"r01AJjVD7VSXCQk1ORuh_no_NRY\",\n\t\t\tversionFiles: nil,\n\t\t},\n\t\t{\n\t\t\tname:   \"custom files provided\",\n\t\t\texpect: \"XBE-VO9pOnQjPQDmLQCvSCdckSQ\",\n\t\t\tversionFiles: []string{\n\t\t\t\t\".terraform-version\",\n\t\t\t\t\".tool-versions\",\n\t\t\t},\n\t\t},\n\t}\n\n\thelpers.CleanupTerraformFolder(t, testFixtureVersionFilesCacheKey)\n\n\tfor _, tt := range testdata {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureVersionFilesCacheKey, tt.versionFiles...)\n\t\t\tpath := filepath.Join(tmpEnvPath, testFixtureVersionFilesCacheKey)\n\t\t\tflags := make([]string, 0, 4+2*len(tt.versionFiles))\n\t\t\tflags = append(flags,\n\t\t\t\t\"-non-interactive\",\n\t\t\t\t\"--log-level debug\",\n\t\t\t\t\"--working-dir\",\n\t\t\t\tpath,\n\t\t\t)\n\n\t\t\tfor _, file := range tt.versionFiles {\n\t\t\t\tflags = append(\n\t\t\t\t\tflags,\n\t\t\t\t\t\"--version-manager-file-name\",\n\t\t\t\t\tfile,\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tcmd := \"terragrunt run apply \" + strings.Join(flags, \" \") + \" -- -auto-approve\"\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.NotEmpty(t, stdout)\n\t\t\tassert.NotEmpty(t, stderr)\n\t\t\tassert.Contains(t, stderr, \"using cache key for version files: \"+tt.expect)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/integration_runner_pool_test.go",
    "content": "package test_test\n\nimport (\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureMixedConfig            = \"fixtures/mixed-config\"\n\ttestFixtureFailFast               = \"fixtures/fail-fast\"\n\ttestFixtureFailFastEarlyExit      = \"fixtures/fail-fast-early-exit\"\n\ttestFixtureRunnerPoolRemoteSource = \"fixtures/runner-pool-remote-source\"\n\ttestFixtureAuthProviderParallel   = \"fixtures/auth-provider-parallel\"\n)\n\nfunc TestRunnerPoolDiscovery(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureDependencyOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDependencyOutput)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureDependencyOutput)\n\t// Run the find command to discover the configs\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --log-level debug --working-dir \"+testPath+\"  -- apply\")\n\trequire.NoError(t, err)\n\t// Verify that the output contains value from the app\n\trequire.Contains(t, stdout, \"output_value = \\\"42\\\"\")\n\n\t// Verify that the output contains value from the dependency\n\trequire.Contains(t, stdout, \"result = \\\"42\\\"\")\n}\n\nfunc TestRunnerPoolDiscoveryNoParallelism(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureDependencyOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDependencyOutput)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureDependencyOutput)\n\t// Run the find command to discover the configs\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --parallelism 1 --working-dir \"+testPath+\"  -- apply\")\n\trequire.NoError(t, err)\n\t// Verify that the output contains value from the app\n\trequire.Contains(t, stdout, \"output_value = \\\"42\\\"\")\n\n\t// Verify that the output contains value from the dependency\n\trequire.Contains(t, stdout, \"result = \\\"42\\\"\")\n}\n\nfunc TestRunnerPoolTerragruntDestroyOrder(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureDestroyOrder)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDestroyOrder)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureDestroyOrder, \"app\")\n\n\t// apply the stack\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// run destroy with runner pool and check the modules are destroyed\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all destroy --non-interactive --tf-forward-stdout --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\t// Parse destroyed modules from stdout\n\tvar destroyOrder []string\n\n\tre := regexp.MustCompile(`Hello, Module ([A-Za-z]+)`)\n\tfor line := range strings.SplitSeq(stdout, \"\\n\") {\n\t\tif match := re.FindStringSubmatch(line); match != nil {\n\t\t\tdestroyOrder = append(destroyOrder, \"module-\"+strings.ToLower(match[1]))\n\t\t}\n\t}\n\n\tt.Logf(\"Destroyed modules: %v\", destroyOrder)\n\n\tindex := make(map[string]int)\n\tfor i, mod := range destroyOrder {\n\t\tindex[mod] = i\n\t}\n\n\t// Verify all expected modules were destroyed\n\t// Note: With parallel execution, stdout order doesn't reflect actual execution order,\n\t// so we only verify all modules were destroyed, not their order.\n\t// Dependency ordering is enforced by the runner pool DAG execution.\n\texpectedModules := []string{\"module-a\", \"module-b\", \"module-c\", \"module-d\", \"module-e\"}\n\tfor _, mod := range expectedModules {\n\t\t_, ok := index[mod]\n\t\tassert.True(t, ok, \"expected module %q to be destroyed, got: %v\", mod, destroyOrder)\n\t}\n}\n\nfunc TestRunnerPoolStackConfigIgnored(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureMixedConfig)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureMixedConfig)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all --non-interactive --working-dir \"+testPath+\" -- apply\",\n\t)\n\trequire.NoError(t, err)\n\trequire.NotContains(t, stderr, \"Error: Unsupported block type\")\n\trequire.NotContains(t, stderr, \"Blocks of type \\\"unit\\\" are not expected here\")\n}\n\nfunc TestRunnerPoolFailFast(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedResults map[string]struct {\n\t\t\tresult string\n\t\t\treason string\n\t\t\tcause  string\n\t\t}\n\t\tname     string\n\t\tfailFast bool\n\t}{\n\t\t{\n\t\t\tname:     \"fail-fast=false\",\n\t\t\tfailFast: false,\n\t\t\texpectedResults: map[string]struct {\n\t\t\t\tresult string\n\t\t\t\treason string\n\t\t\t\tcause  string\n\t\t\t}{\n\t\t\t\t\"failing-unit\":          {result: \"failed\", reason: \"run error\"},\n\t\t\t\t\"succeeding-unit\":       {result: \"succeeded\"},\n\t\t\t\t\"depends-on-failing\":    {result: \"early exit\", reason: \"ancestor error\", cause: \"failing-unit\"},\n\t\t\t\t\"depends-on-succeeding\": {result: \"succeeded\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"fail-fast=true\",\n\t\t\tfailFast: true,\n\t\t\texpectedResults: map[string]struct {\n\t\t\t\tresult string\n\t\t\t\treason string\n\t\t\t\tcause  string\n\t\t\t}{\n\t\t\t\t\"failing-unit\":          {result: \"failed\", reason: \"run error\"},\n\t\t\t\t\"succeeding-unit\":       {result: \"succeeded\"},\n\t\t\t\t\"depends-on-failing\":    {result: \"early exit\", reason: \"ancestor error\", cause: \"failing-unit\"},\n\t\t\t\t\"depends-on-succeeding\": {result: \"early exit\", reason: \"ancestor error\"},\n\t\t\t},\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.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureFailFastEarlyExit)\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureFailFastEarlyExit)\n\t\t\ttestPath := filepath.Join(tmpEnvPath, testFixtureFailFastEarlyExit)\n\n\t\t\tcmd := \"terragrunt run --all --non-interactive --report-file \" + helpers.ReportFile + \" --working-dir \" + testPath\n\t\t\tif tc.failFast {\n\t\t\t\tcmd += \" --fail-fast\"\n\t\t\t}\n\n\t\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, cmd+\" -- apply\")\n\t\t\trequire.Error(t, err)\n\n\t\t\t// Parse and verify the report file\n\t\t\treportFilePath := filepath.Join(testPath, helpers.ReportFile)\n\t\t\tassert.FileExists(t, reportFilePath)\n\n\t\t\truns, err := report.ParseJSONRunsFromFile(reportFilePath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// Verify expected units are in the report\n\t\t\tassert.ElementsMatch(t, []string{\"failing-unit\", \"succeeding-unit\", \"depends-on-failing\", \"depends-on-succeeding\"}, runs.Names())\n\n\t\t\t// Verify each unit's result, reason, and cause\n\t\t\tfor unitName, expected := range tc.expectedResults {\n\t\t\t\trun := runs.FindByName(unitName)\n\t\t\t\trequire.NotNil(t, run, \"run %q not found in report\", unitName)\n\n\t\t\t\tassert.Equal(t, expected.result, run.Result, \"unexpected result for %q\", unitName)\n\n\t\t\t\tif expected.reason != \"\" {\n\t\t\t\t\trequire.NotNil(t, run.Reason, \"expected reason for %q but got nil\", unitName)\n\t\t\t\t\tassert.Equal(t, expected.reason, *run.Reason, \"unexpected reason for %q\", unitName)\n\t\t\t\t}\n\n\t\t\t\tif expected.cause != \"\" {\n\t\t\t\t\trequire.NotNil(t, run.Cause, \"expected cause for %q but got nil\", unitName)\n\t\t\t\t\tassert.Equal(t, expected.cause, *run.Cause, \"unexpected cause for %q\", unitName)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRunnerPoolDestroyFailFast(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureFailFast)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureFailFast)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureFailFast)\n\n\t_, stdout, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --fail-fast --working-dir \"+testPath+\"  -- apply\")\n\trequire.NoError(t, err)\n\n\t// Verify that there are no parsing errors in the output\n\trequire.NotContains(t, stdout, \"Error: Unsupported block type\")\n\trequire.NotContains(t, stdout, \"This object does not have an attribute named \\\"outputs\\\"\")\n\n\t// create fail.txt in unit-a to trigger a failure\n\thelpers.CreateFile(t, testPath, \"unit-b\", \"fail.txt\")\n\tstdout, stderr, _ := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --fail-fast --working-dir \"+testPath+\"  -- destroy\")\n\t// Check that error output contains terraform error details\n\tassert.Contains(t, stderr, \"level=error\")\n\t// Verify that unit-b failed\n\tassert.Contains(t, stderr, \"Failed to execute\")\n\tassert.Contains(t, stderr, \"in ./unit-b\")\n\tassert.NotContains(t, stdout, \"unit-b tf-path=\"+wrappedBinary()+\" msg=Destroy complete! Resources: 1 destroyed\")\n\tassert.NotContains(t, stdout, \"unit-a tf-path=\"+wrappedBinary()+\" msg=Destroy complete! Resources: 1 destroyed.\")\n}\n\nfunc TestRunnerPoolDestroyDependencies(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureFailFast)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureFailFast)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureFailFast)\n\ttestPath, err := filepath.EvalSymlinks(testPath)\n\trequire.NoError(t, err)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --fail-fast --working-dir \"+testPath+\"  -- apply\")\n\trequire.NoError(t, err)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --fail-fast --working-dir \"+testPath+\"  -- destroy\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"unit-b tf-path=\"+wrappedBinary()+\" msg=Destroy complete! Resources: 1 destroyed\")\n\tassert.Contains(t, stdout, \"unit-c tf-path=\"+wrappedBinary()+\" msg=Destroy complete! Resources: 1 destroyed\")\n\tassert.Contains(t, stdout, \"unit-a tf-path=\"+wrappedBinary()+\" msg=Destroy complete! Resources: 1 destroyed.\")\n}\n\nfunc TestRunnerPoolRemoteSource(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureRunnerPoolRemoteSource)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRunnerPoolRemoteSource)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureRunnerPoolRemoteSource)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --log-level debug --working-dir \"+testPath+\"  -- apply\")\n\trequire.NoError(t, err)\n\t// Verify that the output contains value produced from remote unit\n\trequire.Contains(t, stdout, \"data = \\\"unit-a\\\"\")\n}\n\nfunc TestRunnerPoolSourceMap(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureSourceMapSlashes)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureSourceMapSlashes)\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all --non-interactive \"+\n\t\t\t\"--source-map git::ssh://git@github.com/gruntwork-io/i-dont-exist.git=github.com/gruntwork-io/terragrunt.git?ref=v0.85.0 \"+\n\t\t\t\"--working-dir \"+testPath+\" -- apply \",\n\t)\n\trequire.NoError(t, err)\n\t// Verify that source map values are used\n\trequire.Contains(t, stderr, \"configurations from git::https://github.com/gruntwork-io/terragrunt.git?ref=v0.85.0\")\n}\n\n// TestAuthProviderParallelExecution verifies that --auth-provider-cmd is executed in parallel\n// for multiple units during the resolution phase.\n//\n// The test works by:\n// 1. Running terragrunt with --auth-provider-cmd pointing to a script that:\n//   - Creates lock files to coordinate between concurrent invocations\n//   - Detects when multiple auth commands are running simultaneously\n//   - Logs \"Auth concurrent\" when it detects parallel execution\n//     2. Parsing the output to find \"Auth concurrent\" messages\n//     3. Verifying that at least one auth command detected concurrent execution\n//     (which is deterministic proof of parallelism)\nfunc TestAuthProviderParallelExecution(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureAuthProviderParallel)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureAuthProviderParallel)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureAuthProviderParallel)\n\t// Resolve symlinks to avoid path mismatches on macOS where /var -> /private/var\n\ttestPath, err := filepath.EvalSymlinks(testPath)\n\trequire.NoError(t, err)\n\n\tauthProviderScript := filepath.Join(testPath, \"auth-provider.sh\")\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all --non-interactive --auth-provider-cmd \"+authProviderScript+\" --working-dir \"+testPath+\" -- validate\",\n\t)\n\trequire.NoError(t, err)\n\n\tstartCount := strings.Count(stderr, \"Auth start\")\n\tendCount := strings.Count(stderr, \"Auth end\")\n\n\treConcurrent := regexp.MustCompile(`Auth concurrent.*detected=(\\d+)`)\n\tmatches := reConcurrent.FindAllStringSubmatch(stderr, -1)\n\n\tmaxConcurrent := 0\n\n\tfor _, match := range matches {\n\t\tdetected, convErr := strconv.Atoi(match[1])\n\t\trequire.NoError(t, convErr, \"Invalid detected count in stderr: %q\", match[0])\n\n\t\tif detected > maxConcurrent {\n\t\t\tmaxConcurrent = detected\n\t\t}\n\n\t\tt.Logf(\"Auth command detected %d concurrent executions\", detected)\n\t}\n\n\t// Log start/end counts but don't fail - concurrent detection is the real proof of parallelism.\n\t// Due to timing and log buffering, start/end events may not always be captured reliably.\n\tt.Logf(\"Auth start events: %d, end events: %d\", startCount, endCount)\n\n\t// The concurrent detection is the key proof of parallel execution.\n\t// If auth commands detected other concurrent commands, parallelism is working.\n\tassert.GreaterOrEqual(t, len(matches), 1,\n\t\t\"Expected at least one auth command to detect concurrent execution. \"+\n\t\t\t\"This would prove parallel execution. If this fails, auth commands may be running sequentially.\")\n\tassert.GreaterOrEqual(t, maxConcurrent, 2,\n\t\t\"Expected auth commands to detect at least 2 concurrent executions. \"+\n\t\t\t\"Detected max concurrent: %d. This proves parallel execution.\", maxConcurrent)\n\n\thelpers.ValidateAuthProviderScript(t, testPath, authProviderScript)\n}\n"
  },
  {
    "path": "test/integration_s3_encryption_test.go",
    "content": "//go:build aws\n\npackage test_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3\"\n\t\"github.com/aws/aws-sdk-go-v2/service/s3/types\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n)\n\nconst (\n\ts3SSEAESFixturePath            = \"fixtures/s3-encryption/sse-aes\"\n\ts3SSECustomKeyFixturePath      = \"fixtures/s3-encryption/custom-key\"\n\ts3SSBasicEncryptionFixturePath = \"fixtures/s3-encryption/basic-encryption\"\n\ts3SSEKMSFixturePath            = \"fixtures/s3-encryption/sse-kms\"\n)\n\nfunc TestAwsS3SSEAES(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, s3SSEAESFixturePath)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, s3SSEAESFixturePath)\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\tdefer cleanupTableForTest(t, lockTableName, helpers.TerraformRemoteStateS3Region)\n\n\ttmpTerragruntConfigPath := helpers.CreateTmpTerragruntConfig(t, s3SSEAESFixturePath, s3BucketName, lockTableName, config.DefaultTerragruntConfigPath)\n\n\thelpers.RunTerragrunt(t, applyCommand(tmpTerragruntConfigPath, testPath))\n\n\tclient := helpers.CreateS3ClientForTest(t, helpers.TerraformRemoteStateS3Region)\n\tresp, err := client.GetBucketEncryption(t.Context(), &s3.GetBucketEncryptionInput{Bucket: aws.String(s3BucketName)})\n\trequire.NoError(t, err)\n\trequire.Len(t, resp.ServerSideEncryptionConfiguration.Rules, 1)\n\tsseRule := resp.ServerSideEncryptionConfiguration.Rules[0].ApplyServerSideEncryptionByDefault\n\trequire.NotNil(t, sseRule)\n\tassert.Equal(t, types.ServerSideEncryptionAes256, sseRule.SSEAlgorithm)\n\tassert.Nil(t, sseRule.KMSMasterKeyID)\n}\n\nfunc TestAwsS3SSECustomKey(t *testing.T) {\n\tt.Parallel()\n\n\t// Note: This test requires a KMS key with alias 'alias/dedicated-test-key' to exist in the AWS account.\n\t// If the test fails with KMS key not found errors, you need to create the key first:\n\t// aws kms create-key --description \"Test key for Terragrunt integration tests\"\n\t// aws kms create-alias --alias-name alias/dedicated-test-key --target-key-id KEY_ID\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, s3SSECustomKeyFixturePath)\n\ttestPath := filepath.Join(tmpEnvPath, s3SSECustomKeyFixturePath)\n\thelpers.CleanupTerraformFolder(t, testPath)\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\tdefer cleanupTableForTest(t, lockTableName, helpers.TerraformRemoteStateS3Region)\n\n\ttmpTerragruntConfigPath := helpers.CreateTmpTerragruntConfig(t, s3SSECustomKeyFixturePath, s3BucketName, lockTableName, config.DefaultTerragruntConfigPath)\n\thelpers.RunTerragrunt(t, applyCommand(tmpTerragruntConfigPath, testPath))\n\n\tclient := helpers.CreateS3ClientForTest(t, helpers.TerraformRemoteStateS3Region)\n\tresp, err := client.GetBucketEncryption(t.Context(), &s3.GetBucketEncryptionInput{Bucket: aws.String(s3BucketName)})\n\trequire.NoError(t, err)\n\trequire.Len(t, resp.ServerSideEncryptionConfiguration.Rules, 1)\n\tsseRule := resp.ServerSideEncryptionConfiguration.Rules[0].ApplyServerSideEncryptionByDefault\n\trequire.NotNil(t, sseRule)\n\tassert.Equal(t, types.ServerSideEncryptionAwsKms, sseRule.SSEAlgorithm)\n\tassert.True(t, strings.HasSuffix(aws.ToString(sseRule.KMSMasterKeyID), \"alias/dedicated-test-key\"))\n\n\t// Replace the custom key with a new one, and check that the key is updated in s3\n\thelpers.CleanupTerraformFolder(t, testPath)\n\n\tcontents, err := util.ReadFileAsString(tmpTerragruntConfigPath)\n\trequire.NoError(t, err)\n\n\terr = os.Remove(tmpTerragruntConfigPath)\n\trequire.NoError(t, err)\n\n\tcontents = strings.ReplaceAll(contents, \"dedicated-test-key\", \"other-dedicated-test-key\")\n\n\terr = os.WriteFile(tmpTerragruntConfigPath, []byte(contents), 0444)\n\trequire.NoError(t, err)\n\n\thelpers.RunTerragrunt(t, applyCommand(tmpTerragruntConfigPath, testPath))\n\n\tresp, err = client.GetBucketEncryption(t.Context(), &s3.GetBucketEncryptionInput{Bucket: aws.String(s3BucketName)})\n\trequire.NoError(t, err)\n\trequire.Len(t, resp.ServerSideEncryptionConfiguration.Rules, 1)\n\tsseRule = resp.ServerSideEncryptionConfiguration.Rules[0].ApplyServerSideEncryptionByDefault\n\trequire.NotNil(t, sseRule)\n\tassert.Equal(t, types.ServerSideEncryptionAwsKms, sseRule.SSEAlgorithm)\n\n\t// This check is asserting that the following bug still isn't fixed:\n\t// https://github.com/gruntwork-io/terragrunt/issues/3364\n\t//\n\t// There were unanticipated consequences to addressing it that should be resolved before the fix is implemented:\n\t// https://github.com/gruntwork-io/terragrunt/issues/3384\n\t//\n\t// At the very least, it should be documented as a breaking change.\n\tassert.False(t, strings.HasSuffix(aws.ToString(sseRule.KMSMasterKeyID), \"alias/other-dedicated-test-key\"))\n}\n\nfunc TestAwsS3SSEKeyNotReverted(t *testing.T) {\n\tt.Parallel()\n\n\t// Note: This test requires a KMS key with alias 'alias/dedicated-test-key' to exist in the AWS account.\n\t// If the test fails with KMS key not found errors, you need to create the key first:\n\t// aws kms create-key --description \"Test key for Terragrunt integration tests\"\n\t// aws kms create-alias --alias-name alias/dedicated-test-key --target-key-id KEY_ID\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, s3SSBasicEncryptionFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, s3SSBasicEncryptionFixturePath)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\tdefer cleanupTableForTest(t, lockTableName, helpers.TerraformRemoteStateS3Region)\n\n\ttmpTerragruntConfigPath := helpers.CreateTmpTerragruntConfig(t, rootPath, s3BucketName, lockTableName, config.DefaultTerragruntConfigPath)\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --backend-bootstrap --non-interactive --working-dir \"+filepath.Dir(tmpTerragruntConfigPath))\n\trequire.NoError(t, err)\n\n\toutput := stdout + stderr\n\n\t// verify that bucket encryption message is not printed\n\tassert.NotContains(t, output, \"Bucket Server-Side Encryption\")\n\n\ttmpTerragruntConfigPath = helpers.CreateTmpTerragruntConfig(t, rootPath, s3BucketName, lockTableName, config.DefaultTerragruntConfigPath)\n\tstdout, stderr, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --backend-bootstrap --non-interactive --working-dir \"+filepath.Dir(tmpTerragruntConfigPath))\n\trequire.NoError(t, err)\n\n\toutput = stdout + stderr\n\tassert.NotContains(t, output, \"Bucket Server-Side Encryption\")\n\n\t// verify that encryption key is not reverted\n\tclient := helpers.CreateS3ClientForTest(t, helpers.TerraformRemoteStateS3Region)\n\tresp, err := client.GetBucketEncryption(t.Context(), &s3.GetBucketEncryptionInput{Bucket: aws.String(s3BucketName)})\n\trequire.NoError(t, err)\n\trequire.Len(t, resp.ServerSideEncryptionConfiguration.Rules, 1)\n\tsseRule := resp.ServerSideEncryptionConfiguration.Rules[0].ApplyServerSideEncryptionByDefault\n\trequire.NotNil(t, sseRule)\n\tassert.Equal(t, types.ServerSideEncryptionAwsKms, sseRule.SSEAlgorithm)\n\n\tassert.True(t, strings.HasSuffix(aws.ToString(sseRule.KMSMasterKeyID), \"alias/dedicated-test-key\"))\n}\n\nfunc TestAwsS3EncryptionWarning(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, s3SSEKMSFixturePath)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, s3SSEKMSFixturePath)\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(helpers.UniqueID())\n\n\tcreateS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\tdefer cleanupTableForTest(t, lockTableName, helpers.TerraformRemoteStateS3Region)\n\n\ttmpTerragruntConfigPath := helpers.CreateTmpTerragruntConfig(t, s3SSEKMSFixturePath, s3BucketName, lockTableName, config.DefaultTerragruntConfigPath)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, applyCommand(tmpTerragruntConfigPath, testPath))\n\trequire.NoError(t, err)\n\n\toutput := stdout + stderr\n\t// check that warning is printed\n\tassert.Contains(t, output, \"Encryption is not enabled on the S3 remote state bucket \"+s3BucketName)\n\n\t// verify that encryption configuration is set\n\tclient := helpers.CreateS3ClientForTest(t, helpers.TerraformRemoteStateS3Region)\n\tresp, err := client.GetBucketEncryption(t.Context(), &s3.GetBucketEncryptionInput{Bucket: aws.String(s3BucketName)})\n\trequire.NoError(t, err)\n\trequire.Len(t, resp.ServerSideEncryptionConfiguration.Rules, 1)\n\tsseRule := resp.ServerSideEncryptionConfiguration.Rules[0].ApplyServerSideEncryptionByDefault\n\trequire.NotNil(t, sseRule)\n\tassert.Equal(t, types.ServerSideEncryptionAwsKms, sseRule.SSEAlgorithm)\n\n\t// check that second warning is not printed\n\tstdout, stderr, err = helpers.RunTerragruntCommandWithOutput(t, applyCommand(tmpTerragruntConfigPath, testPath))\n\trequire.NoError(t, err)\n\n\toutput = stdout + stderr\n\tassert.NotContains(t, output, \"Encryption is not enabled on the S3 remote state bucket \"+s3BucketName)\n}\n\nfunc TestAwsSkipBackend(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, s3SSEAESFixturePath)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, s3SSEAESFixturePath)\n\n\t// Fill placeholders in the config (bucket and table are intentionally invalid).\n\t// Use config in working directory to ensure lock file is copied to correct location.\n\tconfigPath := filepath.Join(testPath, config.DefaultTerragruntConfigPath)\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, configPath, configPath, \"N/A\", \"N/A\", helpers.TerraformRemoteStateS3Region)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt init --backend-bootstrap --non-interactive --working-dir \"+testPath+\" -backend=false\")\n\trequire.Error(t, err)\n\n\tdotTerraformDir := filepath.Join(testPath, \".terraform\")\n\tassert.False(t, util.FileExists(dotTerraformDir), \".terraform directory %s exists\", dotTerraformDir)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt init --non-interactive --working-dir \"+testPath+\" --disable-bucket-update -backend=false\")\n\trequire.NoError(t, err)\n\n\t// .terraform is created in the cache directory, not the original config directory\n\tcacheDir := helpers.FindCacheWorkingDir(t, testPath)\n\tcacheDotTerraformDir := filepath.Join(cacheDir, \".terraform\")\n\tassert.True(t, util.FileExists(cacheDotTerraformDir), \".terraform directory %s does not exist\", cacheDotTerraformDir)\n}\n\nfunc applyCommand(configPath, fixturePath string) string {\n\treturn fmt.Sprintf(\"terragrunt apply -auto-approve --backend-bootstrap --non-interactive --config %s --working-dir %s\", configPath, fixturePath)\n}\n"
  },
  {
    "path": "test/integration_scaffold_ssh_test.go",
    "content": "//go:build ssh\n\n// We don't want contributors to have to install SSH keys to run these tests, so we skip\n// them by default. Contributors need to opt in to run these tests by setting the\n// build flag `ssh` when running the tests. This is done by adding the `-tags ssh` flag\n// to the `go test` command. For example:\n//\n// go test -tags ssh ./...\n\npackage test_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestScaffoldModuleGit                 = \"git@github.com:gruntwork-io/terragrunt.git//test/fixtures/scaffold/scaffold-module\"\n\ttestScaffoldTemplateModule            = \"git@github.com:gruntwork-io/terragrunt.git//test/fixtures/scaffold/module-with-template\"\n\ttestScaffoldExternalTemplateModule    = \"git@github.com:gruntwork-io/terragrunt.git//test/fixtures/scaffold/external-template/template\"\n\ttestScaffoldWithCustomDefaultTemplate = \"fixtures/scaffold/custom-default-template\"\n)\n\nfunc TestSSHScaffoldWithCustomDefaultTemplate(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testScaffoldWithCustomDefaultTemplate)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testScaffoldWithCustomDefaultTemplate)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\n\t\t\"terragrunt --non-interactive --working-dir %s scaffold %s\",\n\t\tfilepath.Join(testPath, \"unit\"),\n\t\ttestScaffoldModuleURL,\n\t))\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\n\tassert.FileExists(t, filepath.Join(testPath, \"unit\", \"terragrunt.hcl\"))\n\tassert.FileExists(t, filepath.Join(testPath, \"unit\", \"external-template.txt\"))\n}\n\nfunc TestSSHScaffoldModuleExternalTemplate(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt scaffold --non-interactive --working-dir %s %s %s\", tmpEnvPath, testScaffoldModuleGit, testScaffoldExternalTemplateModule))\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\t// check that exists file from external template\n\tassert.FileExists(t, tmpEnvPath+\"/external-template.txt\")\n\tassert.FileExists(t, tmpEnvPath+\"/dependency/dependency.txt\")\n}\n\nfunc TestSSHScaffoldModuleDifferentRevisionAndSSH(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt scaffold --non-interactive --working-dir %s %s --var=Ref=v0.67.4 --var=SourceUrlType=git-ssh\", tmpEnvPath, testScaffoldModuleShort))\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"git::ssh://git@github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=v0.67.4\")\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n}\n\nfunc TestSSHScaffoldModuleSSH(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt scaffold --non-interactive --working-dir %s %s\", tmpEnvPath, testScaffoldModuleGit))\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n}\n\nfunc TestSSHScaffoldModuleTemplate(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt scaffold --non-interactive --working-dir %s %s\", tmpEnvPath, testScaffoldTemplateModule))\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\t// check that exists file from .boilerplate dir\n\tassert.FileExists(t, tmpEnvPath+\"/template-file.txt\")\n}\n\nfunc TestSSHScaffoldModuleVarFile(t *testing.T) {\n\tt.Parallel()\n\t// generate var file with specific version, without root include and use GIT/SSH to clone module.\n\tvarFileContent := `\nRef: v0.67.4\nEnableRootInclude: false\nSourceUrlType: \"git-ssh\"\n`\n\tvarFile := filepath.Join(helpers.TmpDirWOSymlinks(t), \"var-file.yaml\")\n\terr := os.WriteFile(varFile, []byte(varFileContent), 0644)\n\trequire.NoError(t, err)\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt scaffold --non-interactive --working-dir %s %s --var-file=%s\", tmpEnvPath, testScaffoldModuleShort, varFile))\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"git::ssh://git@github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=v0.67.4\")\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\n\tcontent, err := util.ReadFileAsString(tmpEnvPath + \"/terragrunt.hcl\")\n\trequire.NoError(t, err)\n\tassert.NotContains(t, content, \"find_in_parent_folders\")\n}\n"
  },
  {
    "path": "test/integration_scaffold_test.go",
    "content": "package test_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestScaffoldModuleURL           = \"https://github.com/gruntwork-io/terragrunt.git//test/fixtures/scaffold/scaffold-module\"\n\ttestScaffoldModuleShort         = \"github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs\"\n\ttestScaffoldLocalModulePath     = \"fixtures/scaffold/scaffold-module\"\n\ttestScaffoldWithRootHCL         = \"fixtures/scaffold/root-hcl\"\n\ttestScaffold3rdPartyModulePath  = \"git::https://github.com/Azure/terraform-azurerm-avm-res-compute-virtualmachine.git//.?ref=v0.15.0\"\n\ttestScaffoldNoDependencyPrompt  = \"fixtures/scaffold/dependency-prompt-template\"\n\ttestScaffoldLocalTofuModulePath = \"fixtures/scaffold/scaffold-module-tofu\"\n)\n\nfunc TestScaffoldModule(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt scaffold --non-interactive --working-dir %s %s\", tmpEnvPath, testScaffoldModuleURL))\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\tassert.FileExists(t, tmpEnvPath+\"/terragrunt.hcl\")\n}\n\nfunc TestScaffoldModuleShortUrl(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt scaffold --non-interactive --working-dir %s %s\", tmpEnvPath, testScaffoldModuleShort))\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\t// check that find_in_parent_folders is generated in terragrunt.hcl\n\tcontent, err := util.ReadFileAsString(tmpEnvPath + \"/terragrunt.hcl\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, content, \"find_in_parent_folders\")\n}\n\nfunc TestScaffoldModuleShortUrlNoRootInclude(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt scaffold --non-interactive --working-dir %s %s --var=EnableRootInclude=false\", tmpEnvPath, testScaffoldModuleShort))\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\t// check that find_in_parent_folders is NOT generated in  terragrunt.hcl\n\tcontent, err := util.ReadFileAsString(tmpEnvPath + \"/terragrunt.hcl\")\n\trequire.NoError(t, err)\n\tassert.NotContains(t, content, \"find_in_parent_folders\")\n}\n\nfunc TestScaffoldModuleDifferentRevision(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt scaffold --non-interactive --working-dir %s %s --var=Ref=v0.67.4\", tmpEnvPath, testScaffoldModuleShort))\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=v0.67.4\")\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n}\n\nfunc TestScaffoldErrorNoModuleUrl(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt scaffold --non-interactive --working-dir \"+tmpEnvPath)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"No module URL passed\")\n}\n\nfunc TestScaffoldLocalTofuModule(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\n\tworkingDir, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt scaffold --non-interactive --working-dir %s %s\", tmpEnvPath, fmt.Sprintf(\"%s//%s\", workingDir, testScaffoldLocalTofuModulePath)))\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\tassert.FileExists(t, tmpEnvPath+\"/terragrunt.hcl\")\n\n\t// Verify variables from .tofu files were parsed and included in generated config\n\tcontent, err := util.ReadFileAsString(tmpEnvPath + \"/terragrunt.hcl\")\n\trequire.NoError(t, err)\n\tassert.Contains(t, content, \"project_name\", \"Required variable from variables.tofu should be in generated config\")\n\tassert.Contains(t, content, \"open_port\", \"Required variable from variables.tofu should be in generated config\")\n\tassert.Contains(t, content, \"enable_backups\", \"Required variable from variables.tofu should be in generated config\")\n\tassert.Contains(t, content, \"users\", \"Required variable from variables.tofu should be in generated config\")\n\tassert.Contains(t, content, \"policy_map\", \"Required variable from variables.tofu should be in generated config\")\n}\n\nfunc TestScaffoldLocalModule(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\n\tworkingDir, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt scaffold --non-interactive --working-dir %s %s\", tmpEnvPath, fmt.Sprintf(\"%s//%s\", workingDir, testScaffoldLocalModulePath)))\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\tassert.FileExists(t, tmpEnvPath+\"/terragrunt.hcl\")\n}\n\nfunc TestScaffold3rdPartyModule(t *testing.T) {\n\tt.Parallel()\n\n\ttmpRoot := helpers.TmpDirWOSymlinks(t)\n\n\ttmpEnvPath := filepath.Join(tmpRoot, \"app\")\n\terr := os.MkdirAll(tmpEnvPath, 0755)\n\trequire.NoError(t, err)\n\n\t// create \"root\" terragrunt.hcl\n\terr = os.WriteFile(filepath.Join(tmpRoot, \"terragrunt.hcl\"), []byte(\"\"), 0644)\n\trequire.NoError(t, err)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt scaffold --non-interactive --working-dir %s %s\", tmpEnvPath, testScaffold3rdPartyModulePath))\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\tassert.FileExists(t, tmpEnvPath+\"/terragrunt.hcl\")\n\n\t// validate the generated files\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt hcl validate --non-interactive --working-dir \"+tmpEnvPath)\n\trequire.NoError(t, err)\n}\n\nfunc TestScaffoldOutputFolderFlag(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\n\toutputFolder := tmpEnvPath + \"/foo/bar\"\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt --non-interactive --working-dir %s scaffold %s --output-folder %s\", tmpEnvPath, testScaffoldModuleURL, outputFolder))\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\tassert.FileExists(t, outputFolder+\"/terragrunt.hcl\")\n}\n\nfunc TestScaffoldWithRootHCL(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testScaffoldWithRootHCL)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testScaffoldWithRootHCL)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\n\t\t\"terragrunt --non-interactive --working-dir %s scaffold %s\",\n\t\tfilepath.Join(testPath, \"unit\"),\n\t\ttestScaffoldModuleURL,\n\t))\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\n\tassert.FileExists(t, filepath.Join(testPath, \"unit\", \"terragrunt.hcl\"))\n\n\t// Read the file\n\tcontent, err := util.ReadFileAsString(filepath.Join(testPath, \"unit\", \"terragrunt.hcl\"))\n\trequire.NoError(t, err)\n\tassert.Contains(t, content, `path = find_in_parent_folders(\"root.hcl\")`)\n}\n\nfunc TestScaffoldNoDependencyPrompt(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\n\tworkingDir, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\tlocalBoilerplateModuleDir := fmt.Sprintf(\"%s/%s//.\", workingDir, testScaffoldNoDependencyPrompt)\n\n\toutputFolder := tmpEnvPath + \"/foo/bar\"\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt scaffold --non-interactive --working-dir %s --no-dependency-prompt %s --output-folder %s\", tmpEnvPath, localBoilerplateModuleDir, outputFolder))\n\trequire.NoError(t, err)\n\tassert.NotContains(t, stderr, \"This boilerplate template has a dependency!\")\n\tassert.FileExists(t, outputFolder+\"/base/test.hcl\")\n\tassert.FileExists(t, outputFolder+\"/leaf/terragrunt.hcl\")\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n}\n\nfunc TestScaffoldWithShellCommandsEnabled(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\tworkingDir, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\ttemplatePath := workingDir + \"//fixtures/scaffold/with-shell-commands\"\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt scaffold --non-interactive --working-dir %s %s\",\n\t\t\ttmpEnvPath,\n\t\t\ttemplatePath,\n\t\t),\n\t)\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\n\tcontent, err := util.ReadFileAsString(filepath.Join(tmpEnvPath, \"terragrunt.hcl\"))\n\trequire.NoError(t, err)\n\n\tassert.NotContains(t, content, \"{{ shell\", \"Shell template should be processed\")\n\tassert.Contains(t, content, \"SHELL_EXECUTED_VALUE_1\", \"Shell command output should be present\")\n\tassert.Contains(t, content, \"SHELL_EXECUTED_VALUE_2\", \"Shell command output should be present\")\n}\n\nfunc TestScaffoldWithShellCommandsDisabled(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\tworkingDir, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\ttemplatePath := workingDir + \"//fixtures/scaffold/with-shell-commands\"\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt scaffold --non-interactive --no-shell --working-dir %s %s\",\n\t\t\ttmpEnvPath,\n\t\t\ttemplatePath,\n\t\t),\n\t)\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\n\tcontent, err := util.ReadFileAsString(filepath.Join(tmpEnvPath, \"terragrunt.hcl\"))\n\trequire.NoError(t, err)\n\n\tassert.NotContains(t, content, \"SHELL_EXECUTED_VALUE_1\", \"Shell command should not have executed\")\n\tassert.NotContains(t, content, \"SHELL_EXECUTED_VALUE_2\", \"Shell command should not have executed\")\n}\n\nfunc TestScaffoldWithHooksEnabled(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\tworkingDir, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\ttemplatePath := workingDir + \"//fixtures/scaffold/with-hooks\"\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt scaffold --non-interactive --working-dir %s %s\",\n\t\t\ttmpEnvPath,\n\t\t\ttemplatePath,\n\t\t),\n\t)\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\n\tcontent, err := util.ReadFileAsString(filepath.Join(tmpEnvPath, \"terragrunt.hcl\"))\n\trequire.NoError(t, err)\n\tassert.Contains(t, content, \"terraform {\", \"Generated file should be valid\")\n}\n\nfunc TestScaffoldWithHooksDisabled(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\tworkingDir, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\ttemplatePath := workingDir + \"//fixtures/scaffold/with-hooks\"\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt scaffold --non-interactive --no-hooks --working-dir %s %s\",\n\t\t\ttmpEnvPath,\n\t\t\ttemplatePath,\n\t\t),\n\t)\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\n\tcontent, err := util.ReadFileAsString(filepath.Join(tmpEnvPath, \"terragrunt.hcl\"))\n\trequire.NoError(t, err)\n\tassert.Contains(t, content, \"terraform {\", \"Generated file should be valid\")\n}\n\nfunc TestScaffoldWithBothFlagsDisabled(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\tworkingDir, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\ttemplatePath := workingDir + \"//fixtures/scaffold/with-shell-and-hooks\"\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt scaffold --non-interactive --no-shell --no-hooks --working-dir %s %s\",\n\t\t\ttmpEnvPath,\n\t\t\ttemplatePath,\n\t\t),\n\t)\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\n\tcontent, err := util.ReadFileAsString(filepath.Join(tmpEnvPath, \"terragrunt.hcl\"))\n\trequire.NoError(t, err)\n\n\tassert.NotContains(t, content, \"SHELL_OUTPUT_1\", \"Shell command should not have executed\")\n\tassert.NotContains(t, content, \"SHELL_OUTPUT_2\", \"Shell command should not have executed\")\n\n\tassert.Contains(t, content, \"terraform {\", \"Generated file should be valid\")\n}\n\nfunc TestScaffoldCatalogConfigIntegration(t *testing.T) {\n\tt.Parallel()\n\n\tworkingDir, err := os.Getwd()\n\trequire.NoError(t, err)\n\n\tcatalogConfigPath := filepath.Join(workingDir, \"fixtures/scaffold/catalog-config-test/terragrunt.hcl\")\n\ttemplatePath := workingDir + \"//fixtures/scaffold/with-shell-and-hooks\"\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\n\tcatalogContent, err := util.ReadFileAsString(catalogConfigPath)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(tmpEnvPath, \"terragrunt.hcl\"), []byte(catalogContent), 0644)\n\trequire.NoError(t, err)\n\n\toutputDir := filepath.Join(tmpEnvPath, \"output\")\n\terr = os.MkdirAll(outputDir, 0755)\n\trequire.NoError(t, err)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt scaffold --non-interactive --working-dir %s --output-folder %s %s\",\n\t\t\ttmpEnvPath,\n\t\t\toutputDir,\n\t\t\ttemplatePath,\n\t\t),\n\t)\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Scaffolding completed\")\n\n\tcontent, err := util.ReadFileAsString(filepath.Join(outputDir, \"terragrunt.hcl\"))\n\trequire.NoError(t, err)\n\n\tassert.NotContains(t, content, \"SHELL_OUTPUT_1\", \"Shell should be disabled by catalog config\")\n\tassert.NotContains(t, content, \"SHELL_OUTPUT_2\", \"Shell should be disabled by catalog config\")\n\n\tassert.Contains(t, content, \"terraform {\", \"Generated file should be valid\")\n}\n"
  },
  {
    "path": "test/integration_serial_aws_test.go",
    "content": "//go:build aws\n\npackage test_test\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n)\n\nfunc TestTerragruntParallelism(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedTimings        []int\n\t\tparallelism            int\n\t\tnumberOfModules        int\n\t\ttimeToDeployEachModule time.Duration\n\t}{\n\t\t{parallelism: 1, numberOfModules: 10, timeToDeployEachModule: 5 * time.Second, expectedTimings: []int{5, 10, 15, 20, 25, 30, 35, 40, 45, 50}},\n\t\t{parallelism: 3, numberOfModules: 10, timeToDeployEachModule: 5 * time.Second, expectedTimings: []int{5, 5, 5, 10, 10, 10, 15, 15, 15, 20}},\n\t\t{parallelism: 5, numberOfModules: 10, timeToDeployEachModule: 5 * time.Second, expectedTimings: []int{5, 5, 5, 5, 5, 5, 5, 5, 5, 5}},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"parallelism=%d numberOfModules=%d timeToDeployEachModule=%v expectedTimings=%v\", tc.parallelism, tc.numberOfModules, tc.timeToDeployEachModule, tc.expectedTimings), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttestTerragruntParallelism(t, tc.parallelism, tc.numberOfModules, tc.timeToDeployEachModule, tc.expectedTimings)\n\t\t})\n\t}\n}\n\n//nolint:paralleltest\nfunc TestReadTerragruntAuthProviderCmdRemoteState(t *testing.T) {\n\thelpers.CleanupTerraformFolder(t, testFixtureAuthProviderCmd)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureAuthProviderCmd)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureAuthProviderCmd, \"remote-state\")\n\tmockAuthCmd := filepath.Join(tmpEnvPath, testFixtureAuthProviderCmd, \"mock-auth-cmd.sh\")\n\n\thelpers.ValidateAuthProviderScript(t, rootPath, mockAuthCmd)\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tdefer helpers.DeleteS3Bucket(t, helpers.TerraformRemoteStateS3Region, s3BucketName)\n\n\trootTerragruntConfigPath := filepath.Join(rootPath, config.DefaultTerragruntConfigPath)\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(\n\t\tt,\n\t\trootTerragruntConfigPath,\n\t\trootTerragruntConfigPath,\n\t\ts3BucketName,\n\t\t\"not-used\",\n\t\thelpers.TerraformRemoteStateS3Region,\n\t)\n\n\taccessKeyID := os.Getenv(\"AWS_ACCESS_KEY_ID\")\n\tsecretAccessKey := os.Getenv(\"AWS_SECRET_ACCESS_KEY\")\n\n\t// I'm not sure why, but this test doesn't work with tenv\n\tos.Setenv(\"AWS_ACCESS_KEY_ID\", \"\")     //nolint: usetesting\n\tos.Setenv(\"AWS_SECRET_ACCESS_KEY\", \"\") //nolint: usetesting\n\n\tdefer func() {\n\t\tos.Setenv(\"AWS_ACCESS_KEY_ID\", accessKeyID)         //nolint: usetesting\n\t\tos.Setenv(\"AWS_SECRET_ACCESS_KEY\", secretAccessKey) //nolint: usetesting\n\t}()\n\n\tcredsConfig := filepath.Join(rootPath, \"creds.config\")\n\n\thelpers.CopyAndFillMapPlaceholders(t, credsConfig, credsConfig, map[string]string{\n\t\t\"__FILL_AWS_ACCESS_KEY_ID__\":     accessKeyID,\n\t\t\"__FILL_AWS_SECRET_ACCESS_KEY__\": secretAccessKey,\n\t})\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt plan --non-interactive --working-dir %s --auth-provider-cmd %s\", rootPath, mockAuthCmd))\n}\n\nfunc TestReadTerragruntAuthProviderCmdCredsForDependency(t *testing.T) {\n\thelpers.CleanupTerraformFolder(t, testFixtureAuthProviderCmd)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureAuthProviderCmd)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureAuthProviderCmd, \"creds-for-dependency\")\n\tmockAuthCmd := filepath.Join(tmpEnvPath, testFixtureAuthProviderCmd, \"mock-auth-cmd.sh\")\n\n\taccessKeyID := os.Getenv(\"AWS_ACCESS_KEY_ID\")\n\tsecretAccessKey := os.Getenv(\"AWS_SECRET_ACCESS_KEY\")\n\n\tt.Setenv(\"AWS_ACCESS_KEY_ID\", \"\")\n\tt.Setenv(\"AWS_SECRET_ACCESS_KEY\", \"\")\n\n\tdependencyCredsConfig := filepath.Join(rootPath, \"dependency\", \"creds.config\")\n\thelpers.CopyAndFillMapPlaceholders(t, dependencyCredsConfig, dependencyCredsConfig, map[string]string{\n\t\t\"__FILL_AWS_ACCESS_KEY_ID__\":     accessKeyID,\n\t\t\"__FILL_AWS_SECRET_ACCESS_KEY__\": secretAccessKey,\n\t})\n\n\tdependentCredsConfig := filepath.Join(rootPath, \"dependent\", \"creds.config\")\n\thelpers.CopyAndFillMapPlaceholders(t, dependentCredsConfig, dependentCredsConfig, map[string]string{\n\t\t\"__FILL_AWS_ACCESS_KEY_ID__\":     accessKeyID,\n\t\t\"__FILL_AWS_SECRET_ACCESS_KEY__\": secretAccessKey,\n\t})\n\n\thelpers.ValidateAuthProviderScript(t, rootPath, mockAuthCmd)\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all apply --non-interactive --working-dir %s --auth-provider-cmd %s\",\n\t\t\trootPath,\n\t\t\tmockAuthCmd,\n\t\t),\n\t)\n}\n\n// NOTE: the following test asserts precise timing for determining parallelism. As such, it can not be run in parallel\n// with all the other tests as the system load could impact the duration in which the parallel terragrunt goroutines\n// run.\n\nfunc testTerragruntParallelism(t *testing.T, parallelism int, numberOfModules int, timeToDeployEachModule time.Duration, expectedTimings []int) {\n\tt.Helper()\n\n\toutput, testStart, err := testRemoteFixtureParallelism(t, parallelism, numberOfModules, timeToDeployEachModule)\n\trequire.NoError(t, err)\n\n\t// parse output and sort the times, the regex captures a string in the format time.RFC3339 emitted by terraform's timestamp function\n\tregex := regexp.MustCompile(`out = \"([-:\\w]+)\"`)\n\n\tmatches := regex.FindAllStringSubmatch(output, -1)\n\tassert.Len(t, matches, numberOfModules)\n\n\tvar deploymentTimes = make([]int, 0, len(matches))\n\tfor _, match := range matches {\n\t\tparsedTime, err := time.Parse(time.RFC3339, match[1])\n\t\trequire.NoError(t, err)\n\n\t\tdeploymentTime := int(parsedTime.Unix()) - testStart\n\t\tdeploymentTimes = append(deploymentTimes, deploymentTime)\n\t}\n\n\tsort.Ints(deploymentTimes)\n\n\t// the reported times are skewed (running terragrunt/terraform apply adds a little bit of overhead)\n\t// we apply a simple scaling algorithm on the times based on the last expected time and the last actual time\n\tscalingFactor := float64(deploymentTimes[0]) / float64(expectedTimings[0])\n\t// find max skew time deploymentTimes vs expectedTimings\n\tfor i := 1; i < len(deploymentTimes); i++ {\n\t\tfactor := float64(deploymentTimes[i]) / float64(expectedTimings[i])\n\t\tif factor > scalingFactor {\n\t\t\tscalingFactor = factor\n\t\t}\n\t}\n\n\tscaledTimes := make([]float64, len(deploymentTimes))\n\tfor i, deploymentTime := range deploymentTimes {\n\t\tscaledTimes[i] = float64(deploymentTime) / scalingFactor\n\t}\n\n\tt.Logf(\"Parallelism test numberOfModules=%d p=%d expectedTimes=%v deploymentTimes=%v scaledTimes=%v scaleFactor=%f\", numberOfModules, parallelism, expectedTimings, deploymentTimes, scaledTimes, scalingFactor)\n\tmaxDiffInSeconds := 5.0 * scalingFactor\n\n\tfor i, scaledTime := range scaledTimes {\n\t\tdifference := math.Abs(scaledTime - float64(expectedTimings[i]))\n\t\tassert.LessOrEqual(t, difference, maxDiffInSeconds, \"Expected timing %d but got %f\", expectedTimings[i], scaledTime)\n\t}\n}\n\nfunc testRemoteFixtureParallelism(t *testing.T, parallelism int, numberOfModules int, timeToDeployEachModule time.Duration) (string, int, error) {\n\tt.Helper()\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\t// copy the template `numberOfModules` times into the app\n\ttmpEnvPath := helpers.TmpDirWOSymlinks(t)\n\tfor i := range numberOfModules {\n\t\terr := util.CopyFolderContents(createLogger(), testFixtureParallelism, tmpEnvPath, \".terragrunt-test\", nil, nil)\n\t\tif err != nil {\n\t\t\treturn \"\", 0, err\n\t\t}\n\n\t\terr = os.Rename(\n\t\t\tpath.Join(tmpEnvPath, \"template\"),\n\t\t\tpath.Join(tmpEnvPath, \"app\"+strconv.Itoa(i)))\n\t\tif err != nil {\n\t\t\treturn \"\", 0, err\n\t\t}\n\t}\n\n\trootTerragruntConfigPath := filepath.Join(tmpEnvPath, \"root.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, \"not-used\", \"not-used\")\n\n\tenvironmentPath := tmpEnvPath\n\n\t// forces plugin download & initialization (no parallelism control)\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt run --all plan --non-interactive --working-dir %s -var sleep_seconds=%d\", environmentPath, timeToDeployEachModule/time.Second))\n\n\t// NOTE: we can't run just run --all apply and not run --all plan because the time to initialize the plugins skews the results of the test\n\ttestStart := time.Now().Unix()\n\tt.Logf(\"run --all apply start time = %d, %s\", testStart, time.Now().Format(time.RFC3339))\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt run --all apply --parallelism %d --non-interactive --working-dir %s -var sleep_seconds=%d\", parallelism, environmentPath, timeToDeployEachModule/time.Second))\n\n\t// read the output of all modules 1 by 1 sequence, parallel reads mix outputs and make output complicated to parse\n\toutputParallelism := 1\n\t// Call helpers.RunTerragruntCommandWithOutput directly because this command contains failures (which causes helpers.RunTerragruntRedirectOutput to abort) but we don't care.\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt run --all output -no-color --tf-forward-stdout --non-interactive --working-dir %s --parallelism %d\", environmentPath, outputParallelism))\n\tif err != nil {\n\t\treturn \"\", 0, err\n\t}\n\n\treturn stdout, int(testStart), nil\n}\n"
  },
  {
    "path": "test/integration_serial_gcp_test.go",
    "content": "//go:build gcp\n\npackage test_test\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n)\n\nfunc TestGcpCorrectlyMirrorsTerraformGCPAuth(t *testing.T) {\n\t// We need to ensure Terragrunt works correctly when GOOGLE_CREDENTIALS are specified.\n\t// There is no true way to properly unset env vars from the environment, but we still try\n\t// to unset the CI credentials during this test.\n\tdefaultCreds := os.Getenv(\"GCLOUD_SERVICE_KEY\")\n\tdefer os.Setenv(\"GCLOUD_SERVICE_KEY\", defaultCreds) //nolint:usetesting\n\n\tos.Unsetenv(\"GCLOUD_SERVICE_KEY\") //nolint:usetesting\n\tt.Setenv(\"GOOGLE_CREDENTIALS\", defaultCreds)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGcsPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGcsPath)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\t// We need a project to create the bucket in, so we pull one from the recommended environment variable.\n\tproject := os.Getenv(\"GOOGLE_CLOUD_PROJECT\")\n\tgcsBucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\tdefer deleteGCSBucket(t, gcsBucketName)\n\n\ttmpTerragruntGCSConfigPath := createTmpTerragruntGCSConfig(t, rootPath, project, terraformRemoteStateGcpRegion, gcsBucketName, config.DefaultTerragruntConfigPath)\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply -auto-approve --backend-bootstrap --non-interactive --config %s --working-dir %s\", tmpTerragruntGCSConfigPath, rootPath))\n\n\tvar expectedGCSLabels = map[string]string{\n\t\t\"owner\": \"terragrunt_test\",\n\t\t\"name\":  \"terraform_state_storage\"}\n\tvalidateGCSBucketExistsAndIsLabeled(t, terraformRemoteStateGcpRegion, gcsBucketName, expectedGCSLabels)\n}\n\nfunc TestGcpWorksWithImpersonateBackend(t *testing.T) {\n\timpersonatorKey := os.Getenv(\"GCLOUD_SERVICE_KEY_IMPERSONATOR\")\n\tif impersonatorKey == \"\" {\n\t\tt.Fatalf(\"required environment variable `%s` - not found\", \"GCLOUD_SERVICE_KEY_IMPERSONATOR\")\n\t}\n\n\ttmpImpersonatorCreds := helpers.CreateTmpTerragruntConfigContent(t, impersonatorKey, \"impersonator-key.json\")\n\tdefaultCreds := os.Getenv(\"GCLOUD_SERVICE_KEY\")\n\n\tt.Setenv(\"GOOGLE_CREDENTIALS\", defaultCreds)\n\tdefer helpers.RemoveFile(t, tmpImpersonatorCreds)\n\n\tt.Setenv(\"GOOGLE_APPLICATION_CREDENTIALS\", tmpImpersonatorCreds)\n\n\tproject := os.Getenv(\"GOOGLE_CLOUD_PROJECT\")\n\tgcsBucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\t// run with impersonation\n\ttmpTerragruntImpersonateGCSConfigPath := createTmpTerragruntGCSConfig(t, testFixtureGcsImpersonatePath, project, terraformRemoteStateGcpRegion, gcsBucketName, config.DefaultTerragruntConfigPath)\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply -auto-approve --backend-bootstrap --non-interactive --config %s --working-dir %s\", tmpTerragruntImpersonateGCSConfigPath, testFixtureGcsImpersonatePath))\n\n\tvar expectedGCSLabels = map[string]string{\n\t\t\"owner\": \"terragrunt_test\",\n\t\t\"name\":  \"terraform_state_storage\"}\n\tvalidateGCSBucketExistsAndIsLabeled(t, terraformRemoteStateGcpRegion, gcsBucketName, expectedGCSLabels)\n\n\temail := os.Getenv(\"GOOGLE_IDENTITY_EMAIL\")\n\tattrs := gcsObjectAttrs(t, gcsBucketName, \"terraform.tfstate/default.tfstate\")\n\townerEmail := false\n\n\tfor _, a := range attrs.ACL {\n\t\tif (a.Role == \"OWNER\") && (a.Email == email) {\n\t\t\townerEmail = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tassert.True(t, ownerEmail, \"Identity email should match the impersonated account\")\n}\n"
  },
  {
    "path": "test/integration_serial_test.go",
    "content": "//nolint:paralleltest\npackage test_test\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/test\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/hashicorp/hcl/v2/hclwrite\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/info/print\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n)\n\n// NOTE: We don't run these tests in parallel because it modifies the environment variable, so it can affect other tests\n\nfunc TestTerragruntProviderCacheWithFilesystemMirror(t *testing.T) {\n\t// In this test we use os.Setenv to set the Terraform env var TF_CLI_CONFIG_FILE.\n\thelpers.CleanupTerraformFolder(t, testFixtureProviderCacheFilesystemMirror)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureProviderCacheFilesystemMirror)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureProviderCacheFilesystemMirror)\n\n\tappPath := filepath.Join(rootPath, \"app\")\n\tprovidersMirrorPath := filepath.Join(rootPath, \"providers-mirror\")\n\n\tfakeProvider := FakeProvider{\n\t\tRegistryName: \"example.com\",\n\t\tNamespace:    \"hashicorp\",\n\t\tName:         \"aws\",\n\t\tVersion:      \"5.59.0\",\n\t\tPlatformOS:   runtime.GOOS,\n\t\tPlatformArch: runtime.GOARCH,\n\t}\n\tfakeProvider.CreateMirror(t, providersMirrorPath)\n\n\tfakeProvider = FakeProvider{\n\t\tRegistryName: \"example.com\",\n\t\tNamespace:    \"hashicorp\",\n\t\tName:         \"azurerm\",\n\t\tVersion:      \"3.113.0\",\n\t\tPlatformOS:   runtime.GOOS,\n\t\tPlatformArch: runtime.GOARCH,\n\t}\n\tfakeProvider.CreateMirror(t, providersMirrorPath)\n\n\tproviderCacheDir := filepath.Join(rootPath, \"providers-cache\")\n\n\tctx := t.Context()\n\tdefer ctx.Done()\n\n\tcliConfigFilename, err := os.CreateTemp(helpers.TmpDirWOSymlinks(t), \"*\")\n\trequire.NoError(t, err)\n\n\tdefer cliConfigFilename.Close()\n\n\tt.Setenv(tf.EnvNameTFCLIConfigFile, cliConfigFilename.Name())\n\n\tt.Logf(\"%s=%s\", tf.EnvNameTFCLIConfigFile, cliConfigFilename.Name())\n\n\tcliConfigSettings := &test.CLIConfigSettings{\n\t\tFilesystemMirrorMethods: []test.CLIConfigProviderInstallationFilesystemMirror{\n\t\t\t{\n\t\t\t\tPath:    providersMirrorPath,\n\t\t\t\tInclude: []string{\"example.com/*/*\"},\n\t\t\t},\n\t\t},\n\t}\n\ttest.CreateCLIConfig(t, cliConfigFilename, cliConfigSettings)\n\n\texpectedProviderInstallation := `provider_installation { \"filesystem_mirror\" { include = [\"example.com/*/*\"] exclude = [\"example.com/*/*\", \"registry.opentofu.org/*/*\", \"registry.terraform.io/*/*\"] path = \"%s\" } \"filesystem_mirror\" { include = [\"example.com/*/*\", \"registry.opentofu.org/*/*\", \"registry.terraform.io/*/*\"] path = \"%s\" } \"direct\" { } }`\n\texpectedProviderInstallation = fmt.Sprintf(strings.Join(strings.Fields(expectedProviderInstallation), \" \"), providersMirrorPath, providerCacheDir)\n\n\t// Retry to handle intermittent failures due to network issues on CICD\n\trequire.NoError(t, util.DoWithRetry(t.Context(), \"Run terragrunt init with provider cache\", 3, 0, logger.CreateLogger(), log.DebugLevel, func(ctx context.Context) error {\n\t\t// Clean up before each attempt\n\t\thelpers.CleanupTerraformFolder(t, appPath)\n\n\t\t// Run terragrunt init\n\t\tstdout := bytes.Buffer{}\n\t\tstderr := bytes.Buffer{}\n\n\t\terr = helpers.RunTerragruntCommandWithContext(t, ctx, fmt.Sprintf(\"terragrunt run --all init --provider-cache --provider-cache-registry-names example.com --provider-cache-registry-names registry.opentofu.org --provider-cache-registry-names registry.terraform.io --provider-cache-dir %s --non-interactive --working-dir %s\", providerCacheDir, appPath), &stdout, &stderr)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"terragrunt command failed: %w\", err)\n\t\t}\n\n\t\t// Verify the config was created correctly - it's now in the cache directory\n\t\tcacheWorkingDir := helpers.FindCacheWorkingDir(t, appPath)\n\t\tif cacheWorkingDir == \"\" {\n\t\t\treturn fmt.Errorf(\"failed to find cache working directory for %s\", appPath)\n\t\t}\n\n\t\tterraformrcBytes, readErr := os.ReadFile(filepath.Join(cacheWorkingDir, \".terraformrc\"))\n\t\tif readErr != nil {\n\t\t\treturn fmt.Errorf(\"failed to read .terraformrc: %w\", readErr)\n\t\t}\n\n\t\tterraformrc := strings.Join(strings.Fields(string(terraformrcBytes)), \" \")\n\n\t\tif !strings.Contains(terraformrc, expectedProviderInstallation) {\n\t\t\treturn fmt.Errorf(\"config mismatch:\\nactual: %s\\nexpected substring: %s\", terraformrc, expectedProviderInstallation)\n\t\t}\n\n\t\treturn nil\n\t}))\n}\n\nfunc TestTerragruntProviderCacheWithNetworkMirror(t *testing.T) {\n\t// In this test we use os.Setenv to set the Terraform env var TF_CLI_CONFIG_FILE.\n\thelpers.CleanupTerraformFolder(t, testFixtureProviderCacheNetworkMirror)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureProviderCacheNetworkMirror)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureProviderCacheNetworkMirror)\n\n\tappsPath := filepath.Join(rootPath, \"apps\")\n\tprovidersNetkworMirrorPath := filepath.Join(rootPath, \"providers-network-mirror\")\n\tprovidersFilesystemMirrorPath := filepath.Join(rootPath, \"providers-filesystem-mirror\")\n\n\tctx, cancel := context.WithCancel(t.Context())\n\tdefer cancel()\n\n\tnetowrkProvider := FakeProvider{\n\t\tRegistryName: \"example.com\",\n\t\tNamespace:    \"hashicorp\",\n\t\tName:         \"aws\",\n\t\tVersion:      \"5.59.0\",\n\t\tPlatformOS:   runtime.GOOS,\n\t\tPlatformArch: runtime.GOARCH,\n\t}\n\tnetowrkProvider.CreateMirror(t, providersNetkworMirrorPath)\n\n\tfilesystemProvider := FakeProvider{\n\t\tRegistryName: \"example.com\",\n\t\tNamespace:    \"hashicorp\",\n\t\tName:         \"aws\",\n\t\tVersion:      \"5.58.0\",\n\t\tPlatformOS:   runtime.GOOS,\n\t\tPlatformArch: runtime.GOARCH,\n\t}\n\tfilesystemProvider.CreateMirror(t, providersFilesystemMirrorPath)\n\n\tfilesystemProvider = FakeProvider{\n\t\tRegistryName: \"example.com\",\n\t\tNamespace:    \"hashicorp\",\n\t\tName:         \"azurerm\",\n\t\tVersion:      \"3.113.0\",\n\t\tPlatformOS:   runtime.GOOS,\n\t\tPlatformArch: runtime.GOARCH,\n\t}\n\tfilesystemProvider.CreateMirror(t, providersFilesystemMirrorPath)\n\n\t// When we run NetworkMirrorServer, we override the default transport to configure the self-signed certificate.\n\t// After finishing, we need to restore this value.\n\tdefaultTransport := http.DefaultTransport\n\n\tdefer func() {\n\t\thttp.DefaultTransport = defaultTransport\n\t}()\n\n\ttoken := \"123456790\"\n\n\tnetworkMirrorURL := runNetworkMirrorServer(t, ctx, \"/providers/\", providersNetkworMirrorPath, token)\n\tt.Logf(\"Network mirror URL: %s\", networkMirrorURL)\n\tt.Logf(\"Provdiers network mirror path: %s\", providersNetkworMirrorPath)\n\tt.Logf(\"Provdiers filesysmte mirror path: %s\", providersFilesystemMirrorPath)\n\n\tproviderCacheDir := filepath.Join(rootPath, \"providers-cache\")\n\n\tcliConfigFilename, err := os.CreateTemp(helpers.TmpDirWOSymlinks(t), \"*\")\n\trequire.NoError(t, err)\n\n\tdefer cliConfigFilename.Close()\n\n\ttokenEnvName := \"TF_TOKEN_\" + strings.ReplaceAll(networkMirrorURL.Hostname(), \".\", \"_\")\n\n\tt.Setenv(tokenEnvName, token)\n\tdefer os.Unsetenv(tokenEnvName)\n\n\tt.Setenv(tf.EnvNameTFCLIConfigFile, cliConfigFilename.Name())\n\tdefer os.Unsetenv(tf.EnvNameTFCLIConfigFile)\n\n\tt.Logf(\"%s=%s\", tf.EnvNameTFCLIConfigFile, cliConfigFilename.Name())\n\n\tcliConfigSettings := &test.CLIConfigSettings{\n\t\tDirectMethods: []test.CLIConfigProviderInstallationDirect{\n\t\t\t{\n\t\t\t\tExclude: []string{\"example.com/*/*\"},\n\t\t\t},\n\t\t},\n\t\tFilesystemMirrorMethods: []test.CLIConfigProviderInstallationFilesystemMirror{\n\t\t\t{\n\t\t\t\tPath:    providersFilesystemMirrorPath,\n\t\t\t\tInclude: []string{\"example.com/hashicorp/azurerm\", \"example.com/hashicorp/aws\"},\n\t\t\t},\n\t\t},\n\t\tNetworkMirrorMethods: []test.CLIConfigProviderInstallationNetworkMirror{\n\t\t\t{\n\t\t\t\tURL:     networkMirrorURL.String(),\n\t\t\t\tExclude: []string{\"example.com/hashicorp/azurerm\"},\n\t\t\t},\n\t\t},\n\t}\n\ttest.CreateCLIConfig(t, cliConfigFilename, cliConfigSettings)\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt run --all init --provider-cache --provider-cache-registry-names example.com --provider-cache-registry-names registry.opentofu.org --provider-cache-registry-names registry.terraform.io --provider-cache-dir %s --non-interactive --working-dir %s\", providerCacheDir, appsPath))\n\n\texpectedProviderInstallation := `provider_installation { \"filesystem_mirror\" { include = [\"example.com/hashicorp/azurerm\", \"example.com/hashicorp/aws\"] exclude = [\"example.com/*/*\", \"registry.opentofu.org/*/*\", \"registry.terraform.io/*/*\"] path = \"%s\" } \"network_mirror\" { exclude = [\"example.com/hashicorp/azurerm\", \"example.com/*/*\", \"registry.opentofu.org/*/*\", \"registry.terraform.io/*/*\"] url = \"%s\" } \"filesystem_mirror\" { include = [\"example.com/*/*\", \"registry.opentofu.org/*/*\", \"registry.terraform.io/*/*\"] path = \"%s\" } \"direct\" { exclude = [\"example.com/*/*\", \"registry.opentofu.org/*/*\", \"registry.terraform.io/*/*\"] } }`\n\texpectedProviderInstallation = fmt.Sprintf(strings.Join(strings.Fields(expectedProviderInstallation), \" \"), providersFilesystemMirrorPath, networkMirrorURL.String(), providerCacheDir)\n\n\tfor _, appName := range []string{\"app0\", \"app1\"} {\n\t\t// Find the cache directory for each app since .terraformrc is now created there\n\t\tappPath := filepath.Join(appsPath, appName)\n\t\tcacheWorkingDir := helpers.FindCacheWorkingDir(t, appPath)\n\t\trequire.NotEmpty(t, cacheWorkingDir, \"Should find cache working directory for %s\", appPath)\n\n\t\tterraformrcBytes, err := os.ReadFile(filepath.Join(cacheWorkingDir, \".terraformrc\"))\n\t\trequire.NoError(t, err)\n\n\t\tterraformrc := strings.Join(strings.Fields(string(terraformrcBytes)), \" \")\n\n\t\tassert.Contains(t, terraformrc, expectedProviderInstallation, \"%s\\n\\n%s\", terraformrc, expectedProviderInstallation)\n\t}\n}\n\n// TestTerragruntProviderCacheWithAbsoluteModuleURL tests that the provider cache correctly handles\n// registries that return absolute URLs in their .well-known/terraform.json discovery response.\n// See https://github.com/gruntwork-io/terragrunt/issues/5156\nfunc TestTerragruntProviderCacheWithAbsoluteModuleURL(t *testing.T) {\n\trootPath := t.TempDir()\n\tappPath := filepath.Join(rootPath, \"app\")\n\tproviderCacheDir := filepath.Join(rootPath, \"providers-cache\")\n\tprovidersPath := filepath.Join(rootPath, \"providers\")\n\n\terr := os.MkdirAll(appPath, os.ModePerm)\n\trequire.NoError(t, err)\n\n\tctx, cancel := context.WithCancel(t.Context())\n\tdefer cancel()\n\n\tdefaultTransport := http.DefaultTransport\n\n\tdefer func() {\n\t\thttp.DefaultTransport = defaultTransport\n\t}()\n\n\tabsoluteModulesURL := \"https://modules.example.com/registry/v1/modules/\"\n\n\tregistryURL := runMockRegistryWithAbsoluteModuleURL(t, ctx, providersPath, absoluteModulesURL)\n\tregistryHost := registryURL.Host\n\n\tfakeProvider := FakeProvider{\n\t\tRegistryName: registryHost,\n\t\tNamespace:    \"hashicorp\",\n\t\tName:         \"null\",\n\t\tVersion:      \"3.2.0\",\n\t\tPlatformOS:   runtime.GOOS,\n\t\tPlatformArch: runtime.GOARCH,\n\t}\n\tfakeProvider.CreateMirror(t, providersPath)\n\n\tmainTfPath := filepath.Join(appPath, \"main.tf\")\n\tmainTfContent := fmt.Sprintf(`terraform {\n  required_providers {\n    null = {\n      source  = \"%s/hashicorp/null\"\n      version = \"3.2.0\"\n    }\n  }\n}\n`, registryHost)\n\terr = os.WriteFile(mainTfPath, []byte(mainTfContent), 0644)\n\trequire.NoError(t, err)\n\n\tterragruntHclPath := filepath.Join(appPath, \"terragrunt.hcl\")\n\terr = os.WriteFile(terragruntHclPath, []byte(\"# Test fixture for absolute module URL\\n\"), 0644)\n\trequire.NoError(t, err)\n\n\tcliConfigFilename, err := os.CreateTemp(helpers.TmpDirWOSymlinks(t), \"*\")\n\trequire.NoError(t, err)\n\n\tdefer cliConfigFilename.Close()\n\n\tt.Setenv(tf.EnvNameTFCLIConfigFile, cliConfigFilename.Name())\n\tdefer os.Unsetenv(tf.EnvNameTFCLIConfigFile)\n\n\tcliConfigSettings := &test.CLIConfigSettings{\n\t\tFilesystemMirrorMethods: []test.CLIConfigProviderInstallationFilesystemMirror{\n\t\t\t{\n\t\t\t\tPath:    providersPath,\n\t\t\t\tInclude: []string{registryHost + \"/*/*\"},\n\t\t\t},\n\t\t},\n\t\tDirectMethods: []test.CLIConfigProviderInstallationDirect{\n\t\t\t{\n\t\t\t\tExclude: []string{registryHost + \"/*/*\"},\n\t\t\t},\n\t\t},\n\t}\n\ttest.CreateCLIConfig(t, cliConfigFilename, cliConfigSettings)\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt init --provider-cache --provider-cache-registry-names %s --provider-cache-dir %s --non-interactive --working-dir %s\",\n\t\t\tregistryHost,\n\t\t\tproviderCacheDir,\n\t\t\tappPath,\n\t\t),\n\t)\n\n\tcacheWorkingDir := helpers.FindCacheWorkingDir(t, appPath)\n\trequire.NotEmpty(t, cacheWorkingDir, \"Should find cache working directory\")\n\n\tterraformrcBytes, err := os.ReadFile(filepath.Join(cacheWorkingDir, \".terraformrc\"))\n\trequire.NoError(t, err)\n\n\tterraformrc := string(terraformrcBytes)\n\n\tassert.Contains(\n\t\tt,\n\t\tterraformrc,\n\t\tabsoluteModulesURL,\n\t\t\"The .terraformrc should contain the absolute modules.v1 URL as-is\",\n\t)\n\n\tmalformedURL := fmt.Sprintf(\"https://%s%s\", registryHost, absoluteModulesURL)\n\tassert.NotContains(\n\t\tt,\n\t\tterraformrc,\n\t\tmalformedURL,\n\t\t\"The .terraformrc should NOT contain a malformed URL with double hostname\",\n\t)\n}\n\nfunc TestTerragruntDownloadDir(t *testing.T) {\n\thelpers.CleanupTerraformFolder(t, testFixtureLocalRelativeDownloadPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\n\t/* we have 2 terragrunt dirs here. One of them doesn't set the download_dir in the config,\n\tthe other one does. Here we'll be checking for precedence, and if the download_dir is set\n\taccording to the specified settings\n\t*/\n\ttestCases := []struct {\n\t\tname                 string\n\t\trootPath             string\n\t\tdownloadDirEnv       string // download dir set as an env var\n\t\tdownloadDirFlag      string // download dir set as a flag\n\t\tdownloadDirReference string // the expected result\n\t}{\n\t\t{\n\t\t\tname:                 \"download dir not set\",\n\t\t\trootPath:             filepath.Join(tmpEnvPath, testFixtureGetOutput, \"download-dir\", \"not-set\"),\n\t\t\tdownloadDirReference: filepath.Join(tmpEnvPath, testFixtureGetOutput, \"download-dir\", \"not-set\", helpers.TerragruntCache),\n\t\t},\n\t\t{\n\t\t\tname:                 \"download dir set in config\",\n\t\t\trootPath:             filepath.Join(tmpEnvPath, testFixtureGetOutput, \"download-dir\", \"in-config\"),\n\t\t\tdownloadDirReference: filepath.Join(tmpEnvPath, testFixtureGetOutput, \"download-dir\", \"in-config\", \".download\"),\n\t\t},\n\t\t{\n\t\t\tname:                 \"download dir set in config and in env var\",\n\t\t\trootPath:             filepath.Join(tmpEnvPath, testFixtureGetOutput, \"download-dir\", \"in-config\"),\n\t\t\tdownloadDirEnv:       filepath.Join(tmpEnvPath, testFixtureGetOutput, \"download-dir\", \"in-config\", \".env-var\"),\n\t\t\tdownloadDirReference: filepath.Join(tmpEnvPath, testFixtureGetOutput, \"download-dir\", \"in-config\", \".env-var\"),\n\t\t},\n\t\t{\n\t\t\tname:                 \"download dir set in config and as a flag\",\n\t\t\trootPath:             filepath.Join(tmpEnvPath, testFixtureGetOutput, \"download-dir\", \"in-config\"),\n\t\t\tdownloadDirFlag:      \"--download-dir \" + filepath.Join(tmpEnvPath, testFixtureGetOutput, \"download-dir\", \"in-config\", \".flag-download\"),\n\t\t\tdownloadDirReference: filepath.Join(tmpEnvPath, testFixtureGetOutput, \"download-dir\", \"in-config\", \".flag-download\"),\n\t\t},\n\t\t{\n\t\t\tname:                 \"download dir set in config env and as a flag\",\n\t\t\trootPath:             filepath.Join(tmpEnvPath, testFixtureGetOutput, \"download-dir\", \"in-config\"),\n\t\t\tdownloadDirEnv:       filepath.Join(tmpEnvPath, testFixtureGetOutput, \"download-dir\", \"in-config\", \".env-var\"),\n\t\t\tdownloadDirFlag:      \"--download-dir \" + filepath.Join(tmpEnvPath, testFixtureGetOutput, \"download-dir\", \"in-config\", \".flag-download\"),\n\t\t\tdownloadDirReference: filepath.Join(tmpEnvPath, testFixtureGetOutput, \"download-dir\", \"in-config\", \".flag-download\"),\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif tc.downloadDirEnv != \"\" {\n\t\t\t\tt.Setenv(\"TG_DOWNLOAD_DIR\", tc.downloadDirEnv)\n\t\t\t} else {\n\t\t\t\t// Clear the variable if it's not set. This is clearing the variable in case the variable is set outside the test process.\n\t\t\t\trequire.NoError(t, os.Unsetenv(\"TG_DOWNLOAD_DIR\"))\n\t\t\t}\n\n\t\t\tstdout := bytes.Buffer{}\n\t\t\tstderr := bytes.Buffer{}\n\t\t\tcmd := fmt.Sprintf(\"terragrunt info print %s --non-interactive --working-dir %s\", tc.downloadDirFlag, tc.rootPath)\n\t\t\terr := helpers.RunTerragruntCommand(t, cmd, &stdout, &stderr)\n\t\t\thelpers.LogBufferContentsLineByLine(t, stdout, \"stdout\")\n\t\t\thelpers.LogBufferContentsLineByLine(t, stderr, \"stderr\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar dat print.InfoOutput\n\n\t\t\tunmarshalErr := json.Unmarshal(stdout.Bytes(), &dat)\n\t\t\trequire.NoError(t, unmarshalErr)\n\t\t\t// compare the results\n\t\t\tassert.Equal(t, tc.downloadDirReference, dat.DownloadDir)\n\t\t})\n\t}\n}\n\nfunc TestExtraArguments(t *testing.T) {\n\tout := new(bytes.Buffer)\n\thelpers.RunTerragruntRedirectOutput(t, \"terragrunt apply -auto-approve --non-interactive --tf-forward-stdout --working-dir \"+testFixtureExtraArgsPath, out, os.Stderr)\n\tt.Log(out.String())\n\tassert.Contains(t, out.String(), \"Hello, World from dev!\")\n}\n\nfunc TestExtraArgumentsWithEnv(t *testing.T) {\n\tout := new(bytes.Buffer)\n\n\tt.Setenv(\"TF_VAR_env\", \"prod\")\n\thelpers.RunTerragruntRedirectOutput(t, \"terragrunt apply -auto-approve --non-interactive --tf-forward-stdout --working-dir \"+testFixtureExtraArgsPath, out, os.Stderr)\n\tt.Log(out.String())\n\tassert.Contains(t, out.String(), \"Hello, World!\")\n}\n\nfunc TestExtraArgumentsWithEnvVarBlock(t *testing.T) {\n\tout := new(bytes.Buffer)\n\thelpers.RunTerragruntRedirectOutput(t, \"terragrunt apply -auto-approve --non-interactive --tf-forward-stdout --working-dir \"+testFixtureEnvVarsBlockPath, out, os.Stderr)\n\tt.Log(out.String())\n\tassert.Contains(t, out.String(), \"I'm set in extra_arguments env_vars\")\n}\n\nfunc TestExtraArgumentsWithRegion(t *testing.T) {\n\tout := new(bytes.Buffer)\n\n\tt.Setenv(\"TF_VAR_region\", \"us-west-2\")\n\thelpers.RunTerragruntRedirectOutput(t, \"terragrunt apply -auto-approve --non-interactive --tf-forward-stdout --working-dir \"+testFixtureExtraArgsPath, out, os.Stderr)\n\tt.Log(out.String())\n\tassert.Contains(t, out.String(), \"Hello, World from Oregon!\")\n}\n\nfunc TestPreserveEnvVarApplyAll(t *testing.T) {\n\tt.Setenv(\"TF_VAR_seed\", \"from the env\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureRegressions)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRegressions)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureRegressions, \"apply-all-envvar\")\n\n\tstdout := bytes.Buffer{}\n\thelpers.RunTerragruntRedirectOutput(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath, &stdout, os.Stderr)\n\tt.Log(stdout.String())\n\n\t// Check the output of each child module to make sure the inputs were overridden by the env var\n\tassertEnvVarModule := filepath.Join(rootPath, \"require-envvar\")\n\n\tnoRequireEnvVarModule := filepath.Join(rootPath, \"no-require-envvar\")\n\tfor _, mod := range []string{assertEnvVarModule, noRequireEnvVarModule} {\n\t\tstdout := bytes.Buffer{}\n\t\terr := helpers.RunTerragruntCommand(t, \"terragrunt output text -no-color --non-interactive --working-dir \"+mod, &stdout, os.Stderr)\n\t\trequire.NoError(t, err)\n\t\tassert.Contains(t, stdout.String(), \"Hello from the env\")\n\t}\n}\n\nfunc TestPriorityOrderOfArgument(t *testing.T) {\n\tout := new(bytes.Buffer)\n\tinjectedValue := \"Injected-directly-by-argument\"\n\thelpers.RunTerragruntRedirectOutput(t, fmt.Sprintf(\"terragrunt apply -auto-approve -var extra_var=%s --non-interactive --tf-forward-stdout --working-dir %s\", injectedValue, testFixtureExtraArgsPath), out, os.Stderr)\n\tt.Log(out.String())\n\t// And the result value for test should be the injected variable since the injected arguments are injected before the supplied parameters,\n\t// so our override of extra_var should be the last argument.\n\tassert.Contains(t, out.String(), fmt.Sprintf(`test = \"%s\"`, injectedValue))\n}\n\nfunc TestTerragruntValidateInputsWithEnvVar(t *testing.T) {\n\tt.Setenv(\"TF_VAR_input\", \"from the env\")\n\n\tmoduleDir := filepath.Join(\"fixtures/validate-inputs\", \"fail-no-inputs\")\n\thelpers.RunTerragruntValidateInputs(t, moduleDir, nil, true)\n}\n\nfunc TestTerragruntValidateInputsWithUnusedEnvVar(t *testing.T) {\n\tt.Setenv(\"TF_VAR_unused\", \"from the env\")\n\n\tmoduleDir := filepath.Join(\"fixtures\", \"validate-inputs\", \"success-inputs-only\")\n\targs := []string{\"--strict\"}\n\thelpers.RunTerragruntValidateInputs(t, moduleDir, args, false)\n}\n\nfunc TestTerragruntSourceMapEnvArg(t *testing.T) {\n\tfixtureSourceMapPath := filepath.Join(\"fixtures\", \"source-map\")\n\thelpers.CleanupTerraformFolder(t, fixtureSourceMapPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, fixtureSourceMapPath)\n\trootPath := filepath.Join(tmpEnvPath, fixtureSourceMapPath)\n\n\tt.Setenv(\n\t\t\"TG_SOURCE_MAP\",\n\t\tstrings.Join(\n\t\t\t[]string{\n\t\t\t\t\"git::ssh://git@github.com/gruntwork-io/i-dont-exist.git=\" + tmpEnvPath,\n\t\t\t\t\"git::ssh://git@github.com/gruntwork-io/another-dont-exist.git=\" + tmpEnvPath,\n\t\t\t},\n\t\t\t\",\",\n\t\t),\n\t)\n\ttgPath := filepath.Join(rootPath, \"multiple-match\")\n\ttgArgs := \"terragrunt run --all --non-interactive --working-dir \" + tgPath + \" -- apply -auto-approve\"\n\thelpers.RunTerragrunt(t, tgArgs)\n}\n\nfunc TestTerragruntLogLevelEnvVarOverridesDefault(t *testing.T) {\n\t// NOTE: this matches logLevelEnvVar const in util/logger.go\n\tt.Setenv(\"TG_LOG_LEVEL\", \"debug\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureInputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, \".\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureInputs)\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt validate --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\toutput := stderr.String()\n\tassert.Contains(t, output, \"level=debug\")\n}\n\nfunc TestTerragruntLogLevelEnvVarUnparsableLogsError(t *testing.T) {\n\t// NOTE: this matches logLevelEnvVar const in util/logger.go\n\tt.Setenv(\"TG_LOG_LEVEL\", \"unparsable\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureInputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, \".\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureInputs)\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt validate --non-interactive --working-dir \"+rootPath, os.Stdout, os.Stderr)\n\trequire.Error(t, err)\n\n\tassert.Contains(t, err.Error(), \"invalid level\")\n}\n\nfunc TestTerragruntProduceTelemetryTraces(t *testing.T) {\n\tif helpers.IsWindows() {\n\t\tt.Skip(\"Skipping test on Windows since bash script execution is not supported\")\n\t}\n\n\tt.Setenv(\"TG_TELEMETRY_TRACE_EXPORTER\", \"console\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksBeforeAndAfterPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHooksBeforeAndAfterPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksBeforeAndAfterPath)\n\n\toutput, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\t// check that output has Telemetry JSON traces\n\tassert.Contains(t, output, \"\\\"SpanContext\\\":\")\n\tassert.Contains(t, output, \"\\\"TraceID\\\":\")\n\tassert.Contains(t, output, \"\\\"Name\\\":\\\"hook_after_hook_1\\\"\")\n\tassert.Contains(t, output, \"\\\"Name\\\":\\\"hook_after_hook_2\\\"\")\n}\n\nfunc TestTerragruntStackProduceTelemetryTraces(t *testing.T) {\n\tt.Setenv(\"TG_TELEMETRY_TRACE_EXPORTER\", \"console\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksBasic)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksBasic, \"live\")\n\n\toutput, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\t// check that output has Telemetry JSON traces\n\tassert.Contains(t, output, \"\\\"SpanContext\\\":\")\n\tassert.Contains(t, output, \"\\\"TraceID\\\":\")\n\tassert.Contains(t, output, \"\\\"Name\\\":\\\"stack_generate_unit\\\"\")\n\tassert.Contains(t, output, \"\\\"Name\\\":\\\"stack_generate\\\"\")\n}\n\nfunc TestTerragruntFindProduceTelemetryTraces(t *testing.T) {\n\tt.Setenv(\"TG_TELEMETRY_TRACE_EXPORTER\", \"console\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksBasic)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksBasic)\n\n\toutput, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt find --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\t// check that output have Telemetry json output\n\tassert.Contains(t, output, \"\\\"SpanContext\\\":\")\n\tassert.Contains(t, output, \"\\\"TraceID\\\":\")\n\tassert.Contains(t, output, \"\\\"Name\\\":\\\"find_discover\\\"\")\n\tassert.Contains(t, output, \"\\\"Name\\\":\\\"find_discovered_to_found\\\"\")\n}\n\nfunc TestTerragruntListProduceTelemetryTraces(t *testing.T) {\n\tt.Setenv(\"TG_TELEMETRY_TRACE_EXPORTER\", \"console\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksBasic)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksBasic)\n\n\toutput, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt list --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\t// check that output have Telemetry json output\n\tassert.Contains(t, output, \"\\\"SpanContext\\\":\")\n\tassert.Contains(t, output, \"\\\"TraceID\\\":\")\n\tassert.Contains(t, output, \"\\\"Name\\\":\\\"list_discover\\\"\")\n\tassert.Contains(t, output, \"\\\"Name\\\":\\\"list_discovered_to_listed\\\"\")\n}\n\nfunc TestTerragruntProduceTelemetryMetrics(t *testing.T) {\n\tif helpers.IsWindows() {\n\t\tt.Skip(\"Skipping test on Windows since bash script execution is not supported\")\n\t}\n\n\tt.Setenv(\"TG_TELEMETRY_METRIC_EXPORTER\", \"console\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksBeforeAndAfterPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHooksBeforeAndAfterPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksBeforeAndAfterPath)\n\n\toutput, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -no-color -auto-approve --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\t// sleep for a bit to allow the metrics to be flushed\n\ttime.Sleep(1 * time.Second)\n\n\t// check that output have Telemetry json output\n\tassert.Contains(t, output, \"{\\\"Name\\\":\\\"hook_after_hook_2_duration\\\"\")\n\tassert.Contains(t, output, \"{\\\"Name\\\":\\\"run_\")\n\tassert.Contains(t, output, \",\\\"IsMonotonic\\\":true}}\")\n}\n\nfunc TestTerragruntProduceTelemetryTracesWithRootSpanAndTraceID(t *testing.T) {\n\tif helpers.IsWindows() {\n\t\tt.Skip(\"Skipping test on Windows since bash script execution is not supported\")\n\t}\n\n\tt.Setenv(\"TG_TELEMETRY_TRACE_EXPORTER\", \"console\")\n\tt.Setenv(\"TRACEPARENT\", \"00-b2ff2d54551433d53dd807a6c94e81d1-0e6f631d793c718a-01\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksBeforeAndAfterPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHooksBeforeAndAfterPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksBeforeAndAfterPath)\n\n\toutput, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\t// check that output has Telemetry json output\n\tassert.Contains(t, output, \"\\\"SpanContext\\\":{\\\"TraceID\\\":\\\"b2ff2d54551433d53dd807a6c94e81d1\\\"\")\n\tassert.Contains(t, output, \"\\\"Parent\\\":{\\\"TraceID\\\":\\\"b2ff2d54551433d53dd807a6c94e81d1\")\n\tassert.Contains(t, output, \"\\\"SpanID\\\":\\\"0e6f631d793c718a\\\"\")\n\tassert.Contains(t, output, \"\\\"SpanContext\\\":\")\n\tassert.Contains(t, output, \"\\\"TraceID\\\":\")\n\tassert.Contains(t, output, \"\\\"Name\\\":\\\"hook_after_hook_1\\\"\")\n\tassert.Contains(t, output, \"\\\"Name\\\":\\\"hook_after_hook_2\\\"\")\n}\n\nfunc TestTerragruntProduceTelemetryInCaseOfError(t *testing.T) {\n\tif helpers.IsWindows() {\n\t\tt.Skip(\"Skipping test on Windows since bash script execution is not supported\")\n\t}\n\n\tt.Setenv(\"TG_TELEMETRY_TRACE_EXPORTER\", \"console\")\n\tt.Setenv(\"TRACEPARENT\", \"00-b2ff2d54551433d53dd807a6c94e81d1-0e6f631d793c718a-01\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHooksBeforeAndAfterPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHooksBeforeAndAfterPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHooksBeforeAndAfterPath)\n\n\toutput, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt plan no-existing-command -auto-approve --non-interactive --working-dir \"+rootPath)\n\trequire.Error(t, err)\n\n\tassert.Contains(t, output, \"\\\"SpanContext\\\":{\\\"TraceID\\\":\\\"b2ff2d54551433d53dd807a6c94e81d1\\\"\")\n\tassert.Contains(t, output, \"\\\"SpanID\\\":\\\"0e6f631d793c718a\\\"\")\n\tassert.Contains(t, output, \"exception.message\")\n\tassert.Contains(t, output, \"\\\"Name\\\":\\\"exception\\\"\")\n}\n\n// Since this test launches a large number of terraform processes, which sometimes fails with the message `Failed to write to log, write |1: file already closed`, for stability, we need to run it not parallel.\nfunc TestTerragruntProviderCache(t *testing.T) {\n\thelpers.CleanupTerraformFolder(t, testFixtureProviderCacheDirect)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureProviderCacheDirect)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureProviderCacheDirect)\n\n\tcacheDir, err := util.GetCacheDir()\n\trequire.NoError(t, err)\n\n\tproviderCacheDir := filepath.Join(cacheDir, \"provider-cache-test-direct\")\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt run --all init --provider-cache --provider-cache-dir %s --non-interactive --working-dir %s\", providerCacheDir, rootPath))\n\n\tproviders := map[string][]string{\n\t\t\"first\": {\n\t\t\t\"hashicorp/aws/5.36.0\",\n\t\t\t\"hashicorp/azurerm/3.95.0\",\n\t\t},\n\t\t\"second\": {\n\t\t\t\"hashicorp/aws/5.40.0\",\n\t\t\t\"hashicorp/azurerm/3.95.0\",\n\t\t\t\"hashicorp/kubernetes/2.27.0\",\n\t\t},\n\t}\n\n\tregistryName := \"registry.opentofu.org\"\n\tif isTerraform() {\n\t\tregistryName = \"registry.terraform.io\"\n\t}\n\n\tfor subDir, providers := range providers {\n\t\tvar (\n\t\t\tactualApps   int\n\t\t\texpectedApps = 10\n\t\t)\n\n\t\tsubDir = filepath.Join(rootPath, subDir)\n\n\t\tentries, err := os.ReadDir(subDir)\n\t\trequire.NoError(t, err)\n\n\t\tfor _, entry := range entries {\n\t\t\tif !entry.IsDir() {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tactualApps++\n\n\t\t\tappPath := filepath.Join(subDir, entry.Name())\n\n\t\t\tlockfilePath := filepath.Join(appPath, \".terraform.lock.hcl\")\n\t\t\tlockfileContent, err := os.ReadFile(lockfilePath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tlockfile, diags := hclwrite.ParseConfig(lockfileContent, lockfilePath, hcl.Pos{Line: 1, Column: 1})\n\t\t\tassert.False(t, diags.HasErrors())\n\n\t\t\t// Find the cache working directory since providers are now installed there\n\t\t\tcacheWorkingDir := helpers.FindCacheWorkingDir(t, appPath)\n\t\t\trequire.NotEmpty(t, cacheWorkingDir, \"Should find cache working directory for %s\", appPath)\n\n\t\t\tfor _, provider := range providers {\n\t\t\t\tvar (\n\t\t\t\t\tactualProviderSymlinks   int\n\t\t\t\t\texpectedProviderSymlinks = 1\n\t\t\t\t\tprovider                 = path.Join(registryName, provider)\n\t\t\t\t)\n\n\t\t\t\tproviderBlock := lockfile.Body().FirstMatchingBlock(\"provider\", []string{filepath.Dir(provider)})\n\t\t\t\tassert.NotNil(t, providerBlock)\n\n\t\t\t\tproviderPath := filepath.Join(cacheWorkingDir, \".terraform/providers\", provider)\n\t\t\t\tassert.True(t, util.FileExists(providerPath))\n\n\t\t\t\tentries, err := os.ReadDir(providerPath)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tfor _, entry := range entries {\n\t\t\t\t\t// skip .lock files since it is used to lock file action\n\t\t\t\t\tif strings.HasSuffix(entry.Name(), \".lock\") {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tactualProviderSymlinks++\n\n\t\t\t\t\tassert.Equal(t, fs.ModeSymlink, entry.Type())\n\n\t\t\t\t\tsymlinkPath := filepath.Join(providerPath, entry.Name())\n\n\t\t\t\t\tactualPath, err := os.Readlink(symlinkPath)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\texpectedPath := filepath.Join(providerCacheDir, provider, entry.Name())\n\t\t\t\t\tassert.Contains(t, actualPath, expectedPath)\n\t\t\t\t}\n\n\t\t\t\tassert.Equal(t, expectedProviderSymlinks, actualProviderSymlinks)\n\t\t\t}\n\t\t}\n\n\t\tassert.Equal(t, expectedApps, actualApps)\n\t}\n}\n\n// TestTerragruntProviderCacheWithDependency verifies that the provider cache server\n// is used when resolving dependency outputs. Before the fix, ReadTerragruntConfig and\n// NewParsingContext cleared the provider cache hook from the Go context, causing\n// dependency init to bypass the cache server and download providers directly.\nfunc TestTerragruntProviderCacheWithDependency(t *testing.T) {\n\thelpers.CleanupTerraformFolder(t, testFixtureProviderCacheDependency)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureProviderCacheDependency)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureProviderCacheDependency)\n\n\tcacheDir := helpers.TmpDirWOSymlinks(t)\n\n\tproviderCacheDir := filepath.Join(cacheDir, \"provider-cache-test-dependency\")\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --provider-cache --provider-cache-dir %s --non-interactive --working-dir %s -- init\",\n\t\t\tproviderCacheDir,\n\t\t\tfilepath.Join(rootPath, \"app\"),\n\t\t),\n\t)\n\n\tregistryName := \"registry.opentofu.org\"\n\tif isTerraform() {\n\t\tregistryName = \"registry.terraform.io\"\n\t}\n\n\t// Verify that the provider cache directory contains providers from both units.\n\t// dep uses hashicorp/local, app uses hashicorp/null — if both are cached,\n\t// the provider cache server was used for both the dependency and the dependent.\n\tfor _, provider := range []string{\n\t\t\"hashicorp/local/2.7.0\",\n\t\t\"hashicorp/null/3.2.4\",\n\t} {\n\t\tproviderPath := filepath.Join(providerCacheDir, registryName, provider)\n\t\tassert.DirExists(t, providerPath, \"Provider cache dir should contain %s at %s\", provider, providerPath)\n\t}\n\n\t// Verify both units have been initialized by checking for .terraform directories\n\tfor _, unit := range []string{\"dep\", \"app\"} {\n\t\tunitPath := filepath.Join(rootPath, unit)\n\t\tcacheWorkingDir := helpers.FindCacheWorkingDir(t, unitPath)\n\t\trequire.NotEmpty(t, cacheWorkingDir, \"Should find cache working directory for %s\", unitPath)\n\n\t\tterraformDir := filepath.Join(cacheWorkingDir, \".terraform\")\n\t\tassert.DirExists(t, terraformDir, \".terraform directory should exist in cache for %s\", unit)\n\t}\n}\n\nfunc TestParseTFLog(t *testing.T) {\n\tt.Setenv(\"TF_LOG\", \"info\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureLogFormatter)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureLogFormatter)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLogFormatter)\n\n\trootPath, err := filepath.EvalSymlinks(rootPath)\n\trequire.NoError(t, err)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all init --non-interactive --log-format=pretty --no-color --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\tfor _, prefixName := range []string{\"app\", \"dep\"} {\n\t\tassert.Contains(t, stderr, \"INFO   [\"+prefixName+\"] \"+wrappedBinary()+`: TF_LOG: Go runtime version`)\n\t}\n}\n\nfunc TestTerragruntTelemetryPassTraceParent(t *testing.T) {\n\tt.Setenv(\"TG_TELEMETRY_TRACE_EXPORTER\", \"console\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureTraceParent)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureTraceParent)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureTraceParent)\n\n\tstr, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\thelpers.ValidateHookTraceParent(t, \"hook_print_traceparent\", str)\n\n\tt.Setenv(\"TG_TELEMETRY_TRACE_EXPORTER\", \"\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\ttraceparent, found := outputs[\"traceparent_value\"]\n\trequire.True(t, found)\n\tassert.NotEmpty(t, traceparent.Value)\n\trequire.Regexp(t, `^00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$`, traceparent.Value, \"Traceparent value should match W3C traceparent format\")\n}\n\nfunc TestTerragruntTelemetryPassTraceParentEnvVariable(t *testing.T) {\n\tenvParentTrace := \"00-b2ff2d54551433d53dd807a666666666-0e6f631d793c718a-01\"\n\n\tt.Setenv(\"TG_TELEMETRY_TRACE_EXPORTER\", \"console\")\n\tt.Setenv(\"TRACEPARENT\", envParentTrace)\n\n\thelpers.CleanupTerraformFolder(t, testFixtureTraceParent)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureTraceParent)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureTraceParent)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tt.Setenv(\"TG_TELEMETRY_TRACE_EXPORTER\", \"\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\ttraceparent, found := outputs[\"traceparent_value\"]\n\trequire.True(t, found)\n\tassert.NotEmpty(t, traceparent.Value)\n\tassert.Equal(t, envParentTrace, traceparent.Value)\n}\n\nfunc TestRunnerPoolTelemetry(t *testing.T) {\n\tt.Setenv(\"TG_TELEMETRY_TRACE_EXPORTER\", \"console\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureTraceParent)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureTraceParent)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureTraceParent)\n\n\ttelemetryOutput, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\"  -- apply\")\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, telemetryOutput, \"\\\"Name\\\":\\\"runner_pool_discovery\\\"\")\n\tassert.Contains(t, telemetryOutput, \"\\\"Name\\\":\\\"runner_pool_creation\\\"\")\n\tassert.Contains(t, telemetryOutput, \"\\\"Name\\\":\\\"runner_pool_controller\\\"\")\n\tassert.Contains(t, telemetryOutput, \"\\\"Name\\\":\\\"runner_pool_task\\\"\")\n}\n\nfunc TestVersionIsInvokedInDifferentDirectory(t *testing.T) {\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureVersionInvocation, \"**/.tool-versions\")\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureVersionInvocation)\n\ttestPath, err := filepath.EvalSymlinks(testPath)\n\trequire.NoError(t, err)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all --non-interactive --log-level debug --working-dir \"+\n\t\t\ttestPath+\" -- apply\",\n\t)\n\trequire.NoError(t, err)\n\n\tversionCmdPattern := regexp.MustCompile(`Running command: ` + regexp.QuoteMeta(wrappedBinary()) + ` -version`)\n\tmatches := versionCmdPattern.FindAllStringIndex(stderr, -1)\n\n\t// Expected 2 version commands:\n\t// 1. Root directory (initial version check)\n\t// 2. dependency-with-custom-version (has .tool-versions file, different cache key)\n\t// Note: dependency and app share the same cache key as root, so they get cache hits and skip version command\n\texpected := 2\n\n\tassert.Len(t, matches, expected, \"Expected exactly %d occurrence(s) of '-version' command, found %d\", expected, len(matches))\n\tassert.Contains(t, stderr, \"prefix=dependency-with-custom-version msg=Running command: \"+wrappedBinary()+\" -version\")\n}\n\nfunc TestVersionIsInvokedOnlyOnce(t *testing.T) {\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDependencyOutput)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureDependencyOutput)\n\ttestPath, err := filepath.EvalSymlinks(testPath)\n\trequire.NoError(t, err)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all --non-interactive --log-level debug --working-dir \"+testPath+\" -- apply\",\n\t)\n\trequire.NoError(t, err)\n\n\t// check that version command was invoked only once -version\n\tversionCmdPattern := regexp.MustCompile(`Running command: ` + regexp.QuoteMeta(wrappedBinary()) + ` -version`)\n\tmatches := versionCmdPattern.FindAllStringIndex(stderr, -1)\n\n\texpected := 1\n\n\tassert.Len(\n\t\tt,\n\t\tmatches,\n\t\texpected,\n\t\t\"Expected exactly %d occurrence(s) of '-version' command, found %d\",\n\t\texpected,\n\t\tlen(matches),\n\t)\n}\n\nfunc TestTerragruntTelemetryTraces(t *testing.T) {\n\tt.Setenv(\"TG_TELEMETRY_TRACE_EXPORTER\", \"console\")\n\n\thelpers.CleanupTerraformFolder(t, testFixtureDependencyOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDependencyOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureDependencyOutput)\n\n\toutput, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt hcl format --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\t// check that produced output has span traces\n\tassert.Contains(t, output, \"\\\"SpanKind\\\":1\")\n\tassert.Contains(t, output, \"\\\"Parent\\\"\")\n}\n"
  },
  {
    "path": "test/integration_sops_kms_test.go",
    "content": "//go:build aws\n\npackage test_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureSopsKMS = \"fixtures/sops-kms\"\n)\n\n// sops decrypting for inputs\nfunc TestAwsSopsDecryptedKMSCorrectly(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureSopsKMS)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureSopsKMS)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureSopsKMS)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\tassert.Equal(t, []any{true, false}, outputs[\"json_bool_array\"].Value)\n\tassert.Equal(t, []any{\"example_value1\", \"example_value2\"}, outputs[\"json_string_array\"].Value)\n\tassert.InEpsilon(t, 1234.56789, outputs[\"json_number\"].Value, 0.0001)\n\tassert.Equal(t, \"example_value\", outputs[\"json_string\"].Value)\n\tassert.Equal(t, \"Welcome to SOPS! Edit this file as you please!\", outputs[\"json_hello\"].Value)\n\tassert.Equal(t, []any{true, false}, outputs[\"yaml_bool_array\"].Value)\n\tassert.Equal(t, []any{\"example_value1\", \"example_value2\"}, outputs[\"yaml_string_array\"].Value)\n\tassert.InEpsilon(t, 1234.5679, outputs[\"yaml_number\"].Value, 0.0001)\n\tassert.Equal(t, \"example_value\", outputs[\"yaml_string\"].Value)\n\tassert.Equal(t, \"Welcome to SOPS! Edit this file as you please!\", outputs[\"yaml_hello\"].Value)\n\tassert.Equal(t, \"Raw Secret Example\", outputs[\"text_value\"].Value)\n\tassert.Contains(t, outputs[\"env_value\"].Value, \"DB_PASSWORD=tomato\")\n\tassert.Contains(t, outputs[\"ini_value\"].Value, \"password = potato\")\n}\n"
  },
  {
    "path": "test/integration_sops_test.go",
    "content": "//go:build sops\n\n// sops tests assume that you're going to import the test_pgp_key.asc file into your GPG keyring before\n// running the tests. We're not gonna assume that everyone is going to do this, so we're going to skip\n// these tests by default.\n//\n// You can import the key by running the following command:\n//\n//\tgpg --import --no-tty --batch --yes ./test/fixtures/sops/test_pgp_key.asc\n\npackage test_test\n\nimport (\n\t\"bytes\"\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/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureSops        = \"fixtures/sops\"\n\ttestFixtureSopsErrors  = \"fixtures/sops-errors\"\n\ttestFixtureSopsMissing = \"fixtures/sops-missing\"\n)\n\nconst sopsMultiUnitMainTf = `variable \"secret_value\" {\n  type = string\n}\n\nvariable \"unit_name\" {\n  type = string\n}\n\noutput \"secret_value\" {\n  value = var.secret_value\n}\n\noutput \"unit_name\" {\n  value = var.unit_name\n}\n`\n\nconst sopsMultiUnitTerragruntHcl = `locals {\n  secret = try(jsondecode(sops_decrypt_file(\"${get_terragrunt_dir()}/secret.enc.json\")), {})\n}\n\ninputs = {\n  secret_value = lookup(local.secret, \"example_key\", \"DECRYPTION_FAILED\")\n  unit_name    = \"%s\"\n}\n`\n\n// generateSopsMultiUnitFixtures creates numUnits directories, each with a\n// main.tf, terragrunt.hcl, and a copy of the existing SOPS-encrypted secrets.json.\n// Only requires the test PGP key imported in GPG — no sops CLI needed.\nfunc generateSopsMultiUnitFixtures(t *testing.T, numUnits int) string {\n\tt.Helper()\n\n\tdir := t.TempDir()\n\n\t// Reuse existing SOPS-encrypted file from the sops fixture as template\n\tencData, err := os.ReadFile(\"fixtures/sops/secrets.json\")\n\trequire.NoError(t, err, \"failed to read SOPS template file\")\n\n\tfor i := 1; i <= numUnits; i++ {\n\t\tunitName := fmt.Sprintf(\"unit-%02d\", i)\n\t\tunitDir := filepath.Join(dir, unitName)\n\t\trequire.NoError(t, os.MkdirAll(unitDir, 0755))\n\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(unitDir, \"main.tf\"),\n\t\t\t[]byte(sopsMultiUnitMainTf), 0644))\n\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(unitDir, \"terragrunt.hcl\"),\n\t\t\t[]byte(fmt.Sprintf(sopsMultiUnitTerragruntHcl, unitName)), 0644))\n\n\t\trequire.NoError(t, os.WriteFile(\n\t\t\tfilepath.Join(unitDir, \"secret.enc.json\"), encData, 0644))\n\t}\n\n\treturn dir\n}\n\nfunc TestSOPSDecryptedCorrectly(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureSops)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureSops)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureSops)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\tassert.Equal(t, []any{true, false}, outputs[\"json_bool_array\"].Value)\n\tassert.Equal(t, []any{\"example_value1\", \"example_value2\"}, outputs[\"json_string_array\"].Value)\n\tassert.InEpsilon(t, 1234.56789, outputs[\"json_number\"].Value, 0.0001)\n\tassert.Equal(t, \"example_value\", outputs[\"json_string\"].Value)\n\tassert.Equal(t, \"Welcome to SOPS! Edit this file as you please!\", outputs[\"json_hello\"].Value)\n\tassert.Equal(t, []any{true, false}, outputs[\"yaml_bool_array\"].Value)\n\tassert.Equal(t, []any{\"example_value1\", \"example_value2\"}, outputs[\"yaml_string_array\"].Value)\n\tassert.InEpsilon(t, 1234.5679, outputs[\"yaml_number\"].Value, 0.0001)\n\tassert.Equal(t, \"example_value\", outputs[\"yaml_string\"].Value)\n\tassert.Equal(t, \"Welcome to SOPS! Edit this file as you please!\", outputs[\"yaml_hello\"].Value)\n\tassert.Equal(t, \"Raw Secret Example\", outputs[\"text_value\"].Value)\n\tassert.Contains(t, outputs[\"env_value\"].Value, \"DB_PASSWORD=tomato\")\n\tassert.Contains(t, outputs[\"ini_value\"].Value, \"password = potato\")\n}\n\nfunc TestSOPSDecryptedCorrectlyRunAll(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureSops)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureSops)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureSops)\n\trootPath, err := filepath.EvalSymlinks(rootPath)\n\trequire.NoError(t, err)\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all --non-interactive --working-dir %s/../.. --queue-include-dir %s  -- apply -auto-approve\",\n\t\t\trootPath,\n\t\t\ttestFixtureSops,\n\t\t),\n\t)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all --non-interactive --working-dir %s/../.. --queue-include-dir %s  -- output -no-color -json\",\n\t\t\trootPath,\n\t\t\ttestFixtureSops,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal([]byte(stdout), &outputs))\n\n\tassert.Equal(t, []any{true, false}, outputs[\"json_bool_array\"].Value)\n\tassert.Equal(t, []any{\"example_value1\", \"example_value2\"}, outputs[\"json_string_array\"].Value)\n\tassert.InEpsilon(t, 1234.56789, outputs[\"json_number\"].Value, 0.0001)\n\tassert.Equal(t, \"example_value\", outputs[\"json_string\"].Value)\n\tassert.Equal(t, \"Welcome to SOPS! Edit this file as you please!\", outputs[\"json_hello\"].Value)\n\tassert.Equal(t, []any{true, false}, outputs[\"yaml_bool_array\"].Value)\n\tassert.Equal(t, []any{\"example_value1\", \"example_value2\"}, outputs[\"yaml_string_array\"].Value)\n\tassert.InEpsilon(t, 1234.5679, outputs[\"yaml_number\"].Value, 0.0001)\n\tassert.Equal(t, \"example_value\", outputs[\"yaml_string\"].Value)\n\tassert.Equal(t, \"Welcome to SOPS! Edit this file as you please!\", outputs[\"yaml_hello\"].Value)\n\tassert.Equal(t, \"Raw Secret Example\", outputs[\"text_value\"].Value)\n\tassert.Contains(t, outputs[\"env_value\"].Value, \"DB_PASSWORD=tomato\")\n\tassert.Contains(t, outputs[\"ini_value\"].Value, \"password = potato\")\n}\n\nfunc TestSOPSDecryptedCorrectlyRunAllWithFilter(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureSops)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureSops)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureSops)\n\trootPath, err := filepath.EvalSymlinks(rootPath)\n\trequire.NoError(t, err)\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all --non-interactive --working-dir %s/../.. --experiment filter-flag --filter ./%s -- apply -auto-approve\",\n\t\t\trootPath,\n\t\t\ttestFixtureSops,\n\t\t),\n\t)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all --non-interactive --working-dir %s/../.. --experiment filter-flag --filter ./%s -- output -no-color -json\",\n\t\t\trootPath,\n\t\t\ttestFixtureSops,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal([]byte(stdout), &outputs))\n\n\tassert.Equal(t, []any{true, false}, outputs[\"json_bool_array\"].Value)\n\tassert.Equal(t, []any{\"example_value1\", \"example_value2\"}, outputs[\"json_string_array\"].Value)\n\tassert.InEpsilon(t, 1234.56789, outputs[\"json_number\"].Value, 0.0001)\n\tassert.Equal(t, \"example_value\", outputs[\"json_string\"].Value)\n\tassert.Equal(t, \"Welcome to SOPS! Edit this file as you please!\", outputs[\"json_hello\"].Value)\n\tassert.Equal(t, []any{true, false}, outputs[\"yaml_bool_array\"].Value)\n\tassert.Equal(t, []any{\"example_value1\", \"example_value2\"}, outputs[\"yaml_string_array\"].Value)\n\tassert.InEpsilon(t, 1234.5679, outputs[\"yaml_number\"].Value, 0.0001)\n\tassert.Equal(t, \"example_value\", outputs[\"yaml_string\"].Value)\n\tassert.Equal(t, \"Welcome to SOPS! Edit this file as you please!\", outputs[\"yaml_hello\"].Value)\n\tassert.Equal(t, \"Raw Secret Example\", outputs[\"text_value\"].Value)\n\tassert.Contains(t, outputs[\"env_value\"].Value, \"DB_PASSWORD=tomato\")\n\tassert.Contains(t, outputs[\"ini_value\"].Value, \"password = potato\")\n}\n\n// TestSOPSDecryptedCorrectlyRunAllMultipleUnits tests that SOPS decryption works correctly\n// when multiple units with the same encrypted secret are processed in parallel via run --all.\nfunc TestSOPSDecryptedCorrectlyRunAllMultipleUnits(t *testing.T) {\n\tt.Parallel()\n\n\tconst numUnits = 12\n\n\trootPath := generateSopsMultiUnitFixtures(t, numUnits)\n\n\t// Run apply on all units in parallel — this is where the race condition manifests\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all --non-interactive --working-dir %s -- apply -auto-approve\",\n\t\t\trootPath,\n\t\t),\n\t)\n\n\t// Verify each unit successfully decrypted the secret\n\tfor i := 1; i <= numUnits; i++ {\n\t\tunitName := fmt.Sprintf(\"unit-%02d\", i)\n\t\tunitPath := filepath.Join(rootPath, unitName)\n\t\tstdout := bytes.Buffer{}\n\t\tstderr := bytes.Buffer{}\n\n\t\terr := helpers.RunTerragruntCommand(\n\t\t\tt,\n\t\t\t\"terragrunt output -no-color -json --non-interactive --working-dir \"+unitPath,\n\t\t\t&stdout,\n\t\t\t&stderr,\n\t\t)\n\t\trequire.NoError(t, err, \"Failed to get output for %s: %s\", unitName, stderr.String())\n\n\t\toutputs := map[string]helpers.TerraformOutput{}\n\t\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs), \"Failed to parse output for %s\", unitName)\n\n\t\t// Check for the specific failure mode from issue #5515:\n\t\t// If SOPS decryption fails due to race, try() returns {} and lookup returns \"DECRYPTION_FAILED\"\n\t\tsecretValue, ok := outputs[\"secret_value\"].Value.(string)\n\t\trequire.True(t, ok, \"secret_value should be a string for %s\", unitName)\n\n\t\tif secretValue == \"DECRYPTION_FAILED\" {\n\t\t\tt.Fatalf(\"SOPS race condition detected! Unit %s got DECRYPTION_FAILED. \"+\n\t\t\t\t\"This indicates sops_decrypt_file failed and try() returned empty {}.\",\n\t\t\t\tunitName)\n\t\t}\n\n\t\tassert.Equal(t, \"example_value\", secretValue,\n\t\t\t\"Unit %s should have correct decrypted secret value\", unitName)\n\t\tassert.Equal(t, unitName, outputs[\"unit_name\"].Value,\n\t\t\t\"Unit %s should have correct unit name\", unitName)\n\t}\n}\n\nfunc TestSOPSTerragruntLogSopsErrors(t *testing.T) {\n\tt.Parallel()\n\n\t// create temporary directory for plan files\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureSopsErrors)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureSopsErrors)\n\n\t// apply and check for errors\n\t_, errorOut, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply --non-interactive --working-dir \"+testPath)\n\trequire.Error(t, err)\n\n\tassert.Contains(t, errorOut, \"error decrypting key: [error decrypting key\")\n\tassert.Contains(t, errorOut, \"error base64-decoding encrypted data key: illegal base64 data at input byte\")\n}\n\nfunc TestSOPSDecryptOnMissing(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testFixtureSopsMissing)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureSopsMissing)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureSopsMissing)\n\n\t// apply and check for errors\n\t_, errorOut, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt apply --log-level debug --non-interactive --working-dir \"+rootPath,\n\t)\n\trequire.Error(t, err)\n\n\terrorOut = strings.ReplaceAll(errorOut, \"\\n\", \" \")\n\n\tassert.Contains(t, errorOut, \"Encountered error while evaluating locals in file ./terragrunt.hcl\")\n\t// Check for the missing file error components separately since they may be split across log lines\n\tassert.Contains(t, errorOut, \"missing.yaml\", \"Error should reference the missing SOPS file\")\n\tassert.Contains(t, errorOut, \"no such file\", \"Error should indicate file does not exist\")\n}\n"
  },
  {
    "path": "test/integration_ssh_test.go",
    "content": "//go:build ssh\n\n// We don't want contributors to have to install SSH keys to run these tests, so we skip\n// them by default. Contributors need to opt in to run these tests by setting the\n// build flag `ssh` when running the tests. This is done by adding the `-tags ssh` flag\n// to the `go test` command. For example:\n//\n// go test -tags ssh ./...\n\npackage test_test\n\nimport (\n\t\"bytes\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSSHSourceMapWithSlashInRef(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureSourceMapSlashes)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureSourceMapSlashes)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --non-interactive --source-map git::ssh://git@github.com/gruntwork-io/i-dont-exist.git=git::git@github.com:gruntwork-io/terragrunt.git?ref=fixture/test-fixtures --working-dir \"+testPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n}\n\nfunc TestSSHTerragruntNoWarningRemotePath(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureNoSubmodules)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureNoSubmodules)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt init --non-interactive --working-dir \"+testPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\tassert.NotContains(t, stderr.String(), \"No double-slash (//) found in source URL\")\n}\n\nfunc TestSSHDownloadSourceWithRef(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRefSource)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureRefSource)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --non-interactive --working-dir \"+testPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "test/integration_stacks_test.go",
    "content": "package test_test\n\nimport (\n\t\"bytes\"\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/gruntwork-io/terragrunt/internal/discovery\"\n\t\"github.com/gruntwork-io/terragrunt/internal/git\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/stacks/generate\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config/hclparse\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers/logger\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureStacksBasic                     = \"fixtures/stacks/basic\"\n\ttestFixtureStacksLocals                    = \"fixtures/stacks/locals\"\n\ttestFixtureStacksRemote                    = \"fixtures/stacks/remote\"\n\ttestFixtureStacksInputs                    = \"fixtures/stacks/inputs\"\n\ttestFixtureStacksOutputs                   = \"fixtures/stacks/outputs\"\n\ttestFixtureStacksUnitValues                = \"fixtures/stacks/unit-values\"\n\ttestFixtureStacksLocalsError               = \"fixtures/stacks/errors/locals-error\"\n\ttestFixtureStacksUnitEmptyPath             = \"fixtures/stacks/errors/unit-empty-path\"\n\ttestFixtureStacksEmptyPath                 = \"fixtures/stacks/errors/stack-empty-path\"\n\ttestFixtureStackAbsolutePath               = \"fixtures/stacks/errors/absolute-path\"\n\ttestFixtureStackRelativePathOutsideOfStack = \"fixtures/stacks/errors/relative-path-outside-of-stack\"\n\ttestFixtureStackNotExist                   = \"fixtures/stacks/errors/not-existing-path\"\n\ttestFixtureStackValidationUnitPath         = \"fixtures/stacks/errors/validation-unit\"\n\ttestFixtureStackValidationStackPath        = \"fixtures/stacks/errors/validation-stack\"\n\ttestFixtureStackIncorrectSource            = \"fixtures/stacks/errors/incorrect-source\"\n\ttestFixtureStacksUnknownValueError         = \"fixtures/stacks/errors/unknown-value\"\n\ttestFixtureNoStack                         = \"fixtures/stacks/no-stack\"\n\ttestFixtureNestedStacks                    = \"fixtures/stacks/nested\"\n\ttestFixtureStackValues                     = \"fixtures/stacks/stack-values\"\n\ttestFixtureStackDependencies               = \"fixtures/stacks/dependencies\"\n\ttestFixtureStackSourceMap                  = \"fixtures/stacks/source-map\"\n\ttestFixtureStackCycles                     = \"fixtures/stacks/errors/cycles\"\n\ttestFixtureNoStackNoDir                    = \"fixtures/stacks/no-stack-dir\"\n\ttestFixtureMultipleStacks                  = \"fixtures/stacks/multiple-stacks\"\n\ttestFixtureReadStack                       = \"fixtures/stacks/read-stack\"\n\ttestFixtureStackSelfInclude                = \"fixtures/stacks/self-include\"\n\ttestFixtureStackNestedOutputs              = \"fixtures/stacks/nested-outputs\"\n\ttestFixtureStackNoValidation               = \"fixtures/stacks/no-validation\"\n\ttestFixtureStackTerragruntDir              = \"fixtures/stacks/terragrunt-dir\"\n\ttestFixtureStacksAllNoStackDir             = \"fixtures/stacks/all-no-stack-dir\"\n\ttestFixtureStackNoDotTerragruntStackOutput = \"fixtures/stacks/no-dot-terragrunt-stack-output\"\n\ttestFixtureStackFindInParentFolders        = \"fixtures/stacks/find-in-parent-folders\"\n\ttestFixtureStackOriginalTerragruntDir      = \"fixtures/stacks/get-original-terragrunt-dir\"\n\ttestFixtureStackVersionConstraints         = \"fixtures/stacks/version-constraints\"\n\ttestFixtureStackCoexistHclAndStack         = \"fixtures/stacks/coexist-hcl-and-stack\"\n)\n\nfunc TestStacksGenerateBasicWithQueueIncludeDirFlag(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksBasic)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksBasic, \"live\")\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all plan --queue-include-dir .terragrunt-stack/chicks/chick-2 --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tassert.NotContains(t, stderr, \"- Unit .terragrunt-stack/chicks/chick-1\")\n\tassert.NotContains(t, stderr, \"- Unit .terragrunt-stack/father\")\n\tassert.NotContains(t, stderr, \"- Unit .terragrunt-stack/mother\")\n\tassert.Contains(t, stderr, \"- Unit .terragrunt-stack/chicks/chick-2\")\n\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, path)\n}\n\nfunc TestStacksGenerateBasicWithFilterFlag(t *testing.T) {\n\tt.Parallel()\n\n\t// Skip if filter-flag experiment is not enabled\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksBasic)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksBasic, \"live\")\n\trootPath, err := filepath.EvalSymlinks(rootPath)\n\trequire.NoError(t, err)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all plan --filter './.terragrunt-stack/chicks/chick-2' --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\tassert.NotContains(t, stderr, \"- Unit .terragrunt-stack/chicks/chick-1\")\n\tassert.NotContains(t, stderr, \"- Unit .terragrunt-stack/father\")\n\tassert.NotContains(t, stderr, \"- Unit .terragrunt-stack/mother\")\n\tassert.Contains(t, stderr, \"- Unit .terragrunt-stack/chicks/chick-2\")\n\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, path)\n}\n\nfunc TestStacksGenerateBasicWithQueueExcludeDirFlag(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksBasic)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksBasic, \"live\")\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all plan --queue-exclude-dir .terragrunt-stack/chicks/chick-2 --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"- Unit .terragrunt-stack/chicks/chick-1\")\n\tassert.Contains(t, stderr, \"- Unit .terragrunt-stack/father\")\n\tassert.Contains(t, stderr, \"- Unit .terragrunt-stack/mother\")\n\tassert.NotContains(t, stderr, \"- Unit .terragrunt-stack/chicks/chick-2\")\n\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, path)\n}\n\nfunc TestStacksGenerateBasic(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksBasic)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksBasic, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, path)\n}\n\nfunc TestNestedStacksGenerate(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureNestedStacks)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureNestedStacks)\n\tgitPath := filepath.Join(tmpEnvPath, testFixtureNestedStacks)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(gitPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\trootPath := filepath.Join(gitPath, \"live\")\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\t// Check that logs contain stack generation messages\n\tassert.Contains(t, stderr, \"Generating stack prod from ./terragrunt.stack.hcl\")\n\tassert.Contains(t, stderr, \"Generating stack dev from ./terragrunt.stack.hcl\")\n\tassert.Contains(t, stderr, \"Generating unit prod-api from ./.terragrunt-stack/prod/terragrunt.stack.hcl\")\n\tassert.Contains(t, stderr, \"Generating unit dev-web from ./.terragrunt-stack/dev/terragrunt.stack.hcl\")\n\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, path)\n}\n\nfunc TestStacksGenerateErrorOnCoexistingHclAndStackFiles(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackCoexistHclAndStack)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackCoexistHclAndStack)\n\tgitPath := filepath.Join(tmpEnvPath, testFixtureStackCoexistHclAndStack)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(gitPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\trootPath := filepath.Join(gitPath, \"non-prod\", \"dev\")\n\n\t// Create the conflicting terragrunt.hcl alongside terragrunt.stack.hcl in temp copy.\n\t// Not kept on disk in the fixture to avoid breaking `terragrunt find` from repo root.\n\trequire.NoError(t, os.WriteFile(filepath.Join(rootPath, \"terragrunt.hcl\"), []byte(\"\"), 0644))\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\trequire.Error(t, err)\n\n\tvar coexistErr discovery.CoexistenceError\n\trequire.ErrorAs(t, err, &coexistErr)\n}\n\nfunc TestStacksGenerateLocals(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksLocals)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksLocals)\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(tmpEnvPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksLocals, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack generate --working-dir \"+rootPath)\n}\n\nfunc TestStacksGenerateLocalsError(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksLocalsError)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksLocalsError)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksLocalsError)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\trequire.Error(t, err)\n}\n\nfunc TestStacksRunParseErrorNotSilentlySkipped(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksUnknownValueError)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksUnknownValueError)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksUnknownValueError, \"live\")\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt stack run plan --non-interactive --working-dir \"+rootPath,\n\t)\n\n\t// Command should fail with parsing error, not silently skip the unit\n\trequire.Error(t, err)\n\tassert.Contains(t, stderr, \"missing_var\")\n}\n\nfunc TestStacksGenerateRemote(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksRemote)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksRemote)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksRemote)\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, path)\n}\n\nfunc TestStacksBasic(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksBasic)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksBasic, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, path)\n\n\t// check that the stack was applied and .txt files got generated in the stack directory\n\tvar txtFiles []string\n\n\terr := filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !info.IsDir() && info.Name() == \"test.txt\" {\n\t\t\ttxtFiles = append(txtFiles, filePath)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n\tassert.Len(t, txtFiles, 4)\n}\n\nfunc TestStacksNoGenerate(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksBasic)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksBasic, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, path)\n\n\t// clean .terragrunt-stack contents\n\tentries, err := os.ReadDir(path)\n\trequire.NoError(t, err)\n\n\tfor _, entry := range entries {\n\t\terr = os.RemoveAll(filepath.Join(path, entry.Name()))\n\t\trequire.NoError(t, err)\n\t}\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack run apply --no-stack-generate --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"No units discovered. Creating an empty runner.\")\n}\n\nfunc TestStacksInputs(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksInputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksInputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksInputs, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run plan --non-interactive --working-dir \"+rootPath)\n\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, path)\n}\n\nfunc TestStacksPlan(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksInputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksInputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksInputs, \"live\")\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack run plan --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"Plan: 1 to add, 0 to change, 0 to destroy\")\n\tassert.Contains(t, stdout, \"local_file.file will be created\")\n}\n\nfunc TestStacksApply(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksInputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksInputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksInputs, \"live\")\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"Apply complete! Resources: 1 added, 0 changed, 0 destroyed\")\n\tassert.Contains(t, stdout, \"local_file.file: Creation complete\")\n}\n\nfunc TestStacksApplyRemote(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksRemote)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksRemote)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksRemote)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack run apply --log-level debug --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"app1 (git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/stacks/basic/units/chick?ref=main&depth=1)\")\n\tassert.Contains(t, stderr, \"app2 (git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/stacks/basic/units/chick?ref=main&depth=1)\")\n\tassert.Contains(t, stdout, \"Apply complete! Resources: 1 added, 0 changed, 0 destroyed\")\n\tassert.Contains(t, stdout, \"local_file.file: Creation complete\")\n\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, path)\n}\n\nfunc TestStacksApplyClean(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksInputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksInputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksInputs, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\t// check that path exists\n\tassert.DirExists(t, path)\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack clean --working-dir \"+rootPath)\n\t// check that path don't exist\n\tassert.NoDirExists(t, path)\n}\n\nfunc TestStackCleanRecursively(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureNestedStacks)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureNestedStacks)\n\tgitPath := filepath.Join(tmpEnvPath, testFixtureNestedStacks)\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(gitPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\tlive := filepath.Join(gitPath, \"live\")\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+live)\n\trequire.NoError(t, err)\n\n\tliveV2 := filepath.Join(gitPath, \"live-v2\")\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+liveV2)\n\trequire.NoError(t, err)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack clean --working-dir \"+gitPath)\n\trequire.NoError(t, err)\n\n\tassert.NoDirExists(t, filepath.Join(live, \".terragrunt-stack\"))\n\tassert.NoDirExists(t, filepath.Join(liveV2, \".terragrunt-stack\"))\n\n\tassert.Contains(t, stderr, \"Deleting stack directory: live/.terragrunt-stack\")\n\tassert.Contains(t, stderr, \"Deleting stack directory: live-v2/.terragrunt-stack\")\n}\n\nfunc TestStacksDestroy(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksInputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksInputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksInputs, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack run destroy --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"Plan: 0 to add, 0 to change, 1 to destroy\")\n\tassert.Contains(t, stdout, \"local_file.file: Destroying...\")\n}\n\nfunc TestStackOutputs(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksOutputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksOutputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksOutputs, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack output --non-interactive --working-dir \"+rootPath)\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"custom_value2 = \\\"value2\\\"\")\n\tassert.Contains(t, stdout, \"custom_value1 = \\\"value1\\\"\")\n\tassert.Contains(t, stdout, \"name      = \\\"name1\\\"\")\n\n\tparser := hclparse.NewParser()\n\thcl, diags := parser.ParseHCL([]byte(stdout), \"test.hcl\")\n\tassert.Nil(t, diags)\n\n\tattr, _ := hcl.Body.JustAttributes()\n\tassert.Len(t, attr, 4)\n}\n\nfunc TestStackOutputsRaw(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksOutputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksOutputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksOutputs, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\n\t// Using raw with no specific output key should return an error\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack output --format raw --non-interactive --working-dir \"+rootPath)\n\trequire.Error(t, err, \"Should error when no specific output key is provided with --format raw\")\n\tassert.Contains(t, err.Error(), \"requires a single output value\")\n\n\t// With a specific key, it should work for simple values\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack output filtered_app1.custom_value1 --format raw --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"value1\", strings.TrimSpace(stdout), \"Raw output should print only the value without quotes\")\n\n\t// Complex values should return an error\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack output filtered_app1.complex --format raw --non-interactive --working-dir \"+rootPath)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"Unsupported value for raw output\")\n}\n\nfunc TestStackOutputsIndex(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksOutputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksOutputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksOutputs, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack output project2_app2 --non-interactive --working-dir \"+rootPath)\n\n\trequire.NoError(t, err)\n\tassert.NotContains(t, stdout, \"filtered_app1 = {\")\n\tassert.Contains(t, stdout, \"project2_app2 = {\")\n\n\tparser := hclparse.NewParser()\n\thcl, diags := parser.ParseHCL([]byte(stdout), \"test.hcl\")\n\tassert.Nil(t, diags)\n\n\tattr, _ := hcl.Body.JustAttributes()\n\tassert.Len(t, attr, 1)\n}\n\nfunc TestStackOutputsJson(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksOutputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksOutputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksOutputs, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack output --format json --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tvar result map[string]any\n\n\terr = json.Unmarshal([]byte(stdout), &result)\n\trequire.NoError(t, err)\n\n\tassert.Len(t, result, 4)\n}\n\nfunc TestStackOutputsJsonIndex(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksOutputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksOutputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksOutputs, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt stack output project2_app1 --format json --non-interactive --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\tvar result map[string]any\n\n\terr = json.Unmarshal([]byte(stdout), &result)\n\trequire.NoError(t, err)\n\n\tassert.Len(t, result, 1)\n\tassert.Contains(t, result, \"project2_app1\")\n}\n\nfunc TestStackOutputsRawIndex(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksOutputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksOutputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksOutputs, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack output filtered_app1.custom_value1 --format raw --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"value1\")\n\tassert.NotContains(t, stdout, \"filtered_app1 = {\")\n\tassert.NotContains(t, stdout, \"project2_app2 = {\")\n}\n\nfunc TestStackOutputsRawFlag(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksOutputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksOutputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksOutputs, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack output -raw filtered_app2.data --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"app2\")\n\tassert.NotContains(t, stdout, \"project2_app1 = {\")\n\tassert.NotContains(t, stdout, \"project2_app2 = {\")\n}\n\nfunc TestStackOutputsJsonFlag(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksOutputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksOutputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksOutputs, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack output -json --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tvar result map[string]any\n\n\terr = json.Unmarshal([]byte(stdout), &result)\n\trequire.NoError(t, err)\n\n\tassert.Len(t, result, 4)\n}\n\nfunc TestStacksUnitValues(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksUnitValues)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksUnitValues)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksUnitValues, \"live\")\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"deployment = \\\"app1\\\"\")\n\tassert.Contains(t, stdout, \"deployment = \\\"app2\\\"\")\n\tassert.Contains(t, stdout, \"project = \\\"test-project\\\"\")\n\tassert.Contains(t, stdout, \"data = \\\"payload: app1-test-project\\\"\")\n\tassert.Contains(t, stdout, \"data = \\\"payload: app2-test-project\\\"\")\n}\n\nfunc TestStacksUnitValuesRunInApp1(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksUnitValues)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksUnitValues)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksUnitValues, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\n\t// run apply in generated app1 directory\n\tapp1Path := filepath.Join(rootPath, \".terragrunt-stack\", \"app1\")\n\thelpers.RunTerragrunt(t, \"terragrunt apply --non-interactive --working-dir \"+app1Path)\n\n\t// Verify the expected outcomes\n\tvaluesPath := filepath.Join(app1Path, \"terragrunt.values.hcl\")\n\tassert.FileExists(t, valuesPath)\n\n\t// Verify the values file content\n\tcontent, err := os.ReadFile(valuesPath)\n\trequire.NoError(t, err)\n\tassert.Contains(t, string(content), \"deployment = \\\"app1\\\"\")\n}\n\nfunc TestStacksUnitValuesOutput(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksUnitValues)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksUnitValues)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksUnitValues, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack output -json --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tvar result map[string]any\n\n\terr = json.Unmarshal([]byte(stdout), &result)\n\trequire.NoError(t, err)\n\n\tassert.Len(t, result, 2)\n\t// check if app1 and app2 are present in the result\n\tassert.Contains(t, result, \"app1\")\n\tassert.Contains(t, result, \"app2\")\n}\n\nfunc TestStacksUnitEmptyPathError(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksUnitEmptyPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksUnitEmptyPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksUnitEmptyPath, \"live\")\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\trequire.Error(t, err)\n\n\tmessage := err.Error()\n\t// check for app1 and app2 empty path error\n\tassert.Contains(t, message, \"unit 'app1_empty_path' has empty path\")\n\tassert.Contains(t, message, \"unit 'app2_empty_path' has empty path\")\n\tassert.NotContains(t, message, \"unit 'app3_not_empty_path' has empty path\")\n}\n\nfunc TestStackStackEmptyPathError(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksEmptyPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksEmptyPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksEmptyPath)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\trequire.Error(t, err)\n\n\tmessage := err.Error()\n\tassert.Contains(t, message, \"stack 'prod' has empty path\")\n}\n\nfunc TestNestedStackOutput(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureNestedStacks)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureNestedStacks)\n\tgitPath := filepath.Join(tmpEnvPath, testFixtureNestedStacks)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(gitPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\trootPath := filepath.Join(gitPath, \"live\")\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt stack output -json --non-interactive --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\tvar result map[string]any\n\n\terr = json.Unmarshal([]byte(stdout), &result)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, result, \"dev\")\n\tassert.Contains(t, result, \"prod\")\n\n\t// Check dev outputs\n\tdevOutputs := result[\"dev\"].(map[string]any)\n\tassert.Contains(t, devOutputs, \"dev-api\")\n\tassert.Contains(t, devOutputs, \"dev-db\")\n\tassert.Contains(t, devOutputs, \"dev-web\")\n\n\tassert.Equal(t, \"api dev-api 1.0.0\", devOutputs[\"dev-api\"].(map[string]any)[\"data\"])\n\tassert.Equal(t, \"db dev-db 1.0.0\", devOutputs[\"dev-db\"].(map[string]any)[\"data\"])\n\tassert.Equal(t, \"web dev-web 1.0.0\", devOutputs[\"dev-web\"].(map[string]any)[\"data\"])\n\n\t// Check prod outputs\n\tprodOutputs := result[\"prod\"].(map[string]any)\n\tassert.Contains(t, prodOutputs, \"prod-api\")\n\tassert.Contains(t, prodOutputs, \"prod-db\")\n\tassert.Contains(t, prodOutputs, \"prod-web\")\n\n\tassert.Equal(t, \"api prod-api 1.0.0\", prodOutputs[\"prod-api\"].(map[string]any)[\"data\"])\n\tassert.Equal(t, \"db prod-db 1.0.0\", prodOutputs[\"prod-db\"].(map[string]any)[\"data\"])\n\tassert.Equal(t, \"web prod-web 1.0.0\", prodOutputs[\"prod-web\"].(map[string]any)[\"data\"])\n}\n\nfunc TestNestedStacksApply(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureNestedStacks)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureNestedStacks)\n\tgitPath := filepath.Join(tmpEnvPath, testFixtureNestedStacks)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(gitPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\trootPath := filepath.Join(gitPath, \"live\")\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"data = \\\"web dev-web 1.0.0\\\"\")\n\tassert.Contains(t, stdout, \"data = \\\"api dev-api 1.0.0\\\"\")\n\tassert.Contains(t, stdout, \"data = \\\"db dev-db 1.0.0\\\"\")\n\n\tassert.Contains(t, stdout, \"data = \\\"web prod-web 1.0.0\\\"\")\n\tassert.Contains(t, stdout, \"data = \\\"api prod-api 1.0.0\\\"\")\n\tassert.Contains(t, stdout, \"data = \\\"db prod-db 1.0.0\\\"\")\n}\n\nfunc TestStackValuesGeneration(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackValues)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackValues)\n\tgitPath := filepath.Join(tmpEnvPath, testFixtureStackValues)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(gitPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\trootPath := filepath.Join(gitPath, \"live\")\n\thelpers.RunTerragrunt(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, path)\n\n\t// check that is generated terragrunt.values.hcl\n\tvaluesPath := filepath.Join(path, \"dev\", \"terragrunt.values.hcl\")\n\tassert.FileExists(t, valuesPath)\n}\n\nfunc TestStackValuesApply(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackValues)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackValues)\n\tgitPath := filepath.Join(tmpEnvPath, testFixtureStackValues)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(gitPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\trootPath := filepath.Join(gitPath, \"live\")\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"project = \\\"dev-project\\\"\")\n\tassert.Contains(t, stdout, \"env = \\\"dev\\\"\")\n\tassert.Contains(t, stdout, \"data = \\\"dev-app-1\\\"\")\n\n\tassert.Contains(t, stdout, \"project = \\\"prod-project\\\"\")\n\tassert.Contains(t, stdout, \"env = \\\"prod\\\"\")\n\tassert.Contains(t, stdout, \"data = \\\"prod-app-1\\\"\")\n\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, path)\n\n\tdevValuesPath := filepath.Join(path, \"dev\", \"terragrunt.values.hcl\")\n\tassert.FileExists(t, devValuesPath)\n\n\tprodValuesPath := filepath.Join(path, \"prod\", \"terragrunt.values.hcl\")\n\tassert.FileExists(t, prodValuesPath)\n}\n\nfunc TestStackValuesOutput(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackValues)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackValues)\n\tgitPath := filepath.Join(tmpEnvPath, testFixtureStackValues)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(gitPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\trootPath := filepath.Join(gitPath, \"live\")\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack output -json --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tvar result map[string]map[string]map[string]string\n\n\terr = json.Unmarshal([]byte(stdout), &result)\n\trequire.NoError(t, err)\n\n\t// Check the structure of the JSON\n\tassert.Contains(t, result, \"dev\")\n\tassert.Contains(t, result, \"prod\")\n\n\t// Check dev-app-1\n\tdevApp1 := result[\"dev\"][\"dev-app-1\"]\n\tassert.Equal(t, \"dev-project dev dev-app-1\", devApp1[\"config\"])\n\tassert.Equal(t, \"dev-app-1\", devApp1[\"data\"])\n\tassert.Equal(t, \"dev\", devApp1[\"env\"])\n\tassert.Equal(t, \"dev-project\", devApp1[\"project\"])\n\n\t// Check dev-app-2\n\tdevApp2 := result[\"dev\"][\"dev-app-2\"]\n\tassert.Equal(t, \"dev-project dev dev-app-2\", devApp2[\"config\"])\n\tassert.Equal(t, \"dev-app-2\", devApp2[\"data\"])\n\tassert.Equal(t, \"dev\", devApp2[\"env\"])\n\tassert.Equal(t, \"dev-project\", devApp2[\"project\"])\n\n\t// Check prod-app-1\n\tprodApp1 := result[\"prod\"][\"prod-app-1\"]\n\tassert.Equal(t, \"prod-project prod prod-app-1\", prodApp1[\"config\"])\n\tassert.Equal(t, \"prod-app-1\", prodApp1[\"data\"])\n\tassert.Equal(t, \"prod\", prodApp1[\"env\"])\n\tassert.Equal(t, \"prod-project\", prodApp1[\"project\"])\n\n\t// Check prod-app-2\n\tprodApp2 := result[\"prod\"][\"prod-app-2\"]\n\tassert.Equal(t, \"prod-project prod prod-app-2\", prodApp2[\"config\"])\n\tassert.Equal(t, \"prod-app-2\", prodApp2[\"data\"])\n\tassert.Equal(t, \"prod\", prodApp2[\"env\"])\n\tassert.Equal(t, \"prod-project\", prodApp2[\"project\"])\n}\n\nfunc TestStacksGenerateParallelism(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackDependencies)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackDependencies)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackDependencies, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack generate --parallelism 10 --working-dir \"+rootPath)\n\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, path)\n}\n\nfunc TestStackApplyWithDependency(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackDependencies)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackDependencies)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackDependencies, \"live\")\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"Unit .terragrunt-stack/app-with-dependency\")\n\n\t// check that data.txt exists in the cache directory (since terraform now runs from cache)\n\tappWithDepPath := filepath.Join(rootPath, \".terragrunt-stack\", \"app-with-dependency\")\n\tassert.True(t, helpers.FileExistsInCache(t, appWithDepPath, \"data.txt\"))\n}\n\nfunc TestStackApplyWithDependencyParallelism(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackDependencies)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackDependencies)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackDependencies, \"live\")\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack run apply --parallelism 10 --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"Unit .terragrunt-stack/app-with-dependency\")\n\n\t// check that data.txt exists in the cache directory (since terraform now runs from cache)\n\tappWithDepPath := filepath.Join(rootPath, \".terragrunt-stack\", \"app-with-dependency\")\n\tassert.True(t, helpers.FileExistsInCache(t, appWithDepPath, \"data.txt\"))\n}\n\nfunc TestStackApplyWithDependencyReducedParallelism(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackDependencies)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackDependencies)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackDependencies, \"live\")\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack run apply --parallelism 1 --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"Unit .terragrunt-stack/app-with-dependency\")\n\n\t// check that data.txt exists in the cache directory (since terraform now runs from cache)\n\tappWithDepPath := filepath.Join(rootPath, \".terragrunt-stack\", \"app-with-dependency\")\n\tassert.True(t, helpers.FileExistsInCache(t, appWithDepPath, \"data.txt\"))\n}\n\nfunc TestStackApplyDestroyWithDependency(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackDependencies)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackDependencies)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackDependencies, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack run destroy --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Unit .terragrunt-stack/app-with-dependency\")\n\n\t// check that the data.txt file was deleted\n\tdataPath := filepath.Join(rootPath, \".terragrunt-stack\", \"app-with-dependency\", \"data.txt\")\n\tassert.True(t, util.FileNotExists(dataPath))\n}\n\nfunc TestStackOutputWithDependency(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackDependencies)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackDependencies)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackDependencies, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack output -json --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tvar result map[string]any\n\n\terr = json.Unmarshal([]byte(stdout), &result)\n\trequire.NoError(t, err)\n\n\tassert.Len(t, result, 4)\n\n\tassert.Contains(t, result, \"app-with-dependency\")\n\tassert.Contains(t, result, \"app1\")\n\tassert.Contains(t, result, \"app2\")\n\tassert.Contains(t, result, \"app3\")\n\n\t// check that result map under app-with-dependency contains result key with value \"app1\"\n\tif appWithDependency, ok := result[\"app-with-dependency\"].(map[string]any); ok {\n\t\tassert.Contains(t, appWithDependency, \"result\")\n\t\tassert.Equal(t, \"app1\", appWithDependency[\"result\"])\n\t} else {\n\t\tt.Errorf(\"Expected result[\\\"app-with-dependency\\\"] to be a map, but it was not.\")\n\t}\n}\n\nfunc TestStackApplyStrictInclude(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackDependencies)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackDependencies)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackDependencies, \"live\")\n\trootPath, err := filepath.EvalSymlinks(rootPath)\n\trequire.NoError(t, err)\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack generate --non-interactive --working-dir \"+rootPath)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt stack run apply --queue-include-dir=./.terragrunt-stack/app1 --non-interactive --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"Unit .terragrunt-stack/app1\")\n\tassert.NotContains(t, stderr, \"Unit .terragrunt-stack/app2\")\n\tassert.NotContains(t, stderr, \"Unit .terragrunt-stack/app-with-dependency\")\n\n\t// check that test file wasn't created\n\tdataPath := filepath.Join(rootPath, \".terragrunt-stack\", \"app-with-dependency\", \"data.txt\")\n\tassert.True(t, util.FileNotExists(dataPath))\n}\n\nfunc TestStackApplyStrictIncludeWithFilter(t *testing.T) {\n\tt.Parallel()\n\n\t// Skip if filter-flag experiment is not enabled\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackDependencies)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackDependencies)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackDependencies, \"live\")\n\trootPath, err := filepath.EvalSymlinks(rootPath)\n\trequire.NoError(t, err)\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack generate --non-interactive --working-dir \"+rootPath)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt stack run apply --filter ./.terragrunt-stack/app1 --non-interactive --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"Unit .terragrunt-stack/app1\")\n\tassert.NotContains(t, stderr, \"Unit .terragrunt-stack/app2\")\n\tassert.NotContains(t, stderr, \"Unit .terragrunt-stack/app-with-dependency\")\n\n\t// check that test file wasn't created\n\tdataPath := filepath.Join(rootPath, \".terragrunt-stack\", \"app-with-dependency\", \"data.txt\")\n\tassert.True(t, util.FileNotExists(dataPath))\n}\n\nfunc TestStacksSourceMap(t *testing.T) {\n\tt.Parallel()\n\n\t// prepare local path to do override of source url\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksBasic)\n\tlocalTmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksBasic)\n\n\tlocalTmpTest := filepath.Join(localTmpEnvPath, \"test\", \"fixtures\")\n\tif err := os.MkdirAll(localTmpTest, 0755); err != nil {\n\t\tassert.NoError(t, err)\n\t}\n\n\tif err := util.CopyFolderContentsWithFilter(logger.CreateLogger(), filepath.Join(localTmpEnvPath, \"fixtures\"), localTmpTest, \".terragrunt-test\", func(path string) bool {\n\t\treturn true\n\t}); err != nil {\n\t\tassert.NoError(t, err)\n\t}\n\n\t// prepare local environment with remote to use source map to replace\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksRemote)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksRemote)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksRemote)\n\n\t// generate path with replacement of local source with local path\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --source-map git::https://github.com/gruntwork-io/terragrunt.git=\"+localTmpEnvPath+\" --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"Generating unit app1\")\n\tassert.Contains(t, stderr, \"Generating unit app2\")\n\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, path)\n\n\t_, stderr, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack run apply --log-level debug --source-map git::https://github.com/gruntwork-io/terragrunt.git=\"+localTmpEnvPath+\" --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\t// validate that the source map was used to replace the source\n\tassert.NotContains(t, stderr, \"app1 (git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/stacks/basic/units/chick?ref=main&depth=1)\")\n\tassert.NotContains(t, stderr, \"app2 (git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/stacks/basic/units/chick?ref=main&depth=1)\")\n\n\tassert.Contains(t, stderr, \"Running ./.terragrunt-stack/app1\")\n\tassert.Contains(t, stderr, \"Running ./.terragrunt-stack/app2\")\n}\n\nfunc TestStacksSourceMapModule(t *testing.T) {\n\tt.Parallel()\n\t// prepare local environment with remote to use source map to replace\n\thelpers.CleanupTerraformFolder(t, testFixtureStackSourceMap)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackSourceMap)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackSourceMap, \"live\")\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt, \"terragrunt stack generate --source-map git::https://git-host.com/not-existing-repo.git=\"+tmpEnvPath+\" --log-level debug --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\tassert.NotContains(t, stderr, \"git-host.com/not-existing-repo.git\")\n\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, path)\n\n\t_, stderr, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack run apply --log-level debug --source-map git::https://git-host.com/not-existing-repo.git=\"+tmpEnvPath+\"  --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tassert.NotContains(t, stderr, \"git-host.com/not-existing-repo.git\")\n\tassert.Contains(t, stderr, \"Unit .terragrunt-stack/app1\")\n\tassert.Contains(t, stderr, \"Unit .terragrunt-stack/app2\")\n}\n\nfunc TestStacksGenerateAbsolutePathError(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackAbsolutePath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackAbsolutePath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackAbsolutePath, \"live\")\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(rootPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt stack generate --log-level debug --working-dir \"+rootPath,\n\t)\n\n\trequire.Error(t, err)\n}\n\nfunc TestStacksGenerateIncorrectSource(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackIncorrectSource)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackIncorrectSource)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackIncorrectSource, \"live\")\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(rootPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt stack generate --log-level debug --working-dir \"+rootPath,\n\t)\n\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"Failed to fetch unit api\")\n}\n\nfunc TestStacksGenerateRelativePathError(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackRelativePathOutsideOfStack)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackRelativePathOutsideOfStack)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackRelativePathOutsideOfStack, \"live\")\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(rootPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt stack generate --log-level debug --working-dir \"+rootPath,\n\t)\n\n\trequire.Error(t, err)\n\n\tassert.Contains(t, err.Error(), \"app1 destination path\")\n\tassert.Contains(t, err.Error(), \"is outside of the stack directory\")\n}\n\nfunc TestStacksGenerateNoStack(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureNoStack)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureNoStack)\n\tgitPath := filepath.Join(tmpEnvPath, testFixtureNoStack)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(gitPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\trootPath := filepath.Join(gitPath, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\n\tvalidateNoStackDirs(t, rootPath)\n}\n\nfunc TestStacksApplyNoStack(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureNoStack)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureNoStack)\n\tgitPath := filepath.Join(tmpEnvPath, testFixtureNoStack)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(gitPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\trootPath := filepath.Join(gitPath, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --log-level debug --non-interactive --working-dir \"+rootPath)\n\n\tvalidateNoStackDirs(t, rootPath)\n}\n\nfunc TestStacksCyclesErrors(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackCycles)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackCycles)\n\tgitPath := filepath.Join(tmpEnvPath, testFixtureStackCycles)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(gitPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\trootPath := filepath.Join(gitPath, \"live\")\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\trequire.Error(t, err)\n\n\t// On macOS, the error that the filename is too long happens before cycles are detected.\n\tif !strings.Contains(err.Error(), \"Cycle detected\") {\n\t\tassert.Contains(t, err.Error(), \"file name too long\")\n\n\t\treturn\n\t}\n\n\tassert.Contains(t, err.Error(), \"Cycle detected\")\n}\n\nfunc TestStacksNoStackDirDirectoryCreated(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureNoStackNoDir)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureNoStackNoDir)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureNoStackNoDir, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\t// validate that the stack directory is not created\n\tassert.NoDirExists(t, path)\n}\n\nfunc TestStacksGeneratePrintWarning(t *testing.T) {\n\tt.Parallel()\n\n\trootPath := helpers.TmpDirWOSymlinks(t)\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\tassert.Contains(t, stderr, \"No stack files found\")\n\trequire.NoError(t, err)\n}\n\nfunc TestStacksNotExistingPathError(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackNotExist)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackNotExist)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackNotExist)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\trequire.Error(t, err)\n}\n\nfunc TestStacksGenerateMultipleStacks(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureMultipleStacks)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureMultipleStacks)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureMultipleStacks)\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\n\tdevStack := filepath.Join(rootPath, \"dev\", \".terragrunt-stack\")\n\tvalidateStackDir(t, devStack)\n\n\tliveStack := filepath.Join(rootPath, \"live\", \".terragrunt-stack\")\n\tvalidateStackDir(t, liveStack)\n}\n\nfunc TestStacksReadFiles(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureReadStack)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureReadStack)\n\tgitPath := filepath.Join(tmpEnvPath, testFixtureReadStack)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(gitPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\trootPath := filepath.Join(gitPath, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --log-level debug --non-interactive --working-dir \"+rootPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt stack output --non-interactive --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"stack_local_project = \\\"test-project\\\"\")\n\tassert.Contains(t, stdout, \"unit_value_version  = \\\"6.6.6\\\"\")\n\n\tparser := hclparse.NewParser()\n\thcl, diags := parser.ParseHCL([]byte(stdout), \"test.hcl\")\n\tassert.Nil(t, diags)\n\n\tattr, _ := hcl.Body.JustAttributes()\n\tassert.Len(t, attr, 3)\n\n\t// fetch for dev-app-2 output\n\tstdout, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt stack output dev --non-interactive --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\thcl, diags = parser.ParseHCL([]byte(stdout), \"dev.hcl\")\n\trequire.Nil(t, diags, diags.Error())\n\n\ttopLevelAttrs, _ := hcl.Body.JustAttributes()\n\tassert.Len(t, topLevelAttrs, 1, \"Expected one top-level attribute (dev)\")\n\n\tdevAttr, exists := topLevelAttrs[\"dev\"]\n\tassert.True(t, exists, \"dev block should exist\")\n\n\tif exists {\n\t\tdevObjVal, diags := devAttr.Expr.Value(nil)\n\t\tassert.Nil(t, diags)\n\n\t\tif devObjVal.Type().IsObjectType() {\n\t\t\tdevApp2Attr := devObjVal.GetAttr(\"dev-app-2\")\n\t\t\tassert.False(t, devApp2Attr.IsNull(), \"dev-app-2 block should exist in dev\")\n\n\t\t\tif !devApp2Attr.IsNull() {\n\t\t\t\tobjVal := devApp2Attr\n\t\t\t\tassert.False(t, objVal.IsNull(), \"dev-app-2 block should exist in dev\")\n\n\t\t\t\tif !objVal.IsNull() {\n\t\t\t\t\texpectedValues := map[string]string{\n\t\t\t\t\t\t\"config\":              \"dev-project dev dev-app-2\",\n\t\t\t\t\t\t\"data\":                \"dev-app-2\",\n\t\t\t\t\t\t\"env\":                 \"dev\",\n\t\t\t\t\t\t\"project\":             \"dev-project\",\n\t\t\t\t\t\t\"stack_local_project\": \"test-project\",\n\t\t\t\t\t\t\"stack_value_env\":     \"dev\",\n\t\t\t\t\t\t\"unit_name\":           \"test_app\",\n\t\t\t\t\t\t\"unit_source\":         \"../units/app\",\n\t\t\t\t\t\t\"unit_value_version\":  \"6.6.6\",\n\t\t\t\t\t}\n\n\t\t\t\t\tif objVal.Type().IsObjectType() {\n\t\t\t\t\t\tfor field, expectedValue := range expectedValues {\n\t\t\t\t\t\t\tattrVal := objVal.GetAttr(field)\n\t\t\t\t\t\t\tassert.False(t, attrVal.IsNull(), \"Field %s should exist in output\", field)\n\n\t\t\t\t\t\t\tif !attrVal.IsNull() {\n\t\t\t\t\t\t\t\tassert.Equal(\n\t\t\t\t\t\t\t\t\tt,\n\t\t\t\t\t\t\t\t\texpectedValue,\n\t\t\t\t\t\t\t\t\tattrVal.AsString(),\n\t\t\t\t\t\t\t\t\t\"Field %s should have value %s\",\n\t\t\t\t\t\t\t\t\tfield,\n\t\t\t\t\t\t\t\t\texpectedValue,\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\tstackSource := objVal.GetAttr(\"stack_source\")\n\t\t\t\t\t\tassert.False(t, stackSource.IsNull(), \"Field stack_source should exist in output\")\n\n\t\t\t\t\t\tif !stackSource.IsNull() {\n\t\t\t\t\t\t\tassert.Contains(t, stackSource.AsString(), \"/fixtures/stacks/read-stack/stacks/dev\")\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Verify expected fields count (including stack_source)\n\t\t\t\t\t\tvalueMap := objVal.AsValueMap()\n\t\t\t\t\t\tassert.Len(\n\t\t\t\t\t\t\tt,\n\t\t\t\t\t\t\tvalueMap,\n\t\t\t\t\t\t\tlen(expectedValues)+1,\n\t\t\t\t\t\t\t\"Expected %d fields in dev-app-2\",\n\t\t\t\t\t\t\tlen(expectedValues)+1,\n\t\t\t\t\t\t)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.Fatalf(\"Expected dev-app-2 to be an object type, got %s\", objVal.Type().FriendlyName())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tt.Fatalf(\"Expected dev to be an object type, got %s\", devObjVal.Type().FriendlyName())\n\t\t}\n\t}\n}\n\nfunc TestStackUnitValidation(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackValidationUnitPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackValidationUnitPath)\n\tgitPath := filepath.Join(tmpEnvPath, testFixtureStackValidationUnitPath)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(gitPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\trootPath := filepath.Join(gitPath, \"live\")\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt stack generate --no-stack-validate --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\tliveStack := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, liveStack)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"Validation failed for unit v1\")\n\tassert.Contains(t, err.Error(), \"expected unit to generate with terragrunt.hcl file at root of generated directory\")\n}\n\nfunc TestStackValidation(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackValidationStackPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackValidationStackPath)\n\tgitPath := filepath.Join(tmpEnvPath, testFixtureStackValidationStackPath)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(gitPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\trootPath := filepath.Join(gitPath, \"live\")\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt stack generate --no-stack-validate --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\tliveStack := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, liveStack)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\trequire.Error(t, err)\n\n\tassert.Contains(t, err.Error(), \"Validation failed for stack stack-v1\")\n\tassert.Contains(\n\t\tt,\n\t\terr.Error(),\n\t\t\"expected stack to generate with terragrunt.stack.hcl file at root of generated directory\",\n\t)\n}\n\n// validateNoStackDirs check if the directories outside of stack are created and contain test files\nfunc validateNoStackDirs(t *testing.T, rootPath string) {\n\tt.Helper()\n\n\tstackConfig := filepath.Join(rootPath, \"stack-config\")\n\tassert.DirExists(t, stackConfig)\n\n\tunitConfig := filepath.Join(rootPath, \"unit-config\")\n\tassert.DirExists(t, unitConfig)\n\n\tconfigPath := filepath.Join(stackConfig, \"config.txt\")\n\tassert.FileExists(t, configPath)\n\n\tconfigPath = filepath.Join(unitConfig, \"config.txt\")\n\tassert.FileExists(t, configPath)\n\n\tsecondStackUnitConfigDir := filepath.Join(rootPath, \".terragrunt-stack\", \"dev\", \"second-stack-unit-config\")\n\tsecondStackUnitConfig := filepath.Join(secondStackUnitConfigDir, \"config.txt\")\n\n\tassert.DirExists(t, secondStackUnitConfigDir)\n\tassert.FileExists(t, secondStackUnitConfig)\n}\n\nfunc TestStacksSelfInclude(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackSelfInclude)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackSelfInclude)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackSelfInclude, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\n\tpath := filepath.Join(rootPath, \".terragrunt-stack\")\n\tvalidateStackDir(t, path)\n\n\t// validate that subsequent runs don't fail\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n}\n\nfunc TestStackNestedOutputs(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackNestedOutputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackNestedOutputs)\n\tgitPath := filepath.Join(tmpEnvPath, testFixtureStackNestedOutputs)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(gitPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\trootPath := filepath.Join(gitPath, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack run apply --non-interactive --working-dir \"+rootPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt stack output --non-interactive --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\t// Parse the HCL output\n\tparser := hclparse.NewParser()\n\thclFile, diags := parser.ParseHCL([]byte(stdout), \"test.hcl\")\n\trequire.False(t, diags.HasErrors(), \"Failed to parse HCL: %s\", diags.Error())\n\n\trequire.Nil(t, diags)\n\trequire.NotNil(t, hclFile)\n\n\ttopLevelAttrs, _ := hclFile.Body.JustAttributes()\n\t_, app1Exists := topLevelAttrs[\"app_1\"]\n\tassert.True(t, app1Exists, \"app_1 block should exist\")\n\n\t_, app2Exists := topLevelAttrs[\"app_2\"]\n\tassert.True(t, app2Exists, \"app_2 block should exist\")\n\n\t_, stackV2Exists := topLevelAttrs[\"stack_v2\"]\n\tassert.True(t, stackV2Exists, \"stack_v2 block should exist\")\n}\n\nfunc TestStacksNoValidation(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackNoValidation)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackNoValidation)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackNoValidation, \"live\")\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt stack run plan --non-interactive --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"Unit .terragrunt-stack/stack1/stack1/.terragrunt-stack/unit2/app1/code\")\n\tassert.Contains(t, stderr, \"Unit .terragrunt-stack/unit1/app1/code\")\n\n\tassert.Contains(t, stdout, \"Plan: 1 to add, 0 to change, 0 to destroy\")\n\tassert.Contains(t, stdout, \"local_file.file will be created\")\n}\n\n// check if the stack directory is created and contains files.\nfunc validateStackDir(t *testing.T, path string) {\n\tt.Helper()\n\tassert.DirExists(t, path)\n\n\t// check that path is not empty directory\n\tentries, err := os.ReadDir(path)\n\trequire.NoError(t, err, \"Failed to read directory contents\")\n\n\thasSubdirectories := false\n\n\tfor _, entry := range entries {\n\t\tif entry.IsDir() {\n\t\t\thasSubdirectories = true\n\n\t\t\tbreak\n\t\t}\n\t}\n\n\tassert.True(t, hasSubdirectories, \"The .terragrunt-stack directory should contain at least one subdirectory\")\n}\n\nfunc TestStackTerragruntDir(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackTerragruntDir)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackTerragruntDir)\n\tgitPath := filepath.Join(tmpEnvPath, testFixtureStackTerragruntDir)\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(gitPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\trootPath := filepath.Join(gitPath, \"live\")\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt stack generate --no-stack-validate --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\tout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt apply --all --non-interactive --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\texpectedTerragruntDir := filepath.Join(rootPath, \"tennant_1\")\n\tassert.Contains(t, out, fmt.Sprintf(`terragrunt_dir = \"%s\"`, expectedTerragruntDir))\n}\n\nfunc TestStackOriginalTerragruntDir(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackOriginalTerragruntDir)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackOriginalTerragruntDir)\n\tgitPath := filepath.Join(tmpEnvPath, testFixtureStackOriginalTerragruntDir)\n\thelpers.CreateGitRepo(t, gitPath)\n\trootPath := filepath.Join(gitPath, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\n\tvar valuesFiles []string\n\n\tconst (\n\t\tvaluesFileName  = \"terragrunt.values.hcl\"\n\t\tdotStackDirName = \".terragrunt-stack\"\n\t\tnestedUnitDirs  = \"unit_dirs\"\n\t)\n\n\terr := filepath.WalkDir(rootPath, func(path string, d os.DirEntry, err error) error {\n\t\trequire.NoError(t, err)\n\n\t\tif d.IsDir() {\n\t\t\treturn nil\n\t\t}\n\n\t\tif filepath.Base(path) == valuesFileName {\n\t\t\tvaluesFiles = append(valuesFiles, path)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, valuesFiles)\n\n\tfor _, valuesPath := range valuesFiles {\n\t\tcontent, readErr := os.ReadFile(valuesPath)\n\t\trequire.NoError(t, readErr)\n\n\t\tidx := strings.Index(valuesPath, string(os.PathSeparator)+dotStackDirName+string(os.PathSeparator))\n\t\tif idx == -1 {\n\t\t\tcontinue\n\t\t}\n\n\t\texpected := valuesPath[:idx]\n\n\t\tisNoLocals := strings.Contains(valuesPath, \"no-locals\")\n\t\tisNestedReadConfig := strings.Contains(valuesPath, \"read-config-nested\")\n\t\tisNestedLocals := strings.Contains(valuesPath, \"with-locals-nested\")\n\n\t\tif isNoLocals && (isNestedReadConfig || isNestedLocals) {\n\t\t\t// In these fixtures we intentionally validate the \"no locals + nested stacks\" behavior whereby a user,\n\t\t\t// attempts to invoke the get_original_terragrunt_dir() function within the values block. Due to the order\n\t\t\t// of evaluation this scenario will resolve to the generated child stack directory rather than the parent\n\t\t\t// stack root. If users intend to acquire the parent stack directory at generate time they must do it from\n\t\t\t// the locals block either directly or in another config evaluated via read_terragrunt_config().\n\t\t\texpected = filepath.Join(expected, dotStackDirName, nestedUnitDirs)\n\t\t}\n\n\t\texpected = filepath.ToSlash(expected)\n\n\t\tassert.Contains(t, string(content), `stack_dir = \"`+expected+`\"`, \"wrong stack_dir in %s\", valuesPath)\n\t}\n}\n\nfunc TestStackRunAllNoStackDir(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStacksAllNoStackDir)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStacksAllNoStackDir)\n\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStacksAllNoStackDir, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\n\t// Verify that no .terragrunt-stack directory was created since all units have no_dot_terragrunt_stack = true\n\tstackDir := filepath.Join(rootPath, \".terragrunt-stack\")\n\tstackDirExists := util.FileExists(stackDir)\n\tt.Logf(\"Stack directory exists: %v\", stackDirExists)\n\n\t// Verify that units were generated in the same directory as terragrunt.stack.hcl\n\texpectedUnits := []string{\"foo\", \"bar\"}\n\tfor _, unit := range expectedUnits {\n\t\tunitPath := filepath.Join(rootPath, unit)\n\t\tassert.True(t, util.FileExists(unitPath), \"Expected unit %s to exist in root directory\", unit)\n\t\tassert.True(t, util.FileExists(\n\t\t\tfilepath.Join(unitPath, \"terragrunt.hcl\"),\n\t\t), \"Expected terragrunt.hcl to exist in unit %s\", unit)\n\t}\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt stack run plan --non-interactive --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err, \"Expected stack run to succeed when all units have no_dot_terragrunt_stack = true\")\n\n\tassert.Contains(t, stdout, \"Changes to Outputs:\")\n\tassert.Contains(t, stdout, \"+ test = \\\"value\\\"\")\n}\n\nfunc TestStackOutputWithNoDotTerragruntStack(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackNoDotTerragruntStackOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackNoDotTerragruntStackOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackNoDotTerragruntStackOutput, \"live\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt stack generate --working-dir \"+rootPath)\n\n\tunitPath := filepath.Join(rootPath, \"app1\")\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+unitPath)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt stack output --non-interactive --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"name = \\\"app1\\\"\")\n}\n\nfunc TestStackFindInParentFolders(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureStackFindInParentFolders)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackFindInParentFolders)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackFindInParentFolders)\n\tstackPath := filepath.Join(rootPath, \"live\", \"stack\")\n\n\t// Run stack with --queue-exclude-dir to exclude units source directory\n\t// This tests that source templates are skipped during parsing\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt stack run plan --queue-exclude-dir '**/units/**' --working-dir \"+stackPath,\n\t)\n\trequire.NoError(t, err)\n\n\t// Verify generated unit runs\n\tassert.Contains(t, stderr, \"- Unit .terragrunt-stack/foo\")\n\n\t// Verify source template is excluded\n\tassert.NotContains(t, stderr, \"- Unit units/foo\")\n}\n\nfunc TestStackGenerateWithFilter(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureNestedStacks)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureNestedStacks)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureNestedStacks)\n\tliveDir := filepath.Join(rootPath, \"live\")\n\n\trunner, err := git.NewGitRunner()\n\trequire.NoError(t, err)\n\n\trunner = runner.WithWorkDir(rootPath)\n\n\terr = runner.Init(t.Context())\n\trequire.NoError(t, err)\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\t\"terragrunt stack generate --working-dir \"+liveDir,\n\t)\n\n\tstackDir := filepath.Join(liveDir, \".terragrunt-stack\")\n\trequire.DirExists(t, stackDir)\n\n\tdevDir := filepath.Join(stackDir, \"dev\", \".terragrunt-stack\")\n\trequire.DirExists(t, devDir)\n\n\tprodDir := filepath.Join(stackDir, \"prod\", \".terragrunt-stack\")\n\trequire.DirExists(t, prodDir)\n\n\trequire.NoError(t, os.RemoveAll(stackDir))\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\t\"terragrunt stack generate --working-dir \"+liveDir+\" --filter 'live | type=stack' --filter 'dev | type=stack'\",\n\t)\n\n\tstackDir = filepath.Join(liveDir, \".terragrunt-stack\")\n\trequire.DirExists(t, stackDir)\n\n\tdevDir = filepath.Join(stackDir, \"dev\", \".terragrunt-stack\")\n\trequire.DirExists(t, devDir)\n\n\tprodDir = filepath.Join(stackDir, \"prod\", \".terragrunt-stack\")\n\trequire.NoDirExists(t, prodDir)\n\n\trequire.NoError(t, os.RemoveAll(stackDir))\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\t\"terragrunt stack generate --working-dir \"+liveDir+\" --filter 'live | type=stack' --filter 'prod | type=stack'\",\n\t)\n\n\tstackDir = filepath.Join(liveDir, \".terragrunt-stack\")\n\trequire.DirExists(t, stackDir)\n\n\tdevDir = filepath.Join(stackDir, \"dev\", \".terragrunt-stack\")\n\trequire.NoDirExists(t, devDir)\n\n\tprodDir = filepath.Join(stackDir, \"prod\", \".terragrunt-stack\")\n\trequire.DirExists(t, prodDir)\n}\n\nfunc TestStackGenerationWithNestedTopologyWithRacing(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\tsetupNestedStackFixture(t, tmpDir)\n\n\tliveDir := filepath.Join(tmpDir, \"live\")\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+liveDir)\n\trequire.NoError(t, err)\n\n\tstackDir := filepath.Join(liveDir, \".terragrunt-stack\")\n\trequire.DirExists(t, stackDir)\n\n\tfoundFiles := findStackFiles(t, liveDir)\n\trequire.NotEmpty(t, foundFiles, \"Expected to find generated stack files\")\n\n\tl := logger.CreateLogger()\n\ttopology := generate.BuildStackTopology(l, foundFiles, liveDir)\n\trequire.NotEmpty(t, topology, \"Expected non-empty topology\")\n\n\tlevelCounts := make(map[int]int)\n\tfor _, node := range topology {\n\t\tlevelCounts[node.Level]++\n\t}\n\n\tt.Logf(\"Topology levels found: %v\", levelCounts)\n\n\tassert.Len(t, levelCounts, 3, \"Expected levels in nested topology\")\n\n\tassert.Equal(t, 1, levelCounts[0], \"Level 0 should have exactly 1 stack file\")\n\tassert.Equal(t, 3, levelCounts[1], \"Level 1 should have exactly 3 stack files\")\n\tassert.Equal(t, 9, levelCounts[2], \"Level 2 should have exactly 9 stack files\")\n\n\tverifyGeneratedUnits(t, stackDir)\n\n\t// Run one more time just to be sure things don't break when running in a dirty directory\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt stack generate --working-dir \"+liveDir)\n\trequire.NoError(t, err)\n}\n\n// setupNestedStackFixture creates a test fixture similar to testing-nested-stacks\nfunc setupNestedStackFixture(t *testing.T, tmpDir string) {\n\tt.Helper()\n\n\tliveDir := filepath.Join(tmpDir, \"live\")\n\tstacksDir := filepath.Join(tmpDir, \"stacks\")\n\tunitsDir := filepath.Join(tmpDir, \"units\")\n\n\trequire.NoError(t, os.MkdirAll(liveDir, 0755))\n\trequire.NoError(t, os.MkdirAll(filepath.Join(stacksDir, \"foo\"), 0755))\n\trequire.NoError(t, os.MkdirAll(filepath.Join(stacksDir, \"final\"), 0755))\n\trequire.NoError(t, os.MkdirAll(filepath.Join(unitsDir, \"final\"), 0755))\n\n\tliveStackConfig := `stack \"foo\" {\n  source = \"../stacks/foo\"\n  path   = \"foo\"\n}\n\nstack \"foo2\" {\n  source = \"../stacks/foo\"\n  path   = \"foo2\"\n}\n\nstack \"foo3\" {\n  source = \"../stacks/foo\"\n  path   = \"foo3\"\n}\n`\n\tliveStackPath := filepath.Join(liveDir, config.DefaultStackFile)\n\trequire.NoError(t, os.WriteFile(liveStackPath, []byte(liveStackConfig), 0644))\n\n\tfooStackConfig := `locals {\n  final_stack = find_in_parent_folders(\"stacks/final\")\n}\n\nstack \"final\" {\n  source = local.final_stack\n  path   = \"final\"\n}\n\nstack \"final2\" {\n  source = local.final_stack\n  path   = \"final2\"\n}\n\nstack \"final3\" {\n  source = local.final_stack\n  path   = \"final3\"\n}\n`\n\tfooStackPath := filepath.Join(stacksDir, \"foo\", config.DefaultStackFile)\n\trequire.NoError(t, os.WriteFile(fooStackPath, []byte(fooStackConfig), 0644))\n\n\tfinalStackConfig := `locals {\n  final_unit = find_in_parent_folders(\"units/final\")\n}\n\nunit \"final\" {\n  source = local.final_unit\n  path   = \"final\"\n}\n`\n\tfinalStackPath := filepath.Join(stacksDir, \"final\", config.DefaultStackFile)\n\trequire.NoError(t, os.WriteFile(finalStackPath, []byte(finalStackConfig), 0644))\n\n\tfinalUnitPath := filepath.Join(unitsDir, \"final\", config.DefaultTerragruntConfigPath)\n\trequire.NoError(t, os.WriteFile(finalUnitPath, []byte(``), 0644))\n\n\tfinalMainTfPath := filepath.Join(unitsDir, \"final\", \"main.tf\")\n\trequire.NoError(t, os.WriteFile(finalMainTfPath, []byte(``), 0644))\n}\n\n// verifyGeneratedUnits checks that some units were generated correctly\nfunc verifyGeneratedUnits(t *testing.T, stackDir string) {\n\tt.Helper()\n\n\tvar (\n\t\tunitDirs  []string\n\t\tstackDirs []string\n\t)\n\n\terr := filepath.WalkDir(stackDir, func(path string, info os.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !info.IsDir() && info.Name() == \"terragrunt.hcl\" {\n\t\t\tunitDir := filepath.Dir(path)\n\t\t\tunitDirs = append(unitDirs, unitDir)\n\t\t}\n\n\t\tif !info.IsDir() && info.Name() == \"terragrunt.stack.hcl\" {\n\t\t\tstackDir := filepath.Dir(path)\n\t\t\tstackDirs = append(stackDirs, stackDir)\n\t\t}\n\n\t\treturn nil\n\t})\n\trequire.NoError(t, err)\n\n\trequire.Len(t, unitDirs, 9, \"Expected exactly 9 generated units\")\n\trequire.Len(t, stackDirs, 12, \"Expected exactly 12 generated stacks\")\n}\n\n// findStackFiles recursively finds all terragrunt.stack.hcl files in a directory\nfunc findStackFiles(t *testing.T, dir string) []string {\n\tt.Helper()\n\n\tvar stackFiles []string\n\n\terr := filepath.WalkDir(dir, func(path string, info os.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif info.IsDir() {\n\t\t\treturn nil\n\t\t}\n\n\t\tif strings.HasSuffix(path, \"terragrunt.stack.hcl\") {\n\t\t\tstackFiles = append(stackFiles, path)\n\t\t}\n\n\t\treturn nil\n\t})\n\n\trequire.NoError(t, err)\n\n\treturn stackFiles\n}\n\n// TestStackVersionConstraints verifies that version constraints are respected in stack runs.\n// This test cannot be parallelized as it changes the global version.Version.\n//\n//nolint:paralleltest\nfunc TestStackVersionConstraints(t *testing.T) {\n\thelpers.CleanupTerragruntFolder(t, testFixtureStackVersionConstraints)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStackVersionConstraints)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureStackVersionConstraints, \"live\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\t// Run with a version that doesn't meet the constraint (>= 99.0.0)\n\terr := helpers.RunTerragruntVersionCommand(\n\t\tt,\n\t\t\"v0.99.0\",\n\t\t\"terragrunt stack run plan --non-interactive --working-dir \"+rootPath,\n\t\t&stdout,\n\t\t&stderr,\n\t)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"stderr\")\n\n\trequire.Error(t, err)\n\n\tvar invalidVersionError run.InvalidTerragruntVersion\n\tassert.ErrorAs(t, err, &invalidVersionError)\n}\n"
  },
  {
    "path": "test/integration_strict_test.go",
    "content": "package test_test\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureStrictBareInclude = \"fixtures/strict-bare-include\"\n)\n\n// TestRootTerragruntHCLStrictMode uses globally mutated state to determine if strict mode has already\n// been triggered, so we don't run it in parallel.\n//\n//nolint:paralleltest,tparallel\nfunc TestRootTerragruntHCLStrictMode(t *testing.T) {\n\thelpers.CleanupTerraformFolder(t, testFixtureFindParentWithDeprecatedRoot)\n\n\ttestCases := []struct {\n\t\texpectedError  error\n\t\tname           string\n\t\texpectedStderr string\n\t\tcontrols       []string\n\t\tstrictMode     bool\n\t}{\n\t\t{\n\t\t\tname:           \"root terragrunt.hcl\",\n\t\t\tstrictMode:     false,\n\t\t\texpectedStderr: \"Using `terragrunt.hcl` as the root of Terragrunt configurations is an anti-pattern\",\n\t\t},\n\t\t{\n\t\t\tname:          \"root terragrunt.hcl with root-terragrunt-hcl strict control\",\n\t\t\tcontrols:      []string{\"root-terragrunt-hcl\"},\n\t\t\tstrictMode:    false,\n\t\t\texpectedError: errors.New(\"Using `terragrunt.hcl` as the root of Terragrunt configurations is an anti-pattern\"),\n\t\t},\n\t\t// we cannot test `-strict-mode` flag, since we cannot know at which strict control TG will output the error.\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureFindParentWithDeprecatedRoot)\n\t\t\trootPath := filepath.Join(tmpEnvPath, testFixtureFindParentWithDeprecatedRoot, \"app\")\n\n\t\t\targs := \"--non-interactive --log-level debug --working-dir \" + rootPath\n\t\t\tif tc.strictMode {\n\t\t\t\targs = \"--strict-mode \" + args\n\t\t\t}\n\n\t\t\tfor _, control := range tc.controls {\n\t\t\t\targs = \" --strict-control \" + control + \" \" + args\n\t\t\t}\n\n\t\t\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run \"+args+\" -- plan\")\n\n\t\t\tif tc.expectedError != nil {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), tc.expectedError.Error())\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tassert.Contains(t, stderr, tc.expectedStderr)\n\t\t})\n\t}\n}\n\n// TestBareIncludeStrictMode uses globally mutated state to determine if strict mode has already\n// been triggered, so we don't run it in parallel.\n//\n//nolint:paralleltest,tparallel\nfunc TestBareIncludeStrictMode(t *testing.T) {\n\thelpers.CleanupTerraformFolder(t, testFixtureStrictBareInclude)\n\n\ttestCases := []struct {\n\t\texpectedError error\n\t\tname          string\n\t\tcontrols      []string\n\t\tstrictMode    bool\n\t}{\n\t\t{\n\t\t\tname:          \"bare include with no strict mode or control\",\n\t\t\tcontrols:      []string{},\n\t\t\tstrictMode:    false,\n\t\t\texpectedError: nil,\n\t\t},\n\t\t{\n\t\t\tname:          \"bare include with bare-include strict control\",\n\t\t\tcontrols:      []string{\"bare-include\"},\n\t\t\tstrictMode:    false,\n\t\t\texpectedError: errors.New(\"Using an `include` block without a label is deprecated. Please use the `include` block with a label instead.\"),\n\t\t},\n\t\t{\n\t\t\tname:          \"bare include with strict mode\",\n\t\t\tcontrols:      []string{},\n\t\t\tstrictMode:    true,\n\t\t\texpectedError: errors.New(\"Using an `include` block without a label is deprecated. Please use the `include` block with a label instead.\"),\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStrictBareInclude)\n\t\t\trootPath := filepath.Join(tmpEnvPath, testFixtureStrictBareInclude)\n\n\t\t\targs := \"init --non-interactive --working-dir \" + rootPath\n\t\t\tif tc.strictMode {\n\t\t\t\targs = \"--strict-mode \" + args\n\t\t\t}\n\n\t\t\tfor _, control := range tc.controls {\n\t\t\t\targs = \" --strict-control \" + control + \" \" + args\n\t\t\t}\n\n\t\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt \"+args)\n\n\t\t\tif tc.expectedError != nil {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\trequire.ErrorContains(t, err, tc.expectedError.Error())\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "test/integration_test.go",
    "content": "// Package test_test contains integration tests for Terragrunt.\npackage test_test\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/commands/info/print\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags\"\n\t\"github.com/gruntwork-io/terragrunt/internal/cli/flags/shared\"\n\t\"github.com/gruntwork-io/terragrunt/internal/codegen\"\n\t\"github.com/gruntwork-io/terragrunt/internal/errors\"\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/run\"\n\t\"github.com/gruntwork-io/terragrunt/internal/runner/runall\"\n\t\"github.com/gruntwork-io/terragrunt/internal/shell\"\n\t\"github.com/gruntwork-io/terragrunt/internal/tf\"\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/internal/view/diagnostic\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/config\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log\"\n\t\"github.com/gruntwork-io/terragrunt/pkg/log/format/placeholders\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/hashicorp/hcl/v2\"\n\t\"github.com/hashicorp/hcl/v2/hclwrite\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// hard-code this to match the test fixture for now\nconst (\n\ttestFixtureAuthProviderCmd                = \"fixtures/auth-provider-cmd\"\n\ttestFixtureAutoInit                       = \"fixtures/download/init-on-source-change\"\n\ttestFixtureBrokenDependency               = \"fixtures/broken-dependency\"\n\ttestFixtureBufferModuleOutput             = \"fixtures/buffer-module-output\"\n\ttestFixtureCodegenPath                    = \"fixtures/codegen\"\n\ttestFixtureCommandsThatNeedInput          = \"fixtures/commands-that-need-input\"\n\ttestFixtureConfigSingleJSONPath           = \"fixtures/config-files/single-json-config\"\n\ttestFixtureConfigWithNonDefaultNames      = \"fixtures/config-files/with-non-default-names\"\n\ttestFixtureDependenciesOptimisation       = \"fixtures/dependency-optimisation\"\n\ttestFixtureDependencyOutput               = \"fixtures/dependency-output\"\n\ttestFixtureDetailedExitCode               = \"fixtures/detailed-exitcode\"\n\ttestFixtureDirsPath                       = \"fixtures/dirs\"\n\ttestFixtureDisabledModule                 = \"fixtures/disabled/\"\n\ttestFixtureDisabledPath                   = \"fixtures/disabled-path/\"\n\ttestFixtureDisjoint                       = \"fixtures/stack/disjoint\"\n\ttestFixtureDownload                       = \"fixtures/download\"\n\ttestFixtureEmptyState                     = \"fixtures/empty-state/\"\n\ttestFixtureEnvVarsBlockPath               = \"fixtures/env-vars-block/\"\n\ttestFixtureErrorPrint                     = \"fixtures/error-print\"\n\ttestFixtureExcludesFile                   = \"fixtures/excludes-file\"\n\ttestFixtureExternalDependence             = \"fixtures/external-dependencies\"\n\ttestFixtureExternalDependency             = \"fixtures/external-dependency/\"\n\ttestFixtureExtraArgsPath                  = \"fixtures/extra-args/\"\n\ttestFixtureFailedTerraform                = \"fixtures/failure\"\n\ttestFixtureFindParent                     = \"fixtures/find-parent\"\n\ttestFixtureFindParentWithDeprecatedRoot   = \"fixtures/find-parent-with-deprecated-root\"\n\ttestFixtureGetOutput                      = \"fixtures/get-output\"\n\ttestFixtureGetTerragruntSourceCli         = \"fixtures/get-terragrunt-source-cli\"\n\ttestFixtureRunAllSource                   = \"fixtures/get-output/run-all-source\"\n\ttestFixtureGraphDependencies              = \"fixtures/graph-dependencies\"\n\ttestFixtureHclfmtDiff                     = \"fixtures/hclfmt-diff\"\n\ttestFixtureHclfmtStdin                    = \"fixtures/hclfmt-stdin\"\n\ttestFixtureHclvalidate                    = \"fixtures/hclvalidate\"\n\ttestFixtureIamRolesMultipleModules        = \"fixtures/read-config/iam_roles_multiple_modules\"\n\ttestFixtureIncludeParent                  = \"fixtures/include-parent\"\n\ttestFixtureInfoError                      = \"fixtures/terragrunt-info-error\"\n\ttestFixtureInitCache                      = \"fixtures/init-cache\"\n\ttestFixtureInitError                      = \"fixtures/init-error\"\n\ttestFixtureInitOnce                       = \"fixtures/init-once\"\n\ttestFixtureInputs                         = \"fixtures/inputs\"\n\ttestFixtureInputsInterpolation            = \"fixtures/inputs-interpolation\"\n\ttestFixtureLogFormatter                   = \"fixtures/log/formatter\"\n\ttestFixtureLogStdoutLevel                 = \"fixtures/log/levels\"\n\ttestFixtureLogRelPaths                    = \"fixtures/log/rel-paths\"\n\ttestFixtureMissingDependence              = \"fixtures/missing-dependencies/main\"\n\ttestFixtureModulePathError                = \"fixtures/module-path-in-error\"\n\ttestFixtureNoColor                        = \"fixtures/no-color\"\n\ttestFixtureNoSubmodules                   = \"fixtures/no-submodules/\"\n\ttestFixtureNullValue                      = \"fixtures/null-values\"\n\ttestFixtureOutDir                         = \"fixtures/out-dir\"\n\ttestFixtureOutputAll                      = \"fixtures/output-all\"\n\ttestFixtureParallelRun                    = \"fixtures/parallel-run\"\n\ttestFixtureParallelStateInit              = \"fixtures/parallel-state-init\"\n\ttestFixtureParallelism                    = \"fixtures/parallelism\"\n\ttestFixturePath                           = \"fixtures/terragrunt/\"\n\ttestFixturePlanfileOrder                  = \"fixtures/planfile-order-test\"\n\ttestFixtureProviderCacheDependency        = \"fixtures/provider-cache/dependency\"\n\ttestFixtureProviderCacheDirect            = \"fixtures/provider-cache/direct\"\n\ttestFixtureProviderCacheFilesystemMirror  = \"fixtures/provider-cache/filesystem-mirror\"\n\ttestFixtureProviderCacheMultiplePlatforms = \"fixtures/provider-cache/multiple-platforms\"\n\ttestFixtureProviderCacheNetworkMirror     = \"fixtures/provider-cache/network-mirror\"\n\ttestFixtureReadConfig                     = \"fixtures/read-config\"\n\ttestFixtureRefSource                      = \"fixtures/download/remote-ref\"\n\ttestFixtureSkip                           = \"fixtures/skip/\"\n\ttestFixtureSkipLegacyRoot                 = \"fixtures/skip-legacy-root/\"\n\ttestFixtureSkipDependencies               = \"fixtures/skip-dependencies\"\n\ttestFixtureSourceMapSlashes               = \"fixtures/source-map/slashes-in-ref\"\n\ttestFixtureStack                          = \"fixtures/stack/\"\n\ttestFixtureStdout                         = \"fixtures/download/stdout-test\"\n\ttestFixtureTfTest                         = \"fixtures/tftest/\"\n\ttestFixtureExecCmd                        = \"fixtures/exec-cmd\"\n\ttestFixtureExecCmdTfPath                  = \"fixtures/exec-cmd-tf-path\"\n\ttextFixtureDisjointSymlinks               = \"fixtures/stack/disjoint-symlinks\"\n\ttestFixtureLogStreaming                   = \"fixtures/streaming\"\n\ttestFixtureCLIFlagHints                   = \"fixtures/cli-flag-hints\"\n\ttestFixtureEphemeralInputs                = \"fixtures/ephemeral-inputs\"\n\ttestFixtureTfPathBasic                    = \"fixtures/tf-path/basic\"\n\ttestFixtureTfPathTofuTerraform            = \"fixtures/tf-path/tofu-terraform\"\n\ttestFixtureTraceParent                    = \"fixtures/trace-parent\"\n\ttestFixtureVersionInvocation              = \"fixtures/version-invocation\"\n\ttestFixtureVersionFilesCacheKey           = \"fixtures/version-files-cache-key\"\n\ttestFixtureNoColorDependency              = \"fixtures/no-color-dependency\"\n\thiddenRunAllFixturePath                   = \"fixtures/hidden-runall\"\n\n\tterraformFolder = \".terraform\"\n\n\tterraformState = \"terraform.tfstate\"\n\n\tterraformStateBackup = \"terraform.tfstate.backup\"\n)\n\nfunc TestCLIFlagHints(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedError error\n\t\targs          string\n\t}{\n\t\t{\n\t\t\texpectedError: flags.NewGlobalFlagHintError(\"raw\", \"stack output\", \"raw\"),\n\t\t\targs:          \"-raw init\",\n\t\t},\n\t\t{\n\t\t\texpectedError: flags.NewCommandFlagHintError(\"run\", \"no-include-root\", \"catalog\", \"no-include-root\"),\n\t\t\targs:          \"run --no-include-root\",\n\t\t},\n\t\t{\n\t\t\texpectedError: flags.NewPassthroughFlagHintError(\"platform\"),\n\t\t\targs:          \"run --platform\",\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureCLIFlagHints)\n\t\t\trootPath := helpers.CopyEnvironment(t, testFixtureCLIFlagHints)\n\t\t\trootPath, err := filepath.EvalSymlinks(rootPath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt \"+tc.args+\" --working-dir \"+rootPath)\n\t\t\tassert.EqualError(t, err, tc.expectedError.Error())\n\t\t})\n\t}\n}\n\nfunc TestDetailedExitCodeError(t *testing.T) {\n\tt.Parallel()\n\n\ttestFixturePath := filepath.Join(testFixtureDetailedExitCode, \"error\")\n\n\thelpers.CleanupTerraformFolder(t, testFixturePath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, testFixturePath)\n\n\texitCode := tf.NewDetailedExitCodeMap()\n\n\tctx := t.Context()\n\tctx = tf.ContextWithDetailedExitCode(ctx, exitCode)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutputWithContext(\n\t\tt,\n\t\tctx,\n\t\t\"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- plan -detailed-exitcode\",\n\t)\n\trequire.Error(t, err)\n\tassert.Contains(t, stderr, \"not-existing-file.txt: no such file or directory\")\n\tassert.Equal(t, 1, exitCode.GetFinalDetailedExitCode())\n}\n\n// TestRunAllReturnsErrorOnFailure verifies that `terragrunt run --all` returns\n// a non-zero exit code when one of the units fails. This is a regression test\n// for https://github.com/gruntwork-io/terragrunt/issues/5379\nfunc TestRunAllReturnsErrorOnFailure(t *testing.T) {\n\tt.Parallel()\n\n\ttestFixturePath := filepath.Join(testFixtureDetailedExitCode, \"error\")\n\n\thelpers.CleanupTerraformFolder(t, testFixturePath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, testFixturePath)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all --log-level trace --non-interactive --working-dir \"+rootPath+\" -- plan\",\n\t)\n\trequire.Error(t, err)\n\tassert.Contains(t, stderr, \"not-existing-file.txt: no such file or directory\")\n}\n\nfunc TestDetailedExitCodeChangesPresentAll(t *testing.T) {\n\tt.Parallel()\n\n\ttestFixturePath := filepath.Join(testFixtureDetailedExitCode, \"changes\")\n\n\thelpers.CleanupTerraformFolder(t, testFixturePath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, testFixturePath)\n\n\texitCode := tf.NewDetailedExitCodeMap()\n\n\tctx := t.Context()\n\tctx = tf.ContextWithDetailedExitCode(ctx, exitCode)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutputWithContext(t, ctx, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- plan -detailed-exitcode\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, 2, exitCode.GetFinalDetailedExitCode())\n}\n\nfunc TestDetailedExitCodeChangesUnit(t *testing.T) {\n\tt.Parallel()\n\n\ttestFixturePath := filepath.Join(testFixtureDetailedExitCode, \"changes\")\n\n\thelpers.CleanupTerraformFolder(t, testFixturePath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, testFixturePath)\n\tctx := t.Context()\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutputWithContext(t, ctx, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- apply\")\n\trequire.NoError(t, err)\n\n\t// delete example.txt from cache directory to have changes in one unit\n\t// The file is created in the cache directory since source is always copied there\n\tapp1CacheDir := helpers.FindCacheWorkingDir(t, filepath.Join(rootPath, \"app1\"))\n\trequire.NotEmpty(t, app1CacheDir, \"Should find cache working directory for app1\")\n\terr = os.Remove(filepath.Join(app1CacheDir, \"example.txt\"))\n\trequire.NoError(t, err)\n\n\t// check that the exit code is 2 when there are changes in one unit\n\texitCode := tf.NewDetailedExitCodeMap()\n\n\tctx = tf.ContextWithDetailedExitCode(ctx, exitCode)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutputWithContext(t, ctx, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- plan -detailed-exitcode\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, 2, exitCode.GetFinalDetailedExitCode())\n}\n\nfunc TestDetailedExitCodeFailOnFirstRun(t *testing.T) {\n\tt.Parallel()\n\n\ttestFixturePath := filepath.Join(testFixtureDetailedExitCode, \"fail-on-first-run\")\n\n\thelpers.CleanupTerraformFolder(t, testFixturePath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixturePath)\n\n\texitCode := tf.NewDetailedExitCodeMap()\n\n\tctx := t.Context()\n\tctx = tf.ContextWithDetailedExitCode(ctx, exitCode)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutputWithContext(\n\t\tt,\n\t\tctx,\n\t\t\"terragrunt run --all --non-interactive --working-dir \"+filepath.Join(\n\t\t\ttmpEnvPath,\n\t\t\ttestFixturePath,\n\t\t)+\" -- plan -detailed-exitcode\",\n\t)\n\trequire.NoError(t, err)\n\tassert.Equal(t, 0, exitCode.GetFinalDetailedExitCode())\n}\n\nfunc TestDetailedExitCodeFailOnFirstRunWithStatus(t *testing.T) {\n\tt.Parallel()\n\n\ttestFixturePath := filepath.Join(testFixtureDetailedExitCode, \"fail-on-first-run-with-status\")\n\n\thelpers.CleanupTerraformFolder(t, testFixturePath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixturePath)\n\n\texitCode := tf.NewDetailedExitCodeMap()\n\n\tctx := t.Context()\n\tctx = tf.ContextWithDetailedExitCode(ctx, exitCode)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutputWithContext(\n\t\tt,\n\t\tctx,\n\t\t\"terragrunt run --working-dir \"+filepath.Join(\n\t\t\ttmpEnvPath,\n\t\t\ttestFixturePath,\n\t\t)+\" -- plan -detailed-exitcode\",\n\t)\n\trequire.NoError(t, err)\n\tassert.Equal(t, 2, exitCode.GetFinalDetailedExitCode())\n}\n\nfunc TestDetailedExitCodeFailOnFirstRunAllWithStatus(t *testing.T) {\n\tt.Parallel()\n\n\ttestFixturePath := filepath.Join(testFixtureDetailedExitCode, \"fail-on-first-run-with-status\")\n\n\thelpers.CleanupTerraformFolder(t, testFixturePath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixturePath)\n\n\texitCode := tf.NewDetailedExitCodeMap()\n\n\tctx := t.Context()\n\tctx = tf.ContextWithDetailedExitCode(ctx, exitCode)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutputWithContext(\n\t\tt,\n\t\tctx,\n\t\t\"terragrunt run --working-dir \"+filepath.Join(\n\t\t\ttmpEnvPath,\n\t\t\ttestFixturePath,\n\t\t)+\" --all -- plan -detailed-exitcode\",\n\t)\n\trequire.NoError(t, err)\n\tassert.Equal(t, 2, exitCode.GetFinalDetailedExitCode())\n}\n\nfunc TestDetailedExitCodeChangesPresentOne(t *testing.T) {\n\tt.Parallel()\n\n\ttestFixturePath := filepath.Join(testFixtureDetailedExitCode, \"changes\")\n\n\thelpers.CleanupTerraformFolder(t, testFixturePath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, testFixturePath)\n\n\texitCode := tf.NewDetailedExitCodeMap()\n\n\tctx := t.Context()\n\tctx = tf.ContextWithDetailedExitCode(ctx, exitCode)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all apply --non-interactive --working-dir \"+filepath.Join(rootPath, \"app1\"))\n\trequire.NoError(t, err)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutputWithContext(t, ctx, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- plan -detailed-exitcode\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, 2, exitCode.GetFinalDetailedExitCode())\n}\n\nfunc TestDetailedExitCodeNoChanges(t *testing.T) {\n\tt.Parallel()\n\n\ttestFixturePath := filepath.Join(testFixtureDetailedExitCode, \"changes\")\n\n\thelpers.CleanupTerraformFolder(t, testFixturePath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, testFixturePath)\n\n\texitCode := tf.NewDetailedExitCodeMap()\n\n\tctx := t.Context()\n\tctx = tf.ContextWithDetailedExitCode(ctx, exitCode)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutputWithContext(t, ctx, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- plan -detailed-exitcode\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, 0, exitCode.GetFinalDetailedExitCode())\n}\n\nfunc TestRunAllDetailedExitCode_RetryableAfterDrift(t *testing.T) {\n\tt.Parallel()\n\n\ttestFixturePath := filepath.Join(testFixtureDetailedExitCode, \"runall-retry-after-drift\")\n\n\thelpers.CleanupTerraformFolder(t, testFixturePath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, testFixturePath)\n\n\t// Pre-apply the drift unit so it has a file, then delete it to ensure drift exists\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all apply --non-interactive --working-dir \"+\n\t\t\tfilepath.Join(rootPath, \"app_drift\"),\n\t)\n\trequire.NoError(t, err)\n\t// Delete file from cache directory since that's where it was created\n\tappDriftCacheDir := helpers.FindCacheWorkingDir(t, filepath.Join(rootPath, \"app_drift\"))\n\trequire.NotEmpty(t, appDriftCacheDir, \"Should find cache working directory for app_drift\")\n\terr = os.Remove(filepath.Join(appDriftCacheDir, \"example.txt\"))\n\trequire.NoError(t, err)\n\n\texitCode := tf.NewDetailedExitCodeMap()\n\n\tctx := t.Context()\n\tctx = tf.ContextWithDetailedExitCode(ctx, exitCode)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutputWithContext(\n\t\tt, ctx,\n\t\t\"terragrunt run --all --non-interactive --working-dir \"+\n\t\t\trootPath+\n\t\t\t\" -- plan -detailed-exitcode\",\n\t)\n\trequire.NoError(t, err)\n\tassert.Equal(t, 2, exitCode.GetFinalDetailedExitCode())\n}\n\n// TestDetailedExitCodeChangesPresentAllWithSource verifies that run --all correctly\n// propagates the detailed exit code when units use terraform { source = \".\" }.\n// This is a regression test for https://github.com/gruntwork-io/terragrunt/issues/5586\nfunc TestDetailedExitCodeChangesPresentAllWithSource(t *testing.T) {\n\tt.Parallel()\n\n\ttestFixturePath := filepath.Join(testFixtureDetailedExitCode, \"changes-with-source\")\n\n\thelpers.CleanupTerraformFolder(t, testFixturePath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, testFixturePath)\n\n\texitCode := tf.NewDetailedExitCodeMap()\n\n\tctx := t.Context()\n\tctx = tf.ContextWithDetailedExitCode(ctx, exitCode)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutputWithContext(\n\t\tt,\n\t\tctx,\n\t\t\"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- plan -detailed-exitcode\",\n\t)\n\trequire.NoError(t, err)\n\tassert.Equal(t, 2, exitCode.GetFinalDetailedExitCode())\n}\n\nfunc TestLogCustomFormatOutput(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedErr        error\n\t\tlogCustomFormat    string\n\t\texpectedStdOutRegs []*regexp.Regexp\n\t\texpectedStdErrRegs []*regexp.Regexp\n\t}{\n\t\t{\n\t\t\tlogCustomFormat: \"%interval%(content=' plain-text ')%level(case=upper,width=6) %prefix(path=short-relative,suffix=' ')%tf-path(suffix=' ')%tf-command-args(suffix=': ')%msg(path=relative)\",\n\t\t\texpectedStdOutRegs: []*regexp.Regexp{\n\t\t\t\tregexp.MustCompile(`\\d{4}` + regexp.QuoteMeta(\" plain-text STDOUT dep \"+wrappedBinary()+\" init -no-color -input=false: Initializing the backend...\")),\n\t\t\t\tregexp.MustCompile(`\\d{4}` + regexp.QuoteMeta(\" plain-text STDOUT app \"+wrappedBinary()+\" init -no-color -input=false: Initializing the backend...\")),\n\t\t\t},\n\t\t\texpectedStdErrRegs: []*regexp.Regexp{\n\t\t\t\tregexp.MustCompile(`\\d{4}` + regexp.QuoteMeta(\" plain-text DEBUG  Terragrunt Version:\")),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlogCustomFormat: \"%interval%(content=' plain-text ')%level(case=upper,width=6) %prefix(path=short-relative,suffix=' ')%tf-path(suffix=' ')%tf-command(suffix=': ')%msg(path=relative)\",\n\t\t\texpectedStdOutRegs: []*regexp.Regexp{\n\t\t\t\tregexp.MustCompile(`\\d{4}` + regexp.QuoteMeta(\" plain-text STDOUT dep \"+wrappedBinary()+\" init: Initializing the backend...\")),\n\t\t\t\tregexp.MustCompile(`\\d{4}` + regexp.QuoteMeta(\" plain-text STDOUT app \"+wrappedBinary()+\" init: Initializing the backend...\")),\n\t\t\t},\n\t\t\texpectedStdErrRegs: []*regexp.Regexp{\n\t\t\t\tregexp.MustCompile(`\\d{4}` + regexp.QuoteMeta(\" plain-text DEBUG  Terragrunt Version:\")),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlogCustomFormat: \"%interval%(content=' plain-text ')%level(case=upper,width=6) %prefix(path=short-relative,suffix=' ')%tf-path(suffix=' ')%tf-command()-args %msg(path=relative)\",\n\t\t\texpectedStdOutRegs: []*regexp.Regexp{\n\t\t\t\tregexp.MustCompile(`\\d{4}` + regexp.QuoteMeta(\" plain-text STDOUT dep \"+wrappedBinary()+\" init-args Initializing the backend...\")),\n\t\t\t\tregexp.MustCompile(`\\d{4}` + regexp.QuoteMeta(\" plain-text STDOUT app \"+wrappedBinary()+\" init-args Initializing the backend...\")),\n\t\t\t},\n\t\t\texpectedStdErrRegs: []*regexp.Regexp{\n\t\t\t\tregexp.MustCompile(`\\d{4}` + regexp.QuoteMeta(\" plain-text DEBUG  -args Terragrunt Version:\")),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlogCustomFormat: \"%interval%(content=' plain-text ')%level(case=upper,width=6) %prefix(path=short-relative,suffix=' ')%tf-path(suffix=' ')%tf-command()-args % aaa %msg(path=relative) %%bbb % ccc\",\n\t\t\texpectedStdOutRegs: []*regexp.Regexp{\n\t\t\t\tregexp.MustCompile(`\\d{4}` + regexp.QuoteMeta(\" plain-text STDOUT dep \"+wrappedBinary()+\" init-args % aaa Initializing the backend... %bbb % ccc\")),\n\t\t\t\tregexp.MustCompile(`\\d{4}` + regexp.QuoteMeta(\" plain-text STDOUT app \"+wrappedBinary()+\" init-args % aaa Initializing the backend... %bbb % ccc\")),\n\t\t\t},\n\t\t\texpectedStdErrRegs: []*regexp.Regexp{\n\t\t\t\tregexp.MustCompile(`\\d{4}` + regexp.QuoteMeta(\" plain-text DEBUG  -args % aaa Terragrunt Version:\")),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlogCustomFormat: \"%time(color=green) %level %wrong\",\n\t\t\texpectedErr:     errors.Errorf(`invalid value \"%%time(color=green) %%level %%wrong\" for flag -log-custom-format: invalid placeholder name \"wrong\", available names: %s`, strings.Join(placeholders.NewPlaceholderRegister().Names(), \",\")),\n\t\t},\n\t\t{\n\t\t\tlogCustomFormat: \"%time(colorr=green) %level\",\n\t\t\texpectedErr:     errors.Errorf(`invalid value \"%%time(colorr=green) %%level\" for flag -log-custom-format: placeholder \"time\", invalid option name \"colorr\", available names: %s`, strings.Join(placeholders.Time().Options().Names(), \",\")),\n\t\t},\n\t\t{\n\t\t\tlogCustomFormat: \"%time(color=green) %level(format=tinyy)\",\n\t\t\texpectedErr:     errors.New(`invalid value \"%time(color=green) %level(format=tinyy)\" for flag -log-custom-format: placeholder \"level\", option \"format\", invalid value \"tinyy\", available values: full,short,tiny`),\n\t\t},\n\t\t{\n\t\t\tlogCustomFormat: \"%time(=green) %level(format=tiny)\",\n\t\t\texpectedErr:     errors.New(`invalid value \"%time(=green) %level(format=tiny)\" for flag -log-custom-format: placeholder \"time\", empty option name \"=green) %level(format=tiny)\"`),\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureLogFormatter)\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureLogFormatter)\n\t\t\trootPath := filepath.Join(tmpEnvPath, testFixtureLogFormatter)\n\n\t\t\trootPath, err := filepath.EvalSymlinks(rootPath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\t\t\tt,\n\t\t\t\tfmt.Sprintf(\n\t\t\t\t\t\"terragrunt run --all --log-level debug --non-interactive --no-color --log-custom-format=%q --working-dir %s -- init -no-color\",\n\t\t\t\t\ttc.logCustomFormat,\n\t\t\t\t\trootPath,\n\t\t\t\t),\n\t\t\t)\n\n\t\t\tif tc.expectedErr != nil {\n\t\t\t\tassert.EqualError(t, err, tc.expectedErr.Error())\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\tfor _, reg := range tc.expectedStdOutRegs {\n\t\t\t\tassert.Regexp(t, reg, stdout)\n\t\t\t}\n\n\t\t\tfor _, reg := range tc.expectedStdErrRegs {\n\t\t\t\tassert.Regexp(t, reg, stderr)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestBufferModuleOutput(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureBufferModuleOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureBufferModuleOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureBufferModuleOutput)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --log-disable --working-dir \"+rootPath+\" -- plan -out planfile\")\n\trequire.NoError(t, err)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --log-disable --working-dir \"+rootPath+\" -- show -json planfile\")\n\trequire.NoError(t, err)\n\n\tfor stdout := range strings.SplitSeq(stdout, \"\\n\") {\n\t\tif stdout == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar objmap map[string]json.RawMessage\n\n\t\terr = json.Unmarshal([]byte(stdout), &objmap)\n\t\trequire.NoError(t, err)\n\t}\n}\n\nfunc TestDisableLogging(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureLogFormatter)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureLogFormatter)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLogFormatter)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all init --log-disable --non-interactive -no-color --no-color --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"Initializing provider plugins...\")\n\tassert.Empty(t, stderr)\n}\n\nfunc TestLogWithAbsPath(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureLogFormatter)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureLogFormatter)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLogFormatter)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all init --log-show-abs-paths --log-level debug --non-interactive -no-color --no-color --log-format=pretty --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\tfor _, prefixName := range []string{\"app\", \"dep\"} {\n\t\tprefixName = filepath.Join(rootPath, prefixName)\n\t\tassert.Contains(t, stdout, \"STDOUT [\"+prefixName+\"] \"+wrappedBinary()+\": Initializing provider plugins...\")\n\t\tassert.Contains(t, stderr, \"DEBUG  [\"+prefixName+\"] Reading Terragrunt config file at \"+prefixName+\"/terragrunt.hcl\")\n\t}\n}\n\nfunc TestLogWithRelPath(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureLogRelPaths)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureLogRelPaths)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLogRelPaths)\n\n\ttestCases := []struct {\n\t\tassertFn   func(t *testing.T, stdout, stderr string)\n\t\tworkingDir string\n\t}{\n\t\t{\n\t\t\tworkingDir: \"duplicate-dir-names/workspace/one/two/aaa\", // dir `workspace` duplicated twice in path\n\t\t\tassertFn: func(t *testing.T, _, stderr string) {\n\t\t\t\tt.Helper()\n\n\t\t\t\tassert.Contains(t, stderr, \"Unit bbb/ccc/workspace\")\n\t\t\t\tassert.Contains(t, stderr, \"Unit bbb/ccc/module-b\")\n\t\t\t\tassert.Contains(t, stderr, \"Downloading Terraform configurations from .. into ./bbb/ccc/workspace/.terragrunt-cache\")\n\t\t\t\tassert.Contains(t, stderr, \"[bbb/ccc/workspace]\")\n\t\t\t\tassert.Contains(t, stderr, \"[bbb/ccc/module-b]\")\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tworkingDir := filepath.Join(rootPath, tc.workingDir)\n\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\t\t\tt,\n\t\t\t\t\"terragrunt run --all init --non-interactive --no-color --log-format=pretty --working-dir \"+workingDir,\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttc.assertFn(t, stdout, stderr)\n\t\t})\n\t}\n}\n\nfunc TestLogFormatPrettyOutput(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureLogFormatter)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureLogFormatter)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLogFormatter)\n\n\trootPath, err := filepath.EvalSymlinks(rootPath)\n\trequire.NoError(t, err)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all init --log-level debug --non-interactive --no-color --log-format=pretty  --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\tfor _, prefixName := range []string{\"app\", \"dep\"} {\n\t\tassert.Contains(t, stdout, \"STDOUT [\"+prefixName+\"] \"+wrappedBinary()+\": Initializing provider plugins...\")\n\t\tassert.Contains(t, stderr, \"DEBUG  [\"+prefixName+\"] Reading Terragrunt config file at ./\"+prefixName+\"/terragrunt.hcl\")\n\t}\n\n\tassert.NotEmpty(t, stdout)\n\tassert.Contains(t, stderr, \"DEBUG  Terragrunt Version:\")\n}\n\nfunc TestLogStdoutLevel(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureLogStdoutLevel)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureLogStdoutLevel)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLogStdoutLevel)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive -no-color --no-color --log-format=pretty  --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"STDOUT \"+wrappedBinary()+\": Changes to Outputs\")\n\n\tstdout, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt destroy -auto-approve --non-interactive -no-color --no-color --log-format=pretty  --working-dir \"+rootPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"STDOUT \"+wrappedBinary()+\": Changes to Outputs\")\n}\n\nfunc TestLogFormatKeyValueOutput(t *testing.T) {\n\tt.Parallel()\n\n\tfor _, flag := range []string{\"--log-format=key-value\"} {\n\t\tt.Run(\"tc-flag-\"+flag, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\thelpers.CleanupTerraformFolder(t, testFixtureLogFormatter)\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureLogFormatter)\n\t\t\trootPath := filepath.Join(tmpEnvPath, testFixtureLogFormatter)\n\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\t\t\tt,\n\t\t\t\t\"terragrunt run --all --log-level debug --non-interactive \"+flag+\" --working-dir \"+rootPath+\" -- init -no-color\",\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tfor _, prefixName := range []string{\"app\", \"dep\"} {\n\t\t\t\tassert.Contains(\n\t\t\t\t\tt,\n\t\t\t\t\tstdout,\n\t\t\t\t\t\"level=stdout prefix=\"+prefixName+\" tf-path=\"+wrappedBinary()+\" msg=Initializing provider plugins...\\n\",\n\t\t\t\t)\n\t\t\t\tassert.Contains(\n\t\t\t\t\tt,\n\t\t\t\t\tstderr,\n\t\t\t\t\t\"level=debug prefix=\"+prefixName+\" msg=Reading Terragrunt config file at ./\"+prefixName+\"/terragrunt.hcl\\n\",\n\t\t\t\t)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLogRawModuleOutput(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureLogFormatter)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureLogFormatter)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureLogFormatter)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive  --tf-forward-stdout --working-dir \"+rootPath+\" -- init -no-color\")\n\trequire.NoError(t, err)\n\n\tstdoutInline := strings.ReplaceAll(stdout, \"\\n\", \"\")\n\tassert.Contains(t, stdoutInline, \"Initializing the backend...Initializing provider plugins...\")\n\tassert.NotRegexp(t, `(?i)(`+strings.Join(log.AllLevels.Names(), \"|\")+`)+`, stdoutInline)\n}\n\nfunc TestTerragruntExcludesFile(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tflags          string\n\t\texpectedOutput []string\n\t}{\n\t\t{\n\t\t\t\"\",\n\t\t\t[]string{`value = \"b\"`, `value = \"d\"`},\n\t\t},\n\t\t{\n\t\t\t\"--queue-excludes-file ./excludes-file-pass-as-flag\",\n\t\t\t[]string{`value = \"a\"`, `value = \"c\"`},\n\t\t},\n\t}\n\n\tfor i, tc := range testCases {\n\t\tt.Run(fmt.Sprintf(\"testCase-%d\", i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureExcludesFile, \".terragrunt-excludes\")\n\t\t\trootPath := filepath.Join(tmpEnvPath, testFixtureExcludesFile)\n\n\t\t\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt run apply --all --non-interactive --working-dir %s %s -- -auto-approve\", rootPath, tc.flags))\n\n\t\t\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt run output --all --non-interactive --working-dir %s %s\", rootPath, tc.flags))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tactualOutput := strings.Split(strings.TrimSpace(stdout), \"\\n\")\n\t\t\tassert.ElementsMatch(t, tc.expectedOutput, actualOutput)\n\t\t})\n\t}\n}\n\nfunc TestHclvalidateValidConfig(t *testing.T) {\n\tt.Parallel()\n\n\tt.Run(\"using --all\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\thelpers.CleanupTerraformFolder(t, testFixtureHclvalidate)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHclvalidate)\n\t\trootPath := filepath.Join(tmpEnvPath, testFixtureHclvalidate)\n\n\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\t\tt,\n\t\t\t\"terragrunt hcl validate --all --strict --inputs --working-dir \"+filepath.Join(rootPath, \"valid\"),\n\t\t)\n\t\trequire.NoError(t, err)\n\t})\n\n\tt.Run(\"validate each individually\", func(t *testing.T) {\n\t\tt.Parallel()\n\n\t\thelpers.CleanupTerraformFolder(t, testFixtureHclvalidate)\n\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHclvalidate)\n\t\trootPath := filepath.Join(tmpEnvPath, testFixtureHclvalidate, \"valid\")\n\n\t\t// Test each subdirectory individually\n\t\tentries, err := os.ReadDir(rootPath)\n\t\trequire.NoError(t, err)\n\n\t\tfor _, entry := range entries {\n\t\t\tif !entry.IsDir() {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsubPath := filepath.Join(rootPath, entry.Name())\n\n\t\t\tt.Run(entry.Name(), func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\n\t\t\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt hcl validate --strict --inputs --working-dir \"+subPath)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t})\n\t\t}\n\t})\n}\n\nfunc TestHclvalidateDiagnostic(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHclvalidate)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHclvalidate)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHclvalidate)\n\n\texpectedDiags := diagnostic.Diagnostics{\n\t\t&diagnostic.Diagnostic{\n\t\t\tSeverity: diagnostic.DiagnosticSeverity(hcl.DiagError),\n\t\t\tSummary:  \"Invalid expression\",\n\t\t\tDetail:   \"Expected the start of an expression, but found an invalid expression token.\",\n\t\t\tRange: &diagnostic.Range{\n\t\t\t\tFilename: filepath.Join(rootPath, \"second/a/terragrunt.hcl\"),\n\t\t\t\tStart:    diagnostic.Pos{Line: 2, Column: 6, Byte: 14},\n\t\t\t\tEnd:      diagnostic.Pos{Line: 3, Column: 1, Byte: 15},\n\t\t\t},\n\t\t\tSnippet: &diagnostic.Snippet{\n\t\t\t\tContext:              \"locals\",\n\t\t\t\tCode:                 \"  t =\\n}\",\n\t\t\t\tStartLine:            2,\n\t\t\t\tHighlightStartOffset: 5,\n\t\t\t\tHighlightEndOffset:   6,\n\t\t\t},\n\t\t},\n\t\t&diagnostic.Diagnostic{\n\t\t\tSeverity: diagnostic.DiagnosticSeverity(hcl.DiagError),\n\t\t\tSummary:  \"Unsupported attribute\",\n\t\t\tDetail:   \"This object does not have an attribute named \\\"outputs\\\".\",\n\t\t\tRange: &diagnostic.Range{\n\t\t\t\tFilename: filepath.Join(rootPath, \"second/c/terragrunt.hcl\"),\n\t\t\t\tStart:    diagnostic.Pos{Line: 6, Column: 19, Byte: 86},\n\t\t\t\tEnd:      diagnostic.Pos{Line: 6, Column: 27, Byte: 94},\n\t\t\t},\n\t\t\tSnippet: &diagnostic.Snippet{\n\t\t\t\tContext:              \"\",\n\t\t\t\tCode:                 \"  c = dependency.a.outputs.z\",\n\t\t\t\tStartLine:            6,\n\t\t\t\tHighlightStartOffset: 18,\n\t\t\t\tHighlightEndOffset:   26,\n\t\t\t\tValues:               []diagnostic.ExpressionValue{{Traversal: \"dependency.a\", Statement: \"is object with no attributes\"}},\n\t\t\t},\n\t\t},\n\t\t&diagnostic.Diagnostic{\n\t\t\tSeverity: diagnostic.DiagnosticSeverity(hcl.DiagError),\n\t\t\tSummary:  \"Missing required argument\",\n\t\t\tDetail:   \"The argument \\\"config_path\\\" is required, but no definition was found.\",\n\t\t\tRange: &diagnostic.Range{\n\t\t\t\tFilename: filepath.Join(rootPath, \"second/c/terragrunt.hcl\"),\n\t\t\t\tStart:    diagnostic.Pos{Line: 16, Column: 16, Byte: 219},\n\t\t\t\tEnd:      diagnostic.Pos{Line: 16, Column: 17, Byte: 220},\n\t\t\t},\n\t\t\tSnippet: &diagnostic.Snippet{\n\t\t\t\tContext:              \"dependency \\\"iam\\\"\",\n\t\t\t\tCode:                 \"dependency iam {\",\n\t\t\t\tStartLine:            16,\n\t\t\t\tHighlightStartOffset: 15,\n\t\t\t\tHighlightEndOffset:   16,\n\t\t\t},\n\t\t},\n\t\t&diagnostic.Diagnostic{\n\t\t\tSeverity: diagnostic.DiagnosticSeverity(hcl.DiagError),\n\t\t\tSummary:  \"Can't evaluate expression\",\n\t\t\tDetail:   \"You can only reference to other local variables here, but it looks like you're referencing something else (\\\"dependency\\\" is not defined)\",\n\t\t\tRange: &diagnostic.Range{\n\t\t\t\tFilename: filepath.Join(rootPath, \"second/c/terragrunt.hcl\"),\n\t\t\t\tStart:    diagnostic.Pos{Line: 12, Column: 9, Byte: 149},\n\t\t\t\tEnd:      diagnostic.Pos{Line: 12, Column: 21, Byte: 161},\n\t\t\t},\n\t\t\tSnippet: &diagnostic.Snippet{\n\t\t\t\tContext:              \"locals\",\n\t\t\t\tCode:                 \"  ddd = dependency.d\",\n\t\t\t\tStartLine:            12,\n\t\t\t\tHighlightStartOffset: 8,\n\t\t\t\tHighlightEndOffset:   20,\n\t\t\t},\n\t\t},\n\t\t&diagnostic.Diagnostic{\n\t\t\tSeverity: diagnostic.DiagnosticSeverity(hcl.DiagError),\n\t\t\tSummary:  \"Can't evaluate expression\",\n\t\t\tDetail:   \"You can only reference to other local variables here, but it looks like you're referencing something else (\\\"dependency\\\" is not defined)\",\n\t\t\tRange: &diagnostic.Range{\n\t\t\t\tFilename: filepath.Join(rootPath, \"second/c/terragrunt.hcl\"),\n\t\t\t\tStart:    diagnostic.Pos{Line: 10, Column: 9, Byte: 117},\n\t\t\t\tEnd:      diagnostic.Pos{Line: 10, Column: 31, Byte: 139},\n\t\t\t},\n\t\t\tSnippet: &diagnostic.Snippet{\n\t\t\t\tContext:              \"locals\",\n\t\t\t\tCode:                 \"  vvv = dependency.a.outputs.z\",\n\t\t\t\tStartLine:            10,\n\t\t\t\tHighlightStartOffset: 8,\n\t\t\t\tHighlightEndOffset:   30,\n\t\t\t},\n\t\t},\n\t}\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt hcl validate --working-dir %s --json\", rootPath))\n\trequire.Error(t, err)\n\n\tvar actualDiags diagnostic.Diagnostics\n\n\terr = json.Unmarshal([]byte(strings.TrimSpace(stdout)), &actualDiags)\n\trequire.NoError(t, err)\n\n\tassert.ElementsMatch(t, expectedDiags, actualDiags)\n}\n\nfunc TestHclvalidateReturnsNonZeroExitCodeOnError(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHclvalidate)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHclvalidate)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHclvalidate)\n\n\t// We expect an error because the fixture has HCL validation issues.\n\t// The content of stdout and stderr isn't the primary focus here,\n\t// rather the fact that an error (non-zero exit code) is returned.\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt hcl validate --working-dir \"+rootPath)\n\trequire.Error(t, err, \"terragrunt hcl validate should return a non-zero exit code on HCL errors\")\n\n\t// As an additional check, we can verify that the error message indicates HCL validation errors.\n\t// This makes the test more robust.\n\tassert.Contains(t, err.Error(), \"HCL validation error(s) found\")\n}\n\nfunc TestHclvalidateInvalidConfigPath(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHclvalidate)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHclvalidate)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHclvalidate)\n\n\texpectedRelPaths := []string{\n\t\tfilepath.Join(\"second\", \"a\", \"terragrunt.hcl\"),\n\t\tfilepath.Join(\"second\", \"c\", \"terragrunt.hcl\"),\n\t}\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt hcl validate --working-dir %s --json --show-config-path\",\n\t\t\trootPath,\n\t\t),\n\t)\n\trequire.Error(t, err)\n\n\tvar actualPaths []string\n\n\terr = json.Unmarshal([]byte(strings.TrimSpace(stdout)), &actualPaths)\n\trequire.NoError(t, err)\n\n\tfor _, rel := range expectedRelPaths {\n\t\tfound := false\n\n\t\tfor _, p := range actualPaths {\n\t\t\tif strings.HasSuffix(p, rel) {\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tassert.Truef(t, found, \"expected a path ending with %q in %v\", rel, actualPaths)\n\t}\n}\n\nfunc TestTerragruntProviderCacheMultiplePlatforms(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureProviderCacheMultiplePlatforms)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureProviderCacheMultiplePlatforms)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureProviderCacheMultiplePlatforms)\n\n\tproviderCacheDir := helpers.TmpDirWOSymlinks(t)\n\n\tvar (\n\t\tplatforms     = []string{\"linux_amd64\", \"darwin_arm64\"}\n\t\tplatformsArgs = make([]string, 0, len(platforms))\n\t)\n\n\tfor _, platform := range platforms {\n\t\tplatformsArgs = append(platformsArgs, \"-platform=\"+platform)\n\t}\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt run --all --no-auto-init --provider-cache --provider-cache-dir %s --non-interactive --working-dir %s\", providerCacheDir, rootPath)+\" -- providers lock \"+strings.Join(platformsArgs, \" \"))\n\n\tproviders := []string{\n\t\t\"hashicorp/aws/5.36.0\",\n\t\t\"hashicorp/azurerm/3.95.0\",\n\t}\n\n\tregistryName := \"registry.opentofu.org\"\n\tif isTerraform() {\n\t\tregistryName = \"registry.terraform.io\"\n\t}\n\n\tfor _, appName := range []string{\"app1\", \"app2\", \"app3\"} {\n\t\tappPath := filepath.Join(rootPath, appName)\n\t\tassert.True(t, util.FileExists(appPath))\n\n\t\tlockfilePath := filepath.Join(appPath, \".terraform.lock.hcl\")\n\t\tlockfileContent, err := os.ReadFile(lockfilePath)\n\t\trequire.NoError(t, err)\n\n\t\tlockfile, diags := hclwrite.ParseConfig(lockfileContent, lockfilePath, hcl.Pos{Line: 1, Column: 1})\n\t\tassert.False(t, diags.HasErrors())\n\t\tassert.NotNil(t, lockfile)\n\n\t\tfor _, provider := range providers {\n\t\t\tprovider := path.Join(registryName, provider)\n\n\t\t\tproviderBlock := lockfile.Body().FirstMatchingBlock(\"provider\", []string{filepath.Dir(provider)})\n\t\t\tassert.NotNil(t, providerBlock)\n\n\t\t\tproviderPath := filepath.Join(providerCacheDir, provider)\n\t\t\tassert.True(t, util.FileExists(providerPath))\n\n\t\t\tfor _, platform := range platforms {\n\t\t\t\tplatformPath := filepath.Join(providerPath, platform)\n\t\t\t\tassert.True(t, util.FileExists(platformPath))\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestTerragruntInitOnce(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureInitOnce)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureInitOnce)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt plan --non-interactive --tf-forward-stdout --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"Initializing modules\")\n\n\t// update the config creation time without changing content\n\tcfgPath := filepath.Join(rootPath, \"terragrunt.hcl\")\n\tbytes, err := os.ReadFile(cfgPath)\n\trequire.NoError(t, err)\n\terr = os.WriteFile(cfgPath, bytes, 0644)\n\trequire.NoError(t, err)\n\n\tstdout, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt, \"terragrunt plan --non-interactive --tf-forward-stdout --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\tassert.NotContains(t, stdout, \"Initializing modules\", \"init command executed more than once\")\n}\n\nfunc TestTerragruntWorksWithSingleJSONConfig(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureConfigSingleJSONPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureConfigSingleJSONPath)\n\n\trootTerragruntConfigPath := filepath.Join(tmpEnvPath, testFixtureConfigSingleJSONPath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt plan --non-interactive --working-dir \"+rootTerragruntConfigPath)\n}\n\nfunc TestTerragruntWorksWithNonDefaultConfigNamesAndRunAllCommand(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureConfigWithNonDefaultNames)\n\ttmpEnvPath = path.Join(tmpEnvPath, testFixtureConfigWithNonDefaultNames)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all apply --log-level debug --config main.hcl --non-interactive --working-dir \"+tmpEnvPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"run_cmd output: [parent_hcl_file]\")\n\tassert.Contains(t, stderr, \"run_cmd output: [dependency_hcl]\")\n\tassert.Contains(t, stderr, \"run_cmd output: [common_hcl]\")\n}\n\nfunc TestTerragruntWorksWithNonDefaultConfigNames(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureConfigWithNonDefaultNames)\n\ttmpEnvPath = path.Join(tmpEnvPath, testFixtureConfigWithNonDefaultNames)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt apply --config main.hcl --non-interactive --working-dir \"+\n\t\t\tfilepath.Join(tmpEnvPath, \"app\"),\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 1, strings.Count(stdout, \"parent_hcl_file\"))\n\tassert.Equal(t, 1, strings.Count(stdout, \"dependency_hcl\"))\n\tassert.Equal(t, 1, strings.Count(stdout, \"common_hcl\"))\n}\n\nfunc TestTerragruntReportsTerraformErrorsWithPlanAll(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureFailedTerraform)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureFailedTerraform)\n\n\trootTerragruntConfigPath := filepath.Join(tmpEnvPath, \"fixtures/failure\")\n\n\tcmd := \"terragrunt run --all plan --non-interactive --working-dir \" + rootTerragruntConfigPath\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\t// Call helpers.RunTerragruntCommand directly because this command contains failures (which causes helpers.RunTerragruntRedirectOutput to abort) but we don't care.\n\terr := helpers.RunTerragruntCommand(t, cmd, &stdout, &stderr)\n\trequire.Error(t, err)\n\n\toutput := stdout.String()\n\terrOutput := stderr.String()\n\tfmt.Printf(\"STDERR is %s.\\n STDOUT is %s\", errOutput, output)\n\n\tassert.Contains(t, errOutput, \"missingvar1\")\n\tassert.Contains(t, errOutput, \"missingvar2\")\n}\n\nfunc TestTerragruntGraphDependenciesCommand(t *testing.T) {\n\tt.Parallel()\n\n\t// this test doesn't even run plan, it exits right after the stack was created\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGraphDependencies)\n\n\trootTerragruntConfigPath := filepath.Join(tmpEnvPath, testFixtureGraphDependencies, \"root.hcl\")\n\thelpers.CopyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, \"not-used\", \"not-used\")\n\n\tenvironmentPath := fmt.Sprintf(\"%s/%s/root\", tmpEnvPath, testFixtureGraphDependencies)\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\thelpers.RunTerragruntRedirectOutput(t, \"terragrunt dag graph --working-dir \"+environmentPath, &stdout, &stderr)\n\toutput := stdout.String()\n\tassert.Contains(t, output, strings.TrimSpace(`\ndigraph {\n\t\"backend-app\" ;\n\t\"backend-app\" -> \"mysql\";\n\t\"backend-app\" -> \"redis\";\n\t\"backend-app\" -> \"vpc\";\n\t\"frontend-app\" ;\n\t\"frontend-app\" -> \"backend-app\";\n\t\"frontend-app\" -> \"vpc\";\n\t\"mysql\" ;\n\t\"mysql\" -> \"vpc\";\n\t\"redis\" ;\n\t\"redis\" -> \"vpc\";\n\t\"vpc\" ;\n}\n\t`))\n}\n\n// Check that Terragrunt does not pollute stdout with anything\nfunc TestTerragruntStdOut(t *testing.T) {\n\tt.Parallel()\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+testFixtureStdout)\n\thelpers.RunTerragruntRedirectOutput(t, \"terragrunt output foo --non-interactive --working-dir \"+testFixtureStdout, &stdout, &stderr)\n\n\toutput := stdout.String()\n\tassert.Equal(t, \"\\\"foo\\\"\\n\", output)\n}\n\nfunc TestTerragruntStackCommandsWithPlanFile(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath, err := filepath.EvalSymlinks(helpers.CopyEnvironment(t, testFixtureDisjoint))\n\trequire.NoError(t, err)\n\n\tdisjointEnvironmentPath := filepath.Join(tmpEnvPath, testFixtureDisjoint)\n\n\thelpers.CleanupTerraformFolder(t, disjointEnvironmentPath)\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\t\"terragrunt run --all  --log-level info --non-interactive --working-dir \"+disjointEnvironmentPath+\" -- plan -out=plan.tfplan\",\n\t)\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\t\"terragrunt run --all --log-level info --non-interactive --working-dir \"+disjointEnvironmentPath+\" -- apply plan.tfplan\",\n\t)\n}\n\nfunc TestTerragruntStackCommandsWithSymlinks(t *testing.T) {\n\tt.Parallel()\n\n\t// please be aware that helpers.CopyEnvironment resolves symlinks statically,\n\t// so the symlinked directories are copied physically, which defeats the purpose of this test,\n\t// therefore we are going to create the symlinks manually in the destination directory\n\ttmpEnvPath, err := filepath.EvalSymlinks(helpers.CopyEnvironment(t, textFixtureDisjointSymlinks))\n\trequire.NoError(t, err)\n\n\tdisjointSymlinksEnvironmentPath := filepath.Join(tmpEnvPath, textFixtureDisjointSymlinks)\n\trequire.NoError(\n\t\tt,\n\t\tos.Symlink(filepath.Join(disjointSymlinksEnvironmentPath, \"a\"),\n\t\t\tfilepath.Join(disjointSymlinksEnvironmentPath, \"b\"),\n\t\t),\n\t)\n\trequire.NoError(\n\t\tt,\n\t\tos.Symlink(filepath.Join(disjointSymlinksEnvironmentPath, \"a\"),\n\t\t\tfilepath.Join(disjointSymlinksEnvironmentPath, \"c\"),\n\t\t),\n\t)\n\n\thelpers.CleanupTerraformFolder(t, disjointSymlinksEnvironmentPath)\n\n\t// perform the first initialization\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all init --experiment symlinks --log-level info --non-interactive --working-dir \"+disjointSymlinksEnvironmentPath,\n\t)\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Downloading Terraform configurations from ./module into ./a/.terragrunt-cache\")\n\tassert.Contains(t, stderr, \"Downloading Terraform configurations from ./module into ./b/.terragrunt-cache\")\n\tassert.Contains(t, stderr, \"Downloading Terraform configurations from ./module into ./c/.terragrunt-cache\")\n\n\t// perform the second initialization and make sure that the cache is not downloaded again\n\t_, stderr, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all init --experiment symlinks --log-level info --non-interactive --working-dir \"+disjointSymlinksEnvironmentPath,\n\t)\n\trequire.NoError(t, err)\n\tassert.NotContains(t, stderr, \"Downloading Terraform configurations from ./module into ./a/.terragrunt-cache\")\n\tassert.NotContains(t, stderr, \"Downloading Terraform configurations from ./module into ./b/.terragrunt-cache\")\n\tassert.NotContains(t, stderr, \"Downloading Terraform configurations from ./module into ./c/.terragrunt-cache\")\n\n\t// validate the modules\n\t_, stderr, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all validate --experiment symlinks --log-level info --non-interactive --working-dir \"+disjointSymlinksEnvironmentPath,\n\t)\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Unit a\")\n\tassert.Contains(t, stderr, \"Unit b\")\n\tassert.Contains(t, stderr, \"Unit c\")\n\n\t// touch the \"module/main.tf\" file to change the timestamp and make sure that the cache is downloaded again\n\trequire.NoError(t, os.Chtimes(filepath.Join(disjointSymlinksEnvironmentPath, \"module/main.tf\"), time.Now(), time.Now()))\n\n\t// perform the initialization and make sure that the cache is downloaded again\n\t_, stderr, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all init --experiment symlinks --log-level info --non-interactive --working-dir \"+disjointSymlinksEnvironmentPath,\n\t)\n\trequire.NoError(t, err)\n\tassert.Contains(t, stderr, \"Downloading Terraform configurations from ./module into ./a/.terragrunt-cache\")\n\tassert.Contains(t, stderr, \"Downloading Terraform configurations from ./module into ./b/.terragrunt-cache\")\n\tassert.Contains(t, stderr, \"Downloading Terraform configurations from ./module into ./c/.terragrunt-cache\")\n}\n\nfunc TestInvalidSource(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureNotExistingSource)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureNotExistingSource)\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt init --working-dir \"+generateTestCase, &stdout, &stderr)\n\trequire.Error(t, err)\n\n\tvar workingDirNotFoundErr run.WorkingDirNotFound\n\n\tok := errors.As(err, &workingDirNotFoundErr)\n\tassert.True(t, ok)\n}\n\nfunc TestPlanfileOrder(t *testing.T) {\n\tt.Parallel()\n\n\trootPath := helpers.CopyEnvironment(t, testFixturePlanfileOrder)\n\tmodulePath := filepath.Join(rootPath, testFixturePlanfileOrder)\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --working-dir \"+modulePath, os.Stdout, os.Stderr)\n\trequire.NoError(t, err)\n\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --working-dir \"+modulePath, os.Stdout, os.Stderr)\n\trequire.NoError(t, err)\n}\n\n// This tests terragrunt properly passes through terraform commands and any number of specified args\nfunc TestTerraformCommandCliArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpectedErr error\n\t\texpected    string\n\t\tcommand     []string\n\t}{\n\t\t{\n\t\t\tcommand:  []string{\"version\"},\n\t\t\texpected: wrappedBinary() + \" version\",\n\t\t},\n\t\t{\n\t\t\tcommand:  []string{\"--\", \"version\"},\n\t\t\texpected: wrappedBinary() + \" version\",\n\t\t},\n\t\t{\n\t\t\tcommand:  []string{\"--\", \"version\", \"foo\"},\n\t\t\texpected: wrappedBinary() + \" version\",\n\t\t},\n\t\t{\n\t\t\tcommand:  []string{\"--\", \"version\", \"foo\", \"bar\", \"baz\"},\n\t\t\texpected: wrappedBinary() + \" version\",\n\t\t},\n\t\t{\n\t\t\tcommand:  []string{\"--\", \"version\", \"foo\", \"bar\", \"baz\", \"foobar\"},\n\t\t\texpected: wrappedBinary() + \" version\",\n\t\t},\n\t\t{\n\t\t\tcommand:  []string{\"--\", \"graph\"},\n\t\t\texpected: \"digraph\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tcmd := fmt.Sprintf(\n\t\t\t\"terragrunt run --non-interactive --log-level debug --working-dir %s %s\",\n\t\t\ttestFixtureExtraArgsPath,\n\t\t\tstrings.Join(\n\t\t\t\ttc.command,\n\t\t\t\t\" \",\n\t\t\t),\n\t\t)\n\n\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\t\tif tc.expectedErr != nil {\n\t\t\trequire.ErrorIs(t, err, tc.expectedErr)\n\t\t}\n\n\t\tassert.Contains(t, stdout+stderr, tc.expected)\n\t}\n}\n\n// This tests terragrunt properly passes through terraform commands with sub commands\n// and any number of specified args\nfunc TestTerraformSubcommandCliArgs(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\texpected string\n\t\tcommand  []string\n\t}{\n\t\t{\n\t\t\tcommand:  []string{\"force-unlock\"},\n\t\t\texpected: wrappedBinary() + \" force-unlock\",\n\t\t},\n\t\t{\n\t\t\tcommand:  []string{\"force-unlock\", \"foo\"},\n\t\t\texpected: wrappedBinary() + \" force-unlock foo\",\n\t\t},\n\t\t{\n\t\t\tcommand:  []string{\"force-unlock\", \"foo\", \"bar\", \"baz\"},\n\t\t\texpected: wrappedBinary() + \" force-unlock foo bar baz\",\n\t\t},\n\t\t{\n\t\t\tcommand:  []string{\"force-unlock\", \"foo\", \"bar\", \"baz\", \"foobar\"},\n\t\t\texpected: wrappedBinary() + \" force-unlock foo bar baz foobar\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tcmd := fmt.Sprintf(\n\t\t\t\"terragrunt %s --non-interactive --log-level debug --working-dir %s\",\n\t\t\tstrings.Join(\n\t\t\t\ttc.command,\n\t\t\t\t\" \",\n\t\t\t),\n\t\t\ttestFixtureExtraArgsPath,\n\t\t)\n\n\t\t// Call helpers.RunTerragruntCommand directly because this command\n\t\t// contains failures (which causes helpers.RunTerragruntRedirectOutput to abort) but we don't care.\n\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"Failed to properly fail command: %v.\", cmd)\n\t\t}\n\n\t\tassert.True(t, strings.Contains(stderr, tc.expected) || strings.Contains(stdout, tc.expected))\n\t}\n}\n\nfunc validateInputs(t *testing.T, outputs map[string]helpers.TerraformOutput) {\n\tt.Helper()\n\n\tassert.Equal(t, true, outputs[\"bool\"].Value)\n\tassert.Equal(t, []any{true, false}, outputs[\"list_bool\"].Value)\n\tassert.Equal(t, []any{1.0, 2.0, 3.0}, outputs[\"list_number\"].Value)\n\tassert.Equal(t, []any{\"a\", \"b\", \"c\"}, outputs[\"list_string\"].Value)\n\tassert.Equal(t, map[string]any{\"foo\": true, \"bar\": false, \"baz\": true}, outputs[\"map_bool\"].Value)\n\tassert.Equal(t, map[string]any{\"foo\": 42.0, \"bar\": 12345.0}, outputs[\"map_number\"].Value)\n\tassert.Equal(t, map[string]any{\"foo\": \"bar\"}, outputs[\"map_string\"].Value)\n\tassert.InEpsilon(t, 42.0, outputs[\"number\"].Value, 0.0000000001)\n\tassert.Equal(t, map[string]any{\"list\": []any{1.0, 2.0, 3.0}, \"map\": map[string]any{\"foo\": \"bar\"}, \"num\": 42.0, \"str\": \"string\"}, outputs[\"object\"].Value)\n\tassert.Equal(t, \"string\", outputs[\"string\"].Value)\n\tassert.Equal(t, \"default\", outputs[\"from_env\"].Value)\n}\n\nfunc TestInputsPassedThroughCorrectly(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureInputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureInputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureInputs)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\tvalidateInputs(t, outputs)\n}\n\nfunc TestRunCommand(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureInputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureInputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureInputs)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --non-interactive --working-dir \"+rootPath+\" -- apply -auto-approve\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt run -no-color --non-interactive --working-dir \"+rootPath+\" -- output -json\", &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\tvalidateInputs(t, outputs)\n}\n\n// TestInputsWithInterpolationPatterns validates that input variables containing ${...} patterns\n// are passed to Terraform without triggering HCL interpolation errors (issue #3368).\nfunc TestInputsWithInterpolationPatterns(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureInputsInterpolation)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureInputsInterpolation)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureInputsInterpolation)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\t// map_with_interpolation.foo should be the literal string \"test ${bar} test\" (not interpolated)\n\tmapOutput, ok := outputs[\"map_with_interpolation\"]\n\trequire.True(t, ok, \"map_with_interpolation output not found\")\n\tmapValue, ok := mapOutput.Value.(map[string]any)\n\trequire.True(t, ok, \"map_with_interpolation value is not a map\")\n\tassert.Equal(t, \"test ${bar} test\", mapValue[\"foo\"])\n\tassert.Equal(t, \"no interpolation here\", mapValue[\"baz\"])\n}\n\nfunc TestTerragruntMissingDependenciesFail(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/missing-dependencies\")\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureMissingDependence)\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt init --working-dir \"+generateTestCase, &stdout, &stderr)\n\trequire.Error(t, err)\n\n\tvar parsedError config.DependencyDirNotFoundError\n\n\tok := errors.As(err, &parsedError)\n\tassert.True(t, ok)\n\tassert.Len(t, parsedError.Dir, 1)\n\tassert.Contains(t, parsedError.Dir[0], \"hl3-release\")\n}\n\nfunc TestTerragruntExcludeExternalDependencies(t *testing.T) {\n\tt.Parallel()\n\n\texcludedModule := \"module-a\"\n\tincludedModule := \"module-b\"\n\n\tmodules := []string{\n\t\texcludedModule,\n\t\tincludedModule,\n\t}\n\n\thelpers.CleanupTerraformFolder(t, testFixtureExternalDependence)\n\n\tfor _, module := range modules {\n\t\thelpers.CleanupTerraformFolder(t, filepath.Join(testFixtureExternalDependence, module))\n\t}\n\n\tvar (\n\t\tapplyAllStdout bytes.Buffer\n\t\tapplyAllStderr bytes.Buffer\n\t)\n\n\trootPath := helpers.CopyEnvironment(t, testFixtureExternalDependence)\n\tmodulePath := filepath.Join(rootPath, testFixtureExternalDependence, includedModule)\n\n\terr := helpers.RunTerragruntCommand(\n\t\tt,\n\t\t\"terragrunt run --all apply --non-interactive --queue-exclude-external --tf-forward-stdout --working-dir \"+modulePath,\n\t\t&applyAllStdout,\n\t\t&applyAllStderr,\n\t)\n\thelpers.LogBufferContentsLineByLine(t, applyAllStdout, \"run --all apply stdout\")\n\thelpers.LogBufferContentsLineByLine(t, applyAllStderr, \"run --all apply stderr\")\n\n\tapplyAllStdoutString := applyAllStdout.String()\n\n\tif err != nil {\n\t\tt.Errorf(\"Did not expect to get error: %s\", err.Error())\n\t}\n\n\tassert.Contains(t, applyAllStdoutString, \"Hello World, \"+includedModule)\n\tassert.NotContains(t, applyAllStdoutString, \"Hello World, \"+excludedModule)\n}\n\nfunc TestApplySkipTrue(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureSkipLegacyRoot)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureSkipLegacyRoot, \"skip-true\")\n\n\tshowStdout := bytes.Buffer{}\n\tshowStderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, fmt.Sprintf(\"terragrunt apply -auto-approve --log-level info --non-interactive --working-dir %s --var person=Hobbs\", rootPath), &showStdout, &showStderr)\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr\")\n\n\tstdout := showStdout.String()\n\tstderr := showStderr.String()\n\n\trequire.NoError(t, err)\n\t// For single unit execution, early exit message should appear\n\toutput := stderr + stdout\n\tassert.Contains(t, output, \"Early exit in terragrunt unit\")\n\tassert.Contains(t, output, \"due to exclude block with no_run = true\")\n\tassert.NotContains(t, stdout, \"hello, Hobbs\")\n}\n\nfunc TestApplySkipFalse(t *testing.T) {\n\tt.Parallel()\n\n\trootPath := helpers.CopyEnvironment(t, testFixtureSkipLegacyRoot)\n\trootPath = filepath.Join(rootPath, testFixtureSkipLegacyRoot, \"skip-false\")\n\n\tshowStdout := bytes.Buffer{}\n\tshowStderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --tf-forward-stdout --working-dir \"+rootPath, &showStdout, &showStderr)\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr\")\n\n\tstderr := showStderr.String()\n\tstdout := showStdout.String()\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"hello, Hobbs\")\n\tassert.NotContains(t, stderr, \"Early exit in terragrunt unit\")\n}\n\nfunc TestApplyAllSkipTrue(t *testing.T) {\n\tt.Parallel()\n\n\trootPath := helpers.CopyEnvironment(t, testFixtureSkip)\n\trootPath = filepath.Join(rootPath, testFixtureSkip, \"skip-true\")\n\n\tshowStdout := bytes.Buffer{}\n\tshowStderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, fmt.Sprintf(\"terragrunt run --all apply --non-interactive --tf-forward-stdout --working-dir %s --log-level info\", rootPath), &showStdout, &showStderr)\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr\")\n\n\tstdout := showStdout.String()\n\tstderr := showStderr.String()\n\n\t// this test is now prepared to handle the case where skip is inherited from the included terragrunt file\n\t// meaning the skip-true/resource2 module will be skipped as well and only the skip-true/resource1 module will be applied\n\n\trequire.NoError(t, err)\n\t// Check that units were excluded at stack level (shown in Run Summary)\n\toutput := stderr + stdout\n\tassert.Contains(t, output, \"Excluded\")\n\tassert.Contains(t, stdout, \"hello, Ernie\")\n\tassert.NotContains(t, stdout, \"hello, Bert\")\n}\n\nfunc TestApplyAllSkipFalse(t *testing.T) {\n\tt.Parallel()\n\n\trootPath := helpers.CopyEnvironment(t, testFixtureSkip)\n\trootPath = filepath.Join(rootPath, testFixtureSkip, \"skip-false\")\n\n\tshowStdout := bytes.Buffer{}\n\tshowStderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt run --all apply --non-interactive --tf-forward-stdout --working-dir \"+rootPath, &showStdout, &showStderr)\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr\")\n\n\tstdout := showStdout.String()\n\tstderr := showStderr.String()\n\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"hello, Ernie\")\n\tassert.Contains(t, stdout, \"hello, Bert\")\n\tassert.NotContains(t, stderr, \"Early exit in terragrunt unit\")\n}\n\nfunc TestDependencyOutput(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"integration\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// verify expected output 42\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\tapp3Path := filepath.Join(rootPath, \"app3\")\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+app3Path, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\tassert.Equal(t, 42, int(outputs[\"z\"].Value.(float64)))\n}\n\nfunc TestDependencyOutputErrorBeforeApply(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"integration\")\n\tapp3Path := filepath.Join(rootPath, \"app3\")\n\n\tshowStdout := bytes.Buffer{}\n\tshowStderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --non-interactive --working-dir \"+app3Path, &showStdout, &showStderr)\n\trequire.Error(t, err)\n\t// Verify that we fail because the dependency is not applied yet\n\tassert.Contains(t, err.Error(), \"has not been applied yet\")\n\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr\")\n}\n\nfunc TestDependencyOutputSkipOutputs(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"integration\")\n\temptyPath := filepath.Join(rootPath, \"empty\")\n\n\tshowStdout := bytes.Buffer{}\n\tshowStderr := bytes.Buffer{}\n\n\t// Test that even if the dependency (app1) is not applied, using skip_outputs will skip pulling the outputs so there\n\t// will be no errors.\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --non-interactive --working-dir \"+emptyPath, &showStdout, &showStderr)\n\trequire.NoError(t, err)\n\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr\")\n}\n\nfunc TestDependencyOutputSkipOutputsWithMockOutput(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"mock-outputs\")\n\tdependent3Path := filepath.Join(rootPath, \"dependent3\")\n\n\tshowStdout := bytes.Buffer{}\n\tshowStderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+dependent3Path, &showStdout, &showStderr)\n\trequire.NoError(t, err)\n\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr\")\n\n\t// verify expected output when mocks are used: The answer is 0\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+dependent3Path, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\tassert.Equal(t, \"The answer is 0\", outputs[\"truth\"].Value)\n\n\t// Now run --all apply so that the dependency is applied, and verify it still uses the mock output\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath, &showStdout, &showStderr)\n\trequire.NoError(t, err)\n\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr\")\n\n\t// verify expected output when mocks are used: The answer is 0\n\tstdout = bytes.Buffer{}\n\tstderr = bytes.Buffer{}\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+dependent3Path, &stdout, &stderr),\n\t)\n\n\toutputs = map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\tassert.Equal(t, \"The answer is 0\", outputs[\"truth\"].Value)\n}\n\n// Test that when you have a mock_output on a dependency, the dependency will use the mock as the output instead\n// of erroring out.\nfunc TestDependencyMockOutput(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"mock-outputs\")\n\tdependent1Path := filepath.Join(rootPath, \"dependent1\")\n\n\tshowStdout := bytes.Buffer{}\n\tshowStderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+dependent1Path, &showStdout, &showStderr)\n\trequire.NoError(t, err)\n\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr\")\n\n\t// verify expected output when mocks are used: The answer is 0\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+dependent1Path, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\tassert.Equal(t, \"The answer is 0\", outputs[\"truth\"].Value)\n\n\t// Now run --all apply so that the dependency is applied, and verify it uses the dependency output\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath, &showStdout, &showStderr)\n\trequire.NoError(t, err)\n\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr\")\n\n\t// verify expected output when mocks are used: The answer is 0\n\tstdout = bytes.Buffer{}\n\tstderr = bytes.Buffer{}\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+dependent1Path, &stdout, &stderr),\n\t)\n\n\toutputs = map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\tassert.Equal(t, \"The answer is 42\", outputs[\"truth\"].Value)\n}\n\n// Test default behavior when mock_outputs_merge_with_state is not set. It should behave, as before this parameter was added\n// It will fail on any command if the parent state is not applied, because the state of the parent exists and it already has an output\n// but not the newly added output.\nfunc TestDependencyMockOutputMergeWithStateDefault(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"mock-outputs-merge-with-state\", \"merge-with-state-default\", \"live\")\n\tparentPath := filepath.Join(rootPath, \"parent\")\n\tchildPath := filepath.Join(rootPath, \"child\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --non-interactive --working-dir \"+parentPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"plan stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"plan stderr\")\n\n\t// Verify we have the default behavior if mock_outputs_merge_with_state is not set\n\tstdout.Reset()\n\tstderr.Reset()\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt plan --non-interactive --working-dir \"+childPath, &stdout, &stderr)\n\trequire.Error(t, err)\n\t// Verify that we fail because the dependency is not applied yet, and the new attribute is not available and in\n\t// this case, mocked outputs are not used.\n\tassert.Contains(t, err.Error(), \"This object does not have an attribute named \\\"test_output2\\\"\")\n\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"plan stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"plan stderr\")\n}\n\n// Test when mock_outputs_merge_with_state is explicitly set to false. It should behave, as before this parameter was added\n// It will fail on any command if the parent state is not applied, because the state of the parent exists and it already has an output\n// but not the newly added output.\nfunc TestDependencyMockOutputMergeWithStateFalse(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"mock-outputs-merge-with-state\", \"merge-with-state-false\", \"live\")\n\tparentPath := filepath.Join(rootPath, \"parent\")\n\tchildPath := filepath.Join(rootPath, \"child\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --non-interactive --working-dir \"+parentPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"plan stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"plan stderr\")\n\n\t// Verify we have the default behavior if mock_outputs_merge_with_state is set to false\n\tstdout.Reset()\n\tstderr.Reset()\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt plan --non-interactive --working-dir \"+childPath, &stdout, &stderr)\n\trequire.Error(t, err)\n\t// Verify that we fail because the dependency is not applied yet, and the new attribute is not available and in\n\t// this case, mocked outputs are not used.\n\tassert.Contains(t, err.Error(), \"This object does not have an attribute named \\\"test_output2\\\"\")\n\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"plan stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"plan stderr\")\n}\n\n// Test when mock_outputs_merge_with_state is explicitly set to true.\n// It will mock the newly added output from the parent as it was not already applied to the state.\nfunc TestDependencyMockOutputMergeWithStateTrue(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"mock-outputs-merge-with-state\", \"merge-with-state-true\", \"live\")\n\tparentPath := filepath.Join(rootPath, \"parent\")\n\tchildPath := filepath.Join(rootPath, \"child\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --non-interactive --working-dir \"+parentPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"plan stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"plan stderr\")\n\n\t// Verify mocked outputs are used if mock_outputs_merge_with_state is set to true and some output in the parent are not applied yet.\n\tstdout.Reset()\n\tstderr.Reset()\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+childPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"apply stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"apply stderr\")\n\t// Now check the outputs to make sure they are as expected\n\tstdout.Reset()\n\tstderr.Reset()\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+childPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\tassert.Equal(t, \"value1\", outputs[\"test_output1_from_parent\"].Value)\n\tassert.Equal(t, \"fake-data2\", outputs[\"test_output2_from_parent\"].Value)\n\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"output stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"output stderr\")\n}\n\n// Test when mock_outputs_merge_with_state is explicitly set to true, but using an unallowed command. It should ignore\n// the mock output.\nfunc TestDependencyMockOutputMergeWithStateTrueNotAllowed(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"mock-outputs-merge-with-state\", \"merge-with-state-true-validate-only\", \"live\")\n\tparentPath := filepath.Join(rootPath, \"parent\")\n\tchildPath := filepath.Join(rootPath, \"child\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --non-interactive --working-dir \"+parentPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"plan stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"plan stderr\")\n\n\t// Verify mocked outputs are used if mock_outputs_merge_with_state is set to true with an allowed command and some\n\t// output in the parent are not applied yet.\n\tstdout.Reset()\n\tstderr.Reset()\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt validate --non-interactive --working-dir \"+childPath, &stdout, &stderr),\n\t)\n\n\t// ... but not when an unallowed command is used\n\trequire.Error(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+childPath, &stdout, &stderr),\n\t)\n}\n\n// Test when mock_outputs_merge_with_state is explicitly set to true.\n// Mock should not be used as the parent state was already fully applied.\nfunc TestDependencyMockOutputMergeWithStateNoOverride(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"mock-outputs-merge-with-state\", \"merge-with-state-no-override\", \"live\")\n\tparentPath := filepath.Join(rootPath, \"parent\")\n\tchildPath := filepath.Join(rootPath, \"child\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --non-interactive --working-dir \"+parentPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"show stderr\")\n\n\t// Verify mocked outputs are not used if mock_outputs_merge_with_state is set to true and all outputs in the parent have been applied.\n\tstdout.Reset()\n\tstderr.Reset()\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+childPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\t// Now check the outputs to make sure they are as expected\n\tstdout.Reset()\n\tstderr.Reset()\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+childPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\tassert.Equal(t, \"value1\", outputs[\"test_output1_from_parent\"].Value)\n\tassert.Equal(t, \"value2\", outputs[\"test_output2_from_parent\"].Value)\n\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"show stderr\")\n}\n\n// Test when mock_outputs_merge_strategy_with_state or mock_outputs_merge_with_state is not set, the default is no_merge\nfunc TestDependencyMockOutputMergeStrategyWithStateDefault(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"mock-outputs-merge-strategy-with-state\", \"merge-strategy-with-state-default\", \"live\")\n\tchildPath := filepath.Join(rootPath, \"child\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+childPath, &stdout, &stderr)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"This object does not have an attribute named \\\"test_output_list_string\\\"\")\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"apply stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"apply stderr\")\n}\n\n// Test when mock_outputs_merge_with_state = \"false\" that MergeStrategyType is set to no_merge\nfunc TestDependencyMockOutputMergeStrategyWithStateCompatFalse(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"mock-outputs-merge-strategy-with-state\", \"merge-strategy-with-state-compat-false\", \"live\")\n\tchildPath := filepath.Join(rootPath, \"child\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+childPath, &stdout, &stderr)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"This object does not have an attribute named \\\"test_output_list_string\\\"\")\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"apply stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"apply stderr\")\n}\n\n// Test when mock_outputs_merge_with_state = \"true\" that MergeStrategyType is set to shallow\nfunc TestDependencyMockOutputMergeStrategyWithStateCompatTrue(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"mock-outputs-merge-strategy-with-state\", \"merge-strategy-with-state-compat-true\", \"live\")\n\tchildPath := filepath.Join(rootPath, \"child\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+childPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"apply stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"apply stderr\")\n\n\tstdout.Reset()\n\tstderr.Reset()\n\n\trequire.NoError(t, helpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+childPath, &stdout, &stderr))\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"output stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"output stderr\")\n\n\tassert.Equal(t, \"value1\", outputs[\"test_output1_from_parent\"].Value)\n\tassert.Equal(t, \"map_root1_sub1_value\", util.MustWalkTerraformOutput(outputs[\"test_output_map_map_string_from_parent\"].Value, \"map_root1\", \"map_root1_sub1\", \"value\"))\n\tassert.Nil(t, util.MustWalkTerraformOutput(outputs[\"test_output_map_map_string_from_parent\"].Value, \"not_in_state\", \"abc\", \"value\"))\n\tassert.Equal(t, \"fake-list-data\", util.MustWalkTerraformOutput(outputs[\"test_output_list_string\"].Value, \"0\"))\n\tassert.Nil(t, util.MustWalkTerraformOutput(outputs[\"test_output_list_string\"].Value, \"1\"))\n}\n\n// Test when both mock_outputs_merge_with_state and mock_outputs_merge_strategy_with_state are set, mock_outputs_merge_strategy_with_state is used\nfunc TestDependencyMockOutputMergeStrategyWithStateCompatConflict(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"mock-outputs-merge-strategy-with-state\", \"merge-strategy-with-state-compat-true\", \"live\")\n\tchildPath := filepath.Join(rootPath, \"child\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+childPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"apply stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"apply stderr\")\n\n\tstdout.Reset()\n\tstderr.Reset()\n\n\trequire.NoError(t, helpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+childPath, &stdout, &stderr))\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"output stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"output stderr\")\n\n\tassert.Equal(t, \"value1\", outputs[\"test_output1_from_parent\"].Value)\n\tassert.Equal(t, \"map_root1_sub1_value\", util.MustWalkTerraformOutput(outputs[\"test_output_map_map_string_from_parent\"].Value, \"map_root1\", \"map_root1_sub1\", \"value\"))\n\tassert.Nil(t, util.MustWalkTerraformOutput(outputs[\"test_output_map_map_string_from_parent\"].Value, \"not_in_state\", \"abc\", \"value\"))\n\tassert.Equal(t, \"fake-list-data\", util.MustWalkTerraformOutput(outputs[\"test_output_list_string\"].Value, \"0\"))\n\tassert.Nil(t, util.MustWalkTerraformOutput(outputs[\"test_output_list_string\"].Value, \"1\"))\n}\n\n// Test when mock_outputs_merge_strategy_with_state = \"no_merge\" that mocks are not merged into the current state\nfunc TestDependencyMockOutputMergeStrategyWithStateNoMerge(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"mock-outputs-merge-strategy-with-state\", \"merge-strategy-with-state-no-merge\", \"live\")\n\tchildPath := filepath.Join(rootPath, \"child\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+childPath, &stdout, &stderr)\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"This object does not have an attribute named \\\"test_output_list_string\\\"\")\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"apply stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"apply stderr\")\n}\n\n// Test when mock_outputs_merge_strategy_with_state = \"shallow\" that only top level outputs are merged.\n// Lists or keys in existing maps will not be merged\nfunc TestDependencyMockOutputMergeStrategyWithStateShallow(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"mock-outputs-merge-strategy-with-state\", \"merge-strategy-with-state-shallow\", \"live\")\n\tchildPath := filepath.Join(rootPath, \"child\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+childPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"apply stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"apply stderr\")\n\n\tstdout.Reset()\n\tstderr.Reset()\n\n\trequire.NoError(t, helpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+childPath, &stdout, &stderr))\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"output stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"output stderr\")\n\n\tassert.Equal(t, \"value1\", outputs[\"test_output1_from_parent\"].Value)\n\tassert.Equal(t, \"map_root1_sub1_value\", util.MustWalkTerraformOutput(outputs[\"test_output_map_map_string_from_parent\"].Value, \"map_root1\", \"map_root1_sub1\", \"value\"))\n\tassert.Nil(t, util.MustWalkTerraformOutput(outputs[\"test_output_map_map_string_from_parent\"].Value, \"not_in_state\", \"abc\", \"value\"))\n\tassert.Equal(t, \"fake-list-data\", util.MustWalkTerraformOutput(outputs[\"test_output_list_string\"].Value, \"0\"))\n\tassert.Nil(t, util.MustWalkTerraformOutput(outputs[\"test_output_list_string\"].Value, \"1\"))\n}\n\n// Test when mock_outputs_merge_strategy_with_state = \"deep\" that the existing state is deeply merged into the mocks\n// so that the existing state overwrites the mocks. This allows child modules to use new dependency outputs before the\n// dependency has been applied\nfunc TestDependencyMockOutputMergeStrategyWithStateDeepMapOnly(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"mock-outputs-merge-strategy-with-state\", \"merge-strategy-with-state-deep-map-only\", \"live\")\n\tchildPath := filepath.Join(rootPath, \"child\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+childPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"apply stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"apply stderr\")\n\n\tstdout.Reset()\n\tstderr.Reset()\n\n\trequire.NoError(t, helpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+childPath, &stdout, &stderr))\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"output stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"output stderr\")\n\n\tassert.Equal(t, \"value1\", outputs[\"test_output1_from_parent\"].Value)\n\tassert.Equal(t, \"fake-abc\", outputs[\"test_output2_from_parent\"].Value)\n\tassert.Equal(t, \"map_root1_sub1_value\", util.MustWalkTerraformOutput(outputs[\"test_output_map_map_string_from_parent\"].Value, \"map_root1\", \"map_root1_sub1\", \"value\"))\n\tassert.Equal(t, \"fake-abc\", util.MustWalkTerraformOutput(outputs[\"test_output_map_map_string_from_parent\"].Value, \"not_in_state\", \"abc\", \"value\"))\n\tassert.Equal(t, \"a\", util.MustWalkTerraformOutput(outputs[\"test_output_list_string\"].Value, \"0\"))\n\tassert.Nil(t, util.MustWalkTerraformOutput(outputs[\"test_output_list_string\"].Value, \"1\"))\n}\n\n// Test that when you have a mock_output on a dependency, the dependency will use the mock as the output instead\n// of erroring out when running an allowed command.\nfunc TestDependencyMockOutputRestricted(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"mock-outputs\")\n\tdependent2Path := filepath.Join(rootPath, \"dependent2\")\n\n\tshowStdout := bytes.Buffer{}\n\tshowStderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+dependent2Path, &showStdout, &showStderr)\n\trequire.Error(t, err)\n\t// Verify that we fail because the dependency is not applied yet\n\tassert.Contains(t, err.Error(), \"has not been applied yet\")\n\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr\")\n\n\t// Verify we can run when using one of the allowed commands\n\tshowStdout.Reset()\n\tshowStderr.Reset()\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt validate --non-interactive --working-dir \"+dependent2Path, &showStdout, &showStderr)\n\trequire.NoError(t, err)\n\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr\")\n\n\t// Verify that run --all validate works as well.\n\tshowStdout.Reset()\n\tshowStderr.Reset()\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt run --all validate --non-interactive --working-dir \"+rootPath, &showStdout, &showStderr)\n\trequire.NoError(t, err)\n\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr\")\n\n\tshowStdout.Reset()\n\tshowStderr.Reset()\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt run --all validate --non-interactive --working-dir \"+rootPath, &showStdout, &showStderr)\n\trequire.NoError(t, err)\n\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr\")\n}\n\nfunc TestDependencyOutputTypeConversion(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\thelpers.CleanupTerraformFolder(t, testFixtureInputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, \".\")\n\n\tinputsPath := filepath.Join(tmpEnvPath, testFixtureInputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"type-conversion\")\n\n\t// First apply the inputs module\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+inputsPath)\n\n\t// Then apply the outputs module\n\tshowStdout := bytes.Buffer{}\n\tshowStderr := bytes.Buffer{}\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath, &showStdout, &showStderr),\n\t)\n\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr\")\n\n\t// Now check the outputs to make sure they are as expected\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\tassert.Equal(t, true, outputs[\"bool\"].Value)\n\tassert.Equal(t, []any{true, false}, outputs[\"list_bool\"].Value)\n\tassert.Equal(t, []any{1.0, 2.0, 3.0}, outputs[\"list_number\"].Value)\n\tassert.Equal(t, []any{\"a\", \"b\", \"c\"}, outputs[\"list_string\"].Value)\n\tassert.Equal(t, map[string]any{\"foo\": true, \"bar\": false, \"baz\": true}, outputs[\"map_bool\"].Value)\n\tassert.Equal(t, map[string]any{\"foo\": 42.0, \"bar\": 12345.0}, outputs[\"map_number\"].Value)\n\tassert.Equal(t, map[string]any{\"foo\": \"bar\"}, outputs[\"map_string\"].Value)\n\tassert.InEpsilon(t, 42.0, outputs[\"number\"].Value.(float64), 0.0000001)\n\tassert.Equal(t, map[string]any{\"list\": []any{1.0, 2.0, 3.0}, \"map\": map[string]any{\"foo\": \"bar\"}, \"num\": 42.0, \"str\": \"string\"}, outputs[\"object\"].Value)\n\tassert.Equal(t, \"string\", outputs[\"string\"].Value)\n\tassert.Equal(t, \"default\", outputs[\"from_env\"].Value)\n}\n\n// Regression testing for https://github.com/gruntwork-io/terragrunt/issues/1102: Ordering keys from\n// maps to avoid random placements when terraform file is generated.\nfunc TestOrderedMapOutputRegressions1102(t *testing.T) {\n\tt.Parallel()\n\n\tgenerateTestCase := filepath.Join(testFixtureGetOutput, \"regression-1102\")\n\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\tcommand := \"terragrunt apply --non-interactive --working-dir \" + generateTestCase\n\tpath := filepath.Join(generateTestCase, \"backend.tf\")\n\n\t// runs terragrunt for the first time and checks the output \"backend.tf\" file.\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, command, &stdout, &stderr),\n\t)\n\n\texpected, _ := os.ReadFile(path)\n\tassert.Contains(t, string(expected), \"local\")\n\n\t// runs terragrunt again. All the outputs must be\n\t// equal to the first run.\n\tfor range 20 {\n\t\trequire.NoError(\n\t\t\tt,\n\t\t\thelpers.RunTerragruntCommand(t, command, &stdout, &stderr),\n\t\t)\n\n\t\tactual, _ := os.ReadFile(path)\n\t\tassert.Equal(t, expected, actual)\n\t}\n}\n\n// Test that we get the expected error message about dependency cycles when there is a cycle in the dependency chain\nfunc TestDependencyOutputCycleHandling(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\n\ttestCases := []string{\n\t\t\"aa\",\n\t\t\"aba\",\n\t\t\"abca\",\n\t\t\"abcda\",\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\t\t\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"cycle\", tc)\n\t\t\tfooPath := filepath.Join(rootPath, \"foo\")\n\n\t\t\tplanStdout := bytes.Buffer{}\n\t\t\tplanStderr := bytes.Buffer{}\n\t\t\terr := helpers.RunTerragruntCommand(\n\t\t\t\tt,\n\t\t\t\t\"terragrunt plan --non-interactive --working-dir \"+fooPath,\n\t\t\t\t&planStdout,\n\t\t\t\t&planStderr,\n\t\t\t)\n\t\t\thelpers.LogBufferContentsLineByLine(t, planStdout, \"plan stdout\")\n\t\t\thelpers.LogBufferContentsLineByLine(t, planStderr, \"plan stderr\")\n\t\t\trequire.Error(t, err)\n\t\t\tassert.Contains(t, err.Error(), \"Found a dependency cycle between modules\")\n\t\t})\n\t}\n}\n\n// Regression testing for https://github.com/gruntwork-io/terragrunt/issues/854: Referencing a dependency that is a\n// subdirectory of the current config, which includes an `include` block has problems resolving the correct relative\n// path.\nfunc TestDependencyOutputRegression854(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"regression-854\", \"root\")\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all apply --non-interactive --working-dir \"+rootPath+\n\t\t\t\" --filter '!{.}'\",\n\t)\n\trequire.NoError(t, err)\n}\n\n// Regression testing for bug where terragrunt output runs on dependency blocks are done in the terragrunt-cache for the\n// child, not the parent.\nfunc TestDependencyOutputCachePathBug(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"localstate\", \"live\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(\n\t\tt,\n\t\t\"terragrunt run --all apply --non-interactive --working-dir \"+rootPath,\n\t\t&stdout,\n\t\t&stderr,\n\t)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"stderr\")\n\trequire.NoError(t, err)\n}\n\nfunc TestDependencyOutputWithTerragruntSource(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"regression-1124\", \"live\")\n\tmodulePath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"regression-1124\", \"modules\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(\n\t\tt,\n\t\tfmt.Sprintf(\"terragrunt run --all apply --non-interactive --working-dir %s --source %s\", rootPath, modulePath),\n\t\t&stdout,\n\t\t&stderr,\n\t)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"stderr\")\n\trequire.NoError(t, err)\n}\n\nfunc TestRunAllWithSourceFlag(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureRunAllSource)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRunAllSource)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureRunAllSource, \"live\")\n\tmodulePath := filepath.Join(tmpEnvPath, testFixtureRunAllSource, \"modules-marked\")\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\"terragrunt run --all plan --non-interactive --working-dir %s --source %s\", rootPath, modulePath),\n\t)\n\trequire.NoError(t, err)\n\n\t// When we fail to update the unit source location to the download dir correctly, we get an error about no configuration\n\t// files being present.\n\tassert.NotContains(t, stderr, \"Error: No configuration files\")\n\n\tunit1Path := filepath.Join(rootPath, \"unit1\")\n\tunit2Path := filepath.Join(rootPath, \"unit2\")\n\n\t// Find the cache directories for each unit\n\tunit1CacheDir := filepath.Join(unit1Path, helpers.TerragruntCache)\n\tunit2CacheDir := filepath.Join(unit2Path, helpers.TerragruntCache)\n\n\tvar unit1MarkerPath, unit2MarkerPath string\n\n\twalkErr := filepath.WalkDir(unit1CacheDir, func(path string, d os.DirEntry, walkErr error) error {\n\t\tif walkErr != nil {\n\t\t\treturn walkErr\n\t\t}\n\n\t\tif d.Name() == \"MODULE1_MARKER\" {\n\t\t\tunit1MarkerPath = path\n\t\t}\n\n\t\treturn nil\n\t})\n\trequire.NoError(t, walkErr)\n\n\twalkErr = filepath.WalkDir(unit2CacheDir, func(path string, d os.DirEntry, walkErr error) error {\n\t\tif walkErr != nil {\n\t\t\treturn walkErr\n\t\t}\n\n\t\tif d.Name() == \"MODULE2_MARKER\" {\n\t\t\tunit2MarkerPath = path\n\t\t}\n\n\t\treturn nil\n\t})\n\trequire.NoError(t, walkErr)\n\n\tassert.NotEmpty(t, unit1MarkerPath)\n\tassert.NotEmpty(t, unit2MarkerPath)\n}\n\nfunc TestDependencyOutputWithHooks(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"regression-1273\")\n\tdepPath := filepath.Join(rootPath, \"dep\")\n\tmainPath := filepath.Join(rootPath, \"main\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// The file should exist in cache dir (default hook behavior).\n\tassert.True(t, helpers.FileExistsInCache(t, depPath, \"file.out\"))\n\tassert.False(t, helpers.FileExistsInCache(t, mainPath, \"file.out\"))\n\n\t// Now delete file and run plain main again. It should NOT create file.out.\n\tcacheDir := helpers.FindCacheWorkingDir(t, depPath)\n\trequire.NoError(t, os.Remove(filepath.Join(cacheDir, \"file.out\")))\n\thelpers.RunTerragrunt(t, \"terragrunt plan --non-interactive --working-dir \"+mainPath)\n\tassert.False(t, helpers.FileExistsInCache(t, depPath, \"file.out\"))\n\tassert.False(t, helpers.FileExistsInCache(t, mainPath, \"file.out\"))\n}\n\nfunc TestDeepDependencyOutputWithMock(t *testing.T) {\n\t// Test that the terraform command flows through for mock output retrieval to deeper dependencies. Previously the\n\t// terraform command was being overwritten, so by the time the deep dependency retrieval runs, it was replaced with\n\t// \"output\" instead of the original one.\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"nested-mocks\", \"live\")\n\n\t// Since we haven't applied anything, this should only succeed if mock outputs are used.\n\thelpers.RunTerragrunt(t, \"terragrunt validate --non-interactive --working-dir \"+rootPath)\n}\n\nfunc TestDataDir(t *testing.T) {\n\t// Cannot be run in parallel with other tests as it modifies process' environment.\n\thelpers.CleanupTerraformFolder(t, testFixtureDirsPath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDirsPath)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureDirsPath)\n\n\tt.Setenv(\"TF_DATA_DIR\", filepath.Join(tmpEnvPath, \"data_dir\"))\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --non-interactive --tf-forward-stdout --working-dir \"+rootPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout.String(), \"Initializing provider plugins\")\n\n\tstdout = bytes.Buffer{}\n\tstderr = bytes.Buffer{}\n\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt plan --non-interactive --tf-forward-stdout --working-dir \"+rootPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\tassert.NotContains(t, stdout.String(), \"Initializing provider plugins\")\n}\n\nfunc TestReadTerragruntConfigWithDependency(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureReadConfig)\n\thelpers.CleanupTerraformFolder(t, testFixtureInputs)\n\ttmpEnvPath := helpers.CopyEnvironment(t, \".\")\n\n\tinputsPath := filepath.Join(tmpEnvPath, testFixtureInputs)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureReadConfig, \"with_dependency\")\n\n\t// First apply the inputs module\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+inputsPath)\n\n\t// Then apply the read config module\n\tshowStdout := bytes.Buffer{}\n\tshowStderr := bytes.Buffer{}\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath, &showStdout, &showStderr),\n\t)\n\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr\")\n\n\t// Now check the outputs to make sure they are as expected\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\tassert.Equal(t, true, outputs[\"bool\"].Value)\n\tassert.Equal(t, []any{true, false}, outputs[\"list_bool\"].Value)\n\tassert.Equal(t, []any{1.0, 2.0, 3.0}, outputs[\"list_number\"].Value)\n\tassert.Equal(t, []any{\"a\", \"b\", \"c\"}, outputs[\"list_string\"].Value)\n\tassert.Equal(t, map[string]any{\"foo\": true, \"bar\": false, \"baz\": true}, outputs[\"map_bool\"].Value)\n\tassert.Equal(t, map[string]any{\"foo\": 42.0, \"bar\": 12345.0}, outputs[\"map_number\"].Value)\n\tassert.Equal(t, map[string]any{\"foo\": \"bar\"}, outputs[\"map_string\"].Value)\n\tassert.InEpsilon(t, 42.0, outputs[\"number\"].Value.(float64), 0.0000001)\n\tassert.Equal(t, map[string]any{\"list\": []any{1.0, 2.0, 3.0}, \"map\": map[string]any{\"foo\": \"bar\"}, \"num\": 42.0, \"str\": \"string\"}, outputs[\"object\"].Value)\n\tassert.Equal(t, \"string\", outputs[\"string\"].Value)\n\tassert.Equal(t, \"default\", outputs[\"from_env\"].Value)\n}\n\nfunc TestReadTerragruntConfigFromDependency(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureReadConfig)\n\ttmpEnvPath := helpers.CopyEnvironment(t, \".\")\n\trootPath := filepath.Join(tmpEnvPath, testFixtureReadConfig, \"from_dependency\")\n\n\tshowStdout := bytes.Buffer{}\n\tshowStderr := bytes.Buffer{}\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath, &showStdout, &showStderr),\n\t)\n\n\thelpers.LogBufferContentsLineByLine(t, showStdout, \"show stdout\")\n\thelpers.LogBufferContentsLineByLine(t, showStderr, \"show stderr\")\n\n\t// Now check the outputs to make sure they are as expected\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\tassert.Equal(t, \"hello world\", outputs[\"bar\"].Value)\n}\n\nfunc TestReadTerragruntConfigWithDefault(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureReadConfig)\n\thelpers.CleanupTerraformFolder(t, filepath.Join(tmpEnvPath, testFixtureReadConfig))\n\trootPath := filepath.Join(tmpEnvPath, testFixtureReadConfig, \"with_default\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\t// check the outputs to make sure they are as expected\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\tassert.Equal(t, \"default value\", outputs[\"data\"].Value)\n}\n\nfunc TestReadTerragruntConfigWithOriginalTerragruntDir(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureReadConfig)\n\thelpers.CleanupTerraformFolder(t, filepath.Join(tmpEnvPath, testFixtureReadConfig))\n\trootPath := filepath.Join(tmpEnvPath, testFixtureReadConfig, \"with_original_terragrunt_dir\")\n\n\trootPathAbs := filepath.Clean(rootPath)\n\n\tfooPathAbs := filepath.Join(rootPathAbs, \"foo\")\n\tdepPathAbs := filepath.Join(rootPathAbs, \"dep\")\n\n\t// Run apply on the dependency module and make sure we get the outputs we expect\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+depPathAbs)\n\n\tdepStdout := bytes.Buffer{}\n\tdepStderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+depPathAbs, &depStdout, &depStderr),\n\t)\n\n\tdepOutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(depStdout.Bytes(), &depOutputs))\n\n\tassert.Equal(t, depPathAbs, depOutputs[\"terragrunt_dir\"].Value)\n\tassert.Equal(t, depPathAbs, depOutputs[\"original_terragrunt_dir\"].Value)\n\tassert.Equal(t, fooPathAbs, depOutputs[\"bar_terragrunt_dir\"].Value)\n\tassert.Equal(t, depPathAbs, depOutputs[\"bar_original_terragrunt_dir\"].Value)\n\n\t// Run apply on the root module and make sure we get the expected outputs\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\trootStdout := bytes.Buffer{}\n\trootStderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &rootStdout, &rootStderr),\n\t)\n\n\trootOutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(rootStdout.Bytes(), &rootOutputs))\n\n\tassert.Equal(t, fooPathAbs, rootOutputs[\"terragrunt_dir\"].Value)\n\tassert.Equal(t, rootPathAbs, rootOutputs[\"original_terragrunt_dir\"].Value)\n\tassert.Equal(t, depPathAbs, rootOutputs[\"dep_terragrunt_dir\"].Value)\n\tassert.Equal(t, depPathAbs, rootOutputs[\"dep_original_terragrunt_dir\"].Value)\n\tassert.Equal(t, fooPathAbs, rootOutputs[\"dep_bar_terragrunt_dir\"].Value)\n\tassert.Equal(t, depPathAbs, rootOutputs[\"dep_bar_original_terragrunt_dir\"].Value)\n\n\t// Run 'run --all apply' and make sure all the outputs are identical in the root module and the dependency module\n\thelpers.RunTerragrunt(t, \"terragrunt run --all  --non-interactive --working-dir \"+rootPath+\" -- apply -auto-approve\")\n\n\trunAllRootStdout := bytes.Buffer{}\n\trunAllRootStderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &runAllRootStdout, &runAllRootStderr),\n\t)\n\n\trunAllRootOutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(runAllRootStdout.Bytes(), &runAllRootOutputs))\n\n\trunAllDepStdout := bytes.Buffer{}\n\trunAllDepStderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+depPathAbs, &runAllDepStdout, &runAllDepStderr),\n\t)\n\n\trunAllDepOutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(runAllDepStdout.Bytes(), &runAllDepOutputs))\n\n\tassert.Equal(t, fooPathAbs, runAllRootOutputs[\"terragrunt_dir\"].Value)\n\tassert.Equal(t, rootPathAbs, runAllRootOutputs[\"original_terragrunt_dir\"].Value)\n\tassert.Equal(t, depPathAbs, runAllRootOutputs[\"dep_terragrunt_dir\"].Value)\n\tassert.Equal(t, depPathAbs, runAllRootOutputs[\"dep_original_terragrunt_dir\"].Value)\n\tassert.Equal(t, fooPathAbs, runAllRootOutputs[\"dep_bar_terragrunt_dir\"].Value)\n\tassert.Equal(t, depPathAbs, runAllRootOutputs[\"dep_bar_original_terragrunt_dir\"].Value)\n\tassert.Equal(t, depPathAbs, runAllDepOutputs[\"terragrunt_dir\"].Value)\n\tassert.Equal(t, depPathAbs, runAllDepOutputs[\"original_terragrunt_dir\"].Value)\n\tassert.Equal(t, fooPathAbs, runAllDepOutputs[\"bar_terragrunt_dir\"].Value)\n\tassert.Equal(t, depPathAbs, runAllDepOutputs[\"bar_original_terragrunt_dir\"].Value)\n}\n\nfunc TestReadTerragruntConfigFull(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures\")\n\thelpers.CleanupTerraformFolder(t, filepath.Join(tmpEnvPath, testFixtureReadConfig))\n\trootPath := filepath.Join(tmpEnvPath, testFixtureReadConfig, \"full\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+rootPath)\n\n\t// check the outputs to make sure they are as expected\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\tassert.Equal(t, \"terragrunt\", outputs[\"terraform_binary\"].Value)\n\tassert.Equal(t, \"= 0.12.20\", outputs[\"terraform_version_constraint\"].Value)\n\tassert.Equal(t, \"= 0.23.18\", outputs[\"terragrunt_version_constraint\"].Value)\n\tassert.Equal(t, \".terragrunt-cache\", outputs[\"download_dir\"].Value)\n\tassert.Equal(t, \"TerragruntIAMRole\", outputs[\"iam_role\"].Value)\n\t// exclude is now a block, not a simple boolean - just verify it exists\n\tassert.Contains(t, outputs, \"exclude\")\n\tassert.NotEmpty(t, outputs[\"exclude\"].Value)\n\tassert.Equal(t, \"true\", outputs[\"prevent_destroy\"].Value)\n\n\t// Simple maps\n\tlocalstgOut := map[string]any{}\n\trequire.NoError(t, json.Unmarshal([]byte(outputs[\"localstg\"].Value.(string)), &localstgOut))\n\tassert.Equal(t, map[string]any{\"the_answer\": float64(42)}, localstgOut)\n\n\tinputsOut := map[string]any{}\n\trequire.NoError(t, json.Unmarshal([]byte(outputs[\"inputs\"].Value.(string)), &inputsOut))\n\tassert.Equal(t, map[string]any{\"doc\": \"Emmett Brown\"}, inputsOut)\n\n\t// Complex blocks\n\tdepsOut := map[string]any{}\n\trequire.NoError(t, json.Unmarshal([]byte(outputs[\"dependencies\"].Value.(string)), &depsOut))\n\tassert.Equal(\n\t\tt,\n\t\tmap[string]any{\n\t\t\t\"paths\": []any{\"../../terragrunt\"},\n\t\t},\n\t\tdepsOut,\n\t)\n\n\tgenerateOut := map[string]any{}\n\trequire.NoError(t, json.Unmarshal([]byte(outputs[\"generate\"].Value.(string)), &generateOut))\n\tassert.Equal(\n\t\tt,\n\t\tmap[string]any{\n\t\t\t\"provider\": map[string]any{\n\t\t\t\t\"path\":              \"provider.tf\",\n\t\t\t\t\"if_exists\":         \"overwrite_terragrunt\",\n\t\t\t\t\"hcl_fmt\":           nil,\n\t\t\t\t\"if_disabled\":       \"skip\",\n\t\t\t\t\"comment_prefix\":    \"# \",\n\t\t\t\t\"disable_signature\": false,\n\t\t\t\t\"disable\":           false,\n\t\t\t\t\"contents\": `provider \"aws\" {\n  region = \"us-east-1\"\n}\n`,\n\t\t\t},\n\t\t},\n\t\tgenerateOut,\n\t)\n\n\tremoteStateOut := map[string]any{}\n\trequire.NoError(t, json.Unmarshal([]byte(outputs[\"remote_state\"].Value.(string)), &remoteStateOut))\n\tassert.Equal(\n\t\tt,\n\t\tmap[string]any{\n\t\t\t\"backend\":                         \"local\",\n\t\t\t\"disable_init\":                    false,\n\t\t\t\"disable_dependency_optimization\": false,\n\t\t\t\"generate\":                        map[string]any{\"path\": \"backend.tf\", \"if_exists\": \"overwrite_terragrunt\"},\n\t\t\t\"config\":                          map[string]any{\"path\": \"foo.tfstate\"},\n\t\t\t\"encryption\":                      map[string]any{\"key_provider\": \"foo\"},\n\t\t},\n\t\tremoteStateOut,\n\t)\n\n\tterraformOut := map[string]any{}\n\trequire.NoError(t, json.Unmarshal([]byte(outputs[\"terraformtg\"].Value.(string)), &terraformOut))\n\tassert.Equal(\n\t\tt,\n\t\tmap[string]any{\n\t\t\t\"source\":                   \"./delorean\",\n\t\t\t\"include_in_copy\":          []any{\"time_machine.*\"},\n\t\t\t\"exclude_from_copy\":        []any{\"excluded_time_machine.*\"},\n\t\t\t\"copy_terraform_lock_file\": true,\n\t\t\t\"extra_arguments\": map[string]any{\n\t\t\t\t\"var-files\": map[string]any{\n\t\t\t\t\t\"name\":               \"var-files\",\n\t\t\t\t\t\"commands\":           []any{\"apply\", \"plan\"},\n\t\t\t\t\t\"arguments\":          nil,\n\t\t\t\t\t\"required_var_files\": []any{\"extra.tfvars\"},\n\t\t\t\t\t\"optional_var_files\": []any{\"optional.tfvars\"},\n\t\t\t\t\t\"env_vars\": map[string]any{\n\t\t\t\t\t\t\"TF_VAR_custom_var\": \"I'm set in extra_arguments env_vars\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"before_hook\": map[string]any{\n\t\t\t\t\"before_hook_1\": map[string]any{\n\t\t\t\t\t\"name\":            \"before_hook_1\",\n\t\t\t\t\t\"commands\":        []any{\"apply\", \"plan\"},\n\t\t\t\t\t\"execute\":         []any{\"touch\", \"before.out\"},\n\t\t\t\t\t\"working_dir\":     nil,\n\t\t\t\t\t\"run_on_error\":    true,\n\t\t\t\t\t\"if\":              nil,\n\t\t\t\t\t\"suppress_stdout\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"after_hook\": map[string]any{\n\t\t\t\t\"after_hook_1\": map[string]any{\n\t\t\t\t\t\"name\":            \"after_hook_1\",\n\t\t\t\t\t\"commands\":        []any{\"apply\", \"plan\"},\n\t\t\t\t\t\"execute\":         []any{\"touch\", \"after.out\"},\n\t\t\t\t\t\"working_dir\":     nil,\n\t\t\t\t\t\"run_on_error\":    true,\n\t\t\t\t\t\"if\":              nil,\n\t\t\t\t\t\"suppress_stdout\": nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"error_hook\": map[string]any{},\n\t\t},\n\t\tterraformOut,\n\t)\n}\n\nfunc TestTerragruntGenerateBlockSkipRemove(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"remove-file\", \"skip\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+generateTestCase)\n\tassert.FileExists(t, filepath.Join(generateTestCase, \"backend.tf\"))\n}\n\nfunc TestTerragruntGenerateBlockRemove(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"remove-file\", \"remove\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+generateTestCase)\n\t// With cache always used, the generate block removes files from the cache directory\n\tassert.False(t, helpers.FileExistsInCache(t, generateTestCase, \"backend.tf\"))\n}\n\nfunc TestTerragruntGenerateBlockRemoveTerragruntSuccess(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"remove-file\", \"remove_terragrunt\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+generateTestCase)\n\t// With cache always used, the generate block removes files from the cache directory\n\tassert.False(t, helpers.FileExistsInCache(t, generateTestCase, \"backend.tf\"))\n}\n\nfunc TestTerragruntGenerateBlockRemoveTerragruntFail(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"remove-file\", \"remove_terragrunt_error\")\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+generateTestCase)\n\trequire.Error(t, err)\n\n\tvar generateFileRemoveError codegen.GenerateFileRemoveError\n\n\tok := errors.As(err, &generateFileRemoveError)\n\tassert.True(t, ok)\n\n\tassert.FileExists(t, filepath.Join(generateTestCase, \"backend.tf\"))\n}\n\nfunc TestTerragruntGenerateBlockSkip(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"generate-block\", \"skip\")\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+generateTestCase)\n\tassert.False(t, helpers.FileIsInFolder(t, \"foo.tfstate\", generateTestCase))\n}\n\nfunc TestTerragruntGenerateBlockOverwrite(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"generate-block\", \"overwrite\")\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+generateTestCase)\n\t// If the state file was written as foo.tfstate, that means it overwrote the local backend config.\n\tassert.True(t, helpers.FileIsInFolder(t, \"foo.tfstate\", generateTestCase))\n\tassert.False(t, helpers.FileIsInFolder(t, \"bar.tfstate\", generateTestCase))\n}\n\nfunc TestTerragruntGenerateAttr(t *testing.T) {\n\tt.Parallel()\n\n\tgenerateTestCase := filepath.Join(testFixtureCodegenPath, \"generate-attr\")\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\ttext := \"test-terragrunt-generate-attr-hello-world\"\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --tf-forward-stdout --working-dir %s -var text=\\\"%s\\\"\", generateTestCase, text))\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, text)\n}\n\nfunc TestTerragruntGenerateBlockOverwriteTerragruntSuccess(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"generate-block\", \"overwrite_terragrunt\")\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+generateTestCase)\n\t// If the state file was written as foo.tfstate, that means it overwrote the local backend config.\n\tassert.True(t, helpers.FileIsInFolder(t, \"foo.tfstate\", generateTestCase))\n\tassert.False(t, helpers.FileIsInFolder(t, \"bar.tfstate\", generateTestCase))\n}\n\nfunc TestTerragruntGenerateBlockOverwriteTerragruntFail(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"generate-block\", \"overwrite_terragrunt_error\")\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+generateTestCase, &stdout, &stderr)\n\trequire.Error(t, err)\n\n\tvar generateFileExistsError codegen.GenerateFileExistsError\n\n\tok := errors.As(err, &generateFileExistsError)\n\tassert.True(t, ok)\n}\n\nfunc TestTerragruntGenerateBlockNestedInherit(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"generate-block\", \"nested\", \"child_inherit\")\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+generateTestCase)\n\t// If the state file was written as foo.tfstate, that means it inherited the config\n\tassert.True(t, helpers.FileIsInFolder(t, \"foo.tfstate\", generateTestCase))\n\tassert.False(t, helpers.FileIsInFolder(t, \"bar.tfstate\", generateTestCase))\n\t// Also check to make sure the child config generate block was included\n\tassert.True(t, helpers.FileIsInFolder(t, \"random_file.txt\", generateTestCase))\n}\n\nfunc TestTerragruntGenerateBlockNestedOverwrite(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"generate-block\", \"nested\", \"child_overwrite\")\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+generateTestCase)\n\t// If the state file was written as bar.tfstate, that means it overwrite the parent config\n\tassert.False(t, helpers.FileIsInFolder(t, \"foo.tfstate\", generateTestCase))\n\tassert.True(t, helpers.FileIsInFolder(t, \"bar.tfstate\", generateTestCase))\n\t// Also check to make sure the child config generate block was included\n\tassert.True(t, helpers.FileIsInFolder(t, \"random_file.txt\", generateTestCase))\n}\n\nfunc TestTerragruntGenerateBlockDisableSignature(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"generate-block\", \"disable-signature\")\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+generateTestCase)\n\n\t// Now check the outputs to make sure they are as expected\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+generateTestCase, &stdout, &stderr),\n\t)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\n\tassert.Equal(t, \"Hello, World!\", outputs[\"text\"].Value)\n}\n\nfunc TestTerragruntGenerateBlockSameNameFail(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"generate-block\", \"same_name_error\")\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt init --working-dir \"+generateTestCase, &stdout, &stderr)\n\trequire.Error(t, err)\n\n\tvar parsedError config.DuplicatedGenerateBlocksError\n\n\tok := errors.As(err, &parsedError)\n\tassert.True(t, ok)\n\tassert.Len(t, parsedError.BlockName, 1)\n\tassert.Contains(t, parsedError.BlockName, \"backend\")\n}\n\nfunc TestTerragruntGenerateBlockSameNameIncludeFail(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"generate-block\", \"same_name_includes_error\")\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt init --working-dir \"+generateTestCase, &stdout, &stderr)\n\trequire.Error(t, err)\n\n\tvar parsedError config.DuplicatedGenerateBlocksError\n\n\tok := errors.As(err, &parsedError)\n\tassert.True(t, ok)\n\tassert.Len(t, parsedError.BlockName, 1)\n\tassert.Contains(t, parsedError.BlockName, \"backend\")\n}\n\nfunc TestTerragruntGenerateBlockMultipleSameNameFail(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"generate-block\", \"same_name_pair_error\")\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt init --working-dir \"+generateTestCase, &stdout, &stderr)\n\trequire.Error(t, err)\n\n\tvar parsedError config.DuplicatedGenerateBlocksError\n\n\tok := errors.As(err, &parsedError)\n\tassert.True(t, ok)\n\tassert.Len(t, parsedError.BlockName, 2)\n\tassert.Contains(t, parsedError.BlockName, \"backend\")\n\tassert.Contains(t, parsedError.BlockName, \"backend2\")\n}\n\nfunc TestTerragruntGenerateBlockDisable(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"generate-block\", \"disable\")\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt init --working-dir \"+generateTestCase, &stdout, &stderr)\n\trequire.NoError(t, err)\n\tassert.False(t, helpers.FileIsInFolder(t, \"data.txt\", generateTestCase))\n}\n\nfunc TestTerragruntGenerateBlockEnable(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"generate-block\", \"enable\")\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt init --working-dir \"+generateTestCase, &stdout, &stderr)\n\trequire.NoError(t, err)\n\tassert.True(t, helpers.FileIsInFolder(t, \"data.txt\", generateTestCase))\n}\n\nfunc TestTerragruntRemoteStateCodegenGeneratesBackendBlock(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"remote-state\", \"base\")\n\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+generateTestCase)\n\t// If the state file was written as foo.tfstate, that means it wrote out the local backend config.\n\tassert.True(t, helpers.FileIsInFolder(t, \"foo.tfstate\", generateTestCase))\n}\n\nfunc TestTerragruntRemoteStateCodegenOverwrites(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"remote-state\", \"overwrite\")\n\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+generateTestCase)\n\t// If the state file was written as foo.tfstate, that means it overwrote the local backend config.\n\tassert.True(t, helpers.FileIsInFolder(t, \"foo.tfstate\", generateTestCase))\n\tassert.False(t, helpers.FileIsInFolder(t, \"bar.tfstate\", generateTestCase))\n}\n\nfunc TestTerragruntRemoteStateCodegenErrorsIfExists(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"remote-state\", \"error\")\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+generateTestCase, &stdout, &stderr)\n\trequire.Error(t, err)\n\n\tvar generateFileExistsError codegen.GenerateFileExistsError\n\n\tok := errors.As(err, &generateFileExistsError)\n\tassert.True(t, ok)\n}\n\nfunc TestTerragruntRemoteStateCodegenDoesNotGenerateWithSkip(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCodegenPath)\n\tgenerateTestCase := filepath.Join(tmpEnvPath, testFixtureCodegenPath, \"remote-state\", \"skip\")\n\n\thelpers.CleanupTerraformFolder(t, generateTestCase)\n\thelpers.CleanupTerragruntFolder(t, generateTestCase)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+generateTestCase)\n\tassert.False(t, helpers.FileIsInFolder(t, \"foo.tfstate\", generateTestCase))\n}\n\n// This function cannot be parallelized as it changes the global version.Version\n//\n//nolint:paralleltest\nfunc TestTerragruntValidateAllWithVersionChecks(t *testing.T) {\n\ttmpEnvPath := helpers.CopyEnvironment(t, \"fixtures/version-check\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntVersionCommand(t, \"v0.23.21\", \"terragrunt run --all validate --non-interactive --working-dir \"+tmpEnvPath, &stdout, &stderr)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"stderr\")\n\trequire.NoError(t, err)\n}\n\nfunc TestTerragruntIncludeParentHclFile(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureIncludeParent)\n\ttmpEnvPath = path.Join(tmpEnvPath, testFixtureIncludeParent)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --log-level debug --all apply --non-interactive --working-dir \"+tmpEnvPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"parent_hcl_file\")\n\tassert.Contains(t, stderr, \"dependency_hcl\")\n\tassert.Contains(t, stderr, \"common_hcl\")\n}\n\n// The tests here cannot be parallelized.\n// This is due to a race condition brought about by overriding `version.Version` in\n// runTerragruntVersionCommand\n//\n//nolint:paralleltest,funlen\nfunc TestTerragruntVersionConstraints(t *testing.T) {\n\ttestCases := []struct {\n\t\tname                 string\n\t\tterragruntVersion    string\n\t\tterragruntConstraint string\n\t\tshouldSucceed        bool\n\t}{\n\t\t{\n\t\t\t\"version meets constraint equal\",\n\t\t\t\"v0.23.18\",\n\t\t\t\"terragrunt_version_constraint = \\\">= v0.23.18\\\"\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"version meets constraint greater patch\",\n\t\t\t\"v0.23.19\",\n\t\t\t\"terragrunt_version_constraint = \\\">= v0.23.18\\\"\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"version meets constraint greater major\",\n\t\t\t\"v1.0.0\",\n\t\t\t\"terragrunt_version_constraint = \\\">= v0.23.18\\\"\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"version fails constraint less patch\",\n\t\t\t\"v0.23.17\",\n\t\t\t\"terragrunt_version_constraint = \\\">= v0.23.18\\\"\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"version fails constraint less major\",\n\t\t\t\"v0.22.18\",\n\t\t\t\"terragrunt_version_constraint = \\\">= v0.23.18\\\"\",\n\t\t\tfalse,\n\t\t},\n\t\t{\n\t\t\t\"version meets constraint pre-release\",\n\t\t\t\"v0.23.18-alpha2024091301\",\n\t\t\t\"terragrunt_version_constraint = \\\">= v0.23.18\\\"\",\n\t\t\ttrue,\n\t\t},\n\t\t{\n\t\t\t\"version fails constraint pre-release\",\n\t\t\t\"v0.23.18-alpha2024091301\",\n\t\t\t\"terragrunt_version_constraint = \\\"< v0.23.18\\\"\",\n\t\t\tfalse,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases { //nolint:paralleltest\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureReadConfig)\n\t\t\trootPath := filepath.Join(tmpEnvPath, testFixtureReadConfig, \"with_constraints\")\n\n\t\t\ttmpTerragruntConfigPath := helpers.CreateTmpTerragruntConfigContent(t, tc.terragruntConstraint, config.DefaultTerragruntConfigPath)\n\n\t\t\tstdout := bytes.Buffer{}\n\t\t\tstderr := bytes.Buffer{}\n\n\t\t\terr := helpers.RunTerragruntVersionCommand(\n\t\t\t\tt,\n\t\t\t\ttc.terragruntVersion,\n\t\t\t\tfmt.Sprintf(\n\t\t\t\t\t\"terragrunt apply -auto-approve --non-interactive --config %s --working-dir %s\",\n\t\t\t\t\ttmpTerragruntConfigPath,\n\t\t\t\t\trootPath,\n\t\t\t\t),\n\t\t\t\t&stdout,\n\t\t\t\t&stderr,\n\t\t\t)\n\n\t\t\thelpers.LogBufferContentsLineByLine(t, stdout, \"stdout\")\n\t\t\thelpers.LogBufferContentsLineByLine(t, stderr, \"stderr\")\n\n\t\t\tif tc.shouldSucceed {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestReadTerragruntAuthProviderCmd(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureAuthProviderCmd)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureAuthProviderCmd)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureAuthProviderCmd, \"multiple-apps\")\n\tappPath := filepath.Join(rootPath, \"app1\")\n\tmockAuthCmd := filepath.Join(tmpEnvPath, testFixtureAuthProviderCmd, \"mock-auth-cmd.sh\")\n\n\thelpers.ValidateAuthProviderScript(t, rootPath, mockAuthCmd)\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t`terragrunt run --all --non-interactive --working-dir %s --auth-provider-cmd %s -- apply -auto-approve`,\n\t\t\trootPath,\n\t\t\tmockAuthCmd,\n\t\t),\n\t)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt output -json --working-dir %s --auth-provider-cmd %s\", appPath, mockAuthCmd))\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal([]byte(stdout), &outputs))\n\n\tassert.Equal(t, \"app1-bar\", outputs[\"foo-app1\"].Value)\n\tassert.Equal(t, \"app2-bar\", outputs[\"foo-app2\"].Value)\n\tassert.Equal(t, \"app3-bar\", outputs[\"foo-app3\"].Value)\n}\n\nfunc TestIamRolesLoadingFromDifferentModules(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureIamRolesMultipleModules)\n\n\t// Invoke terragrunt and verify used IAM roles for each dependency\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt init --debugreset --log-level debug --working-dir \"+testFixtureIamRolesMultipleModules,\n\t)\n\n\t// Taking all outputs in one string\n\toutput := fmt.Sprintf(\"%v %v %v\", stderr, stdout, err.Error())\n\n\tcomponent1 := \"\"\n\tcomponent2 := \"\"\n\n\t// scan each output line and get lines for component1 and component2\n\tfor line := range strings.SplitSeq(output, \"\\n\") {\n\t\tif strings.Contains(line, \"Assuming IAM role arn:aws:iam::component1:role/terragrunt\") {\n\t\t\tcomponent1 = line\n\t\t\tcontinue\n\t\t}\n\n\t\tif strings.Contains(line, \"Assuming IAM role arn:aws:iam::component2:role/terragrunt\") {\n\t\t\tcomponent2 = line\n\t\t\tcontinue\n\t\t}\n\t}\n\n\tassert.NotEmptyf(t, component1, \"Missing role for component 1\")\n\tassert.NotEmptyf(t, component2, \"Missing role for component 2\")\n}\n\n// This function cannot be parallelized as it changes the global version.Version\n//\n//nolint:paralleltest\nfunc TestTerragruntVersionConstraintsPartialParse(t *testing.T) {\n\tfixturePath := \"fixtures/partial-parse/terragrunt-version-constraint\"\n\thelpers.CleanupTerragruntFolder(t, fixturePath)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntVersionCommand(t, \"0.21.23\", \"terragrunt apply -auto-approve --non-interactive --working-dir \"+fixturePath, &stdout, &stderr)\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"stdout\")\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"stderr\")\n\n\trequire.Error(t, err)\n\n\tvar invalidVersionError run.InvalidTerragruntVersion\n\n\tok := errors.As(err, &invalidVersionError)\n\tassert.True(t, ok)\n}\n\nfunc TestLogFailingDependencies(t *testing.T) {\n\tt.Parallel()\n\n\tpath := filepath.Join(testFixtureBrokenDependency, \"app\")\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --working-dir %s --log-level trace\", path))\n\trequire.Error(t, err)\n\n\t// Check that the error output contains terraform/tofu error details\n\tassert.Contains(t, stderr, \"Getting output of dependency ../dependency/terragrunt.hcl\")\n\tassert.Contains(t, stderr, \"Error: Failed to download module\")\n}\n\nfunc TestDependencyInputsBlockedByDefault(t *testing.T) {\n\tt.Parallel()\n\n\t// Test that using dependency.foo.inputs syntax results in an error by default\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\t// Create a terragrunt.hcl that uses the deprecated dependency.foo.inputs syntax\n\tdependencyConfig := `\ndependency \"dep\" {\n  config_path = \"../dep\"\n}\n\ninputs = {\n  # This should fail - dependency inputs are now blocked by default\n  value = dependency.dep.inputs.some_value\n}\n`\n\n\tconfigPath := filepath.Join(tmpDir, \"terragrunt.hcl\")\n\trequire.NoError(t, os.WriteFile(configPath, []byte(dependencyConfig), 0644))\n\n\tvar (\n\t\tstdout bytes.Buffer\n\t\tstderr bytes.Buffer\n\t)\n\n\t// Try to parse this config - it should fail with an error about dependency inputs\n\terr := helpers.RunTerragruntCommand(\n\t\tt,\n\t\t\"terragrunt validate --non-interactive --working-dir \"+tmpDir,\n\t\t&stdout,\n\t\t&stderr,\n\t)\n\n\trequire.Error(t, err)\n\tassert.Contains(t, err.Error(), \"Reading inputs from dependencies is no longer supported\")\n\tassert.Contains(t, err.Error(), \"use outputs\")\n}\n\nfunc TestDependenciesOptimisation(t *testing.T) {\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDependenciesOptimisation)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureDependenciesOptimisation)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- apply -auto-approve\")\n\trequire.NoError(t, err)\n\n\tassert.NotContains( // Check that we're getting a warning for usage of deprecated functionality.\n\t\tt,\n\t\tstderr,\n\t\t\"Reading inputs from dependencies has been deprecated and will be removed in a future version of Terragrunt. If a value in a dependency is needed, use dependency outputs instead.\",\n\t)\n\n\tmoduleC := filepath.Join(tmpEnvPath, testFixtureDependenciesOptimisation, \"module-c\")\n\n\tt.Setenv(\"TERRAGRUNT_STRICT_CONTROL\", \"skip-dependencies-inputs\")\n\t_, stderr, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+moduleC)\n\trequire.NoError(t, err)\n\n\t// checking that dependencies optimisation is working and outputs from module-a are not retrieved\n\tassert.NotContains(t, stderr, \"Retrieved output from ../module-a/terragrunt.hcl\")\n}\n\nfunc cleanupTerraformFolder(t *testing.T, templatesPath string) {\n\tt.Helper()\n\n\tremoveFile(t, filepath.Join(templatesPath, terraformState))\n\tremoveFile(t, filepath.Join(templatesPath, terraformStateBackup))\n\tremoveFolder(t, filepath.Join(templatesPath, terraformFolder))\n}\n\nfunc removeFile(t *testing.T, path string) {\n\tt.Helper()\n\n\tif util.FileExists(path) {\n\t\tif err := os.Remove(path); err != nil {\n\t\t\tt.Fatalf(\"Error while removing %s: %v\", path, err)\n\t\t}\n\t}\n}\n\nfunc removeFolder(t *testing.T, path string) {\n\tt.Helper()\n\n\tif util.FileExists(path) {\n\t\tif err := os.RemoveAll(path); err != nil {\n\t\t\tt.Fatalf(\"Error while removing %s: %v\", path, err)\n\t\t}\n\t}\n}\n\nfunc TestShowErrorWhenRunAllInvokedWithoutArguments(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureStack)\n\tappPath := filepath.Join(tmpEnvPath, testFixtureStack)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(\n\t\tt,\n\t\t\"terragrunt run --all --non-interactive --working-dir \"+appPath,\n\t\t&stdout,\n\t\t&stderr,\n\t)\n\trequire.Error(t, err)\n\n\tvar missingCommandError runall.MissingCommand\n\n\tok := errors.As(err, &missingCommandError)\n\tassert.True(t, ok)\n}\n\nfunc TestNoMultipleInitsWithoutSourceChange(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDownload)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureStdout)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --non-interactive --tf-forward-stdout --working-dir \"+testPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\t// providers initialization during first plan\n\tassert.Equal(t, 1, strings.Count(stdout.String(), \"has been successfully initialized!\"))\n\n\tstdout = bytes.Buffer{}\n\tstderr = bytes.Buffer{}\n\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt plan --non-interactive --tf-forward-stdout --working-dir \"+testPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\t// no initialization expected for second plan run\n\t// https://github.com/gruntwork-io/terragrunt/issues/1921\n\tassert.Equal(t, 0, strings.Count(stdout.String(), \"has been successfully initialized!\"))\n}\n\nfunc TestAutoInitWhenSourceIsChanged(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDownload)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureAutoInit)\n\n\tterragruntHcl := filepath.Join(testPath, \"terragrunt.hcl\")\n\n\tcontents, err := util.ReadFileAsString(terragruntHcl)\n\tif err != nil {\n\t\trequire.NoError(t, err)\n\t}\n\n\tupdatedHcl := strings.ReplaceAll(contents, \"__TAG_VALUE__\", \"v0.78.4\")\n\trequire.NoError(t, os.WriteFile(terragruntHcl, []byte(updatedHcl), 0444))\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt plan --non-interactive --tf-forward-stdout --working-dir \"+testPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\t// providers initialization during first plan\n\tassert.Equal(t, 1, strings.Count(stdout.String(), \"has been successfully initialized!\"))\n\n\tupdatedHcl = strings.ReplaceAll(contents, \"__TAG_VALUE__\", \"v0.79.0\")\n\trequire.NoError(t, os.WriteFile(terragruntHcl, []byte(updatedHcl), 0444))\n\n\tstdout = bytes.Buffer{}\n\tstderr = bytes.Buffer{}\n\n\terr = helpers.RunTerragruntCommand(t, \"terragrunt plan --non-interactive --tf-forward-stdout --working-dir \"+testPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\t// auto initialization when source is changed\n\tassert.Equal(t, 1, strings.Count(stdout.String(), \"has been successfully initialized!\"))\n}\n\nfunc TestNoColor(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureNoColor)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureNoColor)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan -no-color --tf-forward-stdout --working-dir \"+testPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\t// providers initialization during first plan\n\tassert.Equal(t, 1, strings.Count(stdout.String(), \"has been successfully initialized!\"))\n\n\tassert.NotContains(t, stdout.String(), \"\\x1b\")\n}\n\nfunc TestTerragruntValidateModulePrefix(t *testing.T) {\n\tt.Parallel()\n\n\tfixturePath := testFixtureIncludeParent\n\thelpers.CleanupTerraformFolder(t, fixturePath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, fixturePath)\n\trootPath := filepath.Join(tmpEnvPath, fixturePath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all validate --tf-forward-stdout --non-interactive --working-dir \"+rootPath)\n}\n\nfunc TestInitFailureModulePrefix(t *testing.T) {\n\tt.Parallel()\n\n\tinitTestCase := testFixtureInitError\n\n\thelpers.CleanupTerraformFolder(t, initTestCase)\n\thelpers.CleanupTerragruntFolder(t, initTestCase)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\trequire.Error(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt init -no-color --non-interactive --working-dir \"+initTestCase, &stdout, &stderr),\n\t)\n\thelpers.LogBufferContentsLineByLine(t, stderr, \"init\")\n\t// Check for terraform error in structured log format\n\tassert.Contains(t, stderr.String(), \"level=stderr\")\n}\n\nfunc TestDependencyOutputModulePrefix(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureGetOutput)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureGetOutput)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureGetOutput, \"integration\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all apply --non-interactive --working-dir \"+rootPath)\n\n\t// verify expected output 42\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\tapp3Path := filepath.Join(rootPath, \"app3\")\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+app3Path, &stdout, &stderr),\n\t)\n\t// validate that output is valid json\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &outputs))\n\tassert.Equal(t, 42, int(outputs[\"z\"].Value.(float64)))\n}\n\nfunc TestExplainingMissingCredentials(t *testing.T) {\n\t// no parallel because we need to set env vars\n\tt.Setenv(\"AWS_SHARED_CREDENTIALS_FILE\", \"/tmp/not-existing-creds-46521694\")\n\tt.Setenv(\"AWS_ACCESS_KEY_ID\", \"\")\n\tt.Setenv(\"AWS_SECRET_ACCESS_KEY\", \"\")\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureInitError)\n\tinitTestCase := filepath.Join(tmpEnvPath, testFixtureInitError)\n\n\thelpers.CleanupTerraformFolder(t, initTestCase)\n\thelpers.CleanupTerragruntFolder(t, initTestCase)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(\n\t\tt,\n\t\t\"terragrunt init -no-color --tf-forward-stdout --non-interactive --working-dir \"+initTestCase,\n\t\t&stdout,\n\t\t&stderr,\n\t)\n\texplanation := shell.ExplainError(err)\n\tassert.Contains(t, explanation, \"Missing AWS credentials\")\n}\n\nfunc TestModulePathInPlanErrorMessage(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureModulePathError)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureModulePathError, \"app\")\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt plan -no-color --non-interactive --working-dir \"+rootPath,\n\t)\n\trequire.Error(t, err)\n\toutput := stdout + \"\\n\" + stderr + \"\\n\" + err.Error() + \"\\n\"\n\n\tassert.Contains(t, output, \"error occurred\")\n}\n\nfunc TestModulePathInRunAllPlanErrorMessage(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureModulePathError)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureModulePathError)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all --non-interactive --working-dir \"+rootPath+\" -- plan -no-color\",\n\t)\n\trequire.Error(t, err)\n\n\toutput := fmt.Sprintf(\"%s\\n%s\\n\", stdout, stderr)\n\t// catch \"Run failed\" message printed in case of error in apply of units\n\tassert.Contains(t, output, \"Run failed\")\n\tassert.Contains(t, output, \"Unit d1\", output)\n}\n\nfunc TestHclFmtDiff(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHclfmtDiff)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHclfmtDiff)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHclfmtDiff)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\trequire.NoError(\n\t\tt,\n\t\thelpers.RunTerragruntCommand(t, \"terragrunt hcl fmt --diff --working-dir \"+rootPath, &stdout, &stderr),\n\t)\n\n\toutput := stdout.String()\n\n\texpectedDiff, err := os.ReadFile(filepath.Join(rootPath, \"expected.diff\"))\n\trequire.NoError(t, err)\n\n\thelpers.LogBufferContentsLineByLine(t, stdout, \"output\")\n\tassert.Contains(t, output, string(expectedDiff))\n}\n\nfunc TestHclFmtStdin(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureHclfmtStdin)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureHclfmtStdin)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureHclfmtStdin)\n\n\tos.Stdin, _ = os.Open(filepath.Join(rootPath, \"terragrunt.hcl\"))\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt hcl fmt --stdin\")\n\trequire.NoError(t, err)\n\n\texpectedDiff, err := os.ReadFile(filepath.Join(rootPath, \"expected.hcl\"))\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, string(expectedDiff))\n}\n\nfunc TestInitSkipCache(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureInitCache)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureInitCache)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureInitCache, \"app\")\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt plan --non-interactive --log-level debug --tf-forward-stdout --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\t// verify that init was invoked\n\tassert.Contains(t, stdout, \"has been successfully initialized!\")\n\tassert.Contains(t, stderr, \"Running command: \"+wrappedBinary()+\" init\")\n\n\tstdout, stderr, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt, \"terragrunt plan --non-interactive --log-level debug --tf-forward-stdout --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\t// verify that init wasn't invoked second time since cache directories are ignored\n\tassert.NotContains(t, stdout, \"has been successfully initialized!\")\n\tassert.NotContains(t, stderr, \"Running command: \"+wrappedBinary()+\" init\")\n\n\t// verify that after adding new file, init is executed\n\ttfFile := filepath.Join(tmpEnvPath, testFixtureInitCache, \"app\", \"project.tf\")\n\tif err := os.WriteFile(tfFile, []byte(\"\"), 0644); err != nil {\n\t\tt.Fatalf(\"Error writing new Terraform file to %s: %v\", tfFile, err)\n\t}\n\n\tstdout, stderr, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt plan --non-interactive --log-level debug --tf-forward-stdout --working-dir \"+rootPath,\n\t)\n\trequire.NoError(t, err)\n\n\t// verify that init was invoked\n\tassert.Contains(t, stdout, \"has been successfully initialized!\")\n\tassert.Contains(t, stderr, \"Running command: \"+wrappedBinary()+\" init\")\n}\n\nfunc TestTerragruntFailIfBucketCreationIsrequired(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, testFixturePath)\n\thelpers.CleanupTerraformFolder(t, rootPath)\n\n\ts3BucketName := \"terragrunt-test-bucket-\" + strings.ToLower(helpers.UniqueID())\n\tlockTableName := \"terragrunt-test-locks-\" + strings.ToLower(helpers.UniqueID())\n\n\ttmpTerragruntConfigPath := helpers.CreateTmpTerragruntConfig(t, rootPath, s3BucketName, lockTableName, config.DefaultTerragruntConfigPath)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\terr := helpers.RunTerragruntCommand(t, fmt.Sprintf(\"terragrunt apply --fail-on-state-bucket-creation --non-interactive --config %s --working-dir %s\", tmpTerragruntConfigPath, rootPath), &stdout, &stderr)\n\trequire.Error(t, err)\n}\n\nfunc TestTerragruntNoWarningLocalPath(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDisabledPath)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureDisabledPath)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt apply --non-interactive --working-dir \"+testPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\tassert.NotContains(t, stderr.String(), \"No double-slash (//) found in source URL\")\n}\n\nfunc TestTerragruntDisabledDependency(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDisabledModule)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureDisabledModule, \"app\")\n\n\t_, output, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all plan --non-interactive --working-dir \"+testPath)\n\trequire.NoError(t, err)\n\n\t// check that only enabled dependencies are evaluated\n\n\tfor _, path := range []string{\n\t\tfilepath.Join(tmpEnvPath, testFixtureDisabledModule, \"app\"),\n\t\tfilepath.Join(tmpEnvPath, testFixtureDisabledModule, \"unit-without-enabled\"),\n\t\tfilepath.Join(tmpEnvPath, testFixtureDisabledModule, \"unit-enabled\"),\n\t} {\n\t\trelPath, err := filepath.Rel(testPath, path)\n\t\trequire.NoError(t, err)\n\t\tassert.Contains(t, output, relPath, output)\n\t}\n\n\tfor _, path := range []string{\n\t\tfilepath.Join(tmpEnvPath, testFixtureDisabledModule, \"unit-disabled\"),\n\t} {\n\t\trelPath, err := filepath.Rel(testPath, path)\n\t\trequire.NoError(t, err)\n\t\tassert.NotContains(t, output, \"- Unit \"+relPath, output)\n\t}\n}\n\nfunc TestTerragruntHandleEmptyStateFile(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureEmptyState)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureEmptyState)\n\n\thelpers.CreateEmptyStateFile(t, testPath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+testPath)\n}\n\nfunc TestTerragruntCommandsThatNeedInput(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureCommandsThatNeedInput)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureCommandsThatNeedInput)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply --non-interactive --tf-forward-stdout --working-dir \"+testPath)\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"Apply complete\")\n}\n\nfunc TestTerragruntSkipDependenciesWithSkipFlag(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureSkipDependencies)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureSkipDependencies)\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt run --all apply --no-color --non-interactive --working-dir \"+testPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\toutput := fmt.Sprintf(\"%s %s\", stderr.String(), stdout.String())\n\n\tassert.NotContains(t, output, \"Error reading partial config for dependency\")\n\tassert.NotContains(t, output, \"Call to function \\\"find_in_parent_folders\\\" failed\")\n\tassert.NotContains(t, output, \"ParentFileNotFoundError\")\n\n\t// Check that units were excluded at stack level (shown in Run Summary)\n\tassert.Contains(t, output, \"Excluded\")\n\t// check that no test_file.txt was created in module directory\n\t_, err = os.Stat(filepath.Join(tmpEnvPath, testFixtureSkipDependencies, \"first\", \"test_file.txt\"))\n\trequire.Error(t, err)\n\t_, err = os.Stat(filepath.Join(tmpEnvPath, testFixtureSkipDependencies, \"second\", \"test_file.txt\"))\n\trequire.Error(t, err)\n}\n\nfunc TestTerragruntInfoError(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureInfoError)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureInfoError, \"module-b\")\n\n\tstdout := bytes.Buffer{}\n\tstderr := bytes.Buffer{}\n\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt info print --non-interactive --working-dir \"+testPath, &stdout, &stderr)\n\trequire.NoError(t, err)\n\n\t// parse stdout json as InfoOutput\n\tvar output print.InfoOutput\n\n\terr = json.Unmarshal(stdout.Bytes(), &output)\n\trequire.NoError(t, err)\n}\n\nfunc TestStorePlanFilesRunAllPlanApply(t *testing.T) {\n\tt.Parallel()\n\n\t// create temporary directory for plan files\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutDir)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureOutDir)\n\tdependencyPath := filepath.Join(tmpEnvPath, testFixtureOutDir, \"dependency\")\n\n\thelpers.RunTerragrunt(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt apply -auto-approve --non-interactive --working-dir %s --out-dir %s\",\n\t\t\tdependencyPath,\n\t\t\ttmpDir,\n\t\t),\n\t)\n\n\t// run plan with output directory\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all plan --non-interactive --working-dir %s --out-dir %s\",\n\t\t\ttestPath,\n\t\t\ttmpDir,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n\n\t// verify that tfplan files are created in the tmpDir, 2 files\n\tlist, err := findFilesWithExtension(tmpDir, \".tfplan\")\n\trequire.NoError(t, err)\n\tassert.Len(t, list, 2)\n\n\tfor _, file := range list {\n\t\tassert.Equal(t, \"tfplan.tfplan\", filepath.Base(file))\n\t}\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all apply --non-interactive --working-dir %s --out-dir %s\",\n\t\t\ttestPath,\n\t\t\ttmpDir,\n\t\t),\n\t)\n\trequire.NoError(t, err)\n}\n\nfunc TestStorePlanFilesRunAllPlanApplyRelativePath(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutDir)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureOutDir)\n\n\tdependencyPath := filepath.Join(tmpEnvPath, testFixtureOutDir, \"dependency\")\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --working-dir %s --out-dir %s\", dependencyPath, testPath))\n\n\t// run plan with output directory\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt run --all plan --non-interactive --working-dir %s --out-dir %s\", testPath, \"test\"))\n\trequire.NoError(t, err)\n\n\toutDir := filepath.Join(testPath, \"test\")\n\n\t// verify that tfplan files are created in the tmpDir, 2 files\n\tlist, err := findFilesWithExtension(outDir, \".tfplan\")\n\trequire.NoError(t, err)\n\tassert.Len(t, list, 2)\n\n\tfor _, file := range list {\n\t\tassert.Equal(t, \"tfplan.tfplan\", filepath.Base(file))\n\t}\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt run --all apply --non-interactive --working-dir %s --out-dir test\", testPath))\n\trequire.NoError(t, err)\n}\n\n// TestRunAllApplyWithCustomPlanFileName tests issue #5409\n// When using `run --all apply` with a plan file without .tfplan extension,\n// the plan file should be moved to the end of args, after flags.\nfunc TestRunAllApplyWithCustomPlanFileName(t *testing.T) {\n\tt.Parallel()\n\n\t// Reuse existing fixture\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutDir)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureOutDir)\n\tdependencyPath := filepath.Join(tmpEnvPath, testFixtureOutDir, \"dependency\")\n\n\t// Apply dependency first (required by app)\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+dependencyPath)\n\n\t// Step 1: Create plan with custom name (no .tfplan extension)\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\n\t\t\"terragrunt run --all plan --non-interactive --working-dir %s -- -out=customplan\",\n\t\ttestPath,\n\t))\n\trequire.NoError(t, err)\n\n\t// Step 2: Apply using the custom plan file name\n\t// This should fail before fix with \"Too many command line arguments\"\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\n\t\t\"terragrunt run --all apply --non-interactive --working-dir %s -- customplan\",\n\t\ttestPath,\n\t))\n\n\t// Assertions\n\trequire.NoError(t, err, \"Apply should succeed\")\n\n\toutput := stdout + stderr\n\trequire.NotContains(t, output, \"Too many command line arguments\")\n\trequire.NotContains(t, output, \"Expected at most one positional argument\")\n}\n\nfunc TestUsingAllAndGraphFlagsSimultaneously(t *testing.T) {\n\tt.Parallel()\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --graph --all\")\n\texpectedErr := new(shared.AllGraphFlagsError)\n\trequire.ErrorAs(t, err, &expectedErr)\n}\n\nfunc TestStorePlanFilesJsonRelativePath(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\targs string\n\t}{\n\t\t{\"run --all plan --non-interactive --working-dir %s --out-dir test --json-out-dir json\"},\n\t\t{\"run plan --all --non-interactive --working-dir %s --out-dir test --json-out-dir json\"},\n\t\t{\"run plan -a --non-interactive --working-dir %s --out-dir test --json-out-dir json\"},\n\t\t{\"run --all --non-interactive --working-dir %s --out-dir test --json-out-dir json -- plan\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(\"terragrunt args: \"+tc.args, func(t *testing.T) {\n\t\t\tt.Parallel()\n\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutDir)\n\t\t\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\t\t\ttestPath := filepath.Join(tmpEnvPath, testFixtureOutDir)\n\n\t\t\t// run plan with output directory\n\t\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt \"+tc.args, testPath))\n\t\t\trequire.NoError(t, err)\n\n\t\t\t// verify that tfplan files are created in the tmpDir, 2 files\n\t\t\toutDir := filepath.Join(testPath, \"test\")\n\t\t\tlist, err := findFilesWithExtension(outDir, \".tfplan\")\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Len(t, list, 2)\n\n\t\t\t// verify that json files are create\n\t\t\tjsonDir := filepath.Join(testPath, \"json\")\n\t\t\tlistJSON, err := findFilesWithExtension(jsonDir, \".json\")\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Len(t, listJSON, 2)\n\t\t})\n\t}\n}\n\nfunc TestPlanJsonFilesRunAll(t *testing.T) {\n\tt.Parallel()\n\n\t// create temporary directory for plan files\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\t_, _, _, err := testRunAllPlan(t, \"--json-out-dir \"+tmpDir, \"\")\n\trequire.NoError(t, err)\n\n\t// verify that was generated json files with plan data\n\tlist, err := findFilesWithExtension(tmpDir, \".json\")\n\trequire.NoError(t, err)\n\tassert.Len(t, list, 2)\n\n\tfor _, file := range list {\n\t\tassert.Equal(t, \"tfplan.json\", filepath.Base(file))\n\t\t// verify that file is not empty\n\t\tcontent, err := os.ReadFile(file)\n\t\trequire.NoError(t, err)\n\t\tassert.NotEmpty(t, content)\n\t\t// check that produced json is valid and can be unmarshalled\n\t\tvar plan map[string]any\n\n\t\terr = json.Unmarshal(content, &plan)\n\t\trequire.NoError(t, err)\n\t\t// check that plan is not empty\n\t\tassert.NotEmpty(t, plan)\n\t}\n}\n\nfunc TestPlanJsonPlanBinaryRunAll(t *testing.T) {\n\tt.Parallel()\n\n\t// create temporary directory for plan files\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutDir)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureOutDir)\n\n\tdependencyPath := filepath.Join(tmpEnvPath, testFixtureOutDir, \"dependency\")\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --working-dir %s --out-dir %s\", dependencyPath, tmpDir))\n\n\t// run plan with output directory\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt run --all plan --non-interactive --working-dir %s --json-out-dir %s --out-dir %s\", testPath, tmpDir, tmpDir))\n\trequire.NoError(t, err)\n\n\t// verify that was generated json files with plan data\n\tlist, err := findFilesWithExtension(tmpDir, \".json\")\n\trequire.NoError(t, err)\n\tassert.Len(t, list, 2)\n\n\tfor _, file := range list {\n\t\tassert.Equal(t, \"tfplan.json\", filepath.Base(file))\n\t\t// verify that file is not empty\n\t\tcontent, err := os.ReadFile(file)\n\t\trequire.NoError(t, err)\n\t\tassert.NotEmpty(t, content)\n\t}\n\n\t// verify that was generated binary plan files\n\tlist, err = findFilesWithExtension(tmpDir, \".tfplan\")\n\trequire.NoError(t, err)\n\tassert.Len(t, list, 2)\n\n\tfor _, file := range list {\n\t\tassert.Equal(t, \"tfplan.tfplan\", filepath.Base(file))\n\t}\n}\n\nfunc TestTerragruntRunAllPlanAndShow(t *testing.T) {\n\tt.Parallel()\n\n\t// create temporary directory for plan files\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutDir)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureOutDir)\n\n\tdependencyPath := filepath.Join(tmpEnvPath, testFixtureOutDir, \"dependency\")\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --working-dir %s --out-dir %s\", dependencyPath, tmpDir))\n\n\t// run plan and apply\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt run --all plan --non-interactive --working-dir %s --out-dir %s\", testPath, tmpDir))\n\trequire.NoError(t, err)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt run --all apply --non-interactive --working-dir %s --out-dir %s\", testPath, tmpDir))\n\trequire.NoError(t, err)\n\n\t// run new plan and show\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt run --all plan --non-interactive --working-dir %s --out-dir %s\", testPath, tmpDir))\n\trequire.NoError(t, err)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt run --all show --non-interactive --tf-forward-stdout --working-dir %s --out-dir %s -no-color\", testPath, tmpDir))\n\trequire.NoError(t, err)\n\n\t// Verify that output contains the plan and not plain the actual state output\n\tassert.Contains(t, stdout, \"No changes. Your infrastructure matches the configuration.\")\n}\n\nfunc TestLogFormatJSONOutput(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureNotExistingSource)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureNotExistingSource)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply --log-format=json --non-interactive --working-dir \"+testPath)\n\trequire.Error(t, err)\n\n\t// for windows OS\n\toutput := bytes.ReplaceAll([]byte(stderr), []byte(\"\\r\\n\"), []byte(\"\\n\"))\n\n\tmultipeJSONs := bytes.Split(output, []byte(\"\\n\"))\n\n\tvar msgs = make([]string, 0, len(multipeJSONs))\n\n\tfor _, jsonBytes := range multipeJSONs {\n\t\tif len(jsonBytes) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar output map[string]any\n\n\t\terr = json.Unmarshal(jsonBytes, &output)\n\t\trequire.NoError(t, err)\n\n\t\tmsg, ok := output[\"msg\"].(string)\n\t\tassert.True(t, ok)\n\n\t\tmsgs = append(msgs, msg)\n\t}\n\n\tassert.Contains(t, strings.Join(msgs, \"\"), \"Downloading Terraform configurations from git::https://github.com/gruntwork-io/terragrunt.git?ref=v0.83.2\")\n}\n\nfunc TestTerragruntOutputFromDependencyLogsJson(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\targ string\n\t}{\n\t\t{\"--json\"},\n\t\t{\"--json --log-format json\"},\n\t\t{\"--tf-forward-stdout\"},\n\t\t{\"--json --log-format json --tf-forward-stdout\"},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(\"terragrunt output with \"+tc.arg, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDependencyOutput)\n\t\t\trootTerragruntPath := filepath.Join(tmpEnvPath, testFixtureDependencyOutput)\n\t\t\t// apply dependency first\n\t\t\tdependencyTerragruntConfigPath := filepath.Join(rootTerragruntPath, \"dependency\")\n\t\t\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --working-dir %s \", dependencyTerragruntConfigPath))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tappTerragruntConfigPath := filepath.Join(rootTerragruntPath, \"app\")\n\t\t\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf(\"terragrunt plan --non-interactive --working-dir %s %s\", appTerragruntConfigPath, tc.arg))\n\t\t\trequire.NoError(t, err)\n\n\t\t\toutput := fmt.Sprintf(\"%s %s\", stderr, stdout)\n\t\t\tassert.NotContains(t, output, \"invalid character\")\n\t\t})\n\t}\n}\n\nfunc TestTerragruntJsonPlanJsonOutput(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\ttgArgs string\n\t\ttfArgs string\n\t}{\n\t\t{\"\", \"--json\"},\n\t\t{\"--log-format json\", \"--json\"},\n\t\t{\"--tf-forward-stdout\", \"\"},\n\t\t{\"--log-format json --tf-forward-stdout\", \"--json\"},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(\"terragrunt with \"+tc.tgArgs+\" -- plan \"+tc.tfArgs, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\t\t\t_, _, _, err := testRunAllPlan(t, tc.tgArgs+\" --json-out-dir \"+tmpDir, tc.tfArgs)\n\t\t\trequire.NoError(t, err)\n\t\t\tlist, err := findFilesWithExtension(tmpDir, \".json\")\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Len(t, list, 2)\n\n\t\t\tfor _, file := range list {\n\t\t\t\tassert.Equal(t, \"tfplan.json\", filepath.Base(file))\n\t\t\t\t// verify that file is not empty\n\t\t\t\tcontent, err := os.ReadFile(file)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.NotEmpty(t, content)\n\t\t\t\t// check that produced json is valid and can be unmarshalled\n\t\t\t\tvar plan map[string]any\n\n\t\t\t\terr = json.Unmarshal(content, &plan)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\t// check that plan is not empty\n\t\t\t\tassert.NotEmpty(t, plan)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestErrorMessageIncludeInOutput(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureErrorPrint)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureErrorPrint)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply  --non-interactive --working-dir \"+testPath+\" --tf-path \"+testPath+\"/custom-tf-script.sh --log-level trace\")\n\trequire.Error(t, err)\n\n\tassert.Contains(t, err.Error(), \"Custom error from script\")\n}\n\nfunc TestTerragruntTerraformOutputJson(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureInitError)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureInitError)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply --no-color --log-format=json --non-interactive --working-dir \"+testPath)\n\trequire.Error(t, err)\n\n\t// Sometimes, this is the error returned by AWS.\n\tif !strings.Contains(stderr, \"Error: Failed to get existing workspaces: operation error S3: ListObjectsV2, https response error StatusCode: 301\") {\n\t\tassert.Regexp(t, `\"msg\":\".*`+regexp.QuoteMeta(\"Initializing the backend...\"), stderr)\n\t}\n\n\t// check if output can be extracted in json\n\tjsonStrings := strings.SplitSeq(stderr, \"\\n\")\n\tfor jsonString := range jsonStrings {\n\t\tif len(jsonString) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar output map[string]any\n\n\t\terr = json.Unmarshal([]byte(jsonString), &output)\n\t\trequire.NoErrorf(t, err, \"Failed to parse json %s\", jsonString)\n\t\tassert.NotNil(t, output[\"level\"])\n\t\tassert.NotNil(t, output[\"time\"])\n\t}\n}\n\nfunc TestLogStreaming(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureLogStreaming)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureLogStreaming)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --working-dir \"+testPath+\" apply\")\n\trequire.NoError(t, err)\n\n\tfor _, unit := range []string{\"unit1\", \"unit2\"} {\n\t\t// Find the timestamps for the first and second log entries for this unit\n\t\tfirstTimestamp := time.Time{}\n\t\tsecondTimestamp := time.Time{}\n\n\t\tfor line := range strings.SplitSeq(stdout, \"\\n\") {\n\t\t\tif strings.Contains(line, unit) {\n\t\t\t\tif !strings.Contains(line, \"(local-exec): sleeping...\") && !strings.Contains(line, \"(local-exec): done sleeping\") {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tdateTimestampStr := strings.Split(line, \" \")[0]\n\t\t\t\t// The dateTimestampStr looks like this:\n\t\t\t\t// time=2025-01-09EST15:47:04-05:00\n\t\t\t\t//\n\t\t\t\t// We just need the timestamp\n\t\t\t\ttimestampStr := dateTimestampStr[18:26]\n\n\t\t\t\ttimestamp, err := time.Parse(\"15:04:05.999\", timestampStr)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tif firstTimestamp.IsZero() {\n\t\t\t\t\tassert.Contains(t, line, \"(local-exec): sleeping...\")\n\n\t\t\t\t\tfirstTimestamp = timestamp\n\t\t\t\t} else {\n\t\t\t\t\tassert.Contains(t, line, \"(local-exec): done sleeping\")\n\n\t\t\t\t\tsecondTimestamp = timestamp\n\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Confirm that the timestamps are at least 1 second apart\n\t\trequire.GreaterOrEqualf(t, secondTimestamp.Sub(firstTimestamp), 1*time.Second, \"Second log entry for unit %s is not at least 1 second after the first log entry\", unit)\n\t}\n}\n\nfunc TestLogFormatBare(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureEmptyState)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureEmptyState)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt init --log-format=bare --no-color --non-interactive --working-dir \"+testPath)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"Initializing the backend...\")\n\tassert.NotContains(t, stdout, \"STDO[0000] Initializing the backend...\")\n}\n\nfunc TestTF110EphemeralVars(t *testing.T) {\n\tt.Parallel()\n\n\tif !helpers.IsTerraform110OrHigher(t) {\n\t\tt.Skip(\"This test requires Terraform 1.10 or higher\")\n\n\t\treturn\n\t}\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureEphemeralInputs)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureEphemeralInputs)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt plan --non-interactive --working-dir \"+testPath)\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"Plan: 1 to add, 0 to change, 0 to destroy\")\n\n\tstdout, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply --auto-approve --non-interactive --working-dir \"+testPath)\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"Apply complete! Resources: 1 added, 0 changed, 0 destroyed\")\n}\n\n//nolint:paralleltest\nfunc TestTfPath(t *testing.T) {\n\t// This test can't be parallelized because it explicitly unsets the TG_TF_PATH environment variable.\n\t// t.Parallel()\n\n\t// Test that the terragrunt run version command correctly identifies and uses\n\t// the terraform_binary path configuration if present\n\thelpers.CleanupTerraformFolder(t, testFixtureTfPathBasic)\n\trootPath := helpers.CopyEnvironment(t, testFixtureTfPathBasic)\n\tworkingDir := filepath.Join(rootPath, testFixtureTfPathBasic)\n\tworkingDir, err := filepath.EvalSymlinks(workingDir)\n\trequire.NoError(t, err)\n\n\t// If TG_TF_PATH is not set, we'll use the default tofu binary,\n\t// we'll explicitly set the value so that the test can pass.\n\tif tfPath := os.Getenv(\"TG_TF_PATH\"); tfPath != \"\" {\n\t\t// Unset after using t.Setenv so that it'll be reset after the test.\n\t\tt.Setenv(\"TG_TF_PATH\", \"\")\n\t\tos.Unsetenv(\"TG_TF_PATH\")\n\t}\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run version --working-dir \"+workingDir)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"TF script used!\")\n}\n\nfunc TestTfPathOverridesConfig(t *testing.T) {\n\tt.Parallel()\n\t// Test that the terragrunt run version command correctly identifies and uses\n\t// the terraform_binary path configuration if present\n\thelpers.CleanupTerraformFolder(t, testFixtureTfPathBasic)\n\trootPath := helpers.CopyEnvironment(t, testFixtureTfPathBasic)\n\tworkingDir := filepath.Join(rootPath, testFixtureTfPathBasic)\n\tworkingDir, err := filepath.EvalSymlinks(workingDir)\n\trequire.NoError(t, err)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run version --tf-path ./other-tf.sh --working-dir \"+workingDir)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"Other TF script used!\")\n}\n\nfunc TestTfPathOverridesConfigWithTofuTerraform(t *testing.T) {\n\tt.Parallel()\n\n\t// This test requires that both tofu and terraform are installed.\n\tif !helpers.IsTerraformInstalled() || !helpers.IsOpenTofuInstalled() {\n\t\tt.Skip(\"This test requires that both OpenTofu and Terraform are installed\")\n\n\t\treturn\n\t}\n\n\thelpers.CleanupTerraformFolder(t, testFixtureTfPathTofuTerraform)\n\trootPath := helpers.CopyEnvironment(t, testFixtureTfPathTofuTerraform)\n\tworkingDir := filepath.Join(rootPath, testFixtureTfPathTofuTerraform)\n\tworkingDir, err := filepath.EvalSymlinks(workingDir)\n\trequire.NoError(t, err)\n\n\ttestCases := []struct {\n\t\tfeature  string\n\t\ttfPath   string\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tfeature:  \"tofu\",\n\t\t\ttfPath:   helpers.TofuBinary,\n\t\t\texpected: \"OpenTofu\",\n\t\t},\n\t\t{\n\t\t\tfeature:  \"terraform\",\n\t\t\ttfPath:   helpers.TerraformBinary,\n\t\t\texpected: \"Terraform\",\n\t\t},\n\t\t{\n\t\t\tfeature:  \"tofu\",\n\t\t\ttfPath:   helpers.TerraformBinary,\n\t\t\texpected: \"Terraform\",\n\t\t},\n\t\t{\n\t\t\tfeature:  \"terraform\",\n\t\t\ttfPath:   helpers.TofuBinary,\n\t\t\texpected: \"OpenTofu\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\t\tt,\n\t\t\tfmt.Sprintf(\n\t\t\t\t\"terragrunt run version --feature binary=%s --tf-path %s --working-dir %s\",\n\t\t\t\ttc.feature,\n\t\t\t\ttc.tfPath,\n\t\t\t\tworkingDir,\n\t\t\t),\n\t\t)\n\t\trequire.NoError(t, err)\n\n\t\tassert.Contains(t, stdout, tc.expected)\n\t}\n}\n\nfunc TestMixedStackConfigIgnored(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureMixedConfig)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureMixedConfig)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all --non-interactive --working-dir \"+testPath+\" -- apply\")\n\trequire.NoError(t, err)\n\trequire.NotContains(t, stderr, \"Error: Unsupported block type\")\n\trequire.NotContains(t, stderr, \"Blocks of type \\\"unit\\\" are not expected here\")\n}\n\n// Test that default command forwarding is disabled and users are guided to use `run --`.\nfunc TestNoDefaultForwardingUnknownCommand(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixturePath)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixturePath)\n\trootPath := filepath.Join(tmpEnvPath, testFixturePath)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt workspace list --non-interactive --working-dir \"+rootPath)\n\trequire.Error(t, err, \"expected error when invoking unknown top-level command without 'run'\")\n}\n\nfunc TestDiscoveryDoesntResolveOutputs(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tdepDir := filepath.Join(tmpDir, \"dep\")\n\terr := os.MkdirAll(depDir, 0755)\n\trequire.NoError(t, err)\n\n\tmainDir := filepath.Join(tmpDir, \"main\")\n\terr = os.MkdirAll(mainDir, 0755)\n\trequire.NoError(t, err)\n\n\tdepConfig := `\nterraform {\n  source = \".\"\n}\n`\n\terr = os.WriteFile(filepath.Join(depDir, \"terragrunt.hcl\"), []byte(depConfig), 0644)\n\trequire.NoError(t, err)\n\n\tdepTerraform := `\noutput \"value\" {\n  value = \"hello from dependency\"\n}\n`\n\terr = os.WriteFile(filepath.Join(depDir, \"main.tf\"), []byte(depTerraform), 0644)\n\trequire.NoError(t, err)\n\n\tmainConfig := `\nterraform {\n  source = \".\"\n}\n\ndependency \"dep\" {\n  config_path = \"../dep\"\n\n  mock_outputs = {\n    value = \"mock value\"\n  }\n}\n\ninputs = {\n  dep_value = dependency.dep.outputs.value\n}\n`\n\terr = os.WriteFile(filepath.Join(mainDir, \"terragrunt.hcl\"), []byte(mainConfig), 0644)\n\trequire.NoError(t, err)\n\n\tmainTerraform := `\nvariable \"dep_value\" {\n  type = string\n}\n\noutput \"result\" {\n  value = var.dep_value\n}\n`\n\terr = os.WriteFile(filepath.Join(mainDir, \"main.tf\"), []byte(mainTerraform), 0644)\n\trequire.NoError(t, err)\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+depDir)\n\trequire.NoError(t, err)\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+depDir)\n\trequire.NoError(t, err)\n\tassert.Contains(t, stdout, \"hello from dependency\")\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run --all apply --non-interactive --working-dir \"+tmpDir)\n\trequire.NoError(t, err)\n\n\tassert.NotEmpty(t, stdout)\n\tassert.NotEmpty(t, stderr)\n\n\tassert.NotContains(t, stderr, \"that has no outputs, but mock outputs provided and returning those in dependency output\")\n\n\tstdout, _, err = helpers.RunTerragruntCommandWithOutput(t, \"terragrunt output -no-color -json --non-interactive --working-dir \"+mainDir)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stdout, \"hello from dependency\")\n}\n\nfunc TestExternalDependenciesAreResolved(t *testing.T) {\n\tt.Parallel()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tdepDir := filepath.Join(tmpDir, \"dep\")\n\terr := os.MkdirAll(depDir, 0755)\n\trequire.NoError(t, err)\n\n\tmainDir := filepath.Join(tmpDir, \"main\")\n\terr = os.MkdirAll(mainDir, 0755)\n\trequire.NoError(t, err)\n\n\tdepConfig := `\nterraform {\n  source = \".\"\n}\n`\n\terr = os.WriteFile(filepath.Join(depDir, \"terragrunt.hcl\"), []byte(depConfig), 0644)\n\trequire.NoError(t, err)\n\n\tdepTerraform := `\noutput \"value\" {\n  value = \"hello from dependency\"\n}\n`\n\terr = os.WriteFile(filepath.Join(depDir, \"main.tf\"), []byte(depTerraform), 0644)\n\trequire.NoError(t, err)\n\n\tmainConfig := `\nterraform {\n  source = \".\"\n}\n\ndependency \"dep\" {\n  config_path = \"../dep\"\n\n  mock_outputs = {\n    value = \"mock value\"\n  }\n}\n\ninputs = {\n  dep_value = dependency.dep.outputs.value\n}\n`\n\terr = os.WriteFile(filepath.Join(mainDir, \"terragrunt.hcl\"), []byte(mainConfig), 0644)\n\trequire.NoError(t, err)\n\n\tmainTerraform := `\nvariable \"dep_value\" {\n  type = string\n}\n\noutput \"result\" {\n  value = var.dep_value\n}\n`\n\terr = os.WriteFile(filepath.Join(mainDir, \"main.tf\"), []byte(mainTerraform), 0644)\n\trequire.NoError(t, err)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt run --all plan --non-interactive --queue-exclude-external --working-dir \"+mainDir,\n\t)\n\trequire.NoError(t, err)\n\n\tassert.NotEmpty(t, stdout)\n\tassert.NotEmpty(t, stderr)\n\n\tassert.Contains(\n\t\tt,\n\t\tstderr,\n\t\t\"that has no outputs, but mock outputs provided and returning those in dependency output\",\n\t)\n\tassert.NotContains(\n\t\tt,\n\t\tstderr,\n\t\t`There is no variable named \"dependency\".`,\n\t)\n}\n\nfunc TestRunAllDetectsHiddenDirectories(t *testing.T) {\n\tt.Parallel()\n\trootPath := helpers.CopyEnvironment(t, hiddenRunAllFixturePath, \".cloud/**\")\n\tmodulePath := filepath.Join(rootPath, hiddenRunAllFixturePath)\n\thelpers.CleanupTerraformFolder(t, modulePath)\n\n\treportFile := filepath.Join(modulePath, helpers.ReportFile)\n\n\t// Expect Terragrunt to discover modules under .cloud directory\n\tcommand := fmt.Sprintf(\n\t\t\"terragrunt run --all plan --non-interactive --working-dir %s --report-file %s --report-format json\",\n\t\tmodulePath,\n\t\treportFile,\n\t)\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(t, command)\n\trequire.NoError(t, err)\n\n\t// Parse the report file to verify the correct units ran\n\truns, err := report.ParseJSONRunsFromFile(reportFile)\n\trequire.NoError(t, err, \"Should be able to parse JSON report\")\n\n\trunNames := runs.Names()\n\n\t// Verify both hidden directories were discovered and executed\n\tapp1Run := runs.FindByName(\".cloud/terraform/app1\")\n\trequire.NotNil(t, app1Run, \"Expected .cloud/terraform/app1 unit to be in report. Found: %v\", runNames)\n\n\tapp2Run := runs.FindByName(\".cloud/terraform/app2\")\n\trequire.NotNil(t, app2Run, \"Expected .cloud/terraform/app2 unit to be in report. Found: %v\", runNames)\n}\n\nfunc TestNoColorDependency(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureNoColorDependency)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureNoColorDependency)\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, \"terragrunt run plan -no-color --tf-forward-stdout --working-dir \"+testPath)\n\trequire.NoError(t, err)\n\tassert.Equal(t, 1, strings.Count(stdout, \"has been successfully initialized!\"))\n\n\t// check that no ANSI codes are printed\n\tassert.NotContains(t, stderr, \"\\x1b\")\n\tassert.NotContains(t, stdout, \"\\x1b\")\n}\n\n// TestTerragruntPassNullValues verifies that terragrunt can pass null values to\n// Terraform variables. This is a regression test for:\n// https://github.com/gruntwork-io/terragrunt/issues/5452\nfunc TestTerragruntPassNullValues(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureNullValue)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureNullValue)\n\n\t_, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt apply -auto-approve --non-interactive --working-dir \"+testPath,\n\t)\n\trequire.NoError(t, err)\n\n\tnullVarsFile := filepath.Join(testPath, \".terragrunt-cache\", \"*\", \"*\", \".terragrunt-null-vars.auto.tfvars.json\")\n\tmatches, err := filepath.Glob(nullVarsFile)\n\trequire.NoError(t, err)\n\tassert.Empty(t, matches, \"null vars file should be removed after execution\")\n\n\tstdout, _, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt output -json --non-interactive --working-dir \"+testPath,\n\t)\n\trequire.NoError(t, err)\n\n\toutputs := map[string]helpers.TerraformOutput{}\n\trequire.NoError(t, json.Unmarshal([]byte(stdout), &outputs))\n\n\t// output1 should not be present because OpenTofu/Terraform omit\n\t// null-valued outputs from JSON output\n\t_, ok := outputs[\"output1\"]\n\tassert.False(t, ok, \"expected output1 to not be present since it has a null value\")\n\n\toutput2, ok := outputs[\"output2\"]\n\trequire.True(t, ok, \"expected output2 to be present\")\n\tassert.Equal(t, \"variable 2\", output2.Value)\n}\n"
  },
  {
    "path": "test/integration_tflint_test.go",
    "content": "//go:build tflint\n// +build tflint\n\n//nolint:paralleltest\npackage test_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureTflintNoIssuesFound  = \"fixtures/tflint/no-issues-found\"\n\ttestFixtureTflintIssuesFound    = \"fixtures/tflint/issues-found\"\n\ttestFixtureTflintNoConfigFile   = \"fixtures/tflint/no-config-file\"\n\ttestFixtureTflintModuleFound    = \"fixtures/tflint/module-found\"\n\ttestFixtureTflintNoTfSourcePath = \"fixtures/tflint/no-tf-source\"\n\ttestFixtureTflintExternalTflint = \"fixtures/tflint/external-tflint\"\n\ttestFixtureTflintTfvarPassing   = \"fixtures/tflint/tfvar-passing\"\n\ttestFixtureTflintArgs           = \"fixtures/tflint/tflint-args\"\n\ttestFixtureTflintCustomConfig   = \"fixtures/tflint/custom-tflint-config\"\n\n\t// Number of init samples to detect tflint race conditions\n\ttflintInitSamples = 25\n)\n\n// TODO: Get rid of all of these once we have no internal tflint\n\nfunc TestTflintFindsNoIssuesWithValidCode(t *testing.T) {\n\tout := new(bytes.Buffer)\n\terrOut := new(bytes.Buffer)\n\trootPath := CopyEnvironmentWithTflint(t, testFixtureTflintNoIssuesFound)\n\tmodulePath := filepath.Join(rootPath, testFixtureTflintNoIssuesFound)\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --log-level debug --working-dir \"+modulePath, out, errOut)\n\trequire.NoError(t, err)\n\n\tassert.NotContains(t, errOut.String(), \"Error while running tflint with args:\")\n\tassert.NotContains(t, errOut.String(), \"Tflint found issues in the project. Check for the tflint logs above.\")\n\n\t// TFLint config should be found in the original working directory, not inside .terragrunt-cache\n\t// The config path should end with .tflint.hcl but NOT be inside .terragrunt-cache\n\tfound, err := regexp.MatchString(\"--config .*/\\\\.tflint\\\\.hcl\", errOut.String())\n\trequire.NoError(t, err)\n\tassert.True(t, found, \"Expected to find --config .../.tflint.hcl in output\")\n\tassert.NotContains(t, errOut.String(), \"--config ./.terragrunt-cache\", \"TFLint config should not be inside cache directory\")\n}\n\nfunc TestTflintFindsModule(t *testing.T) {\n\tout := new(bytes.Buffer)\n\terrOut := new(bytes.Buffer)\n\trootPath := CopyEnvironmentWithTflint(t, testFixtureTflintModuleFound)\n\tmodulePath := filepath.Join(rootPath, testFixtureTflintModuleFound)\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --log-level debug --working-dir \"+modulePath, out, errOut)\n\trequire.NoError(t, err)\n\n\tassert.NotContains(t, errOut.String(), \"Error while running tflint with args:\")\n\tassert.NotContains(t, errOut.String(), \"Tflint found issues in the project. Check for the tflint logs above.\")\n}\n\nfunc TestTflintFindsIssuesWithInvalidInput(t *testing.T) {\n\terrOut := new(bytes.Buffer)\n\trootPath := CopyEnvironmentWithTflint(t, testFixtureTflintIssuesFound)\n\tmodulePath := filepath.Join(rootPath, testFixtureTflintIssuesFound)\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --log-level debug --working-dir \"+modulePath, os.Stdout, errOut)\n\tassert.Error(t, err, \"Tflint found issues in the project. Check for the tflint logs\")\n}\n\nfunc TestTflintWithoutConfigFile(t *testing.T) {\n\terrOut := new(bytes.Buffer)\n\trootPath := CopyEnvironmentWithTflint(t, testFixtureTflintNoConfigFile)\n\tmodulePath := filepath.Join(rootPath, testFixtureTflintNoConfigFile)\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --working-dir \"+modulePath, io.Discard, errOut)\n\tassert.Error(t, err, \"Could not find .tflint.hcl config file in the parent folders:\")\n}\n\nfunc TestTflintFindsConfigInCurrentPath(t *testing.T) {\n\trootPath := CopyEnvironmentWithTflint(t, testFixtureTflintNoTfSourcePath)\n\tmodulePath := filepath.Join(rootPath, testFixtureTflintNoTfSourcePath)\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"terragrunt plan --log-level debug --working-dir \"+modulePath,\n\t)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"Tflint has run successfully. No issues found\")\n\n\texpectedTflintHCLPath := filepath.Join(\"..\", \"..\", \"..\", \".tflint.hcl\")\n\tassert.Contains(t, stderr, expectedTflintHCLPath)\n}\n\nfunc TestTflintInitSameModule(t *testing.T) {\n\trootPath := CopyEnvironmentWithTflint(t, testFixtureParallelRun)\n\tt.Cleanup(func() {\n\t\thelpers.RemoveFolder(t, rootPath)\n\t})\n\n\tmodulePath := filepath.Join(rootPath, testFixtureParallelRun)\n\trunPath := filepath.Join(rootPath, testFixtureParallelRun, \"dev\")\n\tappTemplate := filepath.Join(rootPath, testFixtureParallelRun, \"dev\", \"app\")\n\t// generate multiple \"app\" modules that will be initialized in parallel\n\tfor i := 0; i < tflintInitSamples; i++ {\n\t\tappPath := filepath.Join(modulePath, \"dev\", fmt.Sprintf(\"app-%d\", i))\n\t\terr := util.CopyFolderContents(createLogger(), appTemplate, appPath, \".terragrunt-test\", []string{}, []string{})\n\t\trequire.NoError(t, err)\n\t}\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --log-level debug --all init --non-interactive --working-dir \"+runPath)\n}\n\nfunc TestTflintFindsNoIssuesWithValidCodeDifferentDownloadDir(t *testing.T) {\n\tout := new(bytes.Buffer)\n\terrOut := new(bytes.Buffer)\n\n\tdownloadDir := helpers.TmpDirWOSymlinks(t)\n\n\trootPath := CopyEnvironmentWithTflint(t, testFixtureTflintNoIssuesFound)\n\tt.Cleanup(func() {\n\t\thelpers.RemoveFolder(t, rootPath)\n\t})\n\n\tmodulePath := filepath.Join(rootPath, testFixtureTflintNoIssuesFound)\n\terr := helpers.RunTerragruntCommand(t, fmt.Sprintf(\"terragrunt plan --log-level debug --working-dir %s --download-dir %s\", modulePath, downloadDir), out, errOut)\n\trequire.NoError(t, err)\n\n\tassert.NotContains(t, errOut.String(), \"Error while running tflint with args:\")\n\tassert.NotContains(t, errOut.String(), \"Tflint found issues in the project. Check for the tflint logs above.\")\n\n\t// TFLint config should be found in the original working directory, not inside the download directory\n\t// The config path should end with .tflint.hcl but NOT be inside the download directory\n\tfound, err := regexp.MatchString(\"--config .*/\\\\.tflint\\\\.hcl\", errOut.String())\n\trequire.NoError(t, err)\n\tassert.True(t, found, \"Expected to find --config .../.tflint.hcl in output\")\n\n\trelPath, err := filepath.Rel(modulePath, downloadDir)\n\trequire.NoError(t, err)\n\tassert.NotContains(t, errOut.String(), fmt.Sprintf(\"--config %s/\", relPath), \"TFLint config should not be inside download directory\")\n}\n\nfunc TestTflintExternalTflint(t *testing.T) {\n\tout := new(bytes.Buffer)\n\terrOut := new(bytes.Buffer)\n\n\trootPath := CopyEnvironmentWithTflint(t, testFixtureTflintExternalTflint)\n\tt.Cleanup(func() {\n\t\thelpers.RemoveFolder(t, rootPath)\n\t})\n\n\trunPath := filepath.Join(rootPath, testFixtureTflintExternalTflint)\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --log-level debug --working-dir \"+runPath, out, errOut)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, errOut.String(), \"Running external tflint with args\")\n\tassert.Contains(t, errOut.String(), \"Tflint has run successfully. No issues found\")\n}\n\nfunc TestTflintTfvarsArePassedToTflint(t *testing.T) {\n\tout := new(bytes.Buffer)\n\terrOut := new(bytes.Buffer)\n\n\trootPath := CopyEnvironmentWithTflint(t, testFixtureTflintTfvarPassing)\n\tt.Cleanup(func() {\n\t\thelpers.RemoveFolder(t, rootPath)\n\t})\n\n\trunPath := filepath.Join(rootPath, testFixtureTflintTfvarPassing)\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan -log-level debug --working-dir \"+runPath, out, errOut)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, errOut.String(), \"--var-file=extra.tfvars\")\n\tassert.Contains(t, errOut.String(), \"Tflint has run successfully. No issues found\")\n}\n\nfunc TestTflintArgumentsPassedIn(t *testing.T) {\n\tout := new(bytes.Buffer)\n\terrOut := new(bytes.Buffer)\n\n\trootPath := CopyEnvironmentWithTflint(t, testFixtureTflintArgs)\n\tt.Cleanup(func() {\n\t\thelpers.RemoveFolder(t, rootPath)\n\t})\n\n\trunPath := filepath.Join(rootPath, testFixtureTflintArgs)\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --log-level debug --working-dir \"+runPath, out, errOut)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, errOut.String(), \"--minimum-failure-severity=error\")\n\tassert.Contains(t, errOut.String(), \"Tflint has run successfully. No issues found\")\n}\n\nfunc TestTflintCustomConfig(t *testing.T) {\n\tout := new(bytes.Buffer)\n\terrOut := new(bytes.Buffer)\n\n\trootPath := CopyEnvironmentWithTflint(t, testFixtureTflintCustomConfig)\n\tt.Cleanup(func() {\n\t\thelpers.RemoveFolder(t, rootPath)\n\t})\n\n\trunPath := filepath.Join(rootPath, testFixtureTflintCustomConfig)\n\terr := helpers.RunTerragruntCommand(t, \"terragrunt plan --log-level debug --working-dir \"+runPath, out, errOut)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, errOut.String(), \"--config custom.tflint.hcl\")\n\tassert.Contains(t, errOut.String(), \"Tflint has run successfully. No issues found\")\n}\n\nfunc CopyEnvironmentWithTflint(t *testing.T, environmentPath string) string {\n\tt.Helper()\n\n\ttmpDir := helpers.TmpDirWOSymlinks(t)\n\n\tt.Logf(\"Copying %s to %s\", environmentPath, tmpDir)\n\n\trequire.NoError(t, util.CopyFolderContents(createLogger(), environmentPath, filepath.Join(tmpDir, environmentPath), \".terragrunt-test\", []string{\".tflint.hcl\"}, []string{}))\n\n\treturn tmpDir\n}\n"
  },
  {
    "path": "test/integration_tips_test.go",
    "content": "package test_test\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureTips = \"fixtures/tips\"\n)\n\n// TestTipDebuggingDocsShownOnError verifies that the debugging-docs tip\n// is displayed when an error occurs during `run`.\nfunc TestTipDebuggingDocsShownOnError(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureTips)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureTips)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureTips)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"run apply --non-interactive --working-dir \"+rootPath,\n\t)\n\n\trequire.Error(t, err)\n\tassert.Contains(t, stderr, \"TIP (debugging-docs): For help troubleshooting errors\")\n\tassert.Contains(t, stderr, \"docs.terragrunt.com/troubleshooting/debugging\")\n}\n\n// TestTipDebuggingDocsNotShownWithNoTips verifies that the debugging-docs tip\n// is NOT displayed when --no-tips flag is used.\nfunc TestTipDebuggingDocsNotShownWithNoTips(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureTips)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureTips)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureTips)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"run apply --no-tips --non-interactive --working-dir \"+rootPath,\n\t)\n\n\trequire.Error(t, err)\n\tassert.NotContains(t, stderr, \"TIP (debugging-docs): For help troubleshooting errors\")\n}\n\n// TestTipDebuggingDocsNotShownWithNoTipSpecific verifies that the debugging-docs tip\n// is NOT displayed when --no-tip debugging-docs flag is used.\nfunc TestTipDebuggingDocsNotShownWithNoTipSpecific(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureTips)\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureTips)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureTips)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\t\"run apply --no-tip debugging-docs --non-interactive --working-dir \"+rootPath,\n\t)\n\n\trequire.Error(t, err)\n\tassert.NotContains(t, stderr, \"TIP (debugging-docs): For help troubleshooting errors\")\n}\n"
  },
  {
    "path": "test/integration_tofu_aws_state_encryption_test.go",
    "content": "//go:build aws && tofu\n\npackage test_test\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureTofuStateEncryptionPBKDF2 = \"fixtures/tofu-state-encryption/pbkdf2\"\n\ttestFixtureTofuStateEncryptionGCPKMS = \"fixtures/tofu-state-encryption/gcp-kms\"\n\ttestFixtureTofuStateEncryptionAWSKMS = \"fixtures/tofu-state-encryption/aws-kms\"\n\ttestFixtureRenderJSONWithEncryption  = \"fixtures/render-json-with-encryption\"\n\tgcpKMSKeyID                          = \"projects/terragrunt-test/locations/global/keyRings/terragrunt-test/cryptoKeys/terragrunt-test-key\"\n\tawsKMSKeyID                          = \"7a8b0c4e-ff3c-49d0-93ba-15e3ca3488fb\"\n\tawsKMSKeyRegion                      = \"us-east-1\"\n)\n\nfunc TestTofuStateEncryptionPBKDF2(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureTofuStateEncryptionPBKDF2)\n\tworkDir := filepath.Join(tmpEnvPath, testFixtureTofuStateEncryptionPBKDF2)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+workDir)\n\tassert.True(t, helpers.FileIsInFolder(t, stateFile, workDir))\n\tvalidateStateIsEncrypted(t, stateFile, workDir)\n}\n\nfunc TestTofuStateEncryptionGCPKMS(t *testing.T) {\n\tt.Skip(\"Skipping test as the GCP KMS key is not available. You have to setup your own GCP KMS key to run this test.\")\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureTofuStateEncryptionGCPKMS)\n\tworkDir := filepath.Join(tmpEnvPath, testFixtureTofuStateEncryptionGCPKMS)\n\tconfigPath := filepath.Join(workDir, \"terragrunt.hcl\")\n\n\thelpers.CopyAndFillMapPlaceholders(t, configPath, configPath, map[string]string{\n\t\t\"__FILL_IN_KMS_KEY_ID__\": gcpKMSKeyID,\n\t})\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+workDir)\n\tassert.True(t, helpers.FileIsInFolder(t, stateFile, workDir))\n\tvalidateStateIsEncrypted(t, stateFile, workDir)\n}\n\nfunc TestTofuStateEncryptionAWSKMS(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureTofuStateEncryptionAWSKMS)\n\tworkDir := filepath.Join(tmpEnvPath, testFixtureTofuStateEncryptionAWSKMS)\n\tconfigPath := filepath.Join(workDir, \"terragrunt.hcl\")\n\n\thelpers.CopyAndFillMapPlaceholders(t, configPath, configPath, map[string]string{\n\t\t\"__FILL_IN_KMS_KEY_ID__\": awsKMSKeyID,\n\t\t\"__FILL_IN_AWS_REGION__\": awsKMSKeyRegion,\n\t})\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+workDir)\n\tassert.True(t, helpers.FileIsInFolder(t, stateFile, workDir))\n\tvalidateStateIsEncrypted(t, stateFile, workDir)\n}\n\nfunc TestTofuRenderJSONConfigWithEncryption(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRenderJSONWithEncryption)\n\tworkDir := filepath.Join(tmpEnvPath, testFixtureRenderJSONWithEncryption)\n\tmainPath := filepath.Join(workDir, \"main\")\n\tjsonOut := filepath.Join(mainPath, \"terragrunt_rendered.json\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all --non-interactive --working-dir \"+workDir+\" -- apply -auto-approve\")\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt render --json -w --non-interactive --working-dir %s --json-out %s\", mainPath, jsonOut))\n\n\tjsonBytes, err := os.ReadFile(jsonOut)\n\trequire.NoError(t, err)\n\n\tvar rendered map[string]any\n\trequire.NoError(t, json.Unmarshal(jsonBytes, &rendered))\n\n\t// Make sure all terraform block is visible\n\tterraformBlock, hasTerraform := rendered[\"terraform\"]\n\tif assert.True(t, hasTerraform) {\n\t\tsource, hasSource := terraformBlock.(map[string]any)[\"source\"]\n\t\tassert.True(t, hasSource)\n\t\tassert.Equal(t, \"./module\", source)\n\t}\n\n\t// Make sure included remoteState is rendered out\n\tremoteState, hasRemoteState := rendered[\"remote_state\"]\n\tif assert.True(t, hasRemoteState) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"backend\": \"local\",\n\t\t\t\t\"generate\": map[string]any{\n\t\t\t\t\t\"path\":      \"backend.tf\",\n\t\t\t\t\t\"if_exists\": \"overwrite_terragrunt\",\n\t\t\t\t},\n\t\t\t\t\"config\": map[string]any{\n\t\t\t\t\t\"path\": \"foo.tfstate\",\n\t\t\t\t},\n\t\t\t\t\"encryption\": map[string]any{\n\t\t\t\t\t\"key_provider\": \"pbkdf2\",\n\t\t\t\t\t\"passphrase\":   \"correct-horse-battery-staple\",\n\t\t\t\t},\n\t\t\t\t\"disable_init\":                    false,\n\t\t\t\t\"disable_dependency_optimization\": false,\n\t\t\t},\n\t\t\tremoteState.(map[string]any),\n\t\t)\n\t}\n\n\t// Make sure dependency blocks are rendered out\n\tdependencyBlocks, hasDependency := rendered[\"dependency\"]\n\tif assert.True(t, hasDependency) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"dep\": map[string]any{\n\t\t\t\t\t\"name\":         \"dep\",\n\t\t\t\t\t\"config_path\":  \"../dep\",\n\t\t\t\t\t\"outputs\":      nil,\n\t\t\t\t\t\"inputs\":       nil,\n\t\t\t\t\t\"mock_outputs\": nil,\n\t\t\t\t\t\"enabled\":      nil,\n\t\t\t\t\t\"mock_outputs_allowed_terraform_commands\": nil,\n\t\t\t\t\t\"mock_outputs_merge_strategy_with_state\":  nil,\n\t\t\t\t\t\"mock_outputs_merge_with_state\":           nil,\n\t\t\t\t\t\"skip\":                                    nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tdependencyBlocks.(map[string]any),\n\t\t)\n\t}\n\n\t// Make sure included generate block is rendered out\n\tgenerateBlocks, hasGenerate := rendered[\"generate\"]\n\tif assert.True(t, hasGenerate) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"provider\": map[string]any{\n\t\t\t\t\t\"path\":              \"provider.tf\",\n\t\t\t\t\t\"comment_prefix\":    \"# \",\n\t\t\t\t\t\"disable_signature\": false,\n\t\t\t\t\t\"disable\":           false,\n\t\t\t\t\t\"if_exists\":         \"overwrite_terragrunt\",\n\t\t\t\t\t\"if_disabled\":       \"skip\",\n\t\t\t\t\t\"hcl_fmt\":           nil,\n\t\t\t\t\t\"contents\": `provider \"aws\" {\n  region = \"us-east-1\"\n}\n`,\n\t\t\t\t},\n\t\t\t},\n\t\t\tgenerateBlocks.(map[string]any),\n\t\t)\n\t}\n\n\t// Make sure all inputs are merged together\n\tinputsBlock, hasInputs := rendered[\"inputs\"]\n\tif assert.True(t, hasInputs) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"env\":        \"qa\",\n\t\t\t\t\"name\":       \"dep\",\n\t\t\t\t\"type\":       \"main\",\n\t\t\t\t\"aws_region\": \"us-east-1\",\n\t\t\t},\n\t\t\tinputsBlock.(map[string]any),\n\t\t)\n\t}\n}\n\n// This will eventually be the only test for rendering JSON config with encryption\nfunc TestTofuRenderJSONConfigWithEncryptionExp(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureRenderJSONWithEncryption)\n\tworkDir := filepath.Join(tmpEnvPath, testFixtureRenderJSONWithEncryption)\n\tmainPath := filepath.Join(workDir, \"main\")\n\tjsonOut := filepath.Join(mainPath, \"terragrunt.rendered.json\")\n\n\thelpers.RunTerragrunt(t, \"terragrunt run --all --non-interactive --working-dir \"+workDir+\" -- apply -auto-approve\")\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt render --json  -w --non-interactive --working-dir %s --out %s\", mainPath, jsonOut))\n\n\tjsonBytes, err := os.ReadFile(jsonOut)\n\trequire.NoError(t, err)\n\n\tvar rendered map[string]any\n\trequire.NoError(t, json.Unmarshal(jsonBytes, &rendered))\n\n\t// Make sure all terraform block is visible\n\tterraformBlock, hasTerraform := rendered[\"terraform\"]\n\tif assert.True(t, hasTerraform) {\n\t\tsource, hasSource := terraformBlock.(map[string]any)[\"source\"]\n\t\tassert.True(t, hasSource)\n\t\tassert.Equal(t, \"./module\", source)\n\t}\n\n\t// Make sure included remoteState is rendered out\n\tremoteState, hasRemoteState := rendered[\"remote_state\"]\n\tif assert.True(t, hasRemoteState) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"backend\": \"local\",\n\t\t\t\t\"generate\": map[string]any{\n\t\t\t\t\t\"path\":      \"backend.tf\",\n\t\t\t\t\t\"if_exists\": \"overwrite_terragrunt\",\n\t\t\t\t},\n\t\t\t\t\"config\": map[string]any{\n\t\t\t\t\t\"path\": \"foo.tfstate\",\n\t\t\t\t},\n\t\t\t\t\"encryption\": map[string]any{\n\t\t\t\t\t\"key_provider\": \"pbkdf2\",\n\t\t\t\t\t\"passphrase\":   \"correct-horse-battery-staple\",\n\t\t\t\t},\n\t\t\t\t\"disable_init\":                    false,\n\t\t\t\t\"disable_dependency_optimization\": false,\n\t\t\t},\n\t\t\tremoteState.(map[string]any),\n\t\t)\n\t}\n\n\t// Make sure dependency blocks are rendered out\n\tdependencyBlocks, hasDependency := rendered[\"dependency\"]\n\tif assert.True(t, hasDependency) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"dep\": map[string]any{\n\t\t\t\t\t\"name\":         \"dep\",\n\t\t\t\t\t\"config_path\":  \"../dep\",\n\t\t\t\t\t\"outputs\":      nil,\n\t\t\t\t\t\"inputs\":       nil,\n\t\t\t\t\t\"mock_outputs\": nil,\n\t\t\t\t\t\"enabled\":      nil,\n\t\t\t\t\t\"mock_outputs_allowed_terraform_commands\": nil,\n\t\t\t\t\t\"mock_outputs_merge_strategy_with_state\":  nil,\n\t\t\t\t\t\"mock_outputs_merge_with_state\":           nil,\n\t\t\t\t\t\"skip\":                                    nil,\n\t\t\t\t},\n\t\t\t},\n\t\t\tdependencyBlocks.(map[string]any),\n\t\t)\n\t}\n\n\t// Make sure included generate block is rendered out\n\tgenerateBlocks, hasGenerate := rendered[\"generate\"]\n\tif assert.True(t, hasGenerate) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"provider\": map[string]any{\n\t\t\t\t\t\"path\":              \"provider.tf\",\n\t\t\t\t\t\"comment_prefix\":    \"# \",\n\t\t\t\t\t\"disable_signature\": false,\n\t\t\t\t\t\"disable\":           false,\n\t\t\t\t\t\"if_exists\":         \"overwrite_terragrunt\",\n\t\t\t\t\t\"if_disabled\":       \"skip\",\n\t\t\t\t\t\"hcl_fmt\":           nil,\n\t\t\t\t\t\"contents\": `provider \"aws\" {\n  region = \"us-east-1\"\n}\n`,\n\t\t\t\t},\n\t\t\t},\n\t\t\tgenerateBlocks.(map[string]any),\n\t\t)\n\t}\n\n\t// Make sure all inputs are merged together\n\tinputsBlock, hasInputs := rendered[\"inputs\"]\n\tif assert.True(t, hasInputs) {\n\t\tassert.Equal(\n\t\t\tt,\n\t\t\tmap[string]any{\n\t\t\t\t\"env\":        \"qa\",\n\t\t\t\t\"name\":       \"dep\",\n\t\t\t\t\"type\":       \"main\",\n\t\t\t\t\"aws_region\": \"us-east-1\",\n\t\t\t},\n\t\t\tinputsBlock.(map[string]any),\n\t\t)\n\t}\n}\n"
  },
  {
    "path": "test/integration_tofu_openbao_test.go",
    "content": "//go:build docker && tofu\n\npackage test_test\n\nimport (\n\t\"crypto/rand\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/testcontainers/testcontainers-go\"\n\ttcexec \"github.com/testcontainers/testcontainers-go/exec\"\n\t\"github.com/testcontainers/testcontainers-go/wait\"\n)\n\nconst (\n\ttestFixtureTofuStateEncryptionOpenbao = \"fixtures/tofu-state-encryption/openbao\"\n)\n\nfunc setupOpenbao(t *testing.T) (baoC *testcontainers.DockerContainer, baoToken string, baoAddr string) {\n\tt.Helper()\n\n\tbaoToken = rand.Text()\n\n\tbaoC, baoAddr = helpers.RunContainer(t, \"openbao/openbao:2.4.1\", 8200,\n\t\ttestcontainers.WithWaitStrategy(\n\t\t\twait.ForLog(\"core: vault is unsealed\"),\n\t\t),\n\t\ttestcontainers.WithEnv(map[string]string{\n\t\t\t\"BAO_DEV_ROOT_TOKEN_ID\": baoToken,\n\t\t}),\n\t)\n\n\texecOptions := []tcexec.ProcessOption{\n\t\ttcexec.WithEnv([]string{\"BAO_ADDR=http://localhost:8200\", \"VAULT_TOKEN=\" + baoToken}),\n\t}\n\n\thelpers.ContExecNoOutput(t, baoC, \"bao secrets enable transit\", execOptions...)\n\n\treturn baoC, baoToken, baoAddr\n}\n\nfunc TestTofuStateEncryptionOpenbao(t *testing.T) {\n\tt.Parallel()\n\n\tbaoC, baoToken, baoAddr := setupOpenbao(t)\n\tbaoKey := rand.Text()\n\tbaoKeyPath := \"transit/keys/\" + baoKey\n\n\texecOptions := []tcexec.ProcessOption{\n\t\ttcexec.WithEnv([]string{\"BAO_ADDR=http://localhost:8200\", \"VAULT_TOKEN=\" + baoToken}),\n\t}\n\n\thelpers.ContExecNoOutput(t, baoC, \"bao write -f \"+baoKeyPath, execOptions...)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureTofuStateEncryptionOpenbao)\n\tworkDir := filepath.Join(tmpEnvPath, testFixtureTofuStateEncryptionOpenbao)\n\tconfigPath := filepath.Join(workDir, \"terragrunt.hcl\")\n\n\thelpers.CopyAndFillMapPlaceholders(t, configPath, configPath, map[string]string{\n\t\t\"__FILL_IN_OPENBAO_KEY_NAME__\": baoKey,\n\t\t\"__FILL_IN_OPENBAO_ADDRESS__\":  baoAddr,\n\t\t\"__FILL_IN_OPENBAO_TOKEN__\":    baoToken,\n\t})\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+workDir)\n\tassert.True(t, helpers.FileIsInFolder(t, stateFile, workDir))\n\tvalidateStateIsEncrypted(t, stateFile, workDir)\n}\n"
  },
  {
    "path": "test/integration_tofu_test.go",
    "content": "//go:build tofu\n\npackage test_test\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureAutoProviderCacheDir = \"fixtures/auto-provider-cache-dir\"\n\ttestFixtureTfPathDependency     = \"fixtures/tf-path/dependency\"\n\ttestFixtureTofuHTTPEncryption   = \"fixtures/tofu-http-encryption\"\n)\n\nfunc TestAutoProviderCacheDirExperimentBasic(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureAutoProviderCacheDir)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureAutoProviderCacheDir)\n\tunitPath := filepath.Join(testPath, \"basic\", \"unit\")\n\n\tcmd := \"terragrunt init --log-level debug --non-interactive --working-dir \" + unitPath\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"using cache key for version files\")\n\tassert.Contains(t, stderr, \"Auto provider cache dir enabled\")\n\tassert.Regexp(t, `(Reusing previous version|shared cache directory)`, stdout)\n}\n\nfunc TestAutoProviderCacheDirExperimentRunAll(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureAutoProviderCacheDir)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureAutoProviderCacheDir)\n\tunitPath := filepath.Join(testPath, \"basic\", \"unit\")\n\n\t// clone the unit dir 9 times\n\tfor i := range 9 {\n\t\thelpers.CopyDir(t, unitPath, filepath.Join(testPath, \"unit-\"+strconv.Itoa(i)))\n\t}\n\n\tcmd := \"terragrunt run --all init --log-level debug --non-interactive --working-dir \" + testPath\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\trequire.NoError(t, err)\n\n\tassert.Contains(t, stderr, \"Auto provider cache dir enabled\")\n\tassert.Contains(t, stderr, \"using cache key for version files\")\n\tassert.Regexp(t, `(Reusing previous version|shared cache directory)`, stdout)\n}\n\nfunc TestAutoProviderCacheDirDisabled(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureAutoProviderCacheDir)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureAutoProviderCacheDir)\n\tunitPath := filepath.Join(testPath, \"basic\", \"unit\")\n\n\tcmd := \"terragrunt init --log-level debug --no-auto-provider-cache-dir --non-interactive --working-dir \" + unitPath\n\n\tstdout, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\trequire.NoError(t, err)\n\n\tassert.NotContains(t, stderr, \"Auto provider cache dir enabled\")\n\tassert.NotRegexp(t, `Using hashicorp\\/null [^ ]+ from the shared cache directory`, stdout)\n}\n\nfunc TestTfPathRespectedForDependencies(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureTfPathDependency)\n\trootPath := helpers.CopyEnvironment(t, testFixtureTfPathDependency)\n\ttestPath := filepath.Join(rootPath, testFixtureTfPathDependency)\n\ttestPath, err := filepath.EvalSymlinks(testPath)\n\trequire.NoError(t, err)\n\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(\n\t\tt,\n\t\tfmt.Sprintf(\n\t\t\t\"terragrunt run --all --non-interactive --tf-path %s --working-dir %s -- apply\",\n\t\t\tfilepath.Join(testPath, \"custom-tf.sh\"),\n\t\t\ttestPath,\n\t\t),\n\t)\n\trequire.NoError(\n\t\tt,\n\t\terr,\n\t\t\"Expected tf-path to be respected for dependency lookups, but it was overridden by terraform_binary in config\",\n\t)\n\t// Accept relative (./app, ./dep) or absolute paths; PWD in the script is set by the runner.\n\tassert.Regexp(t, `Custom TF script used in .*[/\\\\]app.*!`, stderr)\n\tassert.Regexp(t, `Custom TF script used in .*[/\\\\]dep.*!`, stderr)\n}\n\n// TestHTTPBackendEncryptionDependencyFails tests that OpenTofu state encryption\n// with HTTP backend works correctly when reading dependency outputs.\nfunc TestHTTPBackendEncryptionDependencyFails(t *testing.T) {\n\tt.Parallel()\n\n\tctx, cancel := context.WithCancel(t.Context())\n\tdefer cancel()\n\n\tserverURL := runHTTPStateServer(t, ctx)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureTofuHTTPEncryption)\n\thelpers.CleanupTerraformFolder(t, tmpEnvPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureTofuHTTPEncryption)\n\n\trootHclPath := filepath.Join(testPath, \"root.hcl\")\n\trootHclContent, err := os.ReadFile(rootHclPath)\n\trequire.NoError(t, err)\n\n\tnewContent := strings.ReplaceAll(string(rootHclContent), \"__HTTP_SERVER_URL__\", serverURL)\n\terr = os.WriteFile(rootHclPath, []byte(newContent), 0644)\n\trequire.NoError(t, err)\n\n\tcmd := \"terragrunt run --all apply --non-interactive --working-dir \" + testPath\n\t_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, cmd)\n\trequire.NoError(t, err)\n\n\tassert.NotContains(t, stderr, \"This state file is encrypted and can not be read without an encryption configuration\")\n}\n\n// runHTTPStateServer starts an HTTP server that implements the Terraform HTTP backend API.\n// It returns the server URL.\nfunc runHTTPStateServer(t *testing.T, ctx context.Context) string {\n\tt.Helper()\n\n\tvar (\n\t\tstates = make(map[string][]byte)\n\t\tlocks  = make(map[string]string)\n\t\tmu     sync.RWMutex\n\t)\n\n\tmux := http.NewServeMux()\n\tmux.HandleFunc(\"/state/\", func(w http.ResponseWriter, r *http.Request) {\n\t\tauth := r.Header.Get(\"Authorization\")\n\t\tif auth == \"\" {\n\t\t\thttp.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\n\t\tconst prefix = \"Basic \"\n\t\tif !strings.HasPrefix(auth, prefix) {\n\t\t\thttp.Error(w, \"Invalid auth header\", http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\n\t\tdecoded, err := base64.StdEncoding.DecodeString(auth[len(prefix):])\n\t\tif err != nil {\n\t\t\thttp.Error(w, \"Invalid auth encoding\", http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\n\t\tcredentials := string(decoded)\n\t\tif credentials != \"admin:secret\" {\n\t\t\thttp.Error(w, \"Invalid credentials\", http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\n\t\tpath := r.URL.Path\n\n\t\tswitch r.Method {\n\t\tcase http.MethodGet:\n\t\t\tmu.RLock()\n\n\t\t\tstate, ok := states[path]\n\n\t\t\tmu.RUnlock()\n\n\t\t\tif !ok {\n\t\t\t\thttp.Error(w, \"Not Found\", http.StatusNotFound)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\tw.Write(state)\n\n\t\tcase http.MethodPost:\n\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tmu.Lock()\n\n\t\t\tstates[path] = body\n\n\t\t\tmu.Unlock()\n\n\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\tcase \"LOCK\":\n\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tvar lockInfo struct {\n\t\t\t\tID string `json:\"ID\"`\n\t\t\t}\n\t\t\tif err := json.Unmarshal(body, &lockInfo); err != nil {\n\t\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tmu.Lock()\n\n\t\t\tif existingLock, ok := locks[path]; ok {\n\t\t\t\tmu.Unlock()\n\t\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t\tw.WriteHeader(http.StatusLocked)\n\t\t\t\tjson.NewEncoder(w).Encode(map[string]string{\"ID\": existingLock})\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tlocks[path] = lockInfo.ID\n\n\t\t\tmu.Unlock()\n\n\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\tcase \"UNLOCK\":\n\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tvar lockInfo struct {\n\t\t\t\tID string `json:\"ID\"`\n\t\t\t}\n\t\t\tif err := json.Unmarshal(body, &lockInfo); err != nil {\n\t\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tmu.Lock()\n\t\t\tdelete(locks, path)\n\t\t\tmu.Unlock()\n\n\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\tdefault:\n\t\t\thttp.Error(w, \"Method not allowed\", http.StatusMethodNotAllowed)\n\t\t}\n\t})\n\n\tvar lc net.ListenConfig\n\n\tlistener, err := lc.Listen(ctx, \"tcp\", \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\n\tserver := &http.Server{\n\t\tHandler: mux,\n\t}\n\n\tgo func() {\n\t\tif err := server.Serve(listener); err != nil && err != http.ErrServerClosed {\n\t\t\tt.Logf(\"HTTP server error: %v\", err)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tserver.Shutdown(context.WithoutCancel(ctx))\n\t}()\n\n\treturn \"http://\" + listener.Addr().String()\n}\n"
  },
  {
    "path": "test/integration_units_reading_test.go",
    "content": "//go:build sops\n\n// sops tests assume that you're going to import the test_pgp_key.asc file into your GPG keyring before\n// running the tests. We're not gonna assume that everyone is going to do this, so we're going to skip\n// these tests by default.\n//\n// You can import the key by running the following command:\n//\n//\tgpg --import --no-tty --batch --yes ./test/fixtures/sops/test_pgp_key.asc\n\npackage test_test\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/report\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureUnitsReading = \"fixtures/units-reading/\"\n)\n\nfunc TestSOPSUnitsReading(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testFixtureUnitsReading)\n\n\ttestCases := []struct {\n\t\tname           string\n\t\tunitsReading   []string\n\t\tunitsExcluding []string\n\t\tunitsIncluding []string\n\t\texpectedUnits  []string\n\t}{\n\t\t{\n\t\t\tname:         \"empty\",\n\t\t\tunitsReading: []string{},\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"including\",\n\t\t\t\t\"indirect\",\n\t\t\t\t\"reading-from-tf\",\n\t\t\t\t\"reading-hcl\",\n\t\t\t\t\"reading-hcl-and-tfvars\",\n\t\t\t\t\"reading-json\",\n\t\t\t\t\"reading-sops\",\n\t\t\t\t\"reading-tfvars\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"reading_hcl\",\n\t\t\tunitsReading: []string{\n\t\t\t\t\"shared.hcl\",\n\t\t\t},\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"including\",\n\t\t\t\t\"reading-hcl\",\n\t\t\t\t\"reading-hcl-and-tfvars\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"reading_tfvars\",\n\t\t\tunitsReading: []string{\n\t\t\t\t\"shared.tfvars\",\n\t\t\t},\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"reading-tfvars\",\n\t\t\t\t\"reading-hcl-and-tfvars\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"reading_json\",\n\t\t\tunitsReading: []string{\n\t\t\t\t\"shared.json\",\n\t\t\t},\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"reading-from-tf\",\n\t\t\t\t\"reading-json\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"reading_sops\",\n\t\t\tunitsReading: []string{\n\t\t\t\t\"secrets.txt\",\n\t\t\t},\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"reading-sops\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"reading_from_hcl_with_exclude\",\n\t\t\tunitsReading: []string{\n\t\t\t\t\"shared.hcl\",\n\t\t\t},\n\t\t\tunitsExcluding: []string{\n\t\t\t\t\"reading-hcl-and-tfvars\",\n\t\t\t},\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"including\",\n\t\t\t\t\"reading-hcl\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"reading_from_hcl_with_include\",\n\t\t\tunitsReading: []string{\n\t\t\t\t\"shared.hcl\",\n\t\t\t},\n\t\t\tunitsIncluding: []string{\n\t\t\t\t\"reading-tfvars\",\n\t\t\t},\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"including\",\n\t\t\t\t\"reading-hcl\",\n\t\t\t\t\"reading-hcl-and-tfvars\",\n\t\t\t\t\"reading-tfvars\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"reading_from_hcl_with_include_and_exclude\",\n\t\t\tunitsReading: []string{\n\t\t\t\t\"shared.hcl\",\n\t\t\t\t\"shared.tfvars\",\n\t\t\t},\n\t\t\tunitsIncluding: []string{\n\t\t\t\t\"reading-tfvars\",\n\t\t\t},\n\t\t\tunitsExcluding: []string{\n\t\t\t\t\"reading-hcl-and-tfvars\",\n\t\t\t},\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"including\",\n\t\t\t\t\"reading-hcl\",\n\t\t\t\t\"reading-tfvars\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"indirect\",\n\t\t\tunitsReading: []string{\n\t\t\t\tfilepath.Join(\"indirect\", \"src\", \"test.txt\"),\n\t\t\t},\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"indirect\",\n\t\t\t},\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.Parallel()\n\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureUnitsReading)\n\t\t\trootPath := filepath.Join(tmpEnvPath, testFixtureUnitsReading)\n\t\t\trootPath, err := filepath.EvalSymlinks(rootPath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcmd := \"terragrunt run --all plan --non-interactive --working-dir \" + rootPath + \" --report-file \" + helpers.ReportFile\n\n\t\t\tfor _, f := range tc.unitsReading {\n\t\t\t\tcmd = cmd + \" --queue-include-units-reading \" + f\n\t\t\t}\n\n\t\t\tfor _, unit := range tc.unitsIncluding {\n\t\t\t\tcmd = cmd + \" --queue-include-dir \" + unit\n\t\t\t}\n\n\t\t\tfor _, unit := range tc.unitsExcluding {\n\t\t\t\tcmd = cmd + \" --queue-exclude-dir \" + unit\n\t\t\t}\n\n\t\t\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, cmd)\n\t\t\trequire.NoError(t, err)\n\n\t\t\treportFilePath := filepath.Join(rootPath, helpers.ReportFile)\n\t\t\tassert.FileExists(t, reportFilePath, \"Report file should exist\")\n\n\t\t\truns, err := report.ParseJSONRunsFromFile(reportFilePath)\n\t\t\trequire.NoError(t, err, \"Should be able to parse report file\")\n\n\t\t\tassert.ElementsMatch(t, tc.expectedUnits, runs.Names())\n\t\t})\n\t}\n}\n\nfunc TestUnitsReadingWithFilter(t *testing.T) {\n\tt.Parallel()\n\n\ttestCases := []struct {\n\t\tname           string\n\t\tunitsReading   []string\n\t\tunitsExcluding []string\n\t\tunitsIncluding []string\n\t\texpectedUnits  []string\n\t}{\n\t\t{\n\t\t\tname:         \"empty\",\n\t\t\tunitsReading: []string{},\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"including\",\n\t\t\t\t\"indirect\",\n\t\t\t\t\"reading-from-tf\",\n\t\t\t\t\"reading-hcl\",\n\t\t\t\t\"reading-hcl-and-tfvars\",\n\t\t\t\t\"reading-json\",\n\t\t\t\t\"reading-sops\",\n\t\t\t\t\"reading-tfvars\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"reading_hcl\",\n\t\t\tunitsReading: []string{\n\t\t\t\t\"shared.hcl\",\n\t\t\t},\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"including\",\n\t\t\t\t\"reading-hcl\",\n\t\t\t\t\"reading-hcl-and-tfvars\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"reading_tfvars\",\n\t\t\tunitsReading: []string{\n\t\t\t\t\"shared.tfvars\",\n\t\t\t},\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"reading-tfvars\",\n\t\t\t\t\"reading-hcl-and-tfvars\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"reading_json\",\n\t\t\tunitsReading: []string{\n\t\t\t\t\"shared.json\",\n\t\t\t},\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"reading-from-tf\",\n\t\t\t\t\"reading-json\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"reading_sops\",\n\t\t\tunitsReading: []string{\n\t\t\t\t\"secrets.txt\",\n\t\t\t},\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"reading-sops\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"reading_from_hcl_with_exclude\",\n\t\t\tunitsReading: []string{\n\t\t\t\t\"shared.hcl\",\n\t\t\t},\n\t\t\tunitsExcluding: []string{\n\t\t\t\t\"reading-hcl-and-tfvars\",\n\t\t\t},\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"including\",\n\t\t\t\t\"reading-hcl\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"reading_from_hcl_with_include\",\n\t\t\tunitsReading: []string{\n\t\t\t\t\"shared.hcl\",\n\t\t\t},\n\t\t\tunitsIncluding: []string{\n\t\t\t\t\"reading-tfvars\",\n\t\t\t},\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"including\",\n\t\t\t\t\"reading-hcl\",\n\t\t\t\t\"reading-hcl-and-tfvars\",\n\t\t\t\t\"reading-tfvars\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"reading_from_hcl_with_include_and_exclude\",\n\t\t\tunitsReading: []string{\n\t\t\t\t\"shared.hcl\",\n\t\t\t\t\"shared.tfvars\",\n\t\t\t},\n\t\t\tunitsIncluding: []string{\n\t\t\t\t\"reading-tfvars\",\n\t\t\t},\n\t\t\tunitsExcluding: []string{\n\t\t\t\t\"reading-hcl-and-tfvars\",\n\t\t\t},\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"including\",\n\t\t\t\t\"reading-hcl\",\n\t\t\t\t\"reading-tfvars\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"indirect\",\n\t\t\tunitsReading: []string{\n\t\t\t\tfilepath.Join(\"indirect\", \"src\", \"test.txt\"),\n\t\t\t},\n\t\t\texpectedUnits: []string{\n\t\t\t\t\"indirect\",\n\t\t\t},\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.Parallel()\n\n\t\t\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureUnitsReading)\n\t\t\trootPath := filepath.Join(tmpEnvPath, testFixtureUnitsReading)\n\t\t\trootPath, err := filepath.EvalSymlinks(rootPath)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcmd := \"terragrunt run --all plan --non-interactive --working-dir \" + rootPath + \" --report-file \" + helpers.ReportFile\n\n\t\t\tfor _, f := range tc.unitsReading {\n\t\t\t\tcmd = cmd + \" --filter reading=\" + filepath.Join(rootPath, f)\n\t\t\t}\n\n\t\t\tfor _, unit := range tc.unitsIncluding {\n\t\t\t\tcmd = cmd + \" --filter \" + filepath.Join(rootPath, unit)\n\t\t\t}\n\n\t\t\tfor _, unit := range tc.unitsExcluding {\n\t\t\t\tcmd = cmd + \" --filter !\" + filepath.Join(rootPath, unit)\n\t\t\t}\n\n\t\t\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, cmd)\n\t\t\trequire.NoError(t, err)\n\n\t\t\treportFilePath := filepath.Join(rootPath, helpers.ReportFile)\n\t\t\tassert.FileExists(t, reportFilePath, \"Report file should exist\")\n\n\t\t\truns, err := report.ParseJSONRunsFromFile(reportFilePath)\n\t\t\trequire.NoError(t, err, \"Should be able to parse report file\")\n\n\t\t\tassert.ElementsMatch(t, tc.expectedUnits, runs.Names())\n\t\t})\n\t}\n}\n\n// TestQueueStrictIncludeWithUnitsReading tests that --queue-include-units-reading works correctly\n// with --queue-include-units-reading when no --queue-include-dir is specified.\n// This reproduces the bug where units reading the specified file were not included.\nfunc TestQueueStrictIncludeWithUnitsReading(t *testing.T) {\n\tt.Parallel()\n\n\tcleanupTerraformFolder(t, testFixtureUnitsReading)\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureUnitsReading)\n\trootPath := filepath.Join(tmpEnvPath, testFixtureUnitsReading)\n\trootPath, err := filepath.EvalSymlinks(rootPath)\n\trequire.NoError(t, err)\n\n\t// Test the bug scenario: --queue-include-units-reading\n\t// without --queue-include-dir. Units reading shared.hcl should be included.\n\tcmd := \"terragrunt run --all plan --non-interactive --working-dir \" + rootPath +\n\t\t\" --queue-include-units-reading shared.hcl --report-file \" + helpers.ReportFile\n\n\t_, _, err = helpers.RunTerragruntCommandWithOutput(t, cmd)\n\trequire.NoError(t, err, \"Command should succeed and include units reading shared.hcl\")\n\n\treportFilePath := filepath.Join(rootPath, helpers.ReportFile)\n\tassert.FileExists(t, reportFilePath, \"Report file should exist\")\n\n\truns, err := report.ParseJSONRunsFromFile(reportFilePath)\n\trequire.NoError(t, err, \"Should be able to parse report file\")\n\n\t// Units that read shared.hcl should be included\n\texpectedUnits := []string{\n\t\t\"including\",\n\t\t\"reading-hcl\",\n\t\t\"reading-hcl-and-tfvars\",\n\t}\n\tassert.ElementsMatch(t, expectedUnits, runs.Names(),\n\t\t\"Units reading shared.hcl should be included when using --queue-include-units-reading\")\n}\n"
  },
  {
    "path": "test/integration_unix_test.go",
    "content": "//go:build linux || darwin\n// +build linux darwin\n\npackage test_test\n\nimport (\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureDownloadPath                      = \"fixtures/download\"\n\ttestFixtureLocalRelativeArgsUnixDownloadPath = \"fixtures/download/local-relative-extra-args-unix\"\n)\n\nfunc TestLocalWithRelativeExtraArgsUnix(t *testing.T) {\n\tt.Parallel()\n\n\ttmpEnvPath := helpers.CopyEnvironment(t, testFixtureDownloadPath)\n\ttestPath := filepath.Join(tmpEnvPath, testFixtureLocalRelativeArgsUnixDownloadPath)\n\n\ttestPath, err := filepath.EvalSymlinks(testPath)\n\trequire.NoError(t, err)\n\n\thelpers.CleanupTerraformFolder(t, testPath)\n\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+testPath)\n\n\t// Run a second time to make sure the temporary folder can be reused without errors\n\thelpers.RunTerragrunt(t, \"terragrunt apply -auto-approve --non-interactive --working-dir \"+testPath)\n}\n"
  },
  {
    "path": "test/integration_windows_test.go",
    "content": "//go:build windows\n// +build windows\n\npackage test_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/gruntwork-io/terragrunt/internal/util\"\n\t\"github.com/gruntwork-io/terragrunt/test/helpers\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\ttestFixtureDownloadPath                         = \"fixtures/download\"\n\ttestFixtureLocalRelativeArgsWindowsDownloadPath = \"fixtures/download/local-windows\"\n\ttestFixtureManifestRemoval                      = \"fixtures/manifest-removal\"\n\ttestFixtureTflintNoIssuesFound                  = \"fixtures/tflint/no-issues-found\"\n\n\ttempDir = `C:\\tmp`\n)\n\nfunc TestMain(m *testing.M) {\n\t// By default, TempDir creates a temporary directory inside the user directory\n\t// `C:/Users/circleci/AppData/Local/Temp/`, which ends up exceeding the maximum allowed length\n\t// and causes the error: \"fatal: '$GIT_DIR' too big\". Example:\n\t// \"C:/Users/circleci/AppData/Local/Temp/TestWindowsLocalWithRelativeExtraArgsWindows1263358614/001/fixtures/download/local-windows/.terragrunt-cache/rviFlp3V5mrXldwi6Hbi8p2rDL0/U0tL3quoR7Yt-oR6jROJomrYpTs\".\n\t// Solution, shorten the TMP path.\n\n\tenvVars := map[string]string{\"TMP\": \"\", \"TEMP\": \"\"}\n\n\t// Save current values to restore them at the end.\n\tfor name := range envVars {\n\t\tenvVars[name] = os.Getenv(name)\n\t}\n\n\tdefer func() {\n\t\t// Restore previous values.\n\t\tfor name, val := range envVars {\n\t\t\tos.Setenv(name, val)\n\t\t}\n\t}()\n\n\tif _, err := os.Stat(tempDir); os.IsNotExist(err) {\n\t\tif err := os.Mkdir(tempDir, os.ModePerm); err != nil {\n\t\t\tfmt.Printf(\"Failed to create temp dir due to error: %v\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t} else {\n\t\t// Verify write permissions\n\t\ttestFile := filepath.Join(tempDir, \".write_test\")\n\t\tif err := os.WriteFile(testFile, []byte(\"\"), 0666); err != nil {\n\t\t\tfmt.Printf(\"Temp dir %s is not writable: %v\", tempDir, err)\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\tos.Remove(testFile)\n\t}\n\n\t// Set temporary values.\n\tfor name := range envVars {\n\t\tif err := os.Setenv(name, tempDir); err != nil {\n\t\t\tfmt.Printf(\"Failed to set env var %s=%s due to error: %v\", name, tempDir, err)\n\t\t\tos.Exit(1)\n\t\t}\n\t}\n\n\tos.Exit(m.Run())\n}\n\nfunc TestWindowsLocalWithRelativeExtraArgsWindows(t *testing.T) {\n\tt.Parallel()\n\n\trootPath := CopyEnvironmentWithTflint(t, testFixtureDownloadPath)\n\tmodulePath := filepath.Join(rootPath, testFixtureLocalRelativeArgsWindowsDownloadPath)\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --working-dir %s\", modulePath))\n\n\t// Run a second time to make sure the temporary folder can be reused without errors\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt apply -auto-approve --non-interactive --working-dir %s\", modulePath))\n}\n\n// TestWindowsTerragruntSourceMapDebug copies the test/fixtures/source-map directory to a new Windows path\n// and then ensures that the TG_SOURCE_MAP env var can be used to swap out git sources for local modules\nfunc TestWindowsTerragruntSourceMapDebug(t *testing.T) {\n\ttestCases := []struct {\n\t\tname string\n\t}{\n\t\t{\n\t\t\tname: \"multiple-match\",\n\t\t},\n\t\t{\n\t\t\tname: \"multiple-with-dependency\",\n\t\t},\n\t}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tfixtureSourceMapPath := \"fixtures/source-map\"\n\t\t\thelpers.CleanupTerraformFolder(t, fixtureSourceMapPath)\n\t\t\ttargetPath := \"C:\\\\test\\\\infrastructure-modules/\"\n\t\t\tCopyEnvironmentToPath(t, fixtureSourceMapPath, targetPath)\n\t\t\trootPath := filepath.Join(targetPath, fixtureSourceMapPath)\n\n\t\t\tt.Setenv(\n\t\t\t\t\"TG_SOURCE_MAP\",\n\t\t\t\tstrings.Join(\n\t\t\t\t\t[]string{\n\t\t\t\t\t\tfmt.Sprintf(\"git::ssh://git@github.com/gruntwork-io/i-dont-exist.git=%s\", targetPath),\n\t\t\t\t\t\tfmt.Sprintf(\"git::ssh://git@github.com/gruntwork-io/another-dont-exist.git=%s\", targetPath),\n\t\t\t\t\t},\n\t\t\t\t\t\",\",\n\t\t\t\t),\n\t\t\t)\n\t\t\ttgPath := filepath.Join(rootPath, tc.name)\n\t\t\ttgArgs := fmt.Sprintf(\"terragrunt run --all apply --non-interactive --working-dir '%s'\", tgPath)\n\t\t\thelpers.RunTerragrunt(t, tgArgs)\n\t\t})\n\t}\n}\n\n// Get rid of this once we have no internal tflint\nfunc TestWindowsTflintIsInvoked(t *testing.T) {\n\tout := new(bytes.Buffer)\n\terrOut := new(bytes.Buffer)\n\trootPath := CopyEnvironmentWithTflint(t, testFixtureTflintNoIssuesFound)\n\tmodulePath := filepath.Join(rootPath, testFixtureTflintNoIssuesFound)\n\terr := helpers.RunTerragruntCommand(t, fmt.Sprintf(\"terragrunt plan --log-level debug --working-dir %s\", modulePath), out, errOut)\n\tassert.NoError(t, err)\n\n\tassert.NotContains(t, errOut.String(), \"Error while running tflint with args:\")\n\tassert.NotContains(t, errOut.String(), \"Tflint found issues in the project. Check for the tflint logs above.\")\n\n\t// TFLint config should be found in the original working directory, not inside .terragrunt-cache\n\t// The config path should end with .tflint.hcl but NOT be inside .terragrunt-cache\n\t// Use cross-platform regex patterns that handle both Unix / and Windows \\ path separators\n\tfound, err := regexp.MatchString(`--config\\s+[^\\s]*\\.tflint\\.hcl`, errOut.String())\n\tassert.NoError(t, err)\n\tassert.True(t, found, \"Expected tflint to be invoked with --config pointing to .tflint.hcl\")\n\tassert.NotRegexp(t, `--config\\s+[^\\s]*[/\\\\]?\\.terragrunt-cache`, errOut.String(), \"TFLint config should not be inside cache directory\")\n}\n\nfunc TestWindowsManifestFileIsRemoved(t *testing.T) {\n\tout := new(bytes.Buffer)\n\terrOut := new(bytes.Buffer)\n\trootPath := CopyEnvironmentWithTflint(t, testFixtureManifestRemoval)\n\tmodulePath := filepath.Join(rootPath, testFixtureManifestRemoval, \"app\")\n\terr := helpers.RunTerragruntCommand(t, fmt.Sprintf(\"terragrunt plan --non-interactive --working-dir %s\", modulePath), out, errOut)\n\tassert.NoError(t, err)\n\n\tinfo1, err := fileInfo(modulePath, \".terragrunt-module-manifest\")\n\tassert.NoError(t, err)\n\tassert.NotNil(t, info1)\n\n\tout = new(bytes.Buffer)\n\terrOut = new(bytes.Buffer)\n\terr = helpers.RunTerragruntCommand(t, fmt.Sprintf(\"terragrunt plan --non-interactive --working-dir %s\", modulePath), out, errOut)\n\tassert.NoError(t, err)\n\n\tinfo2, err := fileInfo(modulePath, \".terragrunt-module-manifest\")\n\tassert.NoError(t, err)\n\tassert.NotNil(t, info2)\n\n\t// ensure that .terragrunt-module-manifest was recreated\n\tassert.True(t, (*info2).ModTime().After((*info1).ModTime()))\n}\n\nfunc fileInfo(path, fileName string) (*os.FileInfo, error) {\n\tvar fileInfo *os.FileInfo\n\terr := filepath.Walk(path, func(path string, info os.FileInfo, err error) error {\n\t\tif fileInfo != nil {\n\t\t\treturn nil\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif !info.IsDir() && info.Name() == fileName {\n\t\t\tfileInfo = &info\n\t\t\treturn nil\n\t\t}\n\t\treturn nil\n\t})\n\treturn fileInfo, err\n}\n\nfunc TestWindowsFindParent(t *testing.T) {\n\tt.Parallel()\n\n\thelpers.CleanupTerraformFolder(t, testFixtureFindParent)\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt run --all plan --non-interactive --working-dir %s\", testFixtureFindParent))\n\n\t// second run shouldn't fail with find_in_parent_folders(\"root.hcl\") issue\n\thelpers.RunTerragrunt(t, \"terragrunt run --all --non-interactive --working-dir \"+testFixtureFindParent+\" -- apply -auto-approve\")\n}\n\nfunc TestWindowsScaffold(t *testing.T) {\n\tt.Parallel()\n\n\t// create temp dir\n\ttmpDir, err := os.MkdirTemp(\"\", \"terragrunt-test\")\n\tassert.NoError(t, err)\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt scaffold github.com/gruntwork-io/terragrunt-infrastructure-modules-example//modules/mysql --working-dir '%s'\", tmpDir))\n\n\t// check that terragrunt.hcl was created\n\t_, err = os.Stat(filepath.Join(tmpDir, \"terragrunt.hcl\"))\n\tassert.NoError(t, err)\n}\n\nfunc TestWindowsScaffoldRef(t *testing.T) {\n\tt.Parallel()\n\n\t// create temp dir\n\ttmpDir, err := os.MkdirTemp(\"\", \"terragrunt-test\")\n\tassert.NoError(t, err)\n\n\thelpers.RunTerragrunt(t, fmt.Sprintf(\"terragrunt scaffold github.com/gruntwork-io/terragrunt-infrastructure-modules-example//modules/mysql?ref=v0.8.1 --working-dir '%s'\", tmpDir))\n\n\t// check that terragrunt.hcl was created\n\t_, err = os.Stat(filepath.Join(tmpDir, \"terragrunt.hcl\"))\n\tassert.NoError(t, err)\n}\n\nfunc CopyEnvironmentToPath(t *testing.T, environmentPath, targetPath string) {\n\tif err := os.MkdirAll(targetPath, 0777); err != nil {\n\t\tt.Fatalf(\"Failed to create temp dir %s due to error %v\", targetPath, err)\n\t}\n\n\tcopyErr := util.CopyFolderContents(createLogger(), environmentPath, filepath.Join(targetPath, environmentPath), \".terragrunt-test\", nil, nil)\n\trequire.NoError(t, copyErr)\n}\n\nfunc CopyEnvironmentWithTflint(t *testing.T, environmentPath string) string {\n\tcurrentDir, err := os.Getwd()\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to get current directory: %v\", err)\n\t}\n\ttmpDir, err := os.MkdirTemp(currentDir, \"terragrunt-test\")\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to create temp dir due to error: %v\", err)\n\t}\n\n\tt.Logf(\"Copying %s to %s\", environmentPath, tmpDir)\n\n\trequire.NoError(\n\t\tt,\n\t\tutil.CopyFolderContents(\n\t\t\tcreateLogger(),\n\t\t\tenvironmentPath,\n\t\t\tfilepath.Join(tmpDir, environmentPath),\n\t\t\t\".terragrunt-test\",\n\t\t\t[]string{\".tflint.hcl\"},\n\t\t\t[]string{},\n\t\t),\n\t)\n\n\treturn tmpDir\n}\n"
  }
]