mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-11-01 03:11:12 +00:00
## Why
Manual version bumps create conflicts between PRs and require
contributors to update package metadata before the final release version
is known. Release intent should be declared in the PR title and applied
once after merge.
## How
Require exactly `patch: `, `minor: `, or `major: ` for changes to
assets, scripts, source, Python version, pyproject.toml, or uv.lock.
Forbid release prefixes when none of those paths change. Keep the
required Version policy check and rerun it independently on title edits.
Use hatch-vcs and Git release tags instead of maintained version
numbers. Installed FCC keeps reading its distribution metadata.
Git-aware uv cache keys refresh local development installs without
changing the lockfile.
Post-merge reads the saved squash title, reserves one version at that
commit, stages validated distributions on a draft GitHub Release,
publishes the same files to PyPI, then publishes the Release with
generated notes. Retries reuse the version and staged bytes. Later
releases stop until an unfinished predecessor completes. Recovery
dispatch can retry an earlier main commit using the repaired workflow.
Update the issue-version bot to compare against the latest published
Release and document contributor title rules. Establish the existing,
successfully published 6.2.66 version as the initial release tag.
The accepted contract retains the existing release paths, ordered queue,
trusted PyPI publisher, and ordinary pull-request check trust model.
There are no manual version bumps or release-only commits.
<!-- greptile_comment -->
<!-- greptile_summary -->
<h2><a
href="https://app.greptile.com/api/retrigger?id=69606710"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/RetriggerDark.svg?v=2"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=2"><img
alt="Retrigger"
src="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=2"
align="right"></picture></a>Confidence Score: 4/5</h2>
<!-- greptile-risk -->
Not safe to merge until the outstanding release-finalization issue is
fixed.
<h2><a
href="https://app.greptile.com/api/ide/codex?prompt=IMPORTANT%3A%20Work%20in%20the%20repository%20%22alishahryar1%2Ffree-claude-code%22%20on%20the%20existing%20branch%20%22ali%2Fautomatic-release-versions%22.%20Checkout%20that%20branch%20%E2%80%94%20do%20NOT%20create%20a%20new%20branch%20or%20open%20a%20new%20PR.%20Push%20your%20changes%20to%20%22ali%2Fautomatic-release-versions%22.%0A%0A%23%23%23%20Issue%201%0A.github%2Factions%2Fci-environment%2Faction.yml%3Aundefined-52%0AIf%20a%20fork%20PR%20has%20a%20version%20tag%20pointing%20to%20a%20different%20commit%20than%20the%20base%20repository%E2%80%99s%20tag%2C%20this%20unforced%20fetch%20rejects%20the%20canonical%20tag.%20The%20Version%20policy%20job%20checks%20out%20the%20fork%20with%20full%20history%2C%20so%20setup%20fails%20before%20the%20required%20check%20runs.%20Fetch%20canonical%20tags%20without%20colliding%20with%20fork%20tags.%0A%0A%23%23%23%20Issue%202%0Ascripts%2Fpublish_release.py%3Aundefined-145%0AAfter%20publishing%20the%20distributions%20to%20PyPI%2C%20this%20passes%20%60--verify-tag%60%20to%20%60gh%20release%20edit%60%2C%20which%20rejects%20that%20flag.%20The%20GitHub%20Release%20remains%20a%20draft%2C%20retries%20fail%20at%20the%20same%20step%2C%20and%20the%20next%20release%20cannot%20proceed%20while%20its%20predecessor%20is%20unfinished.%20Remove%20the%20flag%20from%20the%20edit%20command.%0A%0A%60%60%60suggestion%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20self.gh%28%22edit%22%2C%20tag%2C%20%22--draft%3Dfalse%22%2C%20%22--latest%22%29%0A%60%60%60%0A%0A---%0A%0AFor%20each%20issue%20above%2C%20determine%20whether%20it%20is%20valid%20and%20should%20be%20fixed.%20If%20so%2C%20fix%20it%20directly.&repo=alishahryar1%2Ffree-claude-code&pr=1903&platform=github"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodexDark.svg?v=7"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodex.svg?v=7"><img
alt="Fix All in Codex"
src="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodex.svg?v=7"
align="right"></picture></a>Findings</h2>
1. <img alt="P1"
src="https://greptile-static-assets.s3.amazonaws.com/badges/p1.svg?v=9"
align="top"> **Conflicting fork tags fail CI** <a
href="https://github.com/Alishahryar1/free-claude-code/pull/1903#discussion_r4108544662">▶</a>
2. <img alt="P1"
src="https://greptile-static-assets.s3.amazonaws.com/badges/p1.svg?v=9"
align="top"> **Release finalization always fails** <a
href="https://github.com/Alishahryar1/free-claude-code/pull/1903#discussion_r4108544663">▶</a>
<details open><summary>Summary</summary>
The PR automates release versioning and publication. The fork-tag CI
conflict is fixed, but release finalization remains broken.
</details>
<!-- greptile_confidence_score:4 -->
<sub>Reviews (3) · Last reviewed commit: ["Use canonical release tags in
disposable..."](fc416b2667)</sub>
<!-- /greptile_comment -->
118 lines
4.2 KiB
Python
118 lines
4.2 KiB
Python
"""Check the exact FCC distributions that will be published."""
|
|
|
|
import argparse
|
|
import configparser
|
|
import re
|
|
import subprocess
|
|
import tarfile
|
|
import tomllib
|
|
import zipfile
|
|
from email.parser import BytesParser
|
|
from pathlib import Path
|
|
|
|
|
|
def require_files(names: set[str], expected: set[str]) -> None:
|
|
missing = expected - names
|
|
if missing:
|
|
raise ValueError("Missing release files: " + ", ".join(sorted(missing)))
|
|
|
|
|
|
def validate(directory: Path, version: str) -> None:
|
|
project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))[
|
|
"project"
|
|
]
|
|
name = project["name"]
|
|
if not re.fullmatch(
|
|
r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)", version
|
|
):
|
|
raise ValueError("Expected a stable MAJOR.MINOR.PATCH release version")
|
|
if "version" in project or "version" not in project.get("dynamic", []):
|
|
raise ValueError("Release source must use dynamic version metadata")
|
|
stem = f"{name.replace('-', '_')}-{version}"
|
|
wheel_path = directory / f"{stem}-py3-none-any.whl"
|
|
sdist_path = directory / f"{stem}.tar.gz"
|
|
# uv build creates a .gitignore alongside its distributions.
|
|
artifacts = {path for path in directory.iterdir() if path.name != ".gitignore"}
|
|
if artifacts != {wheel_path, sdist_path}:
|
|
raise ValueError(
|
|
"Release directory must contain only the current wheel and sdist"
|
|
)
|
|
tracked = subprocess.run(
|
|
["git", "ls-files", "-z", "src/free_claude_code"],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
encoding="utf-8",
|
|
).stdout
|
|
sources = set(filter(None, tracked.split("\0")))
|
|
if "src/free_claude_code/__init__.py" not in sources:
|
|
raise ValueError("Missing tracked FCC package sources")
|
|
with zipfile.ZipFile(wheel_path) as archive:
|
|
wheel = {
|
|
file: archive.read(file)
|
|
for file in archive.namelist()
|
|
if not file.endswith("/")
|
|
}
|
|
with tarfile.open(sdist_path) as archive:
|
|
source = {}
|
|
for file in archive:
|
|
stream = archive.extractfile(file) if file.isfile() else None
|
|
if stream is not None:
|
|
source[file.name] = stream.read()
|
|
wheel_info = f"{stem}.dist-info"
|
|
updater_names = {"fcc-update", "fcc-update.cmd"}
|
|
require_files(
|
|
set(wheel),
|
|
{file.removeprefix("src/") for file in sources}
|
|
| {f"{stem}.data/scripts/{script}" for script in updater_names}
|
|
| {f"{wheel_info}/METADATA", f"{wheel_info}/entry_points.txt"},
|
|
)
|
|
require_files(
|
|
set(source),
|
|
{f"{stem}/{file}" for file in sources}
|
|
| {f"{stem}/scripts/update/{script}" for script in updater_names}
|
|
| {
|
|
f"{stem}/{file}"
|
|
for file in ("pyproject.toml", "README.md", "LICENSE", "PKG-INFO")
|
|
},
|
|
)
|
|
for data in (wheel[f"{wheel_info}/METADATA"], source[f"{stem}/PKG-INFO"]):
|
|
metadata = BytesParser().parsebytes(data)
|
|
if metadata["Name"] != name or metadata["Version"] != version:
|
|
raise ValueError(
|
|
"Release metadata does not match the project name and version"
|
|
)
|
|
entrypoints = configparser.ConfigParser()
|
|
entrypoints.read_string(wheel[f"{wheel_info}/entry_points.txt"].decode())
|
|
for project_key, section in (
|
|
("scripts", "console_scripts"),
|
|
("gui-scripts", "gui_scripts"),
|
|
):
|
|
actual = dict(entrypoints[section]) if section in entrypoints else {}
|
|
if actual != project.get(project_key, {}):
|
|
raise ValueError(f"Release entry points do not match {project_key}")
|
|
print(f"Validated {wheel_path.name} and {sdist_path.name}")
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("directory", type=Path)
|
|
parser.add_argument("--version", required=True)
|
|
args = parser.parse_args()
|
|
try:
|
|
validate(args.directory, args.version)
|
|
except (
|
|
ValueError,
|
|
KeyError,
|
|
OSError,
|
|
zipfile.BadZipFile,
|
|
tarfile.TarError,
|
|
subprocess.CalledProcessError,
|
|
) as error:
|
|
print(f"Release validation failed: {error}")
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|