[{"data":1,"prerenderedAt":629},["ShallowReactive",2],{"NoscriptNav_XrRK2e2e8meJ0jKVGkb5ULGQDVi3UiFQ9nupAr7Yns":3,"\u002Freports\u002Fforge-specific-repository-folders":8},["Island",4],{"key":5,"result":6},"NoscriptNav_XrRK2e2e8meJ0jKVGkb5ULGQDVi3UiFQ9nupAr7Yns",{"head":7},{},{"id":9,"title":10,"authors":11,"body":13,"canonicalUrl":615,"canonicalWebsiteName":616,"category":617,"date":618,"description":619,"extension":620,"featured":621,"fullWidthLayout":621,"image":622,"imageAlt":622,"location":622,"meta":623,"metaImage":622,"navigation":624,"path":625,"seo":626,"stem":627,"venue":622,"venueUrl":622,"__hash__":628},"reports\u002Freports\u002Fforge-specific-repository-folders.md","Forge-Specific Repository Folders",[12],"andrew",{"type":14,"value":15,"toc":607},"minimark",[16,26,31,34,78,99,106,114,124,128,134,165,180,183,187,190,211,232,242,246,252,265,272,275,281,295,301,370,383,387,398,402,416,453,471,487,519,535,547,556,586,589,603],[17,18,19,20,25],"p",{},"Git doesn't know about CI, code review, or issue templates, but every forge that hosts git repositories has added these features through the same trick: a dot-folder in your repo root that the forge reads on push. The folder names differ, the contents overlap in some places and diverge in others, and the portability story between them is worse than you'd expect. A companion to my earlier post on ",[21,22,24],"a",{"href":23},"\u002Freports\u002Fgit-magic-files","git's magic files",".",[27,28,30],"h3",{"id":29},"github",".github\u002F",[17,32,33],{},"GitHub's folder holds:",[35,36,37,50,60,66,72],"ul",{},[38,39,40,44,45,49],"li",{},[41,42,43],"strong",{},"workflows\u002F"," — GitHub Actions CI\u002FCD configuration (",[46,47,48],"code",{},".github\u002Fworkflows\u002F*.yml",")",[38,51,52,55,56,59],{},[41,53,54],{},"ISSUE_TEMPLATE\u002F"," and ",[41,57,58],{},"PULL_REQUEST_TEMPLATE\u002F"," — issue and PR templates",[38,61,62,65],{},[41,63,64],{},"dependabot.yml"," — automated dependency updates",[38,67,68,71],{},[41,69,70],{},"CODEOWNERS"," — required reviewers for paths",[38,73,74,77],{},[41,75,76],{},"FUNDING.yml"," — sponsor button configuration",[17,79,80,81,83,84,87,88,87,91,94,95,98],{},"GitHub also reads some files from the repo root or from ",[46,82,30],{},": ",[41,85,86],{},"SECURITY.md",", ",[41,89,90],{},"CONTRIBUTING.md",[41,92,93],{},"CODE_OF_CONDUCT.md",". ",[41,96,97],{},"LICENSE"," must be in the repo root for GitHub's license detection to pick it up.",[17,100,101,102,105],{},"The ",[46,103,104],{},".github\u002Fworkflows\u002F"," directory contains YAML files defining Actions workflows. Each file is a separate workflow that runs on events like push, pull request, or schedule.",[17,107,108,109,113],{},"CODEOWNERS uses ",[21,110,112],{"href":111},"\u002Freports\u002Fthe-many-flavors-of-ignore-files","gitignore-style"," glob patterns to map paths to GitHub users or teams who must review changes:",[115,116,121],"pre",{"className":117,"code":119,"language":120},[118],"language-text","# .github\u002FCODEOWNERS\n*.js @frontend-team\n\u002Fdocs\u002F @docs-team\n* @admins\n","text",[46,122,119],{"__ignoreMap":123},"",[27,125,127],{"id":126},"gitlab",".gitlab\u002F",[17,129,130,131,133],{},"GitLab uses ",[46,132,127],{}," for:",[35,135,136,142,148,154,159],{},[38,137,138,141],{},[41,139,140],{},"ci\u002F"," — reusable CI\u002FCD templates",[38,143,144,147],{},[41,145,146],{},"merge_request_templates\u002F"," — MR templates",[38,149,150,153],{},[41,151,152],{},"issue_templates\u002F"," — issue templates",[38,155,156,158],{},[41,157,70],{}," — approval rules",[38,160,161,164],{},[41,162,163],{},"changelog_config.yml"," — built-in changelog generation config",[17,166,167,168,171,172,175,176,179],{},"GitLab's main CI config is ",[46,169,170],{},".gitlab-ci.yml"," at the repo root, not in the folder. Projects often keep reusable CI templates in ",[46,173,174],{},".gitlab\u002Fci\u002F"," and pull them in with ",[46,177,178],{},"include:local",", though the directory name is convention rather than something GitLab treats specially.",[17,181,182],{},"GitLab's CODEOWNERS works similarly to GitHub's but with different approval rule options and integration with GitLab's approval workflows.",[27,184,186],{"id":185},"gitea-and-forgejo",".gitea\u002F and .forgejo\u002F",[17,188,189],{},"Gitea and Forgejo (a fork of Gitea) support:",[35,191,192,204],{},[38,193,194,196,197,200,201,49],{},[41,195,43],{}," — Gitea\u002FForgejo Actions (",[46,198,199],{},".gitea\u002Fworkflows\u002F*.yml"," or ",[46,202,203],{},".forgejo\u002Fworkflows\u002F*.yml",[38,205,206,55,208,210],{},[41,207,54],{},[41,209,58],{}," — templates",[17,212,213,214,217,218,217,221,223,224,217,226,228,229,231],{},"Forgejo checks ",[46,215,216],{},".forgejo\u002F"," then ",[46,219,220],{},".gitea\u002F",[46,222,30],{}," in that order, while Gitea checks ",[46,225,220],{},[46,227,30],{},", so you can keep shared config in ",[46,230,30],{}," and add platform-specific overrides in the forge's own folder.",[17,233,234,235,238,239,25],{},"Gitea's CODEOWNERS uses Go regexp instead of gitignore-style globs. Patterns look like ",[46,236,237],{},".*\\.js$"," instead of ",[46,240,241],{},"*.js",[27,243,245],{"id":244},"bitbucket",".bitbucket\u002F",[17,247,248,249,251],{},"Bitbucket keeps two files in ",[46,250,245],{},":",[35,253,254,259],{},[38,255,256,258],{},[41,257,70],{}," — required reviewers",[38,260,261,264],{},[41,262,263],{},"teams.yaml"," — ad-hoc reviewer groups",[17,266,267,268,271],{},"CI config lives at ",[46,269,270],{},"bitbucket-pipelines.yml"," in the repo root, similar to GitLab's approach.",[17,273,274],{},"Bitbucket's CODEOWNERS has reviewer selection strategies baked into the syntax:",[115,276,279],{"className":277,"code":278,"language":120},[118],"# .bitbucket\u002FCODEOWNERS\n*.js random(1) @frontend-team\n\u002Fapi\u002F least_busy(2) @backend-team\n\u002Fcritical\u002F all @security-team\n",[46,280,278],{"__ignoreMap":123},[17,282,283,286,287,290,291,294],{},[46,284,285],{},"random(1)"," picks one random reviewer from the team, ",[46,288,289],{},"least_busy(2)"," picks the two reviewers with the fewest open PRs, and ",[46,292,293],{},"all"," requires every team member to review. No other forge has reviewer selection strategies in the CODEOWNERS syntax.",[17,296,101,297,300],{},[46,298,299],{},".bitbucket\u002Fteams.yaml"," file lets you define ad-hoc reviewer groups without creating formal Bitbucket teams:",[115,302,306],{"className":303,"code":304,"language":305,"meta":123,"style":123},"language-yaml shiki shiki-themes github-light github-dark","# .bitbucket\u002Fteams.yaml\nsecurity:\n  - alice\n  - bob\nfrontend:\n  - carol\n  - dave\n","yaml",[46,307,308,317,328,338,346,354,362],{"__ignoreMap":123},[309,310,313],"span",{"class":311,"line":312},"line",1,[309,314,316],{"class":315},"sJ8bj","# .bitbucket\u002Fteams.yaml\n",[309,318,320,324],{"class":311,"line":319},2,[309,321,323],{"class":322},"s9eBZ","security",[309,325,327],{"class":326},"sVt8B",":\n",[309,329,331,334],{"class":311,"line":330},3,[309,332,333],{"class":326},"  - ",[309,335,337],{"class":336},"sZZnC","alice\n",[309,339,341,343],{"class":311,"line":340},4,[309,342,333],{"class":326},[309,344,345],{"class":336},"bob\n",[309,347,349,352],{"class":311,"line":348},5,[309,350,351],{"class":322},"frontend",[309,353,327],{"class":326},[309,355,357,359],{"class":311,"line":356},6,[309,358,333],{"class":326},[309,360,361],{"class":336},"carol\n",[309,363,365,367],{"class":311,"line":364},7,[309,366,333],{"class":326},[309,368,369],{"class":336},"dave\n",[17,371,372,373,376,377,200,380,25],{},"These can then be referenced in CODEOWNERS with the ",[46,374,375],{},"@teams\u002F"," prefix, like ",[46,378,379],{},"@teams\u002Fsecurity",[46,381,382],{},"@teams\u002Ffrontend",[27,384,386],{"id":385},"fallback-chains","Fallback chains",[17,388,389,390,392,393,200,395,397],{},"If you host the same repository on multiple platforms, shared config in ",[46,391,30],{}," will be picked up by Gitea and Forgejo, with platform-specific overrides in ",[46,394,220],{},[46,396,216],{}," taking priority. Bitbucket and GitLab only check their own folders, so multi-platform support across all forges still requires some duplication.",[27,399,401],{"id":400},"gotchas","Gotchas",[17,403,404,405,408,409,412,413,415],{},"GitHub's org-level ",[46,406,407],{},".github"," repository lets you set default issue templates, PR templates, and community health files for every repo in the org, but the fallback is all-or-nothing: if a repo has any file in its own ",[46,410,411],{},".github\u002FISSUE_TEMPLATE\u002F"," folder, none of the org-level templates are inherited and there's no way to merge them. The org ",[46,414,407],{}," repo must also be public, so your default templates are visible to everyone.",[17,417,418,419,422,423,425,426,429,430,433,434,440,441,444,445,448,449,452],{},"GitHub looks for CODEOWNERS in three places: ",[46,420,421],{},".github\u002FCODEOWNERS",", then ",[46,424,70],{}," at the root, then ",[46,427,428],{},"docs\u002FCODEOWNERS",". First one found wins and the others are silently ignored. The syntax looks like ",[46,431,432],{},".gitignore"," but ",[21,435,439],{"href":436,"rel":437},"https:\u002F\u002Fdocs.github.com\u002Farticles\u002Fabout-code-owners",[438],"nofollow","doesn't support"," ",[46,442,443],{},"!"," negation, ",[46,446,447],{},"[]"," character ranges, or ",[46,450,451],{},"\\#"," escaping. A syntax error used to cause the entire file to be silently ignored, meaning no owners were assigned to anything. GitHub has since added error highlighting in the web UI but there's still no push-time validation.",[17,454,455,456,461,462,465,466,25],{},"GitLab supports ",[21,457,460],{"href":458,"rel":459},"https:\u002F\u002Fdocs.gitlab.com\u002Fuser\u002Fproject\u002Fcodeowners\u002Fadvanced\u002F",[438],"optional CODEOWNERS sections"," with a ",[46,463,464],{},"^"," prefix, but \"optional\" only applies to merge requests. If someone pushes directly to a protected branch, the docs say approval from those sections is \"still required,\" though how that actually works for a command-line push is ",[21,467,470],{"href":468,"rel":469},"https:\u002F\u002Fforum.gitlab.com\u002Ft\u002Foptional-codeowners-what-does-approval-required-if-pushing-directly-to-protected-branch-mean\u002F107795",[438],"unclear even to GitLab users",[17,472,473,474,477,478,480,481,486],{},"The Gitea\u002FForgejo workflow fallback is all-or-nothing too: if ",[46,475,476],{},".gitea\u002Fworkflows\u002F"," contains any workflow files, ",[46,479,104],{}," is ",[21,482,485],{"href":483,"rel":484},"https:\u002F\u002Fgithub.com\u002Fgo-gitea\u002Fgitea\u002Fissues\u002F31456",[438],"completely ignored",", so you can't run platform-specific workflows side by side.",[17,488,489,490,492,493,87,496,499,500,503,504,506,507,512,513,518],{},"Gitea's CODEOWNERS doesn't check ",[46,491,421],{}," at all, only ",[46,494,495],{},".\u002FCODEOWNERS",[46,497,498],{},".\u002Fdocs\u002FCODEOWNERS",", and ",[46,501,502],{},".gitea\u002FCODEOWNERS",". If you migrate from GitHub with your CODEOWNERS in ",[46,505,30],{},", it silently does nothing. And even when it works, CODEOWNERS on Gitea ",[21,508,511],{"href":509,"rel":510},"https:\u002F\u002Fgithub.com\u002Fgo-gitea\u002Fgitea\u002Fissues\u002F32602",[438],"isn't enforceable",": it adds reviewers but there's no branch protection option to require their approval. Anyone with write access can approve. A regression in Gitea 1.21.9 also ",[21,514,517],{"href":515,"rel":516},"https:\u002F\u002Fgithub.com\u002Fgo-gitea\u002Fgitea\u002Fpull\u002F30476",[438],"broke CODEOWNERS for fork PRs",", which wasn't fixed until 1.21.11.",[17,520,521,522,525,526,531,532,534],{},"Forgejo and Gitea both inherited the ",[46,523,524],{},"pull_request_target"," trigger from GitHub Actions compatibility, which means they also inherited the \"",[21,527,530],{"href":528,"rel":529},"https:\u002F\u002Fsecuritylab.github.com\u002Fresources\u002Fgithub-actions-preventing-pwn-requests\u002F",[438],"pwn request","\" attack surface. The workflow runs from the base branch with access to secrets, and if it checks out and executes the fork's PR code, those secrets can be exfiltrated. Forgejo added a trust-based approval system for fork PRs, but ",[46,533,524],{}," workflows still run with write tokens.",[17,536,537,542,543,546],{},[21,538,541],{"href":539,"rel":540},"https:\u002F\u002Fwww.cvedetails.com\u002Fcve\u002FCVE-2025-68937\u002F",[438],"CVE-2025-68937"," is a symlink-following vulnerability in template repository processing, filed against Forgejo. An attacker creates a template repository with symlinks pointing at sensitive paths like the git user's ",[46,544,545],{},"authorized_keys",", and when someone creates a new repo from that template, the symlinks get dereferenced during template expansion, allowing the attacker to write arbitrary files on the server. Forgejo was affected through v13.0.1 (and v11.0.6 on LTS). Gitea had the same bug since v1.11 and fixed it in v1.24.7 under a separate advisory.",[17,548,549,550,555],{},"The Forgejo runner also fixed a ",[21,551,554],{"href":552,"rel":553},"https:\u002F\u002Fcodeberg.org\u002Fforgejo\u002Fsecurity-announcements\u002Fissues\u002F38",[438],"cache poisoning vulnerability"," in v10.0.0 where PR workflows could write to the shared action cache, letting a malicious PR poison future privileged workflow runs. It's unclear whether Gitea's runner is affected or fixed this quietly, as they haven't published a corresponding advisory.",[17,557,558,559,94,564,567,568,87,571,574,575,578,579,55,582,585],{},"GitHub Actions ",[21,560,563],{"href":561,"rel":562},"https:\u002F\u002Fyossarian.net\u002Ftil\u002Fpost\u002Fgithub-actions-is-surprisingly-case-insensitive\u002F",[438],"expressions are case-insensitive",[46,565,566],{},"${% raw %}{{ github.ref == 'refs\u002Fheads\u002Fmain' }}{% endraw %}"," matches whether the branch is ",[46,569,570],{},"main",[46,572,573],{},"MAIN",", or ",[46,576,577],{},"mAiN",". Context accesses like ",[46,580,581],{},"secrets.MY_SECRET",[46,583,584],{},"SECRETS.my_secret"," resolve to the same thing. Git itself is case-sensitive, so if your workflow security depends on branch naming conventions, there's a mismatch that's easy to miss.",[587,588],"hr",{},[17,590,591,592,597,598,25],{},"If you know of other forge-specific folders or have corrections, reach out on ",[21,593,596],{"href":594,"rel":595},"https:\u002F\u002Fmastodon.social\u002F@andrewnez",[438],"Mastodon"," or submit a pull request on ",[21,599,602],{"href":600,"rel":601},"https:\u002F\u002Fgithub.com\u002Fandrew\u002Fnesbitt.io",[438],"GitHub",[604,605,606],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":123,"searchDepth":319,"depth":319,"links":608},[609,610,611,612,613,614],{"id":29,"depth":330,"text":30},{"id":126,"depth":330,"text":127},{"id":185,"depth":330,"text":186},{"id":244,"depth":330,"text":245},{"id":385,"depth":330,"text":386},{"id":400,"depth":330,"text":401},"https:\u002F\u002Fnesbitt.io\u002F2026\u002F02\u002F22\u002Fforge-specific-repository-folders","nesbitt.io","tooling","2026-02-22","Magic folders in git forges: what .github\u002F, .gitlab\u002F, .gitea\u002F, .forgejo\u002F and .bitbucket\u002F do.","md",false,null,{},true,"\u002Freports\u002Fforge-specific-repository-folders",{"title":10,"description":619},"reports\u002Fforge-specific-repository-folders","7dobK61RcHurCoblqyhE8wIDGcuz_6PiR4vn1wN1ZX4",1780596103400]