ghfleet 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ name: Build
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ tags: [ '*.*.*' ]
7
+ pull_request:
8
+ branches: [ master ]
9
+ workflow_dispatch:
10
+
11
+ jobs:
12
+ Test:
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ contents: read
16
+
17
+ steps:
18
+ - name: Checkout repository
19
+ uses: actions/checkout@v7
20
+
21
+ - name: Install uv
22
+ run: curl -LsSf https://astral.sh/uv/install.sh | sh
23
+
24
+ - name: Install dependencies
25
+ run: |
26
+ export PATH="$HOME/.local/bin:$PATH"
27
+ uv sync --group dev
28
+
29
+ - name: Check formatting
30
+ run: |
31
+ export PATH="$HOME/.local/bin:$PATH"
32
+ uv run ruff format --check .
33
+
34
+ - name: Lint
35
+ run: |
36
+ export PATH="$HOME/.local/bin:$PATH"
37
+ uv run ruff check .
38
+
39
+ - name: Type check
40
+ run: |
41
+ export PATH="$HOME/.local/bin:$PATH"
42
+ uv run mypy ghfleet
43
+
44
+ Publish-PyPI:
45
+ runs-on: ubuntu-latest
46
+ needs: [Test]
47
+ if: startsWith(github.ref, 'refs/tags/')
48
+ permissions:
49
+ contents: read
50
+ id-token: write # Required for OIDC trusted publisher
51
+
52
+ steps:
53
+ - name: Checkout repository
54
+ uses: actions/checkout@v7
55
+
56
+ - name: Install uv
57
+ run: curl -LsSf https://astral.sh/uv/install.sh | sh
58
+
59
+ - name: Build package
60
+ run: |
61
+ export PATH="$HOME/.local/bin:$PATH"
62
+ uv build
63
+
64
+ - name: Publish to PyPI
65
+ uses: pypa/gh-action-pypi-publish@release/v1
66
+
67
+ Documentation:
68
+ if: github.ref == 'refs/heads/master'
69
+ needs: [Test]
70
+ uses: byjg/byjg.github.io/.github/workflows/add-doc.yaml@master
71
+ with:
72
+ folder: devops
73
+ project: ${{ github.event.repository.name }}
74
+ secrets:
75
+ DOC_TOKEN: ${{ secrets.DOC_TOKEN }}
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .idea/
11
+ .vscode/
ghfleet-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joao Gilberto Magalhães
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ghfleet-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,273 @@
1
+ Metadata-Version: 2.5
2
+ Name: ghfleet
3
+ Version: 0.1.0
4
+ Summary: Declare a fleet of GitHub repositories -- settings, secrets, workflow permissions and branch protection rulesets -- from one configuration file, with Pulumi
5
+ Project-URL: Homepage, https://github.com/byjg/pulumi-github-fleet
6
+ Project-URL: Documentation, https://opensource.byjg.com/docs/devops/pulumi-github-fleet/
7
+ Project-URL: Source, https://github.com/byjg/pulumi-github-fleet
8
+ Author-email: Joao Gilberto Magalhaes <joao@byjg.com.br>
9
+ License: MIT License
10
+
11
+ Copyright (c) 2026 Joao Gilberto Magalhães
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
30
+ License-File: LICENSE
31
+ Keywords: branch-protection,github,infrastructure-as-code,pulumi,repository-settings
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: Intended Audience :: System Administrators
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: Topic :: Software Development :: Version Control :: Git
41
+ Classifier: Topic :: System :: Systems Administration
42
+ Requires-Python: >=3.10
43
+ Requires-Dist: pulumi-github<7.0.0,>=6.0.0
44
+ Requires-Dist: pulumi<4.0.0,>=3.0.0
45
+ Requires-Dist: pyyaml>=6.0.0
46
+ Description-Content-Type: text/markdown
47
+
48
+ ---
49
+ sidebar_key: pulumi-github-fleet
50
+ tags: [python, devops]
51
+ ---
52
+
53
+ # ghfleet
54
+
55
+ [![Build](https://github.com/byjg/pulumi-github-fleet/actions/workflows/build.yml/badge.svg)](https://github.com/byjg/pulumi-github-fleet/actions/workflows/build.yml)
56
+ [![PyPI](https://img.shields.io/pypi/v/ghfleet.svg)](https://pypi.org/project/ghfleet/)
57
+
58
+ Declare a fleet of GitHub repositories from one configuration file, with Pulumi:
59
+ repository settings, Actions secrets, workflow permissions and branch protection
60
+ rulesets.
61
+
62
+ It exists because the interesting part of managing dozens of repositories is not the
63
+ API calls, it is answering "which repositories get this, and what is it exactly?" in a
64
+ file you can read, review and diff.
65
+
66
+ ## Install
67
+
68
+ ```bash
69
+ pip install ghfleet # or: uv add ghfleet
70
+ ```
71
+
72
+ ## Use
73
+
74
+ ```python
75
+ import pulumi
76
+
77
+ from ghfleet import apply, load_config
78
+
79
+ apply(load_config("config.yml"))
80
+
81
+ pulumi.export("summary", "GitHub repositories configured successfully")
82
+ ```
83
+
84
+ That is the whole Pulumi program. `apply` takes the configuration as a plain
85
+ dictionary, so it does not care whether it came from YAML, JSON or was built in code.
86
+
87
+ ### The two conventions are yours to change
88
+
89
+ `apply` would otherwise impose where a token and a secret value come from. Both are
90
+ keyword arguments, with the obvious behaviour as the default:
91
+
92
+ | Argument | Default |
93
+ |---|---|
94
+ | `token_for(owner)` | reads `GITHUB_ACCESS_TOKEN_<owner>` from the environment, raising when unset |
95
+ | `secret_value(secret)` | the entry's own `value`, else the environment variable named after the secret |
96
+
97
+ ```python
98
+ apply(
99
+ load_config("config.yml"),
100
+ token_for=lambda owner: vault.read(f"github/{owner}").token,
101
+ secret_value=lambda secret: vault.read(secret["name"]).value,
102
+ )
103
+ ```
104
+
105
+ ## The configuration
106
+
107
+ ```yaml
108
+ lists:
109
+ services:
110
+ - api-gateway
111
+ - billing
112
+ - notifications
113
+
114
+ repository:
115
+ - name: services
116
+ owner: acme
117
+ list:
118
+ - services
119
+ repositories:
120
+ - one-off-repo
121
+ repository_settings:
122
+ allow_update_branch: true
123
+ workflow_permissions:
124
+ default: write
125
+ can_approve_pull_request_reviews: false
126
+ branch_protection:
127
+ enabled: true
128
+ bypass_actor_ids: [5]
129
+ secrets:
130
+ - name: REGISTRY_USER
131
+ value: acme
132
+ - name: REGISTRY_TOKEN
133
+ ```
134
+
135
+ - **`lists`** — named lists of repository names, so a repository set is written once and
136
+ referenced by several entries.
137
+ - **`repository`** — the entries. Each one names an `owner`, the repositories it covers
138
+ (through `list`, `repositories`, or both), and what they get. A repository named by
139
+ more than one entry receives the union.
140
+
141
+ ### `repository_settings`
142
+
143
+ Declare **only** the settings that must be guaranteed. The accepted keys are in
144
+ `REPOSITORY_SETTINGS_KEYS`; an unknown one aborts the run.
145
+
146
+ A repository that declares this block becomes a `github.Repository` resource, adopted
147
+ into the state on the first run that declares it — no import step. The resource covers
148
+ the *whole* repository, so everything the block does not declare (description, topics,
149
+ visibility, the other merge options) is read back from the `github.get_repository` data
150
+ source and handed to the resource unchanged. Two consequences:
151
+
152
+ - `pulumi preview` shows a diff only for the keys you declared, which is what makes it
153
+ a drift report;
154
+ - an undeclared property is *not* enforced: change the description on GitHub and the
155
+ next run simply follows it.
156
+
157
+ The resource is declared with `retain_on_delete`, so neither `pulumi destroy` nor
158
+ removing a line from the configuration deletes a repository. Its secrets and ruleset
159
+ *are* removed — only the repository is retained.
160
+
161
+ `pages` is not an accepted key, and that absence is meaningful: the provider reads it
162
+ as "no Pages site" and disables Pages on any repository that still has one. The one
163
+ exception is the owner's user site, the repository named `<owner>.github.io`, which is
164
+ recognised by name and left alone (see "GitHub says no", below).
165
+
166
+ ### `branch_protection`
167
+
168
+ ```yaml
169
+ branch_protection:
170
+ enabled: true
171
+ enforcement: active # active | evaluate | disabled
172
+ bypass_actor_ids: [5] # 2 = Triage, 3 = Write, 4 = Maintain, 5 = Admin
173
+ rules:
174
+ pull_request:
175
+ required_approving_review_count: 2
176
+ require_code_owner_review: true
177
+ allowed_merge_methods: [squash]
178
+ required_status_checks:
179
+ strict_required_status_checks_policy: true
180
+ required_checks:
181
+ - context: "Build"
182
+ deletion: true
183
+ non_fast_forward: true
184
+ ```
185
+
186
+ `rules`, `ref_name` and `bypass_actors` are handed to the provider **exactly as
187
+ written** — the resource accepts plain dictionaries — so every rule
188
+ `github.RepositoryRuleset` supports is reachable from the configuration without this
189
+ package knowing about it. The keys are the provider's, in snake_case.
190
+
191
+ Left out, `rules` falls back to `DEFAULT_BRANCH_PROTECTION_RULES`: one approving
192
+ review, dismiss stale reviews on push, all three merge methods, no branch deletion and
193
+ no force push. It is a default, not a rule of the package — writing `rules` replaces it
194
+ entirely rather than merging into it.
195
+
196
+ Other keys: `name` (default `Protect <default branch>`), `target` (`branch` or `tag`),
197
+ `ref_name` (`{includes, excludes}`, default the default branch), and `bypass_actors`
198
+ for anything that is not a repository role. An unknown key aborts the run: on a branch
199
+ protection block, a silent typo means a rule that is not enforced.
200
+
201
+ Note that rulesets on **private** repositories require a paid plan. On the free plan
202
+ GitHub answers 403, so the ruleset is skipped with a warning instead of failing the
203
+ run.
204
+
205
+ ### `secrets`
206
+
207
+ ```yaml
208
+ secrets:
209
+ - name: REGISTRY_USER
210
+ value: acme # in the file, for values that are not secret
211
+ - name: REGISTRY_TOKEN # no value: taken from the environment
212
+ ```
213
+
214
+ The run aborts before declaring anything if a secret has no value anywhere. That check
215
+ is not politeness: an empty value would overwrite the real secret with an empty string
216
+ in every repository that declares it.
217
+
218
+ ### Sharing blocks between entries
219
+
220
+ Entries repeat themselves. Use YAML anchors — a feature of the file format, so nothing
221
+ here has to implement it:
222
+
223
+ ```yaml
224
+ x-defaults:
225
+ branch_protection: &protected
226
+ enabled: true
227
+ bypass_actor_ids: [5]
228
+
229
+ repository:
230
+ - name: services
231
+ branch_protection: *protected
232
+
233
+ - name: legacy
234
+ branch_protection:
235
+ <<: *protected # inherit, then override
236
+ enforcement: "disabled"
237
+ ```
238
+
239
+ Precedence stays local and explicit: a key written next to `<<:` wins.
240
+
241
+ ## GitHub says no
242
+
243
+ Two settings answer 422 whatever you send, and both cost a failed run to discover.
244
+
245
+ **`allow_forking` on a private repository of a user account.** The repository-level
246
+ forking policy only exists for private repositories owned by an *organization*, and
247
+ even there an organization owner has to allow forking of private repositories first.
248
+ On a public repository the setting means nothing. It is therefore not among the
249
+ accepted keys, which is also what the provider recommends — its schema says to "leave
250
+ unset for the default behaviour". The restriction is not in the REST reference; it is
251
+ in the [forking policy guide](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/managing-the-forking-policy-for-your-repository).
252
+
253
+ **Deactivating GitHub Pages on the owner's user site.** The repository named
254
+ `<owner>.github.io` cannot have its Pages site unpublished:
255
+
256
+ ```
257
+ DELETE /repos/<owner>/<owner>.github.io/pages: 422 Deactivating GitHub pages
258
+ for this repository is not allowed
259
+ ```
260
+
261
+ This one is documented nowhere — neither the REST reference nor the "Unpublishing a
262
+ GitHub Pages site" guide mentions it, and that guide draws no distinction between a
263
+ user site and a project site. It is only what the API does, so the package recognises
264
+ the repository by name and keeps `pages` out of its diff.
265
+
266
+ ## What this is not
267
+
268
+ An ordinary Python package, not a Pulumi component: it declares its resources into the
269
+ stack that calls it, and can only be consumed from Python.
270
+
271
+ ## License
272
+
273
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,226 @@
1
+ ---
2
+ sidebar_key: pulumi-github-fleet
3
+ tags: [python, devops]
4
+ ---
5
+
6
+ # ghfleet
7
+
8
+ [![Build](https://github.com/byjg/pulumi-github-fleet/actions/workflows/build.yml/badge.svg)](https://github.com/byjg/pulumi-github-fleet/actions/workflows/build.yml)
9
+ [![PyPI](https://img.shields.io/pypi/v/ghfleet.svg)](https://pypi.org/project/ghfleet/)
10
+
11
+ Declare a fleet of GitHub repositories from one configuration file, with Pulumi:
12
+ repository settings, Actions secrets, workflow permissions and branch protection
13
+ rulesets.
14
+
15
+ It exists because the interesting part of managing dozens of repositories is not the
16
+ API calls, it is answering "which repositories get this, and what is it exactly?" in a
17
+ file you can read, review and diff.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ pip install ghfleet # or: uv add ghfleet
23
+ ```
24
+
25
+ ## Use
26
+
27
+ ```python
28
+ import pulumi
29
+
30
+ from ghfleet import apply, load_config
31
+
32
+ apply(load_config("config.yml"))
33
+
34
+ pulumi.export("summary", "GitHub repositories configured successfully")
35
+ ```
36
+
37
+ That is the whole Pulumi program. `apply` takes the configuration as a plain
38
+ dictionary, so it does not care whether it came from YAML, JSON or was built in code.
39
+
40
+ ### The two conventions are yours to change
41
+
42
+ `apply` would otherwise impose where a token and a secret value come from. Both are
43
+ keyword arguments, with the obvious behaviour as the default:
44
+
45
+ | Argument | Default |
46
+ |---|---|
47
+ | `token_for(owner)` | reads `GITHUB_ACCESS_TOKEN_<owner>` from the environment, raising when unset |
48
+ | `secret_value(secret)` | the entry's own `value`, else the environment variable named after the secret |
49
+
50
+ ```python
51
+ apply(
52
+ load_config("config.yml"),
53
+ token_for=lambda owner: vault.read(f"github/{owner}").token,
54
+ secret_value=lambda secret: vault.read(secret["name"]).value,
55
+ )
56
+ ```
57
+
58
+ ## The configuration
59
+
60
+ ```yaml
61
+ lists:
62
+ services:
63
+ - api-gateway
64
+ - billing
65
+ - notifications
66
+
67
+ repository:
68
+ - name: services
69
+ owner: acme
70
+ list:
71
+ - services
72
+ repositories:
73
+ - one-off-repo
74
+ repository_settings:
75
+ allow_update_branch: true
76
+ workflow_permissions:
77
+ default: write
78
+ can_approve_pull_request_reviews: false
79
+ branch_protection:
80
+ enabled: true
81
+ bypass_actor_ids: [5]
82
+ secrets:
83
+ - name: REGISTRY_USER
84
+ value: acme
85
+ - name: REGISTRY_TOKEN
86
+ ```
87
+
88
+ - **`lists`** — named lists of repository names, so a repository set is written once and
89
+ referenced by several entries.
90
+ - **`repository`** — the entries. Each one names an `owner`, the repositories it covers
91
+ (through `list`, `repositories`, or both), and what they get. A repository named by
92
+ more than one entry receives the union.
93
+
94
+ ### `repository_settings`
95
+
96
+ Declare **only** the settings that must be guaranteed. The accepted keys are in
97
+ `REPOSITORY_SETTINGS_KEYS`; an unknown one aborts the run.
98
+
99
+ A repository that declares this block becomes a `github.Repository` resource, adopted
100
+ into the state on the first run that declares it — no import step. The resource covers
101
+ the *whole* repository, so everything the block does not declare (description, topics,
102
+ visibility, the other merge options) is read back from the `github.get_repository` data
103
+ source and handed to the resource unchanged. Two consequences:
104
+
105
+ - `pulumi preview` shows a diff only for the keys you declared, which is what makes it
106
+ a drift report;
107
+ - an undeclared property is *not* enforced: change the description on GitHub and the
108
+ next run simply follows it.
109
+
110
+ The resource is declared with `retain_on_delete`, so neither `pulumi destroy` nor
111
+ removing a line from the configuration deletes a repository. Its secrets and ruleset
112
+ *are* removed — only the repository is retained.
113
+
114
+ `pages` is not an accepted key, and that absence is meaningful: the provider reads it
115
+ as "no Pages site" and disables Pages on any repository that still has one. The one
116
+ exception is the owner's user site, the repository named `<owner>.github.io`, which is
117
+ recognised by name and left alone (see "GitHub says no", below).
118
+
119
+ ### `branch_protection`
120
+
121
+ ```yaml
122
+ branch_protection:
123
+ enabled: true
124
+ enforcement: active # active | evaluate | disabled
125
+ bypass_actor_ids: [5] # 2 = Triage, 3 = Write, 4 = Maintain, 5 = Admin
126
+ rules:
127
+ pull_request:
128
+ required_approving_review_count: 2
129
+ require_code_owner_review: true
130
+ allowed_merge_methods: [squash]
131
+ required_status_checks:
132
+ strict_required_status_checks_policy: true
133
+ required_checks:
134
+ - context: "Build"
135
+ deletion: true
136
+ non_fast_forward: true
137
+ ```
138
+
139
+ `rules`, `ref_name` and `bypass_actors` are handed to the provider **exactly as
140
+ written** — the resource accepts plain dictionaries — so every rule
141
+ `github.RepositoryRuleset` supports is reachable from the configuration without this
142
+ package knowing about it. The keys are the provider's, in snake_case.
143
+
144
+ Left out, `rules` falls back to `DEFAULT_BRANCH_PROTECTION_RULES`: one approving
145
+ review, dismiss stale reviews on push, all three merge methods, no branch deletion and
146
+ no force push. It is a default, not a rule of the package — writing `rules` replaces it
147
+ entirely rather than merging into it.
148
+
149
+ Other keys: `name` (default `Protect <default branch>`), `target` (`branch` or `tag`),
150
+ `ref_name` (`{includes, excludes}`, default the default branch), and `bypass_actors`
151
+ for anything that is not a repository role. An unknown key aborts the run: on a branch
152
+ protection block, a silent typo means a rule that is not enforced.
153
+
154
+ Note that rulesets on **private** repositories require a paid plan. On the free plan
155
+ GitHub answers 403, so the ruleset is skipped with a warning instead of failing the
156
+ run.
157
+
158
+ ### `secrets`
159
+
160
+ ```yaml
161
+ secrets:
162
+ - name: REGISTRY_USER
163
+ value: acme # in the file, for values that are not secret
164
+ - name: REGISTRY_TOKEN # no value: taken from the environment
165
+ ```
166
+
167
+ The run aborts before declaring anything if a secret has no value anywhere. That check
168
+ is not politeness: an empty value would overwrite the real secret with an empty string
169
+ in every repository that declares it.
170
+
171
+ ### Sharing blocks between entries
172
+
173
+ Entries repeat themselves. Use YAML anchors — a feature of the file format, so nothing
174
+ here has to implement it:
175
+
176
+ ```yaml
177
+ x-defaults:
178
+ branch_protection: &protected
179
+ enabled: true
180
+ bypass_actor_ids: [5]
181
+
182
+ repository:
183
+ - name: services
184
+ branch_protection: *protected
185
+
186
+ - name: legacy
187
+ branch_protection:
188
+ <<: *protected # inherit, then override
189
+ enforcement: "disabled"
190
+ ```
191
+
192
+ Precedence stays local and explicit: a key written next to `<<:` wins.
193
+
194
+ ## GitHub says no
195
+
196
+ Two settings answer 422 whatever you send, and both cost a failed run to discover.
197
+
198
+ **`allow_forking` on a private repository of a user account.** The repository-level
199
+ forking policy only exists for private repositories owned by an *organization*, and
200
+ even there an organization owner has to allow forking of private repositories first.
201
+ On a public repository the setting means nothing. It is therefore not among the
202
+ accepted keys, which is also what the provider recommends — its schema says to "leave
203
+ unset for the default behaviour". The restriction is not in the REST reference; it is
204
+ in the [forking policy guide](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/managing-the-forking-policy-for-your-repository).
205
+
206
+ **Deactivating GitHub Pages on the owner's user site.** The repository named
207
+ `<owner>.github.io` cannot have its Pages site unpublished:
208
+
209
+ ```
210
+ DELETE /repos/<owner>/<owner>.github.io/pages: 422 Deactivating GitHub pages
211
+ for this repository is not allowed
212
+ ```
213
+
214
+ This one is documented nowhere — neither the REST reference nor the "Unpublishing a
215
+ GitHub Pages site" guide mentions it, and that guide draws no distinction between a
216
+ user site and a project site. It is only what the API does, so the package recognises
217
+ the repository by name and keeps `pages` out of its diff.
218
+
219
+ ## What this is not
220
+
221
+ An ordinary Python package, not a Pulumi component: it declares its resources into the
222
+ stack that calls it, and can only be consumed from Python.
223
+
224
+ ## License
225
+
226
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,47 @@
1
+ """
2
+ Declare GitHub repositories, secrets, workflow permissions and branch protection
3
+ rulesets from a configuration dictionary.
4
+
5
+ from ghfleet import apply, load_config
6
+
7
+ apply(load_config("config.yml"))
8
+
9
+ `apply` takes the configuration as a plain dictionary, so it does not care whether it
10
+ came from YAML, JSON or was built in code. The two conventions it would otherwise
11
+ impose -- where the token of an owner comes from, and where the value of a secret
12
+ comes from -- are its keyword arguments.
13
+ """
14
+
15
+ from .config import (
16
+ BRANCH_PROTECTION_KEYS,
17
+ DEFAULT_BRANCH_PROTECTION_RULES,
18
+ REPOSITORY_SETTINGS_KEYS,
19
+ branch_protection_of,
20
+ load_config,
21
+ repositories_of,
22
+ repository_settings_of,
23
+ resolve_list_references,
24
+ ruleset_name_of,
25
+ )
26
+ from .resources import (
27
+ apply,
28
+ get_owner_plan,
29
+ secret_value_from_environment,
30
+ token_from_environment,
31
+ )
32
+
33
+ __all__ = [
34
+ "BRANCH_PROTECTION_KEYS",
35
+ "DEFAULT_BRANCH_PROTECTION_RULES",
36
+ "REPOSITORY_SETTINGS_KEYS",
37
+ "apply",
38
+ "branch_protection_of",
39
+ "get_owner_plan",
40
+ "load_config",
41
+ "repositories_of",
42
+ "repository_settings_of",
43
+ "resolve_list_references",
44
+ "ruleset_name_of",
45
+ "secret_value_from_environment",
46
+ "token_from_environment",
47
+ ]