diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..547304ee --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +# editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[Makefile] +indent_style = space +indent_size = 4 + +[*.{bash,sh}] +indent_style = space +indent_size = 4 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..3f47becc --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners +* @onedr0p @bjw-s # original pipeline code written by them +* @JJGadgets # professional yoinker, makes OCD edits to perfectly working code diff --git a/.github/ISSUE_TEMPLATE/container-request.yaml b/.github/ISSUE_TEMPLATE/container-request.yaml new file mode 100644 index 00000000..ec1c023c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/container-request.yaml @@ -0,0 +1,81 @@ +--- +name: Container Request +description: Request a new application to be containerized +labels: ["container-request"] + +body: + - type: markdown + attributes: + value: | + Doing you due diligence and filling out this form throughly + will gauge how serious your request is. + + - type: input + id: application-name + attributes: + label: Application Name + description: Name of the application you would like containerized + placeholder: e.g. Sonarr + validations: + required: true + + - type: input + id: application-source-code + attributes: + label: Application Source Code URL + description: URL to the source code of the application + placeholder: e.g. https://github.com/superseriousbusiness/gotosocial + validations: + required: true + + - type: dropdown + id: application-language + attributes: + label: Application Language + description: Language this application is written in + options: + - Go + - .Net + - Java + - PHP + - Python + - Ruby + - Typescript + - Other + validations: + required: true + + - type: dropdown + id: application-platforms + attributes: + label: Application Architectures + description: Architectures this application supports + multiple: true + options: + - linux/arm64 + - linux/amd64 + validations: + required: true + + - type: textarea + id: additional-information + attributes: + label: Additional Information + description: Mention anything to give further context to this container request + + - type: checkboxes + id: self-assign + attributes: + label: Assign to self + options: + - label: I will create a PR to containerize this application myself + required: false + + - type: checkboxes + id: terms + attributes: + label: Code of Conduct + description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/JJGadgets/containers/blob/main/CODE_OF_CONDUCT.md) + options: + - label: I agree to follow this project's Code of Conduct + required: true diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 00000000..110d0e5d --- /dev/null +++ b/.github/README.md @@ -0,0 +1,13 @@ +# Credits + +Hey there, @JJGadgets here. + +A big portion (most if not all, minus _maybe_ some changes for my own taste) of the pipeline code found here was originally made by @onedr0p and @bjw-s from the Kubernetes@Home community. + +While it would be possible to rewrite a simplified version of this, I decided it was more efficient to use the battle tested pipeline code, which was made available and licensed as open source, cherry-pick changes from upstream (onedr0p's [containers] repo) back into my repo, and where it seems fit, contribute back any fixes or features or whatnot. Effectively, a hivemind. + +This makes it easier to focus on actually building containers instead of worrying about the pipeline code to build and maintain said containers all by myself. + +As of 2023-12-01, the CODEOWNERS file in this folder (.github) will contain @onedr0p, @bjw-s and myself, to effectively reflect all the people who have contributed to the pipeline code found in this repo. +Only if a technical reason arises where the CODEOWNERS file shouldn't contain these amazing people, will it be changed to only reflect myself, and this README will be the main source of attribution from me. + diff --git a/.github/checks/metadata.rules.cue b/.github/checks/metadata.rules.cue new file mode 100644 index 00000000..271416af --- /dev/null +++ b/.github/checks/metadata.rules.cue @@ -0,0 +1,20 @@ +#Spec: { + app: #AcceptableAppName + base: bool + semantic_versioning?: bool + channels: [...#Channels] +} + +#Channels: { + name: #AcceptableChannelName + platforms: [...#AcceptedPlatforms] + stable: bool + tests: { + enabled: bool + type?: =~"^(cli|web)$" + } +} + +#AcceptableAppName: string & !="" & =~"^[a-zA-Z0-9_-]+$" +#AcceptableChannelName: string & !="" & =~"^[a-zA-Z0-9._-]+$" +#AcceptedPlatforms: "linux/amd64" | "linux/arm64" \ No newline at end of file diff --git a/.github/renovate.json5 b/.github/renovate.json5 new file mode 100644 index 00000000..25d13921 --- /dev/null +++ b/.github/renovate.json5 @@ -0,0 +1,35 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended", + "docker:enableMajor", + ":disableRateLimiting", + ":dependencyDashboard", + ":semanticCommits", + ":automergeDigest", + ":automergeBranch", + "helpers:pinGitHubActionDigests" + ], + "platform": "github", + "platformCommit": true, + "onboarding": false, + "requireConfig": "optional", + "dependencyDashboardTitle": "Renovate Dashboard 🤖", + "suppressNotifications": ["prIgnoreNotification"], + "packageRules": [ + { + "description": "Auto-merge Github Actions", + "matchDatasources": ["github-tags"], + "automerge": true, + "automergeType": "branch", + "ignoreTests": true, + "matchUpdateTypes": ["minor", "patch"], + "matchPackagePatterns": ["renovatebot/github-action"] + }, + { + "matchDatasources": ["docker"], + "matchUpdateTypes": ["digest"], + "commitMessagePrefix": "📣 " + } + ] +} diff --git a/.github/scripts/json-to-yaml.py b/.github/scripts/json-to-yaml.py new file mode 100644 index 00000000..fedd8172 --- /dev/null +++ b/.github/scripts/json-to-yaml.py @@ -0,0 +1,27 @@ + +import os +import json +import yaml + +def json_to_yaml(subdir, file): + obj = None + + json_file = os.path.join(subdir, file) + with open(json_file) as f: + obj = json.load(f) + + yaml_file = os.path.join(subdir, "metadata.yaml") + with open(yaml_file, "w") as f: + yaml.dump(obj, f) + + os.remove(json_file) + + +if __name__ == "__main__": + + for subdir, dirs, files in os.walk("./apps"): + for f in files: + if f != "metadata.json": + continue + json_to_yaml(subdir, f) + diff --git a/.github/scripts/prepare-matrices.py b/.github/scripts/prepare-matrices.py new file mode 100644 index 00000000..bfbfcc2d --- /dev/null +++ b/.github/scripts/prepare-matrices.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +import importlib.util +import sys +import os + +import json +import yaml +import requests + +from subprocess import check_output + +from os.path import isfile + +# read repository name and repository owner's username from custom env vars, else read from GitHub Actions default env vars +repo_user = os.environ.get('REPO_USER', os.environ.get('GITHUB_REPOSITORY_OWNER')) +repo_name = os.environ.get('REPO_NAME', os.environ.get('GITHUB_REPOSITORY').replace(repo_user, '')) + +TESTABLE_PLATFORMS = ["linux/amd64"] + +def load_metadata_file_yaml(file_path): + with open(file_path, "r") as f: + return yaml.safe_load(f) + +def load_metadata_file_json(file_path): + with open(file_path, "r") as f: + return json.load(f) + +def get_latest_version_py(latest_py_path, channel_name): + spec = importlib.util.spec_from_file_location("latest", latest_py_path) + latest = importlib.util.module_from_spec(spec) + sys.modules["latest"] = latest + spec.loader.exec_module(latest) + return latest.get_latest(channel_name) + +def get_latest_version_sh(latest_sh_path, channel_name): + out = check_output([latest_sh_path, channel_name]) + return out.decode("utf-8").strip() + +def get_latest_version(subdir, channel_name): + ci_dir = os.path.join(subdir, "ci") + if os.path.isfile(os.path.join(ci_dir, "latest.py")): + return get_latest_version_py(os.path.join(ci_dir, "latest.py"), channel_name) + elif os.path.isfile(os.path.join(ci_dir, "latest.sh")): + return get_latest_version_sh(os.path.join(ci_dir, "latest.sh"), channel_name) + elif os.path.isfile(os.path.join(subdir, channel_name, "latest.py")): + return get_latest_version_py(os.path.join(subdir, channel_name, "latest.py"), channel_name) + elif os.path.isfile(os.path.join(subdir, channel_name, "latest.sh")): + return get_latest_version_sh(os.path.join(subdir, channel_name, "latest.sh"), channel_name) + return None + +def get_published_version(image_name): + r = requests.get( + f"https://api.github.com/users/{repo_user}/packages/container/{image_name}/versions", + headers={ + "Accept": "application/vnd.github.v3+json", + "Authorization": "token " + os.environ["TOKEN"] + }, + ) + + if r.status_code != 200: + return None + + data = json.loads(r.text) + for image in data: + tags = image["metadata"]["container"]["tags"] + if "rolling" in tags: + tags.remove("rolling") + # Assume the longest string is the complete version number + return max(tags, key=len) + +def get_image_metadata(subdir, meta, forRelease=False, force=False, channels=None): + imagesToBuild = { + "images": [], + "imagePlatforms": [] + } + + if channels is None: + channels = meta["channels"] + else: + channels = [channel for channel in meta["channels"] if channel["name"] in channels] + + + for channel in channels: + version = get_latest_version(subdir, channel["name"]) + if version is None: + continue + + # Image Name + toBuild = {} + if channel.get("stable", False): + toBuild["name"] = meta["app"] + else: + toBuild["name"] = "-".join([meta["app"], channel["name"]]) + + # Skip if latest version already published + if not force: + published = get_published_version(toBuild["name"]) + if published is not None and published == version: + continue + toBuild["published_version"] = published + + toBuild["version"] = version + + # Image Tags + toBuild["tags"] = ["rolling", version] + if meta.get("semantic_versioning", False): + parts = version.split(".")[:-1] + while len(parts) > 0: + toBuild["tags"].append(".".join(parts)) + parts = parts[:-1] + + # Platform Metadata + for platform in channel["platforms"]: + + if platform not in TESTABLE_PLATFORMS and not forRelease: + continue + + toBuild.setdefault("platforms", []).append(platform) + + platformToBuild = {} + platformToBuild["name"] = toBuild["name"] + platformToBuild["platform"] = platform + platformToBuild["version"] = version + platformToBuild["channel"] = channel["name"] + + if meta.get("base", False): + platformToBuild["label_type"] ="org.opencontainers.image.base" + else: + platformToBuild["label_type"]="org.opencontainers.image" + + if isfile(os.path.join(subdir, channel["name"], "Dockerfile")): + platformToBuild["dockerfile"] = os.path.join(subdir, channel["name"], "Dockerfile") + platformToBuild["context"] = os.path.join(subdir, channel["name"]) + platformToBuild["goss_config"] = os.path.join(subdir, channel["name"], "goss.yaml") + else: + platformToBuild["dockerfile"] = os.path.join(subdir, "Dockerfile") + platformToBuild["context"] = subdir + platformToBuild["goss_config"] = os.path.join(subdir, "ci", "goss.yaml") + + platformToBuild["goss_args"] = "tail -f /dev/null" if channel["tests"].get("type", "web") == "cli" else "" + + platformToBuild["tests_enabled"] = channel["tests"]["enabled"] and platform in TESTABLE_PLATFORMS + + imagesToBuild["imagePlatforms"].append(platformToBuild) + imagesToBuild["images"].append(toBuild) + return imagesToBuild + +if __name__ == "__main__": + apps = sys.argv[1] + forRelease = sys.argv[2] == "true" + force = sys.argv[3] == "true" + imagesToBuild = { + "images": [], + "imagePlatforms": [] + } + + if apps != "all": + channels=None + apps = apps.split(",") + if len(sys.argv) == 5: + channels = sys.argv[4].split(",") + + for app in apps: + if not os.path.exists(os.path.join("./apps", app)): + print(f"App \"{app}\" not found") + exit(1) + + meta = None + if os.path.isfile(os.path.join("./apps", app, "metadata.yaml")): + meta = load_metadata_file_yaml(os.path.join("./apps", app, "metadata.yaml")) + elif os.path.isfile(os.path.join("./apps", app, "metadata.json")): + meta = load_metadata_file_json(os.path.join("./apps", app, "metadata.json")) + + imageToBuild = get_image_metadata(os.path.join("./apps", app), meta, forRelease, force=force, channels=channels) + if imageToBuild is not None: + imagesToBuild["images"].extend(imageToBuild["images"]) + imagesToBuild["imagePlatforms"].extend(imageToBuild["imagePlatforms"]) + else: + for subdir, dirs, files in os.walk("./apps"): + for file in files: + meta = None + if file == "metadata.yaml": + meta = load_metadata_file_yaml(os.path.join(subdir, file)) + elif file == "metadata.json": + meta = load_metadata_file_json(os.path.join(subdir, file)) + else: + continue + if meta is not None: + imageToBuild = get_image_metadata(subdir, meta, forRelease, force=force) + if imageToBuild is not None: + imagesToBuild["images"].extend(imageToBuild["images"]) + imagesToBuild["imagePlatforms"].extend(imageToBuild["imagePlatforms"]) + print(json.dumps(imagesToBuild)) diff --git a/.github/scripts/render-readme.py b/.github/scripts/render-readme.py new file mode 100644 index 00000000..58da3856 --- /dev/null +++ b/.github/scripts/render-readme.py @@ -0,0 +1,82 @@ +import os +import json +import requests +import yaml + +from jinja2 import Environment, PackageLoader, select_autoescape + +# read repository name and repository owner's username from custom env vars, else read from GitHub Actions default env vars +repo_user = os.environ.get('REPO_USER', os.environ.get('GITHUB_REPOSITORY_OWNER')) +repo_name = os.environ.get('REPO_NAME', os.environ.get('GITHUB_REPOSITORY').replace(repo_user, '')) + +env = Environment( + loader=PackageLoader("render-readme"), + autoescape=select_autoescape() +) + +def load_metadata_file_yaml(file_path): + with open(file_path, "r") as f: + return yaml.safe_load(f) + +def load_metadata_file_json(file_path): + with open(file_path, "r") as f: + return json.load(f) + +def load_metadata_file(file_path): + if file_path.endswith(".json"): + return load_metadata_file_json(file_path) + elif file_path.endswith(".yaml"): + return load_metadata_file_yaml(file_path) + return None + +# TODO: remove hard-coded repo owner +def get_latest_image(name): + r = requests.get( + f"https://api.github.com/users/{repo_user}/packages/container/{name}/versions", + headers={ + "Accept": "application/vnd.github.v3+json", + "Authorization": "token " + os.environ["GITHUB_TOKEN"] + }, + ) + if r.status_code != 200: + print(f"Failed to get versions for {name}: {r.status_code}: {r.text}") + return None + data = r.json() + for image in data: + tags = image["metadata"]["container"]["tags"] + if "rolling" in tags: + return image + print(f"Couldn't find latest tag for {name}") + return None + +if __name__ == "__main__": + base_images = [] + app_images = [] + for subdir, dirs, files in os.walk("./apps"): + for file in files: + if file != "metadata.yaml" and file != "metadata.json": + continue + meta = load_metadata_file(os.path.join(subdir, file)) + for channel in meta["channels"]: + name = "" + if channel.get("stable", False): + name = meta["app"] + else: + name = "-".join([meta["app"], channel["name"]]) + image = { + "name": name, + "channel": channel["name"], + "html_url": "" + } + gh_data = get_latest_image(name) + if gh_data is not None: + image["html_url"] = f"https://github.com/{repo_user}/{repo_name}/pkgs/container/{name}" + image["tags"] = sorted(gh_data["metadata"]["container"]["tags"]) + if meta["base"]: + base_images.append(image) + else: + app_images.append(image) + + template = env.get_template("README.md.j2") + with open("./README.md", "w") as f: + f.write(template.render(base_images=base_images, app_images=app_images)) diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt new file mode 100644 index 00000000..37641f6a --- /dev/null +++ b/.github/scripts/requirements.txt @@ -0,0 +1,4 @@ +requests +pyyaml +packaging +jinja2 \ No newline at end of file diff --git a/.github/scripts/templates/README.md.j2 b/.github/scripts/templates/README.md.j2 new file mode 100644 index 00000000..6072e08d --- /dev/null +++ b/.github/scripts/templates/README.md.j2 @@ -0,0 +1,110 @@ + +
+ + +## Containers + +_A heavily opinionated collection of container images_ + +
+ +
+ +![GitHub Repo stars](https://img.shields.io/github/stars/JJGadgets/containers?style=for-the-badge) +![GitHub forks](https://img.shields.io/github/forks/JJGadgets/containers?style=for-the-badge) +![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/JJGadgets/containers/release-scheduled.yaml?style=for-the-badge&label=Scheduled%20Release) + +
+ +Welcome to @JJGadgets' container images, if looking for a container start by [browsing the Packages tab in GitHub for this repo](https://github.com/JJGadgets?tab=packages&repo_name=containers). + +## Mission statement + +The containers found in this repo have similar baseline goals to the ones found in [onedr0p's containers repo](https://github.com/onedr0p/containers). + +However, the containers found in this repo are more opinionated for my (@JJGadgets) use. + +For example, using distroless base images, or adding on packages/plugins that fit my use case (e.g. Caddy plugins, installing smbclient packages on NextCloud, changing out the web servers on frontend containers from Nginx to Caddy), etc. + +I (@JJGadgets) make no guarantees that the containers found here will work for everyone else, **as long as it works for me and fulfils my uses**, I consider it "stable and tested". If you understand that, feel free to use them too! + +## Tag immutability + +The containers built here do not use immutable tags, as least not in the more common way you have seen from [linuxserver.io](https://fleet.linuxserver.io/) or [Bitnami](https://bitnami.com/stacks/containers). + +We do take a similar approach but instead of appending a `-ls69` or `-r420` prefix to the tag we instead insist on pinning to the sha256 digest of the image, while this is not as pretty it is just as functional in making the images immutable. + +| Container | Immutable | +|---------------------------------------------------------------------------|------------| +| `registry.jjgadgets.tech/jjgadgets/gotosocial:rolling` | ❌ | +| `registry.jjgadgets.tech/jjgadgets/gotosocial:0.12.2.1507` | ❌ | +| `registry.jjgadgets.tech/jjgadgets/gotosocial:rolling@sha256:8053...` | ✅ | +| `registry.jjgadgets.tech/jjgadgets/gotosocial:0.12.2.1507@sha256:8053...` | ✅ | + +_If pinning an image to the sha256 digest, tools like [Renovate](https://github.com/renovatebot/renovate) support updating the container on a digest or application version change._ + +## Passing arguments to a application + +Some applications do not support defining configuration via environment variables and instead only allow certain config to be set in the command line arguments for the app. To circumvent this, for applications that have an `entrypoint.sh` read below. + +1. First read the Kubernetes docs on [defining command and arguments for a Container](https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/). +2. Look up the documentation for the application and find a argument you would like to set. +3. Set the argument in the `args` section, be sure to include `entrypoint.sh` as the first arg and any application specific arguments thereafter. + + ```yaml + args: + - /entrypoint.sh + - --port + - "8080" + ``` + +## Configuration volume + +For applications that need to have persistent configuration data the config volume is hardcoded to `/config` inside the container. This is not able to be changed in most cases. + +## Available Images + +Each Image will be built with a `rolling` tag, along with tags specific to it's version. Available Images Below + +Container | Channel | Image | Latest Tags +--- | --- | --- | --- +{% for image in app_images | sort(attribute="name") -%} +[{{ image.name }}]({{ image.html_url }}) | {{ image.channel }} | registry.jjgadgets.tech/jjgadgets/{{ image.name }} | +{%- set space = joiner(" ") -%} +{%- for tag in image.tags -%} +{{ space() }}![{{ tag }}](https://img.shields.io/badge/{{ tag.replace("-", "--") }}-{% if tag == "latest" %}green{% else %}blue{% endif %}?style=flat-square) +{%- endfor %} +{% endfor %} + +### Automated tags + +Here's an example of how tags are created in the GitHub workflows, be careful with `metadata.json` as it does affect the outcome of how the tags will be created when the application is built. + +| Application | Channel | Stable | Base | Generated Tag | +|-----------------|-----------|---------|---------|----------------------------------| +| `ubuntu` | `focal` | `true` | `true` | `ubuntu:focal-rolling` | +| `ubuntu` | `focal` | `true` | `true` | `ubuntu:focal-19880312` | +| `alpine` | `3.16` | `true` | `true` | `alpine:rolling` | +| `alpine` | `3.16` | `true` | `true` | `alpine:3.16.0` | +| `gotosocial` | `develop` | `false` | `false` | `gotosocial-develop:0.12.2.1538` | +| `gotosocial` | `develop` | `false` | `false` | `gotosocial-develop:rolling` | +| `gotosocial` | `main` | `true` | `false` | `gotosocial:0.12.2.1507` | +| `gotosocial` | `main` | `true` | `false` | `gotosocial:rolling` | + +## Deprecations + +Containers here can be **deprecated** at any point, this could be for any reason described below. + +1. The upstream application is **no longer actively developed** +2. The upstream application has an **official upstream container** that follows closely to the mission statement described here +3. The upstream application has been **replaced with a better alternative** +4. The **maintenance burden** of keeping the container here **is too bothersome** + +**Note**: Deprecated containers in this repo may be removed at any time. Hopefully procrastination will keep the deprecated containers long enough. + +## Credits + +This repo's main pipeline code (found in `.github`) was taken from [onedr0p's containers repo](https://github.com/onedr0p/containers). Full attributions can be found in the `.github/README.md`. diff --git a/.github/workflows/build-images.yaml b/.github/workflows/build-images.yaml new file mode 100644 index 00000000..30fb9f1a --- /dev/null +++ b/.github/workflows/build-images.yaml @@ -0,0 +1,333 @@ +--- +name: "Image Build" + +on: + workflow_call: + inputs: + appsToBuild: + required: false + type: string + default: '' + channelsToBuild: + required: false + type: string + default: '' + pushImages: + required: false + default: false + type: boolean + sendNotifications: + required: false + default: false + type: boolean + force: + required: false + default: true + type: boolean + description: Force rebuild + secrets: + BOT_APP_ID: + description: The ID of the GitHub App + required: true + BOT_APP_PRIVATE_KEY: + description: The private key of the GitHub App + required: true + +jobs: + prepare: + name: Prepare to Build + runs-on: ubuntu-latest + outputs: + matrices: ${{ steps.prepare-matrices.outputs.matrices }} + steps: + - name: Generate Token + uses: actions/create-github-app-token@e995b4e40ace2eb5bf13137d9abe242c98f3aab6 # v1.6.0 + id: app-token + with: + app-id: "${{ secrets.BOT_APP_ID }}" + private-key: "${{ secrets.BOT_APP_PRIVATE_KEY }}" + + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + token: "${{ steps.app-token.outputs.token }}" + + - name: Setup Python + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 + with: + python-version: 3.x + cache: pip + + - name: Install Python Requirements + shell: bash + run: pip install -r ./.github/scripts/requirements.txt && pip freeze + + - name: Prepare Matrices + id: prepare-matrices + env: + TOKEN: ${{ steps.app-token.outputs.token }} + shell: bash + run: | + if [[ -z "${{ inputs.appsToBuild }}" ]]; then + matrices=$(python ./.github/scripts/prepare-matrices.py "all" "${{ inputs.pushImages }}" "${{ inputs.force }}") + else + if [[ -z "${{ inputs.channelsToBuild }}" ]]; then + matrices=$(python ./.github/scripts/prepare-matrices.py "${{ inputs.appsToBuild }}" "${{ inputs.pushImages }}" "${{ inputs.force }}") + else + matrices=$(python ./.github/scripts/prepare-matrices.py "${{ inputs.appsToBuild }}" "${{ inputs.pushImages }}" "${{ inputs.force }}" "${{ inputs.channelsToBuild }}") + fi + fi + echo "matrices=${matrices}" >> $GITHUB_OUTPUT + echo "${matrices}" + + build-platform-images: + name: Build/Test ${{ matrix.image.name }} (${{ matrix.image.platform }}) + needs: prepare + runs-on: ubuntu-latest + if: ${{ toJSON(fromJSON(needs.prepare.outputs.matrices).imagePlatforms) != '[]' && toJSON(fromJSON(needs.prepare.outputs.matrices).imagePlatforms) != '' }} + strategy: + fail-fast: false + matrix: + image: ["${{ fromJSON(needs.prepare.outputs.matrices).imagePlatforms }}"] + steps: + - name: Log Matrix Input + shell: bash + run: | + cat << EOF + ${{ toJSON(matrix.image)}} + EOF + - name: Validate Matrix Input + shell: bash + run: | + if [[ -z "${{ matrix.image.name }}" ]]; then + echo "image.name is empty" + exit 1 + fi + if [[ -z "${{ matrix.image.version }}" ]]; then + echo "image.version is empty" + exit 1 + fi + if [[ -z "${{ matrix.image.context }}" ]]; then + echo "image.context is empty" + exit 1 + fi + if [[ -z "${{ matrix.image.dockerfile }}" ]]; then + echo "image.dockerfile is empty" + exit 1 + fi + if [[ -z "${{ matrix.image.platform }}" ]]; then + echo "image.platform is empty" + exit 1 + fi + if [[ -z "${{ matrix.image.tests_enabled }}" ]]; then + echo "image.tests_enabled is empty" + exit 1 + fi + echo "${{ matrix.image.name }}" | grep -E "[a-zA-Z0-9_\.\-]+" || "Image Name is invalid" + echo "${{ matrix.image.version }}" | grep -E "[a-zA-Z0-9_\.\-]+" || "Image Version is invalid" + + - name: Generate Token + uses: actions/create-github-app-token@e995b4e40ace2eb5bf13137d9abe242c98f3aab6 # v1.6.0 + id: app-token + with: + app-id: "${{ secrets.BOT_APP_ID }}" + private-key: "${{ secrets.BOT_APP_PRIVATE_KEY }}" + + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + token: "${{ steps.app-token.outputs.token }}" + fetch-depth: 1 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + + - name: Login to GitHub Container Registry + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ghcr.io + username: "${{ github.actor }}" + password: "${{ secrets.GITHUB_TOKEN }}" + + - name: Setup Goss + if: ${{ matrix.image.tests_enabled }} + uses: e1himself/goss-installation-action@3b8952d233bcc9bb8e901ec819d8cde6daa7f546 # v1.1.0 + with: + version: v0.4.4 + + - name: Prepare Build Outputs + id: prepare-build-outputs + shell: bash + run: | + if [[ "${{ inputs.pushImages }}" == "true" ]]; then + image_name="ghcr.io/${{ github.repository_owner }}/${{ matrix.image.name }}" + outputs="type=image,name=${image_name},push-by-digest=true,name-canonical=true,push=true" + else + image_name="ghcr.io/${{ github.repository_owner }}/${{ matrix.image.name }}:zztesting" + outputs="type=docker,name=${image_name},push=false" + fi + echo "image_name=${image_name}" >> $GITHUB_OUTPUT + echo "outputs=${outputs}" >> $GITHUB_OUTPUT + + - name: Build Image + uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 # v5.1.0 + id: build + with: + build-args: |- + VERSION=${{ matrix.image.version }} + REVISION=${{ github.sha }} + CHANNEL=${{ matrix.image.channel }} + context: . # TODO: Use ${{ matrix.image.context }}, requires updates to all dockerfiles :-( + platforms: ${{ matrix.image.platform }} + file: ${{ matrix.image.dockerfile }} + outputs: ${{ steps.prepare-build-outputs.outputs.outputs }} + cache-from: type=gha + cache-to: type=gha,mode=max + labels: |- + org.opencontainers.image.title=${{ matrix.image.name }} + org.opencontainers.image.url=https://ghcr.io/${{ github.repository_owner }}/${{ matrix.image.name }} + org.opencontainers.image.source=https://github.com/${{ github.repository_owner }}/containers + org.opencontainers.image.version=${{ matrix.image.version }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.vendor=${{ github.repository_owner }} + + - name: Run Goss Tests + id: dgoss + if: ${{ matrix.image.tests_enabled }} + env: + CONTAINER_RUNTIME: docker + GOSS_FILE: ${{ matrix.image.goss_config }} + GOSS_OPTS: --retry-timeout 60s --sleep 2s --color --format documentation + GOSS_SLEEP: 2 + GOSS_FILES_STRATEGY: cp + CONTAINER_LOG_OUTPUT: goss_container_log_output + shell: bash + run: | + if [[ '${{ inputs.pushImages }}' == 'true' ]]; then + image_name="${{ steps.prepare-build-outputs.outputs.image_name }}@${{ steps.build.outputs.digest }}" + else + image_name="${{ steps.prepare-build-outputs.outputs.image_name }}" + fi + dgoss run ${image_name} ${{ matrix.image.goss_args }} + + - name: Export Digest + id: export-digest + if: ${{ inputs.pushImages }} + shell: bash + run: | + mkdir -p /tmp/${{ matrix.image.name }}/digests + digest="${{ steps.build.outputs.digest }}" + echo "${{ matrix.image.name }}" > "/tmp/${{ matrix.image.name }}/digests/${digest#sha256:}" + + - name: Upload Digest + if: ${{ inputs.pushImages}} + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + with: + name: ${{ matrix.image.name }} + path: /tmp/${{ matrix.image.name }}/* + if-no-files-found: error + retention-days: 1 + + merge: + name: Merge ${{ matrix.image.name }} + runs-on: ubuntu-latest + needs: ["prepare", "build-platform-images"] + # Always run merge, as the prior matrix is all or nothing. We test for prior step failure + # in the "Test Failed Bit" step. This ensures if one app fails, others can still complete. + if: ${{ always() && inputs.pushImages && toJSON(fromJSON(needs.prepare.outputs.matrices).images) != '[]' && toJSON(fromJSON(needs.prepare.outputs.matrices).images) != '' }} + strategy: + matrix: + image: ["${{ fromJSON(needs.prepare.outputs.matrices).images }}"] + fail-fast: false + steps: + - name: Download Digests + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: ${{ matrix.image.name }} + path: /tmp/${{ matrix.image.name }} + + - name: Ensure all platforms were built + id: ensure-platforms + shell: bash + run: | + EXPECTED_COUNT=$(cat << EOF | jq ". | length" + ${{ toJSON(matrix.image.platforms) }} + EOF + ) + ACTUAL_COUNT=$(ls -1 /tmp/${{ matrix.image.name }}/digests | wc -l) + if [[ $EXPECTED_COUNT != $ACTUAL_COUNT ]]; then + echo "Expected $EXPECTED_COUNT platforms, but only found $ACTUAL_COUNT" + echo "Expected: ${{ toJSON(matrix.image.platforms) }}" + echo "Actual: $(cat /tmp/${{ matrix.image.name }}/digests/*)" + exit 1 + fi + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + + - name: Login to GitHub Container Registry + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ghcr.io + username: "${{ github.actor }}" + password: "${{ secrets.GITHUB_TOKEN }}" + + - name: Log Files + working-directory: /tmp/${{ matrix.image.name }}/digests + shell: bash + run: | + ls -la + cat * + + - name: Merge Manifests + id: merge + working-directory: /tmp/${{ matrix.image.name }}/digests + env: + TAGS: ${{ toJSON(matrix.image.tags) }} + shell: bash + run: | + docker buildx imagetools create $(jq -cr '. | map("-t ghcr.io/${{ github.repository_owner }}/${{matrix.image.name}}:" + .) | join(" ")' <<< "$TAGS") \ + $(printf 'ghcr.io/${{ github.repository_owner }}/${{ matrix.image.name }}@sha256:%s ' *) + + - name: Inspect image + id: inspect + shell: bash + run: | + docker buildx imagetools inspect ghcr.io/${{ github.repository_owner }}/${{ matrix.image.name }}:${{ matrix.image.tags[0] }} + + - name: Build successful + id: build-success + if: ${{ always() && steps.merge.outcome == 'success' && steps.inspect.outcome == 'success' }} + shell: bash + run: | + echo "message=🎉 ${{ matrix.image.name }} (${{ matrix.image.tags[0] }})" >> $GITHUB_OUTPUT + echo "color=0x00FF00" >> $GITHUB_OUTPUT + + - name: Build failed + id: build-failed + if: ${{ always() && (steps.merge.outcome == 'failure' || steps.inspect.outcome == 'failure') }} + shell: bash + run: | + echo "message=💥 ${{ matrix.image.name }} (${{ matrix.image.tags[0] }})" >> $GITHUB_OUTPUT + echo "color=0xFF0000" >> $GITHUB_OUTPUT + + - name: Send Discord Webhook + uses: sarisia/actions-status-discord@9904e3130b8905d5b973df25623f17672dcb3466 # v1.13.0 + if: ${{ always() && inputs.sendNotifications == 'true' }} + with: + webhook: ${{ secrets.DISCORD_WEBHOOK }} + title: ${{ steps.build-failed.outputs.message || steps.build-success.outputs.message }} + color: ${{ steps.build-failed.outputs.color || steps.build-success.outputs.color }} + username: GitHub Actions + + # Summarize matrix https://github.community/t/status-check-for-a-matrix-jobs/127354/7 + build_success: + name: Build matrix success + runs-on: ubuntu-latest + needs: ["prepare", "merge"] + if: ${{ always() }} + steps: + - name: Check build matrix status + if: ${{ (inputs.appsToBuild != '' && inputs.appsToBuild != '[]') && (needs.merge.result != 'success' && needs.merge.result != 'skipped' && needs.prepare.result != 'success') }} + shell: bash + run: exit 1 diff --git a/.github/workflows/get-changed-images.yaml b/.github/workflows/get-changed-images.yaml new file mode 100644 index 00000000..aa1c985b --- /dev/null +++ b/.github/workflows/get-changed-images.yaml @@ -0,0 +1,41 @@ +--- +name: "Get Changed Images" + +on: + workflow_call: + outputs: + addedOrModified: + description: "Whether any files were added or modified" + value: ${{ jobs.get-changed-images.outputs.addedOrModified }} + addedOrModifiedImages: + description: "The images that were added or modified" + value: ${{ jobs.get-changed-images.outputs.addedOrModifiedImages }} + +jobs: + get-changed-images: + name: Get Changed Images + runs-on: ubuntu-latest + outputs: + addedOrModified: "${{ steps.filter.outputs.addedOrModified }}" + addedOrModifiedImages: "${{ steps.filter-containers.outputs.addedOrModifiedImages }}" + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Collect changed files + uses: dorny/paths-filter@4512585405083f25c027a35db413c2b3b9006d50 # v2.11.1 + id: filter + with: + list-files: json + filters: | + addedOrModified: + - added|modified: 'apps/*/**' + + - name: Determine changed images + if: ${{ steps.filter.outputs.addedOrModified == 'true' }} + id: filter-containers + shell: bash + run: | + PATHS='${{ steps.filter.outputs.addedOrModified_files }}' + OUTPUT=$(echo $PATHS | jq --raw-output -c 'map(. |= split("/")[1]) | unique | join(",")') + echo "addedOrModifiedImages=${OUTPUT}" >> $GITHUB_OUTPUT diff --git a/.github/workflows/pr-validate.yaml b/.github/workflows/pr-validate.yaml new file mode 100644 index 00000000..de77cdca --- /dev/null +++ b/.github/workflows/pr-validate.yaml @@ -0,0 +1,28 @@ +--- +name: "Pull Request: Validate" + +on: + pull_request: + branches: ["main"] + types: ["opened", "synchronize", "reopened"] + +concurrency: + group: "${{ github.head_ref }}-pr-validate" + cancel-in-progress: true + +jobs: + simple-checks: + uses: "${{ github.repository }}/.github/workflows/simple-checks.yaml@main" + + get-changed-images: + uses: "${{ github.repository }}/.github/workflows/get-changed-images.yaml@main" + + build-images: + needs: ["simple-checks", "get-changed-images"] + if: "${{ needs.get-changed-images.outputs.addedOrModified == 'true' }}" + uses: "${{ github.repository }}/.github/workflows/build-images.yaml@main" + secrets: inherit + with: + appsToBuild: "${{ needs.get-changed-images.outputs.addedOrModifiedImages }}" + pushImages: false + sendNotifications: false diff --git a/.github/workflows/release-on-merge.yaml b/.github/workflows/release-on-merge.yaml new file mode 100644 index 00000000..71e9c876 --- /dev/null +++ b/.github/workflows/release-on-merge.yaml @@ -0,0 +1,41 @@ +--- +name: Release on Merge + +concurrency: + group: container-release + cancel-in-progress: false + +on: + push: + branches: ["main"] + paths: + - "apps/**" + - ".github/scripts/templates/**" + - "!apps/**/metadata.json" + - "!apps/**/metadata.yaml" + - "!apps/**/README.md" + +jobs: + simple-checks: + uses: ./.github/workflows/simple-checks.yaml + + get-changed-images: + needs: ["simple-checks"] + uses: ./.github/workflows/get-changed-images.yaml + + build-images: + needs: ["simple-checks", "get-changed-images"] + if: ${{ needs.get-changed-images.outputs.addedOrModified == 'true' }} + uses: ./.github/workflows/build-images.yaml + secrets: inherit + with: + appsToBuild: "${{ needs.get-changed-images.outputs.addedOrModifiedImages }}" + pushImages: true + sendNotifications: true + + render-readme: + name: Render Readme + needs: build-images + if: ${{ always() && needs.build-images.result != 'failure' }} + uses: ./.github/workflows/render-readme.yaml + secrets: inherit diff --git a/.github/workflows/release-scheduled.yaml b/.github/workflows/release-scheduled.yaml new file mode 100644 index 00000000..dbc657c0 --- /dev/null +++ b/.github/workflows/release-scheduled.yaml @@ -0,0 +1,46 @@ +--- +name: Scheduled Release + +concurrency: + group: container-release + cancel-in-progress: false + +on: + workflow_dispatch: + inputs: + appsToBuild: + description: App(s) to build + required: false + type: string + default: all + force: + description: Force rebuild + type: boolean + default: false + required: true + schedule: + - cron: "0 * * * *" + +jobs: + simple-checks: + name: Simple Checks + uses: ./.github/workflows/simple-checks.yaml + + build-images: + name: Build Images + needs: simple-checks + uses: ./.github/workflows/build-images.yaml + secrets: inherit + permissions: + packages: write + with: + appsToBuild: ${{ inputs.appsToBuild }} + force: ${{ inputs.force == true }} + pushImages: true + sendNotifications: true + + render-readme: + name: Render Readme + needs: build-images + uses: ./.github/workflows/render-readme.yaml + secrets: inherit diff --git a/.github/workflows/render-readme.yaml b/.github/workflows/render-readme.yaml new file mode 100644 index 00000000..ab1f8d1c --- /dev/null +++ b/.github/workflows/render-readme.yaml @@ -0,0 +1,54 @@ +--- +name: "Render Readme" + +on: + workflow_call: + secrets: + BOT_APP_ID: + description: The ID of the GitHub App + required: true + BOT_APP_PRIVATE_KEY: + description: The private key of the GitHub App + required: true + +jobs: + render-readme: + name: Render README + runs-on: ubuntu-latest + steps: + - name: Generate Token + uses: actions/create-github-app-token@e995b4e40ace2eb5bf13137d9abe242c98f3aab6 # v1.6.0 + id: app-token + with: + app-id: "${{ secrets.BOT_APP_ID }}" + private-key: "${{ secrets.BOT_APP_PRIVATE_KEY }}" + + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + token: "${{ steps.app-token.outputs.token }}" + + - name: Setup Python + uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1 + with: + python-version: 3.x + cache: pip + + - name: Install Python Requirements + shell: bash + run: pip install -r ./.github/scripts/requirements.txt && pip freeze + + - name: Render README + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + shell: bash + run: python ./.github/scripts/render-readme.py + + - name: Commit Changes + shell: bash + run: | + git config --global user.name "bot-ross" + git config --global user.email "98030736+bot-ross[bot]@users.noreply.github.com" + git add ./README.md + git commit -m "Render README.md" || echo "No changes to commit" + git push origin || echo "No changes to push" diff --git a/.github/workflows/renovate.yaml b/.github/workflows/renovate.yaml new file mode 100644 index 00000000..389dd7c9 --- /dev/null +++ b/.github/workflows/renovate.yaml @@ -0,0 +1,58 @@ +--- +name: "Renovate" + +on: + workflow_dispatch: + inputs: + dryRun: + description: Dry Run + default: "false" + required: false + logLevel: + description: Log Level + default: "debug" + required: false + schedule: + - cron: "0 * * * *" + push: + branches: ["main"] + paths: + - .github/renovate.json5 + - .github/renovate/**.json5 + +# Retrieve BOT_USER_ID via `curl -s "https://api.github.com/users/${BOT_USERNAME}%5Bbot%5D" | jq .id` +env: + DRY_RUN: false + LOG_LEVEL: debug + RENOVATE_ONBOARDING_CONFIG_FILE_NAME: .github/renovate.json5 + RENOVATE_AUTODISCOVER: true + RENOVATE_AUTODISCOVER_FILTER: "${{ github.repository }}" + RENOVATE_USERNAME: "${{ secrets.BOT_USERNAME }}[bot]" + RENOVATE_GIT_AUTHOR: "${{ secrets.BOT_USERNAME }} <${{ secrets.BOT_USER_ID }}+${{ secrets.BOT_USERNAME }}[bot]@users.noreply.github.com>" + +jobs: + renovate: + name: Renovate + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Generate Token + uses: actions/create-github-app-token@e995b4e40ace2eb5bf13137d9abe242c98f3aab6 # v1.6.0 + id: app-token + with: + app-id: "${{ secrets.BOT_APP_ID }}" + private-key: "${{ secrets.BOT_APP_PRIVATE_KEY }}" + + - name: Override default config from dispatch variables + shell: bash + run: | + echo "RENOVATE_DRY_RUN=${{ github.event.inputs.dryRun || env.DRY_RUN }}" >> "${GITHUB_ENV}" + echo "LOG_LEVEL=${{ github.event.inputs.logLevel || env.LOG_LEVEL }}" >> "${GITHUB_ENV}" + + - name: Renovate + uses: renovatebot/github-action@5d3fbef92a76cbf78f5732d17c07a2e76e6f7555 # v39.1.4 + with: + configurationFile: "${{ env.RENOVATE_ONBOARDING_CONFIG_FILE_NAME }}" + token: "${{ steps.app-token.outputs.token }}" diff --git a/.github/workflows/simple-checks.yaml b/.github/workflows/simple-checks.yaml new file mode 100644 index 00000000..2c13863c --- /dev/null +++ b/.github/workflows/simple-checks.yaml @@ -0,0 +1,36 @@ +--- +name: "Simple Checks" + +on: + workflow_call: + +jobs: + metadata-validation: + name: Validate Image Metadata + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Collect changed files + uses: dorny/paths-filter@4512585405083f25c027a35db413c2b3b9006d50 # v2.11.1 + id: filter + with: + list-files: shell + filters: | + cue: + - added|modified: "apps/**/metadata.yaml" + - added|modified: "apps/**/metadata.json" + - added|modified: "./.github/metadata.rules.cue" + + - name: Setup CUE + if: ${{ steps.filter.outputs.cue == 'true' }} + uses: cue-lang/setup-cue@1713281ae501e533ff06108005dffeab9e2e5203 # v1.0.0 + + # Run against all files to ensure they are tested if the cue schema is changed. + - name: Validate image metadata + if: ${{ steps.filter.outputs.cue == 'true' }} + shell: bash + run: | + find ./apps/ -name metadata.json | xargs -I {} cue vet --schema '#Spec' {} ./.github/metadata.rules.cue + find ./apps/ -name metadata.yaml | xargs -I {} cue vet --schema '#Spec' {} ./.github/metadata.rules.cue diff --git a/.gitignore b/.gitignore index 68bc17f9..efb715b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +.goss +.private +**/*.pyc + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..915019aa --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,127 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at contact@buhl.casa. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/README.md b/README.md index 9b4b31a3..e69de29b 100644 --- a/README.md +++ b/README.md @@ -1,2 +0,0 @@ -# containers -Container images built by JJGadgets.