Migration to GNU Stow

This commit is contained in:
2025-12-31 01:47:19 +01:00
commit a60d41229c
1107 changed files with 114208 additions and 0 deletions

19
zsh/.oh-my-zsh/.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,19 @@
# Plugin owners
plugins/archlinux/ @ratijas
plugins/dbt/ @msempere
plugins/eza/ @pepoluan
plugins/genpass/ @atoponce
plugins/git-lfs/ @hellovietduc
plugins/gitfast/ @felipec
plugins/kube-ps1/ @mcornella
plugins/kubectl/ @mcornella
plugins/kubectx/ @mcornella
plugins/opentofu/ @mcornella
plugins/react-native @esthor
plugins/sdk/ @rgoldberg
plugins/shell-proxy/ @septs
plugins/starship/ @axieax
plugins/terraform/ @mcornella
plugins/universalarchive/ @Konfekt
plugins/wp-cli/ @joshmedeski
plugins/zoxide/ @ajeetdsouza

6
zsh/.oh-my-zsh/.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,6 @@
github:
- ohmyzsh
- robbyrussell
- mcornella
- carlosala
open_collective: ohmyzsh

View File

@@ -0,0 +1,87 @@
# Incident Response Plan
## Reporting a Vulnerability
Please see [the latest guidelines](https://github.com/ohmyzsh/ohmyzsh/blob/master/SECURITY.md) for instructions.
## Phases
### Triage
1. Is this a valid security vulnerability?
- [ ] It affects our CI/CD or any of our repositories.
- [ ] For ohmyzsh/ohmyzsh, it affects the latest commit.
- [ ] For others, it affects the latest commit on the default branch.
- [ ] It affects a third-party dependency:
- [ ] Zsh or git
- [ ] For a plugin, the vulnerability is a result of our usage of the dependency.
2. What's the scope of the vulnerability?
- [ ] Our codebase.
- [ ] A direct third-party dependency (Zsh, git, other plugins).
- [ ] An indirect third-party dependency.
- [ ] Out of scope, a third-party dependency that is the responsibility of the user.
- [ ] Out of scope, any other case (edit this plan and add the details).
3. Is the vulnerability actionable?
- [ ] Yes, we can submit a fix.
- [ ] Yes, we can disable a feature.
- [ ] Yes, we can mitigate the risk.
- [ ] Yes, we can remove a vulnerable dependency.
- [ ] Yes, we can apply a workaround.
- [ ] Yes, we can apply a patch to a vulnerable dependency ([example for CVE-2021-45444](https://github.com/ohmyzsh/ohmyzsh/blob/cb72d7dcbf08b435c7f8a6470802b207b2aa02c3/lib/vcs_info.zsh)).
- [ ] No, the vulnerability is not actionable.
4. What's the impact of the vulnerability?
Assess using the *CIA* triad:
- **Confidentiality**: example: report or sharing of secrets.
- **Integrity**: affects the integrity of the system (deletion, corruption or encryption of data, OS file corruption, etc.).
- **Availability**: denial of login, deletion of required files to boot / login, etc.
5. What's the exploitability of the vulnerability?
Consider how easy it is to exploit, and if it affects all users or requires specific configurations.
6. What's the severity of the vulnerability?
You can use the [CVSS v3.1](https://www.first.org/cvss/specification-document) to assess the severity of the vulnerability.
7. When was the vulnerability introduced?
- Find the responsible code path.
- Find the commit or Pull Request that introduced the vulnerability.
8. Who are our security contacts?
Assess upstream or downstream contacts, and their desired channels of security.
> TODO: add a list of contacts.
### Mitigation
- **Primary focus:** removing possibility of exploitation fast.
- **Secondary focus:** addressing the root cause.
> [!IMPORTANT]
> Make sure to test that the mitigation works as expected, and does not introduce new vulnerabilities.
> When deploying a patch, make sure not to disclose the vulnerability in the commit message or PR description.
> TODO: introduce a fast-track update process for security patches.
### Disclosure
Primary goal: inform our users about the vulnerability, and whether they are affected or not affected based on information they should be able to check themselves in a straightforward way.
> TODO: add a vulnerability disclosure template.
### Learn
- Document the vulnerability, steps performed, and lessons learned.
- Document the timeline of events.
- Document and address improvements on the Security Incident Response Plan.
- Depending on the severity of the vulnerability, consider disclosing the root cause or not based on likely impact on users and estimated potential victims still affected.

View File

@@ -0,0 +1,68 @@
name: Report a bug
description: Report a bug that isn't caused by Oh My Zsh. If unsure, use this form
body:
- type: markdown
attributes:
value: |
## Self Check
- Look for similar errors in existing [GitHub Issues](https://github.com/ohmyzsh/ohmyzsh/issues?q=is%3Aissue) (open or closed).
- Try reaching out on the [Discord server](https://discord.gg/ohmyzsh) for help.
- type: textarea
validations:
required: true
attributes:
label: Describe the bug
description: A clear description of what the bug is.
- type: textarea
validations:
required: true
attributes:
label: Steps to reproduce
description: |
Steps to reproduce the problem.
placeholder: |
For example:
1. Enable plugin '...'
2. Run command '...' or try to complete command '...'
3. See error
- type: textarea
validations:
required: true
attributes:
label: Expected behavior
description: A brief description of what should happen.
- type: textarea
attributes:
label: Screenshots and recordings
description: |
If applicable, add screenshots to help explain your problem. You can also record an asciinema session: https://asciinema.org/
- type: input
validations:
required: true
attributes:
label: OS / Linux distribution
placeholder: Windows 10, Ubuntu 20.04, Arch Linux, macOS 10.15...
- type: input
validations:
required: true
attributes:
label: Zsh version
description: Run `echo $ZSH_VERSION` to check.
placeholder: "5.6"
- type: input
validations:
required: true
attributes:
label: Terminal emulator
placeholder: iTerm2, GNOME Terminal, Terminal.app...
- type: dropdown
attributes:
label: If using WSL on Windows, which version of WSL
description: Run `wsl -l -v` to check.
options:
- WSL1
- WSL2
- type: textarea
attributes:
label: Additional context
description: Add any other context about the problem here. This can be themes, plugins, custom settings...

View File

@@ -0,0 +1,77 @@
name: Report a bug in Oh My Zsh
description: Create a report to help us improve Oh My Zsh
labels: ['Bug']
body:
- type: markdown
attributes:
value: |
## Self Check
- **Make sure this bug only happens with Oh My Zsh enabled**.
- Look for similar errors in existing [GitHub Issues](https://github.com/ohmyzsh/ohmyzsh/issues?q=is%3Aissue) (open or closed).
- Try reaching out on the [Discord server](https://discord.gg/ohmyzsh) for help.
- type: textarea
validations:
required: true
attributes:
label: Describe the bug
description: A clear description of what the bug is.
- type: textarea
validations:
required: true
attributes:
label: Steps to reproduce
description: |
Steps to reproduce the problem.
placeholder: |
For example:
1. Enable plugin '...'
2. Run command '...' or try to complete command '...'
3. See error
- type: textarea
validations:
required: true
attributes:
label: Expected behavior
description: A brief description of what should happen.
- type: textarea
attributes:
label: Screenshots and recordings
description: |
If applicable, add screenshots to help explain your problem. You can also record an asciinema session: https://asciinema.org/
- type: input
validations:
required: true
attributes:
label: OS / Linux distribution
placeholder: Windows 10, Ubuntu 20.04, Arch Linux, macOS 10.15...
- type: input
validations:
required: true
attributes:
label: Zsh version
description: Run `echo $ZSH_VERSION` to check.
placeholder: "5.6"
- type: input
validations:
required: true
attributes:
label: Oh My Zsh version
description: Run `omz version` to check.
placeholder: master (bf303965)
- type: input
validations:
required: true
attributes:
label: Terminal emulator
placeholder: iTerm2, GNOME Terminal, Terminal.app...
- type: dropdown
attributes:
label: If using WSL on Windows, which version of WSL
description: Run `wsl -l -v` to check.
options:
- WSL1
- WSL2
- type: textarea
attributes:
label: Additional context
description: Add any other context about the problem here. This can be themes, plugins, custom settings...

View File

@@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Support
url: https://github.com/ohmyzsh/ohmyzsh/discussions
about: Ask the community for support
- name: Get help on Discord
url: https://discord.gg/ohmyzsh
about: Have a quick question? Join the Discord server and ask on the appropriate channel.

View File

@@ -0,0 +1,37 @@
name: Feature request
description: Suggest a feature for Oh My Zsh
labels: ["Feature"]
body:
- type: markdown
attributes:
value: |
## Self Check
- Look for similar features in existing [GitHub Issues](https://github.com/ohmyzsh/ohmyzsh/issues?q=is%3Aissue) (open or closed).
- type: input
attributes:
label: If the feature request is for a plugin or theme, specify it here.
description: The name of the plugin or theme that you would like us to improve.
placeholder: e.g. Git plugin, Agnoster theme
- type: textarea
attributes:
label: If the feature solves a problem you have, specify it here.
description: A description of what the problem is.
placeholder: Ex. I'm always frustrated when...
- type: textarea
attributes:
label: Describe the proposed feature.
description: A description of what you want to happen. Be as specific as possible.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A description of any alternative solutions or features you've considered. This can also include other plugins or themes.
- type: textarea
attributes:
label: Additional context
description: Add any other context, screenshots or Discord conversations about the feature request here. Also if you have any PRs related to this issue that are already open that you would like us to look at.
- type: textarea
attributes:
label: Related Issues
description: Is there any open or closed issues that is related to this feature request? If so please link them below!

View File

@@ -0,0 +1,20 @@
## Standards checklist:
<!-- Fill with an x the ones that apply. Example: [x] -->
- [ ] The PR title is descriptive.
- [ ] The PR doesn't replicate another PR which is already open.
- [ ] I have read the contribution guide and followed all the instructions.
- [ ] The code follows the code style guide detailed in the wiki.
- [ ] The code is mine or it's from somewhere with an MIT-compatible license.
- [ ] The code is efficient, to the best of my ability, and does not waste computer resources.
- [ ] The code is stable and I have tested it myself, to the best of my abilities.
- [ ] If the code introduces new aliases, I provide a valid use case for all plugin users down below.
## Changes:
- [...]
## Other comments:
...

14
zsh/.oh-my-zsh/.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: "weekly"
day: "sunday"
labels: []
- package-ecosystem: "pip"
directory: "/.github/workflows/dependencies"
schedule:
interval: "weekly"
day: "sunday"
labels: []

48
zsh/.oh-my-zsh/.github/dependencies.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
dependencies:
plugins/gitfast:
repo: felipec/git-completion
branch: master
version: tag:v2.2
postcopy: |
set -e
rm -rf git-completion.plugin.zsh Makefile t tools
mv README.adoc MANUAL.adoc
mv -f src/* .
rmdir src
plugins/gradle:
repo: gradle/gradle-completion
branch: master
version: a9d7c822e42cc6a5b028b59e46cffcc8e7bc1134
precopy: |
set -e
find . ! -name _gradle ! -name LICENSE -delete
plugins/history-substring-search:
repo: zsh-users/zsh-history-substring-search
branch: master
version: 87ce96b1862928d84b1afe7c173316614b30e301
precopy: |
set -e
rm -f zsh-history-substring-search.plugin.zsh
test -e zsh-history-substring-search.zsh && mv zsh-history-substring-search.zsh history-substring-search.zsh
postcopy: |
set -e
test -e dependencies/OMZ-README.md && cat dependencies/OMZ-README.md >> README.md
plugins/wd:
repo: mfaerevaag/wd
branch: master
version: tag:v0.10.1
precopy: |
set -e
rm -r test
rm install.sh tty.gif wd.1
plugins/z:
branch: master
repo: agkozak/zsh-z
version: cf9225feebfae55e557e103e95ce20eca5eff270
precopy: |
set -e
test -e README.md && mv -f README.md MANUAL.md
postcopy: |
set -e
test -e _zshz && mv -f _zshz _z
test -e zsh-z.plugin.zsh && mv -f zsh-z.plugin.zsh z.plugin.zsh

View File

@@ -0,0 +1,43 @@
name: Update dependencies
on:
workflow_dispatch: {}
schedule:
- cron: "0 6 * * 0"
jobs:
check:
name: Check for updates
runs-on: ubuntu-latest
if: github.repository == 'ohmyzsh/ohmyzsh'
permissions:
contents: write # this is needed to push commits and branches
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
fetch-depth: 0
- name: Authenticate as @ohmyzsh
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.OHMYZSH_APP_ID }}
private-key: ${{ secrets.OHMYZSH_APP_PRIVATE_KEY }}
- name: Setup Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.12"
cache: "pip"
- name: Process dependencies
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
GIT_APP_NAME: ohmyzsh[bot]
GIT_APP_EMAIL: 54982679+ohmyzsh[bot]@users.noreply.github.com
TMP_DIR: ${{ runner.temp }}
run: |
pip install -r .github/workflows/dependencies/requirements.txt
python3 .github/workflows/dependencies/updater.py

View File

@@ -0,0 +1 @@
.venv

View File

@@ -0,0 +1,7 @@
certifi==2025.11.12
charset-normalizer==3.4.4
idna==3.11
PyYAML==6.0.3
requests==2.32.5
semver==3.0.4
urllib3==2.6.2

View File

@@ -0,0 +1,606 @@
import json
import os
import re
import shutil
import subprocess
import sys
import timeit
from copy import deepcopy
from typing import Literal, NotRequired, Optional, TypedDict
import requests
import yaml
from semver import Version
# Get TMP_DIR variable from environment
TMP_DIR = os.path.join(os.environ.get("TMP_DIR", "/tmp"), "ohmyzsh")
# Relative path to dependencies.yml file
DEPS_YAML_FILE = ".github/dependencies.yml"
# Dry run flag
DRY_RUN = os.environ.get("DRY_RUN", "0") == "1"
# utils for tag comparison
BASEVERSION = re.compile(
r"""[vV]?
(?P<major>(0|[1-9])\d*)
(\.
(?P<minor>(0|[1-9])\d*)
(\.
(?P<patch>(0|[1-9])\d*)
)?
)?
""",
re.VERBOSE,
)
def coerce(version: str) -> Optional[Version]:
match = BASEVERSION.search(version)
if not match:
return None
# BASEVERSION looks for `MAJOR.minor.patch` in the string given
# it fills with None if any of them is missing (for example `2.1`)
ver = {
key: 0 if value is None else value for key, value in match.groupdict().items()
}
# Version takes `major`, `minor`, `patch` arguments
ver = Version(**ver) # pyright: ignore[reportArgumentType]
return ver
class CodeTimer:
def __init__(self, name=None):
self.name = " '" + name + "'" if name else ""
def __enter__(self):
self.start = timeit.default_timer()
def __exit__(self, exc_type, exc_value, traceback):
self.took = (timeit.default_timer() - self.start) * 1000.0
print("Code block" + self.name + " took: " + str(self.took) + " ms")
### YAML representation
def str_presenter(dumper, data):
"""
Configures yaml for dumping multiline strings
Ref: https://stackoverflow.com/a/33300001
"""
if len(data.splitlines()) > 1: # check for multiline string
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
return dumper.represent_scalar("tag:yaml.org,2002:str", data)
yaml.add_representer(str, str_presenter)
yaml.representer.SafeRepresenter.add_representer(str, str_presenter)
# Types
class DependencyDict(TypedDict):
repo: str
branch: str
version: str
precopy: NotRequired[str]
postcopy: NotRequired[str]
class DependencyYAML(TypedDict):
dependencies: dict[str, DependencyDict]
class UpdateStatusFalse(TypedDict):
has_updates: Literal[False]
class UpdateStatusTrue(TypedDict):
has_updates: Literal[True]
version: str
compare_url: str
head_ref: str
head_url: str
class CommandRunner:
class Exception(Exception):
def __init__(self, message, returncode, stage, stdout, stderr):
super().__init__(message)
self.returncode = returncode
self.stage = stage
self.stdout = stdout
self.stderr = stderr
@staticmethod
def run_or_fail(command: list[str], stage: str, *args, **kwargs):
if DRY_RUN and command[0] == "gh":
command.insert(0, "echo")
result = subprocess.run(command, *args, capture_output=True, **kwargs)
if result.returncode != 0:
raise CommandRunner.Exception(
f"{stage} command failed with exit code {result.returncode}",
returncode=result.returncode,
stage=stage,
stdout=result.stdout.decode("utf-8"),
stderr=result.stderr.decode("utf-8"),
)
return result
class DependencyStore:
store: DependencyYAML = {"dependencies": {}}
@staticmethod
def set(data: DependencyYAML):
DependencyStore.store = data
@staticmethod
def update_dependency_version(path: str, version: str) -> DependencyYAML:
with CodeTimer(f"store deepcopy: {path}"):
store_copy = deepcopy(DependencyStore.store)
dependency = store_copy["dependencies"].get(path)
if dependency is None:
raise ValueError(f"Dependency {path} {version} not found")
dependency["version"] = version
store_copy["dependencies"][path] = dependency
return store_copy
@staticmethod
def write_store(file: str, data: DependencyYAML):
with open(file, "w") as yaml_file:
yaml.safe_dump(data, yaml_file, sort_keys=False)
class Dependency:
def __init__(self, path: str, values: DependencyDict):
self.path = path
self.values = values
self.name: str = ""
self.desc: str = ""
self.kind: str = ""
match path.split("/"):
case ["plugins", name]:
self.name = name
self.kind = "plugin"
self.desc = f"{name} plugin"
case ["themes", name]:
self.name = name.replace(".zsh-theme", "")
self.kind = "theme"
self.desc = f"{self.name} theme"
case _:
self.name = self.desc = path
def __str__(self):
output: str = ""
for key in DependencyDict.__dict__["__annotations__"].keys():
if key not in self.values:
output += f"{key}: None\n"
continue
value = self.values[key]
if "\n" not in value:
output += f"{key}: {value}\n"
else:
output += f"{key}:\n "
output += value.replace("\n", "\n ", value.count("\n") - 1)
return output
def update_or_notify(self):
# Print dependency settings
print(f"Processing {self.desc}...", file=sys.stderr)
print(self, file=sys.stderr)
# Check for updates
repo = self.values["repo"]
remote_branch = self.values["branch"]
version = self.values["version"]
is_tag = version.startswith("tag:")
try:
with CodeTimer(f"update check: {repo}"):
if is_tag:
status = GitHub.check_newer_tag(repo, version.replace("tag:", ""))
else:
status = GitHub.check_updates(repo, remote_branch, version)
if status["has_updates"] is True:
short_sha = status["head_ref"][:8]
new_version = status["version"] if is_tag else short_sha
try:
branch_name = f"update/{self.path}/{new_version}"
# Create new branch
branch = Git.checkout_or_create_branch(branch_name)
# Update dependency files
self.__apply_upstream_changes()
if not Git.repo_is_clean():
# Update dependencies.yml file
self.__update_yaml(
f"tag:{new_version}" if is_tag else status["version"]
)
# Add all changes and commit
has_new_commit = Git.add_and_commit(self.name, new_version)
if has_new_commit:
# Push changes to remote
Git.push(branch)
# Create GitHub PR
GitHub.create_pr(
branch,
f"chore({self.name}): update to version {new_version}",
f"""## Description
Update for **{self.desc}**: update to version [{new_version}]({status["head_url"]}).
Check out the [list of changes]({status["compare_url"]}).
""",
)
# Clean up repository
Git.clean_repo()
except (CommandRunner.Exception, shutil.Error) as e:
# Handle exception on automatic update
match type(e):
case CommandRunner.Exception:
# Print error message
print(
f"Error running {e.stage} command: {e.returncode}", # pyright: ignore[reportAttributeAccessIssue]
file=sys.stderr,
)
print(e.stderr, file=sys.stderr) # pyright: ignore[reportAttributeAccessIssue]
case shutil.Error:
print(f"Error copying files: {e}", file=sys.stderr)
try:
Git.clean_repo()
except CommandRunner.Exception as e:
print(
f"Error reverting repository to clean state: {e}",
file=sys.stderr,
)
sys.exit(1)
# Create a GitHub issue to notify maintainer
title = f"{self.path}: update to {new_version}"
body = f"""## Description
There is a new version of `{self.name}` {self.kind} available.
New version: [{new_version}]({status["head_url"]})
Check out the [list of changes]({status["compare_url"]}).
"""
print("Creating GitHub issue", file=sys.stderr)
print(f"{title}\n\n{body}", file=sys.stderr)
GitHub.create_issue(title, body)
except Exception as e:
print(e, file=sys.stderr)
def __update_yaml(self, new_version: str) -> None:
dep_yaml = DependencyStore.update_dependency_version(self.path, new_version)
DependencyStore.write_store(DEPS_YAML_FILE, dep_yaml)
def __apply_upstream_changes(self) -> None:
# Patterns to ignore in copying files from upstream repo
GLOBAL_IGNORE = [".git", ".github", ".gitignore"]
path = os.path.abspath(self.path)
precopy = self.values.get("precopy")
postcopy = self.values.get("postcopy")
repo = self.values["repo"]
branch = self.values["branch"]
remote_url = f"https://github.com/{repo}.git"
repo_dir = os.path.join(TMP_DIR, repo)
# Clone repository
Git.clone(remote_url, branch, repo_dir, reclone=True)
# Run precopy on tmp repo
if precopy is not None:
print("Running precopy script:", end="\n ", file=sys.stderr)
print(
precopy.replace("\n", "\n ", precopy.count("\n") - 1), file=sys.stderr
)
CommandRunner.run_or_fail(
["bash", "-c", precopy], cwd=repo_dir, stage="Precopy"
)
# Copy files from upstream repo
print(f"Copying files from {repo_dir} to {path}", file=sys.stderr)
shutil.copytree(
repo_dir,
path,
dirs_exist_ok=True,
ignore=shutil.ignore_patterns(*GLOBAL_IGNORE),
)
# Run postcopy on our repository
if postcopy is not None:
print("Running postcopy script:", end="\n ", file=sys.stderr)
print(
postcopy.replace("\n", "\n ", postcopy.count("\n") - 1),
file=sys.stderr,
)
CommandRunner.run_or_fail(
["bash", "-c", postcopy], cwd=path, stage="Postcopy"
)
class Git:
default_branch = "master"
@staticmethod
def clone(remote_url: str, branch: str, repo_dir: str, reclone=False):
# If repo needs to be fresh
if reclone and os.path.exists(repo_dir):
shutil.rmtree(repo_dir)
# Clone repo in tmp directory and checkout branch
if not os.path.exists(repo_dir):
print(
f"Cloning {remote_url} to {repo_dir} and checking out {branch}",
file=sys.stderr,
)
CommandRunner.run_or_fail(
["git", "clone", "--depth=1", "-b", branch, remote_url, repo_dir],
stage="Clone",
)
@staticmethod
def checkout_or_create_branch(branch_name: str):
# Get current branch name
result = CommandRunner.run_or_fail(
["git", "rev-parse", "--abbrev-ref", "HEAD"], stage="GetDefaultBranch"
)
Git.default_branch = result.stdout.decode("utf-8").strip()
# Create new branch and return created branch name
try:
# try to checkout already existing branch
CommandRunner.run_or_fail(
["git", "checkout", branch_name], stage="CreateBranch"
)
except CommandRunner.Exception:
# otherwise create new branch
CommandRunner.run_or_fail(
["git", "checkout", "-b", branch_name], stage="CreateBranch"
)
return branch_name
@staticmethod
def repo_is_clean() -> bool:
"""
Returns `True` if the repo is clean.
Returns `False` if the repo is dirty.
"""
try:
CommandRunner.run_or_fail(
["git", "diff", "--exit-code"], stage="CheckRepoClean"
)
return True
except CommandRunner.Exception:
return False
@staticmethod
def add_and_commit(scope: str, version: str) -> bool:
"""
Returns `True` if there were changes and were indeed commited.
Returns `False` if the repo was clean and no changes were commited.
"""
if Git.repo_is_clean():
return False
user_name = os.environ.get("GIT_APP_NAME")
user_email = os.environ.get("GIT_APP_EMAIL")
# Add all files to git staging
CommandRunner.run_or_fail(["git", "add", "-A", "-v"], stage="AddFiles")
# Reset environment and git config
clean_env = os.environ.copy()
clean_env["LANG"] = "C.UTF-8"
clean_env["GIT_CONFIG_GLOBAL"] = "/dev/null"
clean_env["GIT_CONFIG_NOSYSTEM"] = "1"
# Commit with settings above
CommandRunner.run_or_fail(
[
"git",
"-c",
f"user.name={user_name}",
"-c",
f"user.email={user_email}",
"commit",
"-m",
f"chore({scope}): update to {version}",
],
stage="CreateCommit",
env=clean_env,
)
return True
@staticmethod
def push(branch: str):
CommandRunner.run_or_fail(
["git", "push", "-u", "origin", branch], stage="PushBranch"
)
@staticmethod
def clean_repo():
CommandRunner.run_or_fail(
["git", "reset", "--hard", "HEAD"], stage="ResetRepository"
)
CommandRunner.run_or_fail(
["git", "checkout", Git.default_branch], stage="CheckoutDefaultBranch"
)
class GitHub:
@staticmethod
def check_newer_tag(repo, current_tag) -> UpdateStatusFalse | UpdateStatusTrue:
# GET /repos/:owner/:repo/git/refs/tags
url = f"https://api.github.com/repos/{repo}/git/refs/tags"
# Send a GET request to the GitHub API
response = requests.get(url)
current_version = coerce(current_tag)
if current_version is None:
raise ValueError(
f"Stored {current_version} from {repo} does not follow semver"
)
# If the request was successful
if response.status_code == 200:
# Parse the JSON response
data = response.json()
if len(data) == 0:
return {
"has_updates": False,
}
latest_ref = None
latest_version: Optional[Version] = None
for ref in data:
# we find the tag since GitHub returns it as plain git ref
tag_version = coerce(ref["ref"].replace("refs/tags/", ""))
if tag_version is None:
# we skip every tag that is not semver-complaint
continue
if latest_version is None or tag_version.compare(latest_version) > 0:
# if we have a "greater" semver version, set it as latest
latest_version = tag_version
latest_ref = ref
# raise if no valid semver tag is found
if latest_ref is None or latest_version is None:
raise ValueError(f"No tags following semver found in {repo}")
# we get the tag since GitHub returns it as plain git ref
latest_tag = latest_ref["ref"].replace("refs/tags/", "")
if latest_version.compare(current_version) <= 0:
return {
"has_updates": False,
}
return {
"has_updates": True,
"version": latest_tag,
"compare_url": f"https://github.com/{repo}/compare/{current_tag}...{latest_tag}",
"head_ref": latest_ref["object"]["sha"],
"head_url": f"https://github.com/{repo}/releases/tag/{latest_tag}",
}
else:
# If the request was not successful, raise an exception
raise Exception(
f"GitHub API request failed with status code {response.status_code}: {response.json()}"
)
@staticmethod
def check_updates(repo, branch, version) -> UpdateStatusFalse | UpdateStatusTrue:
url = f"https://api.github.com/repos/{repo}/compare/{version}...{branch}"
# Send a GET request to the GitHub API
response = requests.get(url)
# If the request was successful
if response.status_code == 200:
# Parse the JSON response
data = response.json()
# If the base is behind the head, there is a newer version
has_updates = data["status"] != "identical"
if not has_updates:
return {
"has_updates": False,
}
return {
"has_updates": data["status"] != "identical",
"version": data["commits"][-1]["sha"],
"compare_url": data["permalink_url"],
"head_ref": data["commits"][-1]["sha"],
"head_url": data["commits"][-1]["html_url"],
}
else:
# If the request was not successful, raise an exception
raise Exception(
f"GitHub API request failed with status code {response.status_code}: {response.json()}"
)
@staticmethod
def create_issue(title: str, body: str) -> None:
cmd = ["gh", "issue", "create", "-t", title, "-b", body]
CommandRunner.run_or_fail(cmd, stage="CreateIssue")
@staticmethod
def create_pr(branch: str, title: str, body: str) -> None:
# first of all let's check if PR is already open
check_cmd = [
"gh",
"pr",
"list",
"--state",
"open",
"--head",
branch,
"--json",
"title",
]
# returncode is 0 also if no PRs are found
output = json.loads(
CommandRunner.run_or_fail(check_cmd, stage="CheckPullRequestOpen")
.stdout.decode("utf-8")
.strip()
)
# we have PR in this case!
if len(output) > 0:
return
cmd = [
"gh",
"pr",
"create",
"-B",
Git.default_branch,
"-H",
branch,
"-t",
title,
"-b",
body,
]
CommandRunner.run_or_fail(cmd, stage="CreatePullRequest")
def main():
# Load the YAML file
with open(DEPS_YAML_FILE, "r") as yaml_file:
data: DependencyYAML = yaml.safe_load(yaml_file)
if "dependencies" not in data:
raise Exception("dependencies.yml not properly formatted")
# Cache YAML version
DependencyStore.set(data)
dependencies = data["dependencies"]
for path in dependencies:
dependency = Dependency(path, dependencies[path])
dependency.update_or_notify()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,66 @@
name: Test and Deploy installer
on:
workflow_dispatch: {}
push:
paths:
- 'tools/install.sh'
- '.github/workflows/installer/**'
- '.github/workflows/installer.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: false
permissions:
contents: read # to checkout
jobs:
test:
name: Test installer
if: github.repository == 'ohmyzsh/ohmyzsh'
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
- ubuntu-latest
- macos-latest
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Set up git repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install zsh
if: runner.os == 'Linux'
run: sudo apt-get update; sudo apt-get install zsh
- name: Test installer
run: sh ./tools/install.sh
deploy:
name: Deploy installer in install.ohmyz.sh
if: github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
environment: vercel
needs:
- test
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install Vercel CLI
run: npm install -g vercel
- name: Setup project and deploy
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
run: |
cp tools/install.sh .github/workflows/installer/install.sh
cd .github/workflows/installer
vc deploy --prod -t "$VERCEL_TOKEN"

View File

@@ -0,0 +1 @@
install.sh

View File

@@ -0,0 +1,2 @@
/*
!/install.sh

View File

@@ -0,0 +1,23 @@
{
"headers": [
{
"source": "/(|install.sh)",
"headers": [
{
"key": "Content-Type",
"value": "text/plain"
},
{
"key": "Content-Disposition",
"value": "inline; filename=\"install.sh\""
}
]
}
],
"rewrites": [
{
"source": "/",
"destination": "/install.sh"
}
]
}

View File

@@ -0,0 +1,43 @@
name: CI
on:
pull_request:
types:
- opened
- synchronize
branches:
- master
push:
branches:
- master
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
tests:
name: Run tests
runs-on: ubuntu-latest
if: github.repository == 'ohmyzsh/ohmyzsh'
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Set up git repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install zsh
run: sudo apt-get update; sudo apt-get install zsh
- name: Check syntax
run: |
for file in ./oh-my-zsh.sh \
./lib/*.zsh \
./plugins/*/*.plugin.zsh \
./plugins/*/_* \
./themes/*.zsh-theme; do
zsh -n "$file" || return 1
done

View File

@@ -0,0 +1,155 @@
name: Project tracking
on:
issues:
types: [opened, reopened]
pull_request_target:
types: [opened, reopened, synchronize]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
permissions: {}
jobs:
add-to-project:
name: Add to project
runs-on: ubuntu-latest
if: github.repository == 'ohmyzsh/ohmyzsh'
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: Authenticate as @ohmyzsh
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.OHMYZSH_APP_ID }}
private-key: ${{ secrets.OHMYZSH_APP_PRIVATE_KEY }}
- name: Read project data
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
ORGANIZATION: ohmyzsh
PROJECT_NUMBER: "1"
run: |
# Get Project data
gh api graphql -f query='
query($org: String!, $number: Int!) {
organization(login: $org){
projectV2(number: $number) {
id
fields(first:20) {
nodes {
... on ProjectV2Field {
id
name
}
}
}
}
}
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
# Parse project data
cat >> "$GITHUB_ENV" <<EOF
PROJECT_ID=$(jq '.data.organization.projectV2.id' project_data.json)
PLUGIN_FIELD_ID=$(jq '.data.organization.projectV2.fields.nodes[] | select(.name == "Plugin") | .id' project_data.json)
THEME_FIELD_ID=$(jq '.data.organization.projectV2.fields.nodes[] | select(.name == "Theme") | .id' project_data.json)
EOF
- name: Add to project
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
ISSUE_OR_PR_ID: ${{ github.event.issue.node_id || github.event.pull_request.node_id }}
run: |
item_id="$(gh api graphql -f query='
mutation($project: ID!, $content: ID!) {
addProjectV2ItemById(input: {projectId: $project, contentId: $content}) {
item {
id
}
}
}
' -f project="$PROJECT_ID" -f content="$ISSUE_OR_PR_ID" --jq '.data.addProjectV2ItemById.item.id')"
echo "ITEM_ID=$item_id" >> $GITHUB_ENV
- name: Classify Pull Request
if: github.event_name == 'pull_request_target'
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
# Get the list of modified files in the PR, and extract plugins and themes
gh pr view "$PR_NUMBER" \
--repo "$GITHUB_REPOSITORY" \
--json files --jq '.files.[].path' | awk -F/ '
BEGIN {
plugins = 0
themes = 0
}
/^plugins\// {
if (plugin == $2) next
plugin = $2
plugins++
}
/^themes\// {
gsub(/\.zsh-theme$/, "", $2)
if (theme == $2) next
theme = $2
themes++
}
END {
# plugin and theme are values controlled by the PR author
# so we should sanitize them before using anywhere else
if (plugins == 1) {
gsub(/[^a-zA-Z0-9._-]/, "", plugin)
print "PLUGIN=" plugin
}
if (themes == 1) {
gsub(/[^a-zA-Z0-9._-]/, "", theme)
print "THEME=" theme
}
}
' >> "$GITHUB_ENV"
- name: Fill Pull Request fields in project
if: github.event_name == 'pull_request_target'
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
run: |
gh api graphql -f query='
mutation (
$project: ID!
$item: ID!
$plugin_field: ID!
$plugin_value: String!
$theme_field: ID!
$theme_value: String!
) {
set_plugin: updateProjectV2ItemFieldValue(input: {
projectId: $project
itemId: $item
fieldId: $plugin_field
value: {
text: $plugin_value
}
}) {
projectV2Item {
id
}
}
set_theme: updateProjectV2ItemFieldValue(input: {
projectId: $project
itemId: $item
fieldId: $theme_field
value: {
text: $theme_value
}
}) {
projectV2Item {
id
}
}
}
' -f project="$PROJECT_ID" -f item="$ITEM_ID" \
-f plugin_field="$PLUGIN_FIELD_ID" -f plugin_value="$PLUGIN" \
-f theme_field="$THEME_FIELD_ID" -f theme_value="$THEME" \
--silent

View File

@@ -0,0 +1,65 @@
# This workflow uses actions that are not certified by GitHub. They are provided
# by a third-party and are governed by separate terms of service, privacy
# policy, and support documentation.
name: Scorecard supply-chain security
on:
# For Branch-Protection check. Only the default branch is supported. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection
branch_protection_rule:
# To guarantee Maintained check is occasionally updated. See
# https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained
schedule:
- cron: '20 7 * * 2'
push:
branches: ["master"]
# Declare default permissions as read only.
permissions: read-all
jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
# Needed to publish results and get a badge (see publish_results below).
id-token: write
contents: read
actions: read
# To allow GraphQL ListCommits to work
issues: read
pull-requests: read
# To detect SAST tools
checks: read
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0
with:
egress-policy: audit
- name: "Checkout code"
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
publish_results: true
- name: "Upload artifact"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: SARIF file
path: results.sarif
retention-days: 5
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
with:
sarif_file: results.sarif