Skip to content

Commit ea030b4

Browse files
feat: Add dependabot secrets (#12)
* feat: support dependabot secrets This adds support for setting dependabot secrets * docs: update example to include dependabot secret * feat: support deleting dependabot * feat: support diffing dependabot secrets * ci: update workflows
1 parent 7a34970 commit ea030b4

File tree

9 files changed

+119
-61
lines changed

9 files changed

+119
-61
lines changed

.github/workflows/codeql.yml

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
name: "CodeQL"
22
on:
33
push:
4-
branches: [ main ]
5-
pull_request:
6-
# The branches below must be a subset of the branches above
7-
branches: [ main ]
84
schedule:
9-
- cron: '0 0 * * 1'
5+
- cron: "0 0 * * 1"
106
workflow_dispatch:
117
jobs:
128
analyze:
@@ -17,19 +13,19 @@ jobs:
1713
contents: read
1814
security-events: write
1915
steps:
20-
- name: Cancel Workflow Action
21-
uses: styfle/[email protected]
22-
with:
23-
all_but_latest: true
24-
access_token: ${{ secrets.THIS_PAT }}
25-
- name: Checkout repository
26-
uses: actions/checkout@v3
27-
with:
16+
- name: Cancel Workflow Action
17+
uses: styfle/[email protected]
18+
with:
19+
all_but_latest: true
20+
access_token: ${{ secrets.THIS_PAT }}
21+
- name: Checkout repository
22+
uses: actions/checkout@v3
23+
with:
2824
token: ${{ secrets.GITHUB_TOKEN }}
29-
# Initializes the CodeQL tools for scanning.
30-
- name: Initialize CodeQL
31-
uses: github/codeql-action/init@v2
32-
with:
33-
languages: Python
34-
- name: Perform CodeQL Analysis
35-
uses: github/codeql-action/analyze@v2
25+
# Initializes the CodeQL tools for scanning.
26+
- name: Initialize CodeQL
27+
uses: github/codeql-action/init@v2
28+
with:
29+
languages: Python
30+
- name: Perform CodeQL Analysis
31+
uses: github/codeql-action/analyze@v2

.github/workflows/generate-docs.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
name: Generate Docs
22
on:
33
push:
4-
branches:
5-
- main
6-
pull_request:
74
jobs:
85
docs:
96
name: Generate Docs

.github/workflows/integration.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
name: Integration Test
22
on:
33
push:
4-
branches:
5-
- main
6-
pull_request:
74
jobs:
85
integration-testing:
96
name: Integration Testing
@@ -12,7 +9,7 @@ jobs:
129
- uses: actions/checkout@v3
1310
name: Checkout
1411
with:
15-
token: ${{ secrets.GITHUB_TOKEN }}
12+
token: ${{ secrets.GITHUB_TOKEN }}
1613
- name: Test action
1714
id: test-action
1815
# test with the local checkout of the action

.github/workflows/lint.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
name: Lint
22
on:
33
push:
4-
pull_request:
5-
branches:
6-
- main
74
jobs:
85
lint-and-test:
96
name: Lint and Test
@@ -20,7 +17,7 @@ jobs:
2017
python-version: "3.9.2"
2118
- uses: actions/setup-go@v3
2219
with:
23-
go-version: '>=1.17.0'
20+
go-version: ">=1.17.0"
2421
- run: go install github.com/rhysd/actionlint/cmd/actionlint@latest
2522
- uses: actions/checkout@v3
2623
with:

Dockerfile

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1-
# This file is generated from Docker/ActionDockerfile.j2 as part of the release ci
2-
# Don't modify it directly
3-
FROM andrewthetechie/gha-repo-manager:v1.1.0
1+
# Distroless runs python 3.9.2
2+
FROM python:3.9.2-slim AS builder
3+
ADD Docker/builder/rootfs /
4+
ADD repo_manager /app/repo_manager
5+
ADD main.py /app/main.py
6+
WORKDIR /app
7+
8+
# We are installing a dependency here directly into our app source dir
9+
RUN pip install --target=/app -r /requirements.txt
10+
11+
# A distroless container image with Python and some basics like SSL certificates
12+
# https://github.com/GoogleContainerTools/distroless
13+
FROM gcr.io/distroless/python3
14+
COPY --from=builder /app /app
15+
WORKDIR /app
16+
ENV PYTHONPATH /app
17+
CMD ["/app/main.py"]

examples/settings.yml

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ settings:
2121

2222
# A list of strings to apply as topics on the repo. Set to an empty string to clear topics. Omit or set to null to leave what repo already has
2323
topics:
24-
- gha
25-
- foo
26-
- bar
24+
- gha
25+
- foo
26+
- bar
2727

2828
# Either `true` to make the repository private, or `false` to make it public.
2929
private: false
@@ -75,7 +75,7 @@ labels:
7575

7676
- name: feature
7777
# If including a `#`, make sure to wrap it with quotes!
78-
color: '#336699'
78+
color: "#336699"
7979
description: New functionality.
8080

8181
- name: Help Wanted
@@ -109,10 +109,10 @@ branch_protections:
109109
# # Require branches to be up to date before merging.
110110
# strict: true
111111
# # The list of status checks to require in order to merge into this branch
112-
# checks:
113-
# - lint
114-
# - test
115-
# - docker
112+
# checks:
113+
# - lint
114+
# - test
115+
# - docker
116116
# Enforce all configured restrictions for administrators. Set to true to enforce required status checks for repository administrators. Set to null to disable.
117117
enforce_admins: true
118118
# Prevent merge commits from being pushed to matching branches
@@ -141,6 +141,10 @@ secrets:
141141
# pull the value from an environment variable. If this variable is not found in the env, throw an error and fail the run
142142
# Set env vars on the github action job from secrets in your repo to sync screts across repos
143143
env: SECRET_VALUE
144+
# Set a dependabot secret on the repo
145+
- key: SECRET_KEY
146+
env: SECRET_VALUE
147+
type: dependabot
144148
- key: ANOTHER_SECRET
145149
# set a value directly in your yaml, probably not a good idea for things that are actually a secret
146150
value: bar
@@ -176,9 +180,9 @@ files:
176180
- src_file: remote://README.md
177181
dest_file: README.rst
178182
move: true
179-
commit_msg: 'move readme'
183+
commit_msg: "move readme"
180184
# This removes OLDDOC.md in the dev branch. If OLDDOC.md doesn't exist, the workflow will emit a warning
181185
- dest_file: OLDDOC.md
182186
exists: false
183187
branch: dev
184-
commit_msg: 'remove OLDDOC.md from dev'
188+
commit_msg: "remove OLDDOC.md from dev"

main.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
from repo_manager.github.labels import check_repo_labels
1515
from repo_manager.github.labels import update_label
1616
from repo_manager.github.secrets import check_repo_secrets
17+
from repo_manager.github.secrets import create_secret
18+
from repo_manager.github.secrets import delete_secret
1719
from repo_manager.github.settings import check_repo_settings
1820
from repo_manager.github.settings import update_settings
1921
from repo_manager.schemas import load_config
@@ -65,7 +67,9 @@ def main(): # noqa: C901
6567
for secret in config.secrets:
6668
if secret.exists:
6769
try:
68-
inputs["repo_object"].create_secret(secret.key, secret.expected_value)
70+
create_secret(
71+
inputs["repo_object"], secret.key, secret.expected_value, secret.type == "dependabot"
72+
)
6973
actions_toolkit.info(f"Set {secret.key} to expected value")
7074
except Exception as exc: # this should be tighter
7175
errors.append(
@@ -77,7 +81,7 @@ def main(): # noqa: C901
7781
)
7882
else:
7983
try:
80-
inputs["repo_object"].delete_secret(secret.key)
84+
delete_secret(inputs["repo_object"], secret.key, secret.type == "dependabot")
8185
actions_toolkit.info(f"Deleted {secret.key}")
8286
except Exception as exc: # this should be tighter
8387
errors.append(

repo_manager/github/secrets.py

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Any
33
from typing import Dict
44
from typing import List
5+
from typing import Set
56
from typing import Tuple
67
from typing import Union
78

@@ -10,8 +11,38 @@
1011
from repo_manager.schemas.secret import Secret
1112

1213

13-
def upsert_secret(repo: Repository, secret_key: str, secret_config: Secret):
14-
...
14+
def create_secret(repo: Repository, secret_name: str, unencrypted_value: str, is_dependabot: bool = False) -> bool:
15+
"""
16+
:calls: `PUT /repos/{owner}/{repo}/actions/secrets/{secret_name} <https://docs.github.com/en/rest/reference/actions#get-a-repository-secret>`_
17+
18+
Copied from https://github.com/PyGithub/PyGithub/blob/master/github/Repository.py#L1428 in order to support dependabot
19+
:param secret_name: string
20+
:param unencrypted_value: string
21+
:rtype: bool
22+
"""
23+
public_key = repo.get_public_key()
24+
payload = public_key.encrypt(unencrypted_value)
25+
put_parameters = {
26+
"key_id": public_key.key_id,
27+
"encrypted_value": payload,
28+
}
29+
secret_type = "actions" if not is_dependabot else "dependabot"
30+
status, headers, data = repo._requester.requestJson(
31+
"PUT", f"{repo.url}/actions/{secret_type}/{secret_name}", input=put_parameters
32+
)
33+
return status == 201
34+
35+
36+
def delete_secret(repo: Repository, secret_name: str, is_dependabot: bool = False) -> bool:
37+
"""
38+
Copied from https://github.com/PyGithub/PyGithub/blob/master/github/Repository.py#L1448 to add support for dependabot
39+
:calls: `DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name} <https://docs.github.com/en/rest/reference/actions#delete-a-repository-secret>`_
40+
:param secret_name: string
41+
:rtype: bool
42+
"""
43+
secret_type = "actions" if not is_dependabot else "dependabot"
44+
status, headers, data = repo._requester.requestJson("DELETE", f"{repo.url}/{secret_type}/secrets/{secret_name}")
45+
return status == 204
1546

1647

1748
def check_repo_secrets(
@@ -26,24 +57,23 @@ def check_repo_secrets(
2657
Returns:
2758
Tuple[bool, Optional[List[str]]]: [description]
2859
"""
29-
status, headers, raw_data = repo._requester.requestJson("GET", f"{repo.url}/actions/secrets")
30-
if status != 200:
31-
raise Exception(f"Unable to get repo's secrets {status}")
32-
try:
33-
secret_data = json.loads(raw_data)
34-
except json.JSONDecodeError as exc:
35-
raise Exception(f"Github apu returned invalid json {exc}")
36-
60+
actions_secrets_names = _get_repo_secret_names(repo)
61+
dependabot_secret_names = _get_repo_secret_names(repo, "dependabot")
62+
secrets_dict = {secret.key: secret for secret in secrets}
3763
checked = True
3864

39-
repo_secret_names = [secret["name"] for secret in secret_data["secrets"]]
40-
secrets_dict = {secret.key: secret for secret in secrets}
41-
expected_secret_names = [secret.key for secret in secrets if secret.exists]
65+
actions_expected_secrets_names = {secret.key for secret in secrets if (secret.exists and secret.type == "actions")}
66+
dependabot_expected_secret_names = {
67+
secret.key for secret in secrets if (secret.exists and secret.type == "dependabot")
68+
}
4269
diff = {
43-
"missing": list(set(expected_secret_names) - set(repo_secret_names)),
70+
"missing": list(actions_expected_secrets_names - (actions_secrets_names))
71+
+ list((dependabot_expected_secret_names) - (dependabot_secret_names)),
4472
"extra": [],
4573
}
46-
extra_secret_names = list(set(repo_secret_names) - set(expected_secret_names))
74+
extra_secret_names = (list((actions_secrets_names) - (actions_expected_secrets_names))) + (
75+
list(dependabot_secret_names - dependabot_expected_secret_names)
76+
)
4777
for secret_name in extra_secret_names:
4878
secret_config = secrets_dict.get(secret_name, None)
4979
if secret_config is None:
@@ -56,3 +86,15 @@ def check_repo_secrets(
5686
checked = False
5787

5888
return checked, diff
89+
90+
91+
def _get_repo_secret_names(repo: Repository, type: str = "actions") -> Set[str]:
92+
status, headers, raw_data = repo._requester.requestJson("GET", f"{repo.url}/{type}/secrets")
93+
if status != 200:
94+
raise Exception(f"Unable to get repo's secrets {status}")
95+
try:
96+
secret_data = json.loads(raw_data)
97+
except json.JSONDecodeError as exc:
98+
raise Exception(f"Github apu returned invalid json {exc}")
99+
100+
return {secret["name"] for secret in secret_data["secrets"]}

repo_manager/schemas/secret.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class SecretEnvError(Exception):
1414

1515

1616
class Secret(BaseModel):
17+
type: OptStr = Field(None, description="Type of secret, can be `dependabot` or `actions`")
1718
key: OptStr = Field(None, description="Secret's name.")
1819
env: OptStr = Field(None, description="Environment variable to pull the secret from")
1920
value: OptStr = Field(None, description="Value to set this secret to")
@@ -23,6 +24,12 @@ class Secret(BaseModel):
2324
)
2425
exists: OptBool = Field(True, description="Set to false to delete a secret")
2526

27+
@validator("type")
28+
def validate_type(cls, v):
29+
if v not in ["dependabot", "actions"]:
30+
raise ValueError("Secret type must be either `dependabot` or `actions`")
31+
return v
32+
2633
@validator("value", always=True)
2734
def validate_value(cls, v, values) -> OptStr:
2835
if v is None:

0 commit comments

Comments
 (0)