From f50708852a06e3593f1cdc52281a2e7f4e18774d Mon Sep 17 00:00:00 2001 From: arabcoders Date: Wed, 18 Jun 2025 22:08:11 +0300 Subject: [PATCH] As we no longer need to update each time yt-dlp updates, we going to use simver --- .github/scripts/generate_changelog.py | 107 ++++++++++++++++++++++++++ .github/workflows/main.yml | 93 +++++++++++++++++----- .gitignore | 1 + app/library/config.py | 80 ++++--------------- app/library/version.py | 6 ++ app/main.py | 2 +- ui/layouts/default.vue | 5 +- ui/pages/changelog.vue | 36 +++++---- ui/stores/ConfigStore.js | 3 + 9 files changed, 230 insertions(+), 103 deletions(-) create mode 100644 .github/scripts/generate_changelog.py diff --git a/.github/scripts/generate_changelog.py b/.github/scripts/generate_changelog.py new file mode 100644 index 00000000..895f7171 --- /dev/null +++ b/.github/scripts/generate_changelog.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 + +import argparse +import json +import sys +from datetime import UTC, datetime + +try: + import git # type: ignore +except ImportError: + print("Please install GitPython: pip install GitPython", file=sys.stderr) # noqa: T201 + sys.exit(1) + + +def get_sorted_tags(repo): + """Returns all tags sorted by their commit date (newest first).""" + return sorted(repo.tags, key=lambda t: t.commit.committed_datetime, reverse=True) + + +def get_commits_between(repo, start, end): + """Return commits between two refs (no merges).""" + return list(repo.iter_commits(f"{start}..{end}", no_merges=True)) + + +def generate_changelog(repo_path, changelog_path): + repo = git.Repo(repo_path) + tags = get_sorted_tags(repo) + changelog = [] + + if not tags: + start = repo.commit(repo.git.rev_list("--max-parents=0", "HEAD")) + commits = get_commits_between(repo, start.hexsha, "HEAD") + changelog.append( + { + "tag": "Initial Release", + "date": datetime.now(UTC).isoformat(), + "commits": [ + { + "sha": c.hexsha[:8], + "full_sha": c.hexsha, + "message": c.message.strip(), + "author": c.author.name, + "date": c.committed_datetime.astimezone(UTC).isoformat(), + } + for c in commits + ], + } + ) + else: + for i in range(len(tags) - 1): + newer, older = tags[i], tags[i + 1] + commits = get_commits_between(repo, older.commit.hexsha, newer.commit.hexsha) + if not commits: + continue + changelog.append( + { + "tag": newer.name, + "full_sha": newer.commit.hexsha, + "date": newer.commit.committed_datetime.astimezone(UTC).isoformat(), + "commits": [ + { + "sha": c.hexsha[:8], + "full_sha": c.hexsha, + "message": c.message.strip(), + "author": c.author.name, + "date": c.committed_datetime.astimezone(UTC).isoformat(), + } + for c in commits + ], + } + ) + + # Add HEAD -> latest tag if ahead + head = repo.head.commit + if head.hexsha != tags[0].commit.hexsha: + commits = get_commits_between(repo, tags[0].commit.hexsha, head.hexsha) + if commits: + changelog.insert( + 0, + { + "tag": f"Unreleased ({head.hexsha[:8]})", + "full_sha": head.hexsha, + "date": head.committed_datetime.astimezone(UTC).isoformat(), + "commits": [ + { + "sha": c.hexsha[:8], + "full_sha": c.hexsha, + "message": c.message.strip(), + "author": c.author.name, + "date": c.committed_datetime.astimezone(UTC).isoformat(), + } + for c in commits + ], + }, + ) + + with open(changelog_path, "w", encoding="utf-8") as f: + json.dump(changelog, f, indent=2) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Generate CHANGELOG.json from Git tags.") + parser.add_argument("--repo_path", "-p", default=".", help="Path to git repo") + parser.add_argument("--changelog_path", "-f", default="CHANGELOG.json", help="Output file path") + args = parser.parse_args() + + generate_changelog(args.repo_path, args.changelog_path) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f018b3cd..b6ef0843 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,16 +22,13 @@ on: required: false default: false type: boolean - create_release: - description: "Create Release (requires build)" - required: false - default: false - type: boolean push: branches: - dev - master + tags: + - "v*" paths-ignore: - "**.md" - ".github/**" @@ -96,11 +93,13 @@ jobs: packages: write contents: write env: - PLATFORMS: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && 'linux/amd64,linux/arm64' || 'linux/amd64' }} + PLATFORMS: ${{ startsWith(github.ref, 'refs/tags/v') && 'linux/amd64,linux/arm64' || 'linux/amd64' }} steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install pnpm uses: pnpm/action-setup@v4 @@ -120,13 +119,30 @@ jobs: pnpm install --production --prefer-offline --frozen-lockfile pnpm run generate - - name: Update Version File - uses: ArabCoders/write-version-to-file@master - with: - filename: "/app/library/version.py" - placeholder: "dev-master" - with_date: "true" - with_branch: "true" + - name: Install GitPython + run: pip install gitpython + + - name: Generate CHANGELOG.json + run: | + python3 .github/scripts/generate_changelog.py -p . -f ./ui/exported/CHANGELOG.json + + - name: Update app/library/version.py + run: | + VERSION="${GITHUB_REF##*/}" + SHA=$(git rev-parse HEAD) + DATE=$(date -u +"%Y%m%d") + + echo "Current version: ${VERSION}, SHA: ${SHA}, Date: ${DATE}" + + echo "APP_VERSION=${VERSION}" >> $"$GITHUB_ENV" + echo "APP_SHA=${SHA}" >> "$GITHUB_ENV" + echo "APP_DATE=${DATE}" >> "$GITHUB_ENV" + + sed -i \ + -e "s/^APP_VERSION = \".*\"/APP_VERSION = \"${VERSION}\"/" \ + -e "s/^APP_COMMIT_SHA = \".*\"/APP_COMMIT_SHA = \"${SHA}\"/" \ + -e "s/^APP_BUILD_DATE = \".*\"/APP_BUILD_DATE = \"${DATE}\"/" \ + app/library/version.py - name: Set up QEMU uses: docker/setup-qemu-action@v3 @@ -142,10 +158,9 @@ jobs: ${{ env.DOCKERHUB_SLUG }} ${{ env.GHCR_SLUG }} tags: | - type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} type=ref,event=branch type=ref,event=tag - type=raw,value={{branch}}{{base_ref}}-{{date 'YYYYMMDD'}}-{{sha}} flavor: | latest=false @@ -173,13 +188,49 @@ jobs: cache-from: type=gha, scope=${{ github.workflow }} cache-to: type=gha, mode=max, scope=${{ github.workflow }} - - name: Version tag - uses: arabcoders/action-python-autotagger@master + - name: Overwrite GitHub release notes + if: startsWith(github.ref, 'refs/tags/v') + uses: actions/github-script@v6 with: - token: ${{ secrets.GITHUB_TOKEN }} - repo_name: arabcoders/ytptube - path: app/library/version.py - regex: 'APP_VERSION\s=\s\"(.+)\"' + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const tag = context.ref.replace('refs/tags/', ''); + + const latestRelease = await github.rest.repos.listReleases({ + owner: context.repo.owner, + repo: context.repo.repo, + }); + + const release = latestRelease.data.find(r => r.tag_name === tag); + if (!release) { + core.setFailed(`Release with tag ${tag} not found`); + return; + } + + const { data: commits } = await github.rest.repos.compareCommits({ + owner: context.repo.owner, + repo: context.repo.repo, + base: latestRelease.data[1]?.tag_name || '', // fallback to second latest tag + head: tag, + }); + + const changelog = commits.commits.map( + c => `- ${c.sha.substring(0, 7)} ${c.commit.message.split('\n')[0]} by ${c.commit.author.name}` + ).join('\n'); + + if (!changelog) { + core.setFailed('No commits found for the changelog'); + return; + } + + console.log(`Changelog for ${tag}:\n${changelog}`); + + await github.rest.repos.updateRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: release.id, + body: changelog + }); dockerhub-sync-readme: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index c107c809..d276d02e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # compiled output /frontend/dist /ui/dist +/ui/exported/CHANGELOG.json # dependencies **/node_modules/* diff --git a/app/library/config.py b/app/library/config.py index 919d8d95..ce2986be 100644 --- a/app/library/config.py +++ b/app/library/config.py @@ -12,7 +12,7 @@ import coloredlogs from dotenv import load_dotenv from .Utils import FileLogFormatter, arg_converter -from .version import APP_VERSION +from .version import APP_BUILD_DATE, APP_COMMIT_SHA, APP_VERSION class Config: @@ -113,12 +113,18 @@ class Config: version: str = APP_VERSION "The version of the application." + app_version: str = APP_VERSION + "The version of the application, same as `version`." + + app_commit_sha: str = APP_COMMIT_SHA + "The commit SHA of the application." + + app_build_date: str = APP_BUILD_DATE + "The build date of the application." + __instance = None "The instance of the class." - new_version_available: bool = False - "A new version of the application is available." - started: int = 0 "The time the application was started." @@ -188,11 +194,13 @@ class Config: "version", "__instance", "ytdl_options", - "new_version_available", "started", "ytdlp_cli", "_ytdlp_cli_mutable", "is_native", + "app_version", + "app_commit_sha", + "app_build_date", ) "The variables that are immutable." @@ -246,6 +254,9 @@ class Config: "is_native", "app_env", "tasks_handler_timer", + "app_version", + "app_commit_sha", + "app_build_date", ) "The variables that are relevant to the frontend." @@ -441,9 +452,6 @@ class Config: ) raise ValueError(msg) - if self.version == "dev-master": - self._version_via_git() - def _get_attributes(self) -> dict: attrs: dict = {} vClass: str = self.__class__ @@ -511,59 +519,3 @@ class Config: return YTDLP_VERSION except ImportError: return "0.0.0" - - def _version_via_git(self): - """ - Updates the version of the application using git tags. - This is used to set the version to the latest git tag. - """ - git_path: Path = Path(__file__).parent / ".." / ".." / ".git" - if not git_path.exists(): - logging.info(f"Git directory '{git_path}' does not exist. Cannot determine version.") - return - - try: - import subprocess - - branch_result = subprocess.run( # noqa: S603 - ["git", "rev-parse", "--abbrev-ref", "HEAD"], # noqa: S607 - cwd=git_path.parent, - capture_output=True, - text=True, - check=False, - ) - - if 0 != branch_result.returncode: - logging.error(f"Git rev-parse failed: {branch_result.stderr.strip()}") - return - - branch_name: str = branch_result.stdout.strip() - if not branch_name: - logging.warning("Git branch name is empty.") - return - - commit_result = subprocess.run( # noqa: S603 - ["git", "log", "-1", "--format=%ct_%H"], # noqa: S607 - cwd=git_path.parent, - capture_output=True, - text=True, - check=False, - ) - - if 0 != commit_result.returncode: - logging.error(f"Git log failed: {commit_result.stderr.strip()}") - return - - commit_info: str = commit_result.stdout.strip() - if not commit_info: - logging.warning("Git commit info is empty.") - return - - commit_date, commit_sha = commit_info.split("_", 1) - commit_date = time.strftime("%Y%m%d", time.localtime(int(commit_date))) - commit_sha = commit_sha[:8] - - self.version = f"{branch_name}-{commit_date}-{commit_sha}" - logging.info(f"Application version set to '{self.version}' based on git data.") - except Exception as e: - logging.error(f"Error while getting git version: {e!s}") diff --git a/app/library/version.py b/app/library/version.py index 7efd8aef..e10ec5de 100644 --- a/app/library/version.py +++ b/app/library/version.py @@ -5,3 +5,9 @@ This file is updated by the CI/CD pipeline, do not edit it manually. APP_VERSION = "dev-master" "Application version" + +APP_COMMIT_SHA = "unknown" +"Commit SHA of the application" + +APP_BUILD_DATE = "unknown" +"Build date of the application" diff --git a/app/main.py b/app/main.py index afacd970..ee7fcfd5 100644 --- a/app/main.py +++ b/app/main.py @@ -120,7 +120,7 @@ class Main: def started(_): LOG.info("=" * 40) - LOG.info(f"YTPTube v{self._config.version} - started on http://{host}:{port}{self._config.base_path}") + LOG.info(f"YTPTube {self._config.version} - started on http://{host}:{port}{self._config.base_path}") LOG.info("=" * 40) if cb: cb() diff --git a/ui/layouts/default.vue b/ui/layouts/default.vue index 5c45fc36..c034a18d 100644 --- a/ui/layouts/default.vue +++ b/ui/layouts/default.vue @@ -107,6 +107,7 @@
+
@@ -114,7 +115,9 @@
© {{ Year }} - YTPTube -  ({{ config?.app?.version || 'unknown' }}) + +  ({{ config?.app?.version || 'unknown' }}) - yt-dlp  ({{ config?.app?.ytdlp_version || 'unknown' }}) - CHANGELOG diff --git a/ui/pages/changelog.vue b/ui/pages/changelog.vue index 9b868f60..7c2e5dd6 100644 --- a/ui/pages/changelog.vue +++ b/ui/pages/changelog.vue @@ -27,7 +27,7 @@

- {{ formatTag(log) }} Installed + {{ log.tag }} Installed


    @@ -61,7 +61,7 @@ useHead({ title: 'CHANGELOG' }) const PROJECT = 'ytptube' const REPO = `https://github.com/arabcoders/${PROJECT}` -const REPO_URL = `https://arabcoders.github.io/${PROJECT}/CHANGELOG-{branch}.json?version={version}` +const REPO_URL = `https://arabcoders.github.io/${PROJECT}/CHANGELOG.json?version={version}` const logs = ref([]) const api_version = ref('') @@ -73,18 +73,6 @@ const branch = computed(() => { return ['master', 'dev'].includes(branch) ? branch : 'master' }) -const formatTag = log => { - const parts = log.tag.split('-') - if (parts.length < 3) { - return log.tag - } - - const branch = parts[0] - const date = parts[1] - const shortSha = log.full_sha.substring(0, hashLength.value) - return `${ucFirst(branch)}: ${moment(date, 'YYYYMMDD').format('YYYY-MM-DD')} - ${shortSha}` -} - watch(() => config.app.version, async value => { if (!value) { return @@ -104,8 +92,24 @@ const loadContent = async () => { isLoading.value = true try { - const changes = await fetch(REPO_URL.replace('{branch}', branch.value).replace('{version}', api_version.value)) - logs.value = await changes.json() + try{ + logs.value = await (await request(uri('/CHANGELOG.json'), { method: 'GET' })).json() + }catch(e){ + console.error(e) + const changes = await fetch(REPO_URL.replace('{branch}', branch.value).replace('{version}', api_version.value)) + logs.value = await changes.json() + } + await nextTick() + + logs.value = logs.value.slice(0, 10).map(log => { + log.commits = log.commits.map(commit => { + commit.full_sha = commit.sha.padEnd(hashLength.value, '0') + return commit + }) + log.full_sha = log.tag.padEnd(hashLength.value, '0') + return log + }) + } catch (e) { console.error(e) toast.error('error', 'Error', `Failed to fetch changelog. ${e.message}`) diff --git a/ui/stores/ConfigStore.js b/ui/stores/ConfigStore.js index 93887f2a..0ab2ae68 100644 --- a/ui/stores/ConfigStore.js +++ b/ui/stores/ConfigStore.js @@ -20,6 +20,9 @@ const CONFIG_KEYS = { ytdlp_cli: '', file_logging: false, is_native: false, + app_version: '', + app_commit_sha: '', + app_build_date: '', }, presets: [ {