Skip to content

Commit

Permalink
some intial work
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewthetechie committed Jun 12, 2022
1 parent 64bff62 commit 4b8c4b2
Show file tree
Hide file tree
Showing 12 changed files with 309 additions and 21 deletions.
5 changes: 4 additions & 1 deletion Docker/builder/rootfs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
pydantic
pydantic==1.9.1
actions-toolkit==0.1.13
pygithub==1.55
pyyaml==3.9.2
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pyenv-setup:
pyenv local gha-repo-manager

install-requirements: ## Pip installs our requirements
pip install -r Docker/builder/gsrootfs/requirements.txt
pip install -r Docker/builder/rootfs/requirements.txt
pip install -r requirements-dev.txt

setup-pre-commit:
Expand Down
25 changes: 17 additions & 8 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
name: "Python Container Action Template"
description: "Get started with Python Container actions"
author: "Jacob Tomlinson"
name: "Repo Manager"
description: "Manage your Github repo(s) settings and secrets using Github Actions and a yaml file"
author: "Andrew Herrington"
inputs:
myInput:
description: "Input to use"
default: "world"
action:
description: "What action to take with this action. One of validate, check, or apply. Validate will validate your settings file, but not touch your repo. Check will check your repo with your settings file and output a report of any drift. Apply will apply the settings in your settings file to your repo"
default: "check"
settings_file:
description: What yaml file to use as your settings. This is local to runner running this action.
default: ".github/settings.yml"
repo:
description: What repo to perform this action on. Default is self, as in the repo this action is running in
default: "self"
token:
description: What github token to use with this action.
required: true
outputs:
myOutput:
description: "Output from the action"
result:
description: "Result of the action"
runs:
using: "docker"
image: "Dockerfile"
17 changes: 10 additions & 7 deletions examples/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ settings:
# A URL with more information about the repository. Set to an empty string to clear.
homepage: https://example.github.io/

# A comma-separated list of topics to set on the repository. Set to an empty string ("") to clear all topics.
topics: gha, foo, bar
# 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
topics:
- gha
- foo
- bar

# Either `true` to make the repository private, or `false` to make it public.
private: false
Expand All @@ -38,7 +41,7 @@ settings:
# Either `true` to enable downloads for this repository, `false` to disable them.
has_downloads: true

# Updates the default branch for this repository.
# Set the default branch for this repository.
default_branch: main

# Either `true` to allow squash-merging pull requests, or `false` to prevent
Expand Down Expand Up @@ -83,7 +86,7 @@ labels:
# set exists: false to delete a label. A delete that results in a "not found" will not fail a run
exists: false

branch_protection:
branch_protections:
- name: master
# https://docs.github.com/en/rest/reference/repos#update-branch-protection
# Branch Protection settings. Set to null to disable. Leave a value out to leave set at current repo settings
Expand Down Expand Up @@ -126,13 +129,13 @@ secrets:
- key: SECRET_KEY
# pull the value from an environment variable. If this variable is not found in the env, throw an error and fail the run
# Set env vars on the github action job from secrets in your repo to sync screts across repos
src: env/SECRET_VALUE
env: SECRET_VALUE
- key: ANOTHER_SECRET
# set a value directly in your yaml, probably not a good idea for things that are actually a secret
val: bar
value: bar
- key: THIRD_SECRET
# pull the value from an environment variable
src: env/THIRD_VALUE
env: THIRD_VALUE
# setting a value as not required allows you to not pass in an env var. if THIRD_VALUE is not set in the env, this secret won't be set but no error will be thrown
required: false
- key: DELETED_SECRET
Expand Down
75 changes: 71 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,80 @@
import os
import requests # noqa We are just importing this to prove the dependency installed correctly
import sys
from typing import Any
from typing import Dict

import yaml
from actions_toolkit import core as actions_toolkit

from repo_manager.schemas import load_config

VALID_ACTIONS = {"validate": None, "check": None, "apply": None}


def get_inputs() -> Dict[str, Any]:
"""Get inputs from our workflow, valudate them, and return as a dict
Reads inputs from actions.yaml. Non required inputs that are not set are returned as None
Returns:
Dict[str, Any]: [description]
"""
parsed_inputs = dict()
with open("action.yml") as fh:
action_config = yaml.safe_load(fh)
for input_name, input_config in action_config["inputs"].items():
this_input_value = actions_toolkit.get_input(
input_name, required=input_config.get("required", input_config.get("default", None) == None)
)
parsed_inputs[input_name] = this_input_value if this_input_value != "" else None
# set defaults from actions.yaml if not running in github, this is for local testing
# https://docs.github.com/en/actions/learn-github-actions/environment-variables
if (
os.environ.get("CI", "false").lower() == "false"
and os.environ.get("GITHUB_ACTIONS", "false").lower() == "false"
):
if parsed_inputs[input_name] is None:
parsed_inputs[input_name] = input_config.get("default", None)
if parsed_inputs[input_name] is None:
actions_toolkit.set_failed(f"Error getting inputs. {input_name} is missing a default")

# validate our inputs
parsed_inputs["action"] = parsed_inputs["action"].lower()
if parsed_inputs["action"] not in VALID_ACTIONS.keys():
actions_toolkit.set_failed(
f"Error while loading RepoManager Config. {parsed_inputs['action']} is not a valid action in {VALID_ACTIONS.keys()}"
)

if not os.path.exists(parsed_inputs["settings_file"]):
actions_toolkit.set_failed(
f"Error while loading RepoManager Config. {parsed_inputs['settings_file']} does not exist"
)

if parsed_inputs["repo"] != "self":
if len(parsed_inputs.split("/")) != 2:
actions_toolkit.set_failed(
f"Error while loading RepoManager Config. {parsed_inputs['repo']} is not a valid github repo. Please be sure to enter in the style of 'owner/repo-name'."
)
else:
parsed_inputs["repo"] = os.environ.get("GITHUB_REPOSITORY", None)
if parsed_inputs["repo"] is None:
actions_toolkit.set_failed(
f"Error getting inputs. repo is 'self' GITHUB_REPOSITORY env var is not set. Please set INPUT_REPO or GITHUB_REPOSITORY in the env"
)

return parsed_inputs


def main():
my_input = os.environ["INPUT_MYINPUT"]
inputs = get_inputs()
actions_toolkit.debug(f"Loading config from {inputs['settings_file']}")
config = load_config(inputs["settings_file"])

my_output = f"Hello {my_input}"
actions_toolkit.info(f"Config from {inputs['settings_file']} validated.")
if inputs["action"].lower() == "validate":
actions_toolkit.set_output("result", f"Validated {inputs['settingsFile']}")
sys.exit(0)

print(f"::set-output name=myOutput::{my_output}")
print(inputs)
print(config)


if __name__ == "__main__":
Expand Down
Empty file removed repo-manager/schema.py
Empty file.
File renamed without changes.
26 changes: 26 additions & 0 deletions repo_manager/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import List
from typing import Optional
from typing import Union

import yaml
from pydantic import BaseModel # pylint: disable=E0611
from pydantic import Field

from .branch_protection import BranchProtection
from .label import Label
from .secret import Secret
from .settings import Settings


class RepoManagerConfig(BaseModel):
settings: Optional[Settings]
branch_protections: Optional[List[BranchProtection]]
secrets: Optional[List[Secret]]


def load_config(filename: str) -> RepoManagerConfig:
"""Loads a yaml file into a RepoManagerconfig"""
with open(filename) as fh:
this_dict = yaml.safe_load(fh)

return RepoManagerConfig(**this_dict)
72 changes: 72 additions & 0 deletions repo_manager/schemas/branch_protection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from typing import List
from typing import Optional
from typing import Union

from pydantic import BaseModel # pylint: disable=E0611
from pydantic import conint
from pydantic import Field
from pydantic import HttpUrl # pylint: disable=E0611

OptBool = Optional[bool]
OptStr = Optional[str]


class RestrictionOptions(BaseModel):
apps: Optional[List[str]] = Field(None, description="List of App names that cannot push to this branch")
users: Optional[List[str]] = Field(
None, description="List of users who cannot push to this branch, only available to orgs"
)
teams: Optional[List[str]] = Field(
None, description="List of teams who cannot push to this branch, only available to orgs"
)


class StatusChecksOptions(BaseModel):
strict: OptBool = Field(None, description="Require branches to be up to date before merging.")
checks: Optional[List[str]] = Field(
None, description="The list of status checks to require in order to merge into this branch"
)


class DismissalOptions(BaseModel):
users: Optional[List[str]] = Field(
None, description="List of users who can dismiss pull request reviews, only available to orgs"
)
teams: Optional[List[str]] = Field(
None, description="List of teams who can dismiss pull request reviews, only available to orgs"
)


class PROptions(BaseModel):
required_approving_review_count: Optional[conint(ge=1, le=6)] = Field(
None, description="The number of approvals required. (1-6)"
)
dismiss_stale_reviews: OptBool = Field(
None, description="Dismiss approved reviews automatically when a new commit is pushed."
)
require_code_owner_reviews: OptBool = Field(None, description="Blocks merge until code owners have reviewed.")
dismissal_restrictions: Optional[DismissalOptions] = Field(
None, description="Options related to PR dismissal. Only available to Orgs."
)


class ProtectionOptions(BaseModel):
required_pull_request_reviews: Optional[PROptions] = Field(None, description="Options related to PR reviews")
required_status_checks: Optional[StatusChecksOptions] = Field(
None, description="Options related to required status checks"
)
enforce_admins: OptBool = Field(
None,
description="Enforce all configured restrictions for administrators. Set to true to enforce required status checks for repository administrators. Set to null to disable.",
)
required_linear_history: OptBool = Field(
None, description="Prevent merge commits from being pushed to matching branches"
)
restrictions: Optional[RestrictionOptions] = Field(
None, description="Options related to restricting who can push to this branch"
)


class BranchProtection(BaseModel):
name: OptStr = Field(None, description="Name of the branch")
protection: Optional[ProtectionOptions] = Field(None, description="Protection options for the branch")
18 changes: 18 additions & 0 deletions repo_manager/schemas/label.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import List
from typing import Optional
from typing import Union

from pydantic import BaseModel # pylint: disable=E0611
from pydantic import Field
from pydantic import HttpUrl # pylint: disable=E0611

OptBool = Optional[bool]
OptStr = Optional[str]


class Label(BaseModel):
name: OptStr = Field(None, description="Label's name.")
color: OptStr = Field(None, description="Color code of this label")
description: OptStr = Field(None, description="Description of the label")
new_name: OptBool = Field(None, description="If set, rename a label from name to new_name.")
exists: OptBool = Field(True, description="Set to false to delete a label")
32 changes: 32 additions & 0 deletions repo_manager/schemas/secret.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from typing import List
from typing import Optional
from typing import Union

from pydantic import BaseModel # pylint: disable=E0611
from pydantic import Field
from pydantic import HttpUrl # pylint: disable=E0611
from pydantic import validator

OptBool = Optional[bool]
OptStr = Optional[str]


class Secret(BaseModel):
key: OptStr = Field(None, description="Secret's name.")
env: OptStr = Field(None, description="Environment variable to pull the secret from")
value: OptStr = Field(None, description="Value to set this secret to")
required: OptBool = Field(
True,
description="Setting a value as not required allows you to not pass in an env var without causing an error",
)
exists: OptBool = Field(True, description="Set to false to delete a secret")

@validator("value", always=True)
def validate_value(cls, v, values) -> OptStr:
if v is None:
return None

if values["env"] is not None:
raise ValueError("Cannot set an env and a value in the same secret, remove one.")

return v
58 changes: 58 additions & 0 deletions repo_manager/schemas/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from typing import Optional
from typing import Set
from typing import Union

from pydantic import BaseModel # pylint: disable=E0611
from pydantic import Field
from pydantic import HttpUrl # pylint: disable=E0611

OptBool = Optional[bool]
OptStr = Optional[str]


class Settings(BaseModel):
description: OptStr = Field(None, description="A short description of the repository that will show up on GitHub.")
homepage: Optional[Union[str, HttpUrl]] = Field(
None, description="A URL with more information about the repository."
)
topics: Optional[Union[str, Set[str]]] = Field(
None, description="A list of strings to apply as topics on the repo"
)
private: OptBool = Field(
None, description="Either `true` to make the repository private, or `false` to make it public."
)
has_issues: OptBool = Field(
None, description="Either `true` to enable issues for this repository, `false` to disable them."
)
has_projects: OptBool = Field(
None,
description="Either `true` to enable projects for this repository, or `false` to disable them. If projects are disabled for the organization, passing `true` will cause an API error.",
)
has_wiki: OptBool = Field(
None, description="Either `true` to enable the wiki for this repository, `false` to disable it."
)
has_downloads: OptBool = Field(
None, description="Either `true` to enable downloads for this repository, `false` to disable them."
)
default_branch: OptStr = Field(None, description="Set the default branch for this repository. ")
allow_squash_merge: OptBool = Field(
None, description="Either `true` to allow squash-merging pull requests, or `false` to prevent squash-merging."
)
allow_merge_commit: OptBool = Field(
None,
description="Either `true` to allow merging pull requests with a merge commit, or `false` to prevent merging pull requests with merge commits.",
)
allow_rebase_merge: OptBool = Field(
None,
description=" # Either `true` to allow rebase-merging pull requests, or `false` to prevent rebase-merging.",
)
delete_branch_on_merge: OptBool = Field(
None, description="Either `true` to enable automatic deletion of branches on merge, or `false` to disable"
)
enable_automate_security_fixes: OptBool = Field(
None,
description="Either `true` to enable automated security fixes, or `false` to disable automated security fixes.",
)
enable_vulnerability_alerts: OptBool = Field(
None, description="Either `true` to enable vulnerability alerts, or `false` to disable vulnerability alerts."
)

0 comments on commit 4b8c4b2

Please sign in to comment.