From 7c57f666786011138fa63af1575ab7e2c9ae4cb1 Mon Sep 17 00:00:00 2001 From: Azalea Date: Wed, 3 Jun 2026 07:48:53 +0000 Subject: [PATCH] [F] Fix release recovery workflow --- .github/workflows/release.yml | 48 ++++++++++++++++++------ package.json | 11 ++++-- tools/build_pkg.sh | 4 +- tools/deploy-release.py | 69 +++++++++++++++++++++++++++++++++++ tools/deploy.md | 19 +++++++--- tools/release.py | 34 +++++++++++++++++ 6 files changed, 162 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e384815..be9553ae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,6 +11,11 @@ on: description: "Existing release tag to publish, such as 2.1.1 or v2.1.1" required: true type: string + source_ref: + description: "Ref to build/publish from. Leave empty to use the release tag; set to master/a branch for recovery fixes." + required: false + default: "" + type: string create_github_release: description: "Create or complete the GitHub Release" required: true @@ -40,26 +45,33 @@ jobs: runs-on: ubuntu-22.04 outputs: tag: ${{ steps.metadata.outputs.tag }} + source_ref: ${{ steps.release-refs.outputs.source_ref }} version: ${{ steps.metadata.outputs.version }} python_package: ${{ steps.metadata.outputs.python_package }} npm_package: ${{ steps.metadata.outputs.npm_package }} steps: - - name: Resolve release tag - id: release-tag + - name: Resolve release refs + id: release-refs run: | if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT" + if [[ -n "${{ inputs.source_ref }}" ]]; then + echo "source_ref=${{ inputs.source_ref }}" >> "$GITHUB_OUTPUT" + else + echo "source_ref=${{ inputs.tag }}" >> "$GITHUB_OUTPUT" + fi else echo "tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT" + echo "source_ref=${{ github.ref_name }}" >> "$GITHUB_OUTPUT" fi - uses: actions/checkout@v4 with: - ref: ${{ steps.release-tag.outputs.tag }} + ref: ${{ steps.release-refs.outputs.source_ref }} - name: Validate checked-in versions id: metadata - run: python3 tools/release.py metadata --tag "${{ steps.release-tag.outputs.tag }}" + run: python3 tools/release.py metadata --tag "${{ steps.release-refs.outputs.tag }}" release-state: name: Check completed release stages @@ -73,7 +85,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ needs.metadata.outputs.tag }} + ref: ${{ needs.metadata.outputs.source_ref }} - name: Query registries and GitHub Release id: state @@ -179,7 +191,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ needs.metadata.outputs.tag }} + ref: ${{ needs.metadata.outputs.source_ref }} - uses: actions/setup-python@v5 with: @@ -221,29 +233,44 @@ jobs: && needs.release-state.outputs.github_release_complete != 'true' }} steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.metadata.outputs.source_ref }} + - uses: actions/download-artifact@v4 with: name: python-dist-${{ needs.metadata.outputs.version }} path: dist - - name: Create release if needed + - name: Write release notes + run: | + python3 tools/release.py changelog \ + --version "${{ needs.metadata.outputs.version }}" \ + --output RELEASE_NOTES.md + + - name: Create or update release metadata env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} TAG: ${{ needs.metadata.outputs.tag }} VERSION: ${{ needs.metadata.outputs.version }} run: | if gh release view "$TAG" >/dev/null 2>&1; then echo "GitHub Release $TAG already exists." + gh release edit "$TAG" \ + --title "$VERSION" \ + --notes-file RELEASE_NOTES.md else gh release create "$TAG" \ --verify-tag \ - --title "HyFetch $VERSION" \ - --generate-notes + --title "$VERSION" \ + --notes-file RELEASE_NOTES.md fi - name: Upload missing release assets env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} TAG: ${{ needs.metadata.outputs.tag }} run: | mapfile -t existing_assets < <(gh release view "$TAG" --json assets --jq '.assets[].name') @@ -321,12 +348,11 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ needs.metadata.outputs.tag }} + ref: ${{ needs.metadata.outputs.source_ref }} - uses: actions/setup-node@v4 with: node-version: "24" - registry-url: "https://registry.npmjs.org" - name: Use npm with trusted publishing support run: npm install -g npm@11 diff --git a/package.json b/package.json index 8ab687d8..e8efe5d2 100644 --- a/package.json +++ b/package.json @@ -4,11 +4,14 @@ "description": "Updated neofetch", "repository": { "type": "git", - "url": "git+https://github.com/hykilpikonna/neofetch.git" + "url": "https://github.com/hykilpikonna/hyfetch" }, "bin": { "neowofetch": "neofetch" }, + "files": [ + "neofetch" + ], "keywords": [ "neofetch", "screenfetch" @@ -19,7 +22,7 @@ ], "license": "MIT", "bugs": { - "url": "https://github.com/hykilpikonna/neofetch/issues" + "url": "https://github.com/hykilpikonna/hyfetch/issues" }, - "homepage": "https://github.com/hykilpikonna/neofetch#readme" -} \ No newline at end of file + "homepage": "https://github.com/hykilpikonna/hyfetch#readme" +} diff --git a/tools/build_pkg.sh b/tools/build_pkg.sh index 67849025..fd5df195 100755 --- a/tools/build_pkg.sh +++ b/tools/build_pkg.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -FASTFETCH_VERSION="2.61.0" +FASTFETCH_VERSION="2.63.1" FASTFETCH_DL="https://github.com/fastfetch-cli/fastfetch/releases/download/$FASTFETCH_VERSION/" # Get script directory @@ -85,7 +85,7 @@ function build_for_platform() { rust_platform=$3 echo "Building for $ff_platform" - + # Download the fastfetch binary wget "$FASTFETCH_DL/fastfetch-$ff_platform.zip" -O "fastfetch-$ff_platform.zip" diff --git a/tools/deploy-release.py b/tools/deploy-release.py index 7c818f5e..49dcb0c6 100755 --- a/tools/deploy-release.py +++ b/tools/deploy-release.py @@ -6,6 +6,9 @@ import re import shlex import stat import subprocess +import urllib.error +import urllib.parse +import urllib.request from pathlib import Path from packaging import version as pv @@ -14,6 +17,16 @@ from tools.list_distros import generate_help from tools.reformat_readme import reformat_readme NEOFETCH_NEW_VERSION = "" +FASTFETCH_RELEASE_API = 'https://api.github.com/repos/fastfetch-cli/fastfetch/releases' +FASTFETCH_ASSETS = [ + 'fastfetch-windows-amd64.zip', + 'fastfetch-linux-amd64.zip', + 'fastfetch-linux-aarch64.zip', + 'fastfetch-linux-armv7l.zip', + 'fastfetch-musl-amd64.zip', + 'fastfetch-macos-amd64.zip', + 'fastfetch-macos-aarch64.zip', +] RELEASE_FILES = [ 'Cargo.lock', 'Cargo.toml', @@ -23,6 +36,7 @@ RELEASE_FILES = [ 'hyfetch/__version__.py', 'neofetch', 'package.json', + 'tools/build_pkg.sh', ] @@ -97,6 +111,49 @@ def edit_versions(version: str): NEOFETCH_NEW_VERSION = nf +def fetch_fastfetch_release(release: str) -> dict: + """ + Fetch fastfetch release metadata from GitHub. + """ + if release == 'latest': + url = f'{FASTFETCH_RELEASE_API}/latest' + else: + tag = urllib.parse.quote(release, safe='') + url = f'{FASTFETCH_RELEASE_API}/tags/{tag}' + + request = urllib.request.Request(url, headers={ + 'Accept': 'application/vnd.github+json', + 'User-Agent': 'hyfetch-release-script', + }) + + try: + with urllib.request.urlopen(request, timeout=30) as response: + return json.loads(response.read().decode()) + except urllib.error.HTTPError as e: + if e.code == 404 and release.startswith('v'): + return fetch_fastfetch_release(release[1:]) + raise + + +def update_fastfetch_version(release: str) -> str: + """ + Update the fastfetch binary version embedded in Python wheels. + """ + print(f'Checking fastfetch {release}...') + metadata = fetch_fastfetch_release(release) + tag = metadata['tag_name'] + assets = {asset['name'] for asset in metadata['assets']} + missing = sorted(set(FASTFETCH_ASSETS) - assets) + assert not missing, f'Fastfetch {tag} is missing required release assets: {", ".join(missing)}' + + print(f'Editing tools/build_pkg.sh fastfetch version to {tag}...') + path = Path('tools/build_pkg.sh') + content = path.read_text() + content = re.sub(r'(?<=^FASTFETCH_VERSION=")[^"]+(?="$)', tag, content, flags=re.MULTILINE) + path.write_text(content) + return tag + + def finalize_neofetch(): """ Finalize current version @@ -181,6 +238,16 @@ def deploy(): if __name__ == '__main__': parser = argparse.ArgumentParser(description='HyFetch Release Utility') parser.add_argument('version', help='Version to release') + parser.add_argument( + '--fastfetch-version', + default='latest', + help='Fastfetch release tag to embed in Python wheels, or "latest" (default).', + ) + parser.add_argument( + '--skip-fastfetch-update', + action='store_true', + help='Keep the existing FASTFETCH_VERSION in tools/build_pkg.sh.', + ) parser.add_argument( '--local-deploy', action='store_true', @@ -191,6 +258,8 @@ if __name__ == '__main__': pre_check() edit_versions(args.version) + if not args.skip_fastfetch_update: + update_fastfetch_version(args.fastfetch_version) finalize_neofetch() post_check() diff --git a/tools/deploy.md b/tools/deploy.md index 2fd6fd09..61249187 100644 --- a/tools/deploy.md +++ b/tools/deploy.md @@ -17,11 +17,18 @@ their trusted publishing flows. * [ ] Run `python -m tools.deploy-release {version}` * [ ] Watch the `Release` workflow -The local script prepares the release commit and pushes the version tag. GitHub -Actions creates/completes the GitHub Release, uploads Python artifacts, publishes -to PyPI, and publishes to npm. +The local script prepares the release commit and pushes the version tag. By +default it also updates the built-in fastfetch binaries to the latest upstream +release used by Python wheels. Use `--fastfetch-version {version}` to pin a +specific upstream fastfetch release, or `--skip-fastfetch-update` to keep the +current pin. + +GitHub Actions creates/completes the GitHub Release, uploads Python artifacts, +publishes to PyPI, and publishes to npm. If the workflow fails after one stage already published, fix the problem and -rerun it. If the fix changes the workflow itself, run the workflow manually with -the same tag. The workflow checks PyPI, npm, and the GitHub Release first, so it -will skip completed stages instead of publishing them again. +rerun it manually with the same release tag. If the fix changes files needed by +the failed stage, set `source_ref` to the branch or commit containing the fix, +and turn off any stages that already completed. The workflow checks PyPI, npm, +and the GitHub Release first, so completed stages are skipped instead of being +published again. diff --git a/tools/release.py b/tools/release.py index 20471cdb..4f95a2da 100755 --- a/tools/release.py +++ b/tools/release.py @@ -84,6 +84,25 @@ def read_pyproject_name() -> str: return match.group(1) +def read_changelog(version: str) -> str: + content = (ROOT / "README.md").read_text(encoding="utf-8") + heading = f"### {version}" + match = re.search( + rf"^{re.escape(heading)}\s*\n(?P.*?)(?=^### \S|\Z)", + content, + flags=re.MULTILINE | re.DOTALL, + ) + + if not match: + raise RuntimeError(f"Could not find changelog section {heading!r} in README.md") + + body = match.group("body").strip() + if not body: + raise RuntimeError(f"Changelog section {heading!r} is empty") + + return body + "\n" + + def normalize_tag(tag: str) -> Tuple[str, str]: match = VERSION_RE.match(tag) if not match: @@ -199,6 +218,16 @@ def command_state(args: argparse.Namespace) -> None: write_output("github_release_complete", github_release_complete) +def command_changelog(args: argparse.Namespace) -> None: + _, version = normalize_tag(args.version) + changelog = read_changelog(version) + + if args.output: + Path(args.output).write_text(changelog, encoding="utf-8") + else: + print(changelog, end="") + + def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Release workflow helpers") subparsers = parser.add_subparsers(dest="command", required=True) @@ -215,6 +244,11 @@ def build_parser() -> argparse.ArgumentParser: state.add_argument("--repo", required=True) state.set_defaults(func=command_state) + changelog = subparsers.add_parser("changelog", help="Extract release notes from README.md") + changelog.add_argument("--version", required=True) + changelog.add_argument("--output") + changelog.set_defaults(func=command_changelog) + return parser