Skip to content

Commit 2c1cdbf

Browse files
feat: ap update github actions workflows like ci.yml
1 parent 755bc8c commit 2c1cdbf

2 files changed

Lines changed: 114 additions & 24 deletions

File tree

src/afterpython/cli/commands/update.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def update():
2828
)
2929
@click.option(
3030
"--all",
31+
"all_",
3132
is_flag=True,
3233
help="Also update pre-commit hooks and pixi dependencies",
3334
)
@@ -38,7 +39,7 @@ def update():
3839
metavar="PACKAGE",
3940
help="Package to exclude from upgrade (can be passed multiple times)",
4041
)
41-
def dependencies(upgrade: bool, all: bool, exclude: tuple[str, ...]):
42+
def dependencies(upgrade: bool, all_: bool, exclude: tuple[str, ...]):
4243
"""Check and update project dependencies to latest versions"""
4344
from afterpython.pcu import get_dependencies, update_dependencies
4445
from afterpython.utils import has_pixi, has_uv
@@ -110,7 +111,7 @@ def dependencies(upgrade: bool, all: bool, exclude: tuple[str, ...]):
110111
click.echo(
111112
"uv not found. Updated pyproject.toml only (packages not installed)."
112113
)
113-
if upgrade and all:
114+
if upgrade and all_:
114115
subprocess.run(["ap", "pre-commit", "autoupdate"])
115116
click.echo("All pre-commit hooks updated successfully.")
116117
if has_pixi():
@@ -141,6 +142,61 @@ def dependencies(upgrade: bool, all: bool, exclude: tuple[str, ...]):
141142
update.add_command(dependencies, name="deps") # alias for "dependencies"
142143

143144

145+
@update.command()
146+
@click.option("--deploy", is_flag=True, help="Update .github/workflows/deploy.yml")
147+
@click.option("--ci", is_flag=True, help="Update .github/workflows/ci.yml")
148+
@click.option("--release", is_flag=True, help="Update .github/workflows/release.yml")
149+
@click.option("--dependabot", is_flag=True, help="Update .github/dependabot.yml")
150+
@click.option(
151+
"--all",
152+
"all_",
153+
is_flag=True,
154+
help="Update every supported workflow file",
155+
)
156+
@click.option(
157+
"--no-backup",
158+
is_flag=True,
159+
help="Skip creating .backup copies of existing workflow files",
160+
)
161+
def workflows(
162+
deploy: bool,
163+
ci: bool,
164+
release: bool,
165+
dependabot: bool,
166+
all_: bool,
167+
no_backup: bool,
168+
):
169+
"""Update GitHub Actions workflow files (deploy, ci, release, dependabot)
170+
171+
Existing files are backed up to <file>.backup before being overwritten
172+
so user customizations (matrix tweaks, extra steps, etc.) aren't lost.
173+
"""
174+
from afterpython.tools.github_actions import (
175+
VALID_WORKFLOWS,
176+
update_workflow_file,
177+
)
178+
179+
selected_flags = {
180+
"deploy": deploy,
181+
"ci": ci,
182+
"release": release,
183+
"dependabot": dependabot,
184+
}
185+
individual = [name for name, picked in selected_flags.items() if picked]
186+
187+
if all_ and individual:
188+
raise click.UsageError("Pass --all or individual workflow flags, not both.")
189+
if not all_ and not individual:
190+
raise click.UsageError(
191+
"Specify which workflows to update with flags "
192+
"(e.g. --deploy --ci) or pass --all."
193+
)
194+
195+
selected = VALID_WORKFLOWS if all_ else individual
196+
for name in selected:
197+
update_workflow_file(name, backup=not no_backup)
198+
199+
144200
@update.command()
145201
@click.pass_context
146202
@click.option(
Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,82 @@
1+
# VIBE-CODED
12
import shutil
23
from pathlib import Path
34

45
import click
56

67
import afterpython as ap
78

9+
# Workflow names supported by `create_workflow` / `update_workflow_file`.
10+
# "dependabot" is included even though it lives at .github/dependabot.yml
11+
# (not under workflows/) so a single CLI surface can refresh all GH templates.
12+
VALID_WORKFLOWS = ("deploy", "ci", "release", "dependabot")
813

9-
def _copy_github_template(template_name: str, target_path: Path):
10-
"""Helper to copy GitHub-related templates"""
11-
if target_path.exists():
12-
click.echo(f"{target_path} already exists")
13-
return
1414

15-
# Create parent directory if it doesn't exist
16-
target_path.parent.mkdir(parents=True, exist_ok=True)
15+
def _resolve_workflow_target(name: str) -> tuple[Path, Path]:
16+
"""Return (target_path, template_path) for a workflow name."""
17+
if name not in VALID_WORKFLOWS:
18+
raise ValueError(
19+
f"Unknown workflow '{name}'. Valid: {', '.join(VALID_WORKFLOWS)}"
20+
)
21+
user_path = ap.paths.user_path
22+
templates_path = ap.paths.templates_path
23+
if name == "dependabot":
24+
target = user_path / ".github" / "dependabot.yml"
25+
template = templates_path / "dependabot-template.yml"
26+
else:
27+
target = user_path / ".github" / "workflows" / f"{name}.yml"
28+
template = templates_path / f"{name}-workflow-template.yml"
29+
return target, template
1730

18-
# Copy template from package
19-
template_path = ap.paths.templates_path / template_name
31+
32+
def _copy_github_template(template_path: Path, target_path: Path):
33+
"""Copy a template file into the project, creating parent dirs as needed."""
2034
if not template_path.exists():
2135
raise FileNotFoundError(
2236
f"Template file not found: {template_path}\n"
2337
"This might indicate a corrupted installation. Please reinstall afterpython."
2438
)
25-
39+
target_path.parent.mkdir(parents=True, exist_ok=True)
2640
shutil.copy(template_path, target_path)
27-
print(f"Created {target_path}")
2841

2942

3043
def create_workflow(workflow_name: str):
31-
"""Create a GitHub Actions workflow from template"""
44+
"""Create a GitHub Actions workflow from template (no-op if it exists)."""
3245
if ".yml" in workflow_name:
3346
workflow_name = workflow_name.replace(".yml", "")
3447

35-
user_path = ap.paths.user_path
36-
workflow_path = user_path / ".github" / "workflows" / f"{workflow_name}.yml"
37-
template_name = f"{workflow_name}-workflow-template.yml"
38-
39-
_copy_github_template(template_name, workflow_path)
48+
target_path, template_path = _resolve_workflow_target(workflow_name)
49+
if target_path.exists():
50+
click.echo(f"{target_path} already exists")
51+
return
52+
_copy_github_template(template_path, target_path)
53+
click.echo(f"Created {target_path}")
4054

4155

4256
def create_dependabot():
43-
"""Create Dependabot configuration for GitHub Actions updates"""
44-
user_path = ap.paths.user_path
45-
dependabot_path = user_path / ".github" / "dependabot.yml"
46-
template_name = "dependabot-template.yml"
57+
"""Create Dependabot configuration for GitHub Actions updates."""
58+
target_path, template_path = _resolve_workflow_target("dependabot")
59+
if target_path.exists():
60+
click.echo(f"{target_path} already exists")
61+
return
62+
_copy_github_template(template_path, target_path)
63+
click.echo(f"Created {target_path}")
64+
4765

48-
_copy_github_template(template_name, dependabot_path)
66+
def update_workflow_file(name: str, backup: bool = True):
67+
"""Overwrite a workflow file with the latest template.
68+
69+
If the target already exists and ``backup`` is True, the existing file is
70+
copied to ``<file>.backup`` first so user customizations aren't lost.
71+
"""
72+
target_path, template_path = _resolve_workflow_target(name)
73+
if target_path.exists():
74+
if backup:
75+
backup_path = Path(str(target_path) + ".backup")
76+
shutil.copy(target_path, backup_path)
77+
click.echo(f"Backed up {target_path}{backup_path}")
78+
_copy_github_template(template_path, target_path)
79+
click.echo(f"Updated {target_path}")
80+
else:
81+
_copy_github_template(template_path, target_path)
82+
click.echo(f"Created {target_path}")

0 commit comments

Comments
 (0)